grpc-libp2p-client 0.0.38 → 0.0.40

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.
@@ -41,11 +41,13 @@ export class StreamWriter {
41
41
  private lastBackpressureCheck = 0 // 添加时间戳缓存
42
42
  private bytesDrained = 0 // 统计下游实际消化的字节数
43
43
  private lastDrainEventAt = 0
44
- private watchdogTimer: ReturnType<typeof setInterval> | undefined
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
+ /** drain 事件驱动等待者,替代 flush() 中的 setInterval 轮询 */
50
+ private drainWaiters: Array<() => void> = []
49
51
 
50
52
  private log?: { trace?: (...args: unknown[]) => void }
51
53
 
@@ -169,8 +171,9 @@ export class StreamWriter {
169
171
  // 使用 stream.send() 发送数据,返回 false 表示需要等待 drain
170
172
  const canContinue = this.stream.send(chunk)
171
173
  if (!canContinue) {
172
- // 等待 drain 事件
173
- await this.stream.onDrain()
174
+ // 传入 abort signal,当流被 abort 时 onDrain() 会立即 reject,
175
+ // 避免在 abort 路径下永久挂住
176
+ await this.stream.onDrain({ signal: this.abortController.signal })
174
177
  }
175
178
  } catch (err: unknown) {
176
179
  // Gracefully handle stream closing errors - 不要传递到 handleError
@@ -185,6 +188,9 @@ export class StreamWriter {
185
188
  throw err
186
189
  }
187
190
  }
191
+ // pipeline 正常结束(stream 关闭或 pushable 耗尽)—— 确保资源清理
192
+ // 若已通过 abort() 触发则 cleanup() 内部幂等处理
193
+ this.cleanup()
188
194
  }
189
195
 
