@songfreedom/sse-client 0.0.0-stage → 0.1.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/errors.js ADDED
@@ -0,0 +1,36 @@
1
+ /** 连接异常,区分「可重试」与「不可重试」,避免 401 之类错误被无限重连 */
2
+ export class SSEError extends Error {
3
+ /** 异常分类 */
4
+ kind;
5
+ /** HTTP 状态码,仅 kind === "http" 时有值 */
6
+ status;
7
+ /** 是否允许重试 */
8
+ retryable;
9
+ constructor(message, options = {}) {
10
+ super(message);
11
+ this.name = "SSEError";
12
+ this.kind = options.kind ?? "network";
13
+ this.status = options.status;
14
+ this.retryable = options.retryable ?? true;
15
+ if (options.cause !== undefined) {
16
+ this.cause = options.cause;
17
+ }
18
+ }
19
+ }
20
+ /** 这些状态码说明服务端暂时不可用,可以重试 */
21
+ export function isRetryableStatus(status) {
22
+ return status === 408 || status === 425 || status === 429 || status >= 500;
23
+ }
24
+ /** 把任意异常归一成 SSEError */
25
+ export function toSSEError(error) {
26
+ if (error instanceof SSEError) {
27
+ return error;
28
+ }
29
+ if (error instanceof Error && error.name === "AbortError") {
30
+ return new SSEError("SSE 连接已中断", { kind: "aborted", retryable: false, cause: error });
31
+ }
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ // 网络层失败(DNS 失败、连接被重置等)默认按可重试处理
34
+ return new SSEError(`SSE 连接异常:${message}`, { kind: "network", retryable: true, cause: error });
35
+ }
36
+ //# 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":"AAWA,2CAA2C;AAC3C,MAAM,OAAO,QAAS,SAAQ,KAAK;IACjC,WAAW;IACF,IAAI,CAAe;IAC5B,qCAAqC;IAC5B,MAAM,CAAU;IACzB,aAAa;IACJ,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,2BAA2B;AAC3B,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,wBAAwB;AACxB,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,WAAW,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACxF,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,+BAA+B;IAC/B,OAAO,IAAI,QAAQ,CAAC,YAAY,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AACjG,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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "零依赖 SSE 客户端:fetch 流式传输、事件名路由、断线重连(Last-Event-ID 续传)、空闲看门狗与连接共享",
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,34 @@
1
+ /** 解析出的一条原始事件(尚未做业务解析) */
2
+ export interface ParsedEvent {
3
+ /** 事件 id;服务端未下发时为空字符串 */
4
+ id: string;
5
+ /** 事件类型;服务端未指定时为空字符串,由调用方兜底成 "message" */
6
+ event: string;
7
+ /** data 原文,多行按规范用 \n 拼接 */
8
+ data: string;
9
+ }
10
+ /** 单条消息缓冲上限默认值:1MB,超过即判定为异常流 */
11
+ export declare const DEFAULT_MAX_BUFFER_SIZE: number;
12
+ export interface SSEParserOptions {
13
+ onEvent?: (event: ParsedEvent) => void;
14
+ /** 服务端通过 `retry:` 字段下发的重连间隔(毫秒) */
15
+ onRetry?: (delay: number) => void;
16
+ /** 缓冲区上限(字符数),默认 1MB;超限后停止解析并置 overflowed */
17
+ maxBufferSize?: number;
18
+ }
19
+ export interface SSEParser {
20
+ /** 喂入一段已解码的文本(可任意切分,跨块的半行/半字段会被缓存) */
21
+ feed: (chunk: string) => void;
22
+ /** 丢弃未完成的半行与当前连接上的 id/event 状态(重连时调用) */
23
+ reset: () => void;
24
+ /** 是否发生过缓冲区溢出(说明服务端从不发空行,或单条消息过大) */
25
+ readonly overflowed: boolean;
26
+ }
27
+ /**
28
+ * 创建 SSE 帧解析器(WHATWG 规范子集)。
29
+ *
30
+ * 支持 `data` / `event` / `id` / `retry` 字段、多行 data 合并、注释行(`: keep-alive` 心跳)、
31
+ * CRLF 与 LF 混用,以及任意粒度的分块输入。
32
+ */
33
+ export declare function createSSEParser(options?: SSEParserOptions): SSEParser;
34
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,MAAM,WAAW,WAAW;IAC1B,yBAAyB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,gCAAgC;AAChC,eAAO,MAAM,uBAAuB,QAAc,CAAC;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,mCAAmC;IACnC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,6CAA6C;IAC7C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,sCAAsC;IACtC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,yCAAyC;IACzC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,qCAAqC;IACrC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS,CA8GzE"}
package/parser.js ADDED
@@ -0,0 +1,115 @@
1
+ /** 单条消息缓冲上限默认值:1MB,超过即判定为异常流 */
2
+ export const DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024;
3
+ const LINE_BREAK = /\r\n|\r|\n/;
4
+ /**
5
+ * 创建 SSE 帧解析器(WHATWG 规范子集)。
6
+ *
7
+ * 支持 `data` / `event` / `id` / `retry` 字段、多行 data 合并、注释行(`: keep-alive` 心跳)、
8
+ * CRLF 与 LF 混用,以及任意粒度的分块输入。
9
+ */
10
+ export function createSSEParser(options = {}) {
11
+ const maxBufferSize = options.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
12
+ let buffer = "";
13
+ let isFirstChunk = true;
14
+ let dataLines = [];
15
+ let eventType = "";
16
+ let lastId = "";
17
+ let overflowed = false;
18
+ const dispatch = () => {
19
+ if (!dataLines.length) {
20
+ // 无 data 的空事件(例如只有 `event:` 字段)按规范丢弃
21
+ eventType = "";
22
+ return;
23
+ }
24
+ const event = {
25
+ id: lastId,
26
+ event: eventType,
27
+ data: dataLines.join("\n"),
28
+ };
29
+ dataLines = [];
30
+ eventType = "";
31
+ options.onEvent?.(event);
32
+ };
33
+ const handleLine = (line) => {
34
+ // 空行表示一个事件结束
35
+ if (!line) {
36
+ dispatch();
37
+ return;
38
+ }
39
+ // 以冒号开头的行是注释(常用于心跳),忽略
40
+ if (line.startsWith(":")) {
41
+ return;
42
+ }
43
+ const colonIndex = line.indexOf(":");
44
+ const field = colonIndex === -1 ? line : line.slice(0, colonIndex);
45
+ let value = colonIndex === -1 ? "" : line.slice(colonIndex + 1);
46
+ // 字段值前最多允许一个空格
47
+ if (value.startsWith(" ")) {
48
+ value = value.slice(1);
49
+ }
50
+ switch (field) {
51
+ case "data":
52
+ dataLines.push(value);
53
+ break;
54
+ case "event":
55
+ eventType = value;
56
+ break;
57
+ case "id":
58
+ if (!value.includes("\0")) {
59
+ lastId = value;
60
+ }
61
+ break;
62
+ case "retry":
63
+ if (/^\d+$/.test(value)) {
64
+ options.onRetry?.(Number(value));
65
+ }
66
+ break;
67
+ default:
68
+ break;
69
+ }
70
+ };
71
+ return {
72
+ get overflowed() {
73
+ return overflowed;
74
+ },
75
+ feed(chunk) {
76
+ if (!chunk || overflowed) {
77
+ return;
78
+ }
79
+ // 首块去掉 UTF-8 BOM
80
+ if (isFirstChunk) {
81
+ isFirstChunk = false;
82
+ buffer = chunk.charCodeAt(0) === 0xfeff ? chunk.slice(1) : chunk;
83
+ }
84
+ else {
85
+ buffer += chunk;
86
+ }
87
+ for (;;) {
88
+ const match = LINE_BREAK.exec(buffer);
89
+ if (!match) {
90
+ break;
91
+ }
92
+ // 缓冲区以独立的 \r 结尾时先等下一块,避免把 "\r\n" 拆成两行
93
+ if (match[0] === "\r" && match.index === buffer.length - 1) {
94
+ break;
95
+ }
96
+ handleLine(buffer.slice(0, match.index));
97
+ buffer = buffer.slice(match.index + match[0].length);
98
+ }
99
+ // 迟迟等不到换行说明数据不符合 SSE 约定,直接止损,避免把内存吃满
100
+ if (!overflowed && buffer.length > maxBufferSize) {
101
+ overflowed = true;
102
+ buffer = "";
103
+ dataLines = [];
104
+ }
105
+ },
106
+ reset() {
107
+ buffer = "";
108
+ dataLines = [];
109
+ eventType = "";
110
+ lastId = "";
111
+ overflowed = false;
112
+ },
113
+ };
114
+ }
115
+ //# 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,26 @@
1
+ import type { SSEClient } from "./client.js";
2
+ import type { SSEEventMap } from "./types.js";
3
+ /** 共享连接的句柄 */
4
+ export interface SharedSSEHandle<E extends object = SSEEventMap> {
5
+ client: SSEClient<E>;
6
+ /** 释放引用;最后一个引用释放时自动关闭连接 */
7
+ release: () => void;
8
+ }
9
+ /**
10
+ * 按 key 共享连接(引用计数)。
11
+ *
12
+ * 同一份订阅被多个组件使用时只建一条连接,避免把浏览器同域连接数吃满、也避免服务端重复订阅:
13
+ *
14
+ * ```ts
15
+ * const { client, release } = acquireSSEClient("alarm", () => new SSEClient({ url, body }));
16
+ * onScopeDispose(release);
17
+ * ```
18
+ *
19
+ * 注意:首次创建时的 options 即基准,后续同 key 调用不会覆盖。
20
+ */
21
+ export declare function acquireSSEClient<E extends object = SSEEventMap>(key: string, create: () => SSEClient<E>): SharedSSEHandle<E>;
22
+ /** 关闭并清空所有共享连接(一般用于测试或应用卸载) */
23
+ export declare function closeAllSharedSSEClients(): void;
24
+ /** 当前共享连接数量,便于测试与诊断 */
25
+ export declare function getSharedSSEClientCount(): number;
26
+ //# 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,cAAc;AACd,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW;IAC7D,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACrB,2BAA2B;IAC3B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AASD;;;;;;;;;;;GAWG;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,+BAA+B;AAC/B,wBAAgB,wBAAwB,IAAI,IAAI,CAK/C;AAED,uBAAuB;AACvB,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD"}
package/share.js ADDED
@@ -0,0 +1,64 @@
1
+ const registry = new Map();
2
+ /**
3
+ * 按 key 共享连接(引用计数)。
4
+ *
5
+ * 同一份订阅被多个组件使用时只建一条连接,避免把浏览器同域连接数吃满、也避免服务端重复订阅:
6
+ *
7
+ * ```ts
8
+ * const { client, release } = acquireSSEClient("alarm", () => new SSEClient({ url, body }));
9
+ * onScopeDispose(release);
10
+ * ```
11
+ *
12
+ * 注意:首次创建时的 options 即基准,后续同 key 调用不会覆盖。
13
+ */
14
+ export function acquireSSEClient(key, create) {
15
+ const existing = registry.get(key);
16
+ if (existing) {
17
+ existing.refs += 1;
18
+ return {
19
+ client: existing.client,
20
+ release: once(() => release(key)),
21
+ };
22
+ }
23
+ const client = create();
24
+ registry.set(key, { client: client, refs: 1 });
25
+ return {
26
+ client,
27
+ release: once(() => release(key)),
28
+ };
29
+ }
30
+ /** 关闭并清空所有共享连接(一般用于测试或应用卸载) */
31
+ export function closeAllSharedSSEClients() {
32
+ for (const entry of [...registry.values()]) {
33
+ entry.client.close();
34
+ }
35
+ registry.clear();
36
+ }
37
+ /** 当前共享连接数量,便于测试与诊断 */
38
+ export function getSharedSSEClientCount() {
39
+ return registry.size;
40
+ }
41
+ function release(key) {
42
+ const entry = registry.get(key);
43
+ if (!entry) {
44
+ return;
45
+ }
46
+ entry.refs -= 1;
47
+ if (entry.refs > 0) {
48
+ return;
49
+ }
50
+ registry.delete(key);
51
+ entry.client.close();
52
+ }
53
+ /** 让 release 幂等,避免重复调用把别人的引用也释放掉 */
54
+ function once(fn) {
55
+ let called = false;
56
+ return () => {
57
+ if (called) {
58
+ return;
59
+ }
60
+ called = true;
61
+ fn();
62
+ };
63
+ }
64
+ //# 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;;;;;;;;;;;GAWG;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,+BAA+B;AAC/B,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,uBAAuB;AACvB,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,oCAAoC;AACpC,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,22 @@
1
+ import type { SSEMessage } from "./types.js";
2
+ export interface SSEMessageStreamOptions<T = unknown> {
3
+ /** data 解析,例如 `(raw) => JSON.parse(raw)` */
4
+ parse?: (raw: string) => T;
5
+ /** 单条消息缓冲上限(字符数),默认 1MB */
6
+ maxBufferSize?: number;
7
+ }
8
+ /**
9
+ * 把字节流组装成消息流(Web Streams 组合式,对应 AI SDK 的 `parseJsonEventStream`)。
10
+ *
11
+ * 适合不经过 `SSEClient` 的场景:自定义 transport、Service Worker、Edge Runtime。
12
+ * 解析失败(缓冲溢出)会让流进入 errored 状态,由消费方决定重试。
13
+ *
14
+ * ```ts
15
+ * const response = await fetch(url, { headers });
16
+ * const reader = response.body!
17
+ * .pipeThrough(createSSEMessageStream({ parse: JSON.parse }))
18
+ * .getReader();
19
+ * ```
20
+ */
21
+ export declare function createSSEMessageStream<T = unknown>(options?: SSEMessageStreamOptions<T>): TransformStream<Uint8Array, SSEMessage<T>>;
22
+ //# sourceMappingURL=stream.d.ts.map
@@ -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,4CAA4C;IAC5C,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,CAAC;IAC3B,2BAA2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,GAAG,OAAO,EAChD,OAAO,GAAE,uBAAuB,CAAC,CAAC,CAAM,GACvC,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CA4C5C"}
package/stream.js ADDED
@@ -0,0 +1,60 @@
1
+ import { SSEError } from "./errors.js";
2
+ import { createSSEParser } from "./parser.js";
3
+ /**
4
+ * 把字节流组装成消息流(Web Streams 组合式,对应 AI SDK 的 `parseJsonEventStream`)。
5
+ *
6
+ * 适合不经过 `SSEClient` 的场景:自定义 transport、Service Worker、Edge Runtime。
7
+ * 解析失败(缓冲溢出)会让流进入 errored 状态,由消费方决定重试。
8
+ *
9
+ * ```ts
10
+ * const response = await fetch(url, { headers });
11
+ * const reader = response.body!
12
+ * .pipeThrough(createSSEMessageStream({ parse: JSON.parse }))
13
+ * .getReader();
14
+ * ```
15
+ */
16
+ export function createSSEMessageStream(options = {}) {
17
+ const decoder = new TextDecoder("utf-8");
18
+ let controller;
19
+ const parser = createSSEParser({
20
+ maxBufferSize: options.maxBufferSize,
21
+ onEvent: (event) => {
22
+ let data = event.data;
23
+ if (options.parse) {
24
+ try {
25
+ data = options.parse(event.data);
26
+ }
27
+ catch (error) {
28
+ // 解析失败只丢这一条,不影响后续消息
29
+ console.error("[sse-client] parse 失败,已跳过该条消息", error);
30
+ return;
31
+ }
32
+ }
33
+ controller.enqueue({
34
+ id: event.id,
35
+ event: event.event || "message",
36
+ data: data,
37
+ raw: event.data,
38
+ receivedAt: Date.now(),
39
+ });
40
+ },
41
+ onRetry: () => {
42
+ // 消息流本身不负责重连,retry 字段交给 `SSEClient` 处理
43
+ },
44
+ });
45
+ return new TransformStream({
46
+ start(streamController) {
47
+ controller = streamController;
48
+ },
49
+ transform(chunk) {
50
+ parser.feed(decoder.decode(chunk, { stream: true }));
51
+ if (parser.overflowed) {
52
+ throw new SSEError("SSE 数据超出解析缓冲上限", { kind: "protocol", retryable: false });
53
+ }
54
+ },
55
+ flush() {
56
+ parser.feed(decoder.decode());
57
+ },
58
+ });
59
+ }
60
+ //# 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;;;;;;;;;;;;GAYG;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,oBAAoB;oBACpB,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;oBACtD,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,uCAAuC;QACzC,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,gBAAgB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;YAC/E,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
+ /** 一条 SSE 事件 */
2
+ export interface SSEMessage<T = unknown> {
3
+ /** 事件 id;服务端未下发时沿用上一条,用于断线续传(Last-Event-ID) */
4
+ id: string;
5
+ /** 事件类型,服务端未指定时为 "message" */
6
+ event: string;
7
+ /** 事件负载,默认是原始字符串;传了 `parse` 则是解析后的结果 */
8
+ data: T;
9
+ /** 未解析的 data 原文(多行已用换行拼接),便于排查协议问题 */
10
+ raw: string;
11
+ /** 客户端收到该消息的时间戳,便于做本地过期判断 */
12
+ receivedAt: number;
13
+ }
14
+ /** 连接状态 */
15
+ export type SSEStatus = "idle" | "connecting" | "open" | "reconnecting" | "closed";
16
+ /** 连接最终关闭的原因 */
17
+ export type SSECloseReason = "user" | "end" | "error";
18
+ /** 事件名 → 负载类型的映射,用于让 `on()` 自动推导负载类型 */
19
+ export type SSEEventMap = Record<string, unknown>;
20
+ /** 取消订阅 */
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,gBAAgB;AAChB,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,OAAO;IACrC,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,IAAI,EAAE,CAAC,CAAC;IACR,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,WAAW;AACX,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;AAEnF,gBAAgB;AAChB,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;AAEtD,wCAAwC;AACxC,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElD,WAAW;AACX,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC"}
package/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
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
+ /** `useSSE` 的参数 */
6
+ export interface UseSSEOptions extends SSEClientOptions {
7
+ /** 是否立即连接,默认 true */
8
+ immediate?: boolean;
9
+ /** 本地保留的最近事件条数,默认 200;传 0 表示不缓存 */
10
+ maxEvents?: number;
11
+ }
12
+ /** `useSSE` 的返回值 */
13
+ export interface UseSSEReturn<E extends object = SSEEventMap> {
14
+ /** 底层客户端,用于订阅具体事件名 */
15
+ client: SSEClient<E>;
16
+ /** 连接状态,可直接绑定到 UI */
17
+ status: Ref<SSEStatus>;
18
+ /** 最近一条消息 */
19
+ lastEvent: ShallowRef<SSEMessage | null>;
20
+ /** 最近若干条消息(环形缓冲) */
21
+ events: ShallowRef<SSEMessage[]>;
22
+ /** 最近一次连接错误 */
23
+ error: ShallowRef<SSEError | null>;
24
+ start: () => void;
25
+ stop: () => void;
26
+ /** 断开后重连(保留 lastEventId) */
27
+ restart: () => void;
28
+ /** 订阅事件名并在作用域销毁时自动退订 */
29
+ on: SSEClient<E>["on"];
30
+ }
31
+ /**
32
+ * Vue 组合式封装:自动重连、事件缓存、卸载自动断开。
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,mBAAmB;AACnB,MAAM,WAAW,aAAc,SAAQ,gBAAgB;IACrD,qBAAqB;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,oBAAoB;AACpB,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW;IAC1D,sBAAsB;IACtB,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACrB,qBAAqB;IACrB,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,aAAa;IACb,SAAS,EAAE,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACzC,oBAAoB;IACpB,MAAM,EAAE,UAAU,CAAC,UAAU,EAAE,CAAC,CAAC;IACjC,eAAe;IACf,KAAK,EAAE,UAAU,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACnC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,4BAA4B;IAC5B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,wBAAwB;IACxB,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,CA+DjB"}
package/vue.js ADDED
@@ -0,0 +1,71 @@
1
+ import { getCurrentScope, onScopeDispose, ref, shallowRef, } from "vue";
2
+ import { SSEClient } from "./client.js";
3
+ /**
4
+ * Vue 组合式封装:自动重连、事件缓存、卸载自动断开。
5
+ *
6
+ * ```ts
7
+ * interface AlarmEvent { level: "high" | "low"; deviceId: string }
8
+ * const { lastEvent, on, status } = useSSE<{ ALARM: AlarmEvent }>({
9
+ * url: "/api/push/subscribe",
10
+ * body: { topics: ["alarm"] },
11
+ * parse: (raw) => JSON.parse(raw),
12
+ * });
13
+ * on("ALARM", (alarm) => console.log(alarm.level));
14
+ * ```
15
+ */
16
+ export function useSSE(options) {
17
+ const { immediate = true, maxEvents = 200, ...clientOptions } = options;
18
+ const status = ref("idle");
19
+ const lastEvent = shallowRef(null);
20
+ const events = shallowRef([]);
21
+ const error = shallowRef(null);
22
+ const client = new SSEClient({
23
+ ...clientOptions,
24
+ autoStart: false,
25
+ onStatus: (next) => {
26
+ status.value = next;
27
+ clientOptions.onStatus?.(next);
28
+ },
29
+ onError: (err, attempt) => {
30
+ error.value = err;
31
+ clientOptions.onError?.(err, attempt);
32
+ },
33
+ onMessage: (message) => {
34
+ lastEvent.value = message;
35
+ if (maxEvents > 0) {
36
+ const keep = maxEvents - 1;
37
+ const next = keep > 0 ? events.value.slice(-keep) : [];
38
+ next.push(message);
39
+ // 替换数组引用,保证 shallowRef 能触发更新且不深度代理消息负载
40
+ events.value = next;
41
+ }
42
+ clientOptions.onMessage?.(message);
43
+ },
44
+ });
45
+ const start = () => {
46
+ error.value = null;
47
+ client.start();
48
+ };
49
+ const stop = () => {
50
+ client.close();
51
+ };
52
+ const restart = () => {
53
+ client.restart();
54
+ };
55
+ // 订阅跟着作用域走:在 setup 期间调用 on() 时,组件卸载 / qiankun 子应用 unmount 会自动退订
56
+ const on = ((event, handler) => {
57
+ const unsubscribe = client.on(event, handler);
58
+ if (getCurrentScope()) {
59
+ onScopeDispose(unsubscribe);
60
+ }
61
+ return unsubscribe;
62
+ });
63
+ if (immediate) {
64
+ start();
65
+ }
66
+ if (getCurrentScope()) {
67
+ onScopeDispose(stop);
68
+ }
69
+ return { client, status, lastEvent, events, error, start, stop, restart, on };
70
+ }
71
+ //# sourceMappingURL=vue.js.map
package/vue.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vue.js","sourceRoot":"","sources":["../src/vue.ts"],"names":[],"mappings":"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"}