agents
Version:
A home for your AI agents
107 lines (106 loc) • 3.8 kB
JavaScript
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