UNPKG

agents

Version:

A home for your AI agents

107 lines (106 loc) 3.8 kB
import { a as StreamSerializationError, i as StreamNotFoundError, n as Streams, r as StreamClosedError, t as DEFAULT_MAX_CHUNK_BYTES } from "../streams-CCPRV6dt.js"; //#region src/streams/sse.ts const encoder = new TextEncoder(); function resumeFrom(options) { if (options.from !== void 0) return Math.max(0, options.from); const request = options.request; if (request) { const header = request.headers.get("Last-Event-ID"); if (header !== null && header !== "") { const lastEventId = Number(header); if (Number.isInteger(lastEventId) && lastEventId >= 0) return lastEventId + 1; } const fromParam = new URL(request.url).searchParams.get("from"); if (fromParam !== null) { const from = Number(fromParam); if (Number.isInteger(from) && from >= 0) return from; } } return 0; } function frame(seq, chunk) { return `id: ${seq}\ndata: ${JSON.stringify(chunk)}\n\n`; } function controlFrame(event, data) { return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; } /** * Serve one durable stream as a Server-Sent Events response: replay from the * resume point, emit an `up-to-date` control event on reaching the tail, * tail live appends, and finish with `done` (completed) or `error` (errored, * carrying the recorded reason). Returns a 404 response when the stream does * not exist. * * ```ts * async onRequest(request: Request) { * return sseResponse(this.streams, "reply:123", { request }); * } * // client: new EventSource(url) — reconnect resume is automatic. * ``` * * @experimental The API surface may change before stabilizing. */ async function sseResponse(streams, streamId, options = {}) { if (await streams.status(streamId) === null) return new Response(`Stream "${streamId}" does not exist`, { status: 404 }); const from = resumeFrom(options); const heartbeatMs = options.heartbeatMs ?? 3e4; const abort = new AbortController(); const onUpstreamAbort = () => abort.abort(); if (options.signal?.aborted || options.request?.signal.aborted) abort.abort(); else { options.signal?.addEventListener("abort", onUpstreamAbort, { once: true }); options.request?.signal.addEventListener("abort", onUpstreamAbort, { once: true }); } let open = true; const body = new ReadableStream({ start: (controller) => { const write = (text) => { if (!open) return; try { controller.enqueue(encoder.encode(text)); } catch { open = false; } }; const heartbeat = heartbeatMs > 0 ? setInterval(() => write(": heartbeat\n\n"), heartbeatMs) : null; const finish = () => { if (heartbeat !== null) clearInterval(heartbeat); options.signal?.removeEventListener("abort", onUpstreamAbort); options.request?.signal.removeEventListener("abort", onUpstreamAbort); if (open) { open = false; try { controller.close(); } catch {} } }; (async () => { try { const batches = streams.readBatches(streamId, { from, signal: abort.signal, batchSize: options.batchSize, onUpToDate: () => write(controlFrame("up-to-date", {})) }); for await (const batch of batches) write(batch.map((chunk) => frame(chunk.seq, chunk.chunk)).join("")); const status = await streams.status(streamId); if (status?.state === "errored") write(controlFrame("error", { reason: status.error ?? null })); else write(controlFrame("done", {})); } catch {} finally { finish(); } })(); }, cancel: () => { open = false; abort.abort(); } }); return new Response(body, { headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-store" } }); } //#endregion export { DEFAULT_MAX_CHUNK_BYTES, StreamClosedError, StreamNotFoundError, StreamSerializationError, Streams, sseResponse }; //# sourceMappingURL=index.js.map