From 1b0d7af7635753bce404b3af3d551bdfe0e9ea5e Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 28 May 2021 08:11:15 +0200 Subject: [PATCH 01/63] updated computed method descriptions --- packages/core/src/collection/index.ts | 32 +++++----- packages/core/src/computed/index.ts | 89 ++++++++++++++++----------- packages/core/src/state/index.ts | 7 ++- 3 files changed, 77 insertions(+), 51 deletions(-) diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index d88e53db..18611453 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -40,10 +40,13 @@ export class Collection { public isInstantiated = false; /** + * Class that holds a List of Objects with key and causes rerender on subscribed Components + * * @public - * Collection - Class that holds a List of Objects with key and causes rerender on subscribed Components - * @param agileInstance - An instance of Agile - * @param config - Config + * + * @param agileInstance - Instance of Agile the Collection belongs to + * + * @param config - Configuration */ constructor(agileInstance: Agile, config: CollectionConfig = {}) { this.agileInstance = () => agileInstance; @@ -408,14 +411,15 @@ export class Collection { return !!this.getGroup(groupKey, config); } - //========================================================================================================= - // Get Group - //========================================================================================================= /** + * Retrieves a single Group by key/name. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroup) + * * @public - * Get Group by Key/Name - * @param groupKey - Key/Name of Group - * @param config - Config + * @memberOf Collection + * @param groupKey - key/name Group identifier + * @param config - Configuration */ public getGroup( groupKey: GroupKey | undefined, @@ -1321,12 +1325,12 @@ export interface CollectionPersistentConfigInterface { defaultStorageKey?: StorageKey; } -/* - * @param notExisting - If not existing Items like placeholder Items can be removed. +/** + * @property notExisting - If not existing Items like placeholder Items can be removed. * Keep in mind that sometimes it won't remove the Item entirely * because another Instance (like a Selector) needs to keep reference to it. * https://github.com/agile-ts/agile/pull/152 - * @param - If Selectors that have selected an Item to be removed, should be removed too + * @property removeSelector - If Selectors that have selected an Item to be removed, should be removed too */ export interface RemoveItemsConfigInterface { notExisting?: boolean; @@ -1334,8 +1338,8 @@ export interface RemoveItemsConfigInterface { } /** - * @param patch - If Data gets patched into existing Item - * @param background - If assigning Data happens in background + * @property patch - If Data gets patched into existing Item + * @property background - If assigning Data happens in background */ export interface SetDataConfigInterface { patch?: boolean; diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index e4470c22..cb70ad7e 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -17,16 +17,25 @@ export class Computed extends State< > { public agileInstance: () => Agile; - public computeFunction: () => ComputedValueType; - public deps: Array = []; // All Dependencies of Computed (hardCoded and autoDetected) - public hardCodedDeps: Array = []; // HardCoded Dependencies of Computed + public computeFunction: () => ComputedValueType; // Function to compute the computed value + public deps: Array = []; // All dependencies the Computed depends on (including hardCoded and autoDetected dependencies) + public hardCodedDeps: Array = []; // Only hardCoded dependencies the Computed depends /** + * A extension of the State Class that computes its value based on a compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and only recomputed when one of its direct dependencies changed. + * + * Direct dependencies can be States and Collections. + * Meaning if a dependent State value changes, the computed value will be recomputed. + * + * [Learn more..](https://agile-ts.org/docs/core/computed/) + * * @public - * Computed - Function that recomputes its value if a dependency changes - * @param agileInstance - An instance of Agile - * @param computeFunction - Function for computing value - * @param config - Config + * @param agileInstance - Instance of Agile the Computed belongs to. + * @param computeFunction - Function to compute the computed value. + * @param config - Configuration object */ constructor( agileInstance: Agile, @@ -43,23 +52,23 @@ export class Computed extends State< this.agileInstance = () => agileInstance; this.computeFunction = computeFunction; - // Format hardCodedDeps + // Extract Observer of passed hardcoded dependency instances this.hardCodedDeps = extractObservers(config.computedDeps).filter( (dep): dep is Observer => dep !== undefined ); this.deps = this.hardCodedDeps; - // Recompute for setting initial value and adding missing dependencies + // Initial recompute to assign initial value and autodetect missing dependencies this.recompute({ autodetect: true }); } - //========================================================================================================= - // Recompute - //========================================================================================================= /** + * Forces a recomputation of the cached value based on the compute function. + * + * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#recompute) + * * @public - * Recomputes Value of Computed - * @param config - Config + * @param config - Configuration object */ public recompute(config: RecomputeConfigInterface = {}): this { config = defineConfig(config, { @@ -72,15 +81,21 @@ export class Computed extends State< return this; } - //========================================================================================================= - // Updates Compute Function - //========================================================================================================= /** + * Assigns new function to the Computed to compute the computed value. + * + * The dependencies of the new compute function are automatically detected + * and accordingly updated in the Computed Class. + * + * A initial computation is automatically performed with the new function + * to update the outdated cached value of the Computed. + * + * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#updatecomputefunction) + * * @public - * Applies new compute Function to Computed - * @param computeFunction - New Function for computing value - * @param deps - Hard coded dependencies of Computed Function - * @param config - Config + * @param computeFunction - New function to compute the computed value. + * @param deps - Hard coded dependencies on which the Computed Class depends. + * @param config - Configuration object */ public updateComputeFunction( computeFunction: () => ComputedValueType, @@ -92,7 +107,7 @@ export class Computed extends State< autodetect: true, }); - // Update deps + // Update dependencies of Computed const newDeps = extractObservers(deps).filter( (dep): dep is Observer => dep !== undefined ); @@ -103,25 +118,24 @@ export class Computed extends State< // Update computeFunction this.computeFunction = computeFunction; - // Recompute for setting initial Computed Function Value and adding missing Dependencies + // Recompute to assign new computed value and autodetect missing dependencies this.recompute(removeProperties(config, ['overwriteDeps'])); return this; } - //========================================================================================================= - // Compute - //========================================================================================================= /** + * Computes the value of the Computed Class and autodetects used dependencies in the compute function. + * * @internal - * Recomputes value and adds missing dependencies to Computed + * @param config - Configuration object */ public compute(config: ComputeConfigInterface = {}): ComputedValueType { config = defineConfig(config, { autodetect: true, }); - // Start auto tracking Observers the computeFunction might depend on + // Start auto tracking of Observers on which the computeFunction might depend if (config.autodetect) ComputedTracker.track(); const computedValue = this.computeFunction(); @@ -129,13 +143,11 @@ export class Computed extends State< // Handle auto tracked Observers if (config.autodetect) { const foundDeps = ComputedTracker.getTrackedObservers(); - - // Handle foundDeps and hardCodedDeps const newDeps: Array = []; this.hardCodedDeps.concat(foundDeps).forEach((observer) => { newDeps.push(observer); - // Make this Observer depend on foundDep Observer + // Make this Observer depend on the foundDep Observer observer.depend(this.observer); }); @@ -145,20 +157,25 @@ export class Computed extends State< return computedValue; } - //========================================================================================================= - // Overwriting some functions which aren't allowed to use in Computed - //========================================================================================================= - + /** + * Not usable in Computed Class. + */ public patch() { LogCodeManager.log('19:03:00'); return this; } + /** + * Not usable in Computed Class. + */ public persist(): this { LogCodeManager.log('19:03:01'); return this; } + /** + * Not usable in Computed Class. + */ public invert(): this { LogCodeManager.log('19:03:02'); return this; @@ -166,7 +183,7 @@ export class Computed extends State< } /** - * @param computedDeps - Hard coded dependencies of Computed Function + * @param computedDeps - Hard coded dependencies of compute function */ export interface ComputedConfigInterface extends StateConfigInterface { computedDeps?: Array; diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 3c778f96..90155b2c 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -48,9 +48,14 @@ export class State { /** * @public * State - Class that holds one Value and causes rerender on subscribed Components + * * @param agileInstance - An instance of Agile + * * @param initialValue - Initial Value of State - * @param config - Config + * + * @param config - Configuration + * + * @typeparam ValueType - Type of a the value the State represents */ constructor( agileInstance: Agile, From cfeae997561e1683d1e08a7b6a2d4f019509da60 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 29 May 2021 12:38:07 +0200 Subject: [PATCH 02/63] fixed typos --- .../src/collection/collection.persistent.ts | 268 ++++++++++-------- packages/core/src/collection/index.ts | 60 ++-- packages/core/src/collection/item.ts | 70 +++++ .../core/src/computed/computed.tracker.ts | 19 +- packages/core/src/computed/index.ts | 31 +- packages/core/src/state/index.ts | 3 + packages/core/src/state/state.observer.ts | 3 +- packages/core/src/state/state.persistent.ts | 8 +- .../tests/unit/collection/collection.test.ts | 45 +-- 9 files changed, 315 insertions(+), 192 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 04bbe301..d9dd78c5 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -1,5 +1,4 @@ import { - Agile, Collection, CollectionKey, CreatePersistentConfigInterface, @@ -7,6 +6,7 @@ import { defineConfig, Group, GroupKey, + Item, ItemKey, LogCodeManager, Persistent, @@ -24,10 +24,11 @@ export class CollectionPersistent< static storageGroupKeyPattern = '_${collectionKey}_group_${groupKey}'; /** + * Internal Class for managing the permanent persistence of a Collection. + * * @internal - * Collection Persist Manager - Handles permanent storing of Collection Value - * @param collection - Collection that gets stored - * @param config - Config + * @param collection - Collection to be persisted. + * @param config - Configuration object */ constructor( collection: Collection, @@ -48,47 +49,44 @@ export class CollectionPersistent< defaultStorageKey: config.defaultStorageKey, }); - // Load/Store persisted Value/s for the first Time + // Load/Store persisted value/s for the first time if (this.ready && config.instantiate) this.initialLoading(); } - //========================================================================================================= - // Set Key - //========================================================================================================= /** + * Updates key/name identifier of Persistent. + * * @internal - * Updates Key/Name of Persistent - * @param value - New Key/Name of Persistent + * @param value - New key/name identifier. */ public async setKey(value?: StorageKey): Promise { const oldKey = this._key; const wasReady = this.ready; - // Assign Key + // Assign new key to Persistent if (value === this._key) return; this._key = value || Persistent.placeHolderKey; const isValid = this.validatePersistent(); - // Try to Initial Load Value if persistent wasn't ready + // Try to initial load value if persistent wasn't ready before if (!wasReady) { if (isValid) await this.initialLoading(); return; } - // Remove value at old Key + // Remove persisted values at old key await this.removePersistedValue(oldKey); - // Assign Value to new Key + // Persist values at the new key if (isValid) await this.persistValue(value); } - //========================================================================================================= - // Initial Loading - //========================================================================================================= /** * @internal - * Loads/Saves Storage Value for the first Time + * Loads the persisted value into the Collection + * or persists the Collection value in the corresponding Storage. + * This depends on whether the Collection has been persisted before. */ public async initialLoading() { super.initialLoading().then(() => { @@ -96,14 +94,12 @@ export class CollectionPersistent< }); } - //========================================================================================================= - // Load Persisted Value - //========================================================================================================= /** + * Loads the values of the Collection Instances from the corresponding Storage. + * * @internal - * Loads Collection from Storage - * @param storageItemKey - Prefix Key of Persisted Instances (default PersistentKey) - * @return Success? + * @param storageItemKey - Prefix Key of persisted Collection Instances | default = Persistent.key | + * @return Whether the loading was successful. */ public async loadPersistedValue( storageItemKey?: PersistentKey @@ -111,180 +107,205 @@ export class CollectionPersistent< if (!this.ready) return false; const _storageItemKey = storageItemKey || this._key; - // Check if Collection is Persisted + // Check if Collection is already persisted (indicated by the persistence of true at _storageItemKey) const isPersisted = await this.agileInstance().storages.get( _storageItemKey, this.config.defaultStorageKey as any ); + + // Return false if Collection isn't persisted yet if (!isPersisted) return false; - // Loads Values into Collection + // Helper function to load persisted values into the Collection const loadValuesIntoCollection = async () => { - const defaultGroup = this.collection().getGroup( - this.collection().config.defaultGroupKey - ); + const defaultGroup = this.collection().getDefaultGroup(); if (!defaultGroup) return false; + const defaultGroupPersistKey = CollectionPersistent.getGroupStorageKey( + defaultGroup._key, + _storageItemKey + ); - // Persist Default Group and load its Value manually to be 100% sure it got loaded - defaultGroup.persist({ + // Persist default Group and load its value manually to be 100% sure + // that it was loaded completely + defaultGroup.persist(defaultGroupPersistKey, { loadValue: false, - followCollectionPersistKeyPattern: true, }); - if (defaultGroup.persistent?.ready) { - await defaultGroup.persistent?.initialLoading(); - defaultGroup.isPersisted = true; - } + if (defaultGroup.persistent?.ready) + await defaultGroup.persistent.initialLoading(); - // Load Items into Collection + // Persist Items found in the default Group's value for (const itemKey of defaultGroup._value) { - const itemStorageKey = CollectionPersistent.getItemStorageKey( + const item = this.collection().getItem(itemKey); + const itemPersistKey = CollectionPersistent.getItemStorageKey( itemKey, _storageItemKey ); - // Get Storage Value - const storageValue = await this.agileInstance().storages.get( - itemStorageKey, - this.config.defaultStorageKey as any + // Persist already existing Item + if (item != null) { + item.persist(itemPersistKey); + return true; + } + + // Create temporary placeholder Item in which the Item value will be loaded + const dummyItem = new Item( + this.collection(), + { + [this.collection().config.primaryKey]: itemKey, // Setting PrimaryKey of Item to passed itemKey + dummy: 'item', + } as any, + { isPlaceholder: true } ); - if (!storageValue) continue; - // Collect found Storage Value - this.collection().collect(storageValue); + // Persist dummy Item and load its value manually to be 100% sure + // that it was loaded completely and exists + dummyItem?.persist(itemPersistKey, { + loadValue: false, + }); + if (dummyItem?.persistent?.ready) { + const success = await dummyItem.persistent.loadPersistedValue( + itemPersistKey + ); + + // If successfully loaded add Item to Collection + if (success) this.collection().collectItem(dummyItem); + } } + return true; }; const success = await loadValuesIntoCollection(); - // Persist Collection, so that the Storage Value updates dynamically if the Collection updates + // 'Persist' Collection to setup side effects + // that automatically update the corresponding Storage value if the Collection updates if (success) await this.persistValue(_storageItemKey); return success; } - //========================================================================================================= - // Persist Value - //========================================================================================================= /** + * Persists Collection in corresponding Storage if not already done + * and sets up side effects that dynamically update the storage value when the Collection changes. + * * @internal - * Sets everything up so that the Collection gets saved in the Storage - * @param storageItemKey - Prefix Key of Persisted Instances (default PersistentKey) - * @return Success? + * @param storageItemKey - Prefix Key of persisted Collection Instances | default = Persistent.key | + * @return Whether the persisting and the setting up of the side effects was successful. */ public async persistValue(storageItemKey?: PersistentKey): Promise { if (!this.ready) return false; const _storageItemKey = storageItemKey || this._key; - const defaultGroup = this.collection().getGroup( - this.collection().config.defaultGroupKey - ); + const defaultGroup = this.collection().getDefaultGroup(); if (!defaultGroup) return false; + const defaultGroupPersistKey = CollectionPersistent.getGroupStorageKey( + defaultGroup._key, + _storageItemKey + ); - // Set Collection to Persisted (in Storage) + // Set flag in Storage to indicate that the Collection is persisted this.agileInstance().storages.set(_storageItemKey, true, this.storageKeys); // Persist default Group - if (!defaultGroup.isPersisted) - defaultGroup.persist({ followCollectionPersistKeyPattern: true }); + if (!defaultGroup.isPersisted) defaultGroup.persist(defaultGroupPersistKey); - // Add sideEffect to default Group which adds and removes Items from the Storage depending on the Group Value + // Add side effect to default Group + // that adds or removes Items from the Storage depending on the Group value defaultGroup.addSideEffect( CollectionPersistent.defaultGroupSideEffectKey, () => this.rebuildStorageSideEffect(defaultGroup, _storageItemKey), { weight: 0 } ); - // Persist Collection Items + // Persist Items found in the default Group's value for (const itemKey of defaultGroup._value) { const item = this.collection().getItem(itemKey); - const itemStorageKey = CollectionPersistent.getItemStorageKey( + const itemPersistKey = CollectionPersistent.getItemStorageKey( itemKey, _storageItemKey ); - item?.persist(itemStorageKey); + if (!item?.isPersisted) item?.persist(itemPersistKey); } this.isPersisted = true; return true; } - //========================================================================================================= - // Remove Persisted Value - //========================================================================================================= /** + * Removes Collection from the corresponding Storage. + * -> Collection is no longer persisted + * * @internal - * Removes Collection from the Storage - * @param storageItemKey - Prefix Key of Persisted Instances (default PersistentKey) - * @return Success? + * @param storageItemKey - Prefix Key of persisted Collection Instances | default = Persistent.key | + * @return Whether the removing was successful. */ public async removePersistedValue( storageItemKey?: PersistentKey ): Promise { if (!this.ready) return false; const _storageItemKey = storageItemKey || this._key; - const defaultGroup = this.collection().getGroup( - this.collection().config.defaultGroupKey - ); + const defaultGroup = this.collection().getDefaultGroup(); if (!defaultGroup) return false; + const defaultGroupPersistKey = CollectionPersistent.getGroupStorageKey( + defaultGroup._key, + _storageItemKey + ); - // Set Collection to not Persisted + // Remove Collection is persisted indicator flag this.agileInstance().storages.remove(_storageItemKey, this.storageKeys); - // Remove default Group from Storage - defaultGroup.persistent?.removePersistedValue(); - - // Remove Rebuild Storage sideEffect from default Group + // Remove default Group from the Storage + defaultGroup.persistent?.removePersistedValue(defaultGroupPersistKey); defaultGroup.removeSideEffect( CollectionPersistent.defaultGroupSideEffectKey ); - // Remove Collection Items from Storage + // Remove Items found in the default Group's value from the Storage for (const itemKey of defaultGroup._value) { const item = this.collection().getItem(itemKey); - item?.persistent?.removePersistedValue(); + const itemPersistKey = CollectionPersistent.getItemStorageKey( + itemKey, + _storageItemKey + ); + item?.persistent?.removePersistedValue(itemPersistKey); } this.isPersisted = false; return true; } - //========================================================================================================= - // Format Key - //========================================================================================================= /** + * Formats given key so that it can be used as a valid Storage key. + * If no formatable key is given, an attempt is made to use the Collection key as Storage key. + * If this also fails, undefined is returned. + * * @internal - * Formats Storage Key - * @param key - Key that gets formatted + * @param key - Key to be formatted */ public formatKey(key?: StorageKey): StorageKey | undefined { - const collection = this.collection(); - - // Get key from Collection - if (key == null && collection._key) return collection._key; - + if (key == null && this.collection()._key) return this.collection()._key; if (key == null) return; - - // Set Storage Key to Collection Key if Collection has no key - if (collection._key == null) collection._key = key; - + if (this.collection()._key == null) this.collection()._key = key; return key; } - //========================================================================================================= - // Rebuild Storage SideEffect - //========================================================================================================= /** - * @internal * Rebuilds Storage depending on Group + * + * @internal * @param group - Group - * @param key - Prefix Key of Persisted Instances (default PersistentKey) + * @param storageItemKey - Prefix Key of persisted Collection Instances | default = Persistent.key | */ - public rebuildStorageSideEffect(group: Group, key?: PersistentKey) { + public rebuildStorageSideEffect( + group: Group, + storageItemKey?: PersistentKey + ) { const collection = group.collection(); - const _key = key || collection.persistent?._key; + const _storageItemKey = storageItemKey || collection.persistent?._key; - // Return if only a ItemKey got updated + // Return if no Item got added or removed + // because then the Item performs the Storage update itself if (group.previousStateValue.length === group._value.length) return; + // Extract Item keys that got removed or added to the Group const addedKeys = group._value.filter( (key) => !group.previousStateValue.includes(key) ); @@ -292,32 +313,38 @@ export class CollectionPersistent< (key) => !group._value.includes(key) ); - // Persist Added Keys + // Persist newly added Items addedKeys.forEach((itemKey) => { const item = collection.getItem(itemKey); - const _itemKey = CollectionPersistent.getItemStorageKey(itemKey, _key); - if (!item) return; - if (!item.isPersisted) item.persist(_itemKey); - else item.persistent?.persistValue(_itemKey); + const itemPersistKey = CollectionPersistent.getItemStorageKey( + itemKey, + _storageItemKey + ); + if (item != null) { + if (!item.isPersisted) item.persist(itemPersistKey); + else item.persistent?.persistValue(itemPersistKey); + } }); - // Unpersist removed Keys + // Remove removed Items from the Storage removedKeys.forEach((itemKey) => { const item = collection.getItem(itemKey); - const _itemKey = CollectionPersistent.getItemStorageKey(itemKey, _key); - if (!item) return; - if (item.isPersisted) item.persistent?.removePersistedValue(_itemKey); + const itemPersistKey = CollectionPersistent.getItemStorageKey( + itemKey, + _storageItemKey + ); + if (item != null) + if (item.isPersisted) + item.persistent?.removePersistedValue(itemPersistKey); }); } - //========================================================================================================= - // Get Item Storage Key - //========================================================================================================= /** + * Builds valid Item Storage key based on the 'Collection Item Persist Pattern' + * * @internal - * Build Item StorageKey with Collection Persist Pattern - * @param itemKey - Key of Item - * @param collectionKey - Key of Collection + * @param itemKey - Key identifier of Item + * @param collectionKey - Key identifier of Collection */ public static getItemStorageKey( itemKey?: ItemKey, @@ -332,14 +359,12 @@ export class CollectionPersistent< .replace('${itemKey}', itemKey.toString()); } - //========================================================================================================= - // Get Group Storage Key - //========================================================================================================= /** + * Builds valid Item Storage key based on the 'Collection Group Persist Pattern' + * * @internal - * Build Group StorageKey with Collection Persist Pattern - * @param groupKey - Key of Group - * @param collectionKey - Key of Collection + * @param groupKey - Key identifier of Group + * @param collectionKey - Key identifier of Collection */ public static getGroupStorageKey( groupKey?: GroupKey, @@ -349,7 +374,6 @@ export class CollectionPersistent< LogCodeManager.log('1A:02:01'); if (groupKey == null) groupKey = 'unknown'; if (collectionKey == null) collectionKey = 'unknown'; - return this.storageGroupKeyPattern .replace('${collectionKey}', collectionKey.toString()) .replace('${groupKey}', groupKey.toString()); diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 18611453..f3a31c99 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -264,7 +264,7 @@ export class Collection { const itemKey = data[primaryKey]; // Add Item to Collection - const success = this.setData(data, { + const success = this.assignData(data, { patch: config.patch, background: config.background, }); @@ -1153,9 +1153,11 @@ export class Collection { * @param data - Data * @param config - Config */ - public setData(data: DataType, config: SetDataConfigInterface = {}): boolean { + public assignData( + data: DataType, + config: SetDataConfigInterface = {} + ): boolean { const _data = copy(data as any); // Transformed Data to any because of unknown Object (DataType) - const primaryKey = this.config.primaryKey; config = defineConfig(config, { patch: false, background: false, @@ -1166,13 +1168,14 @@ export class Collection { return false; } - if (!Object.prototype.hasOwnProperty.call(_data, primaryKey)) { + // Check if data has valid primaryKey + if (!Object.prototype.hasOwnProperty.call(_data, this.config.primaryKey)) { LogCodeManager.log('1B:02:05', [this._key, this.config.primaryKey]); _data[this.config.primaryKey] = generateId(); } - const itemKey = _data[primaryKey]; - let item = this.getItem(itemKey, { notExisting: true }); + const itemKey = _data[this.config.primaryKey]; + const item = this.getItem(itemKey, { notExisting: true }); const wasPlaceholder = item?.isPlaceholder || false; const createItem = item == null; @@ -1181,21 +1184,44 @@ export class Collection { item?.patch(_data, { background: config.background }); if (!createItem && !config.patch) item?.set(_data, { background: config.background }); - if (createItem) { - // Create and assign Item to Collection - item = new Item(this, _data); - this.data[itemKey] = item; + if (createItem) this.collectItem(new Item(this, _data)); - // Rebuild Groups That include ItemKey after assigning Item to Collection (otherwise it can't find Item) - this.rebuildGroupsThatIncludeItemKey(itemKey, { - background: config.background, - }); + // Increase size of Collection if Item was before a placeholder + if (wasPlaceholder) this.size++; + + return true; + } + + public collectItem( + item: Item, + config: { background?: boolean } = {} + ): this { + const itemKey = item[this.config.primaryKey]; + + // Check if Item has valid primaryKey + if ( + !Object.prototype.hasOwnProperty.call(item._value, this.config.primaryKey) + ) { + LogCodeManager.log('1B:02:05', [this._key, this.config.primaryKey]); + item.patch( + { [this.config.primaryKey]: generateId() }, + { background: true } + ); } - // Increase size of Collection - if (createItem || wasPlaceholder) this.size++; + // Check if Item already exists + if (this.getItem(itemKey) != null) return this; - return true; + this.data[itemKey] = item; + + // Rebuild Groups That include ItemKey after assigning Item to Collection (otherwise it can't find Item) + this.rebuildGroupsThatIncludeItemKey(itemKey, { + background: config.background, + }); + + this.size++; + + return this; } //========================================================================================================= diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index 35843c30..d64f2ad4 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -6,6 +6,10 @@ import { StateRuntimeJobConfigInterface, defineConfig, SelectorKey, + PersistentKey, + isValidObject, + CollectionPersistent, + StatePersistentConfigInterface, } from '../internal'; export class Item extends State< @@ -85,6 +89,64 @@ export class Item extends State< return this; } + //========================================================================================================= + // Persist + //========================================================================================================= + /** + * @public + * Stores Item Value into Agile Storage permanently + * @param config - Config + */ + public persist(config?: ItemPersistConfigInterface): this; + /** + * @public + * Stores Item Value into Agile Storage permanently + * @param key - Key/Name of created Persistent (Note: Key required if Item has no set Key!) + * @param config - Config + */ + public persist( + key?: PersistentKey, + config?: ItemPersistConfigInterface + ): this; + public persist( + keyOrConfig: PersistentKey | ItemPersistConfigInterface = {}, + config: ItemPersistConfigInterface = {} + ): this { + let _config: ItemPersistConfigInterface; + let key: PersistentKey | undefined; + + if (isValidObject(keyOrConfig)) { + _config = keyOrConfig as ItemPersistConfigInterface; + key = this._key; + } else { + _config = config || {}; + key = keyOrConfig as PersistentKey; + } + + _config = defineConfig(_config, { + loadValue: true, + followCollectionPattern: false, + storageKeys: [], + defaultStorageKey: null, + }); + + // Create storageItemKey based on Collection Name + if (_config.followCollectionPersistKeyPattern) { + key = CollectionPersistent.getItemStorageKey( + key || this._key, + this.collection()._key + ); + } + + super.persist(key, { + loadValue: _config.loadValue, + storageKeys: _config.storageKeys, + defaultStorageKey: _config.defaultStorageKey, + }); + + return this; + } + //========================================================================================================= // Add Rebuild Group That Include ItemKey SideEffect //========================================================================================================= @@ -109,3 +171,11 @@ export class Item extends State< export interface ItemConfigInterface { isPlaceholder?: boolean; } + +/** + * @param useCollectionPattern - If Item storageKey follows the Collection Item StorageKey Pattern + */ +export interface ItemPersistConfigInterface + extends StatePersistentConfigInterface { + followCollectionPersistKeyPattern?: boolean; +} diff --git a/packages/core/src/computed/computed.tracker.ts b/packages/core/src/computed/computed.tracker.ts index 48d8b87e..4274f324 100644 --- a/packages/core/src/computed/computed.tracker.ts +++ b/packages/core/src/computed/computed.tracker.ts @@ -4,40 +4,33 @@ export class ComputedTracker { static isTracking = false; static trackedObservers: Set = new Set(); - //========================================================================================================= - // Track - //========================================================================================================= /** * @internal - * Starts tracking Observers + * Activates Computed Tracker to globally track used Observers. */ static track(): void { this.isTracking = true; } - //========================================================================================================= - // Tracked - //========================================================================================================= /** * @internal - * Adds passed Observer to tracked Observers, if ComputedTracker is currently tracking + * Tracks the passed Observer and caches it + * when the Computed Tracker is actively tracking. * @param observer - Observer */ static tracked(observer: Observer) { if (this.isTracking) this.trackedObservers.add(observer); } - //========================================================================================================= - // Get Tracked Observers - //========================================================================================================= /** * @internal - * Returns tracked Observers and stops tracking anymore Observers + * Returns the last tracked Observers + * and stops the Computed Tracker from tracking any more Observers. */ static getTrackedObservers(): Array { const trackedObservers = Array.from(this.trackedObservers); - // Reset tracking + // Reset Computed Tracker this.isTracking = false; this.trackedObservers = new Set(); diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index cb70ad7e..90c24127 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -22,13 +22,13 @@ export class Computed extends State< public hardCodedDeps: Array = []; // Only hardCoded dependencies the Computed depends /** - * A extension of the State Class that computes its value based on a compute function. + * An extension of the State Class that computes its value based on a compute function. * * The computed value will be cached to avoid unnecessary recomputes - * and only recomputed when one of its direct dependencies changed. + * and is only recomputed when one of its direct dependencies changes. * * Direct dependencies can be States and Collections. - * Meaning if a dependent State value changes, the computed value will be recomputed. + * So if for example a dependent State value changes, the computed value will be recomputed. * * [Learn more..](https://agile-ts.org/docs/core/computed/) * @@ -63,7 +63,7 @@ export class Computed extends State< } /** - * Forces a recomputation of the cached value based on the compute function. + * Forces a recomputation of the cached value with the compute function. * * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#recompute) * @@ -82,18 +82,18 @@ export class Computed extends State< } /** - * Assigns new function to the Computed to compute the computed value. + * Assigns a new function to the Computed Class to compute its value. * * The dependencies of the new compute function are automatically detected - * and accordingly updated in the Computed Class. + * and accordingly updated. * - * A initial computation is automatically performed with the new function - * to update the outdated cached value of the Computed. + * An initial computation is automatically performed with the new function + * to change the obsolete cached value. * * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#updatecomputefunction) * * @public - * @param computeFunction - New function to compute the computed value. + * @param computeFunction - New function to compute the value of the Computed Class. * @param deps - Hard coded dependencies on which the Computed Class depends. * @param config - Configuration object */ @@ -125,7 +125,8 @@ export class Computed extends State< } /** - * Computes the value of the Computed Class and autodetects used dependencies in the compute function. + * Computes the new value of the Computed Class + * and autodetects used dependencies in the compute function. * * @internal * @param config - Configuration object @@ -183,21 +184,25 @@ export class Computed extends State< } /** - * @param computedDeps - Hard coded dependencies of compute function + * @property computedDeps - Hard coded dependencies on which the Computed Class depends. + * | Default = [] | */ export interface ComputedConfigInterface extends StateConfigInterface { computedDeps?: Array; } /** - * @param autodetect - If dependencies get autodetected + * @property autodetect - Whether dependencies used in the compute function should be detected automatically. + * | Default = true | */ export interface ComputeConfigInterface { autodetect?: boolean; } /** - * @param overwriteDeps - If old hardCoded deps get overwritten + * @param overwriteDeps - Whether the old hard coded dependencies + * should be overwritten with the new hard coded dependencies or merged in. + * | Default = true | */ export interface UpdateComputeFunctionConfigInterface extends RecomputeConfigInterface { diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 90155b2c..d40b9cbe 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -407,6 +407,9 @@ export class State { defaultStorageKey: null, }); + // Check if State was already persisted + if (this.persistent != null && this.isPersisted) return this; + // Create persistent -> Persist Value this.persistent = new StatePersistent(this, { instantiate: _config.loadValue, diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index 9c540b6b..32160c75 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -118,7 +118,6 @@ export class StateObserver extends Observer { */ public perform(job: StateRuntimeJob) { const state = job.observer.state(); - const previousValue = copy(state.getPublicValue()); // Assign new State Values state.previousStateValue = copy(state._value); @@ -142,7 +141,7 @@ export class StateObserver extends Observer { // The Observer value is at some point the public Value because Integrations like React are using it as return value. // For example 'useAgile()' returns the Observer.value and not the State.value. job.observer.value = copy(state.getPublicValue()); - job.observer.previousValue = previousValue; + job.observer.previousValue = copy(state.previousStateValue); } //========================================================================================================= diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index 23d2f961..04f1169a 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -107,7 +107,7 @@ export class StatePersistent extends Persistent { if (!loadedValue) return false; // Assign loaded Value to State - this.state().set(loadedValue, { storage: false }); + this.state().set(loadedValue, { storage: false, overwrite: true }); // Persist State, so that the Storage Value updates dynamically if the State updates await this.persistValue(_storageItemKey); @@ -198,18 +198,18 @@ export class StatePersistent extends Persistent { * @internal * Rebuilds Storage depending on the State Value (Saves current State Value into the Storage) * @param state - State that holds the new Value - * @param storageKey - StorageKey where value should be persisted + * @param storageItemKey - StorageKey where value should be persisted * @param config - Config */ public rebuildStorageSideEffect( state: State, - storageKey: PersistentKey, + storageItemKey: PersistentKey, config: { [key: string]: any } = {} ) { if (config.storage !== undefined && !config.storage) return; this.agileInstance().storages.set( - storageKey, + storageItemKey, this.state().getPersistableValue(), this.storageKeys ); diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 482e74e2..32219fba 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -459,7 +459,7 @@ describe('Collection Tests', () => { dummyGroup2: dummyGroup2, }; - collection.setData = jest.fn(); + collection.assignData = jest.fn(); collection.createSelector = jest.fn(); collection.createGroup = jest.fn(); @@ -469,11 +469,11 @@ describe('Collection Tests', () => { }); it('should add Data to Collection and to default Group (default config)', () => { - collection.setData = jest.fn(() => true); + collection.assignData = jest.fn(() => true); collection.collect({ id: '1', name: 'frank' }); - expect(collection.setData).toHaveBeenCalledWith( + expect(collection.assignData).toHaveBeenCalledWith( { id: '1', name: 'frank', @@ -496,7 +496,7 @@ describe('Collection Tests', () => { }); it('should add Data to Collection and to default Group (specific config)', () => { - collection.setData = jest.fn(() => true); + collection.assignData = jest.fn(() => true); collection.collect({ id: '1', name: 'frank' }, [], { background: true, @@ -504,7 +504,7 @@ describe('Collection Tests', () => { patch: true, }); - expect(collection.setData).toHaveBeenCalledWith( + expect(collection.assignData).toHaveBeenCalledWith( { id: '1', name: 'frank', @@ -527,7 +527,7 @@ describe('Collection Tests', () => { }); it('should add Data to Collection and to passed Groups + default Group (default config)', () => { - collection.setData = jest.fn(() => true); + collection.assignData = jest.fn(() => true); collection.collect( [ @@ -537,7 +537,7 @@ describe('Collection Tests', () => { ['dummyGroup1', 'dummyGroup2'] ); - expect(collection.setData).toHaveBeenCalledWith( + expect(collection.assignData).toHaveBeenCalledWith( { id: '1', name: 'frank', @@ -547,7 +547,7 @@ describe('Collection Tests', () => { background: false, } ); - expect(collection.setData).toHaveBeenCalledWith( + expect(collection.assignData).toHaveBeenCalledWith( { id: '2', name: 'hans', @@ -588,14 +588,14 @@ describe('Collection Tests', () => { }); it("should call setData and shouldn't add Items to passed Groups if setData failed (default config)", () => { - collection.setData = jest.fn(() => false); + collection.assignData = jest.fn(() => false); collection.collect({ id: '1', name: 'frank' }, [ 'dummyGroup1', 'dummyGroup2', ]); - expect(collection.setData).toHaveBeenCalledWith( + expect(collection.assignData).toHaveBeenCalledWith( { id: '1', name: 'frank', @@ -617,7 +617,7 @@ describe('Collection Tests', () => { it("should add Data to Collection and create Groups that doesn't exist yet (default config)", () => { const notExistingGroup = new Group(collection); notExistingGroup.add = jest.fn(); - collection.setData = jest.fn(() => true); + collection.assignData = jest.fn(() => true); collection.createGroup = jest.fn(function (groupKey) { //@ts-ignore this.groups[groupKey] = notExistingGroup; @@ -626,7 +626,7 @@ describe('Collection Tests', () => { collection.collect({ id: '1', name: 'frank' }, 'notExistingGroup'); - expect(collection.setData).toHaveBeenCalledWith( + expect(collection.assignData).toHaveBeenCalledWith( { id: '1', name: 'frank', @@ -653,7 +653,7 @@ describe('Collection Tests', () => { }); it('should create Selector for each Item (config.select)', () => { - collection.setData = jest.fn(() => true); + collection.assignData = jest.fn(() => true); collection.collect( [ @@ -669,7 +669,7 @@ describe('Collection Tests', () => { }); it("should call 'forEachItem' for each Item (default config)", () => { - collection.setData = jest.fn(() => true); + collection.assignData = jest.fn(() => true); const forEachItemMock = jest.fn(); collection.collect( @@ -2368,7 +2368,10 @@ describe('Collection Tests', () => { }); it('should create new Item out of valid Data, rebuild Groups and increase size (default config)', () => { - const response = collection.setData({ id: 'dummyItem2', name: 'Hans' }); + const response = collection.assignData({ + id: 'dummyItem2', + name: 'Hans', + }); expect(response).toBeTruthy(); expect(collection.data).toHaveProperty('dummyItem1'); @@ -2385,7 +2388,7 @@ describe('Collection Tests', () => { }); it("shouldn't create new Item if passed Data is no valid Object", () => { - const response = collection.setData('noObject' as any); + const response = collection.assignData('noObject' as any); expect(response).toBeFalsy(); expect(collection.size).toBe(1); @@ -2397,7 +2400,7 @@ describe('Collection Tests', () => { it('should create new Item with random primaryKey if passed Data has no primaryKey', () => { jest.spyOn(Utils, 'generateId').mockReturnValueOnce('randomDummyId'); - const response = collection.setData({ name: 'Frank' } as any); + const response = collection.assignData({ name: 'Frank' } as any); expect(response).toBeTruthy(); expect(response).toBeTruthy(); @@ -2418,7 +2421,7 @@ describe('Collection Tests', () => { }); it("should update Item with valid Data, shouldn't rebuild Groups and shouldn't increase size (default config)", () => { - const response = collection.setData({ + const response = collection.assignData({ id: 'dummyItem1', name: 'Dieter', }); @@ -2440,7 +2443,7 @@ describe('Collection Tests', () => { }); it("should update Item with valid Data, shouldn't rebuild Groups and shouldn't increase size (config.background = true)", () => { - const response = collection.setData( + const response = collection.assignData( { id: 'dummyItem1', name: 'Dieter', @@ -2465,7 +2468,7 @@ describe('Collection Tests', () => { }); it("should update Item with valid Data, shouldn't rebuild Groups and shouldn't increase size (config.patch = true, background: true)", () => { - const response = collection.setData( + const response = collection.assignData( { id: 'dummyItem1', name: 'Dieter', @@ -2493,7 +2496,7 @@ describe('Collection Tests', () => { dummyItem1.isPlaceholder = true; collection.size = 0; - const response = collection.setData({ + const response = collection.assignData({ id: 'dummyItem1', name: 'Dieter', }); From 924bfb764e7a40cd9a4cfaad6ec56bcd72a6310c Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 29 May 2021 14:44:59 +0200 Subject: [PATCH 03/63] added storage logs --- .../src/collection/collection.persistent.ts | 3 +- packages/core/src/logCodeManager.ts | 5 ++- packages/core/src/state/state.persistent.ts | 2 +- packages/core/src/storages/storage.ts | 44 ++++++++++++++++--- 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index d9dd78c5..49a5207f 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -1,4 +1,5 @@ import { + Agile, Collection, CollectionKey, CreatePersistentConfigInterface, @@ -144,7 +145,7 @@ export class CollectionPersistent< // Persist already existing Item if (item != null) { item.persist(itemPersistKey); - return true; + continue; } // Create temporary placeholder Item in which the Item value will be loaded diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 1f098fbd..895f2bc6 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -21,7 +21,7 @@ const logCodeMessages = { '10:02:00': 'Be careful when binding multiple Agile Instances globally in one application!', - // Storage + // Storages '11:02:00': "The 'Local Storage' is not available in your current environment." + "To use the '.persist()' functionality, please provide a custom Storage!", @@ -49,6 +49,9 @@ const logCodeMessages = { "The Storage with the key/name '${1}' doesn't exists!`", // Storage + '13:01:00': "GET value at key '${1}' from Storage '${0}'.", + '13:01:01': "SET value at key '${1}' in Storage '${0}'.", + '13:01:02': "REMOVE value at key '${1}' from Storage '${0}'.", '13:02:00': 'Using normalGet() in a async-based Storage might result in an unexpected return value. ' + 'Instead of a resolved value a Promise is returned!', diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index 04f1169a..7d75d9c4 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -104,7 +104,7 @@ export class StatePersistent extends Persistent { _storageItemKey, this.config.defaultStorageKey as any ); - if (!loadedValue) return false; + if (loadedValue == null) return false; // Assign loaded Value to State this.state().set(loadedValue, { storage: false, overwrite: true }); diff --git a/packages/core/src/storages/storage.ts b/packages/core/src/storages/storage.ts index ed8005be..f8751e4a 100644 --- a/packages/core/src/storages/storage.ts +++ b/packages/core/src/storages/storage.ts @@ -74,13 +74,21 @@ export class Storage { * @param key - Key of Storage property */ public normalGet(key: StorageItemKey): GetTpe | undefined { - if (!this.ready || !this.methods.get) return; + if (!this.ready || !this.methods.get) return undefined; if (isAsyncFunction(this.methods.get)) LogCodeManager.log('13:02:00'); // Get Value const res = this.methods.get(this.getStorageKey(key)); - if (isJsonString(res)) return JSON.parse(res); - return res; + const _res = isJsonString(res) ? JSON.parse(res) : res; + + Agile.logger.if + .tag(['storage']) + .info( + LogCodeManager.getLog('13:01:00', [this.key, this.getStorageKey(key)]), + _res + ); + + return _res; } //========================================================================================================= @@ -103,8 +111,19 @@ export class Storage { this.methods ?.get(this.getStorageKey(key)) .then((res: any) => { - if (isJsonString(res)) resolve(JSON.parse(res)); - resolve(res); + const _res = isJsonString(res) ? JSON.parse(res) : res; + + Agile.logger.if + .tag(['storage']) + .info( + LogCodeManager.getLog('13:01:00', [ + this.key, + this.getStorageKey(key), + ]), + _res + ); + + resolve(_res); }) .catch(reject); }); @@ -121,6 +140,14 @@ export class Storage { */ public set(key: StorageItemKey, value: any): void { if (!this.ready || !this.methods.set) return; + + Agile.logger.if + .tag(['storage']) + .info( + LogCodeManager.getLog('13:01:01', [this.key, this.getStorageKey(key)]), + value + ); + this.methods.set(this.getStorageKey(key), JSON.stringify(value)); } @@ -134,6 +161,13 @@ export class Storage { */ public remove(key: StorageItemKey): void { if (!this.ready || !this.methods.remove) return; + + Agile.logger.if + .tag(['storage']) + .info( + LogCodeManager.getLog('13:01:02', [this.key, this.getStorageKey(key)]) + ); + this.methods.remove(this.getStorageKey(key)); } From 0ca8245ae55be69b8b4e8eb9e08e14f53151da83 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 29 May 2021 15:37:59 +0200 Subject: [PATCH 04/63] outsourced storage sideEffects --- .../src/collection/collection.persistent.ts | 55 ++++++++++++------- packages/core/src/state/state.persistent.ts | 32 ++++++++--- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 49a5207f..94309d0f 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -1,5 +1,4 @@ import { - Agile, Collection, CollectionKey, CreatePersistentConfigInterface, @@ -99,14 +98,14 @@ export class CollectionPersistent< * Loads the values of the Collection Instances from the corresponding Storage. * * @internal - * @param storageItemKey - Prefix Key of persisted Collection Instances | default = Persistent.key | + * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | * @return Whether the loading was successful. */ public async loadPersistedValue( storageItemKey?: PersistentKey ): Promise { if (!this.ready) return false; - const _storageItemKey = storageItemKey || this._key; + const _storageItemKey = storageItemKey ?? this._key; // Check if Collection is already persisted (indicated by the persistence of true at _storageItemKey) const isPersisted = await this.agileInstance().storages.get( @@ -177,9 +176,8 @@ export class CollectionPersistent< }; const success = await loadValuesIntoCollection(); - // 'Persist' Collection to setup side effects - // that automatically update the corresponding Storage value if the Collection updates - if (success) await this.persistValue(_storageItemKey); + // Setup Side Effects to keep the Storage value in sync with the State value + if (success) this.setupSideEffects(storageItemKey); return success; } @@ -189,12 +187,12 @@ export class CollectionPersistent< * and sets up side effects that dynamically update the storage value when the Collection changes. * * @internal - * @param storageItemKey - Prefix Key of persisted Collection Instances | default = Persistent.key | + * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | * @return Whether the persisting and the setting up of the side effects was successful. */ public async persistValue(storageItemKey?: PersistentKey): Promise { if (!this.ready) return false; - const _storageItemKey = storageItemKey || this._key; + const _storageItemKey = storageItemKey ?? this._key; const defaultGroup = this.collection().getDefaultGroup(); if (!defaultGroup) return false; const defaultGroupPersistKey = CollectionPersistent.getGroupStorageKey( @@ -206,15 +204,7 @@ export class CollectionPersistent< this.agileInstance().storages.set(_storageItemKey, true, this.storageKeys); // Persist default Group - if (!defaultGroup.isPersisted) defaultGroup.persist(defaultGroupPersistKey); - - // Add side effect to default Group - // that adds or removes Items from the Storage depending on the Group value - defaultGroup.addSideEffect( - CollectionPersistent.defaultGroupSideEffectKey, - () => this.rebuildStorageSideEffect(defaultGroup, _storageItemKey), - { weight: 0 } - ); + defaultGroup.persist(defaultGroupPersistKey); // Persist Items found in the default Group's value for (const itemKey of defaultGroup._value) { @@ -223,26 +213,49 @@ export class CollectionPersistent< itemKey, _storageItemKey ); - if (!item?.isPersisted) item?.persist(itemPersistKey); + item?.persist(itemPersistKey); } + // Setup Side Effects to keep the Storage value in sync with the Collection value + this.setupSideEffects(_storageItemKey); + this.isPersisted = true; return true; } + /** + * Sets up side effects to keep the Storage value in sync with the Collection value + * + * @internal + * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | + */ + public setupSideEffects(storageItemKey?: PersistentKey): void { + const _storageItemKey = storageItemKey ?? this._key; + const defaultGroup = this.collection().getDefaultGroup(); + if (!defaultGroup) return; + + // Add side effect to default Group + // that adds or removes Items from the Storage based on the Group value + defaultGroup.addSideEffect( + CollectionPersistent.defaultGroupSideEffectKey, + () => this.rebuildStorageSideEffect(defaultGroup, _storageItemKey), + { weight: 0 } + ); + } + /** * Removes Collection from the corresponding Storage. * -> Collection is no longer persisted * * @internal - * @param storageItemKey - Prefix Key of persisted Collection Instances | default = Persistent.key | + * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | * @return Whether the removing was successful. */ public async removePersistedValue( storageItemKey?: PersistentKey ): Promise { if (!this.ready) return false; - const _storageItemKey = storageItemKey || this._key; + const _storageItemKey = storageItemKey ?? this._key; const defaultGroup = this.collection().getDefaultGroup(); if (!defaultGroup) return false; const defaultGroupPersistKey = CollectionPersistent.getGroupStorageKey( @@ -293,7 +306,7 @@ export class CollectionPersistent< * * @internal * @param group - Group - * @param storageItemKey - Prefix Key of persisted Collection Instances | default = Persistent.key | + * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | */ public rebuildStorageSideEffect( group: Group, diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index 7d75d9c4..8736869a 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -109,8 +109,8 @@ export class StatePersistent extends Persistent { // Assign loaded Value to State this.state().set(loadedValue, { storage: false, overwrite: true }); - // Persist State, so that the Storage Value updates dynamically if the State updates - await this.persistValue(_storageItemKey); + // Setup Side Effects to keep the Storage value in sync with the State value + this.setupSideEffects(storageItemKey); return true; } @@ -128,7 +128,27 @@ export class StatePersistent extends Persistent { if (!this.ready) return false; const _storageItemKey = storageItemKey ?? this._key; - // Add SideEffect to State, that updates the saved State Value depending on the current State Value + // Setup side effects to keep the Storage value in sync with the State value + this.setupSideEffects(storageItemKey); + + // Initial rebuild Storage for persisting State value in the corresponding Storage + this.rebuildStorageSideEffect(this.state(), _storageItemKey); + + this.isPersisted = true; + return true; + } + + /** + * Sets up side effects to keep the Storage value in sync with the State value. + * + * @internal + * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | + */ + public setupSideEffects(storageItemKey?: PersistentKey) { + const _storageItemKey = storageItemKey ?? this._key; + + // Add side effect to State + // that updates the Storage value based on the State value this.state().addSideEffect( StatePersistent.storeValueSideEffectKey, (instance, config) => { @@ -136,12 +156,6 @@ export class StatePersistent extends Persistent { }, { weight: 0 } ); - - // Initial rebuild Storage for saving State Value in the Storage - this.rebuildStorageSideEffect(this.state(), _storageItemKey); - - this.isPersisted = true; - return true; } //========================================================================================================= From 4610a0ce39a82414f7051fc5ad3f887e47d4b132 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 29 May 2021 16:39:09 +0200 Subject: [PATCH 05/63] fixed typos --- .../src/collection/collection.persistent.ts | 61 ++++++++++++------- packages/core/src/collection/index.ts | 9 ++- .../collection.persistent.integration.test.ts | 4 +- .../collection/collection.persistent.test.ts | 30 ++++----- 4 files changed, 64 insertions(+), 40 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 94309d0f..13a0fd1c 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -119,16 +119,18 @@ export class CollectionPersistent< // Helper function to load persisted values into the Collection const loadValuesIntoCollection = async () => { const defaultGroup = this.collection().getDefaultGroup(); - if (!defaultGroup) return false; - const defaultGroupPersistKey = CollectionPersistent.getGroupStorageKey( + if (defaultGroup == null) return false; + const defaultGroupStorageKey = CollectionPersistent.getGroupStorageKey( defaultGroup._key, _storageItemKey ); // Persist default Group and load its value manually to be 100% sure // that it was loaded completely - defaultGroup.persist(defaultGroupPersistKey, { + defaultGroup.persist(defaultGroupStorageKey, { loadValue: false, + defaultStorageKey: this.config.defaultStorageKey || undefined, + storageKeys: this.storageKeys, }); if (defaultGroup.persistent?.ready) await defaultGroup.persistent.initialLoading(); @@ -136,14 +138,17 @@ export class CollectionPersistent< // Persist Items found in the default Group's value for (const itemKey of defaultGroup._value) { const item = this.collection().getItem(itemKey); - const itemPersistKey = CollectionPersistent.getItemStorageKey( + const itemStorageKey = CollectionPersistent.getItemStorageKey( itemKey, _storageItemKey ); // Persist already existing Item if (item != null) { - item.persist(itemPersistKey); + item.persist(itemStorageKey, { + defaultStorageKey: this.config.defaultStorageKey || undefined, + storageKeys: this.storageKeys, + }); continue; } @@ -159,12 +164,14 @@ export class CollectionPersistent< // Persist dummy Item and load its value manually to be 100% sure // that it was loaded completely and exists - dummyItem?.persist(itemPersistKey, { + dummyItem?.persist(itemStorageKey, { loadValue: false, + defaultStorageKey: this.config.defaultStorageKey || undefined, + storageKeys: this.storageKeys, }); if (dummyItem?.persistent?.ready) { const success = await dummyItem.persistent.loadPersistedValue( - itemPersistKey + itemStorageKey ); // If successfully loaded add Item to Collection @@ -194,8 +201,8 @@ export class CollectionPersistent< if (!this.ready) return false; const _storageItemKey = storageItemKey ?? this._key; const defaultGroup = this.collection().getDefaultGroup(); - if (!defaultGroup) return false; - const defaultGroupPersistKey = CollectionPersistent.getGroupStorageKey( + if (defaultGroup == null) return false; + const defaultGroupStorageKey = CollectionPersistent.getGroupStorageKey( defaultGroup._key, _storageItemKey ); @@ -204,16 +211,22 @@ export class CollectionPersistent< this.agileInstance().storages.set(_storageItemKey, true, this.storageKeys); // Persist default Group - defaultGroup.persist(defaultGroupPersistKey); + defaultGroup.persist(defaultGroupStorageKey, { + defaultStorageKey: this.config.defaultStorageKey || undefined, + storageKeys: this.storageKeys, + }); // Persist Items found in the default Group's value for (const itemKey of defaultGroup._value) { const item = this.collection().getItem(itemKey); - const itemPersistKey = CollectionPersistent.getItemStorageKey( + const itemStorageKey = CollectionPersistent.getItemStorageKey( itemKey, _storageItemKey ); - item?.persist(itemPersistKey); + item?.persist(itemStorageKey, { + defaultStorageKey: this.config.defaultStorageKey || undefined, + storageKeys: this.storageKeys, + }); } // Setup Side Effects to keep the Storage value in sync with the Collection value @@ -232,7 +245,7 @@ export class CollectionPersistent< public setupSideEffects(storageItemKey?: PersistentKey): void { const _storageItemKey = storageItemKey ?? this._key; const defaultGroup = this.collection().getDefaultGroup(); - if (!defaultGroup) return; + if (defaultGroup == null) return; // Add side effect to default Group // that adds or removes Items from the Storage based on the Group value @@ -258,7 +271,7 @@ export class CollectionPersistent< const _storageItemKey = storageItemKey ?? this._key; const defaultGroup = this.collection().getDefaultGroup(); if (!defaultGroup) return false; - const defaultGroupPersistKey = CollectionPersistent.getGroupStorageKey( + const defaultGroupStorageKey = CollectionPersistent.getGroupStorageKey( defaultGroup._key, _storageItemKey ); @@ -267,7 +280,7 @@ export class CollectionPersistent< this.agileInstance().storages.remove(_storageItemKey, this.storageKeys); // Remove default Group from the Storage - defaultGroup.persistent?.removePersistedValue(defaultGroupPersistKey); + defaultGroup.persistent?.removePersistedValue(defaultGroupStorageKey); defaultGroup.removeSideEffect( CollectionPersistent.defaultGroupSideEffectKey ); @@ -275,11 +288,11 @@ export class CollectionPersistent< // Remove Items found in the default Group's value from the Storage for (const itemKey of defaultGroup._value) { const item = this.collection().getItem(itemKey); - const itemPersistKey = CollectionPersistent.getItemStorageKey( + const itemStorageKey = CollectionPersistent.getItemStorageKey( itemKey, _storageItemKey ); - item?.persistent?.removePersistedValue(itemPersistKey); + item?.persistent?.removePersistedValue(itemStorageKey); } this.isPersisted = false; @@ -330,26 +343,30 @@ export class CollectionPersistent< // Persist newly added Items addedKeys.forEach((itemKey) => { const item = collection.getItem(itemKey); - const itemPersistKey = CollectionPersistent.getItemStorageKey( + const itemStorageKey = CollectionPersistent.getItemStorageKey( itemKey, _storageItemKey ); if (item != null) { - if (!item.isPersisted) item.persist(itemPersistKey); - else item.persistent?.persistValue(itemPersistKey); + if (!item.isPersisted) + item.persist(itemStorageKey, { + defaultStorageKey: this.config.defaultStorageKey || undefined, + storageKeys: this.storageKeys, + }); + else item.persistent?.persistValue(itemStorageKey); } }); // Remove removed Items from the Storage removedKeys.forEach((itemKey) => { const item = collection.getItem(itemKey); - const itemPersistKey = CollectionPersistent.getItemStorageKey( + const itemStorageKey = CollectionPersistent.getItemStorageKey( itemKey, _storageItemKey ); if (item != null) if (item.isPersisted) - item.persistent?.removePersistedValue(itemPersistKey); + item.persistent?.removePersistedValue(itemStorageKey); }); } diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index f3a31c99..38b9b9bb 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -1192,11 +1192,18 @@ export class Collection { return true; } + /** + * Adds passed Item to Collection. + * + * @public + * @param item - Item to be added + * @param config - Configuration object + */ public collectItem( item: Item, config: { background?: boolean } = {} ): this { - const itemKey = item[this.config.primaryKey]; + const itemKey = item._value[this.config.primaryKey]; // Check if Item has valid primaryKey if ( diff --git a/packages/core/tests/integration/collection.persistent.integration.test.ts b/packages/core/tests/integration/collection.persistent.integration.test.ts index 20215777..203fbfd4 100644 --- a/packages/core/tests/integration/collection.persistent.integration.test.ts +++ b/packages/core/tests/integration/collection.persistent.integration.test.ts @@ -1,5 +1,5 @@ import { Agile, Item } from '../../src'; -import mockConsole from 'jest-mock-console'; +import { LogMock } from '../helper/logMock'; describe('Collection Persist Function Tests', () => { const myStorage: any = {}; @@ -34,8 +34,8 @@ describe('Collection Persist Function Tests', () => { } beforeEach(() => { + LogMock.mockLogs(); jest.clearAllMocks(); - mockConsole(['error', 'warn']); }); describe('Collection', () => { diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index c3e4ebb3..bb4b73df 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -289,15 +289,15 @@ describe('CollectionPersistent Tests', () => { .mockReturnValueOnce({ id: '1', name: 'hans' }) .mockReturnValueOnce(undefined) .mockReturnValueOnce({ id: '3', name: 'frank' }); - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); + dummyCollection.getDefaultGroup = jest.fn( + () => dummyDefaultGroup as any + ); const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeTruthy(); - expect(dummyCollection.getGroup).toHaveBeenCalledWith( - dummyCollection.config.defaultGroupKey - ); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); expect(dummyAgile.storages.get).toHaveBeenCalledWith( collectionPersistent._key, @@ -352,13 +352,15 @@ describe('CollectionPersistent Tests', () => { dummyAgile.storages.get = jest .fn() .mockReturnValueOnce(Promise.resolve(undefined)); - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); + dummyCollection.getDefaultGroup = jest.fn( + () => dummyDefaultGroup as any + ); const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeFalsy(); - expect(dummyCollection.getGroup).not.toHaveBeenCalled(); + expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); expect(dummyAgile.storages.get).toHaveBeenCalledWith( collectionPersistent._key, @@ -413,9 +415,7 @@ describe('CollectionPersistent Tests', () => { expect(response).toBeTruthy(); - expect(dummyCollection.getGroup).toHaveBeenCalledWith( - dummyCollection.config.defaultGroupKey - ); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); expect(dummyAgile.storages.get).toHaveBeenCalledWith( 'dummyKey', @@ -461,13 +461,15 @@ describe('CollectionPersistent Tests', () => { dummyAgile.storages.get = jest.fn(() => Promise.resolve(undefined as any) ); - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); + dummyCollection.getDefaultGroup = jest.fn( + () => dummyDefaultGroup as any + ); const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeFalsy(); - expect(dummyCollection.getGroup).not.toHaveBeenCalled(); + expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); expect(dummyAgile.storages.get).not.toHaveBeenCalled(); @@ -488,15 +490,13 @@ describe('CollectionPersistent Tests', () => { dummyAgile.storages.get = jest .fn() .mockReturnValueOnce(Promise.resolve(true)); - dummyCollection.getGroup = jest.fn(() => undefined); + dummyCollection.getDefaultGroup = jest.fn(() => undefined); const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeFalsy(); - expect(dummyCollection.getGroup).toHaveBeenCalledWith( - dummyCollection.config.defaultGroupKey - ); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); expect(dummyAgile.storages.get).toHaveBeenCalledWith( collectionPersistent._key, From f73d6b4a18e5091be5909b3538e85dc429861225 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 29 May 2021 18:40:09 +0200 Subject: [PATCH 06/63] started fixing collection persistent tests --- .../src/collection/collection.persistent.ts | 18 +- packages/core/src/collection/index.ts | 43 +- .../collection/collection.persistent.test.ts | 474 ++++++++++-------- 3 files changed, 312 insertions(+), 223 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 13a0fd1c..21d7c713 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -153,14 +153,7 @@ export class CollectionPersistent< } // Create temporary placeholder Item in which the Item value will be loaded - const dummyItem = new Item( - this.collection(), - { - [this.collection().config.primaryKey]: itemKey, // Setting PrimaryKey of Item to passed itemKey - dummy: 'item', - } as any, - { isPlaceholder: true } - ); + const dummyItem = this.collection().createPlaceholderItem(itemKey); // Persist dummy Item and load its value manually to be 100% sure // that it was loaded completely and exists @@ -170,12 +163,13 @@ export class CollectionPersistent< storageKeys: this.storageKeys, }); if (dummyItem?.persistent?.ready) { - const success = await dummyItem.persistent.loadPersistedValue( + const loadedPersistedValueIntoItem = await dummyItem.persistent.loadPersistedValue( itemStorageKey ); - // If successfully loaded add Item to Collection - if (success) this.collection().collectItem(dummyItem); + // If successfully loaded Item value add Item to Collection + if (loadedPersistedValueIntoItem) + this.collection().collectItem(dummyItem); } } @@ -184,7 +178,7 @@ export class CollectionPersistent< const success = await loadValuesIntoCollection(); // Setup Side Effects to keep the Storage value in sync with the State value - if (success) this.setupSideEffects(storageItemKey); + if (success) this.setupSideEffects(_storageItemKey); return success; } diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 38b9b9bb..55821c8e 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -678,24 +678,41 @@ export class Collection { let item = this.getItem(itemKey, { notExisting: true }); // Create dummy Item to hold reference - if (item == null) { - item = new Item( - this, - { - [this.config.primaryKey]: itemKey, // Setting PrimaryKey of Item to passed itemKey - dummy: 'item', - } as any, - { - isPlaceholder: true, - } - ); - this.data[itemKey] = item; - } + if (item == null) item = this.createPlaceholderItem(itemKey, true); ComputedTracker.tracked(item.observer); return item; } + /** + * Creates a placeholder Item + * that can be used to hold a reference to an Item that doesn't yet exist. + * + * @private + * @param itemKey - Key/Name identifier of the Item to be created + * @param addToCollection - Whether the created Item should be added to the Collection + */ + public createPlaceholderItem( + itemKey: ItemKey, + addToCollection = false + ): Item { + const item = new Item( + this, + { + [this.config.primaryKey]: itemKey, // Setting PrimaryKey of Item to passed itemKey + dummy: 'item', + } as any, + { isPlaceholder: true } + ); + if ( + addToCollection && + !Object.prototype.hasOwnProperty.call(this.data, itemKey) + ) + this.data[itemKey] = item; + + return item; + } + //========================================================================================================= // Get Value by Id //========================================================================================================= diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index bb4b73df..ccdcd974 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -91,7 +91,7 @@ describe('CollectionPersistent Tests', () => { expect(collectionPersistent.onLoad).toBeUndefined(); expect(collectionPersistent.storageKeys).toStrictEqual([]); expect(collectionPersistent.config).toStrictEqual({ - defaultStorageKey: null, // gets set in 'instantiatePersistent' which is mocked + defaultStorageKey: null, // is assigned in 'instantiatePersistent' which is mocked }); }); @@ -152,18 +152,21 @@ describe('CollectionPersistent Tests', () => { name: 'frank', }); dummyItem1.persistent = new StatePersistent(dummyItem1); + dummyItem1.persist = jest.fn(); dummyItem2 = new Item(dummyCollection, { id: '2', name: 'dieter', }); dummyItem2.persistent = new StatePersistent(dummyItem2); + dummyItem2.persist = jest.fn(); dummyItem3 = new Item(dummyCollection, { id: '3', name: 'hans', }); dummyItem3.persistent = new StatePersistent(dummyItem3); + dummyItem3.persist = jest.fn(); dummyItem4WithoutPersistent = new Item(dummyCollection, { id: '4', @@ -263,197 +266,293 @@ describe('CollectionPersistent Tests', () => { describe('loadPersistedValue function tests', () => { let dummyDefaultGroup: Group; + let placeholderItem1: Item; + let placeholderItem2: Item; + let placeholderItem3: Item; beforeEach(() => { collectionPersistent.config.defaultStorageKey = 'test'; - dummyDefaultGroup = new Group(dummyCollection, ['1', '2', '3']); - dummyDefaultGroup.persistent = new StatePersistent(dummyDefaultGroup); - if (dummyDefaultGroup.persistent) - dummyDefaultGroup.persistent.ready = true; - - collectionPersistent.persistValue = jest.fn(); + placeholderItem1 = dummyCollection.createPlaceholderItem('1'); + placeholderItem1.persist = jest.fn(); + placeholderItem2 = dummyCollection.createPlaceholderItem('2'); + placeholderItem2.persist = jest.fn(); + placeholderItem3 = dummyCollection.createPlaceholderItem('3'); + placeholderItem3.persist = jest.fn(); + dummyDefaultGroup = new Group(dummyCollection, ['1', '2', '3'], { + key: 'default', + }); dummyDefaultGroup.persist = jest.fn(); - if (dummyDefaultGroup.persistent) + dummyDefaultGroup.persistent = new StatePersistent(dummyDefaultGroup); + if (dummyDefaultGroup.persistent) { + dummyDefaultGroup.persistent.ready = true; dummyDefaultGroup.persistent.initialLoading = jest.fn(); + } - dummyCollection.collect = jest.fn(); + collectionPersistent.setupSideEffects = jest.fn(); + + dummyCollection.getDefaultGroup = jest.fn( + () => dummyDefaultGroup as any + ); + dummyCollection.collectItem = jest.fn(); }); - it('should load default Group and its Items with the persistentKey and apply it to the Collection if loading was successful', async () => { + it('should load defaultGroup and Items that are already present in Collection and apply the loaded value to the Item (persistentKey)', async () => { collectionPersistent.ready = true; + dummyCollection.data = { + ['3']: dummyItem3, + }; dummyAgile.storages.get = jest .fn() - .mockReturnValueOnce(Promise.resolve(true)) - .mockReturnValueOnce({ id: '1', name: 'hans' }) - .mockReturnValueOnce(undefined) - .mockReturnValueOnce({ id: '3', name: 'frank' }); - dummyCollection.getDefaultGroup = jest.fn( - () => dummyDefaultGroup as any - ); + .mockReturnValueOnce(Promise.resolve(true)); + dummyDefaultGroup._value = ['3']; const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeTruthy(); - - expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( collectionPersistent._key, collectionPersistent.config.defaultStorageKey ); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey( - '1', - collectionPersistent._key - ), - collectionPersistent.config.defaultStorageKey - ); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey( - '2', + + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); + expect(dummyDefaultGroup.persist).toHaveBeenCalledWith( + CollectionPersistent.getGroupStorageKey( + dummyDefaultGroup._key, collectionPersistent._key ), - collectionPersistent.config.defaultStorageKey + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( + expect(dummyDefaultGroup.persistent?.initialLoading).toHaveBeenCalled(); + expect(dummyDefaultGroup.isPersisted).toBeTruthy(); + + expect(dummyItem3.persist).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey( '3', collectionPersistent._key ), - collectionPersistent.config.defaultStorageKey + { + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); - expect(dummyDefaultGroup.persist).toHaveBeenCalledWith({ - loadValue: false, - followCollectionPersistKeyPattern: true, - }); - expect(dummyDefaultGroup.persistent?.initialLoading).toHaveBeenCalled(); - expect(dummyDefaultGroup.isPersisted).toBeTruthy(); - - expect(dummyCollection.collect).toHaveBeenCalledWith({ - id: '1', - name: 'hans', - }); - expect(dummyCollection.collect).not.toHaveBeenCalledWith(undefined); - expect(dummyCollection.collect).toHaveBeenCalledWith({ - id: '3', - name: 'frank', - }); - - expect(collectionPersistent.persistValue).toHaveBeenCalledWith( + expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( collectionPersistent._key ); }); - it("shouldn't load default Group and its Items with the persistentKey and shouldn't apply it to the Collection if loading wasn't successful", async () => { + it("should load default Group and create persisted Items that aren't present in Collection yet (persistentKey)", async () => { collectionPersistent.ready = true; + dummyCollection.data = {}; dummyAgile.storages.get = jest .fn() - .mockReturnValueOnce(Promise.resolve(undefined)); - dummyCollection.getDefaultGroup = jest.fn( - () => dummyDefaultGroup as any - ); + .mockReturnValueOnce(Promise.resolve(true)); + placeholderItem1.persist = jest.fn(function () { + placeholderItem1.persistent = new StatePersistent(placeholderItem1); + placeholderItem1.persistent.ready = true; + placeholderItem1.persistent.loadPersistedValue = jest + .fn() + .mockReturnValueOnce(true); + return null as any; + }); + placeholderItem2.persist = jest.fn(function () { + placeholderItem2.persistent = new StatePersistent(placeholderItem2); + placeholderItem2.persistent.ready = false; + placeholderItem2.persistent.loadPersistedValue = jest.fn(); + return null as any; + }); + placeholderItem3.persist = jest.fn(function () { + placeholderItem3.persistent = new StatePersistent(placeholderItem3); + placeholderItem3.persistent.ready = true; + placeholderItem3.persistent.loadPersistedValue = jest + .fn() + .mockReturnValueOnce(false); + return null as any; + }); + dummyCollection.createPlaceholderItem = jest + .fn() + .mockReturnValueOnce(placeholderItem1) + .mockReturnValueOnce(placeholderItem2) + .mockReturnValueOnce(placeholderItem3); + dummyDefaultGroup._value = ['1', '2', '3']; const response = await collectionPersistent.loadPersistedValue(); - expect(response).toBeFalsy(); - - expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); - + expect(response).toBeTruthy(); expect(dummyAgile.storages.get).toHaveBeenCalledWith( collectionPersistent._key, collectionPersistent.config.defaultStorageKey ); - expect(dummyAgile.storages.get).not.toHaveBeenCalledWith( + + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); + expect(dummyDefaultGroup.persist).toHaveBeenCalledWith( + CollectionPersistent.getGroupStorageKey( + dummyDefaultGroup._key, + collectionPersistent._key + ), + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } + ); + expect(dummyDefaultGroup.persistent?.initialLoading).toHaveBeenCalled(); + expect(dummyDefaultGroup.isPersisted).toBeTruthy(); + + expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith('1'); + expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith('2'); + expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith('3'); + expect(placeholderItem1.persist).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey( '1', collectionPersistent._key ), - collectionPersistent.config.defaultStorageKey + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); - expect(dummyAgile.storages.get).not.toHaveBeenCalledWith( + expect(placeholderItem2.persist).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey( '2', collectionPersistent._key ), - collectionPersistent.config.defaultStorageKey + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); - expect(dummyAgile.storages.get).not.toHaveBeenCalledWith( + expect(placeholderItem3.persist).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey( '3', collectionPersistent._key ), - collectionPersistent.config.defaultStorageKey + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } + ); + expect(dummyCollection.collectItem).toHaveBeenCalledWith( + placeholderItem1 + ); + expect(dummyCollection.collectItem).not.toHaveBeenCalledWith( + placeholderItem2 + ); // Because Item persistent isn't ready + expect(dummyCollection.collectItem).not.toHaveBeenCalledWith( + placeholderItem3 + ); // Because Item persistent 'leadPersistedValue()' returned false + + expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( + collectionPersistent._key ); - - expect(dummyDefaultGroup.persist).not.toHaveBeenCalled(); - expect( - dummyDefaultGroup.persistent?.initialLoading - ).not.toHaveBeenCalled(); - expect(dummyDefaultGroup.isPersisted).toBeFalsy(); - - expect(dummyCollection.collect).not.toHaveBeenCalled(); - - expect(collectionPersistent.persistValue).not.toHaveBeenCalled(); }); - it('should load default Group and its Items with a specific Key and should apply it to the Collection if loading was successful', async () => { + it("should load defaultGroup and Items that are or aren't already present in Collection and apply the loaded value to the Item (specific key)", async () => { collectionPersistent.ready = true; + dummyCollection.data = { + ['3']: dummyItem3, + }; dummyAgile.storages.get = jest .fn() - .mockReturnValueOnce(Promise.resolve(true)) - .mockReturnValueOnce({ id: '1', name: 'hans' }) - .mockReturnValueOnce(undefined) - .mockReturnValueOnce({ id: '3', name: 'frank' }); - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); + .mockReturnValueOnce(Promise.resolve(true)); + placeholderItem1.persist = jest.fn(function () { + placeholderItem1.persistent = new StatePersistent(placeholderItem1); + placeholderItem1.persistent.ready = true; + placeholderItem1.persistent.loadPersistedValue = jest + .fn() + .mockReturnValueOnce(true); + return null as any; + }); + dummyCollection.createPlaceholderItem = jest + .fn() + .mockReturnValueOnce(placeholderItem1); + dummyDefaultGroup._value = ['1', '3']; const response = await collectionPersistent.loadPersistedValue( 'dummyKey' ); expect(response).toBeTruthy(); - - expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( 'dummyKey', collectionPersistent.config.defaultStorageKey ); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( + + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); + expect(dummyDefaultGroup.persist).toHaveBeenCalledWith( + CollectionPersistent.getGroupStorageKey( + dummyDefaultGroup._key, + 'dummyKey' + ), + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } + ); + expect(dummyDefaultGroup.persistent?.initialLoading).toHaveBeenCalled(); + expect(dummyDefaultGroup.isPersisted).toBeTruthy(); + + expect(dummyItem3.persist).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey('3', 'dummyKey'), + { + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } + ); + + expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith('1'); + expect(placeholderItem1.persist).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey('1', 'dummyKey'), - collectionPersistent.config.defaultStorageKey + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey('2', 'dummyKey'), - collectionPersistent.config.defaultStorageKey + expect(dummyCollection.collectItem).toHaveBeenCalledWith( + placeholderItem1 ); + expect(dummyCollection.collectItem).not.toHaveBeenCalledWith( + placeholderItem3 + ); // Because Item is already present in Collection + + expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( + 'dummyKey' + ); + }); + + it("shouldn't load default Group and its Items if Collection flag isn't persisted", async () => { + collectionPersistent.ready = true; + dummyAgile.storages.get = jest + .fn() + .mockReturnValueOnce(Promise.resolve(undefined)); + + const response = await collectionPersistent.loadPersistedValue(); + + expect(response).toBeFalsy(); expect(dummyAgile.storages.get).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey('3', 'dummyKey'), + collectionPersistent._key, collectionPersistent.config.defaultStorageKey ); - expect(dummyDefaultGroup.persist).toHaveBeenCalledWith({ - loadValue: false, - followCollectionPersistKeyPattern: true, - }); - expect(dummyDefaultGroup.persistent?.initialLoading).toHaveBeenCalled(); - expect(dummyDefaultGroup.isPersisted).toBeTruthy(); + expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); + expect(dummyDefaultGroup.persist).not.toHaveBeenCalled(); - expect(dummyCollection.collect).toHaveBeenCalledWith({ - id: '1', - name: 'hans', - }); - expect(dummyCollection.collect).not.toHaveBeenCalledWith(undefined); - expect(dummyCollection.collect).toHaveBeenCalledWith({ - id: '3', - name: 'frank', - }); + expect(placeholderItem1.persist).not.toHaveBeenCalled(); + expect(placeholderItem2.persist).not.toHaveBeenCalled(); + expect(placeholderItem3.persist).not.toHaveBeenCalled(); - expect(collectionPersistent.persistValue).toHaveBeenCalledWith( - 'dummyKey' - ); + expect(collectionPersistent.setupSideEffects).not.toHaveBeenCalled(); }); it("shouldn't load default Group and its Items if Persistent isn't ready", async () => { @@ -461,27 +560,20 @@ describe('CollectionPersistent Tests', () => { dummyAgile.storages.get = jest.fn(() => Promise.resolve(undefined as any) ); - dummyCollection.getDefaultGroup = jest.fn( - () => dummyDefaultGroup as any - ); const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeFalsy(); - - expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); - expect(dummyAgile.storages.get).not.toHaveBeenCalled(); + expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); expect(dummyDefaultGroup.persist).not.toHaveBeenCalled(); - expect( - dummyDefaultGroup.persistent?.initialLoading - ).not.toHaveBeenCalled(); - expect(dummyDefaultGroup.isPersisted).toBeFalsy(); - expect(dummyCollection.collect).not.toHaveBeenCalled(); + expect(placeholderItem1.persist).not.toHaveBeenCalled(); + expect(placeholderItem2.persist).not.toHaveBeenCalled(); + expect(placeholderItem3.persist).not.toHaveBeenCalled(); - expect(collectionPersistent.persistValue).not.toHaveBeenCalled(); + expect(collectionPersistent.setupSideEffects).not.toHaveBeenCalled(); }); it("shouldn't load default Group and its Items if Collection has no defaultGroup", async () => { @@ -495,44 +587,19 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeFalsy(); - - expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( collectionPersistent._key, collectionPersistent.config.defaultStorageKey ); - expect(dummyAgile.storages.get).not.toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey( - '1', - collectionPersistent._key - ), - collectionPersistent.config.defaultStorageKey - ); - expect(dummyAgile.storages.get).not.toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey( - '2', - collectionPersistent._key - ), - collectionPersistent.config.defaultStorageKey - ); - expect(dummyAgile.storages.get).not.toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey( - '3', - collectionPersistent._key - ), - collectionPersistent.config.defaultStorageKey - ); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); expect(dummyDefaultGroup.persist).not.toHaveBeenCalled(); - expect( - dummyDefaultGroup.persistent?.initialLoading - ).not.toHaveBeenCalled(); - expect(dummyDefaultGroup.isPersisted).toBeFalsy(); - expect(dummyCollection.collect).not.toHaveBeenCalled(); + expect(placeholderItem1.persist).not.toHaveBeenCalled(); + expect(placeholderItem2.persist).not.toHaveBeenCalled(); + expect(placeholderItem3.persist).not.toHaveBeenCalled(); - expect(collectionPersistent.persistValue).not.toHaveBeenCalled(); + expect(collectionPersistent.setupSideEffects).not.toHaveBeenCalled(); }); }); @@ -543,26 +610,30 @@ describe('CollectionPersistent Tests', () => { collectionPersistent.storageKeys = ['test1', 'test2']; collectionPersistent.isPersisted = undefined as any; - dummyDefaultGroup = new Group(dummyCollection, ['1', '2', '3']); dummyCollection.data = { ['1']: dummyItem1, ['3']: dummyItem3, }; + dummyDefaultGroup = new Group(dummyCollection, ['1', '2', '3'], { + key: 'default', + }); dummyDefaultGroup.persist = jest.fn(); - jest.spyOn(dummyDefaultGroup, 'addSideEffect'); dummyItem1.persist = jest.fn(); dummyItem3.persist = jest.fn(); - dummyCollection.collect = jest.fn(); + collectionPersistent.setupSideEffects = jest.fn(); + + dummyCollection.getDefaultGroup = jest.fn( + () => dummyDefaultGroup as any + ); dummyAgile.storages.set = jest.fn(); }); - it('should persist defaultGroup and its Items with persistentKey', async () => { + it('should persist defaultGroup and its Items (persistentKey)', async () => { collectionPersistent.ready = true; - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); const response = await collectionPersistent.persistValue(); @@ -574,39 +645,45 @@ describe('CollectionPersistent Tests', () => { collectionPersistent.storageKeys ); - expect(dummyCollection.getGroup).toHaveBeenCalledWith( - dummyCollection.config.defaultGroupKey - ); - expect(dummyDefaultGroup.persist).toHaveBeenCalledWith({ - followCollectionPersistKeyPattern: true, - }); - expect( - dummyDefaultGroup.addSideEffect - ).toHaveBeenCalledWith( - CollectionPersistent.defaultGroupSideEffectKey, - expect.any(Function), - { weight: 0 } + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); + expect(dummyDefaultGroup.persist).toHaveBeenCalledWith( + CollectionPersistent.getGroupStorageKey( + dummyDefaultGroup._key, + collectionPersistent._key + ), + { + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); expect(dummyItem1.persist).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey( dummyItem1._key, collectionPersistent._key - ) + ), + { + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); expect(dummyItem3.persist).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey( dummyItem3._key, collectionPersistent._key - ) + ), + { + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); + expect(collectionPersistent.setupSideEffects).toHaveBeenCalled(); expect(collectionPersistent.isPersisted).toBeTruthy(); }); - it('should persist defaultGroup and its Items with specific Key', async () => { + it('should persist defaultGroup and its Items (specific key)', async () => { collectionPersistent.ready = true; - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); const response = await collectionPersistent.persistValue('dummyKey'); @@ -618,72 +695,73 @@ describe('CollectionPersistent Tests', () => { collectionPersistent.storageKeys ); - expect(dummyCollection.getGroup).toHaveBeenCalledWith( - dummyCollection.config.defaultGroupKey - ); - expect(dummyDefaultGroup.persist).toHaveBeenCalledWith({ - followCollectionPersistKeyPattern: true, - }); - expect( - dummyDefaultGroup.addSideEffect - ).toHaveBeenCalledWith( - CollectionPersistent.defaultGroupSideEffectKey, - expect.any(Function), - { weight: 0 } + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); + expect(dummyDefaultGroup.persist).toHaveBeenCalledWith( + CollectionPersistent.getGroupStorageKey( + dummyDefaultGroup._key, + 'dummyKey' + ), + { + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); expect(dummyItem1.persist).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey(dummyItem1._key, 'dummyKey') + CollectionPersistent.getItemStorageKey(dummyItem1._key, 'dummyKey'), + { + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); expect(dummyItem3.persist).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey(dummyItem3._key, 'dummyKey') + CollectionPersistent.getItemStorageKey(dummyItem3._key, 'dummyKey'), + { + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); + expect(collectionPersistent.setupSideEffects).toHaveBeenCalled(); expect(collectionPersistent.isPersisted).toBeTruthy(); }); it("shouldn't persist defaultGroup and its Items if Persistent isn't ready", async () => { collectionPersistent.ready = false; - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); const response = await collectionPersistent.persistValue('dummyKey'); expect(response).toBeFalsy(); - expect(dummyAgile.storages.set).not.toHaveBeenCalled(); - - expect(dummyCollection.getGroup).not.toHaveBeenCalled(); + expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); expect(dummyDefaultGroup.persist).not.toHaveBeenCalled(); - expect(dummyDefaultGroup.addSideEffect).not.toHaveBeenCalled(); expect(dummyItem1.persist).not.toHaveBeenCalled(); expect(dummyItem3.persist).not.toHaveBeenCalled(); + expect(collectionPersistent.setupSideEffects).not.toHaveBeenCalled(); expect(collectionPersistent.isPersisted).toBeUndefined(); }); it("shouldn't persist defaultGroup and its Items if Collection has no defaultGroup", async () => { collectionPersistent.ready = true; - dummyCollection.getGroup = jest.fn(() => undefined as any); + dummyCollection.getDefaultGroup = jest.fn(() => undefined as any); const response = await collectionPersistent.persistValue(); expect(response).toBeFalsy(); - expect(dummyAgile.storages.set).not.toHaveBeenCalled(); - - expect(dummyCollection.getGroup).toHaveBeenCalledWith( - dummyCollection.config.defaultGroupKey - ); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); expect(dummyDefaultGroup.persist).not.toHaveBeenCalled(); - expect(dummyDefaultGroup.addSideEffect).not.toHaveBeenCalled(); expect(dummyItem1.persist).not.toHaveBeenCalled(); expect(dummyItem3.persist).not.toHaveBeenCalled(); + expect(collectionPersistent.setupSideEffects).not.toHaveBeenCalled(); expect(collectionPersistent.isPersisted).toBeUndefined(); }); + // TODO move since the added sideEffect isn't here anymore!! describe('test added sideEffect called CollectionPersistent.defaultGroupSideEffectKey', () => { beforeEach(() => { collectionPersistent.rebuildStorageSideEffect = jest.fn(); From a4e2d3d2cea9262fe558803c314c9041b537b804 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 29 May 2021 20:04:21 +0200 Subject: [PATCH 07/63] added collection persistent setupSideEffect tests --- packages/core/src/collection/index.ts | 3 + packages/core/src/state/index.ts | 2 +- .../collection/collection.persistent.test.ts | 128 +++++++++++++----- 3 files changed, 96 insertions(+), 37 deletions(-) diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 55821c8e..290cc46d 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -813,6 +813,9 @@ export class Collection { defaultStorageKey: null, }); + // Check if Collection is already persisted + if (this.persistent != null && this.isPersisted) return this; + // Create persistent -> Persist Value this.persistent = new CollectionPersistent(this, { instantiate: _config.loadValue, diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index d40b9cbe..c33afdf3 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -407,7 +407,7 @@ export class State { defaultStorageKey: null, }); - // Check if State was already persisted + // Check if State is already persisted if (this.persistent != null && this.isPersisted) return this; // Create persistent -> Persist Value diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index ccdcd974..46c2bee3 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -760,16 +760,46 @@ describe('CollectionPersistent Tests', () => { expect(collectionPersistent.setupSideEffects).not.toHaveBeenCalled(); expect(collectionPersistent.isPersisted).toBeUndefined(); }); + }); + + describe('setupSideEffect function tests', () => { + let dummyDefaultGroup: Group; + + beforeEach(() => { + dummyDefaultGroup = new Group(dummyCollection, ['1', '2', '3'], { + key: 'default', + }); + jest.spyOn(dummyDefaultGroup, 'addSideEffect'); + + collectionPersistent.rebuildStorageSideEffect = jest.fn(); + + dummyCollection.getDefaultGroup = jest.fn( + () => dummyDefaultGroup as any + ); + }); + + it('should add rebuild Storage side effect to default Group', () => { + collectionPersistent.setupSideEffects(); + + expect( + dummyDefaultGroup.addSideEffect + ).toHaveBeenCalledWith( + CollectionPersistent.defaultGroupSideEffectKey, + expect.any(Function), + { weight: 0 } + ); + }); - // TODO move since the added sideEffect isn't here anymore!! describe('test added sideEffect called CollectionPersistent.defaultGroupSideEffectKey', () => { beforeEach(() => { collectionPersistent.rebuildStorageSideEffect = jest.fn(); }); - it('should call rebuildStorageSideEffect with persistentKey', async () => { + it('should call rebuildStorageSideEffect (persistentKey)', async () => { collectionPersistent.ready = true; - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); + dummyCollection.getDefaultGroup = jest.fn( + () => dummyDefaultGroup as any + ); await collectionPersistent.persistValue(); @@ -782,9 +812,11 @@ describe('CollectionPersistent Tests', () => { ).toHaveBeenCalledWith(dummyDefaultGroup, collectionPersistent._key); }); - it('should call rebuildStorageSideEffect with specific Key', async () => { + it('should call rebuildStorageSideEffect (specific key)', async () => { collectionPersistent.ready = true; - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); + dummyCollection.getDefaultGroup = jest.fn( + () => dummyDefaultGroup as any + ); await collectionPersistent.persistValue('dummyKey'); @@ -806,16 +838,21 @@ describe('CollectionPersistent Tests', () => { collectionPersistent.storageKeys = ['test1', 'test2']; collectionPersistent.isPersisted = undefined as any; - dummyDefaultGroup = new Group(dummyCollection, ['1', '2', '3']); - dummyDefaultGroup.persistent = new StatePersistent(dummyDefaultGroup); dummyCollection.data = { ['1']: dummyItem1, ['3']: dummyItem3, }; + dummyDefaultGroup = new Group(dummyCollection, ['1', '2', '3']); + dummyDefaultGroup.persistent = new StatePersistent(dummyDefaultGroup); + dummyDefaultGroup.removeSideEffect = jest.fn(); + if (dummyDefaultGroup.persistent) dummyDefaultGroup.persistent.removePersistedValue = jest.fn(); - dummyDefaultGroup.removeSideEffect = jest.fn(); + + dummyCollection.getDefaultGroup = jest.fn( + () => dummyDefaultGroup as any + ); if (dummyItem1.persistent) dummyItem1.persistent.removePersistedValue = jest.fn(); @@ -825,77 +862,99 @@ describe('CollectionPersistent Tests', () => { dummyAgile.storages.remove = jest.fn(); }); - it('should remove persisted defaultGroup and its Items from Storage with persistentKey', async () => { + it('should remove persisted default Group and its Items from Storage (persistentKey)', async () => { collectionPersistent.ready = true; - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); const response = await collectionPersistent.removePersistedValue(); expect(response).toBeTruthy(); - expect(dummyAgile.storages.remove).toHaveBeenCalledWith( collectionPersistent._key, collectionPersistent.storageKeys ); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); - expect(dummyCollection.getGroup).toHaveBeenCalledWith( - dummyCollection.config.defaultGroupKey - ); expect( dummyDefaultGroup.persistent?.removePersistedValue - ).toHaveBeenCalled(); + ).toHaveBeenCalledWith( + CollectionPersistent.getGroupStorageKey( + dummyDefaultGroup._key, + collectionPersistent._key + ) + ); expect(dummyDefaultGroup.removeSideEffect).toHaveBeenCalledWith( CollectionPersistent.defaultGroupSideEffectKey ); - expect(dummyItem1.persistent?.removePersistedValue).toHaveBeenCalled(); - expect(dummyItem3.persistent?.removePersistedValue).toHaveBeenCalled(); + expect( + dummyItem1.persistent?.removePersistedValue + ).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey( + dummyItem1._key, + collectionPersistent._key + ) + ); + expect( + dummyItem3.persistent?.removePersistedValue + ).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey( + dummyItem3._key, + collectionPersistent._key + ) + ); expect(collectionPersistent.isPersisted).toBeFalsy(); }); - it('should remove persisted defaultGroup and its Items from Storage with specific Key', async () => { + it('should remove persisted default Group and its Items from Storage (specific key)', async () => { collectionPersistent.ready = true; - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); const response = await collectionPersistent.removePersistedValue( 'dummyKey' ); expect(response).toBeTruthy(); - expect(dummyAgile.storages.remove).toHaveBeenCalledWith( 'dummyKey', collectionPersistent.storageKeys ); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); - expect(dummyCollection.getGroup).toHaveBeenCalledWith( - dummyCollection.config.defaultGroupKey - ); expect( dummyDefaultGroup.persistent?.removePersistedValue - ).toHaveBeenCalled(); + ).toHaveBeenCalledWith( + CollectionPersistent.getGroupStorageKey( + dummyDefaultGroup._key, + 'dummyKey' + ) + ); expect(dummyDefaultGroup.removeSideEffect).toHaveBeenCalledWith( CollectionPersistent.defaultGroupSideEffectKey ); - expect(dummyItem1.persistent?.removePersistedValue).toHaveBeenCalled(); - expect(dummyItem3.persistent?.removePersistedValue).toHaveBeenCalled(); + expect( + dummyItem1.persistent?.removePersistedValue + ).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey(dummyItem1._key, 'dummyKey') + ); + expect( + dummyItem3.persistent?.removePersistedValue + ).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey(dummyItem3._key, 'dummyKey') + ); expect(collectionPersistent.isPersisted).toBeFalsy(); }); - it("shouldn't remove persisted defaultGroup and its Items from Storage if Persistent isn't ready", async () => { + it("shouldn't remove persisted default Group and its Items from Storage if Persistent isn't ready", async () => { collectionPersistent.ready = false; - dummyCollection.getGroup = jest.fn(() => dummyDefaultGroup as any); const response = await collectionPersistent.removePersistedValue(); expect(response).toBeFalsy(); - expect(dummyAgile.storages.remove).not.toHaveBeenCalled(); + expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); - expect(dummyCollection.getGroup).not.toHaveBeenCalled(); expect( dummyDefaultGroup.persistent?.removePersistedValue ).not.toHaveBeenCalled(); @@ -911,19 +970,16 @@ describe('CollectionPersistent Tests', () => { expect(collectionPersistent.isPersisted).toBeUndefined(); }); - it("shouldn't remove persisted defaultGroup and its Items from Storage if Collection has no default Group", async () => { + it("shouldn't remove persisted default Group and its Items from Storage if Collection has no default Group", async () => { collectionPersistent.ready = true; - dummyCollection.getGroup = jest.fn(() => undefined as any); + dummyCollection.getDefaultGroup = jest.fn(() => undefined as any); const response = await collectionPersistent.removePersistedValue(); expect(response).toBeFalsy(); - expect(dummyAgile.storages.remove).not.toHaveBeenCalled(); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); - expect(dummyCollection.getGroup).toHaveBeenCalledWith( - dummyCollection.config.defaultGroupKey - ); expect( dummyDefaultGroup.persistent?.removePersistedValue ).not.toHaveBeenCalled(); From 32d2426593f8d0ba55f57229b3d7c53ee5b29947 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 30 May 2021 07:13:49 +0200 Subject: [PATCH 08/63] fixed typos --- .../src/collection/collection.persistent.ts | 63 +++++++------ .../core/src/computed/computed.tracker.ts | 18 +++- packages/core/src/computed/index.ts | 10 +- .../collection/collection.persistent.test.ts | 91 +++++++++++++------ 4 files changed, 117 insertions(+), 65 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 21d7c713..e2b1f98c 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -6,7 +6,6 @@ import { defineConfig, Group, GroupKey, - Item, ItemKey, LogCodeManager, Persistent, @@ -65,7 +64,7 @@ export class CollectionPersistent< // Assign new key to Persistent if (value === this._key) return; - this._key = value || Persistent.placeHolderKey; + this._key = value ?? Persistent.placeHolderKey; const isValid = this.validatePersistent(); @@ -83,10 +82,11 @@ export class CollectionPersistent< } /** - * @internal * Loads the persisted value into the Collection * or persists the Collection value in the corresponding Storage. - * This depends on whether the Collection has been persisted before. + * This behaviour depends on whether the Collection has been persisted before. + * + * @internal */ public async initialLoading() { super.initialLoading().then(() => { @@ -95,7 +95,8 @@ export class CollectionPersistent< } /** - * Loads the values of the Collection Instances from the corresponding Storage. + * Loads Collection Instances (like Items or Groups) from the corresponding Storage + * and sets up side effects that dynamically update the Storage value when the Collection (Instances) changes. * * @internal * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | @@ -143,33 +144,34 @@ export class CollectionPersistent< _storageItemKey ); - // Persist already existing Item + // Persist and therefore load already present Item if (item != null) { item.persist(itemStorageKey, { defaultStorageKey: this.config.defaultStorageKey || undefined, storageKeys: this.storageKeys, }); - continue; } - - // Create temporary placeholder Item in which the Item value will be loaded - const dummyItem = this.collection().createPlaceholderItem(itemKey); - - // Persist dummy Item and load its value manually to be 100% sure - // that it was loaded completely and exists - dummyItem?.persist(itemStorageKey, { - loadValue: false, - defaultStorageKey: this.config.defaultStorageKey || undefined, - storageKeys: this.storageKeys, - }); - if (dummyItem?.persistent?.ready) { - const loadedPersistedValueIntoItem = await dummyItem.persistent.loadPersistedValue( - itemStorageKey - ); - - // If successfully loaded Item value add Item to Collection - if (loadedPersistedValueIntoItem) - this.collection().collectItem(dummyItem); + // Persist and therefore load not present Item + else { + // Create temporary placeholder Item in which the Item value will be loaded + const dummyItem = this.collection().createPlaceholderItem(itemKey); + + // Persist dummy Item and load its value manually to be 100% sure + // that it was loaded completely and exists at all + dummyItem?.persist(itemStorageKey, { + loadValue: false, + defaultStorageKey: this.config.defaultStorageKey || undefined, + storageKeys: this.storageKeys, + }); + if (dummyItem?.persistent?.ready) { + const loadedPersistedValueIntoItem = await dummyItem.persistent.loadPersistedValue( + itemStorageKey + ); + + // If successfully loaded Item value, add Item to Collection + if (loadedPersistedValueIntoItem) + this.collection().collectItem(dummyItem); + } } } @@ -177,15 +179,16 @@ export class CollectionPersistent< }; const success = await loadValuesIntoCollection(); - // Setup Side Effects to keep the Storage value in sync with the State value + // Setup Side Effects to keep the Storage value in sync with the Collection (Instances) value if (success) this.setupSideEffects(_storageItemKey); return success; } + // TODO STOPPED HERE (in looking at code) /** - * Persists Collection in corresponding Storage if not already done - * and sets up side effects that dynamically update the storage value when the Collection changes. + * Persists Collection Instances (like Items or Groups) in the corresponding Storage + * and sets up side effects that dynamically update the Storage value when the Collection (Instances) changes. * * @internal * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | @@ -223,7 +226,7 @@ export class CollectionPersistent< }); } - // Setup Side Effects to keep the Storage value in sync with the Collection value + // Setup Side Effects to keep the Storage value in sync with the Collection (Instances) value this.setupSideEffects(_storageItemKey); this.isPersisted = true; diff --git a/packages/core/src/computed/computed.tracker.ts b/packages/core/src/computed/computed.tracker.ts index 4274f324..e3254f90 100644 --- a/packages/core/src/computed/computed.tracker.ts +++ b/packages/core/src/computed/computed.tracker.ts @@ -5,17 +5,28 @@ export class ComputedTracker { static trackedObservers: Set = new Set(); /** + * Helper Class for automatic tracking used Observers (dependencies) in a compute function. + * * @internal + */ + constructor() { + // empty + } + + /** * Activates Computed Tracker to globally track used Observers. + * + * @internal */ static track(): void { this.isTracking = true; } /** - * @internal * Tracks the passed Observer and caches it * when the Computed Tracker is actively tracking. + * + * @internal * @param observer - Observer */ static tracked(observer: Observer) { @@ -23,9 +34,10 @@ export class ComputedTracker { } /** - * @internal - * Returns the last tracked Observers + * Returns the latest tracked Observers * and stops the Computed Tracker from tracking any more Observers. + * + * @internal */ static getTrackedObservers(): Array { const trackedObservers = Array.from(this.trackedObservers); diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index 90c24127..1c0e6bdd 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -28,7 +28,7 @@ export class Computed extends State< * and is only recomputed when one of its direct dependencies changes. * * Direct dependencies can be States and Collections. - * So if for example a dependent State value changes, the computed value will be recomputed. + * So when for example a dependent State value changes, the computed value will be recomputed. * * [Learn more..](https://agile-ts.org/docs/core/computed/) * @@ -82,12 +82,12 @@ export class Computed extends State< } /** - * Assigns a new function to the Computed Class to compute its value. + * Assigns a new function to the Computed Class for computing its value. * * The dependencies of the new compute function are automatically detected * and accordingly updated. * - * An initial computation is automatically performed with the new function + * An initial computation is performed with the new function * to change the obsolete cached value. * * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#updatecomputefunction) @@ -148,7 +148,7 @@ export class Computed extends State< this.hardCodedDeps.concat(foundDeps).forEach((observer) => { newDeps.push(observer); - // Make this Observer depend on the foundDep Observer + // Make this Observer depend on the found dep Observers observer.depend(this.observer); }); @@ -201,7 +201,7 @@ export interface ComputeConfigInterface { /** * @param overwriteDeps - Whether the old hard coded dependencies - * should be overwritten with the new hard coded dependencies or merged in. + * should be entirely overwritten with the new hard coded dependencies or merged in. * | Default = true | */ export interface UpdateComputeFunctionConfigInterface diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index 46c2bee3..0f138a35 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -32,43 +32,42 @@ describe('CollectionPersistent Tests', () => { jest.spyOn(CollectionPersistent.prototype, 'initialLoading'); }); - it("should create CollectionPersistent and shouldn't call initialLoading if Persistent isn't ready (default config)", () => { - // Overwrite instantiatePersistent once to not call it and set ready property + it('should create CollectionPersistent and should call initialLoading if Persistent is ready (default config)', () => { + // Overwrite instantiatePersistent once to not call it jest .spyOn(CollectionPersistent.prototype, 'instantiatePersistent') .mockImplementationOnce(function () { // @ts-ignore - this.ready = false; + this.ready = true; }); const collectionPersistent = new CollectionPersistent(dummyCollection); expect(collectionPersistent).toBeInstanceOf(CollectionPersistent); - expect(collectionPersistent.collection()).toBe(dummyCollection); expect(collectionPersistent.instantiatePersistent).toHaveBeenCalledWith({ key: undefined, storageKeys: [], defaultStorageKey: null, }); - expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); + expect(collectionPersistent.initialLoading).toHaveBeenCalled(); expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); - expect(collectionPersistent.ready).toBeFalsy(); + expect(collectionPersistent.ready).toBeTruthy(); expect(collectionPersistent.isPersisted).toBeFalsy(); expect(collectionPersistent.onLoad).toBeUndefined(); expect(collectionPersistent.storageKeys).toStrictEqual([]); expect(collectionPersistent.config).toStrictEqual({ - defaultStorageKey: null, + defaultStorageKey: null, // is assigned in 'instantiatePersistent' which is mocked }); }); - it("should create CollectionPersistent and shouldn't call initialLoading if Persistent isn't ready (specific config)", () => { + it('should create CollectionPersistent and should call initialLoading if Persistent is ready (specific config)', () => { // Overwrite instantiatePersistent once to not call it and set ready property jest .spyOn(CollectionPersistent.prototype, 'instantiatePersistent') .mockImplementationOnce(function () { // @ts-ignore - this.ready = false; + this.ready = true; }); const collectionPersistent = new CollectionPersistent(dummyCollection, { @@ -83,10 +82,10 @@ describe('CollectionPersistent Tests', () => { storageKeys: ['test1', 'test2'], defaultStorageKey: 'test2', }); - expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); + expect(collectionPersistent.initialLoading).toHaveBeenCalled(); expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); - expect(collectionPersistent.ready).toBeFalsy(); + expect(collectionPersistent.ready).toBeTruthy(); expect(collectionPersistent.isPersisted).toBeFalsy(); expect(collectionPersistent.onLoad).toBeUndefined(); expect(collectionPersistent.storageKeys).toStrictEqual([]); @@ -95,18 +94,34 @@ describe('CollectionPersistent Tests', () => { }); }); - it('should create CollectionPersistent and should call initialLoading if Persistent is ready (default config)', () => { - // Overwrite instantiatePersistent once to not call it + it("should create CollectionPersistent and shouldn't call initialLoading if Persistent isn't ready", () => { + // Overwrite instantiatePersistent once to not call it and set ready property jest .spyOn(CollectionPersistent.prototype, 'instantiatePersistent') .mockImplementationOnce(function () { // @ts-ignore - this.ready = true; + this.ready = false; }); const collectionPersistent = new CollectionPersistent(dummyCollection); - expect(collectionPersistent.initialLoading).toHaveBeenCalled(); + expect(collectionPersistent).toBeInstanceOf(CollectionPersistent); + expect(collectionPersistent.collection()).toBe(dummyCollection); + expect(collectionPersistent.instantiatePersistent).toHaveBeenCalledWith({ + key: undefined, + storageKeys: [], + defaultStorageKey: null, + }); + expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); + + expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); + expect(collectionPersistent.ready).toBeFalsy(); + expect(collectionPersistent.isPersisted).toBeFalsy(); + expect(collectionPersistent.onLoad).toBeUndefined(); + expect(collectionPersistent.storageKeys).toStrictEqual([]); + expect(collectionPersistent.config).toStrictEqual({ + defaultStorageKey: null, + }); }); it("should create CollectionPersistent and shouldn't call initialLoading if Persistent is ready (config.instantiate = false)", () => { @@ -122,7 +137,23 @@ describe('CollectionPersistent Tests', () => { instantiate: false, }); + expect(collectionPersistent).toBeInstanceOf(CollectionPersistent); + expect(collectionPersistent.collection()).toBe(dummyCollection); + expect(collectionPersistent.instantiatePersistent).toHaveBeenCalledWith({ + key: undefined, + storageKeys: [], + defaultStorageKey: null, + }); expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); + + expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); + expect(collectionPersistent.ready).toBeTruthy(); + expect(collectionPersistent.isPersisted).toBeFalsy(); + expect(collectionPersistent.onLoad).toBeUndefined(); + expect(collectionPersistent.storageKeys).toStrictEqual([]); + expect(collectionPersistent.config).toStrictEqual({ + defaultStorageKey: null, + }); }); describe('CollectionPersistent Function Tests', () => { @@ -179,17 +210,18 @@ describe('CollectionPersistent Tests', () => { collectionPersistent.removePersistedValue = jest.fn(); collectionPersistent.persistValue = jest.fn(); collectionPersistent.initialLoading = jest.fn(); - jest.spyOn(collectionPersistent, 'validatePersistent'); }); it('should update key with valid key in ready Persistent', async () => { collectionPersistent.ready = true; collectionPersistent._key = 'dummyKey'; + jest + .spyOn(collectionPersistent, 'validatePersistent') + .mockReturnValueOnce(true); await collectionPersistent.setKey('newKey'); expect(collectionPersistent._key).toBe('newKey'); - expect(collectionPersistent.ready).toBeTruthy(); expect(collectionPersistent.validatePersistent).toHaveBeenCalled(); expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); expect(collectionPersistent.persistValue).toHaveBeenCalledWith( @@ -203,13 +235,13 @@ describe('CollectionPersistent Tests', () => { it('should update key with not valid key in ready Persistent', async () => { collectionPersistent.ready = true; collectionPersistent._key = 'dummyKey'; + jest + .spyOn(collectionPersistent, 'validatePersistent') + .mockReturnValueOnce(false); await collectionPersistent.setKey(); - expect(collectionPersistent._key).toBe( - CollectionPersistent.placeHolderKey - ); - expect(collectionPersistent.ready).toBeFalsy(); + expect(collectionPersistent._key).toBe(Persistent.placeHolderKey); expect(collectionPersistent.validatePersistent).toHaveBeenCalled(); expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); expect(collectionPersistent.persistValue).not.toHaveBeenCalled(); @@ -220,11 +252,14 @@ describe('CollectionPersistent Tests', () => { it('should update key with valid key in not ready Persistent', async () => { collectionPersistent.ready = false; + collectionPersistent._key = 'dummyKey'; + jest + .spyOn(collectionPersistent, 'validatePersistent') + .mockReturnValueOnce(true); await collectionPersistent.setKey('newKey'); expect(collectionPersistent._key).toBe('newKey'); - expect(collectionPersistent.ready).toBeTruthy(); expect(collectionPersistent.validatePersistent).toHaveBeenCalled(); expect(collectionPersistent.initialLoading).toHaveBeenCalled(); expect(collectionPersistent.persistValue).not.toHaveBeenCalled(); @@ -235,13 +270,14 @@ describe('CollectionPersistent Tests', () => { it('should update key with not valid key in not ready Persistent', async () => { collectionPersistent.ready = false; + collectionPersistent._key = 'dummyKey'; + jest + .spyOn(collectionPersistent, 'validatePersistent') + .mockReturnValueOnce(false); await collectionPersistent.setKey(); - expect(collectionPersistent._key).toBe( - CollectionPersistent.placeHolderKey - ); - expect(collectionPersistent.ready).toBeFalsy(); + expect(collectionPersistent._key).toBe(Persistent.placeHolderKey); expect(collectionPersistent.validatePersistent).toHaveBeenCalled(); expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); expect(collectionPersistent.persistValue).not.toHaveBeenCalled(); @@ -256,7 +292,7 @@ describe('CollectionPersistent Tests', () => { jest.spyOn(Persistent.prototype, 'initialLoading'); }); - it('should initialLoad and set isPersisted in Collection to true', async () => { + it('should call initialLoad in parent and set Collection.isPersisted to true', async () => { await collectionPersistent.initialLoading(); expect(Persistent.prototype.initialLoading).toHaveBeenCalled(); @@ -264,6 +300,7 @@ describe('CollectionPersistent Tests', () => { }); }); + // TODO STOPPED HERE (in tests) describe('loadPersistedValue function tests', () => { let dummyDefaultGroup: Group; let placeholderItem1: Item; From 3227b6a14a86a24acb73614f5e18e87fe1ff8355 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 30 May 2021 12:16:40 +0200 Subject: [PATCH 09/63] fixed collection persist tests --- .../src/collection/collection.persistent.ts | 67 ++-- packages/core/src/collection/index.ts | 3 + packages/core/src/state/state.persistent.ts | 4 +- .../collection/collection.persistent.test.ts | 305 ++++++++---------- 4 files changed, 179 insertions(+), 200 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index e2b1f98c..33f4ae11 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -99,7 +99,7 @@ export class CollectionPersistent< * and sets up side effects that dynamically update the Storage value when the Collection (Instances) changes. * * @internal - * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | + * @param storageItemKey - Prefix key of persisted Collection Instances. | default = Persistent.key | * @return Whether the loading was successful. */ public async loadPersistedValue( @@ -184,14 +184,13 @@ export class CollectionPersistent< return success; } - // TODO STOPPED HERE (in looking at code) /** * Persists Collection Instances (like Items or Groups) in the corresponding Storage * and sets up side effects that dynamically update the Storage value when the Collection (Instances) changes. * * @internal - * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | + * @param storageItemKey - Prefix key of persisted Collection Instances. | default = Persistent.key | * @return Whether the persisting and the setting up of the side effects was successful. */ public async persistValue(storageItemKey?: PersistentKey): Promise { @@ -234,10 +233,10 @@ export class CollectionPersistent< } /** - * Sets up side effects to keep the Storage value in sync with the Collection value + * Sets up side effects to keep the Storage value in sync with the Collection (Instances) value. * * @internal - * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | + * @param storageItemKey - Prefix key of persisted Collection Instances. | default = Persistent.key | */ public setupSideEffects(storageItemKey?: PersistentKey): void { const _storageItemKey = storageItemKey ?? this._key; @@ -245,10 +244,10 @@ export class CollectionPersistent< if (defaultGroup == null) return; // Add side effect to default Group - // that adds or removes Items from the Storage based on the Group value - defaultGroup.addSideEffect( + // that adds and removes Items from the Storage based on the Group value + defaultGroup.addSideEffect( CollectionPersistent.defaultGroupSideEffectKey, - () => this.rebuildStorageSideEffect(defaultGroup, _storageItemKey), + (instance) => this.rebuildStorageSideEffect(instance, _storageItemKey), { weight: 0 } ); } @@ -258,7 +257,7 @@ export class CollectionPersistent< * -> Collection is no longer persisted * * @internal - * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | + * @param storageItemKey - Prefix key of persisted Collection Instances. | default = Persistent.key | * @return Whether the removing was successful. */ public async removePersistedValue( @@ -273,7 +272,7 @@ export class CollectionPersistent< _storageItemKey ); - // Remove Collection is persisted indicator flag + // Remove Collection is persisted indicator flag from Storage this.agileInstance().storages.remove(_storageItemKey, this.storageKeys); // Remove default Group from the Storage @@ -297,14 +296,14 @@ export class CollectionPersistent< } /** - * Formats given key so that it can be used as a valid Storage key. - * If no formatable key is given, an attempt is made to use the Collection key as Storage key. - * If this also fails, undefined is returned. + * Formats given key so that it can be used as a valid Storage key and returns it. + * If no formatable key (undefined/null) is given, + * an attempt is made to use the Collection key. * * @internal * @param key - Key to be formatted */ - public formatKey(key?: StorageKey): StorageKey | undefined { + public formatKey(key: StorageKey | undefined | null): StorageKey | undefined { if (key == null && this.collection()._key) return this.collection()._key; if (key == null) return; if (this.collection()._key == null) this.collection()._key = key; @@ -312,11 +311,11 @@ export class CollectionPersistent< } /** - * Rebuilds Storage depending on Group + * Adds and removes Items from the Storage based on the Group value. * * @internal - * @param group - Group - * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | + * @param group - Group whose Items should be dynamically added and removed from the Storage. + * @param storageItemKey - Prefix key of persisted Collection Instances. | default = Persistent.key | */ public rebuildStorageSideEffect( group: Group, @@ -326,10 +325,10 @@ export class CollectionPersistent< const _storageItemKey = storageItemKey || collection.persistent?._key; // Return if no Item got added or removed - // because then the Item performs the Storage update itself + // because then the changed Item performs the Storage update itself if (group.previousStateValue.length === group._value.length) return; - // Extract Item keys that got removed or added to the Group + // Extract Item keys that got added or removed from the Group const addedKeys = group._value.filter( (key) => !group.previousStateValue.includes(key) ); @@ -344,14 +343,11 @@ export class CollectionPersistent< itemKey, _storageItemKey ); - if (item != null) { - if (!item.isPersisted) - item.persist(itemStorageKey, { - defaultStorageKey: this.config.defaultStorageKey || undefined, - storageKeys: this.storageKeys, - }); - else item.persistent?.persistValue(itemStorageKey); - } + if (item != null && !item.isPersisted) + item.persist(itemStorageKey, { + defaultStorageKey: this.config.defaultStorageKey || undefined, + storageKeys: this.storageKeys, + }); }); // Remove removed Items from the Storage @@ -361,22 +357,21 @@ export class CollectionPersistent< itemKey, _storageItemKey ); - if (item != null) - if (item.isPersisted) - item.persistent?.removePersistedValue(itemStorageKey); + if (item != null && item.isPersisted) + item.persistent?.removePersistedValue(itemStorageKey); }); } /** - * Builds valid Item Storage key based on the 'Collection Item Persist Pattern' + * Builds valid Item Storage key based on the 'Collection Item Persist Pattern'. * * @internal * @param itemKey - Key identifier of Item * @param collectionKey - Key identifier of Collection */ public static getItemStorageKey( - itemKey?: ItemKey, - collectionKey?: CollectionKey + itemKey: ItemKey | undefined | null, + collectionKey: CollectionKey | undefined | null ): string { if (itemKey == null || collectionKey == null) LogCodeManager.log('1A:02:00'); @@ -388,15 +383,15 @@ export class CollectionPersistent< } /** - * Builds valid Item Storage key based on the 'Collection Group Persist Pattern' + * Builds valid Item Storage key based on the 'Collection Group Persist Pattern'. * * @internal * @param groupKey - Key identifier of Group * @param collectionKey - Key identifier of Collection */ public static getGroupStorageKey( - groupKey?: GroupKey, - collectionKey?: CollectionKey + groupKey: GroupKey | undefined | null, + collectionKey: CollectionKey | undefined | null ): string { if (groupKey == null || collectionKey == null) LogCodeManager.log('1A:02:01'); diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 290cc46d..e67536c5 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -1225,6 +1225,9 @@ export class Collection { ): this { const itemKey = item._value[this.config.primaryKey]; + // TODO add to Groups at least default Group + // and implement it to collect method + // Check if Item has valid primaryKey if ( !Object.prototype.hasOwnProperty.call(item._value, this.config.primaryKey) diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index 8736869a..da15578c 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -191,7 +191,9 @@ export class StatePersistent extends Persistent { * Formats Storage Key * @param key - Key that gets formatted */ - public formatKey(key?: PersistentKey): PersistentKey | undefined { + public formatKey( + key: PersistentKey | undefined | null + ): PersistentKey | undefined { const state = this.state(); // Get key from State diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index 0f138a35..b59dd9ae 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -300,7 +300,6 @@ describe('CollectionPersistent Tests', () => { }); }); - // TODO STOPPED HERE (in tests) describe('loadPersistedValue function tests', () => { let dummyDefaultGroup: Group; let placeholderItem1: Item; @@ -333,9 +332,11 @@ describe('CollectionPersistent Tests', () => { () => dummyDefaultGroup as any ); dummyCollection.collectItem = jest.fn(); + + dummyAgile.storages.get = jest.fn(); }); - it('should load defaultGroup and Items that are already present in Collection and apply the loaded value to the Item (persistentKey)', async () => { + it('should load default Group and apply persisted value to Items that are already present in Collection (persistentKey)', async () => { collectionPersistent.ready = true; dummyCollection.data = { ['3']: dummyItem3, @@ -384,7 +385,7 @@ describe('CollectionPersistent Tests', () => { ); }); - it("should load default Group and create persisted Items that aren't present in Collection yet (persistentKey)", async () => { + it("should load default Group and create/add persisted Items that aren't present in Collection yet (persistentKey)", async () => { collectionPersistent.ready = true; dummyCollection.data = {}; dummyAgile.storages.get = jest @@ -486,87 +487,99 @@ describe('CollectionPersistent Tests', () => { ); // Because Item persistent isn't ready expect(dummyCollection.collectItem).not.toHaveBeenCalledWith( placeholderItem3 - ); // Because Item persistent 'leadPersistedValue()' returned false + ); // Because Item persistent 'leadPersistedValue()' returned false -> Item properly doesn't exist in Storage expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( collectionPersistent._key ); }); - it("should load defaultGroup and Items that are or aren't already present in Collection and apply the loaded value to the Item (specific key)", async () => { - collectionPersistent.ready = true; - dummyCollection.data = { - ['3']: dummyItem3, - }; - dummyAgile.storages.get = jest - .fn() - .mockReturnValueOnce(Promise.resolve(true)); - placeholderItem1.persist = jest.fn(function () { - placeholderItem1.persistent = new StatePersistent(placeholderItem1); - placeholderItem1.persistent.ready = true; - placeholderItem1.persistent.loadPersistedValue = jest + it( + 'should load default Group, ' + + "create/add persisted Items that aren't present in Collection yet " + + 'and apply persisted value to Items that are already present in Collection (specific key)', + async () => { + collectionPersistent.ready = true; + dummyCollection.data = { + ['3']: dummyItem3, + }; + dummyAgile.storages.get = jest .fn() - .mockReturnValueOnce(true); - return null as any; - }); - dummyCollection.createPlaceholderItem = jest - .fn() - .mockReturnValueOnce(placeholderItem1); - dummyDefaultGroup._value = ['1', '3']; - - const response = await collectionPersistent.loadPersistedValue( - 'dummyKey' - ); - - expect(response).toBeTruthy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( - 'dummyKey', - collectionPersistent.config.defaultStorageKey - ); + .mockReturnValueOnce(Promise.resolve(true)); + placeholderItem1.persist = jest.fn(function () { + placeholderItem1.persistent = new StatePersistent(placeholderItem1); + placeholderItem1.persistent.ready = true; + placeholderItem1.persistent.loadPersistedValue = jest + .fn() + .mockReturnValueOnce(true); + return null as any; + }); + dummyCollection.createPlaceholderItem = jest + .fn() + .mockReturnValueOnce(placeholderItem1); + dummyDefaultGroup._value = ['1', '3']; - expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); - expect(dummyDefaultGroup.persist).toHaveBeenCalledWith( - CollectionPersistent.getGroupStorageKey( - dummyDefaultGroup._key, + const response = await collectionPersistent.loadPersistedValue( 'dummyKey' - ), - { - loadValue: false, - defaultStorageKey: collectionPersistent.config.defaultStorageKey, - storageKeys: collectionPersistent.storageKeys, - } - ); - expect(dummyDefaultGroup.persistent?.initialLoading).toHaveBeenCalled(); - expect(dummyDefaultGroup.isPersisted).toBeTruthy(); + ); - expect(dummyItem3.persist).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey('3', 'dummyKey'), - { - defaultStorageKey: collectionPersistent.config.defaultStorageKey, - storageKeys: collectionPersistent.storageKeys, - } - ); + expect(response).toBeTruthy(); + expect(dummyAgile.storages.get).toHaveBeenCalledWith( + 'dummyKey', + collectionPersistent.config.defaultStorageKey + ); - expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith('1'); - expect(placeholderItem1.persist).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey('1', 'dummyKey'), - { - loadValue: false, - defaultStorageKey: collectionPersistent.config.defaultStorageKey, - storageKeys: collectionPersistent.storageKeys, - } - ); - expect(dummyCollection.collectItem).toHaveBeenCalledWith( - placeholderItem1 - ); - expect(dummyCollection.collectItem).not.toHaveBeenCalledWith( - placeholderItem3 - ); // Because Item is already present in Collection + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); + expect(dummyDefaultGroup.persist).toHaveBeenCalledWith( + CollectionPersistent.getGroupStorageKey( + dummyDefaultGroup._key, + 'dummyKey' + ), + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } + ); + expect( + dummyDefaultGroup.persistent?.initialLoading + ).toHaveBeenCalled(); + expect(dummyDefaultGroup.isPersisted).toBeTruthy(); + + expect(dummyItem3.persist).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey('3', 'dummyKey'), + { + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } + ); - expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( - 'dummyKey' - ); - }); + expect( + dummyCollection.createPlaceholderItem + ).not.toHaveBeenCalledWith('3'); // Because Item 3 is already present in Collection + expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith( + '1' + ); + expect(placeholderItem1.persist).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey('1', 'dummyKey'), + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } + ); + expect(dummyCollection.collectItem).toHaveBeenCalledWith( + placeholderItem1 + ); + expect(dummyCollection.collectItem).not.toHaveBeenCalledWith( + placeholderItem3 + ); // Because Item 3 is already present in Collection + + expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( + 'dummyKey' + ); + } + ); it("shouldn't load default Group and its Items if Collection flag isn't persisted", async () => { collectionPersistent.ready = true; @@ -588,15 +601,15 @@ describe('CollectionPersistent Tests', () => { expect(placeholderItem1.persist).not.toHaveBeenCalled(); expect(placeholderItem2.persist).not.toHaveBeenCalled(); expect(placeholderItem3.persist).not.toHaveBeenCalled(); + expect(dummyItem1.persist).not.toHaveBeenCalled(); + expect(dummyItem2.persist).not.toHaveBeenCalled(); + expect(dummyItem3.persist).not.toHaveBeenCalled(); expect(collectionPersistent.setupSideEffects).not.toHaveBeenCalled(); }); it("shouldn't load default Group and its Items if Persistent isn't ready", async () => { collectionPersistent.ready = false; - dummyAgile.storages.get = jest.fn(() => - Promise.resolve(undefined as any) - ); const response = await collectionPersistent.loadPersistedValue(); @@ -609,13 +622,15 @@ describe('CollectionPersistent Tests', () => { expect(placeholderItem1.persist).not.toHaveBeenCalled(); expect(placeholderItem2.persist).not.toHaveBeenCalled(); expect(placeholderItem3.persist).not.toHaveBeenCalled(); + expect(dummyItem1.persist).not.toHaveBeenCalled(); + expect(dummyItem2.persist).not.toHaveBeenCalled(); + expect(dummyItem3.persist).not.toHaveBeenCalled(); expect(collectionPersistent.setupSideEffects).not.toHaveBeenCalled(); }); it("shouldn't load default Group and its Items if Collection has no defaultGroup", async () => { collectionPersistent.ready = true; - dummyCollection.groups = {}; dummyAgile.storages.get = jest .fn() .mockReturnValueOnce(Promise.resolve(true)); @@ -635,6 +650,9 @@ describe('CollectionPersistent Tests', () => { expect(placeholderItem1.persist).not.toHaveBeenCalled(); expect(placeholderItem2.persist).not.toHaveBeenCalled(); expect(placeholderItem3.persist).not.toHaveBeenCalled(); + expect(dummyItem1.persist).not.toHaveBeenCalled(); + expect(dummyItem2.persist).not.toHaveBeenCalled(); + expect(dummyItem3.persist).not.toHaveBeenCalled(); expect(collectionPersistent.setupSideEffects).not.toHaveBeenCalled(); }); @@ -669,13 +687,12 @@ describe('CollectionPersistent Tests', () => { dummyAgile.storages.set = jest.fn(); }); - it('should persist defaultGroup and its Items (persistentKey)', async () => { + it('should persist default Group and its Items (persistentKey)', async () => { collectionPersistent.ready = true; const response = await collectionPersistent.persistValue(); expect(response).toBeTruthy(); - expect(dummyAgile.storages.set).toHaveBeenCalledWith( collectionPersistent._key, true, @@ -719,13 +736,12 @@ describe('CollectionPersistent Tests', () => { expect(collectionPersistent.isPersisted).toBeTruthy(); }); - it('should persist defaultGroup and its Items (specific key)', async () => { + it('should persist default Group and its Items (specific key)', async () => { collectionPersistent.ready = true; const response = await collectionPersistent.persistValue('dummyKey'); expect(response).toBeTruthy(); - expect(dummyAgile.storages.set).toHaveBeenCalledWith( 'dummyKey', true, @@ -763,12 +779,13 @@ describe('CollectionPersistent Tests', () => { expect(collectionPersistent.isPersisted).toBeTruthy(); }); - it("shouldn't persist defaultGroup and its Items if Persistent isn't ready", async () => { + it("shouldn't persist default Group and its Items if Persistent isn't ready", async () => { collectionPersistent.ready = false; const response = await collectionPersistent.persistValue('dummyKey'); expect(response).toBeFalsy(); + expect(dummyAgile.storages.set).not.toHaveBeenCalled(); expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); expect(dummyDefaultGroup.persist).not.toHaveBeenCalled(); @@ -780,13 +797,14 @@ describe('CollectionPersistent Tests', () => { expect(collectionPersistent.isPersisted).toBeUndefined(); }); - it("shouldn't persist defaultGroup and its Items if Collection has no defaultGroup", async () => { + it("shouldn't persist default Group and its Items if Collection has no default Group", async () => { collectionPersistent.ready = true; dummyCollection.getDefaultGroup = jest.fn(() => undefined as any); const response = await collectionPersistent.persistValue(); expect(response).toBeFalsy(); + expect(dummyAgile.storages.set).not.toHaveBeenCalled(); expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); expect(dummyDefaultGroup.persist).not.toHaveBeenCalled(); @@ -815,7 +833,7 @@ describe('CollectionPersistent Tests', () => { ); }); - it('should add rebuild Storage side effect to default Group', () => { + it("shouldn't add rebuild Storage side effect to default Group", () => { collectionPersistent.setupSideEffects(); expect( @@ -827,17 +845,25 @@ describe('CollectionPersistent Tests', () => { ); }); + it("shouldn't add rebuild Storage side effect to default Group if Collection has no default Group", () => { + dummyCollection.getDefaultGroup = jest.fn(() => undefined as any); + + collectionPersistent.setupSideEffects(); + + expect(dummyDefaultGroup.addSideEffect).not.toHaveBeenCalled(); + }); + describe('test added sideEffect called CollectionPersistent.defaultGroupSideEffectKey', () => { beforeEach(() => { - collectionPersistent.rebuildStorageSideEffect = jest.fn(); - }); - - it('should call rebuildStorageSideEffect (persistentKey)', async () => { collectionPersistent.ready = true; + + collectionPersistent.rebuildStorageSideEffect = jest.fn(); dummyCollection.getDefaultGroup = jest.fn( () => dummyDefaultGroup as any ); + }); + it('should call rebuildStorageSideEffect (persistentKey)', async () => { await collectionPersistent.persistValue(); dummyDefaultGroup.sideEffects[ @@ -850,11 +876,6 @@ describe('CollectionPersistent Tests', () => { }); it('should call rebuildStorageSideEffect (specific key)', async () => { - collectionPersistent.ready = true; - dummyCollection.getDefaultGroup = jest.fn( - () => dummyDefaultGroup as any - ); - await collectionPersistent.persistValue('dummyKey'); dummyDefaultGroup.sideEffects[ @@ -909,8 +930,8 @@ describe('CollectionPersistent Tests', () => { collectionPersistent._key, collectionPersistent.storageKeys ); - expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); expect( dummyDefaultGroup.persistent?.removePersistedValue ).toHaveBeenCalledWith( @@ -955,8 +976,8 @@ describe('CollectionPersistent Tests', () => { 'dummyKey', collectionPersistent.storageKeys ); - expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); expect( dummyDefaultGroup.persistent?.removePersistedValue ).toHaveBeenCalledWith( @@ -990,8 +1011,8 @@ describe('CollectionPersistent Tests', () => { expect(response).toBeFalsy(); expect(dummyAgile.storages.remove).not.toHaveBeenCalled(); - expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); + expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); expect( dummyDefaultGroup.persistent?.removePersistedValue ).not.toHaveBeenCalled(); @@ -1015,8 +1036,8 @@ describe('CollectionPersistent Tests', () => { expect(response).toBeFalsy(); expect(dummyAgile.storages.remove).not.toHaveBeenCalled(); - expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); expect( dummyDefaultGroup.persistent?.removePersistedValue ).not.toHaveBeenCalled(); @@ -1034,15 +1055,15 @@ describe('CollectionPersistent Tests', () => { }); describe('formatKey function tests', () => { - it('should return key of Collection if no key got passed', () => { + it('should return key of Collection if no valid key got provided', () => { dummyCollection._key = 'coolKey'; - const response = collectionPersistent.formatKey(); + const response = collectionPersistent.formatKey(null); expect(response).toBe('coolKey'); }); - it('should return passed key', () => { + it('should return provided key if key is valid', () => { dummyCollection._key = 'coolKey'; const response = collectionPersistent.formatKey('awesomeKey'); @@ -1050,7 +1071,7 @@ describe('CollectionPersistent Tests', () => { expect(response).toBe('awesomeKey'); }); - it('should return and apply passed key to Collection if Collection had no own key before', () => { + it('should return and apply valid provided key to Collection if Collection has no own key', () => { dummyCollection._key = undefined; const response = collectionPersistent.formatKey('awesomeKey'); @@ -1059,10 +1080,10 @@ describe('CollectionPersistent Tests', () => { expect(dummyCollection._key).toBe('awesomeKey'); }); - it('should return undefined if no key got passed and Collection has no key', () => { + it('should return undefined if no valid key got provided and Collection has no key', () => { dummyCollection._key = undefined; - const response = collectionPersistent.formatKey(); + const response = collectionPersistent.formatKey(null); expect(response).toBeUndefined(); }); @@ -1092,13 +1113,6 @@ describe('CollectionPersistent Tests', () => { dummyItem2.persistent.removePersistedValue = jest.fn(); if (dummyItem3.persistent) dummyItem3.persistent.removePersistedValue = jest.fn(); - - if (dummyItem1.persistent) - dummyItem1.persistent.persistValue = jest.fn(); - if (dummyItem2.persistent) - dummyItem2.persistent.persistValue = jest.fn(); - if (dummyItem3.persistent) - dummyItem3.persistent.persistValue = jest.fn(); }); it('should return if no Item got added or removed', () => { @@ -1121,13 +1135,9 @@ describe('CollectionPersistent Tests', () => { expect( dummyItem3.persistent?.removePersistedValue ).not.toHaveBeenCalled(); - - expect(dummyItem1.persistent?.persistValue).not.toHaveBeenCalled(); - expect(dummyItem2.persistent?.persistValue).not.toHaveBeenCalled(); - expect(dummyItem3.persistent?.persistValue).not.toHaveBeenCalled(); }); - it('should call removePersistedValue on Items that got removed from Group', () => { + it('should call removePersistedValue() on Items that got removed from Group', () => { dummyGroup.previousStateValue = ['1', '2', '3']; dummyGroup._value = ['2']; @@ -1151,45 +1161,11 @@ describe('CollectionPersistent Tests', () => { ).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey('3', collectionPersistent._key) ); - - expect(dummyItem1.persistent?.persistValue).not.toHaveBeenCalled(); - expect(dummyItem2.persistent?.persistValue).not.toHaveBeenCalled(); - expect(dummyItem3.persistent?.persistValue).not.toHaveBeenCalled(); }); - it('should call persistValue on Items that have a persistent and got added to Group', () => { + it("should call persist on Items that got added to Group and hasn't been persisted yet", () => { dummyGroup.previousStateValue = ['1']; - dummyGroup._value = ['1', '2', '3']; - - collectionPersistent.rebuildStorageSideEffect(dummyGroup); - - expect(dummyItem1.persist).not.toHaveBeenCalled(); - expect(dummyItem2.persist).not.toHaveBeenCalled(); - expect(dummyItem3.persist).not.toHaveBeenCalled(); - expect(dummyItem4WithoutPersistent.persist).not.toHaveBeenCalled(); - - expect( - dummyItem1.persistent?.removePersistedValue - ).not.toHaveBeenCalled(); - expect( - dummyItem2.persistent?.removePersistedValue - ).not.toHaveBeenCalled(); - expect( - dummyItem3.persistent?.removePersistedValue - ).not.toHaveBeenCalled(); - - expect(dummyItem1.persistent?.persistValue).not.toHaveBeenCalled(); - expect(dummyItem2.persistent?.persistValue).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey('2', collectionPersistent._key) - ); - expect(dummyItem3.persistent?.persistValue).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey('3', collectionPersistent._key) - ); - }); - - it('should call persist on Items that have no persistent and got added to Group', () => { - dummyGroup.previousStateValue = ['1']; - dummyGroup._value = ['1', '4']; + dummyGroup._value = ['1', '4', '3']; collectionPersistent.rebuildStorageSideEffect(dummyGroup); @@ -1197,7 +1173,14 @@ describe('CollectionPersistent Tests', () => { expect(dummyItem2.persist).not.toHaveBeenCalled(); expect(dummyItem3.persist).not.toHaveBeenCalled(); expect(dummyItem4WithoutPersistent.persist).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey('4', collectionPersistent._key) + CollectionPersistent.getItemStorageKey( + '4', + collectionPersistent._key + ), + { + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + } ); expect( @@ -1209,15 +1192,11 @@ describe('CollectionPersistent Tests', () => { expect( dummyItem3.persistent?.removePersistedValue ).not.toHaveBeenCalled(); - - expect(dummyItem1.persistent?.persistValue).not.toHaveBeenCalled(); - expect(dummyItem2.persistent?.persistValue).not.toHaveBeenCalled(); - expect(dummyItem3.persistent?.persistValue).not.toHaveBeenCalled(); }); }); describe('getItemStorageKey function tests', () => { - it('should build ItemStorageKey out of itemKey and collectionKey', () => { + it('should build ItemStorageKey based on itemKey and collectionKey', () => { const response = CollectionPersistent.getItemStorageKey( 'itemKey', 'collectionKey' @@ -1227,7 +1206,7 @@ describe('CollectionPersistent Tests', () => { LogMock.hasNotLogged('warn'); }); - it('should build ItemStorageKey out of collectionKey with warning', () => { + it('should build ItemStorageKey based on only collectionKey with warning', () => { const response = CollectionPersistent.getItemStorageKey( undefined, 'collectionKey' @@ -1237,7 +1216,7 @@ describe('CollectionPersistent Tests', () => { LogMock.hasLoggedCode('1A:02:00'); }); - it('should build ItemStorageKey out of itemKey with warning', () => { + it('should build ItemStorageKey based on only itemKey with warning', () => { const response = CollectionPersistent.getItemStorageKey( 'itemKey', undefined @@ -1247,7 +1226,7 @@ describe('CollectionPersistent Tests', () => { LogMock.hasLoggedCode('1A:02:00'); }); - it('should build ItemStorageKey out of nothing with warning', () => { + it('should build ItemStorageKey based on nothing with warning', () => { const response = CollectionPersistent.getItemStorageKey( undefined, undefined @@ -1259,7 +1238,7 @@ describe('CollectionPersistent Tests', () => { }); describe('getGroupStorageKey function tests', () => { - it('should build GroupStorageKey out of groupKey and collectionKey', () => { + it('should build GroupStorageKey based on groupKey and collectionKey', () => { const response = CollectionPersistent.getGroupStorageKey( 'groupKey', 'collectionKey' @@ -1269,7 +1248,7 @@ describe('CollectionPersistent Tests', () => { LogMock.hasNotLogged('warn'); }); - it('should build GroupStorageKey out of collectionKey with warning', () => { + it('should build GroupStorageKey based on only collectionKey with warning', () => { const response = CollectionPersistent.getGroupStorageKey( undefined, 'collectionKey' @@ -1279,7 +1258,7 @@ describe('CollectionPersistent Tests', () => { LogMock.hasLoggedCode('1A:02:01'); }); - it('should build GroupStorageKey out of groupKey with warning', () => { + it('should build GroupStorageKey based on only groupKey with warning', () => { const response = CollectionPersistent.getGroupStorageKey( 'groupKey', undefined @@ -1289,7 +1268,7 @@ describe('CollectionPersistent Tests', () => { LogMock.hasLoggedCode('1A:02:01'); }); - it('should build GroupStorageKey out of nothing with warning', () => { + it('should build GroupStorageKey based on nothing with warning', () => { const response = CollectionPersistent.getGroupStorageKey( undefined, undefined From 7d34ad360e9c72e8e23bbb2249848f109b685416 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 30 May 2021 18:01:57 +0200 Subject: [PATCH 10/63] added assignItem tests --- .../src/collection/collection.persistent.ts | 4 +- packages/core/src/collection/index.ts | 160 +++++---- packages/core/src/logCodeManager.ts | 2 + packages/core/src/utils.ts | 2 +- .../collection/collection.persistent.test.ts | 12 +- .../tests/unit/collection/collection.test.ts | 322 ++++++++++++++++-- packages/logger/src/index.ts | 2 +- packages/multieditor/src/multieditor.ts | 8 +- packages/proxytree/src/branch.ts | 2 +- packages/react/src/hocs/AgileHOC.ts | 6 +- packages/vue/src/bindAgileInstances.ts | 4 +- 11 files changed, 414 insertions(+), 110 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 33f4ae11..0ee6c8b3 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -168,9 +168,9 @@ export class CollectionPersistent< itemStorageKey ); - // If successfully loaded Item value, add Item to Collection + // If successfully loaded Item value, assign Item to Collection if (loadedPersistedValueIntoItem) - this.collection().collectItem(dummyItem); + this.collection().assignItem(dummyItem); } } } diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index e67536c5..6f6e1008 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -237,11 +237,11 @@ export class Collection { * @param config - Config */ public collect( - data: DataType | Array, + data: DataType | Item | Array>, groupKeys?: GroupKey | Array, config: CollectConfigInterface = {} ): this { - const _data = normalizeArray(data); + const _data = normalizeArray>(data); const _groupKeys = normalizeArray(groupKeys); const defaultGroupKey = this.config.defaultGroupKey; const primaryKey = this.config.primaryKey; @@ -262,12 +262,20 @@ export class Collection { _data.forEach((data, index) => { const itemKey = data[primaryKey]; + let success = false; + + // Assign Data or Item to Collection + if (data instanceof Item) { + success = this.assignItem(data, { + background: config.background, + }); + } else { + success = this.assignData(data, { + patch: config.patch, + background: config.background, + }); + } - // Add Item to Collection - const success = this.assignData(data, { - patch: config.patch, - background: config.background, - }); if (!success) return this; // Add ItemKey to provided Groups @@ -417,7 +425,6 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroup) * * @public - * @memberOf Collection * @param groupKey - key/name Group identifier * @param config - Configuration */ @@ -686,11 +693,11 @@ export class Collection { /** * Creates a placeholder Item - * that can be used to hold a reference to an Item that doesn't yet exist. + * that can be used to hold a reference to an Item that doesn't exist yet. * - * @private - * @param itemKey - Key/Name identifier of the Item to be created - * @param addToCollection - Whether the created Item should be added to the Collection + * @internal + * @param itemKey - Key/Name identifier of the Item to be created. + * @param addToCollection - Whether the created Item should be added to the Collection. */ public createPlaceholderItem( itemKey: ItemKey, @@ -704,12 +711,14 @@ export class Collection { } as any, { isPlaceholder: true } ); + if ( addToCollection && !Object.prototype.hasOwnProperty.call(this.data, itemKey) ) this.data[itemKey] = item; + ComputedTracker.tracked(item.observer); return item; } @@ -1164,94 +1173,118 @@ export class Collection { return this; } - //========================================================================================================= - // Set Data - //========================================================================================================= /** + * Assigns provided data object to an already existing Item at itemKey. + * If Item at itemKey doesn't exist yet, + * a new Item with the data object as value is created and added to the Collection. + * * @internal - * Updates existing or creates Item from provided Data - * @param data - Data - * @param config - Config + * @param data - Data object + * @param config - Configuration object */ public assignData( data: DataType, - config: SetDataConfigInterface = {} + config: AssignDataConfigInterface = {} ): boolean { - const _data = copy(data as any); // Transformed Data to any because of unknown Object (DataType) config = defineConfig(config, { patch: false, background: false, }); + const _data = copy(data); // Copy data object to get rid of reference + const primaryKey = this.config.primaryKey; if (!isValidObject(_data)) { LogCodeManager.log('1B:03:05', [this._key]); return false; } - // Check if data has valid primaryKey - if (!Object.prototype.hasOwnProperty.call(_data, this.config.primaryKey)) { - LogCodeManager.log('1B:02:05', [this._key, this.config.primaryKey]); - _data[this.config.primaryKey] = generateId(); + // Check if data object contains valid itemKey + // otherwise add random itemKey to Item + if (!Object.prototype.hasOwnProperty.call(_data, primaryKey)) { + LogCodeManager.log('1B:02:05', [this._key, primaryKey]); + _data[primaryKey] = generateId(); } - const itemKey = _data[this.config.primaryKey]; + const itemKey = _data[primaryKey]; const item = this.getItem(itemKey, { notExisting: true }); const wasPlaceholder = item?.isPlaceholder || false; - const createItem = item == null; - // Create or update Item - if (!createItem && config.patch) - item?.patch(_data, { background: config.background }); - if (!createItem && !config.patch) - item?.set(_data, { background: config.background }); - if (createItem) this.collectItem(new Item(this, _data)); + // Create new Item or update existing Item + if (item != null) { + if (config.patch) { + item.patch(_data, { background: config.background }); + } else { + item.set(_data, { background: config.background }); + } + } else { + this.assignItem(new Item(this, _data), { + background: config.background, + }); + } - // Increase size of Collection if Item was before a placeholder + // Increase size of Collection if Item was previously a placeholder + // (-> didn't officially exit in Collection) if (wasPlaceholder) this.size++; return true; } /** - * Adds passed Item to Collection. + * Adds provided Item to the Collection. * - * @public - * @param item - Item to be added + * @internal + * @param item - Item to be added. * @param config - Configuration object */ - public collectItem( + public assignItem( item: Item, - config: { background?: boolean } = {} - ): this { - const itemKey = item._value[this.config.primaryKey]; - - // TODO add to Groups at least default Group - // and implement it to collect method - - // Check if Item has valid primaryKey - if ( - !Object.prototype.hasOwnProperty.call(item._value, this.config.primaryKey) - ) { - LogCodeManager.log('1B:02:05', [this._key, this.config.primaryKey]); + config: AssignItemConfigInterface = {} + ): boolean { + config = defineConfig(config, { + overwrite: false, + background: false, + }); + const primaryKey = this.config.primaryKey; + let itemKey = item._value[primaryKey]; + let increaseCollectionSize = true; + + // Check if Item has valid itemKey + // otherwise add random itemKey to Item + if (!Object.prototype.hasOwnProperty.call(item._value, primaryKey)) { + LogCodeManager.log('1B:02:05', [this._key, primaryKey]); + itemKey = generateId(); item.patch( - { [this.config.primaryKey]: generateId() }, - { background: true } + { [this.config.primaryKey]: itemKey }, + { background: config.background } ); + item._key = itemKey; + } + + // Check if Item belongs to this Collection + if (item.collection() !== this) { + LogCodeManager.log('1B:03:06', [this._key, item.collection()._key]); + return false; } // Check if Item already exists - if (this.getItem(itemKey) != null) return this; + if (this.getItem(itemKey) != null) { + if (!config.overwrite) return true; + else increaseCollectionSize = false; + } + // Assign/add Item to Collection this.data[itemKey] = item; - // Rebuild Groups That include ItemKey after assigning Item to Collection (otherwise it can't find Item) + // Rebuild Groups that include itemKey + // after adding Item to Collection + // (because otherwise it can't find the Item since it doesn't exist in Collection yet) this.rebuildGroupsThatIncludeItemKey(itemKey, { background: config.background, }); - this.size++; + if (increaseCollectionSize) this.size++; - return this; + return true; } //========================================================================================================= @@ -1331,7 +1364,11 @@ export interface CollectionConfigInterface { export interface CollectConfigInterface { patch?: boolean; method?: 'push' | 'unshift'; - forEachItem?: (data: DataType, key: ItemKey, index: number) => void; + forEachItem?: ( + data: DataType | Item, + key: ItemKey, + index: number + ) => void; background?: boolean; select?: boolean; } @@ -1397,11 +1434,20 @@ export interface RemoveItemsConfigInterface { * @property patch - If Data gets patched into existing Item * @property background - If assigning Data happens in background */ -export interface SetDataConfigInterface { +export interface AssignDataConfigInterface { patch?: boolean; background?: boolean; } +/** + * @property overwrite - If old Item should be overwritten + * @property background - If assigning Data happens in background + */ +export interface AssignItemConfigInterface { + overwrite?: boolean; + background?: boolean; +} + export type CollectionConfig = | CreateCollectionConfigInterface | (( diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 895f2bc6..8ac05a98 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -137,6 +137,8 @@ const logCodeMessages = { "Couldn't update ItemKey from '${0}' to '${1}' " + "because an Item with the key/name '${1}' already exists in the Collection '${2}'!", '1B:03:05': "Item Data of Collection '${0}' has to be a valid object!", + '1B:03:06': + "Item tried to add to the Collection '${0}' belongs to another Collection '${1}'!", // Group '1C:02:00': diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 56f5000d..d998f602 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -39,7 +39,7 @@ export function getAgileInstance(instance: any): Agile | undefined { // Extract Observers //========================================================================================================= /** - * @private + * @internal * Extract Observers from specific Instances * @param instances - Instances that will be formatted */ diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index b59dd9ae..7c10829d 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -331,7 +331,7 @@ describe('CollectionPersistent Tests', () => { dummyCollection.getDefaultGroup = jest.fn( () => dummyDefaultGroup as any ); - dummyCollection.collectItem = jest.fn(); + dummyCollection.assignItem = jest.fn(); dummyAgile.storages.get = jest.fn(); }); @@ -479,13 +479,13 @@ describe('CollectionPersistent Tests', () => { storageKeys: collectionPersistent.storageKeys, } ); - expect(dummyCollection.collectItem).toHaveBeenCalledWith( + expect(dummyCollection.assignItem).toHaveBeenCalledWith( placeholderItem1 ); - expect(dummyCollection.collectItem).not.toHaveBeenCalledWith( + expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( placeholderItem2 ); // Because Item persistent isn't ready - expect(dummyCollection.collectItem).not.toHaveBeenCalledWith( + expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( placeholderItem3 ); // Because Item persistent 'leadPersistedValue()' returned false -> Item properly doesn't exist in Storage @@ -568,10 +568,10 @@ describe('CollectionPersistent Tests', () => { storageKeys: collectionPersistent.storageKeys, } ); - expect(dummyCollection.collectItem).toHaveBeenCalledWith( + expect(dummyCollection.assignItem).toHaveBeenCalledWith( placeholderItem1 ); - expect(dummyCollection.collectItem).not.toHaveBeenCalledWith( + expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( placeholderItem3 ); // Because Item 3 is already present in Collection diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 32219fba..aeedde2b 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -1407,12 +1407,18 @@ describe('Collection Tests', () => { describe('getItemWithReference function tests', () => { let dummyItem: Item; + let placeholderItem: Item; beforeEach(() => { dummyItem = new Item(collection, { id: '1', name: 'Jeff' }); + placeholderItem = collection.createPlaceholderItem('1'); + collection.data = { ['1']: dummyItem, }; + collection.createPlaceholderItem = jest + .fn() + .mockReturnValueOnce(placeholderItem); ComputedTracker.tracked = jest.fn(); }); @@ -1421,6 +1427,7 @@ describe('Collection Tests', () => { const response = collection.getItemWithReference('1'); expect(response).toBe(dummyItem); + expect(collection.createPlaceholderItem).not.toHaveBeenCalled(); expect(ComputedTracker.tracked).toHaveBeenCalledWith( dummyItem.observer ); @@ -1429,14 +1436,82 @@ describe('Collection Tests', () => { it("should return and track created reference Item if Item doesn't exist yet", () => { const response = collection.getItemWithReference('notExistingItem'); - expect(response).toBeInstanceOf(Item); - expect(response.isPlaceholder).toBeTruthy(); - expect(response._key).toBe('notExistingItem'); - expect(collection.data['notExistingItem']).toBe(response); + expect(response).toBe(placeholderItem); + expect(collection.createPlaceholderItem).toHaveBeenCalledWith( + 'notExistingItem', + true + ); expect(ComputedTracker.tracked).toHaveBeenCalledWith(response.observer); }); }); + describe('createPlaceholderItem function tests', () => { + let dummyItem: Item; + + beforeEach(() => { + dummyItem = new Item(collection, { id: '1', name: 'Jeff' }); + + collection.data = { + ['1']: dummyItem, + }; + + ComputedTracker.tracked = jest.fn(); + }); + + it("should create placeholder Item and shouldn't add it to Collection (addToCollection = false)", () => { + const item = collection.createPlaceholderItem('2', false); + + expect(item).not.toBe(dummyItem); + expect(item.collection()).toBe(collection); + expect(item._key).toBe('2'); + expect(item._value).toStrictEqual({ id: '2', dummy: 'item' }); + expect(item.isPlaceholder).toBeTruthy(); + + expect(collection.data).not.toHaveProperty('2'); + + expect(ComputedTracker.tracked).toHaveBeenCalledTimes(1); + expect(ComputedTracker.tracked).not.toHaveBeenCalledWith( + dummyItem.observer + ); + }); + + it("should create placeholder Item and shouldn't add it to Collection if Item already exists (addToCollection = true)", () => { + const item = collection.createPlaceholderItem('1', false); + + expect(item).not.toBe(dummyItem); + expect(item.collection()).toBe(collection); + expect(item._key).toBe('1'); + expect(item._value).toStrictEqual({ id: '1', dummy: 'item' }); + expect(item.isPlaceholder).toBeTruthy(); + + expect(collection.data).toHaveProperty('1'); + expect(collection.data['1']).toBe(dummyItem); + + expect(ComputedTracker.tracked).toHaveBeenCalledTimes(1); + expect(ComputedTracker.tracked).not.toHaveBeenCalledWith( + dummyItem.observer + ); + }); + + it('should create placeholder Item and add it to Collection (addToCollection = true)', () => { + const item = collection.createPlaceholderItem('2', true); + + expect(item).not.toBe(dummyItem); + expect(item.collection()).toBe(collection); + expect(item._key).toBe('2'); + expect(item._value).toStrictEqual({ id: '2', dummy: 'item' }); + expect(item.isPlaceholder).toBeTruthy(); + + expect(collection.data).toHaveProperty('2'); + expect(collection.data['2']).toStrictEqual(expect.any(Item)); + + expect(ComputedTracker.tracked).toHaveBeenCalledTimes(1); + expect(ComputedTracker.tracked).not.toHaveBeenCalledWith( + dummyItem.observer + ); + }); + }); + describe('getItemValue function tests', () => { let dummyItem: Item; @@ -2353,7 +2428,7 @@ describe('Collection Tests', () => { }); }); - describe('setData function tests', () => { + describe('assignData function tests', () => { let dummyItem1: Item; beforeEach(() => { @@ -2363,55 +2438,89 @@ describe('Collection Tests', () => { }; collection.size = 1; + jest.spyOn(collection, 'assignItem'); + dummyItem1.patch = jest.fn(); dummyItem1.set = jest.fn(); }); - it('should create new Item out of valid Data, rebuild Groups and increase size (default config)', () => { + it("should assign Item to Collection if it doesn't exist yet (default config)", () => { const response = collection.assignData({ id: 'dummyItem2', name: 'Hans', }); expect(response).toBeTruthy(); - expect(collection.data).toHaveProperty('dummyItem1'); + expect(collection.size).toBe(2); // Increased by assignItem + expect(collection.assignItem).toHaveBeenCalledWith(expect.any(Item), { + background: false, + }); + + // Check if Item, assignItem was called with, has the correct data expect(collection.data).toHaveProperty('dummyItem2'); - expect(collection.data['dummyItem2']).toBeInstanceOf(Item); expect(collection.data['dummyItem2']._value).toStrictEqual({ id: 'dummyItem2', name: 'Hans', }); - expect(collection.size).toBe(2); LogMock.hasNotLogged('error'); LogMock.hasNotLogged('warn'); }); - it("shouldn't create new Item if passed Data is no valid Object", () => { + it("should assign Item to Collection if it doesn't exist yet (config.background = true)", () => { + const response = collection.assignData( + { + id: 'dummyItem2', + name: 'Hans', + }, + { background: true } + ); + + expect(response).toBeTruthy(); + expect(collection.size).toBe(2); // Increased by assignItem + expect(collection.assignItem).toHaveBeenCalledWith(expect.any(Item), { + background: true, + }); + + // Check if Item, assignItem was called with, has the correct data + expect(collection.data).toHaveProperty('dummyItem2'); + expect(collection.data['dummyItem2']._value).toStrictEqual({ + id: 'dummyItem2', + name: 'Hans', + }); + + LogMock.hasNotLogged('error'); + LogMock.hasNotLogged('warn'); + }); + + it("shouldn't assign or update Item if passed data is no valid object", () => { const response = collection.assignData('noObject' as any); expect(response).toBeFalsy(); expect(collection.size).toBe(1); + expect(collection.assignItem).not.toHaveBeenCalled(); LogMock.hasLoggedCode('1B:03:05', [collection._key]); LogMock.hasNotLogged('warn'); }); - it('should create new Item with random primaryKey if passed Data has no primaryKey', () => { + it("should assign Item to Collection with random itemKey if data object doesn't contain valid itemKey", () => { jest.spyOn(Utils, 'generateId').mockReturnValueOnce('randomDummyId'); const response = collection.assignData({ name: 'Frank' } as any); expect(response).toBeTruthy(); - expect(response).toBeTruthy(); - expect(collection.data).toHaveProperty('dummyItem1'); + expect(collection.size).toBe(2); // Increased by assignItem + expect(collection.assignItem).toHaveBeenCalledWith(expect.any(Item), { + background: false, + }); + + // Check if Item, assignItem was called with, has the correct data expect(collection.data).toHaveProperty('randomDummyId'); - expect(collection.data['randomDummyId']).toBeInstanceOf(Item); expect(collection.data['randomDummyId']._value).toStrictEqual({ id: 'randomDummyId', name: 'Frank', }); - expect(collection.size).toBe(2); LogMock.hasNotLogged('error'); LogMock.hasLoggedCode('1B:02:05', [ @@ -2420,17 +2529,15 @@ describe('Collection Tests', () => { ]); }); - it("should update Item with valid Data, shouldn't rebuild Groups and shouldn't increase size (default config)", () => { + it('should update Item with valid data via set (default config)', () => { const response = collection.assignData({ id: 'dummyItem1', name: 'Dieter', }); expect(response).toBeTruthy(); - - expect(collection.data).toHaveProperty('dummyItem1'); - expect(collection.data['dummyItem1']).toBeInstanceOf(Item); expect(collection.size).toBe(1); + expect(collection.assignItem).not.toHaveBeenCalled(); expect(dummyItem1.set).toHaveBeenCalledWith( { id: 'dummyItem1', name: 'Dieter' }, @@ -2442,7 +2549,7 @@ describe('Collection Tests', () => { LogMock.hasNotLogged('warn'); }); - it("should update Item with valid Data, shouldn't rebuild Groups and shouldn't increase size (config.background = true)", () => { + it('should update Item with valid data via set (config.background = true)', () => { const response = collection.assignData( { id: 'dummyItem1', @@ -2452,10 +2559,8 @@ describe('Collection Tests', () => { ); expect(response).toBeTruthy(); - - expect(collection.data).toHaveProperty('dummyItem1'); - expect(collection.data['dummyItem1']).toBeInstanceOf(Item); expect(collection.size).toBe(1); + expect(collection.assignItem).not.toHaveBeenCalled(); expect(dummyItem1.set).toHaveBeenCalledWith( { id: 'dummyItem1', name: 'Dieter' }, @@ -2467,7 +2572,7 @@ describe('Collection Tests', () => { LogMock.hasNotLogged('warn'); }); - it("should update Item with valid Data, shouldn't rebuild Groups and shouldn't increase size (config.patch = true, background: true)", () => { + it('should update Item with valid data via patch (config.patch = true, background: true)', () => { const response = collection.assignData( { id: 'dummyItem1', @@ -2477,10 +2582,8 @@ describe('Collection Tests', () => { ); expect(response).toBeTruthy(); - - expect(collection.data).toHaveProperty('dummyItem1'); - expect(collection.data['dummyItem1']).toBeInstanceOf(Item); expect(collection.size).toBe(1); + expect(collection.assignItem).not.toHaveBeenCalled(); expect(dummyItem1.set).not.toHaveBeenCalled(); expect(dummyItem1.patch).toHaveBeenCalledWith( @@ -2492,9 +2595,8 @@ describe('Collection Tests', () => { LogMock.hasNotLogged('warn'); }); - it("should update placeholder Item with valid Data, shouldn't rebuild Groups and should increase size (default config)", () => { + it('should update placeholder Item with valid data and increase size', () => { dummyItem1.isPlaceholder = true; - collection.size = 0; const response = collection.assignData({ id: 'dummyItem1', @@ -2502,15 +2604,169 @@ describe('Collection Tests', () => { }); expect(response).toBeTruthy(); - - expect(collection.data).toHaveProperty('dummyItem1'); - expect(collection.data['dummyItem1']).toBeInstanceOf(Item); - expect(collection.size).toBe(1); + expect(collection.size).toBe(2); + expect(collection.assignItem).not.toHaveBeenCalled(); expect(dummyItem1.set).toHaveBeenCalledWith( { id: 'dummyItem1', name: 'Dieter' }, { background: false } ); + expect(dummyItem1.patch).not.toHaveBeenCalled(); + + LogMock.hasNotLogged('error'); + LogMock.hasNotLogged('warn'); + }); + }); + + describe('assignItem function tests', () => { + let dummyItem1: Item; + let toAddDummyItem2: Item; + + beforeEach(() => { + dummyItem1 = new Item(collection, { id: 'dummyItem1', name: 'Jeff' }); + toAddDummyItem2 = new Item(collection, { + id: 'dummyItem2', + name: 'Frank', + }); + collection.data = { + dummyItem1: dummyItem1, + }; + collection.size = 1; + + dummyItem1.patch = jest.fn(); + toAddDummyItem2.patch = jest.fn(); + collection.rebuildGroupsThatIncludeItemKey = jest.fn(); + }); + + it('should assign valid Item to Collection (default config)', () => { + const response = collection.assignItem(toAddDummyItem2); + + expect(response).toBeTruthy(); + expect(collection.size).toBe(2); + expect(collection.data).toHaveProperty('dummyItem2'); + expect(collection.data['dummyItem2']).toBe(toAddDummyItem2); + expect(collection.rebuildGroupsThatIncludeItemKey).toHaveBeenCalledWith( + 'dummyItem2', + { + background: false, + } + ); + + expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); + + LogMock.hasNotLogged('error'); + LogMock.hasNotLogged('warn'); + }); + + it('should assign valid Item to Collection (config.background = true)', () => { + const response = collection.assignItem(toAddDummyItem2, { + background: true, + }); + + expect(response).toBeTruthy(); + expect(collection.size).toBe(2); + expect(collection.data).toHaveProperty('dummyItem2'); + expect(collection.data['dummyItem2']).toBe(toAddDummyItem2); + expect(collection.rebuildGroupsThatIncludeItemKey).toHaveBeenCalledWith( + 'dummyItem2', + { + background: true, + } + ); + + expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); + + LogMock.hasNotLogged('error'); + LogMock.hasNotLogged('warn'); + }); + + it("should assign Item to Collection with random itemKey if data object doesn't contain valid itemKey (default config)", () => { + jest.spyOn(Utils, 'generateId').mockReturnValueOnce('randomDummyId'); + toAddDummyItem2._value = { dummy: 'data' } as any; + toAddDummyItem2._key = undefined; + + const response = collection.assignItem(toAddDummyItem2); + + expect(response).toBeTruthy(); + expect(collection.size).toBe(2); + expect(collection.data).toHaveProperty('randomDummyId'); + expect(collection.data['randomDummyId']).toBe(toAddDummyItem2); + expect(collection.rebuildGroupsThatIncludeItemKey).toHaveBeenCalledWith( + 'randomDummyId', + { + background: false, + } + ); + + expect(toAddDummyItem2.patch).toHaveBeenCalledWith( + { id: 'randomDummyId' }, + { background: false } + ); + expect(toAddDummyItem2._key).toBe('randomDummyId'); + + LogMock.hasNotLogged('error'); + LogMock.hasLoggedCode('1B:02:05', [ + collection._key, + collection.config.primaryKey, + ]); + }); + + it("shouldn't assign Item to Collection that belongs to another Collection", () => { + const anotherCollection = new Collection(dummyAgile, { + key: 'anotherCollection', + }); + toAddDummyItem2.collection = () => anotherCollection; + + const response = collection.assignItem(toAddDummyItem2); + + expect(response).toBeFalsy(); + expect(collection.size).toBe(1); + expect(collection.data).not.toHaveProperty('dummyItem2'); + expect( + collection.rebuildGroupsThatIncludeItemKey + ).not.toHaveBeenCalled(); + + expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); + + LogMock.hasLoggedCode('1B:03:06', [ + collection._key, + anotherCollection._key, + ]); + LogMock.hasNotLogged('warn'); + }); + + it("shouldn't assign Item to Collection if an Item at itemKey already exists (default config)", () => { + const response = collection.assignItem(dummyItem1); + + expect(response).toBeTruthy(); + expect(collection.size).toBe(1); + expect(collection.data).toHaveProperty('dummyItem1'); + expect(collection.data['dummyItem1']).toBe(dummyItem1); + expect( + collection.rebuildGroupsThatIncludeItemKey + ).not.toHaveBeenCalled(); + + expect(dummyItem1.patch).not.toHaveBeenCalled(); + + LogMock.hasNotLogged('error'); + LogMock.hasNotLogged('warn'); + }); + + it('should assign Item to Collection if an Item at itemKey already exists (config.overwrite = true)', () => { + const response = collection.assignItem(dummyItem1, { overwrite: true }); + + expect(response).toBeTruthy(); + expect(collection.size).toBe(1); + expect(collection.data).toHaveProperty('dummyItem1'); + expect(collection.data['dummyItem1']).toBe(dummyItem1); + expect(collection.rebuildGroupsThatIncludeItemKey).toHaveBeenCalledWith( + 'dummyItem1', + { + background: false, + } + ); + + expect(dummyItem1.patch).not.toHaveBeenCalled(); LogMock.hasNotLogged('error'); LogMock.hasNotLogged('warn'); diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index d00a98ea..e3a2a889 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -124,7 +124,7 @@ export class Logger { // Tag //========================================================================================================= /** - * @private + * @internal * Only executes following 'command' if all given tags are included in allowedTags * @param tags - Tags */ diff --git a/packages/multieditor/src/multieditor.ts b/packages/multieditor/src/multieditor.ts index 573ca0ce..107a98b9 100644 --- a/packages/multieditor/src/multieditor.ts +++ b/packages/multieditor/src/multieditor.ts @@ -394,7 +394,7 @@ export class MultiEditor< // Get Validator //========================================================================================================= /** - * @private + * @internal * Get Validator of Item based on validateMethods * @param key - Key/Name of Item */ @@ -429,7 +429,7 @@ export class MultiEditor< // Validate //========================================================================================================= /** - * @private + * @internal * Validates Editor and updates its 'isValid' property */ public validate(): boolean { @@ -451,7 +451,7 @@ export class MultiEditor< // Can Assign Status To Item On Change //========================================================================================================= /** - * @private + * @internal * If Status can be assigned on Change * @param item - Item to which the Status should get applied */ @@ -470,7 +470,7 @@ export class MultiEditor< // Can Assign Status To Item On Submit //========================================================================================================= /** - * @private + * @internal * If Status can be assigned on Submit * @param item - Item to which the Status should get applied */ diff --git a/packages/proxytree/src/branch.ts b/packages/proxytree/src/branch.ts index f9c05f3e..bb13449e 100644 --- a/packages/proxytree/src/branch.ts +++ b/packages/proxytree/src/branch.ts @@ -46,7 +46,7 @@ export class Branch { } /** - * @private + * @internal * Record usage of an accessed property in the passed target object. * @param target - Target object in which a property at key was accessed * @param key - Key that was accessed in the target object diff --git a/packages/react/src/hocs/AgileHOC.ts b/packages/react/src/hocs/AgileHOC.ts index 30bbde0d..8249b15c 100644 --- a/packages/react/src/hocs/AgileHOC.ts +++ b/packages/react/src/hocs/AgileHOC.ts @@ -74,7 +74,7 @@ export function AgileHOC( // Create HOC //========================================================================================================= /** - * @private + * @internal * Creates Higher Order Component based on passed React Component that binds the deps to it * @param ReactComponent - React Component * @param agileInstance - Instance of Agile @@ -151,7 +151,7 @@ const createHOC = ( // Format Deps With No Safe Indicator //========================================================================================================= /** - * @private + * @internal * Extract Observers from dependencies which might not have an indicator. * If a indicator could be found it will be added to 'depsWithIndicator' otherwise to 'depsWithoutIndicator'. * @param deps - Dependencies to be formatted @@ -187,7 +187,7 @@ const formatDepsWithNoSafeIndicator = ( // Format Deps With Indicator //========================================================================================================= /** - * @private + * @internal * Extract Observers from dependencies which have an indicator through the object property key. * @param deps - Dependencies to be formatted */ diff --git a/packages/vue/src/bindAgileInstances.ts b/packages/vue/src/bindAgileInstances.ts index 1d439b44..0e89bdc9 100644 --- a/packages/vue/src/bindAgileInstances.ts +++ b/packages/vue/src/bindAgileInstances.ts @@ -50,7 +50,7 @@ export function bindAgileInstances( // Format Deps With No Safe Indicator //========================================================================================================= /** - * @private + * @internal * Extract Observers from dependencies which might not have an indicator. * If a indicator could be found it will be added to 'depsWithIndicator' otherwise to 'depsWithoutIndicator'. * @param deps - Dependencies to be formatted @@ -86,7 +86,7 @@ const formatDepsWithNoSafeIndicator = ( // Format Deps With Indicator //========================================================================================================= /** - * @private + * @internal * Extract Observers from dependencies which have an indicator through the object property key. * @param deps - Dependencies to be formatted */ From f31c043794069e693c656efe9506fa2df7309b40 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 30 May 2021 20:27:08 +0200 Subject: [PATCH 11/63] fixed persist tests --- .../tests/unit/collection/collection.test.ts | 19 ++++++++++--------- .../core/tests/unit/collection/item.test.ts | 9 +++++++-- packages/core/tests/unit/state/state.test.ts | 17 +++++++---------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index aeedde2b..de26cc2a 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -447,11 +447,13 @@ describe('Collection Tests', () => { let dummyGroup1: Group; let dummyGroup2: Group; let defaultGroup: Group; + let dummyItem: Item; beforeEach(() => { dummyGroup1 = new Group(collection); dummyGroup2 = new Group(collection); defaultGroup = new Group(collection); + dummyItem = new Item(collection, { id: '1', name: 'frank' }); collection.groups = { [collection.config.defaultGroupKey]: defaultGroup, @@ -460,6 +462,7 @@ describe('Collection Tests', () => { }; collection.assignData = jest.fn(); + collection.assignItem = jest.fn(); collection.createSelector = jest.fn(); collection.createGroup = jest.fn(); @@ -1703,19 +1706,17 @@ describe('Collection Tests', () => { }); }); - it('should overwrite existing persistent with a warning', () => { - collection.persistent = new CollectionPersistent(collection); + it("shouldn't overwrite existing persistent", () => { + const dummyPersistent = new CollectionPersistent(collection); + collection.persistent = dummyPersistent; + collection.isPersisted = true; + jest.clearAllMocks(); collection.persist('newPersistentKey'); - expect(collection.persistent).toBeInstanceOf(CollectionPersistent); + expect(collection.persistent).toBe(dummyPersistent); // expect(collection.persistent._key).toBe("newPersistentKey"); // Can not test because of Mocking Persistent - expect(CollectionPersistent).toHaveBeenCalledWith(collection, { - instantiate: true, - storageKeys: [], - key: 'newPersistentKey', - defaultStorageKey: null, - }); + expect(CollectionPersistent).not.toHaveBeenCalled(); }); }); diff --git a/packages/core/tests/unit/collection/item.test.ts b/packages/core/tests/unit/collection/item.test.ts index 32f61557..c3ccb9af 100644 --- a/packages/core/tests/unit/collection/item.test.ts +++ b/packages/core/tests/unit/collection/item.test.ts @@ -2,15 +2,20 @@ import { Item, Collection, Agile, StateObserver, State } from '../../../src'; import { LogMock } from '../../helper/logMock'; describe('Item Tests', () => { + interface ItemInterface { + id: string; + name: string; + } + let dummyAgile: Agile; - let dummyCollection: Collection; + let dummyCollection: Collection; beforeEach(() => { jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); - dummyCollection = new Collection(dummyAgile); + dummyCollection = new Collection(dummyAgile); jest.spyOn(Item.prototype, 'addRebuildGroupThatIncludeItemKeySideEffect'); }); diff --git a/packages/core/tests/unit/state/state.test.ts b/packages/core/tests/unit/state/state.test.ts index 66793053..dbfb84d5 100644 --- a/packages/core/tests/unit/state/state.test.ts +++ b/packages/core/tests/unit/state/state.test.ts @@ -638,20 +638,17 @@ describe('State Tests', () => { }); }); - it('should overwrite existing Persistent', () => { - const oldPersistent = new StatePersistent(numberState); - numberState.persistent = oldPersistent; + it("shouldn't overwrite existing Persistent", () => { + const dummyPersistent = new StatePersistent(numberState); + numberState.persistent = dummyPersistent; + numberState.isPersisted = true; + jest.clearAllMocks(); numberState.persist('newPersistentKey'); - expect(numberState.persistent).toBeInstanceOf(StatePersistent); + expect(numberState.persistent).toBe(dummyPersistent); // expect(numberState.persistent._key).toBe("newPersistentKey"); // Can not test because of Mocking Persistent - expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: true, - storageKeys: [], - key: 'newPersistentKey', - defaultStorageKey: null, - }); + expect(StatePersistent).not.toHaveBeenCalled(); }); }); From 8a7ff482875245ba08131a5a2add8de749d917f6 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 31 May 2021 06:45:33 +0200 Subject: [PATCH 12/63] fixed collect method tests --- packages/core/src/collection/index.ts | 26 ++- packages/core/src/collection/item.ts | 2 +- .../tests/unit/collection/collection.test.ts | 197 ++++++++++++++---- 3 files changed, 174 insertions(+), 51 deletions(-) diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 6f6e1008..f62b9fa3 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -261,7 +261,7 @@ export class Collection { ); _data.forEach((data, index) => { - const itemKey = data[primaryKey]; + let itemKey; let success = false; // Assign Data or Item to Collection @@ -269,25 +269,28 @@ export class Collection { success = this.assignItem(data, { background: config.background, }); + itemKey = data._key; } else { success = this.assignData(data, { patch: config.patch, background: config.background, }); + itemKey = data[primaryKey]; } - if (!success) return this; - - // Add ItemKey to provided Groups - _groupKeys.forEach((groupKey) => { - this.getGroup(groupKey)?.add(itemKey, { - method: config.method, - background: config.background, + // Add ItemKey to provided Groups and create corresponding Selector + if (success) { + _groupKeys.forEach((groupKey) => { + this.getGroup(groupKey)?.add(itemKey, { + method: config.method, + background: config.background, + }); }); - }); - if (config.select) this.createSelector(itemKey, itemKey); - if (config.forEachItem) config.forEachItem(data, itemKey, index); + if (config.select) this.createSelector(itemKey, itemKey); + } + + if (config.forEachItem) config.forEachItem(data, itemKey, success, index); }); return this; @@ -1367,6 +1370,7 @@ export interface CollectConfigInterface { forEachItem?: ( data: DataType | Item, key: ItemKey, + success: boolean, index: number ) => void; background?: boolean; diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index d64f2ad4..28cae1da 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -1,7 +1,6 @@ import { State, Collection, - DefaultItem, StateKey, StateRuntimeJobConfigInterface, defineConfig, @@ -10,6 +9,7 @@ import { isValidObject, CollectionPersistent, StatePersistentConfigInterface, + DefaultItem, } from '../internal'; export class Item extends State< diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index de26cc2a..c79dcf57 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -447,13 +447,13 @@ describe('Collection Tests', () => { let dummyGroup1: Group; let dummyGroup2: Group; let defaultGroup: Group; - let dummyItem: Item; + let dummyItem5: Item; beforeEach(() => { dummyGroup1 = new Group(collection); dummyGroup2 = new Group(collection); defaultGroup = new Group(collection); - dummyItem = new Item(collection, { id: '1', name: 'frank' }); + dummyItem5 = new Item(collection, { id: '5', name: 'frank' }); collection.groups = { [collection.config.defaultGroupKey]: defaultGroup, @@ -471,7 +471,7 @@ describe('Collection Tests', () => { defaultGroup.add = jest.fn(); }); - it('should add Data to Collection and to default Group (default config)', () => { + it('should add data object to Collection and to default Group (default config)', () => { collection.assignData = jest.fn(() => true); collection.collect({ id: '1', name: 'frank' }); @@ -486,6 +486,8 @@ describe('Collection Tests', () => { background: false, } ); + expect(collection.assignItem).not.toHaveBeenCalled(); + expect(collection.createGroup).not.toHaveBeenCalled(); expect(dummyGroup1.add).not.toHaveBeenCalled(); @@ -498,7 +500,7 @@ describe('Collection Tests', () => { expect(collection.createSelector).not.toHaveBeenCalled(); }); - it('should add Data to Collection and to default Group (specific config)', () => { + it('should add data object to Collection and to default Group (specific config)', () => { collection.assignData = jest.fn(() => true); collection.collect({ id: '1', name: 'frank' }, [], { @@ -517,6 +519,8 @@ describe('Collection Tests', () => { background: true, } ); + expect(collection.assignItem).not.toHaveBeenCalled(); + expect(collection.createGroup).not.toHaveBeenCalled(); expect(dummyGroup1.add).not.toHaveBeenCalled(); @@ -529,14 +533,60 @@ describe('Collection Tests', () => { expect(collection.createSelector).not.toHaveBeenCalled(); }); - it('should add Data to Collection and to passed Groups + default Group (default config)', () => { + it('should add Item to Collection and to default Group (default config)', () => { + collection.assignItem = jest.fn(() => true); + + collection.collect(dummyItem5); + + expect(collection.assignData).not.toHaveBeenCalled(); + expect(collection.assignItem).toHaveBeenCalledWith(dummyItem5, { + background: false, + }); + + expect(collection.createGroup).not.toHaveBeenCalled(); + + expect(dummyGroup1.add).not.toHaveBeenCalled(); + expect(dummyGroup2.add).not.toHaveBeenCalled(); + expect(defaultGroup.add).toHaveBeenCalledWith('5', { + method: 'push', + background: false, + }); + + expect(collection.createSelector).not.toHaveBeenCalled(); + }); + + it('should add Item to Collection and to default Group (specific config)', () => { + collection.assignItem = jest.fn(() => true); + + collection.collect(dummyItem5, [], { + background: true, + method: 'unshift', + patch: true, + }); + + expect(collection.assignData).not.toHaveBeenCalled(); + expect(collection.assignItem).toHaveBeenCalledWith(dummyItem5, { + background: true, + }); + + expect(collection.createGroup).not.toHaveBeenCalled(); + + expect(dummyGroup1.add).not.toHaveBeenCalled(); + expect(dummyGroup2.add).not.toHaveBeenCalled(); + expect(defaultGroup.add).toHaveBeenCalledWith('5', { + method: 'unshift', + background: true, + }); + + expect(collection.createSelector).not.toHaveBeenCalled(); + }); + + it('should add data/item to Collection and to given + default Group (default config)', () => { collection.assignData = jest.fn(() => true); + collection.assignItem = jest.fn(() => true); collection.collect( - [ - { id: '1', name: 'frank' }, - { id: '2', name: 'hans' }, - ], + [{ id: '1', name: 'frank' }, dummyItem5, { id: '2', name: 'hans' }], ['dummyGroup1', 'dummyGroup2'] ); @@ -560,6 +610,10 @@ describe('Collection Tests', () => { background: false, } ); + expect(collection.assignItem).toHaveBeenCalledWith(dummyItem5, { + background: false, + }); + expect(collection.createGroup).not.toHaveBeenCalled(); expect(dummyGroup1.add).toHaveBeenCalledWith('1', { @@ -570,6 +624,10 @@ describe('Collection Tests', () => { method: 'push', background: false, }); + expect(dummyGroup1.add).toHaveBeenCalledWith('5', { + method: 'push', + background: false, + }); expect(dummyGroup2.add).toHaveBeenCalledWith('1', { method: 'push', background: false, @@ -578,6 +636,10 @@ describe('Collection Tests', () => { method: 'push', background: false, }); + expect(dummyGroup2.add).toHaveBeenCalledWith('5', { + method: 'push', + background: false, + }); expect(defaultGroup.add).toHaveBeenCalledWith('1', { method: 'push', background: false, @@ -586,17 +648,25 @@ describe('Collection Tests', () => { method: 'push', background: false, }); + expect(defaultGroup.add).toHaveBeenCalledWith('5', { + method: 'push', + background: false, + }); expect(collection.createSelector).not.toHaveBeenCalled(); }); - it("should call setData and shouldn't add Items to passed Groups if setData failed (default config)", () => { - collection.assignData = jest.fn(() => false); + it("should try to add data/item to Collection and shouldn't add it to passed Groups if adding data/item failed (default config)", () => { + collection.assignData = jest + .fn() + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + collection.assignItem = jest.fn(() => false); - collection.collect({ id: '1', name: 'frank' }, [ - 'dummyGroup1', - 'dummyGroup2', - ]); + collection.collect( + [{ id: '1', name: 'frank' }, dummyItem5, { id: '2', name: 'hans' }], + ['dummyGroup1', 'dummyGroup2'] + ); expect(collection.assignData).toHaveBeenCalledWith( { @@ -608,26 +678,72 @@ describe('Collection Tests', () => { background: false, } ); + expect(collection.assignData).toHaveBeenCalledWith( + { + id: '2', + name: 'hans', + }, + { + patch: false, + background: false, + } + ); + expect(collection.assignItem).toHaveBeenCalledWith(dummyItem5, { + background: false, + }); + expect(collection.createGroup).not.toHaveBeenCalled(); - expect(dummyGroup1.add).not.toHaveBeenCalled(); - expect(dummyGroup2.add).not.toHaveBeenCalled(); - expect(defaultGroup.add).not.toHaveBeenCalled(); + expect(dummyGroup1.add).not.toHaveBeenCalledWith('1', { + method: 'push', + background: false, + }); + expect(dummyGroup1.add).toHaveBeenCalledWith('2', { + method: 'push', + background: false, + }); + expect(dummyGroup1.add).not.toHaveBeenCalledWith('5', { + method: 'push', + background: false, + }); + expect(dummyGroup2.add).not.toHaveBeenCalledWith('1', { + method: 'push', + background: false, + }); + expect(dummyGroup2.add).toHaveBeenCalledWith('2', { + method: 'push', + background: false, + }); + expect(dummyGroup2.add).not.toHaveBeenCalledWith('5', { + method: 'push', + background: false, + }); + expect(defaultGroup.add).not.toHaveBeenCalledWith('1', { + method: 'push', + background: false, + }); + expect(defaultGroup.add).toHaveBeenCalledWith('2', { + method: 'push', + background: false, + }); + expect(defaultGroup.add).not.toHaveBeenCalledWith('5', { + method: 'push', + background: false, + }); expect(collection.createSelector).not.toHaveBeenCalled(); }); - it("should add Data to Collection and create Groups that doesn't exist yet (default config)", () => { - const notExistingGroup = new Group(collection); - notExistingGroup.add = jest.fn(); + it("should add data object to Collection and create Groups that doesn't exist yet (default config)", () => { + const newGroup = new Group(collection); + newGroup.add = jest.fn(); collection.assignData = jest.fn(() => true); collection.createGroup = jest.fn(function (groupKey) { - //@ts-ignore - this.groups[groupKey] = notExistingGroup; - return notExistingGroup as any; + collection.groups[groupKey] = newGroup; + return newGroup as any; }); - collection.collect({ id: '1', name: 'frank' }, 'notExistingGroup'); + collection.collect({ id: '1', name: 'frank' }, 'newGroup'); expect(collection.assignData).toHaveBeenCalledWith( { @@ -639,11 +755,11 @@ describe('Collection Tests', () => { background: false, } ); - expect(collection.createGroup).toHaveBeenCalledWith('notExistingGroup'); + expect(collection.createGroup).toHaveBeenCalledWith('newGroup'); expect(dummyGroup1.add).not.toHaveBeenCalled(); expect(dummyGroup2.add).not.toHaveBeenCalled(); - expect(notExistingGroup.add).toHaveBeenCalledWith('1', { + expect(newGroup.add).toHaveBeenCalledWith('1', { method: 'push', background: false, }); @@ -655,31 +771,31 @@ describe('Collection Tests', () => { expect(collection.createSelector).not.toHaveBeenCalled(); }); - it('should create Selector for each Item (config.select)', () => { + it('should add data object to Collection and create Selector for each Item (config.select)', () => { collection.assignData = jest.fn(() => true); + collection.assignItem = jest.fn(() => true); collection.collect( - [ - { id: '1', name: 'frank' }, - { id: '2', name: 'hans' }, - ], + [{ id: '1', name: 'frank' }, dummyItem5, { id: '2', name: 'hans' }], [], { select: true } ); expect(collection.createSelector).toHaveBeenCalledWith('1', '1'); + expect(collection.createSelector).toHaveBeenCalledWith('5', '5'); expect(collection.createSelector).toHaveBeenCalledWith('2', '2'); }); - it("should call 'forEachItem' for each Item (default config)", () => { - collection.assignData = jest.fn(() => true); + it("should add data object to Collection and call 'forEachItem()' for each Item (config.forEachItem)", () => { + collection.assignData = jest + .fn() + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + collection.assignItem = jest.fn(() => true); const forEachItemMock = jest.fn(); collection.collect( - [ - { id: '1', name: 'frank' }, - { id: '2', name: 'hans' }, - ], + [{ id: '1', name: 'frank' }, dummyItem5, { id: '2', name: 'hans' }], [], { forEachItem: forEachItemMock } ); @@ -687,12 +803,15 @@ describe('Collection Tests', () => { expect(forEachItemMock).toHaveBeenCalledWith( { id: '1', name: 'frank' }, '1', + false, 0 ); + expect(forEachItemMock).toHaveBeenCalledWith(dummyItem5, '5', true, 1); expect(forEachItemMock).toHaveBeenCalledWith( { id: '2', name: 'hans' }, '2', - 1 + true, + 2 ); }); }); From 1b3a5d873b528194e9c0a4c82524df632b8e5f61 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 1 Jun 2021 07:59:29 +0200 Subject: [PATCH 13/63] fixed typos --- packages/core/src/collection/index.ts | 104 +++++++++++------- packages/core/src/state/index.ts | 25 +++-- .../tests/unit/collection/collection.test.ts | 32 ++++-- 3 files changed, 97 insertions(+), 64 deletions(-) diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index f62b9fa3..6fae74ec 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -40,13 +40,18 @@ export class Collection { public isInstantiated = false; /** - * Class that holds a List of Objects with key and causes rerender on subscribed Components + * A Collection provides a reactive set of Information that we need to remember globally at a later point in time. + * While providing a toolkit to use and mutate this set of Information. * - * @public + * It is designed for arrays of data objects following the same pattern. * - * @param agileInstance - Instance of Agile the Collection belongs to + * Each of these data object must have a unique primaryKey to be correctly identified later. * - * @param config - Configuration + * [Learn more..](https://agile-ts.org/docs/core/collection/) + * + * @public + * @param agileInstance - Instance of Agile the Collection belongs to. + * @param config - Configuration object */ constructor(agileInstance: Agile, config: CollectionConfig = {}) { this.agileInstance = () => agileInstance; @@ -73,60 +78,68 @@ export class Collection { // Reselect Selector Items // Necessary because the selection of an Item - // hasn't worked with a not 'instantiated' Collection + // hasn't worked with a not 'instantiated' Collection before for (const key in this.selectors) this.selectors[key].reselect(); // Rebuild of Groups // Not necessary because if Items are added to the Collection, - // the Groups which contain these added Items get rebuilt. + // the Groups which contain these added Items are rebuilt. // for (const key in this.groups) this.groups[key].rebuild(); } /** + * Updates key/name identifier of Collection. + * * @public - * Set Key/Name of Collection + * @param value - New key/name identifier. */ public set key(value: CollectionKey | undefined) { this.setKey(value); } /** + * Returns key/name identifier of Collection. + * * @public - * Get Key/Name of Collection */ public get key(): CollectionKey | undefined { return this._key; } - //========================================================================================================= - // Set Key - //========================================================================================================= /** + * Updates key/name identifier of Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#setkey) + * * @public - * Set Key/Name of Collection - * @param value - New Key/Name of Collection + * @param value - New key/name identifier. */ public setKey(value: CollectionKey | undefined) { const oldKey = this._key; - // Update State Key + // Update Collection key this._key = value; - // Update Key in Persistent (only if oldKey equal to persistentKey -> otherwise the PersistentKey got formatted and will be set where other) + // Update key in Persistent (only if oldKey equal to persistentKey + // because otherwise the persistentKey is detached from the Collection key + // -> not managed by Collection anymore) if (value && this.persistent?._key === oldKey) this.persistent?.setKey(value); return this; } - //========================================================================================================= - // Group - //========================================================================================================= /** + * Creates a new Group without associating it to the Collection. + * + * Therefore, this function is intended for use in the Collection configuration object, + * where the `constructor()` takes care of the associating. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#group) + * * @public - * Group - Holds Items of this Collection - * @param initialItems - Initial ItemKeys of Group - * @param config - Config + * @param initialItems - Initial keys of Items that the Group should represent. + * @param config - Configuration object */ public Group( initialItems?: Array, @@ -141,14 +154,17 @@ export class Collection { return new Group(this, initialItems, config); } - //========================================================================================================= - // Selector - //========================================================================================================= /** + * Creates a new Selector without associating it to the Collection. + * + * Therefore, this function is intended for use in the Collection configuration object, + * where the `constructor()` takes care of the associating. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#selector) + * * @public - * Selector - Represents an Item of this Collection - * @param initialKey - Key of Item that the Selector represents - * @param config - Config + * @param initialKey - Initial key of the Item that the Selector should represent. + * @param config - Configuration object */ public Selector( initialKey: ItemKey, @@ -163,18 +179,18 @@ export class Collection { return new Selector(this, initialKey, config); } - //========================================================================================================= - // Init Groups - //========================================================================================================= /** + * Sets up the give Groups or Group keys and initializes the default Group. + * The Groups are then assigned to the Collection after a successful set up. + * * @internal - * Instantiates Groups + * @param groups - Groups or Group keys to be setup. */ - public initGroups(groups: { [key: string]: Group } | string[]) { + public initGroups(groups: { [key: string]: Group } | string[]): void { if (!groups) return; let groupsObject: { [key: string]: Group } = {}; - // If groups is Array of GroupNames transform it to Group Object + // If groups is Array of Group names/keys, create the Groups based these keys if (Array.isArray(groups)) { groups.forEach((groupKey) => { groupsObject[groupKey] = new Group(this, [], { @@ -188,25 +204,25 @@ export class Collection { key: this.config.defaultGroupKey, }); - // Set Key/Name of Group to property Name + // Assign missing key/name to Group based on the property key for (const key in groupsObject) if (groupsObject[key]._key == null) groupsObject[key].setKey(key); this.groups = groupsObject; } - //========================================================================================================= - // Init Selectors - //========================================================================================================= /** + * Sets up the give Selectors or Selector keys + * and assigns them to the Collection if they are valid. + * * @internal - * Instantiates Selectors + * @param selectors - Selectors or Selector keys to be setup. */ public initSelectors(selectors: { [key: string]: Selector } | string[]) { if (!selectors) return; let selectorsObject: { [key: string]: Selector } = {}; - // If selectors is Array of SelectorNames transform it to Selector Object + // If selectors is Array of Selector names/keys, create the Selectors based these keys if (Array.isArray(selectors)) { selectors.forEach((selectorKey) => { selectorsObject[selectorKey] = new Selector( @@ -219,7 +235,7 @@ export class Collection { }); } else selectorsObject = selectors; - // Set Key/Name of Selector to property Name + // Assign missing key/name to Selector based on the property key for (const key in selectorsObject) if (selectorsObject[key]._key == null) selectorsObject[key].setKey(key); @@ -230,8 +246,14 @@ export class Collection { // Collect //========================================================================================================= /** + * todo + * collect data objects or whole items + * adds these data objects to the Collection + * Each to collect data object needs a unique primaryKey (identifier) + * + * + * * @public - * Collect Item/s * @param data - Data that gets added to Collection * @param groupKeys - Add collected Item/s to certain Groups * @param config - Config diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index c33afdf3..ce4e6eaf 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -103,39 +103,42 @@ export class State { } /** + * Updates key/name identifier of State. + * * @public - * Set Key/Name of State + * @param value - New key/name identifier. */ public set key(value: StateKey | undefined) { this.setKey(value); } /** + * Returns key/name identifier of State. + * * @public - * Get Key/Name of State */ public get key(): StateKey | undefined { return this._key; } - //========================================================================================================= - // Set Key - //========================================================================================================= /** - * @internal - * Updates Key/Name of State - * @param value - New Key/Name of State + * Updates key/name identifier of State. + * + * @public + * @param value - New key/name identifier. */ public setKey(value: StateKey | undefined): this { const oldKey = this._key; - // Update State Key + // Update State key this._key = value; - // Update Key in Observer + // Update key in Observer this.observer._key = value; - // Update Key in Persistent (only if oldKey equal to persistentKey -> otherwise the PersistentKey got formatted and will be set where other) + // Update key in Persistent (only if oldKey equal to persistentKey + // because otherwise the persistentKey is detached from the State key + // -> not managed by State anymore) if (value && this.persistent?._key === oldKey) this.persistent?.setKey(value); diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index c79dcf57..e8057b50 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -1555,7 +1555,7 @@ describe('Collection Tests', () => { ); }); - it("should return and track created reference Item if Item doesn't exist yet", () => { + it("should return and track created reference Item if searched Item doesn't exist yet", () => { const response = collection.getItemWithReference('notExistingItem'); expect(response).toBe(placeholderItem); @@ -1563,7 +1563,9 @@ describe('Collection Tests', () => { 'notExistingItem', true ); - expect(ComputedTracker.tracked).toHaveBeenCalledWith(response.observer); + expect(ComputedTracker.tracked).toHaveBeenCalledWith( + placeholderItem.observer + ); }); }); @@ -1626,6 +1628,7 @@ describe('Collection Tests', () => { expect(collection.data).toHaveProperty('2'); expect(collection.data['2']).toStrictEqual(expect.any(Item)); + expect(collection.data['2']._key).toBe('2'); expect(ComputedTracker.tracked).toHaveBeenCalledTimes(1); expect(ComputedTracker.tracked).not.toHaveBeenCalledWith( @@ -2571,12 +2574,12 @@ describe('Collection Tests', () => { }); expect(response).toBeTruthy(); - expect(collection.size).toBe(2); // Increased by assignItem + expect(collection.size).toBe(2); // Increased by assignItem() expect(collection.assignItem).toHaveBeenCalledWith(expect.any(Item), { background: false, }); - // Check if Item, assignItem was called with, has the correct data + // Check if Item, assignItem() was called with, has the correct data expect(collection.data).toHaveProperty('dummyItem2'); expect(collection.data['dummyItem2']._value).toStrictEqual({ id: 'dummyItem2', @@ -2597,12 +2600,12 @@ describe('Collection Tests', () => { ); expect(response).toBeTruthy(); - expect(collection.size).toBe(2); // Increased by assignItem + expect(collection.size).toBe(2); // Increased by assignItem() expect(collection.assignItem).toHaveBeenCalledWith(expect.any(Item), { background: true, }); - // Check if Item, assignItem was called with, has the correct data + // Check if Item, assignItem() was called with, has the correct data expect(collection.data).toHaveProperty('dummyItem2'); expect(collection.data['dummyItem2']._value).toStrictEqual({ id: 'dummyItem2', @@ -2630,12 +2633,12 @@ describe('Collection Tests', () => { const response = collection.assignData({ name: 'Frank' } as any); expect(response).toBeTruthy(); - expect(collection.size).toBe(2); // Increased by assignItem + expect(collection.size).toBe(2); // Increased by assignItem() expect(collection.assignItem).toHaveBeenCalledWith(expect.any(Item), { background: false, }); - // Check if Item, assignItem was called with, has the correct data + // Check if Item, assignItem() was called with, has the correct data expect(collection.data).toHaveProperty('randomDummyId'); expect(collection.data['randomDummyId']._value).toStrictEqual({ id: 'randomDummyId', @@ -2649,7 +2652,7 @@ describe('Collection Tests', () => { ]); }); - it('should update Item with valid data via set (default config)', () => { + it('should update existing Item with valid data via set (default config)', () => { const response = collection.assignData({ id: 'dummyItem1', name: 'Dieter', @@ -2669,7 +2672,7 @@ describe('Collection Tests', () => { LogMock.hasNotLogged('warn'); }); - it('should update Item with valid data via set (config.background = true)', () => { + it('should update existing Item with valid data via set (config.background = true)', () => { const response = collection.assignData( { id: 'dummyItem1', @@ -2692,7 +2695,7 @@ describe('Collection Tests', () => { LogMock.hasNotLogged('warn'); }); - it('should update Item with valid data via patch (config.patch = true, background: true)', () => { + it('should update existing Item with valid data via patch (config.patch = true, background: true)', () => { const response = collection.assignData( { id: 'dummyItem1', @@ -2715,7 +2718,7 @@ describe('Collection Tests', () => { LogMock.hasNotLogged('warn'); }); - it('should update placeholder Item with valid data and increase size', () => { + it('should update placeholder Item with valid data and increase Collection size (default config)', () => { dummyItem1.isPlaceholder = true; const response = collection.assignData({ @@ -2773,6 +2776,7 @@ describe('Collection Tests', () => { ); expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); + expect(toAddDummyItem2._key).toBe(2); LogMock.hasNotLogged('error'); LogMock.hasNotLogged('warn'); @@ -2795,6 +2799,7 @@ describe('Collection Tests', () => { ); expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); + expect(toAddDummyItem2._key).toBe(2); LogMock.hasNotLogged('error'); LogMock.hasNotLogged('warn'); @@ -2847,6 +2852,7 @@ describe('Collection Tests', () => { ).not.toHaveBeenCalled(); expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); + expect(toAddDummyItem2._key).toBe(2); LogMock.hasLoggedCode('1B:03:06', [ collection._key, @@ -2867,6 +2873,7 @@ describe('Collection Tests', () => { ).not.toHaveBeenCalled(); expect(dummyItem1.patch).not.toHaveBeenCalled(); + expect(dummyItem1._key).toBe(1); LogMock.hasNotLogged('error'); LogMock.hasNotLogged('warn'); @@ -2887,6 +2894,7 @@ describe('Collection Tests', () => { ); expect(dummyItem1.patch).not.toHaveBeenCalled(); + expect(dummyItem1._key).toBe(2); LogMock.hasNotLogged('error'); LogMock.hasNotLogged('warn'); From 37d1495f6225aed50c42e4a1e3576719cca8ed38 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 2 Jun 2021 08:17:48 +0200 Subject: [PATCH 14/63] fixed typos --- packages/core/src/collection/index.ts | 120 ++++++++++++++------------ 1 file changed, 63 insertions(+), 57 deletions(-) diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 6fae74ec..15abcd55 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -138,7 +138,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#group) * * @public - * @param initialItems - Initial keys of Items that the Group should represent. + * @param initialItems - Initial keys of Items to be represented by the Group. * @param config - Configuration object */ public Group( @@ -163,7 +163,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#selector) * * @public - * @param initialKey - Initial key of the Item that the Selector should represent. + * @param initialKey - Initial key of Items to be represented by the Selector. * @param config - Configuration object */ public Selector( @@ -184,7 +184,7 @@ export class Collection { * The Groups are then assigned to the Collection after a successful set up. * * @internal - * @param groups - Groups or Group keys to be setup. + * @param groups - Entire Groups or Group keys to be set up. */ public initGroups(groups: { [key: string]: Group } | string[]): void { if (!groups) return; @@ -216,7 +216,7 @@ export class Collection { * and assigns them to the Collection if they are valid. * * @internal - * @param selectors - Selectors or Selector keys to be setup. + * @param selectors - Entire Selectors or Selector keys to be set up. */ public initSelectors(selectors: { [key: string]: Selector } | string[]) { if (!selectors) return; @@ -242,21 +242,26 @@ export class Collection { this.selectors = selectorsObject; } - //========================================================================================================= - // Collect - //========================================================================================================= /** - * todo - * collect data objects or whole items - * adds these data objects to the Collection - * Each to collect data object needs a unique primaryKey (identifier) + * Appends a new data object or whole Items following the same pattern to the end of the Collection. + * + * Each collected data object and Item requires a unique identifier at the primaryKey property + * to be properly identified later. By default, 'id' is the primaryKey property. + * + * For example, a valid data object would look like this: + * + * {id: 1, name: 'jeff'} * + * 'id': PrimaryKey property with a unique identifier '1' * + * 'name': A actual data property in this case 'jeff'. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#collect) * * @public - * @param data - Data that gets added to Collection - * @param groupKeys - Add collected Item/s to certain Groups - * @param config - Config + * @param data - Data object or Items to be added. + * @param groupKeys - Given data objects or Items to be added to certain Group/s. + * @param config - Configuration object */ public collect( data: DataType | Item | Array>, @@ -274,7 +279,7 @@ export class Collection { select: false, }); - // Add default GroupKey, because Items get always added to default Group + // Add default groupKey, since all Items are added to the default Group if (!_groupKeys.includes(defaultGroupKey)) _groupKeys.push(defaultGroupKey); // Create not existing Groups @@ -300,7 +305,7 @@ export class Collection { itemKey = data[primaryKey]; } - // Add ItemKey to provided Groups and create corresponding Selector + // Add itemKey to provided Groups and create corresponding Selector if (success) { _groupKeys.forEach((groupKey) => { this.getGroup(groupKey)?.add(itemKey, { @@ -318,15 +323,15 @@ export class Collection { return this; } - //========================================================================================================= - // Update - //========================================================================================================= /** + * Updates Item data object at give identifier key, if it exists. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#update) + * * @public - * Updates Item at provided Key - * @param itemKey - ItemKey of Item that gets updated - * @param changes - Changes that will be merged into the Item (flatMerge) - * @param config - Config + * @param itemKey - ItemKey of Item to be updated. + * @param changes - Object with changes to be merged into the Item data. + * @param config - Configuration object */ public update( itemKey: ItemKey, @@ -340,6 +345,7 @@ export class Collection { background: false, }); + // Validate passed data if (item == null) { LogCodeManager.log('1B:03:00', [itemKey, this._key]); return undefined; @@ -351,17 +357,17 @@ export class Collection { const oldItemKey = item._value[primaryKey]; const newItemKey = changes[primaryKey] || oldItemKey; - const updateItemKey = oldItemKey !== newItemKey; - // Update ItemKey - if (updateItemKey) + // Update itemKey if the new itemKey differs from the old one + if (oldItemKey !== newItemKey) this.updateItemKey(oldItemKey, newItemKey, { background: config.background, }); - // Patch changes into Item + // Patch changes into Item data object if (config.patch) { - // Delete primaryKey from 'changes' because if it has changed, it gets properly updated in 'updateItemKey' (see above) + // Delete primaryKey property from 'changes object' because if it has changed, + // it is correctly updated in the above called 'updateItemKey()' method if (changes[primaryKey]) delete changes[primaryKey]; let patchConfig: { addNewProperties?: boolean } = @@ -370,22 +376,19 @@ export class Collection { addNewProperties: true, }); - // Apply changes to Item item.patch(changes as any, { background: config.background, addNewProperties: patchConfig.addNewProperties, }); } - - // Set changes into Item - if (!config.patch) { - // To make sure that the primaryKey doesn't differ from the changes object primaryKey + // Apply changes to Item data object + else { + // Ensure that the current Item identifier isn't different from the 'changes object' itemKey if (changes[this.config.primaryKey] !== itemKey) { changes[this.config.primaryKey] = itemKey; LogCodeManager.log('1B:02:02', [], changes); } - // Apply changes to Item item.set(changes as any, { background: config.background, }); @@ -394,21 +397,20 @@ export class Collection { return item; } - //========================================================================================================= - // Create Group - //========================================================================================================= /** + * Creates a new Group and associates it to the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createGroup) + * * @public - * Creates new Group that can hold Items of Collection - * @param groupKey - Name/Key of Group - * @param initialItems - Initial ItemKeys of Group + * @param groupKey - Unique Group identifier of the new Group. + * @param initialItems - Initial keys of Items to be represented by the Group. */ public createGroup( groupKey: GroupKey, initialItems: Array = [] ): Group { let group = this.getGroup(groupKey, { notExisting: true }); - if (!this.isInstantiated) LogCodeManager.log('1B:02:03'); // Check if Group already exists @@ -421,21 +423,22 @@ export class Collection { return group; } - // Create Group + // Create new Group group = new Group(this, initialItems, { key: groupKey }); this.groups[groupKey] = group; return group; } - //========================================================================================================= - // Has Group - //========================================================================================================= /** + * Returns a boolean indicating whether an Group with the specified groupKey + * exists in the Collection or not. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasgroup) + * * @public - * Check if Group exists in Collection - * @param groupKey - Key/Name of Group - * @param config - Config + * @param groupKey - Key/Name identifier of Group. + * @param config - Configuration object */ public hasGroup( groupKey: GroupKey | undefined, @@ -445,13 +448,13 @@ export class Collection { } /** - * Retrieves a single Group by key/name. + * Retrieves a single Group with the specified key/name identifier from the Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroup) * * @public - * @param groupKey - key/name Group identifier - * @param config - Configuration + * @param groupKey - Key/Name identifier of Group. + * @param config - Configuration object */ public getGroup( groupKey: GroupKey | undefined, @@ -465,19 +468,22 @@ export class Collection { const group = groupKey ? this.groups[groupKey] : undefined; // Check if Group exists - if (group == null || (!config.notExisting && group.isPlaceholder)) + if (group == null || (!config.notExisting && group.exists)) return undefined; ComputedTracker.tracked(group.observer); return group; } - //========================================================================================================= - // Get Default Group - //========================================================================================================= /** + * Retrieves the default Group from the Collection. + * + * Every Collection has a default Group, + * which represents the main pattern of the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getdefaultgroup) + * * @public - * Get default Group of Collection */ public getDefaultGroup(): Group | undefined { return this.getGroup(this.config.defaultGroupKey); @@ -606,7 +612,7 @@ export class Collection { const selector = selectorKey ? this.selectors[selectorKey] : undefined; // Check if Selector exists - if (selector == null || (!config.notExisting && selector.isPlaceholder)) + if (selector == null || (!config.notExisting && selector.exists)) return undefined; ComputedTracker.tracked(selector.observer); From f81ce354cfbeaa71c3a16bc37c1393f86fef702c Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 2 Jun 2021 08:42:42 +0200 Subject: [PATCH 15/63] fixed collection tests --- packages/core/src/collection/index.ts | 4 +-- .../tests/unit/collection/collection.test.ts | 26 ++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 15abcd55..1e2f4c37 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -468,7 +468,7 @@ export class Collection { const group = groupKey ? this.groups[groupKey] : undefined; // Check if Group exists - if (group == null || (!config.notExisting && group.exists)) + if (group == null || (!config.notExisting && !group.exists)) return undefined; ComputedTracker.tracked(group.observer); @@ -612,7 +612,7 @@ export class Collection { const selector = selectorKey ? this.selectors[selectorKey] : undefined; // Check if Selector exists - if (selector == null || (!config.notExisting && selector.exists)) + if (selector == null || (!config.notExisting && !selector.exists)) return undefined; ComputedTracker.tracked(selector.observer); diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index e8057b50..500fb289 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -1321,9 +1321,15 @@ describe('Collection Tests', () => { describe('getSelector function tests', () => { let dummySelector: Selector; + let dummyItem1: Item; beforeEach(() => { - dummySelector = new Selector(collection, 'dummyItem', { + dummyItem1 = new Item(collection, { id: 'dummyItem1', name: 'frank' }); + collection.data = { + ['dummyItem1']: dummyItem1, + }; + + dummySelector = new Selector(collection, 'dummyItem1', { key: 'dummySelector', }); collection.selectors = { @@ -1374,9 +1380,15 @@ describe('Collection Tests', () => { describe('getSelectorWithReference function tests', () => { let dummySelector: Selector; + let dummyItem1: Item; beforeEach(() => { - dummySelector = new Selector(collection, 'dummyItem', { + dummyItem1 = new Item(collection, { id: 'dummyItem1', name: 'frank' }); + collection.data = { + ['dummyItem1']: dummyItem1, + }; + + dummySelector = new Selector(collection, 'dummyItem1', { key: 'dummySelector', }); collection.selectors = { @@ -2776,7 +2788,7 @@ describe('Collection Tests', () => { ); expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); - expect(toAddDummyItem2._key).toBe(2); + expect(toAddDummyItem2._key).toBe('dummyItem2'); LogMock.hasNotLogged('error'); LogMock.hasNotLogged('warn'); @@ -2799,7 +2811,7 @@ describe('Collection Tests', () => { ); expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); - expect(toAddDummyItem2._key).toBe(2); + expect(toAddDummyItem2._key).toBe('dummyItem2'); LogMock.hasNotLogged('error'); LogMock.hasNotLogged('warn'); @@ -2852,7 +2864,7 @@ describe('Collection Tests', () => { ).not.toHaveBeenCalled(); expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); - expect(toAddDummyItem2._key).toBe(2); + expect(toAddDummyItem2._key).toBe('dummyItem2'); LogMock.hasLoggedCode('1B:03:06', [ collection._key, @@ -2873,7 +2885,7 @@ describe('Collection Tests', () => { ).not.toHaveBeenCalled(); expect(dummyItem1.patch).not.toHaveBeenCalled(); - expect(dummyItem1._key).toBe(1); + expect(dummyItem1._key).toBe('dummyItem1'); LogMock.hasNotLogged('error'); LogMock.hasNotLogged('warn'); @@ -2894,7 +2906,7 @@ describe('Collection Tests', () => { ); expect(dummyItem1.patch).not.toHaveBeenCalled(); - expect(dummyItem1._key).toBe(2); + expect(dummyItem1._key).toBe('dummyItem1'); LogMock.hasNotLogged('error'); LogMock.hasNotLogged('warn'); From 9a119d79d052be445bd2c59a706bcf0890cd19d1 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 2 Jun 2021 18:59:25 +0200 Subject: [PATCH 16/63] fixed typos --- .../src/collection/collection.persistent.ts | 2 +- packages/core/src/collection/index.ts | 526 ++++++++++-------- packages/core/src/state/index.ts | 43 +- 3 files changed, 331 insertions(+), 240 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 0ee6c8b3..9b73c336 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -314,7 +314,7 @@ export class CollectionPersistent< * Adds and removes Items from the Storage based on the Group value. * * @internal - * @param group - Group whose Items should be dynamically added and removed from the Storage. + * @param group - Group whose Items are to be dynamically added or removed from the Storage. * @param storageItemKey - Prefix key of persisted Collection Instances. | default = Persistent.key | */ public rebuildStorageSideEffect( diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 1e2f4c37..fbdedc79 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -28,24 +28,25 @@ export class Collection { public config: CollectionConfigInterface; private initialConfig: CreateCollectionConfigInterface; - public size = 0; // Amount of Items stored in Collection + public size = 0; // Amount of Items stored in the Collection public data: { [key: string]: Item } = {}; // Collection Data public _key?: CollectionKey; - public isPersisted = false; // If Collection can be stored in Agile Storage (-> successfully integrated persistent) - public persistent: CollectionPersistent | undefined; // Manages storing Collection Value into Storage + public isPersisted = false; // Whether Collection is persisted in any external Storage + public persistent: CollectionPersistent | undefined; // Manages persisting Collection 'value' public groups: { [key: string]: Group } = {}; public selectors: { [key: string]: Selector } = {}; - public isInstantiated = false; + public isInstantiated = false; // Whether the Collection is instantiated completely /** - * A Collection provides a reactive set of Information that we need to remember globally at a later point in time. + * A Collection provides a reactive set of Information + * that we need to remember globally at a later point in time. * While providing a toolkit to use and mutate this set of Information. * * It is designed for arrays of data objects following the same pattern. * - * Each of these data object must have a unique primaryKey to be correctly identified later. + * Each of these data object must have a unique `primaryKey` to be correctly identified later. * * [Learn more..](https://agile-ts.org/docs/core/collection/) * @@ -132,7 +133,7 @@ export class Collection { /** * Creates a new Group without associating it to the Collection. * - * Therefore, this function is intended for use in the Collection configuration object, + * This way of creating a Group is intended for use in the Collection configuration object, * where the `constructor()` takes care of the associating. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#group) @@ -157,13 +158,13 @@ export class Collection { /** * Creates a new Selector without associating it to the Collection. * - * Therefore, this function is intended for use in the Collection configuration object, + * This way of creating a Selector is intended for use in the Collection configuration object, * where the `constructor()` takes care of the associating. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#selector) * * @public - * @param initialKey - Initial key of Items to be represented by the Selector. + * @param initialKey - Initial key of Item to be represented by the Selector. * @param config - Configuration object */ public Selector( @@ -180,8 +181,11 @@ export class Collection { } /** - * Sets up the give Groups or Group keys and initializes the default Group. - * The Groups are then assigned to the Collection after a successful set up. + * Sets up the specified Groups or Group keys + * and assigns them to the Collection if they are valid. + * + * It also assigns the default Group to the Collection. + * The default Group reflects the default pattern of the Collection. * * @internal * @param groups - Entire Groups or Group keys to be set up. @@ -212,7 +216,7 @@ export class Collection { } /** - * Sets up the give Selectors or Selector keys + * Sets up the specified Selectors or Selector keys * and assigns them to the Collection if they are valid. * * @internal @@ -243,24 +247,23 @@ export class Collection { } /** - * Appends a new data object or whole Items following the same pattern to the end of the Collection. - * - * Each collected data object and Item requires a unique identifier at the primaryKey property - * to be properly identified later. By default, 'id' is the primaryKey property. - * - * For example, a valid data object would look like this: + * Appends new data objects following the same pattern to the end of the Collection. * - * {id: 1, name: 'jeff'} + * Each collected `data object` requires a unique identifier at the primaryKey property (by default 'id') + * to be correctly identified later. * - * 'id': PrimaryKey property with a unique identifier '1' - * - * 'name': A actual data property in this case 'jeff'. + * For example, if we collect some kind of user object, + * it must contain such unique identifier at 'id' + * to be added to the Collection. + * ``` + * MY_COLLECTION.collect({id: '1', name: 'jeff'}); + * ``` * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#collect) * * @public - * @param data - Data object or Items to be added. - * @param groupKeys - Given data objects or Items to be added to certain Group/s. + * @param data - Data objects or entire Items to be added. + * @param groupKeys - Group/s to which the specified data objects or Items are to be added. * @param config - Configuration object */ public collect( @@ -324,13 +327,14 @@ export class Collection { } /** - * Updates Item data object at give identifier key, if it exists. + * Updates the Item `data object` with the specified `object with changes`, if the Item exists. + * By default the `object with changes` is merged into the Item `data object`. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#update) * * @public - * @param itemKey - ItemKey of Item to be updated. - * @param changes - Object with changes to be merged into the Item data. + * @param itemKey - Key/Name identifier of Item to be updated. + * @param changes - Object with changes to be merged into the Item data object. * @param config - Configuration object */ public update( @@ -403,7 +407,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createGroup) * * @public - * @param groupKey - Unique Group identifier of the new Group. + * @param groupKey - Unique identifier of the to create Group. * @param initialItems - Initial keys of Items to be represented by the Group. */ public createGroup( @@ -431,7 +435,7 @@ export class Collection { } /** - * Returns a boolean indicating whether an Group with the specified groupKey + * Returns a boolean indicating whether a Group with the specified `groupKey` * exists in the Collection or not. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasgroup) @@ -450,6 +454,8 @@ export class Collection { /** * Retrieves a single Group with the specified key/name identifier from the Collection. * + * If the to retrieve Group doesn't exist, `undefined` is returned. + * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroup) * * @public @@ -464,10 +470,10 @@ export class Collection { notExisting: false, }); - // Get Group + // Retrieve Group const group = groupKey ? this.groups[groupKey] : undefined; - // Check if Group exists + // Check if retrieved Group exists if (group == null || (!config.notExisting && !group.exists)) return undefined; @@ -478,8 +484,8 @@ export class Collection { /** * Retrieves the default Group from the Collection. * - * Every Collection has a default Group, - * which represents the main pattern of the Collection. + * Every Collection should have a default Group, + * which represents the default pattern of the Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getdefaultgroup) * @@ -489,13 +495,17 @@ export class Collection { return this.getGroup(this.config.defaultGroupKey); } - //========================================================================================================= - // Get Group With Reference - //========================================================================================================= /** + * Retrieves a single Group with the specified key/name identifier from the Collection. + * + * If the to retrieve Group doesn't exist, a reference Group is returned. + * This has the advantage that Components that have the reference Group bound to themselves + * are rerenderd when the original Group is created. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupwithreference) + * * @public - * Get Group by Key/Name or a Reference to it if it doesn't exist yet - * @param groupKey - Name/Key of Group + * @param groupKey - Key/Name identifier of Group. */ public getGroupWithReference(groupKey: GroupKey): Group { let group = this.getGroup(groupKey, { notExisting: true }); @@ -513,35 +523,34 @@ export class Collection { return group; } - //========================================================================================================= - // Remove Group - //========================================================================================================= /** + * Removes a Group with the specified identifier from the Collection, + * if it exists in the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removegroup) + * * @public - * Removes Group by Key/Name - * @param groupKey - Name/Key of Group + * @param groupKey - Key/Name identifier of Group. */ public removeGroup(groupKey: GroupKey): this { - if (this.groups[groupKey] == null) return this; - delete this.groups[groupKey]; + if (this.groups[groupKey] != null) delete this.groups[groupKey]; return this; } - //========================================================================================================= - // Create Selector - //========================================================================================================= /** + * Creates a new Selector and associates it to the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createSelector) + * * @public - * Creates new Selector that represents an Item of the Collection - * @param selectorKey - Name/Key of Selector - * @param itemKey - Key of Item which the Selector represents + * @param selectorKey - Unique identifier of the to create Selector. + * @param itemKey - Initial key of Item to be represented by the Selector. */ public createSelector( selectorKey: SelectorKey, itemKey: ItemKey ): Selector { let selector = this.getSelector(selectorKey, { notExisting: true }); - if (!this.isInstantiated) LogCodeManager.log('1B:02:04'); // Check if Selector already exists @@ -554,7 +563,7 @@ export class Collection { return selector; } - // Create Selector + // Create new Selector selector = new Selector(this, itemKey, { key: selectorKey, }); @@ -563,26 +572,34 @@ export class Collection { return selector; } - //========================================================================================================= - // Select - //========================================================================================================= /** + * Creates a new Selector and associates it to the Collection. + * + * The specified `itemKey` is used as the unique identifier key of the new Selector. + * ``` + * MY_COLLECTION.select('1'); + * // is equivalent to + * MY_COLLECTION.createSelector('1', '1'); + * ``` + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#select) + * * @public - * Creates new Selector that represents an Item of the Collection - * @param itemKey - Key of Item which the Selector represents + * @param itemKey - ItemKey to be selected. */ public select(itemKey: ItemKey): Selector { return this.createSelector(itemKey, itemKey); } - //========================================================================================================= - // Has Selector - //========================================================================================================= /** + * Returns a boolean indicating whether a Selector with the specified `selectorKey` + * exists in the Collection or not. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasselector) + * * @public - * Check if Selector exists in Collection - * @param selectorKey - Key/Name of Selector - * @param config - Config + * @param selectorKey - Key/Name identifier of Selector. + * @param config - Configuration object */ public hasSelector( selectorKey: SelectorKey | undefined, @@ -591,14 +608,16 @@ export class Collection { return !!this.getSelector(selectorKey, config); } - //========================================================================================================= - // Get Selector - //========================================================================================================= /** + * Retrieves a single Selector with the specified key/name identifier from the Collection. + * + * If the to retrieve Selector doesn't exist, `undefined` is returned. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselector) + * * @public - * Get Selector by Key/Name - * @param selectorKey - Key/Name of Selector - * @param config - Config + * @param selectorKey - Key/Name identifier of Selector. + * @param config - Configuration object */ public getSelector( selectorKey: SelectorKey | undefined, @@ -619,13 +638,17 @@ export class Collection { return selector; } - //========================================================================================================= - // Get Selector With Reference - //========================================================================================================= /** + * Retrieves a single Selector with the specified key/name identifier from the Collection. + * + * If the to retrieve Selector doesn't exist, a reference Selector is returned. + * This has the advantage that Components that have the reference Selector bound to themselves + * are rerenderd when the original Selector is created. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorwithreference) + * * @public - * Get Selector by Key/Name or a Reference to it if it doesn't exist yet - * @param selectorKey - Name/Key of Selector + * @param selectorKey - Key/Name identifier of Selector. */ public getSelectorWithReference( selectorKey: SelectorKey @@ -649,29 +672,32 @@ export class Collection { return selector; } - //========================================================================================================= - // Remove Selector - //========================================================================================================= /** + * Removes a Selector with the specified identifier from the Collection, + * if it exists in the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeselector) + * * @public - * Removes Selector by Key/Name - * @param selectorKey - Name/Key of Selector + * @param selectorKey - Key/Name identifier of Selector. */ public removeSelector(selectorKey: SelectorKey): this { - if (this.selectors[selectorKey] == null) return this; - this.selectors[selectorKey].unselect(); // Unselects current selected Item - delete this.selectors[selectorKey]; + if (this.selectors[selectorKey] != null) { + this.selectors[selectorKey].unselect(); + delete this.selectors[selectorKey]; + } return this; } - //========================================================================================================= - // Has Item - //========================================================================================================= /** + * Returns a boolean indicating whether a Item with the specified `itemKey` + * exists in the Collection or not. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasitem) + * * @public - * Check if Item exists in Collection - * @param itemKey - Key/Name of Item - * @param config - Config + * @param itemKey - Key/Name identifier of Item. + * @param config - Configuration object */ public hasItem( itemKey: ItemKey | undefined, @@ -680,14 +706,16 @@ export class Collection { return !!this.getItem(itemKey, config); } - //========================================================================================================= - // Get Item by Id - //========================================================================================================= /** + * Retrieves a single Item with the specified key/name identifier from the Collection. + * + * If the to retrieve Item doesn't exist, `undefined` is returned. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitem) + * * @public - * Get Item by Key/Name - * @param itemKey - ItemKey of Item - * @param config - Config + * @param itemKey - Key/Name identifier of Item. + * @param config - Configuration object */ public getItem( itemKey: ItemKey | undefined, @@ -708,9 +736,16 @@ export class Collection { } /** + * Retrieves a single Item with the specified key/name identifier from the Collection. + * + * If the to retrieve Item doesn't exist, a reference Item is returned. + * This has the advantage that Components that have the reference Item bound to themselves + * are rerenderd when the original Item is created. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemwithreference) + * * @public - * Get Item by Key/Name or a Reference to it if it doesn't exist yet - * @param itemKey - Key/Name of Item + * @param itemKey - Key/Name identifier of Item. */ public getItemWithReference(itemKey: ItemKey): Item { let item = this.getItem(itemKey, { notExisting: true }); @@ -724,16 +759,17 @@ export class Collection { /** * Creates a placeholder Item - * that can be used to hold a reference to an Item that doesn't exist yet. + * that can be used to hold a reference to a not existing Item. * * @internal - * @param itemKey - Key/Name identifier of the Item to be created. + * @param itemKey - Unique identifier of the to create placeholder Item. * @param addToCollection - Whether the created Item should be added to the Collection. */ public createPlaceholderItem( itemKey: ItemKey, addToCollection = false ): Item { + // Create placeholder Item const item = new Item( this, { @@ -743,6 +779,7 @@ export class Collection { { isPlaceholder: true } ); + // Add placeholder Item to Collection if ( addToCollection && !Object.prototype.hasOwnProperty.call(this.data, itemKey) @@ -753,14 +790,17 @@ export class Collection { return item; } - //========================================================================================================= - // Get Value by Id - //========================================================================================================= /** + * Retrieves the value (data object) of a single Item + * with the specified key/name identifier from the Collection. + * + * If the to retrieve Item doesn't exist, `undefined` is returned. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemvalue) + * * @public - * Get Value of Item by Key/Name - * @param itemKey - ItemKey of Item that holds the Value - * @param config - Config + * @param itemKey - Key/Name identifier of Item. + * @param config - Configuration object */ public getItemValue( itemKey: ItemKey | undefined, @@ -771,13 +811,18 @@ export class Collection { return item.value; } - //========================================================================================================= - // Get All Items - //========================================================================================================= /** + * Retrieves all Items from the Collection. + * ``` + * MY_COLLECTION.getAllItems(); + * // is equivalent to + * MY_COLLECTION.getDefaultGroup().items; + * ``` + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getallitems) + * * @public - * Get all Items of Collection - * @param config - Config + * @param config - Configuration object */ public getAllItems(config: HasConfigInterface = {}): Array> { config = defineConfig(config, { @@ -793,40 +838,52 @@ export class Collection { } else { // Why defaultGroup Items and not all .exists === true Items? // Because the default Group keeps track of all existing Items - // It also does control the Collection output in useAgile() and should do it here too + // It also does control the Collection output in useAgile() + // and therefore should do it here too. items = defaultGroup?.items || []; } return items; } - //========================================================================================================= - // Get All Item Values - //========================================================================================================= /** + * Retrieves the values (data objects) of all Items from the Collection. + * ``` + * MY_COLLECTION.getAllItemValues(); + * // is equivalent to + * MY_COLLECTION.getDefaultGroup().output; + * ``` + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getallitemvalues) + * * @public - * Get all Values of Items in a Collection - * @param config - Config + * @param config - Configuration object */ public getAllItemValues(config: HasConfigInterface = {}): Array { const items = this.getAllItems(config); return items.map((item) => item.value); } - //========================================================================================================= - // Persist - //========================================================================================================= /** + * Preserves the Collection `value` in the corresponding external Storage. + * + * The Collection key/name is used as the unique identifier for the Persistent. + * If that is not desired, please specify a unique identifier for the Persistent. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#persist) + * * @public - * Stores Collection Value into Agile Storage permanently - * @param config - Config + * @param config - Configuration object */ public persist(config?: CollectionPersistentConfigInterface): this; /** + * Preserves the Collection `value` in the corresponding external Storage. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#persist) + * * @public - * Stores Collection Value into Agile Storage permanently - * @param key - Key/Name of created Persistent (Note: Key required if Collection has no set Key!) - * @param config - Config + * @param key - Key/Name identifier of Persistent. + * @param config - Configuration object */ public persist( key?: StorageKey, @@ -856,7 +913,7 @@ export class Collection { // Check if Collection is already persisted if (this.persistent != null && this.isPersisted) return this; - // Create persistent -> Persist Value + // Create Persistent -> persist value this.persistent = new CollectionPersistent(this, { instantiate: _config.loadValue, storageKeys: _config.storageKeys, @@ -867,67 +924,72 @@ export class Collection { return this; } - //========================================================================================================= - // On Load - //========================================================================================================= /** + * Fires immediately after the persisted `value` + * is loaded into the Collection from a corresponding external Storage. + * + * Registering this callback only makes sense + * when the Collection is [persisted](https://agile-ts.org/docs/core/collection/methods/#persist) in an external Storage. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#onload) + * * @public - * Callback Function that gets called if the persisted Value gets loaded into the Collection for the first Time - * Note: Only useful for persisted Collections! - * @param callback - Callback Function + * @param callback - Callback function */ public onLoad(callback: (success: boolean) => void): this { if (!this.persistent) return this; - // Check if Callback is valid Function + // Check if provided callback is valid function if (!isFunction(callback)) { LogCodeManager.log('00:03:01', ['OnLoad Callback', 'function']); return this; } + // Register provided callback this.persistent.onLoad = callback; - // If Collection is already 'isPersisted' the loading was successful -> callback can be called + // If Collection is already persisted ('isPersisted') fire provided callback immediately if (this.isPersisted) callback(true); return this; } - //========================================================================================================= - // Get Group Count - //========================================================================================================= /** + * Returns the count of registered Groups in the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupcount) + * * @public - * Get count of registered Groups in Collection */ public getGroupCount(): number { let size = 0; - for (const group in this.groups) size++; + Object.keys(this.groups).map(() => size++); return size; } - //========================================================================================================= - // Get Selector Count - //========================================================================================================= /** + * Returns the count of registered Selectors in the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorcount) + * * @public - * Get count of registered Selectors in Collection */ public getSelectorCount(): number { let size = 0; - for (const selector in this.selectors) size++; + Object.keys(this.selectors).map(() => size++); return size; } - //========================================================================================================= - // Reset - //========================================================================================================= /** + * Removes all Items from the Collection + * and resets the Groups and Selectors. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#reset) + * * @public - * Resets this Collection */ public reset(): this { - // Reset Data + // Reset data this.data = {}; this.size = 0; @@ -940,15 +1002,15 @@ export class Collection { return this; } - //========================================================================================================= - // Put - //========================================================================================================= /** + * Puts `itemKeys/s` into Group/s. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#put) + * * @public - * Puts ItemKey/s into Group/s (GroupKey/s) - * @param itemKeys - ItemKey/s that get added to provided Group/s - * @param groupKeys - Group/s to which the ItemKey/s get added - * @param config - Config + * @param itemKeys - `itemKey/s` to be put into the specified Group/s. + * @param groupKeys - Group identifier/s the specified `itemKey/s` are to put in. + * @param config - Configuration object */ public put( itemKeys: ItemKey | Array, @@ -958,7 +1020,7 @@ export class Collection { const _itemKeys = normalizeArray(itemKeys); const _groupKeys = normalizeArray(groupKeys); - // Add ItemKeys to Groups + // Assign itemKeys to Groups _groupKeys.forEach((groupKey) => { this.getGroup(groupKey)?.add(_itemKeys, config); }); @@ -966,16 +1028,16 @@ export class Collection { return this; } - //========================================================================================================= - // Move - //========================================================================================================= /** + * Moves specified `itemKey/s` from one Group to another Group. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#move) + * * @public - * Move ItemKey/s from one Group to another - * @param itemKeys - ItemKey/s that are moved - * @param oldGroupKey - GroupKey of the Group that currently keeps the Items at itemKey/s - * @param newGroupKey - GroupKey of the Group into which the Items at itemKey/s are moved - * @param config - Config + * @param itemKeys - `itemKey/s` to be moved. + * @param oldGroupKey - Group identifier of the Group the `itemKeys` are moved from. + * @param newGroupKey - Group identifier of the Group the `itemKeys` are moved in. + * @param config - Configuration object */ public move( itemKeys: ItemKey | Array, @@ -991,21 +1053,21 @@ export class Collection { removeProperties(config, ['method', 'overwrite']) ); - // Add itemKeys to new Group + // Assign itemKeys to new Group this.getGroup(newGroupKey)?.add(_itemKeys, config); return this; } - //========================================================================================================= - // Update Item Key - //========================================================================================================= /** + * Updates key/name identifier of Item + * and returns a boolean indicating + * whether the Item identifier was updated successfully. + * * @internal - * Updates Key/Name of Item in all Instances (Group, Selector, ..) - * @param oldItemKey - Old ItemKey - * @param newItemKey - New ItemKey - * @param config - Config + * @param oldItemKey - Old Item identifier. + * @param newItemKey - New Item identifier. + * @param config - Configuration object */ public updateItemKey( oldItemKey: ItemKey, @@ -1025,39 +1087,42 @@ export class Collection { return false; } - // Remove Item from old ItemKey and add Item to new ItemKey + // Update itemKey in data object delete this.data[oldItemKey]; this.data[newItemKey] = item; - // Update Key/Name of Item + // Update key/name of Item item.setKey(newItemKey, { background: config.background, }); - // Update persist Key of Item (Doesn't get updated by updating key of Item because PersistKey is special formatted) + // Update Persistent key of Item + // because it differs from the actual Item key + // and therefore isn't updated when the Item key is updated item.persistent?.setKey( CollectionPersistent.getItemStorageKey(newItemKey, this._key) ); - // Update ItemKey in Groups + // Update itemKey in Groups for (const groupKey in this.groups) { const group = this.getGroup(groupKey, { notExisting: true }); - if (!group?.has(oldItemKey)) continue; - group?.replace(oldItemKey, newItemKey, { background: config.background }); + if (group == null || !group.has(oldItemKey)) continue; + group.replace(oldItemKey, newItemKey, { background: config.background }); } - // Update ItemKey in Selectors + // Update itemKey in Selectors for (const selectorKey in this.selectors) { const selector = this.getSelector(selectorKey, { notExisting: true }); if (selector == null) continue; // Reselect Item in Selector that has selected the newItemKey - // Necessary because the reference placeholder Item got removed - // and replaced with the new Item (Item of which the primaryKey was renamed) - // -> needs to find new Item with the same itemKey + // Necessary because potential reference placeholder Item got overwritten + // with the new (renamed) Item + // -> has to find the new Item at selected itemKey + // since the placeholder Item got overwritten if (selector.hasSelected(newItemKey, false)) { selector.reselect({ - force: true, // Because ItemKeys are the same + force: true, // Because itemKeys are the same background: config.background, }); } @@ -1072,30 +1137,45 @@ export class Collection { return true; } - //========================================================================================================= - // Get GroupKeys That Have ItemKey - //========================================================================================================= /** + * Returns all identifier keys/names of Group/s representing the specified `itemKey`. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupkeysthathaveitemkey) + * * @public - * Gets GroupKeys that contain the passed ItemKey - * @param itemKey - ItemKey + * @param itemKey - `itemKey` to be contained in Group/s. */ public getGroupKeysThatHaveItemKey(itemKey: ItemKey): Array { const groupKeys: Array = []; for (const groupKey in this.groups) { - const group = this.getGroup(groupKey, { notExisting: true }); + const group = this.groups[groupKey]; if (group?.has(itemKey)) groupKeys.push(groupKey); } return groupKeys; } - //========================================================================================================= - // Remove - //========================================================================================================= /** + * Removes Item/s from: + * + * - `.everywhere()`: + * Removes Item/s from the entire Collection and all its Groups and Selectors (i.e. from everywhere) + * ``` + * MY_COLLECTION.remove('1').everywhere(); + * // is equivalent to + * MY_COLLECTION.removeItems('1'); + * ``` + * - `.fromGroups()`: + * Removes Item/s only from specified Groups. + * ``` + * MY_COLLECTION.remove('1').fromGroups(['1', '2']); + * // is equivalent to + * MY_COLLECTION.removeFromGroups('1', ['1', '2']); + * ``` + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#remove) + * * @public - * Remove Items from Collection - * @param itemKeys - ItemKey/s that get removed + * @param itemKeys - Item/s with identifier/s to be removed. */ public remove( itemKeys: ItemKey | Array @@ -1110,14 +1190,14 @@ export class Collection { }; } - //========================================================================================================= - // Remove From Groups - //========================================================================================================= /** + * Remove Item/s from Group/s. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removefromgroups) + * * @public - * Removes Item/s from Group/s - * @param itemKeys - ItemKey/s that get removed from Group/s - * @param groupKeys - GroupKey/s of Group/s form which the ItemKey/s will be removed + * @param itemKeys - Item/s with identifier/s to be removed from Group/s. + * @param groupKeys - Group/s with identifier/s the Item/s are to remove from. */ public removeFromGroups( itemKeys: ItemKey | Array, @@ -1129,7 +1209,7 @@ export class Collection { _itemKeys.forEach((itemKey) => { let removedFromGroupsCount = 0; - // Remove ItemKey from Groups + // Remove itemKey from Groups _groupKeys.forEach((groupKey) => { const group = this.getGroup(groupKey, { notExisting: true }); if (!group?.has(itemKey)) return; @@ -1137,7 +1217,8 @@ export class Collection { removedFromGroupsCount++; }); - // If Item got removed from every Groups the Item was in, remove it completely + // If the Item was removed from each Group in which it was represented, + // remove it completely if ( removedFromGroupsCount >= this.getGroupKeysThatHaveItemKey(itemKey).length @@ -1148,13 +1229,13 @@ export class Collection { return this; } - //========================================================================================================= - // Remove Items - //========================================================================================================= /** + * Removes Item/s from the entire Collection and all its Groups and Selectors + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeitems) + * * @public - * Removes Item completely from Collection - * @param itemKeys - ItemKey/s of Item/s + * @param itemKeys - Item/s with identifier/s to be removed from the entire Collection. * @param config - Config */ public removeItems( @@ -1184,16 +1265,18 @@ export class Collection { // Remove Item from Collection delete this.data[itemKey]; - // Reselect or remove Selectors representing the removed Item + // Reselect or remove Selectors which have represented the removed Item for (const selectorKey in this.selectors) { const selector = this.getSelector(selectorKey, { notExisting: true }); - if (selector?.hasSelected(itemKey, false)) { + if (selector != null && selector.hasSelected(itemKey, false)) { if (config.removeSelector) { // Remove Selector - this.removeSelector(selector?._key ?? 'unknown'); + this.removeSelector(selector._key ?? 'unknown'); } else { - // Reselect Item in Selector (to create new dummyItem to hold a reference to this removed Item) - selector?.reselect({ force: true }); + // Reselect Item in Selector + // in order to create a new dummyItem + // to hold a reference to the now not existing Item + selector.reselect({ force: true }); } } } @@ -1318,14 +1401,12 @@ export class Collection { return true; } - //========================================================================================================= - // Rebuild Groups That Includes Item Key - //========================================================================================================= /** + * Rebuilds all Groups that include the specified `itemKey`. + * * @internal - * Rebuilds Groups that include the provided ItemKey - * @itemKey - Item Key - * @config - Config + * @itemKey - `itemKey` Groups must contain to be rebuilt. + * @config - Configuration object */ public rebuildGroupsThatIncludeItemKey( itemKey: ItemKey, @@ -1339,16 +1420,17 @@ export class Collection { }, }); - // Rebuild Groups that include ItemKey + // Rebuild Groups that include itemKey for (const groupKey in this.groups) { const group = this.getGroup(groupKey); if (group?.has(itemKey)) { - // group.rebuild(); Not necessary because a sideEffect of the Group is to rebuild it self + // Not necessary because a sideEffect of ingesting the Group is to rebuilt it self + // group.rebuild(); group?.ingest({ background: config?.background, - force: true, // because Group value doesn't change only the output changes + force: true, // because Group value didn't change, only the output changes sideEffects: config?.sideEffects, - storage: false, // because Group only rebuilds and doesn't change its value + storage: false, // because Group only rebuilds -> actual value hasn't changed }); } } diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index ce4e6eaf..ccb571be 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -370,20 +370,26 @@ export class State { return !!this.watchers[key]; } - //========================================================================================================= - // Persist - //========================================================================================================= /** + * Preserves the State `value` in the corresponding external Storage. + * + * The State key/name is used as the unique identifier for the Persistent. + * If that is not desired, please specify a unique identifier for the Persistent. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) + * * @public - * Stores State Value into Agile Storage permanently - * @param config - Config + * @param config - Configuration object */ public persist(config?: StatePersistentConfigInterface): this; /** + * Preserves the State `value` in the corresponding external Storage. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) + * * @public - * Stores State Value into Agile Storage permanently - * @param key - Key/Name of created Persistent (Note: Key required if State has no set Key!) - * @param config - Config + * @param key - Key/Name identifier of Persistent. + * @param config - Configuration object */ public persist( key?: PersistentKey, @@ -413,7 +419,7 @@ export class State { // Check if State is already persisted if (this.persistent != null && this.isPersisted) return this; - // Create persistent -> Persist Value + // Create Persistent -> persist value this.persistent = new StatePersistent(this, { instantiate: _config.loadValue, storageKeys: _config.storageKeys, @@ -424,27 +430,30 @@ export class State { return this; } - //========================================================================================================= - // On Load - //========================================================================================================= /** + * Fires immediately after the persisted `value` + * is loaded into the State from corresponding the external Storage. + * + * Registering this callback only makes sense when the State is [persisted](https://agile-ts.org/docs/core/state/methods/#persist). + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#onload) + * * @public - * Callback Function that gets called if the persisted Value gets loaded into the State for the first Time - * Note: Only useful for persisted States! - * @param callback - Callback Function + * @param callback - Callback function */ public onLoad(callback: (success: boolean) => void): this { if (!this.persistent) return this; - // Check if Callback is valid Function + // Check if provided callback is valid function if (!isFunction(callback)) { LogCodeManager.log('00:03:01', ['OnLoad Callback', 'function']); return this; } + // Register provided callback this.persistent.onLoad = callback; - // If State is already 'isPersisted' the loading was successful -> callback can be called + // If State is already persisted ('isPersisted') fire provided callback immediately if (this.isPersisted) callback(true); return this; From 6df7e3f1025e18426859eb1c0e8c00c9ca83f0fa Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 3 Jun 2021 07:19:32 +0200 Subject: [PATCH 17/63] fixed typos in Collection --- packages/core/src/collection/index.ts | 136 ++++++++++-------- packages/core/src/state/index.ts | 15 +- packages/core/src/storages/persistent.ts | 2 +- .../tests/unit/collection/collection.test.ts | 67 ++++++++- 4 files changed, 154 insertions(+), 66 deletions(-) diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index fbdedc79..a2b0a98a 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -40,7 +40,7 @@ export class Collection { public isInstantiated = false; // Whether the Collection is instantiated completely /** - * A Collection provides a reactive set of Information + * A Collection manages a reactive set of Information * that we need to remember globally at a later point in time. * While providing a toolkit to use and mutate this set of Information. * @@ -134,7 +134,10 @@ export class Collection { * Creates a new Group without associating it to the Collection. * * This way of creating a Group is intended for use in the Collection configuration object, - * where the `constructor()` takes care of the associating. + * where the `constructor()` takes care of the binding. + * + * After a successful initiation of the Collection we recommend using `createGroup()`, + * because it automatically connects the Group to the Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#group) * @@ -159,7 +162,10 @@ export class Collection { * Creates a new Selector without associating it to the Collection. * * This way of creating a Selector is intended for use in the Collection configuration object, - * where the `constructor()` takes care of the associating. + * where the `constructor()` takes care of the binding. + * + * After a successful initiation of the Collection we recommend using `createSelector()`, + * because it automatically connects the Group to the Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#selector) * @@ -184,7 +190,7 @@ export class Collection { * Sets up the specified Groups or Group keys * and assigns them to the Collection if they are valid. * - * It also assigns the default Group to the Collection. + * It also instantiates and assigns the default Group to the Collection. * The default Group reflects the default pattern of the Collection. * * @internal @@ -194,7 +200,7 @@ export class Collection { if (!groups) return; let groupsObject: { [key: string]: Group } = {}; - // If groups is Array of Group names/keys, create the Groups based these keys + // If groups is Array of Group keys/names, create the Groups based on these keys if (Array.isArray(groups)) { groups.forEach((groupKey) => { groupsObject[groupKey] = new Group(this, [], { @@ -226,7 +232,7 @@ export class Collection { if (!selectors) return; let selectorsObject: { [key: string]: Selector } = {}; - // If selectors is Array of Selector names/keys, create the Selectors based these keys + // If selectors is Array of Selector keys/names, create the Selectors based on these keys if (Array.isArray(selectors)) { selectors.forEach((selectorKey) => { selectorsObject[selectorKey] = new Selector( @@ -256,7 +262,8 @@ export class Collection { * it must contain such unique identifier at 'id' * to be added to the Collection. * ``` - * MY_COLLECTION.collect({id: '1', name: 'jeff'}); + * MY_COLLECTION.collect({id: '1', name: 'jeff'}); // valid + * MY_COLLECTION.collect({name: 'frank'}); // invalid * ``` * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#collect) @@ -328,7 +335,7 @@ export class Collection { /** * Updates the Item `data object` with the specified `object with changes`, if the Item exists. - * By default the `object with changes` is merged into the Item `data object`. + * By default the `object with changes` is merged into the Item `data object` at top level. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#update) * @@ -407,7 +414,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createGroup) * * @public - * @param groupKey - Unique identifier of the to create Group. + * @param groupKey - Unique identifier of the Group to be created. * @param initialItems - Initial keys of Items to be represented by the Group. */ public createGroup( @@ -487,6 +494,8 @@ export class Collection { * Every Collection should have a default Group, * which represents the default pattern of the Collection. * + * If the default Group, for what ever reason, doesn't exist, `undefined` is returned. + * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getdefaultgroup) * * @public @@ -530,20 +539,33 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removegroup) * * @public - * @param groupKey - Key/Name identifier of Group. + * @param groupKey - Key/Name identifier of Group to be removed. */ public removeGroup(groupKey: GroupKey): this { if (this.groups[groupKey] != null) delete this.groups[groupKey]; return this; } + /** + * Returns the count of registered Groups in the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupcount) + * + * @public + */ + public getGroupCount(): number { + let size = 0; + Object.keys(this.groups).map(() => size++); + return size; + } + /** * Creates a new Selector and associates it to the Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createSelector) * * @public - * @param selectorKey - Unique identifier of the to create Selector. + * @param selectorKey - Unique identifier of Selector to be created. * @param itemKey - Initial key of Item to be represented by the Selector. */ public createSelector( @@ -585,7 +607,8 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#select) * * @public - * @param itemKey - ItemKey to be selected. + * @param itemKey - Initial key of Item to be represented by the Selector + * and used as unique identifier of the Selector. */ public select(itemKey: ItemKey): Selector { return this.createSelector(itemKey, itemKey); @@ -679,7 +702,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeselector) * * @public - * @param selectorKey - Key/Name identifier of Selector. + * @param selectorKey - Key/Name identifier of Selector to be removed. */ public removeSelector(selectorKey: SelectorKey): this { if (this.selectors[selectorKey] != null) { @@ -689,6 +712,19 @@ export class Collection { return this; } + /** + * Returns the count of registered Selectors in the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorcount) + * + * @public + */ + public getSelectorCount(): number { + let size = 0; + Object.keys(this.selectors).map(() => size++); + return size; + } + /** * Returns a boolean indicating whether a Item with the specified `itemKey` * exists in the Collection or not. @@ -763,7 +799,7 @@ export class Collection { * * @internal * @param itemKey - Unique identifier of the to create placeholder Item. - * @param addToCollection - Whether the created Item should be added to the Collection. + * @param addToCollection - Whether to add the Item to be created to the Collection. */ public createPlaceholderItem( itemKey: ItemKey, @@ -773,7 +809,7 @@ export class Collection { const item = new Item( this, { - [this.config.primaryKey]: itemKey, // Setting PrimaryKey of Item to passed itemKey + [this.config.primaryKey]: itemKey, // Setting primaryKey of Item to passed itemKey dummy: 'item', } as any, { isPlaceholder: true } @@ -794,7 +830,7 @@ export class Collection { * Retrieves the value (data object) of a single Item * with the specified key/name identifier from the Collection. * - * If the to retrieve Item doesn't exist, `undefined` is returned. + * If the to retrieve Item containing the value doesn't exist, `undefined` is returned. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemvalue) * @@ -832,13 +868,14 @@ export class Collection { const defaultGroup = this.getDefaultGroup(); let items: Array> = []; - // If config.notExisting transform this.data into array, otherwise return the default Group items + // If config.notExisting transform the data object into array since it contains all Items, + // otherwise return the default Group Items if (config.notExisting) { for (const key in this.data) items.push(this.data[key]); } else { - // Why defaultGroup Items and not all .exists === true Items? - // Because the default Group keeps track of all existing Items - // It also does control the Collection output in useAgile() + // Why default Group Items and not all '.exists === true' Items? + // Because the default Group keeps track of all existing Items. + // It also does control the Collection output in binding methods like 'useAgile()' // and therefore should do it here too. items = defaultGroup?.items || []; } @@ -868,7 +905,8 @@ export class Collection { * Preserves the Collection `value` in the corresponding external Storage. * * The Collection key/name is used as the unique identifier for the Persistent. - * If that is not desired, please specify a unique identifier for the Persistent. + * If that is not desired or the Collection has no unique identifier, + * please specify a separate unique identifier for the Persistent. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#persist) * @@ -879,6 +917,8 @@ export class Collection { /** * Preserves the Collection `value` in the corresponding external Storage. * + * The specified key is used as the unique identifier for the Persistent. + * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#persist) * * @public @@ -913,7 +953,7 @@ export class Collection { // Check if Collection is already persisted if (this.persistent != null && this.isPersisted) return this; - // Create Persistent -> persist value + // Create Persistent (-> persist value) this.persistent = new CollectionPersistent(this, { instantiate: _config.loadValue, storageKeys: _config.storageKeys, @@ -954,35 +994,9 @@ export class Collection { return this; } - /** - * Returns the count of registered Groups in the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupcount) - * - * @public - */ - public getGroupCount(): number { - let size = 0; - Object.keys(this.groups).map(() => size++); - return size; - } - - /** - * Returns the count of registered Selectors in the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorcount) - * - * @public - */ - public getSelectorCount(): number { - let size = 0; - Object.keys(this.selectors).map(() => size++); - return size; - } - /** * Removes all Items from the Collection - * and resets the Groups and Selectors. + * and resets all Groups and Selectors of the Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#reset) * @@ -1009,7 +1023,7 @@ export class Collection { * * @public * @param itemKeys - `itemKey/s` to be put into the specified Group/s. - * @param groupKeys - Group identifier/s the specified `itemKey/s` are to put in. + * @param groupKeys - Identifier/s of the Group/s the specified `itemKey/s` are to put in. * @param config - Configuration object */ public put( @@ -1035,8 +1049,8 @@ export class Collection { * * @public * @param itemKeys - `itemKey/s` to be moved. - * @param oldGroupKey - Group identifier of the Group the `itemKeys` are moved from. - * @param newGroupKey - Group identifier of the Group the `itemKeys` are moved in. + * @param oldGroupKey - Identifier of the Group the `itemKey/s` are moved from. + * @param newGroupKey - Identifier of the Group the `itemKey/s` are moved in. * @param config - Configuration object */ public move( @@ -1097,11 +1111,17 @@ export class Collection { }); // Update Persistent key of Item - // because it differs from the actual Item key - // and therefore isn't updated when the Item key is updated - item.persistent?.setKey( - CollectionPersistent.getItemStorageKey(newItemKey, this._key) - ); + // if it follows the Item Storage Key pattern + // and therefore differs from the actual Item key + // (-> isn't automatically updated when the Item key is updated) + if ( + item.persistent != null && + item.persistent._key === + CollectionPersistent.getItemStorageKey(oldItemKey, this._key) + ) + item.persistent?.setKey( + CollectionPersistent.getItemStorageKey(newItemKey, this._key) + ); // Update itemKey in Groups for (const groupKey in this.groups) { @@ -1122,7 +1142,7 @@ export class Collection { // since the placeholder Item got overwritten if (selector.hasSelected(newItemKey, false)) { selector.reselect({ - force: true, // Because itemKeys are the same + force: true, // Because itemKeys are the same (but not the Items) background: config.background, }); } diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index ccb571be..cd412f67 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -139,8 +139,12 @@ export class State { // Update key in Persistent (only if oldKey equal to persistentKey // because otherwise the persistentKey is detached from the State key // -> not managed by State anymore) - if (value && this.persistent?._key === oldKey) - this.persistent?.setKey(value); + if ( + value != null && + this.persistent != null && + this.persistent._key === oldKey + ) + this.persistent.setKey(value); return this; } @@ -374,7 +378,8 @@ export class State { * Preserves the State `value` in the corresponding external Storage. * * The State key/name is used as the unique identifier for the Persistent. - * If that is not desired, please specify a unique identifier for the Persistent. + * If that is not desired or the State has no unique identifier, + * please specify a separate unique identifier for the Persistent. * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) * @@ -385,6 +390,8 @@ export class State { /** * Preserves the State `value` in the corresponding external Storage. * + * The specified key is used as the unique identifier for the Persistent. + * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) * * @public @@ -419,7 +426,7 @@ export class State { // Check if State is already persisted if (this.persistent != null && this.isPersisted) return this; - // Create Persistent -> persist value + // Create Persistent (-> persist value) this.persistent = new StatePersistent(this, { instantiate: _config.loadValue, storageKeys: _config.storageKeys, diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index 19f4cc09..2fa36634 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -92,7 +92,7 @@ export class Persistent { public instantiatePersistent( config: InstantiatePersistentConfigInterface = {} ) { - this._key = this.formatKey(config.key) || Persistent.placeHolderKey; + this._key = this.formatKey(config.key) ?? Persistent.placeHolderKey; this.assignStorageKeys(config.storageKeys, config.defaultStorageKey); this.validatePersistent(); } diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 500fb289..8af0f72e 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -2118,7 +2118,13 @@ describe('Collection Tests', () => { dummySelector3.reselect = jest.fn(); }); - it('should update ItemKey in Collection, Selectors and Groups (default config)', () => { + it('should update ItemKey in Collection, Selectors, Groups and Persistent (default config)', () => { + if (dummyItem1.persistent) + dummyItem1.persistent._key = CollectionPersistent.getItemStorageKey( + dummyItem1._key, + collection._key + ); + const response = collection.updateItemKey('dummyItem1', 'newDummyItem'); expect(response).toBeTruthy(); @@ -2156,7 +2162,13 @@ describe('Collection Tests', () => { LogMock.hasNotLogged('warn'); }); - it('should update ItemKey in Collection, Selectors and Groups (specific config)', () => { + it('should update ItemKey in Collection, Selectors, Groups and Persistent (specific config)', () => { + if (dummyItem1.persistent) + dummyItem1.persistent._key = CollectionPersistent.getItemStorageKey( + dummyItem1._key, + collection._key + ); + const response = collection.updateItemKey( 'dummyItem1', 'newDummyItem', @@ -2196,7 +2208,13 @@ describe('Collection Tests', () => { LogMock.hasNotLogged('warn'); }); - it('should update ItemKey in Collection, dummy Selectors and dummy Groups (default config)', () => { + it('should update ItemKey in Collection, dummy Selectors, dummy Groups and Persistent (default config)', () => { + if (dummyItem1.persistent) + dummyItem1.persistent._key = CollectionPersistent.getItemStorageKey( + dummyItem1._key, + collection._key + ); + dummyGroup1.isPlaceholder = true; dummySelector1.isPlaceholder = true; @@ -2233,6 +2251,49 @@ describe('Collection Tests', () => { LogMock.hasNotLogged('warn'); }); + it( + 'should update ItemKey in Collection, Selectors, Groups ' + + "and shouldn't update it in Persistent if persist key doesn't follow the Item Storage Key pattern (default config)", + () => { + if (dummyItem1.persistent) + dummyItem1.persistent._key = 'randomPersistKey'; + + const response = collection.updateItemKey( + 'dummyItem1', + 'newDummyItem' + ); + + expect(response).toBeTruthy(); + + expect(dummyItem1.setKey).toHaveBeenCalledWith('newDummyItem', { + background: false, + }); + expect(dummyItem2.setKey).not.toHaveBeenCalled(); + expect(dummyItem1.persistent?.setKey).not.toHaveBeenCalled(); + expect(dummyItem2.persistent?.setKey).not.toHaveBeenCalled(); + + expect(dummyGroup1.replace).toHaveBeenCalledWith( + 'dummyItem1', + 'newDummyItem', + { + background: false, + } + ); + expect(dummyGroup2.replace).not.toHaveBeenCalled(); + + expect(dummySelector1.select).toHaveBeenCalledWith('newDummyItem', { + background: false, + }); + expect(dummySelector2.select).not.toHaveBeenCalled(); + expect(dummySelector3.reselect).toHaveBeenCalledWith({ + force: true, + background: false, + }); + + LogMock.hasNotLogged('warn'); + } + ); + it("shouldn't update ItemKey of Item that doesn't exist (default config)", () => { const response = collection.updateItemKey( 'notExistingItem', From cfd9f2c22b15e570443e2262f0f01e2b93f25109 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 3 Jun 2021 16:10:44 +0200 Subject: [PATCH 18/63] added interface descriptions in Collection --- packages/core/src/collection/group.ts | 4 +- packages/core/src/collection/index.ts | 266 ++++++++++++------ packages/core/src/computed/index.ts | 26 +- packages/core/src/state/index.ts | 15 +- packages/core/src/state/state.observer.ts | 3 + .../tests/unit/collection/collection.test.ts | 4 +- 6 files changed, 205 insertions(+), 113 deletions(-) diff --git a/packages/core/src/collection/group.ts b/packages/core/src/collection/group.ts index 98fa4ccc..a8f53b1c 100644 --- a/packages/core/src/collection/group.ts +++ b/packages/core/src/collection/group.ts @@ -56,7 +56,7 @@ export class Group extends State< */ public get output(): Array { ComputedTracker.tracked(this.observer); - return this._output; + return copy(this._output); } /** @@ -64,7 +64,7 @@ export class Group extends State< * Set Item Values of Group */ public set output(value: DataType[]) { - this._output = value; + this._output = copy(value); } /** diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index a2b0a98a..26399664 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -20,6 +20,7 @@ import { removeProperties, isFunction, LogCodeManager, + PatchOptionConfigInterface, } from '../internal'; export class Collection { @@ -1079,8 +1080,8 @@ export class Collection { * whether the Item identifier was updated successfully. * * @internal - * @param oldItemKey - Old Item identifier. - * @param newItemKey - New Item identifier. + * @param oldItemKey - Old key/name Item identifier. + * @param newItemKey - New key/name Item identifier. * @param config - Configuration object */ public updateItemKey( @@ -1110,8 +1111,7 @@ export class Collection { background: config.background, }); - // Update Persistent key of Item - // if it follows the Item Storage Key pattern + // Update Persistent key of Item if it follows the Item Storage Key pattern // and therefore differs from the actual Item key // (-> isn't automatically updated when the Item key is updated) if ( @@ -1135,14 +1135,14 @@ export class Collection { const selector = this.getSelector(selectorKey, { notExisting: true }); if (selector == null) continue; - // Reselect Item in Selector that has selected the newItemKey + // Reselect Item in Selector that has selected the newItemKey. // Necessary because potential reference placeholder Item got overwritten // with the new (renamed) Item // -> has to find the new Item at selected itemKey // since the placeholder Item got overwritten if (selector.hasSelected(newItemKey, false)) { selector.reselect({ - force: true, // Because itemKeys are the same (but not the Items) + force: true, // Because itemKeys are the same (but not the Items at this itemKey anymore) background: config.background, }); } @@ -1158,7 +1158,7 @@ export class Collection { } /** - * Returns all identifier keys/names of Group/s representing the specified `itemKey`. + * Returns all key/name identifiers of the Group/s containing the specified `itemKey`. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupkeysthathaveitemkey) * @@ -1216,8 +1216,8 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removefromgroups) * * @public - * @param itemKeys - Item/s with identifier/s to be removed from Group/s. - * @param groupKeys - Group/s with identifier/s the Item/s are to remove from. + * @param itemKeys - Identifier/s of Item/s to be removed from Group/s. + * @param groupKeys - Identifier/s of Group/s the Item/s are to remove from. */ public removeFromGroups( itemKeys: ItemKey | Array, @@ -1237,7 +1237,7 @@ export class Collection { removedFromGroupsCount++; }); - // If the Item was removed from each Group in which it was represented, + // If the Item was removed from each Group representing the Item, // remove it completely if ( removedFromGroupsCount >= @@ -1250,13 +1250,13 @@ export class Collection { } /** - * Removes Item/s from the entire Collection and all its Groups and Selectors + * Removes Item/s from the entire Collection and all the Collection's Groups and Selectors. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeitems) * * @public - * @param itemKeys - Item/s with identifier/s to be removed from the entire Collection. - * @param config - Config + * @param itemKeys - Identifier/s of Item/s to be removed from the entire Collection. + * @param config - Configuration object */ public removeItems( itemKeys: ItemKey | Array, @@ -1308,9 +1308,13 @@ export class Collection { } /** - * Assigns provided data object to an already existing Item at itemKey. - * If Item at itemKey doesn't exist yet, - * a new Item with the data object as value is created and added to the Collection. + * Assigns the provided `data` object to an already existing Item + * with specified key/name identifier found in the `data` object. + * If the Item doesn't exist yet, a new Item with the `data` object as value + * is created and assigned to the Collection. + * + * Returns a boolean indicating + * whether the `data` object was assigned/updated successfully. * * @internal * @param data - Data object @@ -1332,7 +1336,7 @@ export class Collection { return false; } - // Check if data object contains valid itemKey + // Check if data object contains valid itemKey, // otherwise add random itemKey to Item if (!Object.prototype.hasOwnProperty.call(_data, primaryKey)) { LogCodeManager.log('1B:02:05', [this._key, primaryKey]); @@ -1357,14 +1361,18 @@ export class Collection { } // Increase size of Collection if Item was previously a placeholder - // (-> didn't officially exit in Collection) + // (-> hasn't officially existed in Collection before) if (wasPlaceholder) this.size++; return true; } /** - * Adds provided Item to the Collection. + * Assigns the specified Item to the Collection + * at the key/name identifier of the Item. + * + * And returns a boolean indicating + * whether the Item was assigned successfully. * * @internal * @param item - Item to be added. @@ -1382,7 +1390,7 @@ export class Collection { let itemKey = item._value[primaryKey]; let increaseCollectionSize = true; - // Check if Item has valid itemKey + // Check if Item has valid itemKey, // otherwise add random itemKey to Item if (!Object.prototype.hasOwnProperty.call(item._value, primaryKey)) { LogCodeManager.log('1B:02:05', [this._key, primaryKey]); @@ -1410,8 +1418,8 @@ export class Collection { this.data[itemKey] = item; // Rebuild Groups that include itemKey - // after adding Item to Collection - // (because otherwise it can't find the Item since it doesn't exist in Collection yet) + // after adding Item with itemKey to the Collection + // (because otherwise it can't find the Item as it isn't added yet) this.rebuildGroupsThatIncludeItemKey(itemKey, { background: config.background, }); @@ -1422,7 +1430,7 @@ export class Collection { } /** - * Rebuilds all Groups that include the specified `itemKey`. + * Rebuilds all Groups that contain the specified `itemKey`. * * @internal * @itemKey - `itemKey` Groups must contain to be rebuilt. @@ -1444,13 +1452,15 @@ export class Collection { for (const groupKey in this.groups) { const group = this.getGroup(groupKey); if (group?.has(itemKey)) { - // Not necessary because a sideEffect of ingesting the Group is to rebuilt it self + // Not necessary because a sideEffect of ingesting the Group + // into the runtime is to rebuilt itself // group.rebuild(); + group?.ingest({ background: config?.background, - force: true, // because Group value didn't change, only the output changes + force: true, // because Group value didn't change, only the output might change sideEffects: config?.sideEffects, - storage: false, // because Group only rebuilds -> actual value hasn't changed + storage: false, // because Group only rebuilds (-> actual persisted value hasn't changed) }); } } @@ -1461,129 +1471,203 @@ export type DefaultItem = Record; // same as { [key: string]: any } export type CollectionKey = string | number; export type ItemKey = string | number; -/** - * @param key - Key/Name of Collection - * @param groups - Groups of Collection - * @param selectors - Selectors of Collection - * @param primaryKey - Name of Property that holds the PrimaryKey (default = id) - * @param defaultGroupKey - Key/Name of Default Group that holds all collected Items - * @param initialData - Initial Data of Collection - */ export interface CreateCollectionConfigInterface { + /** + * Initial Groups of Collection. + * @default [] + */ groups?: { [key: string]: Group } | string[]; + /** + * Initial Selectors of Collection + * @default [] + */ selectors?: { [key: string]: Selector } | string[]; + /** + * Key/Name identifier of Collection. + * @default undefined + */ key?: CollectionKey; + /** + * Key/Name of the property + * which should represent the unique Item identifier + * in collected data objects. + * @default 'id' + */ primaryKey?: string; + /** + * Key/Name identifier of the default Group that is created shortly after instantiation. + * The default Group represents the default pattern of the Collection. + * @default 'default' + */ defaultGroupKey?: GroupKey; + /** + * Initial data objects of the Collection. + * @default [] + */ initialData?: Array; } -/** - * @param primaryKey - Name of Property that holds the PrimaryKey (default = id) - * @param defaultGroupKey - Key/Name of Default Group that holds all collected Items - */ +export type CollectionConfig = + | CreateCollectionConfigInterface + | (( + collection: Collection + ) => CreateCollectionConfigInterface); + export interface CollectionConfigInterface { + /** + * Key/Name of the property + * which should represent the unique Item identifier + * in collected data objects. + * @default 'id' + */ primaryKey: string; + /** + * Key/Name identifier of the default Group that is created shortly after instantiation. + * The default Group represents the default pattern of the Collection. + * @default 'default' + */ defaultGroupKey: ItemKey; } -/** - * @param patch - If Item gets patched into existing Item with the same Id - * @param method - Way of adding Item to Collection (push, unshift) - * @param forEachItem - Gets called for each Item that got collected - * @param background - If collecting an Item happens in the background (-> not causing any rerender) - * @param select - If collected Items get selected with a Selector - */ -export interface CollectConfigInterface { - patch?: boolean; +export interface CollectConfigInterface + extends AssignDataConfigInterface { + /** + * In which way the collected data should be added to the Collection. + * - 'push' = at the end + * - 'unshift' = at the beginning + * @default 'push' + */ method?: 'push' | 'unshift'; + /** + * Performs the specified action for each collected data object. + * @default undefined + */ forEachItem?: ( data: DataType | Item, key: ItemKey, success: boolean, index: number ) => void; - background?: boolean; + /** + * Whether a Selector should be created for each collected data object. + * @default false + */ select?: boolean; } -/** - * @param patch - If Data gets merged into the current Data - * @param background - If updating an Item happens in the background (-> not causing any rerender) - */ export interface UpdateConfigInterface { - patch?: boolean | { addNewProperties?: boolean }; + /** + * Whether the data object with changes should be merged into the existing Item data object + * or overwrite it entirely. + * @default true + */ + patch?: boolean | PatchOptionConfigInterface; + /** + * Whether the Item data object should be updated in background. + * So that the UI isn't notified of these changes and thus doesn't rerender. + * @default false + */ background?: boolean; } -/** - * @param background - If updating the primaryKey of an Item happens in the background (-> not causing any rerender) - */ export interface UpdateItemKeyConfigInterface { + /** + * Whether the Item key/name identifier should be updated in background. + * So that the UI isn't notified of these changes and thus doesn't rerender. + * @default false + */ background?: boolean; } -/** - * @param background - If assigning a new value happens in the background (-> not causing any rerender) - * @param force - Force creating and performing Job - * @param sideEffects - If Side Effects of Group gets executed - */ export interface RebuildGroupsThatIncludeItemKeyConfigInterface { + /** + * Whether the Group should be rebuilt in background. + * So that the UI isn't notified of these changes and thus doesn't rerender. + * @default false + */ background?: boolean; - force?: boolean; + /** + * Whether the defined side effects should be executed. + * @default true + */ sideEffects?: SideEffectConfigInterface; } -/** - * @param notExisting - If placeholder can be found - */ export interface HasConfigInterface { + /** + * Whether Items that do not officially exist, + * such as placeholder Items, can be found + * @default true + */ notExisting?: boolean; } -/** - * @param loadValue - If Persistent loads the persisted value into the Collection - * @param storageKeys - Key/Name of Storages which gets used to persist the Collection Value (NOTE: If not passed the default Storage will be used) - * @param defaultStorageKey - Default Storage Key (if not provided it takes the first index of storageKeys or the AgileTs default Storage) - */ export interface CollectionPersistentConfigInterface { + /** + * Whether the Persistent should automatically load + * the persisted value into the Collection after instantiation. + * @default true + */ loadValue?: boolean; + /** + * Key/Name identifier of Storages in which the Collection should be persisted. + * @default [AgileTs default Storage key] + */ storageKeys?: StorageKey[]; + /** + * Default Storage key of the specified Storage keys. + * The Collection value is loaded from the default Storage. + * The value is only loaded from the remaining Storages (storageKeys) + * if the loading of the default Storage failed. + * + * @default first index of the specified Storage keys or the AgileTs default Storage key + */ defaultStorageKey?: StorageKey; } -/** - * @property notExisting - If not existing Items like placeholder Items can be removed. - * Keep in mind that sometimes it won't remove the Item entirely - * because another Instance (like a Selector) needs to keep reference to it. - * https://github.com/agile-ts/agile/pull/152 - * @property removeSelector - If Selectors that have selected an Item to be removed, should be removed too - */ export interface RemoveItemsConfigInterface { + /** + * Whether not officially existing Items (such as placeholder Items) can be removed. + * Keep in mind that sometimes it won't remove the Item entirely + * as another Instance (like a Selector) might need to keep reference to it. + * https://github.com/agile-ts/agile/pull/152 + * @default false + */ notExisting?: boolean; + /** + * Whether to remove Selectors that have selected an Item to be removed. + * @default false + */ removeSelector?: boolean; } -/** - * @property patch - If Data gets patched into existing Item - * @property background - If assigning Data happens in background - */ export interface AssignDataConfigInterface { + /** + * When the Item identifier of the to assign data object already exists in the Collection, + * whether then the newly assigned data should be merged into the existing one + * or overwrite it entirely. + * @default true + */ patch?: boolean; + /** + * Whether to assign the data object to the Collection in background. + * So that the UI isn't notified of these changes and thus doesn't rerender. + * @default false + */ background?: boolean; } -/** - * @property overwrite - If old Item should be overwritten - * @property background - If assigning Data happens in background - */ export interface AssignItemConfigInterface { + /** + * If an Item with the Item identifier already exists, + * whether it should then be overwritten with the new Item. + * @default false + */ overwrite?: boolean; + /** + * Whether to assign the Item to the Collection in background. + * So that the UI isn't notified of these changes and thus doesn't rerender. + * @default false + */ background?: boolean; } - -export type CollectionConfig = - | CreateCollectionConfigInterface - | (( - collection: Collection - ) => CreateCollectionConfigInterface); diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index 1c0e6bdd..42731bef 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -183,29 +183,29 @@ export class Computed extends State< } } -/** - * @property computedDeps - Hard coded dependencies on which the Computed Class depends. - * | Default = [] | - */ export interface ComputedConfigInterface extends StateConfigInterface { + /** + * Hard-coded dependencies on which the Computed Class should depend. + * @default [] + */ computedDeps?: Array; } -/** - * @property autodetect - Whether dependencies used in the compute function should be detected automatically. - * | Default = true | - */ export interface ComputeConfigInterface { + /** + * Whether dependencies used in the compute function should be detected automatically. + * @default true + */ autodetect?: boolean; } -/** - * @param overwriteDeps - Whether the old hard coded dependencies - * should be entirely overwritten with the new hard coded dependencies or merged in. - * | Default = true | - */ export interface UpdateComputeFunctionConfigInterface extends RecomputeConfigInterface { + /** + * Whether the old hard-coded dependencies should be completely overwritten + * with the new ones or merged into the new ones. + * @default false + */ overwriteDeps?: boolean; } diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index cd412f67..745364b6 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -99,7 +99,7 @@ export class State { */ public get value(): ValueType { ComputedTracker.tracked(this.observer); - return this._value; + return copy(this._value); } /** @@ -719,10 +719,15 @@ export interface StateConfigInterface { isPlaceholder?: boolean; } -/** - * @param addNewProperties - If new Properties gets added to the State Value - */ -export interface PatchConfigInterface extends StateIngestConfigInterface { +export interface PatchConfigInterface + extends StateIngestConfigInterface, + PatchOptionConfigInterface {} + +export interface PatchOptionConfigInterface { + /** + * Whether to add new properties to the object during the merge. + * @default true + */ addNewProperties?: boolean; } diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index 32160c75..3da44fe9 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -124,6 +124,9 @@ export class StateObserver extends Observer { state._value = copy(job.observer.nextStateValue); state.nextStateValue = copy(job.observer.nextStateValue); + // https://www.geeksforgeeks.org/object-freeze-javascript/#:~:text=Object.freeze()%20Method&text=freeze()%20which%20is%20used,the%20prototype%20of%20the%20object. + // if (typeof state._value === 'object') Object.freeze(state._value); + // Overwrite old State Values if (job.config.overwrite) { state.initialStateValue = copy(state._value); diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 8af0f72e..3ad578f8 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -1664,7 +1664,7 @@ describe('Collection Tests', () => { it('should return existing Item Value (default config)', () => { const response = collection.getItemValue('1'); - expect(response).toBe(dummyItem._value); + expect(response).toStrictEqual(dummyItem._value); expect(collection.getItem).toHaveBeenCalledWith('1', {}); }); @@ -1691,7 +1691,7 @@ describe('Collection Tests', () => { notExisting: true, }); - expect(response).toBe(dummyItem._value); + expect(response).toStrictEqual(dummyItem._value); expect(collection.getItem).toHaveBeenCalledWith('1', { notExisting: true, }); From 1dd9f3f2bf4ac3194397884a283de2b3a40229f1 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 3 Jun 2021 21:07:44 +0200 Subject: [PATCH 19/63] fixed typos --- packages/core/src/collection/group.ts | 28 ++++--- packages/core/src/collection/item.ts | 78 +++++++++++-------- .../core/tests/unit/collection/item.test.ts | 38 ++++++++- 3 files changed, 102 insertions(+), 42 deletions(-) diff --git a/packages/core/src/collection/group.ts b/packages/core/src/collection/group.ts index a8f53b1c..e8da86dd 100644 --- a/packages/core/src/collection/group.ts +++ b/packages/core/src/collection/group.ts @@ -234,20 +234,29 @@ export class Group extends State< return this; } - //========================================================================================================= - // Persist - //========================================================================================================= /** + * Preserves the Group `value` in the corresponding external Storage. + * + * The Group key/name is used as the unique identifier for the Persistent. + * If that is not desired or the Group has no unique identifier, + * please specify a separate unique identifier for the Persistent. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) + * * @public - * Stores Group Value into Agile Storage permanently - * @param config - Config + * @param config - Configuration object */ public persist(config?: GroupPersistConfigInterface): this; /** + * Preserves the Group `value` in the corresponding external Storage. + * + * The specified key is used as the unique identifier for the Persistent. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) + * * @public - * Stores Group Value into Agile Storage permanently - * @param key - Key/Name of created Persistent (Note: Key required if Group has no set Key!) - * @param config - Config + * @param key - Key/Name identifier of Persistent. + * @param config - Configuration object */ public persist( key?: PersistentKey, @@ -275,7 +284,7 @@ export class Group extends State< defaultStorageKey: null, }); - // Create storageItemKey based on Collection Name + // Create storageItemKey based on Collection key/name identifier if (_config.followCollectionPersistKeyPattern) { key = CollectionPersistent.getGroupStorageKey( key || this._key, @@ -283,6 +292,7 @@ export class Group extends State< ); } + // Persist Group super.persist(key, { loadValue: _config.loadValue, storageKeys: _config.storageKeys, diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index 28cae1da..b4a92569 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -15,16 +15,18 @@ import { export class Item extends State< DataType > { - static updateGroupSideEffectKey = 'rebuildGroup'; - public selectedBy: Set = new Set(); // Keys of Selectors that have selected this Item public collection: () => Collection; + static updateGroupSideEffectKey = 'rebuildGroup'; + public selectedBy: Set = new Set(); // Key/Name Identifiers of Selectors which have selected the Item + /** + * An extension of the State Class that represents a single data object of a Collection. + * * @public - * Item of Collection - * @param collection - Collection to which the Item belongs - * @param data - Data that the Item holds - * @param config - Config + * @param collection - Collection to which the Item belongs. + * @param data - Data object to be represented by the Item. + * @param config - Configuration object */ constructor( collection: Collection, @@ -33,24 +35,23 @@ export class Item extends State< ) { super(collection.agileInstance(), data, { isPlaceholder: config.isPlaceholder, - key: data[collection.config.primaryKey], // Set Key/Name of Item to primaryKey of Data + key: data[collection.config.primaryKey], // Set key/name of Item to identifier at primaryKey property }); this.collection = () => collection; - // Add rebuildGroupsThatIncludeItemKey to sideEffects to rebuild Groups that include this Item if it mutates - this.addRebuildGroupThatIncludeItemKeySideEffect( - this._key != null ? this._key : 'unknown' - ); + // Add 'rebuildGroupsThatIncludeItemKey' side effect + // in order to rebuild all Groups that include this Item whenever it mutates + if (this._key != null) { + this.addRebuildGroupThatIncludeItemKeySideEffect(this._key); + } } - //========================================================================================================= - // Set Key - //========================================================================================================= /** + * Updates key/name identifier of Persistent. + * * @internal - * Updates Key/Name of State - * @param value - New Key/Name of State - * @param config - Config + * @param value - New key/name identifier. + * @param config - Configuration object */ public setKey( value: StateKey | undefined, @@ -69,13 +70,12 @@ export class Item extends State< }); if (value == null) return this; - // Remove old rebuildGroupsThatIncludeItemKey sideEffect + // Update 'rebuildGroupsThatIncludeItemKey' side effect to the new itemKey this.removeSideEffect(Item.updateGroupSideEffectKey); - - // Add rebuildGroupsThatIncludeItemKey to sideEffects to rebuild Groups that include this Item if it mutates this.addRebuildGroupThatIncludeItemKeySideEffect(value); - // Update ItemKey in ItemValue (After updating the sideEffect because otherwise it calls the old sideEffect) + // Update itemKey in Item value + // (After updating the side effect, because otherwise it would call the old side effect) this.patch( { [this.collection().config.primaryKey]: value }, { @@ -89,20 +89,29 @@ export class Item extends State< return this; } - //========================================================================================================= - // Persist - //========================================================================================================= /** + * Preserves the Item `value` in the corresponding external Storage. + * + * The Item key/name is used as the unique identifier for the Persistent. + * If that is not desired or the Item has no unique identifier, + * please specify a separate unique identifier for the Persistent. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) + * * @public - * Stores Item Value into Agile Storage permanently - * @param config - Config + * @param config - Configuration object */ public persist(config?: ItemPersistConfigInterface): this; /** + * Preserves the Item `value` in the corresponding external Storage. + * + * The specified key is used as the unique identifier for the Persistent. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) + * * @public - * Stores Item Value into Agile Storage permanently - * @param key - Key/Name of created Persistent (Note: Key required if Item has no set Key!) - * @param config - Config + * @param key - Key/Name identifier of Persistent. + * @param config - Configuration object */ public persist( key?: PersistentKey, @@ -130,7 +139,7 @@ export class Item extends State< defaultStorageKey: null, }); - // Create storageItemKey based on Collection Name + // Create storageItemKey based on Collection key/name identifier if (_config.followCollectionPersistKeyPattern) { key = CollectionPersistent.getItemStorageKey( key || this._key, @@ -138,6 +147,7 @@ export class Item extends State< ); } + // Persist Item super.persist(key, { loadValue: _config.loadValue, storageKeys: _config.storageKeys, @@ -158,8 +168,12 @@ export class Item extends State< public addRebuildGroupThatIncludeItemKeySideEffect(itemKey: StateKey) { this.addSideEffect>( Item.updateGroupSideEffectKey, - (instance, config) => - instance.collection().rebuildGroupsThatIncludeItemKey(itemKey, config), + (instance, config) => { + // TODO optimise this because currently the whole Group rebuilds + // although only the Item value has changed which definitely needs no complete rebuild + // https://github.com/agile-ts/agile/issues/113 + instance.collection().rebuildGroupsThatIncludeItemKey(itemKey, config); + }, { weight: 100 } ); } diff --git a/packages/core/tests/unit/collection/item.test.ts b/packages/core/tests/unit/collection/item.test.ts index c3ccb9af..bbd902bf 100644 --- a/packages/core/tests/unit/collection/item.test.ts +++ b/packages/core/tests/unit/collection/item.test.ts @@ -94,8 +94,44 @@ describe('Item Tests', () => { expect(item.selectedBy.size).toBe(0); }); + it("should create Item and shouldn't add rebuild Group side effect to it if no itemKey was provided (default config)", () => { + // Overwrite addRebuildGroupThatIncludeItemKeySideEffect once to not call it + jest + .spyOn(Item.prototype, 'addRebuildGroupThatIncludeItemKeySideEffect') + .mockReturnValueOnce(undefined); + + const dummyData = { name: 'dummyName' }; + const item = new Item(dummyCollection, dummyData as any); + + expect(item.collection()).toBe(dummyCollection); + expect( + item.addRebuildGroupThatIncludeItemKeySideEffect + ).not.toHaveBeenCalled(); + + expect(item._key).toBeUndefined(); + expect(item.valueType).toBeUndefined(); + expect(item.isSet).toBeFalsy(); + expect(item.isPlaceholder).toBeFalsy(); + expect(item.initialStateValue).toStrictEqual(dummyData); + expect(item._value).toStrictEqual(dummyData); + expect(item.previousStateValue).toStrictEqual(dummyData); + expect(item.nextStateValue).toStrictEqual(dummyData); + expect(item.observer).toBeInstanceOf(StateObserver); + expect(item.observer.dependents.size).toBe(0); + expect(item.observer._key).toBe( + dummyData[dummyCollection.config.primaryKey] + ); + expect(item.sideEffects).toStrictEqual({}); + expect(item.computeValueMethod).toBeUndefined(); + expect(item.computeExistsMethod).toBeInstanceOf(Function); + expect(item.isPersisted).toBeFalsy(); + expect(item.persistent).toBeUndefined(); + expect(item.watchers).toStrictEqual({}); + expect(item.selectedBy.size).toBe(0); + }); + describe('Item Function Tests', () => { - let item: Item; + let item: Item; beforeEach(() => { item = new Item(dummyCollection, { id: 'dummyId', name: 'dummyName' }); From 74a86efb1ac60308e44b0d1ce50c7735eca6207e Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 4 Jun 2021 07:22:35 +0200 Subject: [PATCH 20/63] added item method descriptions --- packages/core/src/collection/group.ts | 10 +- packages/core/src/collection/index.ts | 37 +++--- packages/core/src/collection/item.ts | 46 ++++---- packages/core/src/computed/index.ts | 6 +- .../core/tests/unit/collection/group.test.ts | 98 +++++++++------- .../core/tests/unit/collection/item.test.ts | 107 +++++++++++++++++- 6 files changed, 211 insertions(+), 93 deletions(-) diff --git a/packages/core/src/collection/group.ts b/packages/core/src/collection/group.ts index e8da86dd..d4654f44 100644 --- a/packages/core/src/collection/group.ts +++ b/packages/core/src/collection/group.ts @@ -279,7 +279,7 @@ export class Group extends State< _config = defineConfig(_config, { loadValue: true, - followCollectionPattern: false, + followCollectionPersistKeyPattern: true, storageKeys: [], defaultStorageKey: null, }); @@ -375,10 +375,12 @@ export interface GroupConfigInterface { isPlaceholder?: boolean; } -/** - * @param useCollectionPattern - If Group storageKey follows the Collection Group StorageKey Pattern - */ export interface GroupPersistConfigInterface extends StatePersistentConfigInterface { + /** + * Whether to format the specified Storage key into the Collection Group Storage key pattern. + * `_${collectionKey}_group_${groupKey}` + * @default true + */ followCollectionPersistKeyPattern?: boolean; } diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 26399664..cf7c7366 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -1489,7 +1489,7 @@ export interface CreateCollectionConfigInterface { key?: CollectionKey; /** * Key/Name of the property - * which should represent the unique Item identifier + * which represents the unique Item identifier * in collected data objects. * @default 'id' */ @@ -1516,7 +1516,7 @@ export type CollectionConfig = export interface CollectionConfigInterface { /** * Key/Name of the property - * which should represent the unique Item identifier + * which represents the unique Item identifier * in collected data objects. * @default 'id' */ @@ -1549,7 +1549,7 @@ export interface CollectConfigInterface index: number ) => void; /** - * Whether a Selector should be created for each collected data object. + * Whether to create a Selector for each collected data object. * @default false */ select?: boolean; @@ -1557,13 +1557,13 @@ export interface CollectConfigInterface export interface UpdateConfigInterface { /** - * Whether the data object with changes should be merged into the existing Item data object - * or overwrite it entirely. + * Whether to merge the data object with changes into the existing Item data object + * or overwrite the existing Item data object entirely. * @default true */ patch?: boolean | PatchOptionConfigInterface; /** - * Whether the Item data object should be updated in background. + * Whether to update the data object in background. * So that the UI isn't notified of these changes and thus doesn't rerender. * @default false */ @@ -1572,7 +1572,7 @@ export interface UpdateConfigInterface { export interface UpdateItemKeyConfigInterface { /** - * Whether the Item key/name identifier should be updated in background. + * Whether to update the Item key/name identifier in background * So that the UI isn't notified of these changes and thus doesn't rerender. * @default false */ @@ -1581,13 +1581,13 @@ export interface UpdateItemKeyConfigInterface { export interface RebuildGroupsThatIncludeItemKeyConfigInterface { /** - * Whether the Group should be rebuilt in background. + * Whether to rebuilt the Group in background. * So that the UI isn't notified of these changes and thus doesn't rerender. * @default false */ background?: boolean; /** - * Whether the defined side effects should be executed. + * Whether to execute the defined side effects. * @default true */ sideEffects?: SideEffectConfigInterface; @@ -1605,19 +1605,20 @@ export interface HasConfigInterface { export interface CollectionPersistentConfigInterface { /** * Whether the Persistent should automatically load - * the persisted value into the Collection after instantiation. + * the persisted value into the Collection after its instantiation. * @default true */ loadValue?: boolean; /** - * Key/Name identifier of Storages in which the Collection should be persisted. + * Key/Name identifier of Storages + * in which the Collection value should be or is persisted. * @default [AgileTs default Storage key] */ storageKeys?: StorageKey[]; /** * Default Storage key of the specified Storage keys. - * The Collection value is loaded from the default Storage. - * The value is only loaded from the remaining Storages (storageKeys) + * The Collection value is loaded from the default Storage + * and only loaded from the remaining Storages (storageKeys) * if the loading of the default Storage failed. * * @default first index of the specified Storage keys or the AgileTs default Storage key @@ -1627,8 +1628,8 @@ export interface CollectionPersistentConfigInterface { export interface RemoveItemsConfigInterface { /** - * Whether not officially existing Items (such as placeholder Items) can be removed. - * Keep in mind that sometimes it won't remove the Item entirely + * Whether to remove not officially existing Items (such as placeholder Items). + * Keep in mind that sometimes it won't remove an Item entirely * as another Instance (like a Selector) might need to keep reference to it. * https://github.com/agile-ts/agile/pull/152 * @default false @@ -1644,8 +1645,8 @@ export interface RemoveItemsConfigInterface { export interface AssignDataConfigInterface { /** * When the Item identifier of the to assign data object already exists in the Collection, - * whether then the newly assigned data should be merged into the existing one - * or overwrite it entirely. + * whether to merge the newly assigned data into the existing one + * or overwrite the existing one entirely. * @default true */ patch?: boolean; @@ -1660,7 +1661,7 @@ export interface AssignDataConfigInterface { export interface AssignItemConfigInterface { /** * If an Item with the Item identifier already exists, - * whether it should then be overwritten with the new Item. + * whether to overwrite it entirely with the new one. * @default false */ overwrite?: boolean; diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index b4a92569..1abdf3c7 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -23,6 +23,8 @@ export class Item extends State< /** * An extension of the State Class that represents a single data object of a Collection. * + * It can be used independently, but is always synchronized with the Collection. + * * @public * @param collection - Collection to which the Item belongs. * @param data - Data object to be represented by the Item. @@ -47,7 +49,7 @@ export class Item extends State< } /** - * Updates key/name identifier of Persistent. + * Updates key/name identifier of Item. * * @internal * @param value - New key/name identifier. @@ -76,16 +78,8 @@ export class Item extends State< // Update itemKey in Item value // (After updating the side effect, because otherwise it would call the old side effect) - this.patch( - { [this.collection().config.primaryKey]: value }, - { - sideEffects: config.sideEffects, - background: config.background, - force: config.force, - storage: config.storage, - overwrite: config.overwrite, - } - ); + this.patch({ [this.collection().config.primaryKey]: value }, config); + return this; } @@ -134,7 +128,7 @@ export class Item extends State< _config = defineConfig(_config, { loadValue: true, - followCollectionPattern: false, + followCollectionPersistKeyPattern: true, storageKeys: [], defaultStorageKey: null, }); @@ -157,20 +151,20 @@ export class Item extends State< return this; } - //========================================================================================================= - // Add Rebuild Group That Include ItemKey SideEffect - //========================================================================================================= /** + * Adds the 'Rebuild Group that include Item Key' action to the Item side effects, + * so that the Groups which include the Item with the identifier `itemKey` + * are rebuilt when the Item changes. + * * @internal - * Adds rebuildGroupThatIncludeItemKey to the Item sideEffects - * @param itemKey - ItemKey at which the groups has to rebuild + * @param itemKey - Item identifier that has to be included in Groups so that these Groups can be rebuilt. */ public addRebuildGroupThatIncludeItemKeySideEffect(itemKey: StateKey) { this.addSideEffect>( Item.updateGroupSideEffectKey, (instance, config) => { // TODO optimise this because currently the whole Group rebuilds - // although only the Item value has changed which definitely needs no complete rebuild + // although only one Item value has changed which definitely needs no complete rebuild // https://github.com/agile-ts/agile/issues/113 instance.collection().rebuildGroupsThatIncludeItemKey(itemKey, config); }, @@ -179,17 +173,21 @@ export class Item extends State< } } -/** - * @param isPlaceholder - If Item is initially a Placeholder - */ export interface ItemConfigInterface { + /** + * Whether the Item should be a placeholder + * and therefore only exists in background. + * @default false + */ isPlaceholder?: boolean; } -/** - * @param useCollectionPattern - If Item storageKey follows the Collection Item StorageKey Pattern - */ export interface ItemPersistConfigInterface extends StatePersistentConfigInterface { + /** + * Whether to format the specified Storage key into the Collection Item Storage key pattern. + * `_${collectionKey}_item_${itemKey}` + * @default true + */ followCollectionPersistKeyPattern?: boolean; } diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index 42731bef..b5648374 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -193,7 +193,7 @@ export interface ComputedConfigInterface extends StateConfigInterface { export interface ComputeConfigInterface { /** - * Whether dependencies used in the compute function should be detected automatically. + * Whether to automatically detect used dependencies in the compute method. * @default true */ autodetect?: boolean; @@ -202,8 +202,8 @@ export interface ComputeConfigInterface { export interface UpdateComputeFunctionConfigInterface extends RecomputeConfigInterface { /** - * Whether the old hard-coded dependencies should be completely overwritten - * with the new ones or merged into the new ones. + * Whether to overwrite the old hard-coded dependencies with the new ones + * or merge them into the new ones. * @default false */ overwriteDeps?: boolean; diff --git a/packages/core/tests/unit/collection/group.test.ts b/packages/core/tests/unit/collection/group.test.ts index 01ef3502..ee05fce5 100644 --- a/packages/core/tests/unit/collection/group.test.ts +++ b/packages/core/tests/unit/collection/group.test.ts @@ -418,60 +418,48 @@ describe('Group Tests', () => { jest.spyOn(State.prototype, 'persist'); }); - it('should persist Group with GroupKey (default config)', () => { + it('should persist Group with formatted groupKey (default config)', () => { group.persist(); - expect(State.prototype.persist).toHaveBeenCalledWith(group._key, { - loadValue: true, - storageKeys: [], - defaultStorageKey: null, - }); + expect(State.prototype.persist).toHaveBeenCalledWith( + CollectionPersistent.getGroupStorageKey( + group._key, + dummyCollection._key + ), + { + loadValue: true, + storageKeys: [], + defaultStorageKey: null, + } + ); }); - it('should persist Group with GroupKey (specific config)', () => { + it('should persist Group with formatted groupKey (specific config)', () => { group.persist({ loadValue: false, storageKeys: ['test1', 'test2'], defaultStorageKey: 'test1', }); - expect(State.prototype.persist).toHaveBeenCalledWith(group._key, { - loadValue: false, - storageKeys: ['test1', 'test2'], - defaultStorageKey: 'test1', - }); + expect(State.prototype.persist).toHaveBeenCalledWith( + CollectionPersistent.getGroupStorageKey( + group._key, + dummyCollection._key + ), + { + loadValue: false, + storageKeys: ['test1', 'test2'], + defaultStorageKey: 'test1', + } + ); }); - it('should persist Group with passed Key (default config)', () => { + it('should persist Group with formatted specified key (default config)', () => { group.persist('dummyKey'); - expect(State.prototype.persist).toHaveBeenCalledWith('dummyKey', { - loadValue: true, - storageKeys: [], - defaultStorageKey: null, - }); - }); - - it('should persist Group with passed Key (specific config)', () => { - group.persist('dummyKey', { - loadValue: false, - storageKeys: ['test1', 'test2'], - defaultStorageKey: 'test1', - }); - - expect(State.prototype.persist).toHaveBeenCalledWith('dummyKey', { - loadValue: false, - storageKeys: ['test1', 'test2'], - defaultStorageKey: 'test1', - }); - }); - - it('should persist Group with formatted GroupKey (config.followCollectionPersistKeyPattern)', () => { - group.persist({ followCollectionPersistKeyPattern: true }); - expect(State.prototype.persist).toHaveBeenCalledWith( CollectionPersistent.getGroupStorageKey( - group._key, + 'dummyKey', dummyCollection._key ), { @@ -482,8 +470,12 @@ describe('Group Tests', () => { ); }); - it('should persist Group with formatted passed Key (config.followCollectionPersistKeyPattern)', () => { - group.persist('dummyKey', { followCollectionPersistKeyPattern: true }); + it('should persist Group with formatted specified key (specific config)', () => { + group.persist('dummyKey', { + loadValue: false, + storageKeys: ['test1', 'test2'], + defaultStorageKey: 'test1', + }); expect(State.prototype.persist).toHaveBeenCalledWith( CollectionPersistent.getGroupStorageKey( @@ -491,12 +483,32 @@ describe('Group Tests', () => { dummyCollection._key ), { - loadValue: true, - storageKeys: [], - defaultStorageKey: null, + loadValue: false, + storageKeys: ['test1', 'test2'], + defaultStorageKey: 'test1', } ); }); + + it('should persist Group with groupKey (config.followCollectionPersistKeyPattern = false)', () => { + group.persist({ followCollectionPersistKeyPattern: false }); + + expect(State.prototype.persist).toHaveBeenCalledWith(group._key, { + loadValue: true, + storageKeys: [], + defaultStorageKey: null, + }); + }); + + it('should persist Group with specified key (config.followCollectionPersistKeyPattern = false)', () => { + group.persist('dummyKey', { followCollectionPersistKeyPattern: false }); + + expect(State.prototype.persist).toHaveBeenCalledWith('dummyKey', { + loadValue: true, + storageKeys: [], + defaultStorageKey: null, + }); + }); }); describe('rebuild function tests', () => { diff --git a/packages/core/tests/unit/collection/item.test.ts b/packages/core/tests/unit/collection/item.test.ts index bbd902bf..80f44955 100644 --- a/packages/core/tests/unit/collection/item.test.ts +++ b/packages/core/tests/unit/collection/item.test.ts @@ -1,4 +1,11 @@ -import { Item, Collection, Agile, StateObserver, State } from '../../../src'; +import { + Item, + Collection, + Agile, + StateObserver, + State, + CollectionPersistent, +} from '../../../src'; import { LogMock } from '../../helper/logMock'; describe('Item Tests', () => { @@ -204,6 +211,104 @@ describe('Item Tests', () => { }); }); + describe('persist function tests', () => { + beforeEach(() => { + jest.spyOn(State.prototype, 'persist'); + }); + + it('should persist Item with formatted itemKey (default config)', () => { + item.persist(); + + expect(State.prototype.persist).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey( + item._key, + dummyCollection._key + ), + { + loadValue: true, + storageKeys: [], + defaultStorageKey: null, + } + ); + }); + + it('should persist Item with formatted itemLeu (specific config)', () => { + item.persist({ + loadValue: false, + storageKeys: ['test1', 'test2'], + defaultStorageKey: 'test1', + }); + + expect(State.prototype.persist).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey( + item._key, + dummyCollection._key + ), + { + loadValue: false, + storageKeys: ['test1', 'test2'], + defaultStorageKey: 'test1', + } + ); + }); + + it('should persist Item with formatted specified key (default config)', () => { + item.persist('dummyKey'); + + expect(State.prototype.persist).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey( + 'dummyKey', + dummyCollection._key + ), + { + loadValue: true, + storageKeys: [], + defaultStorageKey: null, + } + ); + }); + + it('should persist Item with formatted specified key (specific config)', () => { + item.persist('dummyKey', { + loadValue: false, + storageKeys: ['test1', 'test2'], + defaultStorageKey: 'test1', + }); + + expect(State.prototype.persist).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey( + 'dummyKey', + dummyCollection._key + ), + { + loadValue: false, + storageKeys: ['test1', 'test2'], + defaultStorageKey: 'test1', + } + ); + }); + + it('should persist Item with itemKey (config.followCollectionPersistKeyPattern = false)', () => { + item.persist({ followCollectionPersistKeyPattern: false }); + + expect(State.prototype.persist).toHaveBeenCalledWith(item._key, { + loadValue: true, + storageKeys: [], + defaultStorageKey: null, + }); + }); + + it('should persist Item with specified key (config.followCollectionPersistKeyPattern = false)', () => { + item.persist('dummyKey', { followCollectionPersistKeyPattern: false }); + + expect(State.prototype.persist).toHaveBeenCalledWith('dummyKey', { + loadValue: true, + storageKeys: [], + defaultStorageKey: null, + }); + }); + }); + describe('addRebuildGroupThatIncludeItemKeySideEffect function tests', () => { beforeEach(() => { dummyCollection.rebuildGroupsThatIncludeItemKey = jest.fn(); From af4e259637ba9e1f3f947acb26910a24bdb5e72b Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 4 Jun 2021 10:33:55 +0200 Subject: [PATCH 21/63] added descriptions to Group --- packages/core/src/collection/group.ts | 188 ++++++++++-------- packages/core/src/collection/index.ts | 70 +++---- packages/core/src/collection/item.ts | 6 +- packages/core/src/logCodeManager.ts | 6 + .../core/tests/unit/collection/group.test.ts | 19 +- .../core/tests/unit/collection/item.test.ts | 2 +- 6 files changed, 157 insertions(+), 134 deletions(-) diff --git a/packages/core/src/collection/group.ts b/packages/core/src/collection/group.ts index d4654f44..bdef020e 100644 --- a/packages/core/src/collection/group.ts +++ b/packages/core/src/collection/group.ts @@ -22,18 +22,25 @@ export class Group extends State< Array > { static rebuildGroupSideEffectKey = 'rebuildGroup'; - collection: () => Collection; // Collection the Group belongs to + collection: () => Collection; _output: Array = []; // Output of Group _items: Array<() => Item> = []; // Items of Group - notFoundItemKeys: Array = []; // Contains all keys of Group that can't be found in Collection + notFoundItemKeys: Array = []; // Contains all itemKeys that couldn't be found in the Collection /** + * An extension of the State Class that categorizes and preserves the ordering of structured data. + * It allows us to cluster together data from a Collection as an array of Item keys. + * + * Note that a Group doesn't store the actual Items. It only keeps track of the Item keys + * and retrieves the fitting Items when needed. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/group/) + * * @public - * Group - Holds Items of Collection - * @param collection - Collection to that the Group belongs - * @param initialItems - Initial Key of Items in this Group - * @param config - Config + * @param collection - Collection to which the Item belongs. + * @param initialItems - Identifiers of the Items to be clustered by the Group. + * @param config - Configuration object */ constructor( collection: Collection, @@ -43,78 +50,76 @@ export class Group extends State< super(collection.agileInstance(), initialItems || [], config); this.collection = () => collection; - // Add rebuild to sideEffects to rebuild Group on Value Change + // Add the 'Rebuild Group' action to the Group side effects + // in order to rebuild the Group whenever its value changes this.addSideEffect(Group.rebuildGroupSideEffectKey, () => this.rebuild()); - // Initial Rebuild + // Initial rebuild this.rebuild(); } /** + * Retrieves values of the Items clustered by the Group. + * * @public - * Get Item Values of Group */ public get output(): Array { ComputedTracker.tracked(this.observer); return copy(this._output); } - /** - * @public - * Set Item Values of Group - */ public set output(value: DataType[]) { - this._output = copy(value); + LogCodeManager.log('1C:03:00', [this._key]); } /** + * Retrieves Items clustered by the Group. + * * @public - * Get Items of Group */ public get items(): Array> { ComputedTracker.tracked(this.observer); return this._items.map((item) => item()); } - /** - * @public - * Set Items of Group - */ public set items(value: Array>) { - this._items = value.map((item) => () => item); + LogCodeManager.log('1C:03:01', [this._key]); } - //========================================================================================================= - // Has - //========================================================================================================= /** + * + * Returns a boolean indicating whether an Item with the specified `itemKey` + * is clustered in the Group or not. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods/#has) + * * @public - * Checks if Group contains ItemKey - * @param itemKey - ItemKey that gets checked + * @param itemKey - Key/Name identifier of the Item. */ public has(itemKey: ItemKey) { return this.value.findIndex((key) => key === itemKey) !== -1; } - //========================================================================================================= - // Size - //========================================================================================================= /** + * Returns the count of the Items clustered in the Group. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/group/properties#size) + * * @public - * Get size of Group (-> How many Items it contains) */ public get size(): number { return this.value.length; } - //========================================================================================================= - // Remove - //========================================================================================================= /** + * Removes an Item with the specified key/name identifier from the Group, + * if it exists in the Group. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#remove) + * * @public - * Removes ItemKey/s from Group - * @param itemKeys - ItemKey/s that get removed from Group - * @param config - Config + * @param itemKeys - Key/Name identifier/s of the Item/s to be removed. + * @param config - Configuration object */ public remove( itemKeys: ItemKey | ItemKey[], @@ -125,7 +130,7 @@ export class Group extends State< const notExistingItemKeys: Array = []; let newGroupValue = copy(this.nextStateValue); - // Remove ItemKeys from Group + // Remove itemKeys from Group _itemKeys.forEach((itemKey) => { // Check if itemKey exists in Group if (!newGroupValue.includes(itemKey)) { @@ -134,18 +139,19 @@ export class Group extends State< return; } - // Check if ItemKey exists in Collection + // Check if itemKey exists in Collection if (!this.collection().getItem(itemKey)) notExistingItemKeysInCollection.push(itemKey); - // Remove ItemKey from Group + // Remove itemKey from Group newGroupValue = newGroupValue.filter((key) => key !== itemKey); }); - // Return if passed ItemKeys doesn't exist + // Return if none of the specified itemKeys exists if (notExistingItemKeys.length >= _itemKeys.length) return this; - // If all removed ItemKeys doesn't exist in Collection -> no rerender necessary since output doesn't change + // If all removed itemKeys don't exist in the Collection + // -> no rerender necessary since the output won't change if (notExistingItemKeysInCollection.length >= _itemKeys.length) config.background = true; @@ -154,14 +160,14 @@ export class Group extends State< return this; } - //========================================================================================================= - // Add - //========================================================================================================= /** + * Appends new Item/s to the end of the Group. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#add) + * * @public - * Adds ItemKey/s to Group - * @param itemKeys - ItemKey/s that get added to the Group - * @param config - Config + * @param itemKeys - Key/Name identifier/s of Item/s to be added. + * @param config - Configuration object */ public add( itemKeys: ItemKey | ItemKey[], @@ -176,16 +182,14 @@ export class Group extends State< overwrite: false, }); - // Add ItemKeys to Group + // Add itemKeys to Group _itemKeys.forEach((itemKey) => { - const existsInGroup = newGroupValue.includes(itemKey); - - // Check if ItemKey exists in Collection + // Check if itemKey exists in Collection if (!this.collection().getItem(itemKey)) notExistingItemKeysInCollection.push(itemKey); - // Remove ItemKey from Group if it should get overwritten and already exists - if (existsInGroup) { + // Remove itemKey from Group if it should be overwritten and already exists + if (this.has(itemKey)) { if (config.overwrite) { newGroupValue = newGroupValue.filter((key) => key !== itemKey); } else { @@ -194,14 +198,15 @@ export class Group extends State< } } - // Add new ItemKey to Group + // Add new itemKey to Group newGroupValue[config.method || 'push'](itemKey); }); - // Return if passed ItemKeys already exist + // Return if all specified itemKeys already exist if (existingItemKeys.length >= _itemKeys.length) return this; - // If all added ItemKeys doesn't exist in Collection or already exist -> no rerender necessary since output doesn't change + // If all added itemKeys don't exist in the Collection + // -> no rerender necessary since the output won't change if ( notExistingItemKeysInCollection.concat(existingItemKeys).length >= _itemKeys.length @@ -213,20 +218,20 @@ export class Group extends State< return this; } - //========================================================================================================= - // Replace - //========================================================================================================= /** + * Replaces the old `itemKey` with a new specified `itemKey`. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#replace) + * * @public - * Replaces oldItemKey with newItemKey - * @param oldItemKey - Old ItemKey - * @param newItemKey - New ItemKey - * @param config - Config + * @param oldItemKey - Old `itemKey` to be replaced. + * @param newItemKey - New `itemKey` which replaces the specified old `itemKey`. + * @param config - Configuration object */ public replace( oldItemKey: ItemKey, newItemKey: ItemKey, - config: StateRuntimeJobConfigInterface = {} + config: StateIngestConfigInterface = {} ): this { const newGroupValue = copy(this._value); newGroupValue.splice(newGroupValue.indexOf(oldItemKey), 1, newItemKey); @@ -302,30 +307,33 @@ export class Group extends State< return this; } - //========================================================================================================= - // Rebuild - //========================================================================================================= /** + * Rebuilds the entire `output` and `items` property of the Group. + * + * In doing so, it traverses the Group `value` (Item identifiers) + * and fetches the Items that belong to an Item identifier. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#rebuild) + * * @internal - * Rebuilds Output and Items of Group */ public rebuild(): this { - const notFoundItemKeys: Array = []; // Item Keys that couldn't be found in Collection + const notFoundItemKeys: Array = []; // Item keys that couldn't be found in the Collection const groupItems: Array> = []; - // Don't rebuild Group if Collection is not properly instantiated + // Don't rebuild Group if Collection isn't correctly instantiated // (because only after a successful instantiation the Collection - // contains Items which are essential for a proper rebuild) + // contains the Items which are essential for a proper rebuild) if (!this.collection().isInstantiated) return this; - // Create groupItems by finding Item at ItemKey in Collection + // Fetch Items from Collection this._value.forEach((itemKey) => { const item = this.collection().getItem(itemKey); if (item != null) groupItems.push(item); else notFoundItemKeys.push(itemKey); }); - // Create groupOutput out of groupItems + // Get Item values of fetched Items const groupOutput = groupItems.map((item) => { return item.getPublicValue(); }); @@ -339,7 +347,7 @@ export class Group extends State< ); } - this.items = groupItems; + this._items = groupItems.map((item) => () => item); this._output = groupOutput; this.notFoundItemKeys = notFoundItemKeys; @@ -349,36 +357,44 @@ export class Group extends State< export type GroupKey = string | number; -/** - * @param method - Way of adding ItemKey to Group (push, unshift) - * @param overwrite - If adding ItemKey overwrites old ItemKey (-> otherwise it gets added to the end of the Group) - * @param background - If adding ItemKey happens in the background (-> not causing any rerender) - */ export interface GroupAddConfigInterface extends StateIngestConfigInterface { + /** + * In which way the `itemKey` should be added to the Group. + * - 'push' = at the end + * - 'unshift' = at the beginning + * @default 'push' + */ method?: 'unshift' | 'push'; + /** + * If the to add `itemKey` already exists, + * whether to overwrite its position with the position of the new `itemKey`. + * @default false + */ overwrite?: boolean; } -/** - * @param background - If removing ItemKey happens in the background (-> not causing any rerender) - */ -export interface GroupRemoveConfigInterface { - background?: boolean; -} - /** * @param key - Key/Name of Group * @param isPlaceholder - If Group is initially a Placeholder */ export interface GroupConfigInterface { + /** + * Key/Name identifier of Group. + * @default undefined + */ key?: GroupKey; + /** + * Whether the Group should be a placeholder + * and therefore should only exists in the background. + * @default false + */ isPlaceholder?: boolean; } export interface GroupPersistConfigInterface extends StatePersistentConfigInterface { /** - * Whether to format the specified Storage key into the Collection Group Storage key pattern. + * Whether to format the specified Storage key following the Collection Group Storage key pattern. * `_${collectionKey}_group_${groupKey}` * @default true */ diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index cf7c7366..92151144 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -29,7 +29,7 @@ export class Collection { public config: CollectionConfigInterface; private initialConfig: CreateCollectionConfigInterface; - public size = 0; // Amount of Items stored in the Collection + public size = 0; // Amount of the Items stored in the Collection public data: { [key: string]: Item } = {}; // Collection Data public _key?: CollectionKey; public isPersisted = false; // Whether Collection is persisted in any external Storage @@ -143,7 +143,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#group) * * @public - * @param initialItems - Initial keys of Items to be represented by the Group. + * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group. * @param config - Configuration object */ public Group( @@ -171,7 +171,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#selector) * * @public - * @param initialKey - Initial key of Item to be represented by the Selector. + * @param initialKey - Key/Name identifier of the Item to be represented by the Selector. * @param config - Configuration object */ public Selector( @@ -341,7 +341,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#update) * * @public - * @param itemKey - Key/Name identifier of Item to be updated. + * @param itemKey - Key/Name identifier of the Item to be updated. * @param changes - Object with changes to be merged into the Item data object. * @param config - Configuration object */ @@ -416,7 +416,7 @@ export class Collection { * * @public * @param groupKey - Unique identifier of the Group to be created. - * @param initialItems - Initial keys of Items to be represented by the Group. + * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group. */ public createGroup( groupKey: GroupKey, @@ -449,7 +449,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasgroup) * * @public - * @param groupKey - Key/Name identifier of Group. + * @param groupKey - Key/Name identifier of the Group. * @param config - Configuration object */ public hasGroup( @@ -467,7 +467,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroup) * * @public - * @param groupKey - Key/Name identifier of Group. + * @param groupKey - Key/Name identifier of the Group. * @param config - Configuration object */ public getGroup( @@ -515,7 +515,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupwithreference) * * @public - * @param groupKey - Key/Name identifier of Group. + * @param groupKey - Key/Name identifier of the Group. */ public getGroupWithReference(groupKey: GroupKey): Group { let group = this.getGroup(groupKey, { notExisting: true }); @@ -534,13 +534,13 @@ export class Collection { } /** - * Removes a Group with the specified identifier from the Collection, + * Removes a Group with the specified key/name identifier from the Collection, * if it exists in the Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removegroup) * * @public - * @param groupKey - Key/Name identifier of Group to be removed. + * @param groupKey - Key/Name identifier of the Group to be removed. */ public removeGroup(groupKey: GroupKey): this { if (this.groups[groupKey] != null) delete this.groups[groupKey]; @@ -566,8 +566,8 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createSelector) * * @public - * @param selectorKey - Unique identifier of Selector to be created. - * @param itemKey - Initial key of Item to be represented by the Selector. + * @param selectorKey - Unique identifier of the Selector to be created. + * @param itemKey - Key/Name identifier of the Item to be represented by the Selector. */ public createSelector( selectorKey: SelectorKey, @@ -608,7 +608,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#select) * * @public - * @param itemKey - Initial key of Item to be represented by the Selector + * @param itemKey - Key/Name identifier of the Item to be represented by the Selector * and used as unique identifier of the Selector. */ public select(itemKey: ItemKey): Selector { @@ -622,7 +622,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasselector) * * @public - * @param selectorKey - Key/Name identifier of Selector. + * @param selectorKey - Key/Name identifier of the Selector. * @param config - Configuration object */ public hasSelector( @@ -640,7 +640,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselector) * * @public - * @param selectorKey - Key/Name identifier of Selector. + * @param selectorKey - Key/Name identifier of the Selector. * @param config - Configuration object */ public getSelector( @@ -672,7 +672,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorwithreference) * * @public - * @param selectorKey - Key/Name identifier of Selector. + * @param selectorKey - Key/Name identifier of the Selector. */ public getSelectorWithReference( selectorKey: SelectorKey @@ -697,13 +697,13 @@ export class Collection { } /** - * Removes a Selector with the specified identifier from the Collection, + * Removes a Selector with the specified key/name identifier from the Collection, * if it exists in the Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeselector) * * @public - * @param selectorKey - Key/Name identifier of Selector to be removed. + * @param selectorKey - Key/Name identifier of the Selector to be removed. */ public removeSelector(selectorKey: SelectorKey): this { if (this.selectors[selectorKey] != null) { @@ -733,7 +733,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasitem) * * @public - * @param itemKey - Key/Name identifier of Item. + * @param itemKey - Key/Name identifier of the Item. * @param config - Configuration object */ public hasItem( @@ -751,7 +751,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitem) * * @public - * @param itemKey - Key/Name identifier of Item. + * @param itemKey - Key/Name identifier of the Item. * @param config - Configuration object */ public getItem( @@ -782,7 +782,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemwithreference) * * @public - * @param itemKey - Key/Name identifier of Item. + * @param itemKey - Key/Name identifier of the Item. */ public getItemWithReference(itemKey: ItemKey): Item { let item = this.getItem(itemKey, { notExisting: true }); @@ -810,7 +810,7 @@ export class Collection { const item = new Item( this, { - [this.config.primaryKey]: itemKey, // Setting primaryKey of Item to passed itemKey + [this.config.primaryKey]: itemKey, // Setting primaryKey of the Item to passed itemKey dummy: 'item', } as any, { isPlaceholder: true } @@ -836,7 +836,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemvalue) * * @public - * @param itemKey - Key/Name identifier of Item. + * @param itemKey - Key/Name identifier of the Item. * @param config - Configuration object */ public getItemValue( @@ -1024,7 +1024,7 @@ export class Collection { * * @public * @param itemKeys - `itemKey/s` to be put into the specified Group/s. - * @param groupKeys - Identifier/s of the Group/s the specified `itemKey/s` are to put in. + * @param groupKeys - Key/Name Identifier/s of the Group/s the specified `itemKey/s` are to put in. * @param config - Configuration object */ public put( @@ -1050,8 +1050,8 @@ export class Collection { * * @public * @param itemKeys - `itemKey/s` to be moved. - * @param oldGroupKey - Identifier of the Group the `itemKey/s` are moved from. - * @param newGroupKey - Identifier of the Group the `itemKey/s` are moved in. + * @param oldGroupKey - Key/Name Identifier of the Group the `itemKey/s` are moved from. + * @param newGroupKey - Key/Name Identifier of the Group the `itemKey/s` are moved in. * @param config - Configuration object */ public move( @@ -1075,7 +1075,7 @@ export class Collection { } /** - * Updates key/name identifier of Item + * Updates key/name identifier of the Item * and returns a boolean indicating * whether the Item identifier was updated successfully. * @@ -1106,12 +1106,12 @@ export class Collection { delete this.data[oldItemKey]; this.data[newItemKey] = item; - // Update key/name of Item + // Update key/name of the Item item.setKey(newItemKey, { background: config.background, }); - // Update Persistent key of Item if it follows the Item Storage Key pattern + // Update Persistent key of the Item if it follows the Item Storage Key pattern // and therefore differs from the actual Item key // (-> isn't automatically updated when the Item key is updated) if ( @@ -1211,13 +1211,13 @@ export class Collection { } /** - * Remove Item/s from Group/s. + * Remove Item/s from specified Group/s. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removefromgroups) * * @public - * @param itemKeys - Identifier/s of Item/s to be removed from Group/s. - * @param groupKeys - Identifier/s of Group/s the Item/s are to remove from. + * @param itemKeys - Key/Name Identifier/s of the Item/s to be removed from the Group/s. + * @param groupKeys - Key/Name Identifier/s of the Group/s the Item/s are to remove from. */ public removeFromGroups( itemKeys: ItemKey | Array, @@ -1229,7 +1229,7 @@ export class Collection { _itemKeys.forEach((itemKey) => { let removedFromGroupsCount = 0; - // Remove itemKey from Groups + // Remove itemKey from the Groups _groupKeys.forEach((groupKey) => { const group = this.getGroup(groupKey, { notExisting: true }); if (!group?.has(itemKey)) return; @@ -1255,7 +1255,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeitems) * * @public - * @param itemKeys - Identifier/s of Item/s to be removed from the entire Collection. + * @param itemKeys - Key/Name identifier/s of the Item/s to be removed from the entire Collection. * @param config - Configuration object */ public removeItems( @@ -1273,7 +1273,7 @@ export class Collection { if (item == null) return; const wasPlaceholder = item.isPlaceholder; - // Remove Item from Groups + // Remove Item from the Groups for (const groupKey in this.groups) { const group = this.getGroup(groupKey, { notExisting: true }); if (group?.has(itemKey)) group?.remove(itemKey); diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index 1abdf3c7..cc4c5ad6 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -157,7 +157,7 @@ export class Item extends State< * are rebuilt when the Item changes. * * @internal - * @param itemKey - Item identifier that has to be included in Groups so that these Groups can be rebuilt. + * @param itemKey - Item identifier that has to be included in Groups so that these Groups are rebuilt. */ public addRebuildGroupThatIncludeItemKeySideEffect(itemKey: StateKey) { this.addSideEffect>( @@ -176,7 +176,7 @@ export class Item extends State< export interface ItemConfigInterface { /** * Whether the Item should be a placeholder - * and therefore only exists in background. + * and therefore should only exists in the background. * @default false */ isPlaceholder?: boolean; @@ -185,7 +185,7 @@ export interface ItemConfigInterface { export interface ItemPersistConfigInterface extends StatePersistentConfigInterface { /** - * Whether to format the specified Storage key into the Collection Item Storage key pattern. + * Whether to format the specified Storage key following the Collection Item Storage key pattern. * `_${collectionKey}_item_${itemKey}` * @default true */ diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 8ac05a98..1e99b165 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -144,6 +144,12 @@ const logCodeMessages = { '1C:02:00': "Couldn't find some Items in the Collection '${0}' " + "during the rebuild of the Group '${1}' output.", + '1C:03:00': + "The 'output' property of the Group '${0}' is a automatically generated readonly property " + + 'that can only be mutated by the Group itself!', + '1C:03:01': + "The 'item' property of the Group '${0}' is a automatically generated readonly property " + + 'that can only be mutated by the Group itself!', // Utils '20:03:00': 'Failed to get Agile Instance from', diff --git a/packages/core/tests/unit/collection/group.test.ts b/packages/core/tests/unit/collection/group.test.ts index ee05fce5..72ea153b 100644 --- a/packages/core/tests/unit/collection/group.test.ts +++ b/packages/core/tests/unit/collection/group.test.ts @@ -182,16 +182,16 @@ describe('Group Tests', () => { }); describe('output set function tests', () => { - it('should set output to passed value', () => { + it("shouldn't set output to passed value and print error", () => { + group._output = null as any; + group.output = [ { id: '12', name: 'Hans der 3' }, { id: '99', name: 'Frank' }, ]; - expect(group._output).toStrictEqual([ - { id: '12', name: 'Hans der 3' }, - { id: '99', name: 'Frank' }, - ]); + expect(group._output).toStrictEqual(null); + expect(LogMock.hasLoggedCode('1C:03:00', [group._key])); }); }); @@ -211,12 +211,13 @@ describe('Group Tests', () => { }); describe('item set function tests', () => { - it('should set items to passed value', () => { + it("shouldn't set items to passed value and print error", () => { + group._items = null as any; + group.items = [dummyItem1, dummyItem2]; - expect(group._items.length).toBe(2); - expect(group._items[0]()).toBe(dummyItem1); - expect(group._items[1]()).toBe(dummyItem2); + expect(group._items).toStrictEqual(null); + expect(LogMock.hasLoggedCode('1C:03:01', [group._key])); }); }); diff --git a/packages/core/tests/unit/collection/item.test.ts b/packages/core/tests/unit/collection/item.test.ts index 80f44955..3e55cff0 100644 --- a/packages/core/tests/unit/collection/item.test.ts +++ b/packages/core/tests/unit/collection/item.test.ts @@ -232,7 +232,7 @@ describe('Item Tests', () => { ); }); - it('should persist Item with formatted itemLeu (specific config)', () => { + it('should persist Item with formatted itemKey (specific config)', () => { item.persist({ loadValue: false, storageKeys: ['test1', 'test2'], From 44966aa1820810bf620fce95c6fd5310b56c497c Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 4 Jun 2021 12:00:37 +0200 Subject: [PATCH 22/63] refactored the selector method descriptions --- packages/core/src/collection/group.ts | 16 +- packages/core/src/collection/index.ts | 4 + packages/core/src/collection/item.ts | 2 +- packages/core/src/collection/selector.ts | 187 +++++++++++------- .../tests/unit/collection/selector.test.ts | 74 ++++--- 5 files changed, 183 insertions(+), 100 deletions(-) diff --git a/packages/core/src/collection/group.ts b/packages/core/src/collection/group.ts index bdef020e..840fb6c8 100644 --- a/packages/core/src/collection/group.ts +++ b/packages/core/src/collection/group.ts @@ -21,9 +21,10 @@ import { export class Group extends State< Array > { - static rebuildGroupSideEffectKey = 'rebuildGroup'; collection: () => Collection; + static rebuildGroupSideEffectKey = 'rebuildGroup'; + _output: Array = []; // Output of Group _items: Array<() => Item> = []; // Items of Group notFoundItemKeys: Array = []; // Contains all itemKeys that couldn't be found in the Collection @@ -38,8 +39,8 @@ export class Group extends State< * [Learn more..](https://agile-ts.org/docs/core/collection/group/) * * @public - * @param collection - Collection to which the Item belongs. - * @param initialItems - Identifiers of the Items to be clustered by the Group. + * @param collection - Collection to which the Group belongs. + * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group. * @param config - Configuration object */ constructor( @@ -61,6 +62,8 @@ export class Group extends State< /** * Retrieves values of the Items clustered by the Group. * + * [Learn more..](https://agile-ts.org/docs/core/collection/group/properties#output) + * * @public */ public get output(): Array { @@ -75,6 +78,8 @@ export class Group extends State< /** * Retrieves Items clustered by the Group. * + * [Learn more..](https://agile-ts.org/docs/core/collection/group/properties#items) + * * @public */ public get items(): Array> { @@ -87,7 +92,6 @@ export class Group extends State< } /** - * * Returns a boolean indicating whether an Item with the specified `itemKey` * is clustered in the Group or not. * @@ -321,7 +325,7 @@ export class Group extends State< const notFoundItemKeys: Array = []; // Item keys that couldn't be found in the Collection const groupItems: Array> = []; - // Don't rebuild Group if Collection isn't correctly instantiated + // Don't rebuild Group if Collection isn't correctly instantiated yet // (because only after a successful instantiation the Collection // contains the Items which are essential for a proper rebuild) if (!this.collection().isInstantiated) return this; @@ -385,7 +389,7 @@ export interface GroupConfigInterface { key?: GroupKey; /** * Whether the Group should be a placeholder - * and therefore should only exists in the background. + * and therefore should only exist in the background. * @default false */ isPlaceholder?: boolean; diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 92151144..a60d959f 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -92,6 +92,8 @@ export class Collection { /** * Updates key/name identifier of Collection. * + * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key) + * * @public * @param value - New key/name identifier. */ @@ -102,6 +104,8 @@ export class Collection { /** * Returns key/name identifier of Collection. * + * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key) + * * @public */ public get key(): CollectionKey | undefined { diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index cc4c5ad6..eab17158 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -176,7 +176,7 @@ export class Item extends State< export interface ItemConfigInterface { /** * Whether the Item should be a placeholder - * and therefore should only exists in the background. + * and therefore should only exist in the background. * @default false */ isPlaceholder?: boolean; diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index deaf17b7..a0b6ee1b 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -11,19 +11,28 @@ import { export class Selector extends State< DataType | undefined > { + public collection: () => Collection; + static unknownItemPlaceholderKey = '__UNKNOWN__ITEM__KEY__'; static rebuildSelectorSideEffectKey = 'rebuildSelector'; static rebuildItemSideEffectKey = 'rebuildItem'; - public collection: () => Collection; - public item: Item | undefined; - public _itemKey: ItemKey; // Key of Item the Selector represents + + public _item: Item | undefined; // Item the Selector represents + public _itemKey: ItemKey; // Key/Name identifier of the Item the Selector represents /** + * A Selector represents an Item from a Collection in the long term. + * It can be mutated dynamically and remains in sync with the Collection. + * + * Components that need one piece of data from a Collection such as the "current user" + * would benefit from using Selectors. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/selector) + * * @public - * Represents Item of Collection - * @param collection - Collection that contains the Item - * @param itemKey - ItemKey of Item that the Selector represents - * @param config - Config + * @param collection - Collection to which the Selector belongs. + * @param itemKey - Key/Name identifier of the Item to be represented by the Selector. + * @param config - Configuration object */ constructor( collection: Collection, @@ -35,41 +44,71 @@ export class Selector extends State< }); super(collection.agileInstance(), undefined, config); this.collection = () => collection; - this.item = undefined; + this._item = undefined; this._itemKey = !config.isPlaceholder ? itemKey : Selector.unknownItemPlaceholderKey; this._key = config?.key; this.isPlaceholder = true; // Because hasn't selected any Item yet - // Initial Select + // Initial select of the Item if (!config.isPlaceholder) this.select(itemKey, { overwrite: true }); } /** + * Retrieves the `itemKey` currently selected by the Selector. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/selector/properties#itemkey) + * * @public - * Set ItemKey that the Selector represents + */ + public get itemKey(): ItemKey { + return this._itemKey; + } + + /** + * Updates the currently selected Item of the Selector. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/selector/properties#itemkey) + * + * @public + * @param value - New key/name identifier of the Item to be represented by the Selector. */ public set itemKey(value: ItemKey) { this.select(value); } /** + * Retrieves the Item currently selected by the Selector. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/selector/properties#item) + * * @public - * Get ItemKey that the Selector represents */ - public get itemKey() { - return this._itemKey; + public get item(): Item | undefined { + return this._item; } - //========================================================================================================= - // Select - //========================================================================================================= /** + * Updates the currently selected Item of the Selector. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/selector/properties#item) + * * @public - * Select new ItemKey - * @param itemKey - New ItemKey - * @param config - Config + * @param value - New Item to be represented by the Selector. + */ + public set item(value: Item | undefined) { + if (value?._key) this.select(value._key); + } + + /** + * Updates the currently selected Item of the Selector. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#select) + * + * @public + * @param itemKey - New key/name identifier of the Item to be represented by the Selector. + * @param config - Configuration object */ public select( itemKey: ItemKey, @@ -82,11 +121,11 @@ export class Selector extends State< exclude: [], }, force: false, - overwrite: this.item?.isPlaceholder ?? false, + overwrite: this._item?.isPlaceholder ?? false, storage: true, }); - // Don't select Item if Collection is not properly instantiated + // Don't select Item if Collection is not correctly instantiated yet // (because only after a successful instantiation the Collection // contains the Items which are essential for a proper selection) if ( @@ -98,27 +137,29 @@ export class Selector extends State< // Unselect old Item this.unselect({ background: true }); - // Get new Item + // Retrieve new Item from Collection const newItem = this.collection().getItemWithReference(itemKey); // Select new Item this._itemKey = itemKey; - this.item = newItem; + this._item = newItem; newItem.selectedBy.add(this._key as any); - // Add SideEffect to newItem, that rebuild this Selector depending on the current Item Value + // Add side effect to the newly selected Item + // that rebuilds the Selector depending on the current Item value newItem.addSideEffect( Selector.rebuildSelectorSideEffectKey, (instance, config) => this.rebuildSelector(config), { weight: 100 } ); - // Add sideEffect to Selector, that updates the Item Value if this Value got updated + // Add side effect to Selector + // that updates the Item value depending on the current Selector value this.addSideEffect>( Selector.rebuildItemSideEffectKey, (instance, config) => { - if (!instance.item?.isPlaceholder) - instance.item?.set(instance._value as any, { + if (!instance._item?.isPlaceholder) + instance._item?.set(instance._value as any, { ...config, ...{ sideEffects: { @@ -131,21 +172,24 @@ export class Selector extends State< { weight: 90 } ); - // Rebuild Selector for instantiating new 'selected' ItemKey properly + // Rebuild Selector for 'instantiating' the newly selected Item properly this.rebuildSelector(config); return this; } - //========================================================================================================= - // Reselect - //========================================================================================================= /** + * Reselects the currently selected Item. + * + * This might be helpful if the Selector failed to select the Item correctly + * and therefore should try to select it again. + * + * You can use the 'hasSelected()' method to check whether the Item is selected correctly. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#reselect) + * * @public - * Reselects current Item - * Might help if the Selector failed to select an Item correctly. - * You can check with 'hasSelected()' if an Item got correctly selected. - * @param config - Config + * @param config - Configuration object */ public reselect(config: StateRuntimeJobConfigInterface = {}): this { if ( @@ -156,18 +200,19 @@ export class Selector extends State< return this; } - //========================================================================================================= - // Unselect - //========================================================================================================= /** + * Unselects the currently selected Item. + * + * Therefore, it sets the `itemKey` and `item` property to `undefined`, + * since the Selector no longer represents any Item. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#unselect) + * * @public - * Unselects current selected Item. - * Often not necessary because by selecting a new Item, - * the old Item is automatically unselected. - * @param config - Config + * @param config - Configuration object */ public unselect(config: StateRuntimeJobConfigInterface = {}): this { - // Because this.item might be outdated + // Retrieve Item from Collection because 'this._item' might be outdated const item = this.collection().getItem(this._itemKey, { notExisting: true, }); @@ -180,53 +225,54 @@ export class Selector extends State< if (item.isPlaceholder) delete this.collection().data[this._itemKey]; } - // Reset and rebuild Selector - this.item = undefined; + // Reset Selector + this._item = undefined; this._itemKey = Selector.unknownItemPlaceholderKey; this.rebuildSelector(config); - this.isPlaceholder = true; return this; } - //========================================================================================================= - // Has Selected - //========================================================================================================= /** - * Checks if Selector has correctly selected the Item at the passed itemKey - * @param itemKey - ItemKey - * @param correctlySelected - If it should consider only correctly selected Items + * Returns a boolean indicating whether an Item with the specified `itemKey` + * is selected by the Selector or not. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#hasselected) + * + * @public + * @param itemKey - Key/Name identifier of the Item. + * @param correctlySelected - Whether the Item has to be selected correctly. */ public hasSelected(itemKey: ItemKey, correctlySelected = true): boolean { if (correctlySelected) { return ( this._itemKey === itemKey && - this.item != null && - this.item.selectedBy.has(this._key as any) + this._item != null && + this._item.selectedBy.has(this._key as any) ); } return this._itemKey === itemKey; } - //========================================================================================================= - // Rebuild Selector - //========================================================================================================= /** + * Rebuilds the Selector, + * which updates the Selector value based on the Item value. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#rebuild) + * * @public - * Rebuilds Selector, - * which updates the Selector value based on the Item value - * @param config - Config + * @param config - Configuration object */ public rebuildSelector(config: StateRuntimeJobConfigInterface = {}): this { - // Set Selector Value to undefined if Item doesn't exist - if (this.item == null || this.item.isPlaceholder) { + // Assign 'undefined' to the Selector value if no Item is set + if (this._item == null || this._item.isPlaceholder) { this.set(undefined, config); return this; } - // Set Selector Value to updated Item Value - this.set(this.item._value, config); + // Assign the current Item value to the Selector value + this.set(this._item._value, config); return this; } @@ -234,11 +280,16 @@ export class Selector extends State< export type SelectorKey = string | number; -/** - * @param key - Key/Name of Selector - * @param isPlaceholder - If Selector is initially a Placeholder - */ export interface SelectorConfigInterface { + /** + * Key/Name identifier of Selector. + * @default undefined + */ key?: SelectorKey; + /** + * Whether the Selector should be a placeholder + * and therefore should only exist in the background. + * @default false + */ isPlaceholder?: boolean; } diff --git a/packages/core/tests/unit/collection/selector.test.ts b/packages/core/tests/unit/collection/selector.test.ts index 03f088d0..52185ec5 100644 --- a/packages/core/tests/unit/collection/selector.test.ts +++ b/packages/core/tests/unit/collection/selector.test.ts @@ -29,7 +29,7 @@ describe('Selector Tests', () => { const selector = new Selector(dummyCollection, 'dummyItemKey'); expect(selector.collection()).toBe(dummyCollection); - expect(selector.item).toBeUndefined(); + expect(selector._item).toBeUndefined(); expect(selector._itemKey).toBe('dummyItemKey'); expect(selector.select).toHaveBeenCalledWith('dummyItemKey', { overwrite: true, @@ -65,7 +65,7 @@ describe('Selector Tests', () => { }); expect(selector.collection()).toBe(dummyCollection); - expect(selector.item).toBeUndefined(); + expect(selector._item).toBeUndefined(); expect(selector._itemKey).toBe('dummyItemKey'); expect(selector.select).toHaveBeenCalledWith('dummyItemKey', { overwrite: true, @@ -101,7 +101,7 @@ describe('Selector Tests', () => { }); expect(selector.collection()).toBe(dummyCollection); - expect(selector.item).toBeUndefined(); + expect(selector._item).toBeUndefined(); expect(selector._itemKey).toBe(Selector.unknownItemPlaceholderKey); expect(selector.select).not.toHaveBeenCalled(); @@ -143,21 +143,45 @@ describe('Selector Tests', () => { describe('itemKey set function tests', () => { it('should call select function with passed value', () => { selector.select = jest.fn(); + selector._itemKey = null as any; selector.itemKey = 'newItemKey'; expect(selector.select).toHaveBeenCalledWith('newItemKey'); + expect(selector._itemKey).toBeNull(); }); }); describe('itemKey get function tests', () => { - it('should return current ItemKey of Selector', () => { + it('should return currently selected ItemKey of Selector', () => { selector._itemKey = 'coolItemKey'; expect(selector.itemKey).toBe('coolItemKey'); }); }); + describe('item set function tests', () => { + it('should call select function with passed Item identifier', () => { + selector.select = jest.fn(); + selector._item = null as any; + + dummyItem1._key = 'AReallyCoolKey'; + + selector.item = dummyItem1; + + expect(selector.select).toHaveBeenCalledWith('AReallyCoolKey'); + expect(selector._item).toBeNull(); + }); + }); + + describe('item get function tests', () => { + it('should return currently selected Item of Selector', () => { + selector._item = dummyItem1; + + expect(selector.item).toBe(dummyItem1); + }); + }); + describe('select function tests', () => { let dummyItem2: Item; @@ -185,7 +209,7 @@ describe('Selector Tests', () => { ); expect(selector._itemKey).toBe('dummyItem2'); - expect(selector.item).toBe(dummyItem2); + expect(selector._item).toBe(dummyItem2); expect(selector.unselect).toHaveBeenCalledWith({ background: true }); expect(selector.rebuildSelector).toHaveBeenCalledWith({ background: false, @@ -233,7 +257,7 @@ describe('Selector Tests', () => { ); expect(selector._itemKey).toBe('dummyItem2'); - expect(selector.item).toBe(dummyItem2); + expect(selector._item).toBe(dummyItem2); expect(selector.unselect).toHaveBeenCalledWith({ background: true }); expect(selector.rebuildSelector).toHaveBeenCalledWith({ background: true, @@ -270,7 +294,7 @@ describe('Selector Tests', () => { expect(dummyCollection.getItemWithReference).not.toHaveBeenCalled(); expect(selector._itemKey).toBe('dummyItem1'); - expect(selector.item).toBe(dummyItem1); + expect(selector._item).toBe(dummyItem1); expect(selector.unselect).not.toHaveBeenCalled(); expect(selector.rebuildSelector).not.toHaveBeenCalled(); expect(selector.addSideEffect).not.toHaveBeenCalled(); @@ -292,7 +316,7 @@ describe('Selector Tests', () => { ); expect(selector._itemKey).toBe('dummyItem1'); - expect(selector.item).toBe(dummyItem1); + expect(selector._item).toBe(dummyItem1); expect(selector.unselect).toHaveBeenCalledWith({ background: true }); expect(selector.rebuildSelector).toHaveBeenCalledWith({ background: false, @@ -331,7 +355,7 @@ describe('Selector Tests', () => { expect(dummyCollection.getItemWithReference).not.toHaveBeenCalled(); expect(selector._itemKey).toBe('dummyItem1'); - expect(selector.item).toBe(dummyItem1); + expect(selector._item).toBe(dummyItem1); expect(selector.unselect).not.toHaveBeenCalled(); expect(selector.rebuildSelector).not.toHaveBeenCalled(); expect(selector.addSideEffect).not.toHaveBeenCalled(); @@ -353,7 +377,7 @@ describe('Selector Tests', () => { ); expect(selector._itemKey).toBe('dummyItem2'); - expect(selector.item).toBe(dummyItem2); + expect(selector._item).toBe(dummyItem2); expect(selector.unselect).toHaveBeenCalledWith({ background: true }); expect(selector.rebuildSelector).toHaveBeenCalledWith({ background: false, @@ -394,7 +418,7 @@ describe('Selector Tests', () => { 'dummyItem2' ); expect(selector._itemKey).toBe('dummyItem2'); - expect(selector.item).toBe(dummyItem2); + expect(selector._item).toBe(dummyItem2); expect(selector.unselect).toHaveBeenCalledWith({ background: true }); expect(selector.rebuildSelector).toHaveBeenCalledWith({ background: false, @@ -435,7 +459,7 @@ describe('Selector Tests', () => { 'dummyItem2' ); expect(selector._itemKey).toBe('dummyItem2'); - expect(selector.item).toBe(dummyItem2); + expect(selector._item).toBe(dummyItem2); expect(selector.unselect).toHaveBeenCalledWith({ background: true }); expect(selector.rebuildSelector).toHaveBeenCalledWith({ background: false, @@ -589,7 +613,7 @@ describe('Selector Tests', () => { Selector.rebuildSelectorSideEffectKey ); - expect(selector.item).toBeUndefined(); + expect(selector._item).toBeUndefined(); expect(selector._itemKey).toBe(Selector.unknownItemPlaceholderKey); expect(selector.isPlaceholder).toBeTruthy(); expect(selector.rebuildSelector).toHaveBeenCalledWith({}); @@ -609,7 +633,7 @@ describe('Selector Tests', () => { Selector.rebuildSelectorSideEffectKey ); - expect(selector.item).toBeUndefined(); + expect(selector._item).toBeUndefined(); expect(selector._itemKey).toBe(Selector.unknownItemPlaceholderKey); expect(selector.isPlaceholder).toBeTruthy(); expect(selector.rebuildSelector).toHaveBeenCalledWith({ @@ -631,7 +655,7 @@ describe('Selector Tests', () => { Selector.rebuildSelectorSideEffectKey ); - expect(selector.item).toBeUndefined(); + expect(selector._item).toBeUndefined(); expect(selector._itemKey).toBe(Selector.unknownItemPlaceholderKey); expect(selector.isPlaceholder).toBeTruthy(); expect(selector.rebuildSelector).toHaveBeenCalledWith({}); @@ -646,7 +670,7 @@ describe('Selector Tests', () => { }); it('should return true if Selector has selected itemKey correctly and Item isSelected', () => { - if (selector.item) selector.item.selectedBy.add(selector._key as any); + if (selector._item) selector._item.selectedBy.add(selector._key as any); expect(selector.hasSelected('dummyItemKey')).toBeTruthy(); }); @@ -660,25 +684,25 @@ describe('Selector Tests', () => { }); it("should return false if Selector hasn't selected itemKey correctly (item = undefined)", () => { - selector.item = undefined; + selector._item = undefined; expect(selector.hasSelected('dummyItemKey')).toBeFalsy(); }); it("should return true if Selector hasn't selected itemKey correctly (item = undefined, correctlySelected = false)", () => { - selector.item = undefined; + selector._item = undefined; expect(selector.hasSelected('dummyItemKey', false)).toBeTruthy(); }); it("should return false if Selector has selected itemKey correctly and Item isn't isSelected", () => { - if (selector.item) selector.item.selectedBy = new Set(); + if (selector._item) selector._item.selectedBy = new Set(); expect(selector.hasSelected('dummyItemKey')).toBeFalsy(); }); it("should return true if Selector has selected itemKey correctly and Item isn't isSelected (correctlySelected = false)", () => { - if (selector.item) selector.item.selectedBy = new Set(); + if (selector._item) selector._item.selectedBy = new Set(); expect(selector.hasSelected('dummyItemKey', false)).toBeTruthy(); }); @@ -690,15 +714,15 @@ describe('Selector Tests', () => { }); it('should set selector value to item value (default config)', () => { - selector.item = dummyItem1; + selector._item = dummyItem1; selector.rebuildSelector(); - expect(selector.set).toHaveBeenCalledWith(selector.item._value, {}); + expect(selector.set).toHaveBeenCalledWith(selector._item._value, {}); }); it('should set selector value to item value (specific config)', () => { - selector.item = dummyItem1; + selector._item = dummyItem1; selector.rebuildSelector({ sideEffects: { @@ -708,7 +732,7 @@ describe('Selector Tests', () => { force: true, }); - expect(selector.set).toHaveBeenCalledWith(selector.item._value, { + expect(selector.set).toHaveBeenCalledWith(selector._item._value, { sideEffects: { enabled: false, }, @@ -718,7 +742,7 @@ describe('Selector Tests', () => { }); it('should set selector value to undefined if Item is undefined (default config)', () => { - selector.item = undefined; + selector._item = undefined; selector.rebuildSelector(); From 8c14b1212b8ba9567f467e2ce3ab1fd03c292882 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 4 Jun 2021 14:39:59 +0200 Subject: [PATCH 23/63] fixed typos --- packages/core/src/collection/group.ts | 33 +++++++++---------- packages/core/src/collection/index.ts | 7 ++-- packages/core/src/collection/item.ts | 14 ++++---- packages/core/src/collection/selector.ts | 21 +++++++----- .../tests/unit/collection/selector.test.ts | 8 ++--- 5 files changed, 42 insertions(+), 41 deletions(-) diff --git a/packages/core/src/collection/group.ts b/packages/core/src/collection/group.ts index 840fb6c8..adfe93f6 100644 --- a/packages/core/src/collection/group.ts +++ b/packages/core/src/collection/group.ts @@ -12,7 +12,6 @@ import { isValidObject, PersistentKey, ComputedTracker, - StateRuntimeJobConfigInterface, StateIngestConfigInterface, removeProperties, LogCodeManager, @@ -25,9 +24,9 @@ export class Group extends State< static rebuildGroupSideEffectKey = 'rebuildGroup'; - _output: Array = []; // Output of Group - _items: Array<() => Item> = []; // Items of Group - notFoundItemKeys: Array = []; // Contains all itemKeys that couldn't be found in the Collection + _output: Array = []; // Item values represented by the Group + _items: Array<() => Item> = []; // Items represented by the Group + notFoundItemKeys: Array = []; // Contains all Item identifiers for Items that couldn't be found in the Collection /** * An extension of the State Class that categorizes and preserves the ordering of structured data. @@ -51,8 +50,8 @@ export class Group extends State< super(collection.agileInstance(), initialItems || [], config); this.collection = () => collection; - // Add the 'Rebuild Group' action to the Group side effects - // in order to rebuild the Group whenever its value changes + // Add side effect to Group + // that rebuilds the Group whenever the Group value changes this.addSideEffect(Group.rebuildGroupSideEffectKey, () => this.rebuild()); // Initial rebuild @@ -60,7 +59,7 @@ export class Group extends State< } /** - * Retrieves values of the Items clustered by the Group. + * Retrieves the values of the Items clustered by the Group. * * [Learn more..](https://agile-ts.org/docs/core/collection/group/properties#output) * @@ -76,7 +75,7 @@ export class Group extends State< } /** - * Retrieves Items clustered by the Group. + * Retrieves the Items clustered by the Group. * * [Learn more..](https://agile-ts.org/docs/core/collection/group/properties#items) * @@ -105,7 +104,7 @@ export class Group extends State< } /** - * Returns the count of the Items clustered in the Group. + * Returns the count of Items clustered by the Group. * * [Learn more..](https://agile-ts.org/docs/core/collection/group/properties#size) * @@ -192,7 +191,8 @@ export class Group extends State< if (!this.collection().getItem(itemKey)) notExistingItemKeysInCollection.push(itemKey); - // Remove itemKey from Group if it should be overwritten and already exists + // Remove itemKey from Group + // if it should be overwritten and already exists if (this.has(itemKey)) { if (config.overwrite) { newGroupValue = newGroupValue.filter((key) => key !== itemKey); @@ -229,7 +229,7 @@ export class Group extends State< * * @public * @param oldItemKey - Old `itemKey` to be replaced. - * @param newItemKey - New `itemKey` which replaces the specified old `itemKey`. + * @param newItemKey - New `itemKey` to replace the before specified old `itemKey`. * @param config - Configuration object */ public replace( @@ -315,7 +315,7 @@ export class Group extends State< * Rebuilds the entire `output` and `items` property of the Group. * * In doing so, it traverses the Group `value` (Item identifiers) - * and fetches the Items that belong to an Item identifier. + * and fetches the fitting Items accordingly. * * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#rebuild) * @@ -337,7 +337,7 @@ export class Group extends State< else notFoundItemKeys.push(itemKey); }); - // Get Item values of fetched Items + // Extract Item values from the retrieved Items const groupOutput = groupItems.map((item) => { return item.getPublicValue(); }); @@ -366,21 +366,18 @@ export interface GroupAddConfigInterface extends StateIngestConfigInterface { * In which way the `itemKey` should be added to the Group. * - 'push' = at the end * - 'unshift' = at the beginning + * https://www.tutorialspoint.com/what-are-the-differences-between-unshift-and-push-methods-in-javascript * @default 'push' */ method?: 'unshift' | 'push'; /** * If the to add `itemKey` already exists, - * whether to overwrite its position with the position of the new `itemKey`. + * whether its position should be overwritten with the position of the new `itemKey`. * @default false */ overwrite?: boolean; } -/** - * @param key - Key/Name of Group - * @param isPlaceholder - If Group is initially a Placeholder - */ export interface GroupConfigInterface { /** * Key/Name identifier of Group. diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index a60d959f..67b74ecc 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -90,7 +90,7 @@ export class Collection { } /** - * Updates key/name identifier of Collection. + * Updates the key/name identifier of Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key) * @@ -102,7 +102,7 @@ export class Collection { } /** - * Returns key/name identifier of Collection. + * Returns the key/name identifier of Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key) * @@ -113,7 +113,7 @@ export class Collection { } /** - * Updates key/name identifier of Collection. + * Updates the key/name identifier of Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#setkey) * @@ -1539,6 +1539,7 @@ export interface CollectConfigInterface * In which way the collected data should be added to the Collection. * - 'push' = at the end * - 'unshift' = at the beginning + * https://www.tutorialspoint.com/what-are-the-differences-between-unshift-and-push-methods-in-javascript * @default 'push' */ method?: 'push' | 'unshift'; diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index eab17158..d78b4626 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -41,15 +41,15 @@ export class Item extends State< }); this.collection = () => collection; - // Add 'rebuildGroupsThatIncludeItemKey' side effect - // in order to rebuild all Groups that include this Item whenever it mutates + // Add side effect to Item + // that rebuilds all Groups containing the Item whenever it changes if (this._key != null) { this.addRebuildGroupThatIncludeItemKeySideEffect(this._key); } } /** - * Updates key/name identifier of Item. + * Updates the key/name identifier of Item. * * @internal * @param value - New key/name identifier. @@ -152,12 +152,12 @@ export class Item extends State< } /** - * Adds the 'Rebuild Group that include Item Key' action to the Item side effects, - * so that the Groups which include the Item with the identifier `itemKey` - * are rebuilt when the Item changes. + * Adds side effect to Item + * that rebuilds all Groups containing the specified Item identifier + * whenever the Item changes. * * @internal - * @param itemKey - Item identifier that has to be included in Groups so that these Groups are rebuilt. + * @param itemKey - Item identifier that has to be contained in Groups. */ public addRebuildGroupThatIncludeItemKeySideEffect(itemKey: StateKey) { this.addSideEffect>( diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index a0b6ee1b..203cc1f8 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -67,7 +67,8 @@ export class Selector extends State< } /** - * Updates the currently selected Item of the Selector. + * Updates the currently selected Item of the Selector + * based on the specified `itemKey`. * * [Learn more..](https://agile-ts.org/docs/core/collection/selector/properties#itemkey) * @@ -90,7 +91,8 @@ export class Selector extends State< } /** - * Updates the currently selected Item of the Selector. + * Updates the currently selected Item of the Selector + * based on the specified Item. * * [Learn more..](https://agile-ts.org/docs/core/collection/selector/properties#item) * @@ -146,7 +148,7 @@ export class Selector extends State< newItem.selectedBy.add(this._key as any); // Add side effect to the newly selected Item - // that rebuilds the Selector depending on the current Item value + // that rebuilds the Selector value depending on the current Item value newItem.addSideEffect( Selector.rebuildSelectorSideEffectKey, (instance, config) => this.rebuildSelector(config), @@ -172,7 +174,7 @@ export class Selector extends State< { weight: 90 } ); - // Rebuild Selector for 'instantiating' the newly selected Item properly + // Rebuild the Selector to properly 'instantiate' the newly selected Item this.rebuildSelector(config); return this; @@ -181,10 +183,11 @@ export class Selector extends State< /** * Reselects the currently selected Item. * - * This might be helpful if the Selector failed to select the Item correctly + * This might be helpful if the Selector failed to select the Item correctly before * and therefore should try to select it again. * - * You can use the 'hasSelected()' method to check whether the Item is selected correctly. + * You can use the 'hasSelected()' method to check + * whether the 'selected' Item is selected correctly. * * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#reselect) * @@ -212,7 +215,7 @@ export class Selector extends State< * @param config - Configuration object */ public unselect(config: StateRuntimeJobConfigInterface = {}): this { - // Retrieve Item from Collection because 'this._item' might be outdated + // Retrieve Item from the Collection because 'this._item' might be outdated const item = this.collection().getItem(this._itemKey, { notExisting: true, }); @@ -256,8 +259,8 @@ export class Selector extends State< } /** - * Rebuilds the Selector, - * which updates the Selector value based on the Item value. + * Rebuilds the Selector. + * During this process, it updates the Selector `value` based on the Item `value`. * * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#rebuild) * diff --git a/packages/core/tests/unit/collection/selector.test.ts b/packages/core/tests/unit/collection/selector.test.ts index 52185ec5..cb4b2f87 100644 --- a/packages/core/tests/unit/collection/selector.test.ts +++ b/packages/core/tests/unit/collection/selector.test.ts @@ -141,7 +141,7 @@ describe('Selector Tests', () => { }); describe('itemKey set function tests', () => { - it('should call select function with passed value', () => { + it('should call the select() method with the passed value', () => { selector.select = jest.fn(); selector._itemKey = null as any; @@ -153,7 +153,7 @@ describe('Selector Tests', () => { }); describe('itemKey get function tests', () => { - it('should return currently selected ItemKey of Selector', () => { + it('should return the identifier of the Item currently selected by the Selector', () => { selector._itemKey = 'coolItemKey'; expect(selector.itemKey).toBe('coolItemKey'); @@ -161,7 +161,7 @@ describe('Selector Tests', () => { }); describe('item set function tests', () => { - it('should call select function with passed Item identifier', () => { + it('should call the select() method with the Item identifier of the specified Item', () => { selector.select = jest.fn(); selector._item = null as any; @@ -175,7 +175,7 @@ describe('Selector Tests', () => { }); describe('item get function tests', () => { - it('should return currently selected Item of Selector', () => { + it('should return the currently selected Item of the Selector', () => { selector._item = dummyItem1; expect(selector.item).toBe(dummyItem1); From 9313d8c9a313776bfd35ca801f6446be9c2048b0 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 4 Jun 2021 15:09:27 +0200 Subject: [PATCH 24/63] fixed Collection persistent storage key issue --- .../core/src/collection/collection.persistent.ts | 6 ++++++ packages/core/src/collection/group.ts | 6 +++--- .../collection/collection.persistent.test.ts | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 9b73c336..b3831b6c 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -132,6 +132,7 @@ export class CollectionPersistent< loadValue: false, defaultStorageKey: this.config.defaultStorageKey || undefined, storageKeys: this.storageKeys, + followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above }); if (defaultGroup.persistent?.ready) await defaultGroup.persistent.initialLoading(); @@ -149,6 +150,7 @@ export class CollectionPersistent< item.persist(itemStorageKey, { defaultStorageKey: this.config.defaultStorageKey || undefined, storageKeys: this.storageKeys, + followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above }); } // Persist and therefore load not present Item @@ -162,6 +164,7 @@ export class CollectionPersistent< loadValue: false, defaultStorageKey: this.config.defaultStorageKey || undefined, storageKeys: this.storageKeys, + followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above }); if (dummyItem?.persistent?.ready) { const loadedPersistedValueIntoItem = await dummyItem.persistent.loadPersistedValue( @@ -210,6 +213,7 @@ export class CollectionPersistent< defaultGroup.persist(defaultGroupStorageKey, { defaultStorageKey: this.config.defaultStorageKey || undefined, storageKeys: this.storageKeys, + followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above }); // Persist Items found in the default Group's value @@ -222,6 +226,7 @@ export class CollectionPersistent< item?.persist(itemStorageKey, { defaultStorageKey: this.config.defaultStorageKey || undefined, storageKeys: this.storageKeys, + followCollectionPersistKeyPattern: false,// Because of the dynamic 'storageItemKey', the key is already formatted above }); } @@ -347,6 +352,7 @@ export class CollectionPersistent< item.persist(itemStorageKey, { defaultStorageKey: this.config.defaultStorageKey || undefined, storageKeys: this.storageKeys, + followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above }); }); diff --git a/packages/core/src/collection/group.ts b/packages/core/src/collection/group.ts index adfe93f6..716cbb08 100644 --- a/packages/core/src/collection/group.ts +++ b/packages/core/src/collection/group.ts @@ -191,9 +191,9 @@ export class Group extends State< if (!this.collection().getItem(itemKey)) notExistingItemKeysInCollection.push(itemKey); - // Remove itemKey from Group - // if it should be overwritten and already exists - if (this.has(itemKey)) { + // Remove itemKey temporary from newGroupValue + // if it should be overwritten and already exists in the newGroupValue + if (newGroupValue.includes(itemKey)) { if (config.overwrite) { newGroupValue = newGroupValue.filter((key) => key !== itemKey); } else { diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index 7c10829d..0a1044c5 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -364,6 +364,7 @@ describe('CollectionPersistent Tests', () => { loadValue: false, defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); expect(dummyDefaultGroup.persistent?.initialLoading).toHaveBeenCalled(); @@ -377,6 +378,7 @@ describe('CollectionPersistent Tests', () => { { defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); @@ -438,6 +440,7 @@ describe('CollectionPersistent Tests', () => { loadValue: false, defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); expect(dummyDefaultGroup.persistent?.initialLoading).toHaveBeenCalled(); @@ -455,6 +458,7 @@ describe('CollectionPersistent Tests', () => { loadValue: false, defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); expect(placeholderItem2.persist).toHaveBeenCalledWith( @@ -466,6 +470,7 @@ describe('CollectionPersistent Tests', () => { loadValue: false, defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); expect(placeholderItem3.persist).toHaveBeenCalledWith( @@ -477,6 +482,7 @@ describe('CollectionPersistent Tests', () => { loadValue: false, defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); expect(dummyCollection.assignItem).toHaveBeenCalledWith( @@ -539,6 +545,7 @@ describe('CollectionPersistent Tests', () => { loadValue: false, defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); expect( @@ -551,6 +558,7 @@ describe('CollectionPersistent Tests', () => { { defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); @@ -566,6 +574,7 @@ describe('CollectionPersistent Tests', () => { loadValue: false, defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); expect(dummyCollection.assignItem).toHaveBeenCalledWith( @@ -708,6 +717,7 @@ describe('CollectionPersistent Tests', () => { { defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); @@ -719,6 +729,7 @@ describe('CollectionPersistent Tests', () => { { defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); expect(dummyItem3.persist).toHaveBeenCalledWith( @@ -729,6 +740,7 @@ describe('CollectionPersistent Tests', () => { { defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); @@ -757,6 +769,7 @@ describe('CollectionPersistent Tests', () => { { defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); @@ -765,6 +778,7 @@ describe('CollectionPersistent Tests', () => { { defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); expect(dummyItem3.persist).toHaveBeenCalledWith( @@ -772,6 +786,7 @@ describe('CollectionPersistent Tests', () => { { defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); @@ -1180,6 +1195,7 @@ describe('CollectionPersistent Tests', () => { { defaultStorageKey: collectionPersistent.config.defaultStorageKey, storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, } ); From a6de58f4be9252d4f8d405ee9bb5f9e9fda1908d Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 5 Jun 2021 20:09:35 +0200 Subject: [PATCH 25/63] fixed typos --- packages/core/src/collection/group.ts | 4 +- packages/core/src/collection/index.ts | 24 +- packages/core/src/collection/selector.ts | 2 +- packages/core/src/computed/index.ts | 6 +- packages/core/src/state/index.ts | 381 ++++++++++--------- packages/core/tests/unit/state/state.test.ts | 43 ++- packages/utils/src/index.ts | 6 +- 7 files changed, 256 insertions(+), 210 deletions(-) diff --git a/packages/core/src/collection/group.ts b/packages/core/src/collection/group.ts index 716cbb08..8452d83b 100644 --- a/packages/core/src/collection/group.ts +++ b/packages/core/src/collection/group.ts @@ -59,7 +59,7 @@ export class Group extends State< } /** - * Retrieves the values of the Items clustered by the Group. + * Returns the values of the Items clustered by the Group. * * [Learn more..](https://agile-ts.org/docs/core/collection/group/properties#output) * @@ -75,7 +75,7 @@ export class Group extends State< } /** - * Retrieves the Items clustered by the Group. + * Returns the Items clustered by the Group. * * [Learn more..](https://agile-ts.org/docs/core/collection/group/properties#items) * diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 67b74ecc..45ab7394 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -29,16 +29,16 @@ export class Collection { public config: CollectionConfigInterface; private initialConfig: CreateCollectionConfigInterface; - public size = 0; // Amount of the Items stored in the Collection - public data: { [key: string]: Item } = {}; // Collection Data public _key?: CollectionKey; - public isPersisted = false; // Whether Collection is persisted in any external Storage - public persistent: CollectionPersistent | undefined; // Manages persisting Collection 'value' + public size = 0; // Amount of the Items stored in the Collection + public data: { [key: string]: Item } = {}; // Items stored in the Collection + public isPersisted = false; // Whether the Collection is persisted in an external Storage + public persistent: CollectionPersistent | undefined; // Manages persisting the Collection 'value' - public groups: { [key: string]: Group } = {}; - public selectors: { [key: string]: Selector } = {}; + public groups: { [key: string]: Group } = {}; // Groups of Collection + public selectors: { [key: string]: Selector } = {}; // Selectors of Collection - public isInstantiated = false; // Whether the Collection is instantiated completely + public isInstantiated = false; // Whether the Collection was instantiated correctly /** * A Collection manages a reactive set of Information @@ -49,6 +49,8 @@ export class Collection { * * Each of these data object must have a unique `primaryKey` to be correctly identified later. * + * You can create as many global Collections as you need. + * * [Learn more..](https://agile-ts.org/docs/core/collection/) * * @public @@ -90,7 +92,7 @@ export class Collection { } /** - * Updates the key/name identifier of Collection. + * Updates the key/name identifier of the Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key) * @@ -102,7 +104,7 @@ export class Collection { } /** - * Returns the key/name identifier of Collection. + * Returns the key/name identifier of the Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key) * @@ -113,7 +115,7 @@ export class Collection { } /** - * Updates the key/name identifier of Collection. + * Updates the key/name identifier of the Collection. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#setkey) * @@ -361,7 +363,7 @@ export class Collection { background: false, }); - // Validate passed data + // Check if the given conditions are suitable for a update action if (item == null) { LogCodeManager.log('1B:03:00', [itemKey, this._key]); return undefined; diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index 203cc1f8..b6afe661 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -56,7 +56,7 @@ export class Selector extends State< } /** - * Retrieves the `itemKey` currently selected by the Selector. + * Returns the `itemKey` currently selected by the Selector. * * [Learn more..](https://agile-ts.org/docs/core/collection/selector/properties#itemkey) * diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index b5648374..2bb0852d 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -17,9 +17,9 @@ export class Computed extends State< > { public agileInstance: () => Agile; - public computeFunction: () => ComputedValueType; // Function to compute the computed value - public deps: Array = []; // All dependencies the Computed depends on (including hardCoded and autoDetected dependencies) - public hardCodedDeps: Array = []; // Only hardCoded dependencies the Computed depends + public computeFunction: () => ComputedValueType; // Function to compute the Computed Class value + public deps: Array = []; // All dependencies the Computed Class depends on (including hardCoded and automatically detected dependencies) + public hardCodedDeps: Array = []; // Only hardCoded dependencies the Computed Class depends /** * An extension of the State Class that computes its value based on a compute function. diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 745364b6..282b9a4f 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -23,39 +23,42 @@ export class State { public agileInstance: () => Agile; public _key?: StateKey; - public valueType?: string; // primitive Type of State Value (for JS users) - public isSet = false; // If value is not the same as initialValue + public valueType?: string; // Primitive type which constrains the State value (for basic typesafety in Javascript) + public isSet = false; // Whether the current value differs from the initial value public isPlaceholder = false; - public initialStateValue: ValueType; - public _value: ValueType; // Current Value of State - public previousStateValue: ValueType; - public nextStateValue: ValueType; // Represents the next Value of the State (mostly used internal) - public observer: StateObserver; // Handles deps and subs of State and is like an interface to the Runtime + public initialStateValue: ValueType; // First value assigned to the State + public _value: ValueType; // Current value of the State + public previousStateValue: ValueType; // Previous value of the State + public nextStateValue: ValueType; // Next value of the State (which can be used for dynamic State updates) + + // Handles dependencies to other States and subscriptions of UI-Components + // and serves as an interface to the runtime + public observer: StateObserver; public sideEffects: { [key: string]: SideEffectInterface>; - } = {}; // SideEffects of State (will be executed in Runtime) - public computeValueMethod?: ComputeValueMethod; - public computeExistsMethod: ComputeExistsMethod; + } = {}; // Side effects of changing the State value + public computeValueMethod?: ComputeValueMethod; // Method for dynamically computing the State value + public computeExistsMethod: ComputeExistsMethod; // Method for dynamically computing the existence of the State - public isPersisted = false; // If State can be stored in Agile Storage (-> successfully integrated persistent) - public persistent: StatePersistent | undefined; // Manages storing State Value into Storage + public isPersisted = false; // Whether the State is persisted in an external Storage + public persistent: StatePersistent | undefined; // Manages persisting the State value - public watchers: { [key: string]: StateWatcherCallback } = {}; + public watchers: { [key: string]: StateWatcherCallback } = {}; // Callbacks that are fired on State changes - public currentInterval?: NodeJS.Timer | number; + public currentInterval?: NodeJS.Timer | number; // The current active interval /** - * @public - * State - Class that holds one Value and causes rerender on subscribed Components - * - * @param agileInstance - An instance of Agile + * A State manages a piece of Information + * that we need to remember globally at a later point int time. + * While providing a toolkit to use and mutate this set of Information. * - * @param initialValue - Initial Value of State + * You can create as many global States as you need. * - * @param config - Configuration - * - * @typeparam ValueType - Type of a the value the State represents + * @public + * @param agileInstance - Instance of Agile the State belongs to. + * @param initialValue - Initial value of State. + * @param config - Configuration object */ constructor( agileInstance: Agile, @@ -81,21 +84,28 @@ export class State { return v != null; }; - // Initial Set + // Set State value to specified initial value if (!config.isPlaceholder) this.set(initialValue, { overwrite: true }); } /** + * Updates the value of the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/properties#value) + * * @public - * Set Value of State + * @param value - New State value. */ public set value(value: ValueType) { this.set(value); } /** + * Returns a reference-free version of the State value. + * + * [Learn more..](https://agile-ts.org/docs/core/state/properties#value) + * * @public - * Get Value of State */ public get value(): ValueType { ComputedTracker.tracked(this.observer); @@ -103,7 +113,9 @@ export class State { } /** - * Updates key/name identifier of State. + * Updates the key/name identifier of the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/properties#key) * * @public * @param value - New key/name identifier. @@ -113,7 +125,9 @@ export class State { } /** - * Returns key/name identifier of State. + * Returns the key/name identifier of the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/properties#key) * * @public */ @@ -122,7 +136,9 @@ export class State { } /** - * Updates key/name identifier of State. + * Updates the key/name identifier of the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#setkey) * * @public * @param value - New key/name identifier. @@ -133,30 +149,26 @@ export class State { // Update State key this._key = value; - // Update key in Observer + // Update key of Observer this.observer._key = value; // Update key in Persistent (only if oldKey equal to persistentKey // because otherwise the persistentKey is detached from the State key // -> not managed by State anymore) - if ( - value != null && - this.persistent != null && - this.persistent._key === oldKey - ) - this.persistent.setKey(value); + if (value && this.persistent?._key === oldKey) + this.persistent?.setKey(value); return this; } - //========================================================================================================= - // Set - //========================================================================================================= /** + * Assigns a new value to the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#set) + * * @public - * Updates Value of State - * @param value - new State Value - * @param config - Config + * @param value - New State value + * @param config - Configuration object */ public set( value: ValueType | ((value: ValueType) => ValueType), @@ -169,7 +181,7 @@ export class State { ? (value as any)(copy(this._value)) : value; - // Check value has correct Type (js) + // Check value has correct type (Javascript) if (!this.hasCorrectType(_value)) { LogCodeManager.log(config.force ? '14:02:00' : '14:03:00', [ typeof _value, @@ -178,38 +190,42 @@ export class State { if (!config.force) return this; } - // Ingest new value into Runtime + // Ingest the State with the new value into the runtime this.observer.ingestValue(_value, config); return this; } - //========================================================================================================= - // Ingest - //========================================================================================================= /** + * Ingests the State without any specified new value into the runtime. + * + * Since no new value was defined either the State value is computed (Computed Class) + * or the `nextStateValue` is taken. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#ingest) + * * @internal - * Ingests nextStateValue, computedValue into Runtime - * @param config - Config + * @param config - Configuration object */ public ingest(config: StateIngestConfigInterface = {}): this { this.observer.ingest(config); return this; } - //========================================================================================================= - // Type - //========================================================================================================= /** + * Assigns a primitive type to the State + * which constrains the State value on the specified type + * to ensure basic typesafety in Javascript. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#type) + * * @public - * Assign primitive type to State Value - * Note: This function is mainly thought for JS users - * @param type - wished Type ('String', 'Boolean', 'Array', 'Object', 'Number') + * @param type - Primitive type the State value must follow (`String`, `Boolean`, `Array`, `Object`, `Number`). */ public type(type: any): this { const supportedTypes = ['String', 'Boolean', 'Array', 'Object', 'Number']; - // Check if type is a supported Type + // Check if type is a supported type if (!supportedTypes.includes(type.name)) { LogCodeManager.log('14:03:01', [type]); return this; @@ -219,41 +235,45 @@ export class State { return this; } - //========================================================================================================= - // Undo - //========================================================================================================= /** + * Undoes the latest State value change. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#undo) + * * @public - * Undoes latest State Value change - * @param config - Config + * @param config - Configuration object */ public undo(config: StateIngestConfigInterface = {}): this { this.set(this.previousStateValue, config); return this; } - //========================================================================================================= - // Reset - //========================================================================================================= /** + * Resets the State value to its initial value. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#reset) + * * @public - * Resets State to its initial Value - * @param config - Config + * @param config - Configuration object */ public reset(config: StateIngestConfigInterface = {}): this { this.set(this.initialStateValue, config); return this; } - //========================================================================================================= - // Patch - //========================================================================================================= /** + * Merges the specified `targetWithChanges` object into the current State value. + * This merge can be different for different value combinations: + * - If the current State value is an `object`, it does partial update for the object. + * - If the current State value is an `array` an the argument is an array too, + * it concatenates the current value with the value of the argument. + * - If the current State value is not a `object` or an `array` it prints an warning. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#patch) + * * @public - * Patches Object with changes into State Value - * Note: Only useful if State is an Object - * @param targetWithChanges - Object that holds changes which get patched into State Value - * @param config - Config + * @param targetWithChanges - Object to be merged into the current State value. + * @param config - Configuration object */ public patch( targetWithChanges: Object, @@ -263,44 +283,58 @@ export class State { addNewProperties: true, }); + // Check if the given conditions are suitable for a patch action if (!isValidObject(this.nextStateValue, true)) { LogCodeManager.log('14:03:02'); return this; } - if (!isValidObject(targetWithChanges, true)) { LogCodeManager.log('00:03:01', ['TargetWithChanges', 'object']); return this; } - // Merge targetWithChanges into nextStateValue - this.nextStateValue = flatMerge( - copy(this.nextStateValue), - targetWithChanges, - { addNewProperties: config.addNewProperties } - ); + // Merge targetWithChanges object into the nextStateValue + if ( + Array.isArray(targetWithChanges) && + Array.isArray(this.nextStateValue) + ) { + this.nextStateValue = [ + ...this.nextStateValue, + ...targetWithChanges, + ] as any; + } else { + this.nextStateValue = flatMerge( + this.nextStateValue, + targetWithChanges, + { addNewProperties: config.addNewProperties } + ); + } - // Ingest updated nextStateValue into Runtime + // Ingest updated 'nextStateValue' into runtime this.ingest(removeProperties(config, ['addNewProperties'])); return this; } - //========================================================================================================= - // Watch - //========================================================================================================= /** + * Fires on each State value change. + * + * Returns the key/name identifier of the created watcher callback. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#watch) + * * @public - * Watches State and detects State changes - * @param callback - Callback Function that gets called if the State Value changes - * @return Key of Watcher + * @param callback - Callback function */ public watch(callback: StateWatcherCallback): string; /** + * Fires on each State value change. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#watch) + * * @public - * Watches State and detects State changes - * @param key - Key/Name of Watcher Function - * @param callback - Callback Function that gets called if the State Value changes + * @param key - Key/Name identifier of the watcher callback. + * @param callback - Callback function */ public watch(key: string, callback: StateWatcherCallback): this; public watch( @@ -319,13 +353,13 @@ export class State { _callback = callback as StateWatcherCallback; } - // Check if Callback is valid Function + // Check if specified callback is a valid function if (!isFunction(_callback)) { LogCodeManager.log('00:03:01', ['Watcher Callback', 'function']); return this; } - // Check if watcherKey is already occupied + // Check if watcher with key is already occupied if (this.watchers[key]) { LogCodeManager.log('14:03:03', [key]); return this; @@ -335,13 +369,13 @@ export class State { return generateKey ? key : this; } - //========================================================================================================= - // Remove Watcher - //========================================================================================================= /** + * Removes a watcher callback with the specified key/name identifier from the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#removewatcher) + * * @public - * Removes Watcher at given Key - * @param key - Key of Watcher that gets removed + * @param key - Key/Name identifier of the watcher callback to be removed. */ public removeWatcher(key: string): this { delete this.watchers[key]; @@ -349,9 +383,25 @@ export class State { } /** + * Returns a boolean indicating whether a watcher callback with the specified `key` + * exists in the State or not. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#haswatcher) + * * @public - * Creates a Watcher that gets once called when the State Value changes for the first time and than destroys itself - * @param callback - Callback Function that gets called if the State Value changes + * @param key - Key/Name identifier of the watcher callback. + */ + public hasWatcher(key: string): boolean { + return !!this.watchers[key]; + } + + /** + * Fires on the first State value assignment and then destroys itself. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#oninaugurated) + * + * @public + * @param callback - Callback function */ public onInaugurated(callback: StateWatcherCallback): this { const watcherKey = 'InauguratedWatcherKey'; @@ -362,18 +412,6 @@ export class State { return this; } - //========================================================================================================= - // Has Watcher - //========================================================================================================= - /** - * @public - * Checks if watcher at given Key exists - * @param key - Key/Name of Watcher - */ - public hasWatcher(key: string): boolean { - return !!this.watchers[key]; - } - /** * Preserves the State `value` in the corresponding external Storage. * @@ -466,20 +504,21 @@ export class State { return this; } - //========================================================================================================= - // Interval - //========================================================================================================= /** + * Repeatedly calls a function or executes a code snippet, with a fixed time delay between each call. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#interval) + * * @public - * Calls callback at certain intervals in milliseconds and assigns the callback return value to the State - * @param callback- Callback that is called on each interval and should return the new State value - * @param ms - The intervals in milliseconds + * @param handler - A function to be executed every delay milliseconds. + * @param delay - The time, in milliseconds (thousandths of a second), + * the timer should delay in between executions of the specified function. */ public interval( - callback: (value: ValueType) => ValueType, - ms?: number + handler: (value: ValueType) => ValueType, + delay?: number ): this { - if (!isFunction(callback)) { + if (!isFunction(handler)) { LogCodeManager.log('00:03:01', ['Interval Callback', 'function']); return this; } @@ -489,18 +528,19 @@ export class State { } this.currentInterval = setInterval(() => { - this.set(callback(this._value)); - }, ms ?? 1000); + this.set(handler(this._value)); + }, delay ?? 1000); return this; } - //========================================================================================================= - // Clear Interval - //========================================================================================================= /** + * Cancels a active timed, repeating action + * which was previously established by a call to `interval()`. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#clearinterval) + * * @public - * Clears the current Interval */ public clearInterval(): void { if (this.currentInterval) { @@ -509,35 +549,24 @@ export class State { } } - //========================================================================================================= - // Copy - //========================================================================================================= - /** - * @public - * Creates fresh copy of State Value (-> No reference to State Value) - */ - public copy(): ValueType { - return copy(this.value); - } - - //========================================================================================================= - // Exists - //========================================================================================================= /** + * Returns a boolean indicating whether the State exists. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#exists) + * * @public - * Checks if State exists */ public get exists(): boolean { return !this.isPlaceholder && this.computeExistsMethod(this.value); } - //========================================================================================================= - // Compute Exists - //========================================================================================================= /** + * TODO + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#computeexists) + * * @public - * Function that computes the exists status of the State - * @param method - Computed Function + * @param method - Computed function */ public computeExists(method: ComputeExistsMethod): this { if (!isFunction(method)) { @@ -549,25 +578,46 @@ export class State { return this; } - //========================================================================================================= - // Is - //========================================================================================================= /** + * TODO + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#computevalue) + * + * @public + * @param method - Computed function + */ + public computeValue(method: ComputeValueMethod): this { + if (!isFunction(method)) { + LogCodeManager.log('00:03:01', ['Compute Value Method', 'function']); + return this; + } + this.computeValueMethod = method; + + // Initial compute + this.set(method(this.nextStateValue)); + + return this; + } + + /** + * Returns a boolean indicating whether the specified value is equal to the current State value. + * Equivalent to '===' with the difference that it looks at the value + * and not on the reference. + * * @public - * Equivalent to === - * @param value - Value that gets checked if its equals to the State Value + * @param value - Value to be compared with the current State value. */ public is(value: ValueType): boolean { return equal(value, this.value); } - //========================================================================================================= - // Is Not - //========================================================================================================= /** + * Returns a boolean indicating whether the specified value is not equal to the current State value. + * Equivalent to '!==' with the difference that it looks at the value + * and not on the reference. + * * @public - * Equivalent to !== - * @param value - Value that gets checked if its not equals to the State Value + * @param value - Value to be compared with the current State value. */ public isNot(value: ValueType): boolean { return notEqual(value, this.value); @@ -590,27 +640,6 @@ export class State { return this; } - //========================================================================================================= - // Compute Value - //========================================================================================================= - /** - * @public - * Function that recomputes State Value if it changes - * @param method - Computed Function - */ - public computeValue(method: ComputeValueMethod): this { - if (!isFunction(method)) { - LogCodeManager.log('00:03:01', ['Compute Value Method', 'function']); - return this; - } - this.computeValueMethod = method; - - // Initial compute - this.set(method(this.nextStateValue)); - - return this; - } - //========================================================================================================= // Add SideEffect //========================================================================================================= diff --git a/packages/core/tests/unit/state/state.test.ts b/packages/core/tests/unit/state/state.test.ts index dbfb84d5..bff8000e 100644 --- a/packages/core/tests/unit/state/state.test.ts +++ b/packages/core/tests/unit/state/state.test.ts @@ -404,24 +404,25 @@ describe('State Tests', () => { beforeEach(() => { objectState.ingest = jest.fn(); numberState.ingest = jest.fn(); + arrayState.ingest = jest.fn(); jest.spyOn(Utils, 'flatMerge'); }); - it("shouldn't patch and ingest passed object based value into a not object based State (default config)", () => { + it("shouldn't patch specified object value into a not object based State (default config)", () => { numberState.patch({ changed: 'object' }); LogMock.hasLoggedCode('14:03:02'); expect(objectState.ingest).not.toHaveBeenCalled(); }); - it("shouldn't patch and ingest passed not object based value into object based State (default config)", () => { + it("shouldn't patch specified non object value into a object based State (default config)", () => { objectState.patch('number' as any); LogMock.hasLoggedCode('00:03:01', ['TargetWithChanges', 'object']); expect(objectState.ingest).not.toHaveBeenCalled(); }); - it('should patch and ingest passed object based value into a object based State (default config)', () => { + it('should patch specified object value into a object based State (default config)', () => { objectState.patch({ name: 'frank' }); expect(Utils.flatMerge).toHaveBeenCalledWith( @@ -436,7 +437,7 @@ describe('State Tests', () => { expect(objectState.ingest).toHaveBeenCalledWith({}); }); - it('should patch and ingest passed object based value into a object based State (specific config)', () => { + it('should patch specified object value into a object based State (specific config)', () => { objectState.patch( { name: 'frank' }, { @@ -468,6 +469,30 @@ describe('State Tests', () => { }, }); }); + + it('should patch specified array value into a array based State (default config)', () => { + arrayState.patch(['hi']); + + expect(Utils.flatMerge).not.toHaveBeenCalled(); + expect(arrayState.nextStateValue).toStrictEqual(['jeff', 'hi']); + expect(arrayState.ingest).toHaveBeenCalledWith({}); + }); + + it('should patch specified array value into a object based State', () => { + objectState.patch(['hi'], { addNewProperties: true }); + + expect(Utils.flatMerge).toHaveBeenCalledWith( + { age: 10, name: 'jeff' }, + ['hi'], + { addNewProperties: true } + ); + expect(objectState.nextStateValue).toStrictEqual({ + 0: 'hi', + age: 10, + name: 'jeff', + }); + expect(objectState.ingest).toHaveBeenCalledWith({}); + }); }); describe('watch function tests', () => { @@ -807,16 +832,6 @@ describe('State Tests', () => { }); }); - describe('copy function tests', () => { - it('should return a reference free copy of the current State Value', () => { - jest.spyOn(Utils, 'copy'); - const value = numberState.copy(); - - expect(value).toBe(10); - expect(Utils.copy).toHaveBeenCalledWith(10); - }); - }); - describe('exists get function tests', () => { it('should return true if State is no placeholder and computeExistsMethod returns true', () => { numberState.computeExistsMethod = jest.fn().mockReturnValueOnce(true); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 4746c07c..e02d5cc3 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -198,13 +198,13 @@ export function flatMerge( // Copy Source to avoid References const _source = copy(source); - if (!_source) return _source; + if (_source == null) return _source; // Merge Changes Object into Source Object const keys = Object.keys(changes); keys.forEach((property) => { - if (!config.addNewProperties && !_source[property]) return; - _source[property] = changes[property]; + if (config.addNewProperties && _source[property] != null) + _source[property] = changes[property]; }); return _source; From 61b2ee64e0fc6d7f1d19c11379e97560327ec926 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 6 Jun 2021 07:40:02 +0200 Subject: [PATCH 26/63] fixed typos --- packages/core/src/collection/group.ts | 2 +- packages/core/src/collection/index.ts | 5 +- packages/core/src/collection/selector.ts | 2 +- packages/core/src/logCodeManager.ts | 2 +- packages/core/src/state/index.ts | 187 ++++++++++++------- packages/core/src/state/state.observer.ts | 2 +- packages/core/tests/unit/state/state.test.ts | 51 ++++- packages/react/src/hooks/useAgile.ts | 2 +- 8 files changed, 172 insertions(+), 81 deletions(-) diff --git a/packages/core/src/collection/group.ts b/packages/core/src/collection/group.ts index 8452d83b..0ac01960 100644 --- a/packages/core/src/collection/group.ts +++ b/packages/core/src/collection/group.ts @@ -380,7 +380,7 @@ export interface GroupAddConfigInterface extends StateIngestConfigInterface { export interface GroupConfigInterface { /** - * Key/Name identifier of Group. + * Key/Name identifier of the Group. * @default undefined */ key?: GroupKey; diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 45ab7394..f8c13c6a 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -1489,7 +1489,7 @@ export interface CreateCollectionConfigInterface { */ selectors?: { [key: string]: Selector } | string[]; /** - * Key/Name identifier of Collection. + * Key/Name identifier of the Collection. * @default undefined */ key?: CollectionKey; @@ -1625,9 +1625,8 @@ export interface CollectionPersistentConfigInterface { /** * Default Storage key of the specified Storage keys. * The Collection value is loaded from the default Storage - * and only loaded from the remaining Storages (storageKeys) + * and is only loaded from the remaining Storages (storageKeys) * if the loading of the default Storage failed. - * * @default first index of the specified Storage keys or the AgileTs default Storage key */ defaultStorageKey?: StorageKey; diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index b6afe661..15a32b72 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -285,7 +285,7 @@ export type SelectorKey = string | number; export interface SelectorConfigInterface { /** - * Key/Name identifier of Selector. + * Key/Name identifier of the Selector. * @default undefined */ key?: SelectorKey; diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 1e99b165..700b0a26 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -65,7 +65,7 @@ const logCodeMessages = { '14:03:02': "The 'patch()' method works only in object based States!", '14:03:03': "Watcher Callback with the key/name '${0}' already exists!", '14:03:04': 'Only one Interval can be active at once!', - '14:03:05': "The 'invert()' method works only in boolean based States!", + '14:03:05': "Failed to invert value of the type '${0}'!", // SubController '15:01:00': "Unregistered 'Callback' based Subscription.", diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 282b9a4f..34f69297 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -224,13 +224,10 @@ export class State { */ public type(type: any): this { const supportedTypes = ['String', 'Boolean', 'Array', 'Object', 'Number']; - - // Check if type is a supported type if (!supportedTypes.includes(type.name)) { LogCodeManager.log('14:03:01', [type]); return this; } - this.valueType = type.name.toLowerCase(); return this; } @@ -389,7 +386,7 @@ export class State { * [Learn more..](https://agile-ts.org/docs/core/state/methods/#haswatcher) * * @public - * @param key - Key/Name identifier of the watcher callback. + * @param key - Key/Name identifier of the watcher callback to be checked for existence. */ public hasWatcher(key: string): boolean { return !!this.watchers[key]; @@ -488,24 +485,23 @@ export class State { */ public onLoad(callback: (success: boolean) => void): this { if (!this.persistent) return this; - - // Check if provided callback is valid function if (!isFunction(callback)) { LogCodeManager.log('00:03:01', ['OnLoad Callback', 'function']); return this; } - // Register provided callback + // Register specified callback this.persistent.onLoad = callback; - // If State is already persisted ('isPersisted') fire provided callback immediately + // If State is already persisted ('isPersisted') fire specified callback immediately if (this.isPersisted) callback(true); return this; } /** - * Repeatedly calls a function or executes a code snippet, with a fixed time delay between each call. + * Repeatedly calls a function or executes a code snippet, + * with a fixed time delay between each call. * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#interval) * @@ -527,6 +523,7 @@ export class State { return this; } + // Create interval this.currentInterval = setInterval(() => { this.set(handler(this._value)); }, delay ?? 1000); @@ -552,6 +549,9 @@ export class State { /** * Returns a boolean indicating whether the State exists. * + * It calculates the value based on the `computeExistsMethod()` + * and whether the State is a placeholder. + * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#exists) * * @public @@ -561,12 +561,15 @@ export class State { } /** - * TODO + * Defines the method used to compute the existence of the State. + * + * It is retrieved on each `exists()` method call + * to determine whether the State exists or not. * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#computeexists) * * @public - * @param method - Computed function + * @param method - Method to compute the existence of the State. */ public computeExists(method: ComputeExistsMethod): this { if (!isFunction(method)) { @@ -579,12 +582,16 @@ export class State { } /** - * TODO + * Defines the method used to compute the value of the State. + * + * It is retrieved on each State value change, + * in order to compute the new State value + * based on the specified compute method. * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#computevalue) * * @public - * @param method - Computed function + * @param method - Method to compute the value of the State. */ public computeValue(method: ComputeValueMethod): this { if (!isFunction(method)) { @@ -601,8 +608,9 @@ export class State { /** * Returns a boolean indicating whether the specified value is equal to the current State value. - * Equivalent to '===' with the difference that it looks at the value - * and not on the reference. + * + * Equivalent to `===` with the difference that it looks at the value + * and not on the reference in the case of objects. * * @public * @param value - Value to be compared with the current State value. @@ -613,8 +621,9 @@ export class State { /** * Returns a boolean indicating whether the specified value is not equal to the current State value. - * Equivalent to '!==' with the difference that it looks at the value - * and not on the reference. + * + * Equivalent to `!==` with the difference that it looks at the value + * and not on the reference in the case of objects. * * @public * @param value - Value to be compared with the current State value. @@ -623,32 +632,52 @@ export class State { return notEqual(value, this.value); } - //========================================================================================================= - // Invert - //========================================================================================================= /** + * Inverts the current State value. + * + * Some examples are: + * - `'jeff'` -> `'ffej'` + * - `true` -> `false` + * - `[1, 2, 3]` -> `[3, 2, 1]` + * - `10` -> `-10` + * * @public - * Inverts State Value - * Note: Only useful with boolean based States */ public invert(): this { - if (typeof this._value === 'boolean') { - this.set(!this._value as any); - } else { - LogCodeManager.log('14:03:05'); + switch (typeof this.nextStateValue) { + case 'boolean': + this.set(!this.nextStateValue as any); + break; + case 'object': + if (Array.isArray(this.nextStateValue)) + this.set(this.nextStateValue.reverse() as any); + break; + case 'string': + this.set(this.nextStateValue.split('').reverse().join('') as any); + break; + case 'number': + this.set((this.nextStateValue * -1) as any); + break; + default: + LogCodeManager.log('14:03:05', [typeof this.nextStateValue]); } + return this; } - //========================================================================================================= - // Add SideEffect - //========================================================================================================= /** + * + * Registers a `callback` function that is executed during the `runtime` + * as a side effect of State changes. + * + * For example it is called when the State value changes from 'jeff' to 'hans'. + * + * A typical side effect could be the updating of the external Storage value. + * * @internal - * Adds SideEffect to State - * @param key - Key/Name of SideEffect - * @param callback - Callback Function that gets called on every State Value change - * @param config - Config + * @param key - Key/Name identifier of the to register side effect. + * @param callback - Callback function to be called on each State value change. + * @param config - Configuration object */ public addSideEffect>( key: string, @@ -669,26 +698,27 @@ export class State { return this; } - //========================================================================================================= - // Remove SideEffect - //========================================================================================================= /** + * Removes a side effect callback with the specified key/name identifier from the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#removesideeffect) + * * @internal - * Removes SideEffect at given Key - * @param key - Key of the SideEffect that gets removed + * @param key - Key/Name identifier of the side effect callback to be removed. */ public removeSideEffect(key: string): this { delete this.sideEffects[key]; return this; } - //========================================================================================================= - // Has SideEffect - //========================================================================================================= /** + * Returns a boolean indicating whether a side effect callback with the specified `key` + * exists in the State or not. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#hassideeffect) + * * @internal - * Checks if sideEffect at given Key exists - * @param key - Key of SideEffect + * @param key - Key/Name identifier of the side effect callback to be checked for existence. */ public hasSideEffect(key: string): boolean { return !!this.sideEffects[key]; @@ -709,26 +739,23 @@ export class State { return type === this.valueType; } - //========================================================================================================= - // Get Public Value - //========================================================================================================= /** + * Returns the public value of the State. + * * @internal - * Returns public Value of State */ public getPublicValue(): ValueType { - // If State Value is used internally and output represents the real state value (for instance in Group) + // If State value is used internally + // and output represents the public State value (for instance in Group) if (this['output'] !== undefined) return this['output']; return this._value; } - //========================================================================================================= - // Get Persistable Value - //========================================================================================================= /** + * Returns the persistable value of the State. + * * @internal - * Returns Value that gets written into the Agile Storage */ public getPersistableValue(): any { return this._value; @@ -743,8 +770,21 @@ export type StateKey = string | number; * @param isPlaceholder - If State is initially a Placeholder */ export interface StateConfigInterface { + /** + * Key/Name identifier of the State. + * @default undefined + */ key?: StateKey; + /** + * Observers that depend on the State. + * @default undefined + */ dependents?: Array; + /** + * Whether the State should be a placeholder + * and therefore should only exist in the background. + * @default false + */ isPlaceholder?: boolean; } @@ -760,14 +800,26 @@ export interface PatchOptionConfigInterface { addNewProperties?: boolean; } -/** - * @param loadValue - If Persistent loads the persisted value into the State - * @param storageKeys - Key/Name of Storages which gets used to persist the State Value (NOTE: If not passed the default Storage will be used) - * @param defaultStorageKey - Default Storage Key (if not provided it takes the first index of storageKeys or the AgileTs default Storage) - */ export interface StatePersistentConfigInterface { + /** + * Whether the Persistent should automatically load + * the persisted value into the State after its instantiation. + * @default true + */ loadValue?: boolean; + /** + * Key/Name identifier of Storages + * in which the State value should be or is persisted. + * @default [AgileTs default Storage key] + */ storageKeys?: StorageKey[]; + /** + * Default Storage key of the specified Storage keys. + * The State value is loaded from the default Storage + * and is only loaded from the remaining Storages (storageKeys) + * if the loading of the default Storage failed. + * @default first index of the specified Storage keys or the AgileTs default Storage key + */ defaultStorageKey?: StorageKey; } @@ -782,18 +834,25 @@ export type SideEffectFunctionType> = ( } ) => void; -/** - * @param callback - Callback Function that gets called on every State Value change - * @param weight - When the sideEffect gets executed. The higher, the earlier it gets executed. - */ export interface SideEffectInterface> { + /** + * Callback function to be called on every State value change + * @return () => {} + */ callback: SideEffectFunctionType; + /** + * Weight of the side effect. + * Determines the order of execution of the registered side effects. + * The higher the weight, the earlier it is executed. + */ weight: number; } -/** - * @param weight - When the sideEffect gets executed. The higher, the earlier it gets executed. - */ export interface AddSideEffectConfigInterface { + /** + * Weight of the side effect. + * Determines the order of execution of the registered side effects. + * The higher the weight, the earlier it is executed. + */ weight?: number; } diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index 3da44fe9..fd0182dc 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -143,8 +143,8 @@ export class StateObserver extends Observer { // because sometimes (for instance in a Group State) the publicValue() is not the .value (nextStateValue) property. // The Observer value is at some point the public Value because Integrations like React are using it as return value. // For example 'useAgile()' returns the Observer.value and not the State.value. + job.observer.previousValue = copy(job.observer.value); job.observer.value = copy(state.getPublicValue()); - job.observer.previousValue = copy(state.previousStateValue); } //========================================================================================================= diff --git a/packages/core/tests/unit/state/state.test.ts b/packages/core/tests/unit/state/state.test.ts index bff8000e..84cccca9 100644 --- a/packages/core/tests/unit/state/state.test.ts +++ b/packages/core/tests/unit/state/state.test.ts @@ -924,22 +924,55 @@ describe('State Tests', () => { }); describe('invert function tests', () => { + let dummyState: State; + beforeEach(() => { - numberState.set = jest.fn(); - booleanState.set = jest.fn(); + dummyState = new State(dummyAgile, null); + + dummyState.set = jest.fn(); }); - it('should invert current value of a boolean based State', () => { - booleanState.invert(); + it('should invert value of the type boolean', () => { + dummyState.nextStateValue = false; - expect(booleanState.set).toHaveBeenCalledWith(true); + dummyState.invert(); + + expect(dummyState.set).toHaveBeenCalledWith(true); }); - it("shouldn't invert current value if not boolean based State and should print a error", () => { - numberState.invert(); + it('should invert value of the type number', () => { + dummyState.nextStateValue = 10; - expect(numberState.set).not.toHaveBeenCalled(); - LogMock.hasLoggedCode('14:03:05'); + dummyState.invert(); + + expect(dummyState.set).toHaveBeenCalledWith(-10); + }); + + it('should invert value of the type array', () => { + dummyState.nextStateValue = ['1', '2', '3']; + + dummyState.invert(); + + expect(dummyState.set).toHaveBeenCalledWith(['3', '2', '1']); + }); + + it('should invert value of the type string', () => { + dummyState.nextStateValue = 'jeff'; + + dummyState.invert(); + + expect(dummyState.set).toHaveBeenCalledWith('ffej'); + }); + + it("shouldn't invert not invertible types like function, null, undefined, object", () => { + dummyState.nextStateValue = () => { + // empty + }; + + dummyState.invert(); + + expect(dummyState.set).not.toHaveBeenCalled(); + LogMock.hasLoggedCode('14:03:05', ['function']); }); }); diff --git a/packages/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index f38d04d3..7061ed61 100644 --- a/packages/react/src/hooks/useAgile.ts +++ b/packages/react/src/hooks/useAgile.ts @@ -56,7 +56,7 @@ export function useAgile< // Creates Return Value of Hook, depending if deps are in Array shape or not const getReturnValue = ( - depsArray: (State | Observer | undefined)[] + depsArray: (Observer | undefined)[] ): AgileHookArrayType | AgileHookType => { const handleReturn = ( dep: State | Observer | undefined From 305f55340f4bb7f18d148233e9bae1f535e902df Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 6 Jun 2021 12:15:00 +0200 Subject: [PATCH 27/63] refactored the selector method descriptions --- .../src/collection/collection.persistent.ts | 3 +- packages/core/src/collection/group.ts | 11 +- packages/core/src/collection/index.ts | 39 +++-- packages/core/src/collection/item.ts | 5 +- packages/core/src/collection/selector.ts | 7 +- packages/core/src/computed/index.ts | 10 +- packages/core/src/logCodeManager.ts | 5 +- packages/core/src/state/index.ts | 142 +++++++++--------- packages/core/tests/unit/state/state.test.ts | 15 +- packages/utils/src/index.ts | 5 +- 10 files changed, 129 insertions(+), 113 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index b3831b6c..d7e099a8 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -16,6 +16,7 @@ import { export class CollectionPersistent< DataType extends Object = DefaultItem > extends Persistent { + // Collection the Persistent belongs to public collection: () => Collection; static defaultGroupSideEffectKey = 'rebuildGroupStorageValue'; @@ -226,7 +227,7 @@ export class CollectionPersistent< item?.persist(itemStorageKey, { defaultStorageKey: this.config.defaultStorageKey || undefined, storageKeys: this.storageKeys, - followCollectionPersistKeyPattern: false,// Because of the dynamic 'storageItemKey', the key is already formatted above + followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above }); } diff --git a/packages/core/src/collection/group.ts b/packages/core/src/collection/group.ts index 0ac01960..52dd9b6e 100644 --- a/packages/core/src/collection/group.ts +++ b/packages/core/src/collection/group.ts @@ -20,13 +20,18 @@ import { export class Group extends State< Array > { + // Collection the Group belongs to collection: () => Collection; static rebuildGroupSideEffectKey = 'rebuildGroup'; - _output: Array = []; // Item values represented by the Group - _items: Array<() => Item> = []; // Items represented by the Group - notFoundItemKeys: Array = []; // Contains all Item identifiers for Items that couldn't be found in the Collection + // Item values represented by the Group + _output: Array = []; + // Items represented by the Group + _items: Array<() => Item> = []; + + // Keeps track of all Item identifiers for Items that couldn't be found in the Collection + notFoundItemKeys: Array = []; /** * An extension of the State Class that categorizes and preserves the ordering of structured data. diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index f8c13c6a..4e944901 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -24,21 +24,30 @@ import { } from '../internal'; export class Collection { + // Agile Instance the Collection belongs to public agileInstance: () => Agile; public config: CollectionConfigInterface; private initialConfig: CreateCollectionConfigInterface; + // Key/Name identifier of the Collection public _key?: CollectionKey; - public size = 0; // Amount of the Items stored in the Collection - public data: { [key: string]: Item } = {}; // Items stored in the Collection - public isPersisted = false; // Whether the Collection is persisted in an external Storage - public persistent: CollectionPersistent | undefined; // Manages persisting the Collection 'value' + // Amount of the Items stored in the Collection + public size = 0; + // Items stored in the Collection + public data: { [key: string]: Item } = {}; + // Whether the Collection is persisted in an external Storage + public isPersisted = false; + // Manages the permanent persistent in external Storages + public persistent: CollectionPersistent | undefined; - public groups: { [key: string]: Group } = {}; // Groups of Collection - public selectors: { [key: string]: Selector } = {}; // Selectors of Collection + // Registered Groups of Collection + public groups: { [key: string]: Group } = {}; + // Registered Selectors of Collection + public selectors: { [key: string]: Selector } = {}; - public isInstantiated = false; // Whether the Collection was instantiated correctly + // Whether the Collection was instantiated correctly + public isInstantiated = false; /** * A Collection manages a reactive set of Information @@ -128,7 +137,7 @@ export class Collection { // Update Collection key this._key = value; - // Update key in Persistent (only if oldKey equal to persistentKey + // Update key in Persistent (only if oldKey is equal to persistentKey // because otherwise the persistentKey is detached from the Collection key // -> not managed by Collection anymore) if (value && this.persistent?._key === oldKey) @@ -455,7 +464,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasgroup) * * @public - * @param groupKey - Key/Name identifier of the Group. + * @param groupKey - Key/Name identifier of the Group to be checked for existence. * @param config - Configuration object */ public hasGroup( @@ -628,7 +637,7 @@ export class Collection { * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasselector) * * @public - * @param selectorKey - Key/Name identifier of the Selector. + * @param selectorKey - Key/Name identifier of the Selector to be checked for existence. * @param config - Configuration object */ public hasSelector( @@ -975,27 +984,25 @@ export class Collection { * Fires immediately after the persisted `value` * is loaded into the Collection from a corresponding external Storage. * - * Registering this callback only makes sense + * Registering such callback function makes only sense * when the Collection is [persisted](https://agile-ts.org/docs/core/collection/methods/#persist) in an external Storage. * * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#onload) * * @public - * @param callback - Callback function + * @param callback - A function to be executed after the externally persisted `value` was loaded into the Collection. */ public onLoad(callback: (success: boolean) => void): this { if (!this.persistent) return this; - - // Check if provided callback is valid function if (!isFunction(callback)) { LogCodeManager.log('00:03:01', ['OnLoad Callback', 'function']); return this; } - // Register provided callback + // Register specified callback this.persistent.onLoad = callback; - // If Collection is already persisted ('isPersisted') fire provided callback immediately + // If Collection is already persisted ('isPersisted') fire specified callback immediately if (this.isPersisted) callback(true); return this; diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index d78b4626..5375b143 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -15,10 +15,13 @@ import { export class Item extends State< DataType > { + // Collection the Group belongs to public collection: () => Collection; static updateGroupSideEffectKey = 'rebuildGroup'; - public selectedBy: Set = new Set(); // Key/Name Identifiers of Selectors which have selected the Item + + // Key/Name identifiers of Selectors which have selected the Item + public selectedBy: Set = new Set(); /** * An extension of the State Class that represents a single data object of a Collection. diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index 15a32b72..1366f24d 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -11,14 +11,17 @@ import { export class Selector extends State< DataType | undefined > { + // Collection the Selector belongs to public collection: () => Collection; static unknownItemPlaceholderKey = '__UNKNOWN__ITEM__KEY__'; static rebuildSelectorSideEffectKey = 'rebuildSelector'; static rebuildItemSideEffectKey = 'rebuildItem'; - public _item: Item | undefined; // Item the Selector represents - public _itemKey: ItemKey; // Key/Name identifier of the Item the Selector represents + // Item the Selector represents + public _item: Item | undefined; + // Key/Name identifier of the Item the Selector represents + public _itemKey: ItemKey; /** * A Selector represents an Item from a Collection in the long term. diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index 2bb0852d..983fc42c 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -15,11 +15,15 @@ import { export class Computed extends State< ComputedValueType > { + // Agile Instance the Computed belongs to public agileInstance: () => Agile; - public computeFunction: () => ComputedValueType; // Function to compute the Computed Class value - public deps: Array = []; // All dependencies the Computed Class depends on (including hardCoded and automatically detected dependencies) - public hardCodedDeps: Array = []; // Only hardCoded dependencies the Computed Class depends + // Function to compute the Computed Class value + public computeFunction: () => ComputedValueType; + // All dependencies the Computed Class depends on (including hardCoded and automatically detected dependencies) + public deps: Array = []; + // Only hardCoded dependencies the Computed Class depends on + public hardCodedDeps: Array = []; /** * An extension of the State Class that computes its value based on a compute function. diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 700b0a26..28694b93 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -63,9 +63,8 @@ const logCodeMessages = { '14:03:01': "'${1}' is a not supported type! Supported types are: String, Boolean, Array, Object, Number.", '14:03:02': "The 'patch()' method works only in object based States!", - '14:03:03': "Watcher Callback with the key/name '${0}' already exists!", - '14:03:04': 'Only one Interval can be active at once!', - '14:03:05': "Failed to invert value of the type '${0}'!", + '14:03:03': 'Only one Interval can be active at once!', + '14:03:04': "Failed to invert value of the type '${0}'!", // SubController '15:01:00': "Unregistered 'Callback' based Subscription.", diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 34f69297..0fbe3bb3 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -20,37 +20,54 @@ import { } from '../internal'; export class State { + // Agile Instance the State belongs to public agileInstance: () => Agile; + // Key/Name identifier of the State public _key?: StateKey; - public valueType?: string; // Primitive type which constrains the State value (for basic typesafety in Javascript) - public isSet = false; // Whether the current value differs from the initial value + // Primitive type which constrains the State value (for basic typesafety in Javascript) + public valueType?: string; + // Whether the current value differs from the initial value + public isSet = false; + // Whether the State is a placeholder and only exist in the background public isPlaceholder = false; - public initialStateValue: ValueType; // First value assigned to the State - public _value: ValueType; // Current value of the State - public previousStateValue: ValueType; // Previous value of the State - public nextStateValue: ValueType; // Next value of the State (which can be used for dynamic State updates) - - // Handles dependencies to other States and subscriptions of UI-Components - // and serves as an interface to the runtime + // First value assigned to the State + public initialStateValue: ValueType; + // Current value of the State + public _value: ValueType; + // Previous value of the State + public previousStateValue: ValueType; + // Next value of the State (which can be used for dynamic State updates) + public nextStateValue: ValueType; + + // Manages dependencies to other States and subscriptions of UI-Components. + // It also serves as an interface to the runtime. public observer: StateObserver; + // Registered side effects of changing the State value public sideEffects: { [key: string]: SideEffectInterface>; - } = {}; // Side effects of changing the State value - public computeValueMethod?: ComputeValueMethod; // Method for dynamically computing the State value - public computeExistsMethod: ComputeExistsMethod; // Method for dynamically computing the existence of the State + } = {}; + + // Method for dynamically computing the State value + public computeValueMethod?: ComputeValueMethod; + // Method for dynamically computing the existence of the State + public computeExistsMethod: ComputeExistsMethod; - public isPersisted = false; // Whether the State is persisted in an external Storage - public persistent: StatePersistent | undefined; // Manages persisting the State value + // Whether the State is persisted in an external Storage + public isPersisted = false; + // Manages the permanent persistent in external Storages + public persistent: StatePersistent | undefined; - public watchers: { [key: string]: StateWatcherCallback } = {}; // Callbacks that are fired on State changes + // Registered callbacks that are fired on each State value change + public watchers: { [key: string]: StateWatcherCallback } = {}; - public currentInterval?: NodeJS.Timer | number; // The current active interval + // When an interval is active, the 'intervalId' to clear the interval is temporary stored here + public currentInterval?: NodeJS.Timer | number; /** * A State manages a piece of Information - * that we need to remember globally at a later point int time. + * that we need to remember globally at a later point in time. * While providing a toolkit to use and mutate this set of Information. * * You can create as many global States as you need. @@ -89,7 +106,8 @@ export class State { } /** - * Updates the value of the State. + * Assigns a new value to the State + * and rerenders all subscribed Components. * * [Learn more..](https://agile-ts.org/docs/core/state/properties#value) * @@ -101,7 +119,7 @@ export class State { } /** - * Returns a reference-free version of the State value. + * Returns a reference-free version of the current State value. * * [Learn more..](https://agile-ts.org/docs/core/state/properties#value) * @@ -152,7 +170,7 @@ export class State { // Update key of Observer this.observer._key = value; - // Update key in Persistent (only if oldKey equal to persistentKey + // Update key in Persistent (only if oldKey is equal to persistentKey // because otherwise the persistentKey is detached from the State key // -> not managed by State anymore) if (value && this.persistent?._key === oldKey) @@ -162,7 +180,8 @@ export class State { } /** - * Assigns a new value to the State. + * Assigns a new value to the State + * and rerenders all subscribed Components. * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#set) * @@ -181,7 +200,7 @@ export class State { ? (value as any)(copy(this._value)) : value; - // Check value has correct type (Javascript) + // Check if value has correct type (Javascript) if (!this.hasCorrectType(_value)) { LogCodeManager.log(config.force ? '14:02:00' : '14:03:00', [ typeof _value, @@ -199,7 +218,8 @@ export class State { /** * Ingests the State without any specified new value into the runtime. * - * Since no new value was defined either the State value is computed (Computed Class) + * Since no new value was defined either the State value is computed + * based on a compute method (Computed Class) * or the `nextStateValue` is taken. * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#ingest) @@ -260,11 +280,11 @@ export class State { /** * Merges the specified `targetWithChanges` object into the current State value. - * This merge can be different for different value combinations: - * - If the current State value is an `object`, it does partial update for the object. - * - If the current State value is an `array` an the argument is an array too, - * it concatenates the current value with the value of the argument. - * - If the current State value is not a `object` or an `array` it prints an warning. + * This merge can differ for different value combinations: + * - If the current State value is an `object`, it does a partial update for the object. + * - If the current State value is an `array` and the specified argument is an array too, + * it concatenates the current State value with the value of the argument. + * - If the current State value is neither an `object` nor an `array`, the patch can't be performed. * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#patch) * @@ -321,7 +341,7 @@ export class State { * [Learn more..](https://agile-ts.org/docs/core/state/methods/#watch) * * @public - * @param callback - Callback function + * @param callback - A function to be executed on each State value change. */ public watch(callback: StateWatcherCallback): string; /** @@ -331,7 +351,7 @@ export class State { * * @public * @param key - Key/Name identifier of the watcher callback. - * @param callback - Callback function + * @param callback - A function to be executed on each State value change. */ public watch(key: string, callback: StateWatcherCallback): this; public watch( @@ -350,18 +370,10 @@ export class State { _callback = callback as StateWatcherCallback; } - // Check if specified callback is a valid function if (!isFunction(_callback)) { LogCodeManager.log('00:03:01', ['Watcher Callback', 'function']); return this; } - - // Check if watcher with key is already occupied - if (this.watchers[key]) { - LogCodeManager.log('14:03:03', [key]); - return this; - } - this.watchers[key] = _callback; return generateKey ? key : this; } @@ -393,12 +405,12 @@ export class State { } /** - * Fires on the first State value assignment and then destroys itself. + * Fires on the initial State value assignment and then destroys itself. * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#oninaugurated) * * @public - * @param callback - Callback function + * @param callback - A function to be executed after the first State value assignment. */ public onInaugurated(callback: StateWatcherCallback): this { const watcherKey = 'InauguratedWatcherKey'; @@ -474,14 +486,15 @@ export class State { /** * Fires immediately after the persisted `value` - * is loaded into the State from corresponding the external Storage. + * is loaded into the State from a corresponding external Storage. * - * Registering this callback only makes sense when the State is [persisted](https://agile-ts.org/docs/core/state/methods/#persist). + * Registering such callback function makes only sense + * when the State is [persisted](https://agile-ts.org/docs/core/state/methods/#persist). * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#onload) * * @public - * @param callback - Callback function + * @param callback - A function to be executed after the externally persisted `value` was loaded into the State. */ public onLoad(callback: (success: boolean) => void): this { if (!this.persistent) return this; @@ -500,7 +513,7 @@ export class State { } /** - * Repeatedly calls a function or executes a code snippet, + * Repeatedly calls the specified callback function, * with a fixed time delay between each call. * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#interval) @@ -519,15 +532,12 @@ export class State { return this; } if (this.currentInterval) { - LogCodeManager.log('14:03:04', [], this.currentInterval); + LogCodeManager.log('14:03:03', [], this.currentInterval); return this; } - - // Create interval this.currentInterval = setInterval(() => { this.set(handler(this._value)); }, delay ?? 1000); - return this; } @@ -577,7 +587,6 @@ export class State { return this; } this.computeExistsMethod = method; - return this; } @@ -659,24 +668,24 @@ export class State { this.set((this.nextStateValue * -1) as any); break; default: - LogCodeManager.log('14:03:05', [typeof this.nextStateValue]); + LogCodeManager.log('14:03:04', [typeof this.nextStateValue]); } - return this; } /** * - * Registers a `callback` function that is executed during the `runtime` + * Registers a `callback` function that is executed in the `runtime` * as a side effect of State changes. * - * For example it is called when the State value changes from 'jeff' to 'hans'. + * For example, it is called when the State value changes from 'jeff' to 'hans'. * - * A typical side effect could be the updating of the external Storage value. + * A typical side effect of a State change + * could be the updating of the external Storage value. * * @internal * @param key - Key/Name identifier of the to register side effect. - * @param callback - Callback function to be called on each State value change. + * @param callback - Callback function to be fired on each State value change. * @param config - Configuration object */ public addSideEffect>( @@ -724,14 +733,12 @@ export class State { return !!this.sideEffects[key]; } - //========================================================================================================= - // Is Correct Type - //========================================================================================================= /** + * Returns a boolean indicating whether the passed value + * is of the before defined State `valueType` or not. + * * @internal - * Checks if Value has correct valueType (js) - * Note: If no valueType set, it returns true - * @param value - Value that gets checked for its correct Type + * @param value - Value to be checked for the correct type. */ public hasCorrectType(value: any): boolean { if (!this.valueType) return true; @@ -764,11 +771,6 @@ export class State { export type StateKey = string | number; -/** - * @param key - Key/Name of State - * @param deps - Initial deps of State - * @param isPlaceholder - If State is initially a Placeholder - */ export interface StateConfigInterface { /** * Key/Name identifier of the State. @@ -777,7 +779,7 @@ export interface StateConfigInterface { key?: StateKey; /** * Observers that depend on the State. - * @default undefined + * @default [] */ dependents?: Array; /** @@ -836,13 +838,13 @@ export type SideEffectFunctionType> = ( export interface SideEffectInterface> { /** - * Callback function to be called on every State value change + * Callback function to be called on every State value change. * @return () => {} */ callback: SideEffectFunctionType; /** * Weight of the side effect. - * Determines the order of execution of the registered side effects. + * The weight determines the order of execution of the registered side effects. * The higher the weight, the earlier it is executed. */ weight: number; @@ -851,7 +853,7 @@ export interface SideEffectInterface> { export interface AddSideEffectConfigInterface { /** * Weight of the side effect. - * Determines the order of execution of the registered side effects. + * The weight determines the order of execution of the registered side effects. * The higher the weight, the earlier it is executed. */ weight?: number; diff --git a/packages/core/tests/unit/state/state.test.ts b/packages/core/tests/unit/state/state.test.ts index 84cccca9..b3914e79 100644 --- a/packages/core/tests/unit/state/state.test.ts +++ b/packages/core/tests/unit/state/state.test.ts @@ -532,17 +532,6 @@ describe('State Tests', () => { expect(numberState.watchers).not.toHaveProperty('dummyKey'); LogMock.hasLoggedCode('00:03:01', ['Watcher Callback', 'function']); }); - - it("shouldn't add passed watcherFunction to watchers at passed key if passed key is already occupied", () => { - numberState.watchers['dummyKey'] = dummyCallbackFunction2; - - const response = numberState.watch('dummyKey', dummyCallbackFunction1); - - expect(response).toBe(numberState); - expect(numberState.watchers).toHaveProperty('dummyKey'); - expect(numberState.watchers['dummyKey']).toBe(dummyCallbackFunction2); - LogMock.hasLoggedCode('14:03:03', ['dummyKey']); - }); }); describe('removeWatcher function tests', () => { @@ -789,7 +778,7 @@ describe('State Tests', () => { 3000 ); expect(numberState.currentInterval).toStrictEqual(currentInterval); - LogMock.hasLoggedCode('14:03:04', [], numberState.currentInterval); + LogMock.hasLoggedCode('14:03:03', [], numberState.currentInterval); }); it("shouldn't set invalid interval callback function", () => { @@ -972,7 +961,7 @@ describe('State Tests', () => { dummyState.invert(); expect(dummyState.set).not.toHaveBeenCalled(); - LogMock.hasLoggedCode('14:03:05', ['function']); + LogMock.hasLoggedCode('14:03:04', ['function']); }); }); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index e02d5cc3..e9104543 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -203,7 +203,10 @@ export function flatMerge( // Merge Changes Object into Source Object const keys = Object.keys(changes); keys.forEach((property) => { - if (config.addNewProperties && _source[property] != null) + if ( + (!config.addNewProperties && _source[property] != null) || + config.addNewProperties + ) _source[property] = changes[property]; }); From d9249255d6e352e2b1d8a21aab9189d5a46f3c48 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 6 Jun 2021 16:29:36 +0200 Subject: [PATCH 28/63] added weakmap based selectors to SubscriptionContainer --- packages/core/src/collection/index.ts | 4 +- packages/core/src/runtime/index.ts | 124 +++++--------- .../CallbackSubscriptionContainer.ts | 21 ++- .../ComponentSubscriptionContainer.ts | 29 +++- .../container/SubscriptionContainer.ts | 158 ++++++++++++++---- .../runtime/subscription/sub.controller.ts | 3 +- packages/core/src/state/index.ts | 2 +- .../core/tests/unit/runtime/runtime.test.ts | 60 +++---- .../CallbackSubscriptionContainer.test.ts | 6 +- .../ComponentSubscriptionContainer.test.ts | 6 +- .../container/SubscriptionContainer.test.ts | 12 +- .../subscription/sub.controller.test.ts | 10 +- packages/react/src/hooks/useAgile.ts | 35 +--- 13 files changed, 258 insertions(+), 212 deletions(-) diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 4e944901..e2d38d20 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -1486,12 +1486,12 @@ export type ItemKey = string | number; export interface CreateCollectionConfigInterface { /** - * Initial Groups of Collection. + * Initial Groups of the Collection. * @default [] */ groups?: { [key: string]: Group } | string[]; /** - * Initial Selectors of Collection + * Initial Selectors of the Collection * @default [] */ selectors?: { [key: string]: Selector } | string[]; diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index e3b1c409..036cc20a 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -155,17 +155,20 @@ export class Runtime { return; } - // Handle Object based Subscription - if (subscriptionContainer.isObjectBased) - this.handleObjectBasedSubscription(subscriptionContainer, job); - - // Check if subscriptionContainer should be updated - const updateSubscriptionContainer = subscriptionContainer.proxyBased - ? this.handleProxyBasedSubscription(subscriptionContainer, job) - : true; + let updateSubscriptionContainer = true; + + // Handle Selectors + if (subscriptionContainer.hasSelectors) { + updateSubscriptionContainer = this.handleSelectors( + subscriptionContainer, + job + ); + } - if (updateSubscriptionContainer) + if (updateSubscriptionContainer) { + subscriptionContainer.updatedSubscribers.push(job.observer); subscriptionsToUpdate.add(subscriptionContainer); + } job.subscriptionContainersToUpdate.delete(subscriptionContainer); }); @@ -183,8 +186,10 @@ export class Runtime { if (subscriptionContainer instanceof ComponentSubscriptionContainer) this.agileInstance().integrations.update( subscriptionContainer.component, - this.getObjectBasedProps(subscriptionContainer) + this.getUpdatedObserverValues(subscriptionContainer) ); + + subscriptionContainer.updatedSubscribers = []; }); Agile.logger.if @@ -194,56 +199,27 @@ export class Runtime { return true; } - //========================================================================================================= - // Handle Object Based Subscription - //========================================================================================================= /** + * Returns a key map with Observer values that have been updated. + * * @internal - * Finds key of Observer (Job) in subsObject and adds it to 'changedObjectKeys' * @param subscriptionContainer - Object based SubscriptionContainer - * @param job - Job that holds the searched Observer */ - public handleObjectBasedSubscription( - subscriptionContainer: SubscriptionContainer, - job: RuntimeJob - ): void { - let foundKey: string | null = null; - - // Check if SubscriptionContainer is Object based - if (!subscriptionContainer.isObjectBased) return; - - // Find Key of Job Observer in SubscriptionContainer - for (const key in subscriptionContainer.subsObject) - if (subscriptionContainer.subsObject[key] === job.observer) - foundKey = key; - - if (foundKey) subscriptionContainer.observerKeysToUpdate.push(foundKey); - } - - //========================================================================================================= - // Get Object Based Props - //========================================================================================================= - /** - * @internal - * Builds Object out of changedObjectKeys with Observer Value - * @param subscriptionContainer - Object based SubscriptionContainer - */ - public getObjectBasedProps( + public getUpdatedObserverValues( subscriptionContainer: SubscriptionContainer ): { [key: string]: any } { const props: { [key: string]: any } = {}; - // Map trough observerKeysToUpdate and build object out of Observer value - if (subscriptionContainer.subsObject) - for (const updatedKey of subscriptionContainer.observerKeysToUpdate) - props[updatedKey] = subscriptionContainer.subsObject[updatedKey]?.value; - - subscriptionContainer.observerKeysToUpdate = []; + // Map 'Observer To Update' values into the props object + for (const observer of subscriptionContainer.updatedSubscribers) { + const key = subscriptionContainer.subscriberKeysWeakMap.get(observer); + if (key != null) props[key] = observer.value; + } return props; } //========================================================================================================= - // Handle Proxy Based Subscription + // Handle Selectors //========================================================================================================= /** * @internal @@ -256,47 +232,25 @@ export class Runtime { * -> If a from the Proxy Tree detected property differs from the same property in the previous value * or the passed subscriptionContainer isn't properly proxy based */ - public handleProxyBasedSubscription( + public handleSelectors( subscriptionContainer: SubscriptionContainer, job: RuntimeJob ): boolean { - // Return true because in this cases the subscriptionContainer isn't properly proxyBased - if ( - !subscriptionContainer.proxyBased || - !job.observer._key || - !subscriptionContainer.proxyKeyMap[job.observer._key] - ) - return true; - - const paths = subscriptionContainer.proxyKeyMap[job.observer._key].paths; - - if (paths) { - for (const path of paths) { - // Get property in new Value located at path - let newValue = job.observer.value; - let newValueDeepness = 0; - for (const branch of path) { - if (!isValidObject(newValue, true)) break; - newValue = newValue[branch]; - newValueDeepness++; - } + // TODO add Selector support for Object based subscriptions + const selectors = subscriptionContainer.selectorsWeakMap.get(job.observer) + ?.selectors; - // Get property in previous Value located at path - let previousValue = job.observer.previousValue; - let previousValueDeepness = 0; - for (const branch of path) { - if (!isValidObject(previousValue, true)) break; - previousValue = previousValue[branch]; - previousValueDeepness++; - } - - // Check if found values differ - if ( - notEqual(newValue, previousValue) || - newValueDeepness !== previousValueDeepness - ) { - return true; - } + // Return true because in this cases the subscriptionContainer isn't properly proxyBased + if (!subscriptionContainer.hasSelectors || !selectors) return true; + + const previousValue = job.observer.previousValue; + const newValue = job.observer.value; + for (const selector of selectors) { + if ( + notEqual(selector(newValue), selector(previousValue)) + // || newValueDeepness !== previousValueDeepness // Not possible to check + ) { + return true; } } diff --git a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts index e9771a0b..6b3339b5 100644 --- a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts @@ -5,14 +5,27 @@ import { } from '../../../internal'; export class CallbackSubscriptionContainer extends SubscriptionContainer { + /** + * Callback function to trigger a rerender + * on the Component the Subscription Container represents. + */ public callback: Function; /** + * Subscription Container for callback based subscriptions. + * + * In a callback based subscription, a rerender is triggered on the Component via a specified callback function. + * + * The Callback Subscription Container doesn't keep track of the Component itself. + * It only knows how to trigger a rerender on the particular Component through the callback function. + * + * [Learn more..](https://agile-ts.org/docs/core/integration#callback-based) + * * @internal - * CallbackSubscriptionContainer - Subscription Container for Callback based Subscriptions - * @param callback - Callback Function that causes rerender on Component that is subscribed by Agile - * @param subs - Initial Subscriptions - * @param config - Config + * @param callback - Callback function to cause a rerender on the Component + * to be represented by the Subscription Container. + * @param subs - Observers to be subscribed to the Subscription Container. + * @param config - Configuration object */ constructor( callback: Function, diff --git a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts index d71c3e2f..b161f70a 100644 --- a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts @@ -4,18 +4,33 @@ import { SubscriptionContainerConfigInterface, } from '../../../internal'; -export class ComponentSubscriptionContainer extends SubscriptionContainer { - public component: any; +export class ComponentSubscriptionContainer< + C = any +> extends SubscriptionContainer { + /** + * Component the Subscription Container represents. + */ + public component: C; /** + * Subscription Container for component based subscriptions. + * + * In a component based subscription, a rerender is triggered on the Component via muting a local + * State Management instance of the Component. + * For example in a React Class Component the `this.state` property. + * + * The Component Subscription Container keeps track of the Component itself, + * in order to synchronize the Component State Management instance with the subscribed Observer values. + * + * [Learn more..](https://agile-ts.org/docs/core/integration#component-based) + * * @internal - * ComponentSubscriptionContainer - SubscriptionContainer for Component based Subscription - * @param component - Component that is subscribed by Agile - * @param subs - Initial Subscriptions - * @param config - Config + * @param component - Component to be represent by the Subscription Container. + * @param subs - Observers to be subscribed to the Subscription Container. + * @param config - Configuration object */ constructor( - component: any, + component: C, subs: Array = [], config: SubscriptionContainerConfigInterface = {} ) { diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index ef7c7bb5..58aa26df 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -1,73 +1,157 @@ import { defineConfig, generateId, - notEqual, + isValidObject, Observer, } from '../../../internal'; export class SubscriptionContainer { + /** + * Key/Name identifier of the Subscription Container. + */ public key?: SubscriptionContainerKeyType; + /** + * Whether the Subscription Container + * and the Component the Subscription Container represents are ready. + * So that the Subscription Container can trigger rerenders on the Component. + */ public ready = false; - public subscribers: Set; // Observers that are Subscribed to this SubscriptionContainer (Component) - - // Represents the paths to the accessed properties of the State/s this SubscriptionContainer represents - public proxyKeyMap: ProxyKeyMapInterface; - public proxyBased = false; + /** + * Observers that have subscribed the Subscription Container. + * + * The Observers use the Subscription Container + * as an interface to the Component the Subscription Container represents + * in order to cause rerenders on the Component. + */ + public subscribers: Set; + /** + * Temporary stores the subscribed Observers, + * that have been updated and are running through the runtime. + */ + public updatedSubscribers: Array = []; - // For Object based Subscription + /** + * Whether the Subscription Container is object based. + * + * A Observer is object based when the subscribed Observers were provided in a Observer key map. + * ``` + * { + * state1: Observer, + * state2: Observer + * } + * ``` + * Thus each Observer has its unique key stored in the 'subscribersWeakMap'. + * + * Often Component based Subscriptions are object based, + * because each Observer requires a unique identifier + * to properly merge the Observer value into the local State Management instance. + */ public isObjectBased = false; - public observerKeysToUpdate: Array = []; // Holds temporary keys of Observers that got updated (Note: keys based on 'subsObject') - public subsObject?: { [key: string]: Observer }; // Same as subs but in Object shape + /** + * Weak map for storing a key identifier for each Observer. + */ + public subscriberKeysWeakMap: WeakMap; + + /** + * Weak map representing Selectors of the Subscription Container. + */ + public selectorsWeakMap: SelectorWeakMapType; /** - * @internal * SubscriptionContainer - Represents Component/(Way to rerender Component) that is subscribed by Observer/s (Agile) * -> Used to cause rerender on Component - * @param subs - Initial Subscriptions - * @param config - Config + * + * + * A Subscription Container is like an interface to the Components. + * + * When a subscribed Observer value mutates a rerender is triggered on the Component + * through the Subscription Container. + * + * @internal + * @param subs - Observers to be subscribed to the Subscription Container. + * @param config - Configuration object */ constructor( subs: Array = [], config: SubscriptionContainerConfigInterface = {} ) { config = defineConfig(config, { - proxyKeyMap: {}, + proxyWeakMap: new WeakMap(), + selectorWeakMap: new WeakMap(), key: generateId(), }); this.subscribers = new Set(subs); this.key = config.key; - this.proxyKeyMap = config.proxyKeyMap as any; - this.proxyBased = notEqual(this.proxyKeyMap, {}); + this.subscriberKeysWeakMap = new WeakMap(); + + // Create for each proxy path a Selector, + // which selects the property at the path + const selectorWeakMap: SelectorWeakMapType = config.selectorWeakMap as any; + + for (const observer of subs) { + const paths = config.proxyWeakMap?.get(observer)?.paths; + + if (paths != null) { + const selectors: SelectorMethodType[] = []; + for (const path of paths) { + selectors.push((value) => { + let _value = value; + for (const branch of path) { + if (!isValidObject(_value, true)) break; + _value = _value[branch]; + } + return _value; + }); + } + selectorWeakMap.set(observer, { selectors }); + } + } + + this.selectorsWeakMap = selectorWeakMap; } } export type SubscriptionContainerKeyType = string | number; -/** - * @param proxyKeyMap - A keymap with a 2 dimensional arrays with paths/routes to particular properties in the State at key. - * The subscriptionContainer will then only rerender the Component, when a property at a given path changes. - * Not anymore if anything in the State object mutates, although it might not even be displayed in the Component. - * For example: - * { - * myState1: {paths: [['data', 'name']]}, - * myState2: {paths: [['car', 'speed']]} - * } - * Now the subscriptionContain will only trigger a rerender on the Component - * if 'data.name' in myState1 or 'car.speed' in myState2 changes. - * If, for instance, 'data.age' in myState1 mutates it won't trigger a rerender, - * since 'data.age' isn't represented in the proxyKeyMap. - * - * These particular paths can be tracked with the ProxyTree. - * https://github.com/agile-ts/agile/tree/master/packages/proxytree - * @param key - Key/Name of Subscription Container - */ export interface SubscriptionContainerConfigInterface { - proxyKeyMap?: ProxyKeyMapInterface; + /** + * Key/Name identifier of Subscription Container + * @default undefined + */ key?: SubscriptionContainerKeyType; + /** + * A keymap with a 2 dimensional arrays representing paths/routes to particular properties in the State at key. + * The subscriptionContainer will then only rerender the Component, when a property at a given path changes. + * Not anymore if anything in the State object mutates, although it might not even be displayed in the Component. + * For example: + * { + * myState1: {paths: [['data', 'name']]}, + * myState2: {paths: [['car', 'speed']]} + * } + * Now the subscriptionContain will only trigger a rerender on the Component + * if 'data.name' in myState1 or 'car.speed' in myState2 changes. + * If, for instance, 'data.age' in myState1 mutates it won't trigger a rerender, + * since 'data.age' isn't represented in the proxyKeyMap. + * + * These particular paths can be tracked with the ProxyTree. + * https://github.com/agile-ts/agile/tree/master/packages/proxytree + * + * @default {} + */ + proxyWeakMap?: ProxyWeakMapType; + /** + * TODO + * @default undefined + */ + selectorWeakMap?: SelectorWeakMapType; } -export interface ProxyKeyMapInterface { - [key: string]: { paths: string[][] }; -} +export type ProxyWeakMapType = WeakMap; + +export type SelectorWeakMapType = WeakMap< + Observer, + { selectors: SelectorMethodType[] } +>; +export type SelectorMethodType = (value: T) => any; diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index fbd9b37c..c66eb1e1 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -61,7 +61,8 @@ export class SubController { // Set SubscriptionContainer to Object based subscriptionContainer.isObjectBased = true; - subscriptionContainer.subsObject = subs; + for (const key in subs) + subscriptionContainer.subscriberKeysWeakMap.set(subs[key], key); // Register subs and build props object for (const key in subs) { diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 0fbe3bb3..1e70c201 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -74,7 +74,7 @@ export class State { * * @public * @param agileInstance - Instance of Agile the State belongs to. - * @param initialValue - Initial value of State. + * @param initialValue - Initial value of the State. * @param config - Configuration object */ constructor( diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index 95781f72..f129fed4 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -236,7 +236,7 @@ describe('Runtime Tests', () => { jest.spyOn(dummyAgile.integrations, 'update'); jest.spyOn(runtime, 'handleObjectBasedSubscription'); - jest.spyOn(runtime, 'handleProxyBasedSubscription'); + jest.spyOn(runtime, 'handleSelectors'); }); it('should return false if agile has no integration', () => { @@ -272,7 +272,7 @@ describe('Runtime Tests', () => { expect(runtime.jobsToRerender).toStrictEqual([]); expect(runtime.notReadyJobsToRerender.size).toBe(0); - expect(runtime.handleProxyBasedSubscription).not.toHaveBeenCalled(); + expect(runtime.handleSelectors).not.toHaveBeenCalled(); expect(dummyAgile.integrations.update).toHaveBeenCalledTimes(1); expect(dummyAgile.integrations.update).toHaveBeenCalledWith( @@ -299,7 +299,7 @@ describe('Runtime Tests', () => { expect(runtime.jobsToRerender).toStrictEqual([]); expect(runtime.notReadyJobsToRerender.size).toBe(0); - expect(runtime.handleProxyBasedSubscription).not.toHaveBeenCalled(); + expect(runtime.handleSelectors).not.toHaveBeenCalled(); expect(rCallbackSubContainer.callback).toHaveBeenCalledTimes(1); expect(rCallbackSubJob.subscriptionContainersToUpdate.size).toBe(0); @@ -309,11 +309,9 @@ describe('Runtime Tests', () => { }); it('should update ready proxy, callback based SubscriptionContainer if handleProxyBasedSubscriptions() returns true', () => { - jest - .spyOn(runtime, 'handleProxyBasedSubscription') - .mockReturnValueOnce(true); + jest.spyOn(runtime, 'handleSelectors').mockReturnValueOnce(true); dummyAgile.hasIntegration = jest.fn(() => true); - rCallbackSubContainer.proxyBased = true; + rCallbackSubContainer.isProxyBased = true; rCallbackSubContainer.proxyKeyMap = dummyProxyKeyMap; runtime.jobsToRerender.push(rCallbackSubJob); @@ -321,7 +319,7 @@ describe('Runtime Tests', () => { expect(runtime.jobsToRerender).toStrictEqual([]); expect(runtime.notReadyJobsToRerender.size).toBe(0); - expect(runtime.handleProxyBasedSubscription).toHaveBeenCalledWith( + expect(runtime.handleSelectors).toHaveBeenCalledWith( rCallbackSubContainer, rCallbackSubJob ); @@ -334,11 +332,9 @@ describe('Runtime Tests', () => { }); it("shouldn't update ready proxy, callback based SubscriptionContainer if handleProxyBasedSubscriptions() returns false", () => { - jest - .spyOn(runtime, 'handleProxyBasedSubscription') - .mockReturnValueOnce(false); + jest.spyOn(runtime, 'handleSelectors').mockReturnValueOnce(false); dummyAgile.hasIntegration = jest.fn(() => true); - rCallbackSubContainer.proxyBased = true; + rCallbackSubContainer.isProxyBased = true; rCallbackSubContainer.proxyKeyMap = dummyProxyKeyMap; runtime.jobsToRerender.push(rCallbackSubJob); @@ -346,7 +342,7 @@ describe('Runtime Tests', () => { expect(runtime.jobsToRerender).toStrictEqual([]); expect(runtime.notReadyJobsToRerender.size).toBe(0); - expect(runtime.handleProxyBasedSubscription).toHaveBeenCalledWith( + expect(runtime.handleSelectors).toHaveBeenCalledWith( rCallbackSubContainer, rCallbackSubJob ); @@ -561,9 +557,7 @@ describe('Runtime Tests', () => { arrayJob ); - expect(arraySubscriptionContainer.observerKeysToUpdate).toStrictEqual( - [] - ); + expect(arraySubscriptionContainer.updatedSubscribers).toStrictEqual([]); }); it('should add Job Observer to changedObjectKeys in SubscriptionContainer', () => { @@ -572,7 +566,7 @@ describe('Runtime Tests', () => { objectJob1 ); - expect(objectSubscriptionContainer.observerKeysToUpdate).toStrictEqual([ + expect(objectSubscriptionContainer.updatedSubscribers).toStrictEqual([ 'observer1', ]); }); @@ -598,18 +592,18 @@ describe('Runtime Tests', () => { }); it('should build Observer Value Object out of observerKeysToUpdate and Value of Observer', () => { - subscriptionContainer.observerKeysToUpdate.push('observer1'); - subscriptionContainer.observerKeysToUpdate.push('observer2'); - subscriptionContainer.observerKeysToUpdate.push('observer3'); + subscriptionContainer.updatedSubscribers.push('observer1'); + subscriptionContainer.updatedSubscribers.push('observer2'); + subscriptionContainer.updatedSubscribers.push('observer3'); - const props = runtime.getObjectBasedProps(subscriptionContainer); + const props = runtime.getUpdatedObserverValues(subscriptionContainer); expect(props).toStrictEqual({ observer1: 'dummyObserverValue1', observer2: undefined, observer3: 'dummyObserverValue3', }); - expect(subscriptionContainer.observerKeysToUpdate).toStrictEqual([]); + expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); }); }); @@ -640,7 +634,7 @@ describe('Runtime Tests', () => { key: 'dummyObserverValue1', data: { name: 'jeff' }, }; - objectSubscriptionContainer.proxyBased = true; + objectSubscriptionContainer.isProxyBased = true; objectSubscriptionContainer.proxyKeyMap = { [dummyObserver1._key || 'unknown']: { paths: [['data', 'name']] }, }; @@ -672,7 +666,7 @@ describe('Runtime Tests', () => { data: { name: 'hans' }, }, ]; - arraySubscriptionContainer.proxyBased = true; + arraySubscriptionContainer.isProxyBased = true; arraySubscriptionContainer.proxyKeyMap = { [dummyObserver2._key || 'unknown']: { paths: [['0', 'data', 'name']], @@ -688,9 +682,9 @@ describe('Runtime Tests', () => { }); it("should return true if subscriptionContainer isn't proxy based", () => { - objectSubscriptionContainer.proxyBased = false; + objectSubscriptionContainer.isProxyBased = false; - const response = runtime.handleProxyBasedSubscription( + const response = runtime.handleSelectors( objectSubscriptionContainer, objectJob ); @@ -702,7 +696,7 @@ describe('Runtime Tests', () => { it('should return true if observer the job represents has no key', () => { objectJob.observer._key = undefined; - const response = runtime.handleProxyBasedSubscription( + const response = runtime.handleSelectors( objectSubscriptionContainer, objectJob ); @@ -716,7 +710,7 @@ describe('Runtime Tests', () => { unknownKey: { paths: [['a', 'b']] }, }; - const response = runtime.handleProxyBasedSubscription( + const response = runtime.handleSelectors( objectSubscriptionContainer, objectJob ); @@ -731,7 +725,7 @@ describe('Runtime Tests', () => { data: { name: 'hans' }, }; - const response = runtime.handleProxyBasedSubscription( + const response = runtime.handleSelectors( objectSubscriptionContainer, objectJob ); @@ -744,7 +738,7 @@ describe('Runtime Tests', () => { }); it("should return false if used property hasn't changed (object value)", () => { - const response = runtime.handleProxyBasedSubscription( + const response = runtime.handleSelectors( objectSubscriptionContainer, objectJob ); @@ -765,7 +759,7 @@ describe('Runtime Tests', () => { data: { name: undefined }, }; - const response = runtime.handleProxyBasedSubscription( + const response = runtime.handleSelectors( objectSubscriptionContainer, objectJob ); @@ -786,7 +780,7 @@ describe('Runtime Tests', () => { }, ]; - const response = runtime.handleProxyBasedSubscription( + const response = runtime.handleSelectors( arraySubscriptionContainer, arrayJob ); @@ -799,7 +793,7 @@ describe('Runtime Tests', () => { }); it("should return false if used property hasn't changed (array value)", () => { - const response = runtime.handleProxyBasedSubscription( + const response = runtime.handleSelectors( arraySubscriptionContainer, arrayJob ); diff --git a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts index 080c0439..80688764 100644 --- a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts @@ -38,11 +38,11 @@ describe('CallbackSubscriptionContainer Tests', () => { expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy(); expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy(); expect(subscriptionContainer.isObjectBased).toBeFalsy(); - expect(subscriptionContainer.observerKeysToUpdate).toStrictEqual([]); - expect(subscriptionContainer.subsObject).toBeUndefined(); + expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); + expect(subscriptionContainer.subscriberKeysWeakMap).toBeUndefined(); expect(subscriptionContainer.proxyKeyMap).toStrictEqual({ myState: { paths: [['hi']] }, }); - expect(subscriptionContainer.proxyBased).toBeTruthy(); + expect(subscriptionContainer.isProxyBased).toBeTruthy(); }); }); diff --git a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts index 04362b53..786c115a 100644 --- a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts @@ -36,11 +36,11 @@ describe('ComponentSubscriptionContainer Tests', () => { expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy(); expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy(); expect(subscriptionContainer.isObjectBased).toBeFalsy(); - expect(subscriptionContainer.observerKeysToUpdate).toStrictEqual([]); - expect(subscriptionContainer.subsObject).toBeUndefined(); + expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); + expect(subscriptionContainer.subscriberKeysWeakMap).toBeUndefined(); expect(subscriptionContainer.proxyKeyMap).toStrictEqual({ myState: { paths: [['hi']] }, }); - expect(subscriptionContainer.proxyBased).toBeTruthy(); + expect(subscriptionContainer.isProxyBased).toBeTruthy(); }); }); diff --git a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts index 3c9c566a..ebc5fa91 100644 --- a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts @@ -25,10 +25,10 @@ describe('SubscriptionContainer Tests', () => { expect(subscriptionContainer.ready).toBeFalsy(); expect(subscriptionContainer.subscribers.size).toBe(0); expect(subscriptionContainer.isObjectBased).toBeFalsy(); - expect(subscriptionContainer.observerKeysToUpdate).toStrictEqual([]); - expect(subscriptionContainer.subsObject).toBeUndefined(); + expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); + expect(subscriptionContainer.subscriberKeysWeakMap).toBeUndefined(); expect(subscriptionContainer.proxyKeyMap).toStrictEqual({}); - expect(subscriptionContainer.proxyBased).toBeFalsy(); + expect(subscriptionContainer.isProxyBased).toBeFalsy(); }); it('should create SubscriptionContainer (specific config)', () => { @@ -43,11 +43,11 @@ describe('SubscriptionContainer Tests', () => { expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy(); expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy(); expect(subscriptionContainer.isObjectBased).toBeFalsy(); - expect(subscriptionContainer.observerKeysToUpdate).toStrictEqual([]); - expect(subscriptionContainer.subsObject).toBeUndefined(); + expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); + expect(subscriptionContainer.subscriberKeysWeakMap).toBeUndefined(); expect(subscriptionContainer.proxyKeyMap).toStrictEqual({ myState: { paths: [['a', 'b']] }, }); - expect(subscriptionContainer.proxyBased).toBeTruthy(); + expect(subscriptionContainer.isProxyBased).toBeTruthy(); }); }); diff --git a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts index 0dee1506..eb1fbc90 100644 --- a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts +++ b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts @@ -84,7 +84,7 @@ describe('SubController Tests', () => { ); expect(dummySubscriptionContainer.isObjectBased).toBeTruthy(); - expect(dummySubscriptionContainer.subsObject).toStrictEqual({ + expect(dummySubscriptionContainer.subscriberKeysWeakMap).toStrictEqual({ dummyObserver1: dummyObserver1, dummyObserver2: dummyObserver2, }); @@ -144,7 +144,9 @@ describe('SubController Tests', () => { ); expect(dummySubscriptionContainer.isObjectBased).toBeFalsy(); - expect(dummySubscriptionContainer.subsObject).toBeUndefined(); + expect( + dummySubscriptionContainer.subscriberKeysWeakMap + ).toBeUndefined(); expect(dummySubscriptionContainer.subscribers.size).toBe(2); expect( @@ -544,7 +546,7 @@ describe('SubController Tests', () => { // Note:This 'issue' happens in multiple parts of the AgileTs test expect(callbackSubscriptionContainer.key).toBe('randomKey'); expect(callbackSubscriptionContainer.proxyKeyMap).toStrictEqual({}); - expect(callbackSubscriptionContainer.proxyBased).toBeFalsy(); + expect(callbackSubscriptionContainer.isProxyBased).toBeFalsy(); expect(callbackSubscriptionContainer.subscribers.size).toBe(2); expect( @@ -584,7 +586,7 @@ describe('SubController Tests', () => { expect(callbackSubscriptionContainer.proxyKeyMap).toStrictEqual({ jeff: { paths: [[]] }, }); - expect(callbackSubscriptionContainer.proxyBased).toBeTruthy(); + expect(callbackSubscriptionContainer.isProxyBased).toBeTruthy(); expect(callbackSubscriptionContainer.subscribers.size).toBe(2); expect( diff --git a/packages/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index 7061ed61..ec4ccc17 100644 --- a/packages/react/src/hooks/useAgile.ts +++ b/packages/react/src/hooks/useAgile.ts @@ -12,6 +12,7 @@ import { isValidObject, ProxyKeyMapInterface, generateId, + ProxyWeakMapType, } from '@agile-ts/core'; import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; import { ProxyTree } from '@agile-ts/proxytree'; @@ -47,7 +48,7 @@ export function useAgile< config: AgileHookConfigInterface = {} ): AgileHookArrayType | AgileHookType { const depsArray = extractObservers(deps); - const proxyTreeMap: ProxyTreeMapInterface = {}; + const proxyWeakMap: ProxyWeakMapType = new WeakMap(); config = defineConfig(config, { proxyBased: false, key: generateId(), @@ -58,23 +59,16 @@ export function useAgile< const getReturnValue = ( depsArray: (Observer | undefined)[] ): AgileHookArrayType | AgileHookType => { - const handleReturn = ( - dep: State | Observer | undefined - ): AgileHookType => { + const handleReturn = (dep: Observer | undefined): AgileHookType => { const value = dep?.value; - const depKey = dep?.key; // If proxyBased and value is object wrap Proxy around it to track used properties if (config.proxyBased && isValidObject(value, true)) { - if (depKey) { - const proxyTree = new ProxyTree(value); - proxyTreeMap[depKey] = proxyTree; - return proxyTree.proxy; - } - Agile.logger.warn( - 'Keep in mind that without a key no Proxy can be wrapped around the dependency value!', - dep - ); + const proxyTree = new ProxyTree(value); + proxyWeakMap.set(dep, { + paths: proxyTree.getUsedRoutes() as any, + }); + return proxyTree.proxy; } return dep?.value; @@ -112,24 +106,13 @@ export function useAgile< (dep): dep is Observer => dep !== undefined ); - // Build Proxy Key Map - const proxyKeyMap: ProxyKeyMapInterface = {}; - if (config.proxyBased) { - for (const proxyTreeKey in proxyTreeMap) { - const proxyTree = proxyTreeMap[proxyTreeKey]; - proxyKeyMap[proxyTreeKey] = { - paths: proxyTree.getUsedRoutes() as any, - }; - } - } - // Create Callback based Subscription const subscriptionContainer = agileInstance.subController.subscribeWithSubsArray( () => { forceRender(); }, observers, - { key: config.key, proxyKeyMap, waitForMount: false } + { key: config.key, proxyWeakMap, waitForMount: false } ); // Unsubscribe Callback based Subscription on Unmount From c5025166f4d54aece5ee05e79118b0f925818131 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 6 Jun 2021 17:37:36 +0200 Subject: [PATCH 29/63] fixed typos --- packages/core/src/runtime/index.ts | 14 +++++--------- .../container/SubscriptionContainer.ts | 4 ++++ packages/react/src/hooks/useAgile.ts | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index 036cc20a..e2c27103 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -158,12 +158,10 @@ export class Runtime { let updateSubscriptionContainer = true; // Handle Selectors - if (subscriptionContainer.hasSelectors) { - updateSubscriptionContainer = this.handleSelectors( - subscriptionContainer, - job - ); - } + updateSubscriptionContainer = this.handleSelectors( + subscriptionContainer, + job + ); if (updateSubscriptionContainer) { subscriptionContainer.updatedSubscribers.push(job.observer); @@ -239,9 +237,7 @@ export class Runtime { // TODO add Selector support for Object based subscriptions const selectors = subscriptionContainer.selectorsWeakMap.get(job.observer) ?.selectors; - - // Return true because in this cases the subscriptionContainer isn't properly proxyBased - if (!subscriptionContainer.hasSelectors || !selectors) return true; + if (!selectors) return true; const previousValue = job.observer.previousValue; const newValue = job.observer.value; diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 58aa26df..0e88f932 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -50,11 +50,15 @@ export class SubscriptionContainer { public isObjectBased = false; /** * Weak map for storing a key identifier for each Observer. + * + * https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap */ public subscriberKeysWeakMap: WeakMap; /** * Weak map representing Selectors of the Subscription Container. + * + * https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap */ public selectorsWeakMap: SelectorWeakMapType; diff --git a/packages/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index ec4ccc17..cb6fe4fc 100644 --- a/packages/react/src/hooks/useAgile.ts +++ b/packages/react/src/hooks/useAgile.ts @@ -10,7 +10,6 @@ import { SubscriptionContainerKeyType, defineConfig, isValidObject, - ProxyKeyMapInterface, generateId, ProxyWeakMapType, } from '@agile-ts/core'; @@ -60,7 +59,8 @@ export function useAgile< depsArray: (Observer | undefined)[] ): AgileHookArrayType | AgileHookType => { const handleReturn = (dep: Observer | undefined): AgileHookType => { - const value = dep?.value; + if (dep == null) return undefined as any; + const value = dep.value; // If proxyBased and value is object wrap Proxy around it to track used properties if (config.proxyBased && isValidObject(value, true)) { @@ -71,7 +71,7 @@ export function useAgile< return proxyTree.proxy; } - return dep?.value; + return dep.value; }; // Handle single dep From 1d44eb93676278357982126034d65f878a993adf Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 6 Jun 2021 18:41:59 +0200 Subject: [PATCH 30/63] fixed some more typos --- packages/core/src/runtime/index.ts | 10 +- .../container/SubscriptionContainer.ts | 95 +++++++++++++------ packages/react/src/hooks/useAgile.ts | 32 +++++-- 3 files changed, 95 insertions(+), 42 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index e2c27103..c9606c47 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -234,20 +234,24 @@ export class Runtime { subscriptionContainer: SubscriptionContainer, job: RuntimeJob ): boolean { - // TODO add Selector support for Object based subscriptions const selectors = subscriptionContainer.selectorsWeakMap.get(job.observer) ?.selectors; + + // If no selector functions found, return true + // because no specific part of the Observer was selected + // -> The Subscription Container should update + // no matter what was updated in the Observer if (!selectors) return true; + // Check if a selected part of Observer value has changed const previousValue = job.observer.previousValue; const newValue = job.observer.value; for (const selector of selectors) { if ( notEqual(selector(newValue), selector(previousValue)) // || newValueDeepness !== previousValueDeepness // Not possible to check - ) { + ) return true; - } } return false; diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 0e88f932..0d26264e 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -13,21 +13,24 @@ export class SubscriptionContainer { /** * Whether the Subscription Container * and the Component the Subscription Container represents are ready. - * So that the Subscription Container can trigger rerenders on the Component. + * + * When both are ready, the Subscription Container is allowed to trigger rerenders on the Component. */ public ready = false; /** * Observers that have subscribed the Subscription Container. * - * The Observers use the Subscription Container + * The subscribed Observers use the Subscription Container * as an interface to the Component the Subscription Container represents * in order to cause rerenders on the Component. + * + * [Learn more..](https://agile-ts.org/docs/core/integration#-subscriptions) */ public subscribers: Set; /** * Temporary stores the subscribed Observers, - * that have been updated and are running through the runtime. + * that were updated and are currently running through the runtime. */ public updatedSubscribers: Array = []; @@ -41,7 +44,7 @@ export class SubscriptionContainer { * state2: Observer * } * ``` - * Thus each Observer has its unique key stored in the 'subscribersWeakMap'. + * Thus each Observer has its 'external' unique key stored in the 'subscribersWeakMap'. * * Often Component based Subscriptions are object based, * because each Observer requires a unique identifier @@ -49,28 +52,30 @@ export class SubscriptionContainer { */ public isObjectBased = false; /** - * Weak map for storing a key identifier for each Observer. + * Weak map for storing a 'external' key identifier for each Observer. * * https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap */ public subscriberKeysWeakMap: WeakMap; /** - * Weak map representing Selectors of the Subscription Container. + * Weak Map storing selector functions for subscribed Observer. + * + * A selector functions allows the partly subscription to the Observer value. + * So only if the selected part changes, the Subscription Container + * rerenders the Component it represents. * * https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap */ public selectorsWeakMap: SelectorWeakMapType; /** - * SubscriptionContainer - Represents Component/(Way to rerender Component) that is subscribed by Observer/s (Agile) - * -> Used to cause rerender on Component + * A Subscription Container is an interface to a UI-Component, + * that can be subscribed by multiple Observers. * - * - * A Subscription Container is like an interface to the Components. - * - * When a subscribed Observer value mutates a rerender is triggered on the Component - * through the Subscription Container. + * These Observers use the Subscription Container + * to trigger a rerender on the Component it represents, + * when their value change. * * @internal * @param subs - Observers to be subscribed to the Subscription Container. @@ -94,9 +99,31 @@ export class SubscriptionContainer { // which selects the property at the path const selectorWeakMap: SelectorWeakMapType = config.selectorWeakMap as any; - for (const observer of subs) { - const paths = config.proxyWeakMap?.get(observer)?.paths; + // Assign selector functions based on the Proxy Weak Map + this.assignProxySelectors( + selectorWeakMap, + config.proxyWeakMap as any, + subs + ); + this.selectorsWeakMap = selectorWeakMap; + } + + /** + * Assigns selector functions created based on the paths of the Proxy Weak Map + * to the Selector Weak Map. + * + * @param selectorWeakMap + * @param proxyWeakMap + * @param subs + */ + public assignProxySelectors( + selectorWeakMap: SelectorWeakMapType, + proxyWeakMap: ProxyWeakMapType, + subs: Array + ): void { + for (const observer of subs) { + const paths = proxyWeakMap.get(observer)?.paths; if (paths != null) { const selectors: SelectorMethodType[] = []; for (const path of paths) { @@ -112,8 +139,6 @@ export class SubscriptionContainer { selectorWeakMap.set(observer, { selectors }); } } - - this.selectorsWeakMap = selectorWeakMap; } } @@ -126,28 +151,36 @@ export interface SubscriptionContainerConfigInterface { */ key?: SubscriptionContainerKeyType; /** - * A keymap with a 2 dimensional arrays representing paths/routes to particular properties in the State at key. - * The subscriptionContainer will then only rerender the Component, when a property at a given path changes. - * Not anymore if anything in the State object mutates, although it might not even be displayed in the Component. + * A Weak Map with a 2 dimensional arrays representing paths/routes + * to particular properties in the Observer. + * + * The Component the Subscription Container represents + * is then only rerendered, when a property at a specified path changes. + * Not anymore if anything in the Observer object mutates, + * although it might not even be displayed in the Component. + * * For example: - * { - * myState1: {paths: [['data', 'name']]}, - * myState2: {paths: [['car', 'speed']]} + * ``` + * WeakMap: { + * Observer1: {paths: [['data', 'name']]}, + * Observer2: {paths: [['car', 'speed']]} * } - * Now the subscriptionContain will only trigger a rerender on the Component - * if 'data.name' in myState1 or 'car.speed' in myState2 changes. - * If, for instance, 'data.age' in myState1 mutates it won't trigger a rerender, - * since 'data.age' isn't represented in the proxyKeyMap. + * ``` + * Now the Subscription Container will only trigger a rerender on the Component + * if 'data.name' in Observer1 or 'car.speed' in Observer2 changes. + * If, for instance, 'data.age' in Observer1 mutates it won't trigger a rerender, + * since 'data.age' isn't represented in the Proxy Weak Map. * - * These particular paths can be tracked with the ProxyTree. + * These particular paths were tracked via the ProxyTree. * https://github.com/agile-ts/agile/tree/master/packages/proxytree * - * @default {} + * @default new WeakMap() */ proxyWeakMap?: ProxyWeakMapType; /** - * TODO - * @default undefined + * A Weak Map with an array of selector functions for the Observer + * + * @default new WeakMap() */ selectorWeakMap?: SelectorWeakMapType; } diff --git a/packages/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index cb6fe4fc..f3fe5e03 100644 --- a/packages/react/src/hooks/useAgile.ts +++ b/packages/react/src/hooks/useAgile.ts @@ -47,14 +47,14 @@ export function useAgile< config: AgileHookConfigInterface = {} ): AgileHookArrayType | AgileHookType { const depsArray = extractObservers(deps); - const proxyWeakMap: ProxyWeakMapType = new WeakMap(); + const proxyTreeWeakMap = new WeakMap(); config = defineConfig(config, { proxyBased: false, key: generateId(), agileInstance: null, }); - // Creates Return Value of Hook, depending if deps are in Array shape or not + // Creates Return Value of Hook, depending whether deps are in Array shape or not const getReturnValue = ( depsArray: (Observer | undefined)[] ): AgileHookArrayType | AgileHookType => { @@ -62,16 +62,15 @@ export function useAgile< if (dep == null) return undefined as any; const value = dep.value; - // If proxyBased and value is object wrap Proxy around it to track used properties + // If proxyBased and value is of type object. + // Wrap a Proxy around the object to track the used properties if (config.proxyBased && isValidObject(value, true)) { const proxyTree = new ProxyTree(value); - proxyWeakMap.set(dep, { - paths: proxyTree.getUsedRoutes() as any, - }); + proxyTreeWeakMap.set(dep, proxyTree); return proxyTree.proxy; } - return dep.value; + return value; }; // Handle single dep @@ -79,7 +78,7 @@ export function useAgile< return handleReturn(depsArray[0]); } - // Handle dep array + // Handle deps array return depsArray.map((dep) => { return handleReturn(dep); }) as AgileHookArrayType; @@ -106,6 +105,23 @@ export function useAgile< (dep): dep is Observer => dep !== undefined ); + // Build Proxy Path WeakMap Map based on the Proxy Tree WeakMap + // by extracting the routes of the Tree + // Building the Path WeakMap in the 'useIsomorphicLayoutEffect' + // because the 'useIsomorphicLayoutEffect' is called after the rerender + // -> In the Component used paths got successfully tracked + const proxyWeakMap: ProxyWeakMapType = new WeakMap(); + if (config.proxyBased) { + for (const observer of observers) { + const proxyTree = proxyTreeWeakMap.get(observer); + if (proxyTree != null) { + proxyWeakMap.set(observer, { + paths: proxyTree.getUsedRoutes() as any, + }); + } + } + } + // Create Callback based Subscription const subscriptionContainer = agileInstance.subController.subscribeWithSubsArray( () => { From aa98046df37d2f199600b45aee50179b4d8107f2 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 6 Jun 2021 20:25:17 +0200 Subject: [PATCH 31/63] fixed typos --- packages/core/src/runtime/index.ts | 129 +++++++++++------- .../container/SubscriptionContainer.ts | 14 +- 2 files changed, 90 insertions(+), 53 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index c9606c47..6565ab04 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -6,75 +6,93 @@ import { ComponentSubscriptionContainer, defineConfig, notEqual, - isValidObject, LogCodeManager, } from '../internal'; export class Runtime { + // Agile Instance the Runtime belongs to public agileInstance: () => Agile; - // Queue system + // Job that is currently performed public currentJob: RuntimeJob | null = null; + // Jobs to perform public jobQueue: Array = []; - public notReadyJobsToRerender: Set = new Set(); // Jobs that got performed but aren't ready to get rerendered (wait for mount) - public jobsToRerender: Array = []; // Jobs that are performed and will be rendered + + // Jobs that were performed and are ready to rerender + public jobsToRerender: Array = []; + // Jobs that were performed and should rerender + // but the Subscription Container isn't ready to rerender it yet + // For example if the UI-Component isn't mounted yet. + public notReadyJobsToRerender: Set = new Set(); + + // Whether Jobs are currently performed + public isPerformingJobs = false; /** + * The Runtime queues and performs ingested Observer change Jobs. + * + * It prevents race conditions and combines Job Subscription Container rerenders. + * * @internal - * Runtime - Performs ingested Observers - * @param agileInstance - An instance of Agile + * @param agileInstance - Instance of Agile the Runtime belongs to. */ constructor(agileInstance: Agile) { this.agileInstance = () => agileInstance; } - //========================================================================================================= - // Ingest - //========================================================================================================= /** + * Adds the specified Job to the Job queue, + * where it will be performed when it is its turn. + * * @internal - * Ingests Job into Runtime that gets performed - * @param job - Job - * @param config - Config + * @param job - Job to be performed. + * @param config - Configuration object */ public ingest(job: RuntimeJob, config: IngestConfigInterface = {}): void { config = defineConfig(config, { - perform: true, + perform: !this.isPerformingJobs, }); + // Add specified Job to the queue this.jobQueue.push(job); Agile.logger.if .tag(['runtime']) .info(LogCodeManager.getLog('16:01:00', [job._key]), job); - // Perform Job + // Run first Job from the queue if (config.perform) { const performJob = this.jobQueue.shift(); if (performJob) this.perform(performJob); } } - //========================================================================================================= - // Perform - //========================================================================================================= /** + * Performs the specified Job + * and adds it to the rerender queue if necessary. + * + * After the execution it checks if there is still a Job in the queue. + * If so, the next Job in the queue is performed. + * If not, the `jobsToRerender` queue will be started to work off. + * * @internal - * Performs Job and adds it to the rerender queue if necessary - * @param job - Job that gets performed + * @param job - Job to be performed. */ public perform(job: RuntimeJob): void { + this.isPerformingJobs = true; this.currentJob = job; // Perform Job job.observer.perform(job); job.performed = true; - // Ingest Dependents of Observer into Runtime + // Ingest dependents of the Observer into runtime, + // since they depend on the Observer and might have been changed job.observer.dependents.forEach((observer) => observer.ingest({ perform: false }) ); + // Add Job to rerender queue and reset current Job property if (job.rerender) this.jobsToRerender.push(job); this.currentJob = null; @@ -82,11 +100,13 @@ export class Runtime { .tag(['runtime']) .info(LogCodeManager.getLog('16:01:01', [job._key]), job); - // Perform Jobs as long as Jobs are left in queue, if no job left update/rerender Subscribers of jobsToRerender + // Perform Jobs as long as Jobs are left in the queue + // If no job left start updating/rerendering Subscribers of jobsToRerender if (this.jobQueue.length > 0) { const performJob = this.jobQueue.shift(); if (performJob) this.perform(performJob); } else { + this.isPerformingJobs = false; if (this.jobsToRerender.length > 0) { // https://stackoverflow.com/questions/9083594/call-settimeout-without-delay setTimeout(() => { @@ -96,13 +116,13 @@ export class Runtime { } } - //========================================================================================================= - // Update Subscribers - //========================================================================================================= /** + * Executes the `jobsToRerender` queue + * and updates (causes rerender on) the Subscription Container (subscribed Component) + * of each Job Observer. + * * @internal - * Updates/Rerenders all Subscribed Components (SubscriptionContainer) of the Job (Observer) - * @return If any subscriptionContainer got updated (-> triggered a rerender on the Component it represents) + * @return A boolean indicating whether any Subscription Container was updated. */ public updateSubscribers(): boolean { if (!this.agileInstance().hasIntegration()) { @@ -116,20 +136,20 @@ export class Runtime { ) return false; - // Subscriptions that has to be updated/rerendered - // A Set() to combine several equal SubscriptionContainers into one (optimizes rerender) - // (Even better would be to combine SubscriptionContainer based on the Component, - // since a Component can have multiple SubscriptionContainers) + // Subscription Containers that have to be updated (perform rerender on Component it represents). + // Using a 'Set()' to combine several equal SubscriptionContainers into one (rerender optimisation). const subscriptionsToUpdate = new Set(); - // Build final jobsToRerender array based on new jobsToRerender and not ready jobsToRerender + // Build final 'jobsToRerender' array + // based on the new 'jobsToRerender' array and the 'notReadyJobsToRerender' array. const jobsToRerender = this.jobsToRerender.concat( Array.from(this.notReadyJobsToRerender) ); this.notReadyJobsToRerender = new Set(); this.jobsToRerender = []; - // Check if Job SubscriptionContainers should be updated and if so add them to the subscriptionsToUpdate array + // Check if Job Subscription Container of Jobs should be updated + // and if so add it to the 'subscriptionsToUpdate' array jobsToRerender.forEach((job) => { job.subscriptionContainersToUpdate.forEach((subscriptionContainer) => { if (!subscriptionContainer.ready) { @@ -155,7 +175,7 @@ export class Runtime { return; } - let updateSubscriptionContainer = true; + let updateSubscriptionContainer; // Handle Selectors updateSubscriptionContainer = this.handleSelectors( @@ -163,6 +183,15 @@ export class Runtime { job ); + // Check if Subscription Container with same componentId is already in the 'subscriptionToUpdate' queue + // (rerender optimisation) + updateSubscriptionContainer = + updateSubscriptionContainer && + Array.from(subscriptionsToUpdate).findIndex( + (sc) => sc.componentId === subscriptionContainer.componentId + ) === -1; + + // Add Subscription Container to the 'subscriptionsToUpdate' queue if (updateSubscriptionContainer) { subscriptionContainer.updatedSubscribers.push(job.observer); subscriptionsToUpdate.add(subscriptionContainer); @@ -198,37 +227,34 @@ export class Runtime { } /** - * Returns a key map with Observer values that have been updated. + * Maps the values of the updated Observers into a key map. * * @internal - * @param subscriptionContainer - Object based SubscriptionContainer + * @param subscriptionContainer - Subscription Container from which the 'updatedSubscribers' are to be mapped to a key map. */ public getUpdatedObserverValues( subscriptionContainer: SubscriptionContainer ): { [key: string]: any } { const props: { [key: string]: any } = {}; - // Map 'Observer To Update' values into the props object + // Map updated Observer values into the props key map for (const observer of subscriptionContainer.updatedSubscribers) { - const key = subscriptionContainer.subscriberKeysWeakMap.get(observer); + const key = + subscriptionContainer.subscriberKeysWeakMap.get(observer) ?? + subscriptionContainer.key; if (key != null) props[key] = observer.value; } return props; } - //========================================================================================================= - // Handle Selectors - //========================================================================================================= /** - * @internal - * Checks if the subscriptionContainer should be updated. + * Returns a boolean indicating whether the Subscription Container can be updated or not. * Therefore it reviews the '.value' and the '.previousValue' property of the Observer the Job represents. - * If a property at the proxy detected path differs, the subscriptionContainer is allowed to update. - * @param subscriptionContainer - SubscriptionContainer - * @param job - Job - * @return {boolean} If the subscriptionContainer should be updated - * -> If a from the Proxy Tree detected property differs from the same property in the previous value - * or the passed subscriptionContainer isn't properly proxy based + * If a selected property differs, the Subscription Container is allowed to update/rerender. + * + * @internal + * @param subscriptionContainer - Subscription Container to be checked if it can update. + * @param job - Job the Subscription Container belongs to. */ public handleSelectors( subscriptionContainer: SubscriptionContainer, @@ -258,9 +284,10 @@ export class Runtime { } } -/** - * @param perform - If Job gets performed immediately - */ export interface IngestConfigInterface { + /** + * Whether the ingested Job should be performed immediately + * or added to the queue first and then executed when it is his turn. + */ perform?: boolean; } diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 0d26264e..33f80747 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -17,6 +17,7 @@ export class SubscriptionContainer { * When both are ready, the Subscription Container is allowed to trigger rerenders on the Component. */ public ready = false; + public componentId?: ComponentIdType; /** * Observers that have subscribed the Subscription Container. @@ -30,7 +31,8 @@ export class SubscriptionContainer { public subscribers: Set; /** * Temporary stores the subscribed Observers, - * that were updated and are currently running through the runtime. + * that were performed by the runtime + * and are currently running through the update Subscription Container process. */ public updatedSubscribers: Array = []; @@ -93,6 +95,7 @@ export class SubscriptionContainer { this.subscribers = new Set(subs); this.key = config.key; + this.componentId = config?.componentId; this.subscriberKeysWeakMap = new WeakMap(); // Create for each proxy path a Selector, @@ -146,10 +149,15 @@ export type SubscriptionContainerKeyType = string | number; export interface SubscriptionContainerConfigInterface { /** - * Key/Name identifier of Subscription Container + * Key/Name identifier of the Subscription Container * @default undefined */ key?: SubscriptionContainerKeyType; + /** + * Key/Name identifier of the Component the Subscription Container represents. + * @default undefined + */ + componentId?: ComponentIdType; /** * A Weak Map with a 2 dimensional arrays representing paths/routes * to particular properties in the Observer. @@ -192,3 +200,5 @@ export type SelectorWeakMapType = WeakMap< { selectors: SelectorMethodType[] } >; export type SelectorMethodType = (value: T) => any; + +export type ComponentIdType = string | number; From 91c6ae33aca74a22bfcf85c3764c84bafe3082d5 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 7 Jun 2021 08:01:32 +0200 Subject: [PATCH 32/63] fixed typos --- packages/core/src/runtime/index.ts | 7 +-- .../CallbackSubscriptionContainer.ts | 8 +-- .../ComponentSubscriptionContainer.ts | 22 ++++++-- .../container/SubscriptionContainer.ts | 41 +++++++++------ .../runtime/subscription/sub.controller.ts | 50 +++++++++++++------ 5 files changed, 86 insertions(+), 42 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index 6565ab04..e4db26b2 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -227,7 +227,7 @@ export class Runtime { } /** - * Maps the values of the updated Observers into a key map. + * Maps the values of updated Observers (updatedSubscribers) into a key map. * * @internal * @param subscriptionContainer - Subscription Container from which the 'updatedSubscribers' are to be mapped to a key map. @@ -249,8 +249,9 @@ export class Runtime { /** * Returns a boolean indicating whether the Subscription Container can be updated or not. - * Therefore it reviews the '.value' and the '.previousValue' property of the Observer the Job represents. - * If a selected property differs, the Subscription Container is allowed to update/rerender. + * + * Therefore it reviews the '.value' and the '.previousValue' property of the Observer represented by the Job. + * If a selected property differs, the Subscription Container is allowed to update/rerender (returns true). * * @internal * @param subscriptionContainer - Subscription Container to be checked if it can update. diff --git a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts index 6b3339b5..eb3b0913 100644 --- a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts @@ -7,17 +7,19 @@ import { export class CallbackSubscriptionContainer extends SubscriptionContainer { /** * Callback function to trigger a rerender - * on the Component the Subscription Container represents. + * on the Component represented by the Subscription Container. */ public callback: Function; /** * Subscription Container for callback based subscriptions. * - * In a callback based subscription, a rerender is triggered on the Component via a specified callback function. + * In a callback based subscription, a rerender is triggered on the Component + * using a specified callback function. * * The Callback Subscription Container doesn't keep track of the Component itself. - * It only knows how to trigger a rerender on the particular Component through the callback function. + * It only knows how to trigger a rerender + * on the particular Component through the callback function. * * [Learn more..](https://agile-ts.org/docs/core/integration#callback-based) * diff --git a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts index b161f70a..81789168 100644 --- a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts @@ -8,19 +8,33 @@ export class ComponentSubscriptionContainer< C = any > extends SubscriptionContainer { /** - * Component the Subscription Container represents. + * Component the Subscription Container represents + * and mutates to cause rerender on it. */ public component: C; /** * Subscription Container for component based subscriptions. * - * In a component based subscription, a rerender is triggered on the Component via muting a local - * State Management instance of the Component. + * In a component based subscription, a rerender is triggered on the Component + * by muting a local State Management instance/property of the Component. * For example in a React Class Component the `this.state` property. * * The Component Subscription Container keeps track of the Component itself, - * in order to synchronize the Component State Management instance with the subscribed Observer values. + * to synchronize the Component State Management instance with the subscribed Observer values. + * + * For this to work well, a component subscription is often object based + * so that each observer has a uniq key. + * ``` + * // Object based (guaranteed unique key) + * { + * state1: Observer, + * state2: Observer + * } + * + * // Array based (no guaranteed unique key) + * [Observer, Observer] + * ``` * * [Learn more..](https://agile-ts.org/docs/core/integration#component-based) * diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 33f80747..8c18ff08 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -14,17 +14,22 @@ export class SubscriptionContainer { * Whether the Subscription Container * and the Component the Subscription Container represents are ready. * - * When both are ready, the Subscription Container is allowed to trigger rerenders on the Component. + * When both are ready, the Subscription Container is allowed + * to trigger rerenders on the Component based on its type. (Component or Callback based) */ public ready = false; + /** + * Id of the Component the Subscription Container represents. + */ public componentId?: ComponentIdType; /** * Observers that have subscribed the Subscription Container. * * The subscribed Observers use the Subscription Container - * as an interface to the Component the Subscription Container represents - * in order to cause rerenders on the Component. + * as an interface to the Component it represents. + * Through the Subscription Container, they can then trigger rerenders + * on the Component when their value changes. * * [Learn more..](https://agile-ts.org/docs/core/integration#-subscriptions) */ @@ -33,13 +38,16 @@ export class SubscriptionContainer { * Temporary stores the subscribed Observers, * that were performed by the runtime * and are currently running through the update Subscription Container process. + * + * This is used for example, to merge the changed Observer values + * into the Component's local State Management instance for a Component based Subscription. */ public updatedSubscribers: Array = []; /** * Whether the Subscription Container is object based. * - * A Observer is object based when the subscribed Observers were provided in a Observer key map. + * A Observer is object based when the subscribed Observers were provided in an Observer key map. * ``` * { * state1: Observer, @@ -50,22 +58,22 @@ export class SubscriptionContainer { * * Often Component based Subscriptions are object based, * because each Observer requires a unique identifier - * to properly merge the Observer value into the local State Management instance. + * to properly merge the Observer value into the Component's local State Management instance. */ public isObjectBased = false; /** - * Weak map for storing a 'external' key identifier for each Observer. + * Weak map for storing 'external' key identifiers for Observer. * * https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap */ public subscriberKeysWeakMap: WeakMap; /** - * Weak Map storing selector functions for subscribed Observer. + * Weak Map for storing selector functions of subscribed Observer. * - * A selector functions allows the partly subscription to the Observer value. - * So only if the selected part changes, the Subscription Container - * rerenders the Component it represents. + * A selector functions allows the partly subscription to an Observer value. + * Only if the selected Observe value part changes, + * the Subscription Container rerenders the Component it represents. * * https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap */ @@ -98,17 +106,14 @@ export class SubscriptionContainer { this.componentId = config?.componentId; this.subscriberKeysWeakMap = new WeakMap(); - // Create for each proxy path a Selector, - // which selects the property at the path + // Create for each specified proxy path a selector function, + // which selects the property at the path end const selectorWeakMap: SelectorWeakMapType = config.selectorWeakMap as any; - - // Assign selector functions based on the Proxy Weak Map this.assignProxySelectors( selectorWeakMap, config.proxyWeakMap as any, subs ); - this.selectorsWeakMap = selectorWeakMap; } @@ -186,7 +191,11 @@ export interface SubscriptionContainerConfigInterface { */ proxyWeakMap?: ProxyWeakMapType; /** - * A Weak Map with an array of selector functions for the Observer + * A Weak Map with an array of selector functions for Observers. + * + * A selector functions allows the partly subscription to an Observer value. + * Only if the selected Observe value part changes, + * the Subscription Container rerenders the Component it represents. * * @default new WeakMap() */ diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index c66eb1e1..121b3d02 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -12,31 +12,48 @@ import { } from '../../internal'; export class SubController { + // Agile Instance the Runtime belongs to public agileInstance: () => Agile; - public componentSubs: Set = new Set(); // Holds all registered Component based Subscriptions - public callbackSubs: Set = new Set(); // Holds all registered Callback based Subscriptions + // Represents all registered Component based Subscriptions + public componentSubs: Set = new Set(); + // Represents all registered Callback based Subscriptions + public callbackSubs: Set = new Set(); - public mountedComponents: Set = new Set(); // Holds all mounted Components (only if agileInstance.config.mount = true) + // Keeps track of all mounted Components (only if agileInstance.config.mount = true) + public mountedComponents: Set = new Set(); /** + * Manages the subscription to UI-Components. + * * @internal - * SubController - Handles subscriptions to Components - * @param agileInstance - An instance of Agile + * @param agileInstance - Instance of Agile the Subscription Container belongs to. */ public constructor(agileInstance: Agile) { this.agileInstance = () => agileInstance; } - //========================================================================================================= - // Subscribe with Subs Object - //========================================================================================================= /** + * Subscribes the in an object specified Observers to a Component represented by the 'integrationInstance'. + * Such subscription ensures that the Observer is able to trigger rerenders on the Component + * for example if its value changes. + * + * There are two ways of causing a rerender through the 'integrationInstance' on the Component. + * - 1. Via a callback function which triggers a rerender + * on the Component when it is called. (Callback based Subscription) + * [Learn more..](https://agile-ts.org/docs/core/integration/#callback-based) + * - 2. Via the Component itself. + * For example by mutating the local State Management property + * of the Component. (Component based Subscription) + * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) + * + * The Component (way of rerendering the Component) is then represented by a created Subscription Container + * that is added to the Observer and serves like an interface to the Component. + * * @internal - * Subscribe with Object shaped Subscriptions - * @param integrationInstance - Callback Function or Component - * @param subs - Initial Subscription Object - * @param config - Config + * @param integrationInstance - Callback function or Component Instance for triggering a rerender on a UI-Component. + * @param subs - Observers to be subscribed to the Subscription Container in object shape. + * @param config - Configuration object */ public subscribeWithSubsObject( integrationInstance: any, @@ -81,11 +98,12 @@ export class SubController { // Subscribe with Subs Array //========================================================================================================= /** - * @internal * Subscribe with Array shaped Subscriptions - * @param integrationInstance - Callback Function or Component - * @param subs - Initial Subscription Array - * @param config - Config + * + * @internal + * @param integrationInstance - Callback function or Component Instance for triggering a rerender on a UI-Component. + * @param subs - Observers to be subscribed to the Subscription Container in array shape. + * @param config - Configuration object */ public subscribeWithSubsArray( integrationInstance: any, From bd95ed8d6dde90920edfc24c94f8cdbdab254516 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Mon, 7 Jun 2021 17:59:33 +0200 Subject: [PATCH 33/63] fixed typos --- .../runtime/subscription/sub.controller.ts | 84 +++++++++++-------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index 121b3d02..589b35b5 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -34,21 +34,25 @@ export class SubController { } /** - * Subscribes the in an object specified Observers to a Component represented by the 'integrationInstance'. - * Such subscription ensures that the Observer is able to trigger rerenders on the Component - * for example if its value changes. + * Creates a so called Subscription Container which represents an UI-Component in AgileTs. + * Such Subscription Container know how to trigger a rerender on the UI-Component it represents + * through the provided 'integrationInstance'. * - * There are two ways of causing a rerender through the 'integrationInstance' on the Component. - * - 1. Via a callback function which triggers a rerender - * on the Component when it is called. (Callback based Subscription) + * There exist two different ways on how the Subscription Container can cause a rerender on the Component. + * - 1. Via a callback function that triggers a rerender on the Component. (Callback based Subscription) * [Learn more..](https://agile-ts.org/docs/core/integration/#callback-based) - * - 2. Via the Component itself. - * For example by mutating the local State Management property - * of the Component. (Component based Subscription) + * - 2. Via the Component instance itself. + * For example by mutating a local State Management property + * of the Component Instance. (Component based Subscription) * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) * - * The Component (way of rerendering the Component) is then represented by a created Subscription Container - * that is added to the Observer and serves like an interface to the Component. + * The in an object specified Observers are then automatically subscribed + * to the created Subscription Container and thus to the Component the Subscription Container represents. + * + * The advantage of subscribing the Observer via a object keymap, + * is that each Observer has its own unique key identifier. + * Such key can for example required when merging the Observer value at key into + * a local Component State Management property. * * @internal * @param integrationInstance - Callback function or Component Instance for triggering a rerender on a UI-Component. @@ -69,19 +73,21 @@ export class SubController { const subsArray: Observer[] = []; for (const key in subs) subsArray.push(subs[key]); - // Register Subscription -> decide weather subscriptionInstance is callback or component based + // Create a rerender interface to Component + // via the specified 'integrationInstance' (Subscription Container) const subscriptionContainer = this.registerSubscription( integrationInstance, subsArray, config ); - // Set SubscriptionContainer to Object based + // Set SubscriptionContainer to object based + // and assign property keys to the 'subscriberKeysWeakMap' subscriptionContainer.isObjectBased = true; for (const key in subs) subscriptionContainer.subscriberKeysWeakMap.set(subs[key], key); - // Register subs and build props object + // Subscribe Observer to the created Subscription Container and build props object for (const key in subs) { const observer = subs[key]; observer.subscribe(subscriptionContainer); @@ -94,13 +100,23 @@ export class SubController { }; } - //========================================================================================================= - // Subscribe with Subs Array - //========================================================================================================= /** - * Subscribe with Array shaped Subscriptions + * Creates a so called Subscription Container which represents an UI-Component in AgileTs. + * Such Subscription Container know how to trigger a rerender on the UI-Component it represents + * through the provided 'integrationInstance'. + * + * There exist two different ways on how the Subscription Container can cause a rerender on the Component. + * - 1. Via a callback function that triggers a rerender on the Component. (Callback based Subscription) + * [Learn more..](https://agile-ts.org/docs/core/integration/#callback-based) + * - 2. Via the Component instance itself. + * For example by mutating a local State Management property + * of the Component Instance. (Component based Subscription) + * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) * - * @internal + * The in an array specified Observers are then automatically subscribed + * to the created Subscription Container and thus to the Component the Subscription Container represents. + * + * @internal * @param integrationInstance - Callback function or Component Instance for triggering a rerender on a UI-Component. * @param subs - Observers to be subscribed to the Subscription Container in array shape. * @param config - Configuration object @@ -110,26 +126,28 @@ export class SubController { subs: Array = [], config: RegisterSubscriptionConfigInterface = {} ): SubscriptionContainer { - // Register Subscription -> decide weather subscriptionInstance is callback or component based + // Create a rerender interface to Component + // via the specified 'integrationInstance' (Subscription Container) const subscriptionContainer = this.registerSubscription( integrationInstance, subs, config ); - // Register subs + // Subscribe Observer to the created Subscription Container subs.forEach((observer) => observer.subscribe(subscriptionContainer)); return subscriptionContainer; } - //========================================================================================================= - // Unsubscribe - //========================================================================================================= /** + * Unsubscribe the from the specified 'subscriptionInstance' + * extracted SubscriptionContainer from all Observers that + * are subscribed to it. + * * @internal - * Unsubscribes SubscriptionContainer(Component) - * @param subscriptionInstance - SubscriptionContainer or Component that holds an SubscriptionContainer + * @param subscriptionInstance - Subscription Container + * or an UI-Component holding a instance of a Subscription Container */ public unsubscribe(subscriptionInstance: any) { // Helper function to remove SubscriptionContainer from Observer @@ -216,17 +234,9 @@ export class SubController { config = defineConfig(config, { waitForMount: this.agileInstance().config.waitForMount, }); - if (isFunction(integrationInstance)) - return this.registerCallbackSubscription( - integrationInstance, - subs, - config - ); - return this.registerComponentSubscription( - integrationInstance, - subs, - config - ); + return isFunction(integrationInstance) + ? this.registerCallbackSubscription(integrationInstance, subs, config) + : this.registerComponentSubscription(integrationInstance, subs, config); } //========================================================================================================= From f458fd1e5eeffb5ef12973f24dd9380f718c388e Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 7 Jun 2021 20:28:42 +0200 Subject: [PATCH 34/63] fixed typos --- packages/core/src/runtime/index.ts | 45 +++-- .../CallbackSubscriptionContainer.ts | 9 +- .../ComponentSubscriptionContainer.ts | 23 ++- .../container/SubscriptionContainer.ts | 20 +- .../runtime/subscription/sub.controller.ts | 187 ++++++++---------- .../subscription/sub.controller.test.ts | 62 +++--- packages/react/src/hooks/useAgile.ts | 9 +- 7 files changed, 181 insertions(+), 174 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index e4db26b2..f6267a0a 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -13,25 +13,34 @@ export class Runtime { // Agile Instance the Runtime belongs to public agileInstance: () => Agile; - // Job that is currently performed + // Job that is currently being performed public currentJob: RuntimeJob | null = null; - // Jobs to perform + // Jobs to be performed public jobQueue: Array = []; // Jobs that were performed and are ready to rerender public jobsToRerender: Array = []; - // Jobs that were performed and should rerender - // but the Subscription Container isn't ready to rerender it yet - // For example if the UI-Component isn't mounted yet. + // Jobs that were performed and should be rerendered. + // However their Subscription Container isn't ready to rerender yet. + // For example when the UI-Component isn't mounted yet. public notReadyJobsToRerender: Set = new Set(); - // Whether Jobs are currently performed + // Whether the job queue is currently being actively processed public isPerformingJobs = false; /** - * The Runtime queues and performs ingested Observer change Jobs. + * The Runtime executes and queues ingested Observer based Jobs + * to prevent race conditions and optimized rerenders of subscribed Components. * - * It prevents race conditions and combines Job Subscription Container rerenders. + * Each provided Job will be executed when it is its turn + * by calling the Job Observer's 'perform()' method. + * + * After a successful execution the Job is added to a rerender queue, + * which is firstly put into the browser's 'Bucket' and executed when resources are left. + * + * The rerender queue is designed for optimizing the render count + * by combining rerender Jobs of the same Component + * and ingoing rerender requests for unmounted Components. * * @internal * @param agileInstance - Instance of Agile the Runtime belongs to. @@ -41,7 +50,7 @@ export class Runtime { } /** - * Adds the specified Job to the Job queue, + * Adds the specified Observer based Job to the internal Job queue, * where it will be performed when it is its turn. * * @internal @@ -71,9 +80,9 @@ export class Runtime { * Performs the specified Job * and adds it to the rerender queue if necessary. * - * After the execution it checks if there is still a Job in the queue. - * If so, the next Job in the queue is performed. - * If not, the `jobsToRerender` queue will be started to work off. + * After the execution of the Job it checks if there are still Jobs left in the queue. + * - If so, the next Job in the queue is performed. + * - If not, the `jobsToRerender` queue will be started to work off. * * @internal * @param job - Job to be performed. @@ -87,7 +96,7 @@ export class Runtime { job.performed = true; // Ingest dependents of the Observer into runtime, - // since they depend on the Observer and might have been changed + // since they depend on the Observer and have properly changed too job.observer.dependents.forEach((observer) => observer.ingest({ perform: false }) ); @@ -100,8 +109,9 @@ export class Runtime { .tag(['runtime']) .info(LogCodeManager.getLog('16:01:01', [job._key]), job); - // Perform Jobs as long as Jobs are left in the queue - // If no job left start updating/rerendering Subscribers of jobsToRerender + // Perform Jobs as long as Jobs are left in the queue. + // If no Job is left start updating/rerendering Subscribers + // of the Job based on the 'jobsToRerender' queue. if (this.jobQueue.length > 0) { const performJob = this.jobQueue.shift(); if (performJob) this.perform(performJob); @@ -121,8 +131,9 @@ export class Runtime { * and updates (causes rerender on) the Subscription Container (subscribed Component) * of each Job Observer. * + * It returns a boolean indicating whether any Subscription Container was updated. + * * @internal - * @return A boolean indicating whether any Subscription Container was updated. */ public updateSubscribers(): boolean { if (!this.agileInstance().hasIntegration()) { @@ -229,6 +240,8 @@ export class Runtime { /** * Maps the values of updated Observers (updatedSubscribers) into a key map. * + * The key is extracted from the Observer itself or from the Subscription Containers 'subscriberKeysWeakMap'. + * * @internal * @param subscriptionContainer - Subscription Container from which the 'updatedSubscribers' are to be mapped to a key map. */ diff --git a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts index eb3b0913..aad3736b 100644 --- a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts @@ -12,14 +12,11 @@ export class CallbackSubscriptionContainer extends SubscriptionContainer { public callback: Function; /** - * Subscription Container for callback based subscriptions. - * - * In a callback based subscription, a rerender is triggered on the Component - * using a specified callback function. + * A Callback Subscription Container represents a UI-Component in AgileTs + * and triggers a rerender on the UI-Component via a specified callback function. * * The Callback Subscription Container doesn't keep track of the Component itself. - * It only knows how to trigger a rerender - * on the particular Component through the callback function. + * It only knows how to trigger a rerender on it via the callback function. * * [Learn more..](https://agile-ts.org/docs/core/integration#callback-based) * diff --git a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts index 81789168..1d10b209 100644 --- a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts @@ -14,17 +14,16 @@ export class ComponentSubscriptionContainer< public component: C; /** - * Subscription Container for component based subscriptions. - * - * In a component based subscription, a rerender is triggered on the Component - * by muting a local State Management instance/property of the Component. - * For example in a React Class Component the `this.state` property. + * A Component Subscription Container represents a UI-Component in AgileTs + * and triggers a rerender on the UI-Component by muting the specified Component Instance. + * For example by updating a local State Management property of the Component. + * (like in a React Class Components the `this.state` property) * * The Component Subscription Container keeps track of the Component itself, - * to synchronize the Component State Management instance with the subscribed Observer values. + * to mutate it accordingly so that a rerender is triggered. * - * For this to work well, a component subscription is often object based - * so that each observer has a uniq key. + * For this to work well, a Component Subscription Container is often object based. + * Meaning that each Observer was provided in a object keymap with a unique key identifier. * ``` * // Object based (guaranteed unique key) * { @@ -35,6 +34,14 @@ export class ComponentSubscriptionContainer< * // Array based (no guaranteed unique key) * [Observer, Observer] * ``` + * Thus the Integrations 'updateMethod' method can be called + * with an complete object of changed Observer values. + * ``` + * { + * state1: Observer.value, + * state2: Observer.value + * } + * ``` * * [Learn more..](https://agile-ts.org/docs/core/integration#component-based) * diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 8c18ff08..9e4a5540 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -80,12 +80,12 @@ export class SubscriptionContainer { public selectorsWeakMap: SelectorWeakMapType; /** - * A Subscription Container is an interface to a UI-Component, + * A Subscription Container represents a UI-Component in AgileTs * that can be subscribed by multiple Observers. * - * These Observers use the Subscription Container - * to trigger a rerender on the Component it represents, - * when their value change. + * These Observers use the Subscription Container as an interface + * to trigger a rerender on the UI-Component it represents, + * for example when their value has changed. * * @internal * @param subs - Observers to be subscribed to the Subscription Container. @@ -106,7 +106,7 @@ export class SubscriptionContainer { this.componentId = config?.componentId; this.subscriberKeysWeakMap = new WeakMap(); - // Create for each specified proxy path a selector function, + // Create a selector function for each specified proxy path, // which selects the property at the path end const selectorWeakMap: SelectorWeakMapType = config.selectorWeakMap as any; this.assignProxySelectors( @@ -118,12 +118,12 @@ export class SubscriptionContainer { } /** - * Assigns selector functions created based on the paths of the Proxy Weak Map - * to the Selector Weak Map. + * Assigns selector functions created based on the paths of the provided Proxy Weak Map + * to the specified `selectorWeakMap`. * - * @param selectorWeakMap - * @param proxyWeakMap - * @param subs + * @param selectorWeakMap - Selector Weak Map the created proxy selectors are added to. + * @param proxyWeakMap - Proxy Weak Map containing proxy paths for specified Observers in `subs`. + * @param subs - Observers whose values are to be selected based on the specified `proxyWeakMap`. */ public assignProxySelectors( selectorWeakMap: SelectorWeakMapType, diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index 589b35b5..292df72f 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -12,12 +12,12 @@ import { } from '../../internal'; export class SubController { - // Agile Instance the Runtime belongs to + // Agile Instance the SubController belongs to public agileInstance: () => Agile; - // Represents all registered Component based Subscriptions + // Keeps track of all registered Component based Subscriptions public componentSubs: Set = new Set(); - // Represents all registered Callback based Subscriptions + // Keeps track of all registered Callback based Subscriptions public callbackSubs: Set = new Set(); // Keeps track of all mounted Components (only if agileInstance.config.mount = true) @@ -36,14 +36,13 @@ export class SubController { /** * Creates a so called Subscription Container which represents an UI-Component in AgileTs. * Such Subscription Container know how to trigger a rerender on the UI-Component it represents - * through the provided 'integrationInstance'. + * through the provided `integrationInstance`. * * There exist two different ways on how the Subscription Container can cause a rerender on the Component. - * - 1. Via a callback function that triggers a rerender on the Component. (Callback based Subscription) + * - 1. Via a callback function that directly triggers a rerender on the Component. (Callback based Subscription) * [Learn more..](https://agile-ts.org/docs/core/integration/#callback-based) * - 2. Via the Component instance itself. - * For example by mutating a local State Management property - * of the Component Instance. (Component based Subscription) + * For example by mutating a local State Management property. (Component based Subscription) * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) * * The in an object specified Observers are then automatically subscribed @@ -51,12 +50,15 @@ export class SubController { * * The advantage of subscribing the Observer via a object keymap, * is that each Observer has its own unique key identifier. - * Such key can for example required when merging the Observer value at key into + * Such key identifier is for example required when merging the Observer value into * a local Component State Management property. + * ``` + * this.state = {...this.state, {state1: Observer1.value, state2: Observer2.value}} + * ``` * * @internal * @param integrationInstance - Callback function or Component Instance for triggering a rerender on a UI-Component. - * @param subs - Observers to be subscribed to the Subscription Container in object shape. + * @param subs - Observers to be subscribed to the Subscription Container. * @param config - Configuration object */ public subscribeWithSubsObject( @@ -69,15 +71,9 @@ export class SubController { } { const props: { [key: string]: Observer['value'] } = {}; - // Create subsArray - const subsArray: Observer[] = []; - for (const key in subs) subsArray.push(subs[key]); - - // Create a rerender interface to Component - // via the specified 'integrationInstance' (Subscription Container) - const subscriptionContainer = this.registerSubscription( + // Create Subscription Container + const subscriptionContainer = this.createSubscriptionContainer( integrationInstance, - subsArray, config ); @@ -87,7 +83,8 @@ export class SubController { for (const key in subs) subscriptionContainer.subscriberKeysWeakMap.set(subs[key], key); - // Subscribe Observer to the created Subscription Container and build props object + // Subscribe Observers to the created Subscription Container + // and build a Observer value keymap for (const key in subs) { const observer = subs[key]; observer.subscribe(subscriptionContainer); @@ -103,14 +100,13 @@ export class SubController { /** * Creates a so called Subscription Container which represents an UI-Component in AgileTs. * Such Subscription Container know how to trigger a rerender on the UI-Component it represents - * through the provided 'integrationInstance'. + * through the provided `integrationInstance`. * * There exist two different ways on how the Subscription Container can cause a rerender on the Component. - * - 1. Via a callback function that triggers a rerender on the Component. (Callback based Subscription) + * - 1. Via a callback function that directly triggers a rerender on the Component. (Callback based Subscription) * [Learn more..](https://agile-ts.org/docs/core/integration/#callback-based) * - 2. Via the Component instance itself. - * For example by mutating a local State Management property - * of the Component Instance. (Component based Subscription) + * For example by mutating a local State Management property. (Component based Subscription) * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) * * The in an array specified Observers are then automatically subscribed @@ -118,7 +114,7 @@ export class SubController { * * @internal * @param integrationInstance - Callback function or Component Instance for triggering a rerender on a UI-Component. - * @param subs - Observers to be subscribed to the Subscription Container in array shape. + * @param subs - Observers to be subscribed to the Subscription Container. * @param config - Configuration object */ public subscribeWithSubsArray( @@ -126,41 +122,39 @@ export class SubController { subs: Array = [], config: RegisterSubscriptionConfigInterface = {} ): SubscriptionContainer { - // Create a rerender interface to Component - // via the specified 'integrationInstance' (Subscription Container) - const subscriptionContainer = this.registerSubscription( + // Create Subscription Container + const subscriptionContainer = this.createSubscriptionContainer( integrationInstance, - subs, config ); - // Subscribe Observer to the created Subscription Container + // Subscribe Observers to the created Subscription Container subs.forEach((observer) => observer.subscribe(subscriptionContainer)); return subscriptionContainer; } /** - * Unsubscribe the from the specified 'subscriptionInstance' - * extracted SubscriptionContainer from all Observers that - * are subscribed to it. + * Unsubscribe the Subscription Container extracted from the specified 'subscriptionInstance' + * from all Observers that were subscribed to it. + * + * We should always unsubscribe a Subscription Container when it isn't in use anymore, + * for example when the Component it represented has been unmounted. * * @internal * @param subscriptionInstance - Subscription Container - * or an UI-Component holding a instance of a Subscription Container + * or an UI-Component that contains an instance of the Subscription Container to be unsubscribed. */ public unsubscribe(subscriptionInstance: any) { - // Helper function to remove SubscriptionContainer from Observer + // Helper function to remove Subscription Container from Observer const unsub = (subscriptionContainer: SubscriptionContainer) => { subscriptionContainer.ready = false; - - // Remove SubscriptionContainers from Observer subscriptionContainer.subscribers.forEach((observer) => { observer.unsubscribe(subscriptionContainer); }); }; - // Unsubscribe callback based Subscription + // Unsubscribe callback based Subscription Container if (subscriptionInstance instanceof CallbackSubscriptionContainer) { unsub(subscriptionInstance); this.callbackSubs.delete(subscriptionInstance); @@ -171,7 +165,7 @@ export class SubController { return; } - // Unsubscribe component based Subscription + // Unsubscribe component based Subscription Container if (subscriptionInstance instanceof ComponentSubscriptionContainer) { unsub(subscriptionInstance); this.componentSubs.delete(subscriptionInstance); @@ -182,24 +176,9 @@ export class SubController { return; } - // Unsubscribe component based Subscription with subscriptionInstance that holds a componentSubscriptionContainer - if (subscriptionInstance.componentSubscriptionContainer) { - unsub( - subscriptionInstance.componentSubscriptionContainer as ComponentSubscriptionContainer - ); - this.componentSubs.delete( - subscriptionInstance.componentSubscriptionContainer - ); - - Agile.logger.if - .tag(['runtime', 'subscription']) - .info(LogCodeManager.getLog('15:01:01'), subscriptionInstance); - return; - } - - // Unsubscribe component based Subscription with subscriptionInstance that holds componentSubscriptionContainers + // Unsubscribe component based Subscription Container extracted from the 'componentSubscriptionContainers' property if ( - subscriptionInstance.componentSubscriptionContainers && + subscriptionInstance['componentSubscriptionContainers'] !== null && Array.isArray(subscriptionInstance.componentSubscriptionContainers) ) { subscriptionInstance.componentSubscriptionContainers.forEach( @@ -216,60 +195,59 @@ export class SubController { } } - //========================================================================================================= - // Register Subscription - //========================================================================================================= /** + * Returns a Component or Callback based Subscription Container + * based on the specified `integrationInstance`. + * * @internal - * Registers SubscriptionContainer and decides weather integrationInstance is a callback or component based Subscription - * @param integrationInstance - Callback Function or Component - * @param subs - Initial Subscriptions - * @param config - Config + * @param integrationInstance - Callback function or Component Instance for triggering a rerender on a UI-Component. + * @param config - Configuration object */ - public registerSubscription( + public createSubscriptionContainer( integrationInstance: any, - subs: Array = [], config: RegisterSubscriptionConfigInterface = {} ): SubscriptionContainer { config = defineConfig(config, { waitForMount: this.agileInstance().config.waitForMount, }); return isFunction(integrationInstance) - ? this.registerCallbackSubscription(integrationInstance, subs, config) - : this.registerComponentSubscription(integrationInstance, subs, config); + ? this.createCallbackSubscriptionContainer(integrationInstance, config) + : this.createComponentSubscriptionContainer(integrationInstance, config); } //========================================================================================================= - // Register Component Subscription + // Create Component Subscription Container //========================================================================================================= /** - * @internal + * Returns a newly created Component based Subscription Container. + * * Registers Component based Subscription and applies SubscriptionContainer to Component. * If an instance called 'subscriptionContainers' exists in Component it will push the new SubscriptionContainer to this Array, * otherwise it creates a new Instance called 'subscriptionContainer' which holds the new SubscriptionContainer - * @param componentInstance - Component that got subscribed by Observer/s - * @param subs - Initial Subscriptions - * @param config - Config + * + * @internal + * @param componentInstance - Component Instance for triggering a rerender on a UI-Component. + * @param config - Configuration object. */ - public registerComponentSubscription( + public createComponentSubscriptionContainer( componentInstance: any, - subs: Array = [], config: RegisterSubscriptionConfigInterface = {} ): ComponentSubscriptionContainer { const componentSubscriptionContainer = new ComponentSubscriptionContainer( componentInstance, - subs, + [], removeProperties(config, ['waitForMount']) ); this.componentSubs.add(componentSubscriptionContainer); - // Set to ready if not waiting for component to mount + // Define ready state of Subscription Container if (config.waitForMount) { if (this.mountedComponents.has(componentInstance)) componentSubscriptionContainer.ready = true; } else componentSubscriptionContainer.ready = true; - // Add subscriptionContainer to Component, to have an instance of it there (necessary to unsubscribe SubscriptionContainer later) + // Add subscriptionContainer to Component, to have an instance of it there + // (Required to unsubscribe the Subscription Container later via the Component Instance) if ( componentInstance.componentSubscriptionContainers && Array.isArray(componentInstance.componentSubscriptionContainers) @@ -278,7 +256,9 @@ export class SubController { componentSubscriptionContainer ); else - componentInstance.componentSubscriptionContainer = componentSubscriptionContainer; + componentInstance.componentSubscriptionContainers = [ + componentSubscriptionContainer, + ]; Agile.logger.if .tag(['runtime', 'subscription']) @@ -287,24 +267,20 @@ export class SubController { return componentSubscriptionContainer; } - //========================================================================================================= - // Register Callback Subscription - //========================================================================================================= /** + * Returns a newly created Callback based Subscription Container. + * * @internal - * Registers Callback based Subscription - * @param callbackFunction - Callback Function that causes rerender on Component which got subscribed by Observer/s - * @param subs - Initial Subscriptions - * @param config - Config + * @param callbackFunction - Callback function for triggering a rerender on a UI-Component. + * @param config - Configuration object */ - public registerCallbackSubscription( + public createCallbackSubscriptionContainer( callbackFunction: () => void, - subs: Array = [], config: RegisterSubscriptionConfigInterface = {} ): CallbackSubscriptionContainer { const callbackSubscriptionContainer = new CallbackSubscriptionContainer( callbackFunction, - subs, + [], removeProperties(config, ['waitForMount']) ); this.callbackSubs.add(callbackSubscriptionContainer); @@ -317,42 +293,49 @@ export class SubController { return callbackSubscriptionContainer; } - //========================================================================================================= - // Mount - //========================================================================================================= /** + * Mounts Component based Subscription Container. + * * @internal - * Mounts Component based SubscriptionContainer * @param componentInstance - SubscriptionContainer(Component) that gets mounted */ public mount(componentInstance: any) { - if (componentInstance.componentSubscriptionContainer) - componentInstance.componentSubscriptionContainer.ready = true; + if ( + componentInstance.componentSubscriptionContainers && + Array.isArray(componentInstance.componentSubscriptionContainers) + ) + componentInstance.componentSubscriptionContainers.map( + (c) => (c.ready = true) + ); this.mountedComponents.add(componentInstance); } - //========================================================================================================= - // Unmount - //========================================================================================================= /** + * Unmounts Component based Subscription Containers. + * * @internal - * Unmounts Component based SubscriptionContainer * @param componentInstance - SubscriptionContainer(Component) that gets unmounted */ public unmount(componentInstance: any) { - if (componentInstance.componentSubscriptionContainer) - componentInstance.componentSubscriptionContainer.ready = false; + if ( + componentInstance.componentSubscriptionContainers && + Array.isArray(componentInstance.componentSubscriptionContainers) + ) + componentInstance.componentSubscriptionContainers.map( + (c) => (c.ready = false) + ); this.mountedComponents.delete(componentInstance); } } -/** - * @param waitForMount - Whether the subscriptionContainer should only become ready - * when the Component has been mounted. (default = agileInstance.config.waitForMount) - */ interface RegisterSubscriptionConfigInterface extends SubscriptionContainerConfigInterface { + /** + * Whether the Subscription Container should only become ready + * when the Component has been mounted. + * @default agileInstance.config.waitForMount + */ waitForMount?: boolean; } diff --git a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts index eb1fbc90..77634bd9 100644 --- a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts +++ b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts @@ -45,7 +45,7 @@ describe('SubController Tests', () => { dummySubscriptionContainer = new SubscriptionContainer(); dummyObserver1.value = 'myCoolValue'; - subController.registerSubscription = jest.fn( + subController.createSubscriptionContainer = jest.fn( () => dummySubscriptionContainer ); jest.spyOn(dummyObserver1, 'subscribe'); @@ -73,7 +73,7 @@ describe('SubController Tests', () => { subscriptionContainer: dummySubscriptionContainer, }); - expect(subController.registerSubscription).toHaveBeenCalledWith( + expect(subController.createSubscriptionContainer).toHaveBeenCalledWith( dummyIntegration, [dummyObserver1, dummyObserver2], { @@ -113,7 +113,7 @@ describe('SubController Tests', () => { beforeEach(() => { dummySubscriptionContainer = new SubscriptionContainer(); - subController.registerSubscription = jest.fn( + subController.createSubscriptionContainer = jest.fn( () => dummySubscriptionContainer ); jest.spyOn(dummyObserver1, 'subscribe'); @@ -133,7 +133,7 @@ describe('SubController Tests', () => { expect(subscribeWithSubsArrayResponse).toBe(dummySubscriptionContainer); - expect(subController.registerSubscription).toHaveBeenCalledWith( + expect(subController.createSubscriptionContainer).toHaveBeenCalledWith( dummyIntegration, [dummyObserver1, dummyObserver2], { @@ -175,7 +175,7 @@ describe('SubController Tests', () => { const dummyIntegration = () => { /* empty function */ }; - const callbackSubscriptionContainer = subController.registerCallbackSubscription( + const callbackSubscriptionContainer = subController.createCallbackSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2] ); @@ -196,7 +196,7 @@ describe('SubController Tests', () => { const dummyIntegration: any = { dummy: 'integration', }; - const componentSubscriptionContainer = subController.registerComponentSubscription( + const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2] ); @@ -217,7 +217,7 @@ describe('SubController Tests', () => { const dummyIntegration: any = { dummy: 'integration', }; - const componentSubscriptionContainer = subController.registerComponentSubscription( + const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2] ); @@ -239,11 +239,11 @@ describe('SubController Tests', () => { dummy: 'integration', componentSubscriptionContainers: [], }; - const componentSubscriptionContainer = subController.registerComponentSubscription( + const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2] ); - const componentSubscriptionContainer2 = subController.registerComponentSubscription( + const componentSubscriptionContainer2 = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2] ); @@ -277,10 +277,10 @@ describe('SubController Tests', () => { dummySubscriptionContainer = new SubscriptionContainer(); dummyAgile.config.waitForMount = 'dummyWaitForMount' as any; - subController.registerCallbackSubscription = jest.fn( + subController.createCallbackSubscriptionContainer = jest.fn( () => dummySubscriptionContainer as CallbackSubscriptionContainer ); - subController.registerComponentSubscription = jest.fn( + subController.createComponentSubscriptionContainer = jest.fn( () => dummySubscriptionContainer as ComponentSubscriptionContainer ); }); @@ -290,21 +290,21 @@ describe('SubController Tests', () => { /* empty function */ }; - const subscriptionContainer = subController.registerSubscription( + const subscriptionContainer = subController.createSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2] ); expect(subscriptionContainer).toBe(dummySubscriptionContainer); expect( - subController.registerCallbackSubscription + subController.createCallbackSubscriptionContainer ).toHaveBeenCalledWith( dummyIntegration, [dummyObserver1, dummyObserver2], { waitForMount: dummyAgile.config.waitForMount } ); expect( - subController.registerComponentSubscription + subController.createComponentSubscriptionContainer ).not.toHaveBeenCalled(); }); @@ -313,7 +313,7 @@ describe('SubController Tests', () => { /* empty function */ }; - const subscriptionContainer = subController.registerSubscription( + const subscriptionContainer = subController.createSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2], { key: 'niceKey', proxyKeyMap: {}, waitForMount: false } @@ -321,42 +321,42 @@ describe('SubController Tests', () => { expect(subscriptionContainer).toBe(dummySubscriptionContainer); expect( - subController.registerCallbackSubscription + subController.createCallbackSubscriptionContainer ).toHaveBeenCalledWith( dummyIntegration, [dummyObserver1, dummyObserver2], { key: 'niceKey', proxyKeyMap: {}, waitForMount: false } ); expect( - subController.registerComponentSubscription + subController.createComponentSubscriptionContainer ).not.toHaveBeenCalled(); }); it('should call registerComponentSubscription if passed integrationInstance is not a Function (default config)', () => { const dummyIntegration = { dummy: 'integration' }; - const subscriptionContainer = subController.registerSubscription( + const subscriptionContainer = subController.createSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2] ); expect(subscriptionContainer).toBe(dummySubscriptionContainer); expect( - subController.registerComponentSubscription + subController.createComponentSubscriptionContainer ).toHaveBeenCalledWith( dummyIntegration, [dummyObserver1, dummyObserver2], { waitForMount: dummyAgile.config.waitForMount } ); expect( - subController.registerCallbackSubscription + subController.createCallbackSubscriptionContainer ).not.toHaveBeenCalled(); }); it('should call registerComponentSubscription if passed integrationInstance is not a Function (specific config)', () => { const dummyIntegration = { dummy: 'integration' }; - const subscriptionContainer = subController.registerSubscription( + const subscriptionContainer = subController.createSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2], { key: 'niceKey', proxyKeyMap: {}, waitForMount: false } @@ -364,14 +364,14 @@ describe('SubController Tests', () => { expect(subscriptionContainer).toBe(dummySubscriptionContainer); expect( - subController.registerComponentSubscription + subController.createComponentSubscriptionContainer ).toHaveBeenCalledWith( dummyIntegration, [dummyObserver1, dummyObserver2], { key: 'niceKey', proxyKeyMap: {}, waitForMount: false } ); expect( - subController.registerCallbackSubscription + subController.createCallbackSubscriptionContainer ).not.toHaveBeenCalled(); }); }); @@ -380,7 +380,7 @@ describe('SubController Tests', () => { it('should return ready componentSubscriptionContainer and add it to dummyIntegration at componentSubscriptionContainer (config.waitForMount = false)', () => { const dummyIntegration: any = { dummy: 'integration' }; - const componentSubscriptionContainer = subController.registerComponentSubscription( + const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2], { waitForMount: false } @@ -418,7 +418,7 @@ describe('SubController Tests', () => { componentSubscriptionContainers: [], }; - const componentSubscriptionContainer = subController.registerComponentSubscription( + const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2], { waitForMount: false } @@ -457,7 +457,7 @@ describe('SubController Tests', () => { dummy: 'integration', }; - const componentSubscriptionContainer = subController.registerComponentSubscription( + const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2], { waitForMount: true } @@ -491,7 +491,7 @@ describe('SubController Tests', () => { }; subController.mount(dummyIntegration); - const componentSubscriptionContainer = subController.registerComponentSubscription( + const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2], { waitForMount: true } @@ -527,7 +527,7 @@ describe('SubController Tests', () => { /* empty function */ }; - const callbackSubscriptionContainer = subController.registerCallbackSubscription( + const callbackSubscriptionContainer = subController.createCallbackSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2] ); @@ -567,7 +567,7 @@ describe('SubController Tests', () => { /* empty function */ }; - const callbackSubscriptionContainer = subController.registerCallbackSubscription( + const callbackSubscriptionContainer = subController.createCallbackSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2], { @@ -611,7 +611,7 @@ describe('SubController Tests', () => { beforeEach(() => { dummyAgile.config.waitForMount = true; - componentSubscriptionContainer = subController.registerComponentSubscription( + componentSubscriptionContainer = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2] ); @@ -636,7 +636,7 @@ describe('SubController Tests', () => { beforeEach(() => { dummyAgile.config.waitForMount = true; - componentSubscriptionContainer = subController.registerComponentSubscription( + componentSubscriptionContainer = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2] ); diff --git a/packages/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index f3fe5e03..5ddb00f1 100644 --- a/packages/react/src/hooks/useAgile.ts +++ b/packages/react/src/hooks/useAgile.ts @@ -12,6 +12,7 @@ import { isValidObject, generateId, ProxyWeakMapType, + ComponentIdType, } from '@agile-ts/core'; import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; import { ProxyTree } from '@agile-ts/proxytree'; @@ -128,7 +129,12 @@ export function useAgile< forceRender(); }, observers, - { key: config.key, proxyWeakMap, waitForMount: false } + { + key: config.key, + proxyWeakMap, + waitForMount: false, + componentId: config.componentId, + } ); // Unsubscribe Callback based Subscription on Unmount @@ -184,6 +190,7 @@ interface AgileHookConfigInterface { key?: SubscriptionContainerKeyType; agileInstance?: Agile; proxyBased?: boolean; + componentId?: ComponentIdType; } interface ProxyTreeMapInterface { From ea35b95e0586c690f86ddac298d028bf50f6b556 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 8 Jun 2021 08:48:05 +0200 Subject: [PATCH 35/63] fixed typos --- packages/core/src/runtime/index.ts | 51 ++++---- packages/core/src/runtime/observer.ts | 119 +++++++++++------- packages/core/src/runtime/runtime.job.ts | 83 +++++++++--- .../ComponentSubscriptionContainer.ts | 17 ++- .../container/SubscriptionContainer.ts | 62 +++++---- .../runtime/subscription/sub.controller.ts | 66 +++++----- 6 files changed, 240 insertions(+), 158 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index f6267a0a..e4d982bb 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -30,7 +30,7 @@ export class Runtime { /** * The Runtime executes and queues ingested Observer based Jobs - * to prevent race conditions and optimized rerenders of subscribed Components. + * to prevent race conditions and optimized rerender of subscribed Components. * * Each provided Job will be executed when it is its turn * by calling the Job Observer's 'perform()' method. @@ -40,7 +40,7 @@ export class Runtime { * * The rerender queue is designed for optimizing the render count * by combining rerender Jobs of the same Component - * and ingoing rerender requests for unmounted Components. + * and ignoring rerender requests for unmounted Components. * * @internal * @param agileInstance - Instance of Agile the Runtime belongs to. @@ -53,7 +53,11 @@ export class Runtime { * Adds the specified Observer based Job to the internal Job queue, * where it will be performed when it is its turn. * - * @internal + * After a successful execution it is added to the rerender queue, + * where all the Observer's subscribed Subscription Containers + * cause rerender on Components the Observer is represented in. + * + * @public * @param job - Job to be performed. * @param config - Configuration object */ @@ -80,7 +84,8 @@ export class Runtime { * Performs the specified Job * and adds it to the rerender queue if necessary. * - * After the execution of the Job it checks if there are still Jobs left in the queue. + * After the execution of the provided Job it is checked whether + * there are still Jobs left in the Job queue. * - If so, the next Job in the queue is performed. * - If not, the `jobsToRerender` queue will be started to work off. * @@ -127,8 +132,8 @@ export class Runtime { } /** - * Executes the `jobsToRerender` queue - * and updates (causes rerender on) the Subscription Container (subscribed Component) + * Works of the `jobsToRerender` queue by updating (causing rerender on) + * the Subscription Container (subscribed Component) * of each Job Observer. * * It returns a boolean indicating whether any Subscription Container was updated. @@ -147,7 +152,7 @@ export class Runtime { ) return false; - // Subscription Containers that have to be updated (perform rerender on Component it represents). + // Subscription Containers that have to be updated. // Using a 'Set()' to combine several equal SubscriptionContainers into one (rerender optimisation). const subscriptionsToUpdate = new Set(); @@ -188,14 +193,15 @@ export class Runtime { let updateSubscriptionContainer; - // Handle Selectors + // Handle Selectors of Subscription Container + // (-> check if a selected part of the Observer value has changed) updateSubscriptionContainer = this.handleSelectors( subscriptionContainer, job ); - // Check if Subscription Container with same componentId is already in the 'subscriptionToUpdate' queue - // (rerender optimisation) + // Check if Subscription Container with same 'componentId' + // is already in the 'subscriptionToUpdate' queue (rerender optimisation) updateSubscriptionContainer = updateSubscriptionContainer && Array.from(subscriptionsToUpdate).findIndex( @@ -214,13 +220,13 @@ export class Runtime { if (subscriptionsToUpdate.size <= 0) return false; - // Update Subscription Containers (trigger rerender on subscribed Component) + // Update Subscription Containers (trigger rerender on Components they represent) subscriptionsToUpdate.forEach((subscriptionContainer) => { // Call 'callback function' if Callback based Subscription if (subscriptionContainer instanceof CallbackSubscriptionContainer) subscriptionContainer.callback(); - // Call 'update method' if Component based Subscription + // Call 'update method' in Integrations if Component based Subscription if (subscriptionContainer instanceof ComponentSubscriptionContainer) this.agileInstance().integrations.update( subscriptionContainer.component, @@ -238,19 +244,19 @@ export class Runtime { } /** - * Maps the values of updated Observers (updatedSubscribers) into a key map. + * Maps the values of updated Observers (`updatedSubscribers`) + * of the specified Subscription Container into a key map. * - * The key is extracted from the Observer itself or from the Subscription Containers 'subscriberKeysWeakMap'. + * The key containing the Observer value is extracted from the Observer itself + * or from the Subscription Container's `subscriberKeysWeakMap`. * * @internal - * @param subscriptionContainer - Subscription Container from which the 'updatedSubscribers' are to be mapped to a key map. + * @param subscriptionContainer - Subscription Container from which the `updatedSubscribers` are to be mapped to a key map. */ public getUpdatedObserverValues( subscriptionContainer: SubscriptionContainer ): { [key: string]: any } { const props: { [key: string]: any } = {}; - - // Map updated Observer values into the props key map for (const observer of subscriptionContainer.updatedSubscribers) { const key = subscriptionContainer.subscriberKeysWeakMap.get(observer) ?? @@ -261,14 +267,15 @@ export class Runtime { } /** - * Returns a boolean indicating whether the Subscription Container can be updated or not. + * Returns a boolean indicating whether the specified Subscription Container can be updated or not + * based on the selector functions (`selectorsWeakMap`) of the Subscription Container. * - * Therefore it reviews the '.value' and the '.previousValue' property of the Observer represented by the Job. + * This is done by checking the '.value' and the '.previousValue' property of the Observer represented by the Job. * If a selected property differs, the Subscription Container is allowed to update/rerender (returns true). * * @internal * @param subscriptionContainer - Subscription Container to be checked if it can update. - * @param job - Job the Subscription Container belongs to. + * @param job - Job containing the Observer which has subscribed the Subscription Container. */ public handleSelectors( subscriptionContainer: SubscriptionContainer, @@ -281,7 +288,7 @@ export class Runtime { // because no specific part of the Observer was selected // -> The Subscription Container should update // no matter what was updated in the Observer - if (!selectors) return true; + if (selectors == null) return true; // Check if a selected part of Observer value has changed const previousValue = job.observer.previousValue; @@ -289,7 +296,7 @@ export class Runtime { for (const selector of selectors) { if ( notEqual(selector(newValue), selector(previousValue)) - // || newValueDeepness !== previousValueDeepness // Not possible to check + // || newValueDeepness !== previousValueDeepness // Not possible to check the object deepness ) return true; } diff --git a/packages/core/src/runtime/observer.ts b/packages/core/src/runtime/observer.ts index 5710bd18..cfe7e7ce 100644 --- a/packages/core/src/runtime/observer.ts +++ b/packages/core/src/runtime/observer.ts @@ -12,20 +12,32 @@ import { export type ObserverKey = string | number; export class Observer { + // Agile Instance the Observer belongs to public agileInstance: () => Agile; + // Key/Name identifier of the Subscription Container public _key?: ObserverKey; - public dependents: Set = new Set(); // Observers that depend on this Observer - public subscribedTo: Set = new Set(); // SubscriptionContainers (Components) that this Observer is subscribed to - public value?: ValueType; // Value of Observer - public previousValue?: ValueType; // Previous Value of Observer + // Observers that depend on this Observer + public dependents: Set = new Set(); + // Subscription Containers (Components) the Observer is subscribed to + public subscribedTo: Set = new Set(); + // Current value of Observer + public value?: ValueType; + // Previous value of Observer + public previousValue?: ValueType; /** + * Handles the subscriptions to Subscription Containers (Components) + * and keeps track of dependencies. + * + * All Agile Classes that can be bound a UI-Component have their own Observer + * which manages the above mentioned things for them. + * + * The Observer is no standalone class and should be extended from a 'real' Observer. + * * @internal - * Observer - Handles subscriptions and dependencies of an Agile Class and is like an instance to the Runtime - * Note: No stand alone class!! - * @param agileInstance - An instance of Agile - * @param config - Config + * @param agileInstance - Instance of Agile the Observer belongs to. + * @param config - Configuration object */ constructor( agileInstance: Agile, @@ -46,28 +58,31 @@ export class Observer { } /** - * @internal - * Set Key/Name of Observer + * Updates the key/name identifier of the Observer. + * + * @public + * @param value - New key/name identifier. */ public set key(value: StateKey | undefined) { this._key = value; } /** - * @internal - * Get Key/Name of Observer + * Returns the key/name identifier of the State. + * + * @public */ public get key(): StateKey | undefined { return this._key; } - //========================================================================================================= - // Ingest - //========================================================================================================= /** - * @internal - * Ingests Observer into Runtime - * @param config - Configuration + * Ingests the Observer into the runtime, + * by creating a Runtime Job + * and adding the Observer to the created Job. + * + * @public + * @param config - Configuration object */ public ingest(config: ObserverIngestConfigInterface = {}): void { config = defineConfig(config, { @@ -93,54 +108,56 @@ export class Observer { }); } - //========================================================================================================= - // Perform - //========================================================================================================= /** - * @internal - * Performs Job of Runtime - * @param job - Job that gets performed + * Method executed by the Runtime to perform the Runtime Job, + * previously ingested (`ingest()`) by the Observer. + * + * @public + * @param job - Runtime Job to be performed. */ public perform(job: RuntimeJob): void { LogCodeManager.log('17:03:00'); } - //========================================================================================================= - // Depend - //========================================================================================================= /** - * @internal - * Adds Dependent to Observer which gets ingested into the Runtime whenever this Observer mutates - * @param observer - Observer that will depend on this Observer + * Adds specified Observer to the dependents of this Observer. + * + * Every time this Observer is ingested into the Runtime, + * the dependent Observers are ingested into the Runtime too. + * + * @public + * @param observer - Observer to depends on this Observer. */ public depend(observer: Observer): void { if (!this.dependents.has(observer)) this.dependents.add(observer); } - //========================================================================================================= - // Subscribe - //========================================================================================================= /** - * @internal - * Adds Subscription to Observer - * @param subscriptionContainer - SubscriptionContainer(Component) that gets subscribed by this Observer + * Subscribes Observer to the specified Subscription Container (Component). + * + * Every time this Observer is ingested into the Runtime, + * a rerender might be triggered on the Component the Subscription Container represents. + * + * @public + * @param subscriptionContainer - Subscription Container to which the Observer should subscribe. */ public subscribe(subscriptionContainer: SubscriptionContainer): void { if (!this.subscribedTo.has(subscriptionContainer)) { this.subscribedTo.add(subscriptionContainer); - // Add this to subscriptionContainer to keep track of the Observers the subscriptionContainer hold + // Add Observer to Subscription Container + // to keep track of the Observers that have subscribed the Subscription Container. + // For example to unsubscribe the subscribed Observers + // when the Subscription Container (Component) unmounts. subscriptionContainer.subscribers.add(this); } } - //========================================================================================================= - // Unsubscribe - //========================================================================================================= /** - * @internal - * Removes Subscription from Observer - * @param subscriptionContainer - SubscriptionContainer(Component) that gets unsubscribed by this Observer + * Unsubscribes Observer from specified Subscription Container (Component). + * + * @public + * @param subscriptionContainer - Subscription Container that the Observer should unsubscribe. */ public unsubscribe(subscriptionContainer: SubscriptionContainer): void { if (this.subscribedTo.has(subscriptionContainer)) { @@ -157,9 +174,25 @@ export class Observer { * @param value - Initial Value of Observer */ export interface CreateObserverConfigInterface { + /** + * Initial Observers that depend on this Observer. + * @default [] + */ dependents?: Array; + /** + * Initial Subscription Container the Observer is subscribed to. + * @default [] + */ subs?: Array; + /** + * Key/Name identifier of the Observer. + * @default undefined + */ key?: ObserverKey; + /** + * Initial value of the Observer. + * @defualt undefined + */ value?: ValueType; } diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts index a3e559ea..25420d8d 100644 --- a/packages/core/src/runtime/runtime.job.ts +++ b/packages/core/src/runtime/runtime.job.ts @@ -1,19 +1,27 @@ import { Observer, defineConfig, SubscriptionContainer } from '../internal'; export class RuntimeJob { - public _key?: RuntimeJobKey; public config: RuntimeJobConfigInterface; - public observer: ObserverType; // Observer the Job represents - public rerender: boolean; // If Job will cause rerender on subscriptionContainer in Observer - public performed = false; // If Job has been performed by Runtime - public subscriptionContainersToUpdate = new Set(); // SubscriptionContainer (from Observer) that have to be updated/rerendered - public triesToUpdate = 0; // How often not ready subscriptionContainers of this Job have been tried to update + + // Key/Name identifier of the Subscription Container + public _key?: RuntimeJobKey; + // Observer the Job represents + public observer: ObserverType; + // Whether the Subscription Containers (Components) of the Observer can be re-rendered + public rerender: boolean; + // Whether the Job has been performed by the runtime + public performed = false; + // Subscription Container of the Observer that have to be updated/re-rendered + public subscriptionContainersToUpdate = new Set(); + // How often not ready Subscription Container of the Observer have been tried to update + public triesToUpdate = 0; /** + * A Job that contains an Observer to be executed by the runtime. + * * @internal - * Job - Represents Observer that gets performed by the Runtime - * @param observer - Observer - * @param config - Config + * @param observer - Observer to be represented by the Runtime Job. + * @param config - Configuration object */ constructor( observer: ObserverType, @@ -42,22 +50,34 @@ export class RuntimeJob { this.subscriptionContainersToUpdate = new Set(observer.subscribedTo); } - public get key(): RuntimeJobKey | undefined { - return this._key; - } - + /** + * Updates the key/name identifier of the Runtime Job. + * + * @public + * @param value - New key/name identifier. + */ public set key(value: RuntimeJobKey | undefined) { this._key = value; } + + /** + * Returns the key/name identifier of the Runtime Job. + * + * @public + */ + public get key(): RuntimeJobKey | undefined { + return this._key; + } } export type RuntimeJobKey = string | number; -/** - * @param key - Key/Name of RuntimeJob - */ export interface CreateRuntimeJobConfigInterface extends RuntimeJobConfigInterface { + /** + * Key/Name identifier of the Runtime Job. + * @default undefined + */ key?: RuntimeJobKey; } @@ -70,17 +90,40 @@ export interface CreateRuntimeJobConfigInterface * But be aware that this can lead to an overflow of 'old' Jobs after some time. (affects performance) */ export interface RuntimeJobConfigInterface { + /** + * Whether to perform the Runtime Job in background. + * So that the UI isn't notified of these changes and thus doesn't rerender. + * @default false + */ background?: boolean; + /** + * Configuration of the execution of defined side effects. + * @default {enabled: true, exclude: []} + */ sideEffects?: SideEffectConfigInterface; + /** + * Whether the Runtime Job should be forced through the runtime + * although it might be useless from the viewpoint of the runtime. + * @default false + */ force?: boolean; + /** + * How often the runtime should try to update not ready Subscription Containers of the Observer the Job represents. + * If 'null' the runtime tries to update the not ready Subscription Container until they are ready (infinite). + * @default 3 + */ numberOfTriesToUpdate?: number | null; } -/** - * @param enabled - If SideEffects get executed - * @param exclude - SideEffect at Keys that doesn't get executed - */ export interface SideEffectConfigInterface { + /** + * Whether to execute the defined side effects + * @default true + */ enabled?: boolean; + /** + * Side effect key identifier that won't be executed. + * @default [] + */ exclude?: string[]; } diff --git a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts index 1d10b209..9e1daeef 100644 --- a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts @@ -35,18 +35,23 @@ export class ComponentSubscriptionContainer< * [Observer, Observer] * ``` * Thus the Integrations 'updateMethod' method can be called - * with an complete object of changed Observer values. + * with an complete object of changed Observer values. * ``` - * { - * state1: Observer.value, - * state2: Observer.value - * } + * updateMethod: (componentInstance, updatedData) => { + * console.log(componentInstance); // Returns [this.component] + * console.log(updatedData); // Returns changed Observer values (see below) + * // { + * // state1: Observer.value, + * // state2: Observer.value + * // } + * } * ``` * * [Learn more..](https://agile-ts.org/docs/core/integration#component-based) * * @internal - * @param component - Component to be represent by the Subscription Container. + * @param component - Component to be represented by the Subscription Container + * and mutated via the Integration method 'updateMethod()' to trigger rerender on it. * @param subs - Observers to be subscribed to the Subscription Container. * @param config - Configuration object */ diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 9e4a5540..e5791cf9 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -15,11 +15,11 @@ export class SubscriptionContainer { * and the Component the Subscription Container represents are ready. * * When both are ready, the Subscription Container is allowed - * to trigger rerenders on the Component based on its type. (Component or Callback based) + * to trigger rerender on the Component. */ public ready = false; /** - * Id of the Component the Subscription Container represents. + * Unique identifier of the Component the Subscription Container represents. */ public componentId?: ComponentIdType; @@ -28,8 +28,8 @@ export class SubscriptionContainer { * * The subscribed Observers use the Subscription Container * as an interface to the Component it represents. - * Through the Subscription Container, they can then trigger rerenders - * on the Component when their value changes. + * Through the Subscription Container, they can easily trigger rerender + * on the Component, for example, when their value changes. * * [Learn more..](https://agile-ts.org/docs/core/integration#-subscriptions) */ @@ -37,17 +37,15 @@ export class SubscriptionContainer { /** * Temporary stores the subscribed Observers, * that were performed by the runtime - * and are currently running through the update Subscription Container process. - * - * This is used for example, to merge the changed Observer values - * into the Component's local State Management instance for a Component based Subscription. + * and are currently running through the update Subscription Container (rerender) process. */ public updatedSubscribers: Array = []; /** * Whether the Subscription Container is object based. * - * A Observer is object based when the subscribed Observers were provided in an Observer key map. + * An Observer is object based when the subscribed Observers + * have been provided in an Observer key map. * ``` * { * state1: Observer, @@ -58,22 +56,22 @@ export class SubscriptionContainer { * * Often Component based Subscriptions are object based, * because each Observer requires a unique identifier - * to properly merge the Observer value into the Component's local State Management instance. + * to be properly represented in the 'updatedData' object sent to the Integration 'updateMethod()'. */ public isObjectBased = false; /** - * Weak map for storing 'external' key identifiers for Observer. + * Weak map for storing 'external' key identifiers for subscribed Observers. * * https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap */ public subscriberKeysWeakMap: WeakMap; /** - * Weak Map for storing selector functions of subscribed Observer. + * Weak Map for storing selector functions for subscribed Observers. * - * A selector functions allows the partly subscription to an Observer value. - * Only if the selected Observe value part changes, - * the Subscription Container rerenders the Component it represents. + * A selector function allows partial subscription to an Observer value. + * Only when the selected Observer value part changes, + * the Subscription Container rerender the Component. * * https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap */ @@ -81,11 +79,11 @@ export class SubscriptionContainer { /** * A Subscription Container represents a UI-Component in AgileTs - * that can be subscribed by multiple Observers. + * that can be subscribed by multiple Observer Instances. * * These Observers use the Subscription Container as an interface * to trigger a rerender on the UI-Component it represents, - * for example when their value has changed. + * for example, when their value has changed. * * @internal * @param subs - Observers to be subscribed to the Subscription Container. @@ -106,8 +104,8 @@ export class SubscriptionContainer { this.componentId = config?.componentId; this.subscriberKeysWeakMap = new WeakMap(); - // Create a selector function for each specified proxy path, - // which selects the property at the path end + // Create a selector function for each specified proxy path + // that selects the property at the path end const selectorWeakMap: SelectorWeakMapType = config.selectorWeakMap as any; this.assignProxySelectors( selectorWeakMap, @@ -118,10 +116,10 @@ export class SubscriptionContainer { } /** - * Assigns selector functions created based on the paths of the provided Proxy Weak Map - * to the specified `selectorWeakMap`. + * Assigns to the specified `selectorWeakMap` selector functions + * created based on the paths of the specified Proxy WeakMap. * - * @param selectorWeakMap - Selector Weak Map the created proxy selectors are added to. + * @param selectorWeakMap - Selector WeakMap the created proxy selector functions are added to. * @param proxyWeakMap - Proxy Weak Map containing proxy paths for specified Observers in `subs`. * @param subs - Observers whose values are to be selected based on the specified `proxyWeakMap`. */ @@ -159,18 +157,18 @@ export interface SubscriptionContainerConfigInterface { */ key?: SubscriptionContainerKeyType; /** - * Key/Name identifier of the Component the Subscription Container represents. + * Key/Name identifier of the Component to be represented by the Subscription Container. * @default undefined */ componentId?: ComponentIdType; /** - * A Weak Map with a 2 dimensional arrays representing paths/routes - * to particular properties in the Observer. + * A Weak Map with a set of paths to certain properties + * in a Observer value for Observers. * - * The Component the Subscription Container represents - * is then only rerendered, when a property at a specified path changes. - * Not anymore if anything in the Observer object mutates, - * although it might not even be displayed in the Component. + * These paths are then selected via selector functions + * which allow the partly subscription to an Observer value. + * Only if the selected Observer value part changes, + * the Subscription Container rerender the Component. * * For example: * ``` @@ -191,11 +189,11 @@ export interface SubscriptionContainerConfigInterface { */ proxyWeakMap?: ProxyWeakMapType; /** - * A Weak Map with an array of selector functions for Observers. + * A Weak Map with a set of selector functions for Observers. * * A selector functions allows the partly subscription to an Observer value. - * Only if the selected Observe value part changes, - * the Subscription Container rerenders the Component it represents. + * Only if the selected Observer value part changes, + * the Subscription Container rerender the Component. * * @default new WeakMap() */ diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index 292df72f..37b56192 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -24,10 +24,13 @@ export class SubController { public mountedComponents: Set = new Set(); /** - * Manages the subscription to UI-Components. + * The Subscription Controller manages the subscription to UI-Components. + * + * Thus it creates Subscription Containers (Interfaces to UI-Components) + * and assigns them to Observers, so that the Observers can easily trigger rerender on Components. * * @internal - * @param agileInstance - Instance of Agile the Subscription Container belongs to. + * @param agileInstance - Instance of Agile the Subscription Controller belongs to. */ public constructor(agileInstance: Agile) { this.agileInstance = () => agileInstance; @@ -38,7 +41,7 @@ export class SubController { * Such Subscription Container know how to trigger a rerender on the UI-Component it represents * through the provided `integrationInstance`. * - * There exist two different ways on how the Subscription Container can cause a rerender on the Component. + * There exist two different ways the Subscription Container can cause a rerender on the Component. * - 1. Via a callback function that directly triggers a rerender on the Component. (Callback based Subscription) * [Learn more..](https://agile-ts.org/docs/core/integration/#callback-based) * - 2. Via the Component instance itself. @@ -46,7 +49,7 @@ export class SubController { * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) * * The in an object specified Observers are then automatically subscribed - * to the created Subscription Container and thus to the Component the Subscription Container represents. + * to the created Subscription Container and thus to the Component it represents. * * The advantage of subscribing the Observer via a object keymap, * is that each Observer has its own unique key identifier. @@ -56,9 +59,9 @@ export class SubController { * this.state = {...this.state, {state1: Observer1.value, state2: Observer2.value}} * ``` * - * @internal - * @param integrationInstance - Callback function or Component Instance for triggering a rerender on a UI-Component. - * @param subs - Observers to be subscribed to the Subscription Container. + * @public + * @param integrationInstance - Callback function or Component Instance to trigger a rerender on a UI-Component. + * @param subs - Observers to be subscribed to the to create Subscription Container. * @param config - Configuration object */ public subscribeWithSubsObject( @@ -84,7 +87,7 @@ export class SubController { subscriptionContainer.subscriberKeysWeakMap.set(subs[key], key); // Subscribe Observers to the created Subscription Container - // and build a Observer value keymap + // and build a Observer value keymap called props for (const key in subs) { const observer = subs[key]; observer.subscribe(subscriptionContainer); @@ -102,7 +105,7 @@ export class SubController { * Such Subscription Container know how to trigger a rerender on the UI-Component it represents * through the provided `integrationInstance`. * - * There exist two different ways on how the Subscription Container can cause a rerender on the Component. + * There exist two different ways the Subscription Container can cause a rerender on the Component. * - 1. Via a callback function that directly triggers a rerender on the Component. (Callback based Subscription) * [Learn more..](https://agile-ts.org/docs/core/integration/#callback-based) * - 2. Via the Component instance itself. @@ -110,11 +113,11 @@ export class SubController { * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) * * The in an array specified Observers are then automatically subscribed - * to the created Subscription Container and thus to the Component the Subscription Container represents. + * to the created Subscription Container and thus to the Component it represents. * - * @internal - * @param integrationInstance - Callback function or Component Instance for triggering a rerender on a UI-Component. - * @param subs - Observers to be subscribed to the Subscription Container. + * @public + * @param integrationInstance - Callback function or Component Instance to trigger a rerender on a UI-Component. + * @param subs - Observers to be subscribed to the to create Subscription Container. * @param config - Configuration object */ public subscribeWithSubsArray( @@ -138,12 +141,12 @@ export class SubController { * Unsubscribe the Subscription Container extracted from the specified 'subscriptionInstance' * from all Observers that were subscribed to it. * - * We should always unsubscribe a Subscription Container when it isn't in use anymore, - * for example when the Component it represented has been unmounted. + * We should always unregister a Subscription Container when it is no longer in use, + * for example when the Component it represents has been unmounted. * - * @internal + * @public * @param subscriptionInstance - Subscription Container - * or an UI-Component that contains an instance of the Subscription Container to be unsubscribed. + * or a UI-Component that contains an instance of a Subscription Container to be unsubscribed. */ public unsubscribe(subscriptionInstance: any) { // Helper function to remove Subscription Container from Observer @@ -196,11 +199,11 @@ export class SubController { } /** - * Returns a Component or Callback based Subscription Container + * Returns a newly created Component or Callback based Subscription Container * based on the specified `integrationInstance`. * * @internal - * @param integrationInstance - Callback function or Component Instance for triggering a rerender on a UI-Component. + * @param integrationInstance - Callback function or Component Instance to trigger a rerender on a UI-Component. * @param config - Configuration object */ public createSubscriptionContainer( @@ -215,18 +218,11 @@ export class SubController { : this.createComponentSubscriptionContainer(integrationInstance, config); } - //========================================================================================================= - // Create Component Subscription Container - //========================================================================================================= /** * Returns a newly created Component based Subscription Container. * - * Registers Component based Subscription and applies SubscriptionContainer to Component. - * If an instance called 'subscriptionContainers' exists in Component it will push the new SubscriptionContainer to this Array, - * otherwise it creates a new Instance called 'subscriptionContainer' which holds the new SubscriptionContainer - * * @internal - * @param componentInstance - Component Instance for triggering a rerender on a UI-Component. + * @param componentInstance - Component Instance to trigger a rerender on a UI-Component. * @param config - Configuration object. */ public createComponentSubscriptionContainer( @@ -247,7 +243,7 @@ export class SubController { } else componentSubscriptionContainer.ready = true; // Add subscriptionContainer to Component, to have an instance of it there - // (Required to unsubscribe the Subscription Container later via the Component Instance) + // (For example, required to unsubscribe the Subscription Container via the Component Instance) if ( componentInstance.componentSubscriptionContainers && Array.isArray(componentInstance.componentSubscriptionContainers) @@ -271,7 +267,7 @@ export class SubController { * Returns a newly created Callback based Subscription Container. * * @internal - * @param callbackFunction - Callback function for triggering a rerender on a UI-Component. + * @param callbackFunction - Callback function to trigger a rerender on a UI-Component. * @param config - Configuration object */ public createCallbackSubscriptionContainer( @@ -296,8 +292,8 @@ export class SubController { /** * Mounts Component based Subscription Container. * - * @internal - * @param componentInstance - SubscriptionContainer(Component) that gets mounted + * @public + * @param componentInstance - Component Instance containing a Subscription Container to be mounted */ public mount(componentInstance: any) { if ( @@ -314,8 +310,8 @@ export class SubController { /** * Unmounts Component based Subscription Containers. * - * @internal - * @param componentInstance - SubscriptionContainer(Component) that gets unmounted + * @public + * @param componentInstance - Component Instance containing a Subscription Container to be unmounted */ public unmount(componentInstance: any) { if ( @@ -333,8 +329,8 @@ export class SubController { interface RegisterSubscriptionConfigInterface extends SubscriptionContainerConfigInterface { /** - * Whether the Subscription Container should only become ready - * when the Component has been mounted. + * Whether the Subscription Container should be ready only + * when the Component it represents has been mounted. * @default agileInstance.config.waitForMount */ waitForMount?: boolean; From 2e25a44bdfb9ef64d985561f4a74b9f64bd56581 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Tue, 8 Jun 2021 17:44:28 +0200 Subject: [PATCH 36/63] optimized subscription controller --- .../functional-component-ts/package.json | 2 +- .../develop/functional-component-ts/yarn.lock | 46 +++--- packages/core/src/runtime/index.ts | 11 +- packages/core/src/runtime/observer.ts | 11 +- .../CallbackSubscriptionContainer.ts | 2 +- .../ComponentSubscriptionContainer.ts | 2 +- .../container/SubscriptionContainer.ts | 109 ++++++++----- .../runtime/subscription/sub.controller.ts | 145 ++++++++---------- .../core/tests/unit/runtime/runtime.test.ts | 57 +------ .../CallbackSubscriptionContainer.test.ts | 25 ++- .../ComponentSubscriptionContainer.test.ts | 25 ++- .../container/SubscriptionContainer.test.ts | 75 +++++++-- .../subscription/sub.controller.test.ts | 20 +-- packages/event/src/hooks/useEvent.ts | 2 +- packages/react/src/hocs/AgileHOC.ts | 10 +- packages/react/src/hooks/useAgile.ts | 2 +- packages/vue/src/bindAgileInstances.ts | 16 +- 17 files changed, 300 insertions(+), 260 deletions(-) diff --git a/examples/react/develop/functional-component-ts/package.json b/examples/react/develop/functional-component-ts/package.json index be742bb8..08b5324c 100644 --- a/examples/react/develop/functional-component-ts/package.json +++ b/examples/react/develop/functional-component-ts/package.json @@ -30,7 +30,7 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "install:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/api @agile-ts/multieditor @agile-ts/event & yarn install" + "install:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/api @agile-ts/multieditor @agile-ts/event @agile-ts/proxytree & yarn install" }, "eslintConfig": { "extends": "react-app" diff --git a/examples/react/develop/functional-component-ts/yarn.lock b/examples/react/develop/functional-component-ts/yarn.lock index 54955758..21b151e8 100644 --- a/examples/react/develop/functional-component-ts/yarn.lock +++ b/examples/react/develop/functional-component-ts/yarn.lock @@ -3,46 +3,46 @@ "@agile-ts/api@file:.yalc/@agile-ts/api": - version "0.0.16" + version "0.0.18" dependencies: - "@agile-ts/utils" "^0.0.2" + "@agile-ts/utils" "^0.0.4" "@agile-ts/core@file:.yalc/@agile-ts/core": - version "0.0.15" + version "0.0.17" dependencies: - "@agile-ts/logger" "^0.0.2" - "@agile-ts/utils" "^0.0.2" + "@agile-ts/logger" "^0.0.4" + "@agile-ts/utils" "^0.0.4" "@agile-ts/event@file:.yalc/@agile-ts/event": - version "0.0.5" + version "0.0.7" -"@agile-ts/logger@^0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@agile-ts/logger/-/logger-0.0.2.tgz#80a726531dd63ca7d1c9a123383e57b5501efbb0" - integrity sha512-rJJ5pqXtOriYxjuZPhHs2J9N1FnIaAZqItCw0MXW9/5od/uhJ28aiG7w9RUBZts9SjDcICYEfjFMcTJ/kYJsMg== +"@agile-ts/logger@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@agile-ts/logger/-/logger-0.0.4.tgz#7f4d82ef8f03b13089af0878c360575c43f0962d" + integrity sha512-qm0obAKqJMaPKM+c76gktRXyw3OL1v39AnhMZ0FBGwJqHWU+fLRkCzlQwjaROCr3F1XP01Lc/Ls3efF0WzyEPw== dependencies: - "@agile-ts/utils" "^0.0.2" + "@agile-ts/utils" "^0.0.4" "@agile-ts/multieditor@file:.yalc/@agile-ts/multieditor": - version "0.0.15" + version "0.0.17" -"@agile-ts/proxytree@^0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@agile-ts/proxytree/-/proxytree-0.0.2.tgz#516ed19ee8d58aeecb291788a1e47be3dc23df8c" - integrity sha512-PbSiChF0GcUoWnrbnHauzBxZ5r/+4pZSZWpYjkBcIFa48DgTtFzg5DfQzsW3Rc1Y0QSFGYqcZOvCK1xAjLIQ2g== +"@agile-ts/proxytree@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@agile-ts/proxytree/-/proxytree-0.0.3.tgz#e3dacab123a311f2f0d4a0369793fe90fdab7569" + integrity sha512-auO6trCo7ivLJYuLjxrnK4xuUTangVPTq8UuOMTlGbJFjmb8PLEkaXuRoVGSzv9jsT2FeS7KsP7Fs+yvv0WPdg== "@agile-ts/proxytree@file:.yalc/@agile-ts/proxytree": - version "0.0.1" + version "0.0.3" "@agile-ts/react@file:.yalc/@agile-ts/react": - version "0.0.16" + version "0.0.18" dependencies: - "@agile-ts/proxytree" "^0.0.2" + "@agile-ts/proxytree" "^0.0.3" -"@agile-ts/utils@^0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.2.tgz#5f03761ace569b6c9ddd28c22f7b0fbec8b006b1" - integrity sha512-LqgQyMdK+zDuTCmOX6FOxTH4JNXhEvGFqIyNqRDoP99BK6MHGrK+n7nOW+1b4x6ZCYe0+VmwtG5CeOPOm3Siow== +"@agile-ts/utils@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.4.tgz#66e9536e561796489a37155da6b74ce2dc482697" + integrity sha512-GiZyTYmCm4j2N57oDjeMuPpfQdgn9clb0Cxpfuwi2Bq5T/KPXlaROLsVGwHLjwwT+NX7xxr5qNJH8pZTnHnYRQ== "@babel/code-frame@7.8.3": version "7.8.3" diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index e4d982bb..91846493 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -281,21 +281,22 @@ export class Runtime { subscriptionContainer: SubscriptionContainer, job: RuntimeJob ): boolean { - const selectors = subscriptionContainer.selectorsWeakMap.get(job.observer) - ?.selectors; + const selectorMethods = subscriptionContainer.selectorsWeakMap.get( + job.observer + )?.methods; // If no selector functions found, return true // because no specific part of the Observer was selected // -> The Subscription Container should update // no matter what was updated in the Observer - if (selectors == null) return true; + if (selectorMethods == null) return true; // Check if a selected part of Observer value has changed const previousValue = job.observer.previousValue; const newValue = job.observer.value; - for (const selector of selectors) { + for (const selectorMethod of selectorMethods) { if ( - notEqual(selector(newValue), selector(previousValue)) + notEqual(selectorMethod(newValue), selectorMethod(previousValue)) // || newValueDeepness !== previousValueDeepness // Not possible to check the object deepness ) return true; diff --git a/packages/core/src/runtime/observer.ts b/packages/core/src/runtime/observer.ts index cfe7e7ce..6c7bc051 100644 --- a/packages/core/src/runtime/observer.ts +++ b/packages/core/src/runtime/observer.ts @@ -7,6 +7,7 @@ import { IngestConfigInterface, CreateRuntimeJobConfigInterface, LogCodeManager, + AddSubscriptionMethodConfigInterface, } from '../internal'; export type ObserverKey = string | number; @@ -140,8 +141,12 @@ export class Observer { * * @public * @param subscriptionContainer - Subscription Container to which the Observer should subscribe. + * @param config - Configuration object */ - public subscribe(subscriptionContainer: SubscriptionContainer): void { + public subscribe( + subscriptionContainer: SubscriptionContainer, + config: AddSubscriptionMethodConfigInterface = {} + ): void { if (!this.subscribedTo.has(subscriptionContainer)) { this.subscribedTo.add(subscriptionContainer); @@ -149,7 +154,7 @@ export class Observer { // to keep track of the Observers that have subscribed the Subscription Container. // For example to unsubscribe the subscribed Observers // when the Subscription Container (Component) unmounts. - subscriptionContainer.subscribers.add(this); + subscriptionContainer.addSubscription(this, config); } } @@ -162,7 +167,7 @@ export class Observer { public unsubscribe(subscriptionContainer: SubscriptionContainer): void { if (this.subscribedTo.has(subscriptionContainer)) { this.subscribedTo.delete(subscriptionContainer); - subscriptionContainer.subscribers.delete(this); + subscriptionContainer.removeSubscription(this); } } } diff --git a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts index aad3736b..9ba41abe 100644 --- a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts @@ -28,7 +28,7 @@ export class CallbackSubscriptionContainer extends SubscriptionContainer { */ constructor( callback: Function, - subs: Array = [], + subs: Array | { [key: string]: Observer }, config: SubscriptionContainerConfigInterface = {} ) { super(subs, config); diff --git a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts index 9e1daeef..4da1277f 100644 --- a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts @@ -57,7 +57,7 @@ export class ComponentSubscriptionContainer< */ constructor( component: C, - subs: Array = [], + subs: Array | { [key: string]: Observer }, config: SubscriptionContainerConfigInterface = {} ) { super(subs, config); diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index e5791cf9..10ae46bd 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -90,7 +90,7 @@ export class SubscriptionContainer { * @param config - Configuration object */ constructor( - subs: Array = [], + subs: Array | { [key: string]: Observer }, config: SubscriptionContainerConfigInterface = {} ) { config = defineConfig(config, { @@ -99,52 +99,78 @@ export class SubscriptionContainer { key: generateId(), }); - this.subscribers = new Set(subs); + this.subscribers = new Set(); this.key = config.key; this.componentId = config?.componentId; this.subscriberKeysWeakMap = new WeakMap(); + this.selectorsWeakMap = new WeakMap(); + this.isObjectBased = !Array.isArray(subs); - // Create a selector function for each specified proxy path - // that selects the property at the path end - const selectorWeakMap: SelectorWeakMapType = config.selectorWeakMap as any; - this.assignProxySelectors( - selectorWeakMap, - config.proxyWeakMap as any, - subs - ); - this.selectorsWeakMap = selectorWeakMap; + for (const key in subs) { + const sub = subs[key]; + this.addSubscription(sub, { + proxyPaths: config.proxyWeakMap?.get(sub)?.paths, + selectorMethods: config.selectorWeakMap?.get(sub)?.methods, + key: !Array.isArray(subs) ? key : undefined, + }); + } } /** - * Assigns to the specified `selectorWeakMap` selector functions - * created based on the paths of the specified Proxy WeakMap. + * Adds specified Observer to the `subscription` array + * and its selectors to the `selectorsWeakMap`. * - * @param selectorWeakMap - Selector WeakMap the created proxy selector functions are added to. - * @param proxyWeakMap - Proxy Weak Map containing proxy paths for specified Observers in `subs`. - * @param subs - Observers whose values are to be selected based on the specified `proxyWeakMap`. + * @internal + * @param sub - Observer to be subscribed to the Subscription Container + * @param config - Configuration object */ - public assignProxySelectors( - selectorWeakMap: SelectorWeakMapType, - proxyWeakMap: ProxyWeakMapType, - subs: Array + public addSubscription( + sub: Observer, + config: AddSubscriptionMethodConfigInterface = {} ): void { - for (const observer of subs) { - const paths = proxyWeakMap.get(observer)?.paths; - if (paths != null) { - const selectors: SelectorMethodType[] = []; - for (const path of paths) { - selectors.push((value) => { - let _value = value; - for (const branch of path) { - if (!isValidObject(_value, true)) break; - _value = _value[branch]; - } - return _value; - }); + const toAddSelectorMethods: SelectorMethodType[] = + config.selectorMethods ?? []; + const paths = config.proxyPaths ?? []; + + // Create selector methods based on the specified proxy paths + for (const path of paths) { + toAddSelectorMethods.push((value) => { + let _value = value; + for (const branch of path) { + if (!isValidObject(_value, true)) break; + _value = _value[branch]; } - selectorWeakMap.set(observer, { selectors }); - } + return _value; + }); } + + // Add defined/created selector methods to the 'selectorsWeakMap' + const existingSelectorMethods = this.selectorsWeakMap.get(sub)?.methods; + const newSelectorMethods = existingSelectorMethods + ? existingSelectorMethods.concat(toAddSelectorMethods) + : toAddSelectorMethods; + if (newSelectorMethods.length > 0) + this.selectorsWeakMap.set(sub, { methods: newSelectorMethods }); + + // Assign specified key to the 'subscriberKeysWeakMap' + // (Not to the Observer, since the here specified key only counts for this Subscription Container) + if (config.key != null) this.subscriberKeysWeakMap.set(sub, config.key); + + // Add Observer to subscribers + this.subscribers.add(sub); + } + + /** + * Removes the Observer from the Subscription Container + * and from all WeakMaps it might be in. + * + * @internal + * @param sub - Observer to be removed from the Subscription Container + */ + public removeSubscription(sub: Observer) { + this.selectorsWeakMap.delete(sub); + this.subscriberKeysWeakMap.delete(sub); + this.subscribers.delete(sub); } } @@ -200,12 +226,19 @@ export interface SubscriptionContainerConfigInterface { selectorWeakMap?: SelectorWeakMapType; } -export type ProxyWeakMapType = WeakMap; +export interface AddSubscriptionMethodConfigInterface { + proxyPaths?: ProxyPathType[]; + selectorMethods?: SelectorMethodType[]; + key?: string; +} + +export type ProxyPathType = string[]; +export type ProxyWeakMapType = WeakMap; +export type SelectorMethodType = (value: T) => any; export type SelectorWeakMapType = WeakMap< Observer, - { selectors: SelectorMethodType[] } + { methods: SelectorMethodType[] } >; -export type SelectorMethodType = (value: T) => any; export type ComponentIdType = string | number; diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index 37b56192..bfd4ee2c 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -37,7 +37,7 @@ export class SubController { } /** - * Creates a so called Subscription Container which represents an UI-Component in AgileTs. + * Creates a so called Subscription Container that represents an UI-Component in AgileTs. * Such Subscription Container know how to trigger a rerender on the UI-Component it represents * through the provided `integrationInstance`. * @@ -48,60 +48,21 @@ export class SubController { * For example by mutating a local State Management property. (Component based Subscription) * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) * - * The in an object specified Observers are then automatically subscribed + * The in an array specified Observers are then automatically subscribed * to the created Subscription Container and thus to the Component it represents. * - * The advantage of subscribing the Observer via a object keymap, - * is that each Observer has its own unique key identifier. - * Such key identifier is for example required when merging the Observer value into - * a local Component State Management property. - * ``` - * this.state = {...this.state, {state1: Observer1.value, state2: Observer2.value}} - * ``` - * * @public * @param integrationInstance - Callback function or Component Instance to trigger a rerender on a UI-Component. * @param subs - Observers to be subscribed to the to create Subscription Container. * @param config - Configuration object */ - public subscribeWithSubsObject( + public subscribe( integrationInstance: any, - subs: { [key: string]: Observer } = {}, - config: RegisterSubscriptionConfigInterface = {} - ): { - subscriptionContainer: SubscriptionContainer; - props: { [key: string]: Observer['value'] }; - } { - const props: { [key: string]: Observer['value'] } = {}; - - // Create Subscription Container - const subscriptionContainer = this.createSubscriptionContainer( - integrationInstance, - config - ); - - // Set SubscriptionContainer to object based - // and assign property keys to the 'subscriberKeysWeakMap' - subscriptionContainer.isObjectBased = true; - for (const key in subs) - subscriptionContainer.subscriberKeysWeakMap.set(subs[key], key); - - // Subscribe Observers to the created Subscription Container - // and build a Observer value keymap called props - for (const key in subs) { - const observer = subs[key]; - observer.subscribe(subscriptionContainer); - if (observer.value) props[key] = observer.value; - } - - return { - subscriptionContainer: subscriptionContainer, - props: props, - }; - } - + subs: Array, + config: RegisterSubscriptionConfigInterface + ): SubscriptionContainer; /** - * Creates a so called Subscription Container which represents an UI-Component in AgileTs. + * Creates a so called Subscription Container that represents an UI-Component in AgileTs. * Such Subscription Container know how to trigger a rerender on the UI-Component it represents * through the provided `integrationInstance`. * @@ -112,29 +73,73 @@ export class SubController { * For example by mutating a local State Management property. (Component based Subscription) * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) * - * The in an array specified Observers are then automatically subscribed + * The in an object specified Observers are then automatically subscribed * to the created Subscription Container and thus to the Component it represents. * + * The advantage of subscribing the Observer via a object keymap, + * is that each Observer has its own unique key identifier. + * Such key identifier is for example required when merging the Observer value into + * a local Component State Management property. + * ``` + * this.state = {...this.state, {state1: Observer1.value, state2: Observer2.value}} + * ``` + * * @public * @param integrationInstance - Callback function or Component Instance to trigger a rerender on a UI-Component. * @param subs - Observers to be subscribed to the to create Subscription Container. * @param config - Configuration object */ - public subscribeWithSubsArray( + public subscribe( integrationInstance: any, - subs: Array = [], + subs: { [key: string]: Observer }, + config: RegisterSubscriptionConfigInterface + ): { + subscriptionContainer: SubscriptionContainer; + props: { [key: string]: Observer['value'] }; + }; + public subscribe( + integrationInstance: any, + subs: { [key: string]: Observer } | Array, config: RegisterSubscriptionConfigInterface = {} - ): SubscriptionContainer { + ): + | SubscriptionContainer + | { + subscriptionContainer: SubscriptionContainer; + props: { [key: string]: Observer['value'] }; + } { + config = defineConfig(config, { + waitForMount: this.agileInstance().config.waitForMount, + }); + // Create Subscription Container - const subscriptionContainer = this.createSubscriptionContainer( - integrationInstance, - config - ); + const subscriptionContainer = isFunction(integrationInstance) + ? this.createCallbackSubscriptionContainer( + integrationInstance, + subs, + config + ) + : this.createComponentSubscriptionContainer( + integrationInstance, + subs, + config + ); + + const props: { [key: string]: Observer['value'] } = {}; // Subscribe Observers to the created Subscription Container - subs.forEach((observer) => observer.subscribe(subscriptionContainer)); + // and build an Observer value keymap called props + for (const key in subs) { + const observer = subs[key]; + observer.subscribedTo.add(subscriptionContainer); + if (observer.value) props[key] = observer.value; + } - return subscriptionContainer; + return Array.isArray(subs) + ? subscriptionContainer + : { + subscriptionContainer: subscriptionContainer, + props: props, + }; } /** @@ -198,40 +203,22 @@ export class SubController { } } - /** - * Returns a newly created Component or Callback based Subscription Container - * based on the specified `integrationInstance`. - * - * @internal - * @param integrationInstance - Callback function or Component Instance to trigger a rerender on a UI-Component. - * @param config - Configuration object - */ - public createSubscriptionContainer( - integrationInstance: any, - config: RegisterSubscriptionConfigInterface = {} - ): SubscriptionContainer { - config = defineConfig(config, { - waitForMount: this.agileInstance().config.waitForMount, - }); - return isFunction(integrationInstance) - ? this.createCallbackSubscriptionContainer(integrationInstance, config) - : this.createComponentSubscriptionContainer(integrationInstance, config); - } - /** * Returns a newly created Component based Subscription Container. * * @internal * @param componentInstance - Component Instance to trigger a rerender on a UI-Component. + * @param subs - Observers to be subscribed to the to create Subscription Container. * @param config - Configuration object. */ public createComponentSubscriptionContainer( componentInstance: any, + subs: { [key: string]: Observer } | Array, config: RegisterSubscriptionConfigInterface = {} ): ComponentSubscriptionContainer { const componentSubscriptionContainer = new ComponentSubscriptionContainer( componentInstance, - [], + subs, removeProperties(config, ['waitForMount']) ); this.componentSubs.add(componentSubscriptionContainer); @@ -268,15 +255,17 @@ export class SubController { * * @internal * @param callbackFunction - Callback function to trigger a rerender on a UI-Component. + * @param subs - Observers to be subscribed to the to create Subscription Container. * @param config - Configuration object */ public createCallbackSubscriptionContainer( callbackFunction: () => void, + subs: { [key: string]: Observer } | Array, config: RegisterSubscriptionConfigInterface = {} ): CallbackSubscriptionContainer { const callbackSubscriptionContainer = new CallbackSubscriptionContainer( callbackFunction, - [], + subs, removeProperties(config, ['waitForMount']) ); this.callbackSubs.add(callbackSubscriptionContainer); diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index f129fed4..a4fa90ec 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -519,60 +519,7 @@ describe('Runtime Tests', () => { ); }); - describe('handleObjectBasedSubscription function tests', () => { - let arraySubscriptionContainer: SubscriptionContainer; - const dummyComponent = { - my: 'cool component', - }; - let objectSubscriptionContainer: SubscriptionContainer; - const dummyComponent2 = { - my: 'second cool component', - }; - let arrayJob: RuntimeJob; - let objectJob1: RuntimeJob; - let objectJob2: RuntimeJob; - - beforeEach(() => { - arraySubscriptionContainer = dummyAgile.subController.subscribeWithSubsArray( - dummyComponent, - [dummyObserver1, dummyObserver2, dummyObserver3] - ); - arrayJob = new RuntimeJob(dummyObserver1, { key: 'dummyArrayJob' }); - - objectSubscriptionContainer = dummyAgile.subController.subscribeWithSubsObject( - dummyComponent2, - { - observer1: dummyObserver1, - observer2: dummyObserver2, - observer3: dummyObserver3, - } - ).subscriptionContainer; - objectJob1 = new RuntimeJob(dummyObserver1, { key: 'dummyObjectJob1' }); - objectJob2 = new RuntimeJob(dummyObserver3, { key: 'dummyObjectJob2' }); - }); - - it('should ignore not object based SubscriptionContainer', () => { - runtime.handleObjectBasedSubscription( - arraySubscriptionContainer, - arrayJob - ); - - expect(arraySubscriptionContainer.updatedSubscribers).toStrictEqual([]); - }); - - it('should add Job Observer to changedObjectKeys in SubscriptionContainer', () => { - runtime.handleObjectBasedSubscription( - objectSubscriptionContainer, - objectJob1 - ); - - expect(objectSubscriptionContainer.updatedSubscribers).toStrictEqual([ - 'observer1', - ]); - }); - }); - - describe('getObjectBasedProps function tests', () => { + describe('getUpdatedObserverValues function tests', () => { let subscriptionContainer: SubscriptionContainer; const dummyFunction = () => { /* empty function */ @@ -607,7 +554,7 @@ describe('Runtime Tests', () => { }); }); - describe('handleProxyBasedSubscription function tests', () => { + describe('handleSelector function tests', () => { let objectSubscriptionContainer: SubscriptionContainer; const dummyFunction = () => { /* empty function */ diff --git a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts index 80688764..ac6fcd3d 100644 --- a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts @@ -2,6 +2,8 @@ import { Agile, CallbackSubscriptionContainer, Observer, + ProxyWeakMapType, + SelectorWeakMapType, } from '../../../../../src'; import { LogMock } from '../../../../helper/logMock'; @@ -9,6 +11,8 @@ describe('CallbackSubscriptionContainer Tests', () => { let dummyAgile: Agile; let dummyObserver1: Observer; let dummyObserver2: Observer; + let dummySelectorWeakMap: SelectorWeakMapType; + let dummyProxyWeakMap: ProxyWeakMapType; beforeEach(() => { jest.clearAllMocks(); @@ -17,6 +21,8 @@ describe('CallbackSubscriptionContainer Tests', () => { dummyAgile = new Agile(); dummyObserver1 = new Observer(dummyAgile, { key: 'dummyObserver1' }); dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); + dummySelectorWeakMap = new WeakMap(); + dummyProxyWeakMap = new WeakMap(); }); it('should create CallbackSubscriptionContainer', () => { @@ -27,22 +33,27 @@ describe('CallbackSubscriptionContainer Tests', () => { const subscriptionContainer = new CallbackSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2], - { key: 'dummyKey', proxyKeyMap: { myState: { paths: [['hi']] } } } + { + key: 'dummyKey', + proxyWeakMap: dummyProxyWeakMap, + selectorWeakMap: dummySelectorWeakMap, + componentId: 'testID', + } ); expect(subscriptionContainer.callback).toBe(dummyIntegration); expect(subscriptionContainer.key).toBe('dummyKey'); expect(subscriptionContainer.ready).toBeFalsy(); + expect(subscriptionContainer.componentId).toBe('testID'); expect(subscriptionContainer.subscribers.size).toBe(2); expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy(); expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy(); - expect(subscriptionContainer.isObjectBased).toBeFalsy(); expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); - expect(subscriptionContainer.subscriberKeysWeakMap).toBeUndefined(); - expect(subscriptionContainer.proxyKeyMap).toStrictEqual({ - myState: { paths: [['hi']] }, - }); - expect(subscriptionContainer.isProxyBased).toBeTruthy(); + expect(subscriptionContainer.isObjectBased).toBeFalsy(); + expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( + expect.any(WeakMap) + ); + expect(subscriptionContainer.selectorsWeakMap).toBe(dummySelectorWeakMap); }); }); diff --git a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts index 786c115a..7da24042 100644 --- a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts @@ -2,6 +2,8 @@ import { Agile, ComponentSubscriptionContainer, Observer, + ProxyWeakMapType, + SelectorWeakMapType, } from '../../../../../src'; import { LogMock } from '../../../../helper/logMock'; @@ -9,6 +11,8 @@ describe('ComponentSubscriptionContainer Tests', () => { let dummyAgile: Agile; let dummyObserver1: Observer; let dummyObserver2: Observer; + let dummySelectorWeakMap: SelectorWeakMapType; + let dummyProxyWeakMap: ProxyWeakMapType; beforeEach(() => { jest.clearAllMocks(); @@ -17,6 +21,8 @@ describe('ComponentSubscriptionContainer Tests', () => { dummyAgile = new Agile(); dummyObserver1 = new Observer(dummyAgile, { key: 'dummyObserver1' }); dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); + dummySelectorWeakMap = new WeakMap(); + dummyProxyWeakMap = new WeakMap(); }); it('should create ComponentSubscriptionContainer', () => { @@ -25,22 +31,27 @@ describe('ComponentSubscriptionContainer Tests', () => { const subscriptionContainer = new ComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2], - { key: 'dummyKey', proxyKeyMap: { myState: { paths: [['hi']] } } } + { + key: 'dummyKey', + proxyWeakMap: dummyProxyWeakMap, + selectorWeakMap: dummySelectorWeakMap, + componentId: 'testID', + } ); expect(subscriptionContainer.component).toStrictEqual(dummyIntegration); expect(subscriptionContainer.key).toBe('dummyKey'); expect(subscriptionContainer.ready).toBeFalsy(); + expect(subscriptionContainer.componentId).toBe('testID'); expect(subscriptionContainer.subscribers.size).toBe(2); expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy(); expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy(); - expect(subscriptionContainer.isObjectBased).toBeFalsy(); expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); - expect(subscriptionContainer.subscriberKeysWeakMap).toBeUndefined(); - expect(subscriptionContainer.proxyKeyMap).toStrictEqual({ - myState: { paths: [['hi']] }, - }); - expect(subscriptionContainer.isProxyBased).toBeTruthy(); + expect(subscriptionContainer.isObjectBased).toBeFalsy(); + expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( + expect.any(WeakMap) + ); + expect(subscriptionContainer.selectorsWeakMap).toBe(dummySelectorWeakMap); }); }); diff --git a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts index ebc5fa91..61046089 100644 --- a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts @@ -1,4 +1,10 @@ -import { Agile, Observer, SubscriptionContainer } from '../../../../../src'; +import { + Agile, + Observer, + ProxyWeakMapType, + SelectorWeakMapType, + SubscriptionContainer, +} from '../../../../../src'; import * as Utils from '@agile-ts/utils'; import { LogMock } from '../../../../helper/logMock'; @@ -6,6 +12,8 @@ describe('SubscriptionContainer Tests', () => { let dummyAgile: Agile; let dummyObserver1: Observer; let dummyObserver2: Observer; + let dummySelectorWeakMap: SelectorWeakMapType; + let dummyProxyWeakMap: ProxyWeakMapType; beforeEach(() => { jest.clearAllMocks(); @@ -14,6 +22,10 @@ describe('SubscriptionContainer Tests', () => { dummyAgile = new Agile(); dummyObserver1 = new Observer(dummyAgile, { key: 'dummyObserver1' }); dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); + dummySelectorWeakMap = new WeakMap(); + dummyProxyWeakMap = new WeakMap(); + + jest.spyOn(SubscriptionContainer.prototype, 'assignProxySelectors'); }); it('should create SubscriptionContainer (default config)', () => { @@ -21,33 +33,74 @@ describe('SubscriptionContainer Tests', () => { const subscriptionContainer = new SubscriptionContainer(); + expect(subscriptionContainer.assignProxySelectors).toHaveBeenCalledWith( + expect.any(WeakMap), + expect.any(WeakMap), + [] + ); + expect(subscriptionContainer.key).toBe('generatedId'); expect(subscriptionContainer.ready).toBeFalsy(); + expect(subscriptionContainer.componentId).toBeUndefined(); expect(subscriptionContainer.subscribers.size).toBe(0); - expect(subscriptionContainer.isObjectBased).toBeFalsy(); expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); - expect(subscriptionContainer.subscriberKeysWeakMap).toBeUndefined(); - expect(subscriptionContainer.proxyKeyMap).toStrictEqual({}); - expect(subscriptionContainer.isProxyBased).toBeFalsy(); + expect(subscriptionContainer.isObjectBased).toBeFalsy(); + expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( + expect.any(WeakMap) + ); + expect(subscriptionContainer.selectorsWeakMap).not.toBe( + dummySelectorWeakMap + ); + expect(subscriptionContainer.selectorsWeakMap).toStrictEqual( + expect.any(WeakMap) + ); }); it('should create SubscriptionContainer (specific config)', () => { const subscriptionContainer = new SubscriptionContainer( [dummyObserver1, dummyObserver2], - { key: 'dummyKey', proxyKeyMap: { myState: { paths: [['a', 'b']] } } } + { + key: 'dummyKey', + proxyWeakMap: dummyProxyWeakMap, + selectorWeakMap: dummySelectorWeakMap, + componentId: 'testID', + } ); + expect( + subscriptionContainer.assignProxySelectors + ).toHaveBeenCalledWith(dummySelectorWeakMap, dummyProxyWeakMap, [ + dummyObserver1, + dummyObserver2, + ]); + expect(subscriptionContainer.key).toBe('dummyKey'); expect(subscriptionContainer.ready).toBeFalsy(); + expect(subscriptionContainer.componentId).toBe('testID'); expect(subscriptionContainer.subscribers.size).toBe(2); expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy(); expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy(); - expect(subscriptionContainer.isObjectBased).toBeFalsy(); expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); - expect(subscriptionContainer.subscriberKeysWeakMap).toBeUndefined(); - expect(subscriptionContainer.proxyKeyMap).toStrictEqual({ - myState: { paths: [['a', 'b']] }, + expect(subscriptionContainer.isObjectBased).toBeFalsy(); + expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( + expect.any(WeakMap) + ); + expect(subscriptionContainer.selectorsWeakMap).toBe(dummySelectorWeakMap); + }); + + describe('Subscription Container Function Tests', () => { + let observer: SubscriptionContainer; + + beforeEach(() => { + observer = new SubscriptionContainer(); + }); + + describe('assignProxySelectors function tests', () => { + beforeEach(() => {}); + + it('todo', () => { + // TODO + }); }); - expect(subscriptionContainer.isProxyBased).toBeTruthy(); }); }); diff --git a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts index 77634bd9..4ce359d4 100644 --- a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts +++ b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts @@ -45,9 +45,7 @@ describe('SubController Tests', () => { dummySubscriptionContainer = new SubscriptionContainer(); dummyObserver1.value = 'myCoolValue'; - subController.createSubscriptionContainer = jest.fn( - () => dummySubscriptionContainer - ); + subController.subscribe = jest.fn(() => dummySubscriptionContainer); jest.spyOn(dummyObserver1, 'subscribe'); jest.spyOn(dummyObserver2, 'subscribe'); }); @@ -73,7 +71,7 @@ describe('SubController Tests', () => { subscriptionContainer: dummySubscriptionContainer, }); - expect(subController.createSubscriptionContainer).toHaveBeenCalledWith( + expect(subController.subscribe).toHaveBeenCalledWith( dummyIntegration, [dummyObserver1, dummyObserver2], { @@ -113,9 +111,7 @@ describe('SubController Tests', () => { beforeEach(() => { dummySubscriptionContainer = new SubscriptionContainer(); - subController.createSubscriptionContainer = jest.fn( - () => dummySubscriptionContainer - ); + subController.subscribe = jest.fn(() => dummySubscriptionContainer); jest.spyOn(dummyObserver1, 'subscribe'); jest.spyOn(dummyObserver2, 'subscribe'); }); @@ -133,7 +129,7 @@ describe('SubController Tests', () => { expect(subscribeWithSubsArrayResponse).toBe(dummySubscriptionContainer); - expect(subController.createSubscriptionContainer).toHaveBeenCalledWith( + expect(subController.subscribe).toHaveBeenCalledWith( dummyIntegration, [dummyObserver1, dummyObserver2], { @@ -290,7 +286,7 @@ describe('SubController Tests', () => { /* empty function */ }; - const subscriptionContainer = subController.createSubscriptionContainer( + const subscriptionContainer = subController.subscribe( dummyIntegration, [dummyObserver1, dummyObserver2] ); @@ -313,7 +309,7 @@ describe('SubController Tests', () => { /* empty function */ }; - const subscriptionContainer = subController.createSubscriptionContainer( + const subscriptionContainer = subController.subscribe( dummyIntegration, [dummyObserver1, dummyObserver2], { key: 'niceKey', proxyKeyMap: {}, waitForMount: false } @@ -335,7 +331,7 @@ describe('SubController Tests', () => { it('should call registerComponentSubscription if passed integrationInstance is not a Function (default config)', () => { const dummyIntegration = { dummy: 'integration' }; - const subscriptionContainer = subController.createSubscriptionContainer( + const subscriptionContainer = subController.subscribe( dummyIntegration, [dummyObserver1, dummyObserver2] ); @@ -356,7 +352,7 @@ describe('SubController Tests', () => { it('should call registerComponentSubscription if passed integrationInstance is not a Function (specific config)', () => { const dummyIntegration = { dummy: 'integration' }; - const subscriptionContainer = subController.createSubscriptionContainer( + const subscriptionContainer = subController.subscribe( dummyIntegration, [dummyObserver1, dummyObserver2], { key: 'niceKey', proxyKeyMap: {}, waitForMount: false } diff --git a/packages/event/src/hooks/useEvent.ts b/packages/event/src/hooks/useEvent.ts index 1bb4e371..905d6413 100644 --- a/packages/event/src/hooks/useEvent.ts +++ b/packages/event/src/hooks/useEvent.ts @@ -27,7 +27,7 @@ export function useEvent>( } // Create Callback based Subscription - const subscriptionContainer = agileInstance.subController.subscribeWithSubsArray( + const subscriptionContainer = agileInstance.subController.subscribe( () => { forceRender(); }, diff --git a/packages/react/src/hocs/AgileHOC.ts b/packages/react/src/hocs/AgileHOC.ts index 8249b15c..189271d9 100644 --- a/packages/react/src/hocs/AgileHOC.ts +++ b/packages/react/src/hocs/AgileHOC.ts @@ -109,16 +109,14 @@ const createHOC = ( UNSAFE_componentWillMount() { // Create Subscription with Observer that have no Indicator and can't be merged into 'this.state' (Rerender will be caused via force Update) if (depsWithoutIndicator.length > 0) { - this.agileInstance.subController.subscribeWithSubsArray( - this, - depsWithoutIndicator, - { waitForMount: this.waitForMount } - ); + this.agileInstance.subController.subscribe(this, depsWithoutIndicator, { + waitForMount: this.waitForMount, + }); } // Create Subscription with Observer that have an Indicator (Rerender will be cause via mutating 'this.state') if (depsWithIndicator) { - const response = this.agileInstance.subController.subscribeWithSubsObject( + const response = this.agileInstance.subController.subscribe( this, depsWithIndicator, { waitForMount: this.waitForMount } diff --git a/packages/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index 5ddb00f1..bafcce1d 100644 --- a/packages/react/src/hooks/useAgile.ts +++ b/packages/react/src/hooks/useAgile.ts @@ -124,7 +124,7 @@ export function useAgile< } // Create Callback based Subscription - const subscriptionContainer = agileInstance.subController.subscribeWithSubsArray( + const subscriptionContainer = agileInstance.subController.subscribe( () => { forceRender(); }, diff --git a/packages/vue/src/bindAgileInstances.ts b/packages/vue/src/bindAgileInstances.ts index 0e89bdc9..451c759d 100644 --- a/packages/vue/src/bindAgileInstances.ts +++ b/packages/vue/src/bindAgileInstances.ts @@ -27,20 +27,16 @@ export function bindAgileInstances( // Create Subscription with Observer that have no Indicator and can't be merged into the 'sharedState' (Rerender will be caused via force Update) if (depsWithoutIndicator.length > 0) { - agile.subController.subscribeWithSubsArray( - vueComponent, - depsWithoutIndicator, - { waitForMount: false } - ); + agile.subController.subscribe(vueComponent, depsWithoutIndicator, { + waitForMount: false, + }); } // Create Subscription with Observer that have an Indicator (Rerender will be cause via mutating 'this.$data.sharedState') if (depsWithIndicator) { - return agile.subController.subscribeWithSubsObject( - vueComponent, - depsWithIndicator, - { waitForMount: false } - ).props; + return agile.subController.subscribe(vueComponent, depsWithIndicator, { + waitForMount: false, + }).props; } return {}; From f1f0806aeb0f8f64398ffd96f1dea8e56305abf5 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 8 Jun 2021 20:30:10 +0200 Subject: [PATCH 37/63] fixed typos --- .../functional-component-ts/src/App.tsx | 18 +++++--- .../functional-component-ts/src/core/index.ts | 27 ++++++----- examples/vue/develop/my-project/src/core.js | 10 +++-- examples/vue/develop/my-project/yarn.lock | 26 +++++------ packages/core/src/collection/index.ts | 6 ++- packages/core/src/computed/index.ts | 2 +- packages/core/src/runtime/observer.ts | 45 ++----------------- packages/core/src/runtime/runtime.job.ts | 7 ++- .../container/SubscriptionContainer.ts | 17 ++++--- .../runtime/subscription/sub.controller.ts | 2 +- .../core/tests/unit/computed/computed.test.ts | 24 ++++++---- .../core/tests/unit/runtime/observer.test.ts | 6 +-- 12 files changed, 94 insertions(+), 96 deletions(-) diff --git a/examples/react/develop/functional-component-ts/src/App.tsx b/examples/react/develop/functional-component-ts/src/App.tsx index ad7478e5..0574cf89 100644 --- a/examples/react/develop/functional-component-ts/src/App.tsx +++ b/examples/react/develop/functional-component-ts/src/App.tsx @@ -4,6 +4,7 @@ import { useAgile, useWatcher, useProxy } from '@agile-ts/react'; import { useEvent } from '@agile-ts/event'; import { COUNTUP, + externalCreatedItem, MY_COLLECTION, MY_COMPUTED, MY_EVENT, @@ -12,7 +13,7 @@ import { MY_STATE_3, STATE_OBJECT, } from './core'; -import { generateId, globalBind } from '@agile-ts/core'; +import { generateId, globalBind, Item } from '@agile-ts/core'; let rerenderCount = 0; let rerenderCountInCountupView = 0; @@ -42,11 +43,10 @@ const App = (props: any) => { ]); const [myGroup] = useAgile([MY_COLLECTION.getGroupWithReference('myGroup')]); - const [stateObject, item2, collection2] = useProxy([ - STATE_OBJECT, - MY_COLLECTION.getItem('id2'), - MY_COLLECTION, - ]); + const [stateObject, item2, collection2] = useProxy( + [STATE_OBJECT, MY_COLLECTION.getItem('id2'), MY_COLLECTION], + { key: 'useProxy' } + ); console.log('Item1: ', item2?.name); console.log('Collection: ', collection2.slice(0, 2)); @@ -142,6 +142,12 @@ const App = (props: any) => { }> Collect + diff --git a/examples/react/develop/functional-component-ts/src/core/index.ts b/examples/react/develop/functional-component-ts/src/core/index.ts index c55426d9..7c45b97a 100644 --- a/examples/react/develop/functional-component-ts/src/core/index.ts +++ b/examples/react/develop/functional-component-ts/src/core/index.ts @@ -1,10 +1,10 @@ -import { Agile, clone, Collection, Logger } from '@agile-ts/core'; +import { Agile, clone, Item, Logger } from '@agile-ts/core'; import Event from '@agile-ts/event'; export const myStorage: any = {}; export const App = new Agile({ - logConfig: { level: Logger.level.DEBUG }, + logConfig: { level: Logger.level.DEBUG, allowedTags: ['storage'] }, localStorage: true, }); @@ -84,15 +84,20 @@ export const MY_COLLECTION = App.createCollection( ], }) ).persist(); -MY_COLLECTION.collect({ key: 'id1', name: 'test' }); -MY_COLLECTION.collect({ key: 'id2', name: 'test2' }, 'myGroup'); -MY_COLLECTION.update('id1', { id: 'id1Updated', name: 'testUpdated' }); -MY_COLLECTION.getGroup('myGroup')?.persist({ - followCollectionPersistKeyPattern: true, -}); -MY_COLLECTION.onLoad(() => { - console.log('On Load MY_COLLECTION'); -}); +// MY_COLLECTION.collect({ key: 'id1', name: 'test' }); +// MY_COLLECTION.collect({ key: 'id2', name: 'test2' }, 'myGroup'); +// MY_COLLECTION.update('id1', { key: 'id1Updated', name: 'testUpdated' }); +// MY_COLLECTION.getGroup('myGroup')?.persist({ +// followCollectionPersistKeyPattern: true, +// }); +// MY_COLLECTION.onLoad(() => { +// console.log('On Load MY_COLLECTION'); +// }); + +export const externalCreatedItem = new Item(MY_COLLECTION, { + key: 'id10', + name: 'test', +}).persist({ followCollectionPersistKeyPattern: true }); console.log('Initial: myCollection ', clone(MY_COLLECTION)); diff --git a/examples/vue/develop/my-project/src/core.js b/examples/vue/develop/my-project/src/core.js index fe4efb2f..40527a9d 100644 --- a/examples/vue/develop/my-project/src/core.js +++ b/examples/vue/develop/my-project/src/core.js @@ -1,4 +1,4 @@ -import { Agile, Logger } from '@agile-ts/core'; +import { Agile, Logger, globalBind } from '@agile-ts/core'; import vueIntegration from '@agile-ts/vue'; // Create Agile Instance @@ -7,9 +7,13 @@ export const App = new Agile({ }).integrate(vueIntegration); // Create State -export const MY_STATE = App.createState('Hello World'); +export const MY_STATE = App.createState('Hello World', { key: 'my-state' }); // Create Collection export const TODOS = App.createCollection({ initialData: [{ id: 1, name: 'Clean Bathroom' }], -}).persist('todos'); +}); // .persist('todos'); + +// TODOS.collect({ id: 2, name: 'jeff' }); + +globalBind('__core__', { App, MY_STATE, TODOS }); diff --git a/examples/vue/develop/my-project/yarn.lock b/examples/vue/develop/my-project/yarn.lock index 9bd6b58a..04c336c0 100644 --- a/examples/vue/develop/my-project/yarn.lock +++ b/examples/vue/develop/my-project/yarn.lock @@ -3,25 +3,25 @@ "@agile-ts/core@file:.yalc/@agile-ts/core": - version "0.0.16" + version "0.0.17" dependencies: - "@agile-ts/logger" "^0.0.3" - "@agile-ts/utils" "^0.0.3" + "@agile-ts/logger" "^0.0.4" + "@agile-ts/utils" "^0.0.4" -"@agile-ts/logger@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@agile-ts/logger/-/logger-0.0.3.tgz#21f460bab99b5a1f50fbe6be95e1e9ed471ef456" - integrity sha512-8yejNCB7LXJ334smxovGaBWoqyXIUTHHO0/l2jPJt7WiMag0337KWbo1jyx6D8IkDioI9lunsN2U4CIBsRRhYA== +"@agile-ts/logger@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@agile-ts/logger/-/logger-0.0.4.tgz#7f4d82ef8f03b13089af0878c360575c43f0962d" + integrity sha512-qm0obAKqJMaPKM+c76gktRXyw3OL1v39AnhMZ0FBGwJqHWU+fLRkCzlQwjaROCr3F1XP01Lc/Ls3efF0WzyEPw== dependencies: - "@agile-ts/utils" "^0.0.3" + "@agile-ts/utils" "^0.0.4" -"@agile-ts/utils@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.3.tgz#f0e99d9ed9b21744f31effd99f7f7f32d26e3aec" - integrity sha512-h/gbPRRnFYxpIH4D0F/+6gVcZoZ2YPreT+cl8TCysjkjR6XnZ4YgC7patHIopX7ZvR97IMiu+BtpmS1UDbOftg== +"@agile-ts/utils@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.4.tgz#66e9536e561796489a37155da6b74ce2dc482697" + integrity sha512-GiZyTYmCm4j2N57oDjeMuPpfQdgn9clb0Cxpfuwi2Bq5T/KPXlaROLsVGwHLjwwT+NX7xxr5qNJH8pZTnHnYRQ== "@agile-ts/vue@file:.yalc/@agile-ts/vue": - version "0.0.4" + version "0.0.5" "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13": version "7.12.13" diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index e2d38d20..3fe709c5 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -97,7 +97,11 @@ export class Collection { // Rebuild of Groups // Not necessary because if Items are added to the Collection, // the Groups which contain these added Items are rebuilt. - // for (const key in this.groups) this.groups[key].rebuild(); + for (const key in this.groups) this.groups[key].rebuild(); + + // TODO ISSUE with collecting the 'initialData' before 'isInstantiated = true' + // if (_config.initialData) this.collect(_config.initialData); // TODO REMOVE + Agile.logger.debug('END of COLLECTION INSTANTIATION'); // TODO REMOVE } /** diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index 983fc42c..cea3cdc7 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -153,7 +153,7 @@ export class Computed extends State< newDeps.push(observer); // Make this Observer depend on the found dep Observers - observer.depend(this.observer); + observer.addDependent(this.observer); }); this.deps = newDeps; diff --git a/packages/core/src/runtime/observer.ts b/packages/core/src/runtime/observer.ts index 6c7bc051..34e8cf7d 100644 --- a/packages/core/src/runtime/observer.ts +++ b/packages/core/src/runtime/observer.ts @@ -7,7 +7,6 @@ import { IngestConfigInterface, CreateRuntimeJobConfigInterface, LogCodeManager, - AddSubscriptionMethodConfigInterface, } from '../internal'; export type ObserverKey = string | number; @@ -52,9 +51,9 @@ export class Observer { this._key = config.key; this.value = config.value; this.previousValue = config.value; - config.dependents?.forEach((observer) => this.depend(observer)); + config.dependents?.forEach((observer) => this.addDependent(observer)); config.subs?.forEach((subscriptionContainer) => - this.subscribe(subscriptionContainer) + subscriptionContainer.addSubscription(this) ); } @@ -129,47 +128,9 @@ export class Observer { * @public * @param observer - Observer to depends on this Observer. */ - public depend(observer: Observer): void { + public addDependent(observer: Observer): void { if (!this.dependents.has(observer)) this.dependents.add(observer); } - - /** - * Subscribes Observer to the specified Subscription Container (Component). - * - * Every time this Observer is ingested into the Runtime, - * a rerender might be triggered on the Component the Subscription Container represents. - * - * @public - * @param subscriptionContainer - Subscription Container to which the Observer should subscribe. - * @param config - Configuration object - */ - public subscribe( - subscriptionContainer: SubscriptionContainer, - config: AddSubscriptionMethodConfigInterface = {} - ): void { - if (!this.subscribedTo.has(subscriptionContainer)) { - this.subscribedTo.add(subscriptionContainer); - - // Add Observer to Subscription Container - // to keep track of the Observers that have subscribed the Subscription Container. - // For example to unsubscribe the subscribed Observers - // when the Subscription Container (Component) unmounts. - subscriptionContainer.addSubscription(this, config); - } - } - - /** - * Unsubscribes Observer from specified Subscription Container (Component). - * - * @public - * @param subscriptionContainer - Subscription Container that the Observer should unsubscribe. - */ - public unsubscribe(subscriptionContainer: SubscriptionContainer): void { - if (this.subscribedTo.has(subscriptionContainer)) { - this.subscribedTo.delete(subscriptionContainer); - subscriptionContainer.removeSubscription(this); - } - } } /** diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts index 25420d8d..9a459acb 100644 --- a/packages/core/src/runtime/runtime.job.ts +++ b/packages/core/src/runtime/runtime.job.ts @@ -1,4 +1,9 @@ -import { Observer, defineConfig, SubscriptionContainer } from '../internal'; +import { + Observer, + defineConfig, + SubscriptionContainer, + Agile, +} from '../internal'; export class RuntimeJob { public config: RuntimeJobConfigInterface; diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 10ae46bd..74cfd1e7 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -144,7 +144,7 @@ export class SubscriptionContainer { }); } - // Add defined/created selector methods to the 'selectorsWeakMap' + // Assign defined/created selector methods to the 'selectorsWeakMap' const existingSelectorMethods = this.selectorsWeakMap.get(sub)?.methods; const newSelectorMethods = existingSelectorMethods ? existingSelectorMethods.concat(toAddSelectorMethods) @@ -153,11 +153,15 @@ export class SubscriptionContainer { this.selectorsWeakMap.set(sub, { methods: newSelectorMethods }); // Assign specified key to the 'subscriberKeysWeakMap' - // (Not to the Observer, since the here specified key only counts for this Subscription Container) + // (Not to the Observer itself, since the key specified here only counts for this Subscription Container) if (config.key != null) this.subscriberKeysWeakMap.set(sub, config.key); // Add Observer to subscribers this.subscribers.add(sub); + + // Add Subscription Container to Observer + // so that it can be updated when to Observer changes + sub.subscribedTo.add(this); } /** @@ -168,9 +172,12 @@ export class SubscriptionContainer { * @param sub - Observer to be removed from the Subscription Container */ public removeSubscription(sub: Observer) { - this.selectorsWeakMap.delete(sub); - this.subscriberKeysWeakMap.delete(sub); - this.subscribers.delete(sub); + if (this.subscribers.has(sub)) { + this.selectorsWeakMap.delete(sub); + this.subscriberKeysWeakMap.delete(sub); + this.subscribers.delete(sub); + sub.subscribedTo.delete(this); + } } } diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index bfd4ee2c..78ad537d 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -158,7 +158,7 @@ export class SubController { const unsub = (subscriptionContainer: SubscriptionContainer) => { subscriptionContainer.ready = false; subscriptionContainer.subscribers.forEach((observer) => { - observer.unsubscribe(subscriptionContainer); + subscriptionContainer.removeSubscription(observer); }); }; diff --git a/packages/core/tests/unit/computed/computed.test.ts b/packages/core/tests/unit/computed/computed.test.ts index 9f645220..2dd8a4d3 100644 --- a/packages/core/tests/unit/computed/computed.test.ts +++ b/packages/core/tests/unit/computed/computed.test.ts @@ -232,9 +232,9 @@ describe('Computed Tests', () => { computed.hardCodedDeps = [dummyObserver3]; computed.deps = [dummyObserver3]; // normally the hardCodedDeps get automatically added to the deps.. but this time we set the hardCodedProperty after the instantiation - dummyObserver1.depend = jest.fn(); - dummyObserver2.depend = jest.fn(); - dummyObserver3.depend = jest.fn(); + dummyObserver1.addDependent = jest.fn(); + dummyObserver2.addDependent = jest.fn(); + dummyObserver3.addDependent = jest.fn(); jest.spyOn(ComputedTracker, 'track').mockClear(); // mockClear because otherwise the static mock doesn't get reset after each 'it' test jest.spyOn(ComputedTracker, 'getTrackedObservers').mockClear(); }); @@ -257,9 +257,15 @@ describe('Computed Tests', () => { dummyObserver1, dummyObserver2, ]); - expect(dummyObserver1.depend).toHaveBeenCalledWith(computed.observer); - expect(dummyObserver2.depend).toHaveBeenCalledWith(computed.observer); - expect(dummyObserver3.depend).toHaveBeenCalledWith(computed.observer); + expect(dummyObserver1.addDependent).toHaveBeenCalledWith( + computed.observer + ); + expect(dummyObserver2.addDependent).toHaveBeenCalledWith( + computed.observer + ); + expect(dummyObserver3.addDependent).toHaveBeenCalledWith( + computed.observer + ); }); it("should call computeFunction and shouldn't track dependencies the computeFunction depends on (autodetect false)", () => { @@ -273,9 +279,9 @@ describe('Computed Tests', () => { expect(ComputedTracker.getTrackedObservers).not.toHaveBeenCalled(); expect(computed.hardCodedDeps).toStrictEqual([dummyObserver3]); expect(computed.deps).toStrictEqual([dummyObserver3]); - expect(dummyObserver1.depend).not.toHaveBeenCalled(); - expect(dummyObserver2.depend).not.toHaveBeenCalled(); - expect(dummyObserver3.depend).not.toHaveBeenCalled(); + expect(dummyObserver1.addDependent).not.toHaveBeenCalled(); + expect(dummyObserver2.addDependent).not.toHaveBeenCalled(); + expect(dummyObserver3.addDependent).not.toHaveBeenCalled(); }); }); diff --git a/packages/core/tests/unit/runtime/observer.test.ts b/packages/core/tests/unit/runtime/observer.test.ts index b833ac97..11c9ff5b 100644 --- a/packages/core/tests/unit/runtime/observer.test.ts +++ b/packages/core/tests/unit/runtime/observer.test.ts @@ -158,16 +158,16 @@ describe('Observer Tests', () => { }); it('should add passed Observer to deps', () => { - observer.depend(dummyObserver1); + observer.addDependent(dummyObserver1); expect(observer.dependents.size).toBe(1); expect(observer.dependents.has(dummyObserver2)); }); it("shouldn't add the same Observer twice to deps", () => { - observer.depend(dummyObserver1); + observer.addDependent(dummyObserver1); - observer.depend(dummyObserver1); + observer.addDependent(dummyObserver1); expect(observer.dependents.size).toBe(1); expect(observer.dependents.has(dummyObserver1)); From 9c3a9ac81dc2f92f383a1619a7c6baa1ae23bad1 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 9 Jun 2021 06:35:16 +0200 Subject: [PATCH 38/63] fixed adding Item before 'isInstantiated = true' issue --- examples/vue/develop/my-project/src/core.js | 1 + packages/core/src/collection/index.ts | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/vue/develop/my-project/src/core.js b/examples/vue/develop/my-project/src/core.js index 40527a9d..8dafcb95 100644 --- a/examples/vue/develop/my-project/src/core.js +++ b/examples/vue/develop/my-project/src/core.js @@ -12,6 +12,7 @@ export const MY_STATE = App.createState('Hello World', { key: 'my-state' }); // Create Collection export const TODOS = App.createCollection({ initialData: [{ id: 1, name: 'Clean Bathroom' }], + selectors: [1], }); // .persist('todos'); // TODOS.collect({ id: 2, name: 'jeff' }); diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 3fe709c5..8ac6a0e0 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -85,23 +85,22 @@ export class Collection { this.initGroups(_config.groups as any); this.initSelectors(_config.selectors as any); - if (_config.initialData) this.collect(_config.initialData); - this.isInstantiated = true; + // Add 'initialData' to Collection + // (after 'isInstantiated' to add them properly to the Collection) + if (_config.initialData) this.collect(_config.initialData); + // Reselect Selector Items // Necessary because the selection of an Item - // hasn't worked with a not 'instantiated' Collection before + // hasn't worked with a not correctly 'instantiated' Collection before for (const key in this.selectors) this.selectors[key].reselect(); // Rebuild of Groups // Not necessary because if Items are added to the Collection, + // (after 'isInstantiated = true') // the Groups which contain these added Items are rebuilt. - for (const key in this.groups) this.groups[key].rebuild(); - - // TODO ISSUE with collecting the 'initialData' before 'isInstantiated = true' - // if (_config.initialData) this.collect(_config.initialData); // TODO REMOVE - Agile.logger.debug('END of COLLECTION INSTANTIATION'); // TODO REMOVE + // for (const key in this.groups) this.groups[key].rebuild(); } /** From 66aa53a1720ed4b65aa8824d8f7903b3ad4baaee Mon Sep 17 00:00:00 2001 From: BennoDev Date: Wed, 9 Jun 2021 13:50:08 +0200 Subject: [PATCH 39/63] documented loading collection value prints many warnings issue in code --- examples/vue/develop/my-project/src/core.js | 2 +- .../core/src/collection/collection.persistent.ts | 12 ++++++++++-- packages/core/src/state/state.persistent.ts | 5 ++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/vue/develop/my-project/src/core.js b/examples/vue/develop/my-project/src/core.js index 8dafcb95..ecb19e8b 100644 --- a/examples/vue/develop/my-project/src/core.js +++ b/examples/vue/develop/my-project/src/core.js @@ -13,7 +13,7 @@ export const MY_STATE = App.createState('Hello World', { key: 'my-state' }); export const TODOS = App.createCollection({ initialData: [{ id: 1, name: 'Clean Bathroom' }], selectors: [1], -}); // .persist('todos'); +}).persist('todos'); // TODOS.collect({ id: 2, name: 'jeff' }); diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index d7e099a8..380451b8 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -138,6 +138,14 @@ export class CollectionPersistent< if (defaultGroup.persistent?.ready) await defaultGroup.persistent.initialLoading(); + // TODO rebuild the default Group once at the end when all Items were loaded into the Collection + // because otherwise it rebuilds the Group for each loaded Item + // (-> warnings are printed for all not yet loaded Items when rebuilding the Group) + // or rethink the whole Group rebuild process by adding a 'addItem()', 'removeItem()' and 'updateItem()' function + // so that there is no need for rebuilding the whole Group when for example only Item B changed or Item C was added + // + // See Issue by starting the vue develop example app and adding some todos to the _todo_ list + // Persist Items found in the default Group's value for (const itemKey of defaultGroup._value) { const item = this.collection().getItem(itemKey); @@ -170,11 +178,11 @@ export class CollectionPersistent< if (dummyItem?.persistent?.ready) { const loadedPersistedValueIntoItem = await dummyItem.persistent.loadPersistedValue( itemStorageKey - ); + ); // TODO FIRST GROUP REBUILD (by assigning loaded value to Item) // If successfully loaded Item value, assign Item to Collection if (loadedPersistedValueIntoItem) - this.collection().assignItem(dummyItem); + this.collection().assignItem(dummyItem); // TODO SECOND GROUP REBUILD (by calling rebuildGroupThatInclude() in the assignItem() method) } } } diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index da15578c..89a7063a 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -107,7 +107,10 @@ export class StatePersistent extends Persistent { if (loadedValue == null) return false; // Assign loaded Value to State - this.state().set(loadedValue, { storage: false, overwrite: true }); + this.state().set(loadedValue, { + storage: false, + overwrite: true, + }); // Setup Side Effects to keep the Storage value in sync with the State value this.setupSideEffects(storageItemKey); From 326e6ff4cfc5b27e08f57f9c34da7b85d5f6c119 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 9 Jun 2021 16:28:51 +0200 Subject: [PATCH 40/63] added basic tests for SubscriptionContainer --- .../functional-component-ts/src/core/index.ts | 20 +- .../container/SubscriptionContainer.ts | 12 +- .../CallbackSubscriptionContainer.test.ts | 7 +- .../ComponentSubscriptionContainer.test.ts | 7 +- .../container/SubscriptionContainer.test.ts | 277 ++++++++++++++++-- 5 files changed, 279 insertions(+), 44 deletions(-) diff --git a/examples/react/develop/functional-component-ts/src/core/index.ts b/examples/react/develop/functional-component-ts/src/core/index.ts index 7c45b97a..cf448f46 100644 --- a/examples/react/develop/functional-component-ts/src/core/index.ts +++ b/examples/react/develop/functional-component-ts/src/core/index.ts @@ -4,7 +4,7 @@ import Event from '@agile-ts/event'; export const myStorage: any = {}; export const App = new Agile({ - logConfig: { level: Logger.level.DEBUG, allowedTags: ['storage'] }, + logConfig: { level: Logger.level.DEBUG }, localStorage: true, }); @@ -84,15 +84,15 @@ export const MY_COLLECTION = App.createCollection( ], }) ).persist(); -// MY_COLLECTION.collect({ key: 'id1', name: 'test' }); -// MY_COLLECTION.collect({ key: 'id2', name: 'test2' }, 'myGroup'); -// MY_COLLECTION.update('id1', { key: 'id1Updated', name: 'testUpdated' }); -// MY_COLLECTION.getGroup('myGroup')?.persist({ -// followCollectionPersistKeyPattern: true, -// }); -// MY_COLLECTION.onLoad(() => { -// console.log('On Load MY_COLLECTION'); -// }); +MY_COLLECTION.collect({ key: 'id1', name: 'test' }); +MY_COLLECTION.collect({ key: 'id2', name: 'test2' }, 'myGroup'); +MY_COLLECTION.update('id1', { key: 'id1Updated', name: 'testUpdated' }); +MY_COLLECTION.getGroup('myGroup')?.persist({ + followCollectionPersistKeyPattern: true, +}); +MY_COLLECTION.onLoad(() => { + console.log('On Load MY_COLLECTION'); +}); export const externalCreatedItem = new Item(MY_COLLECTION, { key: 'id10', diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 74cfd1e7..e9f345bc 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -145,10 +145,11 @@ export class SubscriptionContainer { } // Assign defined/created selector methods to the 'selectorsWeakMap' - const existingSelectorMethods = this.selectorsWeakMap.get(sub)?.methods; - const newSelectorMethods = existingSelectorMethods - ? existingSelectorMethods.concat(toAddSelectorMethods) - : toAddSelectorMethods; + const existingSelectorMethods = + this.selectorsWeakMap.get(sub)?.methods ?? []; + const newSelectorMethods = existingSelectorMethods.concat( + toAddSelectorMethods + ); if (newSelectorMethods.length > 0) this.selectorsWeakMap.set(sub, { methods: newSelectorMethods }); @@ -160,7 +161,8 @@ export class SubscriptionContainer { this.subscribers.add(sub); // Add Subscription Container to Observer - // so that it can be updated when to Observer changes + // so that it can be updated (cause rerender on the Component it represents) + // when for example the Observer value changes sub.subscribedTo.add(this); } diff --git a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts index ac6fcd3d..2edeff7c 100644 --- a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts @@ -54,6 +54,11 @@ describe('CallbackSubscriptionContainer Tests', () => { expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( expect.any(WeakMap) ); - expect(subscriptionContainer.selectorsWeakMap).toBe(dummySelectorWeakMap); + expect(subscriptionContainer.selectorsWeakMap).toStrictEqual( + expect.any(WeakMap) + ); + expect(subscriptionContainer.selectorsWeakMap).not.toBe( + dummySelectorWeakMap + ); }); }); diff --git a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts index 7da24042..6ebb14fc 100644 --- a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts @@ -52,6 +52,11 @@ describe('ComponentSubscriptionContainer Tests', () => { expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( expect.any(WeakMap) ); - expect(subscriptionContainer.selectorsWeakMap).toBe(dummySelectorWeakMap); + expect(subscriptionContainer.selectorsWeakMap).toStrictEqual( + expect.any(WeakMap) + ); + expect(subscriptionContainer.selectorsWeakMap).not.toBe( + dummySelectorWeakMap + ); }); }); diff --git a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts index 61046089..8a7cb06a 100644 --- a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts @@ -24,39 +24,112 @@ describe('SubscriptionContainer Tests', () => { dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); dummySelectorWeakMap = new WeakMap(); dummyProxyWeakMap = new WeakMap(); - - jest.spyOn(SubscriptionContainer.prototype, 'assignProxySelectors'); }); - it('should create SubscriptionContainer (default config)', () => { - jest.spyOn(Utils, 'generateId').mockReturnValue('generatedId'); + it('should create SubscriptionContainer with passed subs array (default config)', () => { + jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedId'); + jest + .spyOn(SubscriptionContainer.prototype, 'addSubscription') + .mockReturnValueOnce() + .mockReturnValueOnce(); - const subscriptionContainer = new SubscriptionContainer(); + const subscriptionContainer = new SubscriptionContainer([ + dummyObserver1, + dummyObserver2, + ]); - expect(subscriptionContainer.assignProxySelectors).toHaveBeenCalledWith( - expect.any(WeakMap), - expect.any(WeakMap), - [] + expect(subscriptionContainer.addSubscription).toHaveBeenCalledTimes(2); + expect(subscriptionContainer.addSubscription).toHaveBeenCalledWith( + dummyObserver1, + { + proxyPaths: undefined, + selectorMethods: undefined, + key: undefined, + } + ); + expect(subscriptionContainer.addSubscription).toHaveBeenCalledWith( + dummyObserver2, + { + proxyPaths: undefined, + selectorMethods: undefined, + key: undefined, + } ); expect(subscriptionContainer.key).toBe('generatedId'); expect(subscriptionContainer.ready).toBeFalsy(); expect(subscriptionContainer.componentId).toBeUndefined(); - expect(subscriptionContainer.subscribers.size).toBe(0); + expect(subscriptionContainer.subscribers.size).toBe(0); // because of mocking addSubscription expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); expect(subscriptionContainer.isObjectBased).toBeFalsy(); expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( expect.any(WeakMap) ); - expect(subscriptionContainer.selectorsWeakMap).not.toBe( - dummySelectorWeakMap + expect(subscriptionContainer.selectorsWeakMap).toStrictEqual( + expect.any(WeakMap) + ); + }); + + it('should create SubscriptionContainer with passed subs object (default config)', () => { + jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedId'); + jest + .spyOn(SubscriptionContainer.prototype, 'addSubscription') + .mockReturnValueOnce() + .mockReturnValueOnce(); + + const subscriptionContainer = new SubscriptionContainer({ + dummyObserver1: dummyObserver1, + dummyObserver2: dummyObserver2, + }); + + expect(subscriptionContainer.addSubscription).toHaveBeenCalledTimes(2); + expect(subscriptionContainer.addSubscription).toHaveBeenCalledWith( + dummyObserver1, + { + proxyPaths: undefined, + selectorMethods: undefined, + key: 'dummyObserver1', + } + ); + expect(subscriptionContainer.addSubscription).toHaveBeenCalledWith( + dummyObserver2, + { + proxyPaths: undefined, + selectorMethods: undefined, + key: 'dummyObserver2', + } + ); + + expect(subscriptionContainer.key).toBe('generatedId'); + expect(subscriptionContainer.ready).toBeFalsy(); + expect(subscriptionContainer.componentId).toBeUndefined(); + expect(subscriptionContainer.subscribers.size).toBe(0); // because of mocking addSubscription + expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); + expect(subscriptionContainer.isObjectBased).toBeTruthy(); + expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( + expect.any(WeakMap) ); expect(subscriptionContainer.selectorsWeakMap).toStrictEqual( expect.any(WeakMap) ); }); - it('should create SubscriptionContainer (specific config)', () => { + it('should create SubscriptionContainer with passed subs array (specific config)', () => { + jest + .spyOn(SubscriptionContainer.prototype, 'addSubscription') + .mockReturnValueOnce() + .mockReturnValueOnce(); + + dummyProxyWeakMap.set(dummyObserver1, { + paths: 'dummyObserver1_paths' as any, + }); + dummyProxyWeakMap.set(dummyObserver2, { + paths: 'dummyObserver2_paths' as any, + }); + dummySelectorWeakMap.set(dummyObserver2, { + methods: 'dummyObserver2_selectors' as any, + }); + const subscriptionContainer = new SubscriptionContainer( [dummyObserver1, dummyObserver2], { @@ -67,39 +140,189 @@ describe('SubscriptionContainer Tests', () => { } ); - expect( - subscriptionContainer.assignProxySelectors - ).toHaveBeenCalledWith(dummySelectorWeakMap, dummyProxyWeakMap, [ + expect(subscriptionContainer.addSubscription).toHaveBeenCalledTimes(2); + expect(subscriptionContainer.addSubscription).toHaveBeenCalledWith( dummyObserver1, + { + proxyPaths: 'dummyObserver1_paths', + selectorMethods: undefined, + key: undefined, + } + ); + expect(subscriptionContainer.addSubscription).toHaveBeenCalledWith( dummyObserver2, - ]); + { + proxyPaths: 'dummyObserver2_paths', + selectorMethods: 'dummyObserver2_selectors', + key: undefined, + } + ); expect(subscriptionContainer.key).toBe('dummyKey'); expect(subscriptionContainer.ready).toBeFalsy(); expect(subscriptionContainer.componentId).toBe('testID'); - expect(subscriptionContainer.subscribers.size).toBe(2); - expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy(); - expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy(); + expect(subscriptionContainer.subscribers.size).toBe(0); // because of mocking addSubscription expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); expect(subscriptionContainer.isObjectBased).toBeFalsy(); expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( expect.any(WeakMap) ); - expect(subscriptionContainer.selectorsWeakMap).toBe(dummySelectorWeakMap); + expect(subscriptionContainer.selectorsWeakMap).toStrictEqual( + expect.any(WeakMap) + ); + expect(subscriptionContainer.selectorsWeakMap).not.toBe( + dummySelectorWeakMap + ); }); describe('Subscription Container Function Tests', () => { - let observer: SubscriptionContainer; + let subscriptionContainer: SubscriptionContainer; beforeEach(() => { - observer = new SubscriptionContainer(); + subscriptionContainer = new SubscriptionContainer([]); + }); + + describe('addSubscription function tests', () => { + it( + 'should create selectors based on the specified proxies, ' + + 'assigns newly created or provided selectors to the selectorsWeakMap ' + + 'and subscribe the specified Observer to the SubscriptionContainer', + () => { + dummyObserver1.value = { + das: { haus: { vom: 'nikolaus' } }, + alle: { meine: 'entchien' }, + test1: 'test1Value', + test2: 'test2Value', + test3: 'test3Value', + }; + subscriptionContainer.selectorsWeakMap.set(dummyObserver1, { + methods: [(value) => value.test3], + }); + subscriptionContainer.selectorsWeakMap.set(dummyObserver2, { + methods: [(value) => 'doesNotMatter'], + }); + subscriptionContainer.subscriberKeysWeakMap.set( + dummyObserver2, + 'dummyObserver2' + ); + + subscriptionContainer.addSubscription(dummyObserver1, { + key: 'dummyObserver1', + proxyPaths: [['das', 'haus', 'vom'], ['test1']], + selectorMethods: [ + (value) => value.alle.meine, + (value) => value.test2, + ], + }); + + expect(subscriptionContainer.subscribers.size).toBe(1); + expect( + subscriptionContainer.subscribers.has(dummyObserver1) + ).toBeTruthy(); + expect(dummyObserver1.subscribedTo.size).toBe(1); + expect( + dummyObserver1.subscribedTo.has(subscriptionContainer) + ).toBeTruthy(); + + // should assign specified selectors/(and selectors created from proxy paths) to the selectorsWeakMap + const observer1Selector = subscriptionContainer.selectorsWeakMap.get( + dummyObserver1 + ) as any; + expect(observer1Selector.methods.length).toBe(5); + expect(observer1Selector.methods[0](dummyObserver1.value)).toBe( + 'test3Value' + ); + expect(observer1Selector.methods[1](dummyObserver1.value)).toBe( + 'entchien' + ); + expect(observer1Selector.methods[2](dummyObserver1.value)).toBe( + 'test2Value' + ); + expect(observer1Selector.methods[3](dummyObserver1.value)).toBe( + 'nikolaus' + ); + expect(observer1Selector.methods[4](dummyObserver1.value)).toBe( + 'test1Value' + ); + + // shouldn't overwrite already set values in selectorsWeakMap + const observer2Selector = subscriptionContainer.selectorsWeakMap.get( + dummyObserver2 + ) as any; + expect(observer2Selector.methods.length).toBe(1); + expect(observer2Selector.methods[0](null)).toBe('doesNotMatter'); + + // should assign specified key to the subscriberKeysWeakMap + const observer1Key = subscriptionContainer.subscriberKeysWeakMap.get( + dummyObserver1 + ); + expect(observer1Key).toBe('dummyObserver1'); + + // shouldn't overwrite already set values in subscriberKeysWeakMap + const observer2Key = subscriptionContainer.subscriberKeysWeakMap.get( + dummyObserver2 + ); + expect(observer2Key).toBe('dummyObserver2'); + } + ); }); - describe('assignProxySelectors function tests', () => { - beforeEach(() => {}); + describe('removeSubscription function tests', () => { + let subscriptionContainer: SubscriptionContainer; + + beforeEach(() => { + subscriptionContainer = new SubscriptionContainer([]); + + subscriptionContainer.subscribers = new Set([ + dummyObserver1, + dummyObserver2, + ]); + dummyObserver1.subscribedTo = new Set([subscriptionContainer]); + dummyObserver2.subscribedTo = new Set([subscriptionContainer]); + + subscriptionContainer.selectorsWeakMap.set(dummyObserver1, { + methods: [], + }); + subscriptionContainer.selectorsWeakMap.set(dummyObserver2, { + methods: [], + }); + subscriptionContainer.subscriberKeysWeakMap.set( + dummyObserver1, + 'dummyObserver1' + ); + subscriptionContainer.subscriberKeysWeakMap.set( + dummyObserver2, + 'dummyObserver2' + ); + }); + + it('should remove subscribed Observer from Subscription Container', () => { + subscriptionContainer.removeSubscription(dummyObserver1); + + expect(subscriptionContainer.subscribers.size).toBe(1); + expect( + subscriptionContainer.subscribers.has(dummyObserver2) + ).toBeTruthy(); + + expect( + subscriptionContainer.selectorsWeakMap.get(dummyObserver1) + ).toBeUndefined(); + expect( + subscriptionContainer.selectorsWeakMap.get(dummyObserver2) + ).not.toBeUndefined(); + + expect( + subscriptionContainer.subscriberKeysWeakMap.get(dummyObserver1) + ).toBeUndefined(); + expect( + subscriptionContainer.subscriberKeysWeakMap.get(dummyObserver2) + ).toBe('dummyObserver2'); - it('todo', () => { - // TODO + expect(dummyObserver1.subscribedTo.size).toBe(0); + expect(dummyObserver2.subscribedTo.size).toBe(1); + expect( + dummyObserver2.subscribedTo.has(subscriptionContainer) + ).toBeTruthy(); }); }); }); From 5556af0b6a12417c0fa7e62d00da595cd63957f1 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 9 Jun 2021 18:25:32 +0200 Subject: [PATCH 41/63] added basic subController tests --- .../runtime/subscription/sub.controller.ts | 25 +- .../subscription/sub.controller.test.ts | 726 +++++++++--------- 2 files changed, 357 insertions(+), 394 deletions(-) diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index 78ad537d..72f979bc 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -59,7 +59,7 @@ export class SubController { public subscribe( integrationInstance: any, subs: Array, - config: RegisterSubscriptionConfigInterface + config?: RegisterSubscriptionConfigInterface ): SubscriptionContainer; /** * Creates a so called Subscription Container that represents an UI-Component in AgileTs. @@ -92,7 +92,7 @@ export class SubController { public subscribe( integrationInstance: any, subs: { [key: string]: Observer }, - config: RegisterSubscriptionConfigInterface + config?: RegisterSubscriptionConfigInterface ): { subscriptionContainer: SubscriptionContainer; props: { [key: string]: Observer['value'] }; @@ -124,22 +124,17 @@ export class SubController { config ); - const props: { [key: string]: Observer['value'] } = {}; + // Return object based Subscription Container + if (subscriptionContainer.isObjectBased && !Array.isArray(subs)) { + // Build an Observer value keymap + const props: { [key: string]: Observer['value'] } = {}; + for (const key in subs) if (subs[key].value) props[key] = subs[key].value; - // Subscribe Observers to the created Subscription Container - // and build an Observer value keymap called props - for (const key in subs) { - const observer = subs[key]; - observer.subscribedTo.add(subscriptionContainer); - if (observer.value) props[key] = observer.value; + return { subscriptionContainer, props }; } - return Array.isArray(subs) - ? subscriptionContainer - : { - subscriptionContainer: subscriptionContainer, - props: props, - }; + // Return array based Subscription Container + return subscriptionContainer; } /** diff --git a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts index 4ce359d4..c96710fe 100644 --- a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts +++ b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts @@ -4,7 +4,6 @@ import { ComponentSubscriptionContainer, Observer, SubController, - SubscriptionContainer, } from '../../../../src'; import * as Utils from '@agile-ts/utils'; import { LogMock } from '../../../helper/logMock'; @@ -32,141 +31,172 @@ describe('SubController Tests', () => { let dummyObserver2: Observer; beforeEach(() => { - dummyObserver1 = new Observer(dummyAgile, { key: 'dummyObserver1' }); - dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); - subController = new SubController(dummyAgile); - }); - - describe('subscribeWithSubsObject function tests', () => { - const dummyIntegration = 'myDummyIntegration'; - let dummySubscriptionContainer: SubscriptionContainer; - - beforeEach(() => { - dummySubscriptionContainer = new SubscriptionContainer(); - dummyObserver1.value = 'myCoolValue'; - - subController.subscribe = jest.fn(() => dummySubscriptionContainer); - jest.spyOn(dummyObserver1, 'subscribe'); - jest.spyOn(dummyObserver2, 'subscribe'); + dummyObserver1 = new Observer(dummyAgile, { + key: 'dummyObserver1', + value: 'dummyObserver1Value', }); - - it('should create subscriptionContainer and add in Object shape passed Observers to it', () => { - const subscribeWithSubsResponse = subController.subscribeWithSubsObject( - dummyIntegration, - { - dummyObserver1: dummyObserver1, - dummyObserver2: dummyObserver2, - }, - { - key: 'subscribeWithSubsObjectKey', - proxyKeyMap: {}, - waitForMount: false, - } - ); - - expect(subscribeWithSubsResponse).toStrictEqual({ - props: { - dummyObserver1: 'myCoolValue', - }, - subscriptionContainer: dummySubscriptionContainer, - }); - - expect(subController.subscribe).toHaveBeenCalledWith( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { - key: 'subscribeWithSubsObjectKey', - proxyKeyMap: {}, - waitForMount: false, - } - ); - - expect(dummySubscriptionContainer.isObjectBased).toBeTruthy(); - expect(dummySubscriptionContainer.subscriberKeysWeakMap).toStrictEqual({ - dummyObserver1: dummyObserver1, - dummyObserver2: dummyObserver2, - }); - - expect(dummySubscriptionContainer.subscribers.size).toBe(2); - expect( - dummySubscriptionContainer.subscribers.has(dummyObserver1) - ).toBeTruthy(); - expect( - dummySubscriptionContainer.subscribers.has(dummyObserver2) - ).toBeTruthy(); - - expect(dummyObserver1.subscribe).toHaveBeenCalledWith( - dummySubscriptionContainer - ); - expect(dummyObserver2.subscribe).toHaveBeenCalledWith( - dummySubscriptionContainer - ); + dummyObserver2 = new Observer(dummyAgile, { + key: 'dummyObserver2', + value: 'dummyObserver2Value', }); + subController = new SubController(dummyAgile); }); - describe('subscribeWithSubsArray function tests', () => { - const dummyIntegration = 'myDummyIntegration'; - let dummySubscriptionContainer: SubscriptionContainer; - + describe('subscribe function tests', () => { beforeEach(() => { - dummySubscriptionContainer = new SubscriptionContainer(); - - subController.subscribe = jest.fn(() => dummySubscriptionContainer); - jest.spyOn(dummyObserver1, 'subscribe'); - jest.spyOn(dummyObserver2, 'subscribe'); + jest.spyOn(subController, 'createCallbackSubscriptionContainer'); + jest.spyOn(subController, 'createComponentSubscriptionContainer'); }); - it('should create subscriptionContainer and add in Array Shape passed Observers to it', () => { - const subscribeWithSubsArrayResponse = subController.subscribeWithSubsArray( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { - key: 'subscribeWithSubsArrayKey', - proxyKeyMap: {}, - waitForMount: false, - } - ); - - expect(subscribeWithSubsArrayResponse).toBe(dummySubscriptionContainer); - - expect(subController.subscribe).toHaveBeenCalledWith( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { - key: 'subscribeWithSubsArrayKey', - proxyKeyMap: {}, - waitForMount: false, - } - ); - - expect(dummySubscriptionContainer.isObjectBased).toBeFalsy(); - expect( - dummySubscriptionContainer.subscriberKeysWeakMap - ).toBeUndefined(); - - expect(dummySubscriptionContainer.subscribers.size).toBe(2); - expect( - dummySubscriptionContainer.subscribers.has(dummyObserver1) - ).toBeTruthy(); - expect( - dummySubscriptionContainer.subscribers.has(dummyObserver2) - ).toBeTruthy(); - - expect(dummyObserver1.subscribe).toHaveBeenCalledWith( - dummySubscriptionContainer - ); - expect(dummyObserver2.subscribe).toHaveBeenCalledWith( - dummySubscriptionContainer - ); - }); + it( + 'should create a Component based Subscription Container with specified component' + + ' and add in object specified Observers to it', + () => { + dummyAgile.config.waitForMount = 'aFakeBoolean' as any; + const dummyIntegration: any = { + dummy: 'integration', + }; + + const returnValue = subController.subscribe( + dummyIntegration, + { observer1: dummyObserver1, observer2: dummyObserver2 }, + { + key: 'subscriptionContainerKey', + componentId: 'testID', + waitForMount: true, + } + ); + + expect(returnValue.subscriptionContainer).toBeInstanceOf( + ComponentSubscriptionContainer + ); + expect(returnValue.props).toStrictEqual({ + observer1: dummyObserver1.value, + observer2: dummyObserver2.value, + }); + + expect( + subController.createComponentSubscriptionContainer + ).toHaveBeenCalledWith( + dummyIntegration, + { observer1: dummyObserver1, observer2: dummyObserver2 }, + { + key: 'subscriptionContainerKey', + componentId: 'testID', + waitForMount: true, + } + ); + } + ); + + it( + 'should create a Component based Subscription Container with specified component' + + ' and add in array specified Observers to it', + () => { + dummyAgile.config.waitForMount = 'aFakeBoolean' as any; + const dummyIntegration: any = { + dummy: 'integration', + }; + + const returnValue = subController.subscribe( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { key: 'subscriptionContainerKey', componentId: 'testID' } + ); + + expect(returnValue).toBeInstanceOf(ComponentSubscriptionContainer); + + expect( + subController.createComponentSubscriptionContainer + ).toHaveBeenCalledWith( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { + key: 'subscriptionContainerKey', + componentId: 'testID', + waitForMount: dummyAgile.config.waitForMount, + } + ); + } + ); + + it( + 'should create a Callback based Subscription Container with specified callback function' + + ' and add in object specified Observers to it', + () => { + dummyAgile.config.waitForMount = 'aFakeBoolean' as any; + const dummyIntegration = () => { + /* empty function */ + }; + + const returnValue = subController.subscribe( + dummyIntegration, + { observer1: dummyObserver1, observer2: dummyObserver2 }, + { + key: 'subscriptionContainerKey', + componentId: 'testID', + } + ); + + expect(returnValue.subscriptionContainer).toBeInstanceOf( + CallbackSubscriptionContainer + ); + expect(returnValue.props).toStrictEqual({ + observer1: dummyObserver1.value, + observer2: dummyObserver2.value, + }); + + expect( + subController.createCallbackSubscriptionContainer + ).toHaveBeenCalledWith( + dummyIntegration, + { observer1: dummyObserver1, observer2: dummyObserver2 }, + { + key: 'subscriptionContainerKey', + componentId: 'testID', + waitForMount: dummyAgile.config.waitForMount, + } + ); + } + ); + + it( + 'should create a Callback based Subscription Container with specified callback function' + + ' and add in array specified Observers to it', + () => { + dummyAgile.config.waitForMount = 'aFakeBoolean' as any; + const dummyIntegration = () => { + /* empty function */ + }; + + const returnValue = subController.subscribe( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { + key: 'subscriptionContainerKey', + componentId: 'testID', + waitForMount: false, + } + ); + + expect(returnValue).toBeInstanceOf(CallbackSubscriptionContainer); + + expect( + subController.createCallbackSubscriptionContainer + ).toHaveBeenCalledWith( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { + key: 'subscriptionContainerKey', + componentId: 'testID', + waitForMount: false, + } + ); + } + ); }); describe('unsubscribe function tests', () => { - beforeEach(() => { - jest.spyOn(dummyObserver1, 'unsubscribe'); - jest.spyOn(dummyObserver2, 'unsubscribe'); - }); - it('should unsubscribe callbackSubscriptionContainer', () => { const dummyIntegration = () => { /* empty function */ @@ -175,17 +205,21 @@ describe('SubController Tests', () => { dummyIntegration, [dummyObserver1, dummyObserver2] ); + callbackSubscriptionContainer.removeSubscription = jest.fn(); subController.unsubscribe(callbackSubscriptionContainer); expect(subController.callbackSubs.size).toBe(0); expect(callbackSubscriptionContainer.ready).toBeFalsy(); - expect(dummyObserver1.unsubscribe).toHaveBeenCalledWith( - callbackSubscriptionContainer - ); - expect(dummyObserver2.unsubscribe).toHaveBeenCalledWith( - callbackSubscriptionContainer - ); + expect( + callbackSubscriptionContainer.removeSubscription + ).toHaveBeenCalledTimes(2); + expect( + callbackSubscriptionContainer.removeSubscription + ).toHaveBeenCalledWith(dummyObserver1); + expect( + callbackSubscriptionContainer.removeSubscription + ).toHaveBeenCalledWith(dummyObserver2); }); it('should unsubscribe componentSubscriptionContainer', () => { @@ -196,41 +230,24 @@ describe('SubController Tests', () => { dummyIntegration, [dummyObserver1, dummyObserver2] ); + componentSubscriptionContainer.removeSubscription = jest.fn(); subController.unsubscribe(componentSubscriptionContainer); expect(subController.componentSubs.size).toBe(0); expect(componentSubscriptionContainer.ready).toBeFalsy(); - expect(dummyObserver1.unsubscribe).toHaveBeenCalledWith( - componentSubscriptionContainer - ); - expect(dummyObserver2.unsubscribe).toHaveBeenCalledWith( - componentSubscriptionContainer - ); - }); - - it('should unsubscribe componentSubscriptionContainer from passed Object that hold an instance of componentSubscriptionContainer', () => { - const dummyIntegration: any = { - dummy: 'integration', - }; - const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); - - subController.unsubscribe(dummyIntegration); - - expect(subController.componentSubs.size).toBe(0); - expect(componentSubscriptionContainer.ready).toBeFalsy(); - expect(dummyObserver1.unsubscribe).toHaveBeenCalledWith( - componentSubscriptionContainer - ); - expect(dummyObserver2.unsubscribe).toHaveBeenCalledWith( - componentSubscriptionContainer - ); + expect( + componentSubscriptionContainer.removeSubscription + ).toHaveBeenCalledTimes(2); + expect( + componentSubscriptionContainer.removeSubscription + ).toHaveBeenCalledWith(dummyObserver1); + expect( + componentSubscriptionContainer.removeSubscription + ).toHaveBeenCalledWith(dummyObserver2); }); - it('should unsubscribe componentSubscriptionContainers from passed Object that hold an Array of componentSubscriptionContainers', () => { + it('should unsubscribe componentSubscriptionContainers from passed Object that holds an instance of componentSubscriptionContainers', () => { const dummyIntegration: any = { dummy: 'integration', componentSubscriptionContainers: [], @@ -239,185 +256,172 @@ describe('SubController Tests', () => { dummyIntegration, [dummyObserver1, dummyObserver2] ); + componentSubscriptionContainer.removeSubscription = jest.fn(); const componentSubscriptionContainer2 = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2] ); + componentSubscriptionContainer2.removeSubscription = jest.fn(); subController.unsubscribe(dummyIntegration); expect(subController.componentSubs.size).toBe(0); expect(componentSubscriptionContainer.ready).toBeFalsy(); - expect(dummyObserver1.unsubscribe).toHaveBeenCalledWith( - componentSubscriptionContainer - ); - expect(dummyObserver2.unsubscribe).toHaveBeenCalledWith( - componentSubscriptionContainer - ); - - expect(componentSubscriptionContainer2.ready).toBeFalsy(); - expect(dummyObserver1.unsubscribe).toHaveBeenCalledWith( - componentSubscriptionContainer2 - ); - expect(dummyObserver2.unsubscribe).toHaveBeenCalledWith( - componentSubscriptionContainer2 - ); - }); - }); - - describe('registerSubscription function tests', () => { - let dummySubscriptionContainer: SubscriptionContainer; - - beforeEach(() => { - dummySubscriptionContainer = new SubscriptionContainer(); - dummyAgile.config.waitForMount = 'dummyWaitForMount' as any; - - subController.createCallbackSubscriptionContainer = jest.fn( - () => dummySubscriptionContainer as CallbackSubscriptionContainer - ); - subController.createComponentSubscriptionContainer = jest.fn( - () => dummySubscriptionContainer as ComponentSubscriptionContainer - ); - }); - - it('should call registerCallbackSubscription if passed integrationInstance is a Function (default config)', () => { - const dummyIntegration = () => { - /* empty function */ - }; - - const subscriptionContainer = subController.subscribe( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); - - expect(subscriptionContainer).toBe(dummySubscriptionContainer); expect( - subController.createCallbackSubscriptionContainer - ).toHaveBeenCalledWith( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { waitForMount: dummyAgile.config.waitForMount } - ); + componentSubscriptionContainer.removeSubscription + ).toHaveBeenCalledTimes(2); expect( - subController.createComponentSubscriptionContainer - ).not.toHaveBeenCalled(); - }); - - it('should call registerCallbackSubscription if passed integrationInstance is a Function (specific config)', () => { - const dummyIntegration = () => { - /* empty function */ - }; - - const subscriptionContainer = subController.subscribe( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { key: 'niceKey', proxyKeyMap: {}, waitForMount: false } - ); - - expect(subscriptionContainer).toBe(dummySubscriptionContainer); + componentSubscriptionContainer.removeSubscription + ).toHaveBeenCalledWith(dummyObserver1); expect( - subController.createCallbackSubscriptionContainer - ).toHaveBeenCalledWith( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { key: 'niceKey', proxyKeyMap: {}, waitForMount: false } - ); - expect( - subController.createComponentSubscriptionContainer - ).not.toHaveBeenCalled(); - }); - - it('should call registerComponentSubscription if passed integrationInstance is not a Function (default config)', () => { - const dummyIntegration = { dummy: 'integration' }; - - const subscriptionContainer = subController.subscribe( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); + componentSubscriptionContainer.removeSubscription + ).toHaveBeenCalledWith(dummyObserver2); - expect(subscriptionContainer).toBe(dummySubscriptionContainer); + expect(componentSubscriptionContainer2.ready).toBeFalsy(); expect( - subController.createComponentSubscriptionContainer - ).toHaveBeenCalledWith( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { waitForMount: dummyAgile.config.waitForMount } - ); + componentSubscriptionContainer2.removeSubscription + ).toHaveBeenCalledTimes(2); expect( - subController.createCallbackSubscriptionContainer - ).not.toHaveBeenCalled(); - }); - - it('should call registerComponentSubscription if passed integrationInstance is not a Function (specific config)', () => { - const dummyIntegration = { dummy: 'integration' }; - - const subscriptionContainer = subController.subscribe( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { key: 'niceKey', proxyKeyMap: {}, waitForMount: false } - ); - - expect(subscriptionContainer).toBe(dummySubscriptionContainer); - expect( - subController.createComponentSubscriptionContainer - ).toHaveBeenCalledWith( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { key: 'niceKey', proxyKeyMap: {}, waitForMount: false } - ); + componentSubscriptionContainer2.removeSubscription + ).toHaveBeenCalledWith(dummyObserver1); expect( - subController.createCallbackSubscriptionContainer - ).not.toHaveBeenCalled(); + componentSubscriptionContainer2.removeSubscription + ).toHaveBeenCalledWith(dummyObserver2); }); }); - describe('registerComponentSubscription function tests', () => { - it('should return ready componentSubscriptionContainer and add it to dummyIntegration at componentSubscriptionContainer (config.waitForMount = false)', () => { - const dummyIntegration: any = { dummy: 'integration' }; - - const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { waitForMount: false } - ); - - expect(componentSubscriptionContainer).toBeInstanceOf( - ComponentSubscriptionContainer - ); - expect(componentSubscriptionContainer.component).toStrictEqual( - dummyIntegration - ); - expect(componentSubscriptionContainer.ready).toBeTruthy(); - - expect(componentSubscriptionContainer.subscribers.size).toBe(2); - expect( - componentSubscriptionContainer.subscribers.has(dummyObserver1) - ).toBeTruthy(); - expect( - componentSubscriptionContainer.subscribers.has(dummyObserver2) - ).toBeTruthy(); + describe('createComponentSubscriptionContainer function tests', () => { + it( + 'should return ready componentSubscriptionContainer ' + + 'and add an instance of it to the not existing componentSubscriptions property in the dummyIntegration (default config)', + () => { + jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey'); + const dummyIntegration: any = { + dummy: 'integration', + }; + + const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { waitForMount: false } + ); + + expect(componentSubscriptionContainer).toBeInstanceOf( + ComponentSubscriptionContainer + ); + expect(componentSubscriptionContainer.component).toStrictEqual( + dummyIntegration + ); + expect(componentSubscriptionContainer.ready).toBeTruthy(); + + expect(subController.componentSubs.size).toBe(1); + expect( + subController.componentSubs.has(componentSubscriptionContainer) + ).toBeTruthy(); + + expect(dummyIntegration.componentSubscriptionContainers.length).toBe( + 1 + ); + expect(dummyIntegration.componentSubscriptionContainers[0]).toBe( + componentSubscriptionContainer + ); + + // Check if ComponentSubscriptionContainer was called with correct parameters + expect(componentSubscriptionContainer.key).toBe('generatedKey'); + expect(componentSubscriptionContainer.componentId).toBeUndefined(); + expect(componentSubscriptionContainer.subscribers.size).toBe(2); + expect( + componentSubscriptionContainer.subscribers.has(dummyObserver1) + ).toBeTruthy(); + expect( + componentSubscriptionContainer.subscribers.has(dummyObserver2) + ).toBeTruthy(); + } + ); + + it( + 'should return ready componentSubscriptionContainer ' + + 'and add an instance of it to the existing componentSubscriptions property in the dummyIntegration (default config)', + () => { + jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey'); + const dummyIntegration: any = { + dummy: 'integration', + componentSubscriptionContainers: [], + }; + + const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { waitForMount: false } + ); + + expect(dummyIntegration.componentSubscriptionContainers.length).toBe( + 1 + ); + expect(dummyIntegration.componentSubscriptionContainers[0]).toBe( + componentSubscriptionContainer + ); + } + ); + + it( + 'should return ready componentSubscriptionContainer ' + + 'and add an instance of it to the not existing componentSubscriptions property in the dummyIntegration (specific config)', + () => { + const dummyIntegration: any = { + dummy: 'integration', + }; + + const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { waitForMount: false, componentId: 'testID', key: 'dummyKey' } + ); + + expect(componentSubscriptionContainer).toBeInstanceOf( + ComponentSubscriptionContainer + ); + expect(componentSubscriptionContainer.component).toStrictEqual( + dummyIntegration + ); + expect(componentSubscriptionContainer.ready).toBeTruthy(); + + expect(subController.componentSubs.size).toBe(1); + expect( + subController.componentSubs.has(componentSubscriptionContainer) + ).toBeTruthy(); + + expect(dummyIntegration.componentSubscriptionContainers.length).toBe( + 1 + ); + expect(dummyIntegration.componentSubscriptionContainers[0]).toBe( + componentSubscriptionContainer + ); + + // Check if ComponentSubscriptionContainer was called with correct parameters + expect(componentSubscriptionContainer.key).toBe('dummyKey'); + expect(componentSubscriptionContainer.componentId).toBe('testID'); + expect(componentSubscriptionContainer.subscribers.size).toBe(2); + expect( + componentSubscriptionContainer.subscribers.has(dummyObserver1) + ).toBeTruthy(); + expect( + componentSubscriptionContainer.subscribers.has(dummyObserver2) + ).toBeTruthy(); + } + ); - expect(subController.componentSubs.size).toBe(1); - expect( - subController.componentSubs.has(componentSubscriptionContainer) - ).toBeTruthy(); - - expect(dummyIntegration.componentSubscriptionContainer).toBe( - componentSubscriptionContainer - ); - }); - - it('should return ready componentSubscriptionContainer and add it to componentSubscriptions in dummyIntegration (config.waitForMount = false)', () => { + it("should return not ready componentSubscriptionContainer if componentInstance isn't mounted (waitForMount = true)", () => { + jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey'); const dummyIntegration: any = { dummy: 'integration', - componentSubscriptionContainers: [], }; const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( dummyIntegration, [dummyObserver1, dummyObserver2], - { waitForMount: false } + { waitForMount: true } ); expect(componentSubscriptionContainer).toBeInstanceOf( @@ -426,47 +430,16 @@ describe('SubController Tests', () => { expect(componentSubscriptionContainer.component).toStrictEqual( dummyIntegration ); - expect(componentSubscriptionContainer.ready).toBeTruthy(); - - expect(componentSubscriptionContainer.subscribers.size).toBe(2); - expect( - componentSubscriptionContainer.subscribers.has(dummyObserver1) - ).toBeTruthy(); - expect( - componentSubscriptionContainer.subscribers.has(dummyObserver2) - ).toBeTruthy(); + expect(componentSubscriptionContainer.ready).toBeFalsy(); expect(subController.componentSubs.size).toBe(1); expect( subController.componentSubs.has(componentSubscriptionContainer) ).toBeTruthy(); - expect(dummyIntegration.componentSubscriptionContainers.length).toBe(1); - expect(dummyIntegration.componentSubscriptionContainers[0]).toBe( - componentSubscriptionContainer - ); - expect(dummyIntegration.componentSubscriptionContainer).toBeUndefined(); - }); - - it("should return not ready componentSubscriptionContainer if componentInstance isn't mounted (waitForMount = true)", () => { - const dummyIntegration: any = { - dummy: 'integration', - }; - - const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { waitForMount: true } - ); - - expect(componentSubscriptionContainer).toBeInstanceOf( - ComponentSubscriptionContainer - ); - expect(componentSubscriptionContainer.component).toStrictEqual( - dummyIntegration - ); - expect(componentSubscriptionContainer.ready).toBeFalsy(); - + // Check if ComponentSubscriptionContainer was called with correct parameters + expect(componentSubscriptionContainer.key).toBe('generatedKey'); + expect(componentSubscriptionContainer.componentId).toBeUndefined(); expect(componentSubscriptionContainer.subscribers.size).toBe(2); expect( componentSubscriptionContainer.subscribers.has(dummyObserver1) @@ -474,14 +447,10 @@ describe('SubController Tests', () => { expect( componentSubscriptionContainer.subscribers.has(dummyObserver2) ).toBeTruthy(); - - expect(subController.componentSubs.size).toBe(1); - expect( - subController.componentSubs.has(componentSubscriptionContainer) - ).toBeTruthy(); }); it('should return ready componentSubscriptionContainer if componentInstance is mounted (config.waitForMount = true)', () => { + jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey'); const dummyIntegration: any = { dummy: 'integration', }; @@ -501,6 +470,14 @@ describe('SubController Tests', () => { ); expect(componentSubscriptionContainer.ready).toBeTruthy(); + expect(subController.componentSubs.size).toBe(1); + expect( + subController.componentSubs.has(componentSubscriptionContainer) + ).toBeTruthy(); + + // Check if ComponentSubscriptionContainer was called with correct parameters + expect(componentSubscriptionContainer.key).toBe('generatedKey'); + expect(componentSubscriptionContainer.componentId).toBeUndefined(); expect(componentSubscriptionContainer.subscribers.size).toBe(2); expect( componentSubscriptionContainer.subscribers.has(dummyObserver1) @@ -508,17 +485,12 @@ describe('SubController Tests', () => { expect( componentSubscriptionContainer.subscribers.has(dummyObserver2) ).toBeTruthy(); - - expect(subController.componentSubs.size).toBe(1); - expect( - subController.componentSubs.has(componentSubscriptionContainer) - ).toBeTruthy(); }); }); describe('registerCallbackSubscription function tests', () => { it('should return callbackSubscriptionContainer (default config)', () => { - jest.spyOn(Utils, 'generateId').mockReturnValueOnce('randomKey'); + jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey'); const dummyIntegration = () => { /* empty function */ }; @@ -534,16 +506,19 @@ describe('SubController Tests', () => { expect(callbackSubscriptionContainer.callback).toBe(dummyIntegration); expect(callbackSubscriptionContainer.ready).toBeTruthy(); + expect(subController.callbackSubs.size).toBe(1); + expect( + subController.callbackSubs.has(callbackSubscriptionContainer) + ).toBeTruthy(); + // TODO find a way to spy on a class constructor without overwriting it // https://stackoverflow.com/questions/48219267/how-to-spy-on-a-class-constructor-jest/48486214 // Because the below tests are not really related to this test, - // they are checking if the CallbackSubscriptionContainer got called with the right parameters + // they are checking if the CallbackSubscriptionContainer was called with the correct parameters // by checking if CallbackSubscriptionContainer has set its properties correctly // Note:This 'issue' happens in multiple parts of the AgileTs test - expect(callbackSubscriptionContainer.key).toBe('randomKey'); - expect(callbackSubscriptionContainer.proxyKeyMap).toStrictEqual({}); - expect(callbackSubscriptionContainer.isProxyBased).toBeFalsy(); - + expect(callbackSubscriptionContainer.key).toBe('generatedKey'); + expect(callbackSubscriptionContainer.componentId).toBeUndefined(); expect(callbackSubscriptionContainer.subscribers.size).toBe(2); expect( callbackSubscriptionContainer.subscribers.has(dummyObserver1) @@ -551,11 +526,6 @@ describe('SubController Tests', () => { expect( callbackSubscriptionContainer.subscribers.has(dummyObserver2) ).toBeTruthy(); - - expect(subController.callbackSubs.size).toBe(1); - expect( - subController.callbackSubs.has(callbackSubscriptionContainer) - ).toBeTruthy(); }); it('should return callbackSubscriptionContainer (specific config)', () => { @@ -568,8 +538,8 @@ describe('SubController Tests', () => { [dummyObserver1, dummyObserver2], { waitForMount: false, - proxyKeyMap: { jeff: { paths: [[]] } }, - key: 'jeff', + componentId: 'testID', + key: 'dummyKey', } ); @@ -578,12 +548,15 @@ describe('SubController Tests', () => { ); expect(callbackSubscriptionContainer.callback).toBe(dummyIntegration); expect(callbackSubscriptionContainer.ready).toBeTruthy(); - expect(callbackSubscriptionContainer.key).toBe('jeff'); - expect(callbackSubscriptionContainer.proxyKeyMap).toStrictEqual({ - jeff: { paths: [[]] }, - }); - expect(callbackSubscriptionContainer.isProxyBased).toBeTruthy(); + expect(subController.callbackSubs.size).toBe(1); + expect( + subController.callbackSubs.has(callbackSubscriptionContainer) + ).toBeTruthy(); + + // Check if CallbackSubscriptionContainer was called with correct parameters + expect(callbackSubscriptionContainer.key).toBe('dummyKey'); + expect(callbackSubscriptionContainer.componentId).toBe('testID'); expect(callbackSubscriptionContainer.subscribers.size).toBe(2); expect( callbackSubscriptionContainer.subscribers.has(dummyObserver1) @@ -591,11 +564,6 @@ describe('SubController Tests', () => { expect( callbackSubscriptionContainer.subscribers.has(dummyObserver2) ).toBeTruthy(); - - expect(subController.callbackSubs.size).toBe(1); - expect( - subController.callbackSubs.has(callbackSubscriptionContainer) - ).toBeTruthy(); }); }); From 6c698a9e608e8bc13a0c79769fb40780170e6781 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 9 Jun 2021 20:28:07 +0200 Subject: [PATCH 42/63] split the updateSubscription method monster into smaller peaces for better testability --- packages/core/src/runtime/index.ts | 61 ++- .../core/tests/unit/runtime/runtime.test.ts | 374 +----------------- 2 files changed, 57 insertions(+), 378 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index 91846493..2fc93266 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -152,10 +152,6 @@ export class Runtime { ) return false; - // Subscription Containers that have to be updated. - // Using a 'Set()' to combine several equal SubscriptionContainers into one (rerender optimisation). - const subscriptionsToUpdate = new Set(); - // Build final 'jobsToRerender' array // based on the new 'jobsToRerender' array and the 'notReadyJobsToRerender' array. const jobsToRerender = this.jobsToRerender.concat( @@ -164,10 +160,39 @@ export class Runtime { this.notReadyJobsToRerender = new Set(); this.jobsToRerender = []; + // Extract Subscription Container from the Jobs to be rerendered + const subscriptionContainerToUpdate = this.extractToUpdateSubscriptionContainer( + jobsToRerender + ); + if (subscriptionContainerToUpdate.length <= 0) return false; + + // Update Subscription Container (trigger rerender on Components they represent) + this.updateSubscriptionContainer(subscriptionContainerToUpdate); + + return true; + } + + /** + * Extracts the Subscription Containers + * that should be updated from the provided Runtime Jobs. + * + * @internal + * @param jobs - Jobs from which to extract the Subscription Containers to be updated. + */ + public extractToUpdateSubscriptionContainer( + jobs: Array + ): Array { + // Subscription Containers that have to be updated. + // Using a 'Set()' to combine several equal SubscriptionContainers into one (rerender optimisation). + const subscriptionsToUpdate = new Set(); + // Check if Job Subscription Container of Jobs should be updated // and if so add it to the 'subscriptionsToUpdate' array - jobsToRerender.forEach((job) => { + jobs.forEach((job) => { job.subscriptionContainersToUpdate.forEach((subscriptionContainer) => { + let updateSubscriptionContainer = true; + + // Handle not ready Subscription Container if (!subscriptionContainer.ready) { if ( !job.config.numberOfTriesToUpdate || @@ -191,14 +216,11 @@ export class Runtime { return; } - let updateSubscriptionContainer; - // Handle Selectors of Subscription Container // (-> check if a selected part of the Observer value has changed) - updateSubscriptionContainer = this.handleSelectors( - subscriptionContainer, - job - ); + updateSubscriptionContainer = + updateSubscriptionContainer && + this.handleSelectors(subscriptionContainer, job); // Check if Subscription Container with same 'componentId' // is already in the 'subscriptionToUpdate' queue (rerender optimisation) @@ -218,8 +240,21 @@ export class Runtime { }); }); - if (subscriptionsToUpdate.size <= 0) return false; + return Array.from(subscriptionsToUpdate); + } + /** + * Updates the specified Subscription Container. + * + * By updating the SubscriptionContainer a rerender is triggered + * on the Component it represents. + * + * @internal + * @param subscriptionsToUpdate - Subscription Containers to be updated. + */ + public updateSubscriptionContainer( + subscriptionsToUpdate: Array + ): void { // Update Subscription Containers (trigger rerender on Components they represent) subscriptionsToUpdate.forEach((subscriptionContainer) => { // Call 'callback function' if Callback based Subscription @@ -239,8 +274,6 @@ export class Runtime { Agile.logger.if .tag(['runtime']) .info(LogCodeManager.getLog('16:01:02'), subscriptionsToUpdate); - - return true; } /** diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index a4fa90ec..03b28ee5 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -153,370 +153,16 @@ describe('Runtime Tests', () => { }); }); - describe('updateSubscribers function tests', () => { - let dummyObserver4: Observer; - let rCallbackSubJob: RuntimeJob; - let nrArCallbackSubJob: RuntimeJob; - let rComponentSubJob: RuntimeJob; - let nrArComponentSubJob: RuntimeJob; - let rCallbackSubContainer: CallbackSubscriptionContainer; - const rCallbackSubContainerCallbackFunction = () => { - /* empty function */ - }; - let nrCallbackSubContainer: CallbackSubscriptionContainer; - const nrCallbackSubContainerCallbackFunction = () => { - /* empty function */ - }; - let rComponentSubContainer: ComponentSubscriptionContainer; - const rComponentSubContainerComponent = { - my: 'cool component', - }; - let nrComponentSubContainer: ComponentSubscriptionContainer; - const nrComponentSubContainerComponent = { - my: 'second cool component', - }; - const dummyProxyKeyMap = { myState: { paths: [['a', 'b']] } }; - - beforeEach(() => { - dummyAgile.integrate(testIntegration); - dummyObserver4 = new Observer(dummyAgile, { key: 'dummyObserver4' }); - - dummyObserver1.value = 'dummyObserverValue1'; - dummyObserver2.value = 'dummyObserverValue2'; - dummyObserver3.value = 'dummyObserverValue3'; - dummyObserver4.value = 'dummyObserverValue4'; - - // Create Ready Callback Subscription - rCallbackSubContainer = dummyAgile.subController.subscribeWithSubsArray( - rCallbackSubContainerCallbackFunction, - [dummyObserver1, dummyObserver2] - ) as CallbackSubscriptionContainer; - rCallbackSubContainer.callback = jest.fn(); - rCallbackSubContainer.ready = true; - rCallbackSubContainer.key = 'rCallbackSubContainerKey'; - - // Create Not Ready Callback Subscription - nrCallbackSubContainer = dummyAgile.subController.subscribeWithSubsArray( - nrCallbackSubContainerCallbackFunction, - [dummyObserver2] - ) as CallbackSubscriptionContainer; - nrCallbackSubContainer.callback = jest.fn(); - nrCallbackSubContainer.ready = false; - nrCallbackSubContainer.key = 'nrCallbackSubContainerKey'; - - // Create Ready Component Subscription - rComponentSubContainer = dummyAgile.subController.subscribeWithSubsObject( - rComponentSubContainerComponent, - { - observer3: dummyObserver3, - observer4: dummyObserver4, - } - ).subscriptionContainer as ComponentSubscriptionContainer; - rComponentSubContainer.ready = true; - rComponentSubContainer.key = 'rComponentSubContainerKey'; - - // Create Not Ready Component Subscription - nrComponentSubContainer = dummyAgile.subController.subscribeWithSubsObject( - nrComponentSubContainerComponent, - { - observer4: dummyObserver4, - } - ).subscriptionContainer as ComponentSubscriptionContainer; - nrComponentSubContainer.ready = false; - nrComponentSubContainer.key = 'nrComponentSubContainerKey'; - - rComponentSubJob = new RuntimeJob(dummyObserver3, { key: 'dummyJob3' }); // Job with ready Component Subscription - rCallbackSubJob = new RuntimeJob(dummyObserver1, { key: 'dummyJob1' }); // Job with ready CallbackSubscription - nrArComponentSubJob = new RuntimeJob(dummyObserver4, { - key: 'dummyJob4', - }); // Job with not ready and ready Component Subscription - nrArCallbackSubJob = new RuntimeJob(dummyObserver2, { - key: 'dummyJob2', - }); // Job with not ready and ready Callback Subscription - - jest.spyOn(dummyAgile.integrations, 'update'); - jest.spyOn(runtime, 'handleObjectBasedSubscription'); - jest.spyOn(runtime, 'handleSelectors'); - }); - - it('should return false if agile has no integration', () => { - dummyAgile.hasIntegration = jest.fn(() => false); - runtime.jobsToRerender.push(rCallbackSubJob); - runtime.jobsToRerender.push(nrArCallbackSubJob); - - const response = runtime.updateSubscribers(); - - expect(response).toBeFalsy(); - expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(0); - expect(dummyAgile.integrations.update).not.toHaveBeenCalled(); - expect(rCallbackSubContainer.callback).not.toHaveBeenCalled(); - expect(nrCallbackSubContainer.callback).not.toHaveBeenCalled(); - }); - - it('should return false if no Jobs in jobsToRerender and notReadyJobsToRerender left', () => { - dummyAgile.hasIntegration = jest.fn(() => true); - runtime.jobsToRerender = []; - runtime.notReadyJobsToRerender = new Set(); - - const response = runtime.updateSubscribers(); - - expect(response).toBeFalsy(); - }); - - it('should update ready component based SubscriptionContainer', () => { - dummyAgile.hasIntegration = jest.fn(() => true); - runtime.jobsToRerender.push(rComponentSubJob); - - const response = runtime.updateSubscribers(); - - expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(0); - expect(runtime.handleSelectors).not.toHaveBeenCalled(); - - expect(dummyAgile.integrations.update).toHaveBeenCalledTimes(1); - expect(dummyAgile.integrations.update).toHaveBeenCalledWith( - rComponentSubContainerComponent, - { - observer3: 'dummyObserverValue3', - } - ); - expect(runtime.handleObjectBasedSubscription).toHaveBeenCalledWith( - rComponentSubContainer, - rComponentSubJob - ); - expect(rComponentSubJob.subscriptionContainersToUpdate.size).toBe(0); - expect(dummyObserver3.subscribedTo.size).toBe(1); - - expect(response).toBeTruthy(); - }); - - it('should update ready callback based SubscriptionContainer', () => { - dummyAgile.hasIntegration = jest.fn(() => true); - runtime.jobsToRerender.push(rCallbackSubJob); - - const response = runtime.updateSubscribers(); - - expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(0); - expect(runtime.handleSelectors).not.toHaveBeenCalled(); - - expect(rCallbackSubContainer.callback).toHaveBeenCalledTimes(1); - expect(rCallbackSubJob.subscriptionContainersToUpdate.size).toBe(0); - expect(dummyObserver1.subscribedTo.size).toBe(1); - - expect(response).toBeTruthy(); - }); - - it('should update ready proxy, callback based SubscriptionContainer if handleProxyBasedSubscriptions() returns true', () => { - jest.spyOn(runtime, 'handleSelectors').mockReturnValueOnce(true); - dummyAgile.hasIntegration = jest.fn(() => true); - rCallbackSubContainer.isProxyBased = true; - rCallbackSubContainer.proxyKeyMap = dummyProxyKeyMap; - runtime.jobsToRerender.push(rCallbackSubJob); - - const response = runtime.updateSubscribers(); - - expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(0); - expect(runtime.handleSelectors).toHaveBeenCalledWith( - rCallbackSubContainer, - rCallbackSubJob - ); - - expect(rCallbackSubContainer.callback).toHaveBeenCalledTimes(1); - expect(rCallbackSubJob.subscriptionContainersToUpdate.size).toBe(0); - expect(dummyObserver1.subscribedTo.size).toBe(1); - - expect(response).toBeTruthy(); - }); - - it("shouldn't update ready proxy, callback based SubscriptionContainer if handleProxyBasedSubscriptions() returns false", () => { - jest.spyOn(runtime, 'handleSelectors').mockReturnValueOnce(false); - dummyAgile.hasIntegration = jest.fn(() => true); - rCallbackSubContainer.isProxyBased = true; - rCallbackSubContainer.proxyKeyMap = dummyProxyKeyMap; - runtime.jobsToRerender.push(rCallbackSubJob); - - const response = runtime.updateSubscribers(); - - expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(0); - expect(runtime.handleSelectors).toHaveBeenCalledWith( - rCallbackSubContainer, - rCallbackSubJob - ); - - expect(rCallbackSubContainer.callback).not.toHaveBeenCalled(); - expect(rCallbackSubJob.subscriptionContainersToUpdate.size).toBe(0); - expect(dummyObserver1.subscribedTo.size).toBe(1); - - expect(response).toBeFalsy(); - }); - - it("shouldn't update not ready SubscriptionContainers but it should update ready SubscriptionContainers", () => { - dummyAgile.hasIntegration = jest.fn(() => true); - runtime.jobsToRerender.push(nrArCallbackSubJob); - runtime.jobsToRerender.push(nrArComponentSubJob); - - const response = runtime.updateSubscribers(); - - expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(2); - expect( - runtime.notReadyJobsToRerender.has(nrArCallbackSubJob) - ).toBeTruthy(); - expect( - runtime.notReadyJobsToRerender.has(nrArComponentSubJob) - ).toBeTruthy(); - - expect(nrArCallbackSubJob.subscriptionContainersToUpdate.size).toBe(1); - expect( - nrArCallbackSubJob.subscriptionContainersToUpdate.has( - nrCallbackSubContainer - ) - ).toBeTruthy(); - expect(nrArComponentSubJob.subscriptionContainersToUpdate.size).toBe(1); - expect( - nrArComponentSubJob.subscriptionContainersToUpdate.has( - nrComponentSubContainer - ) - ).toBeTruthy(); - - expect(rCallbackSubContainer.callback).toHaveBeenCalledTimes(1); - expect(nrCallbackSubContainer.callback).not.toHaveBeenCalled(); - - expect(dummyAgile.integrations.update).toHaveBeenCalledTimes(1); - expect(dummyAgile.integrations.update).toHaveBeenCalledWith( - rComponentSubContainerComponent, - { - observer4: 'dummyObserverValue4', - } - ); - expect(dummyAgile.integrations.update).not.toHaveBeenCalledWith( - nrComponentSubContainerComponent, - { - observer4: 'dummyObserverValue4', - } - ); - - expect(dummyObserver2.subscribedTo.size).toBe(2); - expect(dummyObserver4.subscribedTo.size).toBe(2); - - expect(runtime.handleObjectBasedSubscription).toHaveBeenCalledWith( - rComponentSubContainer, - nrArComponentSubJob - ); - expect(runtime.handleObjectBasedSubscription).not.toHaveBeenCalledWith( - nrComponentSubContainer, - nrArComponentSubJob - ); - - expect(nrArComponentSubJob.triesToUpdate).toBe(1); - expect(nrArCallbackSubJob.triesToUpdate).toBe(1); - - LogMock.hasLoggedCode( - '16:02:00', - [nrCallbackSubContainer.key], - nrCallbackSubContainer - ); - LogMock.hasLoggedCode( - '16:02:00', - [nrComponentSubContainer.key], - nrComponentSubContainer - ); - - expect(response).toBeTruthy(); // because 2 SubscriptionContainer were ready - }); - - it('should try to update in the past not ready SubscriptionContainers from the notReadyJobsToUpdate queue', () => { - dummyAgile.hasIntegration = jest.fn(() => true); - runtime.notReadyJobsToRerender.add(rCallbackSubJob); - - const response = runtime.updateSubscribers(); - - expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(0); - - expect(rCallbackSubContainer.callback).toHaveBeenCalled(); - expect(rCallbackSubJob.subscriptionContainersToUpdate.size).toBe(0); - expect(dummyObserver1.subscribedTo.size).toBe(1); + describe('updateSubscriptions function tests', () => { + // TODO + }); - expect(response).toBeTruthy(); - }); + describe('extractToUpdateSubscriptionContainer function tests', () => { + // TODO + }); - it( - "shouldn't update not ready SubscriptionContainers from the notReadyJobsToUpdate queue " + - 'and completely remove them from the runtime when it exceeded numberOfTriesToUpdate', - () => { - dummyAgile.hasIntegration = jest.fn(() => true); - rCallbackSubJob.config.numberOfTriesToUpdate = 2; - rCallbackSubJob.triesToUpdate = 2; - rCallbackSubContainer.ready = false; - runtime.notReadyJobsToRerender.add(rCallbackSubJob); - - const response = runtime.updateSubscribers(); - - expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(0); - - expect(rCallbackSubContainer.callback).not.toHaveBeenCalled(); - expect(rCallbackSubJob.subscriptionContainersToUpdate.size).toBe(1); - expect( - rCallbackSubJob.subscriptionContainersToUpdate.has( - rCallbackSubContainer - ) - ).toBeTruthy(); - expect(dummyObserver1.subscribedTo.size).toBe(1); - expect(rCallbackSubJob.triesToUpdate).toBe(2); - - LogMock.hasLoggedCode( - '16:02:01', - [rCallbackSubJob.config.numberOfTriesToUpdate], - rCallbackSubContainer - ); - - expect(response).toBeFalsy(); - } - ); - - it( - "shouldn't update not ready SubscriptionContainer from the notReadyJobsToUpdate queue " + - 'and add it again to the notReadyJobsToUpdate queue if numberOfTriesToUpdate is null', - () => { - dummyAgile.hasIntegration = jest.fn(() => true); - rCallbackSubJob.config.numberOfTriesToUpdate = null; - rCallbackSubJob.triesToUpdate = 2; - rCallbackSubContainer.ready = false; - runtime.notReadyJobsToRerender.add(rCallbackSubJob); - - const response = runtime.updateSubscribers(); - - expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(1); - expect( - runtime.notReadyJobsToRerender.has(rCallbackSubJob) - ).toBeTruthy(); - - expect(rCallbackSubContainer.callback).not.toHaveBeenCalled(); - expect(rCallbackSubJob.subscriptionContainersToUpdate.size).toBe(1); - expect( - rCallbackSubJob.subscriptionContainersToUpdate.has( - rCallbackSubContainer - ) - ).toBeTruthy(); - expect(dummyObserver1.subscribedTo.size).toBe(1); - expect(rCallbackSubJob.triesToUpdate).toBe(3); - - LogMock.hasLoggedCode( - '16:02:00', - [rCallbackSubContainer.key], - rCallbackSubContainer - ); - - expect(response).toBeFalsy(); - } - ); + describe('updateSubscriptionContainer function tests', () => { + // TODO }); describe('getUpdatedObserverValues function tests', () => { @@ -526,7 +172,7 @@ describe('Runtime Tests', () => { }; beforeEach(() => { - subscriptionContainer = dummyAgile.subController.subscribeWithSubsObject( + subscriptionContainer = dummyAgile.subController.subscribe( dummyFunction, { observer1: dummyObserver1, @@ -569,7 +215,7 @@ describe('Runtime Tests', () => { beforeEach(() => { // Create Job with Object value - objectSubscriptionContainer = dummyAgile.subController.subscribeWithSubsObject( + objectSubscriptionContainer = dummyAgile.subController.subscribe( dummyFunction, { observer1: dummyObserver1 } ).subscriptionContainer; From b456ea950eda4ec8430ce98fd19d96adc63d6a96 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 10 Jun 2021 06:51:53 +0200 Subject: [PATCH 43/63] optimized runtime tests --- packages/core/src/runtime/index.ts | 22 +- .../core/tests/unit/runtime/runtime.test.ts | 348 ++++++++++++------ 2 files changed, 235 insertions(+), 135 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index 2fc93266..21fbf74f 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -136,22 +136,11 @@ export class Runtime { * the Subscription Container (subscribed Component) * of each Job Observer. * - * It returns a boolean indicating whether any Subscription Container was updated. + * It returns a boolean indicating whether any Subscription Container was updated or not. * * @internal */ public updateSubscribers(): boolean { - if (!this.agileInstance().hasIntegration()) { - this.jobsToRerender = []; - this.notReadyJobsToRerender = new Set(); - return false; - } - if ( - this.jobsToRerender.length <= 0 && - this.notReadyJobsToRerender.size <= 0 - ) - return false; - // Build final 'jobsToRerender' array // based on the new 'jobsToRerender' array and the 'notReadyJobsToRerender' array. const jobsToRerender = this.jobsToRerender.concat( @@ -160,6 +149,9 @@ export class Runtime { this.notReadyJobsToRerender = new Set(); this.jobsToRerender = []; + if (!this.agileInstance().hasIntegration() || jobsToRerender.length <= 0) + return false; + // Extract Subscription Container from the Jobs to be rerendered const subscriptionContainerToUpdate = this.extractToUpdateSubscriptionContainer( jobsToRerender @@ -277,7 +269,7 @@ export class Runtime { } /** - * Maps the values of updated Observers (`updatedSubscribers`) + * Maps the values of the updated Observers (`updatedSubscribers`) * of the specified Subscription Container into a key map. * * The key containing the Observer value is extracted from the Observer itself @@ -293,7 +285,7 @@ export class Runtime { for (const observer of subscriptionContainer.updatedSubscribers) { const key = subscriptionContainer.subscriberKeysWeakMap.get(observer) ?? - subscriptionContainer.key; + observer.key; if (key != null) props[key] = observer.value; } return props; @@ -324,7 +316,7 @@ export class Runtime { // no matter what was updated in the Observer if (selectorMethods == null) return true; - // Check if a selected part of Observer value has changed + // Check if a selected part of the Observer value has changed const previousValue = job.observer.previousValue; const newValue = job.observer.value; for (const selectorMethod of selectorMethods) { diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index 03b28ee5..850ebc4a 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -52,14 +52,35 @@ describe('Runtime Tests', () => { runtime.perform = jest.fn(); }); - it('should perform passed Job (default config)', () => { + it("should perform specified Job immediately if jobQueue isn't currently being processed (default config)", () => { + runtime.isPerformingJobs = false; + + runtime.ingest(dummyJob); + + expect(runtime.jobQueue.length).toBe(0); + expect(runtime.perform).toHaveBeenCalledWith(dummyJob); + }); + + it("shouldn't perform specified Job immediately if jobQueue is currently being processed (default config)", () => { + runtime.isPerformingJobs = true; + runtime.ingest(dummyJob); + expect(runtime.jobQueue.length).toBe(1); + expect(runtime.jobQueue[0]).toBe(dummyJob); + expect(runtime.perform).not.toHaveBeenCalled(); + }); + + it('should perform specified Job immediately (config.perform = true)', () => { + runtime.isPerformingJobs = true; + runtime.ingest(dummyJob, { perform: true }); + expect(runtime.jobQueue.length).toBe(0); expect(runtime.perform).toHaveBeenCalledWith(dummyJob); }); - it("shouldn't perform passed Job (config.perform = false)", () => { + it("shouldn't perform specified Job immediately (config.perform = false)", () => { + runtime.isPerformingJobs = false; runtime.ingest(dummyJob, { perform: false }); expect(runtime.jobQueue.length).toBe(1); @@ -88,32 +109,36 @@ describe('Runtime Tests', () => { dummyObserver2.ingest = jest.fn(); }); - it('should perform passed and all in jobQueue remaining Jobs and call updateSubscribers', async () => { - runtime.jobQueue.push(dummyJob2); - runtime.jobQueue.push(dummyJob3); + it( + 'should perform specified Job and all remaining Jobs in the jobQueue,' + + ' and call updateSubscribers if at least one performed Job needs to rerender', + async () => { + runtime.jobQueue.push(dummyJob2); + runtime.jobQueue.push(dummyJob3); - runtime.perform(dummyJob1); + runtime.perform(dummyJob1); - expect(dummyObserver1.perform).toHaveBeenCalledWith(dummyJob1); - expect(dummyJob1.performed).toBeTruthy(); - expect(dummyObserver2.perform).toHaveBeenCalledWith(dummyJob2); - expect(dummyJob2.performed).toBeTruthy(); - expect(dummyObserver1.perform).toHaveBeenCalledWith(dummyJob3); - expect(dummyJob3.performed).toBeTruthy(); + expect(dummyObserver1.perform).toHaveBeenCalledWith(dummyJob1); + expect(dummyJob1.performed).toBeTruthy(); + expect(dummyObserver2.perform).toHaveBeenCalledWith(dummyJob2); + expect(dummyJob2.performed).toBeTruthy(); + expect(dummyObserver1.perform).toHaveBeenCalledWith(dummyJob3); + expect(dummyJob3.performed).toBeTruthy(); - expect(runtime.jobQueue.length).toBe(0); - expect(runtime.jobsToRerender.length).toBe(2); - expect(runtime.jobsToRerender.includes(dummyJob1)).toBeTruthy(); - expect(runtime.jobsToRerender.includes(dummyJob2)).toBeTruthy(); - expect(runtime.jobsToRerender.includes(dummyJob3)).toBeFalsy(); + expect(runtime.jobQueue.length).toBe(0); + expect(runtime.jobsToRerender.length).toBe(2); + expect(runtime.jobsToRerender.includes(dummyJob1)).toBeTruthy(); + expect(runtime.jobsToRerender.includes(dummyJob2)).toBeTruthy(); + expect(runtime.jobsToRerender.includes(dummyJob3)).toBeFalsy(); - // Sleep 5ms because updateSubscribers get called in Timeout - await new Promise((resolve) => setTimeout(resolve, 5)); + // Sleep 5ms because updateSubscribers is called in a timeout + await new Promise((resolve) => setTimeout(resolve, 5)); - expect(runtime.updateSubscribers).toHaveBeenCalledTimes(1); - }); + expect(runtime.updateSubscribers).toHaveBeenCalledTimes(1); + } + ); - it('should perform passed Job and update it dependents', async () => { + it('should perform specified Job and ingest its dependents into the runtime', async () => { dummyJob1.observer.dependents.add(dummyObserver2); dummyJob1.observer.dependents.add(dummyObserver1); @@ -132,29 +157,133 @@ describe('Runtime Tests', () => { expect(dummyObserver2.ingest).toHaveBeenCalledTimes(1); }); - it("should perform passed and all in jobQueue remaining Jobs and shouldn't call updateSubscribes if no job needs to rerender", async () => { - dummyJob1.rerender = false; - runtime.jobQueue.push(dummyJob3); + it( + 'should perform specified Job and all remaining Jobs in the jobQueue' + + " and shouldn't call updateSubscribes if no performed Job needs to rerender", + async () => { + dummyJob1.rerender = false; + runtime.jobQueue.push(dummyJob3); - runtime.perform(dummyJob1); + runtime.perform(dummyJob1); - expect(dummyObserver1.perform).toHaveBeenCalledWith(dummyJob1); - expect(dummyJob1.performed).toBeTruthy(); - expect(dummyObserver1.perform).toHaveBeenCalledWith(dummyJob3); - expect(dummyJob3.performed).toBeTruthy(); + expect(dummyObserver1.perform).toHaveBeenCalledWith(dummyJob1); + expect(dummyJob1.performed).toBeTruthy(); + expect(dummyObserver1.perform).toHaveBeenCalledWith(dummyJob3); + expect(dummyJob3.performed).toBeTruthy(); - expect(runtime.jobQueue.length).toBe(0); - expect(runtime.jobsToRerender.length).toBe(0); + expect(runtime.jobQueue.length).toBe(0); + expect(runtime.jobsToRerender.length).toBe(0); - // Sleep 5ms because updateSubscribers get called in Timeout - await new Promise((resolve) => setTimeout(resolve, 5)); + // Sleep 5ms because updateSubscribers is called in a timeout + await new Promise((resolve) => setTimeout(resolve, 5)); - expect(runtime.updateSubscribers).not.toHaveBeenCalled(); - }); + expect(runtime.updateSubscribers).not.toHaveBeenCalled(); + } + ); }); - describe('updateSubscriptions function tests', () => { - // TODO + describe('updateSubscribers function tests', () => { + let dummyJob1: RuntimeJob; + let dummyJob2: RuntimeJob; + let dummyJob3: RuntimeJob; + const dummySubscriptionContainer1IntegrationInstance = () => { + /* empty function */ + }; + let dummySubscriptionContainer1: SubscriptionContainer; + const dummySubscriptionContainer2IntegrationInstance = { + my: 'cool component', + }; + let dummySubscriptionContainer2: SubscriptionContainer; + + beforeEach(() => { + dummySubscriptionContainer1 = dummyAgile.subController.subscribe( + dummySubscriptionContainer1IntegrationInstance, + [dummyObserver1] + ); + dummySubscriptionContainer2 = dummyAgile.subController.subscribe( + dummySubscriptionContainer2IntegrationInstance, + [dummyObserver2, dummyObserver3] + ); + + dummyJob1 = new RuntimeJob(dummyObserver1); + dummyJob2 = new RuntimeJob(dummyObserver2); + dummyJob3 = new RuntimeJob(dummyObserver3); + + runtime.updateSubscriptionContainer = jest.fn(); + jest.spyOn(runtime, 'extractToUpdateSubscriptionContainer'); + }); + + it('should return false if Agile has no registered Integration', () => { + dummyAgile.hasIntegration = jest.fn(() => false); + runtime.jobsToRerender.push(dummyJob1); + runtime.jobsToRerender.push(dummyJob2); + + const response = runtime.updateSubscribers(); + + expect(response).toBeFalsy(); + expect(runtime.jobsToRerender).toStrictEqual([]); + expect(runtime.notReadyJobsToRerender.size).toBe(0); + expect( + runtime.extractToUpdateSubscriptionContainer + ).not.toHaveBeenCalled(); + expect(runtime.updateSubscriptionContainer).not.toHaveBeenCalled(); + }); + + it('should return false if jobsToRerender and notReadyJobsToRerender queue is empty', () => { + dummyAgile.hasIntegration = jest.fn(() => true); + runtime.jobsToRerender = []; + runtime.notReadyJobsToRerender = new Set(); + + const response = runtime.updateSubscribers(); + + expect(response).toBeFalsy(); + }); + + it('should return false if no Subscription Container of the Jobs to rerender needs to update', () => { + dummyAgile.hasIntegration = jest.fn(() => true); + jest + .spyOn(runtime, 'extractToUpdateSubscriptionContainer') + .mockReturnValueOnce([]); + runtime.jobsToRerender.push(dummyJob1); + runtime.jobsToRerender.push(dummyJob2); + runtime.notReadyJobsToRerender.add(dummyJob3); + + const response = runtime.updateSubscribers(); + + expect(response).toBeFalsy(); + expect(runtime.jobsToRerender).toStrictEqual([]); + expect(runtime.notReadyJobsToRerender.size).toBe(0); + expect( + runtime.extractToUpdateSubscriptionContainer + ).toHaveBeenCalledWith([dummyJob1, dummyJob2, dummyJob3]); + expect(runtime.updateSubscriptionContainer).not.toHaveBeenCalled(); + }); + + it('should return true if at least one Subscription Container of the Jobs to rerender needs to update', () => { + dummyAgile.hasIntegration = jest.fn(() => true); + jest + .spyOn(runtime, 'extractToUpdateSubscriptionContainer') + .mockReturnValueOnce([ + dummySubscriptionContainer1, + dummySubscriptionContainer2, + ]); + runtime.jobsToRerender.push(dummyJob1); + runtime.jobsToRerender.push(dummyJob2); + runtime.notReadyJobsToRerender.add(dummyJob3); + + const response = runtime.updateSubscribers(); + + expect(response).toBeTruthy(); + expect(runtime.jobsToRerender).toStrictEqual([]); + expect(runtime.notReadyJobsToRerender.size).toBe(0); + expect( + runtime.extractToUpdateSubscriptionContainer + ).toHaveBeenCalledWith([dummyJob1, dummyJob2, dummyJob3]); + expect(runtime.updateSubscriptionContainer).toHaveBeenCalledWith([ + dummySubscriptionContainer1, + dummySubscriptionContainer2, + ]); + }); }); describe('extractToUpdateSubscriptionContainer function tests', () => { @@ -174,29 +303,41 @@ describe('Runtime Tests', () => { beforeEach(() => { subscriptionContainer = dummyAgile.subController.subscribe( dummyFunction, - { - observer1: dummyObserver1, - observer2: dummyObserver2, - observer3: dummyObserver3, - } - ).subscriptionContainer; + [dummyObserver1, dummyObserver2, dummyObserver3] + ); dummyObserver1.value = 'dummyObserverValue1'; dummyObserver3.value = 'dummyObserverValue3'; + + dummyObserver1._key = 'dummyObserver1KeyInObserver'; + dummyObserver2._key = undefined; + subscriptionContainer.subscriberKeysWeakMap.set( + dummyObserver2, + 'dummyObserver2KeyInWeakMap' + ); + dummyObserver3._key = 'dummyObserver3KeyInObserver'; + subscriptionContainer.subscriberKeysWeakMap.set( + dummyObserver3, + 'dummyObserver3KeyInWeakMap' + ); }); - it('should build Observer Value Object out of observerKeysToUpdate and Value of Observer', () => { - subscriptionContainer.updatedSubscribers.push('observer1'); - subscriptionContainer.updatedSubscribers.push('observer2'); - subscriptionContainer.updatedSubscribers.push('observer3'); + it('should map the values of the updated Observers into an object and return it', () => { + subscriptionContainer.updatedSubscribers.push(dummyObserver1); + subscriptionContainer.updatedSubscribers.push(dummyObserver2); + subscriptionContainer.updatedSubscribers.push(dummyObserver3); const props = runtime.getUpdatedObserverValues(subscriptionContainer); expect(props).toStrictEqual({ - observer1: 'dummyObserverValue1', - observer2: undefined, - observer3: 'dummyObserverValue3', + dummyObserver1KeyInObserver: 'dummyObserverValue1', + dummyObserver2KeyInWeakMap: undefined, + dummyObserver3KeyInWeakMap: 'dummyObserverValue3', }); - expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); + expect(subscriptionContainer.updatedSubscribers).toStrictEqual([ + dummyObserver1, + dummyObserver2, + dummyObserver3, + ]); }); }); @@ -214,38 +355,33 @@ describe('Runtime Tests', () => { let arrayJob: RuntimeJob; beforeEach(() => { - // Create Job with Object value + // Create Job with object based value objectSubscriptionContainer = dummyAgile.subController.subscribe( dummyFunction, - { observer1: dummyObserver1 } - ).subscriptionContainer; + [dummyObserver1] + ); dummyObserver1.value = { - key: 'dummyObserverValue1', data: { name: 'jeff' }, }; dummyObserver1.previousValue = { - key: 'dummyObserverValue1', data: { name: 'jeff' }, }; - objectSubscriptionContainer.isProxyBased = true; - objectSubscriptionContainer.proxyKeyMap = { - [dummyObserver1._key || 'unknown']: { paths: [['data', 'name']] }, - }; + objectSubscriptionContainer.selectorsWeakMap.set(dummyObserver1, { + methods: [(value) => value?.data?.name], + }); objectJob = new RuntimeJob(dummyObserver1, { key: 'dummyObjectJob1' }); - // Create Job with Array value - arraySubscriptionContainer = dummyAgile.subController.subscribeWithSubsObject( + // Create Job with array based value + arraySubscriptionContainer = dummyAgile.subController.subscribe( dummyFunction2, - { observer2: dummyObserver2 } + { dummyObserver2: dummyObserver2 } ).subscriptionContainer; dummyObserver2.value = [ { - key: 'dummyObserver2Value1', data: { name: 'jeff' }, }, { - key: 'dummyObserver2Value2', data: { name: 'hans' }, }, ]; @@ -259,23 +395,20 @@ describe('Runtime Tests', () => { data: { name: 'hans' }, }, ]; - arraySubscriptionContainer.isProxyBased = true; - arraySubscriptionContainer.proxyKeyMap = { - [dummyObserver2._key || 'unknown']: { - paths: [['0', 'data', 'name']], - }, - }; + arraySubscriptionContainer.selectorsWeakMap.set(dummyObserver2, { + methods: [(value) => value[0]?.data?.name], + }); arrayJob = new RuntimeJob(dummyObserver2, { key: 'dummyObjectJob2' }); jest.spyOn(Utils, 'notEqual'); - // Because not equals is called once during the creation of the subscriptionContainer + // Because not equals is called once during the creation of the Subscription Containers jest.clearAllMocks(); }); - it("should return true if subscriptionContainer isn't proxy based", () => { - objectSubscriptionContainer.isProxyBased = false; + it('should return true if Subscritpion Container has no selector methods', () => { + objectSubscriptionContainer.selectorsWeakMap.delete(dummyObserver1); const response = runtime.handleSelectors( objectSubscriptionContainer, @@ -286,33 +419,7 @@ describe('Runtime Tests', () => { expect(Utils.notEqual).not.toHaveBeenCalled(); }); - it('should return true if observer the job represents has no key', () => { - objectJob.observer._key = undefined; - - const response = runtime.handleSelectors( - objectSubscriptionContainer, - objectJob - ); - - expect(response).toBeTruthy(); - expect(Utils.notEqual).not.toHaveBeenCalled(); - }); - - it("should return true if the observer key isn't represented in the proxyKeyMap", () => { - objectSubscriptionContainer.proxyKeyMap = { - unknownKey: { paths: [['a', 'b']] }, - }; - - const response = runtime.handleSelectors( - objectSubscriptionContainer, - objectJob - ); - - expect(response).toBeTruthy(); - expect(Utils.notEqual).not.toHaveBeenCalled(); - }); - - it('should return true if used property has changed (object value)', () => { + it('should return true if selected property has changed (object value)', () => { dummyObserver1.value = { key: 'dummyObserverValue1', data: { name: 'hans' }, @@ -330,7 +437,7 @@ describe('Runtime Tests', () => { ); }); - it("should return false if used property hasn't changed (object value)", () => { + it("should return false if selected property hasn't changed (object value)", () => { const response = runtime.handleSelectors( objectSubscriptionContainer, objectJob @@ -343,23 +450,24 @@ describe('Runtime Tests', () => { ); }); - it('should return true if used property has changed in the deepness (object value)', () => { - dummyObserver1.value = { - key: 'dummyObserverValue1', - }; - dummyObserver1.previousValue = { - key: 'dummyObserverValue1', - data: { name: undefined }, - }; - - const response = runtime.handleSelectors( - objectSubscriptionContainer, - objectJob - ); - - expect(response).toBeTruthy(); - expect(Utils.notEqual).toHaveBeenCalledWith(undefined, undefined); - }); + // TODO the deepness check isn't possible with the custom defined selector methods + // it('should return true if selected property has changed in the deepness (object value)', () => { + // dummyObserver1.value = { + // key: 'dummyObserverValue1', + // }; + // dummyObserver1.previousValue = { + // key: 'dummyObserverValue1', + // data: { name: undefined }, + // }; + // + // const response = runtime.handleSelectors( + // objectSubscriptionContainer, + // objectJob + // ); + // + // expect(response).toBeTruthy(); + // expect(Utils.notEqual).toHaveBeenCalledWith(undefined, undefined); + // }); it('should return true if used property has changed (array value)', () => { dummyObserver2.value = [ From 7cf329fd1c838dae343d2259ff9e551dc3bdc562 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 10 Jun 2021 09:20:57 +0200 Subject: [PATCH 44/63] added basic extractToUpdateSubscriptionContainer tests --- packages/core/src/runtime/index.ts | 18 +- .../container/SubscriptionContainer.ts | 2 +- .../core/tests/unit/runtime/runtime.test.ts | 257 +++++++++++++++++- 3 files changed, 260 insertions(+), 17 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index 21fbf74f..516b9001 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -214,17 +214,21 @@ export class Runtime { updateSubscriptionContainer && this.handleSelectors(subscriptionContainer, job); + // TODO has to be overthought because if it is a Component based Subscription + // the rerender is triggered via merging the changed properties into the Component. + // Although the 'componentId' might be equal, it doesn't mean + // that the changed properties are the equal! (-> changed properties would get missing) // Check if Subscription Container with same 'componentId' // is already in the 'subscriptionToUpdate' queue (rerender optimisation) - updateSubscriptionContainer = - updateSubscriptionContainer && - Array.from(subscriptionsToUpdate).findIndex( - (sc) => sc.componentId === subscriptionContainer.componentId - ) === -1; + // updateSubscriptionContainer = + // updateSubscriptionContainer && + // Array.from(subscriptionsToUpdate).findIndex( + // (sc) => sc.componentId === subscriptionContainer.componentId + // ) === -1; // Add Subscription Container to the 'subscriptionsToUpdate' queue if (updateSubscriptionContainer) { - subscriptionContainer.updatedSubscribers.push(job.observer); + subscriptionContainer.updatedSubscribers.add(job.observer); subscriptionsToUpdate.add(subscriptionContainer); } @@ -260,7 +264,7 @@ export class Runtime { this.getUpdatedObserverValues(subscriptionContainer) ); - subscriptionContainer.updatedSubscribers = []; + subscriptionContainer.updatedSubscribers.clear(); }); Agile.logger.if diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index e9f345bc..3e2ca5f9 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -39,7 +39,7 @@ export class SubscriptionContainer { * that were performed by the runtime * and are currently running through the update Subscription Container (rerender) process. */ - public updatedSubscribers: Array = []; + public updatedSubscribers: Set = new Set(); /** * Whether the Subscription Container is object based. diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index 850ebc4a..dda6d98d 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -287,7 +287,248 @@ describe('Runtime Tests', () => { }); describe('extractToUpdateSubscriptionContainer function tests', () => { - // TODO + let dummyJob1: RuntimeJob; + let dummyJob2: RuntimeJob; + const dummySubscriptionContainer1IntegrationInstance = () => { + /* empty function */ + }; + let dummySubscriptionContainer1: SubscriptionContainer; + const dummySubscriptionContainer2IntegrationInstance = { + my: 'cool component', + }; + let dummySubscriptionContainer2: SubscriptionContainer; + + beforeEach(() => { + dummySubscriptionContainer1 = dummyAgile.subController.subscribe( + dummySubscriptionContainer1IntegrationInstance, + [dummyObserver1] + ); + dummySubscriptionContainer2 = dummyAgile.subController.subscribe( + dummySubscriptionContainer2IntegrationInstance, + [dummyObserver2] + ); + + dummyJob1 = new RuntimeJob(dummyObserver1); + dummyJob2 = new RuntimeJob(dummyObserver2); + + jest.spyOn(runtime, 'handleSelectors'); + }); + + it( + "shouldn't extract not ready Subscription Container from the specified Jobs, " + + "add it to the 'notReadyJobsToRerender' queue and print warning", + () => { + jest + .spyOn(runtime, 'handleSelectors') + .mockReturnValueOnce(true) + .mockReturnValueOnce(true); + dummySubscriptionContainer1.ready = true; + dummySubscriptionContainer2.ready = false; + + const response = runtime.extractToUpdateSubscriptionContainer([ + dummyJob1, + dummyJob2, + ]); + + expect(response).toStrictEqual([dummySubscriptionContainer1]); + + // Called with Job that ran through + expect(runtime.handleSelectors).toHaveBeenCalledTimes(1); + expect(runtime.handleSelectors).toHaveBeenCalledWith( + dummySubscriptionContainer1, + dummyJob1 + ); + + expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([ + dummyJob2, + ]); + + // Job that ran through + expect( + Array.from(dummyJob1.subscriptionContainersToUpdate) + ).toStrictEqual([]); + expect(dummyJob1.triesToUpdate).toBe(0); + expect( + Array.from(dummySubscriptionContainer1.updatedSubscribers) + ).toStrictEqual([dummyObserver1]); + + // Job that didn't ran through + expect( + Array.from(dummyJob2.subscriptionContainersToUpdate) + ).toStrictEqual([dummySubscriptionContainer2]); + expect(dummyJob2.triesToUpdate).toBe(1); + expect( + Array.from(dummySubscriptionContainer2.updatedSubscribers) + ).toStrictEqual([]); + + // Called with Job that didn't ran through + expect(console.warn).toHaveBeenCalledTimes(1); + LogMock.hasLoggedCode( + '16:02:00', + [dummySubscriptionContainer2.key], + dummySubscriptionContainer2 + ); + } + ); + + it( + "shouldn't extract not ready Subscription Container from the specified Jobs, " + + "remove the Job when it exceeded the max 'numberOfTriesToUpdate' " + + 'and print warning', + () => { + jest + .spyOn(runtime, 'handleSelectors') + .mockReturnValueOnce(true) + .mockReturnValueOnce(true); + dummySubscriptionContainer1.ready = true; + dummySubscriptionContainer2.ready = false; + const numberOfTries = + (dummyJob2.config.numberOfTriesToUpdate ?? 0) + 1; + dummyJob2.triesToUpdate = numberOfTries; + + const response = runtime.extractToUpdateSubscriptionContainer([ + dummyJob1, + dummyJob2, + ]); + + expect(response).toStrictEqual([dummySubscriptionContainer1]); + + // Called with Job that ran through + expect(runtime.handleSelectors).toHaveBeenCalledTimes(1); + expect(runtime.handleSelectors).toHaveBeenCalledWith( + dummySubscriptionContainer1, + dummyJob1 + ); + + expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); // Because not ready Job was removed + + // Job that ran through + expect( + Array.from(dummyJob1.subscriptionContainersToUpdate) + ).toStrictEqual([]); + expect(dummyJob1.triesToUpdate).toBe(0); + expect( + Array.from(dummySubscriptionContainer1.updatedSubscribers) + ).toStrictEqual([dummyObserver1]); + + // Job that didn't ran through + expect( + Array.from(dummyJob2.subscriptionContainersToUpdate) + ).toStrictEqual([dummySubscriptionContainer2]); + expect(dummyJob2.triesToUpdate).toBe(numberOfTries); + expect( + Array.from(dummySubscriptionContainer2.updatedSubscribers) + ).toStrictEqual([]); + + // Called with Job that didn't ran through + expect(console.warn).toHaveBeenCalledTimes(1); + LogMock.hasLoggedCode( + '16:02:01', + [numberOfTries], + dummySubscriptionContainer2 + ); + } + ); + + it("shouldn't extract Subscription Container if the selected property hasn't changed", () => { + jest + .spyOn(runtime, 'handleSelectors') + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + dummySubscriptionContainer1.ready = true; + dummySubscriptionContainer2.ready = true; + + const response = runtime.extractToUpdateSubscriptionContainer([ + dummyJob1, + dummyJob2, + ]); + + expect(response).toStrictEqual([dummySubscriptionContainer2]); + + expect(runtime.handleSelectors).toHaveBeenCalledTimes(2); + expect(runtime.handleSelectors).toHaveBeenCalledWith( + dummySubscriptionContainer1, + dummyJob1 + ); + expect(runtime.handleSelectors).toHaveBeenCalledWith( + dummySubscriptionContainer2, + dummyJob2 + ); + + // Since the Job is ready but the Observer value simply hasn't changed + expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); + + // Job that didn't ran through + expect( + Array.from(dummyJob1.subscriptionContainersToUpdate) + ).toStrictEqual([]); + expect(dummyJob1.triesToUpdate).toBe(0); + expect( + Array.from(dummySubscriptionContainer1.updatedSubscribers) + ).toStrictEqual([]); + + // Job that ran through + expect( + Array.from(dummyJob2.subscriptionContainersToUpdate) + ).toStrictEqual([]); + expect(dummyJob2.triesToUpdate).toBe(0); + expect( + Array.from(dummySubscriptionContainer2.updatedSubscribers) + ).toStrictEqual([dummyObserver2]); + + expect(console.warn).toHaveBeenCalledTimes(0); + }); + + it('should extract ready and updated Subscription Containers', () => { + jest + .spyOn(runtime, 'handleSelectors') + .mockReturnValueOnce(true) + .mockReturnValueOnce(true); + dummySubscriptionContainer1.ready = true; + dummySubscriptionContainer2.ready = true; + + const response = runtime.extractToUpdateSubscriptionContainer([ + dummyJob1, + dummyJob2, + ]); + + expect(response).toStrictEqual([ + dummySubscriptionContainer1, + dummySubscriptionContainer2, + ]); + + expect(runtime.handleSelectors).toHaveBeenCalledTimes(2); + expect(runtime.handleSelectors).toHaveBeenCalledWith( + dummySubscriptionContainer1, + dummyJob1 + ); + expect(runtime.handleSelectors).toHaveBeenCalledWith( + dummySubscriptionContainer2, + dummyJob2 + ); + + expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); + + // Job that ran through + expect( + Array.from(dummyJob1.subscriptionContainersToUpdate) + ).toStrictEqual([]); + expect(dummyJob1.triesToUpdate).toBe(0); + expect( + Array.from(dummySubscriptionContainer1.updatedSubscribers) + ).toStrictEqual([dummyObserver1]); + + // Job that ran through + expect( + Array.from(dummyJob2.subscriptionContainersToUpdate) + ).toStrictEqual([]); + expect(dummyJob2.triesToUpdate).toBe(0); + expect( + Array.from(dummySubscriptionContainer2.updatedSubscribers) + ).toStrictEqual([dummyObserver2]); + + expect(console.warn).not.toHaveBeenCalled(); + }); }); describe('updateSubscriptionContainer function tests', () => { @@ -322,9 +563,9 @@ describe('Runtime Tests', () => { }); it('should map the values of the updated Observers into an object and return it', () => { - subscriptionContainer.updatedSubscribers.push(dummyObserver1); - subscriptionContainer.updatedSubscribers.push(dummyObserver2); - subscriptionContainer.updatedSubscribers.push(dummyObserver3); + subscriptionContainer.updatedSubscribers.add(dummyObserver1); + subscriptionContainer.updatedSubscribers.add(dummyObserver2); + subscriptionContainer.updatedSubscribers.add(dummyObserver3); const props = runtime.getUpdatedObserverValues(subscriptionContainer); @@ -333,11 +574,9 @@ describe('Runtime Tests', () => { dummyObserver2KeyInWeakMap: undefined, dummyObserver3KeyInWeakMap: 'dummyObserverValue3', }); - expect(subscriptionContainer.updatedSubscribers).toStrictEqual([ - dummyObserver1, - dummyObserver2, - dummyObserver3, - ]); + expect( + Array.from(subscriptionContainer.updatedSubscribers) + ).toStrictEqual([dummyObserver1, dummyObserver2, dummyObserver3]); }); }); From a640755e957511696685ee2fcb5a6abd8ffed674 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Thu, 10 Jun 2021 18:19:56 +0200 Subject: [PATCH 45/63] fixed runtime tests --- packages/core/src/runtime/index.ts | 9 +- packages/core/src/runtime/observer.ts | 65 ++++++++------ packages/core/src/runtime/runtime.job.ts | 16 +--- .../core/tests/unit/runtime/observer.test.ts | 4 +- .../tests/unit/runtime/runtime.job.test.ts | 12 +-- .../core/tests/unit/runtime/runtime.test.ts | 90 ++++++++++++++++--- 6 files changed, 131 insertions(+), 65 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index 516b9001..9a3285e8 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -187,10 +187,10 @@ export class Runtime { // Handle not ready Subscription Container if (!subscriptionContainer.ready) { if ( - !job.config.numberOfTriesToUpdate || - job.triesToUpdate < job.config.numberOfTriesToUpdate + !job.config.maxOfTriesToUpdate || + job.triedToUpdateCount < job.config.maxOfTriesToUpdate ) { - job.triesToUpdate++; + job.triedToUpdateCount++; this.notReadyJobsToRerender.add(job); LogCodeManager.log( @@ -201,7 +201,7 @@ export class Runtime { } else { LogCodeManager.log( '16:02:01', - [job.config.numberOfTriesToUpdate], + [job.config.maxOfTriesToUpdate], subscriptionContainer ); } @@ -251,7 +251,6 @@ export class Runtime { public updateSubscriptionContainer( subscriptionsToUpdate: Array ): void { - // Update Subscription Containers (trigger rerender on Components they represent) subscriptionsToUpdate.forEach((subscriptionContainer) => { // Call 'callback function' if Callback based Subscription if (subscriptionContainer instanceof CallbackSubscriptionContainer) diff --git a/packages/core/src/runtime/observer.ts b/packages/core/src/runtime/observer.ts index 34e8cf7d..59d1d483 100644 --- a/packages/core/src/runtime/observer.ts +++ b/packages/core/src/runtime/observer.ts @@ -15,25 +15,39 @@ export class Observer { // Agile Instance the Observer belongs to public agileInstance: () => Agile; - // Key/Name identifier of the Subscription Container + // Key/Name identifier of the Observer public _key?: ObserverKey; // Observers that depend on this Observer public dependents: Set = new Set(); // Subscription Containers (Components) the Observer is subscribed to public subscribedTo: Set = new Set(); + // Current value of Observer public value?: ValueType; // Previous value of Observer public previousValue?: ValueType; /** - * Handles the subscriptions to Subscription Containers (Components) - * and keeps track of dependencies. + * An Observer manages the subscriptions to Subscription Containers (UI-Components) + * and dependencies to other Observers (Agile Classes) + * for an Agile Class like the `State Class`. + * + * An Agile Class can use an Observer as an interface to the Runtime. + * Thereby, it ingests its own Observer into the Runtime + * when the Agile Class has changed in such a way + * that these changes need to be applied to UI-Components or dependent Observers. + * + * After the Observer has been ingested into the Runtime + * wrapped into a Runtime-Job, it is first added to the Jobs queue. + * When it is executed, the Observer's `perform()` method is called, + * where the accordingly changes can be applied to the Agile Class. * - * All Agile Classes that can be bound a UI-Component have their own Observer - * which manages the above mentioned things for them. + * Now that the Job was performed, it is added to the rerender queue, + * where the subscribed Subscription Container (UI-Components) are updated (rerender) + * accordingly. * - * The Observer is no standalone class and should be extended from a 'real' Observer. + * Note that the Observer itself is no standalone class + * and should be inherited and adapted to fulfill the Agile Class functions. * * @internal * @param agileInstance - Instance of Agile the Observer belongs to. @@ -68,7 +82,7 @@ export class Observer { } /** - * Returns the key/name identifier of the State. + * Returns the key/name identifier of the Observer. * * @public */ @@ -77,9 +91,10 @@ export class Observer { } /** - * Ingests the Observer into the runtime, - * by creating a Runtime Job - * and adding the Observer to the created Job. + * Passes the Observer into the runtime wrapped into a Runtime-Job + * where it is executed accordingly + * by performing its `perform()` method, updating its dependents + * and the UI-Components it is subscribed to * * @public * @param config - Configuration object @@ -95,7 +110,7 @@ export class Observer { force: false, }); - // Create Job + // Create Runtime-Job const job = new RuntimeJob(this, { force: config.force, sideEffects: config.sideEffects, @@ -103,45 +118,43 @@ export class Observer { key: config.key || this._key, }); + // Pass created Job into the Runtime this.agileInstance().runtime.ingest(job, { perform: config.perform, }); } /** - * Method executed by the Runtime to perform the Runtime Job, - * previously ingested (`ingest()`) by the Observer. + * Method executed by the Runtime to perform the Runtime-Job, + * previously ingested via the `ingest()` method. + * + * Note that this method should be overwritten + * to correctly apply the changes to the Agile Class it belongs to. * * @public - * @param job - Runtime Job to be performed. + * @param job - Runtime-Job to be performed. */ public perform(job: RuntimeJob): void { LogCodeManager.log('17:03:00'); } /** - * Adds specified Observer to the dependents of this Observer. + * Makes specified Observer depend on the Observer. * - * Every time this Observer is ingested into the Runtime, - * the dependent Observers are ingested into the Runtime too. + * A dependent Observer is always ingested into the Runtime, + * when the Observer it depends on was ingested too. * * @public - * @param observer - Observer to depends on this Observer. + * @param observer - Observer to depend on the Observer. */ public addDependent(observer: Observer): void { if (!this.dependents.has(observer)) this.dependents.add(observer); } } -/** - * @param deps - Initial Dependents of Observer - * @param subs - Initial Subscriptions of Observer - * @param key - Key/Name of Observer - * @param value - Initial Value of Observer - */ export interface CreateObserverConfigInterface { /** - * Initial Observers that depend on this Observer. + * Initial Observers to depend on the Observer. * @default [] */ dependents?: Array; @@ -157,7 +170,7 @@ export interface CreateObserverConfigInterface { key?: ObserverKey; /** * Initial value of the Observer. - * @defualt undefined + * @default undefined */ value?: ValueType; } diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts index 9a459acb..b3a1588b 100644 --- a/packages/core/src/runtime/runtime.job.ts +++ b/packages/core/src/runtime/runtime.job.ts @@ -19,7 +19,7 @@ export class RuntimeJob { // Subscription Container of the Observer that have to be updated/re-rendered public subscriptionContainersToUpdate = new Set(); // How often not ready Subscription Container of the Observer have been tried to update - public triesToUpdate = 0; + public triedToUpdateCount = 0; /** * A Job that contains an Observer to be executed by the runtime. @@ -39,13 +39,13 @@ export class RuntimeJob { exclude: [], }, force: false, - numberOfTriesToUpdate: 3, + maxOfTriesToUpdate: 3, }); this.config = { background: config.background, force: config.force, sideEffects: config.sideEffects, - numberOfTriesToUpdate: config.numberOfTriesToUpdate, + maxOfTriesToUpdate: config.maxOfTriesToUpdate, }; this.observer = observer; this.rerender = @@ -86,14 +86,6 @@ export interface CreateRuntimeJobConfigInterface key?: RuntimeJobKey; } -/** - * @param background - If Job gets executed in the background -> not causing any rerender - * @param sideEffects - If SideEffects get executed - * @param force - Force performing Job - * @param numberOfTriesToUpdate - How often the runtime should try to update not ready SubscriptionContainers of this Job - * If 'null' the runtime tries to update the not ready SubscriptionContainer until they are ready (infinite). - * But be aware that this can lead to an overflow of 'old' Jobs after some time. (affects performance) - */ export interface RuntimeJobConfigInterface { /** * Whether to perform the Runtime Job in background. @@ -117,7 +109,7 @@ export interface RuntimeJobConfigInterface { * If 'null' the runtime tries to update the not ready Subscription Container until they are ready (infinite). * @default 3 */ - numberOfTriesToUpdate?: number | null; + maxOfTriesToUpdate?: number | null; } export interface SideEffectConfigInterface { diff --git a/packages/core/tests/unit/runtime/observer.test.ts b/packages/core/tests/unit/runtime/observer.test.ts index 11c9ff5b..d44c4b3e 100644 --- a/packages/core/tests/unit/runtime/observer.test.ts +++ b/packages/core/tests/unit/runtime/observer.test.ts @@ -93,7 +93,7 @@ describe('Observer Tests', () => { exclude: [], }, force: false, - numberOfTriesToUpdate: 3, + maxOfTriesToUpdate: 3, }); }); @@ -118,7 +118,7 @@ describe('Observer Tests', () => { exclude: [], }, force: true, - numberOfTriesToUpdate: 3, + maxOfTriesToUpdate: 3, }); }); diff --git a/packages/core/tests/unit/runtime/runtime.job.test.ts b/packages/core/tests/unit/runtime/runtime.job.test.ts index e9fd39df..b0dbce53 100644 --- a/packages/core/tests/unit/runtime/runtime.job.test.ts +++ b/packages/core/tests/unit/runtime/runtime.job.test.ts @@ -31,12 +31,12 @@ describe('RuntimeJob Tests', () => { exclude: [], }, force: false, - numberOfTriesToUpdate: 3, + maxOfTriesToUpdate: 3, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); expect(job.subscriptionContainersToUpdate.size).toBe(0); - expect(job.triesToUpdate).toBe(0); + expect(job.triedToUpdateCount).toBe(0); }); it('should create RuntimeJob with Agile that has integrations (specific config)', () => { @@ -48,7 +48,7 @@ describe('RuntimeJob Tests', () => { enabled: false, }, force: true, - numberOfTriesToUpdate: 10, + maxOfTriesToUpdate: 10, }); expect(job._key).toBe('dummyJob'); @@ -59,7 +59,7 @@ describe('RuntimeJob Tests', () => { enabled: false, }, force: true, - numberOfTriesToUpdate: 10, + maxOfTriesToUpdate: 10, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); @@ -78,7 +78,7 @@ describe('RuntimeJob Tests', () => { exclude: [], }, force: false, - numberOfTriesToUpdate: 3, + maxOfTriesToUpdate: 3, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); @@ -99,7 +99,7 @@ describe('RuntimeJob Tests', () => { exclude: [], }, force: false, - numberOfTriesToUpdate: 3, + maxOfTriesToUpdate: 3, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index dda6d98d..b967c1cd 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -347,7 +347,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob1.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob1.triesToUpdate).toBe(0); + expect(dummyJob1.triedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer1.updatedSubscribers) ).toStrictEqual([dummyObserver1]); @@ -356,7 +356,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob2.subscriptionContainersToUpdate) ).toStrictEqual([dummySubscriptionContainer2]); - expect(dummyJob2.triesToUpdate).toBe(1); + expect(dummyJob2.triedToUpdateCount).toBe(1); expect( Array.from(dummySubscriptionContainer2.updatedSubscribers) ).toStrictEqual([]); @@ -373,7 +373,7 @@ describe('Runtime Tests', () => { it( "shouldn't extract not ready Subscription Container from the specified Jobs, " + - "remove the Job when it exceeded the max 'numberOfTriesToUpdate' " + + "remove the Job when it exceeded the max 'maxOfTriesToUpdate' " + 'and print warning', () => { jest @@ -382,9 +382,8 @@ describe('Runtime Tests', () => { .mockReturnValueOnce(true); dummySubscriptionContainer1.ready = true; dummySubscriptionContainer2.ready = false; - const numberOfTries = - (dummyJob2.config.numberOfTriesToUpdate ?? 0) + 1; - dummyJob2.triesToUpdate = numberOfTries; + const numberOfTries = (dummyJob2.config.maxOfTriesToUpdate ?? 0) + 1; + dummyJob2.triedToUpdateCount = numberOfTries; const response = runtime.extractToUpdateSubscriptionContainer([ dummyJob1, @@ -406,7 +405,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob1.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob1.triesToUpdate).toBe(0); + expect(dummyJob1.triedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer1.updatedSubscribers) ).toStrictEqual([dummyObserver1]); @@ -415,7 +414,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob2.subscriptionContainersToUpdate) ).toStrictEqual([dummySubscriptionContainer2]); - expect(dummyJob2.triesToUpdate).toBe(numberOfTries); + expect(dummyJob2.triedToUpdateCount).toBe(numberOfTries); expect( Array.from(dummySubscriptionContainer2.updatedSubscribers) ).toStrictEqual([]); @@ -424,7 +423,7 @@ describe('Runtime Tests', () => { expect(console.warn).toHaveBeenCalledTimes(1); LogMock.hasLoggedCode( '16:02:01', - [numberOfTries], + [dummyJob2.config.maxOfTriesToUpdate], dummySubscriptionContainer2 ); } @@ -462,7 +461,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob1.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob1.triesToUpdate).toBe(0); + expect(dummyJob1.triedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer1.updatedSubscribers) ).toStrictEqual([]); @@ -471,7 +470,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob2.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob2.triesToUpdate).toBe(0); + expect(dummyJob2.triedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer2.updatedSubscribers) ).toStrictEqual([dummyObserver2]); @@ -513,7 +512,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob1.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob1.triesToUpdate).toBe(0); + expect(dummyJob1.triedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer1.updatedSubscribers) ).toStrictEqual([dummyObserver1]); @@ -522,7 +521,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob2.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob2.triesToUpdate).toBe(0); + expect(dummyJob2.triedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer2.updatedSubscribers) ).toStrictEqual([dummyObserver2]); @@ -532,7 +531,70 @@ describe('Runtime Tests', () => { }); describe('updateSubscriptionContainer function tests', () => { - // TODO + const dummyIntegration1 = { dummy: 'component' }; + let componentSubscriptionContainer1: ComponentSubscriptionContainer; + const dummyIntegration2 = jest.fn(); + let callbackSubscriptionContainer2: CallbackSubscriptionContainer; + const dummyIntegration3 = jest.fn(); + let callbackSubscriptionContainer3: CallbackSubscriptionContainer; + + beforeEach(() => { + componentSubscriptionContainer1 = dummyAgile.subController.subscribe( + dummyIntegration1, + [dummyObserver1] + ) as ComponentSubscriptionContainer; + componentSubscriptionContainer1.updatedSubscribers = new Set([ + dummyObserver1, + ]); + callbackSubscriptionContainer2 = dummyAgile.subController.subscribe( + dummyIntegration2, + [dummyObserver2] + ) as CallbackSubscriptionContainer; + callbackSubscriptionContainer2.updatedSubscribers = new Set([ + dummyObserver2, + ]); + callbackSubscriptionContainer3 = dummyAgile.subController.subscribe( + dummyIntegration3, + [dummyObserver3] + ) as CallbackSubscriptionContainer; + callbackSubscriptionContainer3.updatedSubscribers = new Set([ + dummyObserver3, + ]); + + dummyAgile.integrations.update = jest.fn(); + }); + + it('should update the specified Subscription Container', () => { + jest + .spyOn(runtime, 'getUpdatedObserverValues') + .mockReturnValueOnce('propsBasedOnUpdatedObservers' as any); + + runtime.updateSubscriptionContainer([ + componentSubscriptionContainer1, + callbackSubscriptionContainer2, + callbackSubscriptionContainer3, + ]); + + // Component Subscription Container 1 + expect(dummyAgile.integrations.update).toHaveBeenCalledTimes(1); + expect(dummyAgile.integrations.update).toHaveBeenCalledWith( + dummyIntegration1, + 'propsBasedOnUpdatedObservers' + ); + expect( + Array.from(componentSubscriptionContainer1.updatedSubscribers) + ).toStrictEqual([]); + + // Callback Subscription Container 2 + expect(callbackSubscriptionContainer2.callback).toHaveBeenCalledTimes( + 1 + ); + + // Callback Subscription Container 3 + expect(callbackSubscriptionContainer3.callback).toHaveBeenCalledTimes( + 1 + ); + }); }); describe('getUpdatedObserverValues function tests', () => { From faf3fc7b0994cbd0d5b201f4b0ea5aedaafea807 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 10 Jun 2021 20:04:20 +0200 Subject: [PATCH 46/63] fixed typos --- packages/core/src/runtime/observer.ts | 30 +++--- .../core/tests/unit/runtime/observer.test.ts | 99 +++++-------------- .../CallbackSubscriptionContainer.test.ts | 4 +- .../ComponentSubscriptionContainer.test.ts | 4 +- .../container/SubscriptionContainer.test.ts | 48 ++++----- 5 files changed, 73 insertions(+), 112 deletions(-) diff --git a/packages/core/src/runtime/observer.ts b/packages/core/src/runtime/observer.ts index 59d1d483..0ec534e2 100644 --- a/packages/core/src/runtime/observer.ts +++ b/packages/core/src/runtime/observer.ts @@ -32,22 +32,23 @@ export class Observer { * and dependencies to other Observers (Agile Classes) * for an Agile Class like the `State Class`. * - * An Agile Class can use an Observer as an interface to the Runtime. - * Thereby, it ingests its own Observer into the Runtime + * Agile Classes often use an Observer as an interface to the Runtime. + * In doing so, they ingest their own Observer into the Runtime * when the Agile Class has changed in such a way * that these changes need to be applied to UI-Components or dependent Observers. * * After the Observer has been ingested into the Runtime - * wrapped into a Runtime-Job, it is first added to the Jobs queue. + * wrapped into a Runtime-Job, it is first added to the Jobs queue + * to prevent race conditions. * When it is executed, the Observer's `perform()` method is called, - * where the accordingly changes can be applied to the Agile Class. + * where the accordingly changes are applied to the Agile Class. * * Now that the Job was performed, it is added to the rerender queue, - * where the subscribed Subscription Container (UI-Components) are updated (rerender) - * accordingly. + * where the subscribed Subscription Container (UI-Components) + * of the Observer are updated (rerender). * * Note that the Observer itself is no standalone class - * and should be inherited and adapted to fulfill the Agile Class functions. + * and should be adapted to the Agile Class it belongs to. * * @internal * @param agileInstance - Instance of Agile the Observer belongs to. @@ -94,7 +95,7 @@ export class Observer { * Passes the Observer into the runtime wrapped into a Runtime-Job * where it is executed accordingly * by performing its `perform()` method, updating its dependents - * and the UI-Components it is subscribed to + * and the UI-Components it is subscribed to. * * @public * @param config - Configuration object @@ -129,7 +130,8 @@ export class Observer { * previously ingested via the `ingest()` method. * * Note that this method should be overwritten - * to correctly apply the changes to the Agile Class it belongs to. + * to correctly apply the changes to the Agile Class + * to which the Observer belongs. * * @public * @param job - Runtime-Job to be performed. @@ -139,10 +141,10 @@ export class Observer { } /** - * Makes specified Observer depend on the Observer. + * Makes the specified Observer depend on the Observer. * * A dependent Observer is always ingested into the Runtime, - * when the Observer it depends on was ingested too. + * when the Observer it depends on has also been ingested. * * @public * @param observer - Observer to depend on the Observer. @@ -159,7 +161,7 @@ export interface CreateObserverConfigInterface { */ dependents?: Array; /** - * Initial Subscription Container the Observer is subscribed to. + * Initial Subscription Containers the Observer is subscribed to. * @default [] */ subs?: Array; @@ -170,6 +172,10 @@ export interface CreateObserverConfigInterface { key?: ObserverKey; /** * Initial value of the Observer. + * + * The value of an Observer is merged into the Component (Component Subscription Container) + * to be represented there, for example, in a local State Management property. + * * @default undefined */ value?: ValueType; diff --git a/packages/core/tests/unit/runtime/observer.test.ts b/packages/core/tests/unit/runtime/observer.test.ts index d44c4b3e..d725221c 100644 --- a/packages/core/tests/unit/runtime/observer.test.ts +++ b/packages/core/tests/unit/runtime/observer.test.ts @@ -20,20 +20,22 @@ describe('Observer Tests', () => { dummyAgile = new Agile(); dummyObserver1 = new Observer(dummyAgile, { key: 'dummyObserver1' }); dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); - dummySubscription1 = new SubscriptionContainer(); - dummySubscription2 = new SubscriptionContainer(); + dummySubscription1 = new SubscriptionContainer([]); + dummySubscription2 = new SubscriptionContainer([]); - jest.spyOn(Observer.prototype, 'subscribe'); + jest.spyOn(dummySubscription1, 'addSubscription'); + jest.spyOn(dummySubscription2, 'addSubscription'); }); it('should create Observer (default config)', () => { const observer = new Observer(dummyAgile); + expect(observer.agileInstance()).toBe(dummyAgile); expect(observer._key).toBeUndefined(); + expect(Array.from(observer.dependents)).toStrictEqual([]); + expect(Array.from(observer.subscribedTo)).toStrictEqual([]); expect(observer.value).toBeUndefined(); expect(observer.previousValue).toBeUndefined(); - expect(observer.dependents.size).toBe(0); - expect(observer.subscribedTo.size).toBe(0); }); it('should create Observer (specific config)', () => { @@ -44,18 +46,21 @@ describe('Observer Tests', () => { value: 'coolValue', }); + expect(observer.agileInstance()).toBe(dummyAgile); expect(observer._key).toBe('testKey'); + expect(Array.from(observer.dependents)).toStrictEqual([ + dummyObserver1, + dummyObserver2, + ]); + expect(Array.from(observer.subscribedTo)).toStrictEqual([ + dummySubscription1, + dummySubscription2, + ]); expect(observer.value).toBe('coolValue'); expect(observer.previousValue).toBe('coolValue'); - expect(observer.dependents.size).toBe(2); - expect(observer.dependents.has(dummyObserver2)).toBeTruthy(); - expect(observer.dependents.has(dummyObserver1)).toBeTruthy(); - expect(observer.subscribedTo.size).toBe(2); - expect(observer.subscribedTo.has(dummySubscription1)).toBeTruthy(); - expect(observer.subscribedTo.has(dummySubscription2)).toBeTruthy(); - - expect(observer.subscribe).toHaveBeenCalledWith(dummySubscription1); - expect(observer.subscribe).toHaveBeenCalledWith(dummySubscription2); + + expect(dummySubscription1.addSubscription).toHaveBeenCalledWith(observer); + expect(dummySubscription2.addSubscription).toHaveBeenCalledWith(observer); }); describe('Observer Function Tests', () => { @@ -82,7 +87,7 @@ describe('Observer Tests', () => { }); describe('ingest function tests', () => { - it('should create RuntimeJob and ingest Observer into the Runtime (default config)', () => { + it('should create RuntimeJob containing the Observer and ingest it into the Runtime (default config)', () => { dummyAgile.runtime.ingest = jest.fn((job: RuntimeJob) => { expect(job._key).toBe(observer._key); expect(job.observer).toBe(observer); @@ -107,7 +112,7 @@ describe('Observer Tests', () => { ); }); - it('should create RuntimeJob and ingest Observer into the Runtime (specific config)', () => { + it('should create RuntimeJob containing the Observer and ingest it into the Runtime (specific config)', () => { dummyAgile.runtime.ingest = jest.fn((job: RuntimeJob) => { expect(job._key).toBe('coolKey'); expect(job.observer).toBe(observer); @@ -148,7 +153,7 @@ describe('Observer Tests', () => { }); }); - describe('depend function tests', () => { + describe('addDependent function tests', () => { let dummyObserver1: Observer; let dummyObserver2: Observer; @@ -157,14 +162,14 @@ describe('Observer Tests', () => { dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); }); - it('should add passed Observer to deps', () => { + it('should add specified Observer to the dependents array', () => { observer.addDependent(dummyObserver1); expect(observer.dependents.size).toBe(1); expect(observer.dependents.has(dummyObserver2)); }); - it("shouldn't add the same Observer twice to deps", () => { + it("shouldn't add specified Observer twice to the dependents array", () => { observer.addDependent(dummyObserver1); observer.addDependent(dummyObserver1); @@ -173,61 +178,5 @@ describe('Observer Tests', () => { expect(observer.dependents.has(dummyObserver1)); }); }); - - describe('subscribe function tests', () => { - let dummySubscriptionContainer1: SubscriptionContainer; - - beforeEach(() => { - dummySubscriptionContainer1 = new SubscriptionContainer(); - }); - - it('should add subscriptionContainer to subs and this(Observer) to SubscriptionContainer subs', () => { - observer.subscribe(dummySubscriptionContainer1); - - expect(observer.subscribedTo.size).toBe(1); - expect(observer.subscribedTo.has(dummySubscriptionContainer1)); - expect(dummySubscriptionContainer1.subscribers.size).toBe(1); - expect( - dummySubscriptionContainer1.subscribers.has(observer) - ).toBeTruthy(); - }); - - it("shouldn't add same subscriptionContainer twice to subs", () => { - observer.subscribe(dummySubscriptionContainer1); - - observer.subscribe(dummySubscriptionContainer1); - - expect(observer.subscribedTo.size).toBe(1); - expect(observer.subscribedTo.has(dummySubscriptionContainer1)); - expect(dummySubscriptionContainer1.subscribers.size).toBe(1); - expect( - dummySubscriptionContainer1.subscribers.has(observer) - ).toBeTruthy(); - }); - }); - - describe('unsubscribe function tests', () => { - let dummySubscriptionContainer1: SubscriptionContainer; - let dummySubscriptionContainer2: SubscriptionContainer; - - beforeEach(() => { - dummySubscriptionContainer1 = new SubscriptionContainer(); - dummySubscriptionContainer2 = new SubscriptionContainer(); - observer.subscribe(dummySubscriptionContainer1); - observer.subscribe(dummySubscriptionContainer2); - }); - - it('should remove subscriptionContainer from subs and this(Observer) from SubscriptionContainer subs', () => { - observer.unsubscribe(dummySubscriptionContainer1); - - expect(observer.subscribedTo.size).toBe(1); - expect(observer.subscribedTo.has(dummySubscriptionContainer1)); - expect(dummySubscriptionContainer1.subscribers.size).toBe(0); - expect(dummySubscriptionContainer2.subscribers.size).toBe(1); - expect( - dummySubscriptionContainer2.subscribers.has(observer) - ).toBeTruthy(); - }); - }); }); }); diff --git a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts index 2edeff7c..75e4c841 100644 --- a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts @@ -49,7 +49,9 @@ describe('CallbackSubscriptionContainer Tests', () => { expect(subscriptionContainer.subscribers.size).toBe(2); expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy(); expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy(); - expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); + expect(Array.from(subscriptionContainer.updatedSubscribers)).toStrictEqual( + [] + ); expect(subscriptionContainer.isObjectBased).toBeFalsy(); expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( expect.any(WeakMap) diff --git a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts index 6ebb14fc..7f9da8c4 100644 --- a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts @@ -47,7 +47,9 @@ describe('ComponentSubscriptionContainer Tests', () => { expect(subscriptionContainer.subscribers.size).toBe(2); expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy(); expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy(); - expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); + expect(Array.from(subscriptionContainer.updatedSubscribers)).toStrictEqual( + [] + ); expect(subscriptionContainer.isObjectBased).toBeFalsy(); expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( expect.any(WeakMap) diff --git a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts index 8a7cb06a..9729177a 100644 --- a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts @@ -59,8 +59,10 @@ describe('SubscriptionContainer Tests', () => { expect(subscriptionContainer.key).toBe('generatedId'); expect(subscriptionContainer.ready).toBeFalsy(); expect(subscriptionContainer.componentId).toBeUndefined(); - expect(subscriptionContainer.subscribers.size).toBe(0); // because of mocking addSubscription - expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); + expect(Array.from(subscriptionContainer.subscribers)).toStrictEqual([]); // because of mocking addSubscription + expect(Array.from(subscriptionContainer.updatedSubscribers)).toStrictEqual( + [] + ); expect(subscriptionContainer.isObjectBased).toBeFalsy(); expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( expect.any(WeakMap) @@ -103,8 +105,10 @@ describe('SubscriptionContainer Tests', () => { expect(subscriptionContainer.key).toBe('generatedId'); expect(subscriptionContainer.ready).toBeFalsy(); expect(subscriptionContainer.componentId).toBeUndefined(); - expect(subscriptionContainer.subscribers.size).toBe(0); // because of mocking addSubscription - expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); + expect(Array.from(subscriptionContainer.subscribers)).toStrictEqual([]); // because of mocking addSubscription + expect(Array.from(subscriptionContainer.updatedSubscribers)).toStrictEqual( + [] + ); expect(subscriptionContainer.isObjectBased).toBeTruthy(); expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( expect.any(WeakMap) @@ -161,8 +165,10 @@ describe('SubscriptionContainer Tests', () => { expect(subscriptionContainer.key).toBe('dummyKey'); expect(subscriptionContainer.ready).toBeFalsy(); expect(subscriptionContainer.componentId).toBe('testID'); - expect(subscriptionContainer.subscribers.size).toBe(0); // because of mocking addSubscription - expect(subscriptionContainer.updatedSubscribers).toStrictEqual([]); + expect(Array.from(subscriptionContainer.subscribers)).toStrictEqual([]); // because of mocking addSubscription + expect(Array.from(subscriptionContainer.updatedSubscribers)).toStrictEqual( + [] + ); expect(subscriptionContainer.isObjectBased).toBeFalsy(); expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual( expect.any(WeakMap) @@ -215,14 +221,12 @@ describe('SubscriptionContainer Tests', () => { ], }); - expect(subscriptionContainer.subscribers.size).toBe(1); - expect( - subscriptionContainer.subscribers.has(dummyObserver1) - ).toBeTruthy(); - expect(dummyObserver1.subscribedTo.size).toBe(1); - expect( - dummyObserver1.subscribedTo.has(subscriptionContainer) - ).toBeTruthy(); + expect(Array.from(subscriptionContainer.subscribers)).toStrictEqual([ + dummyObserver1, + ]); + expect(Array.from(dummyObserver1.subscribedTo)).toStrictEqual([ + subscriptionContainer, + ]); // should assign specified selectors/(and selectors created from proxy paths) to the selectorsWeakMap const observer1Selector = subscriptionContainer.selectorsWeakMap.get( @@ -299,10 +303,9 @@ describe('SubscriptionContainer Tests', () => { it('should remove subscribed Observer from Subscription Container', () => { subscriptionContainer.removeSubscription(dummyObserver1); - expect(subscriptionContainer.subscribers.size).toBe(1); - expect( - subscriptionContainer.subscribers.has(dummyObserver2) - ).toBeTruthy(); + expect(Array.from(subscriptionContainer.subscribers)).toStrictEqual([ + dummyObserver2, + ]); expect( subscriptionContainer.selectorsWeakMap.get(dummyObserver1) @@ -318,11 +321,10 @@ describe('SubscriptionContainer Tests', () => { subscriptionContainer.subscriberKeysWeakMap.get(dummyObserver2) ).toBe('dummyObserver2'); - expect(dummyObserver1.subscribedTo.size).toBe(0); - expect(dummyObserver2.subscribedTo.size).toBe(1); - expect( - dummyObserver2.subscribedTo.has(subscriptionContainer) - ).toBeTruthy(); + expect(Array.from(dummyObserver1.subscribedTo)).toStrictEqual([]); + expect(Array.from(dummyObserver2.subscribedTo)).toStrictEqual([ + subscriptionContainer, + ]); }); }); }); From 646f076c0fcc54317939cab791303f30f533119c Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 11 Jun 2021 06:50:10 +0200 Subject: [PATCH 47/63] fixed typos --- packages/core/src/runtime/observer.ts | 34 +++++++++----- .../CallbackSubscriptionContainer.ts | 10 ++--- .../ComponentSubscriptionContainer.ts | 45 ++++++++++--------- .../container/SubscriptionContainer.ts | 4 +- .../CallbackSubscriptionContainer.test.ts | 8 ++-- .../ComponentSubscriptionContainer.test.ts | 8 ++-- 6 files changed, 63 insertions(+), 46 deletions(-) diff --git a/packages/core/src/runtime/observer.ts b/packages/core/src/runtime/observer.ts index 0ec534e2..eb76f80c 100644 --- a/packages/core/src/runtime/observer.ts +++ b/packages/core/src/runtime/observer.ts @@ -7,6 +7,7 @@ import { IngestConfigInterface, CreateRuntimeJobConfigInterface, LogCodeManager, + generateId, } from '../internal'; export type ObserverKey = string | number; @@ -19,12 +20,12 @@ export class Observer { public _key?: ObserverKey; // Observers that depend on this Observer public dependents: Set = new Set(); - // Subscription Containers (Components) the Observer is subscribed to + // Subscription Containers (UI-Components) the Observer is subscribed to public subscribedTo: Set = new Set(); - // Current value of Observer + // Current value of the Observer public value?: ValueType; - // Previous value of Observer + // Previous value of the Observer public previousValue?: ValueType; /** @@ -35,7 +36,8 @@ export class Observer { * Agile Classes often use an Observer as an interface to the Runtime. * In doing so, they ingest their own Observer into the Runtime * when the Agile Class has changed in such a way - * that these changes need to be applied to UI-Components or dependent Observers. + * that these changes need to be applied to UI-Components + * or dependent other Observers. * * After the Observer has been ingested into the Runtime * wrapped into a Runtime-Job, it is first added to the Jobs queue @@ -45,10 +47,10 @@ export class Observer { * * Now that the Job was performed, it is added to the rerender queue, * where the subscribed Subscription Container (UI-Components) - * of the Observer are updated (rerender). + * of the Observer are updated (re-rendered). * * Note that the Observer itself is no standalone class - * and should be adapted to the Agile Class it belongs to. + * and should be adapted to the Agile Class needs it belongs to. * * @internal * @param agileInstance - Instance of Agile the Observer belongs to. @@ -93,9 +95,10 @@ export class Observer { /** * Passes the Observer into the runtime wrapped into a Runtime-Job - * where it is executed accordingly - * by performing its `perform()` method, updating its dependents - * and the UI-Components it is subscribed to. + * where it is executed accordingly. + * + * During the execution the runtime performs the Observer's `perform()` method, + * updates its dependents and re-renders the UI-Components it is subscribed to. * * @public * @param config - Configuration object @@ -116,7 +119,9 @@ export class Observer { force: config.force, sideEffects: config.sideEffects, background: config.background, - key: config.key || this._key, + key: + config.key ?? + `${this._key != null ? this._key + '_' : ''}${generateId()}`, }); // Pass created Job into the Runtime @@ -173,8 +178,13 @@ export interface CreateObserverConfigInterface { /** * Initial value of the Observer. * - * The value of an Observer is merged into the Component (Component Subscription Container) - * to be represented there, for example, in a local State Management property. + * The value of an Observer is given to the Integration's `updateMethod()` method + * (Component Subscription Container) where it can be, + * for example, merged in a local State Management property of the UI-Component + * it is subscribed to. + * + * Also the selection of specific properties of an Agile Class value + * is based on the Observer `value` and `previousValue`. * * @default undefined */ diff --git a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts index 9ba41abe..bf974bea 100644 --- a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts @@ -6,24 +6,24 @@ import { export class CallbackSubscriptionContainer extends SubscriptionContainer { /** - * Callback function to trigger a rerender - * on the Component represented by the Subscription Container. + * Callback function to trigger a re-render + * on the UI-Component which is represented by the Subscription Container. */ public callback: Function; /** * A Callback Subscription Container represents a UI-Component in AgileTs - * and triggers a rerender on the UI-Component via a specified callback function. + * and triggers re-renders on the UI-Component via the specified callback function. * * The Callback Subscription Container doesn't keep track of the Component itself. - * It only knows how to trigger a rerender on it via the callback function. + * It only knows how to trigger re-renders on it by calling the callback function. * * [Learn more..](https://agile-ts.org/docs/core/integration#callback-based) * * @internal * @param callback - Callback function to cause a rerender on the Component * to be represented by the Subscription Container. - * @param subs - Observers to be subscribed to the Subscription Container. + * @param subs - Observers to be initial subscribed to the Subscription Container. * @param config - Configuration object */ constructor( diff --git a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts index 4da1277f..ff80eded 100644 --- a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts @@ -8,51 +8,54 @@ export class ComponentSubscriptionContainer< C = any > extends SubscriptionContainer { /** - * Component the Subscription Container represents - * and mutates to cause rerender on it. + * UI-Component which is represented by the Subscription Container + * and mutated via the Integration's `updateMethod()` method + * to cause re-renders on it. */ public component: C; /** * A Component Subscription Container represents a UI-Component in AgileTs - * and triggers a rerender on the UI-Component by muting the specified Component Instance. - * For example by updating a local State Management property of the Component. - * (like in a React Class Components the `this.state` property) + * and triggers re-renders on the UI-Component by muting the specified Component Instance + * via the Integration's `updateMethod()` method. + * For example by updating a local State Management property of the Component + * (like in React Class Components the `this.state` property). * * The Component Subscription Container keeps track of the Component itself, - * to mutate it accordingly so that a rerender is triggered. + * to mutate it appropriately so that re-renders can be triggered on it. * * For this to work well, a Component Subscription Container is often object based. - * Meaning that each Observer was provided in a object keymap with a unique key identifier. + * Meaning that each Observer was provided in an object keymap + * with a unique key identifier. * ``` - * // Object based (guaranteed unique key) + * // Object based (guaranteed unique key identifier) * { * state1: Observer, * state2: Observer * } * - * // Array based (no guaranteed unique key) + * // Array based (no guaranteed unique key identifier) * [Observer, Observer] * ``` - * Thus the Integrations 'updateMethod' method can be called - * with an complete object of changed Observer values. + * Thus the Integration's 'updateMethod()' method can be called + * with a complete object of updated Observer values. * ``` * updateMethod: (componentInstance, updatedData) => { - * console.log(componentInstance); // Returns [this.component] - * console.log(updatedData); // Returns changed Observer values (see below) - * // { - * // state1: Observer.value, - * // state2: Observer.value - * // } - * } + * console.log(componentInstance); // Returns 'this.component' + * console.log(updatedData); // Returns updated Observer values keymap (see below) + * // { + * // state1: Observer.value, + * // state2: Observer.value, + * // } + * } * ``` * * [Learn more..](https://agile-ts.org/docs/core/integration#component-based) * * @internal - * @param component - Component to be represented by the Subscription Container - * and mutated via the Integration method 'updateMethod()' to trigger rerender on it. - * @param subs - Observers to be subscribed to the Subscription Container. + * @param component - UI-Component to be represented by the Subscription Container + * and mutated via the Integration's 'updateMethod()' method to trigger re-renders on it. + * @param subs - Observers to be initial subscribed to the Subscription Container. * @param config - Configuration object */ constructor( diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 3e2ca5f9..0af1ae84 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -19,7 +19,7 @@ export class SubscriptionContainer { */ public ready = false; /** - * Unique identifier of the Component the Subscription Container represents. + * Unique identifier of the UI-Component the Subscription Container represents in AgileTs. */ public componentId?: ComponentIdType; @@ -86,7 +86,7 @@ export class SubscriptionContainer { * for example, when their value has changed. * * @internal - * @param subs - Observers to be subscribed to the Subscription Container. + * @param subs - Observers to be initial subscribed to the Subscription Container. * @param config - Configuration object */ constructor( diff --git a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts index 75e4c841..e8bea19e 100644 --- a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts @@ -43,12 +43,14 @@ describe('CallbackSubscriptionContainer Tests', () => { expect(subscriptionContainer.callback).toBe(dummyIntegration); + // Check if SubscriptionContainer was called with correct parameters expect(subscriptionContainer.key).toBe('dummyKey'); expect(subscriptionContainer.ready).toBeFalsy(); expect(subscriptionContainer.componentId).toBe('testID'); - expect(subscriptionContainer.subscribers.size).toBe(2); - expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy(); - expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy(); + expect(Array.from(subscriptionContainer.subscribers)).toStrictEqual([ + dummyObserver1, + dummyObserver2, + ]); expect(Array.from(subscriptionContainer.updatedSubscribers)).toStrictEqual( [] ); diff --git a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts index 7f9da8c4..4401cf88 100644 --- a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts @@ -41,12 +41,14 @@ describe('ComponentSubscriptionContainer Tests', () => { expect(subscriptionContainer.component).toStrictEqual(dummyIntegration); + // Check if SubscriptionContainer was called with correct parameters expect(subscriptionContainer.key).toBe('dummyKey'); expect(subscriptionContainer.ready).toBeFalsy(); expect(subscriptionContainer.componentId).toBe('testID'); - expect(subscriptionContainer.subscribers.size).toBe(2); - expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy(); - expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy(); + expect(Array.from(subscriptionContainer.subscribers)).toStrictEqual([ + dummyObserver1, + dummyObserver2, + ]); expect(Array.from(subscriptionContainer.updatedSubscribers)).toStrictEqual( [] ); From b0bb0f19eeaead34eca3e3a21553aa8a0f9e8faf Mon Sep 17 00:00:00 2001 From: BennoDev Date: Fri, 11 Jun 2021 17:28:36 +0200 Subject: [PATCH 48/63] fixed SubscriptionContainer typos --- .../container/SubscriptionContainer.ts | 124 +++++++++++------- .../runtime/subscription/sub.controller.ts | 3 +- .../container/SubscriptionContainer.test.ts | 14 +- 3 files changed, 86 insertions(+), 55 deletions(-) diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 0af1ae84..4c0dcb17 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -12,56 +12,67 @@ export class SubscriptionContainer { public key?: SubscriptionContainerKeyType; /** * Whether the Subscription Container - * and the Component the Subscription Container represents are ready. + * and the UI-Component it represents are ready. * - * When both are ready, the Subscription Container is allowed - * to trigger rerender on the Component. + * When both instances are ready, + * the Subscription Container is allowed + * to trigger re-renders on the UI-Component. */ public ready = false; /** - * Unique identifier of the UI-Component the Subscription Container represents in AgileTs. + * Unique identifier of the UI-Component + * the Subscription Container represents. */ public componentId?: ComponentIdType; /** - * Observers that have subscribed the Subscription Container. + * Observers that are subscribed to the Subscription Container. * * The subscribed Observers use the Subscription Container - * as an interface to the Component it represents. - * Through the Subscription Container, they can easily trigger rerender - * on the Component, for example, when their value changes. + * as an interface to the UI-Component it represents. + * + * Through the Subscription Container, the Observers can easily trigger re-renders + * on the UI-Component, for example, when their value updates. * * [Learn more..](https://agile-ts.org/docs/core/integration#-subscriptions) */ public subscribers: Set; /** * Temporary stores the subscribed Observers, - * that were performed by the runtime - * and are currently running through the update Subscription Container (rerender) process. + * that were updated by the runtime + * and are currently running through + * the update (rerender) Subscription Container (UI-Component) process. */ public updatedSubscribers: Set = new Set(); /** * Whether the Subscription Container is object based. * - * An Observer is object based when the subscribed Observers - * have been provided in an Observer key map. + * A Subscription Container is object based when the subscribed Observers + * have been provided in an Observer keymap object * ``` * { * state1: Observer, * state2: Observer * } * ``` - * Thus each Observer has its 'external' unique key stored in the 'subscribersWeakMap'. + * Thus each Observer has its 'external' unique key stored in the `subscribersWeakMap`. * * Often Component based Subscriptions are object based, - * because each Observer requires a unique identifier - * to be properly represented in the 'updatedData' object sent to the Integration 'updateMethod()'. + * because each Observer requires in such Subscription a unique identifier. + * Mainly to be properly represented in the `updatedData` object + * sent to the Integration's `updateMethod()` method + * when the Subscription Container updates (re-renders the UI-Component). */ public isObjectBased = false; /** * Weak map for storing 'external' key identifiers for subscribed Observers. * + * Why is the key not applied directly to the Observer? + * + * Because the key defined here should be only valid + * for the scope of the Subscription Container. + * * https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap */ public subscriberKeysWeakMap: WeakMap; @@ -69,9 +80,14 @@ export class SubscriptionContainer { /** * Weak Map for storing selector functions for subscribed Observers. * - * A selector function allows partial subscription to an Observer value. + * A selector function allows the partial subscription to an Observer value. * Only when the selected Observer value part changes, - * the Subscription Container rerender the Component. + * the Subscription Container is updated (-> re-renders the UI-Component). + * + * Why are the selector functions not applied directly to the Observer? + * + * Because the selector function defined here should be only valid + * for the scope of the Subscription Container. * * https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap */ @@ -81,9 +97,10 @@ export class SubscriptionContainer { * A Subscription Container represents a UI-Component in AgileTs * that can be subscribed by multiple Observer Instances. * - * These Observers use the Subscription Container as an interface - * to trigger a rerender on the UI-Component it represents, - * for example, when their value has changed. + * The subscribed Observers can use the Subscription Container as an interface + * to the UI-Component it represents. + * For example, to trigger re-renders on the UI-Component, + * when their value has changed. * * @internal * @param subs - Observers to be initial subscribed to the Subscription Container. @@ -106,19 +123,18 @@ export class SubscriptionContainer { this.selectorsWeakMap = new WeakMap(); this.isObjectBased = !Array.isArray(subs); + // Assign initial Observers to the Subscription Container for (const key in subs) { - const sub = subs[key]; - this.addSubscription(sub, { - proxyPaths: config.proxyWeakMap?.get(sub)?.paths, - selectorMethods: config.selectorWeakMap?.get(sub)?.methods, - key: !Array.isArray(subs) ? key : undefined, + this.addSubscription(subs[key], { + proxyPaths: config.proxyWeakMap?.get(subs[key])?.paths, + selectorMethods: config.selectorWeakMap?.get(subs[key])?.methods, + key: this.isObjectBased ? key : undefined, }); } } /** - * Adds specified Observer to the `subscription` array - * and its selectors to the `selectorsWeakMap`. + * Subscribes the specified Observer to the Subscription Container. * * @internal * @param sub - Observer to be subscribed to the Subscription Container @@ -130,10 +146,10 @@ export class SubscriptionContainer { ): void { const toAddSelectorMethods: SelectorMethodType[] = config.selectorMethods ?? []; - const paths = config.proxyPaths ?? []; + const proxyPaths = config.proxyPaths ?? []; - // Create selector methods based on the specified proxy paths - for (const path of paths) { + // Create additional selector methods based on the specified proxy paths + for (const path of proxyPaths) { toAddSelectorMethods.push((value) => { let _value = value; for (const branch of path) { @@ -145,6 +161,8 @@ export class SubscriptionContainer { } // Assign defined/created selector methods to the 'selectorsWeakMap' + // (Not to the Observer itself, since the selector methods specified here + // only count for the scope of the Subscription Container) const existingSelectorMethods = this.selectorsWeakMap.get(sub)?.methods ?? []; const newSelectorMethods = existingSelectorMethods.concat( @@ -154,24 +172,24 @@ export class SubscriptionContainer { this.selectorsWeakMap.set(sub, { methods: newSelectorMethods }); // Assign specified key to the 'subscriberKeysWeakMap' - // (Not to the Observer itself, since the key specified here only counts for this Subscription Container) + // (Not to the Observer itself, since the key specified here + // only counts for the scope of the Subscription Container) if (config.key != null) this.subscriberKeysWeakMap.set(sub, config.key); // Add Observer to subscribers this.subscribers.add(sub); // Add Subscription Container to Observer - // so that it can be updated (cause rerender on the Component it represents) - // when for example the Observer value changes + // so that the Observer can cause updates on it + // (trigger re-render on the UI-Component it represents). sub.subscribedTo.add(this); } /** - * Removes the Observer from the Subscription Container - * and from all WeakMaps it might be in. + * Unsubscribes the specified Observer from the Subscription Container. * * @internal - * @param sub - Observer to be removed from the Subscription Container + * @param sub - Observer to be unsubscribed from the Subscription Container. */ public removeSubscription(sub: Observer) { if (this.subscribers.has(sub)) { @@ -192,18 +210,18 @@ export interface SubscriptionContainerConfigInterface { */ key?: SubscriptionContainerKeyType; /** - * Key/Name identifier of the Component to be represented by the Subscription Container. + * Key/Name identifier of the UI-Component to be represented by the Subscription Container. * @default undefined */ componentId?: ComponentIdType; /** - * A Weak Map with a set of paths to certain properties - * in a Observer value for Observers. + * A Weak Map with a set of proxy paths to certain properties + * in an Observer value for subscribed Observers. * * These paths are then selected via selector functions * which allow the partly subscription to an Observer value. * Only if the selected Observer value part changes, - * the Subscription Container rerender the Component. + * the Subscription Container re-renders the UI-Component it represents. * * For example: * ``` @@ -212,12 +230,12 @@ export interface SubscriptionContainerConfigInterface { * Observer2: {paths: [['car', 'speed']]} * } * ``` - * Now the Subscription Container will only trigger a rerender on the Component - * if 'data.name' in Observer1 or 'car.speed' in Observer2 changes. - * If, for instance, 'data.age' in Observer1 mutates it won't trigger a rerender, - * since 'data.age' isn't represented in the Proxy Weak Map. + * Now the Subscription Container will only trigger a re-render on the UI-Component + * if 'data.name' in Observer1 or 'car.speed' in Observer2 updates. + * If, for instance, 'data.age' in Observer1 mutates it won't trigger a re-render, + * since 'data.age' isn't represented in the specified Proxy Weak Map. * - * These particular paths were tracked via the ProxyTree. + * These particular paths can, for example, be tracked via the ProxyTree. * https://github.com/agile-ts/agile/tree/master/packages/proxytree * * @default new WeakMap() @@ -226,9 +244,21 @@ export interface SubscriptionContainerConfigInterface { /** * A Weak Map with a set of selector functions for Observers. * - * A selector functions allows the partly subscription to an Observer value. + * Selector functions allow the partly subscription to Observer values. * Only if the selected Observer value part changes, - * the Subscription Container rerender the Component. + * the Subscription Container re-renders the UI-Component it represents. + * + * For example: + * ``` + * WeakMap: { + * Observer1: {methods: [(value) => value.data.name]}, + * Observer2: {methods: [(value) => value.car.speed]} + * } + * ``` + * Now the Subscription Container will only trigger a re-render on the UI-Component + * if 'data.name' in Observer1 or 'car.speed' in Observer2 updates. + * If, for instance, 'data.age' in Observer1 mutates it won't trigger a re-render, + * since 'data.age' isn't selected by any selector method in the specified Selector Weak Map. * * @default new WeakMap() */ diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index 72f979bc..eb131d36 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -27,7 +27,8 @@ export class SubController { * The Subscription Controller manages the subscription to UI-Components. * * Thus it creates Subscription Containers (Interfaces to UI-Components) - * and assigns them to Observers, so that the Observers can easily trigger rerender on Components. + * and assigns them to specified Observers, + * so that these Observers can easily trigger re-renders on UI-Components. * * @internal * @param agileInstance - Instance of Agile the Subscription Controller belongs to. diff --git a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts index 9729177a..c3aa012d 100644 --- a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts @@ -190,9 +190,9 @@ describe('SubscriptionContainer Tests', () => { describe('addSubscription function tests', () => { it( - 'should create selectors based on the specified proxies, ' + - 'assigns newly created or provided selectors to the selectorsWeakMap ' + - 'and subscribe the specified Observer to the SubscriptionContainer', + 'should create selector methods based on the specified proxy paths, ' + + "assign newly created and provided selector methods to the 'selectorsWeakMap' " + + 'and subscribe the specified Observer to the Subscription Container', () => { dummyObserver1.value = { das: { haus: { vom: 'nikolaus' } }, @@ -228,7 +228,7 @@ describe('SubscriptionContainer Tests', () => { subscriptionContainer, ]); - // should assign specified selectors/(and selectors created from proxy paths) to the selectorsWeakMap + // should assign specified selectors/(and selectors created from proxy paths) to the 'selectorsWeakMap' const observer1Selector = subscriptionContainer.selectorsWeakMap.get( dummyObserver1 ) as any; @@ -249,20 +249,20 @@ describe('SubscriptionContainer Tests', () => { 'test1Value' ); - // shouldn't overwrite already set values in selectorsWeakMap + // shouldn't overwrite already set values in 'selectorsWeakMap' (Observer2) const observer2Selector = subscriptionContainer.selectorsWeakMap.get( dummyObserver2 ) as any; expect(observer2Selector.methods.length).toBe(1); expect(observer2Selector.methods[0](null)).toBe('doesNotMatter'); - // should assign specified key to the subscriberKeysWeakMap + // should assign specified key to the 'subscriberKeysWeakMap' const observer1Key = subscriptionContainer.subscriberKeysWeakMap.get( dummyObserver1 ); expect(observer1Key).toBe('dummyObserver1'); - // shouldn't overwrite already set values in subscriberKeysWeakMap + // shouldn't overwrite already set values in 'subscriberKeysWeakMap' (Observer2) const observer2Key = subscriptionContainer.subscriberKeysWeakMap.get( dummyObserver2 ); From bfd78a86763297de6a2e6c46fccf26a1134333da Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 11 Jun 2021 20:25:02 +0200 Subject: [PATCH 49/63] fixed typos --- packages/core/src/runtime/index.ts | 6 +- packages/core/src/runtime/runtime.job.ts | 46 +-- .../runtime/subscription/sub.controller.ts | 109 +++--- .../core/tests/unit/runtime/observer.test.ts | 4 +- .../tests/unit/runtime/runtime.job.test.ts | 28 +- .../core/tests/unit/runtime/runtime.test.ts | 4 +- .../subscription/sub.controller.test.ts | 314 +++++++++--------- 7 files changed, 256 insertions(+), 255 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index 9a3285e8..63dc7e46 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -187,8 +187,8 @@ export class Runtime { // Handle not ready Subscription Container if (!subscriptionContainer.ready) { if ( - !job.config.maxOfTriesToUpdate || - job.triedToUpdateCount < job.config.maxOfTriesToUpdate + !job.config.maxTriesToUpdate || + job.triedToUpdateCount < job.config.maxTriesToUpdate ) { job.triedToUpdateCount++; this.notReadyJobsToRerender.add(job); @@ -201,7 +201,7 @@ export class Runtime { } else { LogCodeManager.log( '16:02:01', - [job.config.maxOfTriesToUpdate], + [job.config.maxTriesToUpdate], subscriptionContainer ); } diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts index b3a1588b..c61b7c02 100644 --- a/packages/core/src/runtime/runtime.job.ts +++ b/packages/core/src/runtime/runtime.job.ts @@ -1,28 +1,28 @@ -import { - Observer, - defineConfig, - SubscriptionContainer, - Agile, -} from '../internal'; +import { Observer, defineConfig, SubscriptionContainer } from '../internal'; export class RuntimeJob { public config: RuntimeJobConfigInterface; - // Key/Name identifier of the Subscription Container + // Key/Name identifier of the Runtime Job public _key?: RuntimeJobKey; // Observer the Job represents public observer: ObserverType; - // Whether the Subscription Containers (Components) of the Observer can be re-rendered + // Whether the Subscription Containers (UI-Components) of the Observer should be updated (re-rendered) public rerender: boolean; - // Whether the Job has been performed by the runtime - public performed = false; - // Subscription Container of the Observer that have to be updated/re-rendered + // Subscription Containers (UI-Components) of the Observer that have to be updated (re-rendered) public subscriptionContainersToUpdate = new Set(); - // How often not ready Subscription Container of the Observer have been tried to update + // How often not ready Subscription Containers of the Observer have been tried to update public triedToUpdateCount = 0; + // Whether the Job has been performed by the runtime + public performed = false; + /** - * A Job that contains an Observer to be executed by the runtime. + * A Runtime Job is sent to the Runtime on behalf of the Observer it represents. + * + * In the Runtime, the Observer is performed via its `perform()` method + * and the Subscription Containers (UI-Components) + * to which it is subscribed are updated (re-rendered) accordingly. * * @internal * @param observer - Observer to be represented by the Runtime Job. @@ -39,13 +39,13 @@ export class RuntimeJob { exclude: [], }, force: false, - maxOfTriesToUpdate: 3, + maxTriesToUpdate: 3, }); this.config = { background: config.background, force: config.force, sideEffects: config.sideEffects, - maxOfTriesToUpdate: config.maxOfTriesToUpdate, + maxTriesToUpdate: config.maxTriesToUpdate, }; this.observer = observer; this.rerender = @@ -89,7 +89,8 @@ export interface CreateRuntimeJobConfigInterface export interface RuntimeJobConfigInterface { /** * Whether to perform the Runtime Job in background. - * So that the UI isn't notified of these changes and thus doesn't rerender. + * So that the Subscription Containers (UI-Components) aren't notified + * of these changes and thus doesn't update (re-render). * @default false */ background?: boolean; @@ -100,21 +101,24 @@ export interface RuntimeJobConfigInterface { sideEffects?: SideEffectConfigInterface; /** * Whether the Runtime Job should be forced through the runtime - * although it might be useless from the viewpoint of the runtime. + * although it might be useless from the current viewpoint of the runtime. * @default false */ force?: boolean; /** - * How often the runtime should try to update not ready Subscription Containers of the Observer the Job represents. - * If 'null' the runtime tries to update the not ready Subscription Container until they are ready (infinite). + * How often the Runtime should try to update not ready Subscription Containers + * subscribed by the Observer which the Job represents. + * + * When `null` the Runtime tries to update the not ready Subscription Containers + * until they are ready (infinite). * @default 3 */ - maxOfTriesToUpdate?: number | null; + maxTriesToUpdate?: number | null; } export interface SideEffectConfigInterface { /** - * Whether to execute the defined side effects + * Whether to execute the defined side effects. * @default true */ enabled?: boolean; diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index eb131d36..d1ac3641 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -24,11 +24,12 @@ export class SubController { public mountedComponents: Set = new Set(); /** - * The Subscription Controller manages the subscription to UI-Components. + * The Subscription Controller manages and simplifies the subscription to UI-Components. * * Thus it creates Subscription Containers (Interfaces to UI-Components) - * and assigns them to specified Observers, - * so that these Observers can easily trigger re-renders on UI-Components. + * and assigns them to the specified Observers. + * These Observers can then easily trigger re-renders on UI-Components + * via the created Subscription Containers. * * @internal * @param agileInstance - Instance of Agile the Subscription Controller belongs to. @@ -39,18 +40,20 @@ export class SubController { /** * Creates a so called Subscription Container that represents an UI-Component in AgileTs. - * Such Subscription Container know how to trigger a rerender on the UI-Component it represents + * Such Subscription Container know how to trigger a re-render on the UI-Component it represents * through the provided `integrationInstance`. * - * There exist two different ways the Subscription Container can cause a rerender on the Component. - * - 1. Via a callback function that directly triggers a rerender on the Component. (Callback based Subscription) - * [Learn more..](https://agile-ts.org/docs/core/integration/#callback-based) + * Currently, there are two different ways the Subscription Container can trigger a re-render on the UI-Component. + * - 1. Via a callback function that directly triggers a rerender on the UI-Component. + * (= Callback based Subscription) + * [Learn more..](https://agile-ts.org/docs/core/integration/#callback-based) * - 2. Via the Component instance itself. - * For example by mutating a local State Management property. (Component based Subscription) - * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) + * For example by mutating a local State Management property. + * (= Component based Subscription) + * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) * * The in an array specified Observers are then automatically subscribed - * to the created Subscription Container and thus to the Component it represents. + * to the created Subscription Container and thus to the UI-Component it represents. * * @public * @param integrationInstance - Callback function or Component Instance to trigger a rerender on a UI-Component. @@ -64,23 +67,25 @@ export class SubController { ): SubscriptionContainer; /** * Creates a so called Subscription Container that represents an UI-Component in AgileTs. - * Such Subscription Container know how to trigger a rerender on the UI-Component it represents + * Such Subscription Container know how to trigger a re-render on the UI-Component it represents * through the provided `integrationInstance`. * - * There exist two different ways the Subscription Container can cause a rerender on the Component. - * - 1. Via a callback function that directly triggers a rerender on the Component. (Callback based Subscription) - * [Learn more..](https://agile-ts.org/docs/core/integration/#callback-based) + * Currently, there are two different ways the Subscription Container can trigger a re-render on the UI-Component. + * - 1. Via a callback function that directly triggers a rerender on the UI-Component. + * (= Callback based Subscription) + * [Learn more..](https://agile-ts.org/docs/core/integration/#callback-based) * - 2. Via the Component instance itself. - * For example by mutating a local State Management property. (Component based Subscription) - * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) + * For example by mutating a local State Management property. + * (= Component based Subscription) + * [Learn more..](https://agile-ts.org/docs/core/integration/#component-based) * - * The in an object specified Observers are then automatically subscribed - * to the created Subscription Container and thus to the Component it represents. + * The in an object keymap specified Observers are then automatically subscribed + * to the created Subscription Container and thus to the UI-Component it represents. * * The advantage of subscribing the Observer via a object keymap, - * is that each Observer has its own unique key identifier. - * Such key identifier is for example required when merging the Observer value into - * a local Component State Management property. + * is that each Observer has its own unique 'external' key identifier. + * Such key identifier is, for example, required when merging the Observer value into + * a local UI-Component State Management property. * ``` * this.state = {...this.state, {state1: Observer1.value, state2: Observer2.value}} * ``` @@ -112,7 +117,7 @@ export class SubController { waitForMount: this.agileInstance().config.waitForMount, }); - // Create Subscription Container + // Create Subscription Container based on specified 'integrationInstance' const subscriptionContainer = isFunction(integrationInstance) ? this.createCallbackSubscriptionContainer( integrationInstance, @@ -125,12 +130,10 @@ export class SubController { config ); - // Return object based Subscription Container + // Return object based Subscription Container and an Observer value keymap if (subscriptionContainer.isObjectBased && !Array.isArray(subs)) { - // Build an Observer value keymap const props: { [key: string]: Observer['value'] } = {}; for (const key in subs) if (subs[key].value) props[key] = subs[key].value; - return { subscriptionContainer, props }; } @@ -139,15 +142,15 @@ export class SubController { } /** - * Unsubscribe the Subscription Container extracted from the specified 'subscriptionInstance' + * Removes the Subscription Container extracted from the specified 'subscriptionInstance' * from all Observers that were subscribed to it. * - * We should always unregister a Subscription Container when it is no longer in use, - * for example when the Component it represents has been unmounted. + * We should always unregister a Subscription Container when it is no longer in use. + * For example, when the UI-Component it represents has been unmounted. * * @public - * @param subscriptionInstance - Subscription Container - * or a UI-Component that contains an instance of a Subscription Container to be unsubscribed. + * @param subscriptionInstance - UI-Component that contains an instance of a Subscription Container + * or a Subscription Container to be unsubscribed/unregistered. */ public unsubscribe(subscriptionInstance: any) { // Helper function to remove Subscription Container from Observer @@ -158,29 +161,28 @@ export class SubController { }); }; - // Unsubscribe callback based Subscription Container + // Unsubscribe Callback based Subscription Container if (subscriptionInstance instanceof CallbackSubscriptionContainer) { unsub(subscriptionInstance); this.callbackSubs.delete(subscriptionInstance); - Agile.logger.if .tag(['runtime', 'subscription']) .info(LogCodeManager.getLog('15:01:00'), subscriptionInstance); return; } - // Unsubscribe component based Subscription Container + // Unsubscribe Component based Subscription Container if (subscriptionInstance instanceof ComponentSubscriptionContainer) { unsub(subscriptionInstance); this.componentSubs.delete(subscriptionInstance); - Agile.logger.if .tag(['runtime', 'subscription']) .info(LogCodeManager.getLog('15:01:01'), subscriptionInstance); return; } - // Unsubscribe component based Subscription Container extracted from the 'componentSubscriptionContainers' property + // Unsubscribe Component based Subscription Container + // extracted from the 'componentSubscriptionContainers' property if ( subscriptionInstance['componentSubscriptionContainers'] !== null && Array.isArray(subscriptionInstance.componentSubscriptionContainers) @@ -189,7 +191,6 @@ export class SubController { (subContainer) => { unsub(subContainer as ComponentSubscriptionContainer); this.componentSubs.delete(subContainer); - Agile.logger.if .tag(['runtime', 'subscription']) .info(LogCodeManager.getLog('15:01:01'), subscriptionInstance); @@ -203,8 +204,9 @@ export class SubController { * Returns a newly created Component based Subscription Container. * * @internal - * @param componentInstance - Component Instance to trigger a rerender on a UI-Component. - * @param subs - Observers to be subscribed to the to create Subscription Container. + * @param componentInstance - UI-Component to be represented by the Subscription Container + * and mutated via the Integration's 'updateMethod()' method to trigger re-renders on it. + * @param subs - Observers to be initial subscribed to the Subscription Container. * @param config - Configuration object. */ public createComponentSubscriptionContainer( @@ -225,17 +227,17 @@ export class SubController { componentSubscriptionContainer.ready = true; } else componentSubscriptionContainer.ready = true; - // Add subscriptionContainer to Component, to have an instance of it there - // (For example, required to unsubscribe the Subscription Container via the Component Instance) + // Add Subscription Container to the UI-Component it represents. + // (For example, useful to unsubscribe the Subscription Container via the Component Instance) if ( - componentInstance.componentSubscriptionContainers && + componentInstance['componentSubscriptionContainers'] != null && Array.isArray(componentInstance.componentSubscriptionContainers) ) componentInstance.componentSubscriptionContainers.push( componentSubscriptionContainer ); else - componentInstance.componentSubscriptionContainers = [ + componentInstance['componentSubscriptionContainers'] = [ componentSubscriptionContainer, ]; @@ -250,8 +252,9 @@ export class SubController { * Returns a newly created Callback based Subscription Container. * * @internal - * @param callbackFunction - Callback function to trigger a rerender on a UI-Component. - * @param subs - Observers to be subscribed to the to create Subscription Container. + * @param callbackFunction - Callback function to cause a rerender on the Component + * to be represented by the Subscription Container. + * @param subs - Observers to be initial subscribed to the Subscription Container. * @param config - Configuration object */ public createCallbackSubscriptionContainer( @@ -275,14 +278,15 @@ export class SubController { } /** - * Mounts Component based Subscription Container. + * Notifies the Subscription Containers representing the specified UI-Component (`componentInstance`) + * that the UI-Component they represent has been mounted. * * @public - * @param componentInstance - Component Instance containing a Subscription Container to be mounted + * @param componentInstance - Component Instance containing Subscription Containers to be mounted. */ public mount(componentInstance: any) { if ( - componentInstance.componentSubscriptionContainers && + componentInstance['componentSubscriptionContainers'] != null && Array.isArray(componentInstance.componentSubscriptionContainers) ) componentInstance.componentSubscriptionContainers.map( @@ -293,14 +297,15 @@ export class SubController { } /** - * Unmounts Component based Subscription Containers. + * Notifies the Subscription Containers representing the specified UI-Component (`componentInstance`) + * that the UI-Component they represent has been unmounted. * * @public - * @param componentInstance - Component Instance containing a Subscription Container to be unmounted + * @param componentInstance - Component Instance containing Subscription Containers to be unmounted */ public unmount(componentInstance: any) { if ( - componentInstance.componentSubscriptionContainers && + componentInstance['componentSubscriptionContainers'] != null && Array.isArray(componentInstance.componentSubscriptionContainers) ) componentInstance.componentSubscriptionContainers.map( @@ -314,8 +319,8 @@ export class SubController { interface RegisterSubscriptionConfigInterface extends SubscriptionContainerConfigInterface { /** - * Whether the Subscription Container should be ready only - * when the Component it represents has been mounted. + * Whether the Subscription Container should not be ready + * until the UI-Component it represents has been mounted. * @default agileInstance.config.waitForMount */ waitForMount?: boolean; diff --git a/packages/core/tests/unit/runtime/observer.test.ts b/packages/core/tests/unit/runtime/observer.test.ts index d725221c..525dcca7 100644 --- a/packages/core/tests/unit/runtime/observer.test.ts +++ b/packages/core/tests/unit/runtime/observer.test.ts @@ -98,7 +98,7 @@ describe('Observer Tests', () => { exclude: [], }, force: false, - maxOfTriesToUpdate: 3, + maxTriesToUpdate: 3, }); }); @@ -123,7 +123,7 @@ describe('Observer Tests', () => { exclude: [], }, force: true, - maxOfTriesToUpdate: 3, + maxTriesToUpdate: 3, }); }); diff --git a/packages/core/tests/unit/runtime/runtime.job.test.ts b/packages/core/tests/unit/runtime/runtime.job.test.ts index b0dbce53..e8989d14 100644 --- a/packages/core/tests/unit/runtime/runtime.job.test.ts +++ b/packages/core/tests/unit/runtime/runtime.job.test.ts @@ -17,7 +17,7 @@ describe('RuntimeJob Tests', () => { dummyObserver = new Observer(dummyAgile); }); - it('should create RuntimeJob with Agile that has integrations (default config)', () => { + it('should create RuntimeJob with a specified Agile Instance that has a registered Integration (default config)', () => { dummyAgile.integrate(dummyIntegration); const job = new RuntimeJob(dummyObserver); @@ -31,24 +31,25 @@ describe('RuntimeJob Tests', () => { exclude: [], }, force: false, - maxOfTriesToUpdate: 3, + maxTriesToUpdate: 3, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); - expect(job.subscriptionContainersToUpdate.size).toBe(0); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); expect(job.triedToUpdateCount).toBe(0); }); - it('should create RuntimeJob with Agile that has integrations (specific config)', () => { + it('should create RuntimeJob with a specified Agile Instance that has a registered Integration (specific config)', () => { dummyAgile.integrate(dummyIntegration); const job = new RuntimeJob(dummyObserver, { key: 'dummyJob', sideEffects: { enabled: false, + exclude: ['jeff'], }, force: true, - maxOfTriesToUpdate: 10, + maxTriesToUpdate: 10, }); expect(job._key).toBe('dummyJob'); @@ -57,16 +58,17 @@ describe('RuntimeJob Tests', () => { background: false, sideEffects: { enabled: false, + exclude: ['jeff'], }, force: true, - maxOfTriesToUpdate: 10, + maxTriesToUpdate: 10, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); - expect(job.subscriptionContainersToUpdate.size).toBe(0); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); }); - it('should create RuntimeJob with Agile that has no integrations (default config)', () => { + it('should create RuntimeJob with a specified Agile Instance that has no registered Integration (default config)', () => { const job = new RuntimeJob(dummyObserver); expect(job._key).toBeUndefined(); @@ -78,14 +80,14 @@ describe('RuntimeJob Tests', () => { exclude: [], }, force: false, - maxOfTriesToUpdate: 3, + maxTriesToUpdate: 3, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); - expect(job.subscriptionContainersToUpdate.size).toBe(0); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); }); - it('should create RuntimeJob and Agile that has integrations (config.background = true)', () => { + it('should create RuntimeJob with a specified Agile Instance that has a registered Integrations (config.background = true)', () => { dummyAgile.integrate(dummyIntegration); const job = new RuntimeJob(dummyObserver, { background: true }); @@ -99,11 +101,11 @@ describe('RuntimeJob Tests', () => { exclude: [], }, force: false, - maxOfTriesToUpdate: 3, + maxTriesToUpdate: 3, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); - expect(job.subscriptionContainersToUpdate.size).toBe(0); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); }); describe('RuntimeJob Function Tests', () => { diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index b967c1cd..2182f056 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -382,7 +382,7 @@ describe('Runtime Tests', () => { .mockReturnValueOnce(true); dummySubscriptionContainer1.ready = true; dummySubscriptionContainer2.ready = false; - const numberOfTries = (dummyJob2.config.maxOfTriesToUpdate ?? 0) + 1; + const numberOfTries = (dummyJob2.config.maxTriesToUpdate ?? 0) + 1; dummyJob2.triedToUpdateCount = numberOfTries; const response = runtime.extractToUpdateSubscriptionContainer([ @@ -423,7 +423,7 @@ describe('Runtime Tests', () => { expect(console.warn).toHaveBeenCalledTimes(1); LogMock.hasLoggedCode( '16:02:01', - [dummyJob2.config.maxOfTriesToUpdate], + [dummyJob2.config.maxTriesToUpdate], dummySubscriptionContainer2 ); } diff --git a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts index c96710fe..1d60caf0 100644 --- a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts +++ b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts @@ -21,8 +21,9 @@ describe('SubController Tests', () => { it('should create SubController', () => { const subController = new SubController(dummyAgile); - expect(subController.callbackSubs.size).toBe(0); - expect(subController.callbackSubs.size).toBe(0); + expect(subController.agileInstance()).toBe(dummyAgile); + expect(Array.from(subController.callbackSubs)).toStrictEqual([]); + expect(Array.from(subController.componentSubs)).toStrictEqual([]); }); describe('SubController Function Tests', () => { @@ -49,8 +50,8 @@ describe('SubController Tests', () => { }); it( - 'should create a Component based Subscription Container with specified component' + - ' and add in object specified Observers to it', + 'should create a Component based Subscription Container with specified Component Instance ' + + 'and assign the in an object specified Observers to it', () => { dummyAgile.config.waitForMount = 'aFakeBoolean' as any; const dummyIntegration: any = { @@ -86,12 +87,15 @@ describe('SubController Tests', () => { waitForMount: true, } ); + expect( + subController.createCallbackSubscriptionContainer + ).not.toHaveBeenCalled(); } ); it( - 'should create a Component based Subscription Container with specified component' + - ' and add in array specified Observers to it', + 'should create a Component based Subscription Container with specified Component Instance ' + + 'and assign the in an array specified Observers to it', () => { dummyAgile.config.waitForMount = 'aFakeBoolean' as any; const dummyIntegration: any = { @@ -114,15 +118,18 @@ describe('SubController Tests', () => { { key: 'subscriptionContainerKey', componentId: 'testID', - waitForMount: dummyAgile.config.waitForMount, + waitForMount: 'aFakeBoolean', } ); + expect( + subController.createCallbackSubscriptionContainer + ).not.toHaveBeenCalled(); } ); it( - 'should create a Callback based Subscription Container with specified callback function' + - ' and add in object specified Observers to it', + 'should create a Callback based Subscription Container with specified callback function ' + + 'and assign the in an object specified Observers to it', () => { dummyAgile.config.waitForMount = 'aFakeBoolean' as any; const dummyIntegration = () => { @@ -154,15 +161,18 @@ describe('SubController Tests', () => { { key: 'subscriptionContainerKey', componentId: 'testID', - waitForMount: dummyAgile.config.waitForMount, + waitForMount: 'aFakeBoolean', } ); + expect( + subController.createComponentSubscriptionContainer + ).not.toHaveBeenCalled(); } ); it( - 'should create a Callback based Subscription Container with specified callback function' + - ' and add in array specified Observers to it', + 'should create a Callback based Subscription Container with specified callback function ' + + 'and assign the in an array specified Observers to it', () => { dummyAgile.config.waitForMount = 'aFakeBoolean' as any; const dummyIntegration = () => { @@ -192,12 +202,15 @@ describe('SubController Tests', () => { waitForMount: false, } ); + expect( + subController.createComponentSubscriptionContainer + ).not.toHaveBeenCalled(); } ); }); describe('unsubscribe function tests', () => { - it('should unsubscribe callbackSubscriptionContainer', () => { + it('should unsubscribe Callback based Subscription Container', () => { const dummyIntegration = () => { /* empty function */ }; @@ -209,7 +222,7 @@ describe('SubController Tests', () => { subController.unsubscribe(callbackSubscriptionContainer); - expect(subController.callbackSubs.size).toBe(0); + expect(Array.from(subController.callbackSubs)).toStrictEqual([]); expect(callbackSubscriptionContainer.ready).toBeFalsy(); expect( callbackSubscriptionContainer.removeSubscription @@ -222,7 +235,7 @@ describe('SubController Tests', () => { ).toHaveBeenCalledWith(dummyObserver2); }); - it('should unsubscribe componentSubscriptionContainer', () => { + it('should unsubscribe Component Subscription Container', () => { const dummyIntegration: any = { dummy: 'integration', }; @@ -234,7 +247,7 @@ describe('SubController Tests', () => { subController.unsubscribe(componentSubscriptionContainer); - expect(subController.componentSubs.size).toBe(0); + expect(Array.from(subController.componentSubs)).toStrictEqual([]); expect(componentSubscriptionContainer.ready).toBeFalsy(); expect( componentSubscriptionContainer.removeSubscription @@ -247,54 +260,59 @@ describe('SubController Tests', () => { ).toHaveBeenCalledWith(dummyObserver2); }); - it('should unsubscribe componentSubscriptionContainers from passed Object that holds an instance of componentSubscriptionContainers', () => { - const dummyIntegration: any = { - dummy: 'integration', - componentSubscriptionContainers: [], - }; - const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); - componentSubscriptionContainer.removeSubscription = jest.fn(); - const componentSubscriptionContainer2 = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); - componentSubscriptionContainer2.removeSubscription = jest.fn(); + it( + 'should unsubscribe Component based Subscription Container ' + + 'from specified object (UI-Component) that contains an instance of the Component Subscription Container', + () => { + const dummyIntegration: any = { + dummy: 'integration', + componentSubscriptionContainers: [], + }; + const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2] + ); + componentSubscriptionContainer.removeSubscription = jest.fn(); + const componentSubscriptionContainer2 = subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2] + ); + componentSubscriptionContainer2.removeSubscription = jest.fn(); - subController.unsubscribe(dummyIntegration); + subController.unsubscribe(dummyIntegration); - expect(subController.componentSubs.size).toBe(0); + expect(Array.from(subController.componentSubs)).toStrictEqual([]); - expect(componentSubscriptionContainer.ready).toBeFalsy(); - expect( - componentSubscriptionContainer.removeSubscription - ).toHaveBeenCalledTimes(2); - expect( - componentSubscriptionContainer.removeSubscription - ).toHaveBeenCalledWith(dummyObserver1); - expect( - componentSubscriptionContainer.removeSubscription - ).toHaveBeenCalledWith(dummyObserver2); + expect(componentSubscriptionContainer.ready).toBeFalsy(); + expect( + componentSubscriptionContainer.removeSubscription + ).toHaveBeenCalledTimes(2); + expect( + componentSubscriptionContainer.removeSubscription + ).toHaveBeenCalledWith(dummyObserver1); + expect( + componentSubscriptionContainer.removeSubscription + ).toHaveBeenCalledWith(dummyObserver2); - expect(componentSubscriptionContainer2.ready).toBeFalsy(); - expect( - componentSubscriptionContainer2.removeSubscription - ).toHaveBeenCalledTimes(2); - expect( - componentSubscriptionContainer2.removeSubscription - ).toHaveBeenCalledWith(dummyObserver1); - expect( - componentSubscriptionContainer2.removeSubscription - ).toHaveBeenCalledWith(dummyObserver2); - }); + expect(componentSubscriptionContainer2.ready).toBeFalsy(); + expect( + componentSubscriptionContainer2.removeSubscription + ).toHaveBeenCalledTimes(2); + expect( + componentSubscriptionContainer2.removeSubscription + ).toHaveBeenCalledWith(dummyObserver1); + expect( + componentSubscriptionContainer2.removeSubscription + ).toHaveBeenCalledWith(dummyObserver2); + } + ); }); describe('createComponentSubscriptionContainer function tests', () => { it( - 'should return ready componentSubscriptionContainer ' + - 'and add an instance of it to the not existing componentSubscriptions property in the dummyIntegration (default config)', + 'should return ready Component based Subscription Container ' + + "and add an instance of it to the not existing 'componentSubscriptions' property " + + 'in the dummyIntegration (default config)', () => { jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey'); const dummyIntegration: any = { @@ -315,34 +333,27 @@ describe('SubController Tests', () => { ); expect(componentSubscriptionContainer.ready).toBeTruthy(); - expect(subController.componentSubs.size).toBe(1); - expect( - subController.componentSubs.has(componentSubscriptionContainer) - ).toBeTruthy(); + expect(Array.from(subController.componentSubs)).toStrictEqual([ + componentSubscriptionContainer, + ]); - expect(dummyIntegration.componentSubscriptionContainers.length).toBe( - 1 - ); - expect(dummyIntegration.componentSubscriptionContainers[0]).toBe( - componentSubscriptionContainer - ); + expect( + dummyIntegration.componentSubscriptionContainers + ).toStrictEqual([componentSubscriptionContainer]); // Check if ComponentSubscriptionContainer was called with correct parameters expect(componentSubscriptionContainer.key).toBe('generatedKey'); expect(componentSubscriptionContainer.componentId).toBeUndefined(); - expect(componentSubscriptionContainer.subscribers.size).toBe(2); expect( - componentSubscriptionContainer.subscribers.has(dummyObserver1) - ).toBeTruthy(); - expect( - componentSubscriptionContainer.subscribers.has(dummyObserver2) - ).toBeTruthy(); + Array.from(componentSubscriptionContainer.subscribers) + ).toStrictEqual([dummyObserver1, dummyObserver2]); } ); it( - 'should return ready componentSubscriptionContainer ' + - 'and add an instance of it to the existing componentSubscriptions property in the dummyIntegration (default config)', + 'should return ready Component based Subscription Container ' + + "and add an instance of it to the existing 'componentSubscriptions' property " + + 'in the dummyIntegration (default config)', () => { jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey'); const dummyIntegration: any = { @@ -356,18 +367,16 @@ describe('SubController Tests', () => { { waitForMount: false } ); - expect(dummyIntegration.componentSubscriptionContainers.length).toBe( - 1 - ); - expect(dummyIntegration.componentSubscriptionContainers[0]).toBe( - componentSubscriptionContainer - ); + expect( + dummyIntegration.componentSubscriptionContainers + ).toStrictEqual([componentSubscriptionContainer]); } ); it( - 'should return ready componentSubscriptionContainer ' + - 'and add an instance of it to the not existing componentSubscriptions property in the dummyIntegration (specific config)', + 'should return ready Component based Subscription Container ' + + "and add an instance of it to the not existing 'componentSubscriptions' property " + + 'in the dummyIntegration (specific config)', () => { const dummyIntegration: any = { dummy: 'integration', @@ -387,32 +396,25 @@ describe('SubController Tests', () => { ); expect(componentSubscriptionContainer.ready).toBeTruthy(); - expect(subController.componentSubs.size).toBe(1); - expect( - subController.componentSubs.has(componentSubscriptionContainer) - ).toBeTruthy(); + expect(Array.from(subController.componentSubs)).toBe([ + componentSubscriptionContainer, + ]); - expect(dummyIntegration.componentSubscriptionContainers.length).toBe( - 1 - ); - expect(dummyIntegration.componentSubscriptionContainers[0]).toBe( - componentSubscriptionContainer - ); + expect( + dummyIntegration.componentSubscriptionContainers + ).toStrictEqual([componentSubscriptionContainer]); // Check if ComponentSubscriptionContainer was called with correct parameters expect(componentSubscriptionContainer.key).toBe('dummyKey'); expect(componentSubscriptionContainer.componentId).toBe('testID'); - expect(componentSubscriptionContainer.subscribers.size).toBe(2); - expect( - componentSubscriptionContainer.subscribers.has(dummyObserver1) - ).toBeTruthy(); - expect( - componentSubscriptionContainer.subscribers.has(dummyObserver2) - ).toBeTruthy(); + expect(Array.from(componentSubscriptionContainer.subscribers)).toBe([ + dummyObserver1, + dummyObserver2, + ]); } ); - it("should return not ready componentSubscriptionContainer if componentInstance isn't mounted (waitForMount = true)", () => { + it("should return not ready Component based Subscription Container if componentInstance isn't mounted (config.waitForMount = true)", () => { jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey'); const dummyIntegration: any = { dummy: 'integration', @@ -432,24 +434,20 @@ describe('SubController Tests', () => { ); expect(componentSubscriptionContainer.ready).toBeFalsy(); - expect(subController.componentSubs.size).toBe(1); - expect( - subController.componentSubs.has(componentSubscriptionContainer) - ).toBeTruthy(); + expect(Array.from(subController.componentSubs)).toBe([ + componentSubscriptionContainer, + ]); // Check if ComponentSubscriptionContainer was called with correct parameters expect(componentSubscriptionContainer.key).toBe('generatedKey'); expect(componentSubscriptionContainer.componentId).toBeUndefined(); - expect(componentSubscriptionContainer.subscribers.size).toBe(2); - expect( - componentSubscriptionContainer.subscribers.has(dummyObserver1) - ).toBeTruthy(); - expect( - componentSubscriptionContainer.subscribers.has(dummyObserver2) - ).toBeTruthy(); + expect(Array.from(componentSubscriptionContainer.subscribers)).toBe([ + dummyObserver1, + dummyObserver2, + ]); }); - it('should return ready componentSubscriptionContainer if componentInstance is mounted (config.waitForMount = true)', () => { + it('should return ready Component based Subscription Container if componentInstance is mounted (config.waitForMount = true)', () => { jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey'); const dummyIntegration: any = { dummy: 'integration', @@ -470,26 +468,21 @@ describe('SubController Tests', () => { ); expect(componentSubscriptionContainer.ready).toBeTruthy(); - expect(subController.componentSubs.size).toBe(1); - expect( - subController.componentSubs.has(componentSubscriptionContainer) - ).toBeTruthy(); + expect(Array.from(subController.componentSubs)).toBe([ + componentSubscriptionContainer, + ]); // Check if ComponentSubscriptionContainer was called with correct parameters expect(componentSubscriptionContainer.key).toBe('generatedKey'); expect(componentSubscriptionContainer.componentId).toBeUndefined(); - expect(componentSubscriptionContainer.subscribers.size).toBe(2); - expect( - componentSubscriptionContainer.subscribers.has(dummyObserver1) - ).toBeTruthy(); expect( - componentSubscriptionContainer.subscribers.has(dummyObserver2) - ).toBeTruthy(); + Array.from(componentSubscriptionContainer.subscribers) + ).toStrictEqual([dummyObserver1, dummyObserver2]); }); }); describe('registerCallbackSubscription function tests', () => { - it('should return callbackSubscriptionContainer (default config)', () => { + it('should return Callback based Subscription Container (default config)', () => { jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey'); const dummyIntegration = () => { /* empty function */ @@ -506,29 +499,24 @@ describe('SubController Tests', () => { expect(callbackSubscriptionContainer.callback).toBe(dummyIntegration); expect(callbackSubscriptionContainer.ready).toBeTruthy(); - expect(subController.callbackSubs.size).toBe(1); - expect( - subController.callbackSubs.has(callbackSubscriptionContainer) - ).toBeTruthy(); + expect(Array.from(subController.callbackSubs)).toStrictEqual([ + callbackSubscriptionContainer, + ]); - // TODO find a way to spy on a class constructor without overwriting it + // TODO find a way to spy on a class constructor without overwriting it. // https://stackoverflow.com/questions/48219267/how-to-spy-on-a-class-constructor-jest/48486214 - // Because the below tests are not really related to this test, - // they are checking if the CallbackSubscriptionContainer was called with the correct parameters - // by checking if CallbackSubscriptionContainer has set its properties correctly - // Note:This 'issue' happens in multiple parts of the AgileTs test + // Because the below tests are not really related to this test. + // They are checking if the CallbackSubscriptionContainer was called with the correct parameters + // by checking if the CallbackSubscriptionContainer has correctly set properties. + // Note: This 'issue' happens in multiple parts of the AgileTs test! expect(callbackSubscriptionContainer.key).toBe('generatedKey'); expect(callbackSubscriptionContainer.componentId).toBeUndefined(); - expect(callbackSubscriptionContainer.subscribers.size).toBe(2); - expect( - callbackSubscriptionContainer.subscribers.has(dummyObserver1) - ).toBeTruthy(); expect( - callbackSubscriptionContainer.subscribers.has(dummyObserver2) - ).toBeTruthy(); + Array.from(callbackSubscriptionContainer.subscribers) + ).toStrictEqual([dummyObserver1, dummyObserver2]); }); - it('should return callbackSubscriptionContainer (specific config)', () => { + it('should return Callback based Subscription Container (specific config)', () => { const dummyIntegration = () => { /* empty function */ }; @@ -549,21 +537,16 @@ describe('SubController Tests', () => { expect(callbackSubscriptionContainer.callback).toBe(dummyIntegration); expect(callbackSubscriptionContainer.ready).toBeTruthy(); - expect(subController.callbackSubs.size).toBe(1); - expect( - subController.callbackSubs.has(callbackSubscriptionContainer) - ).toBeTruthy(); + expect(Array.from(subController.callbackSubs)).toStrictEqual([ + callbackSubscriptionContainer, + ]); // Check if CallbackSubscriptionContainer was called with correct parameters expect(callbackSubscriptionContainer.key).toBe('dummyKey'); expect(callbackSubscriptionContainer.componentId).toBe('testID'); - expect(callbackSubscriptionContainer.subscribers.size).toBe(2); expect( - callbackSubscriptionContainer.subscribers.has(dummyObserver1) - ).toBeTruthy(); - expect( - callbackSubscriptionContainer.subscribers.has(dummyObserver2) - ).toBeTruthy(); + Array.from(callbackSubscriptionContainer.subscribers) + ).toStrictEqual([dummyObserver1, dummyObserver2]); }); }); @@ -581,15 +564,18 @@ describe('SubController Tests', () => { ); }); - it('should add componentInstance to mountedComponents and set its subscriptionContainer to ready', () => { - subController.mount(dummyIntegration); + it( + "should add specified 'componentInstance' to the 'mountedComponents' " + + 'and set the Subscription Container representing the mounted Component to ready', + () => { + subController.mount(dummyIntegration); - expect(componentSubscriptionContainer.ready).toBeTruthy(); - expect(subController.mountedComponents.size).toBe(1); - expect( - subController.mountedComponents.has(dummyIntegration) - ).toBeTruthy(); - }); + expect(componentSubscriptionContainer.ready).toBeTruthy(); + expect(Array.from(subController.mountedComponents)).toStrictEqual([ + dummyIntegration, + ]); + } + ); }); describe('unmount function tests', () => { @@ -607,12 +593,16 @@ describe('SubController Tests', () => { subController.mount(dummyIntegration); }); - it('should remove componentInstance from mountedComponents and set its subscriptionContainer to not ready', () => { - subController.unmount(dummyIntegration); + it( + "should remove specified 'componentInstance' to the 'mountedComponents' " + + 'and set the Subscription Container representing the mounted Component to not ready', + () => { + subController.unmount(dummyIntegration); - expect(componentSubscriptionContainer.ready).toBeFalsy(); - expect(subController.mountedComponents.size).toBe(0); - }); + expect(componentSubscriptionContainer.ready).toBeFalsy(); + expect(Array.from(subController.mountedComponents)).toStrictEqual([]); + } + ); }); }); }); From 47144960e68a3ac4e48bf12c2538498792edc1f4 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 12 Jun 2021 11:06:15 +0200 Subject: [PATCH 50/63] fixed typos in runtime.ts --- packages/core/src/runtime/index.ts | 123 +++++++------- .../core/tests/unit/runtime/runtime.test.ts | 152 ++++++++++++------ 2 files changed, 160 insertions(+), 115 deletions(-) diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index 63dc7e46..ec0b841d 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -18,29 +18,31 @@ export class Runtime { // Jobs to be performed public jobQueue: Array = []; - // Jobs that were performed and are ready to rerender + // Jobs that were performed and are ready to re-render public jobsToRerender: Array = []; - // Jobs that were performed and should be rerendered. - // However their Subscription Container isn't ready to rerender yet. - // For example when the UI-Component isn't mounted yet. + // Jobs that were performed and couldn't be re-rendered yet. + // That is the case when at least one Subscription Container (UI-Component) in the Job + // wasn't ready to update (re-render). public notReadyJobsToRerender: Set = new Set(); - // Whether the job queue is currently being actively processed + // Whether the `jobQueue` is currently being actively processed public isPerformingJobs = false; /** - * The Runtime executes and queues ingested Observer based Jobs - * to prevent race conditions and optimized rerender of subscribed Components. + * The Runtime queues and executes incoming Observer-based Jobs + * to prevent [race conditions](https://en.wikipedia.org/wiki/Race_condition#:~:text=A%20race%20condition%20or%20race,the%20possible%20behaviors%20is%20undesirable.) + * and optimized the re-rendering of the Observer's subscribed UI-Components. * - * Each provided Job will be executed when it is its turn - * by calling the Job Observer's 'perform()' method. + * Each queued Job is executed when it is its turn + * by calling the Job Observer's `perform()` method. * - * After a successful execution the Job is added to a rerender queue, - * which is firstly put into the browser's 'Bucket' and executed when resources are left. + * After successful execution, the Job is added to a re-render queue, + * which is first put into the browser's 'Bucket' and started to work off + * when resources are left. * - * The rerender queue is designed for optimizing the render count - * by combining rerender Jobs of the same Component - * and ignoring rerender requests for unmounted Components. + * The re-render queue is designed for optimizing the render count + * by batching multiple re-render Jobs of the same UI-Component + * and ignoring re-render requests for unmounted UI-Components. * * @internal * @param agileInstance - Instance of Agile the Runtime belongs to. @@ -50,15 +52,15 @@ export class Runtime { } /** - * Adds the specified Observer based Job to the internal Job queue, - * where it will be performed when it is its turn. + * Adds the specified Observer-based Job to the internal Job queue, + * where it is executed when it is its turn. * - * After a successful execution it is added to the rerender queue, - * where all the Observer's subscribed Subscription Containers - * cause rerender on Components the Observer is represented in. + * After successful execution, the Job is assigned to the re-render queue, + * where all the Observer's subscribed Subscription Containers (UI-Components) + * are updated (re-rendered). * * @public - * @param job - Job to be performed. + * @param job - Job to be added to the Job queue. * @param config - Configuration object */ public ingest(job: RuntimeJob, config: IngestConfigInterface = {}): void { @@ -82,12 +84,12 @@ export class Runtime { /** * Performs the specified Job - * and adds it to the rerender queue if necessary. + * and assigns it to the re-render queue if necessary. * - * After the execution of the provided Job it is checked whether + * After the execution of the provided Job, it is checked whether * there are still Jobs left in the Job queue. - * - If so, the next Job in the queue is performed. - * - If not, the `jobsToRerender` queue will be started to work off. + * - If so, the next Job in the `jobQueue` is performed. + * - If not, the `jobsToRerender` queue is started to work off. * * @internal * @param job - Job to be performed. @@ -101,7 +103,7 @@ export class Runtime { job.performed = true; // Ingest dependents of the Observer into runtime, - // since they depend on the Observer and have properly changed too + // since they depend on the Observer and therefore have properly changed too job.observer.dependents.forEach((observer) => observer.ingest({ perform: false }) ); @@ -115,7 +117,7 @@ export class Runtime { .info(LogCodeManager.getLog('16:01:01', [job._key]), job); // Perform Jobs as long as Jobs are left in the queue. - // If no Job is left start updating/rerendering Subscribers + // If no Job is left start updating (re-rendering) Subscription Container (UI-Components) // of the Job based on the 'jobsToRerender' queue. if (this.jobQueue.length > 0) { const performJob = this.jobQueue.shift(); @@ -132,17 +134,17 @@ export class Runtime { } /** - * Works of the `jobsToRerender` queue by updating (causing rerender on) - * the Subscription Container (subscribed Component) - * of each Job Observer. + * Processes the `jobsToRerender` queue by updating (causing a re-render on) + * the subscribed Subscription Containers (UI-Components) of each Job Observer. * - * It returns a boolean indicating whether any Subscription Container was updated or not. + * It returns a boolean indicating whether + * any Subscription Container (UI-Component) was updated (re-rendered) or not. * * @internal */ public updateSubscribers(): boolean { // Build final 'jobsToRerender' array - // based on the new 'jobsToRerender' array and the 'notReadyJobsToRerender' array. + // based on the new 'jobsToRerender' array and the 'notReadyJobsToRerender' array const jobsToRerender = this.jobsToRerender.concat( Array.from(this.notReadyJobsToRerender) ); @@ -152,21 +154,21 @@ export class Runtime { if (!this.agileInstance().hasIntegration() || jobsToRerender.length <= 0) return false; - // Extract Subscription Container from the Jobs to be rerendered + // Extract the Subscription Container to be re-rendered from the Jobs const subscriptionContainerToUpdate = this.extractToUpdateSubscriptionContainer( jobsToRerender ); if (subscriptionContainerToUpdate.length <= 0) return false; - // Update Subscription Container (trigger rerender on Components they represent) + // Update Subscription Container (trigger re-render on the UI-Component they represent) this.updateSubscriptionContainer(subscriptionContainerToUpdate); return true; } /** - * Extracts the Subscription Containers - * that should be updated from the provided Runtime Jobs. + * Extracts the Subscription Containers (UI-Components) + * to be updated (re-rendered) from the specified Runtime Jobs. * * @internal * @param jobs - Jobs from which to extract the Subscription Containers to be updated. @@ -174,12 +176,8 @@ export class Runtime { public extractToUpdateSubscriptionContainer( jobs: Array ): Array { - // Subscription Containers that have to be updated. - // Using a 'Set()' to combine several equal SubscriptionContainers into one (rerender optimisation). const subscriptionsToUpdate = new Set(); - // Check if Job Subscription Container of Jobs should be updated - // and if so add it to the 'subscriptionsToUpdate' array jobs.forEach((job) => { job.subscriptionContainersToUpdate.forEach((subscriptionContainer) => { let updateSubscriptionContainer = true; @@ -192,7 +190,6 @@ export class Runtime { ) { job.triedToUpdateCount++; this.notReadyJobsToRerender.add(job); - LogCodeManager.log( '16:02:00', [subscriptionContainer.key], @@ -208,16 +205,10 @@ export class Runtime { return; } - // Handle Selectors of Subscription Container - // (-> check if a selected part of the Observer value has changed) - updateSubscriptionContainer = - updateSubscriptionContainer && - this.handleSelectors(subscriptionContainer, job); - - // TODO has to be overthought because if it is a Component based Subscription + // TODO has to be overthought because when it is a Component based Subscription // the rerender is triggered via merging the changed properties into the Component. // Although the 'componentId' might be equal, it doesn't mean - // that the changed properties are the equal! (-> changed properties would get missing) + // that the changed properties are equal! (-> changed properties might get missing) // Check if Subscription Container with same 'componentId' // is already in the 'subscriptionToUpdate' queue (rerender optimisation) // updateSubscriptionContainer = @@ -226,6 +217,11 @@ export class Runtime { // (sc) => sc.componentId === subscriptionContainer.componentId // ) === -1; + // Check whether a selected part of the Observer value has changed + updateSubscriptionContainer = + updateSubscriptionContainer && + this.handleSelectors(subscriptionContainer, job); + // Add Subscription Container to the 'subscriptionsToUpdate' queue if (updateSubscriptionContainer) { subscriptionContainer.updatedSubscribers.add(job.observer); @@ -240,10 +236,10 @@ export class Runtime { } /** - * Updates the specified Subscription Container. + * Updates the specified Subscription Containers. * - * By updating the SubscriptionContainer a rerender is triggered - * on the Component it represents. + * Updating a Subscription Container triggers a re-render + * on the Component it represents, based on the type of the Subscription Containers. * * @internal * @param subscriptionsToUpdate - Subscription Containers to be updated. @@ -273,13 +269,13 @@ export class Runtime { /** * Maps the values of the updated Observers (`updatedSubscribers`) - * of the specified Subscription Container into a key map. + * of the specified Subscription Container into a key map object. * * The key containing the Observer value is extracted from the Observer itself * or from the Subscription Container's `subscriberKeysWeakMap`. * * @internal - * @param subscriptionContainer - Subscription Container from which the `updatedSubscribers` are to be mapped to a key map. + * @param subscriptionContainer - Subscription Container from which the `updatedSubscribers` are to be mapped into a key map. */ public getUpdatedObserverValues( subscriptionContainer: SubscriptionContainer @@ -295,15 +291,18 @@ export class Runtime { } /** - * Returns a boolean indicating whether the specified Subscription Container can be updated or not - * based on the selector functions (`selectorsWeakMap`) of the Subscription Container. + * Returns a boolean indicating whether the specified Subscription Container can be updated or not, + * based on its selector functions (`selectorsWeakMap`). * * This is done by checking the '.value' and the '.previousValue' property of the Observer represented by the Job. - * If a selected property differs, the Subscription Container is allowed to update/rerender (returns true). + * If a selected property differs, the Subscription Container (UI-Component) is allowed to update (re-render) + * and `true` is returned. + * + * If the Subscription Container has no selector function at all, `true` is returned. * * @internal - * @param subscriptionContainer - Subscription Container to be checked if it can update. - * @param job - Job containing the Observer which has subscribed the Subscription Container. + * @param subscriptionContainer - Subscription Container to be checked if it can be updated. + * @param job - Job containing the Observer that is subscribed to the Subscription Container. */ public handleSelectors( subscriptionContainer: SubscriptionContainer, @@ -313,10 +312,10 @@ export class Runtime { job.observer )?.methods; - // If no selector functions found, return true - // because no specific part of the Observer was selected - // -> The Subscription Container should update - // no matter what was updated in the Observer + // If no selector functions found, return true. + // Because no specific part of the Observer was selected. + // -> The Subscription Container should be updated + // no matter what has updated in the Observer. if (selectorMethods == null) return true; // Check if a selected part of the Observer value has changed diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index 2182f056..b0814322 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -8,7 +8,6 @@ import { SubscriptionContainer, } from '../../../src'; import * as Utils from '@agile-ts/utils'; -import testIntegration from '../../helper/test.integration'; import { LogMock } from '../../helper/logMock'; describe('Runtime Tests', () => { @@ -24,10 +23,12 @@ describe('Runtime Tests', () => { it('should create Runtime', () => { const runtime = new Runtime(dummyAgile); + expect(runtime.agileInstance()).toBe(dummyAgile); expect(runtime.currentJob).toBeNull(); expect(runtime.jobQueue).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(0); expect(runtime.jobsToRerender).toStrictEqual([]); + expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); + expect(runtime.isPerformingJobs).toBeFalsy(); }); describe('Runtime Function Tests', () => { @@ -52,22 +53,21 @@ describe('Runtime Tests', () => { runtime.perform = jest.fn(); }); - it("should perform specified Job immediately if jobQueue isn't currently being processed (default config)", () => { + it("should perform specified Job immediately if jobQueue isn't being processed (default config)", () => { runtime.isPerformingJobs = false; runtime.ingest(dummyJob); - expect(runtime.jobQueue.length).toBe(0); + expect(runtime.jobQueue).toStrictEqual([]); expect(runtime.perform).toHaveBeenCalledWith(dummyJob); }); - it("shouldn't perform specified Job immediately if jobQueue is currently being processed (default config)", () => { + it("shouldn't perform specified Job immediately if jobQueue is being processed (default config)", () => { runtime.isPerformingJobs = true; runtime.ingest(dummyJob); - expect(runtime.jobQueue.length).toBe(1); - expect(runtime.jobQueue[0]).toBe(dummyJob); + expect(runtime.jobQueue).toStrictEqual([dummyJob]); expect(runtime.perform).not.toHaveBeenCalled(); }); @@ -75,7 +75,7 @@ describe('Runtime Tests', () => { runtime.isPerformingJobs = true; runtime.ingest(dummyJob, { perform: true }); - expect(runtime.jobQueue.length).toBe(0); + expect(runtime.jobQueue).toStrictEqual([]); expect(runtime.perform).toHaveBeenCalledWith(dummyJob); }); @@ -83,8 +83,7 @@ describe('Runtime Tests', () => { runtime.isPerformingJobs = false; runtime.ingest(dummyJob, { perform: false }); - expect(runtime.jobQueue.length).toBe(1); - expect(runtime.jobQueue[0]).toBe(dummyJob); + expect(runtime.jobQueue).toStrictEqual([dummyJob]); expect(runtime.perform).not.toHaveBeenCalled(); }); }); @@ -110,8 +109,8 @@ describe('Runtime Tests', () => { }); it( - 'should perform specified Job and all remaining Jobs in the jobQueue,' + - ' and call updateSubscribers if at least one performed Job needs to rerender', + "should perform specified Job and all remaining Jobs in the 'jobQueue' " + + "and call 'updateSubscribers' if at least one performed Job needs to rerender", async () => { runtime.jobQueue.push(dummyJob2); runtime.jobQueue.push(dummyJob3); @@ -125,11 +124,9 @@ describe('Runtime Tests', () => { expect(dummyObserver1.perform).toHaveBeenCalledWith(dummyJob3); expect(dummyJob3.performed).toBeTruthy(); - expect(runtime.jobQueue.length).toBe(0); - expect(runtime.jobsToRerender.length).toBe(2); - expect(runtime.jobsToRerender.includes(dummyJob1)).toBeTruthy(); - expect(runtime.jobsToRerender.includes(dummyJob2)).toBeTruthy(); - expect(runtime.jobsToRerender.includes(dummyJob3)).toBeFalsy(); + expect(runtime.isPerformingJobs).toBeFalsy(); // because Jobs were performed + expect(runtime.jobQueue).toStrictEqual([]); + expect(runtime.jobsToRerender).toStrictEqual([dummyJob1, dummyJob2]); // Sleep 5ms because updateSubscribers is called in a timeout await new Promise((resolve) => setTimeout(resolve, 5)); @@ -147,19 +144,19 @@ describe('Runtime Tests', () => { expect(dummyObserver1.perform).toHaveBeenCalledWith(dummyJob1); expect(dummyJob1.performed).toBeTruthy(); + expect(dummyObserver1.ingest).toHaveBeenCalledTimes(1); expect(dummyObserver1.ingest).toHaveBeenCalledWith({ perform: false, }); - expect(dummyObserver1.ingest).toHaveBeenCalledTimes(1); + expect(dummyObserver2.ingest).toHaveBeenCalledTimes(1); expect(dummyObserver2.ingest).toHaveBeenCalledWith({ perform: false, }); - expect(dummyObserver2.ingest).toHaveBeenCalledTimes(1); }); it( - 'should perform specified Job and all remaining Jobs in the jobQueue' + - " and shouldn't call updateSubscribes if no performed Job needs to rerender", + "should perform specified Job and all remaining Jobs in the 'jobQueue' " + + "and shouldn't call 'updateSubscribes' if no performed Job needs to rerender", async () => { dummyJob1.rerender = false; runtime.jobQueue.push(dummyJob3); @@ -171,8 +168,9 @@ describe('Runtime Tests', () => { expect(dummyObserver1.perform).toHaveBeenCalledWith(dummyJob3); expect(dummyJob3.performed).toBeTruthy(); - expect(runtime.jobQueue.length).toBe(0); - expect(runtime.jobsToRerender.length).toBe(0); + expect(runtime.isPerformingJobs).toBeFalsy(); // because Jobs were performed + expect(runtime.jobQueue).toStrictEqual([]); + expect(runtime.jobsToRerender).toStrictEqual([]); // Sleep 5ms because updateSubscribers is called in a timeout await new Promise((resolve) => setTimeout(resolve, 5)); @@ -215,21 +213,21 @@ describe('Runtime Tests', () => { it('should return false if Agile has no registered Integration', () => { dummyAgile.hasIntegration = jest.fn(() => false); - runtime.jobsToRerender.push(dummyJob1); - runtime.jobsToRerender.push(dummyJob2); + runtime.jobsToRerender = [dummyJob1, dummyJob2]; const response = runtime.updateSubscribers(); expect(response).toBeFalsy(); + expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(0); + expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); expect( runtime.extractToUpdateSubscriptionContainer ).not.toHaveBeenCalled(); expect(runtime.updateSubscriptionContainer).not.toHaveBeenCalled(); }); - it('should return false if jobsToRerender and notReadyJobsToRerender queue is empty', () => { + it('should return false if jobsToRerender and notReadyJobsToRerender queue are both empty', () => { dummyAgile.hasIntegration = jest.fn(() => true); runtime.jobsToRerender = []; runtime.notReadyJobsToRerender = new Set(); @@ -237,29 +235,36 @@ describe('Runtime Tests', () => { const response = runtime.updateSubscribers(); expect(response).toBeFalsy(); + + expect(response).toBeFalsy(); + expect(runtime.jobsToRerender).toStrictEqual([]); + expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); + expect( + runtime.extractToUpdateSubscriptionContainer + ).not.toHaveBeenCalled(); + expect(runtime.updateSubscriptionContainer).not.toHaveBeenCalled(); }); - it('should return false if no Subscription Container of the Jobs to rerender needs to update', () => { + it('should return false if no Subscription Container of the Jobs to rerender queue needs to update', () => { dummyAgile.hasIntegration = jest.fn(() => true); jest .spyOn(runtime, 'extractToUpdateSubscriptionContainer') .mockReturnValueOnce([]); - runtime.jobsToRerender.push(dummyJob1); - runtime.jobsToRerender.push(dummyJob2); - runtime.notReadyJobsToRerender.add(dummyJob3); + runtime.jobsToRerender = [dummyJob1, dummyJob2]; + runtime.notReadyJobsToRerender = new Set([dummyJob3]); const response = runtime.updateSubscribers(); expect(response).toBeFalsy(); expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(0); + expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); expect( runtime.extractToUpdateSubscriptionContainer ).toHaveBeenCalledWith([dummyJob1, dummyJob2, dummyJob3]); expect(runtime.updateSubscriptionContainer).not.toHaveBeenCalled(); }); - it('should return true if at least one Subscription Container of the Jobs to rerender needs to update', () => { + it('should return true if at least one Subscription Container of the Jobs to rerender queue needs to update', () => { dummyAgile.hasIntegration = jest.fn(() => true); jest .spyOn(runtime, 'extractToUpdateSubscriptionContainer') @@ -267,15 +272,14 @@ describe('Runtime Tests', () => { dummySubscriptionContainer1, dummySubscriptionContainer2, ]); - runtime.jobsToRerender.push(dummyJob1); - runtime.jobsToRerender.push(dummyJob2); - runtime.notReadyJobsToRerender.add(dummyJob3); + runtime.jobsToRerender = [dummyJob1, dummyJob2]; + runtime.notReadyJobsToRerender = new Set([dummyJob3]); const response = runtime.updateSubscribers(); expect(response).toBeTruthy(); expect(runtime.jobsToRerender).toStrictEqual([]); - expect(runtime.notReadyJobsToRerender.size).toBe(0); + expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); expect( runtime.extractToUpdateSubscriptionContainer ).toHaveBeenCalledWith([dummyJob1, dummyJob2, dummyJob3]); @@ -316,7 +320,7 @@ describe('Runtime Tests', () => { it( "shouldn't extract not ready Subscription Container from the specified Jobs, " + - "add it to the 'notReadyJobsToRerender' queue and print warning", + "should add it to the 'notReadyJobsToRerender' queue and print a warning", () => { jest .spyOn(runtime, 'handleSelectors') @@ -373,8 +377,8 @@ describe('Runtime Tests', () => { it( "shouldn't extract not ready Subscription Container from the specified Jobs, " + - "remove the Job when it exceeded the max 'maxOfTriesToUpdate' " + - 'and print warning', + "should remove the Job when it exceeded the max 'maxTriesToUpdate' " + + 'and print a warning', () => { jest .spyOn(runtime, 'handleSelectors') @@ -399,7 +403,7 @@ describe('Runtime Tests', () => { dummyJob1 ); - expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); // Because not ready Job was removed + expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); // Because exceeded Job was removed // Job that ran through expect( @@ -455,6 +459,7 @@ describe('Runtime Tests', () => { ); // Since the Job is ready but the Observer value simply hasn't changed + // -> no point in trying to update it again expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); // Job that didn't ran through @@ -478,7 +483,7 @@ describe('Runtime Tests', () => { expect(console.warn).toHaveBeenCalledTimes(0); }); - it('should extract ready and updated Subscription Containers', () => { + it('should extract ready and to update Subscription Containers', () => { jest .spyOn(runtime, 'handleSelectors') .mockReturnValueOnce(true) @@ -589,11 +594,17 @@ describe('Runtime Tests', () => { expect(callbackSubscriptionContainer2.callback).toHaveBeenCalledTimes( 1 ); + expect( + Array.from(callbackSubscriptionContainer2.updatedSubscribers) + ).toStrictEqual([]); // Callback Subscription Container 3 expect(callbackSubscriptionContainer3.callback).toHaveBeenCalledTimes( 1 ); + expect( + Array.from(callbackSubscriptionContainer2.updatedSubscribers) + ).toStrictEqual([]); }); }); @@ -685,19 +696,26 @@ describe('Runtime Tests', () => { { data: { name: 'hans' }, }, + { + data: { name: 'frank' }, + }, ]; dummyObserver2.previousValue = [ { - key: 'dummyObserver2Value1', data: { name: 'jeff' }, }, { - key: 'dummyObserver2Value2', data: { name: 'hans' }, }, + { + data: { name: 'frank' }, + }, ]; arraySubscriptionContainer.selectorsWeakMap.set(dummyObserver2, { - methods: [(value) => value[0]?.data?.name], + methods: [ + (value) => value[0]?.data?.name, + (value) => value[2]?.data?.name, + ], }); arrayJob = new RuntimeJob(dummyObserver2, { key: 'dummyObjectJob2' }); @@ -708,7 +726,7 @@ describe('Runtime Tests', () => { jest.clearAllMocks(); }); - it('should return true if Subscritpion Container has no selector methods', () => { + it('should return true if Subscription Container has no selector methods', () => { objectSubscriptionContainer.selectorsWeakMap.delete(dummyObserver1); const response = runtime.handleSelectors( @@ -722,8 +740,7 @@ describe('Runtime Tests', () => { it('should return true if selected property has changed (object value)', () => { dummyObserver1.value = { - key: 'dummyObserverValue1', - data: { name: 'hans' }, + data: { name: 'changedName' }, }; const response = runtime.handleSelectors( @@ -732,6 +749,8 @@ describe('Runtime Tests', () => { ); expect(response).toBeTruthy(); + + expect(Utils.notEqual).toHaveBeenCalledTimes(1); expect(Utils.notEqual).toHaveBeenCalledWith( dummyObserver1.value.data.name, dummyObserver1.previousValue.data.name @@ -745,13 +764,15 @@ describe('Runtime Tests', () => { ); expect(response).toBeFalsy(); + + expect(Utils.notEqual).toHaveBeenCalledTimes(1); expect(Utils.notEqual).toHaveBeenCalledWith( dummyObserver1.value.data.name, dummyObserver1.previousValue.data.name ); }); - // TODO the deepness check isn't possible with the custom defined selector methods + // TODO the deepness check isn't possible with the current way of handling selector methods // it('should return true if selected property has changed in the deepness (object value)', () => { // dummyObserver1.value = { // key: 'dummyObserverValue1', @@ -770,16 +791,17 @@ describe('Runtime Tests', () => { // expect(Utils.notEqual).toHaveBeenCalledWith(undefined, undefined); // }); - it('should return true if used property has changed (array value)', () => { + it('should return true if a selected property has changed (array value)', () => { dummyObserver2.value = [ { - key: 'dummyObserver2Value1', - data: { name: 'frank' }, + data: { name: 'jeff' }, }, { - key: 'dummyObserver2Value2', data: { name: 'hans' }, }, + { + data: { name: 'changedName' }, + }, ]; const response = runtime.handleSelectors( @@ -788,23 +810,47 @@ describe('Runtime Tests', () => { ); expect(response).toBeTruthy(); + + expect(Utils.notEqual).toHaveBeenCalledTimes(2); expect(Utils.notEqual).toHaveBeenCalledWith( dummyObserver2.value['0'].data.name, dummyObserver2.previousValue['0'].data.name ); + expect(Utils.notEqual).toHaveBeenCalledWith( + dummyObserver2.value['2'].data.name, + dummyObserver2.previousValue['2'].data.name + ); }); it("should return false if used property hasn't changed (array value)", () => { + dummyObserver2.value = [ + { + data: { name: 'jeff' }, + }, + { + data: { name: 'changedName (but not selected)' }, + }, + { + data: { name: 'frank' }, + }, + ]; + const response = runtime.handleSelectors( arraySubscriptionContainer, arrayJob ); expect(response).toBeFalsy(); + + expect(Utils.notEqual).toHaveBeenCalledTimes(2); expect(Utils.notEqual).toHaveBeenCalledWith( dummyObserver2.value['0'].data.name, dummyObserver2.previousValue['0'].data.name ); + expect(Utils.notEqual).toHaveBeenCalledWith( + dummyObserver2.value['2'].data.name, + dummyObserver2.previousValue['2'].data.name + ); }); }); }); From 55e496a827a775fc9ec4d7add107f0fbb59d3b39 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 12 Jun 2021 11:35:49 +0200 Subject: [PATCH 51/63] fixed typos --- packages/core/src/state/state.observer.ts | 5 ++++- .../core/tests/unit/runtime/observer.test.ts | 5 ++++- .../subscription/sub.controller.test.ts | 20 +++++++++---------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index fd0182dc..a8c986dd 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -14,6 +14,7 @@ import { SideEffectInterface, createArrayFromObject, CreateStateRuntimeJobConfigInterface, + generateId, } from '../internal'; export class StateObserver extends Observer { @@ -100,7 +101,9 @@ export class StateObserver extends Observer { force: config.force, background: config.background, overwrite: config.overwrite, - key: config.key || this._key, + key: + config.key ?? + `${this._key != null ? this._key + '_' : ''}${generateId()}`, }); this.agileInstance().runtime.ingest(job, { diff --git a/packages/core/tests/unit/runtime/observer.test.ts b/packages/core/tests/unit/runtime/observer.test.ts index 525dcca7..7d246abd 100644 --- a/packages/core/tests/unit/runtime/observer.test.ts +++ b/packages/core/tests/unit/runtime/observer.test.ts @@ -4,6 +4,7 @@ import { SubscriptionContainer, RuntimeJob, } from '../../../src'; +import * as Utils from '@agile-ts/utils'; import { LogMock } from '../../helper/logMock'; describe('Observer Tests', () => { @@ -88,8 +89,10 @@ describe('Observer Tests', () => { describe('ingest function tests', () => { it('should create RuntimeJob containing the Observer and ingest it into the Runtime (default config)', () => { + jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey'); + dummyAgile.runtime.ingest = jest.fn((job: RuntimeJob) => { - expect(job._key).toBe(observer._key); + expect(job._key).toBe(`${observer._key}_generatedKey`); expect(job.observer).toBe(observer); expect(job.config).toStrictEqual({ background: false, diff --git a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts index 1d60caf0..e52e81a1 100644 --- a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts +++ b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts @@ -396,7 +396,7 @@ describe('SubController Tests', () => { ); expect(componentSubscriptionContainer.ready).toBeTruthy(); - expect(Array.from(subController.componentSubs)).toBe([ + expect(Array.from(subController.componentSubs)).toStrictEqual([ componentSubscriptionContainer, ]); @@ -407,10 +407,9 @@ describe('SubController Tests', () => { // Check if ComponentSubscriptionContainer was called with correct parameters expect(componentSubscriptionContainer.key).toBe('dummyKey'); expect(componentSubscriptionContainer.componentId).toBe('testID'); - expect(Array.from(componentSubscriptionContainer.subscribers)).toBe([ - dummyObserver1, - dummyObserver2, - ]); + expect( + Array.from(componentSubscriptionContainer.subscribers) + ).toStrictEqual([dummyObserver1, dummyObserver2]); } ); @@ -434,17 +433,16 @@ describe('SubController Tests', () => { ); expect(componentSubscriptionContainer.ready).toBeFalsy(); - expect(Array.from(subController.componentSubs)).toBe([ + expect(Array.from(subController.componentSubs)).toStrictEqual([ componentSubscriptionContainer, ]); // Check if ComponentSubscriptionContainer was called with correct parameters expect(componentSubscriptionContainer.key).toBe('generatedKey'); expect(componentSubscriptionContainer.componentId).toBeUndefined(); - expect(Array.from(componentSubscriptionContainer.subscribers)).toBe([ - dummyObserver1, - dummyObserver2, - ]); + expect( + Array.from(componentSubscriptionContainer.subscribers) + ).toStrictEqual([dummyObserver1, dummyObserver2]); }); it('should return ready Component based Subscription Container if componentInstance is mounted (config.waitForMount = true)', () => { @@ -468,7 +466,7 @@ describe('SubController Tests', () => { ); expect(componentSubscriptionContainer.ready).toBeTruthy(); - expect(Array.from(subController.componentSubs)).toBe([ + expect(Array.from(subController.componentSubs)).toStrictEqual([ componentSubscriptionContainer, ]); From 2a2e75cfc29e17cb3fabc0ae23a0f35f57ac5724 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 12 Jun 2021 14:49:33 +0200 Subject: [PATCH 52/63] optimized state observer method descriptions --- examples/vue/develop/my-project/src/core.js | 6 +- packages/core/src/state/index.ts | 7 +- packages/core/src/state/state.observer.ts | 113 +++++++++++------- .../tests/unit/state/state.observer.test.ts | 30 +++-- 4 files changed, 97 insertions(+), 59 deletions(-) diff --git a/examples/vue/develop/my-project/src/core.js b/examples/vue/develop/my-project/src/core.js index ecb19e8b..2e3f7902 100644 --- a/examples/vue/develop/my-project/src/core.js +++ b/examples/vue/develop/my-project/src/core.js @@ -7,7 +7,11 @@ export const App = new Agile({ }).integrate(vueIntegration); // Create State -export const MY_STATE = App.createState('Hello World', { key: 'my-state' }); +export const MY_STATE = App.createState('World', { + key: 'my-state', +}).computeValue((v) => { + return `Hello ${v}`; +}); // Create Collection export const TODOS = App.createCollection({ diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 1e70c201..3dc8a1fd 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -218,9 +218,9 @@ export class State { /** * Ingests the State without any specified new value into the runtime. * - * Since no new value was defined either the State value is computed + * Since no new value was defined either the new State value is computed * based on a compute method (Computed Class) - * or the `nextStateValue` is taken. + * or the `nextStateValue` is taken as the next State value. * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#ingest) * @@ -610,7 +610,8 @@ export class State { this.computeValueMethod = method; // Initial compute - this.set(method(this.nextStateValue)); + // (not directly computing it here since it is computed once in the runtime!) + this.set(this.nextStateValue); return this; } diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index a8c986dd..7b058fb7 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -18,14 +18,20 @@ import { } from '../internal'; export class StateObserver extends Observer { + // State the Observer belongs to public state: () => State; - public nextStateValue: ValueType; // Next State value + + // Next value applied to the State + public nextStateValue: ValueType; /** + * A State Observer manages the subscriptions to Subscription Containers (UI-Components) + * and dependencies to other Observers (Agile Classes) + * for a State Class. + * * @internal - * State Observer - Handles State changes, dependencies (-> Interface to Runtime) - * @param state - State - * @param config - Config + * @param state - Instance of State the Observer belongs to. + * @param config - Configuration object */ constructor( state: State, @@ -36,13 +42,16 @@ export class StateObserver extends Observer { this.nextStateValue = copy(state._value); } - //========================================================================================================= - // Ingest - //========================================================================================================= /** + * Passes the State Observer into the runtime wrapped into a Runtime-Job + * where it is executed accordingly. + * + * During the execution the runtime applies the `nextStateValue` + * or the `computedValue` (Computed Class) to the State, + * updates its dependents and re-renders the UI-Components it is subscribed to. + * * @internal - * Ingests nextStateValue or computedValue into Runtime and applies it to the State - * @param config - Config + * @param config - Configuration object */ public ingest(config: StateIngestConfigInterface = {}): void { const state = this.state(); @@ -54,14 +63,16 @@ export class StateObserver extends Observer { this.ingestValue(newStateValue, config); } - //========================================================================================================= - // Ingest Value - //========================================================================================================= /** + * Passes the State Observer into the runtime wrapped into a Runtime-Job + * where it is executed accordingly. + * + * During the execution the runtime applies the specified `newStateValue` to the State, + * updates its dependents and re-renders the UI-Components it is subscribed to. + * * @internal - * Ingests new State Value into Runtime and applies it to the State - * @param newStateValue - New Value of the State - * @param config - Config + * @param newStateValue - New value to be applied to the State. + * @param config - Configuration object. */ public ingestValue( newStateValue: ValueType, @@ -80,21 +91,22 @@ export class StateObserver extends Observer { overwrite: false, }); - // Force overwriting State because if assigning Value to State, the State shouldn't be a placeholder anymore + // Force overwriting the State value if it was a placeholder. + // Because after assigning a value to the State it shouldn't be a placeholder anymore. if (state.isPlaceholder) { config.force = true; config.overwrite = true; } - // Assign next State Value and compute it if necessary + // Assign next State value and compute it if necessary this.nextStateValue = state.computeValueMethod ? copy(state.computeValueMethod(newStateValue)) : copy(newStateValue); - // Check if State Value and new/next Value are equals + // Check if current State value and to assign State value are equals if (equal(state._value, this.nextStateValue) && !config.force) return; - // Create Job + // Create Runtime-Job const job = new StateRuntimeJob(this, { storage: config.storage, sideEffects: config.sideEffects, @@ -106,23 +118,26 @@ export class StateObserver extends Observer { `${this._key != null ? this._key + '_' : ''}${generateId()}`, }); + // Pass created Job into the Runtime this.agileInstance().runtime.ingest(job, { perform: config.perform, }); } - //========================================================================================================= - // Perform - //========================================================================================================= /** + * Method executed by the Runtime to perform the Runtime-Job, + * previously ingested via the `ingest()` or `ingestValue()` method. + * + * Thereby the previously defined `nextStateValue` is assigned to the State + * and the side effects (`sideEffects`) are executed. + * * @internal - * Performs Job that holds this Observer - * @param job - Job + * @param job - Runtime-Job to be performed. */ public perform(job: StateRuntimeJob) { const state = job.observer.state(); - // Assign new State Values + // Assign new State values state.previousStateValue = copy(state._value); state._value = copy(job.observer.nextStateValue); state.nextStateValue = copy(job.observer.nextStateValue); @@ -130,7 +145,7 @@ export class StateObserver extends Observer { // https://www.geeksforgeeks.org/object-freeze-javascript/#:~:text=Object.freeze()%20Method&text=freeze()%20which%20is%20used,the%20prototype%20of%20the%20object. // if (typeof state._value === 'object') Object.freeze(state._value); - // Overwrite old State Values + // Overwrite entire State with the newly assigned value if (job.config.overwrite) { state.initialStateValue = copy(state._value); state.previousStateValue = copy(state._value); @@ -138,35 +153,37 @@ export class StateObserver extends Observer { } state.isSet = notEqual(state._value, state.initialStateValue); - - // Execute sideEffects like 'rebuildGroup' or 'rebuildStateStorageValue' this.sideEffects(job); - // Assign Public Value to Observer after sideEffects like 'rebuildGroup', - // because sometimes (for instance in a Group State) the publicValue() is not the .value (nextStateValue) property. - // The Observer value is at some point the public Value because Integrations like React are using it as return value. - // For example 'useAgile()' returns the Observer.value and not the State.value. + // Assign public value to the Observer after sideEffects like 'rebuildGroup' were executed. + // Because sometimes (for instance in a Group State) the 'publicValue()' + // is not the '.value' ('nextStateValue') property. + // The Observer value is at some point the public value + // since Integrations like React are using it as the return value. + // (For example 'useAgile()' returns 'Observer.value' and not 'State.value'.) job.observer.previousValue = copy(job.observer.value); job.observer.value = copy(state.getPublicValue()); } - //========================================================================================================= - // Side Effect - //========================================================================================================= /** + * Performs the side effects of applying the next State value to the State. + * + * Side effects are, for example, calling the watcher functions + * or executing the side effects defined in the State Class + * like 'rebuildGroup' or 'rebuildStateStorageValue'. + * * @internal - * SideEffects of Job - * @param job - Job + * @param job - Job that is currently performed. */ public sideEffects(job: StateRuntimeJob) { const state = job.observer.state(); - // Call Watchers Functions + // Call watcher functions for (const watcherKey in state.watchers) if (isFunction(state.watchers[watcherKey])) state.watchers[watcherKey](state.getPublicValue(), watcherKey); - // Call SideEffect Functions + // Call side effect functions if (job.config?.sideEffects?.enabled) { const sideEffectArray = createArrayFromObject< SideEffectInterface> @@ -174,6 +191,7 @@ export class StateObserver extends Observer { sideEffectArray.sort(function (a, b) { return b.instance.weight - a.instance.weight; }); + for (const sideEffect of sideEffectArray) { if (isFunction(sideEffect.instance.callback)) { if (!job.config.sideEffects.exclude?.includes(sideEffect.key)) @@ -184,14 +202,21 @@ export class StateObserver extends Observer { } } -/** - * @param dependents - Initial Dependents of State Observer - * @param subs - Initial Subscriptions of State Observer - * @param key - Key/Name of State Observer - */ export interface CreateStateObserverConfigInterface { + /** + * Initial Observers to depend on the State Observer. + * @default [] + */ dependents?: Array; + /** + * Initial Subscription Containers the State Observer is subscribed to. + * @default [] + */ subs?: Array; + /** + * Key/Name identifier of the State Observer. + * @default undefined + */ key?: ObserverKey; } diff --git a/packages/core/tests/unit/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index e187a34b..c9d837e8 100644 --- a/packages/core/tests/unit/state/state.observer.test.ts +++ b/packages/core/tests/unit/state/state.observer.test.ts @@ -8,6 +8,7 @@ import { StatePersistent, SubscriptionContainer, } from '../../../src'; +import * as Utils from '@agile-ts/utils'; import { LogMock } from '../../helper/logMock'; describe('StateObserver Tests', () => { @@ -38,8 +39,8 @@ describe('StateObserver Tests', () => { it('should create StateObserver (specific config)', () => { const dummyObserver1 = new Observer(dummyAgile, { key: 'dummyObserver1' }); const dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); - const dummySubscription1 = new SubscriptionContainer(); - const dummySubscription2 = new SubscriptionContainer(); + const dummySubscription1 = new SubscriptionContainer([]); + const dummySubscription2 = new SubscriptionContainer([]); const stateObserver = new StateObserver(dummyState, { key: 'testKey', @@ -145,8 +146,10 @@ describe('StateObserver Tests', () => { }); it("should ingest State into Runtime if newValue isn't equal to currentValue (default config)", () => { + jest.spyOn(Utils, 'generateId').mockReturnValue('randomKey'); + dummyAgile.runtime.ingest = jest.fn((job: StateRuntimeJob) => { - expect(job._key).toBe(stateObserver._key); + expect(job._key).toBe(`${stateObserver._key}_randomKey`); expect(job.observer).toBe(stateObserver); expect(job.config).toStrictEqual({ background: false, @@ -215,9 +218,10 @@ describe('StateObserver Tests', () => { }); it('should ingest State into Runtime if newValue is equal to currentValue (config.force = true)', () => { + jest.spyOn(Utils, 'generateId').mockReturnValue('randomKey'); dummyState._value = 'updatedDummyValue'; dummyAgile.runtime.ingest = jest.fn((job: StateRuntimeJob) => { - expect(job._key).toBe(stateObserver._key); + expect(job._key).toBe(`${stateObserver._key}_randomKey`); expect(job.observer).toBe(stateObserver); expect(job.config).toStrictEqual({ background: false, @@ -243,8 +247,9 @@ describe('StateObserver Tests', () => { }); it('should ingest placeholder State into Runtime (default config)', () => { + jest.spyOn(Utils, 'generateId').mockReturnValue('randomKey'); dummyAgile.runtime.ingest = jest.fn((job: StateRuntimeJob) => { - expect(job._key).toBe(stateObserver._key); + expect(job._key).toBe(`${stateObserver._key}_randomKey`); expect(job.observer).toBe(stateObserver); expect(job.config).toStrictEqual({ background: false, @@ -302,11 +307,11 @@ describe('StateObserver Tests', () => { it('should perform Job', () => { dummyJob.observer.nextStateValue = 'newValue'; + dummyJob.observer.value = 'dummyValue'; dummyState.initialStateValue = 'initialValue'; dummyState._value = 'dummyValue'; dummyState.getPublicValue = jest .fn() - .mockReturnValueOnce('previousPublicValue') .mockReturnValueOnce('newPublicValue'); stateObserver.perform(dummyJob); @@ -316,20 +321,21 @@ describe('StateObserver Tests', () => { expect(dummyState._value).toBe('newValue'); expect(dummyState.nextStateValue).toBe('newValue'); expect(dummyState.isSet).toBeTruthy(); + expect(stateObserver.value).toBe('newPublicValue'); - expect(stateObserver.previousValue).toBe('previousPublicValue'); + expect(stateObserver.previousValue).toBe('dummyValue'); expect(stateObserver.sideEffects).toHaveBeenCalledWith(dummyJob); }); it('should perform Job and overwrite State (job.config.overwrite = true)', () => { dummyJob.observer.nextStateValue = 'newValue'; + dummyJob.observer.value = 'dummyValue'; dummyJob.config.overwrite = true; dummyState.isPlaceholder = true; dummyState.initialStateValue = 'overwriteValue'; dummyState._value = 'dummyValue'; dummyState.getPublicValue = jest .fn() - .mockReturnValueOnce('previousPublicValue') .mockReturnValueOnce('newPublicValue'); stateObserver.perform(dummyJob); @@ -340,18 +346,19 @@ describe('StateObserver Tests', () => { expect(dummyState.nextStateValue).toBe('newValue'); expect(dummyState.isSet).toBeFalsy(); expect(dummyState.isPlaceholder).toBeFalsy(); + expect(stateObserver.value).toBe('newPublicValue'); - expect(stateObserver.previousValue).toBe('previousPublicValue'); + expect(stateObserver.previousValue).toBe('dummyValue'); expect(stateObserver.sideEffects).toHaveBeenCalledWith(dummyJob); }); it('should perform Job and set isSet to false if initialStateValue equals to newStateValue', () => { dummyJob.observer.nextStateValue = 'newValue'; + dummyJob.observer.value = 'dummyValue'; dummyState.initialStateValue = 'newValue'; dummyState._value = 'dummyValue'; dummyState.getPublicValue = jest .fn() - .mockReturnValueOnce('previousPublicValue') .mockReturnValueOnce('newPublicValue'); stateObserver.perform(dummyJob); @@ -361,8 +368,9 @@ describe('StateObserver Tests', () => { expect(dummyState._value).toBe('newValue'); expect(dummyState.nextStateValue).toBe('newValue'); expect(dummyState.isSet).toBeFalsy(); + expect(stateObserver.value).toBe('newPublicValue'); - expect(stateObserver.previousValue).toBe('previousPublicValue'); + expect(stateObserver.previousValue).toBe('dummyValue'); expect(stateObserver.sideEffects).toHaveBeenCalledWith(dummyJob); }); }); From 0c2cd5ee9e40d57c740e63eba20bea3a27aeede7 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 12 Jun 2021 17:45:08 +0200 Subject: [PATCH 53/63] fixed typos --- .../src/collection/collection.persistent.ts | 78 ++--- packages/core/src/state/state.persistent.ts | 159 ++++------ packages/core/src/state/state.runtime.job.ts | 20 +- packages/core/src/storages/persistent.ts | 58 +++- .../collection/collection.persistent.test.ts | 111 +------ .../tests/unit/state/state.persistent.test.ts | 295 +++++++++--------- .../unit/state/state.runtime.job.test.ts | 6 +- .../tests/unit/storages/persistent.test.ts | 68 ++++ 8 files changed, 373 insertions(+), 422 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 380451b8..ee835da6 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -53,35 +53,6 @@ export class CollectionPersistent< if (this.ready && config.instantiate) this.initialLoading(); } - /** - * Updates key/name identifier of Persistent. - * - * @internal - * @param value - New key/name identifier. - */ - public async setKey(value?: StorageKey): Promise { - const oldKey = this._key; - const wasReady = this.ready; - - // Assign new key to Persistent - if (value === this._key) return; - this._key = value ?? Persistent.placeHolderKey; - - const isValid = this.validatePersistent(); - - // Try to initial load value if persistent wasn't ready before - if (!wasReady) { - if (isValid) await this.initialLoading(); - return; - } - - // Remove persisted values at old key - await this.removePersistedValue(oldKey); - - // Persist values at the new key - if (isValid) await this.persistValue(value); - } - /** * Loads the persisted value into the Collection * or persists the Collection value in the corresponding Storage. @@ -97,10 +68,12 @@ export class CollectionPersistent< /** * Loads Collection Instances (like Items or Groups) from the corresponding Storage - * and sets up side effects that dynamically update the Storage value when the Collection (Instances) changes. + * and sets up side effects that dynamically update + * the Storage value when the Collection (Instances) changes. * * @internal - * @param storageItemKey - Prefix key of persisted Collection Instances. | default = Persistent.key | + * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. + * | default = Persistent.key | * @return Whether the loading was successful. */ public async loadPersistedValue( @@ -109,13 +82,14 @@ export class CollectionPersistent< if (!this.ready) return false; const _storageItemKey = storageItemKey ?? this._key; - // Check if Collection is already persisted (indicated by the persistence of true at _storageItemKey) + // Check if Collection is already persisted + // (indicated by the persistence of 'true' at '_storageItemKey') const isPersisted = await this.agileInstance().storages.get( _storageItemKey, this.config.defaultStorageKey as any ); - // Return false if Collection isn't persisted yet + // Return 'false' if Collection isn't persisted yet if (!isPersisted) return false; // Helper function to load persisted values into the Collection @@ -191,7 +165,8 @@ export class CollectionPersistent< }; const success = await loadValuesIntoCollection(); - // Setup Side Effects to keep the Storage value in sync with the Collection (Instances) value + // Setup Side Effects to keep the Storage value in sync + // with the Collection (Instances) value if (success) this.setupSideEffects(_storageItemKey); return success; @@ -199,10 +174,12 @@ export class CollectionPersistent< /** * Persists Collection Instances (like Items or Groups) in the corresponding Storage - * and sets up side effects that dynamically update the Storage value when the Collection (Instances) changes. + * and sets up side effects that dynamically update + * the Storage value when the Collection (Instances) changes. * * @internal - * @param storageItemKey - Prefix key of persisted Collection Instances. | default = Persistent.key | + * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. + * | default = Persistent.key | * @return Whether the persisting and the setting up of the side effects was successful. */ public async persistValue(storageItemKey?: PersistentKey): Promise { @@ -239,7 +216,8 @@ export class CollectionPersistent< }); } - // Setup Side Effects to keep the Storage value in sync with the Collection (Instances) value + // Setup Side Effects to keep the Storage value in sync + // with the Collection (Instances) value this.setupSideEffects(_storageItemKey); this.isPersisted = true; @@ -247,17 +225,19 @@ export class CollectionPersistent< } /** - * Sets up side effects to keep the Storage value in sync with the Collection (Instances) value. + * Sets up side effects to keep the Storage value in sync + * with the Collection (Instances) value. * * @internal - * @param storageItemKey - Prefix key of persisted Collection Instances. | default = Persistent.key | + * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. + * | default = Persistent.key | */ public setupSideEffects(storageItemKey?: PersistentKey): void { const _storageItemKey = storageItemKey ?? this._key; const defaultGroup = this.collection().getDefaultGroup(); if (defaultGroup == null) return; - // Add side effect to default Group + // Add side effect to the default Group // that adds and removes Items from the Storage based on the Group value defaultGroup.addSideEffect( CollectionPersistent.defaultGroupSideEffectKey, @@ -267,12 +247,13 @@ export class CollectionPersistent< } /** - * Removes Collection from the corresponding Storage. + * Removes the Collection from the corresponding Storage. * -> Collection is no longer persisted * * @internal - * @param storageItemKey - Prefix key of persisted Collection Instances. | default = Persistent.key | - * @return Whether the removing was successful. + * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. + * | default = Persistent.key | + * @return Whether the removal of the persisted value was successful. */ public async removePersistedValue( storageItemKey?: PersistentKey @@ -310,12 +291,12 @@ export class CollectionPersistent< } /** - * Formats given key so that it can be used as a valid Storage key and returns it. - * If no formatable key (undefined/null) is given, - * an attempt is made to use the Collection key. + * Formats specified key so that it can be used as a valid Storage key and returns it. + * If no formatable key (undefined/null) was provided, + * an attempt is made to use the Collection identifier key. * * @internal - * @param key - Key to be formatted + * @param key - Key to be formatted. */ public formatKey(key: StorageKey | undefined | null): StorageKey | undefined { if (key == null && this.collection()._key) return this.collection()._key; @@ -329,7 +310,8 @@ export class CollectionPersistent< * * @internal * @param group - Group whose Items are to be dynamically added or removed from the Storage. - * @param storageItemKey - Prefix key of persisted Collection Instances. | default = Persistent.key | + * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. + * | default = Persistent.key | */ public rebuildStorageSideEffect( group: Group, diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index 89a7063a..ef8f1b16 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -4,7 +4,6 @@ import { Persistent, PersistentKey, State, - StorageKey, } from '../internal'; export class StatePersistent extends Persistent { @@ -12,10 +11,11 @@ export class StatePersistent extends Persistent { public state: () => State; /** + * Internal Class for managing the permanent persistence of a State. + * * @internal - * State Persist Manager - Handles permanent storing of State Value - * @param state - State that gets stored - * @param config - Config + * @param state - State to be persisted. + * @param config - Configuration object */ constructor( state: State, @@ -36,47 +36,16 @@ export class StatePersistent extends Persistent { defaultStorageKey: config.defaultStorageKey, }); - // Load/Store persisted Value for the first Time + // Load/Store persisted value/s for the first time if (this.ready && config.instantiate) this.initialLoading(); } - //========================================================================================================= - // Set Key - //========================================================================================================= - /** - * @internal - * Updates Key/Name of Persistent - * @param value - New Key/Name of Persistent - */ - public async setKey(value?: StorageKey): Promise { - const oldKey = this._key; - const wasReady = this.ready; - - // Assign Key - if (value === this._key) return; - this._key = value || Persistent.placeHolderKey; - - const isValid = this.validatePersistent(); - - // Try to Initial Load Value if persistent wasn't ready and return - if (!wasReady) { - if (isValid) await this.initialLoading(); - return; - } - - // Remove value at old Key - await this.removePersistedValue(oldKey); - - // Assign Value to new Key - if (isValid) await this.persistValue(value); - } - - //========================================================================================================= - // Initial Loading - //========================================================================================================= /** + * Loads the persisted value into the State + * or persists the State value in the corresponding Storage. + * This behaviour depends on whether the State has been persisted before. + * * @internal - * Loads/Saves Storage Value for the first Time */ public async initialLoading() { super.initialLoading().then(() => { @@ -84,14 +53,15 @@ export class StatePersistent extends Persistent { }); } - //========================================================================================================= - // Load Persisted Value - //========================================================================================================= /** + * Loads the State from the corresponding Storage + * and sets up side effects that dynamically update + * the Storage value when the State changes. + * * @internal - * Loads State Value from the Storage - * @param storageItemKey - Prefix Key of Persisted Instances (default PersistentKey) - * @return Success? + * @param storageItemKey - Storage key of the persisted State Instance. + * | default = Persistent.key | + * @return Whether the loading was successful. */ public async loadPersistedValue( storageItemKey?: PersistentKey @@ -99,40 +69,43 @@ export class StatePersistent extends Persistent { if (!this.ready) return false; const _storageItemKey = storageItemKey ?? this._key; - // Load Value from default Storage + // Load value from default Storage const loadedValue = await this.agileInstance().storages.get( _storageItemKey, this.config.defaultStorageKey as any ); if (loadedValue == null) return false; - // Assign loaded Value to State + // Assign loaded value to the State this.state().set(loadedValue, { storage: false, overwrite: true, }); - // Setup Side Effects to keep the Storage value in sync with the State value - this.setupSideEffects(storageItemKey); + // Setup Side Effects to keep the Storage value in sync + // with the State value + this.setupSideEffects(_storageItemKey); return true; } - //========================================================================================================= - // Persist Value - //========================================================================================================= /** + * Persists the State in the corresponding Storage + * and sets up side effects that dynamically update + * the Storage value when the State changes. + * * @internal - * Sets everything up so that the State is saved in the Storage on every Value change - * @param storageItemKey - Prefix Key of Persisted Instances (default PersistentKey) - * @return Success? + * @param storageItemKey - Storage key of the persisted State Instance. + * | default = Persistent.key | + * @return Whether the persisting and the setting up of the side effects was successful. */ public async persistValue(storageItemKey?: PersistentKey): Promise { if (!this.ready) return false; const _storageItemKey = storageItemKey ?? this._key; - // Setup side effects to keep the Storage value in sync with the State value - this.setupSideEffects(storageItemKey); + // Setup Side Effects to keep the Storage value in sync + // with the State value + this.setupSideEffects(_storageItemKey); // Initial rebuild Storage for persisting State value in the corresponding Storage this.rebuildStorageSideEffect(this.state(), _storageItemKey); @@ -142,16 +115,18 @@ export class StatePersistent extends Persistent { } /** - * Sets up side effects to keep the Storage value in sync with the State value. + * Sets up side effects to keep the Storage value in sync + * with the State value. * * @internal - * @param storageItemKey - Prefix key of persisted Collection Instances | default = Persistent.key | + * @param storageItemKey - Storage key of the persisted State Instance. + * | default = Persistent.key | */ public setupSideEffects(storageItemKey?: PersistentKey) { const _storageItemKey = storageItemKey ?? this._key; - // Add side effect to State - // that updates the Storage value based on the State value + // Add side effect to the State + // that updates the Storage value based on the current State value this.state().addSideEffect( StatePersistent.storeValueSideEffectKey, (instance, config) => { @@ -161,76 +136,64 @@ export class StatePersistent extends Persistent { ); } - //========================================================================================================= - // Remove Persisted Value - //========================================================================================================= /** + * Removes the State from the corresponding Storage. + * -> State is no longer persisted + * * @internal - * Removes State Value form the Storage - * @param storageItemKey - Prefix Key of Persisted Instances (default PersistentKey) - * @return Success? + * @param storageItemKey - Storage key of the persisted State Instance. + * | default = Persistent.key | + * @return Whether the removal of the persisted value was successful. */ public async removePersistedValue( storageItemKey?: PersistentKey ): Promise { if (!this.ready) return false; const _storageItemKey = storageItemKey || this._key; - - // Remove SideEffect this.state().removeSideEffect(StatePersistent.storeValueSideEffectKey); - - // Remove Value from Storage this.agileInstance().storages.remove(_storageItemKey, this.storageKeys); - this.isPersisted = false; return true; } - //========================================================================================================= - // Format Key - //========================================================================================================= /** + * Formats specified key so that it can be used as a valid Storage key and returns it. + * If no formatable key (undefined/null) was provided, + * an attempt is made to use the State identifier key. + * * @internal - * Formats Storage Key - * @param key - Key that gets formatted + * @param key - Key to be formatted. */ public formatKey( key: PersistentKey | undefined | null ): PersistentKey | undefined { const state = this.state(); - - // Get key from State if (!key && state._key) return state._key; - if (!key) return; - - // Set State Key to Storage Key if State has no key if (!state._key) state._key = key; - return key; } - //========================================================================================================= - // Rebuild Storage SideEffect - //========================================================================================================= /** + * Rebuilds Storage value based on the current State value + * * @internal - * Rebuilds Storage depending on the State Value (Saves current State Value into the Storage) - * @param state - State that holds the new Value - * @param storageItemKey - StorageKey where value should be persisted - * @param config - Config + * @param state - State whose value to be in sync with the Storage value. + * @param storageItemKey - Storage key of the persisted State. + * | default = Persistent.key | + * @param config - Configuration object */ public rebuildStorageSideEffect( state: State, storageItemKey: PersistentKey, config: { [key: string]: any } = {} ) { - if (config.storage !== undefined && !config.storage) return; - - this.agileInstance().storages.set( - storageItemKey, - this.state().getPersistableValue(), - this.storageKeys - ); + if (config['storage'] == null || config.storage) { + this.agileInstance().storages.set( + storageItemKey, + this.state().getPersistableValue(), + this.storageKeys + ); + } } } diff --git a/packages/core/src/state/state.runtime.job.ts b/packages/core/src/state/state.runtime.job.ts index 5bd298f6..af57a67d 100644 --- a/packages/core/src/state/state.runtime.job.ts +++ b/packages/core/src/state/state.runtime.job.ts @@ -35,20 +35,26 @@ export class StateRuntimeJob extends RuntimeJob { } } -/** - * @param key - Key/Name of Job - */ export interface CreateStateRuntimeJobConfigInterface extends StateRuntimeJobConfigInterface { + /** + * Key/Name identifier of the Runtime Job. + * @default undefined + */ key?: RuntimeJobKey; } -/** - * @param overwrite - If whole State Value gets overwritten with Job Value - * @param storage - If Job Value can be saved in Storage - */ export interface StateRuntimeJobConfigInterface extends RuntimeJobConfigInterface { + /** + * Whether to overwrite the whole State with the new State value. + * @default false + */ overwrite?: boolean; + /** + * If the State is persisted, + * whether to store the new State value in an external Storage + * @default true + */ storage?: boolean; } diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index 2fa36634..6c521778 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -52,31 +52,51 @@ export class Persistent { } /** - * @internal - * Set Key/Name of Persistent + * Updates the key/name identifier of the Persistent. + * + * @public + * @param value - New key/name identifier. */ public set key(value: StorageKey) { this.setKey(value); } /** - * @internal - * Get Key/Name of Persistent + * Returns the key/name identifier of the Persistent. + * + * @public */ public get key(): StorageKey { return this._key; } - //========================================================================================================= - // Set Key - //========================================================================================================= /** + * Updates key/name identifier of Persistent. + * * @public - * Sets Key/Name of Persistent - * @param value - New Key/Name of Persistent + * @param value - New key/name identifier. */ - public setKey(value: StorageKey): void { - this._key = value; + public async setKey(value?: StorageKey): Promise { + const oldKey = this._key; + const wasReady = this.ready; + + // Assign new key to Persistent + if (value === this._key) return; + this._key = value ?? Persistent.placeHolderKey; + + const isValid = this.validatePersistent(); + + // Try to initial load value if persistent wasn't ready before + if (!wasReady) { + if (isValid) await this.initialLoading(); + return; + } + + // Remove persisted values with the old key + await this.removePersistedValue(oldKey); + + // Persist Collection values with the new key + if (isValid) await this.persistValue(value); } //========================================================================================================= @@ -181,9 +201,13 @@ export class Persistent { /** * @internal * Loads Value from Storage + * @param storageItemKey - Storage key of the persisted Instance. + * | default = Persistent.key | * @return Success? */ - public async loadPersistedValue(): Promise { + public async loadPersistedValue( + storageItemKey?: PersistentKey + ): Promise { LogCodeManager.log('00:03:00', ['loadPersistedValue', 'Persistent']); return false; } @@ -194,9 +218,11 @@ export class Persistent { /** * @internal * Saves/Updates Value in Storage + * @param storageItemKey - Storage key of the persisted Instance. + * | default = Persistent.key | * @return Success? */ - public async persistValue(): Promise { + public async persistValue(storageItemKey?: PersistentKey): Promise { LogCodeManager.log('00:03:00', ['persistValue', 'Persistent']); return false; } @@ -207,9 +233,13 @@ export class Persistent { /** * @internal * Removes Value form Storage + * @param storageItemKey - Storage key of the persisted Instance. + * | default = Persistent.key | * @return Success? */ - public async removePersistedValue(): Promise { + public async removePersistedValue( + storageItemKey?: PersistentKey + ): Promise { LogCodeManager.log('00:03:00', ['removePersistedValue', 'Persistent']); return false; } diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index 0a1044c5..731da595 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -205,88 +205,6 @@ describe('CollectionPersistent Tests', () => { }); }); - describe('setKey function tests', () => { - beforeEach(() => { - collectionPersistent.removePersistedValue = jest.fn(); - collectionPersistent.persistValue = jest.fn(); - collectionPersistent.initialLoading = jest.fn(); - }); - - it('should update key with valid key in ready Persistent', async () => { - collectionPersistent.ready = true; - collectionPersistent._key = 'dummyKey'; - jest - .spyOn(collectionPersistent, 'validatePersistent') - .mockReturnValueOnce(true); - - await collectionPersistent.setKey('newKey'); - - expect(collectionPersistent._key).toBe('newKey'); - expect(collectionPersistent.validatePersistent).toHaveBeenCalled(); - expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); - expect(collectionPersistent.persistValue).toHaveBeenCalledWith( - 'newKey' - ); - expect(collectionPersistent.removePersistedValue).toHaveBeenCalledWith( - 'dummyKey' - ); - }); - - it('should update key with not valid key in ready Persistent', async () => { - collectionPersistent.ready = true; - collectionPersistent._key = 'dummyKey'; - jest - .spyOn(collectionPersistent, 'validatePersistent') - .mockReturnValueOnce(false); - - await collectionPersistent.setKey(); - - expect(collectionPersistent._key).toBe(Persistent.placeHolderKey); - expect(collectionPersistent.validatePersistent).toHaveBeenCalled(); - expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); - expect(collectionPersistent.persistValue).not.toHaveBeenCalled(); - expect(collectionPersistent.removePersistedValue).toHaveBeenCalledWith( - 'dummyKey' - ); - }); - - it('should update key with valid key in not ready Persistent', async () => { - collectionPersistent.ready = false; - collectionPersistent._key = 'dummyKey'; - jest - .spyOn(collectionPersistent, 'validatePersistent') - .mockReturnValueOnce(true); - - await collectionPersistent.setKey('newKey'); - - expect(collectionPersistent._key).toBe('newKey'); - expect(collectionPersistent.validatePersistent).toHaveBeenCalled(); - expect(collectionPersistent.initialLoading).toHaveBeenCalled(); - expect(collectionPersistent.persistValue).not.toHaveBeenCalled(); - expect( - collectionPersistent.removePersistedValue - ).not.toHaveBeenCalled(); - }); - - it('should update key with not valid key in not ready Persistent', async () => { - collectionPersistent.ready = false; - collectionPersistent._key = 'dummyKey'; - jest - .spyOn(collectionPersistent, 'validatePersistent') - .mockReturnValueOnce(false); - - await collectionPersistent.setKey(); - - expect(collectionPersistent._key).toBe(Persistent.placeHolderKey); - expect(collectionPersistent.validatePersistent).toHaveBeenCalled(); - expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); - expect(collectionPersistent.persistValue).not.toHaveBeenCalled(); - expect( - collectionPersistent.removePersistedValue - ).not.toHaveBeenCalled(); - }); - }); - describe('initialLoading function tests', () => { beforeEach(() => { jest.spyOn(Persistent.prototype, 'initialLoading'); @@ -336,7 +254,7 @@ describe('CollectionPersistent Tests', () => { dummyAgile.storages.get = jest.fn(); }); - it('should load default Group and apply persisted value to Items that are already present in Collection (persistentKey)', async () => { + it('should load default Group and apply persisted value to Items that are already present in the Collection (persistentKey)', async () => { collectionPersistent.ready = true; dummyCollection.data = { ['3']: dummyItem3, @@ -387,7 +305,8 @@ describe('CollectionPersistent Tests', () => { ); }); - it("should load default Group and create/add persisted Items that aren't present in Collection yet (persistentKey)", async () => { + it("should load default Group " + + "and create/add persisted Items that aren't present in the Collection yet (persistentKey)", async () => { collectionPersistent.ready = true; dummyCollection.data = {}; dummyAgile.storages.get = jest @@ -502,8 +421,8 @@ describe('CollectionPersistent Tests', () => { it( 'should load default Group, ' + - "create/add persisted Items that aren't present in Collection yet " + - 'and apply persisted value to Items that are already present in Collection (specific key)', + "create/add persisted Items that aren't present in the Collection yet " + + 'and apply persisted value to Items that are already present in the Collection (specific key)', async () => { collectionPersistent.ready = true; dummyCollection.data = { @@ -564,7 +483,7 @@ describe('CollectionPersistent Tests', () => { expect( dummyCollection.createPlaceholderItem - ).not.toHaveBeenCalledWith('3'); // Because Item 3 is already present in Collection + ).not.toHaveBeenCalledWith('3'); // Because Item 3 is already present in the Collection expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith( '1' ); @@ -582,7 +501,7 @@ describe('CollectionPersistent Tests', () => { ); expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( placeholderItem3 - ); // Because Item 3 is already present in Collection + ); // Because Item 3 is already present in the Collection expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( 'dummyKey' @@ -832,7 +751,7 @@ describe('CollectionPersistent Tests', () => { }); }); - describe('setupSideEffect function tests', () => { + describe('setupSideEffects function tests', () => { let dummyDefaultGroup: Group; beforeEach(() => { @@ -848,7 +767,7 @@ describe('CollectionPersistent Tests', () => { ); }); - it("shouldn't add rebuild Storage side effect to default Group", () => { + it("shouldn't add rebuild Storage side effect to the default Group", () => { collectionPersistent.setupSideEffects(); expect( @@ -860,7 +779,7 @@ describe('CollectionPersistent Tests', () => { ); }); - it("shouldn't add rebuild Storage side effect to default Group if Collection has no default Group", () => { + it("shouldn't add rebuild Storage side effect to the default Group if the Collection has no default Group", () => { dummyCollection.getDefaultGroup = jest.fn(() => undefined as any); collectionPersistent.setupSideEffects(); @@ -868,10 +787,8 @@ describe('CollectionPersistent Tests', () => { expect(dummyDefaultGroup.addSideEffect).not.toHaveBeenCalled(); }); - describe('test added sideEffect called CollectionPersistent.defaultGroupSideEffectKey', () => { + describe("test added sideEffect called 'CollectionPersistent.defaultGroupSideEffectKey'", () => { beforeEach(() => { - collectionPersistent.ready = true; - collectionPersistent.rebuildStorageSideEffect = jest.fn(); dummyCollection.getDefaultGroup = jest.fn( () => dummyDefaultGroup as any @@ -879,7 +796,7 @@ describe('CollectionPersistent Tests', () => { }); it('should call rebuildStorageSideEffect (persistentKey)', async () => { - await collectionPersistent.persistValue(); + await collectionPersistent.setupSideEffects(); dummyDefaultGroup.sideEffects[ CollectionPersistent.defaultGroupSideEffectKey @@ -890,8 +807,8 @@ describe('CollectionPersistent Tests', () => { ).toHaveBeenCalledWith(dummyDefaultGroup, collectionPersistent._key); }); - it('should call rebuildStorageSideEffect (specific key)', async () => { - await collectionPersistent.persistValue('dummyKey'); + it('should call rebuildStorageSideEffect (specified key)', async () => { + await collectionPersistent.setupSideEffects('dummyKey'); dummyDefaultGroup.sideEffects[ CollectionPersistent.defaultGroupSideEffectKey diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index ded895be..ed87d03a 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -133,73 +133,6 @@ describe('StatePersistent Tests', () => { ); }); - describe('setKey function tests', () => { - beforeEach(() => { - statePersistent.removePersistedValue = jest.fn(); - statePersistent.persistValue = jest.fn(); - statePersistent.initialLoading = jest.fn(); - jest.spyOn(statePersistent, 'validatePersistent'); - }); - - it('should update key with valid key in ready Persistent', async () => { - statePersistent.ready = true; - statePersistent._key = 'dummyKey'; - - await statePersistent.setKey('newKey'); - - expect(statePersistent._key).toBe('newKey'); - expect(statePersistent.ready).toBeTruthy(); - expect(statePersistent.validatePersistent).toHaveBeenCalled(); - expect(statePersistent.initialLoading).not.toHaveBeenCalled(); - expect(statePersistent.persistValue).toHaveBeenCalledWith('newKey'); - expect(statePersistent.removePersistedValue).toHaveBeenCalledWith( - 'dummyKey' - ); - }); - - it('should update key with not valid key in ready Persistent', async () => { - statePersistent.ready = true; - statePersistent._key = 'dummyKey'; - - await statePersistent.setKey(); - - expect(statePersistent._key).toBe(StatePersistent.placeHolderKey); - expect(statePersistent.ready).toBeFalsy(); - expect(statePersistent.validatePersistent).toHaveBeenCalled(); - expect(statePersistent.initialLoading).not.toHaveBeenCalled(); - expect(statePersistent.persistValue).not.toHaveBeenCalled(); - expect(statePersistent.removePersistedValue).toHaveBeenCalledWith( - 'dummyKey' - ); - }); - - it('should update key with valid key in not ready Persistent', async () => { - statePersistent.ready = false; - - await statePersistent.setKey('newKey'); - - expect(statePersistent._key).toBe('newKey'); - expect(statePersistent.ready).toBeTruthy(); - expect(statePersistent.validatePersistent).toHaveBeenCalled(); - expect(statePersistent.initialLoading).toHaveBeenCalled(); - expect(statePersistent.persistValue).not.toHaveBeenCalled(); - expect(statePersistent.removePersistedValue).not.toHaveBeenCalled(); - }); - - it('should update key with not valid key in not ready Persistent', async () => { - statePersistent.ready = false; - - await statePersistent.setKey(); - - expect(statePersistent._key).toBe(StatePersistent.placeHolderKey); - expect(statePersistent.ready).toBeFalsy(); - expect(statePersistent.validatePersistent).toHaveBeenCalled(); - expect(statePersistent.initialLoading).not.toHaveBeenCalled(); - expect(statePersistent.persistValue).not.toHaveBeenCalled(); - expect(statePersistent.removePersistedValue).not.toHaveBeenCalled(); - }); - }); - describe('initialLoading function tests', () => { beforeEach(() => { jest.spyOn(Persistent.prototype, 'initialLoading'); @@ -216,101 +149,117 @@ describe('StatePersistent Tests', () => { describe('loadPersistedValue function tests', () => { beforeEach(() => { dummyState.set = jest.fn(); - statePersistent.persistValue = jest.fn(); + statePersistent.setupSideEffects = jest.fn(); }); - it('should load State Value with persistentKey and apply it to the State if loading was successful', async () => { - statePersistent.ready = true; - dummyAgile.storages.get = jest.fn(() => - Promise.resolve('dummyValue' as any) - ); + it( + 'should load State value with Persistent key from the corresponding Storage ' + + 'and apply it to the State if the loading was successful', + async () => { + statePersistent.ready = true; + dummyAgile.storages.get = jest.fn(() => + Promise.resolve('dummyValue' as any) + ); - const response = await statePersistent.loadPersistedValue(); + const response = await statePersistent.loadPersistedValue(); - expect(response).toBeTruthy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( - statePersistent._key, - statePersistent.config.defaultStorageKey - ); - expect(dummyState.set).toHaveBeenCalledWith('dummyValue', { - storage: false, - }); - expect(statePersistent.persistValue).toHaveBeenCalledWith( - statePersistent._key - ); - }); + expect(response).toBeTruthy(); + expect(dummyAgile.storages.get).toHaveBeenCalledWith( + statePersistent._key, + statePersistent.config.defaultStorageKey + ); + expect(dummyState.set).toHaveBeenCalledWith('dummyValue', { + storage: false, + overwrite: true, + }); + expect(statePersistent.setupSideEffects).toHaveBeenCalledWith( + statePersistent._key + ); + } + ); - it("should load State Value with persistentKey and shouldn't apply it to the State if loading wasn't successful", async () => { - statePersistent.ready = true; - dummyAgile.storages.get = jest.fn(() => - Promise.resolve(undefined as any) - ); + it( + "shouldn't load State value with Persistent key from the corresponding Storage " + + "and apply it to the State if the loading wasn't successful", + async () => { + statePersistent.ready = true; + dummyAgile.storages.get = jest.fn(() => + Promise.resolve(undefined as any) + ); - const response = await statePersistent.loadPersistedValue(); + const response = await statePersistent.loadPersistedValue(); - expect(response).toBeFalsy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( - statePersistent._key, - statePersistent.config.defaultStorageKey - ); - expect(dummyState.set).not.toHaveBeenCalled(); - expect(statePersistent.persistValue).not.toHaveBeenCalled(); - }); + expect(response).toBeFalsy(); + expect(dummyAgile.storages.get).toHaveBeenCalledWith( + statePersistent._key, + statePersistent.config.defaultStorageKey + ); + expect(dummyState.set).not.toHaveBeenCalled(); + expect(statePersistent.setupSideEffects).not.toHaveBeenCalled(); + } + ); - it('should load State Value with specific Key and apply it to the State if loading was successful', async () => { - statePersistent.ready = true; - dummyAgile.storages.get = jest.fn(() => - Promise.resolve('dummyValue' as any) - ); + it( + 'should load State value with specified key from the corresponding Storage ' + + 'and apply it to the State if the loading was successful', + async () => { + statePersistent.ready = true; + dummyAgile.storages.get = jest.fn(() => + Promise.resolve('dummyValue' as any) + ); - const response = await statePersistent.loadPersistedValue('coolKey'); + const response = await statePersistent.loadPersistedValue('coolKey'); - expect(response).toBeTruthy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( - 'coolKey', - statePersistent.config.defaultStorageKey - ); - expect(dummyState.set).toHaveBeenCalledWith('dummyValue', { - storage: false, - }); - expect(statePersistent.persistValue).toHaveBeenCalledWith('coolKey'); - }); + expect(response).toBeTruthy(); + expect(dummyAgile.storages.get).toHaveBeenCalledWith( + 'coolKey', + statePersistent.config.defaultStorageKey + ); + expect(dummyState.set).toHaveBeenCalledWith('dummyValue', { + storage: false, + overwrite: true, + }); + expect(statePersistent.setupSideEffects).toHaveBeenCalledWith( + 'coolKey' + ); + } + ); - it("shouldn't load State Value if Persistent isn't ready", async () => { - statePersistent.ready = false; - dummyAgile.storages.get = jest.fn(() => - Promise.resolve(undefined as any) - ); + it( + "shouldn't load State value from the corresponding Storage " + + "if Persistent isn't ready yet", + async () => { + statePersistent.ready = false; + dummyAgile.storages.get = jest.fn(() => + Promise.resolve(undefined as any) + ); - const response = await statePersistent.loadPersistedValue(); + const response = await statePersistent.loadPersistedValue(); - expect(response).toBeFalsy(); - expect(dummyAgile.storages.get).not.toHaveBeenCalled(); - expect(dummyState.set).not.toHaveBeenCalled(); - expect(statePersistent.persistValue).not.toHaveBeenCalled(); - }); + expect(response).toBeFalsy(); + expect(dummyAgile.storages.get).not.toHaveBeenCalled(); + expect(dummyState.set).not.toHaveBeenCalled(); + expect(statePersistent.setupSideEffects).not.toHaveBeenCalled(); + } + ); }); describe('persistValue function tests', () => { beforeEach(() => { - dummyState.addSideEffect = jest.fn(); + statePersistent.setupSideEffects = jest.fn(); statePersistent.rebuildStorageSideEffect = jest.fn(); statePersistent.isPersisted = false; }); - it('should persist State with persistentKey', async () => { + it('should persist State value with Persistent key', async () => { statePersistent.ready = true; const response = await statePersistent.persistValue(); expect(response).toBeTruthy(); - expect( - dummyState.addSideEffect - ).toHaveBeenCalledWith( - StatePersistent.storeValueSideEffectKey, - expect.any(Function), - { weight: 0 } + expect(statePersistent.setupSideEffects).toHaveBeenCalledWith( + statePersistent._key ); expect(statePersistent.rebuildStorageSideEffect).toHaveBeenCalledWith( dummyState, @@ -319,18 +268,14 @@ describe('StatePersistent Tests', () => { expect(statePersistent.isPersisted).toBeTruthy(); }); - it('should persist State with specific Key', async () => { + it('should persist State value with specified key', async () => { statePersistent.ready = true; const response = await statePersistent.persistValue('coolKey'); expect(response).toBeTruthy(); - expect( - dummyState.addSideEffect - ).toHaveBeenCalledWith( - StatePersistent.storeValueSideEffectKey, - expect.any(Function), - { weight: 0 } + expect(statePersistent.setupSideEffects).toHaveBeenCalledWith( + 'coolKey' ); expect(statePersistent.rebuildStorageSideEffect).toHaveBeenCalledWith( dummyState, @@ -339,24 +284,46 @@ describe('StatePersistent Tests', () => { expect(statePersistent.isPersisted).toBeTruthy(); }); - it("shouldn't persist State if Persistent isn't ready", async () => { + it("shouldn't persist State if Persistent isn't ready yet", async () => { statePersistent.ready = false; const response = await statePersistent.persistValue(); expect(response).toBeFalsy(); - expect(dummyState.addSideEffect).not.toHaveBeenCalled(); + expect(statePersistent.setupSideEffects).not.toHaveBeenCalled(); expect(statePersistent.rebuildStorageSideEffect).not.toHaveBeenCalled(); expect(statePersistent.isPersisted).toBeFalsy(); }); + }); + + describe('setupSideEffects function tests', () => { + beforeEach(() => { + jest.spyOn(dummyState, 'addSideEffect'); + }); + + it( + 'should add side effects for keeping the State value in sync ' + + 'with the Storage value to the State', + () => { + statePersistent.setupSideEffects(); - describe('test added sideEffect called StatePersistent.storeValueSideEffectKey', () => { + expect( + dummyState.addSideEffect + ).toHaveBeenCalledWith( + StatePersistent.storeValueSideEffectKey, + expect.any(Function), + { weight: 0 } + ); + } + ); + + describe("test added sideEffect called 'StatePersistent.storeValueSideEffectKey'", () => { beforeEach(() => { statePersistent.rebuildStorageSideEffect = jest.fn(); }); - it('should call rebuildStorageSideEffect', async () => { - await statePersistent.persistValue(); + it('should call rebuildStorageSideEffect (persistentKey)', async () => { + await statePersistent.setupSideEffects(); dummyState.sideEffects[ StatePersistent.storeValueSideEffectKey @@ -372,6 +339,24 @@ describe('StatePersistent Tests', () => { } ); }); + + it('should call rebuildStorageSideEffect (specified key)', async () => { + await statePersistent.setupSideEffects('dummyKey'); + + dummyState.sideEffects[ + StatePersistent.storeValueSideEffectKey + ].callback(dummyState, { + dummy: 'property', + }); + + expect(statePersistent.rebuildStorageSideEffect).toHaveBeenCalledWith( + dummyState, + 'dummyKey', + { + dummy: 'property', + } + ); + }); }); }); @@ -383,7 +368,7 @@ describe('StatePersistent Tests', () => { statePersistent.isPersisted = true; }); - it('should remove persisted State from Storage with persistentKey', async () => { + it('should remove persisted State value from the corresponding Storage with Persistent key', async () => { statePersistent.ready = true; const response = await statePersistent.removePersistedValue(); @@ -399,7 +384,7 @@ describe('StatePersistent Tests', () => { expect(statePersistent.isPersisted).toBeFalsy(); }); - it('should remove persisted State from Storage with specific Key', async () => { + it('should remove persisted State from the corresponding Storage with specified Key', async () => { statePersistent.ready = true; const response = await statePersistent.removePersistedValue('coolKey'); @@ -415,7 +400,7 @@ describe('StatePersistent Tests', () => { expect(statePersistent.isPersisted).toBeFalsy(); }); - it("shouldn't remove State from Storage if Persistent isn't ready", async () => { + it("shouldn't remove State from the corresponding Storage if Persistent isn't ready yet", async () => { statePersistent.ready = false; const response = await statePersistent.removePersistedValue('coolKey'); @@ -428,10 +413,10 @@ describe('StatePersistent Tests', () => { }); describe('formatKey function tests', () => { - it('should return key of State if no key got passed', () => { + it('should return key of the State if no valid key was specified', () => { dummyState._key = 'coolKey'; - const response = statePersistent.formatKey(); + const response = statePersistent.formatKey(undefined); expect(response).toBe('coolKey'); }); @@ -444,7 +429,7 @@ describe('StatePersistent Tests', () => { expect(response).toBe('awesomeKey'); }); - it('should return and apply passed key to State if State had no own key before', () => { + it('should return and apply specified key to State if State has no own valid key before', () => { dummyState._key = undefined; const response = statePersistent.formatKey('awesomeKey'); @@ -453,10 +438,10 @@ describe('StatePersistent Tests', () => { expect(dummyState._key).toBe('awesomeKey'); }); - it('should return undefined if no key got passed and State has no key', () => { + it('should return undefined if no valid key was specified and State has no valid key either', () => { dummyState._key = undefined; - const response = statePersistent.formatKey(); + const response = statePersistent.formatKey(undefined); expect(response).toBeUndefined(); }); diff --git a/packages/core/tests/unit/state/state.runtime.job.test.ts b/packages/core/tests/unit/state/state.runtime.job.test.ts index 24ded7f3..b1f6c04f 100644 --- a/packages/core/tests/unit/state/state.runtime.job.test.ts +++ b/packages/core/tests/unit/state/state.runtime.job.test.ts @@ -27,7 +27,7 @@ describe('RuntimeJob Tests', () => { dummyObserver = new StateObserver(dummyState); }); - it('should create RuntimeJob with Agile that has integrations (default config)', () => { + it('should create RuntimeJob with a specified Agile Instance that has a registered Integration (default config)', () => { dummyAgile.integrate(dummyIntegration); const job = new StateRuntimeJob(dummyObserver); @@ -49,7 +49,7 @@ describe('RuntimeJob Tests', () => { expect(job.subscriptionContainersToUpdate.size).toBe(0); }); - it('should create RuntimeJob with Agile that has integrations (specific config)', () => { + it('should create RuntimeJob with a specified Agile Instance that has a registered Integration (specific config)', () => { dummyAgile.integrate(dummyIntegration); const job = new StateRuntimeJob(dummyObserver, { @@ -76,7 +76,7 @@ describe('RuntimeJob Tests', () => { expect(job.subscriptionContainersToUpdate.size).toBe(0); }); - it('should create RuntimeJob with Agile that has no integrations (default config)', () => { + it('should create RuntimeJob with a specified Agile Instance that has no registered Integration (default config)', () => { const job = new StateRuntimeJob(dummyObserver); expect(job._key).toBeUndefined(); diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index 5377a12e..79ca7ca2 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -118,6 +118,74 @@ describe('Persistent Tests', () => { }); }); + describe('setKey function tests', () => { + beforeEach(() => { + persistent.removePersistedValue = jest.fn(); + persistent.persistValue = jest.fn(); + persistent.initialLoading = jest.fn(); + }); + + it('should update key with valid key in ready Persistent', async () => { + persistent.ready = true; + persistent._key = 'dummyKey'; + jest.spyOn(persistent, 'validatePersistent').mockReturnValueOnce(true); + + await persistent.setKey('newKey'); + + expect(persistent._key).toBe('newKey'); + expect(persistent.validatePersistent).toHaveBeenCalled(); + expect(persistent.initialLoading).not.toHaveBeenCalled(); + expect(persistent.persistValue).toHaveBeenCalledWith('newKey'); + expect(persistent.removePersistedValue).toHaveBeenCalledWith( + 'dummyKey' + ); + }); + + it('should update key with not valid key in ready Persistent', async () => { + persistent.ready = true; + persistent._key = 'dummyKey'; + jest.spyOn(persistent, 'validatePersistent').mockReturnValueOnce(false); + + await persistent.setKey(); + + expect(persistent._key).toBe(Persistent.placeHolderKey); + expect(persistent.validatePersistent).toHaveBeenCalled(); + expect(persistent.initialLoading).not.toHaveBeenCalled(); + expect(persistent.persistValue).not.toHaveBeenCalled(); + expect(persistent.removePersistedValue).toHaveBeenCalledWith( + 'dummyKey' + ); + }); + + it('should update key with valid key in not ready Persistent', async () => { + persistent.ready = false; + persistent._key = 'dummyKey'; + jest.spyOn(persistent, 'validatePersistent').mockReturnValueOnce(true); + + await persistent.setKey('newKey'); + + expect(persistent._key).toBe('newKey'); + expect(persistent.validatePersistent).toHaveBeenCalled(); + expect(persistent.initialLoading).toHaveBeenCalled(); + expect(persistent.persistValue).not.toHaveBeenCalled(); + expect(persistent.removePersistedValue).not.toHaveBeenCalled(); + }); + + it('should update key with not valid key in not ready Persistent', async () => { + persistent.ready = false; + persistent._key = 'dummyKey'; + jest.spyOn(persistent, 'validatePersistent').mockReturnValueOnce(false); + + await persistent.setKey(); + + expect(persistent._key).toBe(Persistent.placeHolderKey); + expect(persistent.validatePersistent).toHaveBeenCalled(); + expect(persistent.initialLoading).not.toHaveBeenCalled(); + expect(persistent.persistValue).not.toHaveBeenCalled(); + expect(persistent.removePersistedValue).not.toHaveBeenCalled(); + }); + }); + describe('instantiatePersistent function tests', () => { it('should call assign key to formatKey and call assignStorageKeys, validatePersistent', () => { jest.spyOn(persistent, 'formatKey'); From 97e03ce4b2e6c92a9a7821cc3a4c8402135a1e0f Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 12 Jun 2021 19:00:21 +0200 Subject: [PATCH 54/63] fixed typos --- .../src/collection/collection.persistent.ts | 12 +- packages/core/src/runtime/index.ts | 4 +- packages/core/src/runtime/runtime.job.ts | 2 +- packages/core/src/state/state.observer.ts | 15 +- packages/core/src/state/state.persistent.ts | 33 +- packages/core/src/state/state.runtime.job.ts | 15 +- .../collection/collection.persistent.test.ts | 235 +++++----- .../tests/unit/runtime/runtime.job.test.ts | 202 +++++---- .../core/tests/unit/runtime/runtime.test.ts | 18 +- .../tests/unit/state/state.observer.test.ts | 412 ++++++++++-------- .../tests/unit/state/state.persistent.test.ts | 14 +- .../unit/state/state.runtime.job.test.ts | 184 ++++---- 12 files changed, 645 insertions(+), 501 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index ee835da6..6454ea63 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -74,7 +74,7 @@ export class CollectionPersistent< * @internal * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. * | default = Persistent.key | - * @return Whether the loading was successful. + * @return Whether the loading and the setting up of the side effects was successful. */ public async loadPersistedValue( storageItemKey?: PersistentKey @@ -165,7 +165,7 @@ export class CollectionPersistent< }; const success = await loadValuesIntoCollection(); - // Setup Side Effects to keep the Storage value in sync + // Setup side effects to keep the Storage value in sync // with the Collection (Instances) value if (success) this.setupSideEffects(_storageItemKey); @@ -216,7 +216,7 @@ export class CollectionPersistent< }); } - // Setup Side Effects to keep the Storage value in sync + // Setup side effects to keep the Storage value in sync // with the Collection (Instances) value this.setupSideEffects(_storageItemKey); @@ -253,7 +253,7 @@ export class CollectionPersistent< * @internal * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. * | default = Persistent.key | - * @return Whether the removal of the persisted value was successful. + * @return Whether the removal of the persisted values was successful. */ public async removePersistedValue( storageItemKey?: PersistentKey @@ -291,7 +291,9 @@ export class CollectionPersistent< } /** - * Formats specified key so that it can be used as a valid Storage key and returns it. + * Formats the specified key so that it can be used as a valid Storage key + * and returns the formatted variant of it. + * * If no formatable key (undefined/null) was provided, * an attempt is made to use the Collection identifier key. * diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index ec0b841d..9a93ce62 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -186,9 +186,9 @@ export class Runtime { if (!subscriptionContainer.ready) { if ( !job.config.maxTriesToUpdate || - job.triedToUpdateCount < job.config.maxTriesToUpdate + job.timesTriedToUpdateCount < job.config.maxTriesToUpdate ) { - job.triedToUpdateCount++; + job.timesTriedToUpdateCount++; this.notReadyJobsToRerender.add(job); LogCodeManager.log( '16:02:00', diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts index c61b7c02..d3eea3f2 100644 --- a/packages/core/src/runtime/runtime.job.ts +++ b/packages/core/src/runtime/runtime.job.ts @@ -12,7 +12,7 @@ export class RuntimeJob { // Subscription Containers (UI-Components) of the Observer that have to be updated (re-rendered) public subscriptionContainersToUpdate = new Set(); // How often not ready Subscription Containers of the Observer have been tried to update - public triedToUpdateCount = 0; + public timesTriedToUpdateCount = 0; // Whether the Job has been performed by the runtime public performed = false; diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index 7b058fb7..b5a4ee2e 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -91,19 +91,19 @@ export class StateObserver extends Observer { overwrite: false, }); - // Force overwriting the State value if it was a placeholder. - // Because after assigning a value to the State it shouldn't be a placeholder anymore. + // Force overwriting the State value if it is a placeholder. + // After assigning a value to the State it shouldn't be a placeholder anymore. if (state.isPlaceholder) { config.force = true; config.overwrite = true; } - // Assign next State value and compute it if necessary + // Assign next State value to Observer and compute it if necessary this.nextStateValue = state.computeValueMethod ? copy(state.computeValueMethod(newStateValue)) : copy(newStateValue); - // Check if current State value and to assign State value are equals + // Check if current State value and to assign State value are equal if (equal(state._value, this.nextStateValue) && !config.force) return; // Create Runtime-Job @@ -128,8 +128,8 @@ export class StateObserver extends Observer { * Method executed by the Runtime to perform the Runtime-Job, * previously ingested via the `ingest()` or `ingestValue()` method. * - * Thereby the previously defined `nextStateValue` is assigned to the State - * and the side effects (`sideEffects`) are executed. + * Thereby the previously defined `nextStateValue` is assigned to the State. + * Also side effects (like calling watcher callbacks) of a State change are executed. * * @internal * @param job - Runtime-Job to be performed. @@ -142,6 +142,7 @@ export class StateObserver extends Observer { state._value = copy(job.observer.nextStateValue); state.nextStateValue = copy(job.observer.nextStateValue); + // TODO think about freezing the State value.. // https://www.geeksforgeeks.org/object-freeze-javascript/#:~:text=Object.freeze()%20Method&text=freeze()%20which%20is%20used,the%20prototype%20of%20the%20object. // if (typeof state._value === 'object') Object.freeze(state._value); @@ -168,7 +169,7 @@ export class StateObserver extends Observer { /** * Performs the side effects of applying the next State value to the State. * - * Side effects are, for example, calling the watcher functions + * Side effects are, for example, calling the watcher callbacks * or executing the side effects defined in the State Class * like 'rebuildGroup' or 'rebuildStateStorageValue'. * diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index ef8f1b16..d1a8243f 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -7,9 +7,11 @@ import { } from '../internal'; export class StatePersistent extends Persistent { - static storeValueSideEffectKey = 'rebuildStateStorageValue'; + // State the Persistent belongs to public state: () => State; + static storeValueSideEffectKey = 'rebuildStateStorageValue'; + /** * Internal Class for managing the permanent persistence of a State. * @@ -61,7 +63,7 @@ export class StatePersistent extends Persistent { * @internal * @param storageItemKey - Storage key of the persisted State Instance. * | default = Persistent.key | - * @return Whether the loading was successful. + * @return Whether the loading and the setting up of the side effects was successful. */ public async loadPersistedValue( storageItemKey?: PersistentKey @@ -69,7 +71,7 @@ export class StatePersistent extends Persistent { if (!this.ready) return false; const _storageItemKey = storageItemKey ?? this._key; - // Load value from default Storage + // Load State value from the default Storage const loadedValue = await this.agileInstance().storages.get( _storageItemKey, this.config.defaultStorageKey as any @@ -82,8 +84,8 @@ export class StatePersistent extends Persistent { overwrite: true, }); - // Setup Side Effects to keep the Storage value in sync - // with the State value + // Setup side effects to keep the Storage value in sync + // with the current State value this.setupSideEffects(_storageItemKey); return true; @@ -103,7 +105,7 @@ export class StatePersistent extends Persistent { if (!this.ready) return false; const _storageItemKey = storageItemKey ?? this._key; - // Setup Side Effects to keep the Storage value in sync + // Setup side effects to keep the Storage value in sync // with the State value this.setupSideEffects(_storageItemKey); @@ -116,7 +118,7 @@ export class StatePersistent extends Persistent { /** * Sets up side effects to keep the Storage value in sync - * with the State value. + * with the current State value. * * @internal * @param storageItemKey - Storage key of the persisted State Instance. @@ -157,7 +159,9 @@ export class StatePersistent extends Persistent { } /** - * Formats specified key so that it can be used as a valid Storage key and returns it. + * Formats the specified key so that it can be used as a valid Storage key + * and returns the formatted variant of it. + * * If no formatable key (undefined/null) was provided, * an attempt is made to use the State identifier key. * @@ -167,19 +171,18 @@ export class StatePersistent extends Persistent { public formatKey( key: PersistentKey | undefined | null ): PersistentKey | undefined { - const state = this.state(); - if (!key && state._key) return state._key; - if (!key) return; - if (!state._key) state._key = key; + if (key == null && this.state()._key) return this.state()._key; + if (key == null) return; + if (this.state()._key == null) this.state()._key = key; return key; } /** - * Rebuilds Storage value based on the current State value + * Rebuilds Storage value based on the current State value. * * @internal - * @param state - State whose value to be in sync with the Storage value. - * @param storageItemKey - Storage key of the persisted State. + * @param state - State whose current value to be applied to the Storage value. + * @param storageItemKey - Storage key of the persisted State Instance. * | default = Persistent.key | * @param config - Configuration object */ diff --git a/packages/core/src/state/state.runtime.job.ts b/packages/core/src/state/state.runtime.job.ts index af57a67d..f42af5c3 100644 --- a/packages/core/src/state/state.runtime.job.ts +++ b/packages/core/src/state/state.runtime.job.ts @@ -9,6 +9,17 @@ import { export class StateRuntimeJob extends RuntimeJob { public config: StateRuntimeJobConfigInterface; + /** + * A State Runtime Job is sent to the Runtime on behalf of the State Observer it represents. + * + * In the Runtime, the State Observer is performed via its `perform()` method + * and the Subscription Containers (UI-Components) + * to which it is subscribed are updated (re-rendered) accordingly. + * + * @internal + * @param observer - State Observer to be represented by the State Runtime Job. + * @param config - Configuration object + */ constructor( observer: StateObserver, config: CreateStateRuntimeJobConfigInterface = {} @@ -38,7 +49,7 @@ export class StateRuntimeJob extends RuntimeJob { export interface CreateStateRuntimeJobConfigInterface extends StateRuntimeJobConfigInterface { /** - * Key/Name identifier of the Runtime Job. + * Key/Name identifier of the State Runtime Job. * @default undefined */ key?: RuntimeJobKey; @@ -53,7 +64,7 @@ export interface StateRuntimeJobConfigInterface overwrite?: boolean; /** * If the State is persisted, - * whether to store the new State value in an external Storage + * whether to apply the new State value to the external Storages. * @default true */ storage?: boolean; diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index 731da595..764b482c 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -305,119 +305,130 @@ describe('CollectionPersistent Tests', () => { ); }); - it("should load default Group " + - "and create/add persisted Items that aren't present in the Collection yet (persistentKey)", async () => { - collectionPersistent.ready = true; - dummyCollection.data = {}; - dummyAgile.storages.get = jest - .fn() - .mockReturnValueOnce(Promise.resolve(true)); - placeholderItem1.persist = jest.fn(function () { - placeholderItem1.persistent = new StatePersistent(placeholderItem1); - placeholderItem1.persistent.ready = true; - placeholderItem1.persistent.loadPersistedValue = jest + it( + 'should load default Group ' + + "and create/add persisted Items that aren't present in the Collection yet (persistentKey)", + async () => { + collectionPersistent.ready = true; + dummyCollection.data = {}; + dummyAgile.storages.get = jest .fn() - .mockReturnValueOnce(true); - return null as any; - }); - placeholderItem2.persist = jest.fn(function () { - placeholderItem2.persistent = new StatePersistent(placeholderItem2); - placeholderItem2.persistent.ready = false; - placeholderItem2.persistent.loadPersistedValue = jest.fn(); - return null as any; - }); - placeholderItem3.persist = jest.fn(function () { - placeholderItem3.persistent = new StatePersistent(placeholderItem3); - placeholderItem3.persistent.ready = true; - placeholderItem3.persistent.loadPersistedValue = jest + .mockReturnValueOnce(Promise.resolve(true)); + placeholderItem1.persist = jest.fn(function () { + placeholderItem1.persistent = new StatePersistent(placeholderItem1); + placeholderItem1.persistent.ready = true; + placeholderItem1.persistent.loadPersistedValue = jest + .fn() + .mockReturnValueOnce(true); + return null as any; + }); + placeholderItem2.persist = jest.fn(function () { + placeholderItem2.persistent = new StatePersistent(placeholderItem2); + placeholderItem2.persistent.ready = false; + placeholderItem2.persistent.loadPersistedValue = jest.fn(); + return null as any; + }); + placeholderItem3.persist = jest.fn(function () { + placeholderItem3.persistent = new StatePersistent(placeholderItem3); + placeholderItem3.persistent.ready = true; + placeholderItem3.persistent.loadPersistedValue = jest + .fn() + .mockReturnValueOnce(false); + return null as any; + }); + dummyCollection.createPlaceholderItem = jest .fn() - .mockReturnValueOnce(false); - return null as any; - }); - dummyCollection.createPlaceholderItem = jest - .fn() - .mockReturnValueOnce(placeholderItem1) - .mockReturnValueOnce(placeholderItem2) - .mockReturnValueOnce(placeholderItem3); - dummyDefaultGroup._value = ['1', '2', '3']; + .mockReturnValueOnce(placeholderItem1) + .mockReturnValueOnce(placeholderItem2) + .mockReturnValueOnce(placeholderItem3); + dummyDefaultGroup._value = ['1', '2', '3']; - const response = await collectionPersistent.loadPersistedValue(); + const response = await collectionPersistent.loadPersistedValue(); - expect(response).toBeTruthy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( - collectionPersistent._key, - collectionPersistent.config.defaultStorageKey - ); + expect(response).toBeTruthy(); + expect(dummyAgile.storages.get).toHaveBeenCalledWith( + collectionPersistent._key, + collectionPersistent.config.defaultStorageKey + ); - expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); - expect(dummyDefaultGroup.persist).toHaveBeenCalledWith( - CollectionPersistent.getGroupStorageKey( - dummyDefaultGroup._key, - collectionPersistent._key - ), - { - loadValue: false, - defaultStorageKey: collectionPersistent.config.defaultStorageKey, - storageKeys: collectionPersistent.storageKeys, - followCollectionPersistKeyPattern: false, - } - ); - expect(dummyDefaultGroup.persistent?.initialLoading).toHaveBeenCalled(); - expect(dummyDefaultGroup.isPersisted).toBeTruthy(); + expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); + expect(dummyDefaultGroup.persist).toHaveBeenCalledWith( + CollectionPersistent.getGroupStorageKey( + dummyDefaultGroup._key, + collectionPersistent._key + ), + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, + } + ); + expect( + dummyDefaultGroup.persistent?.initialLoading + ).toHaveBeenCalled(); + expect(dummyDefaultGroup.isPersisted).toBeTruthy(); - expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith('1'); - expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith('2'); - expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith('3'); - expect(placeholderItem1.persist).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey( - '1', - collectionPersistent._key - ), - { - loadValue: false, - defaultStorageKey: collectionPersistent.config.defaultStorageKey, - storageKeys: collectionPersistent.storageKeys, - followCollectionPersistKeyPattern: false, - } - ); - expect(placeholderItem2.persist).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey( - '2', - collectionPersistent._key - ), - { - loadValue: false, - defaultStorageKey: collectionPersistent.config.defaultStorageKey, - storageKeys: collectionPersistent.storageKeys, - followCollectionPersistKeyPattern: false, - } - ); - expect(placeholderItem3.persist).toHaveBeenCalledWith( - CollectionPersistent.getItemStorageKey( - '3', - collectionPersistent._key - ), - { - loadValue: false, - defaultStorageKey: collectionPersistent.config.defaultStorageKey, - storageKeys: collectionPersistent.storageKeys, - followCollectionPersistKeyPattern: false, - } - ); - expect(dummyCollection.assignItem).toHaveBeenCalledWith( - placeholderItem1 - ); - expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( - placeholderItem2 - ); // Because Item persistent isn't ready - expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( - placeholderItem3 - ); // Because Item persistent 'leadPersistedValue()' returned false -> Item properly doesn't exist in Storage + expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith( + '1' + ); + expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith( + '2' + ); + expect(dummyCollection.createPlaceholderItem).toHaveBeenCalledWith( + '3' + ); + expect(placeholderItem1.persist).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey( + '1', + collectionPersistent._key + ), + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, + } + ); + expect(placeholderItem2.persist).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey( + '2', + collectionPersistent._key + ), + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, + } + ); + expect(placeholderItem3.persist).toHaveBeenCalledWith( + CollectionPersistent.getItemStorageKey( + '3', + collectionPersistent._key + ), + { + loadValue: false, + defaultStorageKey: collectionPersistent.config.defaultStorageKey, + storageKeys: collectionPersistent.storageKeys, + followCollectionPersistKeyPattern: false, + } + ); + expect(dummyCollection.assignItem).toHaveBeenCalledWith( + placeholderItem1 + ); + expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( + placeholderItem2 + ); // Because Item persistent isn't ready + expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( + placeholderItem3 + ); // Because Item persistent 'leadPersistedValue()' returned false -> Item properly doesn't exist in Storage - expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( - collectionPersistent._key - ); - }); + expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( + collectionPersistent._key + ); + } + ); it( 'should load default Group, ' + @@ -987,15 +998,15 @@ describe('CollectionPersistent Tests', () => { }); describe('formatKey function tests', () => { - it('should return key of Collection if no valid key got provided', () => { + it('should return key of the Collection if no valid key was specified', () => { dummyCollection._key = 'coolKey'; - const response = collectionPersistent.formatKey(null); + const response = collectionPersistent.formatKey(undefined); expect(response).toBe('coolKey'); }); - it('should return provided key if key is valid', () => { + it('should return specified key', () => { dummyCollection._key = 'coolKey'; const response = collectionPersistent.formatKey('awesomeKey'); @@ -1003,7 +1014,7 @@ describe('CollectionPersistent Tests', () => { expect(response).toBe('awesomeKey'); }); - it('should return and apply valid provided key to Collection if Collection has no own key', () => { + it('should return and apply specified key to Collection if Collection had no own valid key before', () => { dummyCollection._key = undefined; const response = collectionPersistent.formatKey('awesomeKey'); @@ -1012,10 +1023,10 @@ describe('CollectionPersistent Tests', () => { expect(dummyCollection._key).toBe('awesomeKey'); }); - it('should return undefined if no valid key got provided and Collection has no key', () => { + it('should return undefined if no valid key was specified and Collection has no valid key either', () => { dummyCollection._key = undefined; - const response = collectionPersistent.formatKey(null); + const response = collectionPersistent.formatKey(undefined); expect(response).toBeUndefined(); }); diff --git a/packages/core/tests/unit/runtime/runtime.job.test.ts b/packages/core/tests/unit/runtime/runtime.job.test.ts index e8989d14..4793df21 100644 --- a/packages/core/tests/unit/runtime/runtime.job.test.ts +++ b/packages/core/tests/unit/runtime/runtime.job.test.ts @@ -17,96 +17,120 @@ describe('RuntimeJob Tests', () => { dummyObserver = new Observer(dummyAgile); }); - it('should create RuntimeJob with a specified Agile Instance that has a registered Integration (default config)', () => { - dummyAgile.integrate(dummyIntegration); - - const job = new RuntimeJob(dummyObserver); - - expect(job._key).toBeUndefined(); - expect(job.observer).toBe(dummyObserver); - expect(job.config).toStrictEqual({ - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, - force: false, - maxTriesToUpdate: 3, - }); - expect(job.rerender).toBeTruthy(); - expect(job.performed).toBeFalsy(); - expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); - expect(job.triedToUpdateCount).toBe(0); - }); - - it('should create RuntimeJob with a specified Agile Instance that has a registered Integration (specific config)', () => { - dummyAgile.integrate(dummyIntegration); - - const job = new RuntimeJob(dummyObserver, { - key: 'dummyJob', - sideEffects: { - enabled: false, - exclude: ['jeff'], - }, - force: true, - maxTriesToUpdate: 10, - }); - - expect(job._key).toBe('dummyJob'); - expect(job.observer).toBe(dummyObserver); - expect(job.config).toStrictEqual({ - background: false, - sideEffects: { - enabled: false, - exclude: ['jeff'], - }, - force: true, - maxTriesToUpdate: 10, - }); - expect(job.rerender).toBeTruthy(); - expect(job.performed).toBeFalsy(); - expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); - }); - - it('should create RuntimeJob with a specified Agile Instance that has no registered Integration (default config)', () => { - const job = new RuntimeJob(dummyObserver); - - expect(job._key).toBeUndefined(); - expect(job.observer).toBe(dummyObserver); - expect(job.config).toStrictEqual({ - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, - force: false, - maxTriesToUpdate: 3, - }); - expect(job.rerender).toBeFalsy(); - expect(job.performed).toBeFalsy(); - expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); - }); + it( + 'should create RuntimeJob ' + + 'with a specified Agile Instance that has a registered Integration (default config)', + () => { + dummyAgile.integrate(dummyIntegration); + + const job = new RuntimeJob(dummyObserver); + + expect(job._key).toBeUndefined(); + expect(job.observer).toBe(dummyObserver); + expect(job.config).toStrictEqual({ + background: false, + sideEffects: { + enabled: true, + exclude: [], + }, + force: false, + maxTriesToUpdate: 3, + }); + expect(job.rerender).toBeTruthy(); + expect(job.performed).toBeFalsy(); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); + expect(job.timesTriedToUpdateCount).toBe(0); + expect(job.timesTriedToUpdateCount).toBe(0); + expect(job.performed).toBeFalsy(); + } + ); + + it( + 'should create RuntimeJob ' + + 'with a specified Agile Instance that has a registered Integration (specific config)', + () => { + dummyAgile.integrate(dummyIntegration); + + const job = new RuntimeJob(dummyObserver, { + key: 'dummyJob', + sideEffects: { + enabled: false, + exclude: ['jeff'], + }, + force: true, + maxTriesToUpdate: 10, + }); - it('should create RuntimeJob with a specified Agile Instance that has a registered Integrations (config.background = true)', () => { - dummyAgile.integrate(dummyIntegration); - - const job = new RuntimeJob(dummyObserver, { background: true }); - - expect(job._key).toBeUndefined(); - expect(job.observer).toBe(dummyObserver); - expect(job.config).toStrictEqual({ - background: true, - sideEffects: { - enabled: true, - exclude: [], - }, - force: false, - maxTriesToUpdate: 3, - }); - expect(job.rerender).toBeFalsy(); - expect(job.performed).toBeFalsy(); - expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); - }); + expect(job._key).toBe('dummyJob'); + expect(job.observer).toBe(dummyObserver); + expect(job.config).toStrictEqual({ + background: false, + sideEffects: { + enabled: false, + exclude: ['jeff'], + }, + force: true, + maxTriesToUpdate: 10, + }); + expect(job.rerender).toBeTruthy(); + expect(job.performed).toBeFalsy(); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); + expect(job.timesTriedToUpdateCount).toBe(0); + expect(job.performed).toBeFalsy(); + } + ); + + it( + 'should create RuntimeJob ' + + 'with a specified Agile Instance that has no registered Integration (default config)', + () => { + const job = new RuntimeJob(dummyObserver); + + expect(job._key).toBeUndefined(); + expect(job.observer).toBe(dummyObserver); + expect(job.config).toStrictEqual({ + background: false, + sideEffects: { + enabled: true, + exclude: [], + }, + force: false, + maxTriesToUpdate: 3, + }); + expect(job.rerender).toBeFalsy(); + expect(job.performed).toBeFalsy(); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); + expect(job.timesTriedToUpdateCount).toBe(0); + expect(job.performed).toBeFalsy(); + } + ); + + it( + 'should create RuntimeJob ' + + 'with a specified Agile Instance that has a registered Integrations (config.background = true)', + () => { + dummyAgile.integrate(dummyIntegration); + + const job = new RuntimeJob(dummyObserver, { background: true }); + + expect(job._key).toBeUndefined(); + expect(job.observer).toBe(dummyObserver); + expect(job.config).toStrictEqual({ + background: true, + sideEffects: { + enabled: true, + exclude: [], + }, + force: false, + maxTriesToUpdate: 3, + }); + expect(job.rerender).toBeFalsy(); + expect(job.performed).toBeFalsy(); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); + expect(job.timesTriedToUpdateCount).toBe(0); + expect(job.performed).toBeFalsy(); + } + ); describe('RuntimeJob Function Tests', () => { let job: RuntimeJob; diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index b0814322..a3609ef8 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -351,7 +351,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob1.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob1.triedToUpdateCount).toBe(0); + expect(dummyJob1.timesTriedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer1.updatedSubscribers) ).toStrictEqual([dummyObserver1]); @@ -360,7 +360,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob2.subscriptionContainersToUpdate) ).toStrictEqual([dummySubscriptionContainer2]); - expect(dummyJob2.triedToUpdateCount).toBe(1); + expect(dummyJob2.timesTriedToUpdateCount).toBe(1); expect( Array.from(dummySubscriptionContainer2.updatedSubscribers) ).toStrictEqual([]); @@ -387,7 +387,7 @@ describe('Runtime Tests', () => { dummySubscriptionContainer1.ready = true; dummySubscriptionContainer2.ready = false; const numberOfTries = (dummyJob2.config.maxTriesToUpdate ?? 0) + 1; - dummyJob2.triedToUpdateCount = numberOfTries; + dummyJob2.timesTriedToUpdateCount = numberOfTries; const response = runtime.extractToUpdateSubscriptionContainer([ dummyJob1, @@ -409,7 +409,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob1.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob1.triedToUpdateCount).toBe(0); + expect(dummyJob1.timesTriedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer1.updatedSubscribers) ).toStrictEqual([dummyObserver1]); @@ -418,7 +418,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob2.subscriptionContainersToUpdate) ).toStrictEqual([dummySubscriptionContainer2]); - expect(dummyJob2.triedToUpdateCount).toBe(numberOfTries); + expect(dummyJob2.timesTriedToUpdateCount).toBe(numberOfTries); expect( Array.from(dummySubscriptionContainer2.updatedSubscribers) ).toStrictEqual([]); @@ -466,7 +466,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob1.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob1.triedToUpdateCount).toBe(0); + expect(dummyJob1.timesTriedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer1.updatedSubscribers) ).toStrictEqual([]); @@ -475,7 +475,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob2.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob2.triedToUpdateCount).toBe(0); + expect(dummyJob2.timesTriedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer2.updatedSubscribers) ).toStrictEqual([dummyObserver2]); @@ -517,7 +517,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob1.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob1.triedToUpdateCount).toBe(0); + expect(dummyJob1.timesTriedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer1.updatedSubscribers) ).toStrictEqual([dummyObserver1]); @@ -526,7 +526,7 @@ describe('Runtime Tests', () => { expect( Array.from(dummyJob2.subscriptionContainersToUpdate) ).toStrictEqual([]); - expect(dummyJob2.triedToUpdateCount).toBe(0); + expect(dummyJob2.timesTriedToUpdateCount).toBe(0); expect( Array.from(dummySubscriptionContainer2.updatedSubscribers) ).toStrictEqual([dummyObserver2]); diff --git a/packages/core/tests/unit/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index c9d837e8..a8a102d3 100644 --- a/packages/core/tests/unit/state/state.observer.test.ts +++ b/packages/core/tests/unit/state/state.observer.test.ts @@ -23,7 +23,7 @@ describe('StateObserver Tests', () => { dummyState = new State(dummyAgile, 'dummyValue', { key: 'dummyState' }); }); - it('should create StateObserver (default config)', () => { + it('should create State Observer (default config)', () => { const stateObserver = new StateObserver(dummyState); expect(stateObserver).toBeInstanceOf(StateObserver); @@ -32,11 +32,11 @@ describe('StateObserver Tests', () => { expect(stateObserver.value).toBe('dummyValue'); expect(stateObserver.previousValue).toBe('dummyValue'); expect(stateObserver._key).toBeUndefined(); - expect(stateObserver.dependents.size).toBe(0); - expect(stateObserver.subscribedTo.size).toBe(0); + expect(Array.from(stateObserver.dependents)).toStrictEqual([]); + expect(Array.from(stateObserver.subscribedTo)).toStrictEqual([]); }); - it('should create StateObserver (specific config)', () => { + it('should create State Observer (specific config)', () => { const dummyObserver1 = new Observer(dummyAgile, { key: 'dummyObserver1' }); const dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); const dummySubscription1 = new SubscriptionContainer([]); @@ -54,15 +54,17 @@ describe('StateObserver Tests', () => { expect(stateObserver.value).toBe('dummyValue'); expect(stateObserver.previousValue).toBe('dummyValue'); expect(stateObserver._key).toBe('testKey'); - expect(stateObserver.dependents.size).toBe(2); - expect(stateObserver.dependents.has(dummyObserver2)).toBeTruthy(); - expect(stateObserver.dependents.has(dummyObserver1)).toBeTruthy(); - expect(stateObserver.subscribedTo.size).toBe(2); - expect(stateObserver.subscribedTo.has(dummySubscription1)).toBeTruthy(); - expect(stateObserver.subscribedTo.has(dummySubscription2)).toBeTruthy(); + expect(Array.from(stateObserver.dependents)).toStrictEqual([ + dummyObserver1, + dummyObserver2, + ]); + expect(Array.from(stateObserver.subscribedTo)).toStrictEqual([ + dummySubscription1, + dummySubscription2, + ]); }); - describe('StateObserver Function Tests', () => { + describe('State Observer Function Tests', () => { let stateObserver: StateObserver; beforeEach(() => { @@ -101,7 +103,7 @@ describe('StateObserver Tests', () => { expect(stateObserver.ingestValue).toHaveBeenCalledWith('nextValue', {}); }); - it('should call ingestValue with nextStateValue (specific config)', () => { + it("should call 'ingestValue' with the 'nextStateValue' (specific config)", () => { dummyState.nextStateValue = 'nextValue'; stateObserver.ingest({ @@ -127,17 +129,21 @@ describe('StateObserver Tests', () => { }); }); - it('should call ingestValue with computedValue if Observer belongs to a ComputedState (default config)', () => { - dummyComputed.compute = jest.fn(() => 'computedValue'); - - computedObserver.ingest(); - - expect(computedObserver.ingestValue).toHaveBeenCalledWith( - 'computedValue', - {} - ); - expect(dummyComputed.compute).toHaveBeenCalled(); - }); + it( + "should call 'ingestValue' with computed value " + + 'if Observer belongs to a Computed State (default config)', + () => { + dummyComputed.compute = jest.fn(() => 'computedValue'); + + computedObserver.ingest(); + + expect(computedObserver.ingestValue).toHaveBeenCalledWith( + 'computedValue', + {} + ); + expect(dummyComputed.compute).toHaveBeenCalled(); + } + ); }); describe('ingestValue function tests', () => { @@ -145,108 +151,124 @@ describe('StateObserver Tests', () => { dummyAgile.runtime.ingest = jest.fn(); }); - it("should ingest State into Runtime if newValue isn't equal to currentValue (default config)", () => { - jest.spyOn(Utils, 'generateId').mockReturnValue('randomKey'); - - dummyAgile.runtime.ingest = jest.fn((job: StateRuntimeJob) => { - expect(job._key).toBe(`${stateObserver._key}_randomKey`); - expect(job.observer).toBe(stateObserver); - expect(job.config).toStrictEqual({ - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, - force: false, - storage: true, - overwrite: false, + it( + 'should ingest the State into the Runtime ' + + "if the new value isn't equal to the current value (default config)", + () => { + jest.spyOn(Utils, 'generateId').mockReturnValue('randomKey'); + + dummyAgile.runtime.ingest = jest.fn((job: StateRuntimeJob) => { + expect(job._key).toBe(`${stateObserver._key}_randomKey`); + expect(job.observer).toBe(stateObserver); + expect(job.config).toStrictEqual({ + background: false, + sideEffects: { + enabled: true, + exclude: [], + }, + force: false, + storage: true, + overwrite: false, + }); }); - }); - - stateObserver.ingestValue('updatedDummyValue'); - expect(stateObserver.nextStateValue).toBe('updatedDummyValue'); - expect(dummyAgile.runtime.ingest).toHaveBeenCalledWith( - expect.any(StateRuntimeJob), - { - perform: true, - } - ); - }); + stateObserver.ingestValue('updatedDummyValue'); + + expect(stateObserver.nextStateValue).toBe('updatedDummyValue'); + expect(dummyAgile.runtime.ingest).toHaveBeenCalledWith( + expect.any(StateRuntimeJob), + { + perform: true, + } + ); + } + ); + + it( + 'should ingest the State into the Runtime ' + + "if the new value isn't equal to the current value (specific config)", + () => { + dummyAgile.runtime.ingest = jest.fn((job: StateRuntimeJob) => { + expect(job._key).toBe('dummyJob'); + expect(job.observer).toBe(stateObserver); + expect(job.config).toStrictEqual({ + background: false, + sideEffects: { + enabled: false, + }, + force: true, + storage: true, + overwrite: true, + }); + }); - it("should ingest State into Runtime if newValue isn't equal to currentValue (specific config)", () => { - dummyAgile.runtime.ingest = jest.fn((job: StateRuntimeJob) => { - expect(job._key).toBe('dummyJob'); - expect(job.observer).toBe(stateObserver); - expect(job.config).toStrictEqual({ - background: false, + stateObserver.ingestValue('updatedDummyValue', { + perform: false, + force: true, sideEffects: { enabled: false, }, - force: true, - storage: true, overwrite: true, + key: 'dummyJob', }); - }); - - stateObserver.ingestValue('updatedDummyValue', { - perform: false, - force: true, - sideEffects: { - enabled: false, - }, - overwrite: true, - key: 'dummyJob', - }); - - expect(stateObserver.nextStateValue).toBe('updatedDummyValue'); - expect(dummyAgile.runtime.ingest).toHaveBeenCalledWith( - expect.any(StateRuntimeJob), - { - perform: false, - } - ); - }); - - it("shouldn't ingest State into Runtime if newValue is equal to currentValue (default config)", () => { - dummyState._value = 'updatedDummyValue'; - - stateObserver.ingestValue('updatedDummyValue'); - - expect(stateObserver.nextStateValue).toBe('updatedDummyValue'); - expect(dummyAgile.runtime.ingest).not.toHaveBeenCalled(); - }); - it('should ingest State into Runtime if newValue is equal to currentValue (config.force = true)', () => { - jest.spyOn(Utils, 'generateId').mockReturnValue('randomKey'); - dummyState._value = 'updatedDummyValue'; - dummyAgile.runtime.ingest = jest.fn((job: StateRuntimeJob) => { - expect(job._key).toBe(`${stateObserver._key}_randomKey`); - expect(job.observer).toBe(stateObserver); - expect(job.config).toStrictEqual({ - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, - force: true, - storage: true, - overwrite: false, + expect(stateObserver.nextStateValue).toBe('updatedDummyValue'); + expect(dummyAgile.runtime.ingest).toHaveBeenCalledWith( + expect.any(StateRuntimeJob), + { + perform: false, + } + ); + } + ); + + it( + "shouldn't ingest the State into the Runtime " + + 'if the new value is equal to the current value (default config)', + () => { + dummyState._value = 'updatedDummyValue'; + + stateObserver.ingestValue('updatedDummyValue'); + + expect(stateObserver.nextStateValue).toBe('updatedDummyValue'); + expect(dummyAgile.runtime.ingest).not.toHaveBeenCalled(); + } + ); + + it( + 'should ingest the State into the Runtime ' + + 'if the new value is equal to the current value (config.force = true)', + () => { + jest.spyOn(Utils, 'generateId').mockReturnValue('randomKey'); + dummyState._value = 'updatedDummyValue'; + dummyAgile.runtime.ingest = jest.fn((job: StateRuntimeJob) => { + expect(job._key).toBe(`${stateObserver._key}_randomKey`); + expect(job.observer).toBe(stateObserver); + expect(job.config).toStrictEqual({ + background: false, + sideEffects: { + enabled: true, + exclude: [], + }, + force: true, + storage: true, + overwrite: false, + }); }); - }); - stateObserver.ingestValue('updatedDummyValue', { force: true }); + stateObserver.ingestValue('updatedDummyValue', { force: true }); - expect(stateObserver.nextStateValue).toBe('updatedDummyValue'); - expect(dummyAgile.runtime.ingest).toHaveBeenCalledWith( - expect.any(StateRuntimeJob), - { - perform: true, - } - ); - }); + expect(stateObserver.nextStateValue).toBe('updatedDummyValue'); + expect(dummyAgile.runtime.ingest).toHaveBeenCalledWith( + expect.any(StateRuntimeJob), + { + perform: true, + } + ); + } + ); - it('should ingest placeholder State into Runtime (default config)', () => { + it('should ingest placeholder State into the Runtime (default config)', () => { jest.spyOn(Utils, 'generateId').mockReturnValue('randomKey'); dummyAgile.runtime.ingest = jest.fn((job: StateRuntimeJob) => { expect(job._key).toBe(`${stateObserver._key}_randomKey`); @@ -275,21 +297,25 @@ describe('StateObserver Tests', () => { ); }); - it('should ingest State into Runtime and compute newStateValue if State compute Function is set (default config)', () => { - dummyState.computeValueMethod = (value) => `cool value '${value}'`; - - stateObserver.ingestValue('updatedDummyValue'); - - expect(stateObserver.nextStateValue).toBe( - "cool value 'updatedDummyValue'" - ); - expect(dummyAgile.runtime.ingest).toHaveBeenCalledWith( - expect.any(StateRuntimeJob), - { - perform: true, - } - ); - }); + it( + 'should ingest the State into the Runtime and compute the new value ' + + 'if the State compute function is set (default config)', + () => { + dummyState.computeValueMethod = (value) => `cool value '${value}'`; + + stateObserver.ingestValue('updatedDummyValue'); + + expect(stateObserver.nextStateValue).toBe( + "cool value 'updatedDummyValue'" + ); + expect(dummyAgile.runtime.ingest).toHaveBeenCalledWith( + expect.any(StateRuntimeJob), + { + perform: true, + } + ); + } + ); }); describe('perform function tests', () => { @@ -305,7 +331,7 @@ describe('StateObserver Tests', () => { stateObserver.sideEffects = jest.fn(); }); - it('should perform Job', () => { + it('should perform the specified Job', () => { dummyJob.observer.nextStateValue = 'newValue'; dummyJob.observer.value = 'dummyValue'; dummyState.initialStateValue = 'initialValue'; @@ -327,7 +353,7 @@ describe('StateObserver Tests', () => { expect(stateObserver.sideEffects).toHaveBeenCalledWith(dummyJob); }); - it('should perform Job and overwrite State (job.config.overwrite = true)', () => { + it('should perform the specified Job and overwrite the State it represents (job.config.overwrite = true)', () => { dummyJob.observer.nextStateValue = 'newValue'; dummyJob.observer.value = 'dummyValue'; dummyJob.config.overwrite = true; @@ -352,27 +378,31 @@ describe('StateObserver Tests', () => { expect(stateObserver.sideEffects).toHaveBeenCalledWith(dummyJob); }); - it('should perform Job and set isSet to false if initialStateValue equals to newStateValue', () => { - dummyJob.observer.nextStateValue = 'newValue'; - dummyJob.observer.value = 'dummyValue'; - dummyState.initialStateValue = 'newValue'; - dummyState._value = 'dummyValue'; - dummyState.getPublicValue = jest - .fn() - .mockReturnValueOnce('newPublicValue'); - - stateObserver.perform(dummyJob); - - expect(dummyState.previousStateValue).toBe('dummyValue'); - expect(dummyState.initialStateValue).toBe('newValue'); - expect(dummyState._value).toBe('newValue'); - expect(dummyState.nextStateValue).toBe('newValue'); - expect(dummyState.isSet).toBeFalsy(); - - expect(stateObserver.value).toBe('newPublicValue'); - expect(stateObserver.previousValue).toBe('dummyValue'); - expect(stateObserver.sideEffects).toHaveBeenCalledWith(dummyJob); - }); + it( + "should perform the specified Job and set 'isSet' to false " + + 'if the initial State value is equal to the new State value', + () => { + dummyJob.observer.nextStateValue = 'newValue'; + dummyJob.observer.value = 'dummyValue'; + dummyState.initialStateValue = 'newValue'; + dummyState._value = 'dummyValue'; + dummyState.getPublicValue = jest + .fn() + .mockReturnValueOnce('newPublicValue'); + + stateObserver.perform(dummyJob); + + expect(dummyState.previousStateValue).toBe('dummyValue'); + expect(dummyState.initialStateValue).toBe('newValue'); + expect(dummyState._value).toBe('newValue'); + expect(dummyState.nextStateValue).toBe('newValue'); + expect(dummyState.isSet).toBeFalsy(); + + expect(stateObserver.value).toBe('newPublicValue'); + expect(stateObserver.previousValue).toBe('dummyValue'); + expect(stateObserver.sideEffects).toHaveBeenCalledWith(dummyJob); + } + ); }); describe('sideEffects function tests', () => { @@ -406,8 +436,9 @@ describe('StateObserver Tests', () => { }; }); - it('should call watchers, sideEffects and ingest dependencies of State', () => { + it('should call watcher callbacks and State side effect', () => { dummyState._value = 'dummyValue'; + stateObserver.sideEffects(dummyJob); expect(dummyState.watchers['dummyWatcher']).toHaveBeenCalledWith( @@ -430,27 +461,64 @@ describe('StateObserver Tests', () => { ]); }); - it("should call watchers, ingest dependencies of State and shouldn't call sideEffects (job.config.sideEffects = false)", () => { - dummyState._value = 'dummyValue'; - dummyJob.config.sideEffects = { - enabled: false, - }; - stateObserver.sideEffects(dummyJob); - - expect(dummyState.watchers['dummyWatcher']).toHaveBeenCalledWith( - 'dummyValue', - 'dummyWatcher' - ); - expect( - dummyState.sideEffects['dummySideEffect'].callback - ).not.toHaveBeenCalled(); - expect( - dummyState.sideEffects['dummySideEffect2'].callback - ).not.toHaveBeenCalled(); - expect( - dummyState.sideEffects['dummySideEffect3'].callback - ).not.toHaveBeenCalled(); - }); + it( + 'should call watcher callbacks ' + + "and shouldn't call State side effects (job.config.sideEffects.enabled = false)", + () => { + dummyState._value = 'dummyValue'; + dummyJob.config.sideEffects = { + enabled: false, + }; + + stateObserver.sideEffects(dummyJob); + + expect(dummyState.watchers['dummyWatcher']).toHaveBeenCalledWith( + 'dummyValue', + 'dummyWatcher' + ); + expect( + dummyState.sideEffects['dummySideEffect'].callback + ).not.toHaveBeenCalled(); + expect( + dummyState.sideEffects['dummySideEffect2'].callback + ).not.toHaveBeenCalled(); + expect( + dummyState.sideEffects['dummySideEffect3'].callback + ).not.toHaveBeenCalled(); + } + ); + + it( + 'should call watcher callbacks ' + + "and shouldn't call all State side effects (job.config.sideEffects.exclude = ['dummySideEffect2'])", + () => { + dummyState._value = 'dummyValue'; + dummyJob.config.sideEffects = { + enabled: true, + exclude: ['dummySideEffect2'], + }; + + stateObserver.sideEffects(dummyJob); + + expect(dummyState.watchers['dummyWatcher']).toHaveBeenCalledWith( + 'dummyValue', + 'dummyWatcher' + ); + expect( + dummyState.sideEffects['dummySideEffect'].callback + ).toHaveBeenCalledWith(dummyState, dummyJob.config); + expect( + dummyState.sideEffects['dummySideEffect2'].callback + ).not.toHaveBeenCalled(); + expect( + dummyState.sideEffects['dummySideEffect3'].callback + ).toHaveBeenCalledWith(dummyState, dummyJob.config); + expect(sideEffectCallOrder).toStrictEqual([ + 'dummySideEffect3', + 'dummySideEffect', + ]); + } + ); }); }); }); diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index ed87d03a..1fc7d061 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -322,7 +322,7 @@ describe('StatePersistent Tests', () => { statePersistent.rebuildStorageSideEffect = jest.fn(); }); - it('should call rebuildStorageSideEffect (persistentKey)', async () => { + it("should call 'rebuildStorageSideEffect' (persistentKey)", async () => { await statePersistent.setupSideEffects(); dummyState.sideEffects[ @@ -340,7 +340,7 @@ describe('StatePersistent Tests', () => { ); }); - it('should call rebuildStorageSideEffect (specified key)', async () => { + it("should call 'rebuildStorageSideEffect' (specified key)", async () => { await statePersistent.setupSideEffects('dummyKey'); dummyState.sideEffects[ @@ -384,7 +384,7 @@ describe('StatePersistent Tests', () => { expect(statePersistent.isPersisted).toBeFalsy(); }); - it('should remove persisted State from the corresponding Storage with specified Key', async () => { + it('should remove persisted State from the corresponding Storage with specified key', async () => { statePersistent.ready = true; const response = await statePersistent.removePersistedValue('coolKey'); @@ -421,7 +421,7 @@ describe('StatePersistent Tests', () => { expect(response).toBe('coolKey'); }); - it('should return passed key', () => { + it('should return specified key', () => { dummyState._key = 'coolKey'; const response = statePersistent.formatKey('awesomeKey'); @@ -429,7 +429,7 @@ describe('StatePersistent Tests', () => { expect(response).toBe('awesomeKey'); }); - it('should return and apply specified key to State if State has no own valid key before', () => { + it('should return and apply specified key to State if State had no own valid key before', () => { dummyState._key = undefined; const response = statePersistent.formatKey('awesomeKey'); @@ -452,7 +452,7 @@ describe('StatePersistent Tests', () => { dummyAgile.storages.set = jest.fn(); }); - it('should save State Value in Storage (default config)', () => { + it('should store current State value in the corresponding Storage (default config)', () => { statePersistent.rebuildStorageSideEffect(dummyState, 'coolKey'); expect(dummyAgile.storages.set).toHaveBeenCalledWith( @@ -462,7 +462,7 @@ describe('StatePersistent Tests', () => { ); }); - it("shouldn't save State Value in Storage (config.storage = false)", () => { + it("shouldn't store State value in the corresponding Storage (config.storage = false)", () => { statePersistent.rebuildStorageSideEffect(dummyState, 'coolKey', { storage: false, }); diff --git a/packages/core/tests/unit/state/state.runtime.job.test.ts b/packages/core/tests/unit/state/state.runtime.job.test.ts index b1f6c04f..c5465182 100644 --- a/packages/core/tests/unit/state/state.runtime.job.test.ts +++ b/packages/core/tests/unit/state/state.runtime.job.test.ts @@ -27,94 +27,118 @@ describe('RuntimeJob Tests', () => { dummyObserver = new StateObserver(dummyState); }); - it('should create RuntimeJob with a specified Agile Instance that has a registered Integration (default config)', () => { - dummyAgile.integrate(dummyIntegration); + it( + 'should create StateRuntimeJob ' + + 'with a specified Agile Instance that has a registered Integration (default config)', + () => { + dummyAgile.integrate(dummyIntegration); - const job = new StateRuntimeJob(dummyObserver); + const job = new StateRuntimeJob(dummyObserver); - expect(job._key).toBeUndefined(); - expect(job.observer).toBe(dummyObserver); - expect(job.config).toStrictEqual({ - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, - force: false, - storage: true, - overwrite: false, - }); - expect(job.rerender).toBeTruthy(); - expect(job.performed).toBeFalsy(); - expect(job.subscriptionContainersToUpdate.size).toBe(0); - }); + expect(job._key).toBeUndefined(); + expect(job.observer).toBe(dummyObserver); + expect(job.config).toStrictEqual({ + background: false, + sideEffects: { + enabled: true, + exclude: [], + }, + force: false, + storage: true, + overwrite: false, + }); + expect(job.rerender).toBeTruthy(); + expect(job.performed).toBeFalsy(); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); + expect(job.timesTriedToUpdateCount).toBe(0); + expect(job.performed).toBeFalsy(); + } + ); - it('should create RuntimeJob with a specified Agile Instance that has a registered Integration (specific config)', () => { - dummyAgile.integrate(dummyIntegration); + it( + 'should create StateRuntimeJob ' + + 'with a specified Agile Instance that has a registered Integration (specific config)', + () => { + dummyAgile.integrate(dummyIntegration); - const job = new StateRuntimeJob(dummyObserver, { - key: 'dummyJob', - sideEffects: { - enabled: false, - }, - force: true, - }); + const job = new StateRuntimeJob(dummyObserver, { + key: 'dummyJob', + sideEffects: { + enabled: false, + }, + force: true, + }); - expect(job._key).toBe('dummyJob'); - expect(job.observer).toBe(dummyObserver); - expect(job.config).toStrictEqual({ - background: false, - sideEffects: { - enabled: false, - }, - force: true, - storage: true, - overwrite: false, - }); - expect(job.rerender).toBeTruthy(); - expect(job.performed).toBeFalsy(); - expect(job.subscriptionContainersToUpdate.size).toBe(0); - }); + expect(job._key).toBe('dummyJob'); + expect(job.observer).toBe(dummyObserver); + expect(job.config).toStrictEqual({ + background: false, + sideEffects: { + enabled: false, + }, + force: true, + storage: true, + overwrite: false, + }); + expect(job.rerender).toBeTruthy(); + expect(job.performed).toBeFalsy(); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); + expect(job.timesTriedToUpdateCount).toBe(0); + expect(job.performed).toBeFalsy(); + } + ); - it('should create RuntimeJob with a specified Agile Instance that has no registered Integration (default config)', () => { - const job = new StateRuntimeJob(dummyObserver); + it( + 'should create StateRuntimeJob ' + + 'with a specified Agile Instance that has no registered Integration (default config)', + () => { + const job = new StateRuntimeJob(dummyObserver); - expect(job._key).toBeUndefined(); - expect(job.observer).toBe(dummyObserver); - expect(job.config).toStrictEqual({ - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, - force: false, - storage: true, - overwrite: false, - }); - expect(job.rerender).toBeFalsy(); - expect(job.performed).toBeFalsy(); - expect(job.subscriptionContainersToUpdate.size).toBe(0); - }); + expect(job._key).toBeUndefined(); + expect(job.observer).toBe(dummyObserver); + expect(job.config).toStrictEqual({ + background: false, + sideEffects: { + enabled: true, + exclude: [], + }, + force: false, + storage: true, + overwrite: false, + }); + expect(job.rerender).toBeFalsy(); + expect(job.performed).toBeFalsy(); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); + expect(job.timesTriedToUpdateCount).toBe(0); + expect(job.performed).toBeFalsy(); + } + ); - it('should create RuntimeJob and Agile that has integrations (config.background = true)', () => { - dummyAgile.integrate(dummyIntegration); + it( + 'should create StateRuntimeJob ' + + 'with a specified Agile Instance that has a registered Integrations (config.background = true)', + () => { + dummyAgile.integrate(dummyIntegration); - const job = new StateRuntimeJob(dummyObserver, { background: true }); + const job = new StateRuntimeJob(dummyObserver, { background: true }); - expect(job._key).toBeUndefined(); - expect(job.observer).toBe(dummyObserver); - expect(job.config).toStrictEqual({ - background: true, - sideEffects: { - enabled: true, - exclude: [], - }, - force: false, - storage: true, - overwrite: false, - }); - expect(job.rerender).toBeFalsy(); - expect(job.performed).toBeFalsy(); - expect(job.subscriptionContainersToUpdate.size).toBe(0); - }); + expect(job._key).toBeUndefined(); + expect(job.observer).toBe(dummyObserver); + expect(job.config).toStrictEqual({ + background: true, + sideEffects: { + enabled: true, + exclude: [], + }, + force: false, + storage: true, + overwrite: false, + }); + expect(job.rerender).toBeFalsy(); + expect(job.performed).toBeFalsy(); + expect(Array.from(job.subscriptionContainersToUpdate)).toStrictEqual([]); + expect(job.timesTriedToUpdateCount).toBe(0); + expect(job.performed).toBeFalsy(); + } + ); }); From 4824a138f4b7bc9e06cd9f9be5f5e09296b310cc Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 12 Jun 2021 19:41:05 +0200 Subject: [PATCH 55/63] fixed tests --- packages/event/tests/unit/event.observer.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/event/tests/unit/event.observer.test.ts b/packages/event/tests/unit/event.observer.test.ts index be77973c..221bd2b9 100644 --- a/packages/event/tests/unit/event.observer.test.ts +++ b/packages/event/tests/unit/event.observer.test.ts @@ -28,8 +28,8 @@ describe('EventObserver Tests', () => { it('should create EventObserver (specific config)', () => { const dummyObserver1 = new Observer(dummyAgile, { key: 'dummyObserver1' }); const dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); - const dummySubscription1 = new SubscriptionContainer(); - const dummySubscription2 = new SubscriptionContainer(); + const dummySubscription1 = new SubscriptionContainer([]); + const dummySubscription2 = new SubscriptionContainer([]); const eventObserver = new EventObserver(dummyEvent, { key: 'testKey', From 4534b7387acc0e9092ac71d850118be8d58a4bf3 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 13 Jun 2021 16:16:29 +0200 Subject: [PATCH 56/63] fixed descritpion for agile class --- packages/core/src/agile.ts | 266 ++++++++++++++++--------- packages/core/src/computed/index.ts | 5 +- packages/core/src/state/index.ts | 2 + packages/core/tests/unit/agile.test.ts | 18 +- 4 files changed, 192 insertions(+), 99 deletions(-) diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts index f4c765f2..03d795c1 100644 --- a/packages/core/src/agile.ts +++ b/packages/core/src/agile.ts @@ -8,7 +8,6 @@ import { DefaultItem, Computed, Integrations, - Observer, SubController, globalBind, Storages, @@ -19,35 +18,62 @@ import { CreateLoggerConfigInterface, StateConfigInterface, flatMerge, - Group, LogCodeManager, + ComputedConfigInterface, + SubscribableAgileInstancesType, } from './internal'; export class Agile { public config: AgileConfigInterface; - public runtime: Runtime; // Handles assigning Values to Agile Instances - public subController: SubController; // Handles subscriptions to Components - public storages: Storages; // Handles permanent saving + // Queues and executes incoming Observer-based Jobs + public runtime: Runtime; + // Manages and simplifies the subscription to UI-Components + public subController: SubController; + // Handles the permanent persistence of Agile Classes + public storages: Storages; - // Integrations - public integrations: Integrations; // Integrated frameworks - static initialIntegrations: Integration[] = []; // External added initial Integrations + // Frameworks that are integrated into AgileTs + public integrations: Integrations; + // External added Integrations that are integrated into AgileTs when it is instantiated + static initialIntegrations: Integration[] = []; - // Static Logger with default config -> will be overwritten by config of last created Agile Instance + // Static AgileTs Logger with default config + // (-> is overwritten by the last created Agile Instance) static logger = new Logger({ prefix: 'Agile', active: true, level: Logger.level.WARN, }); - // Key used to bind AgileTs globally + // Identifier used to bind an Agile Instance globally static globalKey = '__agile__'; /** + * The Agile Class is the main Instance of AgileTs + * and should be unique to your application. + * + * Simply put, the Agile Instance is the brain of AgileTs + * and manages all [`Agile Sub Instance`](../main/Introduction.md#agile-sub-instance) + * like States. + * + * It should be noted that it doesn't store the States; + * It only manages them. Each State has an Instance of the Agile Class, + * for example, to ingest its changes into the runtime. + * In summary, the main tasks of the Agile Class are to: + * - queuing [`Agile Sub Instance`](../main/Introduction.md#agile-sub-instance) + * changes in the `runtime` and preventing race conditions + * - update/rerender subscribed Components + * through Integrations like the [React Integration](../packages/react/Introduction.md) + * - Integrating with persistent [Storage](../packages/core/features/storage/Introduction.md) + * - provide configuration object + * + * Each Agile Sub Instance requires an Agile Instance to be instantiated and function properly. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/) + * * @public - * Agile - Global state and logic framework for reactive Typescript & Javascript applications - * @param config - Config + * @param config - Configuration object */ constructor(config: CreateAgileConfigInterface = {}) { config = defineConfig(config, { @@ -73,39 +99,48 @@ export class Agile { localStorage: config.localStorage, }); - // Assign customized config to Logger + // Assign customized config to static Logger Agile.logger = new Logger(config.logConfig); // Logging LogCodeManager.log('10:00:00', [], this, Agile.logger); - // Create global instance of Agile - // Why? getAgileInstance() returns the global AgileInstance if it couldn't find the Agile Instance in the passed Instance - if (config.bindGlobal) { + // Create global instance of Agile. + // Why? 'getAgileInstance()' returns the global Agile Instance + // if it couldn't find any Agile Instance in the specified Instance. + if (config.bindGlobal) if (!globalBind(Agile.globalKey, this)) LogCodeManager.log('10:02:00'); - } } - //========================================================================================================= - // Storage - //========================================================================================================= /** + * Returns a newly created Storage. + * + * A Storage represents an external storage + * such as the Local Storage and is an interface for AgileTs to it. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstorage) + * * @public - * Storage - Handy Interface for storing Items permanently - * @param config - Config + * @param config - Configuration object */ public createStorage(config: CreateStorageConfigInterface): Storage { return new Storage(config); } - //========================================================================================================= - // State - //========================================================================================================= /** + * Returns a newly created State. + * + * A State manages a piece of Information + * that we need to remember globally at a later point in time. + * While providing a toolkit to use and mutate this set of Information. + * + * You can create as many global States as you need. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstate) + * * @public - * State - Class that holds one Value and causes rerender on subscribed Components - * @param initialValue - Initial Value of the State - * @param config - Config + * @param initialValue - Initial value of the State. + * @param config - Configuration object */ public createState( initialValue: ValueType, @@ -114,13 +149,23 @@ export class Agile { return new State(this, initialValue, config); } - //========================================================================================================= - // Collection - //========================================================================================================= /** + * Returns a newly created Collection. + * + * A Collection manages a reactive set of Information + * that we need to remember globally at a later point in time. + * While providing a toolkit to use and mutate this set of Information. + * + * It is designed for arrays of data objects following the same pattern. + * + * Each of these data object must have a unique `primaryKey` to be correctly identified later. + * + * You can create as many global Collections as you need. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcollection) + * * @public - * Collection - Class that holds a List of Objects with key and causes rerender on subscribed Components - * @param config - Config + * @param config - Configuration object */ public createCollection( config?: CollectionConfig @@ -128,77 +173,98 @@ export class Agile { return new Collection(this, config); } - //========================================================================================================= - // Computed - //========================================================================================================= /** + * Returns a newly created Computed. + * + * A Computed is an extension of the State Class + * that computes its value based on a compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and is only recomputed when one of its direct dependencies changes. + * + * Direct dependencies can be States and Collections. + * So when for example a dependent State value changes, the computed value will be recomputed. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstate) + * * @public - * Computed - Function that recomputes its value if a dependency changes - * @param computeFunction - Function for computing value - * @param config - Config - * @param deps - Hard coded dependencies of Computed Function + * @param computeFunction - Function to compute the computed value. + * @param config - Configuration object */ public createComputed( computeFunction: () => ComputedValueType, - config?: StateConfigInterface, - deps?: Array + config?: ComputedConfigInterface ): Computed; /** + * Returns a newly created Computed. + * + * A Computed is an extension of the State Class + * that computes its value based on a compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and is only recomputed when one of its direct dependencies changes. + * + * Direct dependencies can be States and Collections. + * So when for example a dependent State value changes, the computed value will be recomputed. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcomputed) + * * @public - * Computed - Function that recomputes its value if a dependency changes - * @param computeFunction - Function for computing value - * @param deps - Hard coded dependencies of Computed Function + * @param computeFunction - Function to compute the computed value. + * @param deps - Hard-coded dependencies on which the Computed Class should depend. */ public createComputed( computeFunction: () => ComputedValueType, - deps?: Array + deps?: Array ): Computed; public createComputed( computeFunction: () => ComputedValueType, - configOrDeps?: StateConfigInterface | Array, - deps?: Array + configOrDeps?: + | ComputedConfigInterface + | Array ): Computed { - let _deps: Array; - let _config: StateConfigInterface; + let _config: ComputedConfigInterface = {}; if (Array.isArray(configOrDeps)) { - _deps = configOrDeps; - _config = {}; + _config = flatMerge(_config, { + computedDeps: configOrDeps, + }); } else { - _config = configOrDeps || {}; - _deps = deps || []; + if (configOrDeps) _config = configOrDeps; } - return new Computed( - this, - computeFunction, - flatMerge(_config, { - computedDeps: _deps, - }) - ); + return new Computed(this, computeFunction, _config); } - //========================================================================================================= - // Integrate - //========================================================================================================= /** + * Registers the specified Integration with AgileTs. + * + * After a successful registration, + * Agile Sub Instances such as States + * can be bound to the Integration's UI-Components for reactivity. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#integrate) + * * @public - * Integrates framework into Agile - * @param integration - Integration that gets registered/integrated + * @param integration - Integration to be integrated/registered. */ public integrate(integration: Integration) { this.integrations.integrate(integration); return this; } - //========================================================================================================= - // Register Storage - //========================================================================================================= /** + * Registers the specified Storage with AgileTs. + * + * After a successful registration, + * Agile Sub Instances such as States + * can be persisted in the external Storage. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#registerstorage) + * * @public - * Registers new Storage as Agile Storage - * @param storage - new Storage - * @param config - Config + * @param storage - Storage to be registered. + * @param config - Configuration object */ public registerStorage( storage: Storage, @@ -208,45 +274,67 @@ export class Agile { return this; } - //========================================================================================================= - // Has Integration - //========================================================================================================= /** + * Returns a boolean indicating whether any Integration + * has been registered with AgileTs or not. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#hasintegration) + * * @public - * Checks if Agile has any registered Integration */ public hasIntegration(): boolean { return this.integrations.hasIntegration(); } - //========================================================================================================= - // Has Storage - //========================================================================================================= /** + * Returns a boolean indicating whether any Storage + * has been registered with AgileTs or not. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#hasstorage) + * * @public - * Checks if Agile has any registered Storage */ public hasStorage(): boolean { return this.storages.hasStorage(); } } -/** - * @param logJobs - Allow Agile Logs - * @param waitForMount - If Agile should wait until the component mounts - * @param storageConfig - To configure Agile Storage - * @param bindGlobal - Binds Agile Instance Global - */ export interface CreateAgileConfigInterface { + /** + * Configures the logging behaviour of AgileTs. + * @default { + prefix: 'Agile', + active: true, + level: Logger.level.WARN, + canUseCustomStyles: true, + allowedTags: ['runtime', 'storage', 'subscription', 'multieditor'], + } + */ logConfig?: CreateLoggerConfigInterface; + /** + * Whether the Subscription Container should not be ready + * until the UI-Component it represents has been mounted. + * @default true + */ waitForMount?: boolean; + /** + * Whether the Local Storage should be registered as Storage by default. + * @default true + */ localStorage?: boolean; + /** + * Whether the Agile instance should be globally bound (globalThis) + * and thus be globally available. + * @default false + */ bindGlobal?: boolean; } -/** - * @param waitForMount - If Agile should wait until the component mounts - */ export interface AgileConfigInterface { + /** + * Whether the Subscription Container should not be ready + * until the UI-Component it represents has been mounted. + * @default true + */ waitForMount: boolean; } diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index cea3cdc7..0a5c5cdb 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -26,7 +26,8 @@ export class Computed extends State< public hardCodedDeps: Array = []; /** - * An extension of the State Class that computes its value based on a compute function. + * A Computed is an extension of the State Class + * that computes its value based on a compute function. * * The computed value will be cached to avoid unnecessary recomputes * and is only recomputed when one of its direct dependencies changes. @@ -217,4 +218,4 @@ export interface RecomputeConfigInterface extends StateIngestConfigInterface, ComputeConfigInterface {} -type SubscribableAgileInstancesType = State | Collection | Observer; +export type SubscribableAgileInstancesType = State | Collection | Observer; diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 3dc8a1fd..2d949993 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -72,6 +72,8 @@ export class State { * * You can create as many global States as you need. * + * [Learn more..](https://agile-ts.org/docs/core/state/) + * * @public * @param agileInstance - Instance of Agile the State belongs to. * @param initialValue - Initial value of the State. diff --git a/packages/core/tests/unit/agile.test.ts b/packages/core/tests/unit/agile.test.ts index e231a090..11b9e863 100644 --- a/packages/core/tests/unit/agile.test.ts +++ b/packages/core/tests/unit/agile.test.ts @@ -240,26 +240,28 @@ describe('Agile Tests', () => { }); it('should create Computed', () => { - const computed = agile.createComputed(computedFunction, []); + const computed = agile.createComputed(computedFunction, [ + 'dummyDep' as any, + ]); expect(computed).toBeInstanceOf(Computed); expect(ComputedMock).toHaveBeenCalledWith(agile, computedFunction, { - computedDeps: [], + computedDeps: ['dummyDep' as any], }); }); it('should create Computed with config', () => { - const computed = agile.createComputed( - computedFunction, - { key: 'jeff', isPlaceholder: false }, - [] - ); + const computed = agile.createComputed(computedFunction, { + key: 'jeff', + isPlaceholder: false, + computedDeps: ['dummyDep' as any], + }); expect(computed).toBeInstanceOf(Computed); expect(ComputedMock).toHaveBeenCalledWith(agile, computedFunction, { key: 'jeff', isPlaceholder: false, - computedDeps: [], + computedDeps: ['dummyDep' as any], }); }); }); From 5bcd122ca93612003c7fab871b49d413578ba8be Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 13 Jun 2021 18:33:02 +0200 Subject: [PATCH 57/63] fixed descriptions for Integration/s class --- packages/core/src/integrations/index.ts | 46 +++++++------- packages/core/src/integrations/integration.ts | 61 ++++++++++++++----- 2 files changed, 72 insertions(+), 35 deletions(-) diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index 09cfa12f..9b6c140c 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -1,31 +1,35 @@ import { Agile, Integration, LogCodeManager } from '../internal'; export class Integrations { + // Agile Instance the Integrations belongs to public agileInstance: () => Agile; - public integrations: Set = new Set(); // All registered Integrations + // Registered Integrations + public integrations: Set = new Set(); /** + * The Integrations Class manages all Integrations for an Agile Instance + * and provides an interface to easily update + * or invoke functions in all registered Integrations. + * * @internal - * Integrations - Manages Integrations of Agile - * @param agileInstance - An Instance of Agile + * @param agileInstance - Instance of Agile the Integrations belongs to. */ constructor(agileInstance: Agile) { this.agileInstance = () => agileInstance; - // Integrate initial Integrations which are static and got set external + // Integrate initial Integrations which were statically set from external Agile.initialIntegrations.forEach((integration) => this.integrate(integration) ); } - //========================================================================================================= - // Integrate - //========================================================================================================= /** + * Integrates the specified Integration into AgileTs + * and sets it to ready when the binding was successful. + * * @internal - * Integrates Framework(Integration) into Agile - * @param integration - Integration/Framework that gets integrated + * @param integration - Integration to be integrated into AgileTs. */ public async integrate(integration: Integration): Promise { // Check if Integration is valid @@ -34,7 +38,7 @@ export class Integrations { return false; } - // Bind Framework to Agile + // Bind to integrate Integration to AgileTs if (integration.methods.bind) integration.ready = await integration.methods.bind(this.agileInstance()); else integration.ready = true; @@ -48,15 +52,16 @@ export class Integrations { return true; } - //========================================================================================================= - // Update - //========================================================================================================= /** + * Updates the specified UI-Component Instance + * with the updated data object in all registered Integrations that are ready. + * + * In doing so, it calls the `updateMethod()` method + * in all registered Integrations with the specified parameters. + * * @internal - * Updates registered and ready Integrations - * -> calls 'updateMethod' in all registered and ready Integrations - * @param componentInstance - Component that gets updated - * @param updatedData - Properties that differ from the last Value + * @param componentInstance - Component Instance to be updated. + * @param updatedData - Data object with updated data. */ public update(componentInstance: any, updatedData: Object): void { this.integrations.forEach((integration) => { @@ -69,12 +74,11 @@ export class Integrations { }); } - //========================================================================================================= - // Has Integration - //========================================================================================================= /** + * Returns a boolean indicating whether any Integration + * has been registered or not. + * * @internal - * Check if at least one Integration got registered */ public hasIntegration(): boolean { return this.integrations.size > 0; diff --git a/packages/core/src/integrations/integration.ts b/packages/core/src/integrations/integration.ts index 94c011db..41a4d75d 100644 --- a/packages/core/src/integrations/integration.ts +++ b/packages/core/src/integrations/integration.ts @@ -1,16 +1,23 @@ import { Agile } from '../internal'; export class Integration { + // Key/Name identifier of the Integration public _key: IntegrationKey; + // Instance of the Framework the Integration represents public frameworkInstance?: F; + // Whether the Integration is ready public ready = false; + // Whether the Integration was integrated into AgileTs public integrated = false; + // Methods to interact with the Framework represented by the Integration public methods: IntegrationMethods; /** + * An Integrations is an direct interface to an UI-Framework, + * and allows easy interaction with that Framework. + * * @public - * Integration - Represents a Framework/Integration of Agile - * @param config - Config + * @param config - Configuration object */ constructor(config: CreateIntegrationConfig) { this._key = config.key; @@ -22,38 +29,64 @@ export class Integration { } /** + * Updates the key/name identifier of the Integration. + * * @public - * Set Value of Integration + * @param value - New key/name identifier. */ - public set key(key: IntegrationKey) { - this._key = key; + public set key(value: IntegrationKey) { + this._key = value; } /** + * Returns the key/name identifier of the State. + * * @public - * Get Value of Integration */ public get key(): IntegrationKey { return this._key; } } -/** - * @param key - Key/Name of Integration - * @param frameworkInstance - An Instance of the Framework that this Integration represents (for instance React) - */ export interface CreateIntegrationConfig extends IntegrationMethods { + /** + * Key/Name identifier of the Integratioon. + * @default undefined + */ key: string; + /** + * An Instance of the Framework to be represented by the Integration. + * For example, in the case of React, the React Instance. + * @default undefined + */ frameworkInstance?: F; } -/** - * @param bind - Binds the Framework/Integration to Agile | Will be called after a successful integration - * @param updateMethod - Will be called if a Observer updates his subs (Only in Component based Subscriptions!) - */ export interface IntegrationMethods { + /** + * Binds the Framework/Integration to an Agile Instance. + * + * This method is called shortly after the Integration was registered with a Agile Instance. + * It is intended to set up things on the Framework side + * that are important for a integration into AgileTs. + * + * @param agileInstance - Agile Instance into which the Integration is to be integrated. + * @return Indicating whether the to integrate Integration is ready on the Framework side. + */ bind?: (agileInstance: Agile) => Promise; + /** + * Method to apply the specified updated data to the specified UI-Component + * in order to trigger a re-render on it. + * + * This method is called when the value of an Agile Sub Instance + * bound to the UI-Component changes + * in a Component based Subscription. + * The updated Agile Sub Instance values are represented in the `updatedData` object. + * + * @param componentInstance - Component Instance of the to update UI-Component. + * @param updatedData - Data object containing the updated data. + */ updateMethod?: (componentInstance: C, updatedData: Object) => void; } From 56d42f3dda828386b3bf851f0a1945b2c8d4c697 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 13 Jun 2021 20:23:19 +0200 Subject: [PATCH 58/63] fixed typos --- packages/core/src/agile.ts | 57 ++++++++++--------- packages/core/src/computed/index.ts | 4 +- packages/core/src/integrations/index.ts | 4 +- packages/core/src/integrations/integration.ts | 32 ++++++----- .../runtime/subscription/sub.controller.ts | 2 +- packages/core/src/state/index.ts | 2 +- 6 files changed, 53 insertions(+), 48 deletions(-) diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts index 03d795c1..d47775ae 100644 --- a/packages/core/src/agile.ts +++ b/packages/core/src/agile.ts @@ -33,12 +33,12 @@ export class Agile { // Handles the permanent persistence of Agile Classes public storages: Storages; - // Frameworks that are integrated into AgileTs + // Integrations (UI-Frameworks) that are integrated into AgileTs public integrations: Integrations; - // External added Integrations that are integrated into AgileTs when it is instantiated + // External added Integrations that are to integrate into AgileTs when it is instantiated static initialIntegrations: Integration[] = []; - // Static AgileTs Logger with default config + // Static AgileTs Logger with the default config // (-> is overwritten by the last created Agile Instance) static logger = new Logger({ prefix: 'Agile', @@ -54,18 +54,18 @@ export class Agile { * and should be unique to your application. * * Simply put, the Agile Instance is the brain of AgileTs - * and manages all [`Agile Sub Instance`](../main/Introduction.md#agile-sub-instance) - * like States. + * and manages all [Agile Sub Instance](https://agile-ts.org/docs/introduction/#agile-sub-instance) + * such as States. * * It should be noted that it doesn't store the States; * It only manages them. Each State has an Instance of the Agile Class, - * for example, to ingest its changes into the runtime. + * for example, to ingest its changes into the Runtime. * In summary, the main tasks of the Agile Class are to: - * - queuing [`Agile Sub Instance`](../main/Introduction.md#agile-sub-instance) - * changes in the `runtime` and preventing race conditions - * - update/rerender subscribed Components - * through Integrations like the [React Integration](../packages/react/Introduction.md) - * - Integrating with persistent [Storage](../packages/core/features/storage/Introduction.md) + * - queue [Agile Sub Instance](https://agile-ts.org/docs/introduction/#agile-sub-instance) + * changes in the Runtime to prevent race conditions + * - update/rerender subscribed UI-Components through the provided Integrations + * such as the [React Integration](https://agile-ts.org/docs/react) + * - integrate with the persistent [Storage](https://agile-ts.org/docs/core/storage) * - provide configuration object * * Each Agile Sub Instance requires an Agile Instance to be instantiated and function properly. @@ -99,13 +99,12 @@ export class Agile { localStorage: config.localStorage, }); - // Assign customized config to static Logger + // Assign customized Logger config to the static Logger Agile.logger = new Logger(config.logConfig); - // Logging LogCodeManager.log('10:00:00', [], this, Agile.logger); - // Create global instance of Agile. + // Create a global instance of the Agile Instance. // Why? 'getAgileInstance()' returns the global Agile Instance // if it couldn't find any Agile Instance in the specified Instance. if (config.bindGlobal) @@ -115,8 +114,12 @@ export class Agile { /** * Returns a newly created Storage. * - * A Storage represents an external storage - * such as the Local Storage and is an interface for AgileTs to it. + * A Storage Class serves as an interface to external storages, + * such as the [Async Storage](https://github.com/react-native-async-storage/async-storage) or + * [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp). + * + * It creates the foundation to easily [`persist()`](https://agile-ts.org/docs/core/state/methods#persist) [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) + * (like States or Collections) in nearly any external storage. * * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstorage) * @@ -132,7 +135,7 @@ export class Agile { * * A State manages a piece of Information * that we need to remember globally at a later point in time. - * While providing a toolkit to use and mutate this set of Information. + * While providing a toolkit to use and mutate this piece of Information. * * You can create as many global States as you need. * @@ -177,13 +180,13 @@ export class Agile { * Returns a newly created Computed. * * A Computed is an extension of the State Class - * that computes its value based on a compute function. + * that computes its value based on a specified compute function. * * The computed value will be cached to avoid unnecessary recomputes * and is only recomputed when one of its direct dependencies changes. * * Direct dependencies can be States and Collections. - * So when for example a dependent State value changes, the computed value will be recomputed. + * So when, for example, a dependent State value changes, the computed value is recomputed. * * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstate) * @@ -199,13 +202,13 @@ export class Agile { * Returns a newly created Computed. * * A Computed is an extension of the State Class - * that computes its value based on a compute function. + * that computes its value based on a specified compute function. * * The computed value will be cached to avoid unnecessary recomputes * and is only recomputed when one of its direct dependencies changes. * * Direct dependencies can be States and Collections. - * So when for example a dependent State value changes, the computed value will be recomputed. + * So when, for example, a dependent State value changes, the computed value is recomputed. * * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcomputed) * @@ -240,7 +243,7 @@ export class Agile { * Registers the specified Integration with AgileTs. * * After a successful registration, - * Agile Sub Instances such as States + * [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) such as States * can be bound to the Integration's UI-Components for reactivity. * * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#integrate) @@ -257,7 +260,7 @@ export class Agile { * Registers the specified Storage with AgileTs. * * After a successful registration, - * Agile Sub Instances such as States + * [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) such as States * can be persisted in the external Storage. * * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#registerstorage) @@ -312,18 +315,18 @@ export interface CreateAgileConfigInterface { */ logConfig?: CreateLoggerConfigInterface; /** - * Whether the Subscription Container should not be ready + * Whether the Subscription Container shouldn't be ready * until the UI-Component it represents has been mounted. * @default true */ waitForMount?: boolean; /** - * Whether the Local Storage should be registered as Storage by default. + * Whether the Local Storage should be registered as a Agile Storage by default. * @default true */ localStorage?: boolean; /** - * Whether the Agile instance should be globally bound (globalThis) + * Whether the Agile Instance should be globally bound (globalThis) * and thus be globally available. * @default false */ @@ -332,7 +335,7 @@ export interface CreateAgileConfigInterface { export interface AgileConfigInterface { /** - * Whether the Subscription Container should not be ready + * Whether the Subscription Container shouldn't be ready * until the UI-Component it represents has been mounted. * @default true */ diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index 0a5c5cdb..f0d95f6b 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -27,13 +27,13 @@ export class Computed extends State< /** * A Computed is an extension of the State Class - * that computes its value based on a compute function. + * that computes its value based on a specified compute function. * * The computed value will be cached to avoid unnecessary recomputes * and is only recomputed when one of its direct dependencies changes. * * Direct dependencies can be States and Collections. - * So when for example a dependent State value changes, the computed value will be recomputed. + * So when, for example, a dependent State value changes, the computed value is recomputed. * * [Learn more..](https://agile-ts.org/docs/core/computed/) * diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index 9b6c140c..53c41b6b 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -18,7 +18,7 @@ export class Integrations { constructor(agileInstance: Agile) { this.agileInstance = () => agileInstance; - // Integrate initial Integrations which were statically set from external + // Integrate initial Integrations which were statically set externally Agile.initialIntegrations.forEach((integration) => this.integrate(integration) ); @@ -43,7 +43,7 @@ export class Integrations { integration.ready = await integration.methods.bind(this.agileInstance()); else integration.ready = true; - // Integrate Framework + // Integrate Integration this.integrations.add(integration); integration.integrated = true; diff --git a/packages/core/src/integrations/integration.ts b/packages/core/src/integrations/integration.ts index 41a4d75d..f8f0b69b 100644 --- a/packages/core/src/integrations/integration.ts +++ b/packages/core/src/integrations/integration.ts @@ -5,7 +5,7 @@ export class Integration { public _key: IntegrationKey; // Instance of the Framework the Integration represents public frameworkInstance?: F; - // Whether the Integration is ready + // Whether the Integration is ready and the binding to AgileTs was successful public ready = false; // Whether the Integration was integrated into AgileTs public integrated = false; @@ -13,8 +13,11 @@ export class Integration { public methods: IntegrationMethods; /** - * An Integrations is an direct interface to an UI-Framework, - * and allows easy interaction with that Framework. + * An Integrations is an interface to a UI-Framework, + * and allows the easy interaction with that Framework. + * + * Due to the Integration, AgileTs can be integrated into almost any UI-Framework + * without a huge overhead. * * @public * @param config - Configuration object @@ -39,7 +42,7 @@ export class Integration { } /** - * Returns the key/name identifier of the State. + * Returns the key/name identifier of the Integration. * * @public */ @@ -51,12 +54,12 @@ export class Integration { export interface CreateIntegrationConfig extends IntegrationMethods { /** - * Key/Name identifier of the Integratioon. + * Key/Name identifier of the Integration. * @default undefined */ key: string; /** - * An Instance of the Framework to be represented by the Integration. + * An Instance of the UI-Framework to be represented by the Integration. * For example, in the case of React, the React Instance. * @default undefined */ @@ -65,24 +68,23 @@ export interface CreateIntegrationConfig export interface IntegrationMethods { /** - * Binds the Framework/Integration to an Agile Instance. + * Binds the Integration to an Agile Instance. * - * This method is called shortly after the Integration was registered with a Agile Instance. - * It is intended to set up things on the Framework side - * that are important for a integration into AgileTs. + * This method is called shortly after the Integration was registered with an Agile Instance. + * It is intended to set up things that are important + * for an seamless integration into AgileTs on the UI-Framework side. * * @param agileInstance - Agile Instance into which the Integration is to be integrated. * @return Indicating whether the to integrate Integration is ready on the Framework side. */ bind?: (agileInstance: Agile) => Promise; /** - * Method to apply the specified updated data to the specified UI-Component + * Method to apply the updated data to the provided UI-Component * in order to trigger a re-render on it. * - * This method is called when the value of an Agile Sub Instance - * bound to the UI-Component changes - * in a Component based Subscription. - * The updated Agile Sub Instance values are represented in the `updatedData` object. + * This method is called when the value of an [Agile Sub Instance](https://agile-ts.org/docs/introduction/#agile-sub-instance) + * bound to the specified UI-Component changes ([Component based Subscription](https://agile-ts.org/docs/core/integration/#component-based)). + * The updated Agile Sub Instance values were mapped in the provided `updatedData` object. * * @param componentInstance - Component Instance of the to update UI-Component. * @param updatedData - Data object containing the updated data. diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index d1ac3641..d738d5b5 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -319,7 +319,7 @@ export class SubController { interface RegisterSubscriptionConfigInterface extends SubscriptionContainerConfigInterface { /** - * Whether the Subscription Container should not be ready + * Whether the Subscription Container shouldn't be ready * until the UI-Component it represents has been mounted. * @default agileInstance.config.waitForMount */ diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 2d949993..49829a47 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -68,7 +68,7 @@ export class State { /** * A State manages a piece of Information * that we need to remember globally at a later point in time. - * While providing a toolkit to use and mutate this set of Information. + * While providing a toolkit to use and mutate this piece of Information. * * You can create as many global States as you need. * From 598d607fcb6ef68151df6be7b3e6ef46c65740a3 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 15 Jun 2021 07:34:55 +0200 Subject: [PATCH 59/63] added size-limit --- package.json | 2 + packages/core/.size-limit.js | 6 +++ packages/core/package.json | 3 +- yarn.lock | 96 ++++++++++++++++++++++++++++++++---- 4 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 packages/core/.size-limit.js diff --git a/package.json b/package.json index 5ace251e..472b9722 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ }, "devDependencies": { "@changesets/cli": "^2.12.0", + "@size-limit/file": "^4.12.0", "@types/jest": "^26.0.15", "@types/node": "^14.14.7", "@typescript-eslint/eslint-plugin": "^4.12.0", @@ -62,6 +63,7 @@ "lerna-changelog": "^1.0.1", "nodemon": "^2.0.6", "prettier": "2.1.2", + "size-limit": "^4.12.0", "ts-jest": "^26.4.4", "ts-node": "^8.10.2", "tsc-watch": "^4.1.0", diff --git a/packages/core/.size-limit.js b/packages/core/.size-limit.js new file mode 100644 index 00000000..1b8247f6 --- /dev/null +++ b/packages/core/.size-limit.js @@ -0,0 +1,6 @@ +module.exports = [ + { + path: 'dist/*', + limit: '35 kB', + }, +]; diff --git a/packages/core/package.json b/packages/core/package.json index 334f739d..c1b896fb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -36,7 +36,8 @@ "preview": "npm pack", "test": "jest", "test:coverage": "jest --coverage", - "lint": "eslint src/**/*" + "lint": "eslint src/**/*", + "size": "yarn run build && size-limit" }, "devDependencies": { "@agile-ts/logger": "file:../logger", diff --git a/yarn.lock b/yarn.lock index de94593e..024bf2af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,10 +3,10 @@ "@agile-ts/core@file:packages/core": - version "0.0.16" + version "0.0.17" dependencies: - "@agile-ts/logger" "^0.0.3" - "@agile-ts/utils" "^0.0.3" + "@agile-ts/logger" "^0.0.4" + "@agile-ts/utils" "^0.0.4" "@akryum/winattr@^3.0.0": version "3.0.0" @@ -2788,6 +2788,13 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@size-limit/file@^4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-4.12.0.tgz#50166eca7b9b5aa15f51a72b3d9d31e2d8530e6f" + integrity sha512-csgGSAG3s2y9eOl/taahojXY91AXpNgqLs9HJ5c/Qmrs+6UHgXbwJ4vo475NfZmt1Y9simircb1ygqupauNUyA== + dependencies: + semver "7.3.5" + "@surma/rollup-plugin-off-main-thread@^1.1.1": version "1.4.2" resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-1.4.2.tgz#e6786b6af5799f82f7ab3a82e53f6182d2b91a58" @@ -4735,6 +4742,15 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -5006,7 +5022,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.2.1: +buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -5046,6 +5062,11 @@ byte-size@^5.0.1: resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-5.0.1.tgz#4b651039a5ecd96767e71a3d7ed380e48bed4191" integrity sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw== +bytes-iec@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/bytes-iec/-/bytes-iec-3.1.1.tgz#94cd36bf95c2c22a82002c247df8772d1d591083" + integrity sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA== + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -5358,7 +5379,7 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.2.2, chokidar@^3.4.1: +chokidar@^3.2.2, chokidar@^3.4.1, chokidar@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== @@ -5393,6 +5414,11 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +ci-job-number@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ci-job-number/-/ci-job-number-1.2.2.tgz#f4e5918fcaeeda95b604f214be7d7d4a961fe0c0" + integrity sha512-CLOGsVDrVamzv8sXJGaILUVI6dsuAkouJP/n6t+OxLPeeA4DDby7zn9SB6EUpa1H7oIKoE+rMmkW80zYsFfUjA== + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -5464,7 +5490,7 @@ cli-highlight@^2.1.4: parse5-htmlparser2-tree-adapter "^6.0.0" yargs "^16.0.0" -cli-spinners@^2.0.0: +cli-spinners@^2.0.0, cli-spinners@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== @@ -8608,7 +8634,7 @@ globby@11.0.1: merge2 "^1.3.0" slash "^3.0.0" -globby@^11.0.0, globby@^11.0.1, globby@^11.0.2: +globby@^11.0.0, globby@^11.0.1, globby@^11.0.2, globby@^11.0.3: version "11.0.3" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== @@ -9671,6 +9697,11 @@ is-installed-globally@^0.3.1: global-dirs "^2.0.1" is-path-inside "^3.0.1" +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + is-lambda@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" @@ -9860,6 +9891,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -10774,6 +10810,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lilconfig@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd" + integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg== + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -10990,6 +11031,14 @@ log-symbols@^2.2.0: dependencies: chalk "^2.0.1" +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + loglevel@^1.6.7, loglevel@^1.6.8: version "1.7.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" @@ -12283,6 +12332,21 @@ ora@^3.4.0: strip-ansi "^5.2.0" wcwidth "^1.0.1" +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + original@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" @@ -14194,7 +14258,7 @@ read@1, read@~1.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -14882,7 +14946,7 @@ semver@7.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== -semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: +semver@7.3.5, semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== @@ -15067,6 +15131,20 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +size-limit@^4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-4.12.0.tgz#ecc9c0448c049a40b10e76b5e1b4a20f99a54468" + integrity sha512-LwlUDPxFJbJDIJsBE5bKo8kFMuxmuewBMDjgfSoQwnO27V8DSK+j6881nsrX3GoM3bJMFIeEq56thqBEdYC8bw== + dependencies: + bytes-iec "^3.1.1" + chokidar "^3.5.1" + ci-job-number "^1.2.2" + colorette "^1.2.2" + globby "^11.0.3" + lilconfig "^2.0.3" + ora "^5.4.1" + read-pkg-up "^7.0.1" + slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" From 1585c4b4fc2e183f3c9ec5219fb3b549d0d9dd59 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 16 Jun 2021 20:27:18 +0200 Subject: [PATCH 60/63] fixed typos in Storages Class --- packages/core/src/state/index.ts | 6 +- packages/core/src/storages/index.ts | 155 ++++++++++-------- .../template.json | 10 +- packages/cra-template-agile/template.json | 8 +- 4 files changed, 101 insertions(+), 78 deletions(-) diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 49829a47..63d73091 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -183,7 +183,7 @@ export class State { /** * Assigns a new value to the State - * and rerenders all subscribed Components. + * and re-renders all subscribed UI-Components. * * [Learn more..](https://agile-ts.org/docs/core/state/methods/#set) * @@ -832,14 +832,14 @@ export type StateWatcherCallback = (value: T, key: string) => void; export type ComputeValueMethod = (value: T) => T; export type ComputeExistsMethod = (value: T) => boolean; -export type SideEffectFunctionType> = ( +export type SideEffectFunctionType = ( instance: Instance, properties?: { [key: string]: any; } ) => void; -export interface SideEffectInterface> { +export interface SideEffectInterface { /** * Callback function to be called on every State value change. * @return () => {} diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 7d1df906..ec746bb7 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -10,17 +10,23 @@ import { } from '../internal'; export class Storages { + // Agile Instance the Storages belongs to public agileInstance: () => Agile; public config: StoragesConfigInterface; - public storages: { [key: string]: Storage } = {}; // All registered Storages + + // Registered Storages + public storages: { [key: string]: Storage } = {}; + // Persistent from Instances that were persisted public persistentInstances: Set = new Set(); /** + * The Storages Class manages all external Storages for an Agile Instance + * and provides an interface to easily store, load and remove values from multiple Storages at once. + * * @internal - * Storages - Manages Storages of Agile - * @param agileInstance - An Instance of Agile - * @param config - Config + * @param agileInstance - Instance of Agile the Storages belongs to. + * @param config - Configuration object */ constructor( agileInstance: Agile, @@ -35,15 +41,16 @@ export class Storages { if (config.localStorage) this.instantiateLocalStorage(); } - //========================================================================================================= - // Instantiate Local Storage - //========================================================================================================= /** + * Instantiates and registers the + * [Local Storage](https://developer.mozilla.org/de/docs/Web/API/Window/localStorage). + * + * Note that this only works in a web environment! + * * @internal - * Instantiates Local Storage */ public instantiateLocalStorage(): boolean { - // Check if Local Storage is Available + // Check if Local Storage is available in this environment if (!Storages.localStorageAvailable()) { LogCodeManager.log('11:02:00'); return false; @@ -62,14 +69,14 @@ export class Storages { return this.register(_localStorage, { default: true }); } - //========================================================================================================= - // Register - //========================================================================================================= /** + * Registers the specified Storage with AgileTs + * and updates the Persistents that have already attempted + * to use the now registered Storage. + * * @internal - * Register new Storage as Agile Storage - * @param storage - new Storage - * @param config - Config + * @param storage - Storage to be registered with AgileTs. + * @param config - Configuration object */ public register( storage: Storage, @@ -83,7 +90,7 @@ export class Storages { return false; } - // Set first added Storage as default Storage + // Assign first added Storage as default Storage if (!hasRegisteredAnyStorage && config.default === false) LogCodeManager.log('11:02:01'); if (!hasRegisteredAnyStorage) config.default = true; @@ -93,15 +100,16 @@ export class Storages { if (config.default) this.config.defaultStorageKey = storage.key; this.persistentInstances.forEach((persistent) => { - // Revalidate Persistent that includes the newly registered StorageKey + // Revalidate Persistent that includes the newly registered storage key if (persistent.storageKeys.includes(storage.key)) { const isValid = persistent.validatePersistent(); if (isValid) persistent.initialLoading(); return; } - // If persistent has no default StorageKey (reassign StorageKeys since this registered Storage might be tagged as default Storage) - if (!persistent.config.defaultStorageKey) { + // If Persistent has no default storage key + // (reassign storage keys since this registered Storage might be tagged as default Storage) + if (persistent.config.defaultStorageKey == null) { persistent.assignStorageKeys(); const isValid = persistent.validatePersistent(); if (isValid) persistent.initialLoading(); @@ -111,13 +119,13 @@ export class Storages { return true; } - //========================================================================================================= - // Get Storage - //========================================================================================================= /** - * @internal - * Get Storage at Key/Name - * @param storageKey - Key/Name of Storage + * Retrieves a single Storage with the specified key/name identifier from the Storages Class. + * + * If the to retrieve Storage doesn't exist, `undefined` is returned. + * + * @public + * @param storageKey - Key/Name identifier of the Storage. */ public getStorage( storageKey: StorageKey | undefined | null @@ -140,14 +148,17 @@ export class Storages { return storage; } - //========================================================================================================= - // Get - //========================================================================================================= /** + * Retrieves the stored value at the specified Storage Item key + * from the defined external Storage (`storageKey`). + * + * When no Storage has been specified, + * the value is retrieved from the default Storage. + * * @internal - * Gets value at provided Key - * @param storageItemKey - Key of Storage property - * @param storageKey - Key/Name of Storage from which the Item is fetched (if not provided default Storage will be used) + * @param storageItemKey - Key/Name identifier of the value to be retrieved. + * @param storageKey - Key/Name identifier of the external Storage + * from which the value is to be retrieved. */ public get( storageItemKey: StorageItemKey, @@ -158,28 +169,31 @@ export class Storages { return Promise.resolve(undefined); } - // Call get Method in specific Storage + // Call get method on specified Storage if (storageKey) { const storage = this.getStorage(storageKey); if (storage) return storage.get(storageItemKey); } - // Call get Method in default Storage + // Call get method on default Storage const defaultStorage = this.getStorage(this.config.defaultStorageKey); return ( defaultStorage?.get(storageItemKey) || Promise.resolve(undefined) ); } - //========================================================================================================= - // Set - //========================================================================================================= /** + * Stores or updates the value at the specified Storage Item key + * in the defined external Storages (`storageKeys`). + * + * When no Storage has been specified, + * the value is stored/updated in the default Storage + * * @internal - * Saves/Updates value at provided Key - * @param storageItemKey - Key of Storage property - * @param value - new Value that gets set at provided Key - * @param storageKeys - Key/Name of Storages where the Value gets set (if not provided default Storage will be used) + * @param storageItemKey - Key/Name identifier of the value to be stored. + * @param value - Value to be stored in an external Storage. + * @param storageKeys - Key/Name identifier of the external Storage + * where the value is to be stored. */ public set( storageItemKey: StorageItemKey, @@ -191,26 +205,29 @@ export class Storages { return; } - // Call set Method in specific Storages + // Call set method on specified Storages if (storageKeys) { for (const storageKey of storageKeys) this.getStorage(storageKey)?.set(storageItemKey, value); return; } - // Call set Method in default Storage + // Call set method on default Storage const defaultStorage = this.getStorage(this.config.defaultStorageKey); defaultStorage?.set(storageItemKey, value); } - //========================================================================================================= - // Remove - //========================================================================================================= /** + * Removes the value at the specified Storage Item key + * from the defined external Storages (`storageKeys`). + * + * When no Storage has been specified, + * the value is removed from the default Storage + * * @internal - * Removes value at provided Key - * @param storageItemKey - Key of Storage property - * @param storageKeys - Key/Name of Storages where the Value gets removed (if not provided default Storage will be used) + * @param storageItemKey - Key/Name identifier of the value to be removed. + * @param storageKeys - Key/Name identifier of the external Storage + * from which the value is to be removed. */ public remove( storageItemKey: StorageItemKey, @@ -221,35 +238,34 @@ export class Storages { return; } - // Call remove Method in specific Storages + // Call remove method on specified Storages if (storageKeys) { for (const storageKey of storageKeys) this.getStorage(storageKey)?.remove(storageItemKey); return; } - // Call remove Method in default Storage + // Call remove method on default Storage const defaultStorage = this.getStorage(this.config.defaultStorageKey); defaultStorage?.remove(storageItemKey); } - //========================================================================================================= - // Has Storage - //========================================================================================================= /** + * Returns a boolean indicating whether any Storage + * has been registered or not. + * * @internal - * Check if at least one Storage got registered */ public hasStorage(): boolean { return notEqual(this.storages, {}); } - //========================================================================================================= - // Local Storage Available - //========================================================================================================= /** + * Returns a boolean indication whether the + * [Local Storage](https://developer.mozilla.org/de/docs/Web/API/Window/localStorage) + * is available in this environment. + * * @internal - * Checks if localStorage is available in this Environment */ static localStorageAvailable(): boolean { try { @@ -262,25 +278,28 @@ export class Storages { } } -/** - * @param localStorage - If Local Storage should be instantiated - * @param defaultStorage - Default Storage Key - */ export interface CreateStoragesConfigInterface { + /** + * Whether the Local Storage should be registered by default. + * @default true + */ localStorage?: boolean; + /** + * Storage key of the Storage that should be the default Storage. + */ defaultStorageKey?: StorageKey; } -/** - * @param defaultStorage - Default Storage Key - */ export interface StoragesConfigInterface { + /** + * Storage key of the Storage that should be the default Storage. + */ defaultStorageKey: StorageKey | null; } -/** - * @param default - If the registered Storage gets the default Storage - */ export interface RegisterConfigInterface { + /** + * Whether the to register Storage should get the default Storage. + */ default?: boolean; } diff --git a/packages/cra-template-agile-typescript/template.json b/packages/cra-template-agile-typescript/template.json index bac07209..21245663 100644 --- a/packages/cra-template-agile-typescript/template.json +++ b/packages/cra-template-agile-typescript/template.json @@ -3,18 +3,20 @@ "dependencies": { "@agile-ts/core": "^0.0.13", "@agile-ts/react": "^0.0.13", + "typescript": "^4.1.2", + "web-vitals": "^1.0.1" + }, + "devDependencies": { "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/node": "^12.0.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", - "@types/jest": "^26.0.15", - "typescript": "^4.1.2", - "web-vitals": "^1.0.1" + "@types/jest": "^26.0.15" }, "eslintConfig": { "extends": ["react-app", "react-app/jest"] } } -} \ No newline at end of file +} diff --git a/packages/cra-template-agile/template.json b/packages/cra-template-agile/template.json index 7371aa91..64697fb5 100644 --- a/packages/cra-template-agile/template.json +++ b/packages/cra-template-agile/template.json @@ -3,13 +3,15 @@ "dependencies": { "@agile-ts/core": "^0.0.13", "@agile-ts/react": "^0.0.13", + "web-vitals": "^1.0.1" + }, + "devDependencies": { "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", - "@testing-library/user-event": "^12.1.10", - "web-vitals": "^1.0.1" + "@testing-library/user-event": "^12.1.10" }, "eslintConfig": { "extends": ["react-app", "react-app/jest"] } } -} \ No newline at end of file +} From fa7ec0e072dfc72041cf492d9927b88373b559a8 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 17 Jun 2021 17:19:01 +0200 Subject: [PATCH 61/63] fixed typo --- .github/workflows/release.yaml | 2 +- packages/core/src/integrations/index.ts | 8 +- packages/core/src/integrations/integration.ts | 2 +- packages/core/src/storages/index.ts | 44 ++++--- packages/core/src/storages/storage.ts | 111 ++++++++++++------ 5 files changed, 109 insertions(+), 58 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ba0242fc..a46c1a84 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -31,4 +31,4 @@ jobs: title: Next Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} \ No newline at end of file + NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index 53c41b6b..e72df24c 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -28,7 +28,7 @@ export class Integrations { * Integrates the specified Integration into AgileTs * and sets it to ready when the binding was successful. * - * @internal + * @public * @param integration - Integration to be integrated into AgileTs. */ public async integrate(integration: Integration): Promise { @@ -59,7 +59,7 @@ export class Integrations { * In doing so, it calls the `updateMethod()` method * in all registered Integrations with the specified parameters. * - * @internal + * @public * @param componentInstance - Component Instance to be updated. * @param updatedData - Data object with updated data. */ @@ -76,9 +76,9 @@ export class Integrations { /** * Returns a boolean indicating whether any Integration - * has been registered or not. + * has been registered with the Agile Instance or not. * - * @internal + * @public */ public hasIntegration(): boolean { return this.integrations.size > 0; diff --git a/packages/core/src/integrations/integration.ts b/packages/core/src/integrations/integration.ts index f8f0b69b..f2c409ac 100644 --- a/packages/core/src/integrations/integration.ts +++ b/packages/core/src/integrations/integration.ts @@ -13,7 +13,7 @@ export class Integration { public methods: IntegrationMethods; /** - * An Integrations is an interface to a UI-Framework, + * An Integration is an interface to a UI-Framework, * and allows the easy interaction with that Framework. * * Due to the Integration, AgileTs can be integrated into almost any UI-Framework diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index ec746bb7..d3eaefb7 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -74,7 +74,7 @@ export class Storages { * and updates the Persistents that have already attempted * to use the now registered Storage. * - * @internal + * @public * @param storage - Storage to be registered with AgileTs. * @param config - Configuration object */ @@ -120,7 +120,8 @@ export class Storages { } /** - * Retrieves a single Storage with the specified key/name identifier from the Storages Class. + * Retrieves a single Storage with the specified key/name identifier + * from the Storages Class. * * If the to retrieve Storage doesn't exist, `undefined` is returned. * @@ -155,7 +156,7 @@ export class Storages { * When no Storage has been specified, * the value is retrieved from the default Storage. * - * @internal + * @public * @param storageItemKey - Key/Name identifier of the value to be retrieved. * @param storageKey - Key/Name identifier of the external Storage * from which the value is to be retrieved. @@ -187,9 +188,9 @@ export class Storages { * in the defined external Storages (`storageKeys`). * * When no Storage has been specified, - * the value is stored/updated in the default Storage + * the value is stored/updated in the default Storage. * - * @internal + * @public * @param storageItemKey - Key/Name identifier of the value to be stored. * @param value - Value to be stored in an external Storage. * @param storageKeys - Key/Name identifier of the external Storage @@ -222,9 +223,9 @@ export class Storages { * from the defined external Storages (`storageKeys`). * * When no Storage has been specified, - * the value is removed from the default Storage + * the value is removed from the default Storage. * - * @internal + * @public * @param storageItemKey - Key/Name identifier of the value to be removed. * @param storageKeys - Key/Name identifier of the external Storage * from which the value is to be removed. @@ -252,9 +253,9 @@ export class Storages { /** * Returns a boolean indicating whether any Storage - * has been registered or not. + * has been registered with the Agile Instance or not. * - * @internal + * @public */ public hasStorage(): boolean { return notEqual(this.storages, {}); @@ -265,7 +266,7 @@ export class Storages { * [Local Storage](https://developer.mozilla.org/de/docs/Web/API/Window/localStorage) * is available in this environment. * - * @internal + * @public */ static localStorageAvailable(): boolean { try { @@ -281,25 +282,40 @@ export class Storages { export interface CreateStoragesConfigInterface { /** * Whether the Local Storage should be registered by default. - * @default true + * @default false */ localStorage?: boolean; /** - * Storage key of the Storage that should be the default Storage. + * Key/Name identifier of the Storage to become the default Storage. + * + * When no specified Storage has been defined in methods like `get()`, `set()`, `remove()`, + * the default Storage will be used. + * + * @default undefined */ defaultStorageKey?: StorageKey; } export interface StoragesConfigInterface { /** - * Storage key of the Storage that should be the default Storage. + * Key/Name identifier of the Storage to become the default Storage. + * + * When no specified Storage has been defined in methods like `get()`, `set()`, `remove()`, + * the default Storage will be used. + * + * @default undefined */ defaultStorageKey: StorageKey | null; } export interface RegisterConfigInterface { /** - * Whether the to register Storage should get the default Storage. + * Whether the to register Storage should become the default Storage. + * + * When no specified Storage has been defined in methods like `get()`, `set()`, `remove()`, + * the default Storage will be used. + * + * @default false */ default?: boolean; } diff --git a/packages/core/src/storages/storage.ts b/packages/core/src/storages/storage.ts index f8751e4a..524b2d50 100644 --- a/packages/core/src/storages/storage.ts +++ b/packages/core/src/storages/storage.ts @@ -8,15 +8,24 @@ import { } from '../internal'; export class Storage { + public config: StorageConfigInterface; + + // Key/Name identifier of the Storage public key: StorageKey; + // Whether the Storage is ready and is able to persist values public ready = false; + // Methods to interact with the external Storage (get, set, remove) public methods: StorageMethodsInterface; - public config: StorageConfigInterface; /** + * An Storage is an interface to an external Storage, + * and allows the easy interaction with that Storage. + * + * Due to the Storage, AgileTs can easily persist its Instances in almost any Storage + * without a huge overhead. + * * @public - * Storage - Interface for storing Items permanently - * @param config - Config + * @param config - Configuration object */ constructor(config: CreateStorageConfigInterface) { config = defineConfig(config, { @@ -41,12 +50,11 @@ export class Storage { this.config.async = true; } - //========================================================================================================= - // Validate - //========================================================================================================= /** + * Returns a boolean indicating whether the Storage is valid + * and can be used to persist Instances in it or not. + * * @public - * Validates Storage Methods */ public validate(): boolean { if (!isFunction(this.methods?.get)) { @@ -64,20 +72,20 @@ export class Storage { return true; } - //========================================================================================================= - // Normal Get - //========================================================================================================= /** - * @internal - * Gets value at provided Key (normal) - * Note: Only use this if you are 100% sure this Storage doesn't work async - * @param key - Key of Storage property + * Synchronously retrieves the stored value + * at the specified Storage Item key from the Storage. + * + * When the retrieved value is a JSON-String it is parsed automatically. + * + * @public + * @param key - Key/Name identifier of the value to be retrieved. */ public normalGet(key: StorageItemKey): GetTpe | undefined { if (!this.ready || !this.methods.get) return undefined; if (isAsyncFunction(this.methods.get)) LogCodeManager.log('13:02:00'); - // Get Value + // Retrieve value const res = this.methods.get(this.getStorageKey(key)); const _res = isJsonString(res) ? JSON.parse(res) : res; @@ -91,22 +99,23 @@ export class Storage { return _res; } - //========================================================================================================= - // Async Get - //========================================================================================================= /** - * @internal - * Gets value at provided Key (async) - * @param key - Key of Storage property + * Asynchronously retrieves the stored value + * at the specified Storage Item key from the Storage. + * + * When the retrieved value is a JSON-String it is parsed automatically. + * + * @public + * @param key - Key/Name identifier of the value to be retrieved. */ public get(key: StorageItemKey): Promise { if (!this.ready || !this.methods.get) return Promise.resolve(undefined); - // Get Value in 'dummy' promise if get method isn't async + // Retrieve value from not promise based Storage if (!isAsyncFunction(this.methods.get)) return Promise.resolve(this.normalGet(key)); - // Get Value (async) + // Retrieve value from promise based Storage return new Promise((resolve, reject) => { this.methods ?.get(this.getStorageKey(key)) @@ -129,14 +138,12 @@ export class Storage { }); } - //========================================================================================================= - // Set - //========================================================================================================= /** + * Stores or updates the value at the specified Storage Item key in the Storage. + * * @public - * Saves/Updates value at provided Key - * @param key - Key of Storage property - * @param value - new Value that gets set + * @param key - Key/Name identifier of the value to be stored. + * @param value - Value to be stored. */ public set(key: StorageItemKey, value: any): void { if (!this.ready || !this.methods.set) return; @@ -151,13 +158,11 @@ export class Storage { this.methods.set(this.getStorageKey(key), JSON.stringify(value)); } - //========================================================================================================= - // Remove - //========================================================================================================= /** + * Removes the value at the specified Storage Item key from the Storage. + * * @public - * Removes value at provided Key - * @param key - Key of Storage property + * @param key - Key/Name identifier of the value to be removed. */ public remove(key: StorageItemKey): void { if (!this.ready || !this.methods.remove) return; @@ -171,12 +176,10 @@ export class Storage { this.methods.remove(this.getStorageKey(key)); } - //========================================================================================================= - // Get Storage Key - //========================================================================================================= /** + * Generates and returns a valid Storage key based on the specified key. + * * @internal - * Creates Storage Key from provided key * @param key - Key that gets converted into a Storage Key */ public getStorageKey(key: StorageItemKey): string { @@ -194,7 +197,15 @@ export type StorageItemKey = string | number; * @param methods - Storage methods like (get, set, remove) */ export interface CreateStorageConfigInterface extends StorageConfigInterface { + /** + * Key/Name identifier of the Storage + * @default undefined + */ key: string; + /** + * Storage methods for interacting with the external Storage. + * @default undefined + */ methods: StorageMethodsInterface; } @@ -204,8 +215,24 @@ export interface CreateStorageConfigInterface extends StorageConfigInterface { * @param remove - Remove Methods of Storage (removes items from storage) */ export interface StorageMethodsInterface { + /** + * Method to retrieve a value at the specified key from the external Storage. + * + * @param key - Key/Name identifier of the value to be retrieved. + */ get: (key: string) => any; + /** + * Method to store a value at the specified key in the external Storage. + * + * @param key - Key/Name identifier of the value to be stored. + * @param value - Value to be stored. + */ set: (key: string, value: any) => void; + /** + * Method to remove a value at the specified key from the external Storage. + * + * @param key - Key/Name identifier of the value to be removed. + */ remove: (key: string) => void; } @@ -214,6 +241,14 @@ export interface StorageMethodsInterface { * @param prefix - Prefix of Storage Property */ export interface StorageConfigInterface { + /** + * Whether the external Storage works async. + * @default Automatically detected via `isAsyncFunction()` + */ async?: boolean; + /** + * Prefix to be added before each persisted value key/name identifier. + * @default 'agile' + */ prefix?: string; } From e9c2b99afe6d815473bcf85b763b376c9d8aab75 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 17 Jun 2021 20:26:02 +0200 Subject: [PATCH 62/63] optimised descriptions in Persistent --- .../src/collection/collection.persistent.ts | 4 +- packages/core/src/collection/index.ts | 14 +- packages/core/src/runtime/observer.ts | 4 +- packages/core/src/state/index.ts | 12 +- packages/core/src/state/state.persistent.ts | 4 +- packages/core/src/storages/persistent.ts | 205 +++++++++++------- 6 files changed, 152 insertions(+), 91 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 6454ea63..1ed5abb9 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -74,7 +74,7 @@ export class CollectionPersistent< * @internal * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. * | default = Persistent.key | - * @return Whether the loading and the setting up of the side effects was successful. + * @return Whether the loading of the persisted value and the setting up of the side effects was successful. */ public async loadPersistedValue( storageItemKey?: PersistentKey @@ -180,7 +180,7 @@ export class CollectionPersistent< * @internal * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. * | default = Persistent.key | - * @return Whether the persisting and the setting up of the side effects was successful. + * @return Whether the persisting of the value and the setting up of the side effects was successful. */ public async persistValue(storageItemKey?: PersistentKey): Promise { if (!this.ready) return false; diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 8ac6a0e0..edf12d81 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -1091,7 +1091,7 @@ export class Collection { } /** - * Updates key/name identifier of the Item + * Updates the key/name identifier of the Item * and returns a boolean indicating * whether the Item identifier was updated successfully. * @@ -1629,14 +1629,16 @@ export interface CollectionPersistentConfigInterface { /** * Key/Name identifier of Storages * in which the Collection value should be or is persisted. - * @default [AgileTs default Storage key] + * @default [`defaultStorageKey`] */ storageKeys?: StorageKey[]; /** - * Default Storage key of the specified Storage keys. - * The Collection value is loaded from the default Storage - * and is only loaded from the remaining Storages (storageKeys) - * if the loading of the default Storage failed. + * Key/Name identifier of the default Storage of the specified Storage keys. + * + * The Collection value is loaded from the default Storage by default + * and is only loaded from the remaining Storages (`storageKeys`) + * if the loading from the default Storage failed. + * * @default first index of the specified Storage keys or the AgileTs default Storage key */ defaultStorageKey?: StorageKey; diff --git a/packages/core/src/runtime/observer.ts b/packages/core/src/runtime/observer.ts index eb76f80c..ebffc0fb 100644 --- a/packages/core/src/runtime/observer.ts +++ b/packages/core/src/runtime/observer.ts @@ -31,7 +31,7 @@ export class Observer { /** * An Observer manages the subscriptions to Subscription Containers (UI-Components) * and dependencies to other Observers (Agile Classes) - * for an Agile Class like the `State Class`. + * for an Agile Class such as the `State Class`. * * Agile Classes often use an Observer as an interface to the Runtime. * In doing so, they ingest their own Observer into the Runtime @@ -136,7 +136,7 @@ export class Observer { * * Note that this method should be overwritten * to correctly apply the changes to the Agile Class - * to which the Observer belongs. + * the Observer belongs to. * * @public * @param job - Runtime-Job to be performed. diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 63d73091..ac5cedaa 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -815,14 +815,16 @@ export interface StatePersistentConfigInterface { /** * Key/Name identifier of Storages * in which the State value should be or is persisted. - * @default [AgileTs default Storage key] + * @default [`defaultStorageKey`] */ storageKeys?: StorageKey[]; /** - * Default Storage key of the specified Storage keys. - * The State value is loaded from the default Storage - * and is only loaded from the remaining Storages (storageKeys) - * if the loading of the default Storage failed. + * Key/Name identifier of the default Storage of the specified Storage keys. + * + * The State value is loaded from the default Storage by default + * and is only loaded from the remaining Storages (`storageKeys`) + * if the loading from the default Storage failed. + * * @default first index of the specified Storage keys or the AgileTs default Storage key */ defaultStorageKey?: StorageKey; diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index d1a8243f..2b30165f 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -63,7 +63,7 @@ export class StatePersistent extends Persistent { * @internal * @param storageItemKey - Storage key of the persisted State Instance. * | default = Persistent.key | - * @return Whether the loading and the setting up of the side effects was successful. + * @return Whether the loading of the persisted value and the setting up of the side effects was successful. */ public async loadPersistedValue( storageItemKey?: PersistentKey @@ -99,7 +99,7 @@ export class StatePersistent extends Persistent { * @internal * @param storageItemKey - Storage key of the persisted State Instance. * | default = Persistent.key | - * @return Whether the persisting and the setting up of the side effects was successful. + * @return Whether the persisting of the value and the setting up of the side effects was successful. */ public async persistValue(storageItemKey?: PersistentKey): Promise { if (!this.ready) return false; diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index 6c521778..eaec9937 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -7,25 +7,35 @@ import { } from '../internal'; export class Persistent { + // Agile Instance the Persistent belongs to public agileInstance: () => Agile; public static placeHolderKey = '__THIS_IS_A_PLACEHOLDER__'; + public config: PersistentConfigInterface; + // Key/Name identifier of the Persistent public _key: PersistentKey; + // Whether the Persistent is ready and is allowed to persist values public ready = false; - public isPersisted = false; // If Value is stored in Agile Storage - public onLoad: ((success: boolean) => void) | undefined; // Gets called if PersistValue got loaded for the first Time + // Whether the Persistent value is stored in the corresponding Storage/s + public isPersisted = false; + // Callback that is called when the persisted value was loaded into the Persistent for the first time + public onLoad: ((success: boolean) => void) | undefined; - // Storages in which the Persisted Value is saved + // Key/Name identifier of the Storages the Persistent value is stored in public storageKeys: StorageKey[] = []; /** + * A Persistent manages the permanent persistence + * of an Agile Class such as the `State Class` in external Storages. + * + * Note that the Persistent itself is no standalone class + * and should be adapted to the Agile Class needs it belongs to. + * * @internal - * Persistent - Handles storing of Agile Instances - * Note: No stand alone class!! - * @param agileInstance - An instance of Agile - * @param config - Config + * @param agileInstance - Instance of Agile the Persistent belongs to. + * @param config - Configuration object */ constructor( agileInstance: Agile, @@ -71,7 +81,7 @@ export class Persistent { } /** - * Updates key/name identifier of Persistent. + * Updates the key/name identifier of the Persistent. * * @public * @param value - New key/name identifier. @@ -86,28 +96,29 @@ export class Persistent { const isValid = this.validatePersistent(); - // Try to initial load value if persistent wasn't ready before + // Try to initial load value if Persistent hasn't been ready before if (!wasReady) { if (isValid) await this.initialLoading(); return; } - // Remove persisted values with the old key + // Remove persisted value that is located at the old key await this.removePersistedValue(oldKey); - // Persist Collection values with the new key + // Persist value at the new key if (isValid) await this.persistValue(value); } - //========================================================================================================= - // Instantiate Persistent - //========================================================================================================= /** + * Instantiates the Persistent by assigning the specified Storage keys + * and validating the Persistent. + * + * This was moved out of the `constructor` + * because some classes that extend the Persistent need to configure some + * things before they can properly instantiate the parent Persistent. + * * @internal - * Instantiates this Class - * Note: Had to outsource it from the constructor because some extending classes - * have to define some stuff before being able to instantiate the parent (this) - * @param config - Config + * @param config - Configuration object */ public instantiatePersistent( config: InstantiatePersistentConfigInterface = {} @@ -117,29 +128,31 @@ export class Persistent { this.validatePersistent(); } - //========================================================================================================= - // Validate Persistent - //========================================================================================================= /** + * Returns a boolean indicating whether the Persistent was setup correctly + * and is able to persist a value permanently in an external Storage. + * + * Based on this tapped boolean value, + * the Persistent's `ready` property is updated. + * * @internal - * Validates Persistent and updates its 'ready' property */ public validatePersistent(): boolean { let isValid = true; - // Validate Key + // Validate Persistent key/name identifier if (this._key === Persistent.placeHolderKey) { LogCodeManager.log('12:03:00'); isValid = false; } - // Validate StorageKeys + // Validate Storage keys if (!this.config.defaultStorageKey || this.storageKeys.length <= 0) { LogCodeManager.log('12:03:01'); isValid = false; } - // Check if Storages exist + // Check if the Storages exist at the specified Storage keys this.storageKeys.map((key) => { if (!this.agileInstance().storages.storages[key]) { LogCodeManager.log('12:03:02', [this._key, key]); @@ -151,14 +164,14 @@ export class Persistent { return isValid; } - //========================================================================================================= - // Assign StorageKeys - //========================================================================================================= /** + * Assigns the specified Storage keys to the Persistent + * and overwrites the old ones. + * + * * @internal - * Assign new StorageKeys to Persistent and overwrite the old ones - * @param storageKeys - New Storage Keys - * @param defaultStorageKey - Key of default Storage + * @param storageKeys - Key/Name identifiers to be assigned. + * @param defaultStorageKey - Key/Name identifier of the default Storage. */ public assignStorageKeys( storageKeys: StorageKey[] = [], @@ -167,11 +180,13 @@ export class Persistent { const storages = this.agileInstance().storages; const _storageKeys = copy(storageKeys); - // Add passed default Storage Key to 'storageKeys' + // Assign specified default Storage key to the 'storageKeys' array if (defaultStorageKey && !_storageKeys.includes(defaultStorageKey)) _storageKeys.push(defaultStorageKey); - // Add default Storage of AgileTs to storageKeys and assign it as default Storage Key of Persistent if no storageKeys provided + // Assign default Storage of AgileTs to the `storageKeys' array + // and assign it as default Storage key of the Persistent + // if no valid 'storageKeys' were provided if (_storageKeys.length <= 0) { this.config.defaultStorageKey = storages.config.defaultStorageKey as any; _storageKeys.push(storages.config.defaultStorageKey as any); @@ -182,12 +197,10 @@ export class Persistent { this.storageKeys = _storageKeys; } - //========================================================================================================= - // Initial Loading - //========================================================================================================= /** + * Stores or loads the Persistent value from the external Storages for the first time. + * * @internal - * Loads/Saves Storage Value for the first Time */ public async initialLoading(): Promise { const success = await this.loadPersistedValue(); @@ -195,15 +208,17 @@ export class Persistent { if (!success) await this.persistValue(); } - //========================================================================================================= - // Load Value - //========================================================================================================= /** + * Loads the Persistent value from the corresponding Storage. + * + * Note that this method should be overwritten + * to correctly apply the changes to the Agile Class + * the Persistent belongs to. + * * @internal - * Loads Value from Storage - * @param storageItemKey - Storage key of the persisted Instance. + * @param storageItemKey - Storage key of the persisted value. * | default = Persistent.key | - * @return Success? + * @return Whether loading of the persisted value was successful. */ public async loadPersistedValue( storageItemKey?: PersistentKey @@ -212,30 +227,35 @@ export class Persistent { return false; } - //========================================================================================================= - // Update Value - //========================================================================================================= /** + * Persists the Persistent value in the corresponding Storage. + * + * Note that this method should be overwritten + * to correctly apply the changes to the Agile Class + * the Persistent belongs to. + * * @internal - * Saves/Updates Value in Storage - * @param storageItemKey - Storage key of the persisted Instance. + * @param storageItemKey - Storage key of the persisted value * | default = Persistent.key | - * @return Success? + * @return Whether the persisting of the value was successful. */ public async persistValue(storageItemKey?: PersistentKey): Promise { LogCodeManager.log('00:03:00', ['persistValue', 'Persistent']); return false; } - //========================================================================================================= - // Remove Value - //========================================================================================================= /** + * Removes the Persistent value from the corresponding Storage. + * -> Persistent value is no longer persisted + * + * Note that this method should be overwritten + * to correctly apply the changes to the Agile Class + * the Persistent belongs to. + * * @internal - * Removes Value form Storage - * @param storageItemKey - Storage key of the persisted Instance. + * @param storageItemKey - Storage key of the persisted value. * | default = Persistent.key | - * @return Success? + * @return Whether the removal of the persisted value was successful. */ public async removePersistedValue( storageItemKey?: PersistentKey @@ -244,13 +264,16 @@ export class Persistent { return false; } - //========================================================================================================= - // Format Key - //========================================================================================================= /** + * Formats the specified key so that it can be used as a valid Storage key + * and returns the formatted variant of it. + * + * Note that this method should be overwritten + * to correctly apply the changes to the Agile Class + * the Persistent belongs to. + * * @internal - * Validates Storage Key - * @param key - Key that gets validated + * @param key - Key to be formatted. */ public formatKey(key?: PersistentKey): PersistentKey | undefined { return key; @@ -259,33 +282,67 @@ export class Persistent { export type PersistentKey = string | number; -/** - * @param key - Key/Name of Persistent - * @param storageKeys - Keys of Storages in that the persisted Value gets saved - * @param defaultStorage - Default Storage Key - * @param instantiate - If Persistent gets Instantiated immediately - */ export interface CreatePersistentConfigInterface { + /** + * Key/Name identifier of the Persistent. + */ key?: PersistentKey; + /** + * Key/Name identifier of Storages + * in which the Persistent value should be or is persisted. + * @default [`defaultStorageKey`] + */ storageKeys?: StorageKey[]; + /** + * Key/Name identifier of the default Storage of the specified Storage keys. + * + * The Persistent value is loaded from the default Storage by default + * and is only loaded from the remaining Storages (`storageKeys) + * if the loading from the default Storage failed. + * + * @default first index of the specified Storage keys or the AgileTs default Storage key + */ defaultStorageKey?: StorageKey; + /** + * Whether the Persistent should be instantiated immediately + * or whether this should be done manually. + * @default true + */ instantiate?: boolean; } -/** - * @param defaultStorageKey - Default Storage Key - */ export interface PersistentConfigInterface { + /** + * Key/Name identifier of the default Storage of the specified Storage keys. + * + * The Persistent value is loaded from the default Storage by default + * and is only loaded from the remaining Storages (`storageKeys) + * if the loading from the default Storage failed. + * + * @default first index of the specified Storage keys or the AgileTs default Storage key + */ defaultStorageKey: StorageKey | null; } -/** - * @param key - Key/Name of Persistent - * @param storageKeys - Keys of Storages in that the persisted Value gets saved - * @param defaultStorageKey - Default Storage Key (if not provided it takes the first index of storageKeys or the AgileTs default Storage) - */ export interface InstantiatePersistentConfigInterface { + /** + * Key/Name identifier of the Persistent. + */ key?: PersistentKey; + /** + * Key/Name identifier of Storages + * in which the Persistent value should be or is persisted. + * @default [`defaultStorageKey`] + */ storageKeys?: StorageKey[]; + /** + * Key/Name identifier of the default Storage of the specified Storage keys. + * + * The Persistent value is loaded from the default Storage by default + * and is only loaded from the remaining Storages (`storageKeys) + * if the loading from the default Storage failed. + * + * @default first index of the specified Storage keys or the AgileTs default Storage key + */ defaultStorageKey?: StorageKey; } From 95948a74846bfdee172b69b35cae0756771cffbb Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 18 Jun 2021 20:44:09 +0200 Subject: [PATCH 63/63] fixed typos --- .../src/collection/collection.persistent.ts | 18 ++--- packages/core/src/integrations/index.ts | 2 +- packages/core/src/logCodeManager.ts | 37 +++++---- packages/core/src/state/state.persistent.ts | 21 +++-- packages/core/src/storages/index.ts | 66 +++++++++------- packages/core/src/storages/persistent.ts | 77 +++++++++++-------- packages/core/src/storages/storage.ts | 41 ++++------ packages/core/src/utils.ts | 59 +++++++------- 8 files changed, 169 insertions(+), 152 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 1ed5abb9..f7425ff1 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -72,9 +72,9 @@ export class CollectionPersistent< * the Storage value when the Collection (Instances) changes. * * @internal - * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. + * @param storageItemKey - Prefix Storage key of the to load Collection Instances. * | default = Persistent.key | - * @return Whether the loading of the persisted value and the setting up of the side effects was successful. + * @return Whether the loading of the persisted Collection Instances and setting up of the corresponding side effects was successful. */ public async loadPersistedValue( storageItemKey?: PersistentKey @@ -178,9 +178,9 @@ export class CollectionPersistent< * the Storage value when the Collection (Instances) changes. * * @internal - * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. + * @param storageItemKey - Prefix Storage key of the to persist Collection Instances. * | default = Persistent.key | - * @return Whether the persisting of the value and the setting up of the side effects was successful. + * @return Whether the persisting of the Collection Instances and the setting up of the corresponding side effects was successful. */ public async persistValue(storageItemKey?: PersistentKey): Promise { if (!this.ready) return false; @@ -229,7 +229,7 @@ export class CollectionPersistent< * with the Collection (Instances) value. * * @internal - * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. + * @param storageItemKey - Prefix Storage key of the to remove Collection Instances. * | default = Persistent.key | */ public setupSideEffects(storageItemKey?: PersistentKey): void { @@ -253,7 +253,7 @@ export class CollectionPersistent< * @internal * @param storageItemKey - Prefix Storage key of the persisted Collection Instances. * | default = Persistent.key | - * @return Whether the removal of the persisted values was successful. + * @return Whether the removal of the Collection Instances was successful. */ public async removePersistedValue( storageItemKey?: PersistentKey @@ -294,11 +294,11 @@ export class CollectionPersistent< * Formats the specified key so that it can be used as a valid Storage key * and returns the formatted variant of it. * - * If no formatable key (undefined/null) was provided, - * an attempt is made to use the Collection identifier key. + * If no formatable key (`undefined`/`null`) was provided, + * an attempt is made to use the Collection identifier key as Storage key. * * @internal - * @param key - Key to be formatted. + * @param key - Storage key to be formatted. */ public formatKey(key: StorageKey | undefined | null): StorageKey | undefined { if (key == null && this.collection()._key) return this.collection()._key; diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index e72df24c..038166db 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -10,7 +10,7 @@ export class Integrations { /** * The Integrations Class manages all Integrations for an Agile Instance * and provides an interface to easily update - * or invoke functions in all registered Integrations. + * and invoke functions in all registered Integrations. * * @internal * @param agileInstance - Instance of Agile the Integrations belongs to. diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 28694b93..9df75ad7 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -1,11 +1,19 @@ import { Agile } from './agile'; +// The Log Code Manager keeps track +// and manages all important Logs of AgileTs. +// +// How does the identification of Log Messages work? +// Let's take a look at this example: // 00:00:00 +// // |00|:00:00 first digits are based on the Agile Class // 00 = General // 10 = Agile // 11 = Storage // .. +// +// --- // 00:|00|:00 second digits are based on the Log Type const logCodeTypes = { '00': 'success', @@ -13,6 +21,8 @@ const logCodeTypes = { '02': 'warn', '03': 'error', }; +// +// --- // 00:00:|00| third digits are based on the Log Message (ascending counted) const logCodeMessages = { @@ -161,15 +171,13 @@ const logCodeMessages = { '00:03:01': "'${0}' has to be of the type ${1}!", }; -//========================================================================================================= -// Get Log -//========================================================================================================= /** + * Returns the log message according to the specified log code. + * * @internal - * Returns the log message according to the passed logCode - * @param logCode - Log Code of Message + * @param logCode - Log code of the message to be returned. * @param replacers - Instances that replace these '${x}' placeholders based on the index - * For example: replacers[0] replaces '${0}', replacers[1] replaces '${1}', ... + * For example: 'replacers[0]' replaces '${0}', 'replacers[1]' replaces '${1}', .. */ function getLog>( logCode: T, @@ -185,16 +193,15 @@ function getLog>( return result; } -//========================================================================================================= -// Log -//========================================================================================================= /** + * Logs the log message according to the specified log code + * with the Agile Logger. + * * @internal - * Logs message at the provided logCode with the Agile.logger - * @param logCode - Log Code of Message + * @param logCode - Log code of the message to be returned. * @param replacers - Instances that replace these '${x}' placeholders based on the index - * For example: replacers[0] replaces '${0}', replacers[1] replaces '${1}', .. - * @param data - Data attached to the end of the log message + * For example: 'replacers[0]' replaces '${0}', 'replacers[1]' replaces '${1}', .. + * @param data - Data to be attached to the end of the log message. */ function log>( logCode: T, @@ -207,8 +214,10 @@ function log>( } /** + * The Log Code Manager keeps track + * and manages all important Logs of AgileTs. + * * @internal - * Manages logCode based logging of AgileTs */ export const LogCodeManager = { getLog, diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index 2b30165f..03029ff0 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -61,9 +61,9 @@ export class StatePersistent extends Persistent { * the Storage value when the State changes. * * @internal - * @param storageItemKey - Storage key of the persisted State Instance. + * @param storageItemKey - Storage key of the to load State Instance. * | default = Persistent.key | - * @return Whether the loading of the persisted value and the setting up of the side effects was successful. + * @return Whether the loading of the persisted State Instance and the setting up of the corresponding side effects was successful. */ public async loadPersistedValue( storageItemKey?: PersistentKey @@ -97,9 +97,9 @@ export class StatePersistent extends Persistent { * the Storage value when the State changes. * * @internal - * @param storageItemKey - Storage key of the persisted State Instance. + * @param storageItemKey - Storage key of the to persist State Instance. * | default = Persistent.key | - * @return Whether the persisting of the value and the setting up of the side effects was successful. + * @return Whether the persisting of the State Instance and setting up of the corresponding side effects was successful. */ public async persistValue(storageItemKey?: PersistentKey): Promise { if (!this.ready) return false; @@ -126,9 +126,6 @@ export class StatePersistent extends Persistent { */ public setupSideEffects(storageItemKey?: PersistentKey) { const _storageItemKey = storageItemKey ?? this._key; - - // Add side effect to the State - // that updates the Storage value based on the current State value this.state().addSideEffect( StatePersistent.storeValueSideEffectKey, (instance, config) => { @@ -143,9 +140,9 @@ export class StatePersistent extends Persistent { * -> State is no longer persisted * * @internal - * @param storageItemKey - Storage key of the persisted State Instance. + * @param storageItemKey - Storage key of the to remove State Instance. * | default = Persistent.key | - * @return Whether the removal of the persisted value was successful. + * @return Whether the removal of the persisted State Instance was successful. */ public async removePersistedValue( storageItemKey?: PersistentKey @@ -162,11 +159,11 @@ export class StatePersistent extends Persistent { * Formats the specified key so that it can be used as a valid Storage key * and returns the formatted variant of it. * - * If no formatable key (undefined/null) was provided, - * an attempt is made to use the State identifier key. + * If no formatable key (`undefined`/`null`) was provided, + * an attempt is made to use the State identifier key as Storage key. * * @internal - * @param key - Key to be formatted. + * @param key - Storage key to be formatted. */ public formatKey( key: PersistentKey | undefined | null diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index d3eaefb7..552a96b7 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -17,12 +17,13 @@ export class Storages { // Registered Storages public storages: { [key: string]: Storage } = {}; - // Persistent from Instances that were persisted + // Persistent from Instances (for example States) that were persisted public persistentInstances: Set = new Set(); /** * The Storages Class manages all external Storages for an Agile Instance - * and provides an interface to easily store, load and remove values from multiple Storages at once. + * and provides an interface to easily store, + * load and remove values from multiple Storages at once. * * @internal * @param agileInstance - Instance of Agile the Storages belongs to. @@ -45,18 +46,15 @@ export class Storages { * Instantiates and registers the * [Local Storage](https://developer.mozilla.org/de/docs/Web/API/Window/localStorage). * - * Note that this only works in a web environment! + * Note that the Local Storage is only available in a web environment. * * @internal */ public instantiateLocalStorage(): boolean { - // Check if Local Storage is available in this environment if (!Storages.localStorageAvailable()) { LogCodeManager.log('11:02:00'); return false; } - - // Create and register Local Storage const _localStorage = new Storage({ key: 'localStorage', async: false, @@ -71,8 +69,8 @@ export class Storages { /** * Registers the specified Storage with AgileTs - * and updates the Persistents that have already attempted - * to use the now registered Storage. + * and updates the Persistent Instances that have already attempted + * to use the previously unregistered Storage. * * @public * @param storage - Storage to be registered with AgileTs. @@ -90,7 +88,7 @@ export class Storages { return false; } - // Assign first added Storage as default Storage + // Assign Storage as default Storage if it is the first one added if (!hasRegisteredAnyStorage && config.default === false) LogCodeManager.log('11:02:01'); if (!hasRegisteredAnyStorage) config.default = true; @@ -100,15 +98,16 @@ export class Storages { if (config.default) this.config.defaultStorageKey = storage.key; this.persistentInstances.forEach((persistent) => { - // Revalidate Persistent that includes the newly registered storage key + // Revalidate Persistent, which contains key/name identifier of the newly registered Storage if (persistent.storageKeys.includes(storage.key)) { const isValid = persistent.validatePersistent(); if (isValid) persistent.initialLoading(); return; } - // If Persistent has no default storage key - // (reassign storage keys since this registered Storage might be tagged as default Storage) + // If Persistent has no default Storage key, + // reassign Storage keys since the now registered Storage + // might be tagged as default Storage of AgileTs if (persistent.config.defaultStorageKey == null) { persistent.assignStorageKeys(); const isValid = persistent.validatePersistent(); @@ -133,19 +132,14 @@ export class Storages { ): Storage | undefined { if (!storageKey) return undefined; const storage = this.storages[storageKey]; - - // Check if Storage exists if (!storage) { LogCodeManager.log('11:03:01', [storageKey]); return undefined; } - - // Check if Storage is ready if (!storage.ready) { LogCodeManager.log('11:03:02', [storageKey]); return undefined; } - return storage; } @@ -207,7 +201,7 @@ export class Storages { } // Call set method on specified Storages - if (storageKeys) { + if (storageKeys != null) { for (const storageKey of storageKeys) this.getStorage(storageKey)?.set(storageItemKey, value); return; @@ -264,7 +258,7 @@ export class Storages { /** * Returns a boolean indication whether the * [Local Storage](https://developer.mozilla.org/de/docs/Web/API/Window/localStorage) - * is available in this environment. + * is available in the current environment. * * @public */ @@ -281,15 +275,21 @@ export class Storages { export interface CreateStoragesConfigInterface { /** - * Whether the Local Storage should be registered by default. + * Whether to register the Local Storage by default. + * Note that the Local Storage is only available in a web environment. * @default false */ localStorage?: boolean; /** - * Key/Name identifier of the Storage to become the default Storage. + * Key/Name identifier of the default Storage. + * + * The default Storage represents the default Storage of the Storages Class, + * on which executed actions are performed if no specific Storage was specified. * - * When no specified Storage has been defined in methods like `get()`, `set()`, `remove()`, - * the default Storage will be used. + * Also, the persisted value is loaded from the default Storage by default, + * since only one persisted value can be applied. + * If the loading of the value from the default Storage failed, + * an attempt is made to load the value from the remaining Storages. * * @default undefined */ @@ -298,10 +298,15 @@ export interface CreateStoragesConfigInterface { export interface StoragesConfigInterface { /** - * Key/Name identifier of the Storage to become the default Storage. + * Key/Name identifier of the default Storage. * - * When no specified Storage has been defined in methods like `get()`, `set()`, `remove()`, - * the default Storage will be used. + * The default Storage represents the default Storage of the Storages Class, + * on which executed actions are performed if no specific Storage was specified. + * + * Also, the persisted value is loaded from the default Storage by default, + * since only one persisted value can be applied. + * If the loading of the value from the default Storage failed, + * an attempt is made to load the value from the remaining Storages. * * @default undefined */ @@ -312,8 +317,13 @@ export interface RegisterConfigInterface { /** * Whether the to register Storage should become the default Storage. * - * When no specified Storage has been defined in methods like `get()`, `set()`, `remove()`, - * the default Storage will be used. + * The default Storage represents the default Storage of the Storages Class, + * on which executed actions are performed if no specific Storage was specified. + * + * Also, the persisted value is loaded from the default Storage by default, + * since only one persisted value can be applied. + * If the loading of the value from the default Storage failed, + * an attempt is made to load the value from the remaining Storages. * * @default false */ diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index eaec9937..a335d982 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -16,9 +16,10 @@ export class Persistent { // Key/Name identifier of the Persistent public _key: PersistentKey; - // Whether the Persistent is ready and is allowed to persist values + // Whether the Persistent is ready + // and is able to persist values in an external Storage public ready = false; - // Whether the Persistent value is stored in the corresponding Storage/s + // Whether the Persistent value is stored in a corresponding external Storage/s public isPersisted = false; // Callback that is called when the persisted value was loaded into the Persistent for the first time public onLoad: ((success: boolean) => void) | undefined; @@ -110,11 +111,11 @@ export class Persistent { } /** - * Instantiates the Persistent by assigning the specified Storage keys - * and validating the Persistent. + * Instantiates the Persistent by assigning the specified Storage keys to it + * and validating it to make sure everything was setup correctly. * - * This was moved out of the `constructor` - * because some classes that extend the Persistent need to configure some + * This was moved out of the `constructor()` + * because some classes (that extend the Persistent) need to configure some * things before they can properly instantiate the parent Persistent. * * @internal @@ -132,7 +133,7 @@ export class Persistent { * Returns a boolean indicating whether the Persistent was setup correctly * and is able to persist a value permanently in an external Storage. * - * Based on this tapped boolean value, + * Based on the tapped boolean value, * the Persistent's `ready` property is updated. * * @internal @@ -165,12 +166,14 @@ export class Persistent { } /** - * Assigns the specified Storage keys to the Persistent - * and overwrites the old ones. + * Assigns the specified Storage identifiers to the Persistent + * and extracts the default Storage if necessary. * + * When no Storage key was provided the default Storage + * of the Agile Instance is applied to the Persistent. * * @internal - * @param storageKeys - Key/Name identifiers to be assigned. + * @param storageKeys - Key/Name identifier of the Storages to be assigned. * @param defaultStorageKey - Key/Name identifier of the default Storage. */ public assignStorageKeys( @@ -184,21 +187,22 @@ export class Persistent { if (defaultStorageKey && !_storageKeys.includes(defaultStorageKey)) _storageKeys.push(defaultStorageKey); - // Assign default Storage of AgileTs to the `storageKeys' array - // and assign it as default Storage key of the Persistent - // if no valid 'storageKeys' were provided + // Assign the default Storage key of the Agile Instance to the 'storageKeys' array + // and specify it as the Persistent's default Storage key + // if no valid Storage key was provided if (_storageKeys.length <= 0) { this.config.defaultStorageKey = storages.config.defaultStorageKey as any; _storageKeys.push(storages.config.defaultStorageKey as any); } else { - this.config.defaultStorageKey = defaultStorageKey || _storageKeys[0]; + this.config.defaultStorageKey = defaultStorageKey ?? _storageKeys[0]; } this.storageKeys = _storageKeys; } /** - * Stores or loads the Persistent value from the external Storages for the first time. + * Stores or loads the Persistent value + * from the external Storages for the first time. * * @internal */ @@ -216,9 +220,9 @@ export class Persistent { * the Persistent belongs to. * * @internal - * @param storageItemKey - Storage key of the persisted value. + * @param storageItemKey - Storage key of the to load value. * | default = Persistent.key | - * @return Whether loading of the persisted value was successful. + * @return Whether the loading of the persisted value was successful. */ public async loadPersistedValue( storageItemKey?: PersistentKey @@ -235,7 +239,7 @@ export class Persistent { * the Persistent belongs to. * * @internal - * @param storageItemKey - Storage key of the persisted value + * @param storageItemKey - Storage key of the to persist value * | default = Persistent.key | * @return Whether the persisting of the value was successful. */ @@ -253,7 +257,7 @@ export class Persistent { * the Persistent belongs to. * * @internal - * @param storageItemKey - Storage key of the persisted value. + * @param storageItemKey - Storage key of the to remove value. * | default = Persistent.key | * @return Whether the removal of the persisted value was successful. */ @@ -273,7 +277,7 @@ export class Persistent { * the Persistent belongs to. * * @internal - * @param key - Key to be formatted. + * @param key - Storage key to be formatted. */ public formatKey(key?: PersistentKey): PersistentKey | undefined { return key; @@ -289,18 +293,20 @@ export interface CreatePersistentConfigInterface { key?: PersistentKey; /** * Key/Name identifier of Storages - * in which the Persistent value should be or is persisted. + * in which the Persistent value is to be persisted + * or is already persisted. * @default [`defaultStorageKey`] */ storageKeys?: StorageKey[]; /** * Key/Name identifier of the default Storage of the specified Storage keys. * - * The Persistent value is loaded from the default Storage by default - * and is only loaded from the remaining Storages (`storageKeys) - * if the loading from the default Storage failed. + * The persisted value is loaded from the default Storage by default, + * since only one persisted value can be applied. + * If the loading of the value from the default Storage failed, + * an attempt is made to load the value from the remaining Storages. * - * @default first index of the specified Storage keys or the AgileTs default Storage key + * @default first index of the specified Storage keys or the Agile Instance's default Storage key */ defaultStorageKey?: StorageKey; /** @@ -315,11 +321,12 @@ export interface PersistentConfigInterface { /** * Key/Name identifier of the default Storage of the specified Storage keys. * - * The Persistent value is loaded from the default Storage by default - * and is only loaded from the remaining Storages (`storageKeys) - * if the loading from the default Storage failed. + * The persisted value is loaded from the default Storage by default, + * since only one persisted value can be applied. + * If the loading of the value from the default Storage failed, + * an attempt is made to load the value from the remaining Storages. * - * @default first index of the specified Storage keys or the AgileTs default Storage key + * @default first index of the specified Storage keys or the Agile Instance's default Storage key */ defaultStorageKey: StorageKey | null; } @@ -331,18 +338,20 @@ export interface InstantiatePersistentConfigInterface { key?: PersistentKey; /** * Key/Name identifier of Storages - * in which the Persistent value should be or is persisted. + * in which the Persistent value is to be persisted + * or is already persisted. * @default [`defaultStorageKey`] */ storageKeys?: StorageKey[]; /** * Key/Name identifier of the default Storage of the specified Storage keys. * - * The Persistent value is loaded from the default Storage by default - * and is only loaded from the remaining Storages (`storageKeys) - * if the loading from the default Storage failed. + * The persisted value is loaded from the default Storage by default, + * since only one persisted value can be applied. + * If the loading of the value from the default Storage failed, + * an attempt is made to load the value from the remaining Storages. * - * @default first index of the specified Storage keys or the AgileTs default Storage key + * @default first index of the specified Storage keys or the Agile Instance's default Storage key */ defaultStorageKey?: StorageKey; } diff --git a/packages/core/src/storages/storage.ts b/packages/core/src/storages/storage.ts index 524b2d50..894a2a6d 100644 --- a/packages/core/src/storages/storage.ts +++ b/packages/core/src/storages/storage.ts @@ -12,13 +12,13 @@ export class Storage { // Key/Name identifier of the Storage public key: StorageKey; - // Whether the Storage is ready and is able to persist values + // Whether the Storage is ready and is able to store values public ready = false; // Methods to interact with the external Storage (get, set, remove) public methods: StorageMethodsInterface; /** - * An Storage is an interface to an external Storage, + * A Storage is an interface to an external Storage, * and allows the easy interaction with that Storage. * * Due to the Storage, AgileTs can easily persist its Instances in almost any Storage @@ -74,7 +74,7 @@ export class Storage { /** * Synchronously retrieves the stored value - * at the specified Storage Item key from the Storage. + * at the specified Storage Item key from the external Storage. * * When the retrieved value is a JSON-String it is parsed automatically. * @@ -101,7 +101,7 @@ export class Storage { /** * Asynchronously retrieves the stored value - * at the specified Storage Item key from the Storage. + * at the specified Storage Item key from the external Storage. * * When the retrieved value is a JSON-String it is parsed automatically. * @@ -139,10 +139,11 @@ export class Storage { } /** - * Stores or updates the value at the specified Storage Item key in the Storage. + * Stores or updates the value at the specified Storage Item key + * in the external Storage. * * @public - * @param key - Key/Name identifier of the value to be stored. + * @param key - Key/Name identifier of the value to be stored or updated. * @param value - Value to be stored. */ public set(key: StorageItemKey, value: any): void { @@ -159,7 +160,8 @@ export class Storage { } /** - * Removes the value at the specified Storage Item key from the Storage. + * Removes the value at the specified Storage Item key + * from the external Storage. * * @public * @param key - Key/Name identifier of the value to be removed. @@ -180,7 +182,7 @@ export class Storage { * Generates and returns a valid Storage key based on the specified key. * * @internal - * @param key - Key that gets converted into a Storage Key + * @param key - Key to be converted into a valid Storage key. */ public getStorageKey(key: StorageItemKey): string { return this.config.prefix @@ -192,13 +194,9 @@ export class Storage { export type StorageKey = string | number; export type StorageItemKey = string | number; -/** - * @param key - Key/Name of Storage - * @param methods - Storage methods like (get, set, remove) - */ export interface CreateStorageConfigInterface extends StorageConfigInterface { /** - * Key/Name identifier of the Storage + * Key/Name identifier of the Storage. * @default undefined */ key: string; @@ -209,11 +207,6 @@ export interface CreateStorageConfigInterface extends StorageConfigInterface { methods: StorageMethodsInterface; } -/** - * @param get - Get Method of Storage (gets items from storage) - * @param set - Set Method of Storage (saves/updates items in storage) - * @param remove - Remove Methods of Storage (removes items from storage) - */ export interface StorageMethodsInterface { /** * Method to retrieve a value at the specified key from the external Storage. @@ -222,9 +215,9 @@ export interface StorageMethodsInterface { */ get: (key: string) => any; /** - * Method to store a value at the specified key in the external Storage. + * Method to store or update a value at the specified key in the external Storage. * - * @param key - Key/Name identifier of the value to be stored. + * @param key - Key/Name identifier of the value to be stored or updated. * @param value - Value to be stored. */ set: (key: string, value: any) => void; @@ -236,14 +229,10 @@ export interface StorageMethodsInterface { remove: (key: string) => void; } -/** - * @param async - If its an async storage - * @param prefix - Prefix of Storage Property - */ export interface StorageConfigInterface { /** - * Whether the external Storage works async. - * @default Automatically detected via `isAsyncFunction()` + * Whether the external Storage represented by the Storage Class works async. + * @default Automatically detected via the `isAsyncFunction()` method */ async?: boolean; /** diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index d998f602..a36692dd 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -7,18 +7,17 @@ import { LogCodeManager, } from './internal'; -//========================================================================================================= -// Get Agile Instance -//========================================================================================================= /** + * Extracts an Instance of Agile from the specified Instance. + * When no valid Agile Instance was found, + * it returns the global bound Agile Instance or `undefined`. + * * @internal - * Tries to get an Instance of Agile from provided Instance - * If no agileInstance found it returns the global bound Agile Instance - * @param instance - Instance that might hold an Agile Instance + * @param instance - Instance to extract the Agile Instance from. */ export function getAgileInstance(instance: any): Agile | undefined { try { - // Try to get agileInstance from passed Instance + // Try to get Agile Instance from specified Instance if (instance) { const _agileInstance = isFunction(instance['agileInstance']) ? instance['agileInstance']() @@ -26,7 +25,7 @@ export function getAgileInstance(instance: any): Agile | undefined { if (_agileInstance) return _agileInstance; } - // Return global bound agileInstance + // Return global bound Agile Instance return globalThis[Agile.globalKey]; } catch (e) { LogCodeManager.log('20:03:00', [], instance); @@ -35,13 +34,11 @@ export function getAgileInstance(instance: any): Agile | undefined { return undefined; } -//========================================================================================================= -// Extract Observers -//========================================================================================================= /** + * Extracts the Observers from the specified Instances. + * * @internal - * Extract Observers from specific Instances - * @param instances - Instances that will be formatted + * @param instances - Instances to extract the Observers from. */ export function extractObservers(instances: any): Array { const instancesArray: Array = []; @@ -51,13 +48,16 @@ export function extractObservers(instances: any): Array { // Get Observers from Instances for (const instance of tempInstancesArray) { - // If Instance is undefined (We have to add undefined to build a proper return value in for instance 'useAgile' later) - if (!instance) { + // If the Instance equals to 'undefined' + // (We have to add 'undefined' to the return value + // in order to properly build the return value of, + // for example, the 'useAgile()' hook later) + if (instance == null) { instancesArray.push(undefined); continue; } - // If Instance is Collection + // If the Instance equals to a Collection if (instance instanceof Collection) { instancesArray.push( instance.getGroupWithReference(instance.config.defaultGroupKey).observer @@ -65,36 +65,40 @@ export function extractObservers(instances: any): Array { continue; } - // If Instance has property that is an Observer + // If the Instance contains a property that is an Observer if (instance['observer'] && instance['observer'] instanceof Observer) { instancesArray.push(instance['observer']); continue; } - // If Instance is Observer + // If the Instance equals to an Observer if (instance instanceof Observer) { instancesArray.push(instance); continue; } - // Push undefined if no Observer could be found (We have to add undefined to build a proper return value in for instance 'useAgile' later) + // Push 'undefined' if no valid Observer was found + // (We have to add 'undefined' to the return value + // in order to properly build the return value of, + // for example, the 'useAgile()' hook later) instancesArray.push(undefined); } return instancesArray; } -//========================================================================================================= -// Global Bind -//========================================================================================================= /** - * @internal - * Binds passed Instance globally at passed Key + * Binds the specified Instance globally at the provided key identifier. + * + * Learn more about global bound instances: * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis * https://blog.logrocket.com/what-is-globalthis-why-use-it/ - * @param key - Key/Name of Instance - * @param instance - Instance - * @param overwrite - If already existing instance at passed Key gets overwritten + * + * @public + * @param key - Key/Name identifier of the specified Instance. + * @param instance - Instance to be bound globally. + * @param overwrite - When already an Instance exists globally at the specified key, + * whether to overwrite it with the new Instance. */ export function globalBind( key: string, @@ -106,7 +110,6 @@ export function globalBind( globalThis[key] = instance; return true; } - if (globalThis[key] == null) { globalThis[key] = instance; return true;