common-ui/src/lib/services/entities-map.service.ts
2022-10-26 16:07:15 +03:00

137 lines
4.6 KiB
TypeScript

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject, switchMap } from 'rxjs';
import { filter, map, startWith } from 'rxjs/operators';
import { Entity } from '../listing';
import { RequiredParam, shareLast, Validate } from '../utils';
import { Id } from '../listing/models/trackable';
@Injectable({ providedIn: 'root' })
export abstract class EntitiesMapService<Interface, Class extends Entity<Interface, PrimaryKey>, PrimaryKey extends Id = Class['id']> {
protected abstract readonly _primaryKey: string;
protected readonly _map = new Map<Id, BehaviorSubject<Class[]>>();
readonly #entityChanged$ = new Subject<Class>();
readonly #entitiesChanged$ = new BehaviorSubject<boolean>(false);
readonly #entityDeleted$ = new Subject<Class>();
get empty(): boolean {
return this._map.size === 0;
}
delete(keys: Id[]): void {
keys.forEach(key => this._map.delete(key));
}
get$(key: Id) {
if (!this._map.has(key)) {
this._map.set(key, new BehaviorSubject<Class[]>([]));
}
return this._getBehaviourSubject(key).asObservable();
}
has(parentId: Id) {
return this._map.has(parentId);
}
get(key: Id): Class[];
get(key: Id, id: Id): Class | undefined;
get(key: Id, id?: Id): Class | Class[] | undefined {
const value = this._getBehaviourSubject(key)?.value;
if (!id) {
return value ?? [];
}
return value?.find(item => item.id === id);
}
set(key: Id, entities: Class[]): void {
if (!this._map.has(key)) {
this._map.set(key, new BehaviorSubject<Class[]>(entities));
return entities.forEach(entity => this.#entityChanged$.next(entity));
}
const changedEntities: Class[] = [];
const deletedEntities = this.get(key).filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id));
// Keep old object references for unchanged entities
const newEntities: Class[] = entities.map(newEntity => {
const oldEntity: Class | undefined = this.get(key, newEntity.id);
if (oldEntity && newEntity.isEqual(oldEntity)) {
return oldEntity;
}
changedEntities.push(newEntity);
return newEntity;
});
this._getBehaviourSubject(key).next(newEntities);
// Emit observables only after entities have been updated
if (changedEntities.length || deletedEntities.length) {
this.#entitiesChanged$.next(true);
}
for (const entity of changedEntities) {
this.#entityChanged$.next(entity);
}
for (const entity of deletedEntities) {
this.#entityDeleted$.next(entity);
}
}
replace(entities: Class[]) {
/** Return true if entities were replaced or false if not **/
const key = this._pluckPrimaryKey(entities[0]);
const entityIds = entities.map(entity => entity.id);
const existingEntities = this.get(key).filter(entity => entityIds.includes(entity.id));
const newEntities = entities.filter(entity => {
const old = existingEntities.find(e => e.id === entity.id);
return !old || !entity.isEqual(old);
});
if (newEntities.length) {
const all = this.get(key).filter(e => !newEntities.map(entity => entity.id).includes(e.id));
this.set(key, [...all, ...newEntities]);
return true;
}
return false;
}
@Validate()
watch$(@RequiredParam() key: Id, @RequiredParam() entityId: Id): Observable<Class> {
return this.#entityChanged$.pipe(
filter(entity => entity.id === entityId),
startWith(this.get(key, entityId) as Class),
shareLast(),
);
}
@Validate()
watchChanged$(@RequiredParam() key: Id): Observable<boolean> {
return this.#entityChanged$.pipe(
startWith(this.get(key)),
map(entities => entities as Class[]),
map(entities => !!entities.length),
);
}
watchDeleted$(entityId: Id): Observable<Class> {
return this.#entityDeleted$.pipe(filter(entity => entity.id === entityId));
}
private _pluckPrimaryKey(entity: Class): Id {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return entity[this._primaryKey] as Id;
}
private _getBehaviourSubject(key: Id): BehaviorSubject<Class[]> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this._map.get(key)!;
}
}