@peerbit/stream 5.1.3 → 5.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerbit/stream",
3
- "version": "5.1.3",
3
+ "version": "5.1.5",
4
4
  "description": "A building block for direct streaming protocols",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -96,11 +96,11 @@
96
96
  "uint8arraylist": "^2.4.8",
97
97
  "uint8arrays": "^5.1.0",
98
98
  "yallist": "^5.0.0",
99
+ "@peerbit/cache": "3.1.1",
99
100
  "@peerbit/crypto": "3.1.4",
100
- "@peerbit/logger": "2.0.1",
101
- "@peerbit/stream-interface": "6.0.13",
101
+ "@peerbit/stream-interface": "6.0.14",
102
102
  "@peerbit/time": "3.0.1",
103
- "@peerbit/cache": "3.1.0"
103
+ "@peerbit/logger": "2.0.1"
104
104
  },
105
105
  "scripts": {
106
106
  "bench": "TS_NODE_PROJECT=../../../tsconfig.json node --loader ts-node/esm ./benchmark/index.ts",
package/src/index.ts CHANGED
@@ -54,7 +54,11 @@ import {
54
54
  getDeliveryHopTrace,
55
55
  getResponsePriorityFromMessage,
56
56
  hasDeliveryHop,
57
+ isAcknowledgeAnyWhereDeliveryMode,
58
+ isAcknowledgeDeliveryMode,
57
59
  isAcknowledgedDeliveryMode,
60
+ isAnyWhereDeliveryMode,
61
+ isSilentDeliveryMode,
58
62
  setDeliveryOriginHop,
59
63
  } from "@peerbit/stream-interface";
60
64
  import type {
@@ -126,6 +130,7 @@ export {
126
130
  type RustDecodedFanoutUnicast,
127
131
  type RustDecodedPubSubMessage,
128
132
  type RustEagerBlockCache,
133
+ type RustEagerBlockCacheStats,
129
134
  type RustFanoutTree,
130
135
  type RustLanesInit,
131
136
  type RustParentUpgradeGateOptions,
@@ -163,6 +168,17 @@ const markPromiseHandled = <T>(promise: Promise<T>): Promise<T> => {
163
168
  return promise;
164
169
  };
165
170
 
171
+ const closeRawStreamBestEffort = (stream: Stream): void => {
172
+ try {
173
+ // WebRTC close can reject later while waiting for FIN_ACK. These lifecycle
174
+ // paths are intentionally fire-and-forget, so observe the rejection without
175
+ // delaying pruning or shutdown.
176
+ void Promise.resolve(stream.close?.()).catch(() => {});
177
+ } catch {
178
+ // Closing discarded streams is best-effort, including synchronous failures.
179
+ }
180
+ };
181
+
166
182
  const logError = (e?: any) => {
167
183
  if (e?.message === "Cannot push value onto an ended pushable") {
168
184
  return; // ignore since we are trying to push to a closed stream
@@ -478,7 +494,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
478
494
  await waitForDrain(raw, this.outboundAbortController.signal);
479
495
  }
480
496
  }
481
- raw.close?.();
497
+ closeRawStreamBestEffort(raw);
482
498
  } catch (err) {
483
499
  candidate.aborted = true;
484
500
  try {
@@ -518,12 +534,13 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
518
534
  if (this.outboundStreams.length <= 1) return;
519
535
  if (reset && this._outboundPruneTimer) {
520
536
  clearTimeout(this._outboundPruneTimer);
537
+ this._outboundPruneTimer = undefined;
521
538
  }
522
539
  if (!this._outboundPruneTimer) {
523
- this._outboundPruneTimer = setTimeout(
524
- () => this.pruneOutboundCandidates(),
525
- PeerStreams.OUTBOUND_GRACE_MS,
526
- );
540
+ this._outboundPruneTimer = setTimeout(() => {
541
+ this._outboundPruneTimer = undefined;
542
+ this.pruneOutboundCandidates();
543
+ }, PeerStreams.OUTBOUND_GRACE_MS);
527
544
  }
528
545
  }
529
546
  constructor(init: PeerStreamsInit) {
@@ -692,9 +709,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
692
709
  try {
693
710
  f.raw.abort?.(new AbortError("Failed write" as any));
694
711
  } catch {}
695
- try {
696
- f.raw.close?.();
697
- } catch {}
712
+ closeRawStreamBestEffort(f.raw);
698
713
  }
699
714
  // If more than one remains schedule prune; else ensure outbound event raised
700
715
  if (this.outboundStreams.length > 1) this._scheduleOutboundPrune(true);
@@ -805,11 +820,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
805
820
  } catch {
806
821
  logger.error("Failed to abort inbound stream");
807
822
  }
808
- try {
809
- drop.raw.close?.();
810
- } catch {
811
- logger.error("Failed to close inbound stream");
812
- }
823
+ closeRawStreamBestEffort(drop.raw);
813
824
  }
814
825
  const abortController = new AbortController();
815
826
  const decoded = pipe(stream, (source) =>
@@ -868,9 +879,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
868
879
  try {
869
880
  candidate.abortController.abort();
870
881
  } catch {}
871
- try {
872
- candidate.raw.close?.();
873
- } catch {}
882
+ closeRawStreamBestEffort(candidate.raw);
874
883
  }
875
884
  this.inboundStreams = survivors;
876
885
  // update legacy references if they were pruned
@@ -903,7 +912,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
903
912
  try {
904
913
  record.raw.abort?.(reason);
905
914
  } catch {}
906
- void Promise.resolve(record.raw.close?.()).catch(() => {});
915
+ closeRawStreamBestEffort(record.raw);
907
916
  }
