-
-
Notifications
You must be signed in to change notification settings - Fork 140
Description
Describe the bug
aiEventClient.on() does not work as expected due to two issues in EventClient from @tanstack/devtools-event-client:
1. Double-prefixed event names
The AIDevtoolsEventMap type uses fully-prefixed keys (e.g. "tanstack-ai-devtools:text:message:created"), but both emit() and on() already prepend the pluginId at runtime:
// EventClient.on()
const eventName = `${this.#pluginId}:${eventSuffix}`;
// → "tanstack-ai-devtools:tanstack-ai-devtools:text:message:created" ❌The emit() calls in @tanstack/ai source correctly use the short suffix:
// src/activities/chat/index.ts
aiEventClient.emit('text:message:created', { ... }) // ✅ correctSo on() also needs the short suffix to match, but TypeScript demands the full key from AIDevtoolsEventMap:
// TypeScript expects this (from the type map), but it double-prefixes at runtime:
aiEventClient.on("tanstack-ai-devtools:text:message:created", cb) // ❌ won't fire
// This works at runtime, but TypeScript errors:
aiEventClient.on("text:message:created", cb) // ✅ works, but TS errorFix: The keys in AIDevtoolsEventMap should use the short suffix (without the tanstack-ai-devtools: prefix), since the EventClient already prepends the pluginId.
2. on() doesn't work on the server without { withEventTarget: true }
On server environments (Cloudflare Workers, Node, Bun) where there is no window and globalThis.__TANSTACK_EVENT_TARGET__ is not set, getGlobalTarget() falls through to:
const eventTarget = typeof EventTarget !== "undefined" ? new EventTarget() : void 0;
return eventTarget;Since getGlobalTarget() is not cached (called via this.#eventTarget() each time), every call creates a new EventTarget instance. This means emit() and on() dispatch/listen on different targets — events never reach the listener.
Workaround: Passing { withEventTarget: true } to on() forces use of this.#internalEventTarget, which is a single shared instance on the EventClient. Then emit() also dispatches to it.
aiEventClient.on(
// @ts-expect-error -- see issue #1 above
"text:message:created",
(e) => console.log(e.payload),
{ withEventTarget: true } // required on server
)Fix: getGlobalTarget() should cache the EventTarget it creates, so the same instance is used across all on()/emit() calls.
Your minimal, reproducible example
// In a TanStack Start server handler (e.g. API route on Cloudflare Workers)
import { aiEventClient, chat } from "@tanstack/ai"
// ❌ Does NOT fire — double prefix + separate EventTargets on server
aiEventClient.on("tanstack-ai-devtools:text:message:created", (e) => {
console.log(e.payload.content)
})
// ✅ Works with both fixes applied
aiEventClient.on(
// @ts-expect-error
"text:message:created",
(e) => console.log(e.payload.content),
{ withEventTarget: true }
)Steps to reproduce
- Set up a TanStack Start app with
@tanstack/aion a server environment (Cloudflare Workers, Node, or Bun) - Register a listener with
aiEventClient.on("tanstack-ai-devtools:text:message:created", cb) - Trigger a
chat()call - Observe: the callback never fires
Expected behavior
aiEventClient.on("text:message:created", cb) should work without @ts-expect-error and without needing { withEventTarget: true } on the server.
How often does this bug happen?
Every time
Package version
@tanstack/ai 0.6.1, @tanstack/devtools-event-client 0.4.0
TypeScript version
5.x
Additional context
Found while building a chat API route in a TanStack Start + Cloudflare Workers app. The { withEventTarget: true } option works as a workaround for both issues.