From d9249255d6e352e2b1d8a21aab9189d5a46f3c48 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 6 Jun 2021 16:29:36 +0200 Subject: [PATCH 01/24] 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 02/24] 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 03/24] 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 04/24] 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 05/24] 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 06/24] 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 07/24] 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 08/24] 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 09/24] 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 10/24] 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 11/24] 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 12/24] 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 13/24] 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 14/24] 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 15/24] 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 16/24] 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 17/24] 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 18/24] 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 19/24] 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 20/24] 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 21/24] 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 22/24] 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 23/24] 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 24/24] 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, ]);