From 39aa58c5d96802e5ddf79613d80c417e9f76e4d9 Mon Sep 17 00:00:00 2001 From: b-ma Date: Fri, 18 Oct 2024 12:57:28 +0200 Subject: [PATCH] feat: implement SharedStateCollection#onChange --- src/common/SharedState.js | 2 +- src/common/SharedStateCollection.js | 30 +++++++++++++++++-- tests/states/SharedStateCollection.spec.js | 34 ++++++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/common/SharedState.js b/src/common/SharedState.js index ef044260..6d37090d 100644 --- a/src/common/SharedState.js +++ b/src/common/SharedState.js @@ -453,7 +453,7 @@ ${JSON.stringify(initValues, null, 2)}`); } if (isPlainObject(arguments[0]) && isPlainObject(arguments[1])) { - logger.removed('`context` argument in SharedState.set(updates, context)', 'a regular parameter set with `event=true` behavior', '4.0.0-alpha.29'); + logger.removed('`context` argument in SharedState.set(updates, context)', 'a regular parameter configured with `event=true`', '4.0.0-alpha.29'); } if (arguments.length === 2 && isString(updates)) { diff --git a/src/common/SharedStateCollection.js b/src/common/SharedStateCollection.js index 126e0ee1..8b7eb9f8 100644 --- a/src/common/SharedStateCollection.js +++ b/src/common/SharedStateCollection.js @@ -44,6 +44,7 @@ class SharedStateCollection { #onUpdateCallbacks = new Set(); #onAttachCallbacks = new Set(); #onDetachCallbacks = new Set(); + #onChangeCallbacks = new Set(); #unobserve = null; constructor(stateManager, className, filter = null, options = {}) { @@ -77,15 +78,16 @@ class SharedStateCollection { this.#states.splice(index, 1); this.#onDetachCallbacks.forEach(callback => callback(state)); + this.#onChangeCallbacks.forEach(callback => callback()); }); state.onUpdate((newValues, oldValues) => { - Array.from(this.#onUpdateCallbacks).forEach(callback => { - callback(state, newValues, oldValues); - }); + this.#onUpdateCallbacks.forEach(callback => callback(state, newValues, oldValues)); + this.#onChangeCallbacks.forEach(callback => callback()); }); this.#onAttachCallbacks.forEach(callback => callback(state)); + this.#onChangeCallbacks.forEach(callback => callback()); }, this.#options); } @@ -330,18 +332,40 @@ class SharedStateCollection { return () => this.#onDetachCallbacks.delete(callback); } + /** + * Register a function to execute on any change (i.e. create, delete or update) + * that occurs on the the collection. + * + * @param {Function} callback - callback to execute when a change occurs. + * @returns {Function} - Function that delete the registered listener. + * @example + * const collection = await client.stateManager.getCollection('player'); + * collection.onChange(() => renderApp(), true); + */ + onChange(callback, executeListener = false) { + if (executeListener === true) { + callback(); + } + + this.#onChangeCallbacks.add(callback); + + return () => this.#onChangeCallbacks.delete(callback); + } + /** * Detach from the collection, i.e. detach all underlying shared states. * @type {number} */ async detach() { this.#unobserve(); + this.#onAttachCallbacks.clear(); this.#onUpdateCallbacks.clear(); const promises = this.#states.map(state => state.detach()); await Promise.all(promises); this.#onDetachCallbacks.clear(); + this.#onChangeCallbacks.clear(); } /** diff --git a/tests/states/SharedStateCollection.spec.js b/tests/states/SharedStateCollection.spec.js index 8faf5b10..f52e7b88 100644 --- a/tests/states/SharedStateCollection.spec.js +++ b/tests/states/SharedStateCollection.spec.js @@ -410,6 +410,40 @@ describe(`# SharedStateCollection`, () => { }); }); + describe(`## onChange()`, () => { + it(`should be executed on each collection change`, async () => { + const collection = await clients[1].stateManager.getCollection('a'); + let called = 0; + + collection.onChange(() => called += 1); + + const state = await clients[0].stateManager.create('a'); + await delay(10); // make sure the state is properly attached in collection + await state.set({ bool: true }); + await delay(10); + await state.delete(); + await delay(10); + + assert.equal(called, 3); + }); + + it(`should be executed now if executeListener is true`, async () => { + const collection = await clients[1].stateManager.getCollection('a'); + let called = 0; + + collection.onChange(() => called += 1, true); + + const state = await clients[0].stateManager.create('a'); + await delay(10); // make sure the state is properly attached in collection + await state.set({ bool: true }); + await delay(10); + await state.delete(); + await delay(10); + + assert.equal(called, 4); + }); + }); + describe(`## [Symbol.iterator]`, () => { // this tends to show a bug it(`should implement iterator API`, async () => {