@providerprotocol/ai 0.0.42 → 0.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/{chunk-5CJCCJBJ.js → chunk-JPM7MVDO.js} +7 -4
- package/dist/chunk-JPM7MVDO.js.map +1 -0
- package/dist/{chunk-CWGTARDE.js → chunk-ODVES5EU.js} +3 -2
- package/dist/{chunk-CWGTARDE.js.map → chunk-ODVES5EU.js.map} +1 -1
- package/dist/{chunk-L6QWKFGE.js → chunk-PUKD2AV5.js} +11 -2
- package/dist/chunk-PUKD2AV5.js.map +1 -0
- package/dist/{chunk-KVUOTFYZ.js → chunk-SQ7ZUMKC.js} +3 -2
- package/dist/{chunk-KVUOTFYZ.js.map → chunk-SQ7ZUMKC.js.map} +1 -1
- package/dist/{chunk-ZMESKGUY.js → chunk-YVAS343Z.js} +2 -2
- package/dist/middleware/pubsub/index.d.ts +8 -1
- package/dist/middleware/pubsub/index.js.map +1 -1
- package/dist/middleware/pubsub/server/express/index.js +2 -2
- package/dist/middleware/pubsub/server/fastify/index.js +2 -2
- package/dist/middleware/pubsub/server/h3/index.d.ts +29 -9
- package/dist/middleware/pubsub/server/h3/index.js +2 -2
- package/dist/middleware/pubsub/server/index.d.ts +7 -1
- package/dist/middleware/pubsub/server/index.js +5 -5
- package/dist/middleware/pubsub/server/index.js.map +1 -1
- package/dist/middleware/pubsub/server/webapi/index.js +2 -2
- package/dist/openai/index.d.ts +1 -0
- package/dist/openai/index.js +2 -0
- package/dist/openai/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-5CJCCJBJ.js.map +0 -1
- package/dist/chunk-L6QWKFGE.js.map +0 -1
- /package/dist/{chunk-ZMESKGUY.js.map → chunk-YVAS343Z.js.map} +0 -0
package/README.md
CHANGED
|
@@ -822,6 +822,7 @@ app.post('/api/ai', async (request, reply) => {
|
|
|
822
822
|
});
|
|
823
823
|
|
|
824
824
|
// H3/Nuxt
|
|
825
|
+
import { sendStream, setHeader } from 'h3';
|
|
825
826
|
import { h3 } from '@providerprotocol/ai/middleware/pubsub/server';
|
|
826
827
|
|
|
827
828
|
export default defineEventHandler(async (event) => {
|
|
@@ -836,7 +837,13 @@ export default defineEventHandler(async (event) => {
|
|
|
836
837
|
model.stream(messages).then(turn => { /* save to DB */ });
|
|
837
838
|
}
|
|
838
839
|
|
|
839
|
-
|
|
840
|
+
// Required: H3's sendStream does NOT set these headers
|
|
841
|
+
setHeader(event, 'Content-Type', 'text/event-stream');
|
|
842
|
+
setHeader(event, 'Cache-Control', 'no-cache');
|
|
843
|
+
setHeader(event, 'Connection', 'keep-alive');
|
|
844
|
+
setHeader(event, 'X-Accel-Buffering', 'no');
|
|
845
|
+
|
|
846
|
+
return sendStream(event, h3.createSubscriberSSEStream(streamId, adapter));
|
|
840
847
|
});
|
|
841
848
|
```
|
|
842
849
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runSubscriberStream
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PUKD2AV5.js";
|
|
4
4
|
|
|
5
5
|
// src/middleware/pubsub/server/h3.ts
|
|
6
|
-
function createSubscriberSSEStream(streamId, adapter) {
|
|
6
|
+
function createSubscriberSSEStream(streamId, adapter, options = {}) {
|
|
7
7
|
const encoder = new TextEncoder();
|
|
8
8
|
const abortController = new AbortController();
|
|
9
9
|
let closed = false;
|
|
@@ -30,7 +30,10 @@ function createSubscriberSSEStream(streamId, adapter) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
|
-
{
|
|
33
|
+
{
|
|
34
|
+
signal: abortController.signal,
|
|
35
|
+
keepaliveMs: options.keepaliveMs
|
|
36
|
+
}
|
|
34
37
|
);
|
|
35
38
|
},
|
|
36
39
|
cancel() {
|
|
@@ -46,4 +49,4 @@ export {
|
|
|
46
49
|
createSubscriberSSEStream,
|
|
47
50
|
h3
|
|
48
51
|
};
|
|
49
|
-
//# sourceMappingURL=chunk-
|
|
52
|
+
//# sourceMappingURL=chunk-JPM7MVDO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware/pubsub/server/h3.ts"],"sourcesContent":["/**\n * @fileoverview H3/Nitro/Nuxt adapter for pub-sub stream resumption.\n *\n * Provides utilities for H3-based servers (Nuxt, Nitro, or standalone H3)\n * to handle stream reconnections.\n *\n * @module middleware/pubsub/server/h3\n */\n\nimport type { PubSubAdapter } from '../types.ts';\nimport { runSubscriberStream } from './shared.ts';\n\n/**\n * Options for subscriber SSE streams.\n */\nexport interface SubscriberSSEStreamOptions {\n /**\n * Interval in milliseconds between SSE keepalive comments.\n * Set to `0` to disable. Defaults to `5000` (5 seconds).\n */\n keepaliveMs?: number;\n}\n\n/**\n * Creates a ReadableStream that replays buffered events and subscribes to live events.\n *\n * Returns a `ReadableStream<Uint8Array>` for use with H3's `sendStream`.\n *\n * **Important:** H3's `sendStream` does **not** set response headers. You must\n * set SSE headers yourself before calling `sendStream`, otherwise reverse proxies\n * and CDNs (e.g. Cloudflare) won't recognise the response as an event stream\n * and may buffer or timeout the connection.\n *\n * Keepalive comments (`:keepalive\\n\\n`) are sent automatically at a default\n * interval of 5 seconds to prevent idle timeouts during long-running operations\n * like pipeline stages. This can be configured via the `options` parameter.\n *\n * @param streamId - The stream ID to subscribe to\n * @param adapter - The pub-sub adapter instance\n * @param options - Optional stream configuration\n * @returns A ReadableStream of SSE-formatted data\n *\n * @example\n * ```typescript\n * import { sendStream, setHeader } from 'h3';\n * import { llm } from '@providerprotocol/ai';\n * import { anthropic } from '@providerprotocol/ai/anthropic';\n * import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';\n * import { h3 } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * const adapter = memoryAdapter();\n *\n * export default defineEventHandler(async (event) => {\n * const { input, conversationId } = await readBody(event);\n *\n * if (!await adapter.exists(conversationId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId: conversationId })],\n * });\n * model.stream(input).then(turn => saveToDatabase(conversationId, turn));\n * }\n *\n * // Required: H3's sendStream does NOT set these headers\n * setHeader(event, 'Content-Type', 'text/event-stream');\n * setHeader(event, 'Cache-Control', 'no-cache');\n * setHeader(event, 'Connection', 'keep-alive');\n * setHeader(event, 'X-Accel-Buffering', 'no');\n *\n * return sendStream(event, h3.createSubscriberSSEStream(conversationId, adapter));\n * });\n * ```\n */\nexport function createSubscriberSSEStream(\n streamId: string,\n adapter: PubSubAdapter,\n options: SubscriberSSEStreamOptions = {},\n): ReadableStream<Uint8Array> {\n const encoder = new TextEncoder();\n const abortController = new AbortController();\n let closed = false;\n\n return new ReadableStream({\n async start(controller) {\n await runSubscriberStream(\n streamId,\n adapter,\n {\n write: (data: string) => {\n if (closed) {\n return;\n }\n controller.enqueue(encoder.encode(data));\n },\n end: () => {\n if (closed) {\n return;\n }\n closed = true;\n try {\n controller.close();\n } catch {\n // Ignore close errors after cancellation\n }\n },\n },\n {\n signal: abortController.signal,\n keepaliveMs: options.keepaliveMs,\n }\n );\n },\n cancel() {\n abortController.abort();\n },\n });\n}\n\n/**\n * H3 adapter namespace for pub-sub server utilities.\n */\nexport const h3 = {\n createSubscriberSSEStream,\n};\n"],"mappings":";;;;;AAyEO,SAAS,0BACd,UACA,SACA,UAAsC,CAAC,GACX;AAC5B,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,kBAAkB,IAAI,gBAAgB;AAC5C,MAAI,SAAS;AAEb,SAAO,IAAI,eAAe;AAAA,IACxB,MAAM,MAAM,YAAY;AACtB,YAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,UACE,OAAO,CAAC,SAAiB;AACvB,gBAAI,QAAQ;AACV;AAAA,YACF;AACA,uBAAW,QAAQ,QAAQ,OAAO,IAAI,CAAC;AAAA,UACzC;AAAA,UACA,KAAK,MAAM;AACT,gBAAI,QAAQ;AACV;AAAA,YACF;AACA,qBAAS;AACT,gBAAI;AACF,yBAAW,MAAM;AAAA,YACnB,QAAQ;AAAA,YAER;AAAA,UACF;AAAA,QACF;AAAA,QACA;AAAA,UACE,QAAQ,gBAAgB;AAAA,UACxB,aAAa,QAAQ;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,IACA,SAAS;AACP,sBAAgB,MAAM;AAAA,IACxB;AAAA,EACF,CAAC;AACH;AAKO,IAAM,KAAK;AAAA,EAChB;AACF;","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runSubscriberStream
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PUKD2AV5.js";
|
|
4
4
|
|
|
5
5
|
// src/middleware/pubsub/server/fastify.ts
|
|
6
6
|
async function streamSubscriber(streamId, adapter, reply) {
|
|
@@ -8,6 +8,7 @@ async function streamSubscriber(streamId, adapter, reply) {
|
|
|
8
8
|
res.setHeader("Content-Type", "text/event-stream");
|
|
9
9
|
res.setHeader("Cache-Control", "no-cache");
|
|
10
10
|
res.setHeader("Connection", "keep-alive");
|
|
11
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
11
12
|
const abortController = new AbortController();
|
|
12
13
|
res.on("close", () => abortController.abort());
|
|
13
14
|
await runSubscriberStream(
|
|
@@ -28,4 +29,4 @@ export {
|
|
|
28
29
|
streamSubscriber,
|
|
29
30
|
fastify
|
|
30
31
|
};
|
|
31
|
-
//# sourceMappingURL=chunk-
|
|
32
|
+
//# sourceMappingURL=chunk-ODVES5EU.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware/pubsub/server/fastify.ts"],"sourcesContent":["/**\n * @fileoverview Fastify adapter for pub-sub stream resumption.\n *\n * Provides utilities for Fastify servers to handle stream reconnections.\n *\n * @module middleware/pubsub/server/fastify\n */\n\nimport type { PubSubAdapter } from '../types.ts';\nimport { runSubscriberStream } from './shared.ts';\n\n/**\n * Fastify Reply interface (minimal type to avoid dependency).\n */\ninterface FastifyReply {\n raw: {\n setHeader(name: string, value: string): void;\n write(chunk: string): boolean;\n end(): void;\n on(event: 'close', listener: () => void): void;\n };\n}\n\n/**\n * Stream buffered and live events to a Fastify reply.\n *\n * Handles reconnection for Fastify routes:\n * 1. Replays buffered events from the adapter\n * 2. Subscribes to live events until completion signal\n * 3. Ends when stream completes or client disconnects\n *\n * @param streamId - The stream ID to subscribe to\n * @param adapter - The pub-sub adapter instance\n * @param reply - Fastify reply object\n *\n * @example\n * ```typescript\n * import { llm } from '@providerprotocol/ai';\n * import { anthropic } from '@providerprotocol/ai/anthropic';\n * import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';\n * import { fastify as pubsubFastify } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * const adapter = memoryAdapter();\n *\n * app.post('/api/chat', async (request, reply) => {\n * const { input, conversationId } = request.body as { input: string; conversationId: string };\n *\n * if (!await adapter.exists(conversationId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId: conversationId })],\n * });\n * model.stream(input).then(turn => saveToDatabase(conversationId, turn));\n * }\n *\n * return pubsubFastify.streamSubscriber(conversationId, adapter, reply);\n * });\n * ```\n */\nexport async function streamSubscriber(\n streamId: string,\n adapter: PubSubAdapter,\n reply: FastifyReply\n): Promise<void> {\n const res = reply.raw;\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n\n const abortController = new AbortController();\n res.on('close', () => abortController.abort());\n\n await runSubscriberStream(\n streamId,\n adapter,\n {\n write: (data: string) => res.write(data),\n end: () => res.end(),\n },\n { signal: abortController.signal }\n );\n}\n\n/**\n * Fastify adapter namespace for pub-sub server utilities.\n */\nexport const fastify = {\n streamSubscriber,\n};\n"],"mappings":";;;;;AA2DA,eAAsB,iBACpB,UACA,SACA,OACe;AACf,QAAM,MAAM,MAAM;AAClB,MAAI,UAAU,gBAAgB,mBAAmB;AACjD,MAAI,UAAU,iBAAiB,UAAU;AACzC,MAAI,UAAU,cAAc,YAAY;
|
|
1
|
+
{"version":3,"sources":["../src/middleware/pubsub/server/fastify.ts"],"sourcesContent":["/**\n * @fileoverview Fastify adapter for pub-sub stream resumption.\n *\n * Provides utilities for Fastify servers to handle stream reconnections.\n *\n * @module middleware/pubsub/server/fastify\n */\n\nimport type { PubSubAdapter } from '../types.ts';\nimport { runSubscriberStream } from './shared.ts';\n\n/**\n * Fastify Reply interface (minimal type to avoid dependency).\n */\ninterface FastifyReply {\n raw: {\n setHeader(name: string, value: string): void;\n write(chunk: string): boolean;\n end(): void;\n on(event: 'close', listener: () => void): void;\n };\n}\n\n/**\n * Stream buffered and live events to a Fastify reply.\n *\n * Handles reconnection for Fastify routes:\n * 1. Replays buffered events from the adapter\n * 2. Subscribes to live events until completion signal\n * 3. Ends when stream completes or client disconnects\n *\n * @param streamId - The stream ID to subscribe to\n * @param adapter - The pub-sub adapter instance\n * @param reply - Fastify reply object\n *\n * @example\n * ```typescript\n * import { llm } from '@providerprotocol/ai';\n * import { anthropic } from '@providerprotocol/ai/anthropic';\n * import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';\n * import { fastify as pubsubFastify } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * const adapter = memoryAdapter();\n *\n * app.post('/api/chat', async (request, reply) => {\n * const { input, conversationId } = request.body as { input: string; conversationId: string };\n *\n * if (!await adapter.exists(conversationId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId: conversationId })],\n * });\n * model.stream(input).then(turn => saveToDatabase(conversationId, turn));\n * }\n *\n * return pubsubFastify.streamSubscriber(conversationId, adapter, reply);\n * });\n * ```\n */\nexport async function streamSubscriber(\n streamId: string,\n adapter: PubSubAdapter,\n reply: FastifyReply\n): Promise<void> {\n const res = reply.raw;\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n\n const abortController = new AbortController();\n res.on('close', () => abortController.abort());\n\n await runSubscriberStream(\n streamId,\n adapter,\n {\n write: (data: string) => res.write(data),\n end: () => res.end(),\n },\n { signal: abortController.signal }\n );\n}\n\n/**\n * Fastify adapter namespace for pub-sub server utilities.\n */\nexport const fastify = {\n streamSubscriber,\n};\n"],"mappings":";;;;;AA2DA,eAAsB,iBACpB,UACA,SACA,OACe;AACf,QAAM,MAAM,MAAM;AAClB,MAAI,UAAU,gBAAgB,mBAAmB;AACjD,MAAI,UAAU,iBAAiB,UAAU;AACzC,MAAI,UAAU,cAAc,YAAY;AACxC,MAAI,UAAU,qBAAqB,IAAI;AAEvC,QAAM,kBAAkB,IAAI,gBAAgB;AAC5C,MAAI,GAAG,SAAS,MAAM,gBAAgB,MAAM,CAAC;AAE7C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,MACE,OAAO,CAAC,SAAiB,IAAI,MAAM,IAAI;AAAA,MACvC,KAAK,MAAM,IAAI,IAAI;AAAA,IACrB;AAAA,IACA,EAAE,QAAQ,gBAAgB,OAAO;AAAA,EACnC;AACF;AAKO,IAAM,UAAU;AAAA,EACrB;AACF;","names":[]}
|
|
@@ -10,11 +10,16 @@ function formatSSE(event) {
|
|
|
10
10
|
`;
|
|
11
11
|
}
|
|
12
12
|
async function runSubscriberStream(streamId, adapter, writer, options = {}) {
|
|
13
|
-
const { signal } = options;
|
|
13
|
+
const { signal, keepaliveMs = 5e3 } = options;
|
|
14
14
|
if (signal?.aborted) {
|
|
15
15
|
writer.end();
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
|
+
const keepaliveTimer = keepaliveMs > 0 ? setInterval(() => {
|
|
19
|
+
if (!signal?.aborted) {
|
|
20
|
+
writer.write(":keepalive\n\n");
|
|
21
|
+
}
|
|
22
|
+
}, keepaliveMs) : null;
|
|
18
23
|
try {
|
|
19
24
|
if (signal?.aborted) {
|
|
20
25
|
writer.end();
|
|
@@ -119,10 +124,14 @@ async function runSubscriberStream(streamId, adapter, writer, options = {}) {
|
|
|
119
124
|
`);
|
|
120
125
|
}
|
|
121
126
|
writer.end();
|
|
127
|
+
} finally {
|
|
128
|
+
if (keepaliveTimer !== null) {
|
|
129
|
+
clearInterval(keepaliveTimer);
|
|
130
|
+
}
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
export {
|
|
126
135
|
runSubscriberStream
|
|
127
136
|
};
|
|
128
|
-
//# sourceMappingURL=chunk-
|
|
137
|
+
//# sourceMappingURL=chunk-PUKD2AV5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware/pubsub/server/shared.ts"],"sourcesContent":["/**\n * @fileoverview Shared utilities for pub-sub server adapters.\n *\n * @module middleware/pubsub/server/shared\n * @internal\n */\n\nimport type { StreamEvent } from '../../../types/stream.ts';\nimport type { PubSubAdapter } from '../types.ts';\nimport { serializeStreamEvent } from '../../../stream/serialization.ts';\n\n/**\n * Writer interface for abstracting how data is written to responses.\n * @internal\n */\nexport interface StreamWriter {\n write(data: string): void;\n end(): void;\n}\n\n/**\n * Options for runSubscriberStream.\n * @internal\n */\nexport interface StreamOptions {\n signal?: AbortSignal;\n /**\n * Interval in milliseconds between SSE keepalive comments (`:keepalive\\n\\n`).\n *\n * Keeps connections alive through reverse proxies and CDNs that enforce\n * idle timeouts (e.g. Cloudflare HTTP/3 QUIC, nginx proxy_read_timeout).\n * Pipeline stages like image generation can create gaps of 10-30+ seconds\n * with no data on the wire, causing proxies to kill the connection.\n *\n * SSE comments (lines starting with `:`) are ignored by all spec-compliant\n * clients and the providerprotocol SSE parser.\n *\n * Set to `0` to disable. Defaults to `5000` (5 seconds).\n */\n keepaliveMs?: number;\n}\n\n/**\n * Formats a stream event as an SSE data line.\n */\nexport function formatSSE(event: StreamEvent): string {\n const serialized = serializeStreamEvent(event);\n return `data: ${JSON.stringify(serialized)}\\n\\n`;\n}\n\n/**\n * Core subscriber stream logic shared across all adapters.\n *\n * Handles:\n * 1. Subscribing to live events and completion signal\n * 2. Replaying buffered events (empty if stream just started)\n * 3. Processing live events until completion signal\n * 4. Final cleanup\n * 5. Client disconnect via AbortSignal\n *\n * @internal\n */\nexport async function runSubscriberStream(\n streamId: string,\n adapter: PubSubAdapter,\n writer: StreamWriter,\n options: StreamOptions = {}\n): Promise<void> {\n const { signal, keepaliveMs = 5_000 } = options;\n\n if (signal?.aborted) {\n writer.end();\n return;\n }\n\n // Send periodic SSE comments to keep connections alive through reverse\n // proxies and CDNs (e.g. Cloudflare HTTP/3 idle timeout, nginx).\n const keepaliveTimer = keepaliveMs > 0\n ? setInterval(() => {\n if (!signal?.aborted) {\n writer.write(':keepalive\\n\\n');\n }\n }, keepaliveMs)\n : null;\n\n try {\n if (signal?.aborted) {\n writer.end();\n return;\n }\n\n const queue: Array<{ event: StreamEvent; cursor: number | null }> = [];\n let resolveWait: (() => void) | null = null;\n let completed = false;\n let lastSentCursor = -1;\n let finalData: unknown = undefined;\n\n const onEvent = (event: StreamEvent, cursor?: number): void => {\n queue.push({ event, cursor: cursor ?? null });\n resolveWait?.();\n };\n\n const onComplete = (): void => {\n completed = true;\n resolveWait?.();\n };\n\n const onFinalData = (data: unknown): void => {\n finalData = data;\n };\n\n const unsubscribe = adapter.subscribe(streamId, onEvent, onComplete, onFinalData);\n\n const onAbort = (): void => {\n completed = true;\n resolveWait?.();\n };\n signal?.addEventListener('abort', onAbort);\n\n const drainQueue = (): void => {\n while (queue.length > 0 && !signal?.aborted) {\n const item = queue.shift();\n if (!item) break;\n const { event, cursor } = item;\n if (cursor !== null && cursor <= lastSentCursor) continue;\n writer.write(formatSSE(event));\n if (cursor !== null && cursor > lastSentCursor) {\n lastSentCursor = cursor;\n }\n }\n };\n\n const dropReplayDuplicates = (): void => {\n if (queue.length === 0) return;\n const filtered: Array<{ event: StreamEvent; cursor: number | null }> = [];\n for (const item of queue) {\n if (item.cursor !== null && item.cursor <= lastSentCursor) continue;\n filtered.push(item);\n }\n queue.length = 0;\n queue.push(...filtered);\n };\n\n const waitForSignal = (): Promise<void> => new Promise((resolve) => {\n let settled = false;\n\n const settle = (): void => {\n if (settled) return;\n settled = true;\n resolveWait = null;\n resolve();\n };\n\n resolveWait = settle;\n\n if (completed || signal?.aborted || queue.length > 0) {\n settle();\n }\n });\n\n try {\n const events = await adapter.getEvents(streamId);\n const cursorBase = adapter.getCursorBase(streamId);\n\n for (const event of events) {\n if (signal?.aborted) break;\n writer.write(formatSSE(event));\n }\n\n // Use cursor base to set lastSentCursor correctly after clear operations\n // Events have cursors of cursorBase + index, so after replay the last cursor is cursorBase + length - 1\n lastSentCursor = cursorBase + events.length - 1;\n dropReplayDuplicates();\n\n if (signal?.aborted) {\n writer.end();\n return;\n }\n\n // Wait for events or completion signal\n while (!completed && !signal?.aborted) {\n drainQueue();\n if (completed || signal?.aborted) break;\n await waitForSignal();\n }\n\n if (!signal?.aborted) {\n drainQueue();\n }\n } finally {\n signal?.removeEventListener('abort', onAbort);\n unsubscribe();\n }\n\n if (!signal?.aborted) {\n // Emit final data (Turn) if available before [DONE]\n if (finalData !== undefined) {\n writer.write(`data: ${JSON.stringify(finalData)}\\n\\n`);\n }\n writer.write('data: [DONE]\\n\\n');\n }\n writer.end();\n } catch (error) {\n if (!signal?.aborted) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n writer.write(`data: ${JSON.stringify({ error: errorMsg })}\\n\\n`);\n }\n writer.end();\n } finally {\n if (keepaliveTimer !== null) {\n clearInterval(keepaliveTimer);\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runSubscriberStream
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PUKD2AV5.js";
|
|
4
4
|
|
|
5
5
|
// src/middleware/pubsub/server/express.ts
|
|
6
6
|
async function streamSubscriber(streamId, adapter, res) {
|
|
7
7
|
res.setHeader("Content-Type", "text/event-stream");
|
|
8
8
|
res.setHeader("Cache-Control", "no-cache");
|
|
9
9
|
res.setHeader("Connection", "keep-alive");
|
|
10
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
10
11
|
const abortController = new AbortController();
|
|
11
12
|
res.on("close", () => abortController.abort());
|
|
12
13
|
await runSubscriberStream(
|
|
@@ -27,4 +28,4 @@ export {
|
|
|
27
28
|
streamSubscriber,
|
|
28
29
|
express
|
|
29
30
|
};
|
|
30
|
-
//# sourceMappingURL=chunk-
|
|
31
|
+
//# sourceMappingURL=chunk-SQ7ZUMKC.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware/pubsub/server/express.ts"],"sourcesContent":["/**\n * @fileoverview Express/Connect adapter for pub-sub stream resumption.\n *\n * Provides utilities for Express.js or Connect-based servers\n * to handle stream reconnections.\n *\n * @module middleware/pubsub/server/express\n */\n\nimport type { PubSubAdapter } from '../types.ts';\nimport { runSubscriberStream } from './shared.ts';\n\n/**\n * Express Response interface (minimal type to avoid dependency).\n */\ninterface ExpressResponse {\n setHeader(name: string, value: string): void;\n write(chunk: string): boolean;\n end(): void;\n on(event: 'close', listener: () => void): void;\n}\n\n/**\n * Stream buffered and live events to an Express response.\n *\n * Handles reconnection for Express routes:\n * 1. Replays buffered events from the adapter\n * 2. Subscribes to live events until completion signal\n * 3. Ends when stream completes or client disconnects\n *\n * @param streamId - The stream ID to subscribe to\n * @param adapter - The pub-sub adapter instance\n * @param res - Express response object\n *\n * @example\n * ```typescript\n * import { llm } from '@providerprotocol/ai';\n * import { anthropic } from '@providerprotocol/ai/anthropic';\n * import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';\n * import { express } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * const adapter = memoryAdapter();\n *\n * app.post('/api/chat', async (req, res) => {\n * const { input, conversationId } = req.body;\n *\n * if (!await adapter.exists(conversationId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId: conversationId })],\n * });\n * model.stream(input).then(turn => saveToDatabase(conversationId, turn));\n * }\n *\n * return express.streamSubscriber(conversationId, adapter, res);\n * });\n * ```\n */\nexport async function streamSubscriber(\n streamId: string,\n adapter: PubSubAdapter,\n res: ExpressResponse\n): Promise<void> {\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n\n const abortController = new AbortController();\n res.on('close', () => abortController.abort());\n\n await runSubscriberStream(\n streamId,\n adapter,\n {\n write: (data: string) => res.write(data),\n end: () => res.end(),\n },\n { signal: abortController.signal }\n );\n}\n\n/**\n * Express adapter namespace for pub-sub server utilities.\n */\nexport const express = {\n streamSubscriber,\n};\n"],"mappings":";;;;;AA0DA,eAAsB,iBACpB,UACA,SACA,KACe;AACf,MAAI,UAAU,gBAAgB,mBAAmB;AACjD,MAAI,UAAU,iBAAiB,UAAU;AACzC,MAAI,UAAU,cAAc,YAAY;
|
|
1
|
+
{"version":3,"sources":["../src/middleware/pubsub/server/express.ts"],"sourcesContent":["/**\n * @fileoverview Express/Connect adapter for pub-sub stream resumption.\n *\n * Provides utilities for Express.js or Connect-based servers\n * to handle stream reconnections.\n *\n * @module middleware/pubsub/server/express\n */\n\nimport type { PubSubAdapter } from '../types.ts';\nimport { runSubscriberStream } from './shared.ts';\n\n/**\n * Express Response interface (minimal type to avoid dependency).\n */\ninterface ExpressResponse {\n setHeader(name: string, value: string): void;\n write(chunk: string): boolean;\n end(): void;\n on(event: 'close', listener: () => void): void;\n}\n\n/**\n * Stream buffered and live events to an Express response.\n *\n * Handles reconnection for Express routes:\n * 1. Replays buffered events from the adapter\n * 2. Subscribes to live events until completion signal\n * 3. Ends when stream completes or client disconnects\n *\n * @param streamId - The stream ID to subscribe to\n * @param adapter - The pub-sub adapter instance\n * @param res - Express response object\n *\n * @example\n * ```typescript\n * import { llm } from '@providerprotocol/ai';\n * import { anthropic } from '@providerprotocol/ai/anthropic';\n * import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';\n * import { express } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * const adapter = memoryAdapter();\n *\n * app.post('/api/chat', async (req, res) => {\n * const { input, conversationId } = req.body;\n *\n * if (!await adapter.exists(conversationId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId: conversationId })],\n * });\n * model.stream(input).then(turn => saveToDatabase(conversationId, turn));\n * }\n *\n * return express.streamSubscriber(conversationId, adapter, res);\n * });\n * ```\n */\nexport async function streamSubscriber(\n streamId: string,\n adapter: PubSubAdapter,\n res: ExpressResponse\n): Promise<void> {\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n\n const abortController = new AbortController();\n res.on('close', () => abortController.abort());\n\n await runSubscriberStream(\n streamId,\n adapter,\n {\n write: (data: string) => res.write(data),\n end: () => res.end(),\n },\n { signal: abortController.signal }\n );\n}\n\n/**\n * Express adapter namespace for pub-sub server utilities.\n */\nexport const express = {\n streamSubscriber,\n};\n"],"mappings":";;;;;AA0DA,eAAsB,iBACpB,UACA,SACA,KACe;AACf,MAAI,UAAU,gBAAgB,mBAAmB;AACjD,MAAI,UAAU,iBAAiB,UAAU;AACzC,MAAI,UAAU,cAAc,YAAY;AACxC,MAAI,UAAU,qBAAqB,IAAI;AAEvC,QAAM,kBAAkB,IAAI,gBAAgB;AAC5C,MAAI,GAAG,SAAS,MAAM,gBAAgB,MAAM,CAAC;AAE7C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,MACE,OAAO,CAAC,SAAiB,IAAI,MAAM,IAAI;AAAA,MACvC,KAAK,MAAM,IAAI,IAAI;AAAA,IACrB;AAAA,IACA,EAAE,QAAQ,gBAAgB,OAAO;AAAA,EACnC;AACF;AAKO,IAAM,UAAU;AAAA,EACrB;AACF;","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runSubscriberStream
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PUKD2AV5.js";
|
|
4
4
|
|
|
5
5
|
// src/middleware/pubsub/server/webapi.ts
|
|
6
6
|
function createSubscriberStream(streamId, adapter) {
|
|
@@ -46,4 +46,4 @@ export {
|
|
|
46
46
|
createSubscriberStream,
|
|
47
47
|
webapi
|
|
48
48
|
};
|
|
49
|
-
//# sourceMappingURL=chunk-
|
|
49
|
+
//# sourceMappingURL=chunk-YVAS343Z.js.map
|
|
@@ -75,6 +75,7 @@ declare function getAdapter(state: Map<string, unknown>): PubSubAdapter | undefi
|
|
|
75
75
|
* import { llm } from '@providerprotocol/ai';
|
|
76
76
|
* import { anthropic } from '@providerprotocol/ai/anthropic';
|
|
77
77
|
* import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';
|
|
78
|
+
* import { sendStream, setHeader } from 'h3';
|
|
78
79
|
* import { h3 } from '@providerprotocol/ai/middleware/pubsub/server';
|
|
79
80
|
*
|
|
80
81
|
* const adapter = memoryAdapter();
|
|
@@ -91,7 +92,13 @@ declare function getAdapter(state: Map<string, unknown>): PubSubAdapter | undefi
|
|
|
91
92
|
* model.stream(input).then(turn => saveToDatabase(turn));
|
|
92
93
|
* }
|
|
93
94
|
*
|
|
94
|
-
*
|
|
95
|
+
* // Required: H3's sendStream does NOT set these headers
|
|
96
|
+
* setHeader(event, 'Content-Type', 'text/event-stream');
|
|
97
|
+
* setHeader(event, 'Cache-Control', 'no-cache');
|
|
98
|
+
* setHeader(event, 'Connection', 'keep-alive');
|
|
99
|
+
* setHeader(event, 'X-Accel-Buffering', 'no');
|
|
100
|
+
*
|
|
101
|
+
* return sendStream(event, h3.createSubscriberSSEStream(conversationId, adapter));
|
|
95
102
|
* });
|
|
96
103
|
* ```
|
|
97
104
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/middleware/pubsub/memory-adapter.ts","../../../src/middleware/pubsub/index.ts"],"sourcesContent":["/**\n * @fileoverview In-memory storage adapter for pub-sub middleware.\n *\n * Provides a simple Map-based implementation for temporary stream\n * storage during active generation.\n *\n * @module middleware/pubsub/memory-adapter\n */\n\nimport type { StreamEvent } from '../../types/stream.ts';\nimport type {\n PubSubAdapter,\n StoredStream,\n SubscriptionCallback,\n CompletionCallback,\n FinalDataCallback,\n Unsubscribe,\n MemoryAdapterOptions,\n} from './types.ts';\n\ninterface MutableStoredStream {\n streamId: string;\n createdAt: number;\n events: StreamEvent[];\n}\n\ninterface Subscriber {\n onEvent: SubscriptionCallback;\n onComplete: CompletionCallback;\n onFinalData?: FinalDataCallback;\n}\n\ninterface StreamEntry {\n stream: MutableStoredStream;\n subscribers: Set<Subscriber>;\n finalData?: unknown;\n /** Cursor offset incremented on clear to ensure new events have higher cursors */\n cursorBase: number;\n}\n\n/**\n * Creates an in-memory storage adapter for pub-sub middleware.\n *\n * Stores streams in a Map. Throws when maxStreams is exceeded.\n * Streams are created lazily on first append or subscribe.\n *\n * @param options - Adapter configuration\n * @returns A PubSubAdapter instance\n *\n * @example\n * ```typescript\n * import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';\n *\n * const adapter = memoryAdapter({ maxStreams: 500 });\n * ```\n */\nexport function memoryAdapter(options: MemoryAdapterOptions = {}): PubSubAdapter {\n const { maxStreams = 1000 } = options;\n\n const streams = new Map<string, StreamEntry>();\n const eventCursors = new WeakMap<StreamEvent, number>();\n\n const scheduleCallback = (callback: () => void): void => {\n queueMicrotask(() => {\n try {\n callback();\n } catch {\n // Subscriber errors should not affect other subscribers\n }\n });\n };\n\n const getOrCreate = (streamId: string): StreamEntry => {\n let entry = streams.get(streamId);\n if (!entry) {\n if (streams.size >= maxStreams) {\n throw new Error(`Maximum concurrent streams (${maxStreams}) exceeded`);\n }\n entry = {\n stream: {\n streamId,\n createdAt: Date.now(),\n events: [],\n },\n subscribers: new Set(),\n cursorBase: 0,\n };\n streams.set(streamId, entry);\n }\n return entry;\n };\n\n return {\n async exists(streamId): Promise<boolean> {\n return streams.has(streamId);\n },\n\n async append(streamId, event): Promise<void> {\n const entry = getOrCreate(streamId);\n entry.stream.events.push(event);\n // Use cursorBase to ensure cursors increase monotonically across clears\n eventCursors.set(event, entry.cursorBase + entry.stream.events.length - 1);\n },\n\n async getEvents(streamId): Promise<StreamEvent[]> {\n const entry = streams.get(streamId);\n return entry ? [...entry.stream.events] : [];\n },\n\n subscribe(streamId, onEvent, onComplete, onFinalData): Unsubscribe {\n const entry = getOrCreate(streamId);\n const subscriber: Subscriber = { onEvent, onComplete, onFinalData };\n entry.subscribers.add(subscriber);\n\n return () => {\n entry.subscribers.delete(subscriber);\n };\n },\n\n publish(streamId, event): void {\n const entry = streams.get(streamId);\n if (!entry) {\n return;\n }\n\n // Cursor is stored in eventCursors with cursorBase already applied\n const cursor = eventCursors.get(event) ?? (entry.cursorBase + entry.stream.events.length - 1);\n for (const subscriber of entry.subscribers) {\n scheduleCallback(() => {\n subscriber.onEvent(event, cursor);\n });\n }\n },\n\n setFinalData(streamId, data): void {\n const entry = streams.get(streamId);\n if (entry) {\n entry.finalData = data;\n }\n },\n\n async remove(streamId): Promise<void> {\n const entry = streams.get(streamId);\n if (entry) {\n for (const subscriber of entry.subscribers) {\n if (entry.finalData !== undefined && subscriber.onFinalData) {\n scheduleCallback(() => {\n subscriber.onFinalData!(entry.finalData);\n });\n }\n scheduleCallback(subscriber.onComplete);\n }\n streams.delete(streamId);\n }\n },\n\n async clear(streamId): Promise<void> {\n const entry = streams.get(streamId);\n if (entry) {\n // Increment cursor base so new events have higher cursors than cleared events\n // This ensures subscribers don't skip events after a retry\n entry.cursorBase += entry.stream.events.length;\n entry.stream.events = [];\n entry.finalData = undefined;\n }\n },\n\n getCursorBase(streamId): number {\n const entry = streams.get(streamId);\n return entry?.cursorBase ?? 0;\n },\n };\n}\n","/**\n * @fileoverview Pub-sub middleware for stream resumption.\n *\n * Enables reconnecting clients to catch up on missed events during\n * active generation. The middleware buffers events and publishes them\n * to subscribers. Server routes handle reconnection logic using the\n * exported `createSubscriberStream` utility.\n *\n * @module middleware/pubsub\n */\n\nimport type {\n Middleware,\n MiddlewareContext,\n StreamContext,\n} from '../../types/middleware.ts';\nimport type { StreamEvent } from '../../types/stream.ts';\nimport type { Turn } from '../../types/turn.ts';\nimport type { PubSubAdapter, PubSubOptions } from './types.ts';\nimport { memoryAdapter } from './memory-adapter.ts';\nimport { serializeTurn } from '../../providers/proxy/serialization.ts';\n\nexport type {\n PubSubAdapter,\n PubSubOptions,\n StoredStream,\n SubscriptionCallback,\n CompletionCallback,\n FinalDataCallback,\n Unsubscribe,\n MemoryAdapterOptions,\n} from './types.ts';\nexport { memoryAdapter } from './memory-adapter.ts';\n\nconst STATE_KEY_STREAM_ID = 'pubsub:streamId';\nconst STATE_KEY_ADAPTER = 'pubsub:adapter';\nconst STATE_KEY_STREAM_ENDED = 'pubsub:streamEnded';\n\ninterface AppendChainState {\n chain: Promise<void>;\n}\n\n/**\n * Gets the stream ID from middleware state.\n *\n * @param state - Middleware state map\n * @returns Stream ID or undefined if not set\n */\nexport function getStreamId(state: Map<string, unknown>): string | undefined {\n return state.get(STATE_KEY_STREAM_ID) as string | undefined;\n}\n\n/**\n * Gets the adapter from middleware state.\n *\n * @param state - Middleware state map\n * @returns Adapter or undefined if not set\n */\nexport function getAdapter(state: Map<string, unknown>): PubSubAdapter | undefined {\n return state.get(STATE_KEY_ADAPTER) as PubSubAdapter | undefined;\n}\n\n/**\n * Creates pub-sub middleware for stream buffering and publishing.\n *\n * The middleware:\n * - Creates stream entries for new requests\n * - Buffers all stream events\n * - Publishes events to subscribers\n * - On stream end: notifies subscribers, then removes from adapter\n *\n * Server routes handle reconnection logic using `streamSubscriber`.\n *\n * @param options - Middleware configuration\n * @returns Middleware instance\n *\n * @example\n * ```typescript\n * import { llm } from '@providerprotocol/ai';\n * import { anthropic } from '@providerprotocol/ai/anthropic';\n * import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';\n * import { h3 } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * const adapter = memoryAdapter();\n *\n * export default defineEventHandler(async (event) => {\n * const { input, conversationId } = await readBody(event);\n *\n * // Guard: prevent duplicate generations on reconnect\n * if (!await adapter.exists(conversationId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId: conversationId })],\n * });\n * model.stream(input).then(turn => saveToDatabase(turn));\n * }\n *\n * return h3.streamSubscriber(conversationId, adapter, event);\n * });\n * ```\n */\nexport function pubsubMiddleware(options: PubSubOptions = {}): Middleware {\n const {\n adapter = memoryAdapter(),\n streamId,\n } = options;\n\n const appendChains = new Map<string, AppendChainState>();\n\n const enqueueAppend = (id: string, event: StreamEvent): void => {\n const state = appendChains.get(id) ?? { chain: Promise.resolve() };\n\n const task = state.chain\n .catch(() => {})\n .then(async () => {\n await adapter.append(id, event);\n adapter.publish(id, event);\n });\n\n state.chain = task.catch(() => {});\n appendChains.set(id, state);\n };\n\n const waitForAppends = async (id: string): Promise<void> => {\n const state = appendChains.get(id);\n if (!state) {\n return;\n }\n\n await state.chain.catch(() => {});\n };\n\n const clearAppendState = (id: string): void => {\n appendChains.delete(id);\n };\n\n /**\n * Finalizes a stream by marking completion and removing from adapter.\n *\n * Called on any terminal state (complete, error, abort). After finalization,\n * the stream is removed from the adapter. Apps should use `.then()` to persist\n * completed conversations and serve them from their own storage on reconnect.\n */\n const finalizeStreamByState = async (state: Map<string, unknown>): Promise<void> => {\n const id = state.get(STATE_KEY_STREAM_ID) as string | undefined;\n if (!id) {\n return;\n }\n\n await waitForAppends(id);\n clearAppendState(id);\n\n // Remove from adapter (notifies subscribers) - apps persist via .then()\n await adapter.remove(id).catch(() => {});\n };\n\n return {\n name: 'pubsub',\n\n onStart(ctx: MiddlewareContext): void {\n ctx.state.set(STATE_KEY_ADAPTER, adapter);\n\n if (streamId) {\n ctx.state.set(STATE_KEY_STREAM_ID, streamId);\n // Ensure stream exists immediately so exists() returns true\n // before first token arrives (prevents duplicate generations)\n adapter.subscribe(streamId, () => {}, () => {})();\n }\n },\n\n onStreamEvent(event: StreamEvent, ctx: StreamContext): StreamEvent {\n const id = ctx.state.get(STATE_KEY_STREAM_ID) as string | undefined;\n if (!id) {\n return event;\n }\n\n enqueueAppend(id, event);\n\n return event;\n },\n\n async onStreamEnd(ctx: StreamContext): Promise<void> {\n const id = ctx.state.get(STATE_KEY_STREAM_ID) as string | undefined;\n if (!id) {\n return;\n }\n // Wait for all stream-phase appends to complete\n await waitForAppends(id);\n // Clear append state to prevent memory leaks if onTurn is skipped or fails.\n // Other middleware may emit during onTurn - those get new append chains.\n clearAppendState(id);\n ctx.state.set(STATE_KEY_STREAM_ENDED, true);\n },\n\n async onTurn(turn: Turn, ctx: MiddlewareContext): Promise<void> {\n const id = ctx.state.get(STATE_KEY_STREAM_ID) as string | undefined;\n const streamEnded = ctx.state.get(STATE_KEY_STREAM_ENDED) as boolean | undefined;\n\n if (!id) {\n return;\n }\n\n // Only emit Turn if we were streaming (onStreamEnd was called)\n if (streamEnded) {\n // Wait for any late appends from other middleware that emitted during onTurn\n // (e.g., pipeline middleware emits events before pubsub's onTurn runs).\n // These create new append chains since onStreamEnd cleared the stream-phase chains.\n await waitForAppends(id);\n clearAppendState(id);\n // Set the final Turn data so subscribers receive it before completion\n adapter.setFinalData(id, serializeTurn(turn));\n // Now remove the stream (notifies subscribers with final data + completion)\n await adapter.remove(id).catch(() => {});\n } else {\n // streamId was set but .generate() was used instead of .stream()\n // Clean up the orphan stream entry and warn about misuse\n const exists = await adapter.exists(id);\n if (exists) {\n console.warn(\n `[pubsub] streamId \"${id}\" was configured but .generate() was used instead of .stream(). ` +\n `Pubsub middleware only works with streaming. Cleaning up orphan stream.`\n );\n await adapter.remove(id).catch(() => {});\n }\n }\n },\n\n async onError(_error: Error, ctx: MiddlewareContext): Promise<void> {\n await finalizeStreamByState(ctx.state);\n },\n\n async onAbort(_error: Error, ctx: MiddlewareContext): Promise<void> {\n await finalizeStreamByState(ctx.state);\n },\n\n async onRetry(_attempt: number, _error: Error, ctx: MiddlewareContext): Promise<void> {\n const id = ctx.state.get(STATE_KEY_STREAM_ID) as string | undefined;\n if (!id) {\n return;\n }\n\n // Wait for in-flight appends to complete before clearing to prevent\n // stale events from repopulating the buffer after clear (especially\n // with async adapters like Redis)\n await waitForAppends(id);\n\n // Clear pending append chains\n clearAppendState(id);\n\n // Clear buffered events from adapter so subscribers don't receive duplicates\n await adapter.clear(id);\n\n // Reset stream ended flag for new attempt\n ctx.state.delete(STATE_KEY_STREAM_ENDED);\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/middleware/pubsub/memory-adapter.ts","../../../src/middleware/pubsub/index.ts"],"sourcesContent":["/**\n * @fileoverview In-memory storage adapter for pub-sub middleware.\n *\n * Provides a simple Map-based implementation for temporary stream\n * storage during active generation.\n *\n * @module middleware/pubsub/memory-adapter\n */\n\nimport type { StreamEvent } from '../../types/stream.ts';\nimport type {\n PubSubAdapter,\n StoredStream,\n SubscriptionCallback,\n CompletionCallback,\n FinalDataCallback,\n Unsubscribe,\n MemoryAdapterOptions,\n} from './types.ts';\n\ninterface MutableStoredStream {\n streamId: string;\n createdAt: number;\n events: StreamEvent[];\n}\n\ninterface Subscriber {\n onEvent: SubscriptionCallback;\n onComplete: CompletionCallback;\n onFinalData?: FinalDataCallback;\n}\n\ninterface StreamEntry {\n stream: MutableStoredStream;\n subscribers: Set<Subscriber>;\n finalData?: unknown;\n /** Cursor offset incremented on clear to ensure new events have higher cursors */\n cursorBase: number;\n}\n\n/**\n * Creates an in-memory storage adapter for pub-sub middleware.\n *\n * Stores streams in a Map. Throws when maxStreams is exceeded.\n * Streams are created lazily on first append or subscribe.\n *\n * @param options - Adapter configuration\n * @returns A PubSubAdapter instance\n *\n * @example\n * ```typescript\n * import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';\n *\n * const adapter = memoryAdapter({ maxStreams: 500 });\n * ```\n */\nexport function memoryAdapter(options: MemoryAdapterOptions = {}): PubSubAdapter {\n const { maxStreams = 1000 } = options;\n\n const streams = new Map<string, StreamEntry>();\n const eventCursors = new WeakMap<StreamEvent, number>();\n\n const scheduleCallback = (callback: () => void): void => {\n queueMicrotask(() => {\n try {\n callback();\n } catch {\n // Subscriber errors should not affect other subscribers\n }\n });\n };\n\n const getOrCreate = (streamId: string): StreamEntry => {\n let entry = streams.get(streamId);\n if (!entry) {\n if (streams.size >= maxStreams) {\n throw new Error(`Maximum concurrent streams (${maxStreams}) exceeded`);\n }\n entry = {\n stream: {\n streamId,\n createdAt: Date.now(),\n events: [],\n },\n subscribers: new Set(),\n cursorBase: 0,\n };\n streams.set(streamId, entry);\n }\n return entry;\n };\n\n return {\n async exists(streamId): Promise<boolean> {\n return streams.has(streamId);\n },\n\n async append(streamId, event): Promise<void> {\n const entry = getOrCreate(streamId);\n entry.stream.events.push(event);\n // Use cursorBase to ensure cursors increase monotonically across clears\n eventCursors.set(event, entry.cursorBase + entry.stream.events.length - 1);\n },\n\n async getEvents(streamId): Promise<StreamEvent[]> {\n const entry = streams.get(streamId);\n return entry ? [...entry.stream.events] : [];\n },\n\n subscribe(streamId, onEvent, onComplete, onFinalData): Unsubscribe {\n const entry = getOrCreate(streamId);\n const subscriber: Subscriber = { onEvent, onComplete, onFinalData };\n entry.subscribers.add(subscriber);\n\n return () => {\n entry.subscribers.delete(subscriber);\n };\n },\n\n publish(streamId, event): void {\n const entry = streams.get(streamId);\n if (!entry) {\n return;\n }\n\n // Cursor is stored in eventCursors with cursorBase already applied\n const cursor = eventCursors.get(event) ?? (entry.cursorBase + entry.stream.events.length - 1);\n for (const subscriber of entry.subscribers) {\n scheduleCallback(() => {\n subscriber.onEvent(event, cursor);\n });\n }\n },\n\n setFinalData(streamId, data): void {\n const entry = streams.get(streamId);\n if (entry) {\n entry.finalData = data;\n }\n },\n\n async remove(streamId): Promise<void> {\n const entry = streams.get(streamId);\n if (entry) {\n for (const subscriber of entry.subscribers) {\n if (entry.finalData !== undefined && subscriber.onFinalData) {\n scheduleCallback(() => {\n subscriber.onFinalData!(entry.finalData);\n });\n }\n scheduleCallback(subscriber.onComplete);\n }\n streams.delete(streamId);\n }\n },\n\n async clear(streamId): Promise<void> {\n const entry = streams.get(streamId);\n if (entry) {\n // Increment cursor base so new events have higher cursors than cleared events\n // This ensures subscribers don't skip events after a retry\n entry.cursorBase += entry.stream.events.length;\n entry.stream.events = [];\n entry.finalData = undefined;\n }\n },\n\n getCursorBase(streamId): number {\n const entry = streams.get(streamId);\n return entry?.cursorBase ?? 0;\n },\n };\n}\n","/**\n * @fileoverview Pub-sub middleware for stream resumption.\n *\n * Enables reconnecting clients to catch up on missed events during\n * active generation. The middleware buffers events and publishes them\n * to subscribers. Server routes handle reconnection logic using the\n * exported `createSubscriberStream` utility.\n *\n * @module middleware/pubsub\n */\n\nimport type {\n Middleware,\n MiddlewareContext,\n StreamContext,\n} from '../../types/middleware.ts';\nimport type { StreamEvent } from '../../types/stream.ts';\nimport type { Turn } from '../../types/turn.ts';\nimport type { PubSubAdapter, PubSubOptions } from './types.ts';\nimport { memoryAdapter } from './memory-adapter.ts';\nimport { serializeTurn } from '../../providers/proxy/serialization.ts';\n\nexport type {\n PubSubAdapter,\n PubSubOptions,\n StoredStream,\n SubscriptionCallback,\n CompletionCallback,\n FinalDataCallback,\n Unsubscribe,\n MemoryAdapterOptions,\n} from './types.ts';\nexport { memoryAdapter } from './memory-adapter.ts';\n\nconst STATE_KEY_STREAM_ID = 'pubsub:streamId';\nconst STATE_KEY_ADAPTER = 'pubsub:adapter';\nconst STATE_KEY_STREAM_ENDED = 'pubsub:streamEnded';\n\ninterface AppendChainState {\n chain: Promise<void>;\n}\n\n/**\n * Gets the stream ID from middleware state.\n *\n * @param state - Middleware state map\n * @returns Stream ID or undefined if not set\n */\nexport function getStreamId(state: Map<string, unknown>): string | undefined {\n return state.get(STATE_KEY_STREAM_ID) as string | undefined;\n}\n\n/**\n * Gets the adapter from middleware state.\n *\n * @param state - Middleware state map\n * @returns Adapter or undefined if not set\n */\nexport function getAdapter(state: Map<string, unknown>): PubSubAdapter | undefined {\n return state.get(STATE_KEY_ADAPTER) as PubSubAdapter | undefined;\n}\n\n/**\n * Creates pub-sub middleware for stream buffering and publishing.\n *\n * The middleware:\n * - Creates stream entries for new requests\n * - Buffers all stream events\n * - Publishes events to subscribers\n * - On stream end: notifies subscribers, then removes from adapter\n *\n * Server routes handle reconnection logic using `streamSubscriber`.\n *\n * @param options - Middleware configuration\n * @returns Middleware instance\n *\n * @example\n * ```typescript\n * import { llm } from '@providerprotocol/ai';\n * import { anthropic } from '@providerprotocol/ai/anthropic';\n * import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';\n * import { sendStream, setHeader } from 'h3';\n * import { h3 } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * const adapter = memoryAdapter();\n *\n * export default defineEventHandler(async (event) => {\n * const { input, conversationId } = await readBody(event);\n *\n * // Guard: prevent duplicate generations on reconnect\n * if (!await adapter.exists(conversationId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId: conversationId })],\n * });\n * model.stream(input).then(turn => saveToDatabase(turn));\n * }\n *\n * // Required: H3's sendStream does NOT set these headers\n * setHeader(event, 'Content-Type', 'text/event-stream');\n * setHeader(event, 'Cache-Control', 'no-cache');\n * setHeader(event, 'Connection', 'keep-alive');\n * setHeader(event, 'X-Accel-Buffering', 'no');\n *\n * return sendStream(event, h3.createSubscriberSSEStream(conversationId, adapter));\n * });\n * ```\n */\nexport function pubsubMiddleware(options: PubSubOptions = {}): Middleware {\n const {\n adapter = memoryAdapter(),\n streamId,\n } = options;\n\n const appendChains = new Map<string, AppendChainState>();\n\n const enqueueAppend = (id: string, event: StreamEvent): void => {\n const state = appendChains.get(id) ?? { chain: Promise.resolve() };\n\n const task = state.chain\n .catch(() => {})\n .then(async () => {\n await adapter.append(id, event);\n adapter.publish(id, event);\n });\n\n state.chain = task.catch(() => {});\n appendChains.set(id, state);\n };\n\n const waitForAppends = async (id: string): Promise<void> => {\n const state = appendChains.get(id);\n if (!state) {\n return;\n }\n\n await state.chain.catch(() => {});\n };\n\n const clearAppendState = (id: string): void => {\n appendChains.delete(id);\n };\n\n /**\n * Finalizes a stream by marking completion and removing from adapter.\n *\n * Called on any terminal state (complete, error, abort). After finalization,\n * the stream is removed from the adapter. Apps should use `.then()` to persist\n * completed conversations and serve them from their own storage on reconnect.\n */\n const finalizeStreamByState = async (state: Map<string, unknown>): Promise<void> => {\n const id = state.get(STATE_KEY_STREAM_ID) as string | undefined;\n if (!id) {\n return;\n }\n\n await waitForAppends(id);\n clearAppendState(id);\n\n // Remove from adapter (notifies subscribers) - apps persist via .then()\n await adapter.remove(id).catch(() => {});\n };\n\n return {\n name: 'pubsub',\n\n onStart(ctx: MiddlewareContext): void {\n ctx.state.set(STATE_KEY_ADAPTER, adapter);\n\n if (streamId) {\n ctx.state.set(STATE_KEY_STREAM_ID, streamId);\n // Ensure stream exists immediately so exists() returns true\n // before first token arrives (prevents duplicate generations)\n adapter.subscribe(streamId, () => {}, () => {})();\n }\n },\n\n onStreamEvent(event: StreamEvent, ctx: StreamContext): StreamEvent {\n const id = ctx.state.get(STATE_KEY_STREAM_ID) as string | undefined;\n if (!id) {\n return event;\n }\n\n enqueueAppend(id, event);\n\n return event;\n },\n\n async onStreamEnd(ctx: StreamContext): Promise<void> {\n const id = ctx.state.get(STATE_KEY_STREAM_ID) as string | undefined;\n if (!id) {\n return;\n }\n // Wait for all stream-phase appends to complete\n await waitForAppends(id);\n // Clear append state to prevent memory leaks if onTurn is skipped or fails.\n // Other middleware may emit during onTurn - those get new append chains.\n clearAppendState(id);\n ctx.state.set(STATE_KEY_STREAM_ENDED, true);\n },\n\n async onTurn(turn: Turn, ctx: MiddlewareContext): Promise<void> {\n const id = ctx.state.get(STATE_KEY_STREAM_ID) as string | undefined;\n const streamEnded = ctx.state.get(STATE_KEY_STREAM_ENDED) as boolean | undefined;\n\n if (!id) {\n return;\n }\n\n // Only emit Turn if we were streaming (onStreamEnd was called)\n if (streamEnded) {\n // Wait for any late appends from other middleware that emitted during onTurn\n // (e.g., pipeline middleware emits events before pubsub's onTurn runs).\n // These create new append chains since onStreamEnd cleared the stream-phase chains.\n await waitForAppends(id);\n clearAppendState(id);\n // Set the final Turn data so subscribers receive it before completion\n adapter.setFinalData(id, serializeTurn(turn));\n // Now remove the stream (notifies subscribers with final data + completion)\n await adapter.remove(id).catch(() => {});\n } else {\n // streamId was set but .generate() was used instead of .stream()\n // Clean up the orphan stream entry and warn about misuse\n const exists = await adapter.exists(id);\n if (exists) {\n console.warn(\n `[pubsub] streamId \"${id}\" was configured but .generate() was used instead of .stream(). ` +\n `Pubsub middleware only works with streaming. Cleaning up orphan stream.`\n );\n await adapter.remove(id).catch(() => {});\n }\n }\n },\n\n async onError(_error: Error, ctx: MiddlewareContext): Promise<void> {\n await finalizeStreamByState(ctx.state);\n },\n\n async onAbort(_error: Error, ctx: MiddlewareContext): Promise<void> {\n await finalizeStreamByState(ctx.state);\n },\n\n async onRetry(_attempt: number, _error: Error, ctx: MiddlewareContext): Promise<void> {\n const id = ctx.state.get(STATE_KEY_STREAM_ID) as string | undefined;\n if (!id) {\n return;\n }\n\n // Wait for in-flight appends to complete before clearing to prevent\n // stale events from repopulating the buffer after clear (especially\n // with async adapters like Redis)\n await waitForAppends(id);\n\n // Clear pending append chains\n clearAppendState(id);\n\n // Clear buffered events from adapter so subscribers don't receive duplicates\n await adapter.clear(id);\n\n // Reset stream ended flag for new attempt\n ctx.state.delete(STATE_KEY_STREAM_ENDED);\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
express,
|
|
3
3
|
streamSubscriber
|
|
4
|
-
} from "../../../../chunk-
|
|
5
|
-
import "../../../../chunk-
|
|
4
|
+
} from "../../../../chunk-SQ7ZUMKC.js";
|
|
5
|
+
import "../../../../chunk-PUKD2AV5.js";
|
|
6
6
|
import "../../../../chunk-ETBFOLQN.js";
|
|
7
7
|
export {
|
|
8
8
|
express,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fastify,
|
|
3
3
|
streamSubscriber
|
|
4
|
-
} from "../../../../chunk-
|
|
5
|
-
import "../../../../chunk-
|
|
4
|
+
} from "../../../../chunk-ODVES5EU.js";
|
|
5
|
+
import "../../../../chunk-PUKD2AV5.js";
|
|
6
6
|
import "../../../../chunk-ETBFOLQN.js";
|
|
7
7
|
export {
|
|
8
8
|
fastify,
|
|
@@ -11,24 +11,38 @@ import '../../../../tool-BmAfKNBq.js';
|
|
|
11
11
|
* @module middleware/pubsub/server/h3
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Options for subscriber SSE streams.
|
|
16
|
+
*/
|
|
17
|
+
interface SubscriberSSEStreamOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Interval in milliseconds between SSE keepalive comments.
|
|
20
|
+
* Set to `0` to disable. Defaults to `5000` (5 seconds).
|
|
21
|
+
*/
|
|
22
|
+
keepaliveMs?: number;
|
|
23
|
+
}
|
|
14
24
|
/**
|
|
15
25
|
* Creates a ReadableStream that replays buffered events and subscribes to live events.
|
|
16
26
|
*
|
|
17
|
-
*
|
|
18
|
-
* correctly in production (Nitro builds, reverse proxies, compression):
|
|
27
|
+
* Returns a `ReadableStream<Uint8Array>` for use with H3's `sendStream`.
|
|
19
28
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
29
|
+
* **Important:** H3's `sendStream` does **not** set response headers. You must
|
|
30
|
+
* set SSE headers yourself before calling `sendStream`, otherwise reverse proxies
|
|
31
|
+
* and CDNs (e.g. Cloudflare) won't recognise the response as an event stream
|
|
32
|
+
* and may buffer or timeout the connection.
|
|
33
|
+
*
|
|
34
|
+
* Keepalive comments (`:keepalive\n\n`) are sent automatically at a default
|
|
35
|
+
* interval of 5 seconds to prevent idle timeouts during long-running operations
|
|
36
|
+
* like pipeline stages. This can be configured via the `options` parameter.
|
|
24
37
|
*
|
|
25
38
|
* @param streamId - The stream ID to subscribe to
|
|
26
39
|
* @param adapter - The pub-sub adapter instance
|
|
40
|
+
* @param options - Optional stream configuration
|
|
27
41
|
* @returns A ReadableStream of SSE-formatted data
|
|
28
42
|
*
|
|
29
43
|
* @example
|
|
30
44
|
* ```typescript
|
|
31
|
-
* import { sendStream } from 'h3';
|
|
45
|
+
* import { sendStream, setHeader } from 'h3';
|
|
32
46
|
* import { llm } from '@providerprotocol/ai';
|
|
33
47
|
* import { anthropic } from '@providerprotocol/ai/anthropic';
|
|
34
48
|
* import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';
|
|
@@ -47,11 +61,17 @@ import '../../../../tool-BmAfKNBq.js';
|
|
|
47
61
|
* model.stream(input).then(turn => saveToDatabase(conversationId, turn));
|
|
48
62
|
* }
|
|
49
63
|
*
|
|
64
|
+
* // Required: H3's sendStream does NOT set these headers
|
|
65
|
+
* setHeader(event, 'Content-Type', 'text/event-stream');
|
|
66
|
+
* setHeader(event, 'Cache-Control', 'no-cache');
|
|
67
|
+
* setHeader(event, 'Connection', 'keep-alive');
|
|
68
|
+
* setHeader(event, 'X-Accel-Buffering', 'no');
|
|
69
|
+
*
|
|
50
70
|
* return sendStream(event, h3.createSubscriberSSEStream(conversationId, adapter));
|
|
51
71
|
* });
|
|
52
72
|
* ```
|
|
53
73
|
*/
|
|
54
|
-
declare function createSubscriberSSEStream(streamId: string, adapter: PubSubAdapter): ReadableStream<Uint8Array>;
|
|
74
|
+
declare function createSubscriberSSEStream(streamId: string, adapter: PubSubAdapter, options?: SubscriberSSEStreamOptions): ReadableStream<Uint8Array>;
|
|
55
75
|
/**
|
|
56
76
|
* H3 adapter namespace for pub-sub server utilities.
|
|
57
77
|
*/
|
|
@@ -59,4 +79,4 @@ declare const h3: {
|
|
|
59
79
|
createSubscriberSSEStream: typeof createSubscriberSSEStream;
|
|
60
80
|
};
|
|
61
81
|
|
|
62
|
-
export { createSubscriberSSEStream, h3 };
|
|
82
|
+
export { type SubscriberSSEStreamOptions, createSubscriberSSEStream, h3 };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSubscriberSSEStream,
|
|
3
3
|
h3
|
|
4
|
-
} from "../../../../chunk-
|
|
5
|
-
import "../../../../chunk-
|
|
4
|
+
} from "../../../../chunk-JPM7MVDO.js";
|
|
5
|
+
import "../../../../chunk-PUKD2AV5.js";
|
|
6
6
|
import "../../../../chunk-ETBFOLQN.js";
|
|
7
7
|
export {
|
|
8
8
|
createSubscriberSSEStream,
|
|
@@ -80,7 +80,7 @@ import '../../../tool-BmAfKNBq.js';
|
|
|
80
80
|
*
|
|
81
81
|
* @example H3/Nuxt
|
|
82
82
|
* ```typescript
|
|
83
|
-
* import { sendStream } from 'h3';
|
|
83
|
+
* import { sendStream, setHeader } from 'h3';
|
|
84
84
|
* import { h3 } from '@providerprotocol/ai/middleware/pubsub/server';
|
|
85
85
|
*
|
|
86
86
|
* export default defineEventHandler(async (event) => {
|
|
@@ -95,6 +95,12 @@ import '../../../tool-BmAfKNBq.js';
|
|
|
95
95
|
* model.stream(messages).then(turn => saveToDatabase(turn));
|
|
96
96
|
* }
|
|
97
97
|
*
|
|
98
|
+
* // Required: H3's sendStream does NOT set these headers
|
|
99
|
+
* setHeader(event, 'Content-Type', 'text/event-stream');
|
|
100
|
+
* setHeader(event, 'Cache-Control', 'no-cache');
|
|
101
|
+
* setHeader(event, 'Connection', 'keep-alive');
|
|
102
|
+
* setHeader(event, 'X-Accel-Buffering', 'no');
|
|
103
|
+
*
|
|
98
104
|
* return sendStream(event, h3.createSubscriberSSEStream(streamId, adapter));
|
|
99
105
|
* });
|
|
100
106
|
* ```
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
webapi
|
|
3
|
-
} from "../../../chunk-
|
|
3
|
+
} from "../../../chunk-YVAS343Z.js";
|
|
4
4
|
import {
|
|
5
5
|
express
|
|
6
|
-
} from "../../../chunk-
|
|
6
|
+
} from "../../../chunk-SQ7ZUMKC.js";
|
|
7
7
|
import {
|
|
8
8
|
h3
|
|
9
|
-
} from "../../../chunk-
|
|
9
|
+
} from "../../../chunk-JPM7MVDO.js";
|
|
10
10
|
import {
|
|
11
11
|
fastify
|
|
12
|
-
} from "../../../chunk-
|
|
13
|
-
import "../../../chunk-
|
|
12
|
+
} from "../../../chunk-ODVES5EU.js";
|
|
13
|
+
import "../../../chunk-PUKD2AV5.js";
|
|
14
14
|
import "../../../chunk-ETBFOLQN.js";
|
|
15
15
|
|
|
16
16
|
// src/middleware/pubsub/server/index.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/middleware/pubsub/server/index.ts"],"sourcesContent":["/**\n * @fileoverview Framework adapters for pub-sub stream resumption.\n *\n * Provides framework-specific adapters for handling stream reconnections\n * with various server frameworks. The Web API adapter works with modern\n * frameworks like Bun, Deno, Next.js App Router, and Cloudflare Workers.\n * Additional adapters provide native integration for Express, Fastify, and H3/Nuxt.\n *\n * @module middleware/pubsub/server\n */\n\nimport { express } from './express.ts';\nimport { fastify } from './fastify.ts';\nimport { h3 } from './h3.ts';\nimport { webapi } from './webapi.ts';\n\nexport { express, fastify, h3, webapi };\nexport type { PubSubAdapter } from '../types.ts';\n\n/**\n * Server adapters namespace for pub-sub stream resumption.\n *\n * Contains framework-specific adapters for Web API, Express, Fastify, and H3.\n * Always guard with `adapter.exists()` to prevent duplicate generations on reconnect.\n *\n * @example Web API (Next.js App Router, Bun, Deno)\n * ```typescript\n * import { webapi } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * export async function POST(req: Request) {\n * const { messages, streamId } = await req.json();\n *\n * // Guard: prevent duplicate generations on reconnect\n * if (!await adapter.exists(streamId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId })],\n * });\n * model.stream(messages).then(turn => saveToDatabase(turn));\n * }\n *\n * return new Response(webapi.createSubscriberStream(streamId, adapter), {\n * headers: { 'Content-Type': 'text/event-stream' },\n * });\n * }\n * ```\n *\n * @example Express\n * ```typescript\n * import { express } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * app.post('/api/ai', async (req, res) => {\n * const { messages, streamId } = req.body;\n *\n * // Guard: prevent duplicate generations on reconnect\n * if (!await adapter.exists(streamId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId })],\n * });\n * model.stream(messages).then(turn => saveToDatabase(turn));\n * }\n *\n * express.streamSubscriber(streamId, adapter, res);\n * });\n * ```\n *\n * @example Fastify\n * ```typescript\n * import { fastify } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * app.post('/api/ai', async (request, reply) => {\n * const { messages, streamId } = request.body;\n *\n * // Guard: prevent duplicate generations on reconnect\n * if (!await adapter.exists(streamId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId })],\n * });\n * model.stream(messages).then(turn => saveToDatabase(turn));\n * }\n *\n * return fastify.streamSubscriber(streamId, adapter, reply);\n * });\n * ```\n *\n * @example H3/Nuxt\n * ```typescript\n * import { sendStream } from 'h3';\n * import { h3 } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * export default defineEventHandler(async (event) => {\n * const { messages, streamId } = await readBody(event);\n *\n * // Guard: prevent duplicate generations on reconnect\n * if (!await adapter.exists(streamId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId })],\n * });\n * model.stream(messages).then(turn => saveToDatabase(turn));\n * }\n *\n * return sendStream(event, h3.createSubscriberSSEStream(streamId, adapter));\n * });\n * ```\n */\nexport const server = {\n /** Web API adapter (Bun, Deno, Next.js, Workers) */\n webapi,\n /** Express/Connect adapter */\n express,\n /** Fastify adapter */\n fastify,\n /** H3/Nitro/Nuxt adapter */\n h3,\n};\n"],"mappings":";;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"sources":["../../../../src/middleware/pubsub/server/index.ts"],"sourcesContent":["/**\n * @fileoverview Framework adapters for pub-sub stream resumption.\n *\n * Provides framework-specific adapters for handling stream reconnections\n * with various server frameworks. The Web API adapter works with modern\n * frameworks like Bun, Deno, Next.js App Router, and Cloudflare Workers.\n * Additional adapters provide native integration for Express, Fastify, and H3/Nuxt.\n *\n * @module middleware/pubsub/server\n */\n\nimport { express } from './express.ts';\nimport { fastify } from './fastify.ts';\nimport { h3 } from './h3.ts';\nimport { webapi } from './webapi.ts';\n\nexport { express, fastify, h3, webapi };\nexport type { PubSubAdapter } from '../types.ts';\n\n/**\n * Server adapters namespace for pub-sub stream resumption.\n *\n * Contains framework-specific adapters for Web API, Express, Fastify, and H3.\n * Always guard with `adapter.exists()` to prevent duplicate generations on reconnect.\n *\n * @example Web API (Next.js App Router, Bun, Deno)\n * ```typescript\n * import { webapi } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * export async function POST(req: Request) {\n * const { messages, streamId } = await req.json();\n *\n * // Guard: prevent duplicate generations on reconnect\n * if (!await adapter.exists(streamId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId })],\n * });\n * model.stream(messages).then(turn => saveToDatabase(turn));\n * }\n *\n * return new Response(webapi.createSubscriberStream(streamId, adapter), {\n * headers: { 'Content-Type': 'text/event-stream' },\n * });\n * }\n * ```\n *\n * @example Express\n * ```typescript\n * import { express } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * app.post('/api/ai', async (req, res) => {\n * const { messages, streamId } = req.body;\n *\n * // Guard: prevent duplicate generations on reconnect\n * if (!await adapter.exists(streamId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId })],\n * });\n * model.stream(messages).then(turn => saveToDatabase(turn));\n * }\n *\n * express.streamSubscriber(streamId, adapter, res);\n * });\n * ```\n *\n * @example Fastify\n * ```typescript\n * import { fastify } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * app.post('/api/ai', async (request, reply) => {\n * const { messages, streamId } = request.body;\n *\n * // Guard: prevent duplicate generations on reconnect\n * if (!await adapter.exists(streamId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId })],\n * });\n * model.stream(messages).then(turn => saveToDatabase(turn));\n * }\n *\n * return fastify.streamSubscriber(streamId, adapter, reply);\n * });\n * ```\n *\n * @example H3/Nuxt\n * ```typescript\n * import { sendStream, setHeader } from 'h3';\n * import { h3 } from '@providerprotocol/ai/middleware/pubsub/server';\n *\n * export default defineEventHandler(async (event) => {\n * const { messages, streamId } = await readBody(event);\n *\n * // Guard: prevent duplicate generations on reconnect\n * if (!await adapter.exists(streamId)) {\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * middleware: [pubsubMiddleware({ adapter, streamId })],\n * });\n * model.stream(messages).then(turn => saveToDatabase(turn));\n * }\n *\n * // Required: H3's sendStream does NOT set these headers\n * setHeader(event, 'Content-Type', 'text/event-stream');\n * setHeader(event, 'Cache-Control', 'no-cache');\n * setHeader(event, 'Connection', 'keep-alive');\n * setHeader(event, 'X-Accel-Buffering', 'no');\n *\n * return sendStream(event, h3.createSubscriberSSEStream(streamId, adapter));\n * });\n * ```\n */\nexport const server = {\n /** Web API adapter (Bun, Deno, Next.js, Workers) */\n webapi,\n /** Express/Connect adapter */\n express,\n /** Fastify adapter */\n fastify,\n /** H3/Nitro/Nuxt adapter */\n h3,\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAkHO,IAAM,SAAS;AAAA;AAAA,EAEpB;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AACF;","names":[]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSubscriberStream,
|
|
3
3
|
webapi
|
|
4
|
-
} from "../../../../chunk-
|
|
5
|
-
import "../../../../chunk-
|
|
4
|
+
} from "../../../../chunk-YVAS343Z.js";
|
|
5
|
+
import "../../../../chunk-PUKD2AV5.js";
|
|
6
6
|
import "../../../../chunk-ETBFOLQN.js";
|
|
7
7
|
export {
|
|
8
8
|
createSubscriberStream,
|
package/dist/openai/index.d.ts
CHANGED
package/dist/openai/index.js
CHANGED
|
@@ -1974,6 +1974,7 @@ function mcpTool(options) {
|
|
|
1974
1974
|
url,
|
|
1975
1975
|
name,
|
|
1976
1976
|
allowed_tools: allowedTools,
|
|
1977
|
+
allowed_resources: allowedResources,
|
|
1977
1978
|
headers,
|
|
1978
1979
|
require_approval: requireApproval
|
|
1979
1980
|
} = options;
|
|
@@ -1984,6 +1985,7 @@ function mcpTool(options) {
|
|
|
1984
1985
|
url,
|
|
1985
1986
|
name,
|
|
1986
1987
|
...allowedTools && { tool_configuration: { allowed_tools: allowedTools } },
|
|
1988
|
+
...allowedResources && { allowed_resources: allowedResources },
|
|
1987
1989
|
headers,
|
|
1988
1990
|
require_approval: requireApproval
|
|
1989
1991
|
}
|