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
Related
I'm using rxjs to connect to a WebSocket service, and in case of failure, I want to retry 3 times, wait 30 seconds, then repeat infinitely, how can I do this?
I found a solution, first, create the following operator:
function retryWithDelay<T>(
repetitions: number,
delay: number
): (a: Observable<T>) => Observable<T> {
let count = repetitions;
return (source$: Observable<T>) =>
source$.pipe(
retryWhen((errors) =>
errors.pipe(
delayWhen(() => {
count--;
if (count === 0) {
count = repetitions;
return timer(delay);
}
return timer(0);
})
)
)
);
}
Then, use use it like this:
function connect(url: string) {
return webSocket({ url })
.pipe(retryWithDelay(3, 30000));
}
You can do this by doing the following:
//emit value every 200ms
const source = Rx.Observable.interval(200);
//output the observable
const example = source
.map(val => {
if (val > 5) {
throw new Error('The request failed somehow.');
}
return val;
})
.retryWhen(errors => errors
//log error message
.do(val => console.log(`Some error that occur ${val}, pauze for 30 seconds.`))
//restart in 30 seconds
.delayWhen(val => Rx.Observable.timer(30 * 1000))
);
const subscribe = example
.subscribe({
next: val => console.log(val),
error: val => console.log(`This will never happen`)
});
See the working example: https://jsbin.com/goxowiburi/edit?js,console
Be aware that this is an infinite loop and you are not introducing unintended consequences into your code.
I have 2 observables :
a source : emits an event each time a change is made in a form
a "inspector" : emits an event to tell if the change can be saved or not
What I'm trying to do is :
the source emits values
debounce values for a minute
if the controller didn't emit "false" during that time then emit latest value from source
I have seen some similar problems resolved with withLatestFrom(inspector).filter(...) but it does not work for me as I need all values emitted from the inspector observable during the debounce time.
I also tried a 'merge' operator but I only care about the source : if the inspector emits values but the source doesn't, the observable i'm trying to build shouldn't either.
Is there a way to achieve this using only observables ?
It is helpful to break this problem down. I don't quite understand what you're asking for, so here's my best impression:
Source emits a value.
After that initial emission we start listening to the inspector for true values.
Once the debounce time is passed we emit a value if the inspector only emitted true.
The first observation I'd like to make (pardon the pun), is that you don't have to use debounceTime to have a debounce-like effect. I find that switchMap with a timer on the inside can produce the same results: SwitchMap will cancel previous emissions like debounce, and the timer can act to delay the emission.
My proposal is that you use switchMap from your source and then create an observable from the combination of a timer and your inspector. Use a filter operator so you only emit from the source if the inspector's last emitted result duration the duration of the timer was true.
this.source.pipe(
switchMap(x => // switchMap will cancel any emissions if the timer hasn't emitted yet.
// this combineLatest will only emit once - when the timer emits.
combineLatest([
timer(60000),
this.inspector.pipe(filter(x => !x), startWith(true))
]).pipe(
filter(([_, shouldEmit]) => shouldEmit),
mapTo(x) // emit source's value
)
)
)
Note: startWith is used in the inspector's pipe so that at least one result is emitted. This guarantees that there will be a single emission once timer emits. The filter is on the inspector since all you care about is if a false result prevents emission.
If you don't want to force the user to wait a minute, you could just use race instead of combineLatest. It will emit the result from the first observable that emits. So you can have the timer emit true after a minute, and the inspector emit any false result.
switchMap(x =>
race(
timer(6000).pipe(mapTo(true)), // emit true after a minute.
this.inspector.pipe(filter(x => !x)) // only emit false
).pipe(
take(1), // this might not be necessary.
filter((shouldEmit) => shouldEmit),
mapTo(x) // emit source's value
)
)
It could be solved by using the buffer operator that raises notification only after required interval elapsed in case the blocker stream does not cancel it in advance.
source$.pipe(
buffer(source$.pipe(
exhaustMap(() => timer(10).pipe(
takeUntil(blocker$)
))
))
);
const {timer} = rxjs;
const {buffer, exhaustMap, takeUntil} = rxjs.operators;
const {TestScheduler} = rxjs.testing;
const {expect} = chai;
const test = (testName, testFn) => {
try {
testFn();
console.log(`Test PASS "${testName}"`);
} catch (error) {
console.error(`Test FAIL "${testName}"`, error.message);
}
}
const createTestScheduler = () => new TestScheduler((actual, expected) => {
expect(actual).deep.equal(expected);
});
const createTestStream = (source$, blocker$) => {
return source$.pipe(
buffer(source$.pipe(
exhaustMap(() => timer(10).pipe(
takeUntil(blocker$)
))
))
);
}
const testStream = ({ marbles, values}) => {
const testScheduler = createTestScheduler();
testScheduler.run((helpers) => {
const { cold, hot, expectObservable } = helpers;
const source$ = hot(marbles.source);
const blocker$ = hot(marbles.blocker);
const result$ = createTestStream(source$, blocker$)
expectObservable(result$).toBe(marbles.result, values.result);
});
}
test('should buffer changes with 10ms delay', () => {
testStream({
marbles: {
source: ' ^-a-b 7ms ---c 9ms -----| ',
blocker: '^- 10ms --- 10ms -----| ',
result: ' -- 10ms i-- 10ms j----(k|)',
},
values: {
result: {
i: ['a', 'b'],
j: ['c'],
k: [],
},
}
});
});
test('should block buffer in progress and move values to next one', () => {
testStream({
marbles: {
source: ' ^-a-b 7ms ---c 9ms -----| ',
blocker: '^- 8ms e---- 10ms -----| ',
result: ' -- 10ms --- 10ms j----(k|)',
},
values: {
result: {
j: ['a', 'b', 'c'],
k: [],
},
}
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.1.2/chai.js"></script>
<script src="https://unpkg.com/rxjs#^7/dist/bundles/rxjs.umd.min.js"></script>
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 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
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);