UNPKG

@durable-streams/aisdk-transport

Version:

Vercel AI SDK transport adapters for Durable Streams

206 lines (203 loc) 7.2 kB
import { DurableStream, DurableStreamError, stream } from "@durable-streams/client"; //#region src/client.ts function mergeHeaders(headers) { if (!headers) return {}; if (headers instanceof Headers) return Object.fromEntries(headers.entries()); if (Array.isArray(headers)) return Object.fromEntries(headers); return { ...headers }; } function parseBodyStreamUrl(body) { if (body && typeof body === `object` && `streamUrl` in body) { const streamUrl = body.streamUrl; if (typeof streamUrl === `string` && streamUrl.length > 0) return streamUrl; } return void 0; } async function parseJsonSafely(response) { const contentType = response.headers.get(`content-type`) ?? ``; if (!contentType.includes(`application/json`)) return void 0; try { return await response.json(); } catch { return void 0; } } function isAbsoluteUrl(url) { return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url); } function resolveStreamUrl(streamUrl, responseUrl, postUrl) { if (isAbsoluteUrl(streamUrl)) return streamUrl; const candidateBaseUrls = [responseUrl, postUrl]; if (typeof window !== `undefined`) candidateBaseUrls.push(window.location.href); for (const baseUrl of candidateBaseUrls) { if (!baseUrl) continue; try { return new URL(streamUrl, baseUrl).toString(); } catch {} } throw new Error(`Failed to resolve durable stream URL from relative path "${streamUrl}".`); } function toReadableStream(iterable) { const iterator = iterable[Symbol.asyncIterator](); return new ReadableStream({ async pull(controller) { const result = await iterator.next(); if (result.done) { controller.close(); return; } controller.enqueue(result.value); }, async cancel() { await iterator.return?.(); } }); } async function readUiMessageChunksFromDurableStream(streamUrl, abortSignal) { const streamResponse = await stream({ url: streamUrl, live: `sse`, json: true, signal: abortSignal }); return toReadableStream(streamResponse.jsonStream()); } function createDurableChatTransport({ api, reconnectApi, headers, fetchClient }) { return { async sendMessages({ trigger, chatId, messageId, messages, abortSignal, body, headers: requestHeaders }) { const response = await (fetchClient ?? fetch)(api, { method: `POST`, headers: { "Content-Type": `application/json`, ...mergeHeaders(headers), ...mergeHeaders(requestHeaders) }, body: JSON.stringify({ ...body ?? {}, id: chatId, messages, trigger, messageId }), signal: abortSignal }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText.length > 0 ? errorText : `HTTP error! status: ${response.status} ${response.statusText}`); } const headerUrl = response.headers.get(`Location`); if (headerUrl) return readUiMessageChunksFromDurableStream(resolveStreamUrl(headerUrl, response.url, api), abortSignal); const streamUrl = parseBodyStreamUrl(await parseJsonSafely(response)); if (!streamUrl) throw new Error(`Missing durable stream URL. Expected Location header or JSON body with streamUrl.`); return readUiMessageChunksFromDurableStream(resolveStreamUrl(streamUrl, response.url, api), abortSignal); }, async reconnectToStream({ chatId, body: _body, headers: requestHeaders }) { const endpoint = reconnectApi ?? `${api.replace(/\/$/, ``)}/${chatId}/stream`; const response = await (fetchClient ?? fetch)(endpoint, { method: `GET`, headers: { ...mergeHeaders(headers), ...mergeHeaders(requestHeaders) } }); if (response.status === 204) return null; if (!response.ok) { const errorText = await response.text(); throw new Error(errorText.length > 0 ? errorText : `HTTP error! status: ${response.status} ${response.statusText}`); } const streamUrl = response.headers.get(`Location`) ?? parseBodyStreamUrl(await parseJsonSafely(response)); if (!streamUrl) throw new Error(`Missing durable stream URL. Expected Location header or JSON body with streamUrl.`); return readUiMessageChunksFromDurableStream(resolveStreamUrl(streamUrl, response.url, endpoint)); } }; } //#endregion //#region src/server.ts const DEFAULT_CONTENT_TYPE = `application/json`; function resolveUrl(url) { return url instanceof URL ? url.toString() : url; } async function resolveHeaders(headers) { 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]; })); return Object.fromEntries(entries); } async function ensureStreamExists(stream$1, contentType, createIfMissing) { if (!createIfMissing) return; try { await stream$1.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, stream$1, contentType) { let finalOffset = ``; let sourceError = void 0; let appendError = void 0; let lastAppend = Promise.resolve(); try { for await (const chunk of source) { if (appendError !== void 0) break; lastAppend = stream$1.append(JSON.stringify(chunk), { contentType }).catch((err) => { if (appendError === void 0) appendError = err; }); } } catch (error) { sourceError = error; } finally { await lastAppend; try { const closeResult = await stream$1.close(); finalOffset = closeResult.finalOffset; } catch (error) { if (!(error instanceof DurableStreamError && error.code === `STREAM_CLOSED`) && sourceError === void 0 && appendError === void 0) sourceError = error; } } if (appendError !== void 0) throw appendError; if (sourceError !== void 0) throw sourceError; return finalOffset; } async function toDurableStreamResponse(options) { 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$1 = new DurableStream({ url: writeUrl, headers, contentType }); await ensureStreamExists(stream$1, contentType, createIfMissing); const writer = writeSourceToStream(options.source, stream$1, 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 }); } //#endregion export { createDurableChatTransport, toDurableStreamResponse };