diff --git a/packages/element-web-module-api/src/api/watchable.test.ts b/packages/element-web-module-api/src/api/watchable.test.ts index c045286e2c..e55695c29c 100644 --- a/packages/element-web-module-api/src/api/watchable.test.ts +++ b/packages/element-web-module-api/src/api/watchable.test.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ -import { expect, test, vitest } from "vitest"; +import { expect, test, vi, vitest } from "vitest"; import { Watchable } from "./watchable"; @@ -56,3 +56,44 @@ test("when value is an object, shallow comparison works", () => { watchable.unwatch(listener); // Clean up after the test }); + +test("onFirstWatch and onLastWatch are called when appropriate", () => { + const onFirstWatch = vi.fn(); + const onLastWatch = vi.fn(); + class CustomWatchable extends Watchable { + protected onFirstWatch(): void { + onFirstWatch(); + } + protected onLastWatch(): void { + onLastWatch(); + } + } + + const watchable = new CustomWatchable(10); + // No listeners yet, so expect no calls + expect(onFirstWatch).not.toHaveBeenCalled(); + expect(onLastWatch).not.toHaveBeenCalled(); + + // Let's say that we have three listeners + const listeners = [vi.fn(), vi.fn(), vi.fn()]; + + // Let's add all of them via watch + for (const listener of listeners) { + watchable.watch(listener); + } + + // Only expect onFirstWatch() to have been called once + expect(onFirstWatch).toHaveBeenCalledOnce(); + + // Let's remove all the listeners + for (const listener of listeners) { + watchable.unwatch(listener); + } + + // Only expect onLastWatch to have been called once + expect(onLastWatch).toHaveBeenCalledOnce(); + + // Should call onFirstWatch again once we have more listeners + watchable.watch(vi.fn()); + expect(onFirstWatch).toHaveBeenCalledTimes(2); +}); diff --git a/packages/element-web-module-api/src/api/watchable.ts b/packages/element-web-module-api/src/api/watchable.ts index 3fda595d2c..69296571c8 100644 --- a/packages/element-web-module-api/src/api/watchable.ts +++ b/packages/element-web-module-api/src/api/watchable.ts @@ -30,6 +30,10 @@ export class Watchable { public constructor(private currentValue: T) {} + /** + * The value stored in this watchable. + * Warning: Could potentially return stale data if you haven't called {@link Watchable#watch}. + */ public get value(): T { return this.currentValue; } @@ -50,12 +54,32 @@ export class Watchable { } public watch(listener: (value: T) => void): void { + // Call onFirstWatch if there was no listener before. + if (this.listeners.size === 0) { + this.onFirstWatch(); + } this.listeners.add(listener); } public unwatch(listener: (value: T) => void): void { - this.listeners.delete(listener); + const hasDeleted = this.listeners.delete(listener); + // Call onLastWatch if every listener has been removed. + if (hasDeleted && this.listeners.size === 0) { + this.onLastWatch(); + } } + + /** + * This is called when the number of listeners go from zero to one. + * Could be used to add external event listeners. + */ + protected onFirstWatch(): void {} + + /** + * This is called when the number of listeners go from one to zero. + * Could be used to remove external event listeners. + */ + protected onLastWatch(): void {} } /**