@fireflysemantics/slice 14.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1459 @@
1
+ import { ReplaySubject, fromEvent, of } from 'rxjs';
2
+ import { map, debounceTime, distinctUntilChanged, pairwise, switchMap, takeWhile, filter } from 'rxjs/operators';
3
+ import { nanoid } from 'nanoid';
4
+
5
+ ;
6
+
7
+ const { freeze } = Object;
8
+ const ESTORE_DEFAULT_ID_KEY = "id";
9
+ const ESTORE_DEFAULT_GID_KEY = "gid";
10
+ const ESTORE_CONFIG_DEFAULT = freeze({
11
+ idKey: ESTORE_DEFAULT_ID_KEY,
12
+ guidKey: ESTORE_DEFAULT_GID_KEY
13
+ });
14
+ class AbstractStore {
15
+ constructor(config) {
16
+ /**
17
+ * Notifies observers of the store query.
18
+ */
19
+ this.notifyQuery = new ReplaySubject(1);
20
+ /**
21
+ * The current query state.
22
+ */
23
+ this._query = '';
24
+ /**
25
+ * Primary index for the stores elements.
26
+ */
27
+ this.entries = new Map();
28
+ /**
29
+ * The element entries that are keyed by
30
+ * an id generated on the server.
31
+ */
32
+ this.idEntries = new Map();
33
+ /**
34
+ * Create notifications that broacast
35
+ * the entire set of entries.
36
+ */
37
+ this.notify = new ReplaySubject(1);
38
+ /**
39
+ * Create notifications that broacast
40
+ * store or slice delta state changes.
41
+ */
42
+ this.notifyDelta = new ReplaySubject(1);
43
+ this.config = config
44
+ ? freeze(Object.assign(Object.assign({}, ESTORE_CONFIG_DEFAULT), config))
45
+ : ESTORE_CONFIG_DEFAULT;
46
+ }
47
+ /**
48
+ * Sets the current query state and notifies observers.
49
+ */
50
+ set query(query) {
51
+ this._query = query;
52
+ this.notifyQuery.next(this._query);
53
+ }
54
+ /**
55
+ * @return A snapshot of the query state.
56
+ */
57
+ get query() {
58
+ return this._query;
59
+ }
60
+ /**
61
+ * Observe the query.
62
+ * @example
63
+ <pre>
64
+ let query$ = source.observeQuery();
65
+ </pre>
66
+ */
67
+ observeQuery() {
68
+ return this.notifyQuery.asObservable();
69
+ }
70
+ /**
71
+ * The current id key for the EStore instance.
72
+ * @return this.config.idKey;
73
+ */
74
+ get ID_KEY() {
75
+ return this.config.idKey;
76
+ }
77
+ /**
78
+ * The current guid key for the EStore instance.
79
+ * @return this.config.guidKey;
80
+ */
81
+ get GUID_KEY() {
82
+ return this.config.guidKey;
83
+ }
84
+ /**
85
+ * Call all the notifiers at once.
86
+ *
87
+ * @param v
88
+ * @param delta
89
+ */
90
+ notifyAll(v, delta) {
91
+ this.notify.next(v);
92
+ this.notifyDelta.next(delta);
93
+ }
94
+ /**
95
+ * Observe store state changes.
96
+ * @param sort Optional sorting function yielding a sorted observable.
97
+ * @example
98
+ ```
99
+ let todos$ = source.observe();
100
+ //or with a sort by title function
101
+ let todos$ = source.observe((a, b)=>(a.title > b.title ? -1 : 1));
102
+ ```
103
+ */
104
+ observe(sort) {
105
+ if (sort) {
106
+ return this.notify.pipe(map((e) => e.sort(sort)));
107
+ }
108
+ return this.notify.asObservable();
109
+ }
110
+ /**
111
+ * Observe delta updates.
112
+ * @example
113
+ <pre>
114
+ let todos$ = source.observeDelta();
115
+ </pre>
116
+ */
117
+ observeDelta() {
118
+ return this.notifyDelta.asObservable();
119
+ }
120
+ /**
121
+ * Check whether the store is empty.
122
+ *
123
+ * @return A hot {@link Observable} that indicates whether the store is empty.
124
+ *
125
+ * @example
126
+ <pre>
127
+ source.isEmpty();
128
+ </pre>
129
+ */
130
+ isEmpty() {
131
+ return this.notify.pipe(map((entries) => entries.length == 0));
132
+ }
133
+ /**
134
+ * Check whether the store is empty.
135
+ *
136
+ * @return A snapshot that indicates whether the store is empty.
137
+ *
138
+ * @example
139
+ <pre>
140
+ source.isEmpty();
141
+ </pre>
142
+ */
143
+ isEmptySnapshot() {
144
+ return Array.from(this.entries.values()).length == 0;
145
+ }
146
+ /**
147
+ * Returns the number of entries contained.
148
+ * @param p The predicate to apply in order to filter the count
149
+ */
150
+ count(p) {
151
+ if (p) {
152
+ return this.notify.pipe(map((e) => e.reduce((total, e) => total + (p(e) ? 1 : 0), 0)));
153
+ }
154
+ return this.notify.pipe(map((entries) => entries.length));
155
+ }
156
+ /**
157
+ * Returns a snapshot of the number of entries contained in the store.
158
+ * @param p The predicate to apply in order to filter the count
159
+ */
160
+ countSnapshot(p) {
161
+ if (p) {
162
+ return Array.from(this.entries.values()).filter(p).length;
163
+ }
164
+ return Array.from(this.entries.values()).length;
165
+ }
166
+ /**
167
+ * Snapshot of all entries.
168
+ *
169
+ * @return Snapshot array of all the elements the entities the store contains.
170
+ *
171
+ * @example Observe a snapshot of all the entities in the store.
172
+ ```
173
+ let selectedTodos:Todo[] = source.allSnapshot();
174
+ ```
175
+ */
176
+ allSnapshot() {
177
+ return Array.from(this.entries.values());
178
+ }
179
+ /**
180
+ * Returns true if the entries contain the identified instance.
181
+ *
182
+ * @param target Either an instance of type `E` or a `guid` identifying the instance.
183
+ * @param byId Whether the lookup should be performed with the `id` key rather than the `guid`.
184
+ * @returns true if the instance identified by the guid exists, false otherwise.
185
+ *
186
+ * @example
187
+ <pre>
188
+ let contains:boolean = source.contains(guid);
189
+ </pre>
190
+ */
191
+ contains(target) {
192
+ if (typeof target === "string") {
193
+ return this.entries.get(target) ? true : false;
194
+ }
195
+ const guid = target[this.config.guidKey];
196
+ return this.entries.get(guid) ? true : false;
197
+ }
198
+ /**
199
+ * Returns true if the entries contain the identified instance.
200
+ *
201
+ * @param target Either an instance of type `E` or a `id` identifying the instance.
202
+ * @returns true if the instance identified by the `id` exists, false otherwise.
203
+ *
204
+ * @example
205
+ <pre>
206
+ let contains:boolean = source.contains(guid);
207
+ </pre>
208
+ */
209
+ containsById(target) {
210
+ if (typeof target === "string") {
211
+ return this.idEntries.get(target) ? true : false;
212
+ }
213
+ const id = target[this.config.idKey];
214
+ return this.idEntries.get(id) ? true : false;
215
+ }
216
+ /**
217
+ * Find and return the entity identified by the GUID parameter
218
+ * if it exists and return it.
219
+ *
220
+ * @param guid
221
+ * @return The entity instance if it exists, null otherwise
222
+ */
223
+ findOne(guid) {
224
+ return this.entries.get(guid);
225
+ }
226
+ /**
227
+ * Find and return the entity identified by the ID parameter
228
+ * if it exists and return it.
229
+ *
230
+ * @param id
231
+ * @return The entity instance if it exists, null otherwise
232
+ */
233
+ findOneByID(id) {
234
+ return this.idEntries.get(id);
235
+ }
236
+ /**
237
+ * Snapshot of the entries that match the predicate.
238
+ *
239
+ * @param p The predicate used to query for the selection.
240
+ * @return A snapshot array containing the entities that match the predicate.
241
+ *
242
+ * @example Select all the `Todo` instance where the `title` length is greater than 100.
243
+ ```
244
+ let todos:Todo[]=store.select(todo=>todo.title.length>100);
245
+ ```
246
+ */
247
+ select(p) {
248
+ const selected = [];
249
+ Array.from(this.entries.values()).forEach(e => {
250
+ if (p(e)) {
251
+ selected.push(e);
252
+ }
253
+ });
254
+ return selected;
255
+ }
256
+ /**
257
+ * Compare entities by GUID
258
+ * @param e1 The first entity
259
+ * @param e2 The second entity
260
+ * @return true if the two entities have equal GUID ids
261
+ * @example Compare `todo1` with `todo2` by `gid`.
262
+ ```
263
+ if (equalsByGUID(todo1, todo2)){...};
264
+ ```
265
+ */
266
+ equalsByGUID(e1, e2) {
267
+ return e1[this.GUID_KEY] == e2[this.GUID_KEY];
268
+ }
269
+ /**
270
+ * Compare entities by ID
271
+ * @param e1 The first entity
272
+ * @param e2 The second entity
273
+ * @return true if the two entities have equal ID ids
274
+ * @example Compare `todo1` with `todo2` by `id`.
275
+ ```
276
+ if (equalsByID(todo1, todo2)){...};
277
+ ```
278
+ */
279
+ equalsByID(e1, e2) {
280
+ return e1[this.ID_KEY] == e2[this.ID_KEY];
281
+ }
282
+ /**
283
+ * Calls complete on all {@link BehaviorSubject} instances.
284
+ *
285
+ * Call destroy when disposing of the store.
286
+ */
287
+ destroy() {
288
+ this.notify.complete();
289
+ this.notifyDelta.complete();
290
+ this.notifyQuery.complete();
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Returns all the entities are distinct by the
296
+ * `property` value argument.
297
+ *
298
+ * Note that the implementation uses a `Map<string, E>` to
299
+ * index the entities by key. Therefore the more recent occurences
300
+ * matching a key instance will overwrite the previous ones.
301
+ *
302
+ * @param property The name of the property to check for distinct values by.
303
+ * @param entities The entities in the array.
304
+ *
305
+ * @example
306
+ ```
307
+ let todos: Todo[] = [
308
+ { id: 1, title: "Lets do it!" },
309
+ { id: 1, title: "Lets do it again!" },
310
+ { id: 2, title: "All done!" }
311
+ ];
312
+
313
+ let todos2: Todo[] = [
314
+ { id: 1, title: "Lets do it!" },
315
+ { id: 2, title: "All done!" }
316
+ ];
317
+
318
+ expect(distinct(todos, "id").length).toEqual(2);
319
+ expect(distinct(todos2, "id").length).toEqual(2);
320
+
321
+ ```
322
+ */
323
+ function distinct(entities, property) {
324
+ const entitiesByProperty = new Map(entities.map(e => [e[property], e]));
325
+ return Array.from(entitiesByProperty.values());
326
+ }
327
+ /**
328
+ * Returns true if all the entities are distinct by the
329
+ * `property` value argument.
330
+ *
331
+ * @param property The name of the property to check for distinct values by.
332
+ * @param entities The entities in the array.
333
+ *
334
+ * @example
335
+ *
336
+ ```
337
+ let todos: Todo[] = [
338
+ { id: 1, title: "Lets do it!" },
339
+ { id: 1, title: "Lets do it again!" },
340
+ { id: 2, title: "All done!" }
341
+ ];
342
+
343
+ let todos2: Todo[] = [
344
+ { id: 1, title: "Lets do it!" },
345
+ { id: 2, title: "All done!" }
346
+ ];
347
+
348
+ expect(unique(todos, "id")).toBeFalsy();
349
+ expect(unique(todos2, "id")).toBeTruthy();
350
+ ```
351
+ */
352
+ function unique(entities, property) {
353
+ return entities.length == distinct(entities, property).length ? true : false;
354
+ }
355
+ /**
356
+ * Create a global ID
357
+ * @return The global id.
358
+ *
359
+ * @example
360
+ * let e.guid = GUID();
361
+ */
362
+ function GUID() {
363
+ return nanoid();
364
+ }
365
+ /**
366
+ * Set the global identfication property on the instance.
367
+ *
368
+ * @param e Entity we want to set the global identifier on.
369
+ * @param gid The name of the `gid` property. If not specified it defaults to `ESTORE_CONFIG_DEFAULT.guidKey`.
370
+ */
371
+ function attachGUID(e, gid) {
372
+ const guidKey = gid ? gid : ESTORE_CONFIG_DEFAULT.guidKey;
373
+ let id = nanoid();
374
+ e[guidKey] = id;
375
+ return id;
376
+ }
377
+ /**
378
+ * Set the global identfication property on the instance.
379
+ *
380
+ * @param e[] Entity array we want to set the global identifiers on.
381
+ * @param gid The name of the `gid` property. If not specified it defaults to `gid`.
382
+ */
383
+ function attachGUIDs(e, gid) {
384
+ e.forEach(e => {
385
+ attachGUID(e, gid);
386
+ });
387
+ }
388
+ /**
389
+ * Create a shallow copy of the argument.
390
+ * @param o The object to copy
391
+ */
392
+ function shallowCopy(o) {
393
+ return Object.assign({}, o);
394
+ }
395
+ /**
396
+ * Create a deep copy of the argument.
397
+ * @param o The object to copy
398
+ */
399
+ function deepCopy(o) {
400
+ return JSON.parse(JSON.stringify(o));
401
+ }
402
+ /**
403
+ * Gets the current active value from the `active`
404
+ * Map.
405
+ *
406
+ * This is used for the scenario where we are managing
407
+ * a single active instance. For example
408
+ * when selecting a book from a collection of books.
409
+ *
410
+ * The selected `Book` instance becomes the active value.
411
+ *
412
+ * @example
413
+ * const book:Book = getActiveValue(bookStore.active);
414
+ * @param m
415
+ */
416
+ function getActiveValue(m) {
417
+ if (m.size) {
418
+ return m.entries().next().value[1];
419
+ }
420
+ return null;
421
+ }
422
+ /**
423
+ * The method can be used to exclude keys from an instance
424
+ * of type `E`.
425
+ *
426
+ * We can use this to exclude values when searching an object.
427
+ *
428
+ * @param entity An instance of type E
429
+ * @param exclude The keys to exclude
430
+ *
431
+ * @example
432
+ * todo = { id: '1', description: 'Do it!' }
433
+ * let keys = excludeKeys<Todo>(todo, ['id]);
434
+ * // keys = ['description']
435
+ */
436
+ function excludeKeys(entity, exclude) {
437
+ const keys = Object.keys(entity);
438
+ return keys.filter((key) => {
439
+ return exclude.indexOf(key) < 0;
440
+ });
441
+ }
442
+ /**
443
+ *
444
+ * @param entities The entity to search
445
+ * @param exclude Keys to exclude from each entity
446
+ *
447
+ * @return E[] Array of entities with properties containing the search term.
448
+ */
449
+ function search(query = '', entities, exclude = []) {
450
+ const { isArray } = Array;
451
+ query = query.toLowerCase();
452
+ return entities.filter(function (e) {
453
+ //Do the keys calculation on each instance e:E
454
+ //because an instance can have optional parameters,
455
+ //and thus we have to check each instance, not just
456
+ //the first one in the array.
457
+ const keys = excludeKeys(e, exclude);
458
+ return keys.some((key) => {
459
+ const value = e[key];
460
+ if (!value) {
461
+ return false;
462
+ }
463
+ if (isArray(value)) {
464
+ return value.some(v => {
465
+ return String(v).toLowerCase().includes(query);
466
+ });
467
+ }
468
+ else {
469
+ return String(value).toLowerCase().includes(query);
470
+ }
471
+ });
472
+ });
473
+ }
474
+ /**
475
+ * @param scrollable The element being scrolled
476
+ * @param debounceMS The number of milliseconds to debounce scroll events
477
+ * @param sp The function returning the scroll position coordinates.
478
+ * @return A boolean valued observable indicating whether the element is scrolling up or down
479
+ */
480
+ function scrollingUp(scrollable, debounceMS, sp) {
481
+ return fromEvent(scrollable, 'scroll').pipe(debounceTime(debounceMS), distinctUntilChanged(), map(v => sp()), pairwise(), switchMap(p => {
482
+ const y1 = p[0][1];
483
+ const y2 = p[1][1];
484
+ return y1 - y2 > 0 ? of(false) : of(true);
485
+ }));
486
+ }
487
+ /**
488
+ * Filters the entities properties to the set contained in the
489
+ * `keys` array.
490
+ *
491
+ * @param keys The array of keys that the entity be limited to
492
+ * @param entity The entity to map
493
+ * @return An entity instance that has only the keys provided in the keys array
494
+ */
495
+ function mapEntity(keys, entity) {
496
+ const result = {};
497
+ keys.forEach(k => {
498
+ result[k] = entity[k];
499
+ });
500
+ return result;
501
+ }
502
+
503
+ const { isArray } = Array;
504
+ class Slice extends AbstractStore {
505
+ /**
506
+ * perform initial notification to all observers,
507
+ * such that operations like {@link combineLatest}{}
508
+ * will execute at least once.
509
+ *
510
+ * @param label The slice label
511
+ * @param predicate The slice predicate
512
+ * @param eStore The EStore instance containing the elements considered for slicing
513
+ *
514
+ * @example
515
+ <pre>
516
+ //Empty slice
517
+ new Slice<Todo>(Todo.COMPLETE, todo=>!todo.complete);
518
+
519
+ //Initialized slice
520
+ let todos = [new Todo(false, "You complete me!"),
521
+ new Todo(true, "You completed me!")];
522
+ new Slice<Todo>(Todo.COMPLETE, todo=>!todo.complete, todos);
523
+ </pre>
524
+ */
525
+ constructor(label, predicate, eStore) {
526
+ super();
527
+ this.label = label;
528
+ this.predicate = predicate;
529
+ this.eStore = eStore;
530
+ /* The slice element entries */
531
+ this.entries = new Map();
532
+ const entities = eStore.allSnapshot();
533
+ this.config = eStore.config;
534
+ let passed = this.test(predicate, entities);
535
+ const delta = { type: "Initialize" /* INTIALIZE */, entries: passed };
536
+ this.post(passed);
537
+ this.notifyDelta.next(delta);
538
+ }
539
+ /**
540
+ * Add the element if it satisfies the predicate
541
+ * and notify subscribers that an element was added.
542
+ *
543
+ * @param e The element to be considered for slicing
544
+ */
545
+ post(e) {
546
+ if (isArray(e)) {
547
+ this.postA(e);
548
+ }
549
+ else {
550
+ if (this.predicate(e)) {
551
+ const id = e[this.config.guidKey];
552
+ this.entries.set(id, e);
553
+ const delta = { type: "Post" /* POST */, entries: [e] };
554
+ this.notifyAll([...Array.from(this.entries.values())], delta);
555
+ }
556
+ }
557
+ }
558
+ /**
559
+ * Add the elements if they satisfy the predicate
560
+ * and notify subscribers that elements were added.
561
+ *
562
+ * @param e The element to be considered for slicing
563
+ */
564
+ postN(...e) {
565
+ this.postA(e);
566
+ }
567
+ /**
568
+ * Add the elements if they satisfy the predicate
569
+ * and notify subscribers that elements were added.
570
+ *
571
+ * @param e The element to be considered for slicing
572
+ */
573
+ postA(e) {
574
+ const d = [];
575
+ e.forEach(e => {
576
+ if (this.predicate(e)) {
577
+ const id = e[this.config.guidKey];
578
+ this.entries.set(id, e);
579
+ d.push(e);
580
+ }
581
+ });
582
+ const delta = { type: "Post" /* POST */, entries: d };
583
+ this.notifyAll([...Array.from(this.entries.values())], delta);
584
+ }
585
+ /**
586
+ * Delete an element from the slice.
587
+ *
588
+ * @param e The element to be deleted if it satisfies the predicate
589
+ */
590
+ delete(e) {
591
+ if (isArray(e)) {
592
+ this.deleteA(e);
593
+ }
594
+ else {
595
+ if (this.predicate(e)) {
596
+ const id = e[this.config.guidKey];
597
+ this.entries.delete(id);
598
+ const delta = { type: "Delete" /* DELETE */, entries: [e] };
599
+ this.notifyAll(Array.from(this.entries.values()), delta);
600
+ }
601
+ }
602
+ }
603
+ /**
604
+ * @param e The elements to be deleted if it satisfies the predicate
605
+ */
606
+ deleteN(...e) {
607
+ this.deleteA(e);
608
+ }
609
+ /**
610
+ * @param e The elements to be deleted if they satisfy the predicate
611
+ */
612
+ deleteA(e) {
613
+ const d = [];
614
+ e.forEach(e => {
615
+ if (this.predicate(e)) {
616
+ const id = e[this.config.guidKey];
617
+ d.push(this.entries.get(id));
618
+ this.entries.delete(id);
619
+ }
620
+ });
621
+ const delta = { type: "Delete" /* DELETE */, entries: d };
622
+ this.notifyAll([...Array.from(this.entries.values())], delta);
623
+ }
624
+ /**
625
+ * Update the slice when an Entity instance mutates.
626
+ *
627
+ * @param e The element to be added or deleted depending on predicate reevaluation
628
+ */
629
+ put(e) {
630
+ if (isArray(e)) {
631
+ this.putA(e);
632
+ }
633
+ else {
634
+ const id = e[this.config.guidKey];
635
+ if (this.entries.get(id)) {
636
+ if (!this.predicate(e)) {
637
+ //Note that this is a ActionTypes.DELETE because we are removing the
638
+ //entity from the slice.
639
+ const delta = { type: "Delete" /* DELETE */, entries: [e] };
640
+ this.entries.delete(id);
641
+ this.notifyAll([...Array.from(this.entries.values())], delta);
642
+ }
643
+ }
644
+ else if (this.predicate(e)) {
645
+ this.entries.set(id, e);
646
+ const delta = { type: "Put" /* PUT */, entries: [e] };
647
+ this.notifyAll([...Array.from(this.entries.values())], delta);
648
+ }
649
+ }
650
+ }
651
+ /**
652
+ * Update the slice with mutated Entity instances.
653
+ *
654
+ * @param e The elements to be deleted if it satisfies the predicate
655
+ */
656
+ putN(...e) {
657
+ this.putA(e);
658
+ }
659
+ /**
660
+ * @param e The elements to be put
661
+ */
662
+ putA(e) {
663
+ const d = []; //instances to delete
664
+ const u = []; //instances to update
665
+ e.forEach(e => {
666
+ const id = e[this.config.guidKey];
667
+ if (this.entries.get(id)) {
668
+ if (!this.predicate(e)) {
669
+ d.push(this.entries.get(id));
670
+ }
671
+ }
672
+ else if (this.predicate(e)) {
673
+ u.push(e);
674
+ }
675
+ });
676
+ if (d.length > 0) {
677
+ d.forEach(e => {
678
+ this.entries.delete(e[this.config.guidKey]);
679
+ });
680
+ const delta = { type: "Delete" /* DELETE */, entries: d };
681
+ this.notifyAll([...Array.from(this.entries.values())], delta);
682
+ }
683
+ if (u.length > 0) {
684
+ u.forEach(e => {
685
+ this.entries.set(e[this.config.guidKey], e);
686
+ });
687
+ const delta = { type: "Put" /* PUT */, entries: u };
688
+ this.notifyAll([...Array.from(this.entries.values())], delta);
689
+ }
690
+ }
691
+ /**
692
+ * Resets the slice to empty.
693
+ */
694
+ reset() {
695
+ let delta = {
696
+ type: "Reset" /* RESET */,
697
+ entries: [...Array.from(this.entries.values())]
698
+ };
699
+ this.notifyAll([], delta);
700
+ this.entries = new Map();
701
+ }
702
+ /**
703
+ * Utility method that applies the predicate to an array
704
+ * of entities and return the ones that pass the test.
705
+ *
706
+ * Used to create an initial set of values
707
+ * that should be part of the `Slice`.
708
+ *
709
+ * @param p
710
+ * @param e
711
+ * @return The the array of entities that pass the predicate test.
712
+ */
713
+ test(p, e) {
714
+ let v = [];
715
+ e.forEach((e) => {
716
+ if (p(e)) {
717
+ v.push(e);
718
+ }
719
+ });
720
+ return v;
721
+ }
722
+ }
723
+
724
+ /**
725
+ * This `todoFactory` code will be used to illustrate the API examples. The following
726
+ * utilities are used in the tests and the API Typedoc examples contained here.
727
+ * @example Utilities for API Examples
728
+ ```
729
+ export const enum TodoSliceEnum {
730
+ COMPLETE = "Complete",
731
+ INCOMPLETE = "Incomplete"
732
+ }
733
+
734
+ export class Todo {
735
+ constructor(public complete: boolean, public title: string,public gid?:string, public id?:string) {}
736
+ }
737
+
738
+ export let todos = [new Todo(false, "You complete me!"), new Todo(true, "You completed me!")];
739
+
740
+ export function todosFactory():Todo[] {
741
+ return [new Todo(false, "You complete me!"), new Todo(true, "You completed me!")];
742
+ }
743
+ ```
744
+ */
745
+ class EStore extends AbstractStore {
746
+ /**
747
+ * Store constructor (Initialization with element is optional)
748
+ *
749
+ * perform initial notification to all observers,
750
+ * such that function like {@link combineLatest}{}
751
+ * will execute at least once.
752
+ * @param entities
753
+ * @example Dynamic `EStore<Todo>` Creation
754
+ ```
755
+ // Initialize the Store
756
+ let store: EStore<Todo> = new EStore<Todo>(todosFactory());
757
+ ```*/
758
+ constructor(entities = [], config) {
759
+ super(config);
760
+ /**
761
+ * An Observable<E[]> reference so that
762
+ *
763
+ */
764
+ this.observable = this.observe();
765
+ /**
766
+ * Notifies observers when the store is empty.
767
+ */
768
+ this.notifyActive = new ReplaySubject(1);
769
+ /**
770
+ * `Map` of active entties. The instance is public and can be used
771
+ * directly to add and remove active entities, however we recommend
772
+ * using the {@link addActive} and {@link deleteActive} methods.
773
+ */
774
+ this.active = new Map();
775
+ /**
776
+ * Notifies observers when the store is loading.
777
+ *
778
+ * This is a common pattern found when implementing
779
+ * `Observable` data sources.
780
+ */
781
+ this.notifyLoading = new ReplaySubject(1);
782
+ /**
783
+ * The current loading state. Use loading when fetching new
784
+ * data for the store. The default loading state is `true`.
785
+ *
786
+ * This is such that if data is fetched asynchronously
787
+ * in a service, components can wait on loading notification
788
+ * before attempting to retrieve data from the service.
789
+ *
790
+ * Loading could be based on a composite response. For example
791
+ * when the stock and mutual funds have loaded, set loading to `false`.
792
+ */
793
+ this._loading = true;
794
+ /**
795
+ * Notifies observers that a search is in progress.
796
+ *
797
+ * This is a common pattern found when implementing
798
+ * `Observable` data sources.
799
+ */
800
+ this.notifySearching = new ReplaySubject(1);
801
+ /**
802
+ * The current `searching` state. Use `searching`
803
+ * for example to display a spinnner
804
+ * when performing a search.
805
+ * The default `searching` state is `false`.
806
+ */
807
+ this._searching = false;
808
+ /**
809
+ * Store slices
810
+ */
811
+ this.slices = new Map();
812
+ const delta = { type: "Initialize" /* INTIALIZE */, entries: entities };
813
+ this.post(entities);
814
+ this.notifyDelta.next(delta);
815
+ }
816
+ /**
817
+ * Calls complete on all {@link BehaviorSubject} instances.
818
+ *
819
+ * Call destroy when disposing of the store.
820
+ */
821
+ destroy() {
822
+ super.destroy();
823
+ this.notifyLoading.complete();
824
+ this.notifyActive.complete();
825
+ this.slices.forEach(slice => slice.destroy());
826
+ }
827
+ /**
828
+ * Toggles the entity:
829
+ *
830
+ * If the store contains the entity
831
+ * it will be deleted. If the store
832
+ * does not contains the entity,
833
+ * it is added.
834
+ * @param e
835
+ * @example Toggle the `Todo` instance
836
+ ```
837
+ estore.post(todo);
838
+ // Remove todo
839
+ estore.toggle(todo);
840
+ // Add it back
841
+ estore.toggle(todo);
842
+
843
+ ```
844
+ */
845
+ toggle(e) {
846
+ if (this.contains(e)) {
847
+ this.delete(e);
848
+ }
849
+ else {
850
+ this.post(e);
851
+ }
852
+ }
853
+ /**
854
+ * Add multiple entity entities to active.
855
+ *
856
+ * If the entity is not contained in the store it is added
857
+ * to the store before it is added to `active`.
858
+ *
859
+ * Also we clone the map prior to broadcasting it with
860
+ * `notifyActive` to make sure we will trigger Angular
861
+ * change detection in the event that it maintains
862
+ * a reference to the `active` state `Map` instance.
863
+ *
864
+ * @example Add a `todo1` and `todo2` as active
865
+ ```
866
+ addActive(todo1);
867
+ addActive(todo2);
868
+ ```
869
+ */
870
+ addActive(e) {
871
+ if (this.contains(e)) {
872
+ this.active.set(e.gid, e);
873
+ this.notifyActive.next(new Map(this.active));
874
+ }
875
+ else {
876
+ this.post(e);
877
+ this.active.set(e.gid, e);
878
+ this.notifyActive.next(new Map(this.active));
879
+ }
880
+ }
881
+ /**
882
+ * Delete an entity as active.
883
+ *
884
+ * Also we clone the map prior to broadcasting it with
885
+ * `notifyActive` to make sure we will trigger Angular
886
+ * change detection in the event that it maintains
887
+ * a reference to the `active` state `Map` instance.
888
+ *
889
+ * @example Mark a `todo` instance as active
890
+ ```
891
+ deleteActive(todo1);
892
+ deleteActive(todo2);
893
+ ```
894
+ */
895
+ deleteActive(e) {
896
+ this.active.delete(e.gid);
897
+ this.notifyActive.next(new Map(this.active));
898
+ }
899
+ /**
900
+ * Clear / reset the active entity map.
901
+ *
902
+ * Also we clone the map prior to broadcasting it with
903
+ * `notifyActive` to make sure we will trigger Angular
904
+ * change detection in the event that it maintains
905
+ * a reference to the `active` state `Map` instance.
906
+ *
907
+ * @example Mark a `todo` instance as active
908
+ ```
909
+ deleteActive(todo1);
910
+ deleteActive(todo2);
911
+ ```
912
+ */
913
+ clearActive() {
914
+ this.active.clear();
915
+ this.notifyActive.next(new Map(this.active));
916
+ }
917
+ /**
918
+ * Observe the active entity.
919
+ * @example
920
+ <pre>
921
+ let active$ = source.observeActive();
922
+ </pre>
923
+ */
924
+ observeActive() {
925
+ return this.notifyActive.asObservable();
926
+ }
927
+ /**
928
+ * Sets the current loading state and notifies observers.
929
+ */
930
+ set loading(loading) {
931
+ this._loading = loading;
932
+ this.notifyLoading.next(this._loading);
933
+ }
934
+ /**
935
+ * @return A snapshot of the loading state.
936
+ */
937
+ get loading() {
938
+ return this._loading;
939
+ }
940
+ /**
941
+ * Observe loading.
942
+ * @example
943
+ <pre>
944
+ let loading$ = source.observeLoading();
945
+ </pre>
946
+
947
+ Note that this obverable piped through
948
+ `takeWhile(v->v, true), such that it will
949
+ complete after each emission.
950
+
951
+ See:
952
+ https://medium.com/@ole.ersoy/waiting-on-estore-to-load-8dcbe161613c
953
+
954
+ For more details.
955
+ */
956
+ observeLoading() {
957
+ return this.notifyLoading.asObservable().
958
+ pipe(takeWhile(v => v, true));
959
+ }
960
+ /**
961
+ * Notfiies when loading has completed.
962
+ */
963
+ observeLoadingComplete() {
964
+ return this.observeLoading().pipe(filter(loading => loading == false), switchMap(() => of(true)));
965
+ }
966
+ /**
967
+ * Sets the current searching state and notifies observers.
968
+ */
969
+ set searching(searching) {
970
+ this._searching = searching;
971
+ this.notifySearching.next(this._searching);
972
+ }
973
+ /**
974
+ * @return A snapshot of the searching state.
975
+ */
976
+ get searching() {
977
+ return this._searching;
978
+ }
979
+ /**
980
+ * Observe searching.
981
+ * @example
982
+ <pre>
983
+ let searching$ = source.observeSearching();
984
+ </pre>
985
+
986
+ Note that this obverable piped through
987
+ `takeWhile(v->v, true), such that it will
988
+ complete after each emission.
989
+
990
+ See:
991
+ https://medium.com/@ole.ersoy/waiting-on-estore-to-load-8dcbe161613c
992
+
993
+ For more details.
994
+ */
995
+ observeSearching() {
996
+ return this.notifySearching.asObservable().
997
+ pipe(takeWhile(v => v, true));
998
+ }
999
+ /**
1000
+ * Notfiies when searching has completed.
1001
+ */
1002
+ observeSearchingComplete() {
1003
+ return this.observeSearching().pipe(filter(searching => searching == false), switchMap(() => of(true)));
1004
+ }
1005
+ /**
1006
+ * Adds a slice to the store and keys it by the slices label.
1007
+ *
1008
+ * @param p
1009
+ * @param label
1010
+ *
1011
+ * @example Setup a Todo Slice for COMPLETE Todos
1012
+ ```
1013
+ source.addSlice(todo => todo.complete, TodoSlices.COMPLETE);
1014
+ ```
1015
+ */
1016
+ addSlice(p, label) {
1017
+ const slice = new Slice(label, p, this);
1018
+ this.slices.set(slice.label, slice);
1019
+ }
1020
+ /**
1021
+ * Remove a slice
1022
+ * @param label The label identifying the slice
1023
+ *
1024
+ * @example Remove the TodoSlices.COMPLETE Slice
1025
+ ```
1026
+ source.removeSlice(TodoSlices.COMPLETE);
1027
+ ```
1028
+ */
1029
+ removeSlice(label) {
1030
+ this.slices.delete(label);
1031
+ }
1032
+ /**
1033
+ * Get a slice
1034
+ * @param label The label identifying the slice
1035
+ * @return The Slice instance or undefined
1036
+ *
1037
+ * @example Get the TodoSlices.COMPLETE slice
1038
+ ```
1039
+ source.getSlice(TodoSlices.COMPLETE);
1040
+ ```
1041
+ */
1042
+ getSlice(label) {
1043
+ return this.slices.get(label);
1044
+ }
1045
+ /**
1046
+ * Post (Add a new) element(s) to the store.
1047
+ * @param e An indiidual entity or an array of entities
1048
+ * @example Post a `todo`.
1049
+ ```
1050
+ store.post(todo);
1051
+ ```
1052
+ */
1053
+ post(e) {
1054
+ if (!Array.isArray(e)) {
1055
+ const guid = e[this.GUID_KEY]
1056
+ ? e[this.GUID_KEY]
1057
+ : GUID();
1058
+ e[this.GUID_KEY] = guid;
1059
+ this.entries.set(guid, e);
1060
+ this.updateIDEntry(e);
1061
+ Array.from(this.slices.values()).forEach(s => {
1062
+ s.post(e);
1063
+ });
1064
+ //Create a new array reference to trigger Angular change detection.
1065
+ let v = [...Array.from(this.entries.values())];
1066
+ const delta = { type: "Post" /* POST */, entries: [e] };
1067
+ this.notifyAll(v, delta);
1068
+ }
1069
+ else {
1070
+ this.postA(e);
1071
+ }
1072
+ }
1073
+ /**
1074
+ * Post elements to the store.
1075
+ * @param ...e
1076
+ * @example Post two `Todo` instances.
1077
+ ```
1078
+ store.post(todo1, todo2);
1079
+ ```
1080
+ */
1081
+ postN(...e) {
1082
+ e.forEach(e => {
1083
+ const guid = e[this.GUID_KEY]
1084
+ ? e[this.GUID_KEY]
1085
+ : GUID();
1086
+ e[this.GUID_KEY] = guid;
1087
+ this.entries.set(guid, e);
1088
+ this.updateIDEntry(e);
1089
+ });
1090
+ Array.from(this.slices.values()).forEach(s => {
1091
+ s.postA(e);
1092
+ });
1093
+ //Create a new array reference to trigger Angular change detection.
1094
+ let v = [...Array.from(this.entries.values())];
1095
+ const delta = { type: "Post" /* POST */, entries: e };
1096
+ this.notifyAll(v, delta);
1097
+ }
1098
+ /**
1099
+ * Post (Add) an array of elements to the store.
1100
+ * @param e
1101
+ * @example Post a `Todo` array.
1102
+ ```
1103
+ store.post([todo1, todo2]);
1104
+ ```
1105
+ */
1106
+ postA(e) {
1107
+ this.postN(...e);
1108
+ }
1109
+ /**
1110
+ * Put (Update) an element.
1111
+ * @param e
1112
+ * @example Put a Todo instance.
1113
+ ```
1114
+ store.put(todo1);
1115
+ ```
1116
+ */
1117
+ put(e) {
1118
+ if (!Array.isArray(e)) {
1119
+ let id = e[this.GUID_KEY];
1120
+ this.entries.set(id, e);
1121
+ this.updateIDEntry(e);
1122
+ let v = [...Array.from(this.entries.values())];
1123
+ this.notify.next(v);
1124
+ const delta = { type: "Put" /* PUT */, entries: [e] };
1125
+ this.notifyDelta.next(delta);
1126
+ Array.from(this.slices.values()).forEach(s => {
1127
+ s.put(e);
1128
+ });
1129
+ }
1130
+ else {
1131
+ this.putA(e);
1132
+ }
1133
+ }
1134
+ /**
1135
+ * Put (Update) an element or add an element that was read from a persistence source
1136
+ * and thus already has an assigned global id`.
1137
+ * @param e
1138
+ * @example Put Todo instances.
1139
+ ```
1140
+ store.put(todo1, todo2);
1141
+ ```
1142
+ */
1143
+ putN(...e) {
1144
+ this.putA(e);
1145
+ }
1146
+ /**
1147
+ * Put (Update) the array of elements.
1148
+ * @param e
1149
+ * @example Put Todo instances.
1150
+ ```
1151
+ store.put([todo1, todo2]);
1152
+ ```
1153
+ */
1154
+ putA(e) {
1155
+ e.forEach(e => {
1156
+ let guid = e[this.GUID_KEY];
1157
+ this.entries.set(guid, e);
1158
+ this.updateIDEntry(e);
1159
+ });
1160
+ //Create a new array reference to trigger Angular change detection.
1161
+ let v = [...Array.from(this.entries.values())];
1162
+ this.notify.next(v);
1163
+ const delta = { type: "Put" /* PUT */, entries: e };
1164
+ this.notifyDelta.next(delta);
1165
+ Array.from(this.slices.values()).forEach(s => {
1166
+ s.putA(e);
1167
+ });
1168
+ }
1169
+ /**
1170
+ * Delete (Update) the array of elements.
1171
+ * @param e
1172
+ * @example Delete todo1.
1173
+ ```
1174
+ store.delete(todo1]);
1175
+ ```
1176
+ */
1177
+ delete(e) {
1178
+ if (!Array.isArray(e)) {
1179
+ this.deleteActive(e);
1180
+ const guid = e[this.GUID_KEY];
1181
+ this.entries.delete(guid);
1182
+ this.deleteIDEntry(e);
1183
+ Array.from(this.slices.values()).forEach(s => {
1184
+ s.entries.delete(guid);
1185
+ });
1186
+ //Create a new array reference to trigger Angular change detection.
1187
+ let v = [...Array.from(this.entries.values())];
1188
+ const delta = { type: "Delete" /* DELETE */, entries: [e] };
1189
+ this.notifyAll(v, delta);
1190
+ Array.from(this.slices.values()).forEach(s => {
1191
+ s.delete(e);
1192
+ });
1193
+ }
1194
+ else {
1195
+ this.deleteA(e);
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Delete N elements.
1200
+ * @param ...e
1201
+ * @example Put Todo instances.
1202
+ ```
1203
+ store.delete(todo1, todo2);
1204
+ ```
1205
+ */
1206
+ deleteN(...e) {
1207
+ this.deleteA(e);
1208
+ }
1209
+ /**
1210
+ * Delete N elements.
1211
+ * @param ...e
1212
+ * @example Put Todo instances.
1213
+ ```
1214
+ store.delete(todo1, todo2);
1215
+ ```
1216
+ */
1217
+ deleteA(e) {
1218
+ e.forEach(e => {
1219
+ this.deleteActive(e);
1220
+ const guid = e[this.GUID_KEY];
1221
+ this.entries.delete(guid);
1222
+ this.deleteIDEntry(e);
1223
+ Array.from(this.slices.values()).forEach(s => {
1224
+ s.entries.delete(guid);
1225
+ });
1226
+ });
1227
+ //Create a new array reference to trigger Angular change detection.
1228
+ let v = [...Array.from(this.entries.values())];
1229
+ const delta = { type: "Delete" /* DELETE */, entries: e };
1230
+ this.notifyAll(v, delta);
1231
+ Array.from(this.slices.values()).forEach(s => {
1232
+ s.deleteA(e);
1233
+ });
1234
+ }
1235
+ /**
1236
+ * Delete elements by {@link Predicate}.
1237
+ * @param p The predicate.
1238
+ * @example Put Todo instances.
1239
+ ```
1240
+ store.delete(todo1, todo2);
1241
+ ```
1242
+ */
1243
+ deleteP(p) {
1244
+ const d = [];
1245
+ Array.from(this.entries.values()).forEach(e => {
1246
+ if (p(e)) {
1247
+ d.push(e);
1248
+ const id = e[this.GUID_KEY];
1249
+ this.entries.delete(id);
1250
+ this.deleteActive(e);
1251
+ this.deleteIDEntry(e);
1252
+ }
1253
+ });
1254
+ //Create a new array reference to trigger Angular change detection.
1255
+ let v = [...Array.from(this.entries.values())];
1256
+ const delta = { type: "Delete" /* DELETE */, entries: d };
1257
+ this.notifyAll(v, delta);
1258
+ Array.from(this.slices.values()).forEach(s => {
1259
+ s.deleteA(d);
1260
+ });
1261
+ }
1262
+ /**
1263
+ * If the entity has the `id` key initialized with a value,
1264
+ * then also add the entity to the `idEntries`.
1265
+ *
1266
+ * @param e The element to be added to the `idEntries`.
1267
+ */
1268
+ updateIDEntry(e) {
1269
+ if (e[this.ID_KEY]) {
1270
+ this.idEntries.set(e[this.ID_KEY], e);
1271
+ }
1272
+ }
1273
+ /**
1274
+ * If the entity has the `id` key initialized with a value,
1275
+ * then also delete the entity to the `idEntries`.
1276
+ *
1277
+ * @param e The element to be added to the `idEntries`.
1278
+ */
1279
+ deleteIDEntry(e) {
1280
+ if (e[this.ID_KEY]) {
1281
+ this.idEntries.delete(e[this.ID_KEY]);
1282
+ }
1283
+ }
1284
+ /**
1285
+ * Resets the store and all contained slice instances to empty.
1286
+ * Also perform delta notification that sends all current store entries.
1287
+ * The ActionType.RESET code is sent with the delta notification. Slices
1288
+ * send their own delta notification.
1289
+ *
1290
+ * @example Reset the store.
1291
+ ```
1292
+ store.reset();
1293
+ ```
1294
+ */
1295
+ reset() {
1296
+ const delta = {
1297
+ type: "Reset" /* RESET */,
1298
+ entries: Array.from(this.entries.values())
1299
+ };
1300
+ this.notifyAll([], delta);
1301
+ this.entries = new Map();
1302
+ Array.from(this.slices.values()).forEach(s => {
1303
+ s.reset();
1304
+ });
1305
+ }
1306
+ /**
1307
+ * Call all the notifiers at once.
1308
+ *
1309
+ * @param v
1310
+ * @param delta
1311
+ */
1312
+ notifyAll(v, delta) {
1313
+ super.notifyAll(v, delta);
1314
+ this.notifyLoading.next(this.loading);
1315
+ }
1316
+ }
1317
+
1318
+ class OStore {
1319
+ constructor(start) {
1320
+ /**
1321
+ * Map of Key Value pair entries
1322
+ * containing values store in this store.
1323
+ */
1324
+ this.entries = new Map();
1325
+ /**
1326
+ * Map of replay subject id to `ReplaySubject` instance.
1327
+ */
1328
+ this.subjects = new Map();
1329
+ if (start) {
1330
+ this.S = start;
1331
+ const keys = Object.keys(start);
1332
+ keys.forEach((k) => {
1333
+ const ovr = start[k];
1334
+ this.post(ovr, ovr.value);
1335
+ ovr.obs = this.observe(ovr);
1336
+ });
1337
+ }
1338
+ }
1339
+ /**
1340
+ * Reset the state of the OStore to the
1341
+ * values or reset provided in the constructor
1342
+ * {@link OStoreStart} instance.
1343
+ */
1344
+ reset() {
1345
+ if (this.S) {
1346
+ const keys = Object.keys(this.S);
1347
+ keys.forEach((k) => {
1348
+ const ovr = this.S[k];
1349
+ this.put(ovr, ovr.reset ? ovr.reset : ovr.value);
1350
+ });
1351
+ }
1352
+ }
1353
+ /**
1354
+ * Clear all entries
1355
+ */
1356
+ clear() {
1357
+ this.entries.clear();
1358
+ }
1359
+ /**
1360
+ * Set create a key value pair entry and creates a
1361
+ * corresponding replay subject instance that will
1362
+ * be used to broadcast updates.
1363
+ *
1364
+ * @param key The key identifying the value
1365
+ * @param value The value
1366
+ */
1367
+ post(key, value) {
1368
+ this.entries.set(key, value);
1369
+ this.subjects.set(key, new ReplaySubject(1));
1370
+ //Emit immediately so that Observers can receive
1371
+ //the value straight away.
1372
+ const subject = this.subjects.get(key);
1373
+ if (subject) {
1374
+ subject.next(value);
1375
+ }
1376
+ }
1377
+ /**
1378
+ * Update a value and notify subscribers.
1379
+ *
1380
+ * @param key
1381
+ * @param value
1382
+ */
1383
+ put(key, value) {
1384
+ this.entries.set(key, value);
1385
+ const subject = this.subjects.get(key);
1386
+ if (subject) {
1387
+ subject.next(value);
1388
+ }
1389
+ }
1390
+ /**
1391
+ * Deletes both the value entry and the corresponding {@link ReplaySubject}.
1392
+ * Will unsubscribe the {@link ReplaySubject} prior to deleting it,
1393
+ * severing communication with corresponding {@link Observable}s.
1394
+ *
1395
+ * @param key
1396
+ */
1397
+ delete(key) {
1398
+ this.entries.delete(key);
1399
+ this.subjects.delete(key);
1400
+ const subject = this.subjects.get(key);
1401
+ if (subject) {
1402
+ subject.unsubscribe();
1403
+ }
1404
+ }
1405
+ /**
1406
+ * Observe changes to the values.
1407
+ *
1408
+ * @param key
1409
+ * @return An {@link Observable} of the value
1410
+ */
1411
+ observe(key) {
1412
+ return this.subjects.get(key).asObservable();
1413
+ }
1414
+ /**
1415
+ * Check whether a value exists.
1416
+ *
1417
+ * @param key
1418
+ * @return True if the entry exists ( Is not null or undefined ) and false otherwise.
1419
+ */
1420
+ exists(key) {
1421
+ return this.entries.get(key) != null;
1422
+ }
1423
+ /**
1424
+ * Retrieve a snapshot of the
1425
+ * value.
1426
+ *
1427
+ * @param key
1428
+ * @return A snapshot of the value corresponding to the key.
1429
+ */
1430
+ snapshot(key) {
1431
+ return this.entries.get(key);
1432
+ }
1433
+ /**
1434
+ * Indicates whether the store is empty.
1435
+ * @return true if the store is empty, false otherwise.
1436
+ */
1437
+ isEmpty() {
1438
+ return Array.from(this.entries.values()).length == 0;
1439
+ }
1440
+ /**
1441
+ * Returns the number of key value pairs contained.
1442
+ *
1443
+ * @return the number of entries in the store.
1444
+ */
1445
+ count() {
1446
+ return Array.from(this.entries.values()).length;
1447
+ }
1448
+ }
1449
+
1450
+ /*
1451
+ * Public API Surface of slice
1452
+ */
1453
+
1454
+ /**
1455
+ * Generated bundle index. Do not edit.
1456
+ */
1457
+
1458
+ export { AbstractStore, ESTORE_CONFIG_DEFAULT, EStore, GUID, OStore, Slice, attachGUID, attachGUIDs, deepCopy, distinct, excludeKeys, getActiveValue, mapEntity, scrollingUp, search, shallowCopy, unique };
1459
+ //# sourceMappingURL=fireflysemantics-slice.js.map