Skip to content

feat: network transport fallback for isolated server runtimes#384

Open
AlemTuzlak wants to merge 16 commits intomainfrom
worktree-polished-cuddling-lark
Open

feat: network transport fallback for isolated server runtimes#384
AlemTuzlak wants to merge 16 commits intomainfrom
worktree-polished-cuddling-lark

Conversation

@AlemTuzlak
Copy link
Collaborator

@AlemTuzlak AlemTuzlak commented Mar 12, 2026

Summary

Fixes TanStack/ai#339

When TanStack Start uses Nitro v3's nitro() Vite plugin (or any runtime that isolates server code in a separate thread/process), devtools events break because globalThis.__TANSTACK_EVENT_TARGET__ is not shared across isolation boundaries.

This PR adds automatic network transport fallback:

  • EventClient detects isolated server environments (no shared EventTarget, no window) and automatically connects to ServerEventBus via WebSocket
  • ServerEventBus distinguishes "server bridge" connections (?bridge=server) from browser clients and routes bridge messages through both emitEventToClients() (browser devtools) and emitToServer() (in-process EventTarget)
  • Echo prevention via 200-entry ring buffer of event IDs
  • Reconnection with exponential backoff (100ms → 5s, max 10 attempts) then HTTP POST fallback
  • Zero API changes — existing consumers work unchanged
  • Zero configuration — detection and fallback are automatic
  • Dev-only — network transport only activates when Vite plugin replaces compile-time placeholders

Changes

  • packages/event-bus/src/server/server.ts — bridge WebSocket support, POST source-based routing
  • packages/event-bus/src/client/client.ts — interface update (eventId, source fields)
  • packages/event-bus-client/src/plugin.ts — network transport detection, WebSocket connection, emit/receive, reconnect, HTTP fallback
  • packages/event-bus-client/src/ring-buffer.ts — new RingBuffer utility for dedup
  • packages/event-bus-client/src/index.ts — export createNetworkTransportClient

Test plan

  • 60 event-bus tests pass (26 server tests including 5 new bridge/POST tests)
  • 28 event-bus-client tests pass (including 4 network transport + 2 integration + 3 ring buffer)
  • Bidirectional events: client→server and server→client via WebSocket
  • Echo deduplication: client does not receive its own events back
  • Event queuing: events queue during connection and flush on connect
  • Multiple isolated clients work simultaneously
  • Manual test with Nitro v3 nitro() plugin in examples/react/start

Summary by CodeRabbit

  • New Features

    • Network transport fallback for devtools in isolated server runtimes (e.g., Cloudflare Workers, Nitro v3).
    • Server event tracking and deduplication in devtools.
    • New React examples demonstrating server event monitoring.
    • createNetworkTransportClient API for enabling network-based event communication.
  • Documentation

    • Added design and implementation guides for network transport fallback.

…er runtimes

Addresses the issue where devtools events are lost when server code runs in
isolated environments (Nitro v3 worker threads, Cloudflare Workers, etc.)
that don't share globalThis with the Vite main thread.
Fix problem description precision, URL matching and handleNewConnection
signature issues, POST handler routing, placeholder convention, triplicate
interface sync, queue preservation, and multi-worker echo safety.
Disambiguate that both standalone and external server POST/upgrade
handlers need updates, and that only WebSocket URL matching needs
prefix change (not SSE/POST URLs).
7-task plan covering: interface updates, ServerEventBus bridge support,
POST handler routing, RingBuffer utility, EventClient network transport
detection, WebSocket connection/emit/receive, and integration tests.
- Accept WebSocket connections with ?bridge=server query parameter
- Track bridge clients separately for proper routing
- Bridge messages route through emit() (broadcast to WS clients + EventTarget)
- Regular browser messages route through emitToServer() (EventTarget only)
- Clean up bridge client tracking on disconnect and stop()
When EventClient detects it is in an isolated server environment
(no shared globalThis.__TANSTACK_EVENT_TARGET__, no window), it
automatically connects to ServerEventBus via WebSocket. Bidirectional:
events emitted in the worker reach the devtools panel, and events
from the devtools panel reach listeners in the worker.

Includes echo prevention via 200-entry ring buffer, exponential
backoff reconnection, HTTP POST fallback, and event queuing.
- Add scheduleReconnect() call in error handler for non-browser runtimes
  where 'close' may not follow 'error'
- Reset wsGaveUp, wsReconnectAttempts, wsReconnectDelay in
  ___destroyNetworkTransport for safe reuse
@changeset-bot
Copy link

changeset-bot bot commented Mar 12, 2026

⚠️ No Changeset found

Latest commit: 3f82eeb

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@nx-cloud
Copy link

nx-cloud bot commented Mar 12, 2026

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit 64ae1bb

Command Status Duration Result
nx affected --targets=test:eslint,test:sherif,t... ❌ Failed 2m 17s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 22s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-12 14:38:15 UTC

AlemTuzlak and others added 2 commits March 12, 2026 16:59
Two minimal examples for manually testing the network transport fallback:

