diff --git a/.changeset/real-weeks-relax.md b/.changeset/real-weeks-relax.md new file mode 100644 index 00000000..4d7ff56e --- /dev/null +++ b/.changeset/real-weeks-relax.md @@ -0,0 +1,5 @@ +--- +'@tanstack/devtools-event-client': minor +--- + +fix memory leak and add internal event emission diff --git a/package.json b/package.json index c8d90d6a..04f4a63a 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ }, { "path": "packages/event-bus-client/dist/esm/plugin.js", - "limit": "1.1 KB" + "limit": "1.2 KB" } ], "devDependencies": { diff --git a/packages/event-bus-client/src/plugin.ts b/packages/event-bus-client/src/plugin.ts index 6372ea1f..99b36151 100644 --- a/packages/event-bus-client/src/plugin.ts +++ b/packages/event-bus-client/src/plugin.ts @@ -30,6 +30,8 @@ export class EventClient< #retryCount = 0 #maxRetries = 5 #connecting = false + #failedToConnect = false + #internalEventTarget: EventTarget | null = null #onConnected = () => { this.debugLog('Connected to event bus') @@ -56,7 +58,7 @@ export class EventClient< 'tanstack-connect', this.#retryConnection, ) - + this.#failedToConnect = true this.debugLog('Max retries reached, giving up on connection') this.stopConnectLoop() } @@ -90,6 +92,7 @@ export class EventClient< this.debugLog(' Initializing event subscription for plugin', this.#pluginId) this.#queuedEvents = [] this.#connected = false + this.#failedToConnect = false this.#connectIntervalId = null this.#connectEveryMs = reconnectEveryMs } @@ -107,11 +110,13 @@ export class EventClient< private stopConnectLoop() { this.#connecting = false + if (this.#connectIntervalId === null) { return } clearInterval(this.#connectIntervalId) this.#connectIntervalId = null + this.#queuedEvents = [] this.debugLog('Stopped connect loop') } @@ -189,6 +194,23 @@ export class EventClient< this.dispatchCustomEvent('tanstack-dispatch-event', event) } + createEventPayload< + TSuffix extends Extract< + keyof TEventMap, + `${TPluginId & string}:${string}` + > extends `${TPluginId & string}:${infer S}` + ? S + : never, + >( + eventSuffix: TSuffix, + payload: TEventMap[`${TPluginId & string}:${TSuffix}`], + ) { + return { + type: `${this.#pluginId}:${eventSuffix}`, + payload, + pluginId: this.#pluginId, + } + } emit< TSuffix extends Extract< keyof TEventMap, @@ -208,14 +230,27 @@ export class EventClient< ) return } + if (this.#internalEventTarget) { + this.debugLog( + 'Emitting event to internal event target', + eventSuffix, + payload, + ) + this.#internalEventTarget.dispatchEvent( + new CustomEvent(`${this.#pluginId}:${eventSuffix}`, { + detail: this.createEventPayload(eventSuffix, payload), + }), + ) + } + + if (this.#failedToConnect) { + this.debugLog('Previously failed to connect, not emitting to bus') + return + } // wait to connect to the bus if (!this.#connected) { this.debugLog('Bus not available, will be pushed as soon as connected') - this.#queuedEvents.push({ - type: `${this.#pluginId}:${eventSuffix}`, - payload, - pluginId: this.#pluginId, - }) + this.#queuedEvents.push(this.createEventPayload(eventSuffix, payload)) // start connection to event bus if (typeof CustomEvent !== 'undefined' && !this.#connecting) { this.#connectFunction() @@ -224,11 +259,7 @@ export class EventClient< return } // emit right now - return this.emitEventToBus({ - type: `${this.#pluginId}:${eventSuffix}`, - payload, - pluginId: this.#pluginId, - }) + return this.emitEventToBus(this.createEventPayload(eventSuffix, payload)) } on< @@ -246,8 +277,20 @@ export class EventClient< TEventMap[`${TPluginId & string}:${TSuffix}`] >, ) => void, + options?: { + withEventTarget?: boolean + }, ) { + const withEventTarget = options?.withEventTarget ?? false const eventName = `${this.#pluginId}:${eventSuffix}` as const + if (withEventTarget) { + if (!this.#internalEventTarget) { + this.#internalEventTarget = new EventTarget() + } + this.#internalEventTarget.addEventListener(eventName, (e) => { + cb((e as CustomEvent).detail) + }) + } if (!this.#enabled) { this.debugLog( 'Event bus client is disabled, not registering event', @@ -262,6 +305,9 @@ export class EventClient< this.#eventTarget().addEventListener(eventName, handler) this.debugLog('Registered event to bus', eventName) return () => { + if (withEventTarget) { + this.#internalEventTarget?.removeEventListener(eventName, handler) + } this.#eventTarget().removeEventListener(eventName, handler) } } diff --git a/packages/event-bus-client/tests/index.test.ts b/packages/event-bus-client/tests/index.test.ts index 219ae67d..3d9bc2ba 100644 --- a/packages/event-bus-client/tests/index.test.ts +++ b/packages/event-bus-client/tests/index.test.ts @@ -284,6 +284,27 @@ describe('EventClient', () => { }) }) + describe('emitting to internal event target', () => { + it('should initialize and dispatch events to the internal event target', () => { + const client = new EventClient({ + debug: false, + pluginId: 'test-internal', + }) + const internalEventHandler = vi.fn() + client.on('event', internalEventHandler, { + withEventTarget: true, + }) + client.emit('event', { foo: 'bar' }) + expect(internalEventHandler).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'test-internal:event', + payload: { foo: 'bar' }, + pluginId: 'test-internal', + }), + ) + }) + }) + describe('connecting behavior', () => { it('should only attempt connection once when #connecting flag is set', async () => { bus.stop()