feat(server): stream events across locations
This commit is contained in:
parent
57ce1b9ca8
commit
595cc91ddc
11 changed files with 2172 additions and 310 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue