Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/real-weeks-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/devtools-event-client': minor
---

fix memory leak and add internal event emission
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
},
{
"path": "packages/event-bus-client/dist/esm/plugin.js",
"limit": "1.1 KB"
"limit": "1.2 KB"
}
],
"devDependencies": {
Expand Down
68 changes: 57 additions & 11 deletions packages/event-bus-client/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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()
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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')
}

Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand All @@ -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<
Expand All @@ -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',
Expand All @@ -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)
}
}
Expand Down
21 changes: 21 additions & 0 deletions packages/event-bus-client/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading