i need help on managing delay on an array iteration.
Regarding my https://jsfiddle.net/mlefree/vrL813j2/93/, two questions :
How to add delay on each iteration action ?
How to reduce all iteration computed values ?
```
...
const arrayAsObservable = of(null).pipe(
delay(500),
switchMap(_ => getObjectWithArrayInPromise()),
map(val => {
log('array', val);
return (val.myArray);
}),
switchMap(val => from(val))
);
const eachElementAsObservable = arrayAsObservable.pipe(
delay(500), // Not working : we want to wait 500ms more for each value
map(val => {
log('value', val);
return val ;
}),
switchMap(val => getNewValueInPromise(val)),
map(val => {
// Not working : why not all new values ?
log('value after computing (KO)', val);
return (val);
})
);
const summarizeAsObservable = eachElementAsObservable.pipe(
// Not working : we want to sum all new values
map(val => {
log('value before reduce (KO)', val);
return val ;
}),
reduce((a,b) => a + b)
);
summarizeAsObservable.subscribe(msg => {
log('We did a total of (KO)', msg);
});
```
Overall the code is a bit too complex, there's a few lines become the root problem of your code.
The reason that you only receive one value after computing (KO) is you used switchMap which will unsubscribe the inner observable once the source emit, so you always get the last emitted value. I also change delay to timer and mapTo the emitted value
const eachElementAsObservable = arrayAsObservable.pipe(
concatMap(value => timer(1500).pipe(mapTo(value))), // Not working : we want to wait 500ms for each value
map(val => {
log('value', val);
return val;
}),
mergeMap(val => from(getNewValueInPromise(val))),
map(val => {
// Not working : why not all new values ?
console.log('value after computing (KO)', val);
return (val);
})
);
this is a updated fiddle https://jsfiddle.net/fancheung/vrL813j2/109/
The code is not fully working because you throw an error by reject in promise that will cause the observable to stop emitting, you will need to put a catchError somewhere in the stream
Related
What is the correct way in RXJS to remap an observable into a timer start value, without interrupting the original stream?
obs.pipe(take(1000), startTimer())
.subscribe(start => {
// show how long it took to finish streaming 1000 values:
const duration = Date.now() - start;
console.log(duration);
});
I want startTimer to remap into once-off subscription with start, but without interrupting the original stream, i.e. in this case subscribe is to be triggered only after all 1000 values have finished streaming.
How do I implement such startTimer? It's supposed to result into a once-off Date.now() value to help measure full stream duration.
Or is there maybe a standard solution for this already that I'm missing?
update-1
The expected result is like the one below, but without the need for creating start as an external variable, and instead make it part of the stream:
const start = Date.now();
obs.pipe(take(1000))
.subscribe({
complete() {
const duration = Date.now() - start;
console.log(duration);
}
});
The reason I want to make it part of a stream is because the original observable and subscribers are very much detached from each other, as in sitting in unrelated source files.
P.S. Alternatively, a solution that emits duration in the end would also be good, if that is at all possible.
update-2
In the end, I used a generic drain operand, designed to drain an observable stream, and then produce an observable at the end:
/**
* Drains the source observable till it completes, and then posts a new value-observable.
*/
function drain<T>(value: T | Observable<T> | (() => T | Observable<T>)) {
const v = () => {
const a = typeof value === 'function' ? value.call(null) : value;
return a instanceof Observable ? a : of(a);
}
return s => defer(() => s.pipe(filter(_ => false), c => concat(c, v()))) as Observable<T>;
}
Using this operand, I can rewrite startTimer like this:
const startTimer = () => drain(Date.now);
Some code that does what you describe pretty much exactly the way you describe it:
function logRunTime<T>(prefix: string): MonoTypeOperatorFunction<T> {
return s => defer(() => {
const start = Date.now();
return s.pipe(
tap({
complete: () => console.log(`${prefix}: ${Date.now() - start}ms`)
})
);
});
}
interval(1000).pipe(
take(10),
logRunTime("Ten Seconds of Interval")
).subscribe(console.log);
Output:
0
1
2
3
4
5
6
7
8
9
Ten Seconds of Interval: 10014ms
Update 1
do not make the original observable stop emitting values [...] we just do not want the source values
It seem to me that either you keep emitting the values or you don't.
Here is a version that drops the source emissions.
Is this what you're after?
function reduceRunTime<T>(prefix: string): OperatorFunction<T, string> {
return s => defer(() => {
const start = Date.now();
return s.pipe(
filter(_ => false),
c => concat(c, of(null)),
map(_ => `${prefix}: ${Date.now() - start}ms`)
);
}) as Observable<string>;
}
interval(1000).pipe(
take(10),
reduceRunTime("Ten Seconds of Interval")
).subscribe(console.log);
Output:
Ten Seconds of Interval: 10013ms
Update 2
If you don't want a string, this will emit the start time once the observable completes.
function startTimer() {
return s => s.pipe(
filter(_ => false),
c => concat(c, of(Date.now()))
) as Observable<number>;
}
Update 3
Two separate behaviours
I think update 2 may have been cleaned up too much. Consider this example:
const timed$ = interval(500).pipe(
take(5),
startTimer()
);
const logDiff = (start: number) => console.log(Date.now() - start);
timed$.subscribe(logDiff);
setTimeout(() => {
timed$.subscribe(logDiff);
}, 1000);
setTimeout(() => {
timed$.subscribe(logDiff);
}, 5000);
The output:
2521
3507
7511
Notably, because Observables are lazy (do nothing until subscribed), but Date.now is called when the observable is created. Your startTime may well be set long before the observable even starts. Making a 2.5s observable appear to require 7.5s.
Using defer fixes this problem as it doesn't create the observable until it is subscribed.
Updated startTimer
function startTimer() {
return s => defer(() => s.pipe(
filter(_ => false),
c => concat(c, of(Date.now()))
)) as Observable<number>;
}
New output for example above:
2521
2507
2511
Now you can do fun things like run the same observable 10 times and average out the runtime to get a better idea of how long it will take.
const average = arr => arr.reduce( ( p, c ) => p + c, 0 ) / arr.length;
concat(...Array.from(Array(10)).map(_ => timed$)).pipe(
map(start => Date.now() - start),
tap(console.log),
toArray()
).subscribe(runs => console.log("Average Runtime: ", average(runs)));
Output:
2515
2506
2506
2506
2507
2505
2506
2506
2507
2507
Average Runtime: 2507.1
I added a start, stop, pause button. Start will start a count down timer which will start from a value, keep decrementing until value reaches 0. We can pause the timer on clicking the pause button. On click of Stop also timer observable completes.
However, once the timer is completed ( either when value reaches 0 or
when clicked on stop button ), I am not able to start properly. I
tried adding repeatWhen operator. It starts on clicking twice. Not at
the first time.
Also, at stop, value is not resetting back to the initial value.
const subscription = merge(
startClick$.pipe(mapTo(true)),
pauseBtn$.pipe(mapTo(false))
)
.pipe(
tap(val => {
console.log(val);
}),
switchMap(val => (val ? interval(10).pipe(takeUntil(stopClick$)) : EMPTY)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue),
takeWhile(val => val >= 0),
repeatWhen(() => startClick$),
startWith(startValue)
)
.subscribe(val => {
counterDisplayHeader.innerHTML = val.toString();
});
Stackblitz Code link is available here
This is a pretty complicated usecase. There are two issues I think:
You have two subscriptions to startClick$ and the order of subscriptions matters in this case. When the chain completes repeatWhen is waiting for startClick$ to emit. However, when you click the button the emission is first propagated into the first subscription inside merge(...) and does nothing because the chain has already completed. Only after that it resubscribes thanks to repeatWhen but you have to press the button again to trigger the switchMap() operator.
When you use repeatWhen() it'll resubscribe every time the inner Observable emits so you want it to emit on startClick$ but only once. At the same time you don't want it to complete so you need to use something like this:
repeatWhen(notifier$ => notifier$.pipe(
switchMap(() => startClick$.pipe(take(1))),
)),
So to avoid all that I think you can just complete the chain using takeUntil(stopClick$) and then immediatelly resubscribe with repeat() to start over.
merge(
startClick$.pipe(mapTo(true)),
pauseBtn$.pipe(mapTo(false))
)
.pipe(
switchMap(val => (val ? interval(10) : EMPTY)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue),
takeWhile(val => val >= 0),
startWith(startValue),
takeUntil(stopClick$),
repeat(),
)
.subscribe(val => {
counterDisplayHeader.innerHTML = val.toString();
});
Your updated demo: https://stackblitz.com/edit/rxjs-tum4xq?file=index.ts
Here's an example stopwatch that counts up instead of down. Perhaps you can re-tool it.
type StopwatchAction = "START" | "STOP" | "RESET" | "END";
function createStopwatch(
control$: Observable<StopwatchAction>,
interval = 1000
): Observable<number>{
return defer(() => {
let toggle: boolean = false;
let count: number = 0;
const ticker = timer(0, interval).pipe(
map(x => count++)
);
const end$ = of("END");
return concat(
control$,
end$
).pipe(
catchError(_ => end$),
switchMap(control => {
if(control === "START" && !toggle){
toggle = true;
return ticker;
}else if(control === "STOP" && toggle){
toggle = false;
return EMPTY;
}else if(control === "RESET"){
count = 0;
if(toggle){
return ticker;
}
}
return EMPTY;
})
);
});
}
Here's an example of this in use:
const start$: Observable<StopwatchAction> = fromEvent(startBtn, 'click').pipe(mapTo("START"));
const reset$: Observable<StopwatchAction> = fromEvent(resetBtn, 'click').pipe(mapTo("RESET"));
createStopwatch(merge(start$,reset$)).subscribe(seconds => {
secondsField.innerHTML = seconds % 60;
minuitesField.innerHTML = Math.floor(seconds / 60) % 60;
hoursField.innerHTML = Math.floor(seconds / 3600);
});
You can achieve that in another way without completing the main observable or resubscribing to it using takeUntil, repeatWhen, or other operators, like the following:
create a simple state to handle the counter changes (count, isTicking)
merge all the observables that affecting the counter within one observable.
create intermediate observable to interact with the main merge observable (start/stop counting).
interface CounterStateModel {
count: number;
isTicking: boolean;
}
// Setup counter state
const initialCounterState: CounterStateModel = {
count: startValue,
isTicking: false
};
const patchCounterState = new Subject<Partial<CounterStateModel>>();
const counterCommands$ = merge(
startClick$.pipe(mapTo({ isTicking: true })),
pauseBtn$.pipe(mapTo({ isTicking: false })),
stopClick$.pipe(mapTo({ ...initialCounterState })),
patchCounterState.asObservable()
);
const counterState$: Observable<CounterStateModel> = counterCommands$.pipe(
startWith(initialCounterState),
scan(
(counterState: CounterStateModel, command): CounterStateModel => ({
...counterState,
...command
})
),
shareReplay(1)
);
const isTicking$ = counterState$.pipe(
map(state => state.isTicking),
distinctUntilChanged()
);
const commandFromTick$ = isTicking$.pipe(
switchMap(isTicking => (isTicking ? timer(0, 10) : NEVER)),
withLatestFrom(counterState$, (_, counterState) => ({
count: counterState.count
})),
tap(({ count }) => {
if (count) {
patchCounterState.next({ count: count - 1 });
} else {
patchCounterState.next({ ...initialCounterState });
}
})
);
const commandFromReset$ = stopClick$.pipe(mapTo({ ...initialCounterState }));
merge(commandFromTick$, commandFromReset$)
.pipe(startWith(initialCounterState))
.subscribe(
state => (counterDisplayHeader.innerHTML = state.count.toString())
);
Also here is the working version:
https://stackblitz.com/edit/rxjs-o86zg5
I'm using RxJS 6 to lazily step through iterable objects using code similar to example running below. This is working well but I'm having trouble solving my final use case.
Full code here
import { EMPTY, defer, from, of } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator (iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => result.done ? EMPTY : of(result.value))
);
}
function iterateValues ({ params }) {
const { values, delay: delayMilliseconds } = params;
const isIterable = typeof values[Symbol.iterator] === "function";
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
if (isIterable && delayMilliseconds > 0) {
const iterator = values[Symbol.iterator]();
// The first value is emitted immediately, the rest are emitted after time.
return stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
);
} else {
return from(values);
}
}
const options = {
params: {
// Any iterable object is walked manually. Otherwise delegate to `from()`.
values: ["Mary", "had", "a", "little", "lamb"],
// Delay _between_ values.
delay: 350,
// Delay before the stream restarts _after the last value_.
runAgainAfter: 1000,
}
};
iterateValues(options)
// Is not repeating?!
.pipe(repeat(3))
.subscribe(
v => {
console.log(v, Date.now());
},
console.error,
() => {
console.log('Complete');
}
);
I'd like to add in another option which will re-execute the stream, an indefinite number of times, after a delay (runAgainAfter). I'm having trouble composing this in cleanly without factoring the result.done case deeper. So far I've been unable to compose the run-again behavior around iterateValues.
What's the best approach to accomplish the use case?
Thanks!
Edit 1: repeat just hit me in the face. Perhaps it means to be friendly.
Edit 2: No, repeat isn't repeating but the observable is completing. Thanks for any help. I'm confused.
For posterity here is the full code sample for a revised edition is repeat-able and uses a consistent delay between items.
import { concat, EMPTY, defer, from, interval, of, throwError } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator(iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => (result.done ? EMPTY : of(result.value)))
);
}
function iterateValues({ params }) {
const { values, delay: delayMilliseconds, times = 1 } = params;
const isIterable =
values != null && typeof values[Symbol.iterator] === "function";
if (!isIterable) {
return throwError(new Error(`\`${values}\` is not iterable`));
}
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
const observable =
delayMilliseconds > 0
? defer(() => of(values[Symbol.iterator]())).pipe(
mergeMap(iterator =>
stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
)
)
)
: from(values);
return observable.pipe(repeat(times));
}
I'm gonna be honest, but there could be better solution for sure. In my solution, I ended up encapsulating delay logic in a custom runAgainAfter operator. Making it an independent part, that doesn't affect your code logic directly.
Full working code is here
And the code of runAgainAfter if anybody needs it:
import { Observable } from "rxjs";
export const runAgainAfter = delay => observable => {
return new Observable(observer => {
let timeout;
let subscription;
const subscribe = () => {
return observable.subscribe({
next(value) {
observer.next(value);
},
error(err) {
observer.error(err);
},
complete() {
timeout = setTimeout(() => {
subscription = subscribe();
}, delay);
}
});
};
subscription = subscribe();
return () => {
subscription.unsubscribe();
clearTimeout(timeout);
};
});
};
Hope it helps <3
I have a ReplaySubject that accumulate data with scan operator and every 10000 ms should be reset. Is there any another way to do it?
Now:
let subject = new ReplaySubject();
subject.scan((acc, cur) => {
acc.push(cur);
return acc;
}, [])
.subscribe(events => {
localStorage.setItem('data', JSON.stringify(events))
});
subject
.bufferTime(10000)
.map(() => {
subject.observers[0]._seed = [];
})
.subscribe(() => localStorage.removeItem('data'));
I asked a very similar question few days ago and later answered myself
accumulating values such as with scan but with the possibility to reset the accumulator over time
maybe this can help you
SOME MORE DETAILS
An alternative approach is to have an Observable which acts as a timer which emits at a fixed interval, 10000ms in your case.
Once this timer emits, you pass the control to the Observable that cumululates via scan operator. To pass the control you use the switchMap operator to make sure the previous instance of the Observable completes.
If I understand correctly what you want to achieve, I would use a normal Subject rather than ReplaySubject.
The code could look something like this
const subject = new Subject<number>();
const timer = Observable.timer(0, 1000).take(4);
const obs = timer.switchMap(
() => {
console.log('-----');
return subject
.scan((acc, cur) => {
acc.push(cur);
return acc;
}, []);
}
)
obs.subscribe(
events => {
console.log(JSON.stringify(events))
}
);
// TEST DATA EMITTED BY THE SUBJECT
setTimeout(() => {
subject.next(1);
}, 100);
setTimeout(() => {
subject.next(2);
}, 1100);
setTimeout(() => {
subject.next(3);
}, 2100);
setTimeout(() => {
subject.next(4);
}, 2200);
I have two source observables.
I would like to merge the two source observables, but the merged observable sould complete as soon as one of the source observables completes.
Desired behavior:
Source 1: ---1--------3--4-----------------------------x
Source 2: -------2----------x
"merged" ---1---2----3--4--x
In case of an error on one of the sources, the error should propagate to the merged observable:
Source 1: ---1--------3--4-----------------------------x
Source 2: -------2----------e
"merged" ---1---2----3--4--ex
The "merge" operator only completes the merged stream when both sources have completed:
Source 1: ---1--------3--4-----------------------------x
Source 2: -------2----------x
"merged" ---1---2----3--4-----------------------------x
How can I achieve my desired behavior?
You need to work with the metadata, information about each observable. To do this, use the materialize() operator on each stream and the use dematerialize() on the merged stream to actually emit the data.
Observable.merge( observableA.materialize(),
observableB.materialize() )
.takeWhile( notification -> notification.hasValue() )
.dematerialize()
.subscribe( ... );
This will merge the two observables until either one of them completes or emits an error.
I sure hope someone else answers with more elegant method but this works.
I think you would have to use one of the take operators. You could complete all sources when one source completes like so:
const a = Rx.Observable.interval(1000).take(3).map(x => `a${x}`);
const b = Rx.Observable.interval(800).take(6).map(x => `b${x}`);
Rx.Observable.merge(a.takeUntil(b.last()), b.takeUntil(a.last()))
.subscribe(
x => { console.log('next', x); },
null,
() => { console.log('complete'); }
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Or a less readable but more scaleable version:
function merge(...obs) {
return Rx.Observable.merge(...obs.map(x => x.takeUntil(Rx.Observable.race(obs.filter(y => y !== x).map(z => z.last())))));
}
const a = Rx.Observable.interval(1000).take(3).map(x => `a${x}`);
const b = Rx.Observable.interval(800).take(6).map(x => `b${x}`);
merge(a, b)
.subscribe(
x => { console.log('next', x); },
null,
() => { console.log('complete'); }
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Here is an illustration with error propagation:
function merge(...obs) {
return Rx.Observable.merge(...obs.map(x => x.takeUntil(Rx.Observable.race(obs.filter(y => y !== x).map(z => z.last())))));
}
const a = Rx.Observable.interval(1000).take(3).map(x => `a${x}`);
const b = Rx.Observable.interval(800).take(6).map(x => `b${x}`);
const c = Rx.Observable.timer(2200).map(x => { throw 'oops!'; });
merge(a, b, c)
.subscribe(
x => { console.log('next', x); },
x => { console.log('error', x); },
() => { console.log('complete'); }
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Using the takeUntil outside on the merge is tricky as you would loose the last emitted value.
When an an observable completes, it does not emit a value, but we can concat it with another 'signal' observable that emits a single value. We can then watch for the 'signal' observable's value with the takeWhile operator.
Of course you'd have to ensure that the 'signal' observable's emitted value is not a value that could be emitted by the observables that are being merged - an empty object will suffice if the takeWhile predicate compares by reference.
Here's an example:
const obs1$ = Rx.Observable.interval(1000)
.map(x => `obs1: ${x}`)
.take(5);
const obs2$ = Rx.Observable.interval(300)
.map(x => `obs2: ${x}`)
.take(9);
const signalFinishMessage = {};
const signalFinish$ = Rx.Observable.of(signalFinishMessage);
Rx.Observable.merge(obs1$.concat(signalFinish$), obs2$.concat(signalFinish$))
.takeWhile(x => x !== signalFinishMessage)
.subscribe(
x => console.log(x),
err => console.log('received error:', err),
() => console.log('complete')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Errors will also get propagated:
const obs1$ = Rx.Observable.interval(1000)
.map(x => `obs1: ${x}`)
.take(5);
const obs2$ = Rx.Observable.interval(300)
.map(x => `obs2: ${x}`)
.take(9)
.concat(Rx.Observable.throw(`the world's about to end`));
const signalFinishMessage = {};
const signalFinish$ = Rx.Observable.of(signalFinishMessage);
Rx.Observable.merge(obs1$.concat(signalFinish$), obs2$.concat(signalFinish$))
.takeWhile(x => x !== signalFinishMessage)
.subscribe(
x => console.log(x),
err => console.log('received error:', err),
() => console.log('complete')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
I ended up rolling my own:
import { Observable } from 'rxjs';
export function whileAll<T>(...observables: Observable<T>[]): Observable<T> {
return new Observable<T>(function (observer) {
if (observables.length === 0)
observer.complete();
else {
const next = observer.next.bind(observer);
const error = observer.error.bind(observer);
const complete = observer.complete.bind(observer);
for (let i = 0; i < observables.length; i++)
observer.add(observables[i].subscribe(next, error, complete));
}
});
}