From 4e42654c4f0a250492ebb12dcbd6e51b0133ec65 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Tue, 5 Aug 2025 14:09:07 +0530 Subject: [PATCH] Introduce disposables to track sub vms and event listeners --- src/viewmodels/base/Disposables.ts | 69 ++++++++++++++++++++++++ test/viewmodels/base/Disposables-test.ts | 57 ++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/viewmodels/base/Disposables.ts create mode 100644 test/viewmodels/base/Disposables-test.ts diff --git a/src/viewmodels/base/Disposables.ts b/src/viewmodels/base/Disposables.ts new file mode 100644 index 0000000000..ccda8b1fb0 --- /dev/null +++ b/src/viewmodels/base/Disposables.ts @@ -0,0 +1,69 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import type { EventEmitter } from "events"; + +/** + * Something that needs to be eventually disposed. This can be: + * - A function that does the disposing + * - An object containing a dispose method which does the disposing + */ +export type DisposableItem = { dispose: () => void } | (() => void); + +/** + * This class provides a way for the view-model to track any resource + * that it needs to eventually relinquish. + */ +export class Disposables { + private readonly disposables: DisposableItem[] = []; + private _isDisposed: boolean = false; + + /** + * Relinquish all tracked disposable values + */ + public dispose(): void { + this.throwIfDisposed(); + this._isDisposed = true; + for (const disposable of this.disposables) { + if (typeof disposable === "function") { + disposable(); + } else { + disposable.dispose(); + } + } + } + + /** + * Track a value that needs to be eventually relinquished + */ + public track(disposable: T): T { + this.throwIfDisposed(); + this.disposables.push(disposable); + return disposable; + } + + /** + * Add an event listener that will be removed on dispose + */ + public trackListener(emitter: EventEmitter, event: string, callback: (...args: unknown[]) => void): void { + emitter.on(event, callback); + this.track(() => { + emitter.off(event, callback); + }); + } + + private throwIfDisposed(): void { + if (this.isDisposed) throw new Error("Disposable is already disposed"); + } + + /** + * Whether this disposable has been disposed + */ + public get isDisposed(): boolean { + return this._isDisposed; + } +} diff --git a/test/viewmodels/base/Disposables-test.ts b/test/viewmodels/base/Disposables-test.ts new file mode 100644 index 0000000000..577374a644 --- /dev/null +++ b/test/viewmodels/base/Disposables-test.ts @@ -0,0 +1,57 @@ +/* +Copyright 2025 New Vector Ltd. +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { EventEmitter } from "events"; + +import { Disposables } from "../../../src/viewmodels/base/Disposables"; + +describe("Disposable", () => { + it("isDisposed is true after dispose() is called", () => { + const disposables = new Disposables(); + expect(disposables.isDisposed).toEqual(false); + disposables.dispose(); + expect(disposables.isDisposed).toEqual(true); + }); + + it("dispose() calls the correct disposing function", () => { + const disposables = new Disposables(); + + const item1 = { + foo: 5, + dispose: jest.fn(), + }; + disposables.track(item1); + + const item2 = jest.fn(); + disposables.track(item2); + + disposables.dispose(); + + expect(item1.dispose).toHaveBeenCalledTimes(1); + expect(item2).toHaveBeenCalledTimes(1); + }); + + it("Throws error if acting on already disposed disposables", () => { + const disposables = new Disposables(); + disposables.dispose(); + expect(() => { + disposables.track(jest.fn); + }).toThrow(); + }); + + it("Removes tracked event listeners on dispose", () => { + const disposables = new Disposables(); + const emitter = new EventEmitter(); + + const fn = jest.fn(); + disposables.trackListener(emitter, "FooEvent", fn); + emitter.emit("FooEvent"); + expect(fn).toHaveBeenCalled(); + + disposables.dispose(); + expect(emitter.listenerCount("FooEvent", fn)).toEqual(0); + }); +});