grpc-libp2p-client 0.0.37 → 0.0.39
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/dist/dc-http2/frame.cjs.js +6 -5
- package/dist/dc-http2/frame.cjs.js.map +1 -1
- package/dist/dc-http2/frame.d.ts +1 -1
- package/dist/dc-http2/frame.esm.js +6 -5
- package/dist/dc-http2/frame.esm.js.map +1 -1
- package/dist/dc-http2/hpack.cjs.js +3 -3
- package/dist/dc-http2/hpack.cjs.js.map +1 -1
- package/dist/dc-http2/hpack.esm.js +3 -3
- package/dist/dc-http2/hpack.esm.js.map +1 -1
- package/dist/dc-http2/parser.cjs.js +39 -28
- package/dist/dc-http2/parser.cjs.js.map +1 -1
- package/dist/dc-http2/parser.d.ts +7 -7
- package/dist/dc-http2/parser.esm.js +39 -28
- package/dist/dc-http2/parser.esm.js.map +1 -1
- package/dist/dc-http2/stream.cjs.js +26 -12
- package/dist/dc-http2/stream.cjs.js.map +1 -1
- package/dist/dc-http2/stream.d.ts +1 -1
- package/dist/dc-http2/stream.esm.js +26 -12
- package/dist/dc-http2/stream.esm.js.map +1 -1
- package/dist/grpc.js +245 -269
- package/dist/grpc.js.map +1 -1
- package/dist/grpc.min.js +1 -1
- package/dist/grpc.min.js.map +1 -1
- package/dist/index.cjs.js +103 -103
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +103 -103
- package/dist/index.esm.js.map +1 -1
- package/dist/stats.html +1 -1
- package/package.json +4 -4
- package/src/dc-http2/encoder.ts +3 -2
- package/src/dc-http2/hpack.ts +3 -3
- package/src/dc-http2/parser.ts +43 -38
- package/src/dc-http2/stream.ts +36 -23
- package/src/dc-http2/types.ts +1 -1
- package/src/index.ts +57 -85
package/src/dc-http2/parser.ts
CHANGED
|
@@ -8,22 +8,24 @@ type ParserOptions = {
|
|
|
8
8
|
compatibilityMode?: boolean
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const HTTP2_PREFACE = new TextEncoder().encode("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
|
|
12
|
+
|
|
11
13
|
export class HTTP2Parser {
|
|
12
14
|
buffer: Uint8Array;
|
|
13
15
|
settingsAckReceived: boolean;
|
|
14
16
|
peerSettingsReceived: boolean;
|
|
15
17
|
connectionWindowSize: number;
|
|
16
|
-
streams: Map<number,
|
|
18
|
+
streams: Map<number, unknown>;
|
|
17
19
|
defaultStreamWindowSize: number;
|
|
18
20
|
// 发送方向(对端的接收窗口)跟踪
|
|
19
21
|
sendConnWindow: number;
|
|
20
22
|
sendStreamWindows: Map<number, number>;
|
|
21
23
|
peerInitialStreamWindow: number;
|
|
22
24
|
private sendWindowWaiters: Array<() => void>;
|
|
23
|
-
onSettings?: (frameHeader:
|
|
24
|
-
onData?: (payload: Uint8Array, frameHeader:
|
|
25
|
+
onSettings?: (frameHeader: Frame) => void;
|
|
26
|
+
onData?: (payload: Uint8Array, frameHeader: Frame) => void;
|
|
25
27
|
onEnd?: () => void;
|
|
26
|
-
onHeaders?: (headers: Uint8Array, frameHeader:
|
|
28
|
+
onHeaders?: (headers: Uint8Array, frameHeader: Frame) => void;
|
|
27
29
|
onGoaway?: (info: { lastStreamId?: number; errorCode?: number }) => void;
|
|
28
30
|
onSettingsParsed?: (settings: { maxConcurrentStreams?: number; initialWindowSize?: number }) => void;
|
|
29
31
|
endFlag: boolean;
|
|
@@ -76,53 +78,58 @@ export class HTTP2Parser {
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
// 处理单个数据块
|
|
79
|
-
private _processChunk(chunk:
|
|
81
|
+
private _processChunk(chunk: Uint8Array | { subarray(): Uint8Array }): void {
|
|
80
82
|
// chunk 是 Uint8ArrayList 或 Uint8Array
|
|
81
|
-
const newData =
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const newData: Uint8Array = 'subarray' in chunk && typeof chunk.subarray === 'function'
|
|
84
|
+
? chunk.subarray()
|
|
85
|
+
: (chunk as Uint8Array);
|
|
86
|
+
|
|
87
|
+
// 原作者之前的 O(N) 内存拷贝优化被保留,去掉了存在 onEnd 竞态的 setTimeout
|
|
84
88
|
const newBuffer = new Uint8Array(this.buffer.length + newData.length);
|
|
85
89
|
newBuffer.set(this.buffer);
|
|
86
90
|
newBuffer.set(newData, this.buffer.length);
|
|
87
91
|
this.buffer = newBuffer;
|
|
88
|
-
|
|
92
|
+
|
|
89
93
|
// 持续处理所有完整的帧
|
|
90
|
-
|
|
94
|
+
let readOffset = 0;
|
|
95
|
+
while (this.buffer.length - readOffset >= 9) {
|
|
91
96
|
// 判断是否有HTTP/2前导
|
|
92
|
-
if (this.buffer.length >= 24 && this.isHttp2Preface(this.buffer)) {
|
|
93
|
-
|
|
97
|
+
if (this.buffer.length - readOffset >= 24 && this.isHttp2Preface(this.buffer.subarray(readOffset))) {
|
|
98
|
+
readOffset += 24;
|
|
94
99
|
// 发送SETTINGS帧
|
|
95
100
|
const settingFrame = Http2Frame.createSettingsFrame();
|
|
96
|
-
this.writer.write(settingFrame
|
|
97
|
-
|
|
101
|
+
this.writer.write(settingFrame);
|
|
102
|
+
continue;
|
|
98
103
|
}
|
|
99
|
-
|
|
104
|
+
|
|
105
|
+
const frameHeader = this._parseFrameHeader(this.buffer.subarray(readOffset));
|
|
100
106
|
const totalFrameLength = 9 + frameHeader.length;
|
|
101
107
|
|
|
102
108
|
// 检查是否有完整的帧
|
|
103
|
-
if (this.buffer.length < totalFrameLength) {
|
|
109
|
+
if (this.buffer.length - readOffset < totalFrameLength) {
|
|
104
110
|
break;
|
|
105
111
|
}
|
|
106
112
|
// 获取完整帧数据
|
|
107
|
-
const frameData = this.buffer.
|
|
113
|
+
const frameData = this.buffer.subarray(readOffset, readOffset + totalFrameLength);
|
|
108
114
|
|
|
109
115
|
// 处理不同类型的帧
|
|
110
116
|
this._handleFrame(frameHeader, frameData).catch((err) => {
|
|
111
117
|
console.error("Error handling frame:", err);
|
|
112
118
|
});
|
|
113
119
|
|
|
114
|
-
//
|
|
115
|
-
|
|
120
|
+
// 移动偏移量
|
|
121
|
+
readOffset += totalFrameLength;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (readOffset > 0) {
|
|
125
|
+
this.buffer = this.buffer.slice(readOffset);
|
|
116
126
|
}
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
private isHttp2Preface(buffer: Uint8Array): boolean {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (buffer.length < PREFACE.length) return false;
|
|
124
|
-
for (let i = 0; i < PREFACE.length; i++) {
|
|
125
|
-
if (buffer[i] !== PREFACE[i]) return false;
|
|
130
|
+
if (buffer.length < HTTP2_PREFACE.length) return false;
|
|
131
|
+
for (let i = 0; i < HTTP2_PREFACE.length; i++) {
|
|
132
|
+
if (buffer[i] !== HTTP2_PREFACE[i]) return false;
|
|
126
133
|
}
|
|
127
134
|
return true;
|
|
128
135
|
}
|
|
@@ -209,7 +216,7 @@ export class HTTP2Parser {
|
|
|
209
216
|
async waitForSendWindow(streamId: number, minBytes: number = 1, timeoutMs: number = 30000): Promise<void> {
|
|
210
217
|
const start = Date.now();
|
|
211
218
|
return new Promise((resolve, reject) => {
|
|
212
|
-
let interval:
|
|
219
|
+
let interval: ReturnType<typeof setInterval> | null = null;
|
|
213
220
|
let settled = false;
|
|
214
221
|
const check = () => {
|
|
215
222
|
const { conn, stream } = this.getSendWindows(streamId);
|
|
@@ -319,7 +326,7 @@ export class HTTP2Parser {
|
|
|
319
326
|
this.peerSettingsReceived = true;
|
|
320
327
|
// 唤醒等待窗口(以防部分实现通过 SETTINGS 改变有效窗口)
|
|
321
328
|
const waiters = this.sendWindowWaiters.splice(0);
|
|
322
|
-
waiters.forEach(fn => { try { fn(); } catch {} });
|
|
329
|
+
waiters.forEach(fn => { try { fn(); } catch (e) { console.debug('waiter error', e); } });
|
|
323
330
|
}
|
|
324
331
|
break;
|
|
325
332
|
|
|
@@ -336,7 +343,7 @@ export class HTTP2Parser {
|
|
|
336
343
|
frameHeader.streamId,
|
|
337
344
|
frameHeader.length ?? 0
|
|
338
345
|
);
|
|
339
|
-
this.writer.write(streamWindowUpdate
|
|
346
|
+
this.writer.write(streamWindowUpdate);
|
|
340
347
|
}
|
|
341
348
|
|
|
342
349
|
// 更新连接级别的窗口
|
|
@@ -344,7 +351,7 @@ export class HTTP2Parser {
|
|
|
344
351
|
0,
|
|
345
352
|
frameHeader.length ?? 0
|
|
346
353
|
);
|
|
347
|
-
this.writer.write(connWindowUpdate
|
|
354
|
+
this.writer.write(connWindowUpdate);
|
|
348
355
|
} catch (err) {
|
|
349
356
|
console.error("[HTTP2] Error sending window update:", err);
|
|
350
357
|
}
|
|
@@ -381,8 +388,7 @@ export class HTTP2Parser {
|
|
|
381
388
|
// 处理窗口更新帧
|
|
382
389
|
this.handleWindowUpdateFrame(
|
|
383
390
|
frameHeader,
|
|
384
|
-
frameData
|
|
385
|
-
frameHeader.streamId
|
|
391
|
+
frameData
|
|
386
392
|
);
|
|
387
393
|
// 更新发送窗口(对端接收窗口)
|
|
388
394
|
try {
|
|
@@ -394,8 +400,8 @@ export class HTTP2Parser {
|
|
|
394
400
|
this.sendStreamWindows.set(frameHeader.streamId, cur + inc);
|
|
395
401
|
}
|
|
396
402
|
const waiters = this.sendWindowWaiters.splice(0);
|
|
397
|
-
waiters.forEach(fn => { try { fn(); } catch {} });
|
|
398
|
-
} catch
|
|
403
|
+
waiters.forEach(fn => { try { fn(); } catch (e) { console.debug('waiter error', e); } });
|
|
404
|
+
} catch { /* ignore WINDOW_UPDATE parse errors */ }
|
|
399
405
|
break;
|
|
400
406
|
case FRAME_TYPES.PING:
|
|
401
407
|
// 处理PING帧
|
|
@@ -415,7 +421,7 @@ export class HTTP2Parser {
|
|
|
415
421
|
console.warn('[HTTP2] GOAWAY received');
|
|
416
422
|
info = {};
|
|
417
423
|
}
|
|
418
|
-
} catch {}
|
|
424
|
+
} catch { /* ignore GOAWAY parse errors */ }
|
|
419
425
|
try {
|
|
420
426
|
this.onGoaway?.(info ?? {});
|
|
421
427
|
} catch (err) {
|
|
@@ -473,7 +479,7 @@ export class HTTP2Parser {
|
|
|
473
479
|
// 反馈PONG帧
|
|
474
480
|
const pongFrame = Http2Frame.createPongFrame(frameData.slice(9));
|
|
475
481
|
try {
|
|
476
|
-
this.writer.write(pongFrame
|
|
482
|
+
this.writer.write(pongFrame);
|
|
477
483
|
} catch (error) {
|
|
478
484
|
console.error("Error sending PONG frame:", error);
|
|
479
485
|
throw error;
|
|
@@ -489,7 +495,7 @@ export class HTTP2Parser {
|
|
|
489
495
|
return;
|
|
490
496
|
}
|
|
491
497
|
// 如果是0 ,则不设置超时
|
|
492
|
-
let timeout:
|
|
498
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
493
499
|
if (waitTime > 0) {
|
|
494
500
|
timeout = setTimeout(() => {
|
|
495
501
|
clearInterval(interval);
|
|
@@ -561,8 +567,7 @@ export class HTTP2Parser {
|
|
|
561
567
|
// 处理 WINDOW_UPDATE 帧
|
|
562
568
|
handleWindowUpdateFrame(
|
|
563
569
|
frameHeader: Frame,
|
|
564
|
-
payload: Uint8Array
|
|
565
|
-
streamId: number
|
|
570
|
+
payload: Uint8Array
|
|
566
571
|
) {
|
|
567
572
|
try {
|
|
568
573
|
const windowUpdate = this.parseWindowUpdateFrame(payload, frameHeader);
|
package/src/dc-http2/stream.ts
CHANGED
|
@@ -41,13 +41,13 @@ export class StreamWriter {
|
|
|
41
41
|
private lastBackpressureCheck = 0 // 添加时间戳缓存
|
|
42
42
|
private bytesDrained = 0 // 统计下游实际消化的字节数
|
|
43
43
|
private lastDrainEventAt = 0
|
|
44
|
-
private watchdogTimer:
|
|
44
|
+
private watchdogTimer: ReturnType<typeof setTimeout> | undefined
|
|
45
45
|
private stallStartAt = 0
|
|
46
46
|
private lastBytesDrainedSeen = 0
|
|
47
47
|
private lastBpWarnAt = 0
|
|
48
48
|
private isHandlingError = false // 防止重复错误处理
|
|
49
49
|
|
|
50
|
-
private log?: { trace?: (...args:
|
|
50
|
+
private log?: { trace?: (...args: unknown[]) => void }
|
|
51
51
|
|
|
52
52
|
constructor(
|
|
53
53
|
private stream: Stream,
|
|
@@ -58,7 +58,7 @@ export class StreamWriter {
|
|
|
58
58
|
throw new Error('StreamWriter requires a valid stream object')
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
this.log = { trace: (...args:
|
|
61
|
+
this.log = { trace: (...args: unknown[]) => console.debug('[StreamWriter]', ...args) }
|
|
62
62
|
|
|
63
63
|
if (options){
|
|
64
64
|
this.options = {
|
|
@@ -172,10 +172,10 @@ export class StreamWriter {
|
|
|
172
172
|
// 等待 drain 事件
|
|
173
173
|
await this.stream.onDrain()
|
|
174
174
|
}
|
|
175
|
-
} catch (err:
|
|
175
|
+
} catch (err: unknown) {
|
|
176
176
|
// Gracefully handle stream closing errors - 不要传递到 handleError
|
|
177
|
-
const errMsg = err.message
|
|
178
|
-
if (err.name === 'StreamStateError' ||
|
|
177
|
+
const errMsg = (err instanceof Error ? err.message : '').toLowerCase()
|
|
178
|
+
if ((err instanceof Error && err.name === 'StreamStateError') ||
|
|
179
179
|
errMsg.includes('closing') ||
|
|
180
180
|
errMsg.includes('closed') ||
|
|
181
181
|
errMsg.includes('write to a stream that is closed')) {
|
|
@@ -188,6 +188,7 @@ export class StreamWriter {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
private createTransform() {
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
191
192
|
const self = this;
|
|
192
193
|
return async function* (source: AsyncIterable<Uint8Array>) {
|
|
193
194
|
for await (const chunk of source) {
|
|
@@ -197,8 +198,8 @@ export class StreamWriter {
|
|
|
197
198
|
// 因此这里统计的 bytesDrained 更接近实际被 sink 消费的字节数
|
|
198
199
|
try {
|
|
199
200
|
// 在下游消费后再扣减待消费队列,避免误判“next 没取”
|
|
200
|
-
if (
|
|
201
|
-
|
|
201
|
+
if (self.p._queueSize != null) {
|
|
202
|
+
self.p._queueSize = Math.max(0, self.p._queueSize - chunk.byteLength)
|
|
202
203
|
}
|
|
203
204
|
self.bytesDrained += chunk.byteLength
|
|
204
205
|
const now = Date.now()
|
|
@@ -218,9 +219,10 @@ export class StreamWriter {
|
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
// 简单的卡顿看门狗:当队列长期高位且 bytesDrained 无进展时发出 stalled 事件
|
|
222
|
+
// 使用递归 setTimeout 而非 setInterval,避免标签页后台恢复时回调堆积导致 Violation
|
|
221
223
|
private startWatchdog(intervalMs: number = 500, stallMs: number = 1500) {
|
|
222
224
|
if (this.watchdogTimer) return
|
|
223
|
-
|
|
225
|
+
const tick = () => {
|
|
224
226
|
if (this.abortController.signal.aborted) return
|
|
225
227
|
const baseThreshold = this.options.bufferSize! * 0.7
|
|
226
228
|
const q = this.queueSize
|
|
@@ -229,7 +231,13 @@ export class StreamWriter {
|
|
|
229
231
|
if (this.lastBytesDrainedSeen === this.bytesDrained) {
|
|
230
232
|
if (!this.stallStartAt) this.stallStartAt = now
|
|
231
233
|
if (now - this.stallStartAt >= stallMs) {
|
|
232
|
-
|
|
234
|
+
// 异步触发事件,让当前 tick 立即返回,避免同步事件处理器阻塞主线程
|
|
235
|
+
const detail = { queueSize: q, drained: this.bytesDrained, sinceMs: now - this.stallStartAt }
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
if (!this.abortController.signal.aborted) {
|
|
238
|
+
this.dispatchEvent(new CustomEvent('stalled', { detail }))
|
|
239
|
+
}
|
|
240
|
+
}, 0)
|
|
233
241
|
// 避免持续触发,推进起点
|
|
234
242
|
this.stallStartAt = now
|
|
235
243
|
}
|
|
@@ -242,10 +250,13 @@ export class StreamWriter {
|
|
|
242
250
|
// 队列回落,重置
|
|
243
251
|
this.stallStartAt = 0
|
|
244
252
|
}
|
|
245
|
-
|
|
253
|
+
// 本次 tick 完成后再安排下一次,不会因主线程繁忙而堆积
|
|
254
|
+
this.watchdogTimer = setTimeout(tick, intervalMs)
|
|
255
|
+
}
|
|
256
|
+
this.watchdogTimer = setTimeout(tick, intervalMs)
|
|
246
257
|
}
|
|
247
258
|
|
|
248
|
-
async write(data: ArrayBuffer | Blob | string): Promise<void> {
|
|
259
|
+
async write(data: ArrayBuffer | Uint8Array | Blob | string): Promise<void> {
|
|
249
260
|
// 静默处理 aborted 状态,避免在正常的流关闭场景下抛出错误
|
|
250
261
|
if (this.abortController.signal.aborted) {
|
|
251
262
|
return Promise.resolve()
|
|
@@ -272,9 +283,10 @@ export class StreamWriter {
|
|
|
272
283
|
})
|
|
273
284
|
}
|
|
274
285
|
|
|
275
|
-
private async convertToBuffer(data: ArrayBuffer | Blob | string): Promise<ArrayBuffer> {
|
|
286
|
+
private async convertToBuffer(data: ArrayBuffer | Uint8Array | Blob | string): Promise<ArrayBuffer> {
|
|
276
287
|
if (data instanceof Blob) return data.arrayBuffer()
|
|
277
|
-
if (typeof data === 'string') return new TextEncoder().encode(data).buffer
|
|
288
|
+
if (typeof data === 'string') return new TextEncoder().encode(data).buffer as ArrayBuffer
|
|
289
|
+
if (data instanceof Uint8Array) return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer
|
|
278
290
|
return data
|
|
279
291
|
}
|
|
280
292
|
|
|
@@ -438,9 +450,9 @@ export class StreamWriter {
|
|
|
438
450
|
if (this.stream && typeof this.stream.close === 'function') {
|
|
439
451
|
try {
|
|
440
452
|
await this.stream.close()
|
|
441
|
-
} catch (err:
|
|
453
|
+
} catch (err: unknown) {
|
|
442
454
|
// 忽略关闭已关闭流的错误
|
|
443
|
-
const errMsg = err.message
|
|
455
|
+
const errMsg = (err instanceof Error ? err.message : '').toLowerCase()
|
|
444
456
|
if (!errMsg.includes('closed') && !errMsg.includes('closing')) {
|
|
445
457
|
this.log?.trace?.('Stream close error:', err)
|
|
446
458
|
}
|
|
@@ -461,15 +473,16 @@ export class StreamWriter {
|
|
|
461
473
|
// 先检查流状态,避免在已关闭的流上调用 abort
|
|
462
474
|
if (this.stream && typeof this.stream.abort === 'function') {
|
|
463
475
|
// 检查流的状态,避免操作已关闭的流
|
|
464
|
-
const streamState = (this.stream as
|
|
476
|
+
const streamState = (this.stream as { status?: string; state?: string }).status ||
|
|
477
|
+
(this.stream as { status?: string; state?: string }).state
|
|
465
478
|
if (streamState !== 'closed' && streamState !== 'closing') {
|
|
466
479
|
this.stream.abort(new Error(reason))
|
|
467
480
|
}
|
|
468
481
|
}
|
|
469
|
-
} catch (err:
|
|
482
|
+
} catch (err: unknown) {
|
|
470
483
|
// Stream may already be closed, ignore all stream-related errors
|
|
471
484
|
// 完全忽略流操作错误,避免在错误处理中再次抛出错误
|
|
472
|
-
const errMsg = err.message
|
|
485
|
+
const errMsg = (err instanceof Error ? err.message : '').toLowerCase()
|
|
473
486
|
if (!errMsg.includes('closed') && !errMsg.includes('closing') && !errMsg.includes('write')) {
|
|
474
487
|
this.log?.trace?.('Stream abort error:', err)
|
|
475
488
|
}
|
|
@@ -494,17 +507,17 @@ export class StreamWriter {
|
|
|
494
507
|
|
|
495
508
|
// 立即拒绝所有待处理的写入任务,避免它们继续执行
|
|
496
509
|
const pendingTasks = this.writeQueue.splice(0)
|
|
497
|
-
pendingTasks.forEach(
|
|
510
|
+
pendingTasks.forEach(() => {
|
|
498
511
|
// 这些任务的 Promise 会在执行时因为检查到 aborted 而被拒绝
|
|
499
512
|
})
|
|
500
513
|
|
|
501
514
|
try {
|
|
502
515
|
this.p.end()
|
|
503
|
-
} catch
|
|
516
|
+
} catch {
|
|
504
517
|
// Ignore errors when ending pushable
|
|
505
518
|
}
|
|
506
519
|
|
|
507
|
-
if (this.watchdogTimer) {
|
|
520
|
+
if (this.watchdogTimer) { clearTimeout(this.watchdogTimer); this.watchdogTimer = undefined }
|
|
508
521
|
}
|
|
509
522
|
|
|
510
523
|
// 等待内部队列被下游完全消费(用于在结束前确保尽量发送完数据)
|
|
@@ -526,7 +539,7 @@ export class StreamWriter {
|
|
|
526
539
|
}
|
|
527
540
|
|
|
528
541
|
// 事件系统
|
|
529
|
-
private listeners = new Map<string,
|
|
542
|
+
private listeners = new Map<string, ((event: CustomEvent) => void)[]>()
|
|
530
543
|
|
|
531
544
|
addEventListener(type: string, callback: (event: CustomEvent) => void) {
|
|
532
545
|
const handlers = this.listeners.get(type) || []
|