@tangle-network/agent-core 0.13.2 → 0.14.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.
- package/README.md +1 -1
- package/dist/auth/browser.d.ts +1 -1
- package/dist/auth/browser.js +1 -2
- package/dist/auth/index.d.ts +1 -7
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +4 -7
- package/dist/auth/index.js.map +1 -1
- package/dist/computer-use/index.d.ts +0 -6
- package/dist/computer-use/index.d.ts.map +1 -1
- package/dist/computer-use/index.js +3 -6
- package/dist/computer-use/index.js.map +1 -1
- package/dist/{errors-GuWACHTg.js → errors-B5KiSXnw.js} +2 -3
- package/dist/{errors-GuWACHTg.js.map → errors-B5KiSXnw.js.map} +1 -1
- package/dist/events/index.d.ts +1 -1
- package/dist/events/index.js +1 -2
- package/dist/{events-U7kxNUpM.js → events-C2vdiRD_.js} +4 -7
- package/dist/{events-U7kxNUpM.js.map → events-C2vdiRD_.js.map} +1 -1
- package/dist/{index-68is3nof.d.ts → index-AEKS1kB8.d.ts} +1 -9
- package/dist/index-AEKS1kB8.d.ts.map +1 -0
- package/dist/{index-Dq6SfGaX.d.ts → index-CevHCod7.d.ts} +1 -9
- package/dist/index-CevHCod7.d.ts.map +1 -0
- package/dist/{index-CKDeaS7l.d.ts → index-DWPciD1S.d.ts} +2 -8
- package/dist/index-DWPciD1S.d.ts.map +1 -0
- package/dist/{index-BFYASYJk.d.ts → index-DwvNnL6X.d.ts} +1 -3
- package/dist/index-DwvNnL6X.d.ts.map +1 -0
- package/dist/{index-CuBvSsBf.d.ts → index-OCnM4OoP.d.ts} +1 -7
- package/dist/index-OCnM4OoP.d.ts.map +1 -0
- package/dist/{index-CQPGv8jg.d.ts → index-v0euJYbX.d.ts} +10 -12
- package/dist/index-v0euJYbX.d.ts.map +1 -0
- package/dist/index.d.ts +7 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -23
- package/dist/index.js.map +1 -1
- package/dist/interactions/index.d.ts +1 -3
- package/dist/interactions/index.d.ts.map +1 -1
- package/dist/interactions/index.js +4 -7
- package/dist/interactions/index.js.map +1 -1
- package/dist/interactions/mcp-server.d.ts +1 -3
- package/dist/interactions/mcp-server.d.ts.map +1 -1
- package/dist/interactions/mcp-server.js +2 -3
- package/dist/{lifecycle-BljLcnY5.js → lifecycle-wjO8lqUZ.js} +2 -3
- package/dist/{lifecycle-BljLcnY5.js.map → lifecycle-wjO8lqUZ.js.map} +1 -1
- package/dist/node/index.d.ts +0 -2
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -2
- package/dist/resilience/index.d.ts +1 -1
- package/dist/resilience/index.js +1 -2
- package/dist/{resilience-Cv5tqIDU.js → resilience-Bb1XDoKt.js} +5 -8
- package/dist/{resilience-Cv5tqIDU.js.map → resilience-Bb1XDoKt.js.map} +1 -1
- package/dist/sse/index.d.ts +123 -11
- package/dist/sse/index.d.ts.map +1 -1
- package/dist/sse/index.js +164 -37
- package/dist/sse/index.js.map +1 -1
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +1 -2
- package/dist/{telemetry--KwUEdTv.js → telemetry-CF4pOM9u.js} +5 -9
- package/dist/{telemetry--KwUEdTv.js.map → telemetry-CF4pOM9u.js.map} +1 -1
- package/dist/{tokens-browser-DCV3-WHc.d.ts → tokens-browser-CGLWUfZ7.d.ts} +5 -11
- package/dist/tokens-browser-CGLWUfZ7.d.ts.map +1 -0
- package/dist/{tools-DeZkIW3h.d.ts → tools-C3tUmLEu.d.ts} +1 -5
- package/dist/tools-C3tUmLEu.d.ts.map +1 -0
- package/dist/transport/index.d.ts +1 -1
- package/dist/transport/index.js +1 -2
- package/dist/{transport-DAfw3oL0.js → transport-BqEVpHWb.js} +5 -9
- package/dist/{transport-DAfw3oL0.js.map → transport-BqEVpHWb.js.map} +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -2
- package/dist/{types--u-Om3vE.js → types-BKMl10eN.js} +5 -8
- package/dist/{types--u-Om3vE.js.map → types-BKMl10eN.js.map} +1 -1
- package/dist/{types-DVN5qATk.js → types-UwiuyvMA.js} +2 -3
- package/dist/{types-DVN5qATk.js.map → types-UwiuyvMA.js.map} +1 -1
- package/package.json +9 -9
- package/dist/index-68is3nof.d.ts.map +0 -1
- package/dist/index-BFYASYJk.d.ts.map +0 -1
- package/dist/index-CKDeaS7l.d.ts.map +0 -1
- package/dist/index-CQPGv8jg.d.ts.map +0 -1
- package/dist/index-CuBvSsBf.d.ts.map +0 -1
- package/dist/index-Dq6SfGaX.d.ts.map +0 -1
- package/dist/tokens-browser-DCV3-WHc.d.ts.map +0 -1
- package/dist/tools-DeZkIW3h.d.ts.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as closeHttpServer, r as waitForSettled, t as SerializedTransportLifecycle } from "../lifecycle-
|
|
1
|
+
import { n as closeHttpServer, r as waitForSettled, t as SerializedTransportLifecycle } from "../lifecycle-wjO8lqUZ.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
4
|
import { createServer } from "node:http";
|
|
@@ -6,7 +6,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8
8
|
import { isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
|
|
9
|
+
// src/interactions/mcp-server.ts
|
|
10
10
|
const DEFAULT_INTERACTION_MCP_SERVER_NAME = "tangle-interaction";
|
|
11
11
|
const MAX_MCP_BODY_BYTES = 4194304;
|
|
12
12
|
const MAX_MCP_REPLAY_EVENTS = 1024;
|
|
@@ -363,7 +363,6 @@ var InteractionMcpServer = class {
|
|
|
363
363
|
return generationId;
|
|
364
364
|
}
|
|
365
365
|
};
|
|
366
|
-
//#endregion
|
|
367
366
|
export { DEFAULT_INTERACTION_MCP_SERVER_NAME, InteractionMcpServer, interactionMcpToolName };
|
|
368
367
|
|
|
369
368
|
//# sourceMappingURL=mcp-server.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/interactions/lifecycle.ts
|
|
2
2
|
/** Serialize one transport's single-use start and stop operations. */
|
|
3
3
|
var SerializedTransportLifecycle = class {
|
|
4
4
|
state = "idle";
|
|
@@ -62,7 +62,6 @@ async function waitForSettled(promises, graceMs = 1e3) {
|
|
|
62
62
|
if (timer) clearTimeout(timer);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
//#endregion
|
|
66
65
|
export { closeHttpServer as n, waitForSettled as r, SerializedTransportLifecycle as t };
|
|
67
66
|
|
|
68
|
-
//# sourceMappingURL=lifecycle-
|
|
67
|
+
//# sourceMappingURL=lifecycle-wjO8lqUZ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle-
|
|
1
|
+
{"version":3,"file":"lifecycle-wjO8lqUZ.js","names":[],"sources":["../src/interactions/lifecycle.ts"],"sourcesContent":["type LifecycleState = \"idle\" | \"starting\" | \"running\" | \"stopping\" | \"stopped\";\n\n/** Serialize one transport's single-use start and stop operations. */\nexport class SerializedTransportLifecycle {\n private state: LifecycleState = \"idle\";\n private tail: Promise<void> = Promise.resolve();\n\n start(open: () => Promise<void>): Promise<void> {\n return this.enqueue(async () => {\n if (this.state !== \"idle\") {\n throw new Error(`Interaction transport cannot start while ${this.state}`);\n }\n this.state = \"starting\";\n try {\n await open();\n this.state = \"running\";\n } catch (error) {\n this.state = \"stopped\";\n throw error;\n }\n });\n }\n\n stop(close: () => Promise<void>): Promise<void> {\n return this.enqueue(async () => {\n if (this.state === \"idle\" || this.state === \"stopped\") return;\n this.state = \"stopping\";\n try {\n await close();\n } finally {\n this.state = \"stopped\";\n }\n });\n }\n\n private enqueue(operation: () => Promise<void>): Promise<void> {\n const result = this.tail.then(operation);\n this.tail = result.catch(() => undefined);\n return result;\n }\n}\n\n/** Close active requests gracefully, then force-close a stuck local socket. */\nexport async function closeHttpServer(\n server: import(\"node:http\").Server,\n graceMs = 1_000,\n): Promise<void> {\n await new Promise<void>((resolve) => {\n let finished = false;\n const finish = () => {\n if (finished) return;\n finished = true;\n clearTimeout(forceClose);\n resolve();\n };\n const forceClose = setTimeout(() => {\n server.closeAllConnections?.();\n finish();\n }, graceMs);\n server.close(finish);\n server.closeIdleConnections?.();\n });\n}\n\n/** Give active work a bounded grace period before transport shutdown. */\nexport async function waitForSettled(\n promises: Iterable<Promise<unknown>>,\n graceMs = 1_000,\n): Promise<void> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n await Promise.race([\n Promise.allSettled([...promises]),\n new Promise<void>((resolve) => {\n timer = setTimeout(resolve, graceMs);\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n"],"mappings":";;AAGA,IAAa,+BAAb,MAA0C;CACxC,QAAgC;CAChC,OAA8B,QAAQ,QAAQ;CAE9C,MAAM,MAA0C;EAC9C,OAAO,KAAK,QAAQ,YAAY;GAC9B,IAAI,KAAK,UAAU,QACjB,MAAM,IAAI,MAAM,4CAA4C,KAAK,OAAO;GAE1E,KAAK,QAAQ;GACb,IAAI;IACF,MAAM,KAAK;IACX,KAAK,QAAQ;GACf,SAAS,OAAO;IACd,KAAK,QAAQ;IACb,MAAM;GACR;EACF,CAAC;CACH;CAEA,KAAK,OAA2C;EAC9C,OAAO,KAAK,QAAQ,YAAY;GAC9B,IAAI,KAAK,UAAU,UAAU,KAAK,UAAU,WAAW;GACvD,KAAK,QAAQ;GACb,IAAI;IACF,MAAM,MAAM;GACd,UAAU;IACR,KAAK,QAAQ;GACf;EACF,CAAC;CACH;CAEA,QAAgB,WAA+C;EAC7D,MAAM,SAAS,KAAK,KAAK,KAAK,SAAS;EACvC,KAAK,OAAO,OAAO,YAAY,KAAA,CAAS;EACxC,OAAO;CACT;AACF;;AAGA,eAAsB,gBACpB,QACA,UAAU,KACK;CACf,MAAM,IAAI,SAAe,YAAY;EACnC,IAAI,WAAW;EACf,MAAM,eAAe;GACnB,IAAI,UAAU;GACd,WAAW;GACX,aAAa,UAAU;GACvB,QAAQ;EACV;EACA,MAAM,aAAa,iBAAiB;GAClC,OAAO,sBAAsB;GAC7B,OAAO;EACT,GAAG,OAAO;EACV,OAAO,MAAM,MAAM;EACnB,OAAO,uBAAuB;CAChC,CAAC;AACH;;AAGA,eAAsB,eACpB,UACA,UAAU,KACK;CACf,IAAI;CACJ,IAAI;EACF,MAAM,QAAQ,KAAK,CACjB,QAAQ,WAAW,CAAC,GAAG,QAAQ,CAAC,GAChC,IAAI,SAAe,YAAY;GAC7B,QAAQ,WAAW,SAAS,OAAO;EACrC,CAAC,CACH,CAAC;CACH,UAAU;EACR,IAAI,OAAO,aAAa,KAAK;CAC/B;AACF"}
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
//#region src/node/index.d.ts
|
|
2
1
|
/** Lowercase hex SHA-256 of a string or bytes. */
|
|
3
2
|
export declare function sha256Hex(value: string | Uint8Array): string;
|
|
4
3
|
/** Quote one argument for a POSIX shell: single quotes, embedded `'` as `'\''`. */
|
|
5
4
|
export declare function shellQuote(value: string): string;
|
|
6
|
-
//#endregion
|
|
7
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/node/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/node/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/node/index.ts"],"mappings":";wBAOgB,UAAU,gBAAgB;;wBAK1B,WAAW"}
|
package/dist/node/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
2
|
+
// src/node/index.ts
|
|
3
3
|
/**
|
|
4
4
|
* Node-only primitives. Kept off the root entry so browser bundles of
|
|
5
5
|
* `@tangle-network/agent-core` never pull in `node:crypto`.
|
|
@@ -12,7 +12,6 @@ function sha256Hex(value) {
|
|
|
12
12
|
function shellQuote(value) {
|
|
13
13
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14
14
|
}
|
|
15
|
-
//#endregion
|
|
16
15
|
export { sha256Hex, shellQuote };
|
|
17
16
|
|
|
18
17
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as ProtectionOptions, c as CircuitBreaker, d as CircuitOpenError, f as CircuitState, i as withTimeout, l as CircuitBreakerConfig, n as createTimeoutController, o as createProtectedFn, r as sleep, s as withProtection, t as TimeoutError, u as CircuitBreakerStats } from "../index-
|
|
1
|
+
import { a as ProtectionOptions, c as CircuitBreaker, d as CircuitOpenError, f as CircuitState, i as withTimeout, l as CircuitBreakerConfig, n as createTimeoutController, o as createProtectedFn, r as sleep, s as withProtection, t as TimeoutError, u as CircuitBreakerStats } from "../index-DWPciD1S.js";
|
|
2
2
|
export { CircuitBreaker, type CircuitBreakerConfig, type CircuitBreakerStats, CircuitOpenError, type CircuitState, type ProtectionOptions, TimeoutError, createProtectedFn, createTimeoutController, sleep, withProtection, withTimeout };
|
package/dist/resilience/index.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { CircuitBreaker, CircuitOpenError, TimeoutError, createProtectedFn, createTimeoutController, sleep, withProtection, withTimeout };
|
|
1
|
+
export { s as CircuitBreaker, c as CircuitOpenError, r as TimeoutError, t as createProtectedFn, i as createTimeoutController, a as sleep, n as withProtection, o as withTimeout } from "../resilience-Bb1XDoKt.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as SDKError } from "./errors-
|
|
2
|
-
|
|
1
|
+
import { t as SDKError } from "./errors-B5KiSXnw.js";
|
|
2
|
+
// src/resilience/circuit-breaker.ts
|
|
3
3
|
/**
|
|
4
4
|
* Circuit Breaker
|
|
5
5
|
*
|
|
@@ -122,8 +122,7 @@ var CircuitBreaker = class {
|
|
|
122
122
|
this.lastFailureTime = Date.now();
|
|
123
123
|
}
|
|
124
124
|
};
|
|
125
|
-
|
|
126
|
-
//#region src/resilience/timeout.ts
|
|
125
|
+
// src/resilience/timeout.ts
|
|
127
126
|
/**
|
|
128
127
|
* Timeout Utilities
|
|
129
128
|
*
|
|
@@ -216,8 +215,7 @@ function armTimeout(ms, callback) {
|
|
|
216
215
|
if (timer !== void 0) clearTimeout(timer);
|
|
217
216
|
};
|
|
218
217
|
}
|
|
219
|
-
|
|
220
|
-
//#region src/resilience/protection.ts
|
|
218
|
+
// src/resilience/protection.ts
|
|
221
219
|
/**
|
|
222
220
|
* Execute a function with timeout and circuit breaker protection.
|
|
223
221
|
*
|
|
@@ -242,7 +240,6 @@ async function withProtection(fn, options = {}) {
|
|
|
242
240
|
function createProtectedFn(fn, options) {
|
|
243
241
|
return (...args) => withProtection(() => fn(...args), options);
|
|
244
242
|
}
|
|
245
|
-
//#endregion
|
|
246
243
|
export { sleep as a, CircuitOpenError as c, createTimeoutController as i, withProtection as n, withTimeout as o, TimeoutError as r, CircuitBreaker as s, createProtectedFn as t };
|
|
247
244
|
|
|
248
|
-
//# sourceMappingURL=resilience-
|
|
245
|
+
//# sourceMappingURL=resilience-Bb1XDoKt.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resilience-Cv5tqIDU.js","names":[],"sources":["../src/resilience/circuit-breaker.ts","../src/resilience/timeout.ts","../src/resilience/protection.ts"],"sourcesContent":["/**\n * Circuit Breaker\n *\n * Implements the circuit breaker pattern for fail-fast behavior when\n * downstream services are unhealthy. Prevents cascade failures by\n * stopping requests to failing services.\n *\n * States:\n * - CLOSED: Normal operation, requests pass through\n * - OPEN: Service unhealthy, requests fail immediately\n * - HALF_OPEN: Testing if service recovered\n */\n\nimport { SDKError } from \"../errors/index.js\";\n\nexport type CircuitState = \"closed\" | \"open\" | \"half-open\";\n\nexport interface CircuitBreakerConfig {\n /** Number of failures before opening circuit */\n failureThreshold: number;\n /** Time in ms to wait before testing recovery (half-open) */\n resetTimeoutMs: number;\n /** Number of successes in half-open state to close circuit */\n successThreshold: number;\n /** Optional: Only count these error types as failures */\n failureFilter?: (error: unknown) => boolean;\n}\n\nexport interface CircuitBreakerStats {\n state: CircuitState;\n failures: number;\n successes: number;\n lastFailureTime: number | null;\n totalFailures: number;\n totalSuccesses: number;\n totalRejected: number;\n}\n\nexport class CircuitOpenError extends SDKError {\n readonly circuitName: string;\n readonly resetAfterMs: number;\n\n constructor(name: string, resetAfterMs: number) {\n super(`Circuit breaker '${name}' is open`, {\n code: \"SERVER\",\n retryable: true,\n retryAfterMs: resetAfterMs,\n context: { circuitName: name },\n });\n this.name = \"CircuitOpenError\";\n this.circuitName = name;\n this.resetAfterMs = resetAfterMs;\n }\n}\n\nexport class CircuitBreaker {\n private state: CircuitState = \"closed\";\n private failures = 0;\n private successes = 0;\n private lastFailureTime: number | null = null;\n\n // Lifetime stats\n private totalFailures = 0;\n private totalSuccesses = 0;\n private totalRejected = 0;\n\n constructor(\n private readonly name: string,\n private readonly config: CircuitBreakerConfig,\n ) {\n if (config.failureThreshold < 1) {\n throw new Error(\"failureThreshold must be >= 1\");\n }\n if (config.resetTimeoutMs < 0) {\n throw new Error(\"resetTimeoutMs must be >= 0\");\n }\n if (config.successThreshold < 1) {\n throw new Error(\"successThreshold must be >= 1\");\n }\n }\n\n /**\n * Execute a function through the circuit breaker.\n * Throws CircuitOpenError if circuit is open.\n */\n async execute<T>(fn: () => Promise<T>): Promise<T> {\n // Check if we should transition from open to half-open\n if (this.state === \"open\") {\n const timeSinceFailure = this.lastFailureTime\n ? Date.now() - this.lastFailureTime\n : Number.POSITIVE_INFINITY;\n\n if (timeSinceFailure >= this.config.resetTimeoutMs) {\n this.state = \"half-open\";\n this.successes = 0;\n } else {\n this.totalRejected++;\n throw new CircuitOpenError(\n this.name,\n this.config.resetTimeoutMs - timeSinceFailure,\n );\n }\n }\n\n try {\n const result = await fn();\n this.onSuccess();\n return result;\n } catch (error) {\n this.onFailure(error);\n throw error;\n }\n }\n\n private onSuccess(): void {\n this.totalSuccesses++;\n\n if (this.state === \"half-open\") {\n this.successes++;\n if (this.successes >= this.config.successThreshold) {\n // Service recovered, close circuit\n this.state = \"closed\";\n this.failures = 0;\n this.successes = 0;\n }\n } else if (this.state === \"closed\") {\n // Reset failure count on success\n this.failures = 0;\n }\n }\n\n private onFailure(error: unknown): void {\n // Check if this error should count as a failure\n if (this.config.failureFilter && !this.config.failureFilter(error)) {\n return;\n }\n\n this.totalFailures++;\n this.failures++;\n this.lastFailureTime = Date.now();\n this.successes = 0;\n\n if (this.state === \"half-open\") {\n // Any failure in half-open returns to open\n this.state = \"open\";\n } else if (\n this.state === \"closed\" &&\n this.failures >= this.config.failureThreshold\n ) {\n // Threshold exceeded, open circuit\n this.state = \"open\";\n }\n }\n\n /** Get current circuit state */\n getState(): CircuitState {\n return this.state;\n }\n\n /** Get circuit statistics */\n getStats(): CircuitBreakerStats {\n return {\n state: this.state,\n failures: this.failures,\n successes: this.successes,\n lastFailureTime: this.lastFailureTime,\n totalFailures: this.totalFailures,\n totalSuccesses: this.totalSuccesses,\n totalRejected: this.totalRejected,\n };\n }\n\n /** Check if circuit is allowing requests */\n isAllowingRequests(): boolean {\n if (this.state === \"closed\" || this.state === \"half-open\") {\n return true;\n }\n\n // Check if reset timeout has passed\n const timeSinceFailure = this.lastFailureTime\n ? Date.now() - this.lastFailureTime\n : Number.POSITIVE_INFINITY;\n\n return timeSinceFailure >= this.config.resetTimeoutMs;\n }\n\n /** Manually reset the circuit to closed state */\n reset(): void {\n this.state = \"closed\";\n this.failures = 0;\n this.successes = 0;\n this.lastFailureTime = null;\n }\n\n /** Manually open the circuit */\n open(): void {\n this.state = \"open\";\n this.lastFailureTime = Date.now();\n }\n}\n","/**\n * Timeout Utilities\n *\n * Promise-based timeout handling with proper cleanup.\n */\n\nimport { SDKError } from \"../errors/index.js\";\n\nexport class TimeoutError extends SDKError {\n readonly timeoutMs: number;\n\n constructor(message: string, timeoutMs: number) {\n super(message, {\n code: \"TIMEOUT\",\n retryable: true,\n context: { timeoutMs },\n });\n this.name = \"TimeoutError\";\n this.timeoutMs = timeoutMs;\n }\n}\n\n/**\n * Wrap a promise with a timeout.\n * Rejects with TimeoutError if the promise doesn't resolve in time.\n */\nexport function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n message?: string,\n): Promise<T> {\n if (timeoutMs <= 0) {\n return promise;\n }\n\n let clearTimer: (() => void) | undefined;\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n clearTimer = armTimeout(timeoutMs, () => {\n reject(\n new TimeoutError(\n message || `Operation timed out after ${timeoutMs}ms`,\n timeoutMs,\n ),\n );\n });\n });\n\n return Promise.race([promise, timeoutPromise]).finally(() => {\n clearTimer?.();\n });\n}\n\n/**\n * Create an AbortController that auto-aborts after a timeout.\n * Returns the controller and a cleanup function.\n */\nexport function createTimeoutController(timeoutMs: number): {\n controller: AbortController;\n cleanup: () => void;\n} {\n const controller = new AbortController();\n let clearTimer: (() => void) | undefined;\n\n if (timeoutMs > 0) {\n clearTimer = armTimeout(timeoutMs, () => {\n controller.abort(new TimeoutError(\"Request timeout\", timeoutMs));\n });\n }\n\n return {\n controller,\n cleanup: () => {\n clearTimer?.();\n },\n };\n}\n\n/**\n * Sleep for a specified duration.\n * Optionally accepts an AbortSignal for cancellation.\n */\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n const onAbort = () => {\n clearTimer();\n signal?.removeEventListener(\"abort\", onAbort);\n reject(signal?.reason);\n };\n const clearTimer = armTimeout(ms, () => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n });\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n/** Keep one absolute deadline; native timers overflow to 1ms beyond this chunk size. */\nfunction armTimeout(ms: number, callback: () => void): () => void {\n const deadline = Date.now() + Math.max(0, ms);\n if (!Number.isFinite(ms) || !Number.isSafeInteger(Math.ceil(deadline))) {\n throw new SDKError(\"Timeout must produce a finite safe deadline\", { code: \"VALIDATION\" });\n }\n let cleared = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const arm = () => {\n if (cleared) return;\n timer = setTimeout(() => {\n if (cleared) return;\n if (Date.now() >= deadline) callback();\n else arm();\n }, Math.min(2_147_483_647, Math.max(0, deadline - Date.now())));\n };\n arm();\n return () => {\n cleared = true;\n if (timer !== undefined) clearTimeout(timer);\n };\n}\n","/**\n * Combines timeout + circuit breaker into a single request guard.\n */\n\nimport type { CircuitBreaker } from \"./circuit-breaker.js\";\nimport { withTimeout } from \"./timeout.js\";\n\nexport interface ProtectionOptions {\n /** Timeout in milliseconds (0 or undefined = no timeout) */\n timeoutMs?: number;\n /** Circuit breaker instance (optional) */\n breaker?: CircuitBreaker;\n /** Operation name for error messages */\n operationName?: string;\n}\n\n/**\n * Execute a function with timeout and circuit breaker protection.\n *\n * Order of execution:\n * 1. Circuit breaker check (fail fast if open)\n * 2. Timeout wrapper\n * 3. Execute function\n * 4. Update circuit breaker state\n */\nexport async function withProtection<T>(\n fn: () => Promise<T>,\n options: ProtectionOptions = {},\n): Promise<T> {\n const { timeoutMs, breaker, operationName = \"operation\" } = options;\n\n const execute = async (): Promise<T> => {\n if (timeoutMs && timeoutMs > 0) {\n return withTimeout(fn(), timeoutMs, `${operationName} timed out`);\n }\n return fn();\n };\n\n if (breaker) {\n return breaker.execute(execute);\n }\n\n return execute();\n}\n\n/**\n * Create a protected function that always applies the same protection options.\n */\nexport function createProtectedFn<TArgs extends unknown[], TResult>(\n fn: (...args: TArgs) => Promise<TResult>,\n options: ProtectionOptions,\n): (...args: TArgs) => Promise<TResult> {\n return (...args: TArgs) => withProtection(() => fn(...args), options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"resilience-Bb1XDoKt.js","names":[],"sources":["../src/resilience/circuit-breaker.ts","../src/resilience/timeout.ts","../src/resilience/protection.ts"],"sourcesContent":["/**\n * Circuit Breaker\n *\n * Implements the circuit breaker pattern for fail-fast behavior when\n * downstream services are unhealthy. Prevents cascade failures by\n * stopping requests to failing services.\n *\n * States:\n * - CLOSED: Normal operation, requests pass through\n * - OPEN: Service unhealthy, requests fail immediately\n * - HALF_OPEN: Testing if service recovered\n */\n\nimport { SDKError } from \"../errors/index.js\";\n\nexport type CircuitState = \"closed\" | \"open\" | \"half-open\";\n\nexport interface CircuitBreakerConfig {\n /** Number of failures before opening circuit */\n failureThreshold: number;\n /** Time in ms to wait before testing recovery (half-open) */\n resetTimeoutMs: number;\n /** Number of successes in half-open state to close circuit */\n successThreshold: number;\n /** Optional: Only count these error types as failures */\n failureFilter?: (error: unknown) => boolean;\n}\n\nexport interface CircuitBreakerStats {\n state: CircuitState;\n failures: number;\n successes: number;\n lastFailureTime: number | null;\n totalFailures: number;\n totalSuccesses: number;\n totalRejected: number;\n}\n\nexport class CircuitOpenError extends SDKError {\n readonly circuitName: string;\n readonly resetAfterMs: number;\n\n constructor(name: string, resetAfterMs: number) {\n super(`Circuit breaker '${name}' is open`, {\n code: \"SERVER\",\n retryable: true,\n retryAfterMs: resetAfterMs,\n context: { circuitName: name },\n });\n this.name = \"CircuitOpenError\";\n this.circuitName = name;\n this.resetAfterMs = resetAfterMs;\n }\n}\n\nexport class CircuitBreaker {\n private state: CircuitState = \"closed\";\n private failures = 0;\n private successes = 0;\n private lastFailureTime: number | null = null;\n\n // Lifetime stats\n private totalFailures = 0;\n private totalSuccesses = 0;\n private totalRejected = 0;\n\n constructor(\n private readonly name: string,\n private readonly config: CircuitBreakerConfig,\n ) {\n if (config.failureThreshold < 1) {\n throw new Error(\"failureThreshold must be >= 1\");\n }\n if (config.resetTimeoutMs < 0) {\n throw new Error(\"resetTimeoutMs must be >= 0\");\n }\n if (config.successThreshold < 1) {\n throw new Error(\"successThreshold must be >= 1\");\n }\n }\n\n /**\n * Execute a function through the circuit breaker.\n * Throws CircuitOpenError if circuit is open.\n */\n async execute<T>(fn: () => Promise<T>): Promise<T> {\n // Check if we should transition from open to half-open\n if (this.state === \"open\") {\n const timeSinceFailure = this.lastFailureTime\n ? Date.now() - this.lastFailureTime\n : Number.POSITIVE_INFINITY;\n\n if (timeSinceFailure >= this.config.resetTimeoutMs) {\n this.state = \"half-open\";\n this.successes = 0;\n } else {\n this.totalRejected++;\n throw new CircuitOpenError(\n this.name,\n this.config.resetTimeoutMs - timeSinceFailure,\n );\n }\n }\n\n try {\n const result = await fn();\n this.onSuccess();\n return result;\n } catch (error) {\n this.onFailure(error);\n throw error;\n }\n }\n\n private onSuccess(): void {\n this.totalSuccesses++;\n\n if (this.state === \"half-open\") {\n this.successes++;\n if (this.successes >= this.config.successThreshold) {\n // Service recovered, close circuit\n this.state = \"closed\";\n this.failures = 0;\n this.successes = 0;\n }\n } else if (this.state === \"closed\") {\n // Reset failure count on success\n this.failures = 0;\n }\n }\n\n private onFailure(error: unknown): void {\n // Check if this error should count as a failure\n if (this.config.failureFilter && !this.config.failureFilter(error)) {\n return;\n }\n\n this.totalFailures++;\n this.failures++;\n this.lastFailureTime = Date.now();\n this.successes = 0;\n\n if (this.state === \"half-open\") {\n // Any failure in half-open returns to open\n this.state = \"open\";\n } else if (\n this.state === \"closed\" &&\n this.failures >= this.config.failureThreshold\n ) {\n // Threshold exceeded, open circuit\n this.state = \"open\";\n }\n }\n\n /** Get current circuit state */\n getState(): CircuitState {\n return this.state;\n }\n\n /** Get circuit statistics */\n getStats(): CircuitBreakerStats {\n return {\n state: this.state,\n failures: this.failures,\n successes: this.successes,\n lastFailureTime: this.lastFailureTime,\n totalFailures: this.totalFailures,\n totalSuccesses: this.totalSuccesses,\n totalRejected: this.totalRejected,\n };\n }\n\n /** Check if circuit is allowing requests */\n isAllowingRequests(): boolean {\n if (this.state === \"closed\" || this.state === \"half-open\") {\n return true;\n }\n\n // Check if reset timeout has passed\n const timeSinceFailure = this.lastFailureTime\n ? Date.now() - this.lastFailureTime\n : Number.POSITIVE_INFINITY;\n\n return timeSinceFailure >= this.config.resetTimeoutMs;\n }\n\n /** Manually reset the circuit to closed state */\n reset(): void {\n this.state = \"closed\";\n this.failures = 0;\n this.successes = 0;\n this.lastFailureTime = null;\n }\n\n /** Manually open the circuit */\n open(): void {\n this.state = \"open\";\n this.lastFailureTime = Date.now();\n }\n}\n","/**\n * Timeout Utilities\n *\n * Promise-based timeout handling with proper cleanup.\n */\n\nimport { SDKError } from \"../errors/index.js\";\n\nexport class TimeoutError extends SDKError {\n readonly timeoutMs: number;\n\n constructor(message: string, timeoutMs: number) {\n super(message, {\n code: \"TIMEOUT\",\n retryable: true,\n context: { timeoutMs },\n });\n this.name = \"TimeoutError\";\n this.timeoutMs = timeoutMs;\n }\n}\n\n/**\n * Wrap a promise with a timeout.\n * Rejects with TimeoutError if the promise doesn't resolve in time.\n */\nexport function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n message?: string,\n): Promise<T> {\n if (timeoutMs <= 0) {\n return promise;\n }\n\n let clearTimer: (() => void) | undefined;\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n clearTimer = armTimeout(timeoutMs, () => {\n reject(\n new TimeoutError(\n message || `Operation timed out after ${timeoutMs}ms`,\n timeoutMs,\n ),\n );\n });\n });\n\n return Promise.race([promise, timeoutPromise]).finally(() => {\n clearTimer?.();\n });\n}\n\n/**\n * Create an AbortController that auto-aborts after a timeout.\n * Returns the controller and a cleanup function.\n */\nexport function createTimeoutController(timeoutMs: number): {\n controller: AbortController;\n cleanup: () => void;\n} {\n const controller = new AbortController();\n let clearTimer: (() => void) | undefined;\n\n if (timeoutMs > 0) {\n clearTimer = armTimeout(timeoutMs, () => {\n controller.abort(new TimeoutError(\"Request timeout\", timeoutMs));\n });\n }\n\n return {\n controller,\n cleanup: () => {\n clearTimer?.();\n },\n };\n}\n\n/**\n * Sleep for a specified duration.\n * Optionally accepts an AbortSignal for cancellation.\n */\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n const onAbort = () => {\n clearTimer();\n signal?.removeEventListener(\"abort\", onAbort);\n reject(signal?.reason);\n };\n const clearTimer = armTimeout(ms, () => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n });\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n/** Keep one absolute deadline; native timers overflow to 1ms beyond this chunk size. */\nfunction armTimeout(ms: number, callback: () => void): () => void {\n const deadline = Date.now() + Math.max(0, ms);\n if (!Number.isFinite(ms) || !Number.isSafeInteger(Math.ceil(deadline))) {\n throw new SDKError(\"Timeout must produce a finite safe deadline\", { code: \"VALIDATION\" });\n }\n let cleared = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const arm = () => {\n if (cleared) return;\n timer = setTimeout(() => {\n if (cleared) return;\n if (Date.now() >= deadline) callback();\n else arm();\n }, Math.min(2_147_483_647, Math.max(0, deadline - Date.now())));\n };\n arm();\n return () => {\n cleared = true;\n if (timer !== undefined) clearTimeout(timer);\n };\n}\n","/**\n * Combines timeout + circuit breaker into a single request guard.\n */\n\nimport type { CircuitBreaker } from \"./circuit-breaker.js\";\nimport { withTimeout } from \"./timeout.js\";\n\nexport interface ProtectionOptions {\n /** Timeout in milliseconds (0 or undefined = no timeout) */\n timeoutMs?: number;\n /** Circuit breaker instance (optional) */\n breaker?: CircuitBreaker;\n /** Operation name for error messages */\n operationName?: string;\n}\n\n/**\n * Execute a function with timeout and circuit breaker protection.\n *\n * Order of execution:\n * 1. Circuit breaker check (fail fast if open)\n * 2. Timeout wrapper\n * 3. Execute function\n * 4. Update circuit breaker state\n */\nexport async function withProtection<T>(\n fn: () => Promise<T>,\n options: ProtectionOptions = {},\n): Promise<T> {\n const { timeoutMs, breaker, operationName = \"operation\" } = options;\n\n const execute = async (): Promise<T> => {\n if (timeoutMs && timeoutMs > 0) {\n return withTimeout(fn(), timeoutMs, `${operationName} timed out`);\n }\n return fn();\n };\n\n if (breaker) {\n return breaker.execute(execute);\n }\n\n return execute();\n}\n\n/**\n * Create a protected function that always applies the same protection options.\n */\nexport function createProtectedFn<TArgs extends unknown[], TResult>(\n fn: (...args: TArgs) => Promise<TResult>,\n options: ProtectionOptions,\n): (...args: TArgs) => Promise<TResult> {\n return (...args: TArgs) => withProtection(() => fn(...args), options);\n}\n"],"mappings":"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"}
|
package/dist/sse/index.d.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
|
-
//#region src/sse/index.d.ts
|
|
2
1
|
/**
|
|
3
2
|
* SSE Utilities
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The one Server-Sent Events wire-format implementation: frame parsing and
|
|
5
|
+
* framing, plus the JSON event layer and stream usage extraction built on it.
|
|
6
|
+
* Everything here is pure ECMAScript over `ReadableStream`/`TextDecoder`
|
|
7
|
+
* globals and imports nothing, so Node, browser and Cloudflare Worker bundles
|
|
8
|
+
* can take this subpath without pulling a dependency.
|
|
9
|
+
*
|
|
10
|
+
* Frame parser guarantees (WHATWG Server-Sent Events, §9.2 "Interpreting an
|
|
11
|
+
* event stream"):
|
|
12
|
+
* - Buffers frames that straddle chunk boundaries; `push()` emits only
|
|
13
|
+
* complete frames and `flush()` dispatches a trailing frame whose
|
|
14
|
+
* terminating blank line never arrived.
|
|
15
|
+
* - Accepts LF and CRLF line terminators interchangeably. Bare CR is
|
|
16
|
+
* intentionally not treated as a terminator: no known emitter uses
|
|
17
|
+
* it, and supporting it would risk splitting payloads that
|
|
18
|
+
* legitimately contain CR.
|
|
19
|
+
* - Joins multi-line `data:` fields with "\n" (§9.2.6 step 3).
|
|
20
|
+
* - Skips comment lines (a leading ":") — the heartbeat mechanism.
|
|
21
|
+
* - Ignores `retry:` and unrecognized fields.
|
|
22
|
+
* - Strips one leading U+FEFF (BOM) from the stream.
|
|
23
|
+
* - Applies the `event:`/`id:` state of a frame only to that frame.
|
|
24
|
+
*
|
|
25
|
+
* The frame layer does no JSON parsing, reconnect/`Last-Event-ID` tracking or
|
|
26
|
+
* event-type filtering; `SSEChunkParser` adds JSON payload handling on top.
|
|
7
27
|
*/
|
|
8
28
|
/** Raw SSE event data from stream */
|
|
9
29
|
export interface SSEEventData {
|
|
@@ -46,6 +66,99 @@ export interface SSEEventData {
|
|
|
46
66
|
* ```
|
|
47
67
|
*/
|
|
48
68
|
export declare function parseSSEData(rawData: string): SSEEventData | null;
|
|
69
|
+
/**
|
|
70
|
+
* One parsed SSE frame: the fields accumulated between blank lines.
|
|
71
|
+
*
|
|
72
|
+
* `event` and `id` share no memory with the chunk the frame arrived in, so a
|
|
73
|
+
* consumer can keep them as long as it keeps its events. `data` can share
|
|
74
|
+
* that memory: parse it, and keep what the parse returns.
|
|
75
|
+
*/
|
|
76
|
+
export interface SSEFrame {
|
|
77
|
+
/** Value of the `event:` field; "" when the frame carries none. */
|
|
78
|
+
event: string;
|
|
79
|
+
/** `data:` field lines joined with "\n". */
|
|
80
|
+
data: string;
|
|
81
|
+
/** Value of the `id:` field; undefined when the frame carries none. */
|
|
82
|
+
id?: string;
|
|
83
|
+
}
|
|
84
|
+
/** Options for {@link streamSSEFrames}. */
|
|
85
|
+
export interface SSEStreamOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Aborts the read loop. When the signal fires, the generator throws a
|
|
88
|
+
* `DOMException` named "AbortError" so consumers can distinguish a
|
|
89
|
+
* cancellation from a clean end-of-stream without inspecting signal
|
|
90
|
+
* state.
|
|
91
|
+
*/
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
/**
|
|
94
|
+
* The longest silence the stream may keep, read before each chunk read.
|
|
95
|
+
* Any bytes reset it, comment heartbeats included, because a heartbeat is
|
|
96
|
+
* the producer saying the connection is still live. A read that outlasts it
|
|
97
|
+
* throws {@link SSEStreamSilentError}. `undefined` waits without bound, which
|
|
98
|
+
* suits a producer that promises no heartbeat.
|
|
99
|
+
*/
|
|
100
|
+
idleMs?: () => number | undefined;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Thrown when a stream sends no bytes for longer than its `idleMs`. A
|
|
104
|
+
* producer that heartbeats would have written in that time, so the
|
|
105
|
+
* connection is dead even though it never closed: a half-open TCP connection
|
|
106
|
+
* or a stalled proxy hold the socket open while nothing arrives.
|
|
107
|
+
*/
|
|
108
|
+
export declare class SSEStreamSilentError extends Error {
|
|
109
|
+
readonly name = "SSEStreamSilentError";
|
|
110
|
+
readonly idleMs: number;
|
|
111
|
+
constructor(idleMs: number);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Incremental SSE frame parser. Feed decoded text via `push()`; call
|
|
115
|
+
* `flush()` when the stream ends.
|
|
116
|
+
*/
|
|
117
|
+
export declare class SSEFrameParser {
|
|
118
|
+
private buffer;
|
|
119
|
+
private frameEvent;
|
|
120
|
+
private frameId;
|
|
121
|
+
private frameDataLines;
|
|
122
|
+
private bomChecked;
|
|
123
|
+
/**
|
|
124
|
+
* Append a decoded text chunk and return every frame the chunk
|
|
125
|
+
* completes. Frame state survives across calls: one frame can
|
|
126
|
+
* straddle any number of chunks.
|
|
127
|
+
*/
|
|
128
|
+
push(chunk: string): SSEFrame[];
|
|
129
|
+
/**
|
|
130
|
+
* End-of-stream: process the unterminated trailing line, then
|
|
131
|
+
* dispatch a dangling frame that never got its blank line.
|
|
132
|
+
* Well-behaved servers end frames with "\n\n", but some close early.
|
|
133
|
+
*/
|
|
134
|
+
flush(): SSEFrame[];
|
|
135
|
+
private processLines;
|
|
136
|
+
/**
|
|
137
|
+
* Dispatch the accumulated frame. A frame with no `data:` field (an
|
|
138
|
+
* `event:`/`id:`-only frame, or a bare comment) produces nothing, and
|
|
139
|
+
* its field state is consumed and reset either way.
|
|
140
|
+
*/
|
|
141
|
+
private dispatch;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Serialize one frame to its wire form. Multi-line `data` becomes one
|
|
145
|
+
* `data:` line per source line so the payload survives the trip without
|
|
146
|
+
* premature frame dispatch.
|
|
147
|
+
*/
|
|
148
|
+
export declare function encodeSSEFrame(frame: {
|
|
149
|
+
data: string;
|
|
150
|
+
event?: string;
|
|
151
|
+
id?: string;
|
|
152
|
+
}): string;
|
|
153
|
+
/**
|
|
154
|
+
* Read a streaming SSE response body into parsed frames.
|
|
155
|
+
*
|
|
156
|
+
* @throws `TypeError` when the body is missing.
|
|
157
|
+
* @throws `DOMException` with `name === "AbortError"` on cancellation —
|
|
158
|
+
* including the WHATWG-fetch variant where an aborted body resolves
|
|
159
|
+
* `done: true` instead of rejecting the read.
|
|
160
|
+
*/
|
|
161
|
+
export declare function streamSSEFrames(body: ReadableStream<Uint8Array> | null | undefined, options?: SSEStreamOptions): AsyncGenerator<SSEFrame>;
|
|
49
162
|
export interface SSEParserOptions<T = unknown> {
|
|
50
163
|
/** Log malformed data lines (default: false) */
|
|
51
164
|
logMalformed?: boolean;
|
|
@@ -59,18 +172,18 @@ export interface ParsedSSEEvent<T = unknown> {
|
|
|
59
172
|
eventType?: string;
|
|
60
173
|
}
|
|
61
174
|
/**
|
|
62
|
-
* Incremental
|
|
63
|
-
* Feed decoded string chunks via push(); call flush()
|
|
175
|
+
* Incremental JSON event parser for chunked streams, framed by
|
|
176
|
+
* {@link SSEFrameParser}. Feed decoded string chunks via push(); call flush()
|
|
177
|
+
* on stream end. Frames whose trimmed data is empty produce no event.
|
|
64
178
|
*/
|
|
65
179
|
export declare class SSEChunkParser<T = unknown> {
|
|
66
|
-
private
|
|
67
|
-
private
|
|
68
|
-
private options;
|
|
180
|
+
private readonly frames;
|
|
181
|
+
private readonly options;
|
|
69
182
|
constructor(options?: SSEParserOptions<T>);
|
|
70
183
|
push(chunk: string): ParsedSSEEvent<T>[];
|
|
71
184
|
flush(): ParsedSSEEvent<T>[];
|
|
72
|
-
private
|
|
73
|
-
private
|
|
185
|
+
private toEvents;
|
|
186
|
+
private toEvent;
|
|
74
187
|
}
|
|
75
188
|
/**
|
|
76
189
|
* Parse SSE stream into typed events via async iteration.
|
|
@@ -169,5 +282,4 @@ export declare function createUsageCallback<T extends Record<string, unknown>>(f
|
|
|
169
282
|
getUsage: () => StreamUsageAccumulator;
|
|
170
283
|
extractor: StreamUsageExtractor;
|
|
171
284
|
};
|
|
172
|
-
//#endregion
|
|
173
285
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/sse/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/sse/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/sse/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6BiB;EACf;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EAEA;EAEA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;IACE;IACA;IACA;IACA;;GAGD;;;;;;;;;;;wBAYa,aAAa,kBAAkB;;;;;;;;iBAsC9B;;EAEf;;EAEA;;EAEA;;;iBAee;;;;;;;EAOf,SAAS;;;;;;;;EAQT;;;;;;;;qBASW,6BAA6B;WACtB;WACT;EACG,YAAA;;;;;;qBAUD;UACH;UACA;UACA;UACA;UACA;;;;;;EAOR,KAAK,gBAAgB;;;;;;EAqBrB,SAAS;UASD;;;;;;UAoCA;;;;;;;wBAqBM,eAAe;EAC7B;EACA;EACA;;;;;;;;;;wBAiBqB,gBACrB,MAAM,eAAe,gCACrB,UAAU,mBACT,eAAe;iBA4DD,iBAAiB;;EAEhC;;EAEA,aAAa,gBAAgB;;iBAGd,eAAe;EAC9B,MAAM;EACN;EACA;EACA;;;;;;;qBAQW,eAAe;mBACT;mBACA;EAEL,YAAA,UAAS,iBAAiB;EAItC,KAAK,gBAAgB,eAAe;EAIpC,SAAS,eAAe;UAIhB;UASA;;;;;wBAuCa,eAAe,aACpC,QAAQ,eAAe,aACvB,UAAS,iBAAiB,KACzB,eAAe,eAAe;;;;wBA4CX,iBAAiB,aACrC,QAAQ,eAAe,aACvB,UAAU,OAAO,eAAe,aAChC,UAAU,OAAO,gBACjB,UAAS,iBAAiB,KACzB;;iBAuCc;EACf;EACA;EACA;EACA;;;iBAIe;;EAEf,OAAO;;EAEP;;EAEA;;EAEA;;EAEA,WAAW;;EAEX;;;;;;;;;;;;;;;;;;;qBA0BW;UACH;UACA;;;;;EAiBR,cAAc;;;;EAUd,aAAa,OAAO,eAAe;;;;;EAoDnC,YAAY;;;;EAeZ,WAAW,SAAS;;;;EAOpB;UAaQ;UAyCA;;;;;wBA4BM,8BAA8B;;;;;;;;;;;;;;wBAiB9B,oBAAoB,UAAU,yBAC5C,WAAW,OAAO;EAElB,WAAW,OAAO;EAClB,gBAAgB;EAChB,WAAW"}
|
package/dist/sse/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/sse/index.ts
|
|
2
2
|
/**
|
|
3
3
|
* Parse raw SSE data string into structured event data.
|
|
4
4
|
*
|
|
@@ -26,63 +26,191 @@ function parseSSEData(rawData) {
|
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* A copy of `value` that shares no memory with the string it was cut from.
|
|
30
|
+
*
|
|
31
|
+
* V8 can return a short substring of a long string as a view that keeps the
|
|
32
|
+
* whole string alive. Consumers keep event names and ids as long as they keep
|
|
33
|
+
* events, so a view would keep every chunk its frame arrived in alive.
|
|
31
34
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
function detached(value) {
|
|
36
|
+
return value === "" ? value : JSON.parse(JSON.stringify(value));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Thrown when a stream sends no bytes for longer than its `idleMs`. A
|
|
40
|
+
* producer that heartbeats would have written in that time, so the
|
|
41
|
+
* connection is dead even though it never closed: a half-open TCP connection
|
|
42
|
+
* or a stalled proxy hold the socket open while nothing arrives.
|
|
43
|
+
*/
|
|
44
|
+
var SSEStreamSilentError = class extends Error {
|
|
45
|
+
name = "SSEStreamSilentError";
|
|
46
|
+
idleMs;
|
|
47
|
+
constructor(idleMs) {
|
|
48
|
+
super(`SSE stream sent no bytes for ${idleMs} ms`);
|
|
49
|
+
this.idleMs = idleMs;
|
|
38
50
|
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Incremental SSE frame parser. Feed decoded text via `push()`; call
|
|
54
|
+
* `flush()` when the stream ends.
|
|
55
|
+
*/
|
|
56
|
+
var SSEFrameParser = class {
|
|
57
|
+
buffer = "";
|
|
58
|
+
frameEvent = "";
|
|
59
|
+
frameId;
|
|
60
|
+
frameDataLines = null;
|
|
61
|
+
bomChecked = false;
|
|
62
|
+
/**
|
|
63
|
+
* Append a decoded text chunk and return every frame the chunk
|
|
64
|
+
* completes. Frame state survives across calls: one frame can
|
|
65
|
+
* straddle any number of chunks.
|
|
66
|
+
*/
|
|
39
67
|
push(chunk) {
|
|
40
|
-
|
|
68
|
+
let text = chunk;
|
|
69
|
+
if (!this.bomChecked && text.length > 0) {
|
|
70
|
+
this.bomChecked = true;
|
|
71
|
+
if (text.charCodeAt(0) === 65279) text = text.slice(1);
|
|
72
|
+
}
|
|
73
|
+
this.buffer += text;
|
|
41
74
|
const lines = this.buffer.split("\n");
|
|
42
75
|
this.buffer = lines.pop() ?? "";
|
|
43
76
|
return this.processLines(lines);
|
|
44
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* End-of-stream: process the unterminated trailing line, then
|
|
80
|
+
* dispatch a dangling frame that never got its blank line.
|
|
81
|
+
* Well-behaved servers end frames with "\n\n", but some close early.
|
|
82
|
+
*/
|
|
45
83
|
flush() {
|
|
46
|
-
const
|
|
84
|
+
const rest = this.buffer;
|
|
47
85
|
this.buffer = "";
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
this.currentEvent = {};
|
|
53
|
-
}
|
|
54
|
-
return events;
|
|
86
|
+
const frames = rest.length > 0 ? this.processLines([rest]) : [];
|
|
87
|
+
const dangling = this.dispatch();
|
|
88
|
+
if (dangling) frames.push(dangling);
|
|
89
|
+
return frames;
|
|
55
90
|
}
|
|
56
91
|
processLines(lines) {
|
|
57
|
-
const
|
|
92
|
+
const frames = [];
|
|
58
93
|
for (const rawLine of lines) {
|
|
59
94
|
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
60
|
-
if (line.startsWith(":")) continue;
|
|
61
95
|
if (line === "") {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
this.currentEvent = {};
|
|
96
|
+
const frame = this.dispatch();
|
|
97
|
+
if (frame) frames.push(frame);
|
|
65
98
|
continue;
|
|
66
99
|
}
|
|
67
|
-
if (line.startsWith("
|
|
68
|
-
|
|
100
|
+
if (line.startsWith(":")) continue;
|
|
101
|
+
if (line.startsWith("data:")) {
|
|
102
|
+
let value = line.slice(5);
|
|
103
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
104
|
+
if (this.frameDataLines === null) this.frameDataLines = [];
|
|
105
|
+
this.frameDataLines.push(value);
|
|
69
106
|
continue;
|
|
70
107
|
}
|
|
71
108
|
if (line.startsWith("event:")) {
|
|
72
|
-
this.
|
|
109
|
+
this.frameEvent = line.slice(6).trim();
|
|
73
110
|
continue;
|
|
74
111
|
}
|
|
75
|
-
if (line.startsWith("
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
112
|
+
if (line.startsWith("id:")) this.frameId = line.slice(3).trim();
|
|
113
|
+
}
|
|
114
|
+
return frames;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Dispatch the accumulated frame. A frame with no `data:` field (an
|
|
118
|
+
* `event:`/`id:`-only frame, or a bare comment) produces nothing, and
|
|
119
|
+
* its field state is consumed and reset either way.
|
|
120
|
+
*/
|
|
121
|
+
dispatch() {
|
|
122
|
+
const dataLines = this.frameDataLines;
|
|
123
|
+
const frame = dataLines ? {
|
|
124
|
+
event: detached(this.frameEvent),
|
|
125
|
+
data: dataLines.join("\n"),
|
|
126
|
+
id: this.frameId ? detached(this.frameId) : void 0
|
|
127
|
+
} : null;
|
|
128
|
+
this.frameEvent = "";
|
|
129
|
+
this.frameId = void 0;
|
|
130
|
+
this.frameDataLines = null;
|
|
131
|
+
return frame;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Serialize one frame to its wire form. Multi-line `data` becomes one
|
|
136
|
+
* `data:` line per source line so the payload survives the trip without
|
|
137
|
+
* premature frame dispatch.
|
|
138
|
+
*/
|
|
139
|
+
function encodeSSEFrame(frame) {
|
|
140
|
+
let out = "";
|
|
141
|
+
if (frame.event !== void 0) out += `event: ${frame.event}\n`;
|
|
142
|
+
if (frame.id !== void 0) out += `id: ${frame.id}\n`;
|
|
143
|
+
for (const line of frame.data.split("\n")) out += `data: ${line}\n`;
|
|
144
|
+
return `${out}\n`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read a streaming SSE response body into parsed frames.
|
|
148
|
+
*
|
|
149
|
+
* @throws `TypeError` when the body is missing.
|
|
150
|
+
* @throws `DOMException` with `name === "AbortError"` on cancellation —
|
|
151
|
+
* including the WHATWG-fetch variant where an aborted body resolves
|
|
152
|
+
* `done: true` instead of rejecting the read.
|
|
153
|
+
*/
|
|
154
|
+
async function* streamSSEFrames(body, options) {
|
|
155
|
+
if (!body) throw new TypeError("SSE stream has no response body");
|
|
156
|
+
const reader = body.getReader();
|
|
157
|
+
const decoder = new TextDecoder();
|
|
158
|
+
const parser = new SSEFrameParser();
|
|
159
|
+
const { signal, idleMs } = options ?? {};
|
|
160
|
+
const read = () => {
|
|
161
|
+
const limit = idleMs?.();
|
|
162
|
+
if (limit === void 0) return reader.read();
|
|
163
|
+
let timer;
|
|
164
|
+
return Promise.race([reader.read(), new Promise((_, reject) => {
|
|
165
|
+
timer = setTimeout(() => reject(new SSEStreamSilentError(limit)), limit);
|
|
166
|
+
})]).finally(() => clearTimeout(timer));
|
|
167
|
+
};
|
|
168
|
+
let completed = false;
|
|
169
|
+
try {
|
|
170
|
+
for (;;) {
|
|
171
|
+
if (signal?.aborted) throw new DOMException("SSE stream aborted", "AbortError");
|
|
172
|
+
const { done, value } = await read();
|
|
173
|
+
if (done) {
|
|
174
|
+
if (signal?.aborted) throw new DOMException("SSE stream aborted", "AbortError");
|
|
175
|
+
break;
|
|
79
176
|
}
|
|
177
|
+
for (const frame of parser.push(decoder.decode(value, { stream: true }))) yield frame;
|
|
178
|
+
}
|
|
179
|
+
for (const frame of parser.push(decoder.decode())) yield frame;
|
|
180
|
+
for (const frame of parser.flush()) yield frame;
|
|
181
|
+
completed = true;
|
|
182
|
+
} finally {
|
|
183
|
+
if (!completed) await reader.cancel().catch(() => void 0);
|
|
184
|
+
reader.releaseLock();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Incremental JSON event parser for chunked streams, framed by
|
|
189
|
+
* {@link SSEFrameParser}. Feed decoded string chunks via push(); call flush()
|
|
190
|
+
* on stream end. Frames whose trimmed data is empty produce no event.
|
|
191
|
+
*/
|
|
192
|
+
var SSEChunkParser = class {
|
|
193
|
+
frames = new SSEFrameParser();
|
|
194
|
+
options;
|
|
195
|
+
constructor(options = {}) {
|
|
196
|
+
this.options = options;
|
|
197
|
+
}
|
|
198
|
+
push(chunk) {
|
|
199
|
+
return this.toEvents(this.frames.push(chunk));
|
|
200
|
+
}
|
|
201
|
+
flush() {
|
|
202
|
+
return this.toEvents(this.frames.flush());
|
|
203
|
+
}
|
|
204
|
+
toEvents(frames) {
|
|
205
|
+
const events = [];
|
|
206
|
+
for (const frame of frames) {
|
|
207
|
+
const event = this.toEvent(frame);
|
|
208
|
+
if (event) events.push(event);
|
|
80
209
|
}
|
|
81
210
|
return events;
|
|
82
211
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const rawData = this.currentEvent.data.trim();
|
|
212
|
+
toEvent(frame) {
|
|
213
|
+
const rawData = frame.data.trim();
|
|
86
214
|
if (!rawData) return null;
|
|
87
215
|
let data;
|
|
88
216
|
if (this.options.transform) {
|
|
@@ -101,8 +229,8 @@ var SSEChunkParser = class {
|
|
|
101
229
|
return {
|
|
102
230
|
data,
|
|
103
231
|
rawData,
|
|
104
|
-
eventId:
|
|
105
|
-
eventType:
|
|
232
|
+
eventId: frame.id,
|
|
233
|
+
eventType: frame.event || void 0
|
|
106
234
|
};
|
|
107
235
|
}
|
|
108
236
|
};
|
|
@@ -297,7 +425,6 @@ function createUsageCallback(forward) {
|
|
|
297
425
|
extractor
|
|
298
426
|
};
|
|
299
427
|
}
|
|
300
|
-
|
|
301
|
-
export { SSEChunkParser, StreamUsageExtractor, consumeSSEStream, createStreamUsageExtractor, createUsageCallback, parseSSEData, parseSSEStream };
|
|
428
|
+
export { SSEChunkParser, SSEFrameParser, SSEStreamSilentError, StreamUsageExtractor, consumeSSEStream, createStreamUsageExtractor, createUsageCallback, encodeSSEFrame, parseSSEData, parseSSEStream, streamSSEFrames };
|
|
302
429
|
|
|
303
430
|
//# sourceMappingURL=index.js.map
|