@devilsdev/rag-pipeline-utils 2.3.1 → 2.4.0

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 (160) hide show
  1. package/README.md +293 -369
  2. package/contracts/loader-contract.json +11 -6
  3. package/contracts/reranker-contract.json +18 -26
  4. package/dist/index.cjs +21190 -47
  5. package/dist/index.d.ts +298 -68
  6. package/dist/index.mjs +9 -76
  7. package/package.json +20 -8
  8. package/src/agentic/agentic-pipeline.js +235 -0
  9. package/src/agentic/iterative-retriever.js +213 -0
  10. package/src/agentic/query-planner.js +208 -0
  11. package/src/agentic/self-critique.js +102 -0
  12. package/src/ai/adaptive-retrieval.js +0 -3
  13. package/src/ai/federated-learning.js +0 -3
  14. package/src/ai/federation.js +1 -6
  15. package/src/ai/index.js +9 -20
  16. package/src/ai/model-training.js +0 -3
  17. package/src/ai/multimodal-processing.js +0 -3
  18. package/src/ai/multimodal.js +1 -6
  19. package/src/ai/orchestrator.js +1 -6
  20. package/src/ai/retrieval-engine.js +1 -6
  21. package/src/chunking/chunking-engine.js +142 -0
  22. package/src/chunking/strategies/fixed-size.js +54 -0
  23. package/src/chunking/strategies/recursive.js +127 -0
  24. package/src/chunking/strategies/semantic.js +128 -0
  25. package/src/chunking/strategies/sentence.js +32 -0
  26. package/src/chunking/strategies/structure-aware.js +138 -0
  27. package/src/citation/citation-tracker.js +161 -0
  28. package/src/citation/hallucination-detector.js +74 -0
  29. package/src/citation/source-mapper.js +95 -0
  30. package/src/cli/commands/ai-ml/adaptive-commands.js +178 -0
  31. package/src/cli/commands/ai-ml/federated-commands.js +207 -0
  32. package/src/cli/commands/ai-ml/index.js +17 -0
  33. package/src/cli/commands/ai-ml/multimodal-commands.js +187 -0
  34. package/src/cli/commands/ai-ml/shared.js +51 -0
  35. package/src/cli/commands/ai-ml/training-commands.js +228 -0
  36. package/src/cli/commands/ai-ml.js +176 -757
  37. package/src/cli/commands/plugin-hub/certification-commands.js +221 -0
  38. package/src/cli/commands/plugin-hub/event-handlers.js +64 -0
  39. package/src/cli/commands/plugin-hub/hub-commands.js +290 -0
  40. package/src/cli/commands/plugin-hub/review-commands.js +78 -0
  41. package/src/cli/commands/plugin-hub.js +106 -782
  42. package/src/cli/diagnostics/config-checker.js +81 -0
  43. package/src/cli/diagnostics/dependency-checker.js +69 -0
  44. package/src/cli/diagnostics/environment-checker.js +45 -0
  45. package/src/cli/diagnostics/performance-checker.js +51 -0
  46. package/src/cli/diagnostics/plugin-checker.js +106 -0
  47. package/src/cli/diagnostics/security-checker.js +61 -0
  48. package/src/cli/doctor-command.js +70 -369
  49. package/src/cli/enhanced-cli-commands.js +162 -706
  50. package/src/cli/handlers/config-handler.js +126 -0
  51. package/src/cli/handlers/diagnostics-handler.js +113 -0
  52. package/src/cli/handlers/ingest-handler.js +42 -0
  53. package/src/cli/handlers/init-handler.js +51 -0
  54. package/src/cli/handlers/query-handler.js +45 -0
  55. package/src/cli/handlers/run-handler.js +181 -0
  56. package/src/cli/interactive-wizard.js +99 -723
  57. package/src/cli/shared/cli-utils.js +89 -0
  58. package/src/cli/wizard/configuration.js +148 -0
  59. package/src/cli/wizard/observability.js +79 -0
  60. package/src/cli/wizard/performance.js +76 -0
  61. package/src/cli/wizard/plugin-selection.js +297 -0
  62. package/src/cli/wizard/preview-save.js +172 -0
  63. package/src/cli/wizard/project-setup.js +66 -0
  64. package/src/config/memory-optimization.js +3 -3
  65. package/src/connectors/anthropic-connector.js +179 -0
  66. package/src/connectors/base-connector.js +94 -0
  67. package/src/connectors/cohere-connector.js +143 -0
  68. package/src/connectors/local-embedder.js +171 -0
  69. package/src/connectors/memory-retriever.js +150 -0
  70. package/src/connectors/ollama-connector.js +205 -0
  71. package/src/connectors/openai-connector.js +173 -0
  72. package/src/core/create-pipeline.js +21 -1
  73. package/src/core/observability/tracing.js +2 -1
  74. package/src/core/pipeline-factory.js +0 -1
  75. package/src/core/plugin-registry.js +14 -14
  76. package/src/cost/cost-calculator.js +146 -0
  77. package/src/cost/cost-tracker.js +260 -0
  78. package/src/cost/token-budget.js +245 -0
  79. package/src/dag/core/execution-scheduler.js +2 -1
  80. package/src/dag/core/topology-validation.js +0 -3
  81. package/src/dag/dag-engine.js +3 -5
  82. package/src/dag/dag-node.js +0 -1
  83. package/src/dag/enhanced-dag-engine.js +171 -99
  84. package/src/dashboard/dashboard-generator.js +642 -0
  85. package/src/dashboard/metrics-aggregator.js +409 -0
  86. package/src/debug/execution-tracer.js +210 -0
  87. package/src/debug/trace-inspector.js +281 -0
  88. package/src/dev/dev-server.js +5 -5
  89. package/src/dev/hot-reload.js +3 -2
  90. package/src/dx/integration-templates.js +0 -3
  91. package/src/ecosystem/plugin-analytics-dashboard.js +2 -2
  92. package/src/ecosystem/plugin-hub.js +2 -2
  93. package/src/enterprise/audit-logging.js +2 -2
  94. package/src/enterprise/sso-integration.js +52 -32
  95. package/src/evaluate/context-metrics.js +127 -0
  96. package/src/evaluate/evaluator.js +4 -2
  97. package/src/evaluate/faithfulness.js +117 -0
  98. package/src/evaluate/groundedness.js +60 -0
  99. package/src/evaluate/pipeline-evaluator.js +156 -0
  100. package/src/evaluate/relevance.js +129 -0
  101. package/src/evaluate/scoring.js +27 -0
  102. package/src/graph/entity-extractor.js +183 -0
  103. package/src/graph/graph-index.js +113 -0
  104. package/src/graph/graph-retriever.js +244 -0
  105. package/src/graph/knowledge-graph.js +256 -0
  106. package/src/guardrails/guardrails-pipeline.js +136 -0
  107. package/src/guardrails/post-generation-guard.js +170 -0
  108. package/src/guardrails/pre-retrieval-guard.js +161 -0
  109. package/src/guardrails/retrieval-guard.js +126 -0
  110. package/src/index.js +255 -9
  111. package/src/loader/html-loader.js +4 -15
  112. package/src/loader/markdown-loader.js +4 -15
  113. package/src/mcp/mcp-server.js +199 -0
  114. package/src/mcp/mcp-tool-builder.js +121 -0
  115. package/src/observability/audit-logger.js +0 -2
  116. package/src/observability/event-logger.js +2 -1
  117. package/src/observability/security-monitor.js +0 -2
  118. package/src/observability/telemetry.js +0 -2
  119. package/src/reranker/cascade-reranker.js +79 -0
  120. package/src/reranker/embedding-reranker.js +124 -0
  121. package/src/reranker/llm-reranker.js +6 -3
  122. package/src/reranker/scoring-reranker.js +123 -0
  123. package/src/retrieval/bm25-search.js +157 -0
  124. package/src/retrieval/hybrid-retriever.js +111 -0
  125. package/src/retrieval/rank-fusion.js +62 -0
  126. package/src/security/jwt-config.js +4 -3
  127. package/src/security/jwt-validator.js +2 -1
  128. package/src/security/plugin-sandbox.js +0 -3
  129. package/src/security/plugin-signature-verifier.js +4 -6
  130. package/src/security/validator.js +0 -2
  131. package/src/streaming/embedding-stream.js +197 -0
  132. package/src/streaming/sse-adapter.js +168 -0
  133. package/src/streaming/stream-router.js +210 -0
  134. package/src/streaming/streaming-embedder.js +231 -0
  135. package/src/streaming/websocket-adapter.js +171 -0
  136. package/src/templates/scaffold.js +147 -0
  137. package/src/templates/template-registry.js +272 -0
  138. package/src/utils/contract-validator.js +2 -1
  139. package/src/utils/input-sanitizer.js +80 -99
  140. package/src/utils/performance-monitor.js +0 -3
  141. package/src/utils/plugin-scaffolder.js +9 -16
  142. package/src/utils/rate-limiter.js +0 -1
  143. package/src/utils/retry-policy.js +0 -3
  144. package/src/utils/safe-expression-evaluator.js +0 -3
  145. package/src/utils/secure-logger-config.js +2 -4
  146. package/src/utils/secure-logging.js +0 -3
  147. package/src/utils/structured-logger.js +0 -3
  148. package/src/ai/index-fixed.js +0 -509
  149. package/src/ai/module-template.js +0 -37
  150. package/src/ci/workflow-analyzer.js +0 -71
  151. package/src/cli/plugin-marketplace-commands.js +0 -869
  152. package/src/dev/README.md +0 -126
  153. package/src/dev/examples/hot-reload-usage.js +0 -355
  154. package/src/mocks/openai-embedder.js +0 -119
  155. package/src/mocks/openai-llm.js +0 -77
  156. package/src/mocks/pdf-loader.js +0 -21
  157. package/src/mocks/pinecone-retriever.js +0 -106
  158. package/src/scripts/script-utilities.js +0 -36
  159. package/src/utils/ci/diagnostic-reporter.js +0 -57
  160. package/src/utils/examples/error-formatter-usage.js +0 -351
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_CONFIG = { k: 60 };
4
+
5
+ /**
6
+ * Reciprocal Rank Fusion - merges multiple ranked result lists
7
+ * @param {Array<Array>} resultSets - Array of result arrays, each result has {id, score, ...}
8
+ * @param {object} options - {k: 60, weights: number[] (per result set)}
9
+ * @returns {Array} Fused, deduplicated, sorted results
10
+ */
11
+ function reciprocalRankFusion(resultSets, options = {}) {
12
+ const { k = DEFAULT_CONFIG.k, weights } = options;
13
+
14
+ if (!Array.isArray(resultSets) || resultSets.length === 0) {
15
+ return [];
16
+ }
17
+
18
+ /** @type {Map<string, {score: number, fields: object}>} */
19
+ const scoreMap = new Map();
20
+
21
+ for (let setIndex = 0; setIndex < resultSets.length; setIndex++) {
22
+ const results = resultSets[setIndex];
23
+ if (!Array.isArray(results)) {
24
+ continue;
25
+ }
26
+
27
+ const weight =
28
+ Array.isArray(weights) && weights[setIndex] != null
29
+ ? weights[setIndex]
30
+ : 1.0;
31
+
32
+ for (let rank = 0; rank < results.length; rank++) {
33
+ const doc = results[rank];
34
+ if (!doc || doc.id == null) {
35
+ continue;
36
+ }
37
+
38
+ const id = String(doc.id);
39
+ const rrfScore = weight / (k + rank);
40
+
41
+ if (scoreMap.has(id)) {
42
+ const entry = scoreMap.get(id);
43
+ entry.score += rrfScore;
44
+ } else {
45
+ // Copy all original fields except score
46
+ const { score: _origScore, ...rest } = doc;
47
+ scoreMap.set(id, { score: rrfScore, fields: rest });
48
+ }
49
+ }
50
+ }
51
+
52
+ const fused = [];
53
+ for (const [id, entry] of scoreMap) {
54
+ fused.push({ ...entry.fields, id, score: entry.score });
55
+ }
56
+
57
+ fused.sort((a, b) => b.score - a.score);
58
+
59
+ return fused;
60
+ }
61
+
62
+ module.exports = { reciprocalRankFusion, DEFAULT_CONFIG };
@@ -13,6 +13,7 @@
13
13
  const crypto = require('crypto');
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
+ const { logger } = require('../utils/logger');
16
17
 
