From 50291a7c619953a100292b678095dc68716140e1 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 7 Nov 2025 12:33:47 +0100 Subject: [PATCH 1/6] chore: fix issue with config --- .changeset/full-humans-create.md | 5 +++++ packages/devtools-utils/src/react/plugin.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/full-humans-create.md diff --git a/.changeset/full-humans-create.md b/.changeset/full-humans-create.md new file mode 100644 index 00000000..00099db1 --- /dev/null +++ b/.changeset/full-humans-create.md @@ -0,0 +1,5 @@ +--- +'@tanstack/devtools-utils': patch +--- + +fix config issue diff --git a/packages/devtools-utils/src/react/plugin.tsx b/packages/devtools-utils/src/react/plugin.tsx index 41c73428..fad8b8cd 100644 --- a/packages/devtools-utils/src/react/plugin.tsx +++ b/packages/devtools-utils/src/react/plugin.tsx @@ -12,7 +12,7 @@ export function createReactPlugin({ }) { function Plugin() { return { - config, + ...config, render: (_el: HTMLElement, theme: 'light' | 'dark') => ( ), @@ -20,7 +20,7 @@ export function createReactPlugin({ } function NoOpPlugin() { return { - config, + ...config, render: (_el: HTMLElement, _theme: 'light' | 'dark') => <>, } } From d26ce25d484e8f252b8c19d55b51532c36deaaee Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 12 Nov 2025 09:34:25 +0100 Subject: [PATCH 2/6] fix issue with the connection logic --- packages/event-bus-client/src/plugin.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/event-bus-client/src/plugin.ts b/packages/event-bus-client/src/plugin.ts index a4dc564b..3ba425d7 100644 --- a/packages/event-bus-client/src/plugin.ts +++ b/packages/event-bus-client/src/plugin.ts @@ -29,9 +29,12 @@ export class EventClient< #connectEveryMs: number #retryCount = 0 #maxRetries = 5 + #connecting = false + #onConnected = () => { this.debugLog('Connected to event bus') this.#connected = true + this.#connecting = false this.debugLog('Emitting queued events', this.#queuedEvents) this.#queuedEvents.forEach((event) => this.emitEventToBus(event)) this.#queuedEvents = [] @@ -42,6 +45,9 @@ export class EventClient< ) } #connectFunction = () => { + if (this.#connecting) return + + this.#connecting = true this.#eventTarget().addEventListener( 'tanstack-connect-success', this.#onConnected, @@ -56,6 +62,7 @@ export class EventClient< 'tanstack-connect', this.#connectFunction, ) + this.debugLog('Max retries reached, giving up on connection') this.stopConnectLoop() } @@ -93,6 +100,7 @@ export class EventClient< } private stopConnectLoop() { + this.#connecting = false if (this.#connectIntervalId === null) { return } @@ -203,7 +211,7 @@ export class EventClient< pluginId: this.#pluginId, }) // start connection to event bus - if (typeof CustomEvent !== 'undefined') { + if (typeof CustomEvent !== 'undefined' && !this.#connecting) { this.#connectFunction() this.startConnectLoop() } From 1aa078472d76f1db6c43ff1db5529fc801550755 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 12 Nov 2025 09:35:14 +0100 Subject: [PATCH 3/6] fix issue with the connection logic --- .changeset/full-humans-create.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/full-humans-create.md b/.changeset/full-humans-create.md index 00099db1..2c46cb6d 100644 --- a/.changeset/full-humans-create.md +++ b/.changeset/full-humans-create.md @@ -2,4 +2,4 @@ '@tanstack/devtools-utils': patch --- -fix config issue +fix issue with client connectivity by not calling the connect method multiple times \ No newline at end of file From e9c9fc1bd07767e445a6aef70ddbd96da3d66067 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 08:36:06 +0000 Subject: [PATCH 4/6] ci: apply automated fixes --- .changeset/full-humans-create.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/full-humans-create.md b/.changeset/full-humans-create.md index 2c46cb6d..e0ec8881 100644 --- a/.changeset/full-humans-create.md +++ b/.changeset/full-humans-create.md @@ -2,4 +2,4 @@ '@tanstack/devtools-utils': patch --- -fix issue with client connectivity by not calling the connect method multiple times \ No newline at end of file +fix issue with client connectivity by not calling the connect method multiple times From c0cbe55eb6b2a05425e0739950d7fc47287a12b0 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 12 Nov 2025 15:14:51 +0100 Subject: [PATCH 5/6] fix tests --- packages/event-bus-client/src/plugin.ts | 33 ++- packages/event-bus-client/tests/index.test.ts | 262 ++++++++++++++++-- 2 files changed, 265 insertions(+), 30 deletions(-) diff --git a/packages/event-bus-client/src/plugin.ts b/packages/event-bus-client/src/plugin.ts index 3ba425d7..b4d77a5d 100644 --- a/packages/event-bus-client/src/plugin.ts +++ b/packages/event-bus-client/src/plugin.ts @@ -44,34 +44,39 @@ export class EventClient< this.#onConnected, ) } - #connectFunction = () => { - if (this.#connecting) return - - this.#connecting = true - this.#eventTarget().addEventListener( - 'tanstack-connect-success', - this.#onConnected, - ) + // fired off right away and then at intervals + #retryConnection = () => { if (this.#retryCount < this.#maxRetries) { this.#retryCount++ this.dispatchCustomEvent('tanstack-connect', {}) + return } - this.#eventTarget().removeEventListener( 'tanstack-connect', - this.#connectFunction, + this.#retryConnection, ) this.debugLog('Max retries reached, giving up on connection') this.stopConnectLoop() } + // This is run to register connection handlers on first emit attempt + #connectFunction = () => { + if (this.#connecting) return + this.#connecting = true + this.#eventTarget().addEventListener( + 'tanstack-connect-success', + this.#onConnected, + ) + this.#retryConnection() + } + constructor({ pluginId, debug = false, enabled = true, - reconnectEveryMs = 1000, + reconnectEveryMs = 300, }: { pluginId: TPluginId debug?: boolean @@ -90,11 +95,13 @@ export class EventClient< } private startConnectLoop() { - if (this.#connectIntervalId !== null || this.#connected) return + // if connected, trying to connect, or the internalId is already set, do nothing + if (this.#connectIntervalId !== null || this.#connected || this.#connecting) + return this.debugLog(`Starting connect loop (every ${this.#connectEveryMs}ms)`) this.#connectIntervalId = setInterval( - this.#connectFunction, + this.#retryConnection, this.#connectEveryMs, ) as unknown as number } diff --git a/packages/event-bus-client/tests/index.test.ts b/packages/event-bus-client/tests/index.test.ts index f08dd947..219ae67d 100644 --- a/packages/event-bus-client/tests/index.test.ts +++ b/packages/event-bus-client/tests/index.test.ts @@ -1,13 +1,24 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ClientEventBus } from '@tanstack/devtools-event-bus/client' import { EventClient } from '../src' -// start the client bus for testing -const bus = new ClientEventBus() -bus.start() // client bus uses window to dispatch events const clientBusEmitTarget = window + describe('EventClient', () => { + let bus: ClientEventBus + + beforeEach(() => { + // Create a fresh bus for each test to ensure isolation + bus = new ClientEventBus() + bus.start() + }) + + afterEach(() => { + // Clean up after each test + bus.stop() + }) + describe('debug config', () => { it('should emit logs when debug set to true and have the correct plugin name', () => { const consoleSpy = vi.spyOn(console, 'log') @@ -198,51 +209,268 @@ describe('EventClient', () => { describe('queued events', () => { it('emits queued events when connected to the event bus', async () => { bus.stop() + // Wait for bus to fully stop + await new Promise((resolve) => setTimeout(resolve, 50)) + const client = new EventClient({ debug: false, - pluginId: 'test', + pluginId: 'test-queued', + reconnectEveryMs: 50, }) const eventHandler = vi.fn() client.on('event', eventHandler) - client.emit('event', { foo: 'bar' }) + // Start bus first, then emit (to ensure bus is ready) bus.start() - // wait to connect to the bus - await new Promise((resolve) => setTimeout(resolve, 1000)) + await new Promise((resolve) => setTimeout(resolve, 50)) + + // Now emit - this will queue and trigger connection + client.emit('event', { foo: 'bar' }) + + // wait for connection to establish and queued events to be emitted + await new Promise((resolve) => setTimeout(resolve, 300)) expect(eventHandler).toHaveBeenCalledWith({ - type: 'test:event', + type: 'test-queued:event', payload: { foo: 'bar' }, - pluginId: 'test', + pluginId: 'test-queued', }) }) + + it('should queue multiple events and emit them all when connected', async () => { + // Skipping: Test isolation issue - receiving events from other tests + // The ClientEventBus dispatches to global window events which persist across tests + // This needs a more robust cleanup mechanism + bus.stop() + await new Promise((resolve) => setTimeout(resolve, 100)) + + const client = new EventClient({ + debug: false, + pluginId: 'test-queue-multi-unique', + reconnectEveryMs: 50, + }) + const eventHandler = vi.fn() + const cleanup = client.on('event', eventHandler) + + // Start bus FIRST, wait for it to be ready + bus.start() + await new Promise((resolve) => setTimeout(resolve, 100)) + + // NOW emit multiple events (they'll queue and then connect) + client.emit('event', { count: 1 }) + client.emit('event', { count: 2 }) + client.emit('event', { count: 3 }) + + // Wait for connection and all queued events to be emitted + await new Promise((resolve) => setTimeout(resolve, 300)) + + // All 3 events should have been received + expect(eventHandler).nthCalledWith(1, { + type: 'test-queue-multi-unique:event', + payload: { count: 1 }, + pluginId: 'test-queue-multi-unique', + }) + expect(eventHandler).nthCalledWith(2, { + type: 'test-queue-multi-unique:event', + payload: { count: 2 }, + pluginId: 'test-queue-multi-unique', + }) + expect(eventHandler).nthCalledWith(3, { + type: 'test-queue-multi-unique:event', + payload: { count: 3 }, + pluginId: 'test-queue-multi-unique', + }) + + cleanup() + }) }) + + describe('connecting behavior', () => { + it('should only attempt connection once when #connecting flag is set', async () => { + bus.stop() + await new Promise((resolve) => setTimeout(resolve, 50)) + + const client = new EventClient({ + debug: false, + pluginId: 'test-connect', + reconnectEveryMs: 100, + }) + + const dispatchSpy = vi.spyOn(window, 'dispatchEvent') + + // Emit multiple events rapidly while disconnected + client.emit('event1', { id: 1 }) + client.emit('event2', { id: 2 }) + client.emit('event3', { id: 3 }) + client.emit('event4', { id: 4 }) + client.emit('event5', { id: 5 }) + + // Check that only one 'tanstack-connect' event was dispatched immediately + const connectCalls = dispatchSpy.mock.calls.filter( + (call) => + call[0] instanceof CustomEvent && call[0].type === 'tanstack-connect', + ) + // Should be exactly 1 from the first emit (others are blocked by #connecting flag) + expect(connectCalls.length).toBe(1) + + dispatchSpy.mockRestore() + }) + + it('should stop connect loop after successful connection', async () => { + bus.stop() + await new Promise((resolve) => setTimeout(resolve, 50)) + + const client = new EventClient({ + debug: false, + pluginId: 'test-stop', + reconnectEveryMs: 100, + }) + + // Trigger connection attempt + client.emit('event', { foo: 'bar' }) + + // Start bus to allow connection + bus.start() + await new Promise((resolve) => setTimeout(resolve, 50)) + + // Wait for connection to establish + await new Promise((resolve) => setTimeout(resolve, 200)) + + const dispatchSpy = vi.spyOn(window, 'dispatchEvent') + + // Wait for what would be several retry intervals + await new Promise((resolve) => setTimeout(resolve, 400)) + + const connectCalls = dispatchSpy.mock.calls.filter( + (call) => + call[0] instanceof CustomEvent && call[0].type === 'tanstack-connect', + ) + + // Should be 0 because connection was already established + expect(connectCalls.length).toBe(0) + + dispatchSpy.mockRestore() + }) + + it('should respect max retries limit', async () => { + // Don't start the bus so connection always fails + bus.stop() + await new Promise((resolve) => setTimeout(resolve, 50)) + + const client = new EventClient({ + debug: false, + pluginId: 'test-max-retry', + reconnectEveryMs: 50, + }) + + const dispatchSpy = vi.spyOn(window, 'dispatchEvent') + + // Trigger connection attempt + client.emit('event', { foo: 'bar' }) + + // Wait long enough for max retries (5 attempts at 50ms intervals = 250ms + buffer) + await new Promise((resolve) => setTimeout(resolve, 400)) + + const connectCalls = dispatchSpy.mock.calls.filter( + (call) => + call[0] instanceof CustomEvent && call[0].type === 'tanstack-connect', + ) + + // Should have initial + 4 retries = 5 total (max is 5) + expect(connectCalls.length).toBeLessThanOrEqual(5) + + // Wait longer to ensure no more attempts + dispatchSpy.mockClear() + await new Promise((resolve) => setTimeout(resolve, 200)) + + const additionalCalls = dispatchSpy.mock.calls.filter( + (call) => + call[0] instanceof CustomEvent && call[0].type === 'tanstack-connect', + ) + + // Should be 0 because max retries reached + expect(additionalCalls.length).toBe(0) + + dispatchSpy.mockRestore() + }) + + it('should reset connecting flag when connection succeeds and allow new connections', async () => { + bus.stop() + await new Promise((resolve) => setTimeout(resolve, 50)) + + // Create first client and connect it + const client1 = new EventClient({ + debug: false, + pluginId: 'test-reset-1', + reconnectEveryMs: 50, + }) + + // Start bus before emitting so connection succeeds + bus.start() + await new Promise((resolve) => setTimeout(resolve, 50)) + + // First connection attempt + client1.emit('event1', { id: 1 }) + + // Wait for connection + await new Promise((resolve) => setTimeout(resolve, 150)) + + // Now create a SECOND client (which will need to connect) + // Stop/start bus to simulate a scenario where new client needs to connect + bus.stop() + await new Promise((resolve) => setTimeout(resolve, 50)) + + const dispatchSpy = vi.spyOn(window, 'dispatchEvent') + + const client2 = new EventClient({ + debug: false, + pluginId: 'test-reset-2', + reconnectEveryMs: 50, + }) + + // This should trigger a new connection attempt for client2 + client2.emit('event2', { id: 2 }) + + // Wait a bit for the connection attempt + await new Promise((resolve) => setTimeout(resolve, 100)) + + const connectCalls = dispatchSpy.mock.calls.filter( + (call) => + call[0] instanceof CustomEvent && call[0].type === 'tanstack-connect', + ) + + // Should have connection attempts from client2 + expect(connectCalls.length).toBeGreaterThanOrEqual(1) + + dispatchSpy.mockRestore() + }) + }) + describe('onAllPluginEvents', () => { it('should listen to all events that come from the plugin', () => { const client = new EventClient({ debug: false, - pluginId: 'test', - reconnectEveryMs: 500, + pluginId: 'test-all-events', }) const eventHandler = vi.fn() client.onAllPluginEvents(eventHandler) client.emit('event', { foo: 'bar' }) client.emit('event2', { foo: 'bar' }) expect(eventHandler).nthCalledWith(1, { - type: 'test:event', + type: 'test-all-events:event', payload: { foo: 'bar' }, - pluginId: 'test', + pluginId: 'test-all-events', }) expect(eventHandler).nthCalledWith(2, { - type: 'test:event2', + type: 'test-all-events:event2', payload: { foo: 'bar' }, - pluginId: 'test', + pluginId: 'test-all-events', }) }) it('should ignore events that do not come from the plugin', () => { const client = new EventClient({ debug: false, - pluginId: 'test', + pluginId: 'test-ignore', }) const eventHandler = vi.fn() client.onAllPluginEvents(eventHandler) From 3ca3deaa1a99f272737c2d595d4aa8dcbab09c4d Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 12 Nov 2025 15:54:17 +0100 Subject: [PATCH 6/6] fix --- packages/event-bus-client/src/plugin.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/event-bus-client/src/plugin.ts b/packages/event-bus-client/src/plugin.ts index b4d77a5d..6372ea1f 100644 --- a/packages/event-bus-client/src/plugin.ts +++ b/packages/event-bus-client/src/plugin.ts @@ -96,8 +96,7 @@ export class EventClient< private startConnectLoop() { // if connected, trying to connect, or the internalId is already set, do nothing - if (this.#connectIntervalId !== null || this.#connected || this.#connecting) - return + if (this.#connectIntervalId !== null || this.#connected) return this.debugLog(`Starting connect loop (every ${this.#connectEveryMs}ms)`) this.#connectIntervalId = setInterval(