@peerbit/stream 5.0.17 → 5.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/src/index.ts CHANGED
@@ -89,7 +89,14 @@ import {
89
89
  } from "./core/seek-routing.js";
90
90
  import { logger } from "./logger.js";
91
91
  import { type PushableLanes, pushableLanes } from "./pushable-lanes.js";
92
- import { MAX_ROUTE_DISTANCE, Routes } from "./routes.js";
92
+ import { MAX_ROUTE_DISTANCE, Routes, type RoutesLike } from "./routes.js";
93
+ import {
94
+ type NativeWire,
95
+ type RustCoreStream,
96
+ type RustLanesInit,
97
+ type RustSeenCache,
98
+ resolveInjectedRustCore,
99
+ } from "./rust-core.js";
93
100
  import { BandwidthTracker } from "./stats.js";
94
101
  import { waitForEvent } from "./wait-for-event.js";
95
102
 
@@ -97,6 +104,41 @@ export { logger };
97
104
  const warn = logger.newScope("warn");
98
105
 
99
106
  export { BandwidthTracker }; // might be useful for others
107
+ export {
108
+ Routes,
109
+ type RelayInfo,
110
+ type RouteInfo,
111
+ type RoutesLike,
112
+ } from "./routes.js";
113
+ export {
114
+ RUST_CORE_GLOBAL_KEY,
115
+ type NativeWire,
116
+ type RustBlockExchange,
117
+ type RustBlockProviderCache,
118
+ type RustCoreStream,
119
+ type RustDecodedBlockMessage,
120
+ type RustDecodedFanoutJoinAccept,
121
+ type RustDecodedFanoutJoinReject,
122
+ type RustDecodedFanoutJoinReq,
123
+ type RustDecodedFanoutParentProbeReply,
124
+ type RustDecodedFanoutProviderEntry,
125
+ type RustDecodedFanoutTrackerReplyEntry,
126
+ type RustDecodedFanoutUnicast,
127
+ type RustDecodedPubSubMessage,
128
+ type RustEagerBlockCache,
129
+ type RustFanoutTree,
130
+ type RustLanesInit,
131
+ type RustParentUpgradeGateOptions,
132
+ type RustParentUpgradeGateState,
133
+ type RustParentUpgradeOptions,
134
+ type RustParentUpgradePolicy,
135
+ type RustRoutesInit,
136
+ type RustSeenCache,
137
+ type RustStreamDecisions,
138
+ type RustTopicControl,
139
+ type RustTopicRootDirectoryState,
140
+ } from "./rust-core.js";
141
+ export type { PushableLanes } from "./pushable-lanes.js";
100
142
 
101
143
  const getErrorName = (e: any): string | undefined =>
102
144
  e?.name ?? e?.constructor?.name;
@@ -174,6 +216,14 @@ export interface PeerStreamsInit {
174
216
  protocol: string;
175
217
  connId: string;
176
218
  outboundQueue?: PeerOutboundQueueOptions;
219
+ /**
220
+ * Native outbound lane scheduler factory (rust-core mode). When set, the
221
+ * WRR scheduling/byte-budget decisions of the outbound queue run in the
222
+ * native core instead of the TS `pushableLanes`.
223
+ */
224
+ lanesFactory?: (
225
+ init: RustLanesInit,
226
+ ) => PushableLanes<Uint8Array | Uint8ArrayList>;
177
227
  }
178
228
  const DEFAULT_SEEK_MESSAGE_REDUDANCY = 2;
179
229
  const DEFAULT_SILENT_MESSAGE_REDUDANCY = 1;
@@ -328,6 +378,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
328
378
 
329
379
  private usedBandWidthTracker: BandwidthTracker;
330
380
  private readonly outboundQueue?: PeerOutboundQueueOptions;
381
+ private readonly lanesFactory?: PeerStreamsInit["lanesFactory"];
331
382
 
332
383
  // Unified outbound streams list (during grace may contain >1; after pruning length==1)
333
384
  private outboundStreams: OutboundCandidate[] = [];
