UNPKG

@fireflysemantics/slice

Version:
1,643 lines (1,623 loc) 48.4 kB
import { ReplaySubject, of, fromEvent } from 'rxjs'; import { map, debounceTime, distinctUntilChanged, pairwise, switchMap, filter, takeWhile } from 'rxjs/operators'; import { nanoid } from 'nanoid'; ; /** * Abstract Entity base class with the * gid and id properties declared. */ class Entity { } const SCROLL_UP_DEBOUNCE_TIME_20 = 20; const SEARCH_DEBOUNCE_TIME_300 = 300; const { freeze } = Object; const ESTORE_DEFAULT_ID_KEY = 'id'; const ESTORE_DEFAULT_GID_KEY = 'gid'; const ESTORE_CONFIG_DEFAULT = freeze({ idKey: ESTORE_DEFAULT_ID_KEY, guidKey: ESTORE_DEFAULT_GID_KEY, }); class AbstractStore { constructor(config) { /** * Notifies observers of the store query. */ this.notifyQuery = new ReplaySubject(1); /** * The current query state. */ this._query = ''; /** * Primary index for the stores elements. */ this.entries = new Map(); /** * The element entries that are keyed by * an id generated on the server. */ this.idEntries = new Map(); /** * Create notifications that broacast * the entire set of entries. */ this.notify = new ReplaySubject(1); /** * Create notifications that broacast * store or slice delta state changes. */ this.notifyDelta = new ReplaySubject(1); /** * An Observable<E[]> reference * to the entities in the store or * Slice instance. */ this.obs = this.observe(); this.config = config ? freeze({ ...ESTORE_CONFIG_DEFAULT, ...config }) : ESTORE_CONFIG_DEFAULT; } /** * Sets the current query state and notifies observers. */ set query(query) { this._query = query; this.notifyQuery.next(this._query); } /** * @return A snapshot of the query state. */ get query() { return this._query; } /** * Observe the query. * @example <pre> let query$ = source.observeQuery(); </pre> */ observeQuery() { return this.notifyQuery.asObservable(); } /** * The current id key for the EStore instance. * @return this.config.idKey; */ get ID_KEY() { return this.config.idKey; } /** * The current guid key for the EStore instance. * @return this.config.guidKey; */ get GUID_KEY() { return this.config.guidKey; } /** * Call all the notifiers at once. * * @param v * @param delta */ notifyAll(v, delta) { this.notify.next(v); this.notifyDelta.next(delta); } /** * Observe store state changes. * * @param sort Optional sorting function yielding a sorted observable. * @example * ``` * let todos$ = source.observe(); * //or with a sort by title function * let todos$ = source.observe((a, b)=>(a.title > b.title ? -1 : 1)); * ``` */ observe(sort) { if (sort) { return this.notify.pipe(map((e) => e.sort(sort))); } return this.notify.asObservable(); } /** * Observe delta updates. * * @example * ``` * let todos$ = source.observeDelta(); * ``` */ observeDelta() { return this.notifyDelta.asObservable(); } /** * Check whether the store is empty. * * @return A hot {@link Observable} that indicates whether the store is empty. * * @example * ``` * const empty$:Observable<boolean> = source.isEmpty(); * ``` */ isEmpty() { return this.notify.pipe(map((entries) => entries.length == 0)); } /** * Check whether the store is empty. * * @return A snapshot that indicates whether the store is empty. * * @example * ``` * const empty:boolean = source.isEmptySnapshot(); * ``` */ isEmptySnapshot() { return Array.from(this.entries.values()).length == 0; } /** * Returns the number of entries contained. * @param p The predicate to apply in order to filter the count * * @example * ``` * const completePredicate: Predicate<Todo> = function pred(t: Todo) { * return t.complete; * }; * * const incompletePredicate: Predicate<Todo> = function pred(t: Todo) { * return !t.complete; * }; * * store.count().subscribe((c) => { * console.log(`The observed count of Todo entities is ${c}`); * }); * store.count(incompletePredicate).subscribe((c) => { * console.log(`The observed count of incomplete Todo enttiies is ${c}`); * }); * store.count(completePredicate).subscribe((c) => { * console.log(`The observed count of complete Todo enttiies is ${c}`); * }); * ``` */ count(p) { if (p) { return this.notify.pipe(map((e) => e.reduce((total, e) => total + (p(e) ? 1 : 0), 0))); } return this.notify.pipe(map((entries) => entries.length)); } /** * Returns a snapshot of the number of entries contained in the store. * @param p The predicate to apply in order to filter the count * * @example * ``` * const completePredicate: Predicate<Todo> = function pred(t: Todo) { * return t.complete; * }; * * const incompletePredicate: Predicate<Todo> = function pred(t: Todo) { * return !t.complete; * }; * * const snapshotCount = store.countSnapshot(completePredicate); * console.log(`The count is ${snapshotCount}`); * * const completeSnapshotCount = store.countSnapshot(completePredicate); * console.log( * `The complete Todo Entity Snapshot count is ${completeSnapshotCount}` * ); * * const incompleteSnapshotCount = store.countSnapshot(incompletePredicate); * console.log( * `The incomplete Todo Entity Snapshot count is ${incompleteSnapshotCount}` * ); * ``` */ countSnapshot(p) { if (p) { return Array.from(this.entries.values()).filter(p).length; } return Array.from(this.entries.values()).length; } /** * Snapshot of all entries. * * @return Snapshot array of all the elements the entities the store contains. * * @example Observe a snapshot of all the entities in the store. * * ``` * let selectedTodos:Todo[] = source.allSnapshot(); * ``` */ allSnapshot() { return Array.from(this.entries.values()); } /** * Returns true if the entries contain the identified instance. * * @param target Either an instance of type `E` or a `guid` identifying the instance. * @param byId Whether the lookup should be performed with the `id` key rather than the `guid`. * @returns true if the instance identified by the guid exists, false otherwise. * * @example * ``` * let contains:boolean = source.contains(guid); * ``` */ contains(target) { if (typeof target === 'string') { return this.entries.get(target) ? true : false; } const guid = target[this.config.guidKey]; return this.entries.get(guid) ? true : false; } /** * Returns true if the entries contain the identified instance. * * @param target Either an instance of type `E` or a `id` identifying the instance. * @returns true if the instance identified by the `id` exists, false otherwise. * * @example * ``` * let contains:boolean = source.contains(guid); * ``` */ containsById(target) { if (typeof target === 'string') { return this.idEntries.get(target) ? true : false; } const id = target[this.config.idKey]; return this.idEntries.get(id) ? true : false; } /** * Find and return the entity identified by the GUID parameter * if it exists and return it. * * @param guid * @return The entity instance if it exists, null otherwise * * @example * ``` * const globalID: string = '1'; * let findThisTodo = new Todo(false, 'Find this Todo', globalID); * store.post(findThisTodo); * const todo = store.findOne(globalID); * ``` */ findOne(guid) { return this.entries.get(guid); } /** * Find and return the entity identified by the ID parameter * if it exists and return it. * * @param id * @return The entity instance if it exists, null otherwise * * @example * ``` * const todoLater: Todo = new Todo(false, 'Do me later.'); * todoLater.id = 'findMe'; * store.post(todoLater); * const postedTodo = store.findOneByID('findMe'); * ``` */ findOneByID(id) { return this.idEntries.get(id); } /** * Snapshot of the entries that match the predicate. * * @param p The predicate used to query for the selection. * @return A snapshot array containing the entities that match the predicate. * * @example Select all the Todo instances where the title length is greater than 100. * ``` * let todos:Todo[]=store.select(todo=>todo.title.length>100); * ``` */ select(p) { const selected = []; Array.from(this.entries.values()).forEach((e) => { if (p(e)) { selected.push(e); } }); return selected; } /** * Compare entities by GUID * * @param e1 The first entity * @param e2 The second entity * @return true if the two entities have equal GUID ids * * @example Compare todo1 with todo2 by gid. * ``` * if (equalsByGUID(todo1, todo2)){...}; * ``` */ equalsByGUID(e1, e2) { return e1[this.GUID_KEY] == e2[this.GUID_KEY]; } /** * Compare entities by ID * * @param e1 The first entity * @param e2 The second entity * @return true if the two entities have equal ID ids * * @example Compare todo1 with todo2 by id. * * ``` * if (equalsByID(todo1, todo2)){...}; * ``` */ equalsByID(e1, e2) { return e1[this.ID_KEY] == e2[this.ID_KEY]; } /** * Call destroy when disposing of the store, as it * completes all {@link ReplaySubject} instances. * * @example * ``` * store.destroy(); * ``` */ destroy() { this.notify.complete(); this.notifyDelta.complete(); this.notifyQuery.complete(); } } /** * Returns all the entities are distinct by the * `property` value argument. * * Note that the implementation uses a `Map<string, E>` to * index the entities by key. Therefore the more recent occurences * matching a key instance will overwrite the previous ones. * * @param property The name of the property to check for distinct values by. * @param entities The entities in the array. * * @example ``` let todos: Todo[] = [ { id: 1, title: "Lets do it!" }, { id: 1, title: "Lets do it again!" }, { id: 2, title: "All done!" } ]; let todos2: Todo[] = [ { id: 1, title: "Lets do it!" }, { id: 2, title: "All done!" } ]; expect(distinct(todos, "id").length).toEqual(2); expect(distinct(todos2, "id").length).toEqual(2); ``` */ function distinct(entities, property) { const entitiesByProperty = new Map(entities.map(e => [e[property], e])); return Array.from(entitiesByProperty.values()); } /** * Returns true if all the entities are distinct by the * `property` value argument. * * @param property The name of the property to check for distinct values by. * @param entities The entities in the array. * * @example * ``` let todos: Todo[] = [ { id: 1, title: "Lets do it!" }, { id: 1, title: "Lets do it again!" }, { id: 2, title: "All done!" } ]; let todos2: Todo[] = [ { id: 1, title: "Lets do it!" }, { id: 2, title: "All done!" } ]; expect(unique(todos, "id")).toBeFalsy(); expect(unique(todos2, "id")).toBeTruthy(); ``` */ function unique(entities, property) { return entities.length == distinct(entities, property).length ? true : false; } /** * Create a global ID * @return The global id. * * @example * let e.guid = GUID(); */ function GUID() { return nanoid(); } /** * Set the global identfication property on the instance. * * @param e Entity we want to set the global identifier on. * @param gid The name of the `gid` property. If not specified it defaults to `ESTORE_CONFIG_DEFAULT.guidKey`. */ function attachGUID(e, gid) { const guidKey = gid ? gid : ESTORE_CONFIG_DEFAULT.guidKey; let id = nanoid(); e[guidKey] = id; return id; } /** * Set the global identfication property on the instance. * * @param e[] Entity array we want to set the global identifiers on. * @param gid The name of the `gid` property. If not specified it defaults to `gid`. */ function attachGUIDs(e, gid) { e.forEach(e => { attachGUID(e, gid); }); } /** * Create a shallow copy of the argument. * @param o The object to copy */ function shallowCopy(o) { return { ...o }; } /** * Create a deep copy of the argument. * @param o The object to copy */ function deepCopy(o) { return JSON.parse(JSON.stringify(o)); } /** * Gets the current active value from the `active` * Map. * * This is used for the scenario where we are managing * a single active instance. For example * when selecting a book from a collection of books. * * The selected `Book` instance becomes the active value. * * @example * const book:Book = getActiveValue(bookStore.active); * @param m */ function getActiveValue(m) { if (m.size) { return m.entries().next().value[1]; } return null; } /** * The method can be used to exclude keys from an instance * of type `E`. * * We can use this to exclude values when searching an object. * * @param entity An instance of type E * @param exclude The keys to exclude * * @example * todo = { id: '1', description: 'Do it!' } * let keys = excludeKeys<Todo>(todo, ['id]); * // keys = ['description'] */ function excludeKeys(entity, exclude) { const keys = Object.keys(entity); return keys.filter((key) => { return exclude.indexOf(key) < 0; }); } /** * * @param entities The entity to search * @param exclude Keys to exclude from each entity * * @return E[] Array of entities with properties containing the search term. */ function search(query = '', entities, exclude = []) { const { isArray } = Array; query = query.toLowerCase(); return entities.filter(function (e) { //Do the keys calculation on each instance e:E //because an instance can have optional parameters, //and thus we have to check each instance, not just //the first one in the array. const keys = excludeKeys(e, exclude); return keys.some((key) => { const value = e[key]; if (!value) { return false; } if (isArray(value)) { return value.some(v => { return String(v).toLowerCase().includes(query); }); } else { return String(value).toLowerCase().includes(query); } }); }); } /** * @param scrollable The element being scrolled * @param debounceMS The number of milliseconds to debounce scroll events * @param sp The function returning the scroll position coordinates. * @return A boolean valued observable indicating whether the element is scrolling up or down */ function scrollingUp(scrollable, debounceMS, sp) { return fromEvent(scrollable, 'scroll').pipe(debounceTime(debounceMS), distinctUntilChanged(), map(v => sp()), pairwise(), switchMap(p => { const y1 = p[0][1]; const y2 = p[1][1]; return y1 - y2 > 0 ? of(false) : of(true); })); } /** * Filters the entities properties to the set contained in the * `keys` array. * * @param keys The array of keys that the entity be limited to * @param entity The entity to map * @return An entity instance that has only the keys provided in the keys array */ function mapEntity(keys, entity) { const result = {}; keys.forEach(k => { result[k] = entity[k]; }); return result; } /** * Returns an `Observable<E>` instance that * filters for arguments where the property * value matches the provided value. * * @param value The value targeted * @param propertyName The name of the property to contain the value * @param obs The Slice Object Store Observable * @returns Observable<E> */ function onFilteredEvent(value, propertyName, obs) { return obs.pipe(filter((e) => !!(e && e[propertyName] === value))); } const { isArray } = Array; class Slice extends AbstractStore { /** * perform initial notification to all observers, * such that operations like {@link combineLatest}{} * will execute at least once. * * @param label The slice label * @param predicate The slice predicate * @param eStore The EStore instance containing the elements considered for slicing * * @example * ``` * //Empty slice * new Slice<Todo>(Todo.COMPLETE, todo=>!todo.complete); * * //Initialized slice * let todos = [new Todo(false, "You complete me!"), * new Todo(true, "You completed me!")]; * new Slice<Todo>(Todo.COMPLETE, todo=>!todo.complete, todos); * ``` */ constructor(label, predicate, eStore) { super(); this.label = label; this.predicate = predicate; this.eStore = eStore; /* The slice element entries */ this.entries = new Map(); const entities = eStore.allSnapshot(); this.config = eStore.config; let passed = this.test(predicate, entities); const delta = { type: "Initialize" /* ActionTypes.INTIALIZE */, entries: passed }; this.post(passed); this.notifyDelta.next(delta); } /** * Add the element if it satisfies the predicate * and notify subscribers that an element was added. * * @param e The element to be considered for slicing */ post(e) { if (isArray(e)) { this.postA(e); } else { if (this.predicate(e)) { const id = e[this.config.guidKey]; this.entries.set(id, e); const delta = { type: "Post" /* ActionTypes.POST */, entries: [e] }; this.notifyAll([...Array.from(this.entries.values())], delta); } } } /** * Add the elements if they satisfy the predicate * and notify subscribers that elements were added. * * @param e The element to be considered for slicing */ postN(...e) { this.postA(e); } /** * Add the elements if they satisfy the predicate * and notify subscribers that elements were added. * * @param e The element to be considered for slicing */ postA(e) { const d = []; e.forEach(e => { if (this.predicate(e)) { const id = e[this.config.guidKey]; this.entries.set(id, e); d.push(e); } }); const delta = { type: "Post" /* ActionTypes.POST */, entries: d }; this.notifyAll([...Array.from(this.entries.values())], delta); } /** * Delete an element from the slice. * * @param e The element to be deleted if it satisfies the predicate */ delete(e) { if (isArray(e)) { this.deleteA(e); } else { if (this.predicate(e)) { const id = e[this.config.guidKey]; this.entries.delete(id); const delta = { type: "Delete" /* ActionTypes.DELETE */, entries: [e] }; this.notifyAll(Array.from(this.entries.values()), delta); } } } /** * @param e The elements to be deleted if it satisfies the predicate */ deleteN(...e) { this.deleteA(e); } /** * @param e The elements to be deleted if they satisfy the predicate */ deleteA(e) { const d = []; e.forEach(e => { if (this.predicate(e)) { const id = e[this.config.guidKey]; d.push(this.entries.get(id)); this.entries.delete(id); } }); const delta = { type: "Delete" /* ActionTypes.DELETE */, entries: d }; this.notifyAll([...Array.from(this.entries.values())], delta); } /** * Update the slice when an Entity instance mutates. * * @param e The element to be added or deleted depending on predicate reevaluation */ put(e) { if (isArray(e)) { this.putA(e); } else { const id = e[this.config.guidKey]; if (this.entries.get(id)) { if (!this.predicate(e)) { //Note that this is a ActionTypes.DELETE because we are removing the //entity from the slice. const delta = { type: "Delete" /* ActionTypes.DELETE */, entries: [e] }; this.entries.delete(id); this.notifyAll([...Array.from(this.entries.values())], delta); } } else if (this.predicate(e)) { this.entries.set(id, e); const delta = { type: "Put" /* ActionTypes.PUT */, entries: [e] }; this.notifyAll([...Array.from(this.entries.values())], delta); } } } /** * Update the slice with mutated Entity instances. * * @param e The elements to be deleted if it satisfies the predicate */ putN(...e) { this.putA(e); } /** * @param e The elements to be put */ putA(e) { const d = []; //instances to delete const u = []; //instances to update e.forEach(e => { const id = e[this.config.guidKey]; if (this.entries.get(id)) { if (!this.predicate(e)) { d.push(this.entries.get(id)); } } else if (this.predicate(e)) { u.push(e); } }); if (d.length > 0) { d.forEach(e => { this.entries.delete(e[this.config.guidKey]); }); const delta = { type: "Delete" /* ActionTypes.DELETE */, entries: d }; this.notifyAll([...Array.from(this.entries.values())], delta); } if (u.length > 0) { u.forEach(e => { this.entries.set(e[this.config.guidKey], e); }); const delta = { type: "Put" /* ActionTypes.PUT */, entries: u }; this.notifyAll([...Array.from(this.entries.values())], delta); } } /** * Resets the slice to empty. */ reset() { let delta = { type: "Reset" /* ActionTypes.RESET */, entries: [...Array.from(this.entries.values())] }; this.notifyAll([], delta); this.entries = new Map(); } /** * Utility method that applies the predicate to an array * of entities and return the ones that pass the test. * * Used to create an initial set of values * that should be part of the `Slice`. * * @param p * @param e * @return The the array of entities that pass the predicate test. */ test(p, e) { let v = []; e.forEach((e) => { if (p(e)) { v.push(e); } }); return v; } } /** * This `todoFactory` code will be used to illustrate the API examples. The following * utilities are used in the tests and the API Typedoc examples contained here. * @example Utilities for API Examples * ``` * export const enum TodoSliceEnum { * COMPLETE = "Complete", * INCOMPLETE = "Incomplete" * } * export class Todo { * constructor( * public complete: boolean, * public title: string, * public gid?:string, * public id?:string) {} * } * * export let todos = [new Todo(false, "You complete me!"), new Todo(true, "You completed me!")]; * * export function todosFactory():Todo[] { * return [new Todo(false, "You complete me!"), new Todo(true, "You completed me!")]; * } * ``` */ class EStore extends AbstractStore { /** * Store constructor (Initialization with element is optional) * * perform initial notification to all observers, * such that functions like {@link combineLatest}{} * will execute at least once. * * @param entities The entities to initialize the store with. * @param config The optional configuration instance. * * @example EStore<Todo> Creation * ``` * // Initialize the Store * let store: EStore<Todo> = new EStore<Todo>(todosFactory()); * ``` */ constructor(entities = [], config) { super(config); /** * Notifies observers when the store is empty. */ this.notifyActive = new ReplaySubject(1); /** * `Map` of active entties. The instance is public and can be used * directly to add and remove active entities, however we recommend * using the {@link addActive} and {@link deleteActive} methods. */ this.active = new Map(); /** * Notifies observers when the store is loading. * * This is a common pattern found when implementing * `Observable` data sources. */ this.notifyLoading = new ReplaySubject(1); /** * The current loading state. Use loading when fetching new * data for the store. The default loading state is `true`. * * This is such that if data is fetched asynchronously * in a service, components can wait on loading notification * before attempting to retrieve data from the service. * * Loading could be based on a composite response. For example * when the stock and mutual funds have loaded, set loading to `false`. */ this._loading = true; /** * Notifies observers that a search is in progress. * * This is a common pattern found when implementing * `Observable` data sources. */ this.notifySearching = new ReplaySubject(1); /** * The current `searching` state. Use `searching` * for example to display a spinnner * when performing a search. * The default `searching` state is `false`. */ this._searching = false; /** * Store slices */ this.slices = new Map(); const delta = { type: "Initialize" /* ActionTypes.INTIALIZE */, entries: entities }; this.post(entities); this.notifyDelta.next(delta); } /** * Calls complete on all EStore {@link ReplaySubject} instances. * * Call destroy when disposing of the store. */ destroy() { super.destroy(); this.notifyLoading.complete(); this.notifyActive.complete(); this.slices.forEach((slice) => slice.destroy()); } /** * Toggles the entity: * * If the store contains the entity * it will be deleted. If the store * does not contains the entity, * it is added. * @param e The entity to toggle * @example Toggle the Todo instance * ``` * estore.post(todo); * // Remove todo * estore.toggle(todo); * // Add it back * estore.toggle(todo); * ``` */ toggle(e) { if (this.contains(e)) { this.delete(e); } else { this.post(e); } } /** * Add multiple entity entities to active. * * If the entity is not contained in the store it is added * to the store before it is added to `active`. * * Also we clone the map prior to broadcasting it with * `notifyActive` to make sure we will trigger Angular * change detection in the event that it maintains * a reference to the `active` state `Map` instance. * * @example Add todo1 and todo2 as active * ``` * addActive(todo1); * addActive(todo2); * ``` */ addActive(e) { if (this.contains(e)) { this.active.set(e.gid, e); this.notifyActive.next(new Map(this.active)); } else { this.post(e); this.active.set(e.gid, e); this.notifyActive.next(new Map(this.active)); } } /** * Delete an active entity. * * Also we clone the map prior to broadcasting it with * `notifyActive` to make sure we will trigger Angular * change detection in the event that it maintains * a reference to the `active` state `Map` instance. * * @example Remove todo1 and todo2 as active entities * ``` * deleteActive(todo1); * deleteActive(todo2); * ``` */ deleteActive(e) { this.active.delete(e.gid); this.notifyActive.next(new Map(this.active)); } /** * Clear / reset the active entity map. * * Also we clone the map prior to broadcasting it with * `notifyActive` to make sure we will trigger Angular * change detection in the event that it maintains * a reference to the `active` state `Map` instance. * * @example Clear active todo instances * ``` * store.clearActive(); * ``` */ clearActive() { this.active.clear(); this.notifyActive.next(new Map(this.active)); } /** * Observe the active entities. * * @example * ``` * let active$ = store.observeActive(); * ``` */ observeActive() { return this.notifyActive.asObservable(); } /** * Observe the active entity. * @example <pre> let active$ = source.activeSnapshot(); </pre> */ activeSnapshot() { return Array.from(this.active.values()); } /** * Sets the current loading state and notifies observers. */ set loading(loading) { this._loading = loading; this.notifyLoading.next(this._loading); } /** * @return A snapshot of the loading state. * @example Create a reference to the loading state * ``` * const loading:boolean = todoStore.loading; * ``` */ get loading() { return this._loading; } /** * Observe loading. * * Note that this obverable piped through * `takeWhile(v->v, true), such that it will * complete after each emission. * * See: * https://fireflysemantics.medium.com/waiting-on-estore-to-load-8dcbe161613c * * For more details. * Also note that v=>v is the same as v=>v!=false * * @example * ``` * const observeLoadingHandler: Observer<boolean> = { * complete: () => { * console.log(`Data Loaded and Observable Marked as Complete`); * }, // completeHandler * error: () => { * console.log(`Any Errors?`); * }, // errorHandler * next: (l) => { * console.log(`Data loaded and loading is ${l}`); * }, * }; * * const observeLoadingResubscribeHandler: Observer<boolean> = { * complete: () => { * console.log(`Data Loaded and Resubscribe Observable Marked as Complete`); * }, // completeHandler * error: () => { * console.log(`Any Resubscribe Errors?`); * }, // errorHandler * next: (l) => { * console.log(`Data loaded and resusbscribe loading value is ${l}`); * }, * }; * * const todoStore: EStore<Todo> = new EStore(); * //============================================ * // Loading is true by default * //============================================ * console.log(`The initial value of loading is ${todoStore.loading}`); * //============================================ * // Observe Loading * //============================================ * let loading$: Observable<boolean> = todoStore.observeLoading(); * loading$.subscribe((l) => console.log(`The value of loading is ${l}`)); * * todoStore.loading = false; * loading$.subscribe(observeLoadingHandler); * //============================================ * // The subscription no longer fires * //============================================ * todoStore.loading = true; * todoStore.loading = false; * * //============================================ * // The subscription no longer fires, * // so if we want to observe loading again * // resusbscribe. * //============================================ * todoStore.loading = true; * loading$ = todoStore.observeLoading(); * loading$.subscribe(observeLoadingResubscribeHandler); * todoStore.loading = false; * ``` */ observeLoading() { return this.notifyLoading.asObservable().pipe(takeWhile((v) => v, true)); } /** * Notfiies when loading has completed. */ observeLoadingComplete() { return this.observeLoading().pipe(filter((loading) => loading == false), switchMap(() => of(true))); } /** * Sets the current searching state and notifies observers. */ set searching(searching) { this._searching = searching; this.notifySearching.next(this._searching); } /** * @return A snapshot of the searching state. */ get searching() { return this._searching; } /** * Observe searching. * @example <pre> let searching$ = source.observeSearching(); </pre> Note that this obverable piped through `takeWhile(v->v, true), such that it will complete after each emission. See: https://medium.com/@ole.ersoy/waiting-on-estore-to-load-8dcbe161613c For more details. */ observeSearching() { return this.notifySearching.asObservable().pipe(takeWhile((v) => v, true)); } /** * Notfiies when searching has completed. */ observeSearchingComplete() { return this.observeSearching().pipe(filter((searching) => searching == false), switchMap(() => of(true))); } /** * Adds a slice to the store and keys it by the slices label. * * @param p * @param label * * @example Setup a Todo Slice for COMPLETE Todos ``` source.addSlice(todo => todo.complete, TodoSlices.COMPLETE); ``` */ addSlice(p, label) { const slice = new Slice(label, p, this); this.slices.set(slice.label, slice); } /** * Remove a slice * @param label The label identifying the slice * * @example Remove the TodoSlices.COMPLETE Slice ``` source.removeSlice(TodoSlices.COMPLETE); ``` */ removeSlice(label) { this.slices.delete(label); } /** * Get a slice * @param label The label identifying the slice * @return The Slice instance or undefined * * @example Get the TodoSlices.COMPLETE slice ``` source.getSlice(TodoSlices.COMPLETE); ``` */ getSlice(label) { return this.slices.get(label); } /** * Post (Add a new) element(s) to the store. * @param e An indiidual entity or an array of entities * @example Post a Todo instance. * *``` * store.post(todo); *``` */ post(e) { if (!Array.isArray(e)) { const guid = e[this.GUID_KEY] ? e[this.GUID_KEY] : GUID(); e[this.GUID_KEY] = guid; this.entries.set(guid, e); this.updateIDEntry(e); Array.from(this.slices.values()).forEach((s) => { s.post(e); }); //Create a new array reference to trigger Angular change detection. let v = [...Array.from(this.entries.values())]; const delta = { type: "Post" /* ActionTypes.POST */, entries: [e] }; this.notifyAll(v, delta); } else { this.postA(e); } } /** * Post N entities to the store. * @param ...e * @example Post two Todo instances. * ``` * store.post(todo1, todo2); * ``` */ postN(...e) { e.forEach((e) => { const guid = e[this.GUID_KEY] ? e[this.GUID_KEY] : GUID(); e[this.GUID_KEY] = guid; this.entries.set(guid, e); this.updateIDEntry(e); }); Array.from(this.slices.values()).forEach((s) => { s.postA(e); }); //Create a new array reference to trigger Angular change detection. let v = [...Array.from(this.entries.values())]; const delta = { type: "Post" /* ActionTypes.POST */, entries: e }; this.notifyAll(v, delta); } /** * Post (Add) an array of elements to the store. * @param e * @example Post a Todo array. * * ``` * store.post([todo1, todo2]); * ``` */ postA(e) { this.postN(...e); } /** * Put (Update) an entity. * @param e * @example Put a Todo instance. * ``` * store.put(todo1); * ``` */ put(e) { if (!Array.isArray(e)) { let id = e[this.GUID_KEY]; this.entries.set(id, e); this.updateIDEntry(e); let v = [...Array.from(this.entries.values())]; this.notify.next(v); const delta = { type: "Put" /* ActionTypes.PUT */, entries: [e] }; this.notifyDelta.next(delta); Array.from(this.slices.values()).forEach((s) => { s.put(e); }); } else { this.putA(e); } } /** * Put (Update) an element or add an element that was read from a persistence source * and thus already has an assigned global id`. * @param e The enetity instances to update. * @example Put N Todo instances. * * ``` * store.put(todo1, todo2); * ``` */ putN(...e) { this.putA(e); } /** * Put (Update) the array of enntities. * @param e The array of enntities to update * @example Put an array of Todo instances. * ``` * store.put([todo1, todo2]); * ``` */ putA(e) { e.forEach((e) => { let guid = e[this.GUID_KEY]; this.entries.set(guid, e); this.updateIDEntry(e); }); //Create a new array reference to trigger Angular change detection. let v = [...Array.from(this.entries.values())]; this.notify.next(v); const delta = { type: "Put" /* ActionTypes.PUT */, entries: e }; this.notifyDelta.next(delta); Array.from(this.slices.values()).forEach((s) => { s.putA(e); }); } /** * Delete (Update) the array of elements. * @param e * @example Delete todo1. * ``` * store.delete(todo1]); * ``` */ delete(e) { if (!Array.isArray(e)) { this.deleteActive(e); const guid = e[this.GUID_KEY]; this.entries.delete(guid); this.deleteIDEntry(e); Array.from(this.slices.values()).forEach((s) => { s.entries.delete(guid); }); //Create a new array reference to trigger Angular change detection. let v = [...Array.from(this.entries.values())]; const delta = { type: "Delete" /* ActionTypes.DELETE */, entries: [e] }; this.notifyAll(v, delta); Array.from(this.slices.values()).forEach((s) => { s.delete(e); }); } else { this.deleteA(e); } } /** * Delete N elements. * @param ...e * @example Delete N Todo instance argument. * ``` * store.deleteN(todo1, todo2); * ``` */ deleteN(...e) { this.deleteA(e); } /** * Delete an array of elements. * @param e The array of instances to be deleted * @example Delete the array of Todo instances. * ``` * store.deleteA([todo1, todo2]); * ``` */ deleteA(e) { e.forEach((e) => { this.deleteActive(e); const guid = e[this.GUID_KEY]; this.entries.delete(guid); this.deleteIDEntry(e); Array.from(this.slices.values()).forEach((s) => { s.entries.delete(guid); }); }); //Create a new array reference to trigger Angular change detection. let v = [...Array.from(this.entries.values())]; const delta = { type: "Delete" /* ActionTypes.DELETE */, entries: e }; this.notifyAll(v, delta); Array.from(this.slices.values()).forEach((s) => { s.deleteA(e); }); } /** * Delete elements by {@link Predicate}. * @param p The predicate. * @example Delete the Todo instances. * ``` * store.delete(todo1, todo2); * ``` */ deleteP(p) { const d = []; Array.from(this.entries.values()).forEach((e) => { if (p(e)) { d.push(e); const id = e[this.GUID_KEY]; this.entries.delete(id); this.deleteActive(e); this.deleteIDEntry(e); } }); //Create a new array reference to trigger Angular change detection. let v = [...Array.from(this.entries.values())]; const delta = { type: "Delete" /* ActionTypes.DELETE */, entries: d }; this.notifyAll(v, delta); Array.from(this.slices.values()).forEach((s) => { s.deleteA(d); }); } /** * If the entity has the `id` key initialized with a value, * then also add the entity to the `idEntries`. * * @param e The element to be added to the `idEntries`. */ updateIDEntry(e) { if (e[this.ID_KEY]) { this.idEntries.set(e[this.ID_KEY], e); } } /** * If the entity has the `id` key initialized with a value, * then also delete the entity to the `idEntries`. * * @param e The element to be added to the `idEntries`. */ deleteIDEntry(e) { if (e[this.ID_KEY]) { this.idEntries.delete(e[this.ID_KEY]); } } /** * Resets the store and all contained slice instances to empty. * Also perform delta notification that sends all current store entries. * The ActionType.RESET code is sent with the delta notification. Slices * send their own delta notification. * * @example Reset the store. * ``` * store.reset(); * ``` */ reset() { const delta = { type: "Reset" /* ActionTypes.RESET */, entries: Array.from(this.entries.values()), }; this.notifyAll([], delta); this.entries = new Map(); Array.from(this.slices.values()).forEach((s) => { s.reset(); }); } /** * Call all the notifiers at once. * * @param v * @param delta */ notifyAll(v, delta) { super.notifyAll(v, delta); this.notifyLoading.next(this.loading); } } class OStore { constructor(start) { /** * Map of Key Value pair entries * containing values store in this store. */ this.entries = new Map(); /** * Map of replay subject id to `ReplaySubject` instance. */ this.subjects = new Map(); if (start) { this.S = start; const keys = Object.keys(start); keys.forEach((k) => { const ovr = start[k]; this.post(ovr, ovr.value); ovr.obs = this.observe(ovr); }); } } /** * Reset the state of the OStore to the * values or reset provided in the constructor * {@link OStoreStart} instance. */ reset() { if (this.S) { const keys = Object.keys(this.S); keys.forEach((k) => { const ovr = this.S[k]; this.put(ovr, ovr.reset ? ovr.reset : ovr.value); }); } } /** * Set create a key value pair entry and creates a * corresponding replay subject instance that will * be used to broadcast updates. * * @param key The key identifying the value * @param value The value */ post(key, value) { this.entries.set(key, value); this.subjects.set(key, new ReplaySubject(1)); //Emit immediately so that Observers can receive //the value straight away. const subject = this.subjects.get(key); if (subject) { subject.next(value); } } /** * Update a value and notify subscribers. * * @param key * @param value */ put(key, value) { this.entries.set(key, value); const subject = this.subjects.get(key); if (subject) { subject.next(value); } } /** * Deletes both the value entry and the corresponding {@link ReplaySubject}. * Will unsubscribe the {@link ReplaySubject} prior to deleting it, * severing communication with corresponding {@link Observable}s. * * @param key */ delete(key) { //=========================================== // Delete the entry //=========================================== this.entries.delete(key); const subject = this.subjects.get(key); if (subject) { subject.next(undefined); } } /** * Clear all entries. * * Note that * this will call delete for on all * keys defined which also also * unsubscribes and deletes * all the sbujects. */ clear() { for (let key of this.entries.keys()) { this.delete(key); } } /** * Observe changes to the values. * * @param key * @return An {@link Observable} of the value */ observe(key) { return this.subjects.get(key) ? this.subjects.get(key).asObservable() : undefined; } /** * Check whether a value exists. * * @param key * @return True if the entry exists ( Is not null or undefined ) and false otherwise. */ exists(key) { return !!this.entries.get(key); } /** * Retrieve a snapshot of the * value. * * @param key * @return A snapshot of the value corresponding to the key. */ snapshot(key) { return this.entries.get(key); } /** * Indicates whether the store is empty. * @return true if the store is empty, false otherwise. */ isEmpty() { return Array.from(this.entries.values()).length == 0; } /** * Returns the number of key value pairs contained. * * @return the number of entries in the store. */ count() { return Array.from(this.entries.values()).length; } } /* * Public API Surface of slice */ /** * Generated bundle index. Do not edit. */ export { AbstractStore, ESTORE_CONFIG_DEFAULT, EStore, Entity, GUID, OStore, SCROLL_UP_DEBOUNCE_TIME_20, SEARCH_DEBOUNCE_TIME_300, Slice, attachGUID, attachGUIDs, deepCopy, distinct, excludeKeys, getActiveValue, mapEntity, onFilteredEvent, scrollingUp, search, shallowCopy, unique }; //# sourceMappingURL=fireflysemantics-slice.mjs.map