190
196
  private createTransform() {
@@ -207,6 +213,11 @@ export class StreamWriter {
207
213
  self.lastDrainEventAt = now
208
214
  self.dispatchEvent(new CustomEvent('drain', { detail: { drained: self.bytesDrained, queueSize: self.queueSize } }))
209
215
  }
216
+ // 唤醒所有在等 flush() 或背压解除 的 drainWaiters(队列降低时就可唤醒)
217
+ if (self.drainWaiters.length > 0) {
218
+ const ws = self.drainWaiters.splice(0);
219
+ for (const fn of ws) { try { fn(); } catch { /* ignore */ } }
220
+ }
210
221
  // 记录本次已消耗字节,用于看门狗判断是否前进
211
222
  self.lastBytesDrainedSeen = self.bytesDrained
212
223
  } catch (err) {
@@ -219,9 +230,10 @@ export class StreamWriter {
219
230
  }
220
231
 
221
232
  // 简单的卡顿看门狗:当队列长期高位且 bytesDrained 无进展时发出 stalled 事件
233
+ // 使用递归 setTimeout 而非 setInterval,避免标签页后台恢复时回调堆积导致 Violation
222
234
  private startWatchdog(intervalMs: number = 500, stallMs: number = 1500) {
223
235
  if (this.watchdogTimer) return
224
- this.watchdogTimer = setInterval(() => {
236
+ const tick = () => {
225
237
  if (this.abortController.signal.aborted) return
226
238
  const baseThreshold = this.options.bufferSize! * 0.7
227
239
  const q = this.queueSize
@@ -230,7 +242,13 @@ export class StreamWriter {
230
242
  if (this.lastBytesDrainedSeen === this.bytesDrained) {
231
243
  if (!this.stallStartAt) this.stallStartAt = now
232
244
  if (now - this.stallStartAt >= stallMs) {
233
- this.dispatchEvent(new CustomEvent('stalled', { detail: { queueSize: q, drained: this.bytesDrained, sinceMs: now - this.stallStartAt } }))
245
+ // 异步触发事件,让当前 tick 立即返回,避免同步事件处理器阻塞主线程
246
+ const detail = { queueSize: q, drained: this.bytesDrained, sinceMs: now - this.stallStartAt }
247
+ setTimeout(() => {
248
+ if (!this.abortController.signal.aborted) {
249
+ this.dispatchEvent(new CustomEvent('stalled', { detail }))
250
+ }
251
+ }, 0)
234
252
  // 避免持续触发,推进起点
235
253
  this.stallStartAt = now
236
254
  }
@@ -243,7 +261,10 @@ export class StreamWriter {
243
261
  // 队列回落,重置
244
262
  this.stallStartAt = 0
245
263
  }
246
- }, intervalMs)
264
+ // 本次 tick 完成后再安排下一次,不会因主线程繁忙而堆积
265
+ this.watchdogTimer = setTimeout(tick, intervalMs)
266
+ }
267
+ this.watchdogTimer = setTimeout(tick, intervalMs)
247
268
  }
248
269
 
249
270
  async write(data: ArrayBuffer | Uint8Array | Blob | string): Promise<void> {
@@ -281,10 +302,11 @@ export class StreamWriter {
281
302
  }
282
303
 
283
304
  private async writeChunks(buffer: ArrayBuffer) {
284
- for (let offset = 0; offset < buffer.byteLength; offset += this.options.chunkSize!) {
285
- const end = Math.min(offset + this.options.chunkSize!, buffer.byteLength)
286
- const chunk = new Uint8Array( end - offset)
287
- chunk.set(new Uint8Array(buffer.slice(offset, end)))
305
+ const src = new Uint8Array(buffer);
306
+ for (let offset = 0; offset < src.byteLength; offset += this.options.chunkSize!) {
307
+ const end = Math.min(offset + this.options.chunkSize!, src.byteLength)
308
+ // subarray 创建视图,不拷贝内存。pushable.push 不修改内容,安全。
309
+ const chunk = src.subarray(offset, end)
288
310
 
289
311
  await this.retryableWrite(chunk)
290
312
  this.updateProgress(chunk.byteLength)
@@ -311,16 +333,11 @@ export class StreamWriter {
311
333
  throw new Error('Stream aborted during backpressure monitoring')
312
334
  }
313
335
 
314
- await new Promise<void>((resolve, reject) => {
315
- try {
316
- this.p.push(chunk)
317
- }catch(err){
318
- reject(err)
319
- }
320
- resolve()
321
- })
322
- } catch (err) {
323
- if (attempt < this.options.retries!) {
336
+ // push 是同步操作,直接调用即可
337
+ this.p.push(chunk)
338
+ } catch (err) {
339
+ // aborted 时不重试,立即抛出
340
+ if (!this.abortController.signal.aborted && attempt < this.options.retries!) {
324
341
  const delay = this.calculateRetryDelay(attempt)
325
342
  await new Promise(r => setTimeout(r, delay))
326
343
  return this.retryableWrite(chunk, attempt + 1)
@@ -329,71 +346,48 @@ export class StreamWriter {
329
346
  }
330
347
  }
331
348
 
332
- private async monitorBackpressure(): Promise<void> {
333
- const currentSize = this.queueSize
334
- const baseThreshold = this.options.bufferSize! * 0.7 // 降低基础阈值,更早检测
335
- const criticalThreshold = this.options.bufferSize! * 0.9 // 临界阈值
336
-
337
- // 快速路径:无背压时直接返回
338
- if (currentSize < baseThreshold) {
349
+ private async monitorBackpressure(): Promise<void> {
350
+ const baseThreshold = this.options.bufferSize! * 0.7
351
+ const criticalThreshold = this.options.bufferSize! * 0.9
352
+
353
+ // 快速路径
354
+ if (this.queueSize < baseThreshold) {
339
355
  if (this.isBackpressure) {
340
356
  this.isBackpressure = false
341
- this.dispatchBackpressureEvent({
342
- currentSize,
343
- averageSize: this.getAverageQueueSize(),
344
- threshold: baseThreshold,
345
- waitingTime: 0
346
- })
357
+ this.dispatchBackpressureEvent({ currentSize: this.queueSize, averageSize: this.getAverageQueueSize(), threshold: baseThreshold, waitingTime: 0 })
347
358
  }
348
359
  return
349
360
  }
350
-
351
- // 进入背压状态
361
+
352
362
  if (!this.isBackpressure) {
353
363
  this.isBackpressure = true
354
- this.dispatchBackpressureEvent({
355
- currentSize,
356
- averageSize: this.getAverageQueueSize(),
357
- threshold: baseThreshold,
358
- waitingTime: 0
359
- })
360
- }
361
-
362
- // 智能等待策略
363
- const pressure = currentSize / this.options.bufferSize!
364
- let waitTime: number
365
-
366
- if (currentSize >= criticalThreshold) {
367
- // 临界状态:长时间等待
368
- waitTime = 50 + Math.min(200, pressure * 100)
369
- } else {
370
- // 轻度背压:短时间等待
371
- waitTime = Math.min(20, pressure * 30)
364
+ this.dispatchBackpressureEvent({ currentSize: this.queueSize, averageSize: this.getAverageQueueSize(), threshold: baseThreshold, waitingTime: 0 })
372
365
  }
373
-
374
- // 使用指数退避,但最多等待3
375
- let retryCount = 0
376
- const maxRetries = 3
377
-
378
- while (this.queueSize >= baseThreshold && retryCount < maxRetries) {
366
+
367
+ // 事件驱动等待:每轮等到 drain 触发或超时,最多 3
368
+ const maxRounds = 3
369
+ for (let i = 0; i < maxRounds; i++) {
379
370
  if (this.abortController.signal.aborted) break
380
-
381
- await new Promise(r => setTimeout(r, waitTime))
382
- retryCount++
383
-
384
- // 动态调整等待时间
385
- waitTime = Math.min(waitTime * 1.5, 100)
371
+ if (this.queueSize < baseThreshold) break
372
+
373
+ const isCritical = this.queueSize >= criticalThreshold
374
+ const waitMs = isCritical ? 100 : 30
375
+
376
+ await new Promise<void>(resolve => {
377
+ let done = false
378
+ const timer = setTimeout(() => { if (!done) { done = true; resolve() } }, waitMs)
379
+ this.drainWaiters.push(() => { if (!done) { done = true; clearTimeout(timer); resolve() } })
380
+ })
386
381
  }
387
-
388
- // 如果仍然背压但达到最大重试次数,记录警告但继续执行
382
+
389
383
  if (this.queueSize >= baseThreshold) {
390
384
  const now = Date.now()
391
- if (now - this.lastBpWarnAt > 1000) { // 节流警告
385
+ if (now - this.lastBpWarnAt > 1000) {
392
386
  this.lastBpWarnAt = now
393
387
  console.warn(`Stream writer: High backpressure detected (${this.queueSize} bytes), continuing anyway`)
394
388
  }
395
389
  }
396
- }
390
+ }
397
391
 
398
392
 
399
393
  private calculateRetryDelay(attempt: number): number {
@@ -495,11 +489,17 @@ export class StreamWriter {
495
489
  this.abortController.abort()
496
490
  }
497
491
 
498
- // 立即拒绝所有待处理的写入任务,避免它们继续执行
492
+ // 执行所有待处理的写入任务:它们会检查 signal.aborted 并立即 resolve,
493
+ // 不执行的话调用方的 Promise 会永远挂住
499
494
  const pendingTasks = this.writeQueue.splice(0)
500
- pendingTasks.forEach(() => {
501
- // 这些任务的 Promise 会在执行时因为检查到 aborted 而被拒绝
502
- })
495
+ for (const task of pendingTasks) {
496
+ task().catch(() => { /* already aborted, ignore */ })
497
+ }
498
+
499
+ // 唤醒所有 drainWaiters(flush / monitorBackpressure 中的等待者),
500
+ // 让它们检查 signal.aborted 并立即 resolve,不必等到各自的超时
501
+ const ws = this.drainWaiters.splice(0)
502
+ for (const fn of ws) { try { fn() } catch { /* ignore */ } }
503
503
 
504
504
  try {
505
505
  this.p.end()
@@ -507,25 +507,40 @@ export class StreamWriter {
507
507
  // Ignore errors when ending pushable
508
508
  }
509
509
 
510
- if (this.watchdogTimer) { clearInterval(this.watchdogTimer); this.watchdogTimer = undefined }
510
+ if (this.watchdogTimer) { clearTimeout(this.watchdogTimer); this.watchdogTimer = undefined }
511
511
  }
512
512
 
513
513
  // 等待内部队列被下游完全消费(用于在结束前确保尽量发送完数据)
514
514
  // 默认超时 10s,避免无限等待
515
515
  async flush(timeoutMs: number = 10000): Promise<void> {
516
- const start = Date.now()
517
516
  // 快速路径
518
517
  if (this.queueSize <= 0 && !this.isProcessingQueue && this.writeQueue.length === 0) return
519
- // 轮询等待队列清空
520
- while (true) {
521
- if (this.abortController.signal.aborted) return
522
- if (this.queueSize <= 0 && !this.isProcessingQueue && this.writeQueue.length === 0) return
523
- if (Date.now() - start > timeoutMs) {
524
- console.warn(`Stream writer: flush timeout with ${this.queueSize} bytes still queued`)
525
- return
518
+ if (this.abortController.signal.aborted) return
519
+
520
+ await new Promise<void>((resolve) => {
521
+ // 已经清空
522
+ if (this.queueSize <= 0 && !this.isProcessingQueue && this.writeQueue.length === 0) {
523
+ resolve(); return
526
524
  }
527
- await new Promise(r => setTimeout(r, 10))
528
- }
525
+ let done = false
526
+ const timer = setTimeout(() => {
527
+ if (!done) {
528
+ done = true
529
+ console.warn(`Stream writer: flush timeout with ${this.queueSize} bytes still queued`)
530
+ resolve()
531
+ }
532
+ }, timeoutMs)
533
+ // 由 createTransform 在每个 chunk 被下游消耗后唤醒
534
+ const check = () => {
535
+ if (this.abortController.signal.aborted || (this.queueSize <= 0 && !this.isProcessingQueue && this.writeQueue.length === 0)) {
536
+ if (!done) { done = true; clearTimeout(timer); resolve() }
537
+ } else {
538
+ // 下次 drain 时再检查
539
+ this.drainWaiters.push(check)
540
+ }
541
+ }
542
+ this.drainWaiters.push(check)
543
+ })
529
544
  }
530
545
 
531
546
  // 事件系统