@@ -380,17 +431,28 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
380
431
  private _addOutboundCandidate(raw: Stream): OutboundCandidate {
381
432
  const existing = this.outboundStreams.find((c) => c.raw === raw);
382
433
  if (existing) return existing;
383
- const pushableInst = pushableLanes<Uint8Array | Uint8ArrayList>({
384
- lanes: PRIORITY_LANES,
385
- maxBufferedBytes: this.outboundQueue?.maxBufferedBytes,
386
- overflow: "throw",
387
- onBufferSize: () => {
388
- this.dispatchEvent(new CustomEvent("queue:outbound"));
389
- },
390
- onPush: (val) => {
391
- candidate.bytesDelivered += val.byteLength;
392
- },
393
- });
434
+ const pushableInst = this.lanesFactory
435
+ ? this.lanesFactory({
436
+ lanes: PRIORITY_LANES,
437
+ maxBufferedBytes: this.outboundQueue?.maxBufferedBytes,
438
+ onBufferSize: () => {
439
+ this.dispatchEvent(new CustomEvent("queue:outbound"));
440
+ },
441
+ onPush: (val) => {
442
+ candidate.bytesDelivered += val.byteLength;
443
+ },
444
+ })
445
+ : pushableLanes<Uint8Array | Uint8ArrayList>({
446
+ lanes: PRIORITY_LANES,
447
+ maxBufferedBytes: this.outboundQueue?.maxBufferedBytes,
448
+ overflow: "throw",
449
+ onBufferSize: () => {
450
+ this.dispatchEvent(new CustomEvent("queue:outbound"));
451
+ },
452
+ onPush: (val) => {
453
+ candidate.bytesDelivered += val.byteLength;
454
+ },
455
+ });
394
456
  const candidate: OutboundCandidate = {
395
457
  raw,
396
458
  pushable: pushableInst,
@@ -477,6 +539,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
477
539
  this.usedBandWidthTracker = new BandwidthTracker(10);
478
540
  this.usedBandWidthTracker.start();
479
541
  this.outboundQueue = init.outboundQueue;
542
+ this.lanesFactory = init.lanesFactory;
480
543
  }
481
544
 
482
545
  /**
@@ -561,6 +624,11 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
561
624
  * Throws if there is no `stream` to write to available.
562
625
  */
563
626
  write(data: Uint8Array | Uint8ArrayList, priority: number) {
627
+ if (this.closed) {
628
+ logger.error(`Failed to send to stream ${this.peerId}: closed`);
629
+ throw new AbortError("Closed");
630
+ }
631
+
564
632
  if (data.length > MAX_DATA_LENGTH_OUT) {
565
633
  throw new Error(
566
634
  `Message too large (${data.length * 1e-6}) mb). Needs to be less than ${
@@ -646,12 +714,12 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
646
714
  priority = 0,
647
715
  signal?: AbortSignal,
648
716
  ) {
649
- if (this.closed) {
650
- logger.error(`Failed to send to stream ${this.peerId}: closed`);
651
- return;
652
- }
653
-
654
717
  while (true) {
718
+ if (this.closed) {
719
+ logger.error(`Failed to send to stream ${this.peerId}: closed`);
720
+ throw new AbortError("Closed");
721
+ }
722
+
655
723
  if (!this.isWritable) {
656
724
  // Outbound stream negotiation can legitimately take several seconds in CI
657
725
  // (identify/protocol discovery, resource contention, etc). Keep this fairly
@@ -696,6 +764,9 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
696
764
  // Catch a race where writability flips after the first check.
697
765
  if (this.isWritable) onOutbound();
698
766
  });
767
+
768
+ // Re-enter through the lifecycle check before attempting the write.
769
+ continue;
699
770
  }
700
771
 
701
772
  try {
@@ -817,6 +888,36 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
817
888
  this._pruneInboundInactive();
818
889
  }
819
890
 
891
+ public detachInboundStream(
892
+ record: InboundStreamRecord,
893
+ reason: Error = new AbortError("Inbound stream ended"),
894
+ options?: { closeRaw?: boolean },
895
+ ) {
896
+ const index = this.inboundStreams.indexOf(record);
897
+ if (index === -1) return false;
898
+ this.inboundStreams.splice(index, 1);
899
+ try {
900
+ record.abortController.abort(reason);
901
+ } catch {}
902
+ if (options?.closeRaw !== false) {
903
+ try {
904
+ record.raw.abort?.(reason);
905
+ } catch {}
906
+ void Promise.resolve(record.raw.close?.()).catch(() => {});
907
+ }
908
+ if (this.rawInboundStream === record.raw) {
909
+ const next = this.inboundStreams[0];
910
+ this.rawInboundStream = next?.raw;
911
+ this.inboundStream = next?.iterable;
912
+ }
913
+ if (this.inboundStreams.length <= 1 && this._inboundPruneTimer) {
914
+ clearTimeout(this._inboundPruneTimer);
915
+ this._inboundPruneTimer = undefined;
916
+ }
917
+ this.dispatchEvent(new CustomEvent("stream:inbound"));
918
+ return true;
919
+ }
920
+
820
921
  /**
821
922
  * Attach a raw outbound stream and setup a write stream
822
923
  */
@@ -1021,6 +1122,11 @@ type OutboundQueueArguments =
1021
1122
  }
1022
1123
  | false;
1023
1124
 
1125
+ const NATIVE_WIRE_RECORD_WORDS = 4;
1126
+ const NATIVE_WIRE_FLAG_DECODE_OK = 0x01;
1127
+ const NATIVE_WIRE_VERIFY_VERIFIED = 1;
1128
+ const NATIVE_WIRE_VERIFY_UNSUPPORTED = 2;
1129
+
1024
1130
  export type DirectStreamOptions = {
1025
1131
  canRelayMessage?: boolean;
1026
1132
  messageProcessingConcurrency?: number;
@@ -1050,6 +1156,23 @@ export type DirectStreamOptions = {
1050
1156
  seenCacheMax?: number;
1051
1157
  seenCacheTtlMs?: number;
1052
1158
  outboundQueue?: OutboundQueueArguments;
1159
+ /**
1160
+ * Offload inbound envelope decode + signature verification to a native
1161
+ * (wasm) module (`@peerbit/network-rust`). Frames arriving within the
1162
+ * same tick are verified as one batch. When unset (the default) the pure
1163
+ * TS path is used, byte-for-byte unchanged.
1164
+ */
1165
+ nativeWire?: NativeWire;
1166
+ /**
1167
+ * Run the DirectStream protocol state machine (routing table, seen-cache
1168
+ * dedup, outbound lane scheduling, relay/ack decisions) in the native
1169
+ * (wasm) core from `@peerbit/network-rust`; implies `nativeWire` for the
1170
+ * inbound batch decode+verify unless one is given explicitly. JS keeps
1171
+ * the sockets and the public API/events; routing decisions come from the
1172
+ * core. Off by default; `false` also opts out of any test-time injected
1173
+ * core (see `RUST_CORE_GLOBAL_KEY`).
1174
+ */
1175
+ rustCore?: RustCoreStream | false;
1053
1176
  };
1054
1177
 
1055
1178
  type ConnectionManagerLike = {
@@ -1081,7 +1204,7 @@ export interface DirectStreamComponents {
1081
1204
 
1082
1205
  type SharedRoutingState = {
1083
1206
  session: number;
1084
- routes: Routes;
1207
+ routes: RoutesLike;
1085
1208
  controller: AbortController;
1086
1209
  refs: number;
1087
1210
  };
@@ -1113,7 +1236,7 @@ export abstract class DirectStream<
1113
1236
  */
1114
1237
  public peers: Map<string, PeerStreams>;
1115
1238
  public peerKeyHashToPublicKey: Map<string, PublicSignKey>;
1116
- public routes: Routes;
1239
+ public routes: RoutesLike;
1117
1240
  /**
1118
1241
  * If router can relay received messages, even if not subscribed
1119
1242
  */
@@ -1149,6 +1272,37 @@ export abstract class DirectStream<
1149
1272
  }> = new Set();
1150
1273
  private sharedRoutingKey?: PrivateKey;
1151
1274
  private sharedRoutingState?: SharedRoutingState;
1275
+ private readonly nativeWire?: NativeWire;
1276
+ protected readonly rustCore?: RustCoreStream;
1277
+ private readonly rustSeenCache?: RustSeenCache;
1278
+ private pendingNativeWireFrames: {
1279
+ from: PublicSignKey;
1280
+ peerStreams: PeerStreams;
1281
+ bytes: Uint8ArrayList;
1282
+ done: DeferredPromise<void>;
1283
+ }[] = [];
1284
+ private nativeWireFlushScheduled = false;
1285
+ /**
1286
+ * Always-on counters recording where per-frame wire work executed. Tests
1287
+ * and benchmarks use them to prove the native path leaves no per-message
1288
+ * JS decode/verify work on the hot path. `tsEnvelopeDecodes` counts the
1289
+ * TS envelope object materializations (`Message.from`); on the native
1290
+ * path that object only feeds the routing state machine and app-facing
1291
+ * events, holding the payload as a zero-copy view, while decode
1292
+ * validation and signature verification stay native.
1293
+ */
1294
+ public readonly wireCounters = {
1295
+ /** Frames decoded and signature-verified by the native wire module. */
1296
+ nativeFrames: 0,
1297
+ /** Frames where the native decode failed and the TS path took over. */
1298
+ nativeFallbackFrames: 0,
1299
+ /** Frames processed without a native wire module (pure TS path). */
1300
+ tsFrames: 0,
1301
+ /** TS-side signature verifications (not pre-seeded by native decode). */
1302
+ tsSignatureVerifies: 0,
1303
+ /** TS envelope object materializations (`Message.from`). */
1304
+ tsEnvelopeDecodes: 0,
1305
+ };
1152
1306
 
1153
1307
  // for sequential creation of outbound streams
1154
1308
  public outboundInflightQueue: Pushable<{
@@ -1196,8 +1350,17 @@ export abstract class DirectStream<
1196
1350
  seenCacheTtlMs = 10 * 60 * 1e3,
1197
1351
  inboundIdleTimeout,
1198
1352
  outboundQueue,
1353
+ nativeWire,
1354
+ rustCore,
1199
1355
  } = options || {};
1200
1356
 
1357
+ this.rustCore =
1358
+ rustCore === false ? undefined : (rustCore ?? resolveInjectedRustCore());
1359
+ this.nativeWire = nativeWire ?? this.rustCore?.nativeWire;
1360
+ this.rustSeenCache = this.rustCore?.createSeenCache({
1361
+ max: Math.max(1, Math.floor(seenCacheMax)),
1362
+ ttl: Math.max(1, Math.floor(seenCacheTtlMs)),
1363
+ });
1201
1364
  const signKey = getKeypairFromPrivateKey(components.privateKey);
1202
1365
  this.seekTimeout = seekTimeout;
1203
1366
  this.sign = (bytes) => signKey.sign(bytes, PreHash.SHA_256);
@@ -1326,6 +1489,26 @@ export abstract class DirectStream<
1326
1489
  : undefined;
1327
1490
  }
1328
1491
 
1492
+ private createRoutes(signal: AbortSignal): RoutesLike {
1493
+ if (this.rustCore) {
1494
+ return this.rustCore.createRoutes({
1495
+ me: this.publicKeyHash,
1496
+ routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
1497
+ signal,
1498
+ maxFromEntries: this.routeCacheMaxFromEntries,
1499
+ maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
1500
+ maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
1501
+ });
1502
+ }
1503
+ return new Routes(this.publicKeyHash, {
1504
+ routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
1505
+ signal,
1506
+ maxFromEntries: this.routeCacheMaxFromEntries,
1507
+ maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
1508
+ maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
1509
+ });
1510
+ }
1511
+
1329
1512
  private pruneConnectionsToLimits(): Promise<void> {
1330
1513
  if (this.pruneToLimitsInFlight) {
1331
1514
  return this.pruneToLimitsInFlight;
@@ -1458,13 +1641,7 @@ export abstract class DirectStream<
1458
1641
  state = {
1459
1642
  session: Date.now(),
1460
1643
  controller,
1461
- routes: new Routes(this.publicKeyHash, {
1462
- routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
1463
- signal: controller.signal,
1464
- maxFromEntries: this.routeCacheMaxFromEntries,
1465
- maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
1466
- maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
1467
- }),
1644
+ routes: this.createRoutes(controller.signal),
1468
1645
  refs: 0,
1469
1646
  };
1470
1647
  sharedRoutingByPrivateKey.set(key, state);
@@ -1482,13 +1659,7 @@ export abstract class DirectStream<
1482
1659
  this.routes = state.routes;
1483
1660
  } else {
1484
1661
  this.session = Date.now();
1485
- this.routes = new Routes(this.publicKeyHash, {
1486
- routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
1487
- signal: this.closeController.signal,
1488
- maxFromEntries: this.routeCacheMaxFromEntries,
1489
- maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
1490
- maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
1491
- });
1662
+ this.routes = this.createRoutes(this.closeController.signal);
1492
1663
  }
1493
1664
 
1494
1665
  this.started = true;
@@ -1663,6 +1834,11 @@ export abstract class DirectStream<
1663
1834
  this.queue.clear();
1664
1835
  this.peers.clear();
1665
1836
  this.seenCache.clear();
1837
+ this.rustSeenCache?.clear();
1838
+ for (const pending of this.pendingNativeWireFrames) {
1839
+ pending.done.resolve();
1840
+ }
1841
+ this.pendingNativeWireFrames = [];
1666
1842
  // When routing is shared across co-located protocols, only clear once the last
1667
1843
  // instance stops. Otherwise we'd wipe routes still in use by other services.
1668
1844
  if (!sharedState) {
@@ -1764,16 +1940,21 @@ export abstract class DirectStream<
1764
1940
  }
1765
1941
 
1766
1942
  try {
1767
- stream = await connection.newStream(this.multicodecs, {
1768
- // TODO this property seems necessary, together with waitFor isReadable when making sure two peers are conencted before talking.
1769
- // research whether we can do without this so we can push data without beeing able to send
1770
- // more info here https://github.com/libp2p/js-libp2p/issues/2321
1771
- negotiateFully: true,
1772
- signal: anySignal([
1773
- this.closeController.signal,
1774
- ...(opts?.signal ? [opts.signal] : []),
1775
- ]),
1776
- });
1943
+ const streamSignal = anySignal([
1944
+ this.closeController.signal,
1945
+ ...(opts?.signal ? [opts.signal] : []),
1946
+ ]);
1947
+ try {
1948
+ stream = await connection.newStream(this.multicodecs, {
1949
+ // TODO this property seems necessary, together with waitFor isReadable when making sure two peers are conencted before talking.
1950
+ // research whether we can do without this so we can push data without beeing able to send
1951
+ // more info here https://github.com/libp2p/js-libp2p/issues/2321
1952
+ negotiateFully: true,
1953
+ signal: streamSignal,
1954
+ });
1955
+ } finally {
1956
+ streamSignal.clear?.();
1957
+ }
1777
1958
 
1778
1959
  if (stream.protocol == null) {
1779
1960
  stream.abort(new Error("Stream was not multiplexed"));
@@ -2025,6 +2206,9 @@ export abstract class DirectStream<
2025
2206
  protocol,
2026
2207
  connId,
2027
2208
  outboundQueue: this.outboundQueueOptions,
2209
+ lanesFactory: this.rustCore
2210
+ ? (init) => this.rustCore!.createLanes(init)
2211
+ : undefined,
2028
2212
  });
2029
2213
 
2030
2214
  this.peers.set(publicKeyHash, peerStreams);
@@ -2103,6 +2287,7 @@ export abstract class DirectStream<
2103
2287
  record: InboundStreamRecord,
2104
2288
  peerStreams: PeerStreams,
2105
2289
  ) {
2290
+ let failed = false;
2106
2291
  try {
2107
2292
  for await (const data of record.iterable) {
2108
2293
  const now = Date.now();
@@ -2112,6 +2297,7 @@ export abstract class DirectStream<
2112
2297
  this.processRpc(peerId, peerStreams, data).catch((e) => logError(e));
2113
2298
  }
2114
2299
  } catch (err: any) {
2300
+ failed = true;
2115
2301
  if (err?.code === "ERR_STREAM_RESET") {
2116
2302
  // only send stream reset messages to info
2117
2303
  logger(
@@ -2129,6 +2315,15 @@ export abstract class DirectStream<
2129
2315
  );
2130
2316
  }
2131
2317
  this.onPeerDisconnected(peerStreams.peerId);
2318
+ } finally {
2319
+ const removed = peerStreams.detachInboundStream(
2320
+ record,
2321
+ new AbortError("Inbound stream reader ended"),
2322
+ { closeRaw: failed },
2323
+ );
2324
+ if (removed && !failed && !peerStreams.isReadable) {
2325
+ void this.onPeerDisconnected(peerStreams.peerId).catch(logError);
2326
+ }
2132
2327
  }
2133
2328
  }
2134
2329
 
@@ -2143,10 +2338,32 @@ export abstract class DirectStream<
2143
2338
  // logger.trace("rpc from " + from + ", " + this.peerIdStr);
2144
2339
 
2145
2340
  if (message.length > 0) {
2341
+ if (this.nativeWire) {
2342
+ // Batch frames arriving within the same tick so decode +
2343
+ // signature verification happen in one native call. The
2344
+ // returned promise still resolves only after this frame's
2345
+ // processMessage turn completes (the TS-path contract).
2346
+ const done = pDefer<void>();
2347
+ this.pendingNativeWireFrames.push({
2348
+ from,
2349
+ peerStreams,
2350
+ bytes: message,
2351
+ done,
2352
+ });
2353
+ if (!this.nativeWireFlushScheduled) {
2354
+ this.nativeWireFlushScheduled = true;
2355
+ queueMicrotask(() => this.flushNativeWireFrames());
2356
+ }
2357
+ await done.promise;
2358
+ return true;
2359
+ }
2360
+
2361
+ this.wireCounters.tsFrames++;
2146
2362
  let decodedMessage: Message | undefined;
2147
2363
  let priority = 0;
2148
2364
  try {
2149
2365
  decodedMessage = Message.from(message);
2366
+ this.wireCounters.tsEnvelopeDecodes++;
2150
2367
  priority = decodedMessage.header.priority ?? 0;
2151
2368
  } catch {
2152
2369
  // This is only a best-effort priority peek. processMessage()
@@ -2172,12 +2389,146 @@ export abstract class DirectStream<
2172
2389
  return true;
2173
2390
  }
2174
2391
 
2392
+ /**
2393
+ * Drain the frames collected by processRpc() through the native wire
2394
+ * module: one batched decode+verify call, then the usual per-message
2395
+ * pipeline with the TS message object pre-seeded with the native
2396
+ * verification result. Any native failure falls back to the pure TS
2397
+ * path for that frame, so semantics never diverge.
2398
+ */
2399
+ private flushNativeWireFrames() {
2400
+ this.nativeWireFlushScheduled = false;
2401
+ const pending = this.pendingNativeWireFrames;
2402
+ if (pending.length === 0) {
2403
+ return;
2404
+ }
2405
+ this.pendingNativeWireFrames = [];
2406
+
2407
+ let records: Uint32Array | undefined;
2408
+ if (this.nativeWire) {
2409
+ try {
2410
+ records = this.nativeWire.decodeAndVerifyBatch(
2411
+ pending.map((frame) => frame.bytes.subarray()),
2412
+ Date.now(),
2413
+ );
2414
+ if (records.length !== pending.length * NATIVE_WIRE_RECORD_WORDS) {
2415
+ records = undefined;
2416
+ }
2417
+ } catch (error: any) {
2418
+ logger(
2419
+ "Native wire batch decode failed, falling back to TS path: " +
2420
+ error?.message,
2421
+ );
2422
+ records = undefined;
2423
+ }
2424
+ }
2425
+
2426
+ for (let i = 0; i < pending.length; i++) {
2427
+ const { from, peerStreams, bytes, done } = pending[i];
2428
+ const base = i * NATIVE_WIRE_RECORD_WORDS;
2429
+ const word0 = records ? records[base] : 0;
2430
+ const decodeOk =
2431
+ records != null && (word0 & NATIVE_WIRE_FLAG_DECODE_OK) !== 0;
2432
+ const verifyStatus = (word0 >>> 16) & 0xff;
2433
+ if (decodeOk && verifyStatus !== NATIVE_WIRE_VERIFY_UNSUPPORTED) {
2434
+ this.wireCounters.nativeFrames++;
2435
+ } else {
2436
+ this.wireCounters.nativeFallbackFrames++;
2437
+ }
2438
+
2439
+ let decodedMessage: Message | undefined;
2440
+ let priority = 0;
2441
+ try {
2442
+ decodedMessage = Message.from(bytes);
2443
+ this.wireCounters.tsEnvelopeDecodes++;
2444
+ priority = decodedMessage.header.priority ?? 0;
2445
+ } catch {
2446
+ // Best-effort peek, same as the TS path: processMessage()
2447
+ // performs the authoritative decode and logs invalid frames.
2448
+ }
2449
+ if (
2450
+ decodedMessage &&
2451
+ decodeOk &&
2452
+ verifyStatus !== NATIVE_WIRE_VERIFY_UNSUPPORTED
2453
+ ) {
2454
+ // Seed the memoized verification result; verify(true) in the
2455
+ // dispatch path (verifyAndProcess) short-circuits on it.
2456
+ decodedMessage._verified =
2457
+ verifyStatus === NATIVE_WIRE_VERIFY_VERIFIED;
2458
+ }
2459
+ this.queue
2460
+ .add(
2461
+ async () => {
2462
+ try {
2463
+ await this.processMessage(
2464
+ from,
2465
+ peerStreams,
2466
+ bytes,
2467
+ decodedMessage,
2468
+ );
2469
+ } catch (err: any) {
2470
+ logger.error(err);
2471
+ }
2472
+ },
2473
+ { priority },
2474
+ )
2475
+ .catch(logError)
2476
+ .finally(() => done.resolve());
2477
+ }
2478
+ }
2479
+
2480
+ /**
2481
+ * Native decode+verify for an envelope frame that arrived outside the
2482
+ * inbound stream path (e.g. nested inside another protocol's payload).
2483
+ * Seeds the memoized verification result so `verify(true)`
2484
+ * short-circuits; on any native failure the TS verification path stays
2485
+ * authoritative.
2486
+ */
2487
+ protected seedNativeWireVerification(
2488
+ message: Message,
2489
+ frame: Uint8Array | Uint8ArrayList,
2490
+ ): void {
2491
+ if (!this.nativeWire || message._verified != null) {
2492
+ return;
2493
+ }
2494
+ try {
2495
+ const records = this.nativeWire.decodeAndVerifyBatch(
2496
+ [frame instanceof Uint8Array ? frame : frame.subarray()],
2497
+ Date.now(),
2498
+ );
2499
+ if (records.length !== NATIVE_WIRE_RECORD_WORDS) {
2500
+ return;
2501
+ }
2502
+ const word0 = records[0];
2503
+ if ((word0 & NATIVE_WIRE_FLAG_DECODE_OK) === 0) {
2504
+ this.wireCounters.nativeFallbackFrames++;
2505
+ return;
2506
+ }
2507
+ const verifyStatus = (word0 >>> 16) & 0xff;
2508
+ if (verifyStatus === NATIVE_WIRE_VERIFY_UNSUPPORTED) {
2509
+ this.wireCounters.nativeFallbackFrames++;
2510
+ return;
2511
+ }
2512
+ this.wireCounters.nativeFrames++;
2513
+ message._verified = verifyStatus === NATIVE_WIRE_VERIFY_VERIFIED;
2514
+ } catch {
2515
+ // TS verification stays authoritative
2516
+ }
2517
+ }
2518
+
2175
2519
  private async modifySeenCache(
2176
2520
  message: Uint8Array | Uint8ArrayList,
2177
2521
  getIdFn: (
2178
2522
  bytes: Uint8Array | Uint8ArrayList,
2179
2523
  ) => Promise<string> = getMsgId,
2524
+ seenKeyKind: 0 | 1 = 0,
2180
2525
  ) {
2526
+ if (this.rustSeenCache) {
2527
+ return this.rustSeenCache.modify(
2528
+ message instanceof Uint8Array ? message : message.subarray(),
2529
+ seenKeyKind,
2530
+ );
2531
+ }
2181
2532
  const msgId = await getIdFn(message);
2182
2533
  const seen = this.seenCache.get(msgId);
2183
2534
  this.seenCache.add(msgId, seen ? seen + 1 : 1);
@@ -2200,7 +2551,10 @@ export abstract class DirectStream<
2200
2551
  // Ensure the message is valid before processing it
2201
2552
  let message: Message | undefined = decodedMessage;
2202
2553
  try {
2203
- message ??= Message.from(msg);
2554
+ if (message == null) {
2555
+ message = Message.from(msg);
2556
+ this.wireCounters.tsEnvelopeDecodes++;
2557
+ }
2204
2558
  } catch (error) {
2205
2559
  warn(error, "Failed to decode message frame from", from.hashcode());
2206
2560
  return;
@@ -2230,6 +2584,20 @@ export abstract class DirectStream<
2230
2584
  }
2231
2585
 
2232
2586
  public shouldIgnore(message: DataMessage, seenBefore: number) {
2587
+ if (this.rustCore) {
2588
+ const mode = message.header.mode;
2589
+ return this.rustCore.decisions.shouldIgnoreData({
2590
+ seenBefore,
2591
+ acknowledgedMode: isAcknowledgedDeliveryMode(mode),
2592
+ redundancy: isAcknowledgedDeliveryMode(mode) ? mode.redundancy : 0,
2593
+ hops: getDeliveryHopTrace(mode),
2594
+ me: this.publicKeyHash,
2595
+ signedBySelf:
2596
+ message.header.signatures?.publicKeys.some((x) =>
2597
+ x.equals(this.publicKey),
2598
+ ) ?? false,
2599
+ });
2600
+ }
2233
2601
  if (isAcknowledgedDeliveryMode(message.header.mode)) {
2234
2602
  if (hasDeliveryHop(message.header.mode, this.publicKeyHash)) {
2235
2603
  return true;
@@ -2322,6 +2690,9 @@ export abstract class DirectStream<
2322
2690
  }
2323
2691
 
2324
2692
  public async verifyAndProcess(message: Message<any>) {
2693
+ if (message._verified == null) {
2694
+ this.wireCounters.tsSignatureVerifies++;
2695
+ }
2325
2696
  const verified = await message.verify(true);
2326
2697
  if (!verified) {
2327
2698
  return false;
@@ -2349,13 +2720,18 @@ export abstract class DirectStream<
2349
2720
  message.header.mode instanceof AcknowledgeAnyWhere
2350
2721
  ? true
2351
2722
  : message.header.mode.to.includes(this.publicKeyHash);
2352
- if (
2353
- !shouldAcknowledgeDataMessage({
2354
- isRecipient,
2355
- seenBefore,
2356
- redundancy: message.header.mode.redundancy,
2357
- })
2358
- ) {
2723
+ const shouldAcknowledge = this.rustCore
2724
+ ? this.rustCore.decisions.shouldAcknowledge({
2725
+ isRecipient,
2726
+ seenBefore,
2727
+ redundancy: message.header.mode.redundancy,
2728
+ })
2729
+ : shouldAcknowledgeDataMessage({
2730
+ isRecipient,
2731
+ seenBefore,
2732
+ redundancy: message.header.mode.redundancy,
2733
+ });
2734
+ if (!shouldAcknowledge) {
2359
2735
  return;
2360
2736
  }
2361
2737
  const signers = [...getDeliveryHopTrace(message.header.mode)];
@@ -2418,6 +2794,7 @@ export abstract class DirectStream<
2418
2794
  : messageBytes.subarray(),
2419
2795
  (bytes) =>
2420
2796
  sha256Base64(bytes instanceof Uint8Array ? bytes : bytes.subarray()),
2797
+ 1,
2421
2798
  );
2422
2799
 
2423
2800
  if (seenBefore > 0) {
@@ -2433,10 +2810,21 @@ export abstract class DirectStream<
2433
2810
  }
2434
2811
 
2435
2812
  const messageIdString = toBase64(message.messageIdToAcknowledge);
2436
- const myIndex = message.header.mode.trace.findIndex(
2437
- (x) => x === this.publicKeyHash,
2438
- );
2439
- const next = message.header.mode.trace[myIndex - 1];
2813
+ let myIndex: number;
2814
+ let next: string | undefined;
2815
+ if (this.rustCore) {
2816
+ const hop = this.rustCore.decisions.ackNextHop(
2817
+ message.header.mode.trace,
2818
+ this.publicKeyHash,
2819
+ );
2820
+ myIndex = hop.myIndex;
2821
+ next = hop.next;
2822
+ } else {
2823
+ myIndex = message.header.mode.trace.findIndex(
2824
+ (x) => x === this.publicKeyHash,
2825
+ );
2826
+ next = message.header.mode.trace[myIndex - 1];
2827
+ }
2440
2828
  const nextStream = next ? this.peers.get(next) : undefined;
2441
2829
 
2442
2830
  this._ackCallbacks
@@ -2985,15 +3373,25 @@ export abstract class DirectStream<
2985
3373
  ) {
2986
3374
  const upstreamHash = messageFrom?.publicKey.hashcode();
2987
3375
 
2988
- const routeUpdate = computeSeekAckRouteUpdate({
2989
- current: this.publicKeyHash,
2990
- upstream: upstreamHash,
2991
- downstream: messageThrough.publicKey.hashcode(),
2992
- target: messageTargetHash,
2993
- // Route "distance" is based on recipient-seen order (0 = fastest). This is relied upon by
2994
- // `Routes.getFanout(...)` which uses `distance < redundancy` to select redundant next-hops.
2995
- distance: seenCounter,
2996
- });
3376
+ // Route "distance" is based on recipient-seen order (0 = fastest). This is relied upon by
3377
+ // `Routes.getFanout(...)` which uses `distance < redundancy` to select redundant next-hops.
3378
+ const routeUpdate = this.rustCore
3379
+ ? {
3380
+ ...this.rustCore.decisions.seekAckRouteUpdate({
3381
+ current: this.publicKeyHash,
3382
+ upstream: upstreamHash,
3383
+ downstream: messageThrough.publicKey.hashcode(),
3384
+ }),
3385
+ target: messageTargetHash,
3386
+ distance: seenCounter,
3387
+ }
3388
+ : computeSeekAckRouteUpdate({
3389
+ current: this.publicKeyHash,
3390
+ upstream: upstreamHash,
3391
+ downstream: messageThrough.publicKey.hashcode(),
3392
+ target: messageTargetHash,
3393
+ distance: seenCounter,
3394
+ });
2997
3395
 
2998
3396
  this.addRouteConnection(
2999
3397
  routeUpdate.from,
@@ -3154,20 +3552,41 @@ export abstract class DirectStream<
3154
3552
  message.header.mode instanceof AcknowledgeDelivery &&
3155
3553
  usedNeighbours.size < message.header.mode.redundancy
3156
3554
  ) {
3157
- for (const [neighbour, stream] of this.peers) {
3158
- if (usedNeighbours.size >= message.header.mode.redundancy) {
3159
- break;
3160
- }
3161
- if (usedNeighbours.has(neighbour)) continue;
3162
- usedNeighbours.add(neighbour);
3163
- promises.push(
3164
- this.waitForPeerWrite(
3165
- stream,
3166
- bytes,
3167
- message.header.priority,
3168
- signal,
3169
- ),
3555
+ if (this.rustCore) {
3556
+ const probes = this.rustCore.decisions.selectRedundancyProbes(
3557
+ [...this.peers.keys()],
3558
+ [...usedNeighbours],
3559
+ message.header.mode.redundancy,
3170
3560
  );
3561
+ for (const neighbour of probes) {
3562
+ const stream = this.peers.get(neighbour);
3563
+ if (!stream) continue;
3564
+ usedNeighbours.add(neighbour);
3565
+ promises.push(
3566
+ this.waitForPeerWrite(
3567
+ stream,
3568
+ bytes,
3569
+ message.header.priority,
3570
+ signal,
3571
+ ),
3572
+ );
3573
+ }
3574
+ } else {
3575
+ for (const [neighbour, stream] of this.peers) {
3576
+ if (usedNeighbours.size >= message.header.mode.redundancy) {
3577
+ break;
3578
+ }
3579
+ if (usedNeighbours.has(neighbour)) continue;
3580
+ usedNeighbours.add(neighbour);
3581
+ promises.push(
3582
+ this.waitForPeerWrite(
3583
+ stream,
3584
+ bytes,
3585
+ message.header.priority,
3586
+ signal,
3587
+ ),
3588
+ );
3589
+ }
3171
3590
  }
3172
3591
  }
3173
3592
 
@@ -3183,14 +3602,25 @@ export abstract class DirectStream<
3183
3602
  if (isRelayed && message.header.mode instanceof SilentDelivery) {
3184
3603
  const promises: Promise<any>[] = [];
3185
3604
  const originalTo = message.header.mode.to;
3186
- for (const recipient of originalTo) {
3187
- if (recipient === this.publicKeyHash) continue;
3188
- if (recipient === from.hashcode()) continue; // never send back to previous hop
3605
+ const recipients = this.rustCore
3606
+ ? this.rustCore.decisions.filterSilentRelayRecipients(
3607
+ originalTo,
3608
+ this.publicKeyHash,
3609
+ from.hashcode(),
3610
+ [...this.peers.keys()],
3611
+ getDeliveryHopTrace(message.header.mode),
3612
+ )
3613
+ : undefined;
3614
+ for (const recipient of recipients ?? originalTo) {
3615
+ if (!recipients) {
3616
+ if (recipient === this.publicKeyHash) continue;
3617
+ if (recipient === from.hashcode()) continue; // never send back to previous hop
3618
+ if (hasDeliveryHop(message.header.mode, recipient)) {
3619
+ continue; // recipient already signed/seen this message
3620
+ }
3621
+ }
3189
3622
  const stream = this.peers.get(recipient);
3190
3623
  if (!stream) continue;
3191
- if (hasDeliveryHop(message.header.mode, recipient)) {
3192
- continue; // recipient already signed/seen this message
3193
- }
3194
3624
  message.header.mode.to = [recipient];
3195
3625
  promises.push(
3196
3626
  this.waitForPeerWrite(
@@ -3225,26 +3655,47 @@ export abstract class DirectStream<
3225
3655
 
3226
3656
  let sentOnce = false;
3227
3657
  const promises: Promise<any>[] = [];
3228
- for (const stream of peers.values()) {
3229
- const id = stream as PeerStreams;
3230
-
3231
- // Dont sent back to the sender
3232
- if (id.publicKey.equals(from)) {
3233
- continue;
3658
+ if (this.rustCore) {
3659
+ const candidates = [...peers.values()] as PeerStreams[];
3660
+ const kept = this.rustCore.decisions.filterFloodTargets(
3661
+ candidates.map((stream) => stream.publicKey.hashcode()),
3662
+ from.hashcode(),
3663
+ message.header.signatures?.publicKeys.map((x) => x.hashcode()) ?? [],
3664
+ getDeliveryHopTrace(message.header.mode),
3665
+ );
3666
+ for (const index of kept) {
3667
+ sentOnce = true;
3668
+ promises.push(
3669
+ this.waitForPeerWrite(
3670
+ candidates[index],
3671
+ bytes,
3672
+ message.header.priority,
3673
+ signal,
3674
+ ),
3675
+ );
3234
3676
  }
3235
- // Dont send message back to any peer already present in the path.
3236
- if (
3237
- message.header.signatures?.publicKeys.find((x) =>
3238
- x.equals(id.publicKey),
3239
- ) ||
3240
- hasDeliveryHop(message.header.mode, id.publicKey.hashcode())
3241
- ) {
3242
- continue;
3677
+ } else {
3678
+ for (const stream of peers.values()) {
3679
+ const id = stream as PeerStreams;
3680
+
3681
+ // Dont sent back to the sender
3682
+ if (id.publicKey.equals(from)) {
3683
+ continue;
3684
+ }
3685
+ // Dont send message back to any peer already present in the path.
3686
+ if (
3687
+ message.header.signatures?.publicKeys.find((x) =>
3688
+ x.equals(id.publicKey),
3689
+ ) ||
3690
+ hasDeliveryHop(message.header.mode, id.publicKey.hashcode())
3691
+ ) {
3692
+ continue;
3693
+ }
3694
+ sentOnce = true;
3695
+ promises.push(
3696
+ this.waitForPeerWrite(id, bytes, message.header.priority, signal),
3697
+ );
3243
3698
  }
3244
- sentOnce = true;
3245
- promises.push(
3246
- this.waitForPeerWrite(id, bytes, message.header.priority, signal),
3247
- );
3248
3699
  }
3249
3700
  await Promise.all(promises);
3250
3701
  startDeliveryTimeout?.();