sse-stream-plugin-sdk 1.0.0 → 1.0.2
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/CHANGELOG.md +36 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -996
- package/dist/index.mp.js +1 -0
- package/dist/index.umd.js +1 -1018
- package/dist/package.json +3 -0
- package/package.json +6 -4
- package/dist/index.cjs +0 -1012
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/index.umd.js.map +0 -1
package/dist/index.cjs
DELETED
|
@@ -1,1012 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
class Emitter {
|
|
4
|
-
constructor() {
|
|
5
|
-
Object.defineProperty(this, "listeners", {
|
|
6
|
-
enumerable: true,
|
|
7
|
-
configurable: true,
|
|
8
|
-
writable: true,
|
|
9
|
-
value: new Map()
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* 订阅事件
|
|
14
|
-
* @returns 取消订阅函数
|
|
15
|
-
*/
|
|
16
|
-
on(event, listener) {
|
|
17
|
-
let set = this.listeners.get(event);
|
|
18
|
-
if (!set) {
|
|
19
|
-
set = new Set();
|
|
20
|
-
this.listeners.set(event, set);
|
|
21
|
-
}
|
|
22
|
-
set.add(listener);
|
|
23
|
-
return () => this.off(event, listener);
|
|
24
|
-
}
|
|
25
|
-
/** 订阅一次,触发后自动移除 */
|
|
26
|
-
once(event, listener) {
|
|
27
|
-
const off = this.on(event, (...args) => {
|
|
28
|
-
off();
|
|
29
|
-
listener(...args);
|
|
30
|
-
});
|
|
31
|
-
return off;
|
|
32
|
-
}
|
|
33
|
-
/** 取消订阅 */
|
|
34
|
-
off(event, listener) {
|
|
35
|
-
this.listeners.get(event)?.delete(listener);
|
|
36
|
-
}
|
|
37
|
-
/** 派发事件,按订阅顺序同步调用 */
|
|
38
|
-
emit(event, ...args) {
|
|
39
|
-
const set = this.listeners.get(event);
|
|
40
|
-
if (!set)
|
|
41
|
-
return;
|
|
42
|
-
// 复制一份:回调中可能增删订阅者
|
|
43
|
-
for (const listener of [...set]) {
|
|
44
|
-
listener(...args);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
/** 清空全部订阅(destroy 后阻止任何插件再响应) */
|
|
48
|
-
clear() {
|
|
49
|
-
this.listeners.clear();
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 创建标准 SSE 解析器
|
|
55
|
-
* @description 规范允许 "\r\n" / "\n" / "\r" 三种换行,先归一化再按空行切块;
|
|
56
|
-
* 最后一段可能不完整,保留在缓冲中等待后续数据拼接。
|
|
57
|
-
*/
|
|
58
|
-
function createSSEParser() {
|
|
59
|
-
/** 尚未按空行切分完整的残留数据 */
|
|
60
|
-
let buffer = '';
|
|
61
|
-
const write = (chunk, emit) => {
|
|
62
|
-
buffer += chunk;
|
|
63
|
-
const normalized = buffer.replace(/\r\n?/g, '\n');
|
|
64
|
-
const parts = normalized.split('\n\n');
|
|
65
|
-
// 最后一段(不完整内容或空串)留存
|
|
66
|
-
buffer = parts.pop() ?? '';
|
|
67
|
-
for (const part of parts) {
|
|
68
|
-
if (!part.trim())
|
|
69
|
-
continue;
|
|
70
|
-
const event = parseBlock(part);
|
|
71
|
-
if (event)
|
|
72
|
-
emit(event);
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
const flush = (emit) => {
|
|
76
|
-
const normalized = buffer.replace(/\r\n?/g, '\n').trim();
|
|
77
|
-
buffer = '';
|
|
78
|
-
if (!normalized)
|
|
79
|
-
return;
|
|
80
|
-
const event = parseBlock(normalized);
|
|
81
|
-
if (event)
|
|
82
|
-
emit(event);
|
|
83
|
-
};
|
|
84
|
-
return { write, flush };
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* 解析单个 SSE 事件块
|
|
88
|
-
* @description
|
|
89
|
-
* 1. 冒号开头为注释行,忽略;
|
|
90
|
-
* 2. 值的第一个前导空格恰好移除一个;
|
|
91
|
-
* 3. 多条 data 行以 "\n" 连接;
|
|
92
|
-
* 4. 无 data 行的块返回 null(不派发);
|
|
93
|
-
* 5. id 含 U+0000 时忽略。
|
|
94
|
-
*/
|
|
95
|
-
function parseBlock(block) {
|
|
96
|
-
let name = 'message';
|
|
97
|
-
const dataLines = [];
|
|
98
|
-
let id;
|
|
99
|
-
let hasId = false;
|
|
100
|
-
for (const line of block.split('\n')) {
|
|
101
|
-
if (!line || line.startsWith(':'))
|
|
102
|
-
continue;
|
|
103
|
-
const colonIndex = line.indexOf(':');
|
|
104
|
-
let field;
|
|
105
|
-
let value;
|
|
106
|
-
if (colonIndex === -1) {
|
|
107
|
-
field = line;
|
|
108
|
-
value = '';
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
field = line.slice(0, colonIndex);
|
|
112
|
-
value = line.slice(colonIndex + 1);
|
|
113
|
-
if (value.charAt(0) === ' ')
|
|
114
|
-
value = value.slice(1);
|
|
115
|
-
}
|
|
116
|
-
switch (field) {
|
|
117
|
-
case 'event':
|
|
118
|
-
name = value;
|
|
119
|
-
break;
|
|
120
|
-
case 'data':
|
|
121
|
-
dataLines.push(value);
|
|
122
|
-
break;
|
|
123
|
-
case 'id':
|
|
124
|
-
if (!value.includes('\0')) {
|
|
125
|
-
id = value;
|
|
126
|
-
hasId = true;
|
|
127
|
-
}
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (dataLines.length === 0)
|
|
132
|
-
return null;
|
|
133
|
-
return { name, data: dataLines.join('\n'), ...(hasId ? { id } : {}) };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const webFetchTransport = () => {
|
|
137
|
-
let reader = null;
|
|
138
|
-
/** 是否已被外部中止,中止导致的 catch 不再上报 error */
|
|
139
|
-
let aborted = false;
|
|
140
|
-
return {
|
|
141
|
-
name: 'web-fetch',
|
|
142
|
-
async connect(req, events) {
|
|
143
|
-
try {
|
|
144
|
-
const res = await fetch(req.url, {
|
|
145
|
-
method: req.method,
|
|
146
|
-
headers: { 'Content-Type': 'application/json', ...req.headers },
|
|
147
|
-
body: req.method === 'POST' ? req.body : undefined,
|
|
148
|
-
});
|
|
149
|
-
events.response?.({
|
|
150
|
-
status: res.status,
|
|
151
|
-
headers: headerRecord(res.headers),
|
|
152
|
-
});
|
|
153
|
-
if (!res.ok || !res.body) {
|
|
154
|
-
throw new Error(`HTTP ${res.status} 流不可用`);
|
|
155
|
-
}
|
|
156
|
-
reader = res.body.getReader();
|
|
157
|
-
const decoder = new TextDecoder('utf-8');
|
|
158
|
-
for (;;) {
|
|
159
|
-
const { done, value } = await reader.read();
|
|
160
|
-
if (aborted)
|
|
161
|
-
break;
|
|
162
|
-
if (done) {
|
|
163
|
-
events.end();
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
events.data(decoder.decode(value, { stream: true }));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
catch (err) {
|
|
170
|
-
if (!aborted)
|
|
171
|
-
events.error(err);
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
abort() {
|
|
175
|
-
aborted = true;
|
|
176
|
-
// cancel 会令 pending read settle;releaseLock 在读取中会抛错
|
|
177
|
-
reader?.cancel().catch(() => { });
|
|
178
|
-
reader = null;
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
};
|
|
182
|
-
/** 将 Headers 对象拍平为普通键值对(值取首个) */
|
|
183
|
-
function headerRecord(headers) {
|
|
184
|
-
const record = {};
|
|
185
|
-
headers.forEach((value, key) => {
|
|
186
|
-
record[key] = value;
|
|
187
|
-
});
|
|
188
|
-
return record;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* 创建通用小程序传输层工厂
|
|
193
|
-
* @param getRequest 获取平台 request API 的函数(延迟求值,避免模块加载时全局对象未就绪)
|
|
194
|
-
* @param name 传输层名称,默认 'miniprogram'
|
|
195
|
-
*/
|
|
196
|
-
function createMpTransport(getRequest, name = 'miniprogram') {
|
|
197
|
-
return () => {
|
|
198
|
-
let task = null;
|
|
199
|
-
let aborted = false;
|
|
200
|
-
let decoder;
|
|
201
|
-
return {
|
|
202
|
-
name,
|
|
203
|
-
connect(req, events) {
|
|
204
|
-
const api = getRequest();
|
|
205
|
-
task = api.request({
|
|
206
|
-
url: req.url,
|
|
207
|
-
method: req.method,
|
|
208
|
-
header: { ...req.headers },
|
|
209
|
-
// 小程序 data 需为对象:将 JSON 字符串解析回对象
|
|
210
|
-
data: req.method === 'POST' && req.body ? JSON.parse(req.body) : undefined,
|
|
211
|
-
enableChunked: true,
|
|
212
|
-
responseType: 'arraybuffer',
|
|
213
|
-
// 请求完成(物理结束),并上报状态码
|
|
214
|
-
success: (res) => {
|
|
215
|
-
if (aborted)
|
|
216
|
-
return;
|
|
217
|
-
events.response?.({ status: res.statusCode });
|
|
218
|
-
events.end();
|
|
219
|
-
},
|
|
220
|
-
fail: (err) => {
|
|
221
|
-
if (!aborted)
|
|
222
|
-
events.error(normalizeMpError(err));
|
|
223
|
-
},
|
|
224
|
-
});
|
|
225
|
-
task.onChunkReceived((res) => {
|
|
226
|
-
if (aborted)
|
|
227
|
-
return;
|
|
228
|
-
events.data(decodeBytes(new Uint8Array(res.data)));
|
|
229
|
-
});
|
|
230
|
-
/** 解码分片,优先 TextDecoder stream 模式,回退百分号解码 */
|
|
231
|
-
function decodeBytes(bytes) {
|
|
232
|
-
if (decoder === undefined) {
|
|
233
|
-
decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8') : null;
|
|
234
|
-
}
|
|
235
|
-
if (decoder)
|
|
236
|
-
return decoder.decode(bytes, { stream: true });
|
|
237
|
-
let hex = '';
|
|
238
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
239
|
-
hex += '%' + bytes[i].toString(16).padStart(2, '0');
|
|
240
|
-
}
|
|
241
|
-
return decodeURIComponent(hex);
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
abort() {
|
|
245
|
-
aborted = true;
|
|
246
|
-
task?.abort();
|
|
247
|
-
task = null;
|
|
248
|
-
},
|
|
249
|
-
};
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
/** 将平台回调错误归一化为 Error:优先取 errMsg,保证错误信息可读 */
|
|
253
|
-
function normalizeMpError(err) {
|
|
254
|
-
if (err instanceof Error)
|
|
255
|
-
return err;
|
|
256
|
-
if (err !== null && typeof err === 'object' && 'errMsg' in err) {
|
|
257
|
-
const msg = err.errMsg;
|
|
258
|
-
return new Error(typeof msg === 'string' && msg ? msg : 'request 失败');
|
|
259
|
-
}
|
|
260
|
-
return new Error(String(err));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** 内置示例脚本(注册表中默认 'mock' 使用) */
|
|
264
|
-
function defaultScript() {
|
|
265
|
-
return [
|
|
266
|
-
{ id: 'm1', data: '开始解释:\n' },
|
|
267
|
-
{ id: 'm2', data: '输入内容:你好\n' },
|
|
268
|
-
{ name: 'ping', data: '', gap: 200 },
|
|
269
|
-
{ id: 'm3', data: '\n1. 语法解析完成' },
|
|
270
|
-
{ id: 'm4', data: '\n2. 语义分析完成' },
|
|
271
|
-
{ id: 'm5', data: '\n3. 生成解释结果' },
|
|
272
|
-
{ id: 'm6', data: '\n✅ 解释完毕' },
|
|
273
|
-
];
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* 创建 Mock 传输层工厂
|
|
277
|
-
* @description 注意 failAfter 的“只失败一次”状态记忆在工厂闭包外层,
|
|
278
|
-
* 重连产生的新实例不会再次失败。
|
|
279
|
-
*/
|
|
280
|
-
function mockTransport(options) {
|
|
281
|
-
const script = options?.events ?? defaultScript();
|
|
282
|
-
const failAfter = options?.failAfter ?? 0;
|
|
283
|
-
const errorMessage = options?.errorMessage ?? '模拟网络中断(连接被重置)';
|
|
284
|
-
/** 本次失败是否已经发生过(跨重连实例共享) */
|
|
285
|
-
let failedOnce = false;
|
|
286
|
-
return ({ request }) => {
|
|
287
|
-
const timers = [];
|
|
288
|
-
let aborted = false;
|
|
289
|
-
// 续传:依据 Last-Event-ID 定位起始事件
|
|
290
|
-
let cursor = 0;
|
|
291
|
-
const lastId = request.headers['Last-Event-ID'] ?? request.headers['last-event-id'];
|
|
292
|
-
if (lastId) {
|
|
293
|
-
const index = script.findIndex((item) => item.id === lastId);
|
|
294
|
-
if (index >= 0)
|
|
295
|
-
cursor = index + 1;
|
|
296
|
-
}
|
|
297
|
-
return {
|
|
298
|
-
name: 'mock',
|
|
299
|
-
connect(_req, events) {
|
|
300
|
-
const schedule = () => {
|
|
301
|
-
if (aborted)
|
|
302
|
-
return;
|
|
303
|
-
// 已投递 failAfter 个事件:制造唯一一次错误
|
|
304
|
-
if (failAfter > 0 && cursor === failAfter && !failedOnce) {
|
|
305
|
-
failedOnce = true;
|
|
306
|
-
const timer = setTimeout(() => {
|
|
307
|
-
if (!aborted)
|
|
308
|
-
events.error(new Error(errorMessage));
|
|
309
|
-
}, 500);
|
|
310
|
-
timers.push(timer);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
if (cursor >= script.length) {
|
|
314
|
-
const timer = setTimeout(() => {
|
|
315
|
-
if (!aborted)
|
|
316
|
-
events.end();
|
|
317
|
-
}, 200);
|
|
318
|
-
timers.push(timer);
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const item = script[cursor];
|
|
322
|
-
const timer = setTimeout(() => {
|
|
323
|
-
if (aborted)
|
|
324
|
-
return;
|
|
325
|
-
events.data(serializeSSE(item));
|
|
326
|
-
cursor++;
|
|
327
|
-
schedule();
|
|
328
|
-
}, item.gap ?? 350);
|
|
329
|
-
timers.push(timer);
|
|
330
|
-
};
|
|
331
|
-
schedule();
|
|
332
|
-
},
|
|
333
|
-
abort() {
|
|
334
|
-
aborted = true;
|
|
335
|
-
for (const timer of timers)
|
|
336
|
-
clearTimeout(timer);
|
|
337
|
-
timers.length = 0;
|
|
338
|
-
},
|
|
339
|
-
};
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
/** 将脚本事件序列化为标准 SSE 文本块 */
|
|
343
|
-
function serializeSSE(item) {
|
|
344
|
-
const lines = [];
|
|
345
|
-
if (item.id)
|
|
346
|
-
lines.push(`id:${item.id}`);
|
|
347
|
-
lines.push(`event:${item.name ?? 'message'}`);
|
|
348
|
-
for (const line of (item.data ?? '').split('\n')) {
|
|
349
|
-
lines.push(`data:${line}`);
|
|
350
|
-
}
|
|
351
|
-
return lines.join('\n') + '\n\n';
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* 自动探测可用传输层
|
|
356
|
-
* @description
|
|
357
|
-
* 仅探测 Web 标准 fetch——fetch 是跨环境通用标准,自动探测合理;
|
|
358
|
-
* 小程序等碎片化平台需通过插件显式注册传输层(createMpTransport),
|
|
359
|
-
* 内核不做任何平台假设。
|
|
360
|
-
*/
|
|
361
|
-
function detectTransport() {
|
|
362
|
-
const global = globalThis;
|
|
363
|
-
if (typeof global.fetch === 'function')
|
|
364
|
-
return webFetchTransport;
|
|
365
|
-
throw new Error('未检测到可用传输层,请通过 options.transport 显式指定,或用 createMpTransport 注册小程序传输层');
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* @file 插件化 SSE 客户端内核
|
|
370
|
-
* @description
|
|
371
|
-
* 内核只做“编排”:
|
|
372
|
-
* 连接开始 → 请求中间件链 → 传输层收发 → 解析器产出事件 →
|
|
373
|
-
* 事件中间件链 → 事件总线派发;
|
|
374
|
-
* 重连、心跳、节流、鉴权等能力全部下沉为插件,内核不内置任何平台分支。
|
|
375
|
-
*/
|
|
376
|
-
class PluginSSE {
|
|
377
|
-
constructor(options) {
|
|
378
|
-
/** 公共事件总线 */
|
|
379
|
-
Object.defineProperty(this, "emitter", {
|
|
380
|
-
enumerable: true,
|
|
381
|
-
configurable: true,
|
|
382
|
-
writable: true,
|
|
383
|
-
value: new Emitter()
|
|
384
|
-
});
|
|
385
|
-
/** 已注册插件 */
|
|
386
|
-
Object.defineProperty(this, "plugins", {
|
|
387
|
-
enumerable: true,
|
|
388
|
-
configurable: true,
|
|
389
|
-
writable: true,
|
|
390
|
-
value: []
|
|
391
|
-
});
|
|
392
|
-
/** 插件是否已完成安装(只安装一次) */
|
|
393
|
-
Object.defineProperty(this, "installed", {
|
|
394
|
-
enumerable: true,
|
|
395
|
-
configurable: true,
|
|
396
|
-
writable: true,
|
|
397
|
-
value: false
|
|
398
|
-
});
|
|
399
|
-
/** 暴露给插件的上下文 */
|
|
400
|
-
Object.defineProperty(this, "context", {
|
|
401
|
-
enumerable: true,
|
|
402
|
-
configurable: true,
|
|
403
|
-
writable: true,
|
|
404
|
-
value: void 0
|
|
405
|
-
});
|
|
406
|
-
/** 命名传输层注册表 */
|
|
407
|
-
Object.defineProperty(this, "transportRegistry", {
|
|
408
|
-
enumerable: true,
|
|
409
|
-
configurable: true,
|
|
410
|
-
writable: true,
|
|
411
|
-
value: new Map()
|
|
412
|
-
});
|
|
413
|
-
/** 请求中间件(按注册顺序) */
|
|
414
|
-
Object.defineProperty(this, "requestMiddleware", {
|
|
415
|
-
enumerable: true,
|
|
416
|
-
configurable: true,
|
|
417
|
-
writable: true,
|
|
418
|
-
value: []
|
|
419
|
-
});
|
|
420
|
-
/** 事件中间件(按注册顺序) */
|
|
421
|
-
Object.defineProperty(this, "eventMiddleware", {
|
|
422
|
-
enumerable: true,
|
|
423
|
-
configurable: true,
|
|
424
|
-
writable: true,
|
|
425
|
-
value: []
|
|
426
|
-
});
|
|
427
|
-
/** 插件销毁回调 */
|
|
428
|
-
Object.defineProperty(this, "disposeFns", {
|
|
429
|
-
enumerable: true,
|
|
430
|
-
configurable: true,
|
|
431
|
-
writable: true,
|
|
432
|
-
value: []
|
|
433
|
-
});
|
|
434
|
-
/** 解析器工厂,默认标准 SSE */
|
|
435
|
-
Object.defineProperty(this, "parserFactory", {
|
|
436
|
-
enumerable: true,
|
|
437
|
-
configurable: true,
|
|
438
|
-
writable: true,
|
|
439
|
-
value: createSSEParser
|
|
440
|
-
});
|
|
441
|
-
/** 当前连接使用的解析器 */
|
|
442
|
-
Object.defineProperty(this, "parser", {
|
|
443
|
-
enumerable: true,
|
|
444
|
-
configurable: true,
|
|
445
|
-
writable: true,
|
|
446
|
-
value: null
|
|
447
|
-
});
|
|
448
|
-
/** 当前连接的传输层实例 */
|
|
449
|
-
Object.defineProperty(this, "activeTransport", {
|
|
450
|
-
enumerable: true,
|
|
451
|
-
configurable: true,
|
|
452
|
-
writable: true,
|
|
453
|
-
value: null
|
|
454
|
-
});
|
|
455
|
-
/** 当前请求配置(每次连接开始时重建) */
|
|
456
|
-
Object.defineProperty(this, "currentRequest", {
|
|
457
|
-
enumerable: true,
|
|
458
|
-
configurable: true,
|
|
459
|
-
writable: true,
|
|
460
|
-
value: {
|
|
461
|
-
url: '',
|
|
462
|
-
method: 'GET',
|
|
463
|
-
headers: {},
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
/** 初始化配置(合并默认值后) */
|
|
467
|
-
Object.defineProperty(this, "options", {
|
|
468
|
-
enumerable: true,
|
|
469
|
-
configurable: true,
|
|
470
|
-
writable: true,
|
|
471
|
-
value: void 0
|
|
472
|
-
});
|
|
473
|
-
/** 内核状态 */
|
|
474
|
-
Object.defineProperty(this, "state", {
|
|
475
|
-
enumerable: true,
|
|
476
|
-
configurable: true,
|
|
477
|
-
writable: true,
|
|
478
|
-
value: 'idle'
|
|
479
|
-
});
|
|
480
|
-
/** 会话代际,每次开新流/出错自增,用于作废迟到回调 */
|
|
481
|
-
Object.defineProperty(this, "sessionId", {
|
|
482
|
-
enumerable: true,
|
|
483
|
-
configurable: true,
|
|
484
|
-
writable: true,
|
|
485
|
-
value: 0
|
|
486
|
-
});
|
|
487
|
-
/** 是否已永久销毁 */
|
|
488
|
-
Object.defineProperty(this, "dead", {
|
|
489
|
-
enumerable: true,
|
|
490
|
-
configurable: true,
|
|
491
|
-
writable: true,
|
|
492
|
-
value: false
|
|
493
|
-
});
|
|
494
|
-
/**
|
|
495
|
-
* 将解析出的事件送入事件中间件链,链尾派发到总线
|
|
496
|
-
* @description 最终同时派发通用 'event' 与具体事件名(如 'message')
|
|
497
|
-
*/
|
|
498
|
-
Object.defineProperty(this, "dispatch", {
|
|
499
|
-
enumerable: true,
|
|
500
|
-
configurable: true,
|
|
501
|
-
writable: true,
|
|
502
|
-
value: (event) => {
|
|
503
|
-
runEventChain(this.eventMiddleware, event, (final) => {
|
|
504
|
-
this.emitter.emit('event', final);
|
|
505
|
-
this.emitter.emit(final.name, final.data, final);
|
|
506
|
-
}, (err) => this.settleError(err));
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
this.options = {
|
|
510
|
-
url: options.url,
|
|
511
|
-
method: options.method ?? 'GET',
|
|
512
|
-
body: options.body ?? '',
|
|
513
|
-
headers: options.headers ?? {},
|
|
514
|
-
transport: options.transport,
|
|
515
|
-
plugins: options.plugins ?? [],
|
|
516
|
-
parser: options.parser,
|
|
517
|
-
};
|
|
518
|
-
if (this.options.parser)
|
|
519
|
-
this.parserFactory = this.options.parser;
|
|
520
|
-
// 注册内置命名传输层
|
|
521
|
-
// web / mock 为通用内置;小程序等平台传输层由使用方通过插件注册
|
|
522
|
-
this.transportRegistry.set('web', webFetchTransport);
|
|
523
|
-
this.transportRegistry.set('mock', mockTransport());
|
|
524
|
-
// 构造稳定的插件上下文
|
|
525
|
-
const self = this;
|
|
526
|
-
this.context = {
|
|
527
|
-
emitter: this.emitter,
|
|
528
|
-
get request() {
|
|
529
|
-
return self.currentRequest;
|
|
530
|
-
},
|
|
531
|
-
addRequestMiddleware: (mw) => this.requestMiddleware.push(mw),
|
|
532
|
-
addEventMiddleware: (mw) => this.eventMiddleware.push(mw),
|
|
533
|
-
setParser: (factory) => {
|
|
534
|
-
this.parserFactory = factory;
|
|
535
|
-
},
|
|
536
|
-
registerTransport: (name, factory) => this.transportRegistry.set(name, factory),
|
|
537
|
-
start: () => this.start(),
|
|
538
|
-
fail: (err) => this.settleError(err),
|
|
539
|
-
getState: () => this.state,
|
|
540
|
-
isDead: () => this.dead,
|
|
541
|
-
onDispose: (fn) => this.disposeFns.push(fn),
|
|
542
|
-
};
|
|
543
|
-
// 注册构造时传入的插件
|
|
544
|
-
for (const plugin of this.options.plugins)
|
|
545
|
-
this.use(plugin);
|
|
546
|
-
}
|
|
547
|
-
/**
|
|
548
|
-
* 注册插件
|
|
549
|
-
* @description 首次连接前注册会在连接时统一安装;连接后注册则立即安装,
|
|
550
|
-
* 新管道只影响后续的连接与事件。
|
|
551
|
-
*/
|
|
552
|
-
use(plugin) {
|
|
553
|
-
if (this.dead)
|
|
554
|
-
throw new Error('实例已销毁,无法再注册插件');
|
|
555
|
-
if (this.plugins.some((p) => p.name === plugin.name)) {
|
|
556
|
-
// 同名插件忽略,避免重复订阅
|
|
557
|
-
return this;
|
|
558
|
-
}
|
|
559
|
-
this.plugins.push(plugin);
|
|
560
|
-
if (this.installed)
|
|
561
|
-
plugin.install(this.context);
|
|
562
|
-
return this;
|
|
563
|
-
}
|
|
564
|
-
/** 建立连接(公开入口) */
|
|
565
|
-
async connect() {
|
|
566
|
-
if (this.dead)
|
|
567
|
-
throw new Error('实例已销毁,请创建新的 PluginSSE');
|
|
568
|
-
await this.start();
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* 发起一次连接(连接与插件触发的重连共用)
|
|
572
|
-
* @description 中止旧连接 → 重建请求 → 请求中间件 → 创建解析器/传输层 → 收发
|
|
573
|
-
*/
|
|
574
|
-
async start() {
|
|
575
|
-
if (this.dead)
|
|
576
|
-
return;
|
|
577
|
-
if (!this.installed)
|
|
578
|
-
this.installPlugins();
|
|
579
|
-
this.silentAbort();
|
|
580
|
-
const session = ++this.sessionId;
|
|
581
|
-
this.state = 'connecting';
|
|
582
|
-
this.currentRequest = {
|
|
583
|
-
url: this.options.url,
|
|
584
|
-
method: this.options.method,
|
|
585
|
-
headers: { ...this.options.headers },
|
|
586
|
-
body: this.options.method === 'POST' ? this.options.body : undefined,
|
|
587
|
-
};
|
|
588
|
-
this.emitter.emit('connecting', { session });
|
|
589
|
-
// 请求管道:插件可在此注入鉴权头、改写请求
|
|
590
|
-
let req;
|
|
591
|
-
try {
|
|
592
|
-
req = await composeRequest(this.requestMiddleware)(this.currentRequest);
|
|
593
|
-
}
|
|
594
|
-
catch (err) {
|
|
595
|
-
if (session === this.sessionId)
|
|
596
|
-
this.settleError(err);
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
if (session !== this.sessionId || this.dead)
|
|
600
|
-
return;
|
|
601
|
-
this.currentRequest = req;
|
|
602
|
-
this.parser = this.parserFactory();
|
|
603
|
-
const factory = this.resolveTransportFactory();
|
|
604
|
-
const transport = factory({ request: req });
|
|
605
|
-
this.activeTransport = transport;
|
|
606
|
-
transport.connect(req, {
|
|
607
|
-
data: (chunk) => {
|
|
608
|
-
if (session !== this.sessionId)
|
|
609
|
-
return;
|
|
610
|
-
this.emitter.emit('data', chunk);
|
|
611
|
-
this.parser?.write(chunk, this.dispatch);
|
|
612
|
-
},
|
|
613
|
-
end: () => {
|
|
614
|
-
if (session !== this.sessionId)
|
|
615
|
-
return;
|
|
616
|
-
this.parser?.flush(this.dispatch);
|
|
617
|
-
this.state = 'closed';
|
|
618
|
-
this.emitter.emit('end');
|
|
619
|
-
},
|
|
620
|
-
error: (err) => {
|
|
621
|
-
if (session !== this.sessionId)
|
|
622
|
-
return;
|
|
623
|
-
this.settleError(err);
|
|
624
|
-
},
|
|
625
|
-
response: (info) => {
|
|
626
|
-
if (session !== this.sessionId)
|
|
627
|
-
return;
|
|
628
|
-
this.emitter.emit('response', info);
|
|
629
|
-
},
|
|
630
|
-
});
|
|
631
|
-
this.state = 'open';
|
|
632
|
-
this.emitter.emit('open', { transport: transport.name });
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* 永久销毁:中止连接、释放插件资源、清空订阅
|
|
636
|
-
* @description 销毁后不再有任何自动重连
|
|
637
|
-
*/
|
|
638
|
-
destroy() {
|
|
639
|
-
if (this.dead)
|
|
640
|
-
return;
|
|
641
|
-
this.dead = true;
|
|
642
|
-
this.sessionId++;
|
|
643
|
-
this.silentAbort();
|
|
644
|
-
this.state = 'closed';
|
|
645
|
-
for (const fn of this.disposeFns) {
|
|
646
|
-
try {
|
|
647
|
-
fn();
|
|
648
|
-
}
|
|
649
|
-
catch {
|
|
650
|
-
// 单个插件销毁异常不影响其他清理
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
this.emitter.emit('closed');
|
|
654
|
-
this.emitter.clear();
|
|
655
|
-
}
|
|
656
|
-
// ---- 事件订阅(事件总线的便捷代理) ----
|
|
657
|
-
on(event, listener) {
|
|
658
|
-
return this.emitter.on(event, listener);
|
|
659
|
-
}
|
|
660
|
-
once(event, listener) {
|
|
661
|
-
return this.emitter.once(event, listener);
|
|
662
|
-
}
|
|
663
|
-
off(event, listener) {
|
|
664
|
-
this.emitter.off(event, listener);
|
|
665
|
-
}
|
|
666
|
-
/** 安装全部插件(仅一次) */
|
|
667
|
-
installPlugins() {
|
|
668
|
-
this.installed = true;
|
|
669
|
-
for (const plugin of this.plugins)
|
|
670
|
-
plugin.install(this.context);
|
|
671
|
-
}
|
|
672
|
-
/** 解析传输层配置:名称 → 工厂;省略则自动探测 */
|
|
673
|
-
resolveTransportFactory() {
|
|
674
|
-
const transport = this.options.transport;
|
|
675
|
-
if (typeof transport === 'string') {
|
|
676
|
-
const factory = this.transportRegistry.get(transport);
|
|
677
|
-
if (!factory)
|
|
678
|
-
throw new Error(`未注册的传输层:${transport}`);
|
|
679
|
-
return factory;
|
|
680
|
-
}
|
|
681
|
-
if (transport)
|
|
682
|
-
return transport;
|
|
683
|
-
return detectTransport();
|
|
684
|
-
}
|
|
685
|
-
/**
|
|
686
|
-
* 结束错误会话:作废代际、中止传输、派发错误事件
|
|
687
|
-
* @description 是否重连交由重连插件等订阅者决定,内核自身不重连
|
|
688
|
-
*/
|
|
689
|
-
settleError(err) {
|
|
690
|
-
if (this.dead)
|
|
691
|
-
return;
|
|
692
|
-
this.sessionId++;
|
|
693
|
-
this.silentAbort();
|
|
694
|
-
this.state = 'closed';
|
|
695
|
-
this.emitter.emit('error', err);
|
|
696
|
-
}
|
|
697
|
-
/** 静默中止当前传输并清理引用(不派发任何事件) */
|
|
698
|
-
silentAbort() {
|
|
699
|
-
if (this.activeTransport) {
|
|
700
|
-
try {
|
|
701
|
-
this.activeTransport.abort();
|
|
702
|
-
}
|
|
703
|
-
catch {
|
|
704
|
-
// 忽略中止异常
|
|
705
|
-
}
|
|
706
|
-
this.activeTransport = null;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
/**
|
|
711
|
-
* 组装请求中间件链
|
|
712
|
-
* @description 类 Koa 的线性 next 模型,最后一个中间件结束即返回请求
|
|
713
|
-
*/
|
|
714
|
-
function composeRequest(middleware) {
|
|
715
|
-
return (arg) => {
|
|
716
|
-
let index = -1;
|
|
717
|
-
const dispatch = (i, req) => {
|
|
718
|
-
if (i <= index)
|
|
719
|
-
return Promise.reject(new Error('next() 被多次调用'));
|
|
720
|
-
index = i;
|
|
721
|
-
if (i === middleware.length)
|
|
722
|
-
return Promise.resolve(req);
|
|
723
|
-
try {
|
|
724
|
-
return Promise.resolve(middleware[i](req, (modified) => dispatch(i + 1, modified ?? req)));
|
|
725
|
-
}
|
|
726
|
-
catch (err) {
|
|
727
|
-
return Promise.reject(err);
|
|
728
|
-
}
|
|
729
|
-
};
|
|
730
|
-
return dispatch(0, arg);
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
/**
|
|
734
|
-
* 执行事件中间件链
|
|
735
|
-
* @param middleware 中间件列表
|
|
736
|
-
* @param event 初始事件
|
|
737
|
-
* @param terminal 链尾回调(真正派发)
|
|
738
|
-
* @param onError 链上异常处理
|
|
739
|
-
*/
|
|
740
|
-
function runEventChain(middleware, event, terminal, onError) {
|
|
741
|
-
let index = -1;
|
|
742
|
-
const dispatch = (i, e) => {
|
|
743
|
-
if (i <= index)
|
|
744
|
-
return Promise.reject(new Error('next() 被多次调用'));
|
|
745
|
-
index = i;
|
|
746
|
-
if (i === middleware.length) {
|
|
747
|
-
terminal(e);
|
|
748
|
-
return Promise.resolve();
|
|
749
|
-
}
|
|
750
|
-
try {
|
|
751
|
-
return Promise.resolve(middleware[i](e, (modified) => dispatch(i + 1, modified ?? e)));
|
|
752
|
-
}
|
|
753
|
-
catch (err) {
|
|
754
|
-
return Promise.reject(err);
|
|
755
|
-
}
|
|
756
|
-
};
|
|
757
|
-
dispatch(0, event).catch(onError);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
function reconnectPlugin(options) {
|
|
761
|
-
const maxRetries = options?.maxRetries ?? 3;
|
|
762
|
-
const delay = options?.delay ?? 1500;
|
|
763
|
-
/** 最近事件 id(续传位点) */
|
|
764
|
-
let lastId = '';
|
|
765
|
-
/** 已重试次数 */
|
|
766
|
-
let attempts = 0;
|
|
767
|
-
let timer = null;
|
|
768
|
-
return {
|
|
769
|
-
name: 'reconnect',
|
|
770
|
-
install(ctx) {
|
|
771
|
-
// 请求中间件:重连时携带续传位点
|
|
772
|
-
ctx.addRequestMiddleware(async (req, next) => {
|
|
773
|
-
if (lastId)
|
|
774
|
-
req.headers['Last-Event-ID'] = lastId;
|
|
775
|
-
return next(req);
|
|
776
|
-
});
|
|
777
|
-
// 记录最近的事件 id(ping 等无 id 事件不改变位点)
|
|
778
|
-
ctx.emitter.on('event', (event) => {
|
|
779
|
-
if (event.id)
|
|
780
|
-
lastId = event.id;
|
|
781
|
-
});
|
|
782
|
-
ctx.emitter.on('error', (err) => {
|
|
783
|
-
if (ctx.isDead())
|
|
784
|
-
return;
|
|
785
|
-
if (attempts >= maxRetries) {
|
|
786
|
-
ctx.emitter.emit('reconnect-failed', err);
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
attempts++;
|
|
790
|
-
ctx.emitter.emit('reconnect', { attempt: attempts, inMs: delay });
|
|
791
|
-
timer = setTimeout(() => {
|
|
792
|
-
timer = null;
|
|
793
|
-
void ctx.start();
|
|
794
|
-
}, delay);
|
|
795
|
-
});
|
|
796
|
-
// 正常结束:重置重试计数与位点
|
|
797
|
-
ctx.emitter.on('end', () => {
|
|
798
|
-
attempts = 0;
|
|
799
|
-
lastId = '';
|
|
800
|
-
});
|
|
801
|
-
ctx.onDispose(() => {
|
|
802
|
-
if (timer)
|
|
803
|
-
clearTimeout(timer);
|
|
804
|
-
timer = null;
|
|
805
|
-
});
|
|
806
|
-
},
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function heartbeatPlugin(options) {
|
|
811
|
-
const interval = options?.interval ?? 10000;
|
|
812
|
-
let timer = null;
|
|
813
|
-
return {
|
|
814
|
-
name: 'heartbeat',
|
|
815
|
-
install(ctx) {
|
|
816
|
-
const clear = () => {
|
|
817
|
-
if (timer) {
|
|
818
|
-
clearTimeout(timer);
|
|
819
|
-
timer = null;
|
|
820
|
-
}
|
|
821
|
-
};
|
|
822
|
-
const arm = () => {
|
|
823
|
-
clear();
|
|
824
|
-
timer = setTimeout(() => {
|
|
825
|
-
ctx.emitter.emit('heartbeat-lost');
|
|
826
|
-
ctx.fail(new Error(`心跳丢失:${interval}ms 内未收到任何数据`));
|
|
827
|
-
}, interval);
|
|
828
|
-
};
|
|
829
|
-
ctx.emitter.on('connecting', arm);
|
|
830
|
-
// 任意原始数据到达即续命(包含 ping 事件文本)
|
|
831
|
-
ctx.emitter.on('data', arm);
|
|
832
|
-
// 结束/错误后撤掉计时,避免误判
|
|
833
|
-
ctx.emitter.on('end', clear);
|
|
834
|
-
ctx.emitter.on('error', clear);
|
|
835
|
-
ctx.onDispose(clear);
|
|
836
|
-
},
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function throttlePlugin(options) {
|
|
841
|
-
const windowMs = options?.windowMs ?? 100;
|
|
842
|
-
/** 窗口内累积的数据 */
|
|
843
|
-
let buffer = '';
|
|
844
|
-
/** 窗口内最后一个事件 id */
|
|
845
|
-
let lastEventId;
|
|
846
|
-
let timer = null;
|
|
847
|
-
/** 被拦截事件的 next:窗口结束时一次性放行合并事件 */
|
|
848
|
-
let pendingNext = null;
|
|
849
|
-
return {
|
|
850
|
-
name: 'throttle',
|
|
851
|
-
install(ctx) {
|
|
852
|
-
const flush = () => {
|
|
853
|
-
if (timer) {
|
|
854
|
-
clearTimeout(timer);
|
|
855
|
-
timer = null;
|
|
856
|
-
}
|
|
857
|
-
if (buffer && pendingNext) {
|
|
858
|
-
const data = buffer;
|
|
859
|
-
const id = lastEventId;
|
|
860
|
-
buffer = '';
|
|
861
|
-
lastEventId = undefined;
|
|
862
|
-
const next = pendingNext;
|
|
863
|
-
pendingNext = null;
|
|
864
|
-
next({ name: 'message', data, ...(id ? { id } : {}) });
|
|
865
|
-
}
|
|
866
|
-
};
|
|
867
|
-
const middleware = (event, next) => {
|
|
868
|
-
if (event.name !== 'message')
|
|
869
|
-
return next(event);
|
|
870
|
-
buffer += event.data;
|
|
871
|
-
if (event.id)
|
|
872
|
-
lastEventId = event.id;
|
|
873
|
-
pendingNext = next;
|
|
874
|
-
if (!timer) {
|
|
875
|
-
timer = setTimeout(flush, windowMs);
|
|
876
|
-
}
|
|
877
|
-
// 不调用 next:事件被暂存,窗口结束时统一放行
|
|
878
|
-
};
|
|
879
|
-
ctx.addEventMiddleware(middleware);
|
|
880
|
-
// 结束/出错/销毁时立即冲刷,避免丢消息
|
|
881
|
-
ctx.emitter.on('end', flush);
|
|
882
|
-
ctx.emitter.on('error', flush);
|
|
883
|
-
ctx.onDispose(flush);
|
|
884
|
-
},
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function loggerPlugin(options) {
|
|
889
|
-
const sink = options?.sink ?? ((message) => console.log(message));
|
|
890
|
-
return {
|
|
891
|
-
name: 'logger',
|
|
892
|
-
install(ctx) {
|
|
893
|
-
ctx.emitter.on('open', ({ transport }) => {
|
|
894
|
-
const headers = JSON.stringify(ctx.request.headers);
|
|
895
|
-
sink(`连接已打开(${transport})请求头:${headers}`);
|
|
896
|
-
});
|
|
897
|
-
ctx.emitter.on('response', ({ status }) => {
|
|
898
|
-
sink(`收到响应,状态码:${status}`);
|
|
899
|
-
});
|
|
900
|
-
ctx.emitter.on('event', (event) => {
|
|
901
|
-
const preview = event.data.replace(/\n/g, '\\n');
|
|
902
|
-
const shown = preview.length > 40 ? preview.slice(0, 40) + '…' : preview;
|
|
903
|
-
sink(`事件 ${event.name}${event.id ? ' #' + event.id : ''} ` +
|
|
904
|
-
`(${event.data.length} 字): ${shown}`);
|
|
905
|
-
});
|
|
906
|
-
ctx.emitter.on('reconnect', ({ attempt, inMs }) => {
|
|
907
|
-
sink(`连接中断,${inMs}ms 后进行第 ${attempt} 次重连…`);
|
|
908
|
-
});
|
|
909
|
-
ctx.emitter.on('heartbeat-lost', () => {
|
|
910
|
-
sink('心跳丢失!');
|
|
911
|
-
});
|
|
912
|
-
ctx.emitter.on('reconnect-failed', () => {
|
|
913
|
-
sink('已达最大重试次数,不再重连');
|
|
914
|
-
});
|
|
915
|
-
ctx.emitter.on('error', (err) => {
|
|
916
|
-
sink(`发生错误:${err instanceof Error ? err.message : String(err)}`);
|
|
917
|
-
});
|
|
918
|
-
ctx.emitter.on('end', () => {
|
|
919
|
-
sink('流正常结束');
|
|
920
|
-
});
|
|
921
|
-
ctx.emitter.on('closed', () => {
|
|
922
|
-
sink('实例已销毁,资源已释放');
|
|
923
|
-
});
|
|
924
|
-
},
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
function authPlugin(options) {
|
|
929
|
-
const header = options.header ?? 'Authorization';
|
|
930
|
-
const scheme = options.scheme ?? 'Bearer';
|
|
931
|
-
/** 是否正在刷新,避免多个 401 并发刷新 */
|
|
932
|
-
let refreshing = false;
|
|
933
|
-
return {
|
|
934
|
-
name: 'auth',
|
|
935
|
-
install(ctx) {
|
|
936
|
-
// 请求管道:注入鉴权头
|
|
937
|
-
ctx.addRequestMiddleware(async (req, next) => {
|
|
938
|
-
const token = await options.getToken();
|
|
939
|
-
req.headers[header] = scheme ? `${scheme} ${token}` : token;
|
|
940
|
-
return next(req);
|
|
941
|
-
});
|
|
942
|
-
// 状态码感知:401 → 刷新 → 重连
|
|
943
|
-
ctx.emitter.on('response', async ({ status }) => {
|
|
944
|
-
if (status !== 401 || refreshing || ctx.isDead())
|
|
945
|
-
return;
|
|
946
|
-
refreshing = true;
|
|
947
|
-
ctx.emitter.emit('auth-refreshing');
|
|
948
|
-
try {
|
|
949
|
-
await options.refreshToken();
|
|
950
|
-
}
|
|
951
|
-
finally {
|
|
952
|
-
refreshing = false;
|
|
953
|
-
}
|
|
954
|
-
void ctx.start();
|
|
955
|
-
});
|
|
956
|
-
},
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
function statsPlugin(options) {
|
|
961
|
-
const keyword = options?.keyword ?? '✅';
|
|
962
|
-
return {
|
|
963
|
-
name: 'stats',
|
|
964
|
-
install(ctx) {
|
|
965
|
-
let messages = 0;
|
|
966
|
-
let chars = 0;
|
|
967
|
-
ctx.emitter.on('message', (data) => {
|
|
968
|
-
messages++;
|
|
969
|
-
chars += data.length;
|
|
970
|
-
const payload = { messages, chars };
|
|
971
|
-
ctx.emitter.emit('stats', payload);
|
|
972
|
-
if (data.includes(keyword)) {
|
|
973
|
-
ctx.emitter.emit('keyword', { keyword, at: chars });
|
|
974
|
-
}
|
|
975
|
-
});
|
|
976
|
-
// 新一轮连接时归零统计
|
|
977
|
-
ctx.emitter.on('connecting', () => {
|
|
978
|
-
messages = 0;
|
|
979
|
-
chars = 0;
|
|
980
|
-
ctx.emitter.emit('stats', { messages, chars });
|
|
981
|
-
});
|
|
982
|
-
},
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/**
|
|
987
|
-
* @file 插件化 SSE SDK 统一出口
|
|
988
|
-
*/
|
|
989
|
-
/**
|
|
990
|
-
* 推荐插件组合(无副作用,不含日志/鉴权等业务相关插件)
|
|
991
|
-
* @description 自动重连 + 心跳检测 + 消息节流
|
|
992
|
-
*/
|
|
993
|
-
function recommendedPlugins() {
|
|
994
|
-
return [reconnectPlugin(), heartbeatPlugin(), throttlePlugin()];
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
exports.Emitter = Emitter;
|
|
998
|
-
exports.PluginSSE = PluginSSE;
|
|
999
|
-
exports.authPlugin = authPlugin;
|
|
1000
|
-
exports.createMpTransport = createMpTransport;
|
|
1001
|
-
exports.createSSEParser = createSSEParser;
|
|
1002
|
-
exports.defaultScript = defaultScript;
|
|
1003
|
-
exports.detectTransport = detectTransport;
|
|
1004
|
-
exports.heartbeatPlugin = heartbeatPlugin;
|
|
1005
|
-
exports.loggerPlugin = loggerPlugin;
|
|
1006
|
-
exports.mockTransport = mockTransport;
|
|
1007
|
-
exports.recommendedPlugins = recommendedPlugins;
|
|
1008
|
-
exports.reconnectPlugin = reconnectPlugin;
|
|
1009
|
-
exports.statsPlugin = statsPlugin;
|
|
1010
|
-
exports.throttlePlugin = throttlePlugin;
|
|
1011
|
-
exports.webFetchTransport = webFetchTransport;
|
|
1012
|
-
//# sourceMappingURL=index.cjs.map
|