I have a collection of widgets for each ownerId. I am trying to cache the state of these collections in an Angular service by using a Map<string, ReplaySubject<Widget[]>>.
@Injectable({
providedIn: 'root',
})
export class WidgetService {
private readonly widgetsByOwnerId: Map<string, ReplaySubject<Widget[]>> =
new Map<string, ReplaySubject<Widget[]>>();
constructor(
private readonly httpClient: HttpClient
) {}
getWidgets(ownerId: string): Observable<Widget[]> {
if (!this.widgetsByOwnerId.has(ownerId)) {
const widgets$ = this.httpClient.get<Widget[]>(`api/${ownerId}/widgets`);
const widgetsCache$ = new ReplaySubject<Widget[]>(1);
widgets$.subscribe(widgetsCache$);
this.widgetsById.set(ownerId, widgetsCache$);
}
return this.widgetsById.get(ownerId).asObservable();
}
createWidget(widget: Widget): Observable<Widget> {
return this.httpClient
.post<Widget>(`api/${widget.ownerId}/widgets`, widget)
.pipe(
tap((createdWidget): void => {
this.widgetsByOwnerId.forEach((widgets$, ownerId) =>
widgets$.pipe(take(1)).subscribe({
next: (widgets: Widget[]) => {
if(createdWidget.ownerId == ownerId || isOwnedById) {
widgets.push(createdWidget);
widgets$.next(widgets);
}
},
})
);
}
);
}
//additional CRUD functions excluded for brevity
}
Some special types of widgets can be owned by multiple widget owners, so some of the CRUD functions in the WidgetService can next() on multiple ownerId's ReplaySubjects. In the sample above, I omitted some of this logic for brevity using isOwnedById.
A consumer of this service looks like this:
@Component({
selector: 'app-widgets',
templateUrl: './widgets.component.html',
})
export class WidgetsComponent implements OnInit, OnDestroy {
widgets: Widget[];
private readonly ngUnsubscribe$: Subject<unknown> = new Subject();
constructor(
private readonly widgetService: WidgetService
) {}
ngOnInit(): void {
const ownerId = this.getOwnerId();
this.getWidgets(ownerId);
}
ngOnDestroy(): void {
this.ngUnsubscribe$.next();
this.ngUnsubscribe$.complete();
}
private getOwnerId(): string {
...
}
private getWidgets(ownerId: string): void {
this.widgetService
.getWidgets(ownerId)
.pipe(takeUntil(this.ngUnsubscribe$))
.subscribe({
// TODO: this next is called once when first subscribed, but not when future widgets are created
next: (widgets) => {
this.widgets = widgets;
},
});
}
}
For some reason, when the consuming component first initializes, it gets the cached Widget[] successfully. However, when CRUD operations like WidgetService.createWidget() execute in other components (while the instance of WidgetComponent remains alive, e.g. before WidgetComponent.ngOnDestroy()), this WidgetComponent consumer never receives the next()-ed updates (as noted in the TODO comment).
Any suggestions for how to solve this so that consumers continue to get fed updates from the WidgetService's ReplaySubjects? Thanks.
This question is kind of similar to this one, but different enough that I was not able to figure out how to adapt their answer to this use case.