From 30a696c47655d5767625f43a59a8ca8fd412086b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:39:00 +1000 Subject: [PATCH] feat(ui): add simple pubsub --- .../web/src/common/util/PubSub/PubSub.test.ts | 117 ++++++++++++++++++ .../web/src/common/util/PubSub/PubSub.ts | 76 ++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 invokeai/frontend/web/src/common/util/PubSub/PubSub.test.ts create mode 100644 invokeai/frontend/web/src/common/util/PubSub/PubSub.ts diff --git a/invokeai/frontend/web/src/common/util/PubSub/PubSub.test.ts b/invokeai/frontend/web/src/common/util/PubSub/PubSub.test.ts new file mode 100644 index 0000000000..108d68ff88 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/PubSub/PubSub.test.ts @@ -0,0 +1,117 @@ +import { PubSub } from 'common/util/PubSub/PubSub'; +import { describe, expect, it, vi } from 'vitest'; + +describe('PubSub', () => { + it('should call listener when value is published and value changes', () => { + const pubsub = new PubSub(1); + const listener = vi.fn(); + + pubsub.subscribe(listener); + pubsub.publish(42); + + expect(listener).toHaveBeenCalledWith(42, 1); + }); + + it('should not call listener if value does not change', () => { + const pubsub = new PubSub(42); + const listener = vi.fn(); + + pubsub.subscribe(listener); + pubsub.publish(42); + + expect(listener).not.toHaveBeenCalled(); + }); + + it('should handle non-primitive values', () => { + const pubsub = new PubSub<{ foo: string }>({ foo: 'bar' }); + const listener = vi.fn(); + + pubsub.subscribe(listener); + pubsub.publish({ foo: 'bar' }); + + expect(listener).toHaveBeenCalled(); + }); + + it('should call listener with old and new value', () => { + const pubsub = new PubSub(1); + const listener = vi.fn(); + + pubsub.subscribe(listener); + pubsub.publish(2); + + expect(listener).toHaveBeenCalledWith(2, 1); + }); + + it('should allow unsubscribing', () => { + const pubsub = new PubSub(1); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + + const unsubscribe = pubsub.subscribe(listener1); + pubsub.subscribe(listener2); + unsubscribe(); + pubsub.publish(42); + + expect(listener1).not.toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + expect(pubsub.getListeners().size).toBe(1); + }); + + it('should clear all listeners', () => { + const pubsub = new PubSub(1); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + + pubsub.subscribe(listener1); + pubsub.subscribe(listener2); + pubsub.off(); + pubsub.publish(42); + + expect(listener1).not.toHaveBeenCalled(); + expect(listener2).not.toHaveBeenCalled(); + expect(pubsub.getListeners().size).toBe(0); + }); + + it('should use custom compareFn', () => { + const compareFn = (a: number, b: number) => Math.abs(a) === Math.abs(b); + const pubsub = new PubSub(1, compareFn); + const listener = vi.fn(); + + pubsub.subscribe(listener); + pubsub.publish(-1); + + expect(listener).not.toHaveBeenCalled(); + }); + + it('should handle multiple listeners', () => { + const pubsub = new PubSub(1); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + + pubsub.subscribe(listener1); + pubsub.subscribe(listener2); + pubsub.publish(42); + + expect(listener1).toHaveBeenCalledWith(42, 1); + expect(listener2).toHaveBeenCalledWith(42, 1); + expect(pubsub.getListeners().size).toBe(2); + }); + + it('should get the current value', () => { + const pubsub = new PubSub(42); + expect(pubsub.getValue()).toBe(42); + pubsub.publish(43); + expect(pubsub.getValue()).toBe(43); + }); + + it('should get the listeners', () => { + const pubsub = new PubSub(1); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + + pubsub.subscribe(listener1); + pubsub.subscribe(listener2); + + expect(pubsub.getListeners()).toEqual(new Set([listener1, listener2])); + }); +}); diff --git a/invokeai/frontend/web/src/common/util/PubSub/PubSub.ts b/invokeai/frontend/web/src/common/util/PubSub/PubSub.ts new file mode 100644 index 0000000000..5ae6779b74 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/PubSub/PubSub.ts @@ -0,0 +1,76 @@ +export type Listener = (newValue: T, oldValue: T) => void; +export type CompareFn = (a: T, b: T) => boolean; + +/** + * A simple PubSub implementation. + * + * @template T The type of the value to be published. + * @param initialValue The initial value to publish. + */ +export class PubSub { + private _listeners: Set> = new Set(); + private _oldValue: T; + private _compareFn: CompareFn; + + public constructor(initialValue: T, compareFn?: CompareFn) { + this._oldValue = initialValue; + this._compareFn = compareFn || ((a, b) => a === b); + } + + /** + * Subscribes to the PubSub. + * @param listener The listener to be called when the value is published. + * @returns A function that can be called to unsubscribe the listener. + */ + public subscribe = (listener: Listener): (() => void) => { + this._listeners.add(listener); + + return () => { + this.unsubscribe(listener); + }; + }; + + /** + * Unsubscribes a listener from the PubSub. + * @param listener The listener to unsubscribe. + */ + public unsubscribe = (listener: Listener): void => { + this._listeners.delete(listener); + }; + + /** + * Publishes a new value to the PubSub. + * @param newValue The new value to publish. + */ + public publish = (newValue: T): void => { + if (!this._compareFn(this._oldValue, newValue)) { + for (const listener of this._listeners) { + listener(newValue, this._oldValue); + } + this._oldValue = newValue; + } + }; + + /** + * Clears all listeners from the PubSub. + */ + public off = (): void => { + this._listeners.clear(); + }; + + /** + * Gets the current value of the PubSub. + * @returns The current value of the PubSub. + */ + public getValue = (): T | undefined => { + return this._oldValue; + }; + + /** + * Gets the listeners of the PubSub. + * @returns The listeners of the PubSub. + */ + public getListeners = (): Set> => { + return this._listeners; + }; +}