UNPKG

@durable-streams/aisdk-transport

Version:

Vercel AI SDK transport adapters for Durable Streams

231 lines (227 loc) 8.35 kB
"use strict"; //#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion const __durable_streams_client = __toESM(require("@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 (0, __durable_streams_client.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 __durable_streams_client.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 __durable_streams_client.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 __durable_streams_client.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 exports.createDurableChatTransport = createDurableChatTransport exports.toDurableStreamResponse = toDurableStreamResponse