17
18
  /**
18
19
  * Load JWT configuration from environment or defaults
@@ -59,7 +60,7 @@ function loadJWTConfig() {
59
60
  // Generate a random secret for development/test/undefined environments
60
61
  secret = crypto.randomBytes(64).toString('hex');
61
62
  if (!isUndefined) {
62
- console.warn(
63
+ logger.warn(
63
64
  '[JWT_CONFIG] Warning: Using generated JWT_SECRET for development. ' +
64
65
  'Set JWT_SECRET environment variable for consistent tokens.',
65
66
  );
@@ -97,7 +98,7 @@ function loadJWTConfig() {
97
98
  'JWT_PUBLIC_KEY_PATH environment variable is required for asymmetric algorithms in production',
98
99
  );
99
100
  } else {
100
- console.warn(
101
+ logger.warn(
101
102
  '[JWT_CONFIG] Warning: No JWT_PUBLIC_KEY_PATH set. Token verification will not be available.',
102
103
  );
103
104
  }
@@ -112,7 +113,7 @@ function loadJWTConfig() {
112
113
  }
113
114
 
114
115
  if (!privateKeyPath) {
115
- console.warn(
116
+ logger.warn(
116
117
  '[JWT_CONFIG] Warning: No JWT_PRIVATE_KEY_PATH set. Token signing will not be available.',
117
118
  );
118
119
  } else {
@@ -18,6 +18,7 @@
18
18
  const jwt = require('jsonwebtoken');
19
19
  const crypto = require('crypto');
20
20
  const { EventEmitter } = require('events');
21
+ const { logger } = require('../utils/logger');
21
22
 
22
23
  /**
23
24
  * Secure JWT signing and validation algorithms
@@ -198,7 +199,7 @@ class JWTValidator extends EventEmitter {
198
199
  throw new Error('publicKey is required for asymmetric algorithms');
199
200
  }
200
201
  if (!privateKey) {
201
- console.warn(
202
+ logger.warn(
202
203
  '[JWT_VALIDATOR] Warning: privateKey not provided. Token signing will not be available.',
203
204
  );
204
205
  }
@@ -581,6 +581,3 @@ module.exports = {
581
581
  OutputSanitizer,
582
582
  run,
583
583
  };
584
-
585
- // CommonJS default export compatibility
586
- module.exports.default = module.exports;
@@ -10,6 +10,7 @@
10
10
  const crypto = require('crypto');
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
+ const { logger } = require('../utils/logger');
13
14
 
14
15
  /**
15
16
  * Plugin signature verification with Ed25519
@@ -285,9 +286,9 @@ class PluginSignatureVerifier {
285
286
 
286
287
  // In production, this should write to secure audit log
287
288
  if (process.env.NODE_ENV === 'production') {
288
- console.warn('[AUDIT]', JSON.stringify(auditEntry));
289
+ logger.warn('[AUDIT]', JSON.stringify(auditEntry));
289
290
  } else {
290
- console.log('[AUDIT]', auditEntry);
291
+ logger.info('[AUDIT]', auditEntry);
291
292
  }
292
293
  }
293
294
 
@@ -301,7 +302,7 @@ class PluginSignatureVerifier {
301
302
  this.auditLogger(result, manifest);
302
303
  } catch (error) {
303
304
  // Audit logging failure should not break verification
304
- console.error('Audit logging failed:', error.message);
305
+ logger.error('Audit logging failed:', error.message);
305
306
  }
306
307
  }
307
308
 
@@ -409,6 +410,3 @@ module.exports = {
409
410
  PluginSignatureVerifier,
410
411
  createPluginVerifier,
411
412
  };
412
-
413
- // CommonJS/ESM interop
414
- module.exports.default = module.exports;
@@ -31,6 +31,4 @@ function validateInput(input, opts = {}) {
31
31
  };
32
32
  }
33
33
 
34
- // CJS+ESM interop pattern
35
34
  module.exports = { validateInput };
36
- module.exports.default = module.exports;
@@ -0,0 +1,197 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Streaming Embeddings Generator
5
+ *
6
+ * Async generator that yields embeddings chunk by chunk with backpressure
7
+ * support. Processes documents in configurable batches while monitoring
8
+ * memory usage to prevent OOM conditions in large-scale pipelines.
9
+ *
10
+ * Features:
11
+ * - Batch-based document embedding with configurable size
12
+ * - Memory-aware backpressure (pauses when heap exceeds threshold)
13
+ * - Per-batch error isolation (failures don't abort the stream)
14
+ * - Progress tracking via yielded metadata
15
+ * - Support for both `.embed()` and `.embedQuery()` embedder APIs
16
+ *
17
+ * @module streaming/embedding-stream
18
+ * @since 2.5.0
19
+ */
20
+
21
+ /**
22
+ * Default configuration for the embedding stream
23
+ * @type {Object}
24
+ */
25
+ const DEFAULT_CONFIG = {
26
+ /** @type {number} Number of documents per embedding batch */
27
+ batchSize: 50,
28
+ /** @type {number} Maximum concurrent batch operations */
29
+ maxConcurrency: 3,
30
+ /** @type {number} Maximum heap memory budget in megabytes */
31
+ maxMemoryMB: 512,
32
+ /** @type {number} Fraction of maxMemoryMB that triggers backpressure (0-1) */
33
+ backpressureThreshold: 0.8,
34
+ };
35
+
36
+ /**
37
+ * Delay helper that resolves after the given milliseconds.
38
+ *
39
+ * @param {number} ms - Milliseconds to wait
40
+ * @returns {Promise<void>}
41
+ * @private
42
+ */
43
+ function delay(ms) {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+
47
+ /**
48
+ * Returns current heap usage in megabytes.
49
+ *
50
+ * @returns {number} Heap used in MB
51
+ * @private
52
+ */
53
+ function heapUsedMB() {
54
+ return process.memoryUsage().heapUsed / (1024 * 1024);
55
+ }
56
+
57
+ /**
58
+ * Validates that the provided embedder exposes a supported embedding method.
59
+ *
60
+ * @param {Object} embedder - The embedder instance to validate
61
+ * @throws {TypeError} If the embedder lacks both `.embed()` and `.embedQuery()`
62
+ * @private
63
+ */
64
+ function validateEmbedder(embedder) {
65
+ if (
66
+ !embedder ||
67
+ (typeof embedder.embed !== 'function' &&
68
+ typeof embedder.embedQuery !== 'function')
69
+ ) {
70
+ throw new TypeError(
71
+ 'Embedder must expose an .embed() or .embedQuery() method',
72
+ );
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Embeds a single document using the best available method on the embedder.
78
+ *
79
+ * Prefers `.embed()` (which typically accepts an array and returns an array of
80
+ * vectors) but falls back to `.embedQuery()` for single-document embedders.
81
+ *
82
+ * @param {Object} embedder - Embedder instance
83
+ * @param {string} content - Text content to embed
84
+ * @returns {Promise<number[]>} The embedding vector
85
+ * @private
86
+ */
87
+ async function embedOne(embedder, content) {
88
+ if (typeof embedder.embed === 'function') {
89
+ const result = await embedder.embed([content]);
90
+ return Array.isArray(result) ? result[0] : result;
91
+ }
92
+ return embedder.embedQuery(content);
93
+ }
94
+
95
+ /**
96
+ * Async generator that streams embedding results for a collection of documents.
97
+ *
98
+ * Documents are processed in batches of `batchSize`. Before each batch the
99
+ * generator checks heap memory; when usage exceeds
100
+ * `backpressureThreshold * maxMemoryMB` it pauses briefly and retries,
101
+ * giving the GC time to reclaim memory.
102
+ *
103
+ * Each yielded value is an object containing the document id, the computed
104
+ * vector, the document index, and a progress fraction (0-1).
105
+ *
106
+ * Errors within a batch are caught and yielded as objects with an `error`
107
+ * property so that consumers can decide how to handle partial failures
108
+ * without losing the rest of the stream.
109
+ *
110
+ * @param {Array<{id: string, content: string}>} documents - Documents to embed
111
+ * @param {Object} embedder - Embedder with `.embed()` or `.embedQuery()`
112
+ * @param {Object} [options] - Override any key from {@link DEFAULT_CONFIG}
113
+ * @yields {{ id: string, vector: number[], index: number, progress: number }
114
+ * | { id: string, error: Error, index: number, progress: number }}
115
+ *
116
+ * @example
117
+ * const docs = [{ id: '1', content: 'hello' }, { id: '2', content: 'world' }];
118
+ * for await (const emb of createEmbeddingStream(docs, myEmbedder)) {
119
+ * console.log(emb.id, emb.vector.length);
120
+ * }
121
+ */
122
+ async function* createEmbeddingStream(documents, embedder, options = {}) {
123
+ // --- Validate inputs ---
124
+ if (!Array.isArray(documents)) {
125
+ throw new TypeError('documents must be an array');
126
+ }
127
+ validateEmbedder(embedder);
128
+
129
+ const config = { ...DEFAULT_CONFIG, ...options };
130
+ const total = documents.length;
131
+
132
+ if (total === 0) {
133
+ return;
134
+ }
135
+
136
+ const memoryLimit = config.backpressureThreshold * config.maxMemoryMB;
137
+
138
+ // --- Process batches ---
139
+ for (let batchStart = 0; batchStart < total; batchStart += config.batchSize) {
140
+ const batchEnd = Math.min(batchStart + config.batchSize, total);
141
+ const batch = documents.slice(batchStart, batchEnd);
142
+
143
+ // --- Backpressure: wait while memory is above threshold ---
144
+ let backpressureApplied = false;
145
+ while (heapUsedMB() > memoryLimit) {
146
+ backpressureApplied = true;
147
+ await delay(100);
148
+ }
149
+
150
+ // --- Embed the batch (isolate errors per batch) ---
151
+ try {
152
+ // Attempt bulk embed if the embedder supports arrays natively
153
+ if (typeof embedder.embed === 'function') {
154
+ const contents = batch.map((doc) => doc.content || '');
155
+ const vectors = await embedder.embed(contents);
156
+ const vectorArray = Array.isArray(vectors) ? vectors : [vectors];
157
+
158
+ for (let j = 0; j < batch.length; j++) {
159
+ const globalIndex = batchStart + j;
160
+ yield {
161
+ id: batch[j].id,
162
+ vector: vectorArray[j] || [],
163
+ index: globalIndex,
164
+ progress: (globalIndex + 1) / total,
165
+ ...(backpressureApplied ? { backpressure: true } : {}),
166
+ };
167
+ }
168
+ } else {
169
+ // Fall back to embedQuery one at a time
170
+ for (let j = 0; j < batch.length; j++) {
171
+ const globalIndex = batchStart + j;
172
+ const vector = await embedder.embedQuery(batch[j].content || '');
173
+ yield {
174
+ id: batch[j].id,
175
+ vector,
176
+ index: globalIndex,
177
+ progress: (globalIndex + 1) / total,
178
+ ...(backpressureApplied ? { backpressure: true } : {}),
179
+ };
180
+ }
181
+ }
182
+ } catch (err) {
183
+ // Yield error entries for each document in the failed batch
184
+ for (let j = 0; j < batch.length; j++) {
185
+ const globalIndex = batchStart + j;
186
+ yield {
187
+ id: batch[j].id,
188
+ error: err,
189
+ index: globalIndex,
190
+ progress: (globalIndex + 1) / total,
191
+ };
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ module.exports = { createEmbeddingStream, DEFAULT_CONFIG };
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SSE (Server-Sent Events) Adapter
5
+ *
6
+ * Converts AsyncGenerator tokens to Server-Sent Events format
7
+ * for real-time streaming of RAG pipeline responses over HTTP.
8
+ *
9
+ * @module streaming/sse-adapter
10
+ * @since 2.4.0
11
+ */
12
+
13
+ const { EventEmitter } = require('events');
14
+
15
+ /**
16
+ * Default configuration for SSE streaming
17
+ * @type {Object}
18
+ */
19
+ const DEFAULT_CONFIG = {
20
+ /** @type {string} SSE event name for token events */
21
+ eventName: 'token',
22
+ /** @type {string} Sentinel value indicating stream completion */
23
+ doneEvent: '[DONE]',
24
+ /** @type {number} Interval in ms between heartbeat comments */
25
+ heartbeatIntervalMs: 15000,
26
+ };
27
+
28
+ /**
29
+ * Adapts an AsyncGenerator to the Server-Sent Events protocol.
30
+ *
31
+ * Writes well-formed SSE frames to an HTTP response, manages heartbeat
32
+ * keep-alive comments, and handles client disconnects gracefully.
33
+ *
34
+ * @extends EventEmitter
35
+ *
36
+ * @example
37
+ * const adapter = new SSEAdapter({ heartbeatIntervalMs: 10000 });
38
+ * adapter.on('token', (t) => console.log('sent', t));
39
+ * await adapter.stream(asyncTokenGenerator, res);
40
+ */
41
+ class SSEAdapter extends EventEmitter {
42
+ /**
43
+ * @param {Object} [options] - Configuration overrides
44
+ * @param {string} [options.eventName='token'] - SSE event name for token frames
45
+ * @param {string} [options.doneEvent='[DONE]'] - Sentinel value for the final data frame
46
+ * @param {number} [options.heartbeatIntervalMs=15000] - Heartbeat interval in milliseconds
47
+ */
48
+ constructor(options = {}) {
49
+ super();
50
+ this.config = { ...DEFAULT_CONFIG, ...options };
51
+ }
52
+
53
+ /**
54
+ * Stream tokens from an AsyncGenerator to an HTTP response as SSE.
55
+ *
56
+ * Sets the required SSE headers, starts a heartbeat timer, and iterates
57
+ * over the generator. Each yielded value should be an object with at least
58
+ * a `token` property and an optional `done` boolean.
59
+ *
60
+ * @param {AsyncGenerator<{token: string, done?: boolean}>} asyncGenerator - Token source
61
+ * @param {import('http').ServerResponse} httpResponse - Writable HTTP response
62
+ * @returns {Promise<void>} Resolves when the stream ends or the client disconnects
63
+ */
64
+ async stream(asyncGenerator, httpResponse) {
65
+ let heartbeatTimer = null;
66
+ let clientDisconnected = false;
67
+
68
+ // --- helpers --------------------------------------------------------
69
+ const cleanup = () => {
70
+ if (heartbeatTimer) {
71
+ clearInterval(heartbeatTimer);
72
+ heartbeatTimer = null;
73
+ }
74
+ };
75
+
76
+ // --- 1. SSE headers -------------------------------------------------
77
+ httpResponse.writeHead(200, {
78
+ 'Content-Type': 'text/event-stream',
79
+ 'Cache-Control': 'no-cache',
80
+ Connection: 'keep-alive',
81
+ });
82
+
83
+ // --- 2. Heartbeat keep-alive ----------------------------------------
84
+ heartbeatTimer = setInterval(() => {
85
+ if (!clientDisconnected) {
86
+ httpResponse.write(': heartbeat\n\n');
87
+ }
88
+ }, this.config.heartbeatIntervalMs);
89
+
90
+ // --- 3. Client disconnect handler -----------------------------------
91
+ const onClose = () => {
92
+ clientDisconnected = true;
93
+ cleanup();
94
+ this.emit('disconnect');
95
+ };
96
+ httpResponse.on('close', onClose);
97
+
98
+ // --- 4. Iterate over the generator ----------------------------------
99
+ try {
100
+ this.emit('started');
101
+
102
+ for await (const value of asyncGenerator) {
103
+ if (clientDisconnected) {
104
+ break;
105
+ }
106
+
107
+ if (value && value.done) {
108
+ // Final frame
109
+ httpResponse.write(`data: ${this.config.doneEvent}\n\n`);
110
+ this.emit('done');
111
+ break;
112
+ }
113
+
114
+ const token = value && value.token !== undefined ? value.token : value;
115
+ const frame = this.formatEvent(this.config.eventName, { token });
116
+ httpResponse.write(frame);
117
+ this.emit('token', token);
118
+ }
119
+
120
+ // If the generator is exhausted without an explicit done flag, send DONE
121
+ if (!clientDisconnected && !httpResponse.writableEnded) {
122
+ httpResponse.write(`data: ${this.config.doneEvent}\n\n`);
123
+ this.emit('done');
124
+ }
125
+ } catch (err) {
126
+ this.emit('error', err);
127
+
128
+ if (!clientDisconnected && !httpResponse.writableEnded) {
129
+ const errorFrame = this.formatEvent('error', {
130
+ message: err.message || 'Internal streaming error',
131
+ });
132
+ httpResponse.write(errorFrame);
133
+ }
134
+ } finally {
135
+ cleanup();
136
+ httpResponse.removeListener('close', onClose);
137
+
138
+ if (!httpResponse.writableEnded) {
139
+ httpResponse.end();
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Format a single SSE event string.
146
+ *
147
+ * Multi-line data is split so each line is prefixed with `data: ` as
148
+ * required by the SSE specification.
149
+ *
150
+ * @param {string} eventName - The SSE event type
151
+ * @param {*} data - Payload (will be JSON-stringified)
152
+ * @returns {string} Fully formatted SSE frame including trailing blank line
153
+ */
154
+ formatEvent(eventName, data) {
155
+ const json = JSON.stringify(data);
156
+ const lines = json.split('\n');
157
+
158
+ let frame = `event: ${eventName}\n`;
159
+ for (const line of lines) {
160
+ frame += `data: ${line}\n`;
161
+ }
162
+ frame += '\n';
163
+
164
+ return frame;
165
+ }
166
+ }
167
+
168
+ module.exports = { SSEAdapter, DEFAULT_CONFIG };