145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
import { Injectable } from '@angular/core';
|
|
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
|
import { filter, map, startWith } from 'rxjs/operators';
|
|
import { Entity, Id } from '../listing';
|
|
import { List, shareLast } from '../utils';
|
|
import { isArray } from '../permissions';
|
|
|
|
@Injectable()
|
|
export abstract class EntitiesMapService<Interface, Class extends Entity<Interface, PrimaryKey>, PrimaryKey extends Id = Class['id']> {
|
|
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: List<Id>): void;
|
|
delete(key: Id, entityId: PrimaryKey | Class): void;
|
|
delete(keys: List<Id> | Id, entity?: PrimaryKey | Class): void {
|
|
if (isArray(keys)) {
|
|
return keys.forEach(key => this._map.delete(key));
|
|
}
|
|
|
|
if (entity) {
|
|
const entityId = typeof entity === 'string' || typeof entity === 'number' ? entity : entity.id;
|
|
const entities = this.get(keys).filter(entity => entity.id !== entityId);
|
|
return this.set(keys, entities);
|
|
}
|
|
|
|
console.error('entityId is null when deleting from EntitiesMapService');
|
|
}
|
|
|
|
get$(key: Id) {
|
|
if (!this.has(key)) {
|
|
this._map.set(key, new BehaviorSubject<Class[]>([]));
|
|
}
|
|
|
|
return this._getBehaviourSubject(key).asObservable();
|
|
}
|
|
|
|
has(key: Id) {
|
|
return this._map.has(key);
|
|
}
|
|
|
|
get(key: Id): Class[];
|
|
get(key: Id, id: PrimaryKey): Class | undefined;
|
|
get(key: Id, id?: PrimaryKey): 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 = 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);
|
|
}
|
|
}
|
|
|
|
add(key: string, entity: Class) {
|
|
const entities = this.get(key);
|
|
this.set(key, [...entities, entity]);
|
|
}
|
|
|
|
/** Return true if entities were replaced or false if not */
|
|
replace(key: string, entities: Class[]) {
|
|
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 newEntitiesIds = newEntities.map(entity => entity.id);
|
|
const all = this.get(key).filter(e => !newEntitiesIds.includes(e.id));
|
|
this.set(key, [...all, ...newEntities]);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
watch$(key: string, entityId: PrimaryKey): Observable<Class> {
|
|
return this.#entityChanged$.pipe(
|
|
filter(entity => entity.id === entityId),
|
|
startWith(this.get(key, entityId) as Class),
|
|
shareLast(),
|
|
);
|
|
}
|
|
|
|
watchChanged$(key: Id): Observable<boolean> {
|
|
// TODO: This is wrong, entityChanged emits only one entity at a time
|
|
return this.#entityChanged$.pipe(
|
|
startWith(this.get(key)),
|
|
map(entities => entities as Class[]),
|
|
map(entities => !!entities.length),
|
|
);
|
|
}
|
|
|
|
watchDeleted$(entityId: PrimaryKey): Observable<Class> {
|
|
return this.#entityDeleted$.pipe(filter(entity => entity.id === entityId));
|
|
}
|
|
|
|
private _getBehaviourSubject(key: Id): BehaviorSubject<Class[]> {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
return this._map.get(key)!;
|
|
}
|
|
}
|