- examples/react/start-nitro — TanStack Start + Nitro v3 (worker threads)
- examples/react/start-cloudflare — TanStack Start + Cloudflare Workers

Both emit devtools events from server functions and display them in a
custom "Server Events" devtools panel. If events appear in the panel,
the network transport fallback is working correctly.
@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

This pull request implements a network transport fallback system for TanStack devtools to support isolated server runtimes (Cloudflare Workers, Nitro). It introduces optional WebSocket bridging with HTTP POST fallback, adds event deduplication using a RingBuffer utility, and extends the event protocol with eventId and source tracking. Two new example projects demonstrate the integrated solution.

Changes

Cohort / File(s) Summary
Design & Documentation
docs/superpowers/plans/2026-03-12-network-transport-fallback.md, docs/superpowers/specs/2026-03-12-network-transport-fallback-design.md
Planning and detailed design specifications for network transport fallback implementation, including architecture, protocol changes, and test scaffolding plan.
Event Bus Core — Interface Extensions
packages/event-bus/src/server/server.ts, packages/event-bus/src/client/client.ts, packages/event-bus-client/src/plugin.ts
Added eventId?: string and source?: 'server-bridge' optional fields to TanStackDevtoolsEvent interface across three export points to enable event deduplication and source tracking.
RingBuffer Utility
packages/event-bus-client/src/ring-buffer.ts, packages/event-bus-client/tests/ring-buffer.test.ts
New exported RingBuffer class for fixed-capacity circular buffer with set-based lookups for deduplication; includes unit tests validating add/has operations and wrap-around eviction behavior.
Network Transport Client
packages/event-bus-client/src/plugin.ts, packages/event-bus-client/src/index.ts
Extended EventClient with network transport support including WebSocket connection, exponential backoff retry, HTTP POST fallback, event queuing, and lifecycle control; exported createNetworkTransportClient factory function.
Server Bridge Routing
packages/event-bus/src/server/server.ts
Updated ServerEventBus to track bridge clients, detect bridge connections via URL query parameter, and route bridge vs. browser messages to appropriate targets; handles WebSocket upgrade and POST message routing based on source.
Network Transport Tests
packages/event-bus-client/tests/network-transport.test.ts, packages/event-bus-client/tests/integration.test.ts, packages/event-bus/tests/server.test.ts
Comprehensive test coverage for WebSocket lifecycle, event emission/reception, echo deduplication, event queuing on reconnect, multi-client scenarios, and server bridge acceptance/routing.
Cloudflare Example Project
examples/react/start-cloudflare/.gitignore, examples/react/start-cloudflare/package.json, examples/react/start-cloudflare/tsconfig.json, examples/react/start-cloudflare/vite.config.ts, examples/react/start-cloudflare/wrangler.jsonc, examples/react/start-cloudflare/src/devtools/*, examples/react/start-cloudflare/src/routes/*, examples/react/start-cloudflare/src/router.tsx
New React + Start example for Cloudflare Workers demonstrating network transport integration, server function event tracking, and ServerEventsPanel UI component.
Nitro Example Project
examples/react/start-nitro/.gitignore, examples/react/start-nitro/package.json, examples/react/start-nitro/tsconfig.json, examples/react/start-nitro/vite.config.ts, examples/react/start-nitro/src/devtools/*, examples/react/start-nitro/src/routes/*, examples/react/start-nitro/src/router.tsx
New React + Start example for Nitro v3 demonstrating network transport integration, server function event tracking, and ServerEventsPanel UI component.

Sequence Diagram

sequenceDiagram
    participant Client as Isolated Client<br/>(EventClient)
    participant WS as WebSocket<br/>Connection
    participant HTTP as HTTP Fallback<br/>POST Endpoint
    participant Server as ServerEventBus<br/>(Main Process)
    
    Note over Client,Server: Network Transport Initialization
    Client->>WS: connectWebSocket()<br/>/__devtools/ws?bridge=server
    alt WebSocket Available
        WS->>Server: Upgrade request (bridge=server)
        Server->>Server: Track in bridgeClients
        Server-->>WS: Connection established
        Client->>Client: Store WebSocket reference
    else WebSocket Unavailable
        Client->>Client: scheduleReconnect()<br/>with backoff
    end
    
    Note over Client,Server: Event Emission & Routing
    Client->>Client: Generate eventId
    Client->>Client: Add eventId to RingBuffer
    alt WebSocket Connected
        Client->>WS: Send event{eventId,source}
        WS->>Server: Receive message
        Server->>Server: Route to in-process<br/>& browser clients
        Server-->>WS: Broadcast to browsers
    else WebSocket Disconnected
        Client->>Client: Queue event
        Client->>HTTP: Fallback POST<br/>/__devtools/send
        HTTP->>Server: Receive {eventId,source}
        Server->>Server: Route via POST handler
        Server-->>WS: Broadcast to browsers
    end
    
    Note over Client,Server: Server→Client Event Delivery
    Server->>Server: Dispatch server event
    Server->>WS: Send to bridge client
    WS->>Client: Receive event
    Client->>Client: Check RingBuffer.has(eventId)
    alt Not Duplicate
        Client->>Client: Deliver to listeners
    else Self-Echo (Dedup)
        Client->>Client: Discard (skip)
    end
    
    Note over Client,Server: Reconnection with Buffering
    WS->>WS: Connection lost
    Client->>Client: Queue pending events
    Client->>Client: scheduleReconnect()<br/>exponential backoff
    Client->>WS: Retry connectWebSocket()
    WS->>Server: Reconnect
    Server-->>WS: OK
    Client->>Client: Flush queued events
    Client->>WS: Send all buffered<br/>with fresh eventIds
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hops with delight

Through network, we now bound with grace,
WebSocket and HTTP save the race,
A RingBuffer tracks our IDs with care,
While servers and clients share events in the air,
Cloudflare and Nitro—no more left behind! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding automatic network transport fallback for isolated server runtimes, which is the primary focus of the entire changeset.
Description check ✅ Passed The PR description covers the issue link, motivation, architectural changes, test plan, and includes a detailed summary of the changes with clear explanations of what was modified and why.
Linked Issues check ✅ Passed The PR successfully implements the infrastructure required to fix issue #339 by adding network transport fallback so server events emitted in isolated runtimes reach the ServerEventBus. The EventClient detects isolation and connects via WebSocket, ServerEventBus routes bridge messages appropriately, and the implementation includes deduplication, reconnection, and HTTP fallback—addressing all requirements specified in the issue.
Out of Scope Changes check ✅ Passed All changes are within scope: they directly support network transport fallback for isolated runtimes (ServerEventBus bridge routing, EventClient network detection, RingBuffer dedup, and examples for Nitro/Cloudflare). No unrelated features or refactoring detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch worktree-polished-cuddling-lark
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (8)
examples/react/start-cloudflare/.gitignore (1)

6-11: Consider adding env variant ignores to reduce secret leak risk.

You already ignore .env; adding .env.* and Cloudflare local secret files (for example .dev.vars / .dev.vars.*) would provide better protection against accidental commits in local/dev workflows.

Proposed update
 .env
+.env.*
+.dev.vars
+.dev.vars.*
 .nitro
 .tanstack
 .wrangler
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-cloudflare/.gitignore` around lines 6 - 11, Update the
.gitignore entry that currently lists ".env" to also ignore environment file
variants and common Cloudflare local secret files: add patterns like ".env.*",
"*.env.local", ".dev.vars" and ".dev.vars.*" (or other local secret naming used
in this project) so accidental commits of local/dev secrets are prevented;
ensure these patterns are added alongside existing entries (".env", ".nitro",
".tanstack", ".wrangler", ".output", ".vinxi") in the
examples/react/start-cloudflare/.gitignore file.
examples/react/start-cloudflare/package.json (1)

12-20: Consider moving build-time Vite plugins to devDependencies.

The following packages are Vite plugins used only during development/build and do not run at runtime in Cloudflare Workers:

  • @cloudflare/vite-plugin (line 12)
  • @tanstack/router-plugin (line 17)
  • vite-tsconfig-paths (line 20)

Moving them to devDependencies more accurately reflects their purpose and can reduce confusion about what runs in the deployed worker.

♻️ Proposed fix
   "dependencies": {
-    "@cloudflare/vite-plugin": "^1.13.8",
     "@tanstack/devtools-event-client": "workspace:*",
     "@tanstack/react-devtools": "workspace:*",
     "@tanstack/react-router": "^1.132.0",
     "@tanstack/react-start": "^1.132.0",
-    "@tanstack/router-plugin": "^1.132.0",
     "react": "^19.2.0",
-    "react-dom": "^19.2.0",
-    "vite-tsconfig-paths": "^6.0.2"
+    "react-dom": "^19.2.0"
   },
   "devDependencies": {
+    "@cloudflare/vite-plugin": "^1.13.8",
     "@tanstack/devtools-vite": "workspace:*",
+    "@tanstack/router-plugin": "^1.132.0",
     "@types/node": "^22.15.2",
     "@types/react": "^19.2.0",
     "@types/react-dom": "^19.2.0",
     "@vitejs/plugin-react": "^5.0.4",
     "typescript": "~5.9.2",
     "vite": "^7.1.7",
+    "vite-tsconfig-paths": "^6.0.2",
     "wrangler": "^4.40.3"
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-cloudflare/package.json` around lines 12 - 20, The
package.json currently lists build-time Vite plugins as runtime dependencies;
remove "@cloudflare/vite-plugin", "@tanstack/router-plugin", and
"vite-tsconfig-paths" from the "dependencies" section and add them to
"devDependencies" with the same version strings so they are only installed for
build/dev. Ensure you update the package.json keys accordingly (keep other
packages untouched) and run your package manager to refresh lockfiles so the
change is reflected in installs.
examples/react/start-nitro/package.json (1)

16-16: Pin nitro to a specific version instead of using latest.

At Line 16, "nitro": "latest" is inconsistent with the rest of the dependencies and makes this example non-reproducible. The lockfile resolves to nitro@3.0.1-alpha.2; pin the dependency to this version or a specific stable release:

-    "nitro": "latest",
+    "nitro": "^3.0.1-alpha.2",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-nitro/package.json` at line 16, The dependency entry
"nitro": "latest" in package.json makes the example non-reproducible; update the
"nitro" dependency (the "nitro" property in package.json) to a specific version
(e.g., "3.0.1-alpha.2" or a chosen stable release) so the example and lockfile
remain deterministic, then run npm/yarn install to refresh the lockfile.
examples/react/start-cloudflare/src/routes/index.tsx (1)

60-63: Consider adding error handling for server function calls.

The async button handlers call server functions without try/catch. If a server function fails (network error, worker crash), the promise rejection will be unhandled.

💡 Example error handling
         onClick={async () => {
+          try {
             const msg = await greet()
             addResult(msg)
+          } catch (e) {
+            addResult(`Error: ${e instanceof Error ? e.message : 'Unknown error'}`)
+          }
         }}

Also applies to: 78-81, 96-99

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-cloudflare/src/routes/index.tsx` around lines 60 - 63,
Wrap each async server-call handler (the onClick callbacks that call greet(),
and similar handlers at the other spots) in a try/catch: call the server
function inside try, await the result, and then call addResult(msg) on success;
in catch, log the error and surface a user-friendly message (e.g., via addResult
or an error state) so promise rejections from greet() (or other server
functions) are handled and do not stay unhandled. Ensure you update all three
handlers (the one invoking greet() and the other handlers at the 78-81 and 96-99
locations) and reference the existing functions greet() and addResult when
implementing the try/catch handling.
examples/react/start-cloudflare/src/devtools/ServerEventsPanel.tsx (1)

105-107: Consider a more unique key for event list items.

Using ${ev.timestamp}-${index} as the key could cause React reconciliation issues if events arrive with identical timestamps (possible with rapid emission). Since events are prepended, the index of existing items shifts on each new event, which combined with timestamp collisions could lead to incorrect DOM updates.

💡 Consider adding a unique identifier

If events gain an eventId field from the transport layer (as mentioned in the PR objectives), use that instead:

-            <div
-              key={`${ev.timestamp}-${index}`}
+            <div
+              key={ev.eventId ?? `${ev.timestamp}-${index}`}

Alternatively, generate a client-side ID when storing events.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-cloudflare/src/devtools/ServerEventsPanel.tsx` around
lines 105 - 107, The list key for events in ServerEventsPanel (events.map ->
key={`${ev.timestamp}-${index}`}) is unstable; change it to use a stable unique
id: prefer using ev.eventId (if provided by the transport) or a client-generated
id assigned when the event is created/added (e.g., set ev.id or event.id inside
the event insertion logic). Update the mapping to use that stable id
(key={ev.eventId || ev.id}) so prepending new events won't shift keys and break
React reconciliation.
packages/event-bus/src/server/server.ts (1)

156-167: Extract the POST bridge routing into one helper.

Both server modes now duplicate the same source === 'server-bridge' branch. Pulling that into a shared helper will keep standalone and piggyback mode from drifting the next time the bridge protocol changes.

Also applies to: 274-288

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus/src/server/server.ts` around lines 156 - 167, Extract the
duplicated POST bridge routing into a single helper (e.g.
handleDevtoolsSendRequest) that performs the body collection, calls
parseWithBigInt, logs via this.debugLog('Received event from client', msg) and
then calls either this.emit(msg) or this.emitToServer(msg) based on msg.source
=== 'server-bridge'; replace the duplicated blocks currently handling req.url
=== '/__devtools/send' in both server modes (the block around the
parseWithBigInt/this.debugLog/this.emit/this.emitToServer sequence and the
similar block at the other location) with calls to this new helper so both modes
share the same logic.
packages/event-bus-client/src/plugin.ts (1)

581-595: Use plain comments for these private helpers.

@internal JSDoc on private methods will still flow into generated docs, which makes the website output noisy for implementation details. A normal // comment here is a better fit.

As per coding guidelines, **/*.{js,ts,jsx,tsx}: JSDoc should read like documentation as it gets converted to markdown docs for the website.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus-client/src/plugin.ts` around lines 581 - 595, The JSDoc
`@internal` on private helper methods is leaking into generated docs; replace the
/** `@internal` */ JSDoc blocks on the private helpers (specifically
___enableNetworkTransport and ___destroyNetworkTransport) with plain single-line
comments (e.g., // internal — only for testing) so they are treated as
implementation comments rather than documentation and won’t be emitted into the
site output.
examples/react/start-nitro/src/devtools/ServerEventsPanel.tsx (1)

20-28: Consider hydration safety for time formatting.

toLocaleTimeString can produce different output on server vs client due to locale/timezone differences, potentially causing hydration mismatches. Since this is a devtools panel (client-only rendering context), this is likely fine, but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-nitro/src/devtools/ServerEventsPanel.tsx` around lines
20 - 28, formatTime currently uses toLocaleTimeString which can differ between
server and client and cause hydration mismatches; make the output deterministic
by specifying an explicit timeZone (e.g., add timeZone: 'UTC' to the options) or
move formatting to run only on the client (compute inside a useEffect or when
rendering client-only components) so timezone/locale differences won't cause
hydration errors — update the formatTime function to use Intl.DateTimeFormat or
toLocaleTimeString with a fixed timeZone option, or ensure calls to formatTime
occur only on the client side.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/react/start-nitro/package.json`:
- Line 19: Move the "vite-tsconfig-paths" dependency from runtime dependencies
into devDependencies: remove the "vite-tsconfig-paths": "^6.0.2" entry under
dependencies in package.json and add the same entry under devDependencies (so
build-time only tools used by vite.config.ts are not bundled at runtime); ensure
package-lock/yarn lock is updated by reinstalling or running the package manager
after the change.

In `@packages/event-bus-client/src/plugin.ts`:
- Around line 50-54: The event ID generation (generateEventId using
globalEventIdCounter) is only unique per module instance and can collide across
workers; create a process-scoped random/unique prefix (e.g., instancePrefix
generated once at module init using crypto.randomBytes or a high-entropy
Math.random hex) and prepend it to the existing
`${++globalEventIdCounter}-${Date.now()}` scheme so generateEventId returns
`${instancePrefix}-${++globalEventIdCounter}-${Date.now()}`, ensuring
instance-scoped entropy and avoiding cross-worker collisions; ensure
instancePrefix is initialized once (top-level) and referenced in
generateEventId.
- Around line 475-483: The branch inside the this.#useNetworkTransport path
incorrectly queues events when the client has given up on WebSocket; update the
logic in the block that calls createEventPayload so that if this.#wsGaveUp is
true you bypass queuing and connectWebSocket(), and immediately call
sendViaNetwork (or sendViaHttp for non-network transport) instead; specifically,
in the code handling this.#useNetworkTransport, check this.#wsGaveUp before
pushing to this.#queuedEvents and short-circuit to sendViaNetwork/sendViaHttp,
leaving connectWebSocket() only for cases where a reconnect attempt should
actually be made.
- Around line 304-308: The bridge currently uses JSON.parse/JSON.stringify which
drops BigInt values; change the ws message handler in
ws.addEventListener('message') to call parseWithBigInt(data) instead of
JSON.parse(data), and update the bridge send methods (the functions around the
send calls at the locations corresponding to lines 408 and 428) to use
stringifyWithBigInt(payload) instead of JSON.stringify(payload). Ensure
parseWithBigInt and stringifyWithBigInt are imported from the event-bus
utilities and replace all plain JSON.parse/JSON.stringify usages in those
handlers/sendters to maintain BigInt-safe wire format compatibility.

In `@packages/event-bus-client/src/ring-buffer.ts`:
- Around line 7-11: The RingBuffer constructor must validate the incoming
capacity and fail fast for non-positive or non-integer values: in the
constructor (class RingBuffer) add a guard that checks capacity is a positive
integer (capacity > 0 && Number.isInteger(capacity)) and throw a clear
RangeError or TypeError if not, so fields like `#capacity`, `#buffer` and `#index`
remain valid and add() cannot enter a broken state; update the constructor that
currently sets this.#capacity, this.#buffer and this.#set to perform this
validation before initializing those fields.

