@durable-streams/aisdk-transport
Version:
Vercel AI SDK transport adapters for Durable Streams
141 lines (124 loc) • 3.89 kB
text/typescript
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 }
)
}