908
917
  if (this.rawInboundStream === record.raw) {
909
918
  const next = this.inboundStreams[0];
@@ -923,8 +932,9 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
923
932
  */
924
933
 
925
934
  async attachOutboundStream(stream: Stream) {
926
- if (this.outboundStreams[0] && stream.id === this.outboundStreams[0].raw.id)
935
+ if (this.outboundStreams.some((candidate) => candidate.raw === stream)) {
927
936
  return; // duplicate
937
+ }
928
938
  this._addOutboundCandidate(stream);
929
939
  if (this.outboundStreams.length === 1) {
930
940
  this.dispatchEvent(new CustomEvent("stream:outbound"));
@@ -971,11 +981,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
971
981
  } catch {
972
982
  logger.error("Failed to close outbound pushable");
973
983
  }
974
- try {
975
- c.raw.close?.();
976
- } catch {
977
- logger.error("Failed to close outbound stream");
978
- }
984
+ closeRawStreamBestEffort(c.raw);
979
985
  }
980
986
  this.outboundStreams = [chosen];
981
987
  } catch (e) {
@@ -1071,7 +1077,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
1071
1077
  } catch {
1072
1078
  // ignore
1073
1079
  }
1074
- void Promise.resolve(inbound.raw.close?.()).catch(() => {});
1080
+ closeRawStreamBestEffort(inbound.raw);
1075
1081
  } catch {
1076
1082
  logger.error("Failed to close inbound stream");
1077
1083
  }
@@ -2629,16 +2635,16 @@ export abstract class DirectStream<
2629
2635
 
2630
2636
  let isForMe = false;
2631
2637
  if (
2632
- message.header.mode instanceof AnyWhere ||
2633
- message.header.mode instanceof AcknowledgeAnyWhere
2638
+ isAnyWhereDeliveryMode(message.header.mode) ||
2639
+ isAcknowledgeAnyWhereDeliveryMode(message.header.mode)
2634
2640
  ) {
2635
2641
  isForMe = true;
2636
2642
  } else {
2637
2643
  const isFromSelf = this.publicKey.equals(from);
2638
2644
  if (
2639
2645
  !isFromSelf &&
2640
- (message.header.mode instanceof SilentDelivery ||
2641
- message.header.mode instanceof AcknowledgeDelivery)
2646
+ (isSilentDeliveryMode(message.header.mode) ||
2647
+ isAcknowledgeDeliveryMode(message.header.mode))
2642
2648
  ) {
2643
2649
  isForMe = message.header.mode.to.includes(this.publicKeyHash);
2644
2650
  }
@@ -2663,8 +2669,8 @@ export abstract class DirectStream<
2663
2669
  }
2664
2670
 
