How can I achive something like the following using RxJS?
a-b-c-d-e-f-g-h
ab---cd---ef---gh
or
a-b-c-d-e-f-g-h
abc----def----gh
I have an array which I need to split by specified partitions and emit values with specified interval.
Note you can't get 'ab--' from 'a-b-' because you must wait for 'b', so strictly the marble diagrams will be
a-b-c-d-e-f-g-h-|
-(ab)---(cd)---(ef)---(gh|)
or
a-b-c-d-e-f-g-h-|
--(abc)----(def)----(gh|)
console.clear()
const Observable = Rx.Observable
const timedEmitter = (observable, interval) =>
Observable.zip(observable, Observable.timer(0, interval))
.map(x => x[0])
const source = Observable.from(['a','b','c','d','e','f','g','h'])
const timedSource = timedEmitter(source, 1000)
const interval = 5000
const size = 3
const output = timedEmitter(timedSource.bufferCount(size), interval)
//Display
const start = new Date()
output.timestamp()
.map(x => { return {value: x.value, elapsed: x.timestamp - start} })
.subscribe(console.log)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>
Footnote, changing Observable.interval(interval) to Observable.timer(0, interval) in timedEmitter to get first emit asap.
Footnote#2, this is not quite right, because the complete() of source shorts the last interval.
This was just an artifact of the chosen intervals.
Here's a custom operator version using pipe() from RxJs 5.5, ref Build your own operators easily
console.clear()
const Observable = Rx.Observable
const timedEmitter = (observable, interval) =>
Observable.zip(observable, Observable.timer(0, interval))
.map(x => x[0])
const source = Observable.from(['a','b','c','d','e','f','g','h'])
const timedSource = timedEmitter(source, 1000)
// Custom operator
const timedBufferedEmitter = (interval, bufferSize) => (observable) =>
Observable.zip(observable.bufferCount(bufferSize), Observable.timer(0, interval))
.map(x => x[0])
const interval = 5000
const size = 3
const output = timedSource.pipe(timedBufferedEmitter(interval, size))
//Display
const start = new Date()
output.timestamp()
.map(x => { return {value: x.value, elapsed: x.timestamp - start} })
.subscribe(console.log)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>
Related
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
Suppose that you have a function that returns an rxjs observable that contains a list of objects.
const getItems = () =>
of([
{
id: 1,
value: 10
},
{
id: 2,
value: 20
},
{
id: 3,
value: 30
}
]);
and a second function that returns an observable with a single object
const getItem = id =>
of({
id,
value: Math.floor(Math.random() * 30) + 1
});
Now we want to create an observable that will get the first list and at a regular interval will randomly update any list item.
const source = getItems().pipe(
switchMap(items =>
interval(5000).pipe(
switchMap(x => {
// pick up a random id
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId).pipe(
map(item =>
items.reduce(
(acc, cur) =>
cur.id === item.id ? [...acc, item] : [...acc, cur],
[]
)
)
);
})
)
)
);
source.subscribe(x => console.log(JSON.stringify(x)));
The problem with the above code is that each time the interval is triggered the items from the previous iteration reset to their initial form. e.g,
[{"id":1,"value":10},{"id":2,"value":13},{"id":3,"value":30}]
[{"id":1,"value":10},{"id":2,"value":20},{"id":3,"value":18}]
[{"id":1,"value":10},{"id":2,"value":16},{"id":3,"value":30}]
[{"id":1,"value":21},{"id":2,"value":20},{"id":3,"value":30}]
As you see, on each interval our code is resetting the list and updates a new item (eg value 13 is lost in the second iteration and reverts to 20).
The behaviour seems reasonable since the items argument in the first switchMap acts like a closure.
I managed to somehow solve the issue by using BehaviorSubject but i think that my solution is somehow dirty.
const items$ = new BehaviorSubject([]);
const source = getItems().pipe(
tap(items => items$.next(items)),
switchMap(() =>
interval(5000).pipe(
switchMap(() => {
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId).pipe(
map(item =>
items$
.getValue()
.reduce(
(acc, cur) =>
cur.id === item.id ? [...acc, item] : [...acc, cur],
[]
)
),
tap(items => items$.next(items)),
switchMap(() => items$)
);
})
)
)
);
Is there a better approach ?
Example code can be found here
I believe this should be doing what you want:
const source = getItems().pipe(
switchMap(items =>
interval(1000).pipe(
switchMap(() => {
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId);
}),
scan((acc, item) => {
acc[acc.findIndex(i => i.id === item.id)] = item;
return acc;
}, items),
)
)
);
It's basically what you're doing but I'm using scan (that is initialized with the original items) to keep the output array in acc so I can update it later again.
Live demo: https://stackblitz.com/edit/rxjs-kvygy1?file=index.ts
I want to combine 2 or more observables dynamically into one combined observable.
I understood how to combine two observables that already exist with merge, but how can I solve "merging" when the additional observable needs to be added dynamically (e.g. after a timeout)?
Also the existing subscriptions on combinedStream$ should not be lost when merging another observable "on the fly".
Here is what I have so far:
const action1$ = interval(1000).pipe(map(data => 'Action1 value:' + data));
const action2$ = interval(1000).pipe(map(data => 'Action2 value:' + data));
const combinedStream$ = merge(action1$, action2$);
combinedStream$.subscribe(data => console.log('Combined Stream Output:', data));
// Add another observable after some time...
setTimeout(() => {
const action3$ = interval(1000).pipe(map(data => 'Action3 value:' + data));
// How add this action3$ to the combined stream ?
}, 1000);
Here is my stackblitz: https://stackblitz.com/edit/rxjs-s2cyzj
When dealing with that use case, the easiest thing to do is to have an observable... of observable and then use a higher order function like concatAll, switch, mergeAll...
const action1$: Observable<string> = interval(2000).pipe(
map(data => "Action1 value:" + data)
);
const action2$: Observable<string> = interval(2000).pipe(
map(data => "Action2 value:" + data)
);
const action3$: Observable<string> = interval(2000).pipe(
map(data => "Action3 value:" + data)
);
const mainStream$$: Subject<Observable<string>> = new Subject();
const combinedStream$ = mainStream$$.pipe(mergeAll());
combinedStream$.subscribe(data => console.log("Combined Stream Output:", data));
mainStream$$.next(action1$);
mainStream$$.next(action2$);
// Add another stream after some time...
setTimeout(() => {
mainStream$$.next(action3$);
}, 1000);
Demo: https://stackblitz.com/edit/rxjs-stwvtc?file=index.ts
Requirement:
urls = [url1, url2, url3]
Fire all 3 urls parallely and paint the Dom in the sequnce of the urls list
ex: Finished order of urls = [url3, url1, url2]
when url1 finishes Immediately render the DOM, without waiting for url2
If url2, url3 finishes before url1, then store url2, url3 and paint the DOM after url1 arrives
Paint the DOM with order [url1, url2, url3]
My Work using promises:
// Fired all 3 urls at the same time
p1 = fetch(url1)
p2 = fetch(url2)
p3 = fetch(url3)
p1.then(updateDom)
.then(() => p2)
.then(updateDom)
.then(() => p3)
.then(updateDom)
I wanted to do the same thing in Observables.
from(urls)
.pipe(
mergeMap(x => fetch(x))
)
To fire them parallely I used merge map, but how can I order the sequence of the results?
The best way to preserve order with async tasks like this is with concatMap.
The problem is that if we apply this alone, we lose the parallelisation. If we were to do something like this:
from(urls)
.pipe(
concatMap(x => fetch(x))
);
the second request is not fired until the first is complete.
We can get around this by separating out the map into its own operator:
from(urls)
.pipe(
map(x => fetch(x)),
concatMap(x => x)
);
The requests will all be fired at the same time, but the results will be emitted in request order.
See Adrian's example adapted to use this approach below:
const { from } = rxjs;
const { concatMap, map } = rxjs.operators;
function delayPromise(value, delay) {
return new Promise(resolve => setTimeout(() => resolve(value), delay));
}
var delay = 3;
from([1, 2, 3]).pipe(
map(x => delayPromise(x, delay-- * 1000)),
concatMap(x => x)
).subscribe(result => { console.log(result); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
I couldn't find anything that preserves the order so I came up with something a bit convoluted.
const { concat, of, BehaviorSubject, Subject } = rxjs;
const { delay, filter } = rxjs.operators;
const parallelExecute = (...obs$) => {
const subjects = obs$.map(o$ => {
const subject$ = new BehaviorSubject();
const sub = o$.subscribe(o => { subject$.next(o); });
return { sub: sub, obs$: subject$.pipe(filter(val => val)) };
});
const subject$ = new Subject();
sub(0);
function sub(index) {
const current = subjects[index];
current.obs$.subscribe(c => {
subject$.next(c);
current.obs$.complete();
current.sub.unsubscribe();
if (index < subjects.length -1) {
sub(index + 1);
} else {
subject$.complete();
}
});
}
return subject$;
}
parallelExecute(
of(1).pipe(delay(3000)),
of(2).pipe(delay(2000)),
of(3).pipe(delay(1000))
).subscribe(result => { console.log(result); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
You can form a sequence with fetch and paint then forkJoin/Promise.all them
p1 = fetch(url1)
p2 = fetch(url2)
p3 = fetch(url3)
forkJoin(
from(p1).pipe(tap(_=>paint dom...))
from(p1).pipe(tap(_=>paint dom...))
from(p1).pipe(tap(_=>paint dom...))
).subscribe()
I'm working on a service layer that manages subscriptions.
I provide subject-backed observables to consumers like this:
const subject = new Subject();
_trackedSubjects.push(subject);
return subject.asObservable();
Different consumers may monitor the channel, so there may be several observables attached to each subject.
I'd like to monitor the count of subject.observers and if it ever drops back to 0, do some cleanup in my library.
I have looked at refCount, but this only is available on Observable.
I'd love to find something like:
subject.onObserverCountChange((cur, prev) =>
if(cur === 0 && prev !== 0) { cleanUp(subject) }
)
Is there a way to automatic cleanup like this on a subject?
Instead of using Subject - you should probably describe setup/cleanup logic when creating observable. See the example:
const { Observable } = rxjs; // = require("rxjs")
const { share } = rxjs.operators; // = require("rxjs/operators")
const eventSource$ = Observable.create(o => {
console.log('setup');
let i = 0
const interval = setInterval(
() => o.next(i++),
1000
);
return () => {
console.log('cleanup');
clearInterval(interval);
}
});
const events$ = eventSource$.pipe(share());
const first = events$.subscribe(e => console.log('first: ', e));
const second = events$.subscribe(e => console.log('second: ', e));
setTimeout(() => first.unsubscribe(), 3000);
setTimeout(() => second.unsubscribe(), 5000);
<script src="https://unpkg.com/rxjs#6.2.2/bundles/rxjs.umd.min.js"></script>