import { Observable } from 'rxjs'; import { merge, of } from 'rxjs'; import { concat, switchMap, concatMapTo, filter, map, scan } from 'rxjs/operators'; import type { ReactiveServiceMixin } from './interfaces'; import { Paginated } from '@feathersjs/feathers'; export function strategies() { return { // created$: new Observable(), // updated$: new Observable(), // patched$: new Observable(), // removed$: new Observable(), // reset$: new Observable(), never(source$: Observable) { return source$; }, always(source$: Observable, options, args) { const params = args[0] || {}; const query = Object.assign({}, params.query); // A function that returns if an item matches the query const matches = options.matcher(query); const events$ = merge( this.created$.pipe(filter(matches)), this.removed$, this.updated$, this.patched$ ); return source$.pipe(concat(events$.pipe(concatMapTo(source$)))); }, smart(source$: Observable, options, args) { const params = args[0] || {}; const query = Object.assign({}, params.query); // A function that returns if an item matches the query const matches = options.matcher(query); // A function that sorts a limits a result (paginated or not) const sortAndTrim = options.sorter(query, options); const onCreated = (eventData: any) => { return (page: Paginated | T[]) => { const isPaginated = !!(page as any)[options.dataField]; const process = (data: T[]) => { // We should not add object twice const exists = data.find( (current) => eventData[options.idField] === (current as any)[options.idField] ); return exists ? data : data.concat(eventData); }; if (isPaginated) { return Object.assign({}, page, { total: (page as Paginated).total + 1, [options.dataField]: process((page as any)[options.dataField]) }); } return process(page as T[]); }; }; const onRemoved = (eventData: any) => { return (page: Paginated | T[]) => { const isPaginated = !!(page as any)[options.dataField]; const process = (data: T[]) => data.filter( (current) => eventData[options.idField] !== (current as any)[options.idField] ); if (isPaginated) { return Object.assign({}, page, { total: matches(eventData) ? (page as Paginated).total - 1 : (page as Paginated).total, [options.dataField]: process((page as any)[options.dataField]) }); } return process(page as T[]); }; }; const onUpdated = (eventData: any) => { return (page: Paginated | T[]) => { const isPaginated = !!(page as any)[options.dataField]; const length = isPaginated ? (page as any)[options.dataField].length : (page as T[]).length; const process = (data: T[]) => { // Remove previous matching object if any let newData = data.filter( (current) => eventData[options.idField] !== (current as any)[options.idField] ); // We should not add object we are not already aware of // except if update has changed it from an unmatched to a matched object if (newData.length < data.length || matches([eventData])) { newData = newData.concat(eventData); } return newData.filter(matches); }; if (isPaginated) { const processed = process((page as any)[options.dataField]); return Object.assign({}, page, { // Total can be either decreased or increased based // on if the update removed or added the item to the list total: (page as Paginated).total - (length - processed.length), [options.dataField]: processed }); } return process(page as T[]); }; }; const events$ = merge( this.created$.pipe(filter(matches), map(onCreated)), this.removed$.pipe(map(onRemoved)), merge(this.updated$, this.patched$).pipe(map(onUpdated)) ); const reset$ = this.reset$; return merge(source$, reset$.pipe(concatMapTo(source$))).pipe( switchMap((data) => of(data).pipe( concat( events$.pipe( scan( (current, callback) => sortAndTrim((callback as any)(current)), // TODO: Hacky type cast to make typescript happy data ) ) ) ) ) ); } } as ReactiveServiceMixin; }