2665
2671
  if (
2666
- message.header.mode instanceof SilentDelivery ||
2667
- message.header.mode instanceof AcknowledgeDelivery
2672
+ isSilentDeliveryMode(message.header.mode) ||
2673
+ isAcknowledgeDeliveryMode(message.header.mode)
2668
2674
  ) {
2669
2675
  if (
2670
2676
  message.header.mode.to &&
@@ -2679,8 +2685,8 @@ export abstract class DirectStream<
2679
2685
  // Forward
2680
2686
  const shouldForward =
2681
2687
  seenBefore === 0 ||
2682
- ((message.header.mode instanceof AcknowledgeDelivery ||
2683
- message.header.mode instanceof AcknowledgeAnyWhere) &&
2688
+ ((isAcknowledgeDeliveryMode(message.header.mode) ||
2689
+ isAcknowledgeAnyWhereDeliveryMode(message.header.mode)) &&
2684
2690
  seenBefore < message.header.mode.redundancy);
2685
2691
 
2686
2692
  if (shouldForward) {
@@ -2712,14 +2718,12 @@ export abstract class DirectStream<
2712
2718
  message: DataMessage | Goodbye,
2713
2719
  seenBefore: number,
2714
2720
  ) {
2715
- if (
2716
- message.header.mode instanceof AcknowledgeDelivery ||
2717
- message.header.mode instanceof AcknowledgeAnyWhere
2718
- ) {
2721
+ if (isAcknowledgedDeliveryMode(message.header.mode)) {
2719
2722
  const isRecipient =
2720
- message.header.mode instanceof AcknowledgeAnyWhere
2721
- ? true
2722
- : message.header.mode.to.includes(this.publicKeyHash);
2723
+ isAcknowledgeAnyWhereDeliveryMode(message.header.mode) ||
2724
+ (message.header.mode as AcknowledgeDelivery).to.includes(
2725
+ this.publicKeyHash,
2726
+ );
2723
2727
  const shouldAcknowledge = this.rustCore
2724
2728
  ? this.rustCore.decisions.shouldAcknowledge({
2725
2729
  isRecipient,
@@ -2752,8 +2756,7 @@ export abstract class DirectStream<
2752
2756
  // only include once (seenBefore=0) and only if we have not recently pruned
2753
2757
  // a connection to any signer in the path
2754
2758
  origin:
2755
- (message.header.mode instanceof AcknowledgeAnyWhere ||
2756
- message.header.mode instanceof AcknowledgeDelivery) &&
2759
+ isAcknowledgedDeliveryMode(message.header.mode) &&
2757
2760
  seenBefore === 0 &&
2758
2761
  !signers.find((x) =>
2759
2762
  this.prunedConnectionsCache?.has(x),
@@ -2974,8 +2977,8 @@ export abstract class DirectStream<
2974
2977
  });
2975
2978
 
2976
2979
  if (
2977
- mode instanceof AcknowledgeDelivery ||
2978
- mode instanceof SilentDelivery
2980
+ isAcknowledgeDeliveryMode(mode) ||
2981
+ isSilentDeliveryMode(mode)
2979
2982
  ) {
2980
2983
  if (mode.to) {
2981
2984
  let preLength = mode.to.length;
@@ -3168,7 +3171,7 @@ export abstract class DirectStream<
3168
3171
  relayed?: boolean,
3169
3172
  signal?: AbortSignal,
3170
3173
  ): Promise<{ promise: Promise<void>; startTimeout: () => void }> {
3171
- if (message.header.mode instanceof AnyWhere) {
3174
+ if (isAnyWhereDeliveryMode(message.header.mode)) {
3172
3175
  return {
3173
3176
  promise: Promise.resolve(),
3174
3177
  startTimeout: () => {},
@@ -3333,7 +3336,7 @@ export abstract class DirectStream<
3333
3336
  ) {
3334
3337
  const shouldKeepCallbackForRouteLearning =
3335
3338
  !relayed &&
3336
- message.header.mode instanceof AcknowledgeDelivery &&
3339
+ isAcknowledgeDeliveryMode(message.header.mode) &&
3337
3340
  message.header.mode.redundancy > 1;
3338
3341
  if (haveReceivers && !shouldKeepCallbackForRouteLearning) {
3339
3342
  // If we have an explicit recipient list we can clear the ACK callback once we
@@ -3368,8 +3371,7 @@ export abstract class DirectStream<
3368
3371
  // This matters because relays may modify `to`, and because "ack-anywhere" style probes intentionally
3369
3372
  // do not provide an explicit recipient list.
3370
3373
  if (
3371
- message.header.mode instanceof AcknowledgeDelivery ||
3372
- message.header.mode instanceof AcknowledgeAnyWhere
3374
+ isAcknowledgedDeliveryMode(message.header.mode)
3373
3375
  ) {
3374
3376
  const upstreamHash = messageFrom?.publicKey.hashcode();
3375
3377
 
@@ -3453,7 +3455,7 @@ export abstract class DirectStream<
3453
3455
  (!message.header.signatures ||
3454
3456
  message.header.signatures.publicKeys.length === 0) &&
3455
3457
  message instanceof DataMessage &&
3456
- message.header.mode instanceof SilentDelivery === false
3458
+ !isSilentDeliveryMode(message.header.mode)
3457
3459
  ) {
3458
3460
  throw new Error("Missing signature");
3459
3461
  }
@@ -3464,8 +3466,7 @@ export abstract class DirectStream<
3464
3466
 
3465
3467
  if (
3466
3468
  (message instanceof DataMessage || message instanceof Goodbye) &&
3467
- (message.header.mode instanceof AcknowledgeDelivery ||
3468
- message.header.mode instanceof AcknowledgeAnyWhere)
3469
+ isAcknowledgedDeliveryMode(message.header.mode)
3469
3470
  ) {
3470
3471
  const deliveryDeferredPromise = await this.createDeliveryPromise(
3471
3472
  from,
@@ -3491,8 +3492,8 @@ export abstract class DirectStream<
3491
3492
 
3492
3493
  if (message instanceof DataMessage) {
3493
3494
  if (
3494
- (message.header.mode instanceof AcknowledgeDelivery ||
3495
- message.header.mode instanceof SilentDelivery) &&
3495
+ (isAcknowledgeDeliveryMode(message.header.mode) ||
3496
+ isSilentDeliveryMode(message.header.mode)) &&
3496
3497
  !to
3497
3498
  ) {
3498
3499
  if (message.header.mode.to.length === 0) {
@@ -3517,7 +3518,7 @@ export abstract class DirectStream<
3517
3518
  for (const [neighbour, _distantPeers] of fanout) {
3518
3519
  const stream = this.peers.get(neighbour);
3519
3520
  if (!stream) continue;
3520
- if (message.header.mode instanceof SilentDelivery) {
3521
+ if (isSilentDeliveryMode(message.header.mode)) {
3521
3522
  message.header.mode.to = [..._distantPeers.keys()];
3522
3523
  promises.push(
3523
3524
  this.waitForPeerWrite(
@@ -3539,7 +3540,7 @@ export abstract class DirectStream<
3539
3540
  }
3540
3541
  usedNeighbours.add(neighbour);
3541
3542
  }
3542
- if (message.header.mode instanceof SilentDelivery) {
3543
+ if (isSilentDeliveryMode(message.header.mode)) {
3543
3544
  message.header.mode.to = originalTo;
3544
3545
  }
3545
3546
 
@@ -3549,7 +3550,7 @@ export abstract class DirectStream<
3549
3550
  // a separate delivery mode.
3550
3551
  if (
3551
3552
  !isRelayed &&
3552
- message.header.mode instanceof AcknowledgeDelivery &&
3553
+ isAcknowledgeDeliveryMode(message.header.mode) &&
3553
3554
  usedNeighbours.size < message.header.mode.redundancy
3554
3555
  ) {
3555
3556
  if (this.rustCore) {
@@ -3599,7 +3600,7 @@ export abstract class DirectStream<
3599
3600
  // - For acknowledged deliveries, fall through to flooding (route discovery / repair).
3600
3601
  // - For silent deliveries, relays should not flood (prevents unnecessary fanout); origin may still flood.
3601
3602
  // We still allow direct neighbour delivery to explicit recipients (if connected).
3602
- if (isRelayed && message.header.mode instanceof SilentDelivery) {
3603
+ if (isRelayed && isSilentDeliveryMode(message.header.mode)) {
3603
3604
  const promises: Promise<any>[] = [];
3604
3605
  const originalTo = message.header.mode.to;
3605
3606
  const recipients = this.rustCore
package/src/rust-core.ts CHANGED
@@ -112,15 +112,25 @@ export interface RustBlockProviderCache {
112
112
  clear(): void;
113
113
  }
114
114
 
115
+ export type RustEagerBlockCacheStats = {
116
+ entries: number;
117
+ bytes: number;
118
+ peakEntries: number;
119
+ peakBytes: number;
120
+ evictions: number;
121
+ expirations: number;
122
+ };
123
+
115
124
  /**
116
125
  * Eager-block cache with native bookkeeping. Block bytes stay host-side;
117
126
  * the native index decides retention/eviction.
118
127
  */
119
128
  export interface RustEagerBlockCache {
120
- add(cid: string, bytes: Uint8Array): void;
129
+ add(cid: string, bytes: Uint8Array): boolean;
121
130
  get(cid: string): Uint8Array | undefined;
122
131
  del(cid: string): void;
123
132
  clear(): void;
133
+ stats(): RustEagerBlockCacheStats;
124
134
  }
125
135
 
126
136
  /**
@@ -150,7 +160,11 @@ export interface RustBlockExchange {
150
160
  ttlMs: number;
151
161
  maxProvidersPerCid: number;
152
162
  }): RustBlockProviderCache;
153
- createEagerCache(init: { max: number; ttl: number }): RustEagerBlockCache;
163
+ createEagerCache(init: {
164
+ maxEntries: number;
165
+ maxBytes: number;
166
+ ttlMs: number;
167
+ }): RustEagerBlockCache;
154
168
  }
155
169
 
156
170
  /**