@fireflysemantics/slice
Version:

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