UNPKG

@durable-streams/aisdk-transport

Version:

Vercel AI SDK transport adapters for Durable Streams

141 lines (124 loc) 3.89 kB
import { DurableStream, DurableStreamError } from "@durable-streams/client" import type { HeadersRecord } from "@durable-streams/client" import type { ToDurableStreamResponseOptions } from "./types" const DEFAULT_CONTENT_TYPE = `application/json` function resolveUrl(url: string | URL): string { return url instanceof URL ? url.toString() : url } async function resolveHeaders( headers: HeadersRecord | undefined ): Promise<Record<string, string>> { if (!headers) return {} const entries = await Promise.all( Object.entries(headers).map(async ([key, value]) => { const resolved = typeof value === `function` ? await value() : value return [key, resolved] as const }) ) return Object.fromEntries(entries) } async function ensureStreamExists( stream: DurableStream, contentType: string, createIfMissing: boolean ): Promise<void> { if (!createIfMissing) return try { await stream.create({ contentType }) } catch (error) { if ( error instanceof DurableStreamError && error.status === 409 && (error.code === `CONFLICT_EXISTS` || error.code === `CONFLICT_SEQ`) ) { return } throw error } } async function writeSourceToStream( source: AsyncIterable<unknown>, stream: DurableStream, contentType: string ): Promise<string> { let finalOffset = `` let sourceError: unknown = undefined let appendError: unknown = undefined let lastAppend: Promise<void> = Promise.resolve() try { for await (const chunk of source) { if (appendError !== undefined) break lastAppend = stream .append(JSON.stringify(chunk), { contentType }) .catch((err) => { if (appendError === undefined) appendError = err }) } } catch (error) { sourceError = error } finally { // Drain queued appends before closing. The client queue is FIFO with // concurrency 1, so awaiting the last tracked promise drains all prior ones. await lastAppend try { const closeResult = await stream.close() finalOffset = closeResult.finalOffset } catch (error) { if ( !( error instanceof DurableStreamError && error.code === `STREAM_CLOSED` ) && sourceError === undefined && appendError === undefined ) { sourceError = error } } } if (appendError !== undefined) { throw appendError } if (sourceError !== undefined) { throw sourceError } return finalOffset } export async function toDurableStreamResponse( options: ToDurableStreamResponseOptions ): Promise<Response> { const mode = options.mode ?? `immediate` const contentType = options.stream.contentType ?? DEFAULT_CONTENT_TYPE const writeUrl = resolveUrl(options.stream.writeUrl) const readUrl = resolveUrl(options.stream.readUrl ?? options.stream.writeUrl) const headers = await resolveHeaders(options.stream.headers) const createIfMissing = options.stream.createIfMissing ?? true const stream = new DurableStream({ url: writeUrl, headers, contentType, }) await ensureStreamExists(stream, contentType, createIfMissing) const writer = writeSourceToStream(options.source, stream, contentType) if (mode === `await`) { const finalOffset = await writer return Response.json( { streamUrl: readUrl, finalOffset }, { status: 200, headers: { Location: readUrl } } ) } const backgroundTask = writer.catch((error) => { console.error(`Durable stream write failed`, error) }) options.waitUntil?.(backgroundTask) const responseHeaders = new Headers({ Location: readUrl, "Cache-Control": `no-store`, }) if (options.exposeLocationHeader !== false) { responseHeaders.set(`Access-Control-Expose-Headers`, `Location`) } return Response.json( { streamUrl: readUrl }, { status: 201, headers: responseHeaders } ) }