@peerbit/stream 5.0.16 → 5.1.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
@@ -34,6 +34,7 @@ import {
34
34
  AcknowledgeAnyWhere,
35
35
  AcknowledgeDelivery,
36
36
  AnyWhere,
37
+ BACKGROUND_MESSAGE_PRIORITY,
37
38
  DataMessage,
38
39
  DeliveryError,
39
40
  Goodbye,
@@ -88,7 +89,14 @@ import {
88
89
  } from "./core/seek-routing.js";
89
90
  import { logger } from "./logger.js";
90
91
  import { type PushableLanes, pushableLanes } from "./pushable-lanes.js";
91
- 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";
92
100
  import { BandwidthTracker } from "./stats.js";
93
101
  import { waitForEvent } from "./wait-for-event.js";
94
102
 
@@ -96,6 +104,41 @@ export { logger };
96
104
  const warn = logger.newScope("warn");
97
105
 
98
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";
99
142
 
100
143
  const getErrorName = (e: any): string | undefined =>
101
144
  e?.name ?? e?.constructor?.name;
@@ -173,6 +216,14 @@ export interface PeerStreamsInit {
173
216
  protocol: string;
174
217
  connId: string;
175
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>;
176
227
  }
177
228
  const DEFAULT_SEEK_MESSAGE_REDUDANCY = 2;
178
229
  const DEFAULT_SILENT_MESSAGE_REDUDANCY = 1;
@@ -327,6 +378,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
327
378
 
328
379
  private usedBandWidthTracker: BandwidthTracker;
329
380
  private readonly outboundQueue?: PeerOutboundQueueOptions;
381
+ private readonly lanesFactory?: PeerStreamsInit["lanesFactory"];
330
382
 
331
383
  // Unified outbound streams list (during grace may contain >1; after pruning length==1)
332
384
  private outboundStreams: OutboundCandidate[] = [];
@@ -379,17 +431,28 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
379
431
  private _addOutboundCandidate(raw: Stream): OutboundCandidate {
380
432
  const existing = this.outboundStreams.find((c) => c.raw === raw);
381
433
  if (existing) return existing;
382
- const pushableInst = pushableLanes<Uint8Array | Uint8ArrayList>({
383
- lanes: PRIORITY_LANES,
384
- maxBufferedBytes: this.outboundQueue?.maxBufferedBytes,
385
- overflow: "throw",
386
- onBufferSize: () => {
387
- this.dispatchEvent(new CustomEvent("queue:outbound"));
388
- },
389
- onPush: (val) => {
390
- candidate.bytesDelivered += val.byteLength;
391
- },
392
- });
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
+ });
393
456
  const candidate: OutboundCandidate = {
394
457
  raw,
395
458
  pushable: pushableInst,
@@ -476,6 +539,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
476
539
  this.usedBandWidthTracker = new BandwidthTracker(10);
477
540
  this.usedBandWidthTracker.start();
478
541
  this.outboundQueue = init.outboundQueue;
542
+ this.lanesFactory = init.lanesFactory;
479
543
  }
480
544
 
481
545
  /**
@@ -500,7 +564,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
500
564
  if (!this.outboundQueue) {
501
565
  return undefined;
502
566
  }
503
- if (lane === getLaneFromPriority(0)) {
567
+ if (lane === getLaneFromPriority(BACKGROUND_MESSAGE_PRIORITY)) {
504
568
  return Math.max(
505
569
  0,
506
570
  this.outboundQueue.maxBufferedBytes -
@@ -816,6 +880,36 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
816
880
  this._pruneInboundInactive();
817
881
  }
818
882
 
883
+ public detachInboundStream(
884
+ record: InboundStreamRecord,
885
+ reason: Error = new AbortError("Inbound stream ended"),
886
+ options?: { closeRaw?: boolean },
887
+ ) {
888
+ const index = this.inboundStreams.indexOf(record);
889
+ if (index === -1) return false;
890
+ this.inboundStreams.splice(index, 1);
891
+ try {
892
+ record.abortController.abort(reason);
893
+ } catch {}
894
+ if (options?.closeRaw !== false) {
895
+ try {
896
+ record.raw.abort?.(reason);
897
+ } catch {}
898
+ void Promise.resolve(record.raw.close?.()).catch(() => {});
899
+ }
900
+ if (this.rawInboundStream === record.raw) {
901
+ const next = this.inboundStreams[0];
902
+ this.rawInboundStream = next?.raw;
903
+ this.inboundStream = next?.iterable;
904
+ }
905
+ if (this.inboundStreams.length <= 1 && this._inboundPruneTimer) {
906
+ clearTimeout(this._inboundPruneTimer);
907
+ this._inboundPruneTimer = undefined;
908
+ }
909
+ this.dispatchEvent(new CustomEvent("stream:inbound"));
910
+ return true;
911
+ }
912
+
819
913
  /**
820
914
  * Attach a raw outbound stream and setup a write stream
821
915
  */
