@fireflysemantics/slice
Version:

1,643 lines (1,623 loc) • 48.4 kB
JavaScript
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