@durable-streams/aisdk-transport
Version:
Vercel AI SDK transport adapters for Durable Streams
231 lines (227 loc) • 8.35 kB
JavaScript
;
//#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