@@ -1020,6 +1114,11 @@ type OutboundQueueArguments =
1020
1114
  }
1021
1115
  | false;
1022
1116
 
1117
+ const NATIVE_WIRE_RECORD_WORDS = 4;
1118
+ const NATIVE_WIRE_FLAG_DECODE_OK = 0x01;
1119
+ const NATIVE_WIRE_VERIFY_VERIFIED = 1;
1120
+ const NATIVE_WIRE_VERIFY_UNSUPPORTED = 2;
1121
+
1023
1122
  export type DirectStreamOptions = {
1024
1123
  canRelayMessage?: boolean;
1025
1124
  messageProcessingConcurrency?: number;
@@ -1049,6 +1148,23 @@ export type DirectStreamOptions = {
1049
1148
  seenCacheMax?: number;
1050
1149
  seenCacheTtlMs?: number;
1051
1150
  outboundQueue?: OutboundQueueArguments;
1151
+ /**
1152
+ * Offload inbound envelope decode + signature verification to a native
1153
+ * (wasm) module (`@peerbit/network-rust`). Frames arriving within the
1154
+ * same tick are verified as one batch. When unset (the default) the pure
1155
+ * TS path is used, byte-for-byte unchanged.
1156
+ */
1157
+ nativeWire?: NativeWire;
1158
+ /**
1159
+ * Run the DirectStream protocol state machine (routing table, seen-cache
1160
+ * dedup, outbound lane scheduling, relay/ack decisions) in the native
1161
+ * (wasm) core from `@peerbit/network-rust`; implies `nativeWire` for the
1162
+ * inbound batch decode+verify unless one is given explicitly. JS keeps
1163
+ * the sockets and the public API/events; routing decisions come from the
1164
+ * core. Off by default; `false` also opts out of any test-time injected
1165
+ * core (see `RUST_CORE_GLOBAL_KEY`).
1166
+ */
1167
+ rustCore?: RustCoreStream | false;
1052
1168
  };
1053
1169
 
1054
1170
  type ConnectionManagerLike = {
@@ -1080,7 +1196,7 @@ export interface DirectStreamComponents {
1080
1196
 
1081
1197
  type SharedRoutingState = {
1082
1198
  session: number;
1083
- routes: Routes;
1199
+ routes: RoutesLike;
1084
1200
  controller: AbortController;
1085
1201
  refs: number;
1086
1202
  };
@@ -1112,7 +1228,7 @@ export abstract class DirectStream<
1112
1228
  */
1113
1229
  public peers: Map<string, PeerStreams>;
1114
1230
  public peerKeyHashToPublicKey: Map<string, PublicSignKey>;
1115
- public routes: Routes;
1231
+ public routes: RoutesLike;
1116
1232
  /**
1117
1233
  * If router can relay received messages, even if not subscribed
1118
1234
  */
@@ -1148,6 +1264,37 @@ export abstract class DirectStream<
1148
1264
  }> = new Set();
1149
1265
  private sharedRoutingKey?: PrivateKey;
1150
1266
  private sharedRoutingState?: SharedRoutingState;
1267
+ private readonly nativeWire?: NativeWire;
1268
+ protected readonly rustCore?: RustCoreStream;
1269
+ private readonly rustSeenCache?: RustSeenCache;
1270
+ private pendingNativeWireFrames: {
1271
+ from: PublicSignKey;
1272
+ peerStreams: PeerStreams;
1273
+ bytes: Uint8ArrayList;
1274
+ done: DeferredPromise<void>;
1275
+ }[] = [];
1276
+ private nativeWireFlushScheduled = false;
1277
+ /**
1278
+ * Always-on counters recording where per-frame wire work executed. Tests
1279
+ * and benchmarks use them to prove the native path leaves no per-message
1280
+ * JS decode/verify work on the hot path. `tsEnvelopeDecodes` counts the
1281
+ * TS envelope object materializations (`Message.from`); on the native
1282
+ * path that object only feeds the routing state machine and app-facing
1283
+ * events, holding the payload as a zero-copy view, while decode
1284
+ * validation and signature verification stay native.
1285
+ */
1286
+ public readonly wireCounters = {
1287
+ /** Frames decoded and signature-verified by the native wire module. */
1288
+ nativeFrames: 0,
1289
+ /** Frames where the native decode failed and the TS path took over. */
1290
+ nativeFallbackFrames: 0,
1291
+ /** Frames processed without a native wire module (pure TS path). */
1292
+ tsFrames: 0,
1293
+ /** TS-side signature verifications (not pre-seeded by native decode). */
1294
+ tsSignatureVerifies: 0,
1295
+ /** TS envelope object materializations (`Message.from`). */
1296
+ tsEnvelopeDecodes: 0,
1297
+ };
1151
1298
 
1152
1299
  // for sequential creation of outbound streams
1153
1300
  public outboundInflightQueue: Pushable<{
@@ -1195,8 +1342,17 @@ export abstract class DirectStream<
1195
1342
  seenCacheTtlMs = 10 * 60 * 1e3,
1196
1343
  inboundIdleTimeout,
1197
1344
  outboundQueue,
1345
+ nativeWire,
1346
+ rustCore,
1198
1347
  } = options || {};
1199
1348
 
1349
+ this.rustCore =
1350
+ rustCore === false ? undefined : (rustCore ?? resolveInjectedRustCore());
1351
+ this.nativeWire = nativeWire ?? this.rustCore?.nativeWire;
1352
+ this.rustSeenCache = this.rustCore?.createSeenCache({
1353
+ max: Math.max(1, Math.floor(seenCacheMax)),
1354
+ ttl: Math.max(1, Math.floor(seenCacheTtlMs)),
1355
+ });
1200
1356
  const signKey = getKeypairFromPrivateKey(components.privateKey);
1201
1357
  this.seekTimeout = seekTimeout;
1202
1358
  this.sign = (bytes) => signKey.sign(bytes, PreHash.SHA_256);
@@ -1325,6 +1481,26 @@ export abstract class DirectStream<
1325
1481
  : undefined;
1326
1482
  }
1327
1483
 
1484
+ private createRoutes(signal: AbortSignal): RoutesLike {
1485
+ if (this.rustCore) {
1486
+ return this.rustCore.createRoutes({
1487
+ me: this.publicKeyHash,
1488
+ routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
1489
+ signal,
1490
+ maxFromEntries: this.routeCacheMaxFromEntries,
1491
+ maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
1492
+ maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
1493
+ });
1494
+ }
1495
+ return new Routes(this.publicKeyHash, {
1496
+ routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
1497
+ signal,
1498
+ maxFromEntries: this.routeCacheMaxFromEntries,
1499
+ maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
1500
+ maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
1501
+ });
1502
+ }
1503
+
1328
1504
  private pruneConnectionsToLimits(): Promise<void> {
1329
1505
  if (this.pruneToLimitsInFlight) {
1330
1506
  return this.pruneToLimitsInFlight;
@@ -1457,13 +1633,7 @@ export abstract class DirectStream<
1457
1633
  state = {
1458
1634
  session: Date.now(),
1459
1635
  controller,
1460
- routes: new Routes(this.publicKeyHash, {
1461
- routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
1462
- signal: controller.signal,
1463
- maxFromEntries: this.routeCacheMaxFromEntries,
1464
- maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
1465
- maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
1466
- }),
1636
+ routes: this.createRoutes(controller.signal),
1467
1637
  refs: 0,
1468
1638
  };
1469
1639
  sharedRoutingByPrivateKey.set(key, state);
@@ -1481,13 +1651,7 @@ export abstract class DirectStream<
1481
1651
  this.routes = state.routes;
1482
1652
  } else {
1483
1653
  this.session = Date.now();
1484
- this.routes = new Routes(this.publicKeyHash, {
1485
- routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
1486
- signal: this.closeController.signal,
1487
- maxFromEntries: this.routeCacheMaxFromEntries,
1488
- maxTargetsPerFrom: this.routeCacheMaxTargetsPerFrom,
1489
- maxRelaysPerTarget: this.routeCacheMaxRelaysPerTarget,
1490
- });
1654
+ this.routes = this.createRoutes(this.closeController.signal);
1491
1655
  }
1492
1656
 
1493
1657
  this.started = true;
@@ -1662,6 +1826,11 @@ export abstract class DirectStream<
1662
1826
  this.queue.clear();
1663
1827
  this.peers.clear();
1664
1828
  this.seenCache.clear();
1829
+ this.rustSeenCache?.clear();
1830
+ for (const pending of this.pendingNativeWireFrames) {
1831
+ pending.done.resolve();
1832
+ }
1833
+ this.pendingNativeWireFrames = [];
1665
1834
  // When routing is shared across co-located protocols, only clear once the last
1666
1835
  // instance stops. Otherwise we'd wipe routes still in use by other services.
1667
1836
  if (!sharedState) {
@@ -1763,16 +1932,21 @@ export abstract class DirectStream<
1763
1932
  }
1764
1933
 
1765
1934
  try {
1766
- stream = await connection.newStream(this.multicodecs, {
1767
- // TODO this property seems necessary, together with waitFor isReadable when making sure two peers are conencted before talking.
1768
- // research whether we can do without this so we can push data without beeing able to send
1769
- // more info here https://github.com/libp2p/js-libp2p/issues/2321
1770
- negotiateFully: true,
1771
- signal: anySignal([
1772
- this.closeController.signal,
1773
- ...(opts?.signal ? [opts.signal] : []),
1774
- ]),
1775
- });
1935
+ const streamSignal = anySignal([
1936
+ this.closeController.signal,
1937
+ ...(opts?.signal ? [opts.signal] : []),
1938
+ ]);
1939
+ try {
1940
+ stream = await connection.newStream(this.multicodecs, {
1941
+ // TODO this property seems necessary, together with waitFor isReadable when making sure two peers are conencted before talking.
1942
+ // research whether we can do without this so we can push data without beeing able to send
1943
+ // more info here https://github.com/libp2p/js-libp2p/issues/2321
1944
+ negotiateFully: true,
1945
+ signal: streamSignal,
1946
+ });
1947
+ } finally {
1948
+ streamSignal.clear?.();
1949
+ }
1776
1950
 
1777
1951
  if (stream.protocol == null) {
1778
1952
  stream.abort(new Error("Stream was not multiplexed"));
@@ -2024,6 +2198,9 @@ export abstract class DirectStream<
2024
2198
  protocol,
2025
2199
  connId,
2026
2200
  outboundQueue: this.outboundQueueOptions,
2201
+ lanesFactory: this.rustCore
2202
+ ? (init) => this.rustCore!.createLanes(init)
2203
+ : undefined,
2027
2204
  });
2028
2205
 
2029
2206
  this.peers.set(publicKeyHash, peerStreams);
@@ -2102,6 +2279,7 @@ export abstract class DirectStream<
2102
2279
  record: InboundStreamRecord,
2103
2280
  peerStreams: PeerStreams,
2104
2281
  ) {
2282
+ let failed = false;
2105
2283
  try {
2106
2284
  for await (const data of record.iterable) {
2107
2285
  const now = Date.now();
@@ -2111,6 +2289,7 @@ export abstract class DirectStream<
2111
2289
  this.processRpc(peerId, peerStreams, data).catch((e) => logError(e));
2112
2290
  }
2113
2291
  } catch (err: any) {
2292
+ failed = true;
2114
2293
  if (err?.code === "ERR_STREAM_RESET") {
2115
2294
  // only send stream reset messages to info
2116
2295
  logger(
@@ -2128,6 +2307,15 @@ export abstract class DirectStream<
2128
2307
  );
2129
2308
  }
2130
2309
  this.onPeerDisconnected(peerStreams.peerId);
2310
+ } finally {
2311
+ const removed = peerStreams.detachInboundStream(
2312
+ record,
2313
+ new AbortError("Inbound stream reader ended"),
2314
+ { closeRaw: failed },
2315
+ );
2316
+ if (removed && !failed && !peerStreams.isReadable) {
2317
+ void this.onPeerDisconnected(peerStreams.peerId).catch(logError);
2318
+ }
2131
2319
  }
2132
2320
  }
2133
2321
 
@@ -2142,10 +2330,32 @@ export abstract class DirectStream<
2142
2330
  // logger.trace("rpc from " + from + ", " + this.peerIdStr);
2143
2331
 
2144
2332
  if (message.length > 0) {
2333
+ if (this.nativeWire) {
2334
+ // Batch frames arriving within the same tick so decode +
2335
+ // signature verification happen in one native call. The
2336
+ // returned promise still resolves only after this frame's
2337
+ // processMessage turn completes (the TS-path contract).
2338
+ const done = pDefer<void>();
2339
+ this.pendingNativeWireFrames.push({
2340
+ from,
2341
+ peerStreams,
2342
+ bytes: message,
2343
+ done,
2344
+ });
2345
+ if (!this.nativeWireFlushScheduled) {
2346
+ this.nativeWireFlushScheduled = true;
2347
+ queueMicrotask(() => this.flushNativeWireFrames());
2348
+ }
2349
+ await done.promise;
2350
+ return true;
2351
+ }
2352
+
2353
+ this.wireCounters.tsFrames++;
2145
2354
  let decodedMessage: Message | undefined;
2146
2355
  let priority = 0;
2147
2356
  try {
2148
2357
  decodedMessage = Message.from(message);
2358
+ this.wireCounters.tsEnvelopeDecodes++;
2149
2359
  priority = decodedMessage.header.priority ?? 0;
2150
2360
  } catch {
2151
2361
  // This is only a best-effort priority peek. processMessage()
@@ -2171,12 +2381,146 @@ export abstract class DirectStream<
2171
2381
  return true;
2172
2382
  }
2173
2383
 
2384
+ /**
2385
+ * Drain the frames collected by processRpc() through the native wire
2386
+ * module: one batched decode+verify call, then the usual per-message
2387
+ * pipeline with the TS message object pre-seeded with the native
2388
+ * verification result. Any native failure falls back to the pure TS
2389
+ * path for that frame, so semantics never diverge.
2390
+ */
2391
+ private flushNativeWireFrames() {
2392
+ this.nativeWireFlushScheduled = false;
2393
+ const pending = this.pendingNativeWireFrames;
2394
+ if (pending.length === 0) {
2395
+ return;
2396
+ }
2397
+ this.pendingNativeWireFrames = [];
2398
+
2399
+ let records: Uint32Array | undefined;
2400
+ if (this.nativeWire) {
2401
+ try {
2402
+ records = this.nativeWire.decodeAndVerifyBatch(
2403
+ pending.map((frame) => frame.bytes.subarray()),
2404
+ Date.now(),
2405
+ );
2406
+ if (records.length !== pending.length * NATIVE_WIRE_RECORD_WORDS) {
2407
+ records = undefined;
2408
+ }
2409
+ } catch (error: any) {
2410
+ logger(
2411
+ "Native wire batch decode failed, falling back to TS path: " +
2412
+ error?.message,
2413
+ );
2414
+ records = undefined;
2415
+ }
2416
+ }
2417
+
2418
+ for (let i = 0; i < pending.length; i++) {
2419
+ const { from, peerStreams, bytes, done } = pending[i];
2420
+ const base = i * NATIVE_WIRE_RECORD_WORDS;
2421
+ const word0 = records ? records[base] : 0;
2422
+ const decodeOk =
2423
+ records != null && (word0 & NATIVE_WIRE_FLAG_DECODE_OK) !== 0;
2424
+ const verifyStatus = (word0 >>> 16) & 0xff;
2425
+ if (decodeOk && verifyStatus !== NATIVE_WIRE_VERIFY_UNSUPPORTED) {
2426
+ this.wireCounters.nativeFrames++;
2427
+ } else {
2428
+ this.wireCounters.nativeFallbackFrames++;
2429
+ }
2430
+
2431
+ let decodedMessage: Message | undefined;
2432
+ let priority = 0;
2433
+ try {
2434
+ decodedMessage = Message.from(bytes);
2435
+ this.wireCounters.tsEnvelopeDecodes++;
2436
+ priority = decodedMessage.header.priority ?? 0;
2437
+ } catch {
2438
+ // Best-effort peek, same as the TS path: processMessage()
2439
+ // performs the authoritative decode and logs invalid frames.
2440
+ }
2441
+ if (
2442
+ decodedMessage &&
2443
+ decodeOk &&
2444
+ verifyStatus !== NATIVE_WIRE_VERIFY_UNSUPPORTED
2445
+ ) {
2446
+ // Seed the memoized verification result; verify(true) in the
2447
+ // dispatch path (verifyAndProcess) short-circuits on it.
2448
+ decodedMessage._verified =
2449
+ verifyStatus === NATIVE_WIRE_VERIFY_VERIFIED;
2450
+ }
2451
+ this.queue
2452
+ .add(
2453
+ async () => {
2454
+ try {
2455
+ await this.processMessage(
2456
+ from,
2457
+ peerStreams,
2458
+ bytes,
2459
+ decodedMessage,
2460
+ );
2461
+ } catch (err: any) {
2462
+ logger.error(err);
2463
+ }
2464
+ },
2465
+ { priority },
2466
+ )
2467
+ .catch(logError)
2468
+ .finally(() => done.resolve());
2469
+ }
2470
+ }
2471
+
2472
+ /**
2473
+ * Native decode+verify for an envelope frame that arrived outside the
2474
+ * inbound stream path (e.g. nested inside another protocol's payload).
2475
+ * Seeds the memoized verification result so `verify(true)`
2476
+ * short-circuits; on any native failure the TS verification path stays
2477
+ * authoritative.
2478
+ */
2479
+ protected seedNativeWireVerification(
2480
+ message: Message,
2481
+ frame: Uint8Array | Uint8ArrayList,
2482
+ ): void {
2483
+ if (!this.nativeWire || message._verified != null) {
2484
+ return;
2485
+ }
2486
+ try {
2487
+ const records = this.nativeWire.decodeAndVerifyBatch(
2488
+ [frame instanceof Uint8Array ? frame : frame.subarray()],
2489
+ Date.now(),
2490
+ );
2491
+ if (records.length !== NATIVE_WIRE_RECORD_WORDS) {
2492
+ return;
2493
+ }
2494
+ const word0 = records[0];
2495
+ if ((word0 & NATIVE_WIRE_FLAG_DECODE_OK) === 0) {
2496
+ this.wireCounters.nativeFallbackFrames++;
2497
+ return;
2498
+ }
2499
+ const verifyStatus = (word0 >>> 16) & 0xff;
2500
+ if (verifyStatus === NATIVE_WIRE_VERIFY_UNSUPPORTED) {
2501
+ this.wireCounters.nativeFallbackFrames++;
2502
+ return;
2503
+ }
2504
+ this.wireCounters.nativeFrames++;
2505
+ message._verified = verifyStatus === NATIVE_WIRE_VERIFY_VERIFIED;
2506
+ } catch {
2507
+ // TS verification stays authoritative
2508
+ }
2509
+ }
2510
+
2174
2511
  private async modifySeenCache(
2175
2512
  message: Uint8Array | Uint8ArrayList,
2176
2513
  getIdFn: (
2177
2514
  bytes: Uint8Array | Uint8ArrayList,
2178
2515
  ) => Promise<string> = getMsgId,
2516
+ seenKeyKind: 0 | 1 = 0,
2179
2517
  ) {
2518
+ if (this.rustSeenCache) {
2519
+ return this.rustSeenCache.modify(
2520
+ message instanceof Uint8Array ? message : message.subarray(),
2521
+ seenKeyKind,
2522
+ );
2523
+ }
2180
2524
  const msgId = await getIdFn(message);
2181
2525
  const seen = this.seenCache.get(msgId);
2182
2526
  this.seenCache.add(msgId, seen ? seen + 1 : 1);
@@ -2199,7 +2543,10 @@ export abstract class DirectStream<
2199
2543
  // Ensure the message is valid before processing it
2200
2544
  let message: Message | undefined = decodedMessage;
2201
2545
  try {
2202
- message ??= Message.from(msg);
2546
+ if (message == null) {
2547
+ message = Message.from(msg);
2548
+ this.wireCounters.tsEnvelopeDecodes++;
2549
+ }
2203
2550
  } catch (error) {
2204
2551
  warn(error, "Failed to decode message frame from", from.hashcode());
2205
2552
  return;
@@ -2229,6 +2576,20 @@ export abstract class DirectStream<
2229
2576
  }
2230
2577
 
2231
2578
  public shouldIgnore(message: DataMessage, seenBefore: number) {
2579
+ if (this.rustCore) {
2580
+ const mode = message.header.mode;
2581
+ return this.rustCore.decisions.shouldIgnoreData({
2582
+ seenBefore,
2583
+ acknowledgedMode: isAcknowledgedDeliveryMode(mode),
2584
+ redundancy: isAcknowledgedDeliveryMode(mode) ? mode.redundancy : 0,
2585
+ hops: getDeliveryHopTrace(mode),
2586
+ me: this.publicKeyHash,
2587
+ signedBySelf:
2588
+ message.header.signatures?.publicKeys.some((x) =>
2589
+ x.equals(this.publicKey),
2590
+ ) ?? false,
2591
+ });
2592
+ }
2232
2593
  if (isAcknowledgedDeliveryMode(message.header.mode)) {
2233
2594
  if (hasDeliveryHop(message.header.mode, this.publicKeyHash)) {
2234
2595
  return true;
@@ -2321,6 +2682,9 @@ export abstract class DirectStream<
2321
2682
  }
2322
2683
 
2323
2684
  public async verifyAndProcess(message: Message<any>) {
2685
+ if (message._verified == null) {
2686
+ this.wireCounters.tsSignatureVerifies++;
2687
+ }
2324
2688
  const verified = await message.verify(true);
2325
2689
  if (!verified) {
2326
2690
  return false;
@@ -2348,13 +2712,18 @@ export abstract class DirectStream<
2348
2712
  message.header.mode instanceof AcknowledgeAnyWhere
2349
2713
  ? true
2350
2714
  : message.header.mode.to.includes(this.publicKeyHash);
2351
- if (
2352
- !shouldAcknowledgeDataMessage({
2353
- isRecipient,
2354
- seenBefore,
2355
- redundancy: message.header.mode.redundancy,
2356
- })
2357
- ) {
2715
+ const shouldAcknowledge = this.rustCore
2716
+ ? this.rustCore.decisions.shouldAcknowledge({
2717
+ isRecipient,
2718
+ seenBefore,
2719
+ redundancy: message.header.mode.redundancy,
2720
+ })
2721
+ : shouldAcknowledgeDataMessage({
2722
+ isRecipient,
2723
+ seenBefore,
2724
+ redundancy: message.header.mode.redundancy,
2725
+ });
2726
+ if (!shouldAcknowledge) {
2358
2727
  return;
2359
2728
  }
2360
2729
  const signers = [...getDeliveryHopTrace(message.header.mode)];
@@ -2417,6 +2786,7 @@ export abstract class DirectStream<
2417
2786
  : messageBytes.subarray(),
2418
2787
  (bytes) =>
2419
2788
  sha256Base64(bytes instanceof Uint8Array ? bytes : bytes.subarray()),
2789
+ 1,
2420
2790
  );
2421
2791
 
2422
2792
  if (seenBefore > 0) {
@@ -2432,10 +2802,21 @@ export abstract class DirectStream<
2432
2802
  }
2433
2803
 
2434
2804
  const messageIdString = toBase64(message.messageIdToAcknowledge);
2435
- const myIndex = message.header.mode.trace.findIndex(
2436
- (x) => x === this.publicKeyHash,
2437
- );
2438
- const next = message.header.mode.trace[myIndex - 1];
2805
+ let myIndex: number;
2806
+ let next: string | undefined;
2807
+ if (this.rustCore) {
2808
+ const hop = this.rustCore.decisions.ackNextHop(
2809
+ message.header.mode.trace,
2810
+ this.publicKeyHash,
2811
+ );
2812
+ myIndex = hop.myIndex;
2813
+ next = hop.next;
2814
+ } else {
2815
+ myIndex = message.header.mode.trace.findIndex(
2816
+ (x) => x === this.publicKeyHash,
2817
+ );
2818
+ next = message.header.mode.trace[myIndex - 1];
2819
+ }
2439
2820
  const nextStream = next ? this.peers.get(next) : undefined;
2440
2821
 
2441
2822
  this._ackCallbacks
@@ -2984,15 +3365,25 @@ export abstract class DirectStream<
2984
3365
  ) {
2985
3366
  const upstreamHash = messageFrom?.publicKey.hashcode();
2986
3367
 
2987
- const routeUpdate = computeSeekAckRouteUpdate({
2988
- current: this.publicKeyHash,
2989
- upstream: upstreamHash,
2990
- downstream: messageThrough.publicKey.hashcode(),
2991
- target: messageTargetHash,
2992
- // Route "distance" is based on recipient-seen order (0 = fastest). This is relied upon by
2993
- // `Routes.getFanout(...)` which uses `distance < redundancy` to select redundant next-hops.
2994
- distance: seenCounter,
2995
- });
3368
+ // Route "distance" is based on recipient-seen order (0 = fastest). This is relied upon by
3369
+ // `Routes.getFanout(...)` which uses `distance < redundancy` to select redundant next-hops.
3370
+ const routeUpdate = this.rustCore
3371
+ ? {
3372
+ ...this.rustCore.decisions.seekAckRouteUpdate({
3373
+ current: this.publicKeyHash,
3374
+ upstream: upstreamHash,
3375
+ downstream: messageThrough.publicKey.hashcode(),
3376
+ }),
3377
+ target: messageTargetHash,
3378
+ distance: seenCounter,
3379
+ }
3380
+ : computeSeekAckRouteUpdate({
3381
+ current: this.publicKeyHash,
3382
+ upstream: upstreamHash,
3383
+ downstream: messageThrough.publicKey.hashcode(),
3384
+ target: messageTargetHash,
3385
+ distance: seenCounter,
3386
+ });
2996
3387
 
2997
3388
  this.addRouteConnection(
2998
3389
  routeUpdate.from,
@@ -3153,20 +3544,41 @@ export abstract class DirectStream<
3153
3544
  message.header.mode instanceof AcknowledgeDelivery &&
3154
3545
  usedNeighbours.size < message.header.mode.redundancy
3155
3546
  ) {
3156
- for (const [neighbour, stream] of this.peers) {
3157
- if (usedNeighbours.size >= message.header.mode.redundancy) {
3158
- break;
3159
- }
3160
- if (usedNeighbours.has(neighbour)) continue;
3161
- usedNeighbours.add(neighbour);
3162
- promises.push(
3163
- this.waitForPeerWrite(
3164
- stream,
3165
- bytes,
3166
- message.header.priority,
3167
- signal,
3168
- ),
3547
+ if (this.rustCore) {
3548
+ const probes = this.rustCore.decisions.selectRedundancyProbes(
3549
+ [...this.peers.keys()],
3550
+ [...usedNeighbours],
3551
+ message.header.mode.redundancy,
3169
3552
  );
3553
+ for (const neighbour of probes) {
3554
+ const stream = this.peers.get(neighbour);
3555
+ if (!stream) continue;
3556
+ usedNeighbours.add(neighbour);
3557
+ promises.push(
3558
+ this.waitForPeerWrite(
3559
+ stream,
3560
+ bytes,
3561
+ message.header.priority,
3562
+ signal,
3563
+ ),
3564
+ );
3565
+ }
3566
+ } else {
3567
+ for (const [neighbour, stream] of this.peers) {
3568
+ if (usedNeighbours.size >= message.header.mode.redundancy) {
3569
+ break;
3570
+ }
3571
+ if (usedNeighbours.has(neighbour)) continue;
3572
+ usedNeighbours.add(neighbour);
3573
+ promises.push(
3574
+ this.waitForPeerWrite(
3575
+ stream,
3576
+ bytes,
3577
+ message.header.priority,
3578
+ signal,
3579
+ ),
3580
+ );
3581
+ }
3170
3582
  }
3171
3583
  }
3172
3584
 
@@ -3182,14 +3594,25 @@ export abstract class DirectStream<
3182
3594
  if (isRelayed && message.header.mode instanceof SilentDelivery) {
3183
3595
  const promises: Promise<any>[] = [];
3184
3596
  const originalTo = message.header.mode.to;
3185
- for (const recipient of originalTo) {
3186
- if (recipient === this.publicKeyHash) continue;
3187
- if (recipient === from.hashcode()) continue; // never send back to previous hop
3597
+ const recipients = this.rustCore
3598
+ ? this.rustCore.decisions.filterSilentRelayRecipients(
3599
+ originalTo,
3600
+ this.publicKeyHash,
3601
+ from.hashcode(),
3602
+ [...this.peers.keys()],
3603
+ getDeliveryHopTrace(message.header.mode),
3604
+ )
3605
+ : undefined;
3606
+ for (const recipient of recipients ?? originalTo) {
3607
+ if (!recipients) {
3608
+ if (recipient === this.publicKeyHash) continue;
3609
+ if (recipient === from.hashcode()) continue; // never send back to previous hop
3610
+ if (hasDeliveryHop(message.header.mode, recipient)) {
3611
+ continue; // recipient already signed/seen this message
3612
+ }
3613
+ }
3188
3614
  const stream = this.peers.get(recipient);
3189
3615
  if (!stream) continue;
3190
- if (hasDeliveryHop(message.header.mode, recipient)) {
3191
- continue; // recipient already signed/seen this message
3192
- }
3193
3616
  message.header.mode.to = [recipient];
3194
3617
  promises.push(
3195
3618
  this.waitForPeerWrite(
@@ -3224,26 +3647,47 @@ export abstract class DirectStream<
3224
3647
 
3225
3648
  let sentOnce = false;
3226
3649
  const promises: Promise<any>[] = [];
3227
- for (const stream of peers.values()) {
3228
- const id = stream as PeerStreams;
3229
-
3230
- // Dont sent back to the sender
3231
- if (id.publicKey.equals(from)) {
3232
- continue;
3650
+ if (this.rustCore) {
3651
+ const candidates = [...peers.values()] as PeerStreams[];
3652
+ const kept = this.rustCore.decisions.filterFloodTargets(
3653
+ candidates.map((stream) => stream.publicKey.hashcode()),
3654
+ from.hashcode(),
3655
+ message.header.signatures?.publicKeys.map((x) => x.hashcode()) ?? [],
3656
+ getDeliveryHopTrace(message.header.mode),
3657
+ );
3658
+ for (const index of kept) {
3659
+ sentOnce = true;
3660
+ promises.push(
3661
+ this.waitForPeerWrite(
3662
+ candidates[index],
3663
+ bytes,
3664
+ message.header.priority,
3665
+ signal,
3666
+ ),
3667
+ );
3233
3668
  }
3234
- // Dont send message back to any peer already present in the path.
3235
- if (
3236
- message.header.signatures?.publicKeys.find((x) =>
3237
- x.equals(id.publicKey),
3238
- ) ||
3239
- hasDeliveryHop(message.header.mode, id.publicKey.hashcode())
3240
- ) {
3241
- continue;
3669
+ } else {
3670
+ for (const stream of peers.values()) {
3671
+ const id = stream as PeerStreams;
3672
+
3673
+ // Dont sent back to the sender
3674
+ if (id.publicKey.equals(from)) {
3675
+ continue;
3676
+ }
3677
+ // Dont send message back to any peer already present in the path.
3678
+ if (
3679
+ message.header.signatures?.publicKeys.find((x) =>
3680
+ x.equals(id.publicKey),
3681
+ ) ||
3682
+ hasDeliveryHop(message.header.mode, id.publicKey.hashcode())
3683
+ ) {
3684
+ continue;
3685
+ }
3686
+ sentOnce = true;
3687
+ promises.push(
3688
+ this.waitForPeerWrite(id, bytes, message.header.priority, signal),
3689
+ );
3242
3690
  }
3243
- sentOnce = true;
3244
- promises.push(
3245
- this.waitForPeerWrite(id, bytes, message.header.priority, signal),
3246
- );
3247
3691
  }
3248
3692
  await Promise.all(promises);
3249
3693
  startDeliveryTimeout?.();
@@ -3528,7 +3972,10 @@ export abstract class DirectStream<
3528
3972
  if (limitBytes == null) {
3529
3973
  return undefined;
3530
3974
  }
3531
- if (getLaneFromPriority(priority) === getLaneFromPriority(0)) {
3975
+ if (
3976
+ getLaneFromPriority(priority) ===
3977
+ getLaneFromPriority(BACKGROUND_MESSAGE_PRIORITY)
3978
+ ) {
3532
3979
  return Math.max(
3533
3980
  0,
3534
3981
  limitBytes - this.outboundQueueOptions.reservedTotalPriorityBytes,