---

Nitpick comments:
In `@examples/react/start-cloudflare/.gitignore`:
- Around line 6-11: Update the .gitignore entry that currently lists ".env" to
also ignore environment file variants and common Cloudflare local secret files:
add patterns like ".env.*", "*.env.local", ".dev.vars" and ".dev.vars.*" (or
other local secret naming used in this project) so accidental commits of
local/dev secrets are prevented; ensure these patterns are added alongside
existing entries (".env", ".nitro", ".tanstack", ".wrangler", ".output",
".vinxi") in the examples/react/start-cloudflare/.gitignore file.

In `@examples/react/start-cloudflare/package.json`:
- Around line 12-20: The package.json currently lists build-time Vite plugins as
runtime dependencies; remove "@cloudflare/vite-plugin",
"@tanstack/router-plugin", and "vite-tsconfig-paths" from the "dependencies"
section and add them to "devDependencies" with the same version strings so they
are only installed for build/dev. Ensure you update the package.json keys
accordingly (keep other packages untouched) and run your package manager to
refresh lockfiles so the change is reflected in installs.

In `@examples/react/start-cloudflare/src/devtools/ServerEventsPanel.tsx`:
- Around line 105-107: The list key for events in ServerEventsPanel (events.map
-> key={`${ev.timestamp}-${index}`}) is unstable; change it to use a stable
unique id: prefer using ev.eventId (if provided by the transport) or a
client-generated id assigned when the event is created/added (e.g., set ev.id or
event.id inside the event insertion logic). Update the mapping to use that
stable id (key={ev.eventId || ev.id}) so prepending new events won't shift keys
and break React reconciliation.

