@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/dist/src/index.d.ts +76 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +400 -85
- package/dist/src/index.js.map +1 -1
- package/dist/src/routes.d.ts +31 -3
- package/dist/src/routes.d.ts.map +1 -1
- package/dist/src/routes.js.map +1 -1
- package/dist/src/rust-core.d.ts +561 -0
- package/dist/src/rust-core.d.ts.map +1 -0
- package/dist/src/rust-core.js +16 -0
- package/dist/src/rust-core.js.map +1 -0
- package/package.json +8 -11
- package/src/index.ts +552 -101
- package/src/routes.ts +43 -2
- package/src/rust-core.ts +696 -0
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 =
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
this.
|
|
1774
|
-
|
|
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
|
|
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
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
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
|
-
|
|
2437
|
-
|
|
2438
|
-
)
|
|
2439
|
-
|
|
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
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
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
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
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
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
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
|
-
|
|
3229
|
-
const
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
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
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
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?.();
|