@providerprotocol/ai 0.0.40 → 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.
Files changed (30) hide show
  1. package/README.md +8 -1
  2. package/dist/chunk-JPM7MVDO.js +52 -0
  3. package/dist/chunk-JPM7MVDO.js.map +1 -0
  4. package/dist/{chunk-CWGTARDE.js → chunk-ODVES5EU.js} +3 -2
  5. package/dist/{chunk-CWGTARDE.js.map → chunk-ODVES5EU.js.map} +1 -1
  6. package/dist/{chunk-L6QWKFGE.js → chunk-PUKD2AV5.js} +11 -2
  7. package/dist/chunk-PUKD2AV5.js.map +1 -0
  8. package/dist/{chunk-KVUOTFYZ.js → chunk-SQ7ZUMKC.js} +3 -2
  9. package/dist/{chunk-KVUOTFYZ.js.map → chunk-SQ7ZUMKC.js.map} +1 -1
  10. package/dist/{chunk-ZMESKGUY.js → chunk-YVAS343Z.js} +2 -2
  11. package/dist/middleware/persistence/index.js +14 -7
  12. package/dist/middleware/persistence/index.js.map +1 -1
  13. package/dist/middleware/pubsub/index.d.ts +8 -1
  14. package/dist/middleware/pubsub/index.js.map +1 -1
  15. package/dist/middleware/pubsub/server/express/index.js +2 -2
  16. package/dist/middleware/pubsub/server/fastify/index.js +2 -2
  17. package/dist/middleware/pubsub/server/h3/index.d.ts +31 -20
  18. package/dist/middleware/pubsub/server/h3/index.js +6 -6
  19. package/dist/middleware/pubsub/server/index.d.ts +10 -3
  20. package/dist/middleware/pubsub/server/index.js +5 -5
  21. package/dist/middleware/pubsub/server/index.js.map +1 -1
  22. package/dist/middleware/pubsub/server/webapi/index.js +2 -2
  23. package/dist/openai/index.d.ts +1 -0
  24. package/dist/openai/index.js +2 -0
  25. package/dist/openai/index.js.map +1 -1
  26. package/package.json +7 -3
  27. package/dist/chunk-KBI45OXI.js +0 -31
  28. package/dist/chunk-KBI45OXI.js.map +0 -1
  29. package/dist/chunk-L6QWKFGE.js.map +0 -1
  30. /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
- return h3.streamSubscriber(streamId, adapter, event);
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
 
