@peerbit/stream 4.3.10 → 4.4.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/src/index.ts CHANGED
@@ -23,13 +23,13 @@ import { Circuit } from "@multiformats/multiaddr-matcher";
23
23
  import { Cache } from "@peerbit/cache";
24
24
  import {
25
25
  PublicSignKey,
26
- type SignatureWithKey,
27
26
  getKeypairFromPrivateKey,
28
27
  getPublicKeyFromPeerId,
29
28
  ready,
30
29
  sha256Base64,
31
30
  toBase64,
32
31
  } from "@peerbit/crypto";
32
+ import type { SignatureWithKey } from "@peerbit/crypto";
33
33
  import {
34
34
  ACK,
35
35
  AcknowledgeDelivery,
@@ -37,25 +37,27 @@ import {
37
37
  DataMessage,
38
38
  DeliveryError,
39
39
  Goodbye,
40
- type IdOptions,
41
40
  InvalidMessageError,
42
41
  Message,
43
42
  MessageHeader,
44
43
  MultiAddrinfo,
45
44
  NotStartedError,
46
- type PriorityOptions,
47
- type PublicKeyFromHashResolver,
48
45
  SeekDelivery,
49
46
  SilentDelivery,
50
- type StreamEvents,
51
47
  TracedDelivery,
52
- type WaitForPeer,
53
- type WithExtraSigners,
54
- type WithMode,
55
- type WithTo,
56
48
  deliveryModeHasReceiver,
57
49
  getMsgId,
58
50
  } from "@peerbit/stream-interface";
51
+ import type {
52
+ IdOptions,
53
+ PriorityOptions,
54
+ PublicKeyFromHashResolver,
55
+ StreamEvents,
56
+ WaitForPeer,
57
+ WithExtraSigners,
58
+ WithMode,
59
+ WithTo,
60
+ } from "@peerbit/stream-interface";
59
61
  import { AbortError, TimeoutError, delay } from "@peerbit/time";
60
62
  import { abortableSource } from "abortable-iterator";
61
63
  import * as lp from "it-length-prefixed";
@@ -86,7 +88,7 @@ export const dontThrowIfDeliveryError = (e: any) => {
86
88
  throw e;
87
89
  };
88
90
 
89
- const logError = (e?: { message: string }) => {
91
+ const logError = (e?: any) => {
90
92
  if (e?.message === "Cannot push value onto an ended pushable") {
91
93
  return; // ignore since we are trying to push to a closed stream
92
94
  }
@@ -134,6 +136,41 @@ const getLaneFromPriority = (priority: number) => {
134
136
  }
135
137
  return 1;
136
138
  };
139
+ interface OutboundCandidate {
140
+ raw: Stream;
141
+ pushable: PushableLanes<Uint8Array>;
142
+ created: number;
143
+ bytesDelivered: number;
144
+ aborted: boolean;
145
+ existing: boolean;
146
+ }
147
+ export interface InboundStreamRecord {
148
+ raw: Stream;
149
+ iterable: AsyncIterable<Uint8ArrayList>;
150
+ abortController: AbortController;
151
+ created: number;
152
+ lastActivity: number;
153
+ bytesReceived: number;
154
+ }
155
+ // Hook for tests to override queued length measurement (peerStreams, default impl)
156
+ export let measureOutboundQueuedBytes: (
157
+ ps: PeerStreams, // return queued bytes for active outbound (lane 0) or 0 if none
158
+ ) => number = (ps: PeerStreams) => {
159
+ const active = ps._getActiveOutboundPushable();
160
+ if (!active) return 0;
161
+ // Prefer lane-aware helper if present
162
+ // @ts-ignore - optional test helper
163
+ if (typeof active.getReadableLength === "function") {
164
+ try {
165
+ // lane 0 only
166
+ return active.getReadableLength(0) || 0;
167
+ } catch {
168
+ // ignore
169
+ }
170
+ }
171
+ // @ts-ignore fallback for vanilla pushable
172
+ return active.readableLength || 0;
173
+ };
137
174
  /**
138
175
  * Thin wrapper around a peer's inbound / outbound pubsub streams
139
176
  */
@@ -141,27 +178,24 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
141
178
  public readonly peerId: PeerId;
142
179
  public readonly publicKey: PublicSignKey;
143
180
  public readonly protocol: string;
144
- /**
145
- * Write stream - it's preferable to use the write method
146
- */
147
- public outboundStream?: PushableLanes<Uint8Array>;
181
+ // Removed dedicated outboundStream; first element of outboundStreams[] is active
148
182
 
149
183
  /**
150
- * Read stream
184
+ * Backwards compatible single inbound references (points to first inbound candidate)
151
185
  */
152
186
  public inboundStream?: AsyncIterable<Uint8ArrayList>;
153
- /**
154
- * The raw outbound stream, as retrieved from conn.newStream
155
- */
156
- public rawOutboundStream?: Stream;
157
- /**
158
- * The raw inbound stream, as retrieved from the callback from libp2p.handle
159
- */
160
187
  public rawInboundStream?: Stream;
188
+
161
189
  /**
162
- * An AbortController for controlled shutdown of the treams
190
+ * Multiple inbound stream support (more permissive than outbound)
191
+ * We retain concurrent inbound streams to avoid races during migration; inactive ones can later be pruned.
163
192
  */
164
- private inboundAbortController: AbortController;
193
+ public inboundStreams: InboundStreamRecord[] = [];
194
+
195
+ private _inboundPruneTimer?: ReturnType<typeof setTimeout>;
196
+ public static INBOUND_IDLE_MS = 10_000; // configurable grace for inactivity (made public for tests)
197
+ static MAX_INBOUND_STREAMS = 8; // sensible default to prevent flood
198
+
165
199
  private outboundAbortController: AbortController;
166
200
 
167
201
  private closed: boolean;
@@ -171,13 +205,92 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
171
205
  public seekedOnce: boolean;
172
206
 
173
207
  private usedBandWidthTracker: BandwidthTracker;
208
+
209
+ // Unified outbound streams list (during grace may contain >1; after pruning length==1)
210
+ private outboundStreams: OutboundCandidate[] = [];
211
+ // Public debug exposure of current raw outbound streams (during grace may contain >1)
212
+ public get rawOutboundStreams(): Stream[] {
213
+ return this.outboundStreams.map((c) => c.raw);
214
+ }
215
+ public _getActiveOutboundPushable(): PushableLanes<Uint8Array> | undefined {
216
+ return this.outboundStreams[0]?.pushable;
217
+ }
218
+ public _getOutboundCount() {
219
+ return this.outboundStreams.length;
220
+ }
221
+
222
+ public _getInboundCount() {
223
+ return this.inboundStreams.length;
224
+ }
225
+
226
+ public _debugInboundStats(): {
227
+ id: string;
228
+ created: number;
229
+ lastActivity: number;
230
+ bytesReceived: number;
231
+ }[] {
232
+ return this.inboundStreams.map((c) => ({
233
+ id: c.raw.id,
234
+ created: c.created,
235
+ lastActivity: c.lastActivity,
236
+ bytesReceived: c.bytesReceived,
237
+ }));
238
+ }
239
+ private _outboundPruneTimer?: ReturnType<typeof setTimeout>;
240
+ private static readonly OUTBOUND_GRACE_MS = 500; // TODO configurable
241
+
242
+ private _addOutboundCandidate(raw: Stream): OutboundCandidate {
243
+ const existing = this.outboundStreams.find((c) => c.raw === raw);
244
+ if (existing) return existing;
245
+ const pushableInst = pushableLanes<Uint8Array>({
246
+ lanes: 2,
247
+ onPush: (val: Uint8Array) => {
248
+ candidate.bytesDelivered += val.length || val.byteLength || 0;
249
+ },
250
+ });
251
+ const candidate: OutboundCandidate = {
252
+ raw,
253
+ pushable: pushableInst,
254
+ created: Date.now(),
255
+ bytesDelivered: 0,
256
+ aborted: false,
257
+ existing: false,
258
+ };
259
+ pipe(
260
+ pushableInst,
261
+ (source) => lp.encode(source, { maxDataLength: MAX_DATA_LENGTH_OUT }),
262
+ raw,
263
+ ).catch((e: any) => {
264
+ candidate.aborted = true;
265
+ logError(e as { message: string } as any);
266
+ });
267
+ this.outboundStreams.push(candidate);
268
+ const origAbort = raw.abort?.bind(raw);
269
+ raw.abort = (err?: any) => {
270
+ candidate.aborted = true;
271
+ return origAbort?.(err);
272
+ };
273
+ return candidate;
274
+ }
275
+
276
+ private _scheduleOutboundPrune(reset = true) {
277
+ if (this.outboundStreams.length <= 1) return;
278
+ if (reset && this._outboundPruneTimer) {
279
+ clearTimeout(this._outboundPruneTimer);
280
+ }
281
+ if (!this._outboundPruneTimer) {
282
+ this._outboundPruneTimer = setTimeout(
283
+ () => this.pruneOutboundCandidates(),
284
+ PeerStreams.OUTBOUND_GRACE_MS,
285
+ );
286
+ }
287
+ }
174
288
  constructor(init: PeerStreamsInit) {
175
289
  super();
176
290
 
177
291
  this.peerId = init.peerId;
178
292
  this.publicKey = init.publicKey;
179
293
  this.protocol = init.protocol;
180
- this.inboundAbortController = new AbortController();
181
294
  this.outboundAbortController = new AbortController();
182
295
 
183
296
  this.closed = false;
@@ -190,14 +303,14 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
190
303
  * Do we have a connection to read from?
191
304
  */
192
305
  get isReadable() {
193
- return Boolean(this.inboundStream);
306
+ return this.inboundStreams.length > 0;
194
307
  }
195
308
 
196
309
  /**
197
310
  * Do we have a connection to write on?
198
311
  */
199
312
  get isWritable() {
200
- return Boolean(this.outboundStream);
313
+ return this.outboundStreams.length > 0;
201
314
  }
202
315
 
203
316
  get usedBandwidth() {
@@ -216,19 +329,62 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
216
329
  } mb`,
217
330
  );
218
331
  }
219
- if (this.outboundStream == null) {
332
+ if (!this.isWritable) {
220
333
  logger.error("No writable connection to " + this.peerId.toString());
221
334
  throw new Error("No writable connection to " + this.peerId.toString());
222
335
  }
223
336
 
224
337
  this.usedBandWidthTracker.add(data.byteLength);
225
338
 
226
- this.outboundStream.push(
227
- data instanceof Uint8Array ? data : data.subarray(),
228
- this.outboundStream.getReadableLength(0) === 0
229
- ? 0
230
- : getLaneFromPriority(priority), // TODO use more lanes
231
- );
339
+ // Write to all current outbound streams (normally 1, but >1 during grace)
340
+ const payload = data instanceof Uint8Array ? data : data.subarray();
341
+ let successes = 0;
342
+ let failures: any[] = [];
343
+ const failed: OutboundCandidate[] = [];
344
+ for (const c of this.outboundStreams) {
345
+ try {
346
+ c.pushable.push(
347
+ payload,
348
+ c.pushable.getReadableLength(0) === 0
349
+ ? 0
350
+ : getLaneFromPriority(priority),
351
+ );
352
+ successes++;
353
+ } catch (e) {
354
+ failures.push(e);
355
+ failed.push(c);
356
+ logError(e);
357
+ }
358
+ }
359
+ if (successes === 0) {
360
+ throw new Error(
361
+ "All outbound writes failed (" +
362
+ failures.map((f) => f?.message).join(", ") +
363
+ ")",
364
+ );
365
+ }
366
+ if (failures.length > 0) {
367
+ logger.warn(
368
+ `Partial outbound write failure: ${failures.length} failed, ${successes} succeeded`,
369
+ );
370
+ // Remove failed streams immediately (best-effort)
371
+ if (failed.length) {
372
+ this.outboundStreams = this.outboundStreams.filter(
373
+ (c) => !failed.includes(c),
374
+ );
375
+ for (const f of failed) {
376
+ try {
377
+ f.raw.abort?.(new AbortError("Failed write" as any));
378
+ } catch {}
379
+ try {
380
+ f.raw.close?.();
381
+ } catch {}
382
+ }
383
+ // If more than one remains schedule prune; else ensure outbound event raised
384
+ if (this.outboundStreams.length > 1) this._scheduleOutboundPrune(true);
385
+ else this.dispatchEvent(new CustomEvent("stream:outbound"));
386
+ }
387
+ }
232
388
  }
233
389
 
234
390
  /**
@@ -296,41 +452,108 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
296
452
  /**
297
453
  * Attach a raw inbound stream and setup a read stream
298
454
  */
299
- attachInboundStream(stream: Stream) {
300
- // Create and attach a new inbound stream
301
- // The inbound stream is:
302
- // - abortable, set to only return on abort, rather than throw
303
- // - transformed with length-prefix transform
304
- this.rawInboundStream = stream;
305
- this.inboundStream = abortableSource(
306
- pipe(this.rawInboundStream, (source) =>
307
- lp.decode(source, { maxDataLength: MAX_DATA_LENGTH_IN }),
308
- ),
309
- this.inboundAbortController.signal,
310
- {
311
- returnOnAbort: true,
312
- onReturnError: (err) => {
313
- logger.error("Inbound stream error", err?.message);
314
- },
315
- },
455
+ attachInboundStream(stream: Stream): InboundStreamRecord {
456
+ // Support multiple concurrent inbound streams with inactivity pruning.
457
+ // Enforce max inbound streams (drop least recently active)
458
+ if (this.inboundStreams.length >= PeerStreams.MAX_INBOUND_STREAMS) {
459
+ let dropIndex = 0;
460
+ for (let i = 1; i < this.inboundStreams.length; i++) {
461
+ const a = this.inboundStreams[i];
462
+ const b = this.inboundStreams[dropIndex];
463
+ if (
464
+ a.lastActivity < b.lastActivity ||
465
+ (a.lastActivity === b.lastActivity && a.created < b.created)
466
+ ) {
467
+ dropIndex = i;
468
+ }
469
+ }
470
+ const [drop] = this.inboundStreams.splice(dropIndex, 1);
471
+ try {
472
+ drop.abortController.abort();
473
+ } catch {
474
+ logger.error("Failed to abort inbound stream");
475
+ }
476
+ try {
477
+ drop.raw.close?.();
478
+ } catch {
479
+ logger.error("Failed to close inbound stream");
480
+ }
481
+ }
482
+ const abortController = new AbortController();
483
+ const decoded = pipe(stream, (source) =>
484
+ lp.decode(source, { maxDataLength: MAX_DATA_LENGTH_IN }),
316
485
  );
486
+ const iterable = abortableSource(decoded, abortController.signal, {
487
+ returnOnAbort: true,
488
+ onReturnError: (err) => {
489
+ logger.error("Inbound stream error", err?.message);
490
+ },
491
+ });
492
+ const record: InboundStreamRecord = {
493
+ raw: stream,
494
+ iterable,
495
+ abortController,
496
+ created: Date.now(),
497
+ lastActivity: Date.now(),
498
+ bytesReceived: 0,
499
+ };
500
+ this.inboundStreams.push(record);
501
+ this._scheduleInboundPrune();
502
+ // Backwards compatibility: keep first inbound as public properties
503
+ if (this.inboundStreams.length === 1) {
504
+ this.rawInboundStream = stream;
505
+ this.inboundStream = iterable;
506
+ }
507
+ this.dispatchEvent(new CustomEvent("stream:inbound"));
508
+ return record;
509
+ }
317
510
 
318
- /* this.rawInboundStream = stream
319
- this.inboundAbortController = new AbortController()
320
- this.inboundAbortController.signal.addEventListener('abort', () => {
321
- this.rawInboundStream!.close()
322
- .catch(err => {
323
- this.rawInboundStream?.abort(err)
324
- })
325
- })
511
+ private _scheduleInboundPrune() {
512
+ if (this._inboundPruneTimer) return; // already scheduled
513
+ this._inboundPruneTimer = setTimeout(() => {
514
+ this._inboundPruneTimer = undefined;
515
+ this._pruneInboundInactive();
516
+ if (this.inboundStreams.length > 1) {
517
+ // schedule again if still multiple
518
+ this._scheduleInboundPrune();
519
+ }
520
+ }, PeerStreams.INBOUND_IDLE_MS);
521
+ }
326
522
 
327
- this.inboundStream = pipe(
328
- this.rawInboundStream!,
329
- (source) => lp.decode(source, { maxDataLength: MAX_DATA_LENGTH_IN }),
330
- )
331
- */
332
- this.dispatchEvent(new CustomEvent("stream:inbound"));
333
- return this.inboundStream;
523
+ private _pruneInboundInactive() {
524
+ if (this.inboundStreams.length <= 1) return;
525
+ const now = Date.now();
526
+ // Keep at least one (the most recently active)
527
+ this.inboundStreams.sort((a, b) => b.lastActivity - a.lastActivity);
528
+ const keep = this.inboundStreams[0];
529
+ const survivors: typeof this.inboundStreams = [keep];
530
+ for (let i = 1; i < this.inboundStreams.length; i++) {
531
+ const candidate = this.inboundStreams[i];
532
+ if (now - candidate.lastActivity <= PeerStreams.INBOUND_IDLE_MS) {
533
+ survivors.push(candidate);
534
+ continue; // still active
535
+ }
536
+ try {
537
+ candidate.abortController.abort();
538
+ } catch {}
539
+ try {
540
+ candidate.raw.close?.();
541
+ } catch {}
542
+ }
543
+ this.inboundStreams = survivors;
544
+ // update legacy references if they were pruned
545
+ if (!this.inboundStreams.includes(keep)) {
546
+ this.rawInboundStream = this.inboundStreams[0]?.raw;
547
+ this.inboundStream = this.inboundStreams[0]?.iterable;
548
+ }
549
+ }
550
+
551
+ public forcePruneInbound() {
552
+ if (this._inboundPruneTimer) {
553
+ clearTimeout(this._inboundPruneTimer);
554
+ this._inboundPruneTimer = undefined;
555
+ }
556
+ this._pruneInboundInactive();
334
557
  }
335
558
 
336
559
  /**
@@ -338,39 +561,111 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
338
561
  */
339
562
 
340
563
  async attachOutboundStream(stream: Stream) {
341
- // If an outbound stream already exists and is active, ignore the new one; if a raw stream exists, close it to avoid leaks
342
- if (
343
- this.outboundStream != null &&
344
- stream.id !== this.rawOutboundStream?.id
345
- ) {
346
- logger.info(
347
- `Outbound stream already exists for ${this.peerId.toString()}, ignoring additional stream`,
564
+ if (this.outboundStreams[0] && stream.id === this.outboundStreams[0].raw.id)
565
+ return; // duplicate
566
+ this._addOutboundCandidate(stream);
567
+ if (this.outboundStreams.length === 1) {
568
+ this.dispatchEvent(new CustomEvent("stream:outbound"));
569
+ return;
570
+ }
571
+ this._scheduleOutboundPrune(true);
572
+ }
573
+
574
+ private pruneOutboundCandidates() {
575
+ try {
576
+ const candidates = this.outboundStreams;
577
+ if (!candidates.length) return;
578
+ const now = Date.now();
579
+ const healthy = candidates.filter(
580
+ (c: OutboundCandidate) => !c.aborted && c.bytesDelivered > 0,
348
581
  );
349
- try {
350
- await stream.abort?.(
351
- new AbortError("Superseded outbound stream" as any),
582
+ let chosen: OutboundCandidate | undefined;
583
+ if (healthy.length === 0) {
584
+ chosen = candidates.reduce(
585
+ (a: OutboundCandidate, b: OutboundCandidate) =>
586
+ b.created > a.created ? b : a,
352
587
  );
353
- } catch {}
354
- return;
588
+ } else {
589
+ let bestScore = -Infinity;
590
+ for (const c of healthy) {
591
+ const age = now - c.created || 1;
592
+ const score = c.bytesDelivered / age;
593
+ if (
594
+ score > bestScore ||
595
+ (score === bestScore && chosen && c.created > chosen.created)
596
+ ) {
597
+ bestScore = score;
598
+ chosen = c;
599
+ }
600
+ }
601
+ }
602
+ if (!chosen) return;
603
+ for (const c of candidates) {
604
+ if (c === chosen) continue; // never abort chosen
605
+ try {
606
+ c.raw.abort?.(new AbortError("Replaced outbound stream" as any));
607
+ } catch {
608
+ logger.error("Failed to abort outbound stream");
609
+ }
610
+ try {
611
+ c.pushable.return?.();
612
+ } catch {
613
+ logger.error("Failed to close outbound pushable");
614
+ }
615
+ try {
616
+ c.raw.close?.();
617
+ } catch {
618
+ logger.error("Failed to close outbound stream");
619
+ }
620
+ }
621
+ this.outboundStreams = [chosen];
622
+ } catch (e) {
623
+ logger.error(
624
+ "Error promoting outbound candidate: " + (e as any)?.message,
625
+ );
626
+ } finally {
627
+ this.dispatchEvent(new CustomEvent("stream:outbound"));
355
628
  }
629
+ }
356
630
 
357
- this.rawOutboundStream = stream;
358
- this.outboundStream = pushableLanes({ lanes: 2 });
631
+ public forcePruneOutbound() {
632
+ if (this._outboundPruneTimer) {
633
+ clearTimeout(this._outboundPruneTimer);
634
+ this._outboundPruneTimer = undefined;
635
+ }
636
+ this.pruneOutboundCandidates();
637
+ }
359
638
 
360
- this.outboundAbortController.signal.addEventListener("abort", () => {
361
- this.rawOutboundStream?.close().catch((err) => {
362
- this.rawOutboundStream?.abort(err);
363
- });
364
- });
639
+ /**
640
+ * Internal helper to perform the actual outbound replacement & piping.
641
+ */
642
+ // _replaceOutboundStream removed (legacy path)
365
643
 
366
- pipe(
367
- this.outboundStream,
368
- (source) => lp.encode(source, { maxDataLength: MAX_DATA_LENGTH_OUT }),
369
- this.rawOutboundStream,
370
- ).catch(logError);
644
+ // Debug/testing helper: list active outbound raw stream ids
645
+ public _debugActiveOutboundIds(): string[] {
646
+ if (this.outboundStreams.length) {
647
+ return this.outboundStreams.map((c) => c.raw.id);
648
+ }
649
+ return this.outboundStreams.map((c) => c.raw.id);
650
+ }
371
651
 
372
- // Emit if the connection is new
373
- this.dispatchEvent(new CustomEvent("stream:outbound"));
652
+ public _debugOutboundStats(): {
653
+ id: string;
654
+ bytes: number;
655
+ aborted: boolean;
656
+ }[] {
657
+ if (this.outboundStreams.length) {
658
+ return this.outboundStreams.map((c) => ({
659
+ id: c.raw.id,
660
+ bytes: c.bytesDelivered,
661
+ aborted: !!c.aborted,
662
+ }));
663
+ }
664
+ return this.outboundStreams.map((c) => ({
665
+ id: c.raw.id,
666
+ bytes: c.bytesDelivered,
667
+ aborted: !!c.aborted,
668
+ }));
374
669
  }
375
670
 
376
671
  /**
@@ -382,29 +677,49 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
382
677
  }
383
678
 
384
679
  this.closed = true;
680
+ if (this._outboundPruneTimer) {
681
+ clearTimeout(this._outboundPruneTimer);
682
+ this._outboundPruneTimer = undefined;
683
+ }
385
684
 
386
685
  // End the outbound stream
387
- if (this.outboundStream != null) {
388
- await this.outboundStream.return();
389
- this.rawOutboundStream?.abort(new AbortError("Closed"));
686
+ if (this.outboundStreams.length) {
687
+ for (const c of this.outboundStreams) {
688
+ try {
689
+ await c.pushable.return?.();
690
+ } catch {}
691
+ try {
692
+ c.raw.abort?.(new AbortError("Closed"));
693
+ } catch {}
694
+ }
390
695
  this.outboundAbortController.abort();
391
696
  }
392
697
 
393
- // End the inbound stream
394
- if (this.inboundStream != null) {
395
- this.inboundAbortController.abort();
396
- await this.rawInboundStream?.close();
698
+ // End inbound streams
699
+ if (this.inboundStreams.length) {
700
+ for (const inbound of this.inboundStreams) {
701
+ try {
702
+ inbound.abortController.abort();
703
+ } catch {
704
+ logger.error("Failed to abort inbound stream");
705
+ }
706
+ try {
707
+ await inbound.raw.close?.();
708
+ } catch {
709
+ logger.error("Failed to close inbound stream");
710
+ }
711
+ }
397
712
  }
398
713
 
399
714
  this.usedBandWidthTracker.stop();
400
715
 
401
716
  this.dispatchEvent(new CustomEvent("close"));
402
717
 
403
- this.rawOutboundStream = undefined;
404
- this.outboundStream = undefined;
718
+ this.outboundStreams = [];
405
719
 
406
720
  this.rawInboundStream = undefined;
407
721
  this.inboundStream = undefined;
722
+ this.inboundStreams = [];
408
723
  }
409
724
  }
410
725
 
@@ -436,6 +751,7 @@ export type DirectStreamOptions = {
436
751
  messageProcessingConcurrency?: number;
437
752
  maxInboundStreams?: number;
438
753
  maxOutboundStreams?: number;
754
+ inboundIdleTimeout?: number; // override PeerStreams.INBOUND_IDLE_MS
439
755
  connectionManager?: ConnectionManagerArguments;
440
756
  routeSeekInterval?: number;
441
757
  seekTimeout?: number;
@@ -534,6 +850,7 @@ export abstract class DirectStream<
534
850
  routeSeekInterval = ROUTE_UPDATE_DELAY_FACTOR,
535
851
  seekTimeout = SEEK_DELIVERY_TIMEOUT,
536
852
  routeMaxRetentionPeriod = ROUTE_MAX_RETANTION_PERIOD,
853
+ inboundIdleTimeout,
537
854
  } = options || {};
538
855
 
539
856
  const signKey = getKeypairFromPrivateKey(components.privateKey);
@@ -541,6 +858,10 @@ export abstract class DirectStream<
541
858
  this.sign = signKey.sign.bind(signKey);
542
859
  this.peerId = components.peerId;
543
860
  this.publicKey = signKey.publicKey;
861
+ if (inboundIdleTimeout != null)
862
+ PeerStreams.INBOUND_IDLE_MS = inboundIdleTimeout;
863
+ if (maxInboundStreams != null)
864
+ PeerStreams.MAX_INBOUND_STREAMS = maxInboundStreams;
544
865
  this.publicKeyHash = signKey.publicKey.hashcode();
545
866
  this.multicodecs = multicodecs;
546
867
  this.started = false;
@@ -795,8 +1116,8 @@ export abstract class DirectStream<
795
1116
  );
796
1117
 
797
1118
  // handle inbound
798
- const inboundStream = peer.attachInboundStream(stream);
799
- this.processMessages(peer.publicKey, inboundStream, peer).catch(logError);
1119
+ const inboundRecord = peer.attachInboundStream(stream);
1120
+ this.processMessages(peer.publicKey, inboundRecord, peer).catch(logError);
800
1121
 
801
1122
  // try to create outbound stream
802
1123
  await this.outboundInflightQueue.push({ peerId, connection });
@@ -1109,17 +1430,17 @@ export abstract class DirectStream<
1109
1430
  */
1110
1431
  async processMessages(
1111
1432
  peerId: PublicSignKey,
1112
- stream: AsyncIterable<Uint8ArrayList>,
1433
+ record: InboundStreamRecord,
1113
1434
  peerStreams: PeerStreams,
1114
1435
  ) {
1115
1436
  try {
1116
- await pipe(stream, async (source) => {
1117
- for await (const data of source) {
1118
- this.processRpc(peerId, peerStreams, data).catch((e) => {
1119
- logError(e);
1120
- });
1121
- }
1122
- });
1437
+ for await (const data of record.iterable) {
1438
+ const now = Date.now();
1439
+ record.lastActivity = now;
1440
+ record.bytesReceived += data.length || data.byteLength || 0;
1441
+
1442
+ this.processRpc(peerId, peerStreams, data).catch((e) => logError(e));
1443
+ }
1123
1444
  } catch (err: any) {
1124
1445
  if (err?.code === "ERR_STREAM_RESET") {
1125
1446
  // only send stream reset messages to info
@@ -2241,9 +2562,8 @@ export abstract class DirectStream<
2241
2562
 
2242
2563
  getQueuedBytes(): number {
2243
2564
  let sum = 0;
2244
- for (const peer of this.peers) {
2245
- const out = peer[1].outboundStream;
2246
- sum += out ? out.readableLength : 0;
2565
+ for (const [_k, ps] of this.peers) {
2566
+ sum += measureOutboundQueuedBytes(ps as any); // cast to access hook
2247
2567
  }
2248
2568
  return sum;
2249
2569
  }