feat(server): stream events across locations

This commit is contained in:
Dax Raad 2026-06-22 08:12:38 -04:00
commit 595cc91ddc
11 changed files with 2172 additions and 310 deletions

View file

@ -26,6 +26,7 @@ function emitEvent(events: ReturnType<typeof createEventSource>, payload: Event)
}
test("refreshes resources into reactive getters", async () => {
const events = createEventSource()
const location = {
directory,
project: { id: "proj_test", directory },
@ -49,8 +50,7 @@ test("refreshes resources into reactive getters", async () => {
data: [{ id: "build", request: { headers: {}, body: {} }, mode: "primary", hidden: false, permissions: [] }],
})
return undefined
})
const events = createEventSource()
}, events)
let data!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
@ -119,7 +119,7 @@ test("refreshes integrations after integration updates", async () => {
},
],
})
})
}, events)
let data!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
@ -171,7 +171,7 @@ test("refreshes effective catalog data after catalog updates", async () => {
requests.provider++
return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
}
})
}, events)
const app = await testRender(() => (
<TestTuiContexts>
@ -205,7 +205,7 @@ test("refreshes references after updates", async () => {
location: { directory, project: { id: "proj_test", directory } },
data: requests === 1 ? [] : [{ name: "docs", path: "/docs", source: { type: "local", path: "/docs" } }],
})
})
}, events)
let data!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
@ -243,7 +243,7 @@ test("refreshes references after updates", async () => {
test("settles pending tools when a live failure arrives", async () => {
const events = createEventSource()
const calls = createFetch()
const calls = createFetch(undefined, events)
let sync!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
@ -372,7 +372,7 @@ test("settles pending tools when a live failure arrives", async () => {
test("renders admitted prompts only after promotion", async () => {
const events = createEventSource()
const calls = createFetch()
const calls = createFetch(undefined, events)
let sync!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
@ -436,7 +436,7 @@ test("renders admitted prompts only after promotion", async () => {
test("renders a promoted prompt when admission was missed", async () => {
const events = createEventSource()
const calls = createFetch()
const calls = createFetch(undefined, events)
let sync!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
@ -484,7 +484,7 @@ test("renders a promoted prompt when admission was missed", async () => {
test("projects live context updates with their message ID", async () => {
const events = createEventSource()
const calls = createFetch()
const calls = createFetch(undefined, events)
let sync!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {

View file

@ -17,6 +17,8 @@ export function eventSource(): EventSource {
export function createEventSource() {
let fn: ((event: GlobalEvent) => void) | undefined
let stream: ReadableStreamDefaultController<Uint8Array> | undefined
const pending: Uint8Array[] = []
return {
source: {
subscribe: async (handler: (event: GlobalEvent) => void) => {
@ -29,19 +31,44 @@ export function createEventSource() {
emit(event: GlobalEvent) {
if (!fn) throw new Error("event source not ready")
fn(event)
if (!("properties" in event.payload)) return
const chunk = new TextEncoder().encode(
`data: ${JSON.stringify({
...event.payload,
location: { directory: event.directory, workspaceID: event.workspace },
data: event.payload.properties,
})}\n\n`,
)
if (stream) return stream.enqueue(chunk)
pending.push(chunk)
},
response() {
return new Response(
new ReadableStream<Uint8Array>({
start(controller) {
stream = controller
for (const chunk of pending.splice(0)) controller.enqueue(chunk)
},
cancel() {
stream = undefined
},
}),
{ headers: { "content-type": "text/event-stream" } },
)
},
}
}
export type FetchHandler = (url: URL) => Response | Promise<Response> | undefined
export function createFetch(override?: FetchHandler) {
export function createFetch(override?: FetchHandler, events?: ReturnType<typeof createEventSource>) {
const session = [] as URL[]
const fetch = (async (input: RequestInfo | URL) => {
const url = new URL(input instanceof Request ? input.url : String(input))
if (url.pathname === "/session") session.push(url)
const overridden = await override?.(url)
if (overridden) return overridden
if (url.pathname === "/api/event" && events) return events.response()
if (
[