@songfreedom/sse-client 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +301 -2
- package/batch.d.ts +39 -0
- package/batch.d.ts.map +1 -0
- package/batch.js +44 -0
- package/batch.js.map +1 -0
- package/client.d.ts +197 -0
- package/client.d.ts.map +1 -0
- package/client.js +625 -0
- package/client.js.map +1 -0
- package/errors.d.ts +33 -0
- package/errors.d.ts.map +1 -0
- package/errors.js +47 -0
- package/errors.js.map +1 -0
- package/index.d.ts +13 -0
- package/index.d.ts.map +1 -0
- package/index.js +7 -0
- package/index.js.map +1 -0
- package/package.json +47 -3
- package/parser.d.ts +35 -0
- package/parser.d.ts.map +1 -0
- package/parser.js +118 -0
- package/parser.js.map +1 -0
- package/share.d.ts +29 -0
- package/share.d.ts.map +1 -0
- package/share.js +67 -0
- package/share.js.map +1 -0
- package/stream.d.ts +24 -0
- package/stream.d.ts.map +1 -0
- package/stream.js +65 -0
- package/stream.js.map +1 -0
- package/types.d.ts +22 -0
- package/types.d.ts.map +1 -0
- package/types.js +2 -0
- package/types.js.map +1 -0
- package/vue.d.ts +45 -0
- package/vue.d.ts.map +1 -0
- package/vue.js +72 -0
- package/vue.js.map +1 -0
package/errors.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error classification, useful for logging/telemetry and for branching inside a `retry` policy:
|
|
3
|
+
* - `http`: the server responded with a non-2xx status
|
|
4
|
+
* - `network`: transport-level failure (DNS, connection reset, …)
|
|
5
|
+
* - `heartbeat`: no bytes received for `idleTimeout`, the connection looks zombie
|
|
6
|
+
* - `protocol`: the response violates the SSE contract (wrong content-type, buffer overflow)
|
|
7
|
+
* - `aborted`: the attempt was interrupted (hidden page, offline, manual close)
|
|
8
|
+
* - `retry-exhausted`: retries ran out
|
|
9
|
+
*/
|
|
10
|
+
export type SSEErrorKind = "http" | "network" | "heartbeat" | "protocol" | "aborted" | "retry-exhausted";
|
|
11
|
+
/**
|
|
12
|
+
* Connection error. Carries a `retryable` flag so callers (and the built-in retry
|
|
13
|
+
* policy) can tell transient failures apart from terminal ones such as 401.
|
|
14
|
+
*/
|
|
15
|
+
export declare class SSEError extends Error {
|
|
16
|
+
/** Error category. */
|
|
17
|
+
readonly kind: SSEErrorKind;
|
|
18
|
+
/** HTTP status code, only set when `kind === "http"`. */
|
|
19
|
+
readonly status?: number;
|
|
20
|
+
/** Whether the connection may be retried. */
|
|
21
|
+
readonly retryable: boolean;
|
|
22
|
+
constructor(message: string, options?: {
|
|
23
|
+
kind?: SSEErrorKind;
|
|
24
|
+
status?: number;
|
|
25
|
+
retryable?: boolean;
|
|
26
|
+
cause?: unknown;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Statuses that indicate a temporary server-side problem and are worth retrying. */
|
|
30
|
+
export declare function isRetryableStatus(status: number): boolean;
|
|
31
|
+
/** Normalize any thrown value into an `SSEError`. */
|
|
32
|
+
export declare function toSSEError(error: unknown): SSEError;
|
|
33
|
+
//# sourceMappingURL=errors.d.ts.map
|
package/errors.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,SAAS,GACT,WAAW,GACX,UAAU,GACV,SAAS,GACT,iBAAiB,CAAC;AAEtB;;;GAGG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,sBAAsB;IACtB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;gBAG1B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,IAAI,CAAC,EAAE,YAAY,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAW/F;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAkBnD"}
|
package/errors.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection error. Carries a `retryable` flag so callers (and the built-in retry
|
|
3
|
+
* policy) can tell transient failures apart from terminal ones such as 401.
|
|
4
|
+
*/
|
|
5
|
+
export class SSEError extends Error {
|
|
6
|
+
/** Error category. */
|
|
7
|
+
kind;
|
|
8
|
+
/** HTTP status code, only set when `kind === "http"`. */
|
|
9
|
+
status;
|
|
10
|
+
/** Whether the connection may be retried. */
|
|
11
|
+
retryable;
|
|
12
|
+
constructor(message, options = {}) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "SSEError";
|
|
15
|
+
this.kind = options.kind ?? "network";
|
|
16
|
+
this.status = options.status;
|
|
17
|
+
this.retryable = options.retryable ?? true;
|
|
18
|
+
if (options.cause !== undefined) {
|
|
19
|
+
this.cause = options.cause;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Statuses that indicate a temporary server-side problem and are worth retrying. */
|
|
24
|
+
export function isRetryableStatus(status) {
|
|
25
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
26
|
+
}
|
|
27
|
+
/** Normalize any thrown value into an `SSEError`. */
|
|
28
|
+
export function toSSEError(error) {
|
|
29
|
+
if (error instanceof SSEError) {
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
33
|
+
return new SSEError("SSE connection aborted", {
|
|
34
|
+
kind: "aborted",
|
|
35
|
+
retryable: false,
|
|
36
|
+
cause: error,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
// Transport failures (DNS errors, reset connections) are retryable by default.
|
|
41
|
+
return new SSEError(`SSE connection error: ${message}`, {
|
|
42
|
+
kind: "network",
|
|
43
|
+
retryable: true,
|
|
44
|
+
cause: error,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=errors.js.map
|
package/errors.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAiBA;;;GAGG;AACH,MAAM,OAAO,QAAS,SAAQ,KAAK;IACjC,sBAAsB;IACb,IAAI,CAAe;IAC5B,yDAAyD;IAChD,MAAM,CAAU;IACzB,6CAA6C;IACpC,SAAS,CAAU;IAE5B,YACE,OAAe,EACf,UAA0F,EAAE;QAE5F,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,SAAS,CAAC;QACtC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC;QAC3C,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC/B,IAA4B,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QACtD,CAAC;IACH,CAAC;CACF;AAED,qFAAqF;AACrF,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,OAAO,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,IAAI,GAAG,CAAC;AAC7E,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAC1D,OAAO,IAAI,QAAQ,CAAC,wBAAwB,EAAE;YAC5C,IAAI,EAAE,SAAS;YACf,SAAS,EAAE,KAAK;YAChB,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;IACL,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,+EAA+E;IAC/E,OAAO,IAAI,QAAQ,CAAC,yBAAyB,OAAO,EAAE,EAAE;QACtD,IAAI,EAAE,SAAS;QACf,SAAS,EAAE,IAAI;QACf,KAAK,EAAE,KAAK;KACb,CAAC,CAAC;AACL,CAAC"}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { SSEClient } from "./client.js";
|
|
2
|
+
export type { FetchLike, RetryContext, RetryPolicy, SSEClientOptions } from "./client.js";
|
|
3
|
+
export { SSEError, isRetryableStatus, toSSEError } from "./errors.js";
|
|
4
|
+
export { createSSEParser } from "./parser.js";
|
|
5
|
+
export type { ParsedEvent, SSEParser, SSEParserOptions } from "./parser.js";
|
|
6
|
+
export { createBatcher } from "./batch.js";
|
|
7
|
+
export type { Batcher, BatcherOptions } from "./batch.js";
|
|
8
|
+
export { createSSEMessageStream } from "./stream.js";
|
|
9
|
+
export type { SSEMessageStreamOptions } from "./stream.js";
|
|
10
|
+
export { acquireSSEClient, closeAllSharedSSEClients, getSharedSSEClientCount, } from "./share.js";
|
|
11
|
+
export type { SharedSSEHandle } from "./share.js";
|
|
12
|
+
export type { SSECloseReason, SSEEventMap, SSEMessage, SSEStatus, Unsubscribe } from "./types.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE1F,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE5E,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,YAAY,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
|
package/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { SSEClient } from "./client.js";
|
|
2
|
+
export { SSEError, isRetryableStatus, toSSEError } from "./errors.js";
|
|
3
|
+
export { createSSEParser } from "./parser.js";
|
|
4
|
+
export { createBatcher } from "./batch.js";
|
|
5
|
+
export { createSSEMessageStream } from "./stream.js";
|
|
6
|
+
export { acquireSSEClient, closeAllSharedSSEClients, getSharedSSEClientCount, } from "./share.js";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
package/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGxC,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAGrD,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,YAAY,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@songfreedom/sse-client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Dependency-free Server-Sent Events client: fetch transport, typed event routing, reconnect with Last-Event-ID resume, idle watchdog and shared connections",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sse",
|
|
7
|
+
"server-sent-events",
|
|
8
|
+
"eventsource",
|
|
9
|
+
"fetch",
|
|
10
|
+
"stream",
|
|
11
|
+
"reconnect",
|
|
12
|
+
"vue",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://gitea.meos.center/Persagy-AI/meos-ai-target-web.git",
|
|
19
|
+
"directory": "packages/sse-client"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"main": "./index.js",
|
|
27
|
+
"types": "./index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./index.d.ts",
|
|
31
|
+
"default": "./index.js"
|
|
32
|
+
},
|
|
33
|
+
"./vue": {
|
|
34
|
+
"types": "./vue.d.ts",
|
|
35
|
+
"default": "./vue.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"vue": "^3.3.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"vue": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"registry": "https://registry.npmjs.org/",
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
6
50
|
}
|
package/parser.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** A parsed event, before any payload decoding. */
|
|
2
|
+
export interface ParsedEvent {
|
|
3
|
+
/** Event id; empty string when the server did not send one. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Event name; empty string when the server did not send one (callers default to "message"). */
|
|
6
|
+
event: string;
|
|
7
|
+
/** Raw payload text; multi-line `data:` fields are joined with a newline. */
|
|
8
|
+
data: string;
|
|
9
|
+
}
|
|
10
|
+
/** Default cap for a single message buffer: 1MB. */
|
|
11
|
+
export declare const DEFAULT_MAX_BUFFER_SIZE: number;
|
|
12
|
+
export interface SSEParserOptions {
|
|
13
|
+
onEvent?: (event: ParsedEvent) => void;
|
|
14
|
+
/** Reconnect delay requested by the server through a `retry:` field, in milliseconds. */
|
|
15
|
+
onRetry?: (delay: number) => void;
|
|
16
|
+
/** Buffer cap in characters (default 1MB). Once exceeded, parsing stops and `overflowed` flips to true. */
|
|
17
|
+
maxBufferSize?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface SSEParser {
|
|
20
|
+
/** Feed decoded text. Chunks may split lines or fields arbitrarily; partial input is buffered. */
|
|
21
|
+
feed: (chunk: string) => void;
|
|
22
|
+
/** Drop any partial line and per-connection `id`/`event` state (called before a reconnect). */
|
|
23
|
+
reset: () => void;
|
|
24
|
+
/** Whether the buffer cap was hit (server never terminates a line, or a single message is huge). */
|
|
25
|
+
readonly overflowed: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create an SSE frame parser (a subset of the WHATWG spec).
|
|
29
|
+
*
|
|
30
|
+
* Handles the `data` / `event` / `id` / `retry` fields, multi-line payloads,
|
|
31
|
+
* comment lines (`: keep-alive` heartbeats), mixed CRLF/LF, and arbitrarily
|
|
32
|
+
* chunked input.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createSSEParser(options?: SSEParserOptions): SSEParser;
|
|
35
|
+
//# sourceMappingURL=parser.d.ts.map
|
package/parser.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,MAAM,WAAW,WAAW;IAC1B,+DAA+D;IAC/D,EAAE,EAAE,MAAM,CAAC;IACX,gGAAgG;IAChG,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,oDAAoD;AACpD,eAAO,MAAM,uBAAuB,QAAc,CAAC;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,yFAAyF;IACzF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,2GAA2G;IAC3G,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,kGAAkG;IAClG,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,+FAA+F;IAC/F,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,oGAAoG;IACpG,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAID;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS,CAgHzE"}
|
package/parser.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Default cap for a single message buffer: 1MB. */
|
|
2
|
+
export const DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024;
|
|
3
|
+
const LINE_BREAK = /\r\n|\r|\n/;
|
|
4
|
+
/**
|
|
5
|
+
* Create an SSE frame parser (a subset of the WHATWG spec).
|
|
6
|
+
*
|
|
7
|
+
* Handles the `data` / `event` / `id` / `retry` fields, multi-line payloads,
|
|
8
|
+
* comment lines (`: keep-alive` heartbeats), mixed CRLF/LF, and arbitrarily
|
|
9
|
+
* chunked input.
|
|
10
|
+
*/
|
|
11
|
+
export function createSSEParser(options = {}) {
|
|
12
|
+
const maxBufferSize = options.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
|
|
13
|
+
let buffer = "";
|
|
14
|
+
let isFirstChunk = true;
|
|
15
|
+
let dataLines = [];
|
|
16
|
+
let eventType = "";
|
|
17
|
+
let lastId = "";
|
|
18
|
+
let overflowed = false;
|
|
19
|
+
const dispatch = () => {
|
|
20
|
+
if (!dataLines.length) {
|
|
21
|
+
// Per spec, an event without `data:` is discarded.
|
|
22
|
+
eventType = "";
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const event = {
|
|
26
|
+
id: lastId,
|
|
27
|
+
event: eventType,
|
|
28
|
+
data: dataLines.join("\n"),
|
|
29
|
+
};
|
|
30
|
+
dataLines = [];
|
|
31
|
+
eventType = "";
|
|
32
|
+
options.onEvent?.(event);
|
|
33
|
+
};
|
|
34
|
+
const handleLine = (line) => {
|
|
35
|
+
// An empty line terminates the current event.
|
|
36
|
+
if (!line) {
|
|
37
|
+
dispatch();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Lines starting with a colon are comments (usually heartbeats); ignore them.
|
|
41
|
+
if (line.startsWith(":")) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const colonIndex = line.indexOf(":");
|
|
45
|
+
const field = colonIndex === -1 ? line : line.slice(0, colonIndex);
|
|
46
|
+
let value = colonIndex === -1 ? "" : line.slice(colonIndex + 1);
|
|
47
|
+
// The spec allows exactly one leading space in the value.
|
|
48
|
+
if (value.startsWith(" ")) {
|
|
49
|
+
value = value.slice(1);
|
|
50
|
+
}
|
|
51
|
+
switch (field) {
|
|
52
|
+
case "data":
|
|
53
|
+
dataLines.push(value);
|
|
54
|
+
break;
|
|
55
|
+
case "event":
|
|
56
|
+
eventType = value;
|
|
57
|
+
break;
|
|
58
|
+
case "id":
|
|
59
|
+
if (!value.includes("\0")) {
|
|
60
|
+
lastId = value;
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
case "retry":
|
|
64
|
+
if (/^\d+$/.test(value)) {
|
|
65
|
+
options.onRetry?.(Number(value));
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
get overflowed() {
|
|
74
|
+
return overflowed;
|
|
75
|
+
},
|
|
76
|
+
feed(chunk) {
|
|
77
|
+
if (!chunk || overflowed) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Strip a UTF-8 BOM from the first chunk.
|
|
81
|
+
if (isFirstChunk) {
|
|
82
|
+
isFirstChunk = false;
|
|
83
|
+
buffer = chunk.charCodeAt(0) === 0xfeff ? chunk.slice(1) : chunk;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
buffer += chunk;
|
|
87
|
+
}
|
|
88
|
+
for (;;) {
|
|
89
|
+
const match = LINE_BREAK.exec(buffer);
|
|
90
|
+
if (!match) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
// Wait for the next chunk when the buffer ends with a lone \r, so that
|
|
94
|
+
// a split "\r\n" is not treated as two line breaks.
|
|
95
|
+
if (match[0] === "\r" && match.index === buffer.length - 1) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
handleLine(buffer.slice(0, match.index));
|
|
99
|
+
buffer = buffer.slice(match.index + match[0].length);
|
|
100
|
+
}
|
|
101
|
+
// No line break for far too long means the payload is not valid SSE:
|
|
102
|
+
// stop early instead of growing the buffer until the process dies.
|
|
103
|
+
if (!overflowed && buffer.length > maxBufferSize) {
|
|
104
|
+
overflowed = true;
|
|
105
|
+
buffer = "";
|
|
106
|
+
dataLines = [];
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
reset() {
|
|
110
|
+
buffer = "";
|
|
111
|
+
dataLines = [];
|
|
112
|
+
eventType = "";
|
|
113
|
+
lastId = "";
|
|
114
|
+
overflowed = false;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=parser.js.map
|
package/parser.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"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"}
|
package/share.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SSEClient } from "./client.js";
|
|
2
|
+
import type { SSEEventMap } from "./types.js";
|
|
3
|
+
/** Handle for a shared connection. */
|
|
4
|
+
export interface SharedSSEHandle<E extends object = SSEEventMap> {
|
|
5
|
+
client: SSEClient<E>;
|
|
6
|
+
/** Release one reference; the connection closes when the last reference is released. */
|
|
7
|
+
release: () => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Share a connection by key, using reference counting.
|
|
11
|
+
*
|
|
12
|
+
* When several components consume the same subscription they should not open one
|
|
13
|
+
* connection each: HTTP/1.1 allows only six per origin, so push channels can
|
|
14
|
+
* starve regular requests, and the server would receive duplicate subscriptions.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const { client, release } = acquireSSEClient("alarm", () => new SSEClient({ url, body }));
|
|
18
|
+
* onScopeDispose(release);
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* The options of the first caller win; later calls with the same key reuse that
|
|
22
|
+
* connection and ignore their own options.
|
|
23
|
+
*/
|
|
24
|
+
export declare function acquireSSEClient<E extends object = SSEEventMap>(key: string, create: () => SSEClient<E>): SharedSSEHandle<E>;
|
|
25
|
+
/** Close and drop every shared connection (mainly for tests and teardown). */
|
|
26
|
+
export declare function closeAllSharedSSEClients(): void;
|
|
27
|
+
/** Number of live shared connections, useful for tests and diagnostics. */
|
|
28
|
+
export declare function getSharedSSEClientCount(): number;
|
|
29
|
+
//# sourceMappingURL=share.d.ts.map
|
package/share.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,sCAAsC;AACtC,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW;IAC7D,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACrB,wFAAwF;IACxF,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AASD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW,EAC7D,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,GACzB,eAAe,CAAC,CAAC,CAAC,CAepB;AAED,8EAA8E;AAC9E,wBAAgB,wBAAwB,IAAI,IAAI,CAK/C;AAED,2EAA2E;AAC3E,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD"}
|
package/share.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const registry = new Map();
|
|
2
|
+
/**
|
|
3
|
+
* Share a connection by key, using reference counting.
|
|
4
|
+
*
|
|
5
|
+
* When several components consume the same subscription they should not open one
|
|
6
|
+
* connection each: HTTP/1.1 allows only six per origin, so push channels can
|
|
7
|
+
* starve regular requests, and the server would receive duplicate subscriptions.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const { client, release } = acquireSSEClient("alarm", () => new SSEClient({ url, body }));
|
|
11
|
+
* onScopeDispose(release);
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The options of the first caller win; later calls with the same key reuse that
|
|
15
|
+
* connection and ignore their own options.
|
|
16
|
+
*/
|
|
17
|
+
export function acquireSSEClient(key, create) {
|
|
18
|
+
const existing = registry.get(key);
|
|
19
|
+
if (existing) {
|
|
20
|
+
existing.refs += 1;
|
|
21
|
+
return {
|
|
22
|
+
client: existing.client,
|
|
23
|
+
release: once(() => release(key)),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const client = create();
|
|
27
|
+
registry.set(key, { client: client, refs: 1 });
|
|
28
|
+
return {
|
|
29
|
+
client,
|
|
30
|
+
release: once(() => release(key)),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Close and drop every shared connection (mainly for tests and teardown). */
|
|
34
|
+
export function closeAllSharedSSEClients() {
|
|
35
|
+
for (const entry of [...registry.values()]) {
|
|
36
|
+
entry.client.close();
|
|
37
|
+
}
|
|
38
|
+
registry.clear();
|
|
39
|
+
}
|
|
40
|
+
/** Number of live shared connections, useful for tests and diagnostics. */
|
|
41
|
+
export function getSharedSSEClientCount() {
|
|
42
|
+
return registry.size;
|
|
43
|
+
}
|
|
44
|
+
function release(key) {
|
|
45
|
+
const entry = registry.get(key);
|
|
46
|
+
if (!entry) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
entry.refs -= 1;
|
|
50
|
+
if (entry.refs > 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
registry.delete(key);
|
|
54
|
+
entry.client.close();
|
|
55
|
+
}
|
|
56
|
+
/** Make `release` idempotent so a double call cannot free someone else's reference. */
|
|
57
|
+
function once(fn) {
|
|
58
|
+
let called = false;
|
|
59
|
+
return () => {
|
|
60
|
+
if (called) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
called = true;
|
|
64
|
+
fn();
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=share.js.map
|
package/share.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share.js","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AAeA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAuB,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAW,EACX,MAA0B;IAE1B,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,QAAQ,EAAE,CAAC;QACb,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC;QACnB,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAiC;YAClD,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;SAClC,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,MAAsC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/E,OAAO;QACL,MAAM;QACN,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,wBAAwB;IACtC,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;QAC3C,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IACD,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,uBAAuB;IACrC,OAAO,QAAQ,CAAC,IAAI,CAAC;AACvB,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;IACT,CAAC;IACD,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;IAChB,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO;IACT,CAAC;IACD,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;AACvB,CAAC;AAED,uFAAuF;AACvF,SAAS,IAAI,CAAC,EAAc;IAC1B,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,OAAO,GAAG,EAAE;QACV,IAAI,MAAM,EAAE,CAAC;YACX,OAAO;QACT,CAAC;QACD,MAAM,GAAG,IAAI,CAAC;QACd,EAAE,EAAE,CAAC;IACP,CAAC,CAAC;AACJ,CAAC"}
|
package/stream.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SSEMessage } from "./types.js";
|
|
2
|
+
export interface SSEMessageStreamOptions<T = unknown> {
|
|
3
|
+
/** Payload decoder, e.g. `(raw) => JSON.parse(raw)`. */
|
|
4
|
+
parse?: (raw: string) => T;
|
|
5
|
+
/** Buffer cap in characters for a single message (default 1MB). */
|
|
6
|
+
maxBufferSize?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Turn a byte stream into a message stream using Web Streams composition —
|
|
10
|
+
* the same shape as AI SDK's `parseJsonEventStream`.
|
|
11
|
+
*
|
|
12
|
+
* Useful when `SSEClient` is not involved: custom transports, Service Workers,
|
|
13
|
+
* edge runtimes. A parse failure (buffer overflow) errors the stream and the
|
|
14
|
+
* consumer decides whether to retry.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const response = await fetch(url, { headers });
|
|
18
|
+
* const reader = response.body!
|
|
19
|
+
* .pipeThrough(createSSEMessageStream({ parse: JSON.parse }))
|
|
20
|
+
* .getReader();
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function createSSEMessageStream<T = unknown>(options?: SSEMessageStreamOptions<T>): TransformStream<Uint8Array, SSEMessage<T>>;
|
|
24
|
+
//# sourceMappingURL=stream.d.ts.map
|
package/stream.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,WAAW,uBAAuB,CAAC,CAAC,GAAG,OAAO;IAClD,wDAAwD;IACxD,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,CAAC;IAC3B,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,GAAG,OAAO,EAChD,OAAO,GAAE,uBAAuB,CAAC,CAAC,CAAM,GACvC,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CA+C5C"}
|
package/stream.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { SSEError } from "./errors.js";
|
|
2
|
+
import { createSSEParser } from "./parser.js";
|
|
3
|
+
/**
|
|
4
|
+
* Turn a byte stream into a message stream using Web Streams composition —
|
|
5
|
+
* the same shape as AI SDK's `parseJsonEventStream`.
|
|
6
|
+
*
|
|
7
|
+
* Useful when `SSEClient` is not involved: custom transports, Service Workers,
|
|
8
|
+
* edge runtimes. A parse failure (buffer overflow) errors the stream and the
|
|
9
|
+
* consumer decides whether to retry.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const response = await fetch(url, { headers });
|
|
13
|
+
* const reader = response.body!
|
|
14
|
+
* .pipeThrough(createSSEMessageStream({ parse: JSON.parse }))
|
|
15
|
+
* .getReader();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function createSSEMessageStream(options = {}) {
|
|
19
|
+
const decoder = new TextDecoder("utf-8");
|
|
20
|
+
let controller;
|
|
21
|
+
const parser = createSSEParser({
|
|
22
|
+
maxBufferSize: options.maxBufferSize,
|
|
23
|
+
onEvent: (event) => {
|
|
24
|
+
let data = event.data;
|
|
25
|
+
if (options.parse) {
|
|
26
|
+
try {
|
|
27
|
+
data = options.parse(event.data);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// Drop only this message; the stream keeps flowing.
|
|
31
|
+
console.error("[sse-client] failed to parse message, skipping it", error);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
controller.enqueue({
|
|
36
|
+
id: event.id,
|
|
37
|
+
event: event.event || "message",
|
|
38
|
+
data: data,
|
|
39
|
+
raw: event.data,
|
|
40
|
+
receivedAt: Date.now(),
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
onRetry: () => {
|
|
44
|
+
// The plain message stream does not reconnect; `retry:` is handled by SSEClient.
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
return new TransformStream({
|
|
48
|
+
start(streamController) {
|
|
49
|
+
controller = streamController;
|
|
50
|
+
},
|
|
51
|
+
transform(chunk) {
|
|
52
|
+
parser.feed(decoder.decode(chunk, { stream: true }));
|
|
53
|
+
if (parser.overflowed) {
|
|
54
|
+
throw new SSEError("SSE buffer size limit exceeded", {
|
|
55
|
+
kind: "protocol",
|
|
56
|
+
retryable: false,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
flush() {
|
|
61
|
+
parser.feed(decoder.decode());
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=stream.js.map
|
package/stream.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAU9C;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,sBAAsB,CACpC,UAAsC,EAAE;IAExC,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,UAA2D,CAAC;IAEhE,MAAM,MAAM,GAAG,eAAe,CAAC;QAC7B,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACjB,IAAI,IAAI,GAAY,KAAK,CAAC,IAAI,CAAC;YAC/B,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;gBAClB,IAAI,CAAC;oBACH,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACnC,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,oDAAoD;oBACpD,OAAO,CAAC,KAAK,CAAC,mDAAmD,EAAE,KAAK,CAAC,CAAC;oBAC1E,OAAO;gBACT,CAAC;YACH,CAAC;YACD,UAAU,CAAC,OAAO,CAAC;gBACjB,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,SAAS;gBAC/B,IAAI,EAAE,IAAS;gBACf,GAAG,EAAE,KAAK,CAAC,IAAI;gBACf,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;aACvB,CAAC,CAAC;QACL,CAAC;QACD,OAAO,EAAE,GAAG,EAAE;YACZ,iFAAiF;QACnF,CAAC;KACF,CAAC,CAAC;IAEH,OAAO,IAAI,eAAe,CAA4B;QACpD,KAAK,CAAC,gBAAgB;YACpB,UAAU,GAAG,gBAAgB,CAAC;QAChC,CAAC;QACD,SAAS,CAAC,KAAK;YACb,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;YACrD,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;gBACtB,MAAM,IAAI,QAAQ,CAAC,gCAAgC,EAAE;oBACnD,IAAI,EAAE,UAAU;oBAChB,SAAS,EAAE,KAAK;iBACjB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,KAAK;YACH,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAChC,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** A single Server-Sent Event message. */
|
|
2
|
+
export interface SSEMessage<T = unknown> {
|
|
3
|
+
/** Event id. Falls back to the previous id when the server omits it, used for Last-Event-ID resume. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Event name; defaults to "message" when the server does not send an `event:` field. */
|
|
6
|
+
event: string;
|
|
7
|
+
/** Parsed payload. Raw string by default, or whatever `parse` returns. */
|
|
8
|
+
data: T;
|
|
9
|
+
/** Raw `data:` text (multi-line payloads joined with a newline), useful for debugging. */
|
|
10
|
+
raw: string;
|
|
11
|
+
/** Client-side receive timestamp, handy for staleness checks. */
|
|
12
|
+
receivedAt: number;
|
|
13
|
+
}
|
|
14
|
+
/** Connection state machine: idle → connecting → open → reconnecting → closed. */
|
|
15
|
+
export type SSEStatus = "idle" | "connecting" | "open" | "reconnecting" | "closed";
|
|
16
|
+
/** Why the connection ended. */
|
|
17
|
+
export type SSECloseReason = "user" | "end" | "error";
|
|
18
|
+
/** Event-name → payload map, used to infer handler argument types. */
|
|
19
|
+
export type SSEEventMap = Record<string, unknown>;
|
|
20
|
+
/** Unsubscribe function returned by every subscription helper. */
|
|
21
|
+
export type Unsubscribe = () => void;
|
|
22
|
+
//# sourceMappingURL=types.d.ts.map
|
package/types.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,OAAO;IACrC,uGAAuG;IACvG,EAAE,EAAE,MAAM,CAAC;IACX,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,IAAI,EAAE,CAAC,CAAC;IACR,0FAA0F;IAC1F,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,kFAAkF;AAClF,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;AAEnF,gCAAgC;AAChC,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;AAEtD,sEAAsE;AACtE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElD,kEAAkE;AAClE,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC"}
|
package/types.js
ADDED
package/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/vue.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Ref, type ShallowRef } from "vue";
|
|
2
|
+
import { SSEClient, type SSEClientOptions } from "./client.js";
|
|
3
|
+
import type { SSEError } from "./errors.js";
|
|
4
|
+
import type { SSEEventMap, SSEMessage, SSEStatus } from "./types.js";
|
|
5
|
+
/** Options for `useSSE`. */
|
|
6
|
+
export interface UseSSEOptions extends SSEClientOptions {
|
|
7
|
+
/** Connect immediately, defaults to true. */
|
|
8
|
+
immediate?: boolean;
|
|
9
|
+
/** How many recent messages to keep, defaults to 200. Pass 0 to keep only the latest one. */
|
|
10
|
+
maxEvents?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Return value of `useSSE`. */
|
|
13
|
+
export interface UseSSEReturn<E extends object = SSEEventMap> {
|
|
14
|
+
/** Underlying client, use it to subscribe to specific event names. */
|
|
15
|
+
client: SSEClient<E>;
|
|
16
|
+
/** Connection state, ready to bind to the UI. */
|
|
17
|
+
status: Ref<SSEStatus>;
|
|
18
|
+
/** Most recent message. */
|
|
19
|
+
lastEvent: ShallowRef<SSEMessage | null>;
|
|
20
|
+
/** Recent messages, kept in a ring buffer. */
|
|
21
|
+
events: ShallowRef<SSEMessage[]>;
|
|
22
|
+
/** Most recent connection error. */
|
|
23
|
+
error: ShallowRef<SSEError | null>;
|
|
24
|
+
start: () => void;
|
|
25
|
+
stop: () => void;
|
|
26
|
+
/** Reconnect while keeping `lastEventId` so the server can resume. */
|
|
27
|
+
restart: () => void;
|
|
28
|
+
/** Subscribe to an event name and unsubscribe automatically when the scope is disposed. */
|
|
29
|
+
on: SSEClient<E>["on"];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Vue composable: auto-reconnect, message ring buffer, cleanup on unmount.
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* interface AlarmEvent { level: "high" | "low"; deviceId: string }
|
|
36
|
+
* const { lastEvent, on, status } = useSSE<{ ALARM: AlarmEvent }>({
|
|
37
|
+
* url: "/api/push/subscribe",
|
|
38
|
+
* body: { topics: ["alarm"] },
|
|
39
|
+
* parse: (raw) => JSON.parse(raw),
|
|
40
|
+
* });
|
|
41
|
+
* on("ALARM", (alarm) => console.log(alarm.level));
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function useSSE<E extends object = SSEEventMap>(options: UseSSEOptions): UseSSEReturn<E>;
|
|
45
|
+
//# sourceMappingURL=vue.d.ts.map
|
package/vue.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vue.d.ts","sourceRoot":"","sources":["../src/vue.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,GAAG,EACR,KAAK,UAAU,EAChB,MAAM,KAAK,CAAC;AACb,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAErE,4BAA4B;AAC5B,MAAM,WAAW,aAAc,SAAQ,gBAAgB;IACrD,6CAA6C;IAC7C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6FAA6F;IAC7F,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,gCAAgC;AAChC,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW;IAC1D,sEAAsE;IACtE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACrB,iDAAiD;IACjD,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,2BAA2B;IAC3B,SAAS,EAAE,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACzC,8CAA8C;IAC9C,MAAM,EAAE,UAAU,CAAC,UAAU,EAAE,CAAC,CAAC;IACjC,oCAAoC;IACpC,KAAK,EAAE,UAAU,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACnC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,sEAAsE;IACtE,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,2FAA2F;IAC3F,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW,EACnD,OAAO,EAAE,aAAa,GACrB,YAAY,CAAC,CAAC,CAAC,CAgEjB"}
|