In `@examples/react/start-cloudflare/src/routes/index.tsx`:
- Around line 60-63: Wrap each async server-call handler (the onClick callbacks
that call greet(), and similar handlers at the other spots) in a try/catch: call
the server function inside try, await the result, and then call addResult(msg)
on success; in catch, log the error and surface a user-friendly message (e.g.,
via addResult or an error state) so promise rejections from greet() (or other
server functions) are handled and do not stay unhandled. Ensure you update all
three handlers (the one invoking greet() and the other handlers at the 78-81 and
96-99 locations) and reference the existing functions greet() and addResult when
implementing the try/catch handling.

In `@examples/react/start-nitro/package.json`:
- Line 16: The dependency entry "nitro": "latest" in package.json makes the
example non-reproducible; update the "nitro" dependency (the "nitro" property in
package.json) to a specific version (e.g., "3.0.1-alpha.2" or a chosen stable
release) so the example and lockfile remain deterministic, then run npm/yarn
install to refresh the lockfile.

In `@examples/react/start-nitro/src/devtools/ServerEventsPanel.tsx`:
- Around line 20-28: formatTime currently uses toLocaleTimeString which can
differ between server and client and cause hydration mismatches; make the output
deterministic by specifying an explicit timeZone (e.g., add timeZone: 'UTC' to
the options) or move formatting to run only on the client (compute inside a
useEffect or when rendering client-only components) so timezone/locale
differences won't cause hydration errors — update the formatTime function to use
Intl.DateTimeFormat or toLocaleTimeString with a fixed timeZone option, or
ensure calls to formatTime occur only on the client side.

