I have a filterable 'activity log' that's currently implemented using a ReplaySubject (since a few components use it and they might subscribe at different times).
When the user changes the filter settings, a new request is made, however the results are appended to the ReplaySubject rather than replacing it.
I was wondering if there is anyway to update the ReplaySubject to only send through the new items using something like a switchMap?
Otherwise, I might need to either use a BehaviorSubject that returns an array of all the activity entries or recreate the ReplaySubject and notify users (probably by using another observable) to unsubscribe and resubscribe to the new observable.
If you want to be able to reset a subject without having its subscribers explicitly unsubscribe and resubscribe, you could do something like this:
import { Observable, Subject } from "rxjs";
import { startWith, switchMap } from "rxjs/operators";
function resettable<T>(factory: () => Subject<T>): {
observable: Observable<T>,
reset(): void,
subject: Subject<T>
} {
const resetter = new Subject<any>();
const source = new Subject<T>();
let destination = factory();
let subscription = source.subscribe(destination);
return {
observable: resetter.asObservable().pipe(
startWith(null),
switchMap(() => destination)
),
reset: () => {
subscription.unsubscribe();
destination = factory();
subscription = source.subscribe(destination);
resetter.next();
},
subject: source
};
}
resettable will return an object containing:
an observable to which subscribers to the re-settable subject should subscribe;
a subject upon which you'd call next, error or complete; and
a reset function that will reset the (inner) subject.
You'd use it like this:
import { ReplaySubject } from "rxjs";
const { observable, reset, subject } = resettable(() => new ReplaySubject(3));
observable.subscribe(value => console.log(`a${value}`)); // a1, a2, a3, a4, a5, a6
subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);
observable.subscribe(value => console.log(`b${value}`)); // b2, b3, b4, b5, b6
reset();
observable.subscribe(value => console.log(`c${value}`)); // c5, c6
subject.next(5);
subject.next(6);
Here is a class that is using the resettable factory posted here before, so you can use
const myReplaySubject = new ResettableReplaySubject<myType>()
import { ReplaySubject, Subject, Observable, SchedulerLike } from "rxjs";
import { startWith, switchMap } from "rxjs/operators";
export class ResettableReplaySubject<T> extends ReplaySubject<T> {
reset: () => void;
constructor(bufferSize?: number, windowTime?: number, scheduler?: SchedulerLike) {
super(bufferSize, windowTime, scheduler);
const resetable = this.resettable(() => new ReplaySubject<T>(bufferSize, windowTime, scheduler));
Object.keys(resetable.subject).forEach(key => {
this[key] = resetable.subject[key];
})
Object.keys(resetable.observable).forEach(key => {
this[key] = resetable.observable[key];
})
this.reset = resetable.reset;
}
private resettable<T>(factory: () => Subject<T>): {
observable: Observable<T>,
reset(): void,
subject: Subject<T>,
} {
const resetter = new Subject<any>();
const source = new Subject<T>();
let destination = factory();
let subscription = source.subscribe(destination);
return {
observable: resetter.asObservable().pipe(
startWith(null),
switchMap(() => destination)
) as Observable<T>,
reset: () => {
subscription.unsubscribe();
destination = factory();
subscription = source.subscribe(destination);
resetter.next();
},
subject: source,
};
}
}
I had kind of same problem: One of my components subscribed to an ReplaySubject of a shared service. Once navigated away and coming back the former values where still delivered to the component.
Just completing the subject was not enough.
The solutions above seemed to complicated for this purpose but I found another real simple solution in just completing the subject and assigning a newly created one in the shared service like so:
constructor() {
this.selectedFeatures = new ReplaySubject()
this.selectedFeaturesObservable$ = this.selectedFeatures.asObservable()
}
completeSelectedFeatures() {
this.selectedFeatures.complete()
this.selectedFeatures = new ReplaySubject()
this.selectedFeaturesObservable$ = this.selectedFeatures.asObservable()
}
I also printed the constructor of the shared service to show the types I used.
That way any time I move away from my component I just call that method on my shared service and hence get a new fresh and empty ReplaySubject anytime I navigate back to my component thats consuming the shared services observable.
I call that method inside ngOnDestroy Angular lifecycle hook:
ngOnDestroy() {
console.log('unsubscribe')
this.featureSub.unsubscribe()
this.sharedDataService.completeSelectedFeatures()
}
The problem becomes easier if you can use the fact that the buffer consumes data from the original source, and that subscribers to buffered data can switch to the original source after receiving all the old values.
Eg.
let data$ = new Subject<any>() // Data source
let buffer$ = new ReplaySubject<any>()
let bs = data$.subscribe(buffer$) // Buffer subscribes to data
// Observable that returns values until nearest reset
let getRepeater = () => {
return concat(buffer$.pipe(
takeUntil(data$), // Switch from buffer to original source when data comes in
), data$)
}
To clear, replace the buffer
// Begin Buffer Clear Sequence
bs.unsubscribe()
buffer$.complete()
buffer$ = new ReplaySubject()
bs = data$.subscribe(buffer$)
buffObs.next(buffer$)
To make the code more functional, you can replace the function getRepeater() with a subject that reflects the latest reference
let buffObs = new ReplaySubject<ReplaySubject<any>>(1)
buffObs.next(buffer$)
let repeater$ = concat(buffObs.pipe(
takeUntil(data$),
switchMap((e) => e),
), data$)
The following
let data$ = new Subject<any>()
let buffer$ = new ReplaySubject<any>()
let bs = data$.subscribe(buffer$)
let buffObs = new ReplaySubject<ReplaySubject<any>>(1)
buffObs.next(buffer$)
let repeater$ = concat(buffObs.pipe(
takeUntil(data$),
switchMap((e) => e),
), data$)
// Begin Test
data$.next(1)
data$.next(2)
data$.next(3)
console.log('rep1 sub')
let r1 = repeater$.subscribe((e) => {
console.log('rep1 ' + e)
})
// Begin Buffer Clear Sequence
bs.unsubscribe()
buffer$.complete()
buffer$ = new ReplaySubject()
bs = data$.subscribe(buffer$)
buffObs.next(buffer$)
// End Buffer Clear Sequence
console.log('rep2 sub')
let r2 = repeater$.subscribe((e) => {
console.log('rep2 ' + e)
})
data$.next(4)
data$.next(5)
data$.next(6)
r1.unsubscribe()
r2.unsubscribe()
data$.next(7)
data$.next(8)
data$.next(9)
console.log('rep3 sub')
let r3 = repeater$.subscribe((e) => {
console.log('rep3 ' + e)
})
Outputs
rep1 sub
rep1 1
rep1 2
rep1 3
rep2 sub
rep1 4
rep2 4
rep1 5
rep2 5
rep1 6
rep2 6
rep3 sub
rep3 4
rep3 5
rep3 6
rep3 7
rep3 8
rep3 9
For certain situations (ex. where everything is contained in one class), here's what I believe is a very concise solution, with few moving parts:
new subscribers will get the latest from value$$ unless reset$$ has been called since the last value
existing subscribers get each new item emitted to value$$
const value$$ = new Subject();
const reset$$ = new Subject();
const value$ = reset$$.pipe(
// can optionally startWith(null)
map(() => value$$.pipe(shareReplay(1)), // create a new stream every time reset emits
shareReplay(1), // this shares the latest cached value stream emitted
switchAll(), // subscribe to the inner cached value stream
)
Related
My function (lets call it myFunction) is getting an array of streams (myFunction(streams: Observable<number>[])). Each of those streams produces values from 1 to 100, which acts as a progress indicator. When it hits 100 it is done and completed. Now, when all of those observables are done I want to emit a value. I could do it this way:
public myFunction(streams: Observable<number>[]) {
forkJoin(streams).subscribe(_values => this.done$.emit());
}
This works fine, but imagine following case:
myFunction gets called with 2 streams
one of those streams is done, second one is still progressing
myFunction gets called (again) with 3 more streams (2nd one from previous call is still progressing)
I'd like to somehow add those new streams from 3rd bullet to the "queue", which would result in having 5 streams in forkJoin (1 completed, 4 progressing).
I've tried multiple approaches but can't get it working anyhow... My latest approach was this:
private currentProgressObs: Observable<any> | null = null;
private currentProgressSub: Subscription | null = null;
public myFunction(progressStreams: Observable<number>[]) {
const isUploading = this.cumulativeUploadProgressSub && !this.cumulativeUploadProgressSub.closed;
const currentConcatObs = this.currentProgressObs?.pipe(concatAll());
const currentStream = isUploading && this.currentProgressObs ? this.currentProgressObs : of([100]);
if (this.currentProgressSub) {
this.currentProgressSub.unsubscribe();
this.currentProgressSub = null;
}
this.currentProgressObs = forkJoin([currentStream, ...progressStreams]);
this.currentProgressSub = this.currentProgressObs.subscribe(
_lastProgresses => {
this._isUploading$.next(false); // <----- this is the event I want to emit when all progress is completed
this.currentProgressSub?.unsubscribe();
this.currentProgressSub = null;
this.currentProgressObs = null;
},
);
}
Above code only works for the first time. Second call to the myFunction will never emit the event.
I also tried other ways. I've tried recursion with one global stream array, in which I can add streams while the subscription is still avctive but... I failed. How can I achieve this? Which operator and in what oreder should I use? Why it will or won't work?
Here is my suggestion for your issue.
We will have two subjects, one to count the number of request being processed (requestsInProgress) and one more to mange the requests that are being processed (requestMerger)
So the thing that will do is whenever we want to add new request we will pass it to the requestMerger Subject.
Whenever we receive new request for processing in the requestMerger stream we will first increment the requestInProgress counter and after that we will merge the request itself in the source observable. While merging the new request/observable to the source we will also add the finalize operator in order to track when the request has been completed (reached 100), and when we hit the completion criteria we will decrement the request counter with the decrementCounter function.
In order to emit result e.g. to notify someone else in the app for the state of the pending requests we can subscribe to the requestsInProgress Subject.
You can test it out either here or in this stackBlitz
let {
interval,
Subject,
BehaviorSubject
} = rxjs
let {
mergeMap,
map,
takeWhile,
finalize,
first,
distinctUntilChanged
} = rxjs.operators
// Imagine next lines as a service
// Subject responsible for managing strems
let requestMerger = new Subject();
// Subject responsible for tracking streams in progress
let requestsInProgress = new BehaviorSubject(0);
function incrementCounter() {
requestsInProgress.pipe(first()).subscribe(x => {
requestsInProgress.next(x + 1);
});
}
function decrementCounter() {
requestsInProgress.pipe(first()).subscribe(x => {
requestsInProgress.next(x - 1);
});
}
// Adds request to the request being processed
function addRequest(req) {
// The take while is used to complete the request when we have `value === 100` , if you are dealing with http-request `takeWhile` might be redudant, because http request complete by themseves (e.g. the finalize method of the stream will be called even without the `takeWhile` which will decrement the requestInProgress counter)
requestMerger.next(req.pipe(takeWhile(x => x < 100)));
}
// By subscribing to this stream you can determine if all request are processed or if there are any still pending
requestsInProgress
.pipe(
map(x => (x === 0 ? "Loaded" : "Loading")),
distinctUntilChanged()
)
.subscribe(x => {
console.log(x);
document.getElementById("loadingState").innerHTML = x;
});
// This Subject is taking care to store or request that are in progress
requestMerger
.pipe(
mergeMap(x => {
// when new request is added (recieved from the requestMerger Subject) increment the requrest being processed counter
incrementCounter();
return x.pipe(
finalize(() => {
// when new request has been completed decrement the requrest being processed counter
decrementCounter();
})
);
})
)
.subscribe(x => {
console.log(x);
});
// End of fictional service
// Button that adds request to be processed
document.getElementById("add-stream").addEventListener("click", () => {
addRequest(interval(1000).pipe(map(x => x * 25)));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.6.6/rxjs.umd.min.js"></script>
<div style="display:flex">
<button id="add-stream">Add stream</button>
<h5>Loading State: <span id="loadingState">false</span> </h5>
</div>
Your problem is that each time your call your function, you are creating a new observable. Your life would be much easier if all calls of your function pushed all upload jobs through the same stream.
You can achieve this using a Subject.
I would suggest you push single "Upload Jobs" though a simple subject and design an observable that emits the state of all upload jobs whenever anything changes: A simple class that offers a createJob() method to submit jobs, and a jobs$ observable to reference the state:
class UploadService {
private jobs = new Subject<UploadJob>();
public jobs$ = this.jobs.pipe(
mergeMap(job => this.processJob(job)),
scan((collection, job) => collection.set(job.id, job), new Map<string, UploadJob>()),
map(jobsMap => Array.from(jobsMap.values()))
);
constructor() {
this.jobs$.subscribe();
}
public createJob(id: string) {
this.jobs.next({ id, progress: 0 });
}
private processJob(job: UploadJob) {
// do work and return observable that
// emits updated status of UploadJob
}
}
Let's break it down:
jobs is a simple subject, that we can push "jobs" through
createJob simply calls jobs.next() to push the new job through the stream
jobs$ is where all the magic happens. It receives each UploadJob and uses:
mergeMap to execute whatever function actually does the work (I called it processJob() for this example) and emits its values into the stream
scan is used to accumulate these UploadJob emissions into a Map (for ease of inserting or updating)
map is used to convert the map into an array (Map<string, UploadJob> => UploadJob[])
this.jobs$.subscribe() is called in the constructor of the class so that jobs will be processed
Now, we can easily derive your isUploading and cumulativeProgress from this jobs$ observable like so:
public isUploading$ = this.jobs$.pipe(
map(jobs => jobs.some(j => j.progress !== 100)),
distinctUntilChanged()
);
public progress$ = this.jobs$.pipe(
map(jobs => {
const current = jobs.reduce((sum, j) => sum + j.progress, 0) / 100;
const total = jobs.length ?? current;
return current / total;
})
);
Here's a working StackBlitz demo.
const a$ = new BehaviorSubject(['a']).pipe(
// op.share() does not have share
);
a$.subscribe((a) => {
console.log('a$', a)
})
const b$ = new BehaviorSubject([1]);
const c$ = b$.pipe(
op.withLatestFrom(a$)
)
c$.subscribe(c => {
console.log('c$ test', c) // you can find logs in console
})
but if I add a share to a$, subscription of c fail to run.
const a$ = new BehaviorSubject(['a']).pipe(
op.share()
);
a$.subscribe((a) => {
console.log('a$', a)
})
const b$ = new BehaviorSubject([1]);
const c$ = b$.pipe(
op.withLatestFrom(a$)
)
c$.subscribe(c => {
console.log('c$ test', c) // logs here cannot be found in console
})
I cannot understand why this happened. and I do need a$ to be shared. Is there any solution for this.
share doesn't replay values to late subscribers, so you lose the replay functionality of your BehaviorSubject. The first subscription to a$ triggers a subscribe to the BehaviorSubject and share forewards its value. When you subscribe a second time to a$ share has already subscribed to the BehaviorSubject so it won't get its value again and, as share has no replay functionality, won't replay its value.
If your source is a BehaviorSubject and you don't use any other operators you don't need to share it.
If your source is acutally something else your can use shareReplay(1) instead of share to replay its latest value to late subscribers.
I'm using rxjs.
I have a Browser that's responsible for a number of Page objects. Each page has an Observable<Event> that yields a stream of events.
Page objects are closed and opened at various times. I want to create one observable, called TheOneObservable that will merge all the events from all the currently active Page objects, and also merge in custom events from the Browser object itself.
Closing a Page means that the subscription to it should be closed so it doesn't prevent it from being GC'd.
My problem is that Pages can be closed at any time, which means that the number of Observables being merged is always changing. I've thought of using an Observable of Pages and using mergeMap, but there are problems with this. For example, a subscriber will only receive events of Pages that are opened after it subscribes.
Note that this question has been answered here for .NET, but using an ObservableCollection that isn't available in rxjs.
Here is some code to illustrate the problem:
class Page {
private _events = new Subject<Event>();
get events(): Observable<Event> {
return this._events.asObservable();
}
}
class Browser {
pages = [] as Page[];
private _ownEvents = new Subject<Event>();
addPage(page : Page) {
this.pages.push(page);
}
removePage(page : Page) {
let ixPage = this.pages.indexOf(page);
if (ixPage < 0) return;
this.pages.splice(ixPage, 1);
}
get oneObservable() {
//this won't work for aforementioned reasons
return Observable.from(this.pages).mergeMap(x => x.events).merge(this._ownEvents);
}
}
It's in TypeScript, but it should be understandable.
You can switchMap() on a Subject() linked to array changes, replacing oneObservable with a fresh one when the array changes.
pagesChanged = new Rx.Subject();
addPage(page : Page) {
this.pages.push(page);
this.pagesChanged.next();
}
removePage(page : Page) {
let ixPage = this.pages.indexOf(page);
if (ixPage < 0) return;
this.pages.splice(ixPage, 1);
this.pagesChanged.next();
}
get oneObservable() {
return pagesChanged
.switchMap(changeEvent =>
Observable.from(this.pages).mergeMap(x => x.events).merge(this._ownEvents)
)
}
Testing,
const page1 = { events: Rx.Observable.of('page1Event') }
const page2 = { events: Rx.Observable.of('page2Event') }
let pages = [];
const pagesChanged = new Rx.Subject();
const addPage = (page) => {
pages.push(page);
pagesChanged.next();
}
const removePage = (page) => {
let ixPage = pages.indexOf(page);
if (ixPage < 0) return;
pages.splice(ixPage, 1);
pagesChanged.next();
}
const _ownEvents = Rx.Observable.of('ownEvent')
const oneObservable =
pagesChanged
.switchMap(pp =>
Rx.Observable.from(pages)
.mergeMap(x => x.events)
.merge(_ownEvents)
)
oneObservable.subscribe(x => console.log('subscribe', x))
console.log('adding 1')
addPage(page1)
console.log('adding 2')
addPage(page2)
console.log('removing 1')
removePage(page1)
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
You will need to manage the subscriptions to the pages yourself and feed its events into the resulting subject yourself:
const theOneObservable$ = new Subject<Event>();
function openPage(page: Page): Subscription {
return page.events$.subscribe(val => this.theOneObservable$.next(val));
}
Closing the page, i.e. calling unsubscribe on the returned subscription, will already do everything it has to do.
Note that theOneObservable$ is a hot observable here.
You can, of course, take this a bit further by writing your own observable type which encapsulates all of this API. In particular, this would allow you to unsubscribe all inner observables when it is being closed.
A slightly different approach is this:
const observables$ = new Subject<Observable<Event>>();
const theOneObservable$ = observables$.mergeMap(obs$ => obs$);
// Add a page's events; note that takeUntil takes care of the
// unsubscription process here.
observables$.next(page.events$.takeUntil(page.closed$));
This approach is superior in the sense that it will unsubscribe the inner observables automatically when the observable is unsubscribed.
I am trying to use rxjs in my project. I have following sequences, what I expected is that the 1rd sequence will only be handled after a value arrive in another sequence, and only the latest value in the 1st sequence will be reserved. Any suggestion for it?
s1$ |a---b----c-
s2$ |------o----
expected result:
s3$ |------b--c-
I'd combine sample() that is already very similar to what you need and skipUntil().
const start = Scheduler.async.now();
const trigger = new Subject();
const source = Observable
.timer(0, 1000)
.share();
Observable.merge(source.sample(trigger).take(1), source.skipUntil(trigger))
.subscribe(val => console.log(Scheduler.async.now() - start, val));
setTimeout(() => {
trigger.next();
}, 2500);
This will output numbers starting with 2.
source 0-----1-----2-----3-----4
trigger ---------------X---------
output ---------------2--3-----4
Console output with timestamps:
2535 2
3021 3
4024 4
5028 5
Alternatively you could use switchMap() and ReplaySubject but it's probably not as obvious as the previous example and you need two Subjects.
const start = Scheduler.async.now();
const trigger = new Subject();
const source = Observable
.timer(0, 1000)
.share();
const replayedSubject = new ReplaySubject(1);
source.subscribe(replayedSubject);
trigger
.switchMap(() => replayedSubject)
.subscribe(val => console.log(Scheduler.async.now() - start, val));
setTimeout(() => {
trigger.next();
}, 2500);
The output is exactly the same.
I guess I would do this using a ReplaySubject:
const subject$ = new Rx.ReplaySubject(1)
const one$ = Rx.Observable.interval(1000)
const two$ = Rx.Observable.interval(2500)
one$.subscribe(subject$)
const three$ = two$
.take(1)
.flatMap(() => subject$)
// one$ |----0---1---2---3---4---
// two$ |----------0---------1---
// three$ |----------1-2---3---4---
last + takeUntil will work
here is an example:
let one$ = Rx.Observable.interval(1000);
let two$ = Rx.Observable.timer(5000, 1000).mapTo('stop');
one$
.takeUntil(two$)
.last()
.subscribe(
x=>console.log(x),
err =>console.error(err),
()=>console.log('done')
);
I have a websocket connection that is generating internal message events with a ReplaySubect. I process these events and add a delay to certain messages. Internally I use publish().refCount() twice, once on the internal ReplaySubject and again on the published output stream.
Should the internal subject have both 'publish' and 'refCount' called on it? I use 'publish' because I have multiple subscribers but I'm not entirely sure when to use 'refCount'.
Is it okay to just dispose of the internal subject? Will that clean up everything else?
Whoever subscribes to 'eventStream' should get the latest revision but the connection shouldn't wait for any subscribers
Example code:
function Connection(...) {
var messageSubject = new Rx.ReplaySubject(1);
var messageStream = messageSubject.publish().refCount();
// please ignore that we're not using rxdom's websocket.
var ws = new WebSocket(...);
ws.onmessage = function(messageEvent) {
var message = JSON.parse(messageEvent.data);
messageSubject.onNext(message);
}
ws.onclose = function(closeEvent) {
messageSubject.dispose(); // is this all I need to dispose?
}
var immediateRevisions = messageStream
.filter((e) => e[0] === "immediate")
.map((e) => ["revision", e[1]]);
var delayedRevisions = messageStream
.filter((e) => e[0] === "delayed")
.map((e) => ["revision", e[1]]).delay(1000);
var eventStream = Rx.Observable.merge(immediateRevisions, delayedRevisions).publish().refCount();
Object.defineProperties(this, {
"eventStream": { get: function() { return eventStream; }},
});
}
// using the eventStream
var cxn = new Connection(...)
cxn.eventStream.subscribe((e) => {
if (e[0] === "revision") {
// ...
}
});
publish and refCounting is basically what shareReplay does in RxJS4. Honestly though, you should just let your observable be "warm" and then use a ReplaySubject as a subscriber if you really want to guarantee that the last message gets pushed to new subscribers even if subscription count falls below one. e.g:
const wsStream = Observable.create(observer => {
ws.onmessage = message => observer.next(message);
ws.onclose = () => observer.complete();
});
const latestWsMessages = new ReplaySubject(1);
wsStream.subscribe(latestWsMessages);
Make sure you review how Observables work: after creating an observable, normally, each subscriber will call the subscription (cold), but in this case, you probably want a hot observable so that you have multiple subscribers sharing a subscription. See Andre's video here and the RxJS docs on creating observables for some more info.
Also, as useful as classes can be, looks like in this case you just want a function of makeWebsocketObservable(WebsocketConfig): Observable<WebsocketEvent>