@xuzhiyang/syncvar 1.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 +15 -0
- package/README.md +444 -0
- package/dist/syncvar.js +2944 -0
- package/dist/syncvar.min.js +5 -0
- package/dist/syncvar.min.mjs +5 -0
- package/dist/syncvar.mjs +2932 -0
- package/dist/types.d.ts +317 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xuzhiyang
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
# WaterBear SDK - WebSocket 变量同步 SDK
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/syncvar)
|
|
4
|
+
|
|
5
|
+
WaterBear SDK 提供一套基于 WebSocket 的变量同步、锁、RPC、事件订阅和 Worker 执行能力,帮助快速搭建协同编辑、状态共享与后台任务管理场景。
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
- 🔄 **实时变量同步** - 自动检测变化并同步到所有客户端
|
|
10
|
+
- 🔒 **分布式锁** - 防止并发写入冲突
|
|
11
|
+
- 📡 **RPC 调用** - 远程过程调用,支持双向通信
|
|
12
|
+
- 📢 **事件订阅** - 基于 SSE 的服务器推送事件
|
|
13
|
+
- 🚀 **MessagePack** - 高效二进制传输格式,减少 30-50% 数据量
|
|
14
|
+
- 🛠️ **Worker 执行** - 远程代码执行能力
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install syncvar
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 快速开始
|
|
23
|
+
|
|
24
|
+
### Node.js 环境
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
import { WebSocketClient, SyncVar } from 'syncvar';
|
|
28
|
+
|
|
29
|
+
// 创建 WebSocket 连接
|
|
30
|
+
const ws = await WebSocketClient({
|
|
31
|
+
namespace: 'room1',
|
|
32
|
+
packFormat: 1, // MessagePack 格式(推荐)
|
|
33
|
+
onopen: () => console.log('已连接'),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 创建 SyncVar 并绑定
|
|
37
|
+
const syncVar = new SyncVar();
|
|
38
|
+
await ws.bind(syncVar);
|
|
39
|
+
|
|
40
|
+
// 同步变量
|
|
41
|
+
const { position } = await syncVar.sync('position', {
|
|
42
|
+
reset: { position: { x: 0, y: 0 } },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 监听变化
|
|
46
|
+
position.watch((value) => {
|
|
47
|
+
console.log('position 更新:', value);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// 修改变量(自动同步)
|
|
51
|
+
position.x = 10;
|
|
52
|
+
await syncVar.waitUpdated();
|
|
53
|
+
|
|
54
|
+
// RPC 调用
|
|
55
|
+
await syncVar.rpcBind(function rpc_add({ a, b }) {
|
|
56
|
+
return a + b;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = await syncVar.rpcCall('rpc_add', { a: 10, b: 20 });
|
|
60
|
+
console.log('RPC 结果:', result); // 30
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 浏览器环境
|
|
64
|
+
|
|
65
|
+
#### 方式 1:ESM 模块(推荐)
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<script type="module">
|
|
69
|
+
import { WebSocketClient, SyncVar } from 'syncvar';
|
|
70
|
+
|
|
71
|
+
const ws = await WebSocketClient({
|
|
72
|
+
namespace: 'room1',
|
|
73
|
+
packFormat: 1,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const syncVar = new SyncVar();
|
|
77
|
+
await ws.bind(syncVar);
|
|
78
|
+
|
|
79
|
+
const { counter } = await syncVar.sync('counter', {
|
|
80
|
+
reset: { counter: 0 },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
counter.watch((value) => {
|
|
84
|
+
document.getElementById('counter').textContent = value;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
counter.value++;
|
|
88
|
+
</script>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### 方式 2:CDN 引入
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<!-- 引入 UMD 版本 -->
|
|
95
|
+
<script src="https://unpkg.com/syncvar/dist/syncvar.js"></script>
|
|
96
|
+
|
|
97
|
+
<script>
|
|
98
|
+
// SDK 挂载到全局变量 WaterBear
|
|
99
|
+
const { WebSocketClient, SyncVar } = window.WaterBear;
|
|
100
|
+
|
|
101
|
+
(async function() {
|
|
102
|
+
const ws = await WebSocketClient({
|
|
103
|
+
namespace: 'room1',
|
|
104
|
+
packFormat: 1,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const syncVar = new SyncVar();
|
|
108
|
+
await ws.bind(syncVar);
|
|
109
|
+
|
|
110
|
+
const { counter } = await syncVar.sync('counter', {
|
|
111
|
+
reset: { counter: 0 },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
counter.watch((value) => {
|
|
115
|
+
console.log('计数器:', value);
|
|
116
|
+
});
|
|
117
|
+
})();
|
|
118
|
+
</script>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### CommonJS 环境
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const { WebSocketClient, SyncVar } = require('syncvar');
|
|
125
|
+
|
|
126
|
+
const ws = await WebSocketClient({
|
|
127
|
+
namespace: 'room1',
|
|
128
|
+
packFormat: 1,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const syncVar = new SyncVar();
|
|
132
|
+
await ws.bind(syncVar);
|
|
133
|
+
|
|
134
|
+
// 使用方式与 ESM 相同
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### TypeScript 环境
|
|
138
|
+
|
|
139
|
+
SDK 内置完整的 TypeScript 类型定义,开箱即用:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { WebSocketClient, SyncVar, NetPackFormat } from 'syncvar';
|
|
143
|
+
|
|
144
|
+
interface Position {
|
|
145
|
+
x: number;
|
|
146
|
+
y: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 完整的类型推断
|
|
150
|
+
const ws = await WebSocketClient({
|
|
151
|
+
namespace: 'room1',
|
|
152
|
+
packFormat: NetPackFormat.MessagePack,
|
|
153
|
+
onopen: (ws: WebSocket, event: Event) => {
|
|
154
|
+
console.log('已连接');
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const syncVar = new SyncVar({ delay: 100 });
|
|
159
|
+
await ws.bind(syncVar);
|
|
160
|
+
|
|
161
|
+
// 类型推断为 Position
|
|
162
|
+
const { position } = await syncVar.sync<Position>('position', {
|
|
163
|
+
reset: { position: { x: 0, y: 0 } },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 类型安全
|
|
167
|
+
position.watch((value: Position) => {
|
|
168
|
+
console.log('position 更新:', value.x, value.y);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// RPC 调用也有类型检查
|
|
172
|
+
await syncVar.rpcBind(function rpc_add({ a, b }: { a: number; b: number }) {
|
|
173
|
+
return a + b;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result: number = await syncVar.rpcCall('rpc_add', { a: 10, b: 20 });
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**TypeScript 类型包含:**
|
|
180
|
+
- 所有 API 方法的类型定义
|
|
181
|
+
- 完整的接口和枚举
|
|
182
|
+
- 详细的 JSDoc 文档(IDE 中自动显示)
|
|
183
|
+
- 浏览器全局类型(UMD 模式下 `window.WaterBear`)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
## 使用场景示例
|
|
187
|
+
|
|
188
|
+
### 协同编辑器
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
// 多人实时编辑文档
|
|
192
|
+
const document = await syncVar.sync('document', {
|
|
193
|
+
reset: { document: { title: '', content: '' } },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
document.watch((value) => {
|
|
197
|
+
// 更新编辑器内容
|
|
198
|
+
editor.setValue(value.content);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 编辑器变化时同步
|
|
202
|
+
editor.on('change', () => {
|
|
203
|
+
document.content = editor.getValue();
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### 实时计数器
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
const { counter } = await syncVar.sync('counter', {
|
|
211
|
+
reset: { counter: 0 },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// 获取锁后安全修改
|
|
215
|
+
await counter.lock();
|
|
216
|
+
counter.value++;
|
|
217
|
+
// 锁在下一个 sync 时自动释放
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 在线用户列表
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
const { users } = await syncVar.sync('users', {
|
|
224
|
+
reset: { users: [] },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
users.watch((value) => {
|
|
228
|
+
updateUserListUI(value);
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 1. API 文档
|
|
235
|
+
|
|
236
|
+
## 3. WebSocketClient
|
|
237
|
+
|
|
238
|
+
### 创建方式
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const ws = await WebSocketClient(options);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 参数说明
|
|
245
|
+
|
|
246
|
+
| 参数 | 类型 | 描述 |
|
|
247
|
+
| --- | --- | --- |
|
|
248
|
+
| `options.url` | `string` | 可选:WebSocket 服务地址,默认 `wss://varserver.popx.com/ws/syncvar`。 |
|
|
249
|
+
| `options.namespace` | `string` | 可选:命名空间/房间,隔离不同业务。 |
|
|
250
|
+
| `options.guid` | `string` | 可选:客户端 GUID,用于恢复会话或追踪。 |
|
|
251
|
+
| `options.packFormat` | `number` | 可选:数据传输格式,`0` 为 JSON 格式,`1` 为 MessagePack 格式,默认 `1`(MessagePack)。 |
|
|
252
|
+
| `options.onopen` | `(ws, event) => void` | 可选:连接成功后的回调。 |
|
|
253
|
+
| `options.onclose` | `(event) => void` | 可选:连接关闭时的回调。 |
|
|
254
|
+
| `options.onerror` | `(error) => void` | 可选:连接或消息处理异常时的回调。 |
|
|
255
|
+
| `options.timeout` | `number` | 可选:连接超时时间(毫秒),默认 `5000`。 |
|
|
256
|
+
| `options.maxReconnectCount` | `number` | 可选:最大重连次数,默认 `100`。 |
|
|
257
|
+
|
|
258
|
+
### 使用步骤
|
|
259
|
+
|
|
260
|
+
1. 创建连接:`await WebSocketClient(options)`。
|
|
261
|
+
2. 处理 `namespace`、`guid`、`packFormat`、`onopen`/`onerror`/`onclose` 以保证可观测性。
|
|
262
|
+
3. 绑定:`await ws.bind(new SyncVar())` 或 `await ws.bind(new Worker(...))`。
|
|
263
|
+
|
|
264
|
+
### 数据传输格式
|
|
265
|
+
|
|
266
|
+
WebSocketClient 支持两种数据传输格式:
|
|
267
|
+
|
|
268
|
+
- **JSON 格式** (`packFormat = 0`):传统文本传输,向后兼容
|
|
269
|
+
- **MessagePack 格式** (`packFormat = 1`):高效二进制传输,默认推荐
|
|
270
|
+
|
|
271
|
+
**MessagePack 优势**:
|
|
272
|
+
- 相比 JSON 减少 30-50% 数据传输量
|
|
273
|
+
- 更快的编码解码速度
|
|
274
|
+
- 支持更多数据类型(如二进制数据)
|
|
275
|
+
- 自动与服务器协商最优格式
|
|
276
|
+
|
|
277
|
+
**使用示例**:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
// 使用 MessagePack 格式(推荐)
|
|
281
|
+
const ws = await WebSocketClient({
|
|
282
|
+
namespace: 'room1',
|
|
283
|
+
packFormat: 1, // MessagePack 格式
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// 或使用 JSON 格式(兼容旧版本)
|
|
287
|
+
const wsJson = await WebSocketClient({
|
|
288
|
+
namespace: 'room1',
|
|
289
|
+
packFormat: 0, // JSON 格式
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 示例
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
// MessagePack 格式(推荐)
|
|
297
|
+
const ws = await WebSocketClient({
|
|
298
|
+
namespace: 'room1',
|
|
299
|
+
guid: 'client-123',
|
|
300
|
+
packFormat: 1, // 使用高效的 MessagePack 格式
|
|
301
|
+
onopen: () => console.log('connected'),
|
|
302
|
+
onerror: (err) => console.error('ws error', err),
|
|
303
|
+
});
|
|
304
|
+
await ws.bind(new SyncVar());
|
|
305
|
+
|
|
306
|
+
// JSON 格式(向后兼容)
|
|
307
|
+
const wsJson = await WebSocketClient({
|
|
308
|
+
namespace: 'room1',
|
|
309
|
+
guid: 'client-123',
|
|
310
|
+
packFormat: 0, // 使用传统 JSON 格式
|
|
311
|
+
onopen: () => console.log('connected'),
|
|
312
|
+
onerror: (err) => console.error('ws error', err),
|
|
313
|
+
});
|
|
314
|
+
await wsJson.bind(new SyncVar());
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## 4. SyncVar
|
|
318
|
+
|
|
319
|
+
### 创建与参数
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
const syncVar = new SyncVar({ delay?, promiseTimeout? });
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
| 参数 | 类型 | 描述 |
|
|
326
|
+
| --- | --- | --- |
|
|
327
|
+
| `delay` | `number` | 可选:变更打包延迟(毫秒),合并频繁修改,默认 `50`。 |
|
|
328
|
+
| `promiseTimeout` | `number` | 可选:与服务端交互的 Promise 超时时间(毫秒),默认 `10000`。 |
|
|
329
|
+
|
|
330
|
+
### 核心用法
|
|
331
|
+
|
|
332
|
+
1. 绑定 `ws.bind(syncVar)`。
|
|
333
|
+
2. `await syncVar.sync(names, { reset?, store? })` 获取代理,`names` 支持字符串或数组。
|
|
334
|
+
3. 修改代理上属性自动收集 diff 并延迟发送。
|
|
335
|
+
4. 通过 `proxy.lock({ timeout? })` 安全写入,`proxy.unlock()` 释放。
|
|
336
|
+
5. `await syncVar.waitUpdated()` 等待 `$set` 回执。
|
|
337
|
+
6. `proxy.watch(cb)` 注册更新回调。
|
|
338
|
+
7. `rpcBind` / `rpcCall` 实现远程函数调用。
|
|
339
|
+
|
|
340
|
+
### 示例
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
const syncVar = new SyncVar({ delay: 100 });
|
|
344
|
+
await ws.bind(syncVar);
|
|
345
|
+
|
|
346
|
+
const { position } = await syncVar.sync('position', {
|
|
347
|
+
reset: { position: { x: 0, y: 0 } },
|
|
348
|
+
});
|
|
349
|
+
position.watch((value) => console.log('position updated', value));
|
|
350
|
+
position.x += 10;
|
|
351
|
+
await syncVar.waitUpdated();
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
await syncVar.rpcBind(function rpc_sum({ a, b }) {
|
|
356
|
+
return a + b;
|
|
357
|
+
});
|
|
358
|
+
const ret = await syncVar.rpcCall('rpc_sum', { a: 3, b: 5 });
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## 5. SSE(单向推送)
|
|
362
|
+
|
|
363
|
+
### 创建与参数
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
const sse = new SSE({ host, namespace? });
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
| 参数 | 类型 | 描述 |
|
|
370
|
+
| --- | --- | --- |
|
|
371
|
+
| `host` | `string` | 必需:SSE 服务地址。 |
|
|
372
|
+
| `namespace` | `string` | 可选:隔离订阅范围。 |
|
|
373
|
+
|
|
374
|
+
### 典型流程
|
|
375
|
+
|
|
376
|
+
1. `await sse.watch({ vars: object|array, events: object|array })`:
|
|
377
|
+
- `vars` 支持对象或数组格式。
|
|
378
|
+
- 对象:`{ varName: ({ value }) => { ... }, ... }`(键名为变量名)
|
|
379
|
+
- 数组:`[function varName({ value }) { ... }, 'var2', ...]`
|
|
380
|
+
- `events` 支持对象或数组格式。
|
|
381
|
+
- 对象:`{ eventName: ({ data }) => { ... }, ... }`
|
|
382
|
+
- 数组:`[function eventName({ data }) { ... }, 'event2', ...]`
|
|
383
|
+
2. `sse.get(name)` / `sse.getAll()` 读取快照。
|
|
384
|
+
3. `await sse.emit(eventName, data)` 发送事件。
|
|
385
|
+
4. `sse.close()` 断开连接。
|
|
386
|
+
|
|
387
|
+
### 示例
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
const sse = new SSE({ namespace: 'room1' });
|
|
391
|
+
await sse.watch({
|
|
392
|
+
vars: {
|
|
393
|
+
position: ({ value }) => console.log('position', value),
|
|
394
|
+
test: ({ value }) => console.log('test', value)
|
|
395
|
+
},
|
|
396
|
+
events: {
|
|
397
|
+
myevent: ({ data }) => console.log('event', JSON.parse(data))
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
await sse.emit('myevent', { hello: 'world' });
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## 6. Worker
|
|
404
|
+
|
|
405
|
+
### 创建与参数
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
const worker = new Worker({ onlog?, onerror?, onexit?, promiseTimeout? });
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
| 参数 | 类型 | 描述 |
|
|
412
|
+
| --- | --- | --- |
|
|
413
|
+
| `onlog` | `(args) => void` | 接收服务端日志。 |
|
|
414
|
+
| `onerror` | `(error) => void` | 接收服务端错误。 |
|
|
415
|
+
| `onexit` | `(exitInfo) => void` | 接收 worker 退出事件。 |
|
|
416
|
+
| `promiseTimeout` | `number` | 操作超时时间,默认 `10000`。 |
|
|
417
|
+
|
|
418
|
+
### 使用方式
|
|
419
|
+
|
|
420
|
+
1. `await ws.bind(worker)`。
|
|
421
|
+
2. `await worker.run({ code, name, workerData })` 启动。
|
|
422
|
+
3. `kill` / `attach` / `lists` 管理实例。
|
|
423
|
+
4. 监听 `onlog/onerror/onexit` 反馈。
|
|
424
|
+
|
|
425
|
+
### 示例
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
const worker = new Worker({
|
|
429
|
+
onlog: (args) => console.log('worker log', args),
|
|
430
|
+
});
|
|
431
|
+
await ws.bind(worker);
|
|
432
|
+
await worker.run({ code: 'console.log("hi")', name: 'task-1' });
|
|
433
|
+
const list = await worker.lists();
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
## 7. 推荐流程
|
|
437
|
+
|
|
438
|
+
1. 建立 `WebSocketClient`。
|
|
439
|
+
2. 创建 `SyncVar` 并绑定后调用 `sync()` 获取 proxy。
|
|
440
|
+
3. 注册 `watch`、调用 `lock`、使用 `waitUpdated()` 监控。
|
|
441
|
+
4. 需要 RPC 时使用 `rpcBind` / `rpcCall`。
|
|
442
|
+
5. 仅需订阅时使用 `SSE`。
|
|
443
|
+
6. 后台任务交给 `Worker` 执行。
|
|
444
|
+
7. 连接关闭或异常时调用 `syncVar.destroy()`/`sse.close()` 清理。
|