In `@packages/event-bus-client/src/plugin.ts`:
- Around line 581-595: The JSDoc `@internal` on private helper methods is leaking
into generated docs; replace the /** `@internal` */ JSDoc blocks on the private
helpers (specifically ___enableNetworkTransport and ___destroyNetworkTransport)
with plain single-line comments (e.g., // internal — only for testing) so they
are treated as implementation comments rather than documentation and won’t be
emitted into the site output.

In `@packages/event-bus/src/server/server.ts`:
- Around line 156-167: Extract the duplicated POST bridge routing into a single
helper (e.g. handleDevtoolsSendRequest) that performs the body collection, calls
parseWithBigInt, logs via this.debugLog('Received event from client', msg) and
then calls either this.emit(msg) or this.emitToServer(msg) based on msg.source
=== 'server-bridge'; replace the duplicated blocks currently handling req.url
=== '/__devtools/send' in both server modes (the block around the
parseWithBigInt/this.debugLog/this.emit/this.emitToServer sequence and the
similar block at the other location) with calls to this new helper so both modes
share the same logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 23c57afd-2ae3-40ad-89fd-677242f015bd

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc0afa and 3f82eeb.

📒 Files selected for processing (32)
  • docs/superpowers/plans/2026-03-12-network-transport-fallback.md
  • docs/superpowers/specs/2026-03-12-network-transport-fallback-design.md
  • examples/react/start-cloudflare/.gitignore
  • examples/react/start-cloudflare/package.json
  • examples/react/start-cloudflare/src/devtools/ServerEventsPanel.tsx
  • examples/react/start-cloudflare/src/devtools/index.ts
  • examples/react/start-cloudflare/src/devtools/server-event-client.ts
  • examples/react/start-cloudflare/src/router.tsx
  • examples/react/start-cloudflare/src/routes/__root.tsx
  • examples/react/start-cloudflare/src/routes/index.tsx
  • examples/react/start-cloudflare/tsconfig.json
  • examples/react/start-cloudflare/vite.config.ts
  • examples/react/start-cloudflare/wrangler.jsonc
  • examples/react/start-nitro/.gitignore
  • examples/react/start-nitro/package.json
  • examples/react/start-nitro/src/devtools/ServerEventsPanel.tsx
  • examples/react/start-nitro/src/devtools/index.ts
  • examples/react/start-nitro/src/devtools/server-event-client.ts
  • examples/react/start-nitro/src/router.tsx
  • examples/react/start-nitro/src/routes/__root.tsx
  • examples/react/start-nitro/src/routes/index.tsx
  • examples/react/start-nitro/tsconfig.json
  • examples/react/start-nitro/vite.config.ts
  • packages/event-bus-client/src/index.ts
  • packages/event-bus-client/src/plugin.ts
  • packages/event-bus-client/src/ring-buffer.ts
  • packages/event-bus-client/tests/integration.test.ts
  • packages/event-bus-client/tests/network-transport.test.ts
  • packages/event-bus-client/tests/ring-buffer.test.ts
  • packages/event-bus/src/client/client.ts
  • packages/event-bus/src/server/server.ts
  • packages/event-bus/tests/server.test.ts

"nitro": "latest",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"vite-tsconfig-paths": "^6.0.2"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Search for vite-tsconfig-paths usage:"
rg -n 'vite-tsconfig-paths|tsconfigPaths\s*\(' --glob '**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}'

Repository: TanStack/devtools

Length of output: 484


🏁 Script executed:

cat examples/react/start-nitro/package.json

Repository: TanStack/devtools

Length of output: 860


Move vite-tsconfig-paths to devDependencies.

This package is imported only in vite.config.ts (build-time configuration). It should not be in dependencies since it's not used at runtime.

Proposed change
   "dependencies": {
     "@tanstack/devtools-event-client": "workspace:*",
     "@tanstack/react-devtools": "workspace:*",
     "@tanstack/react-router": "^1.132.0",
     "@tanstack/react-start": "^1.132.0",
     "@tanstack/router-plugin": "^1.132.0",
     "nitro": "latest",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
-    "vite-tsconfig-paths": "^6.0.2"
   },
   "devDependencies": {
     "@tanstack/devtools-vite": "workspace:*",
     "@types/node": "^22.15.2",
     "@types/react": "^19.2.0",
     "@types/react-dom": "^19.2.0",
     "@vitejs/plugin-react": "^5.0.4",
+    "vite-tsconfig-paths": "^6.0.2",
     "typescript": "~5.9.2",
     "vite": "^7.1.7"
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"vite-tsconfig-paths": "^6.0.2"
"dependencies": {
"@tanstack/devtools-event-client": "workspace:*",
"@tanstack/react-devtools": "workspace:*",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-start": "^1.132.0",
"@tanstack/router-plugin": "^1.132.0",
"nitro": "latest",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@tanstack/devtools-vite": "workspace:*",
"@types/node": "^22.15.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"vite-tsconfig-paths": "^6.0.2",
"typescript": "~5.9.2",
"vite": "^7.1.7"
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-nitro/package.json` at line 19, Move the
"vite-tsconfig-paths" dependency from runtime dependencies into devDependencies:
remove the "vite-tsconfig-paths": "^6.0.2" entry under dependencies in
package.json and add the same entry under devDependencies (so build-time only
tools used by vite.config.ts are not bundled at runtime); ensure
package-lock/yarn lock is updated by reinstalling or running the package manager
after the change.

Comment on lines +50 to +54
let globalEventIdCounter = 0

function generateEventId(): string {
return `${++globalEventIdCounter}-${Date.now()}`
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make eventId unique across worker instances.

++counter + Date.now() is only unique inside one module instance. Two isolated workers can both generate the same first IDs in the same millisecond, and then falsely drop each other's events as echoes. Add a per-process random prefix or similar instance-scoped entropy.

🔧 One low-dependency fix
 let globalEventIdCounter = 0
+const eventIdPrefix =
+  `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`

 function generateEventId(): string {
-  return `${++globalEventIdCounter}-${Date.now()}`
+  return `${eventIdPrefix}-${++globalEventIdCounter}`
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let globalEventIdCounter = 0
function generateEventId(): string {
return `${++globalEventIdCounter}-${Date.now()}`
}
let globalEventIdCounter = 0
const eventIdPrefix =
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
function generateEventId(): string {
return `${eventIdPrefix}-${++globalEventIdCounter}`
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus-client/src/plugin.ts` around lines 50 - 54, The event ID
generation (generateEventId using globalEventIdCounter) is only unique per
module instance and can collide across workers; create a process-scoped
random/unique prefix (e.g., instancePrefix generated once at module init using
crypto.randomBytes or a high-entropy Math.random hex) and prepend it to the
existing `${++globalEventIdCounter}-${Date.now()}` scheme so generateEventId
returns `${instancePrefix}-${++globalEventIdCounter}-${Date.now()}`, ensuring
instance-scoped entropy and avoiding cross-worker collisions; ensure
instancePrefix is initialized once (top-level) and referenced in
generateEventId.

Comment on lines +304 to +308
ws.addEventListener('message', (e) => {
try {
const data = typeof e.data === 'string' ? e.data : e.data.toString()
const event = JSON.parse(data)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare serializer usage in the bridge and core event-bus transport code.
fd 'plugin\.ts$|client\.ts$|server\.ts$' packages/event-bus packages/event-bus-client -t f \
  | xargs rg -n 'parseWithBigInt|stringifyWithBigInt|JSON\.parse|JSON\.stringify'

Repository: TanStack/devtools

Length of output: 1336


🏁 Script executed:

cat -n packages/event-bus-client/src/plugin.ts | sed -n '300,315p'

Repository: TanStack/devtools

Length of output: 703


🏁 Script executed:

cat -n packages/event-bus-client/src/plugin.ts | sed -n '385,435p'

Repository: TanStack/devtools

Length of output: 2030


🏁 Script executed:

head -20 packages/event-bus-client/src/plugin.ts | cat -n

Repository: TanStack/devtools

Length of output: 983


🏁 Script executed:

ls -la packages/event-bus-client/src/ | head -20

Repository: TanStack/devtools

Length of output: 358


🏁 Script executed:

fd -t f 'json\.' packages/event-bus-client packages/event-bus

Repository: TanStack/devtools

Length of output: 139


🏁 Script executed:

cat packages/event-bus/src/utils/json.ts

Repository: TanStack/devtools

Length of output: 855


Use parseWithBigInt and stringifyWithBigInt for bridge serialization to match the rest of the event-bus.

The server and browser bus use parseWithBigInt / stringifyWithBigInt for transport serialization. The bridge currently uses plain JSON.parse and JSON.stringify, which silently loses BigInt values and breaks wire format compatibility. Update line 307 (message handler) and lines 408, 428 (send methods) to use the BigInt-safe serializers imported from the event-bus utilities.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus-client/src/plugin.ts` around lines 304 - 308, The bridge
currently uses JSON.parse/JSON.stringify which drops BigInt values; change the
ws message handler in ws.addEventListener('message') to call
parseWithBigInt(data) instead of JSON.parse(data), and update the bridge send
methods (the functions around the send calls at the locations corresponding to
lines 408 and 428) to use stringifyWithBigInt(payload) instead of
JSON.stringify(payload). Ensure parseWithBigInt and stringifyWithBigInt are
imported from the event-bus utilities and replace all plain
JSON.parse/JSON.stringify usages in those handlers/sendters to maintain
BigInt-safe wire format compatibility.

Comment on lines +475 to +483
if (this.#useNetworkTransport) {
const event = this.createEventPayload(eventSuffix, payload)
if (!this.#connected) {
this.#queuedEvents.push(event)
this.connectWebSocket()
return
}
this.sendViaNetwork(event)
return
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

HTTP-only mode never sends later emits.

After scheduleReconnect() sets #wsGaveUp = true, this branch still queues on !#connected and connectWebSocket() immediately returns, so every subsequent event stays in #queuedEvents forever. Short-circuit to sendViaNetwork() / sendViaHttp() once the client has already given up on WebSocket.

💡 Minimal fix
     if (this.#useNetworkTransport) {
       const event = this.createEventPayload(eventSuffix, payload)
+      if (this.#wsGaveUp) {
+        this.sendViaNetwork(event)
+        return
+      }
       if (!this.#connected) {
         this.#queuedEvents.push(event)
         this.connectWebSocket()
         return
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (this.#useNetworkTransport) {
const event = this.createEventPayload(eventSuffix, payload)
if (!this.#connected) {
this.#queuedEvents.push(event)
this.connectWebSocket()
return
}
this.sendViaNetwork(event)
return
if (this.#useNetworkTransport) {
const event = this.createEventPayload(eventSuffix, payload)
if (this.#wsGaveUp) {
this.sendViaNetwork(event)
return
}
if (!this.#connected) {
this.#queuedEvents.push(event)
this.connectWebSocket()
return
}
this.sendViaNetwork(event)
return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus-client/src/plugin.ts` around lines 475 - 483, The branch
inside the this.#useNetworkTransport path incorrectly queues events when the
client has given up on WebSocket; update the logic in the block that calls
createEventPayload so that if this.#wsGaveUp is true you bypass queuing and
connectWebSocket(), and immediately call sendViaNetwork (or sendViaHttp for
non-network transport) instead; specifically, in the code handling
this.#useNetworkTransport, check this.#wsGaveUp before pushing to
this.#queuedEvents and short-circuit to sendViaNetwork/sendViaHttp, leaving
connectWebSocket() only for cases where a reconnect attempt should actually be
made.

Comment on lines +7 to +11
constructor(capacity: number) {
this.#capacity = capacity
this.#buffer = new Array(capacity).fill('')
this.#set = new Set()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject invalid capacities in the constructor.

capacity <= 0 leaves add() in a broken state (#index becomes NaN) and negative values surface a generic Array() error. Since RingBuffer is exported, fail fast with a clear positive-integer check.

🛡️ Proposed guard
  constructor(capacity: number) {
+   if (!Number.isInteger(capacity) || capacity <= 0) {
+     throw new RangeError('RingBuffer capacity must be a positive integer')
+   }
    this.#capacity = capacity
    this.#buffer = new Array(capacity).fill('')
    this.#set = new Set()
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor(capacity: number) {
this.#capacity = capacity
this.#buffer = new Array(capacity).fill('')
this.#set = new Set()
}
constructor(capacity: number) {
if (!Number.isInteger(capacity) || capacity <= 0) {
throw new RangeError('RingBuffer capacity must be a positive integer')
}
this.#capacity = capacity
this.#buffer = new Array(capacity).fill('')
this.#set = new Set()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus-client/src/ring-buffer.ts` around lines 7 - 11, The
RingBuffer constructor must validate the incoming capacity and fail fast for
non-positive or non-integer values: in the constructor (class RingBuffer) add a
guard that checks capacity is a positive integer (capacity > 0 &&
Number.isInteger(capacity)) and throw a clear RangeError or TypeError if not, so
fields like `#capacity`, `#buffer` and `#index` remain valid and add() cannot enter a
broken state; update the constructor that currently sets this.#capacity,
this.#buffer and this.#set to perform this validation before initializing those
fields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: AI Devtools ignores client-sourced assistant messages and streaming chunks

1 participant