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/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..cf448f46 100644
--- a/examples/react/develop/functional-component-ts/src/core/index.ts
+++ b/examples/react/develop/functional-component-ts/src/core/index.ts
@@ -1,4 +1,4 @@
-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 = {};
@@ -86,7 +86,7 @@ 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.update('id1', { key: 'id1Updated', name: 'testUpdated' });
MY_COLLECTION.getGroup('myGroup')?.persist({
followCollectionPersistKeyPattern: true,
});
@@ -94,6 +94,11 @@ 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));
export const MY_EVENT = new Event<{ name: string }>(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/examples/vue/develop/my-project/src/core.js b/examples/vue/develop/my-project/src/core.js
index fe4efb2f..ecb19e8b 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,14 @@ 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' }],
+ selectors: [1],
}).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/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/collection/index.ts b/packages/core/src/collection/index.ts
index 4e944901..8ac6a0e0 100644
--- a/packages/core/src/collection/index.ts
+++ b/packages/core/src/collection/index.ts
@@ -85,17 +85,20 @@ 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();
}
@@ -1486,12 +1489,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/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/index.ts b/packages/core/src/runtime/index.ts
index e3b1c409..ec0b841d 100644
--- a/packages/core/src/runtime/index.ts
+++ b/packages/core/src/runtime/index.ts
@@ -6,75 +6,109 @@ 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 being performed
public currentJob: RuntimeJob | null = null;
+ // Jobs to be performed
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 re-render
+ public jobsToRerender: Array = [];
+ // 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 `jobQueue` is currently being actively processed
+ public isPerformingJobs = false;
/**
+ * 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 queued Job is executed when it is its turn
+ * by calling the Job Observer's `perform()` method.
+ *
+ * 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 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
- * 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
- //=========================================================================================================
/**
- * @internal
- * Ingests Job into Runtime that gets performed
- * @param job - Job
- * @param config - Config
+ * Adds the specified Observer-based Job to the internal Job queue,
+ * where it is executed when it is its turn.
+ *
+ * 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 added to the Job queue.
+ * @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 assigns it to the re-render queue if necessary.
+ *
+ * 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 `jobQueue` is performed.
+ * - If not, the `jobsToRerender` queue is 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 therefore have properly changed too
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 +116,14 @@ 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 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();
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,50 +133,63 @@ export class Runtime {
}
}
- //=========================================================================================================
- // Update Subscribers
- //=========================================================================================================
/**
+ * 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 (UI-Component) was updated (re-rendered) or not.
+ *
* @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)
*/
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;
-
- // 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)
- 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
- jobsToRerender.forEach((job) => {
+ if (!this.agileInstance().hasIntegration() || jobsToRerender.length <= 0)
+ return false;
+
+ // 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 re-render on the UI-Component they represent)
+ this.updateSubscriptionContainer(subscriptionContainerToUpdate);
+
+ return true;
+ }
+
+ /**
+ * 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.
+ */
+ public extractToUpdateSubscriptionContainer(
+ jobs: Array
+ ): Array {
+ const subscriptionsToUpdate = new Set();
+
+ jobs.forEach((job) => {
job.subscriptionContainersToUpdate.forEach((subscriptionContainer) => {
+ let updateSubscriptionContainer = true;
+
+ // Handle not ready Subscription Container
if (!subscriptionContainer.ready) {
if (
- !job.config.numberOfTriesToUpdate ||
- job.triesToUpdate < job.config.numberOfTriesToUpdate
+ !job.config.maxTriesToUpdate ||
+ job.triedToUpdateCount < job.config.maxTriesToUpdate
) {
- job.triesToUpdate++;
+ job.triedToUpdateCount++;
this.notReadyJobsToRerender.add(job);
-
LogCodeManager.log(
'16:02:00',
[subscriptionContainer.key],
@@ -148,165 +198,145 @@ export class Runtime {
} else {
LogCodeManager.log(
'16:02:01',
- [job.config.numberOfTriesToUpdate],
+ [job.config.maxTriesToUpdate],
subscriptionContainer
);
}
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;
-
- if (updateSubscriptionContainer)
+ // 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 equal! (-> changed properties might 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;
+
+ // 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);
subscriptionsToUpdate.add(subscriptionContainer);
+ }
job.subscriptionContainersToUpdate.delete(subscriptionContainer);
});
});
- if (subscriptionsToUpdate.size <= 0) return false;
+ return Array.from(subscriptionsToUpdate);
+ }
- // Update Subscription Containers (trigger rerender on subscribed Component)
+ /**
+ * Updates the specified Subscription Containers.
+ *
+ * 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.
+ */
+ public updateSubscriptionContainer(
+ subscriptionsToUpdate: Array
+ ): void {
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,
- this.getObjectBasedProps(subscriptionContainer)
+ this.getUpdatedObserverValues(subscriptionContainer)
);
+
+ subscriptionContainer.updatedSubscribers.clear();
});
Agile.logger.if
.tag(['runtime'])
.info(LogCodeManager.getLog('16:01:02'), subscriptionsToUpdate);
-
- return true;
}
- //=========================================================================================================
- // Handle Object Based Subscription
- //=========================================================================================================
/**
+ * Maps the values of the updated Observers (`updatedSubscribers`)
+ * 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
- * 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
+ * @param subscriptionContainer - Subscription Container from which the `updatedSubscribers` are to be mapped into a key map.
*/
- 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 = [];
+ for (const observer of subscriptionContainer.updatedSubscribers) {
+ const key =
+ subscriptionContainer.subscriberKeysWeakMap.get(observer) ??
+ observer.key;
+ if (key != null) props[key] = observer.value;
+ }
return props;
}
- //=========================================================================================================
- // Handle Proxy Based Subscription
- //=========================================================================================================
/**
+ * 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 (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
- * Checks if the subscriptionContainer should be updated.
- * 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
+ * @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 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++;
- }
-
- // 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;
- }
- }
+ 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 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
+ const previousValue = job.observer.previousValue;
+ const newValue = job.observer.value;
+ for (const selectorMethod of selectorMethods) {
+ if (
+ notEqual(selectorMethod(newValue), selectorMethod(previousValue))
+ // || newValueDeepness !== previousValueDeepness // Not possible to check the object deepness
+ )
+ return true;
}
return false;
}
}
-/**
- * @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/observer.ts b/packages/core/src/runtime/observer.ts
index 5710bd18..eb76f80c 100644
--- a/packages/core/src/runtime/observer.ts
+++ b/packages/core/src/runtime/observer.ts
@@ -7,25 +7,54 @@ import {
IngestConfigInterface,
CreateRuntimeJobConfigInterface,
LogCodeManager,
+ generateId,
} from '../internal';
export type ObserverKey = string | number;
export class Observer {
+ // Agile Instance the Observer belongs to
public agileInstance: () => Agile;
+ // Key/Name identifier of the Observer
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 (UI-Components) the Observer is subscribed to
+ public subscribedTo: Set = new Set();
+
+ // Current value of the Observer
+ public value?: ValueType;
+ // Previous value of the Observer
+ public previousValue?: ValueType;
/**
+ * 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`.
+ *
+ * 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 other Observers.
+ *
+ * After the Observer has been ingested into the Runtime
+ * 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 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)
+ * of the Observer are updated (re-rendered).
+ *
+ * Note that the Observer itself is no standalone class
+ * and should be adapted to the Agile Class needs it belongs to.
+ *
* @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,
@@ -39,35 +68,40 @@ 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)
);
}
/**
- * @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 Observer.
+ *
+ * @public
*/
public get key(): StateKey | undefined {
return this._key;
}
- //=========================================================================================================
- // Ingest
- //=========================================================================================================
/**
- * @internal
- * Ingests Observer into Runtime
- * @param config - Configuration
+ * Passes the Observer into the runtime wrapped into a Runtime-Job
+ * 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
*/
public ingest(config: ObserverIngestConfigInterface = {}): void {
config = defineConfig(config, {
@@ -80,86 +114,80 @@ export class Observer {
force: false,
});
- // Create Job
+ // Create Runtime-Job
const job = new RuntimeJob(this, {
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
this.agileInstance().runtime.ingest(job, {
perform: config.perform,
});
}
- //=========================================================================================================
- // Perform
- //=========================================================================================================
/**
- * @internal
- * Performs Job of Runtime
- * @param job - Job that gets performed
+ * 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
+ * to which the Observer belongs.
+ *
+ * @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
+ * Makes the specified Observer depend on the Observer.
+ *
+ * A dependent Observer is always ingested into the Runtime,
+ * when the Observer it depends on has also been ingested.
+ *
+ * @public
+ * @param observer - Observer to depend on the Observer.
*/
- public depend(observer: Observer): void {
+ public addDependent(observer: Observer): void {
if (!this.dependents.has(observer)) this.dependents.add(observer);
}
+}
- //=========================================================================================================
- // Subscribe
- //=========================================================================================================
+export interface CreateObserverConfigInterface {
/**
- * @internal
- * Adds Subscription to Observer
- * @param subscriptionContainer - SubscriptionContainer(Component) that gets subscribed by this Observer
+ * Initial Observers to depend on the Observer.
+ * @default []
*/
- 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
- subscriptionContainer.subscribers.add(this);
- }
- }
-
- //=========================================================================================================
- // Unsubscribe
- //=========================================================================================================
+ dependents?: Array;
/**
- * @internal
- * Removes Subscription from Observer
- * @param subscriptionContainer - SubscriptionContainer(Component) that gets unsubscribed by this Observer
+ * Initial Subscription Containers the Observer is subscribed to.
+ * @default []
*/
- public unsubscribe(subscriptionContainer: SubscriptionContainer): void {
- if (this.subscribedTo.has(subscriptionContainer)) {
- this.subscribedTo.delete(subscriptionContainer);
- subscriptionContainer.subscribers.delete(this);
- }
- }
-}
-
-/**
- * @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 {
- dependents?: Array;
subs?: Array;
+ /**
+ * Key/Name identifier of the Observer.
+ * @default undefined
+ */
key?: ObserverKey;
+ /**
+ * Initial value of the Observer.
+ *
+ * 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
+ */
value?: ValueType;
}
diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts
index a3e559ea..c61b7c02 100644
--- a/packages/core/src/runtime/runtime.job.ts
+++ b/packages/core/src/runtime/runtime.job.ts
@@ -1,19 +1,32 @@
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 Runtime Job
+ public _key?: RuntimeJobKey;
+ // Observer the Job represents
+ public observer: ObserverType;
+ // Whether the Subscription Containers (UI-Components) of the Observer should be updated (re-rendered)
+ public rerender: boolean;
+ // Subscription Containers (UI-Components) of the Observer that have to be updated (re-rendered)
+ public subscriptionContainersToUpdate = new Set();
+ // How often not ready Subscription Containers of the Observer have been tried to update
+ public triedToUpdateCount = 0;
+
+ // Whether the Job has been performed by the runtime
+ public performed = false;
/**
+ * 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
- * 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,
@@ -26,13 +39,13 @@ export class RuntimeJob {
exclude: [],
},
force: false,
- numberOfTriesToUpdate: 3,
+ maxTriesToUpdate: 3,
});
this.config = {
background: config.background,
force: config.force,
sideEffects: config.sideEffects,
- numberOfTriesToUpdate: config.numberOfTriesToUpdate,
+ maxTriesToUpdate: config.maxTriesToUpdate,
};
this.observer = observer;
this.rerender =
@@ -42,45 +55,76 @@ 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;
}
-/**
- * @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.
+ * So that the Subscription Containers (UI-Components) aren't notified
+ * of these changes and thus doesn't update (re-render).
+ * @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 current viewpoint of the runtime.
+ * @default false
+ */
force?: boolean;
- numberOfTriesToUpdate?: number | null;
+ /**
+ * 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
+ */
+ maxTriesToUpdate?: 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/CallbackSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts
index e9771a0b..bf974bea 100644
--- a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts
+++ b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts
@@ -5,18 +5,30 @@ import {
} from '../../../internal';
export class CallbackSubscriptionContainer extends SubscriptionContainer {
+ /**
+ * 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 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 re-renders on it by calling 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 initial subscribed to the Subscription Container.
+ * @param config - Configuration object
*/
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 d71c3e2f..ff80eded 100644
--- a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts
+++ b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts
@@ -4,19 +4,63 @@ import {
SubscriptionContainerConfigInterface,
} from '../../../internal';
-export class ComponentSubscriptionContainer extends SubscriptionContainer {
- public component: any;
+export class ComponentSubscriptionContainer<
+ C = any
+> extends SubscriptionContainer {
+ /**
+ * 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 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 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 an object keymap
+ * with a unique key identifier.
+ * ```
+ * // Object based (guaranteed unique key identifier)
+ * {
+ * state1: Observer,
+ * state2: Observer
+ * }
+ *
+ * // Array based (no guaranteed unique key identifier)
+ * [Observer, Observer]
+ * ```
+ * 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 updated Observer values keymap (see below)
+ * // {
+ * // state1: Observer.value,
+ * // state2: Observer.value,
+ * // }
+ * }
+ * ```
+ *
+ * [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 - 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(
- component: any,
- subs: Array = [],
+ component: C,
+ 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 ef7c7bb5..4c0dcb17 100644
--- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts
+++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts
@@ -1,73 +1,283 @@
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 UI-Component it represents are ready.
+ *
+ * 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.
+ */
+ public componentId?: ComponentIdType;
- 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 are subscribed to the Subscription Container.
+ *
+ * The subscribed Observers use the Subscription Container
+ * 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 updated by the runtime
+ * and are currently running through
+ * the update (rerender) Subscription Container (UI-Component) process.
+ */
+ public updatedSubscribers: Set = new Set();
- // For Object based Subscription
+ /**
+ * Whether the Subscription Container is object based.
+ *
+ * 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`.
+ *
+ * Often Component based Subscriptions are object based,
+ * 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;
- 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 '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;
+
+ /**
+ * Weak Map for storing selector functions for subscribed Observers.
+ *
+ * A selector function allows the partial subscription to an Observer value.
+ * Only when the selected Observer value part changes,
+ * 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
+ */
+ public selectorsWeakMap: SelectorWeakMapType;
/**
+ * A Subscription Container represents a UI-Component in AgileTs
+ * that can be subscribed by multiple Observer Instances.
+ *
+ * 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
- * 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
+ * @param subs - Observers to be initial subscribed to the Subscription Container.
+ * @param config - Configuration object
*/
constructor(
- subs: Array = [],
+ subs: Array | { [key: string]: Observer },
config: SubscriptionContainerConfigInterface = {}
) {
config = defineConfig(config, {
- proxyKeyMap: {},
+ proxyWeakMap: new WeakMap(),
+ selectorWeakMap: new WeakMap(),
key: generateId(),
});
- this.subscribers = new Set(subs);
+ this.subscribers = new Set();
this.key = config.key;
- this.proxyKeyMap = config.proxyKeyMap as any;
- this.proxyBased = notEqual(this.proxyKeyMap, {});
+ this.componentId = config?.componentId;
+ this.subscriberKeysWeakMap = new WeakMap();
+ this.selectorsWeakMap = new WeakMap();
+ this.isObjectBased = !Array.isArray(subs);
+
+ // Assign initial Observers to the Subscription Container
+ for (const key in subs) {
+ this.addSubscription(subs[key], {
+ proxyPaths: config.proxyWeakMap?.get(subs[key])?.paths,
+ selectorMethods: config.selectorWeakMap?.get(subs[key])?.methods,
+ key: this.isObjectBased ? key : undefined,
+ });
+ }
+ }
+
+ /**
+ * Subscribes the specified Observer to the Subscription Container.
+ *
+ * @internal
+ * @param sub - Observer to be subscribed to the Subscription Container
+ * @param config - Configuration object
+ */
+ public addSubscription(
+ sub: Observer,
+ config: AddSubscriptionMethodConfigInterface = {}
+ ): void {
+ const toAddSelectorMethods: SelectorMethodType[] =
+ config.selectorMethods ?? [];
+ const proxyPaths = config.proxyPaths ?? [];
+
+ // 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) {
+ if (!isValidObject(_value, true)) break;
+ _value = _value[branch];
+ }
+ return _value;
+ });
+ }
+
+ // 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(
+ toAddSelectorMethods
+ );
+ if (newSelectorMethods.length > 0)
+ 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 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 the Observer can cause updates on it
+ // (trigger re-render on the UI-Component it represents).
+ sub.subscribedTo.add(this);
+ }
+
+ /**
+ * Unsubscribes the specified Observer from the Subscription Container.
+ *
+ * @internal
+ * @param sub - Observer to be unsubscribed from the Subscription Container.
+ */
+ public removeSubscription(sub: Observer) {
+ if (this.subscribers.has(sub)) {
+ this.selectorsWeakMap.delete(sub);
+ this.subscriberKeysWeakMap.delete(sub);
+ this.subscribers.delete(sub);
+ sub.subscribedTo.delete(this);
+ }
}
}
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 the Subscription Container
+ * @default undefined
+ */
key?: SubscriptionContainerKeyType;
+ /**
+ * 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 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 re-renders the UI-Component it represents.
+ *
+ * For example:
+ * ```
+ * WeakMap: {
+ * Observer1: {paths: [['data', 'name']]},
+ * Observer2: {paths: [['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 represented in the specified Proxy Weak Map.
+ *
+ * These particular paths can, for example, be tracked via the ProxyTree.
+ * https://github.com/agile-ts/agile/tree/master/packages/proxytree
+ *
+ * @default new WeakMap()
+ */
+ proxyWeakMap?: ProxyWeakMapType;
+ /**
+ * A Weak Map with a set of selector functions for Observers.
+ *
+ * Selector functions allow the partly subscription to Observer values.
+ * Only if the selected Observer value part changes,
+ * 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()
+ */
+ selectorWeakMap?: SelectorWeakMapType;
}
-export interface ProxyKeyMapInterface {
- [key: string]: { paths: string[][] };
+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,
+ { methods: SelectorMethodType[] }
+>;
+
+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 fbd9b37c..d1ac3641 100644
--- a/packages/core/src/runtime/subscription/sub.controller.ts
+++ b/packages/core/src/runtime/subscription/sub.controller.ts
@@ -12,164 +12,185 @@ import {
} from '../../internal';
export class SubController {
+ // Agile Instance the SubController 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
+ // Keeps track of all registered Component based Subscriptions
+ public componentSubs: Set = new Set();
+ // Keeps track of 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();
/**
+ * The Subscription Controller manages and simplifies the subscription to UI-Components.
+ *
+ * Thus it creates Subscription Containers (Interfaces to 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
- * SubController - Handles subscriptions to Components
- * @param agileInstance - An instance of Agile
+ * @param agileInstance - Instance of Agile the Subscription Controller belongs to.
*/
public constructor(agileInstance: Agile) {
this.agileInstance = () => agileInstance;
}
- //=========================================================================================================
- // Subscribe with Subs Object
- //=========================================================================================================
/**
- * @internal
- * Subscribe with Object shaped Subscriptions
- * @param integrationInstance - Callback Function or Component
- * @param subs - Initial Subscription Object
- * @param config - Config
+ * Creates a so called Subscription Container that represents an UI-Component in AgileTs.
+ * Such Subscription Container know how to trigger a re-render on the UI-Component it represents
+ * through the provided `integrationInstance`.
+ *
+ * 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)
+ *
+ * The in an array specified Observers are then automatically subscribed
+ * 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.
+ * @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 = {}
+ subs: Array,
+ config?: RegisterSubscriptionConfigInterface
+ ): SubscriptionContainer;
+ /**
+ * Creates a so called Subscription Container that represents an UI-Component in AgileTs.
+ * Such Subscription Container know how to trigger a re-render on the UI-Component it represents
+ * through the provided `integrationInstance`.
+ *
+ * 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)
+ *
+ * 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 '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}}
+ * ```
+ *
+ * @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 subscribe(
+ integrationInstance: any,
+ subs: { [key: string]: Observer },
+ config?: RegisterSubscriptionConfigInterface
): {
subscriptionContainer: SubscriptionContainer;
props: { [key: string]: Observer['value'] };
- } {
- const props: { [key: string]: Observer['value'] } = {};
-
- // Create subsArray
- const subsArray: Observer[] = [];
- for (const key in subs) subsArray.push(subs[key]);
-
- // Register Subscription -> decide weather subscriptionInstance is callback or component based
- const subscriptionContainer = this.registerSubscription(
- integrationInstance,
- subsArray,
- config
- );
-
- // Set SubscriptionContainer to Object based
- subscriptionContainer.isObjectBased = true;
- subscriptionContainer.subsObject = subs;
-
- // Register subs and build props object
- for (const key in subs) {
- const observer = subs[key];
- observer.subscribe(subscriptionContainer);
- if (observer.value) props[key] = observer.value;
- }
-
- return {
- subscriptionContainer: subscriptionContainer,
- props: props,
- };
- }
-
- //=========================================================================================================
- // Subscribe with Subs Array
- //=========================================================================================================
- /**
- * @internal
- * Subscribe with Array shaped Subscriptions
- * @param integrationInstance - Callback Function or Component
- * @param subs - Initial Subscription Array
- * @param config - Config
- */
- public subscribeWithSubsArray(
+ };
+ public subscribe(
integrationInstance: any,
- subs: Array = [],
+ subs: { [key: string]: Observer } | Array,
config: RegisterSubscriptionConfigInterface = {}
- ): SubscriptionContainer {
- // Register Subscription -> decide weather subscriptionInstance is callback or component based
- const subscriptionContainer = this.registerSubscription(
- integrationInstance,
- subs,
- config
- );
+ ):
+ | SubscriptionContainer
+ | {
+ subscriptionContainer: SubscriptionContainer;
+ props: { [key: string]: Observer['value'] };
+ } {
+ config = defineConfig(config, {
+ waitForMount: this.agileInstance().config.waitForMount,
+ });
- // Register subs
- subs.forEach((observer) => observer.subscribe(subscriptionContainer));
+ // Create Subscription Container based on specified 'integrationInstance'
+ const subscriptionContainer = isFunction(integrationInstance)
+ ? this.createCallbackSubscriptionContainer(
+ integrationInstance,
+ subs,
+ config
+ )
+ : this.createComponentSubscriptionContainer(
+ integrationInstance,
+ subs,
+ config
+ );
+
+ // Return object based Subscription Container and an Observer value keymap
+ if (subscriptionContainer.isObjectBased && !Array.isArray(subs)) {
+ const props: { [key: string]: Observer['value'] } = {};
+ for (const key in subs) if (subs[key].value) props[key] = subs[key].value;
+ return { subscriptionContainer, props };
+ }
+ // Return array based Subscription Container
return subscriptionContainer;
}
- //=========================================================================================================
- // Unsubscribe
- //=========================================================================================================
/**
- * @internal
- * Unsubscribes SubscriptionContainer(Component)
- * @param subscriptionInstance - SubscriptionContainer or Component that holds an SubscriptionContainer
+ * 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 UI-Component it represents has been unmounted.
+ *
+ * @public
+ * @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 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);
+ subscriptionContainer.removeSubscription(observer);
});
};
- // Unsubscribe callback based Subscription
+ // 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
+ // 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 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(
(subContainer) => {
unsub(subContainer as ComponentSubscriptionContainer);
this.componentSubs.delete(subContainer);
-
Agile.logger.if
.tag(['runtime', 'subscription'])
.info(LogCodeManager.getLog('15:01:01'), subscriptionInstance);
@@ -179,52 +200,18 @@ export class SubController {
}
}
- //=========================================================================================================
- // Register Subscription
- //=========================================================================================================
/**
+ * Returns a newly created Component based Subscription Container.
+ *
* @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 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 registerSubscription(
- integrationInstance: any,
- subs: Array = [],
- config: RegisterSubscriptionConfigInterface = {}
- ): SubscriptionContainer {
- config = defineConfig(config, {
- waitForMount: this.agileInstance().config.waitForMount,
- });
- if (isFunction(integrationInstance))
- return this.registerCallbackSubscription(
- integrationInstance,
- subs,
- config
- );
- return this.registerComponentSubscription(
- integrationInstance,
- subs,
- config
- );
- }
-
- //=========================================================================================================
- // Register Component Subscription
- //=========================================================================================================
- /**
- * @internal
- * 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
- */
- public registerComponentSubscription(
+ public createComponentSubscriptionContainer(
componentInstance: any,
- subs: Array = [],
+ subs: { [key: string]: Observer } | Array,
config: RegisterSubscriptionConfigInterface = {}
): ComponentSubscriptionContainer {
const componentSubscriptionContainer = new ComponentSubscriptionContainer(
@@ -234,22 +221,25 @@ export class SubController {
);
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 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.componentSubscriptionContainer = componentSubscriptionContainer;
+ componentInstance['componentSubscriptionContainers'] = [
+ componentSubscriptionContainer,
+ ];
Agile.logger.if
.tag(['runtime', 'subscription'])
@@ -258,19 +248,18 @@ 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 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 registerCallbackSubscription(
+ public createCallbackSubscriptionContainer(
callbackFunction: () => void,
- subs: Array = [],
+ subs: { [key: string]: Observer } | Array,
config: RegisterSubscriptionConfigInterface = {}
): CallbackSubscriptionContainer {
const callbackSubscriptionContainer = new CallbackSubscriptionContainer(
@@ -288,42 +277,51 @@ export class SubController {
return callbackSubscriptionContainer;
}
- //=========================================================================================================
- // Mount
- //=========================================================================================================
/**
- * @internal
- * Mounts Component based SubscriptionContainer
- * @param componentInstance - SubscriptionContainer(Component) that gets mounted
+ * 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 Subscription Containers to be mounted.
*/
public mount(componentInstance: any) {
- if (componentInstance.componentSubscriptionContainer)
- componentInstance.componentSubscriptionContainer.ready = true;
+ if (
+ componentInstance['componentSubscriptionContainers'] != null &&
+ Array.isArray(componentInstance.componentSubscriptionContainers)
+ )
+ componentInstance.componentSubscriptionContainers.map(
+ (c) => (c.ready = true)
+ );
this.mountedComponents.add(componentInstance);
}
- //=========================================================================================================
- // Unmount
- //=========================================================================================================
/**
- * @internal
- * Unmounts Component based SubscriptionContainer
- * @param componentInstance - SubscriptionContainer(Component) that gets unmounted
+ * 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 Subscription Containers to be unmounted
*/
public unmount(componentInstance: any) {
- if (componentInstance.componentSubscriptionContainer)
- componentInstance.componentSubscriptionContainer.ready = false;
+ if (
+ componentInstance['componentSubscriptionContainers'] != null &&
+ 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 not be ready
+ * until the UI-Component it represents has been mounted.
+ * @default agileInstance.config.waitForMount
+ */
waitForMount?: boolean;
}
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/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/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);
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..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', () => {
@@ -20,20 +21,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 +47,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,9 +88,11 @@ 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)', () => {
+ 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,
@@ -93,7 +101,7 @@ describe('Observer Tests', () => {
exclude: [],
},
force: false,
- numberOfTriesToUpdate: 3,
+ maxTriesToUpdate: 3,
});
});
@@ -107,7 +115,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);
@@ -118,7 +126,7 @@ describe('Observer Tests', () => {
exclude: [],
},
force: true,
- numberOfTriesToUpdate: 3,
+ maxTriesToUpdate: 3,
});
});
@@ -148,7 +156,7 @@ describe('Observer Tests', () => {
});
});
- describe('depend function tests', () => {
+ describe('addDependent function tests', () => {
let dummyObserver1: Observer;
let dummyObserver2: Observer;
@@ -157,77 +165,21 @@ describe('Observer Tests', () => {
dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' });
});
- it('should add passed Observer to deps', () => {
- observer.depend(dummyObserver1);
+ 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", () => {
- observer.depend(dummyObserver1);
+ it("shouldn't add specified Observer twice to the dependents array", () => {
+ observer.addDependent(dummyObserver1);
- observer.depend(dummyObserver1);
+ observer.addDependent(dummyObserver1);
expect(observer.dependents.size).toBe(1);
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/runtime.job.test.ts b/packages/core/tests/unit/runtime/runtime.job.test.ts
index e9fd39df..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,
- numberOfTriesToUpdate: 3,
+ maxTriesToUpdate: 3,
});
expect(job.rerender).toBeTruthy();
expect(job.performed).toBeFalsy();
- expect(job.subscriptionContainersToUpdate.size).toBe(0);
- expect(job.triesToUpdate).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,
- numberOfTriesToUpdate: 10,
+ maxTriesToUpdate: 10,
});
expect(job._key).toBe('dummyJob');
@@ -57,16 +58,17 @@ describe('RuntimeJob Tests', () => {
background: false,
sideEffects: {
enabled: false,
+ exclude: ['jeff'],
},
force: true,
- numberOfTriesToUpdate: 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,
- numberOfTriesToUpdate: 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,
- numberOfTriesToUpdate: 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 95781f72..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,18 +53,37 @@ describe('Runtime Tests', () => {
runtime.perform = jest.fn();
});
- it('should perform passed Job (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 passed Job (config.perform = false)", () => {
+ it("shouldn't perform specified Job immediately if jobQueue is being processed (default config)", () => {
+ runtime.isPerformingJobs = true;
+
+ runtime.ingest(dummyJob);
+
+ expect(runtime.jobQueue).toStrictEqual([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).toStrictEqual([]);
+ expect(runtime.perform).toHaveBeenCalledWith(dummyJob);
+ });
+
+ it("shouldn't perform specified Job immediately (config.perform = false)", () => {
+ 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();
});
});
@@ -88,32 +108,34 @@ 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.isPerformingJobs).toBeFalsy(); // because Jobs were performed
+ expect(runtime.jobQueue).toStrictEqual([]);
+ expect(runtime.jobsToRerender).toStrictEqual([dummyJob1, dummyJob2]);
- // 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);
@@ -122,139 +144,90 @@ 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 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.isPerformingJobs).toBeFalsy(); // because Jobs were performed
+ expect(runtime.jobQueue).toStrictEqual([]);
+ expect(runtime.jobsToRerender).toStrictEqual([]);
- // 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('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 = () => {
+ let dummyJob1: RuntimeJob;
+ let dummyJob2: RuntimeJob;
+ let dummyJob3: RuntimeJob;
+ const dummySubscriptionContainer1IntegrationInstance = () => {
/* empty function */
};
- let rComponentSubContainer: ComponentSubscriptionContainer;
- const rComponentSubContainerComponent = {
+ let dummySubscriptionContainer1: SubscriptionContainer;
+ const dummySubscriptionContainer2IntegrationInstance = {
my: 'cool component',
};
- let nrComponentSubContainer: ComponentSubscriptionContainer;
- const nrComponentSubContainerComponent = {
- my: 'second cool component',
- };
- const dummyProxyKeyMap = { myState: { paths: [['a', 'b']] } };
+ let dummySubscriptionContainer2: SubscriptionContainer;
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';
+ dummySubscriptionContainer1 = dummyAgile.subController.subscribe(
+ dummySubscriptionContainer1IntegrationInstance,
+ [dummyObserver1]
+ );
+ dummySubscriptionContainer2 = dummyAgile.subController.subscribe(
+ dummySubscriptionContainer2IntegrationInstance,
+ [dummyObserver2, dummyObserver3]
+ );
- // Create Not Ready Callback Subscription
- nrCallbackSubContainer = dummyAgile.subController.subscribeWithSubsArray(
- nrCallbackSubContainerCallbackFunction,
- [dummyObserver2]
- ) as CallbackSubscriptionContainer;
- nrCallbackSubContainer.callback = jest.fn();
- nrCallbackSubContainer.ready = false;
- nrCallbackSubContainer.key = 'nrCallbackSubContainerKey';
+ dummyJob1 = new RuntimeJob(dummyObserver1);
+ dummyJob2 = new RuntimeJob(dummyObserver2);
+ dummyJob3 = new RuntimeJob(dummyObserver3);
- // 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, 'handleProxyBasedSubscription');
+ runtime.updateSubscriptionContainer = jest.fn();
+ jest.spyOn(runtime, 'extractToUpdateSubscriptionContainer');
});
- it('should return false if agile has no integration', () => {
+ it('should return false if Agile has no registered Integration', () => {
dummyAgile.hasIntegration = jest.fn(() => false);
- runtime.jobsToRerender.push(rCallbackSubJob);
- runtime.jobsToRerender.push(nrArCallbackSubJob);
+ runtime.jobsToRerender = [dummyJob1, dummyJob2];
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();
+ expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]);
+ expect(
+ runtime.extractToUpdateSubscriptionContainer
+ ).not.toHaveBeenCalled();
+ expect(runtime.updateSubscriptionContainer).not.toHaveBeenCalled();
});
- it('should return false if no Jobs in jobsToRerender and notReadyJobsToRerender left', () => {
+ it('should return false if jobsToRerender and notReadyJobsToRerender queue are both empty', () => {
dummyAgile.hasIntegration = jest.fn(() => true);
runtime.jobsToRerender = [];
runtime.notReadyJobsToRerender = new Set();
@@ -262,358 +235,425 @@ describe('Runtime Tests', () => {
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.handleProxyBasedSubscription).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(response).toBeFalsy();
expect(runtime.jobsToRerender).toStrictEqual([]);
- expect(runtime.notReadyJobsToRerender.size).toBe(0);
- expect(runtime.handleProxyBasedSubscription).not.toHaveBeenCalled();
-
- expect(rCallbackSubContainer.callback).toHaveBeenCalledTimes(1);
- expect(rCallbackSubJob.subscriptionContainersToUpdate.size).toBe(0);
- expect(dummyObserver1.subscribedTo.size).toBe(1);
-
- expect(response).toBeTruthy();
+ expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]);
+ expect(
+ runtime.extractToUpdateSubscriptionContainer
+ ).not.toHaveBeenCalled();
+ expect(runtime.updateSubscriptionContainer).not.toHaveBeenCalled();
});
- it('should update ready proxy, callback based SubscriptionContainer if handleProxyBasedSubscriptions() returns true', () => {
- jest
- .spyOn(runtime, 'handleProxyBasedSubscription')
- .mockReturnValueOnce(true);
+ it('should return false if no Subscription Container of the Jobs to rerender queue needs to update', () => {
dummyAgile.hasIntegration = jest.fn(() => true);
- rCallbackSubContainer.proxyBased = true;
- rCallbackSubContainer.proxyKeyMap = dummyProxyKeyMap;
- runtime.jobsToRerender.push(rCallbackSubJob);
-
- const response = runtime.updateSubscribers();
-
- expect(runtime.jobsToRerender).toStrictEqual([]);
- expect(runtime.notReadyJobsToRerender.size).toBe(0);
- expect(runtime.handleProxyBasedSubscription).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, 'handleProxyBasedSubscription')
- .mockReturnValueOnce(false);
- dummyAgile.hasIntegration = jest.fn(() => true);
- rCallbackSubContainer.proxyBased = true;
- rCallbackSubContainer.proxyKeyMap = dummyProxyKeyMap;
- runtime.jobsToRerender.push(rCallbackSubJob);
+ .spyOn(runtime, 'extractToUpdateSubscriptionContainer')
+ .mockReturnValueOnce([]);
+ runtime.jobsToRerender = [dummyJob1, dummyJob2];
+ runtime.notReadyJobsToRerender = new Set([dummyJob3]);
const response = runtime.updateSubscribers();
- expect(runtime.jobsToRerender).toStrictEqual([]);
- expect(runtime.notReadyJobsToRerender.size).toBe(0);
- expect(runtime.handleProxyBasedSubscription).toHaveBeenCalledWith(
- rCallbackSubContainer,
- rCallbackSubJob
- );
-
- expect(rCallbackSubContainer.callback).not.toHaveBeenCalled();
- expect(rCallbackSubJob.subscriptionContainersToUpdate.size).toBe(0);
- expect(dummyObserver1.subscribedTo.size).toBe(1);
-
expect(response).toBeFalsy();
+ expect(runtime.jobsToRerender).toStrictEqual([]);
+ expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]);
+ expect(
+ runtime.extractToUpdateSubscriptionContainer
+ ).toHaveBeenCalledWith([dummyJob1, dummyJob2, dummyJob3]);
+ expect(runtime.updateSubscriptionContainer).not.toHaveBeenCalled();
});
- it("shouldn't update not ready SubscriptionContainers but it should update ready SubscriptionContainers", () => {
+ it('should return true if at least one Subscription Container of the Jobs to rerender queue needs to update', () => {
dummyAgile.hasIntegration = jest.fn(() => true);
- runtime.jobsToRerender.push(nrArCallbackSubJob);
- runtime.jobsToRerender.push(nrArComponentSubJob);
+ jest
+ .spyOn(runtime, 'extractToUpdateSubscriptionContainer')
+ .mockReturnValueOnce([
+ dummySubscriptionContainer1,
+ dummySubscriptionContainer2,
+ ]);
+ 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(2);
- expect(
- runtime.notReadyJobsToRerender.has(nrArCallbackSubJob)
- ).toBeTruthy();
+ expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]);
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
- );
+ runtime.extractToUpdateSubscriptionContainer
+ ).toHaveBeenCalledWith([dummyJob1, dummyJob2, dummyJob3]);
+ expect(runtime.updateSubscriptionContainer).toHaveBeenCalledWith([
+ dummySubscriptionContainer1,
+ dummySubscriptionContainer2,
+ ]);
+ });
+ });
- expect(nrArComponentSubJob.triesToUpdate).toBe(1);
- expect(nrArCallbackSubJob.triesToUpdate).toBe(1);
+ describe('extractToUpdateSubscriptionContainer function tests', () => {
+ let dummyJob1: RuntimeJob;
+ let dummyJob2: RuntimeJob;
+ const dummySubscriptionContainer1IntegrationInstance = () => {
+ /* empty function */
+ };
+ let dummySubscriptionContainer1: SubscriptionContainer;
+ const dummySubscriptionContainer2IntegrationInstance = {
+ my: 'cool component',
+ };
+ let dummySubscriptionContainer2: SubscriptionContainer;
- LogMock.hasLoggedCode(
- '16:02:00',
- [nrCallbackSubContainer.key],
- nrCallbackSubContainer
+ beforeEach(() => {
+ dummySubscriptionContainer1 = dummyAgile.subController.subscribe(
+ dummySubscriptionContainer1IntegrationInstance,
+ [dummyObserver1]
);
- LogMock.hasLoggedCode(
- '16:02:00',
- [nrComponentSubContainer.key],
- nrComponentSubContainer
+ dummySubscriptionContainer2 = dummyAgile.subController.subscribe(
+ dummySubscriptionContainer2IntegrationInstance,
+ [dummyObserver2]
);
- 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);
+ dummyJob1 = new RuntimeJob(dummyObserver1);
+ dummyJob2 = new RuntimeJob(dummyObserver2);
- expect(response).toBeTruthy();
+ jest.spyOn(runtime, 'handleSelectors');
});
it(
- "shouldn't update not ready SubscriptionContainers from the notReadyJobsToUpdate queue " +
- 'and completely remove them from the runtime when it exceeded numberOfTriesToUpdate',
+ "shouldn't extract not ready Subscription Container from the specified Jobs, " +
+ "should add it to the 'notReadyJobsToRerender' queue and print a warning",
() => {
- dummyAgile.hasIntegration = jest.fn(() => true);
- rCallbackSubJob.config.numberOfTriesToUpdate = 2;
- rCallbackSubJob.triesToUpdate = 2;
- rCallbackSubContainer.ready = false;
- runtime.notReadyJobsToRerender.add(rCallbackSubJob);
+ 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
+ );
- const response = runtime.updateSubscribers();
+ expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([
+ dummyJob2,
+ ]);
- expect(runtime.jobsToRerender).toStrictEqual([]);
- expect(runtime.notReadyJobsToRerender.size).toBe(0);
+ // Job that ran through
+ expect(
+ Array.from(dummyJob1.subscriptionContainersToUpdate)
+ ).toStrictEqual([]);
+ expect(dummyJob1.triedToUpdateCount).toBe(0);
+ expect(
+ Array.from(dummySubscriptionContainer1.updatedSubscribers)
+ ).toStrictEqual([dummyObserver1]);
- expect(rCallbackSubContainer.callback).not.toHaveBeenCalled();
- expect(rCallbackSubJob.subscriptionContainersToUpdate.size).toBe(1);
+ // Job that didn't ran through
+ expect(
+ Array.from(dummyJob2.subscriptionContainersToUpdate)
+ ).toStrictEqual([dummySubscriptionContainer2]);
+ expect(dummyJob2.triedToUpdateCount).toBe(1);
expect(
- rCallbackSubJob.subscriptionContainersToUpdate.has(
- rCallbackSubContainer
- )
- ).toBeTruthy();
- expect(dummyObserver1.subscribedTo.size).toBe(1);
- expect(rCallbackSubJob.triesToUpdate).toBe(2);
+ Array.from(dummySubscriptionContainer2.updatedSubscribers)
+ ).toStrictEqual([]);
+ // Called with Job that didn't ran through
+ expect(console.warn).toHaveBeenCalledTimes(1);
LogMock.hasLoggedCode(
- '16:02:01',
- [rCallbackSubJob.config.numberOfTriesToUpdate],
- rCallbackSubContainer
+ '16:02:00',
+ [dummySubscriptionContainer2.key],
+ dummySubscriptionContainer2
);
-
- 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',
+ "shouldn't extract not ready Subscription Container from the specified Jobs, " +
+ "should remove the Job when it exceeded the max 'maxTriesToUpdate' " +
+ 'and print a warning',
() => {
- dummyAgile.hasIntegration = jest.fn(() => true);
- rCallbackSubJob.config.numberOfTriesToUpdate = null;
- rCallbackSubJob.triesToUpdate = 2;
- rCallbackSubContainer.ready = false;
- runtime.notReadyJobsToRerender.add(rCallbackSubJob);
+ jest
+ .spyOn(runtime, 'handleSelectors')
+ .mockReturnValueOnce(true)
+ .mockReturnValueOnce(true);
+ dummySubscriptionContainer1.ready = true;
+ dummySubscriptionContainer2.ready = false;
+ const numberOfTries = (dummyJob2.config.maxTriesToUpdate ?? 0) + 1;
+ dummyJob2.triedToUpdateCount = 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
+ );
- const response = runtime.updateSubscribers();
+ expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); // Because exceeded Job was removed
- expect(runtime.jobsToRerender).toStrictEqual([]);
- expect(runtime.notReadyJobsToRerender.size).toBe(1);
+ // Job that ran through
expect(
- runtime.notReadyJobsToRerender.has(rCallbackSubJob)
- ).toBeTruthy();
+ Array.from(dummyJob1.subscriptionContainersToUpdate)
+ ).toStrictEqual([]);
+ expect(dummyJob1.triedToUpdateCount).toBe(0);
+ expect(
+ Array.from(dummySubscriptionContainer1.updatedSubscribers)
+ ).toStrictEqual([dummyObserver1]);
- expect(rCallbackSubContainer.callback).not.toHaveBeenCalled();
- expect(rCallbackSubJob.subscriptionContainersToUpdate.size).toBe(1);
+ // Job that didn't ran through
+ expect(
+ Array.from(dummyJob2.subscriptionContainersToUpdate)
+ ).toStrictEqual([dummySubscriptionContainer2]);
+ expect(dummyJob2.triedToUpdateCount).toBe(numberOfTries);
expect(
- rCallbackSubJob.subscriptionContainersToUpdate.has(
- rCallbackSubContainer
- )
- ).toBeTruthy();
- expect(dummyObserver1.subscribedTo.size).toBe(1);
- expect(rCallbackSubJob.triesToUpdate).toBe(3);
+ Array.from(dummySubscriptionContainer2.updatedSubscribers)
+ ).toStrictEqual([]);
+ // Called with Job that didn't ran through
+ expect(console.warn).toHaveBeenCalledTimes(1);
LogMock.hasLoggedCode(
- '16:02:00',
- [rCallbackSubContainer.key],
- rCallbackSubContainer
+ '16:02:01',
+ [dummyJob2.config.maxTriesToUpdate],
+ dummySubscriptionContainer2
);
-
- expect(response).toBeFalsy();
}
);
- });
- 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;
+ 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;
- beforeEach(() => {
- arraySubscriptionContainer = dummyAgile.subController.subscribeWithSubsArray(
- dummyComponent,
- [dummyObserver1, dummyObserver2, dummyObserver3]
+ 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
);
- 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' });
+ // 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
+ expect(
+ Array.from(dummyJob1.subscriptionContainersToUpdate)
+ ).toStrictEqual([]);
+ expect(dummyJob1.triedToUpdateCount).toBe(0);
+ expect(
+ Array.from(dummySubscriptionContainer1.updatedSubscribers)
+ ).toStrictEqual([]);
+
+ // Job that ran through
+ expect(
+ Array.from(dummyJob2.subscriptionContainersToUpdate)
+ ).toStrictEqual([]);
+ expect(dummyJob2.triedToUpdateCount).toBe(0);
+ expect(
+ Array.from(dummySubscriptionContainer2.updatedSubscribers)
+ ).toStrictEqual([dummyObserver2]);
+
+ expect(console.warn).toHaveBeenCalledTimes(0);
});
- it('should ignore not object based SubscriptionContainer', () => {
- runtime.handleObjectBasedSubscription(
- arraySubscriptionContainer,
- arrayJob
- );
+ it('should extract ready and to update Subscription Containers', () => {
+ jest
+ .spyOn(runtime, 'handleSelectors')
+ .mockReturnValueOnce(true)
+ .mockReturnValueOnce(true);
+ dummySubscriptionContainer1.ready = true;
+ dummySubscriptionContainer2.ready = true;
+
+ const response = runtime.extractToUpdateSubscriptionContainer([
+ dummyJob1,
+ dummyJob2,
+ ]);
- expect(arraySubscriptionContainer.observerKeysToUpdate).toStrictEqual(
- []
+ 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.triedToUpdateCount).toBe(0);
+ expect(
+ Array.from(dummySubscriptionContainer1.updatedSubscribers)
+ ).toStrictEqual([dummyObserver1]);
+
+ // Job that ran through
+ expect(
+ Array.from(dummyJob2.subscriptionContainersToUpdate)
+ ).toStrictEqual([]);
+ expect(dummyJob2.triedToUpdateCount).toBe(0);
+ expect(
+ Array.from(dummySubscriptionContainer2.updatedSubscribers)
+ ).toStrictEqual([dummyObserver2]);
+
+ expect(console.warn).not.toHaveBeenCalled();
});
+ });
- it('should add Job Observer to changedObjectKeys in SubscriptionContainer', () => {
- runtime.handleObjectBasedSubscription(
- objectSubscriptionContainer,
- objectJob1
- );
+ describe('updateSubscriptionContainer function tests', () => {
+ 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,
+ ]);
- expect(objectSubscriptionContainer.observerKeysToUpdate).toStrictEqual([
- 'observer1',
+ 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
+ );
+ expect(
+ Array.from(callbackSubscriptionContainer2.updatedSubscribers)
+ ).toStrictEqual([]);
+
+ // Callback Subscription Container 3
+ expect(callbackSubscriptionContainer3.callback).toHaveBeenCalledTimes(
+ 1
+ );
+ expect(
+ Array.from(callbackSubscriptionContainer2.updatedSubscribers)
+ ).toStrictEqual([]);
});
});
- describe('getObjectBasedProps function tests', () => {
+ describe('getUpdatedObserverValues function tests', () => {
let subscriptionContainer: SubscriptionContainer;
const dummyFunction = () => {
/* empty function */
};
beforeEach(() => {
- subscriptionContainer = dummyAgile.subController.subscribeWithSubsObject(
+ 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.observerKeysToUpdate.push('observer1');
- subscriptionContainer.observerKeysToUpdate.push('observer2');
- subscriptionContainer.observerKeysToUpdate.push('observer3');
+ it('should map the values of the updated Observers into an object and return it', () => {
+ subscriptionContainer.updatedSubscribers.add(dummyObserver1);
+ subscriptionContainer.updatedSubscribers.add(dummyObserver2);
+ subscriptionContainer.updatedSubscribers.add(dummyObserver3);
- const props = runtime.getObjectBasedProps(subscriptionContainer);
+ const props = runtime.getUpdatedObserverValues(subscriptionContainer);
expect(props).toStrictEqual({
- observer1: 'dummyObserverValue1',
- observer2: undefined,
- observer3: 'dummyObserverValue3',
+ dummyObserver1KeyInObserver: 'dummyObserverValue1',
+ dummyObserver2KeyInWeakMap: undefined,
+ dummyObserver3KeyInWeakMap: 'dummyObserverValue3',
});
- expect(subscriptionContainer.observerKeysToUpdate).toStrictEqual([]);
+ expect(
+ Array.from(subscriptionContainer.updatedSubscribers)
+ ).toStrictEqual([dummyObserver1, dummyObserver2, dummyObserver3]);
});
});
- describe('handleProxyBasedSubscription function tests', () => {
+ describe('handleSelector function tests', () => {
let objectSubscriptionContainer: SubscriptionContainer;
const dummyFunction = () => {
/* empty function */
@@ -627,96 +667,69 @@ describe('Runtime Tests', () => {
let arrayJob: RuntimeJob;
beforeEach(() => {
- // Create Job with Object value
- objectSubscriptionContainer = dummyAgile.subController.subscribeWithSubsObject(
+ // 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.proxyBased = 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' },
},
+ {
+ data: { name: 'frank' },
+ },
];
dummyObserver2.previousValue = [
{
- key: 'dummyObserver2Value1',
data: { name: 'jeff' },
},
{
- key: 'dummyObserver2Value2',
data: { name: 'hans' },
},
- ];
- arraySubscriptionContainer.proxyBased = true;
- arraySubscriptionContainer.proxyKeyMap = {
- [dummyObserver2._key || 'unknown']: {
- paths: [['0', 'data', 'name']],
+ {
+ data: { name: 'frank' },
},
- };
+ ];
+ arraySubscriptionContainer.selectorsWeakMap.set(dummyObserver2, {
+ methods: [
+ (value) => value[0]?.data?.name,
+ (value) => value[2]?.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.proxyBased = false;
-
- const response = runtime.handleProxyBasedSubscription(
- objectSubscriptionContainer,
- objectJob
- );
-
- expect(response).toBeTruthy();
- expect(Utils.notEqual).not.toHaveBeenCalled();
- });
-
- it('should return true if observer the job represents has no key', () => {
- objectJob.observer._key = undefined;
-
- const response = runtime.handleProxyBasedSubscription(
- 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']] },
- };
+ it('should return true if Subscription Container has no selector methods', () => {
+ objectSubscriptionContainer.selectorsWeakMap.delete(dummyObserver1);
- const response = runtime.handleProxyBasedSubscription(
+ const response = runtime.handleSelectors(
objectSubscriptionContainer,
objectJob
);
@@ -725,90 +738,119 @@ describe('Runtime Tests', () => {
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' },
+ data: { name: 'changedName' },
};
- const response = runtime.handleProxyBasedSubscription(
+ const response = runtime.handleSelectors(
objectSubscriptionContainer,
objectJob
);
expect(response).toBeTruthy();
+
+ expect(Utils.notEqual).toHaveBeenCalledTimes(1);
expect(Utils.notEqual).toHaveBeenCalledWith(
dummyObserver1.value.data.name,
dummyObserver1.previousValue.data.name
);
});
- it("should return false if used property hasn't changed (object value)", () => {
- const response = runtime.handleProxyBasedSubscription(
+ it("should return false if selected property hasn't changed (object value)", () => {
+ const response = runtime.handleSelectors(
objectSubscriptionContainer,
objectJob
);
expect(response).toBeFalsy();
+
+ expect(Utils.notEqual).toHaveBeenCalledTimes(1);
expect(Utils.notEqual).toHaveBeenCalledWith(
dummyObserver1.value.data.name,
dummyObserver1.previousValue.data.name
);
});
- 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.handleProxyBasedSubscription(
- objectSubscriptionContainer,
- objectJob
- );
-
- expect(response).toBeTruthy();
- expect(Utils.notEqual).toHaveBeenCalledWith(undefined, undefined);
- });
-
- it('should return true if used property has changed (array value)', () => {
+ // 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',
+ // };
+ // 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 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.handleProxyBasedSubscription(
+ const response = runtime.handleSelectors(
arraySubscriptionContainer,
arrayJob
);
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)", () => {
- const response = runtime.handleProxyBasedSubscription(
+ 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
+ );
});
});
});
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..e8bea19e 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,36 @@ 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);
+ // Check if SubscriptionContainer was called with correct parameters
expect(subscriptionContainer.key).toBe('dummyKey');
expect(subscriptionContainer.ready).toBeFalsy();
- expect(subscriptionContainer.subscribers.size).toBe(2);
- expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy();
- expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy();
+ expect(subscriptionContainer.componentId).toBe('testID');
+ expect(Array.from(subscriptionContainer.subscribers)).toStrictEqual([
+ dummyObserver1,
+ dummyObserver2,
+ ]);
+ expect(Array.from(subscriptionContainer.updatedSubscribers)).toStrictEqual(
+ []
+ );
expect(subscriptionContainer.isObjectBased).toBeFalsy();
- expect(subscriptionContainer.observerKeysToUpdate).toStrictEqual([]);
- expect(subscriptionContainer.subsObject).toBeUndefined();
- expect(subscriptionContainer.proxyKeyMap).toStrictEqual({
- myState: { paths: [['hi']] },
- });
- expect(subscriptionContainer.proxyBased).toBeTruthy();
+ expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual(
+ expect.any(WeakMap)
+ );
+ 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 04362b53..4401cf88 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,36 @@ 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);
+ // Check if SubscriptionContainer was called with correct parameters
expect(subscriptionContainer.key).toBe('dummyKey');
expect(subscriptionContainer.ready).toBeFalsy();
- expect(subscriptionContainer.subscribers.size).toBe(2);
- expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy();
- expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy();
+ expect(subscriptionContainer.componentId).toBe('testID');
+ expect(Array.from(subscriptionContainer.subscribers)).toStrictEqual([
+ dummyObserver1,
+ dummyObserver2,
+ ]);
+ expect(Array.from(subscriptionContainer.updatedSubscribers)).toStrictEqual(
+ []
+ );
expect(subscriptionContainer.isObjectBased).toBeFalsy();
- expect(subscriptionContainer.observerKeysToUpdate).toStrictEqual([]);
- expect(subscriptionContainer.subsObject).toBeUndefined();
- expect(subscriptionContainer.proxyKeyMap).toStrictEqual({
- myState: { paths: [['hi']] },
- });
- expect(subscriptionContainer.proxyBased).toBeTruthy();
+ expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual(
+ expect.any(WeakMap)
+ );
+ 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 3c9c566a..c3aa012d 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,40 +22,310 @@ describe('SubscriptionContainer 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 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.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.subscribers.size).toBe(0);
+ expect(subscriptionContainer.componentId).toBeUndefined();
+ expect(Array.from(subscriptionContainer.subscribers)).toStrictEqual([]); // because of mocking addSubscription
+ expect(Array.from(subscriptionContainer.updatedSubscribers)).toStrictEqual(
+ []
+ );
expect(subscriptionContainer.isObjectBased).toBeFalsy();
- expect(subscriptionContainer.observerKeysToUpdate).toStrictEqual([]);
- expect(subscriptionContainer.subsObject).toBeUndefined();
- expect(subscriptionContainer.proxyKeyMap).toStrictEqual({});
- expect(subscriptionContainer.proxyBased).toBeFalsy();
+ expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual(
+ expect.any(WeakMap)
+ );
+ 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(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)
+ );
+ 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],
- { key: 'dummyKey', proxyKeyMap: { myState: { paths: [['a', 'b']] } } }
+ {
+ key: 'dummyKey',
+ proxyWeakMap: dummyProxyWeakMap,
+ selectorWeakMap: dummySelectorWeakMap,
+ componentId: 'testID',
+ }
+ );
+
+ 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.subscribers.size).toBe(2);
- expect(subscriptionContainer.subscribers.has(dummyObserver1)).toBeTruthy();
- expect(subscriptionContainer.subscribers.has(dummyObserver2)).toBeTruthy();
+ expect(subscriptionContainer.componentId).toBe('testID');
+ expect(Array.from(subscriptionContainer.subscribers)).toStrictEqual([]); // because of mocking addSubscription
+ expect(Array.from(subscriptionContainer.updatedSubscribers)).toStrictEqual(
+ []
+ );
expect(subscriptionContainer.isObjectBased).toBeFalsy();
- expect(subscriptionContainer.observerKeysToUpdate).toStrictEqual([]);
- expect(subscriptionContainer.subsObject).toBeUndefined();
- expect(subscriptionContainer.proxyKeyMap).toStrictEqual({
- myState: { paths: [['a', 'b']] },
+ expect(subscriptionContainer.subscriberKeysWeakMap).toStrictEqual(
+ expect.any(WeakMap)
+ );
+ expect(subscriptionContainer.selectorsWeakMap).toStrictEqual(
+ expect.any(WeakMap)
+ );
+ expect(subscriptionContainer.selectorsWeakMap).not.toBe(
+ dummySelectorWeakMap
+ );
+ });
+
+ describe('Subscription Container Function Tests', () => {
+ let subscriptionContainer: SubscriptionContainer;
+
+ beforeEach(() => {
+ subscriptionContainer = new SubscriptionContainer([]);
+ });
+
+ describe('addSubscription function tests', () => {
+ it(
+ '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' } },
+ 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(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(
+ 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' (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'
+ const observer1Key = subscriptionContainer.subscriberKeysWeakMap.get(
+ dummyObserver1
+ );
+ expect(observer1Key).toBe('dummyObserver1');
+
+ // shouldn't overwrite already set values in 'subscriberKeysWeakMap' (Observer2)
+ const observer2Key = subscriptionContainer.subscriberKeysWeakMap.get(
+ dummyObserver2
+ );
+ expect(observer2Key).toBe('dummyObserver2');
+ }
+ );
+ });
+
+ 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(Array.from(subscriptionContainer.subscribers)).toStrictEqual([
+ dummyObserver2,
+ ]);
+
+ 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');
+
+ expect(Array.from(dummyObserver1.subscribedTo)).toStrictEqual([]);
+ expect(Array.from(dummyObserver2.subscribedTo)).toStrictEqual([
+ subscriptionContainer,
+ ]);
+ });
});
- expect(subscriptionContainer.proxyBased).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..e52e81a1 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';
@@ -22,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', () => {
@@ -32,430 +32,394 @@ 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.registerSubscription = 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.registerSubscription).toHaveBeenCalledWith(
- dummyIntegration,
- [dummyObserver1, dummyObserver2],
- {
- key: 'subscribeWithSubsObjectKey',
- proxyKeyMap: {},
- waitForMount: false,
- }
- );
-
- expect(dummySubscriptionContainer.isObjectBased).toBeTruthy();
- expect(dummySubscriptionContainer.subsObject).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.registerSubscription = 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.registerSubscription).toHaveBeenCalledWith(
- dummyIntegration,
- [dummyObserver1, dummyObserver2],
- {
- key: 'subscribeWithSubsArrayKey',
- proxyKeyMap: {},
- waitForMount: false,
- }
- );
-
- expect(dummySubscriptionContainer.isObjectBased).toBeFalsy();
- expect(dummySubscriptionContainer.subsObject).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 Instance ' +
+ 'and assign the in an 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,
+ }
+ );
+ expect(
+ subController.createCallbackSubscriptionContainer
+ ).not.toHaveBeenCalled();
+ }
+ );
+
+ 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 = {
+ 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: 'aFakeBoolean',
+ }
+ );
+ expect(
+ subController.createCallbackSubscriptionContainer
+ ).not.toHaveBeenCalled();
+ }
+ );
+
+ 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 = () => {
+ /* 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: 'aFakeBoolean',
+ }
+ );
+ expect(
+ subController.createComponentSubscriptionContainer
+ ).not.toHaveBeenCalled();
+ }
+ );
+
+ 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 = () => {
+ /* 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,
+ }
+ );
+ expect(
+ subController.createComponentSubscriptionContainer
+ ).not.toHaveBeenCalled();
+ }
+ );
});
describe('unsubscribe function tests', () => {
- beforeEach(() => {
- jest.spyOn(dummyObserver1, 'unsubscribe');
- jest.spyOn(dummyObserver2, 'unsubscribe');
- });
-
- it('should unsubscribe callbackSubscriptionContainer', () => {
+ it('should unsubscribe Callback based Subscription Container', () => {
const dummyIntegration = () => {
/* empty function */
};
- const callbackSubscriptionContainer = subController.registerCallbackSubscription(
+ const callbackSubscriptionContainer = subController.createCallbackSubscriptionContainer(
dummyIntegration,
[dummyObserver1, dummyObserver2]
);
+ callbackSubscriptionContainer.removeSubscription = jest.fn();
subController.unsubscribe(callbackSubscriptionContainer);
- expect(subController.callbackSubs.size).toBe(0);
+ expect(Array.from(subController.callbackSubs)).toStrictEqual([]);
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', () => {
+ it('should unsubscribe Component Subscription Container', () => {
const dummyIntegration: any = {
dummy: 'integration',
};
- const componentSubscriptionContainer = subController.registerComponentSubscription(
+ const componentSubscriptionContainer = subController.createComponentSubscriptionContainer(
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.registerComponentSubscription(
- 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
- );
- });
-
- it('should unsubscribe componentSubscriptionContainers from passed Object that hold an Array of componentSubscriptionContainers', () => {
- const dummyIntegration: any = {
- dummy: 'integration',
- componentSubscriptionContainers: [],
- };
- const componentSubscriptionContainer = subController.registerComponentSubscription(
- dummyIntegration,
- [dummyObserver1, dummyObserver2]
- );
- const componentSubscriptionContainer2 = subController.registerComponentSubscription(
- dummyIntegration,
- [dummyObserver1, dummyObserver2]
- );
-
- subController.unsubscribe(dummyIntegration);
-
- expect(subController.componentSubs.size).toBe(0);
-
+ expect(Array.from(subController.componentSubs)).toStrictEqual([]);
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.registerCallbackSubscription = jest.fn(
- () => dummySubscriptionContainer as CallbackSubscriptionContainer
- );
- subController.registerComponentSubscription = jest.fn(
- () => dummySubscriptionContainer as ComponentSubscriptionContainer
- );
- });
-
- it('should call registerCallbackSubscription if passed integrationInstance is a Function (default config)', () => {
- const dummyIntegration = () => {
- /* empty function */
- };
-
- const subscriptionContainer = subController.registerSubscription(
- dummyIntegration,
- [dummyObserver1, dummyObserver2]
- );
-
- expect(subscriptionContainer).toBe(dummySubscriptionContainer);
expect(
- subController.registerCallbackSubscription
- ).toHaveBeenCalledWith(
- dummyIntegration,
- [dummyObserver1, dummyObserver2],
- { waitForMount: dummyAgile.config.waitForMount }
- );
- expect(
- subController.registerComponentSubscription
- ).not.toHaveBeenCalled();
- });
-
- it('should call registerCallbackSubscription if passed integrationInstance is a Function (specific config)', () => {
- const dummyIntegration = () => {
- /* empty function */
- };
-
- const subscriptionContainer = subController.registerSubscription(
- dummyIntegration,
- [dummyObserver1, dummyObserver2],
- { key: 'niceKey', proxyKeyMap: {}, waitForMount: false }
- );
-
- expect(subscriptionContainer).toBe(dummySubscriptionContainer);
+ componentSubscriptionContainer.removeSubscription
+ ).toHaveBeenCalledTimes(2);
expect(
- subController.registerCallbackSubscription
- ).toHaveBeenCalledWith(
- dummyIntegration,
- [dummyObserver1, dummyObserver2],
- { key: 'niceKey', proxyKeyMap: {}, waitForMount: false }
- );
+ componentSubscriptionContainer.removeSubscription
+ ).toHaveBeenCalledWith(dummyObserver1);
expect(
- subController.registerComponentSubscription
- ).not.toHaveBeenCalled();
+ componentSubscriptionContainer.removeSubscription
+ ).toHaveBeenCalledWith(dummyObserver2);
});
- it('should call registerComponentSubscription if passed integrationInstance is not a Function (default config)', () => {
- const dummyIntegration = { dummy: 'integration' };
-
- const subscriptionContainer = subController.registerSubscription(
- dummyIntegration,
- [dummyObserver1, dummyObserver2]
- );
-
- expect(subscriptionContainer).toBe(dummySubscriptionContainer);
- expect(
- subController.registerComponentSubscription
- ).toHaveBeenCalledWith(
- dummyIntegration,
- [dummyObserver1, dummyObserver2],
- { waitForMount: dummyAgile.config.waitForMount }
- );
- expect(
- subController.registerCallbackSubscription
- ).not.toHaveBeenCalled();
- });
-
- it('should call registerComponentSubscription if passed integrationInstance is not a Function (specific config)', () => {
- const dummyIntegration = { dummy: 'integration' };
-
- const subscriptionContainer = subController.registerSubscription(
- dummyIntegration,
- [dummyObserver1, dummyObserver2],
- { key: 'niceKey', proxyKeyMap: {}, waitForMount: false }
- );
-
- expect(subscriptionContainer).toBe(dummySubscriptionContainer);
- expect(
- subController.registerComponentSubscription
- ).toHaveBeenCalledWith(
- dummyIntegration,
- [dummyObserver1, dummyObserver2],
- { key: 'niceKey', proxyKeyMap: {}, waitForMount: false }
- );
- expect(
- subController.registerCallbackSubscription
- ).not.toHaveBeenCalled();
- });
+ 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);
+
+ 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(componentSubscriptionContainer2.ready).toBeFalsy();
+ expect(
+ componentSubscriptionContainer2.removeSubscription
+ ).toHaveBeenCalledTimes(2);
+ expect(
+ componentSubscriptionContainer2.removeSubscription
+ ).toHaveBeenCalledWith(dummyObserver1);
+ expect(
+ 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.registerComponentSubscription(
- 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();
-
- 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)', () => {
- const dummyIntegration: any = {
- dummy: 'integration',
- componentSubscriptionContainers: [],
- };
-
- const componentSubscriptionContainer = subController.registerComponentSubscription(
- 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();
-
- 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)", () => {
+ describe('createComponentSubscriptionContainer function tests', () => {
+ it(
+ '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 = {
+ 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(Array.from(subController.componentSubs)).toStrictEqual([
+ componentSubscriptionContainer,
+ ]);
+
+ expect(
+ dummyIntegration.componentSubscriptionContainers
+ ).toStrictEqual([componentSubscriptionContainer]);
+
+ // Check if ComponentSubscriptionContainer was called with correct parameters
+ expect(componentSubscriptionContainer.key).toBe('generatedKey');
+ expect(componentSubscriptionContainer.componentId).toBeUndefined();
+ expect(
+ Array.from(componentSubscriptionContainer.subscribers)
+ ).toStrictEqual([dummyObserver1, dummyObserver2]);
+ }
+ );
+
+ it(
+ '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 = {
+ dummy: 'integration',
+ componentSubscriptionContainers: [],
+ };
+
+ const componentSubscriptionContainer = subController.createComponentSubscriptionContainer(
+ dummyIntegration,
+ [dummyObserver1, dummyObserver2],
+ { waitForMount: false }
+ );
+
+ expect(
+ dummyIntegration.componentSubscriptionContainers
+ ).toStrictEqual([componentSubscriptionContainer]);
+ }
+ );
+
+ it(
+ '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',
+ };
+
+ 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(Array.from(subController.componentSubs)).toStrictEqual([
+ 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(
+ Array.from(componentSubscriptionContainer.subscribers)
+ ).toStrictEqual([dummyObserver1, dummyObserver2]);
+ }
+ );
+
+ 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',
};
- const componentSubscriptionContainer = subController.registerComponentSubscription(
+ const componentSubscriptionContainer = subController.createComponentSubscriptionContainer(
dummyIntegration,
[dummyObserver1, dummyObserver2],
{ waitForMount: true }
@@ -469,27 +433,26 @@ describe('SubController Tests', () => {
);
expect(componentSubscriptionContainer.ready).toBeFalsy();
- expect(componentSubscriptionContainer.subscribers.size).toBe(2);
- expect(
- componentSubscriptionContainer.subscribers.has(dummyObserver1)
- ).toBeTruthy();
- expect(
- componentSubscriptionContainer.subscribers.has(dummyObserver2)
- ).toBeTruthy();
+ expect(Array.from(subController.componentSubs)).toStrictEqual([
+ componentSubscriptionContainer,
+ ]);
- expect(subController.componentSubs.size).toBe(1);
+ // Check if ComponentSubscriptionContainer was called with correct parameters
+ expect(componentSubscriptionContainer.key).toBe('generatedKey');
+ expect(componentSubscriptionContainer.componentId).toBeUndefined();
expect(
- subController.componentSubs.has(componentSubscriptionContainer)
- ).toBeTruthy();
+ Array.from(componentSubscriptionContainer.subscribers)
+ ).toStrictEqual([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',
};
subController.mount(dummyIntegration);
- const componentSubscriptionContainer = subController.registerComponentSubscription(
+ const componentSubscriptionContainer = subController.createComponentSubscriptionContainer(
dummyIntegration,
[dummyObserver1, dummyObserver2],
{ waitForMount: true }
@@ -503,29 +466,27 @@ describe('SubController Tests', () => {
);
expect(componentSubscriptionContainer.ready).toBeTruthy();
- expect(componentSubscriptionContainer.subscribers.size).toBe(2);
- expect(
- componentSubscriptionContainer.subscribers.has(dummyObserver1)
- ).toBeTruthy();
- expect(
- componentSubscriptionContainer.subscribers.has(dummyObserver2)
- ).toBeTruthy();
+ expect(Array.from(subController.componentSubs)).toStrictEqual([
+ componentSubscriptionContainer,
+ ]);
- expect(subController.componentSubs.size).toBe(1);
+ // Check if ComponentSubscriptionContainer was called with correct parameters
+ expect(componentSubscriptionContainer.key).toBe('generatedKey');
+ expect(componentSubscriptionContainer.componentId).toBeUndefined();
expect(
- subController.componentSubs.has(componentSubscriptionContainer)
- ).toBeTruthy();
+ Array.from(componentSubscriptionContainer.subscribers)
+ ).toStrictEqual([dummyObserver1, dummyObserver2]);
});
});
describe('registerCallbackSubscription function tests', () => {
- it('should return callbackSubscriptionContainer (default config)', () => {
- jest.spyOn(Utils, 'generateId').mockReturnValueOnce('randomKey');
+ it('should return Callback based Subscription Container (default config)', () => {
+ jest.spyOn(Utils, 'generateId').mockReturnValueOnce('generatedKey');
const dummyIntegration = () => {
/* empty function */
};
- const callbackSubscriptionContainer = subController.registerCallbackSubscription(
+ const callbackSubscriptionContainer = subController.createCallbackSubscriptionContainer(
dummyIntegration,
[dummyObserver1, dummyObserver2]
);
@@ -536,42 +497,35 @@ describe('SubController Tests', () => {
expect(callbackSubscriptionContainer.callback).toBe(dummyIntegration);
expect(callbackSubscriptionContainer.ready).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
- // 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.proxyBased).toBeFalsy();
-
- expect(callbackSubscriptionContainer.subscribers.size).toBe(2);
- expect(
- callbackSubscriptionContainer.subscribers.has(dummyObserver1)
- ).toBeTruthy();
- expect(
- callbackSubscriptionContainer.subscribers.has(dummyObserver2)
- ).toBeTruthy();
+ expect(Array.from(subController.callbackSubs)).toStrictEqual([
+ callbackSubscriptionContainer,
+ ]);
- expect(subController.callbackSubs.size).toBe(1);
+ // 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 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(
- subController.callbackSubs.has(callbackSubscriptionContainer)
- ).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 */
};
- const callbackSubscriptionContainer = subController.registerCallbackSubscription(
+ const callbackSubscriptionContainer = subController.createCallbackSubscriptionContainer(
dummyIntegration,
[dummyObserver1, dummyObserver2],
{
waitForMount: false,
- proxyKeyMap: { jeff: { paths: [[]] } },
- key: 'jeff',
+ componentId: 'testID',
+ key: 'dummyKey',
}
);
@@ -580,24 +534,17 @@ 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.proxyBased).toBeTruthy();
- expect(callbackSubscriptionContainer.subscribers.size).toBe(2);
- expect(
- callbackSubscriptionContainer.subscribers.has(dummyObserver1)
- ).toBeTruthy();
- expect(
- callbackSubscriptionContainer.subscribers.has(dummyObserver2)
- ).toBeTruthy();
+ expect(Array.from(subController.callbackSubs)).toStrictEqual([
+ callbackSubscriptionContainer,
+ ]);
- expect(subController.callbackSubs.size).toBe(1);
+ // Check if CallbackSubscriptionContainer was called with correct parameters
+ expect(callbackSubscriptionContainer.key).toBe('dummyKey');
+ expect(callbackSubscriptionContainer.componentId).toBe('testID');
expect(
- subController.callbackSubs.has(callbackSubscriptionContainer)
- ).toBeTruthy();
+ Array.from(callbackSubscriptionContainer.subscribers)
+ ).toStrictEqual([dummyObserver1, dummyObserver2]);
});
});
@@ -609,21 +556,24 @@ describe('SubController Tests', () => {
beforeEach(() => {
dummyAgile.config.waitForMount = true;
- componentSubscriptionContainer = subController.registerComponentSubscription(
+ componentSubscriptionContainer = subController.createComponentSubscriptionContainer(
dummyIntegration,
[dummyObserver1, dummyObserver2]
);
});
- it('should add componentInstance to mountedComponents and set its subscriptionContainer to ready', () => {
- subController.mount(dummyIntegration);
-
- expect(componentSubscriptionContainer.ready).toBeTruthy();
- expect(subController.mountedComponents.size).toBe(1);
- expect(
- subController.mountedComponents.has(dummyIntegration)
- ).toBeTruthy();
- });
+ 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(Array.from(subController.mountedComponents)).toStrictEqual([
+ dummyIntegration,
+ ]);
+ }
+ );
});
describe('unmount function tests', () => {
@@ -634,19 +584,23 @@ describe('SubController Tests', () => {
beforeEach(() => {
dummyAgile.config.waitForMount = true;
- componentSubscriptionContainer = subController.registerComponentSubscription(
+ componentSubscriptionContainer = subController.createComponentSubscriptionContainer(
dummyIntegration,
[dummyObserver1, dummyObserver2]
);
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([]);
+ }
+ );
});
});
});
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 7061ed61..bafcce1d 100644
--- a/packages/react/src/hooks/useAgile.ts
+++ b/packages/react/src/hooks/useAgile.ts
@@ -10,8 +10,9 @@ import {
SubscriptionContainerKeyType,
defineConfig,
isValidObject,
- ProxyKeyMapInterface,
generateId,
+ ProxyWeakMapType,
+ ComponentIdType,
} from '@agile-ts/core';
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
import { ProxyTree } from '@agile-ts/proxytree';
@@ -47,37 +48,30 @@ export function useAgile<
config: AgileHookConfigInterface = {}
): AgileHookArrayType | AgileHookType {
const depsArray = extractObservers(deps);
- const proxyTreeMap: ProxyTreeMapInterface = {};
+ 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 => {
- const handleReturn = (
- dep: State | Observer | undefined
- ): AgileHookType => {
- const value = dep?.value;
- const depKey = dep?.key;
+ const handleReturn = (dep: Observer | undefined): AgileHookType => {
+ 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)) {
- 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);
+ proxyTreeWeakMap.set(dep, proxyTree);
+ return proxyTree.proxy;
}
- return dep?.value;
+ return value;
};
// Handle single dep
@@ -85,7 +79,7 @@ export function useAgile<
return handleReturn(depsArray[0]);
}
- // Handle dep array
+ // Handle deps array
return depsArray.map((dep) => {
return handleReturn(dep);
}) as AgileHookArrayType;
@@ -112,24 +106,35 @@ export function useAgile<
(dep): dep is Observer => dep !== undefined
);
- // Build Proxy Key Map
- const proxyKeyMap: ProxyKeyMapInterface = {};
+ // 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 proxyTreeKey in proxyTreeMap) {
- const proxyTree = proxyTreeMap[proxyTreeKey];
- proxyKeyMap[proxyTreeKey] = {
- paths: proxyTree.getUsedRoutes() as any,
- };
+ 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(
+ const subscriptionContainer = agileInstance.subController.subscribe(
() => {
forceRender();
},
observers,
- { key: config.key, proxyKeyMap, waitForMount: false }
+ {
+ key: config.key,
+ proxyWeakMap,
+ waitForMount: false,
+ componentId: config.componentId,
+ }
);
// Unsubscribe Callback based Subscription on Unmount
@@ -185,6 +190,7 @@ interface AgileHookConfigInterface {
key?: SubscriptionContainerKeyType;
agileInstance?: Agile;
proxyBased?: boolean;
+ componentId?: ComponentIdType;
}
interface ProxyTreeMapInterface {
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 {};