@@ -0,0 +1,52 @@
1
+ import {
2
+ runSubscriberStream
3
+ } from "./chunk-PUKD2AV5.js";
4
+
5
+ // src/middleware/pubsub/server/h3.ts
6
+ function createSubscriberSSEStream(streamId, adapter, options = {}) {
7
+ const encoder = new TextEncoder();
8
+ const abortController = new AbortController();
9
+ let closed = false;
10
+ return new ReadableStream({
11
+ async start(controller) {
12
+ await runSubscriberStream(
13
+ streamId,
14
+ adapter,
15
+ {
16
+ write: (data) => {
17
+ if (closed) {
18
+ return;
19
+ }
20
+ controller.enqueue(encoder.encode(data));
21
+ },
22
+ end: () => {
23
+ if (closed) {
24
+ return;
25
+ }
26
+ closed = true;
27
+ try {
28
+ controller.close();
29
+ } catch {
30
+ }
31
+ }
32
+ },
33
+ {
34
+ signal: abortController.signal,
35
+ keepaliveMs: options.keepaliveMs
36
+ }
37
+ );
38
+ },
39
+ cancel() {
40
+ abortController.abort();
41
+ }
42
+ });
43
+ }
44
+ var h3 = {
45
+ createSubscriberSSEStream
46
+ };
47
+
48
+ export {
49
+ createSubscriberSSEStream,
50
+ h3
51
+ };
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-L6QWKFGE.js";
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-CWGTARDE.js.map
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;AAExC,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
+ {"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-L6QWKFGE.js.map
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-L6QWKFGE.js";
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-KVUOTFYZ.js.map
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;AAExC,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
+ {"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-L6QWKFGE.js";
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-ZMESKGUY.js.map
49
+ //# sourceMappingURL=chunk-YVAS343Z.js.map
@@ -93,14 +93,21 @@ function persistenceMiddleware(options) {
93
93
  }
94
94
  ctx.state.set(STATE_KEY_THREAD, thread);
95
95
  if (thread.messages.length > 0) {
96
- const existingIds = new Set(ctx.request.messages.map((message) => message.id));
97
- const missing = thread.messages.filter((message) => !existingIds.has(message.id));
98
- if (missing.length > 0) {
99
- ctx.request.messages.unshift(...missing);
100
- const currentIndex = ctx.state.get(TURN_START_INDEX_KEY);
101
- const nextIndex = (typeof currentIndex === "number" ? currentIndex : 0) + missing.length;
102
- ctx.state.set(TURN_START_INDEX_KEY, nextIndex);
96
+ const requestById = new Map(ctx.request.messages.map((message) => [message.id, message]));
97
+ const threadIds = new Set(thread.messages.map((message) => message.id));
98
+ const mergedMessages = [];
99
+ for (const message of thread.messages) {
100
+ mergedMessages.push(requestById.get(message.id) ?? message);
103
101
  }
102
+ for (const message of ctx.request.messages) {
103
+ if (!threadIds.has(message.id)) {
104
+ mergedMessages.push(message);
105
+ }
106
+ }
107
+ ctx.request.messages.splice(0, ctx.request.messages.length, ...mergedMessages);
108
+ const currentIndex = ctx.state.get(TURN_START_INDEX_KEY);
109
+ const nextIndex = (typeof currentIndex === "number" ? currentIndex : 0) + thread.messages.length;
110
+ ctx.state.set(TURN_START_INDEX_KEY, nextIndex);
104
111
  }
105
112
  },
106
113
  async onTurn(turn, ctx) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/middleware/persistence.ts"],"sourcesContent":["/**\n * @fileoverview Persistence middleware for thread storage.\n *\n * Loads a conversation thread before execution and saves it after completion.\n * Designed for LLM requests; other modalities are ignored.\n *\n * @module middleware/persistence\n */\n\nimport type { Middleware, MiddlewareContext } from '../types/middleware.ts';\nimport type { LLMRequest } from '../types/llm.ts';\nimport type { Turn } from '../types/turn.ts';\nimport type { ThreadJSON } from '../types/thread.ts';\nimport { Thread } from '../types/thread.ts';\nimport { toError } from '../utils/error.ts';\n\nconst STATE_KEY_THREAD = 'persistence:thread';\nconst STATE_KEY_ID = 'persistence:id';\nconst TURN_START_INDEX_KEY = 'llm:turnStartIndex';\n\nconst isLLMRequest = (request: MiddlewareContext['request']): request is LLMRequest => (\n 'messages' in request\n);\n\n/**\n * Load result for persistence adapters.\n */\nexport type PersistenceLoadResult = Thread | ThreadJSON | null | undefined;\n\n/**\n * Adapter configuration for persistence middleware.\n */\nexport interface PersistenceAdapterConfig {\n /**\n * Unique identifier for the conversation.\n */\n id: string;\n\n /**\n * Loads a thread for the provided ID.\n *\n * Return a Thread instance, ThreadJSON, or null/undefined for new threads.\n *\n * @param id - Conversation identifier\n */\n load(id: string): Promise<PersistenceLoadResult>;\n\n /**\n * Persists the thread after a turn completes.\n *\n * @param id - Conversation identifier\n * @param thread - Updated thread instance\n * @param turn - Completed turn (undefined if not available)\n */\n save(id: string, thread: Thread, turn: Turn | undefined): Promise<void>;\n}\n\n/**\n * Persistence adapter implementation.\n *\n * Provides a thin wrapper around load/save callbacks.\n */\nexport class PersistenceAdapter {\n readonly id: string;\n\n private readonly loader: PersistenceAdapterConfig['load'];\n\n private readonly saver: PersistenceAdapterConfig['save'];\n\n /**\n * Creates a persistence adapter.\n *\n * @param config - Adapter configuration\n */\n constructor(config: PersistenceAdapterConfig) {\n this.id = config.id;\n this.loader = config.load;\n this.saver = config.save;\n }\n\n /**\n * Loads a thread for the provided ID.\n *\n * @param id - Conversation identifier\n */\n async load(id: string): Promise<PersistenceLoadResult> {\n return this.loader(id);\n }\n\n /**\n * Persists the thread after a turn completes.\n *\n * @param id - Conversation identifier\n * @param thread - Updated thread instance\n * @param turn - Completed turn (undefined if not available)\n */\n async save(id: string, thread: Thread, turn: Turn | undefined): Promise<void> {\n await this.saver(id, thread, turn);\n }\n}\n\n/**\n * Options for persistence middleware.\n */\nexport interface PersistenceOptions {\n /**\n * Adapter instance for loading and saving threads.\n */\n adapter: PersistenceAdapter;\n}\n\n/**\n * Gets the loaded thread from middleware state.\n *\n * @param state - Middleware state map\n * @returns Thread instance or undefined if not set\n */\nexport function getThread(state: Map<string, unknown>): Thread | undefined {\n return state.get(STATE_KEY_THREAD) as Thread | undefined;\n}\n\n/**\n * Gets the conversation ID from middleware state.\n *\n * @param state - Middleware state map\n * @returns Conversation ID or undefined if not set\n */\nexport function getThreadId(state: Map<string, unknown>): string | undefined {\n return state.get(STATE_KEY_ID) as string | undefined;\n}\n\n/**\n * Creates persistence middleware for thread storage.\n *\n * Loads a thread before requests and saves it after completion. The middleware\n * prepends loaded messages that are not already present in the request so turn\n * slicing excludes persisted history without duplicating explicit history.\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 { persistenceMiddleware, PersistenceAdapter } from '@providerprotocol/ai/middleware/persistence';\n *\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * system: 'You are a helpful assistant.',\n * middleware: [\n * persistenceMiddleware({\n * adapter: new PersistenceAdapter({\n * id: 'conversation-id',\n * load: async (id) => loadThreadFromMemory(id),\n * save: async (id, thread) => saveThreadToMemory(id, thread),\n * }),\n * }),\n * ],\n * });\n * ```\n */\nexport function persistenceMiddleware(options: PersistenceOptions): Middleware {\n const { adapter } = options;\n\n if (!adapter?.id) {\n throw new Error('persistenceMiddleware requires an adapter with a non-empty id');\n }\n if (typeof adapter.load !== 'function' || typeof adapter.save !== 'function') {\n throw new Error('persistenceMiddleware requires an adapter with load and save functions');\n }\n\n return {\n name: 'persistence',\n\n async onRequest(ctx: MiddlewareContext): Promise<void> {\n if (ctx.modality !== 'llm' || !isLLMRequest(ctx.request)) {\n return;\n }\n\n ctx.state.set(STATE_KEY_ID, adapter.id);\n\n let loaded: PersistenceLoadResult;\n try {\n loaded = await adapter.load(adapter.id);\n } catch (error) {\n const err = toError(error);\n throw new Error(`Persistence adapter failed to load thread \"${adapter.id}\": ${err.message}`, {\n cause: err,\n });\n }\n\n let thread: Thread;\n if (!loaded) {\n thread = new Thread();\n } else if (loaded instanceof Thread) {\n thread = loaded;\n } else {\n try {\n thread = Thread.fromJSON(loaded);\n } catch (error) {\n const err = toError(error);\n throw new Error(`Persistence adapter failed to deserialize thread \"${adapter.id}\": ${err.message}`, {\n cause: err,\n });\n }\n }\n\n ctx.state.set(STATE_KEY_THREAD, thread);\n\n if (thread.messages.length > 0) {\n const existingIds = new Set(ctx.request.messages.map((message) => message.id));\n const missing = thread.messages.filter((message) => !existingIds.has(message.id));\n if (missing.length > 0) {\n ctx.request.messages.unshift(...missing);\n const currentIndex = ctx.state.get(TURN_START_INDEX_KEY);\n const nextIndex = (typeof currentIndex === 'number' ? currentIndex : 0) + missing.length;\n ctx.state.set(TURN_START_INDEX_KEY, nextIndex);\n }\n }\n },\n\n async onTurn(turn: Turn, ctx: MiddlewareContext): Promise<void> {\n if (ctx.modality !== 'llm') {\n return;\n }\n\n const thread = getThread(ctx.state);\n if (!thread) {\n return;\n }\n\n if (isLLMRequest(ctx.request)) {\n const turnMessageIds = new Set(turn.messages.map((message) => message.id));\n const existingIds = new Set(thread.messages.map((message) => message.id));\n for (const message of ctx.request.messages) {\n if (turnMessageIds.has(message.id)) {\n continue;\n }\n if (!existingIds.has(message.id)) {\n thread.push(message);\n existingIds.add(message.id);\n }\n }\n }\n\n thread.append(turn);\n\n try {\n await adapter.save(adapter.id, thread, turn);\n } catch (error) {\n const err = toError(error);\n throw new Error(`Persistence adapter failed to save thread \"${adapter.id}\": ${err.message}`, {\n cause: err,\n });\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/middleware/persistence.ts"],"sourcesContent":["/**\n * @fileoverview Persistence middleware for thread storage.\n *\n * Loads a conversation thread before execution and saves it after completion.\n * Designed for LLM requests; other modalities are ignored.\n *\n * @module middleware/persistence\n */\n\nimport type { Middleware, MiddlewareContext } from '../types/middleware.ts';\nimport type { LLMRequest } from '../types/llm.ts';\nimport type { Turn } from '../types/turn.ts';\nimport type { ThreadJSON } from '../types/thread.ts';\nimport { Thread } from '../types/thread.ts';\nimport { toError } from '../utils/error.ts';\n\nconst STATE_KEY_THREAD = 'persistence:thread';\nconst STATE_KEY_ID = 'persistence:id';\nconst TURN_START_INDEX_KEY = 'llm:turnStartIndex';\n\nconst isLLMRequest = (request: MiddlewareContext['request']): request is LLMRequest => (\n 'messages' in request\n);\n\n/**\n * Load result for persistence adapters.\n */\nexport type PersistenceLoadResult = Thread | ThreadJSON | null | undefined;\n\n/**\n * Adapter configuration for persistence middleware.\n */\nexport interface PersistenceAdapterConfig {\n /**\n * Unique identifier for the conversation.\n */\n id: string;\n\n /**\n * Loads a thread for the provided ID.\n *\n * Return a Thread instance, ThreadJSON, or null/undefined for new threads.\n *\n * @param id - Conversation identifier\n */\n load(id: string): Promise<PersistenceLoadResult>;\n\n /**\n * Persists the thread after a turn completes.\n *\n * @param id - Conversation identifier\n * @param thread - Updated thread instance\n * @param turn - Completed turn (undefined if not available)\n */\n save(id: string, thread: Thread, turn: Turn | undefined): Promise<void>;\n}\n\n/**\n * Persistence adapter implementation.\n *\n * Provides a thin wrapper around load/save callbacks.\n */\nexport class PersistenceAdapter {\n readonly id: string;\n\n private readonly loader: PersistenceAdapterConfig['load'];\n\n private readonly saver: PersistenceAdapterConfig['save'];\n\n /**\n * Creates a persistence adapter.\n *\n * @param config - Adapter configuration\n */\n constructor(config: PersistenceAdapterConfig) {\n this.id = config.id;\n this.loader = config.load;\n this.saver = config.save;\n }\n\n /**\n * Loads a thread for the provided ID.\n *\n * @param id - Conversation identifier\n */\n async load(id: string): Promise<PersistenceLoadResult> {\n return this.loader(id);\n }\n\n /**\n * Persists the thread after a turn completes.\n *\n * @param id - Conversation identifier\n * @param thread - Updated thread instance\n * @param turn - Completed turn (undefined if not available)\n */\n async save(id: string, thread: Thread, turn: Turn | undefined): Promise<void> {\n await this.saver(id, thread, turn);\n }\n}\n\n/**\n * Options for persistence middleware.\n */\nexport interface PersistenceOptions {\n /**\n * Adapter instance for loading and saving threads.\n */\n adapter: PersistenceAdapter;\n}\n\n/**\n * Gets the loaded thread from middleware state.\n *\n * @param state - Middleware state map\n * @returns Thread instance or undefined if not set\n */\nexport function getThread(state: Map<string, unknown>): Thread | undefined {\n return state.get(STATE_KEY_THREAD) as Thread | undefined;\n}\n\n/**\n * Gets the conversation ID from middleware state.\n *\n * @param state - Middleware state map\n * @returns Conversation ID or undefined if not set\n */\nexport function getThreadId(state: Map<string, unknown>): string | undefined {\n return state.get(STATE_KEY_ID) as string | undefined;\n}\n\n/**\n * Creates persistence middleware for thread storage.\n *\n * Loads a thread before requests and saves it after completion. The middleware\n * prepends loaded messages that are not already present in the request so turn\n * slicing excludes persisted history without duplicating explicit history.\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 { persistenceMiddleware, PersistenceAdapter } from '@providerprotocol/ai/middleware/persistence';\n *\n * const model = llm({\n * model: anthropic('claude-sonnet-4-20250514'),\n * system: 'You are a helpful assistant.',\n * middleware: [\n * persistenceMiddleware({\n * adapter: new PersistenceAdapter({\n * id: 'conversation-id',\n * load: async (id) => loadThreadFromMemory(id),\n * save: async (id, thread) => saveThreadToMemory(id, thread),\n * }),\n * }),\n * ],\n * });\n * ```\n */\nexport function persistenceMiddleware(options: PersistenceOptions): Middleware {\n const { adapter } = options;\n\n if (!adapter?.id) {\n throw new Error('persistenceMiddleware requires an adapter with a non-empty id');\n }\n if (typeof adapter.load !== 'function' || typeof adapter.save !== 'function') {\n throw new Error('persistenceMiddleware requires an adapter with load and save functions');\n }\n\n return {\n name: 'persistence',\n\n async onRequest(ctx: MiddlewareContext): Promise<void> {\n if (ctx.modality !== 'llm' || !isLLMRequest(ctx.request)) {\n return;\n }\n\n ctx.state.set(STATE_KEY_ID, adapter.id);\n\n let loaded: PersistenceLoadResult;\n try {\n loaded = await adapter.load(adapter.id);\n } catch (error) {\n const err = toError(error);\n throw new Error(`Persistence adapter failed to load thread \"${adapter.id}\": ${err.message}`, {\n cause: err,\n });\n }\n\n let thread: Thread;\n if (!loaded) {\n thread = new Thread();\n } else if (loaded instanceof Thread) {\n thread = loaded;\n } else {\n try {\n thread = Thread.fromJSON(loaded);\n } catch (error) {\n const err = toError(error);\n throw new Error(`Persistence adapter failed to deserialize thread \"${adapter.id}\": ${err.message}`, {\n cause: err,\n });\n }\n }\n\n ctx.state.set(STATE_KEY_THREAD, thread);\n\n if (thread.messages.length > 0) {\n const requestById = new Map(ctx.request.messages.map((message) => [message.id, message]));\n const threadIds = new Set(thread.messages.map((message) => message.id));\n const mergedMessages: LLMRequest['messages'] = [];\n\n for (const message of thread.messages) {\n mergedMessages.push(requestById.get(message.id) ?? message);\n }\n\n for (const message of ctx.request.messages) {\n if (!threadIds.has(message.id)) {\n mergedMessages.push(message);\n }\n }\n\n ctx.request.messages.splice(0, ctx.request.messages.length, ...mergedMessages);\n\n const currentIndex = ctx.state.get(TURN_START_INDEX_KEY);\n const nextIndex = (typeof currentIndex === 'number' ? currentIndex : 0) + thread.messages.length;\n ctx.state.set(TURN_START_INDEX_KEY, nextIndex);\n }\n },\n\n async onTurn(turn: Turn, ctx: MiddlewareContext): Promise<void> {\n if (ctx.modality !== 'llm') {\n return;\n }\n\n const thread = getThread(ctx.state);\n if (!thread) {\n return;\n }\n\n if (isLLMRequest(ctx.request)) {\n const turnMessageIds = new Set(turn.messages.map((message) => message.id));\n const existingIds = new Set(thread.messages.map((message) => message.id));\n for (const message of ctx.request.messages) {\n if (turnMessageIds.has(message.id)) {\n continue;\n }\n if (!existingIds.has(message.id)) {\n thread.push(message);\n existingIds.add(message.id);\n }\n }\n }\n\n thread.append(turn);\n\n try {\n await adapter.save(adapter.id, thread, turn);\n } catch (error) {\n const err = toError(error);\n throw new Error(`Persistence adapter failed to save thread \"${adapter.id}\": ${err.message}`, {\n cause: err,\n });\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -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
- * return h3.streamSubscriber(conversationId, adapter, event);
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-KVUOTFYZ.js";
5
- import "../../../../chunk-L6QWKFGE.js";
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-CWGTARDE.js";
5
- import "../../../../chunk-L6QWKFGE.js";
4
+ } from "../../../../chunk-ODVES5EU.js";
5
+ import "../../../../chunk-PUKD2AV5.js";
6
6
  import "../../../../chunk-ETBFOLQN.js";
7
7
  export {
8
8
  fastify,
@@ -12,32 +12,37 @@ import '../../../../tool-BmAfKNBq.js';
12
12
  */
13
13
 
14
14
  /**
15
- * H3 Event interface (minimal type to avoid dependency).
15
+ * Options for subscriber SSE streams.
16
16
  */
17
- interface H3Event {
18
- node: {
19
- res: {
20
- setHeader(name: string, value: string): void;
21
- write(chunk: string): boolean;
22
- end(): void;
23
- on(event: 'close', listener: () => void): void;
24
- };
25
- };
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;
26
23
  }
27
24
  /**
28
- * Stream buffered and live events to an H3 event response.
25
+ * Creates a ReadableStream that replays buffered events and subscribes to live events.
29
26
  *
30
- * Handles reconnection for H3/Nuxt routes:
31
- * 1. Replays buffered events from the adapter
32
- * 2. Subscribes to live events until completion signal
33
- * 3. Ends when stream completes or client disconnects
27
+ * Returns a `ReadableStream<Uint8Array>` for use with H3's `sendStream`.
28
+ *
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.
34
37
  *
35
38
  * @param streamId - The stream ID to subscribe to
36
39
  * @param adapter - The pub-sub adapter instance
37
- * @param event - H3 event object
40
+ * @param options - Optional stream configuration
41
+ * @returns A ReadableStream of SSE-formatted data
38
42
  *
39
43
  * @example
40
44
  * ```typescript
45
+ * import { sendStream, setHeader } from 'h3';
41
46
  * import { llm } from '@providerprotocol/ai';
42
47
  * import { anthropic } from '@providerprotocol/ai/anthropic';
43
48
  * import { pubsubMiddleware, memoryAdapter } from '@providerprotocol/ai/middleware/pubsub';
@@ -56,16 +61,22 @@ interface H3Event {
56
61
  * model.stream(input).then(turn => saveToDatabase(conversationId, turn));
57
62
  * }
58
63
  *
59
- * return h3.streamSubscriber(conversationId, adapter, event);
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
+ *
70
+ * return sendStream(event, h3.createSubscriberSSEStream(conversationId, adapter));
60
71
  * });
61
72
  * ```
62
73
  */
63
- declare function streamSubscriber(streamId: string, adapter: PubSubAdapter, event: H3Event): Promise<void>;
74
+ declare function createSubscriberSSEStream(streamId: string, adapter: PubSubAdapter, options?: SubscriberSSEStreamOptions): ReadableStream<Uint8Array>;
64
75
  /**
65
76
  * H3 adapter namespace for pub-sub server utilities.
66
77
  */
67
78
  declare const h3: {
68
- streamSubscriber: typeof streamSubscriber;
79
+ createSubscriberSSEStream: typeof createSubscriberSSEStream;
69
80
  };
70
81
 
71
- export { h3, streamSubscriber };
82
+ export { type SubscriberSSEStreamOptions, createSubscriberSSEStream, h3 };
@@ -1,11 +1,11 @@
1
1
  import {
2
- h3,
3
- streamSubscriber
4
- } from "../../../../chunk-KBI45OXI.js";
5
- import "../../../../chunk-L6QWKFGE.js";
2
+ createSubscriberSSEStream,
3
+ h3
4
+ } from "../../../../chunk-JPM7MVDO.js";
5
+ import "../../../../chunk-PUKD2AV5.js";
6
6
  import "../../../../chunk-ETBFOLQN.js";
7
7
  export {
8
- h3,
9
- streamSubscriber
8
+ createSubscriberSSEStream,
9
+ h3
10
10
  };
11
11
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- import { streamSubscriber as streamSubscriber$2 } from './h3/index.js';
1
+ import { createSubscriberSSEStream } from './h3/index.js';
2
2
  export { h3 } from './h3/index.js';
3
3
  import { streamSubscriber as streamSubscriber$1 } from './fastify/index.js';
4
4
  export { fastify } from './fastify/index.js';
@@ -80,6 +80,7 @@ import '../../../tool-BmAfKNBq.js';
80
80
  *
81
81
  * @example H3/Nuxt
82
82
  * ```typescript
83
+ * import { sendStream, setHeader } from 'h3';
83
84
  * import { h3 } from '@providerprotocol/ai/middleware/pubsub/server';
84
85
  *
85
86
  * export default defineEventHandler(async (event) => {
@@ -94,7 +95,13 @@ import '../../../tool-BmAfKNBq.js';
94
95
  * model.stream(messages).then(turn => saveToDatabase(turn));
95
96
  * }
96
97
  *
97
- * return h3.streamSubscriber(streamId, adapter, event);
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
+ *
104
+ * return sendStream(event, h3.createSubscriberSSEStream(streamId, adapter));
98
105
  * });
99
106
  * ```
100
107
  */
@@ -113,7 +120,7 @@ declare const server: {
113
120
  };
114
121
  /** H3/Nitro/Nuxt adapter */
115
122
  h3: {
116
- streamSubscriber: typeof streamSubscriber$2;
123
+ createSubscriberSSEStream: typeof createSubscriberSSEStream;
117
124
  };
118
125
  };
119
126