@peerbit/libp2p-test-utils 2.2.0-bbf27fa → 2.2.0-cb91e7b

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.
@@ -0,0 +1,918 @@
1
+ import { UnsupportedProtocolError, } from "@libp2p/interface";
2
+ import { multiaddr } from "@multiformats/multiaddr";
3
+ import { getPublicKeyFromPeerId } from "@peerbit/crypto";
4
+ import { pushable } from "it-pushable";
5
+ import sodium from "libsodium-wrappers";
6
+ const parseTcpPort = (addr) => {
7
+ const str = addr.toString();
8
+ const m = str.match(/\/tcp\/(\d+)/);
9
+ if (!m)
10
+ return undefined;
11
+ return Number(m[1]);
12
+ };
13
+ const isPeerId = (value) => {
14
+ return (typeof value === "object" &&
15
+ value != null &&
16
+ typeof value.toString === "function" &&
17
+ value.type != null &&
18
+ value.publicKey != null);
19
+ };
20
+ const decodeUVarint = (buf) => {
21
+ let x = 0;
22
+ let s = 0;
23
+ for (let i = 0; i < buf.length; i++) {
24
+ const b = buf[i];
25
+ if (b < 0x80) {
26
+ if (i > 9 || (i === 9 && b > 1))
27
+ throw new Error("varint overflow");
28
+ return { value: x | (b << s), bytes: i + 1 };
29
+ }
30
+ x |= (b & 0x7f) << s;
31
+ s += 7;
32
+ }
33
+ throw new Error("unexpected eof decoding varint");
34
+ };
35
+ const readU32LE = (buf, offset) => (buf[offset + 0] |
36
+ (buf[offset + 1] << 8) |
37
+ (buf[offset + 2] << 16) |
38
+ (buf[offset + 3] << 24)) >>>
39
+ 0;
40
+ const shouldDropByMessageIdPrefix = (encodedFrame, base) => {
41
+ // MessageHeader.id starts at base+2 (after DataMessage + MessageHeader variants).
42
+ const idOffset = base + 2;
43
+ const b0 = encodedFrame[idOffset + 0];
44
+ const b1 = encodedFrame[idOffset + 1];
45
+ const b2 = encodedFrame[idOffset + 2];
46
+ const b3 = encodedFrame[idOffset + 3];
47
+ // Allow loss injection without breaking stream-level control plane:
48
+ // - "FOUT": FanoutTree data plane
49
+ return (b0 === 0x46 && b1 === 0x4f && b2 === 0x55 && b3 === 0x54 // "FOUT"
50
+ );
51
+ };
52
+ const mulberry32 = (seed) => {
53
+ let t = seed >>> 0;
54
+ return () => {
55
+ t += 0x6d2b79f5;
56
+ let x = t;
57
+ x = Math.imul(x ^ (x >>> 15), x | 1);
58
+ x ^= x + Math.imul(x ^ (x >>> 7), x | 61);
59
+ return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
60
+ };
61
+ };
62
+ export class InMemoryRegistrar {
63
+ handlers = new Map();
64
+ topologies = new Map();
65
+ topologySeq = 0;
66
+ async handle(protocol, handler, _opts) {
67
+ this.handlers.set(protocol, handler);
68
+ }
69
+ async unhandle(protocol) {
70
+ this.handlers.delete(protocol);
71
+ }
72
+ async register(protocol, topology) {
73
+ const id = `topology-${++this.topologySeq}`;
74
+ this.topologies.set(id, { protocol, topology });
75
+ return id;
76
+ }
77
+ async unregister(id) {
78
+ this.topologies.delete(id);
79
+ }
80
+ getHandler(protocol) {
81
+ return this.handlers.get(protocol);
82
+ }
83
+ getTopologies() {
84
+ return this.topologies.values();
85
+ }
86
+ }
87
+ class InMemoryStream extends EventTarget {
88
+ protocol;
89
+ direction;
90
+ id;
91
+ inbound;
92
+ readable;
93
+ rxDelayMs;
94
+ closed = false;
95
+ bufferedBytes = 0;
96
+ highWaterMark;
97
+ lowWaterMark;
98
+ backpressured = false;
99
+ pendingLengthPrefix;
100
+ peer;
101
+ recordSend;
102
+ shouldDrop;
103
+ recordDrop;
104
+ constructor(opts) {
105
+ super();
106
+ this.id = opts.id;
107
+ this.protocol = opts.protocol;
108
+ this.direction = opts.direction;
109
+ this.recordSend = opts.recordSend;
110
+ this.shouldDrop = opts.shouldDrop;
111
+ this.recordDrop = opts.recordDrop;
112
+ this.rxDelayMs = opts.rxDelayMs ?? 0;
113
+ this.highWaterMark = opts.highWaterMarkBytes ?? 256 * 1024;
114
+ this.lowWaterMark = Math.floor(this.highWaterMark / 2);
115
+ this.inbound = pushable({ objectMode: true });
116
+ const self = this;
117
+ this.readable = (async function* () {
118
+ for await (const chunk of self.inbound) {
119
+ if (self.rxDelayMs > 0) {
120
+ await new Promise((resolve) => setTimeout(resolve, self.rxDelayMs));
121
+ }
122
+ self.bufferedBytes = Math.max(0, self.bufferedBytes - chunk.byteLength);
123
+ if (self.backpressured && self.bufferedBytes <= self.lowWaterMark) {
124
+ self.backpressured = false;
125
+ self.peer?.dispatchEvent(new Event("drain"));
126
+ }
127
+ yield chunk;
128
+ }
129
+ })();
130
+ }
131
+ [Symbol.asyncIterator]() {
132
+ return this.readable[Symbol.asyncIterator]();
133
+ }
134
+ send(data) {
135
+ if (this.closed) {
136
+ throw new Error("Cannot send on closed stream");
137
+ }
138
+ // `it-length-prefixed` may yield the length prefix and the message body as
139
+ // separate chunks. Dropping only one of them would corrupt framing and can
140
+ // deadlock decoders. Buffer the prefix chunk so loss injection operates on
141
+ // whole messages.
142
+ if (this.pendingLengthPrefix) {
143
+ const prefix = this.pendingLengthPrefix;
144
+ this.pendingLengthPrefix = undefined;
145
+ const frame = new Uint8Array(prefix.byteLength + data.byteLength);
146
+ frame.set(prefix, 0);
147
+ frame.set(data, prefix.byteLength);
148
+ this.recordSend?.(frame);
149
+ if (this.shouldDrop?.(frame)) {
150
+ this.recordDrop?.(frame);
151
+ return true;
152
+ }
153
+ const remote = this.peer;
154
+ if (!remote)
155
+ throw new Error("Missing remote stream endpoint");
156
+ remote.bufferedBytes += frame.byteLength;
157
+ remote.inbound.push(frame);
158
+ if (remote.bufferedBytes > remote.highWaterMark) {
159
+ remote.backpressured = true;
160
+ return false;
161
+ }
162
+ return true;
163
+ }
164
+ // Prefix-only chunk? Buffer it and wait for the body chunk.
165
+ try {
166
+ const { bytes } = decodeUVarint(data);
167
+ if (bytes === data.byteLength && data.byteLength <= 10) {
168
+ this.pendingLengthPrefix = data;
169
+ return true;
170
+ }
171
+ }
172
+ catch {
173
+ // not a varint prefix
174
+ }
175
+ // Full frame already (prefix+body).
176
+ this.recordSend?.(data);
177
+ if (this.shouldDrop?.(data)) {
178
+ this.recordDrop?.(data);
179
+ return true;
180
+ }
181
+ const remote = this.peer;
182
+ if (!remote) {
183
+ throw new Error("Missing remote stream endpoint");
184
+ }
185
+ remote.bufferedBytes += data.byteLength;
186
+ remote.inbound.push(data);
187
+ if (remote.bufferedBytes > remote.highWaterMark) {
188
+ remote.backpressured = true;
189
+ return false;
190
+ }
191
+ return true;
192
+ }
193
+ abort(_err) {
194
+ if (this.closed)
195
+ return;
196
+ this.closed = true;
197
+ try {
198
+ this.inbound.end();
199
+ }
200
+ catch { }
201
+ this.dispatchEvent(new Event("close"));
202
+ }
203
+ close(_opts) {
204
+ this.abort();
205
+ return Promise.resolve();
206
+ }
207
+ }
208
+ class InMemoryConnection {
209
+ id;
210
+ remotePeer;
211
+ remoteAddr;
212
+ streams = [];
213
+ status = "open";
214
+ limits = undefined;
215
+ timeline = { open: Date.now(), close: undefined };
216
+ getLocalRegistrar;
217
+ getRemoteRegistrar;
218
+ getRemoteConnection;
219
+ recordSend;
220
+ shouldDrop;
221
+ recordDrop;
222
+ recordStreamOpen;
223
+ streamHighWaterMarkBytes;
224
+ streamRxDelayMs;
225
+ streamSeq = 0;
226
+ pair;
227
+ constructor(opts) {
228
+ this.id = opts.id;
229
+ this.remotePeer = opts.remotePeer;
230
+ this.remoteAddr = opts.remoteAddr;
231
+ this.getLocalRegistrar = opts.getLocalRegistrar;
232
+ this.getRemoteRegistrar = opts.getRemoteRegistrar;
233
+ this.getRemoteConnection = opts.getRemoteConnection;
234
+ this.recordSend = opts.recordSend;
235
+ this.shouldDrop = opts.shouldDrop;
236
+ this.recordDrop = opts.recordDrop;
237
+ this.recordStreamOpen = opts.recordStreamOpen;
238
+ this.streamHighWaterMarkBytes = opts.streamHighWaterMarkBytes;
239
+ this.streamRxDelayMs = opts.streamRxDelayMs;
240
+ }
241
+ _setPair(pair) {
242
+ this.pair = pair;
243
+ }
244
+ async newStream(protocols, opts) {
245
+ if (this.status !== "open") {
246
+ throw new Error("Connection is not open");
247
+ }
248
+ if (opts?.signal?.aborted) {
249
+ throw opts.signal.reason ?? new Error("Stream open aborted");
250
+ }
251
+ const list = Array.isArray(protocols) ? protocols : [protocols];
252
+ const protocol = list[0];
253
+ if (!protocol) {
254
+ throw new Error("Missing protocol");
255
+ }
256
+ const handler = this.getRemoteRegistrar().getHandler(protocol);
257
+ if (!handler) {
258
+ const err = new UnsupportedProtocolError(`No handler registered for protocol ${protocol}`);
259
+ err.code = "ERR_UNSUPPORTED_PROTOCOL";
260
+ throw err;
261
+ }
262
+ this.recordStreamOpen();
263
+ const remoteConn = this.getRemoteConnection();
264
+ const localStreamNo = ++this.streamSeq;
265
+ const remoteStreamNo = ++remoteConn.streamSeq;
266
+ const localStreamId = `${this.id}:out:${localStreamNo}`;
267
+ const remoteStreamId = `${remoteConn.id}:in:${remoteStreamNo}`;
268
+ const outbound = new InMemoryStream({
269
+ id: localStreamId,
270
+ protocol,
271
+ direction: "outbound",
272
+ recordSend: this.recordSend,
273
+ shouldDrop: this.shouldDrop,
274
+ recordDrop: this.recordDrop,
275
+ highWaterMarkBytes: this.streamHighWaterMarkBytes,
276
+ });
277
+ const inbound = new InMemoryStream({
278
+ id: remoteStreamId,
279
+ protocol,
280
+ direction: "inbound",
281
+ highWaterMarkBytes: this.streamHighWaterMarkBytes,
282
+ rxDelayMs: this.streamRxDelayMs,
283
+ });
284
+ outbound.peer = inbound;
285
+ inbound.peer = outbound;
286
+ this.streams.push(outbound);
287
+ remoteConn.streams.push(inbound);
288
+ const invokeHandler = () => handler(inbound, remoteConn).catch(() => {
289
+ // ignore handler errors in the transport shim
290
+ });
291
+ if (opts?.negotiateFully === false) {
292
+ // Fire handler async to better match real libp2p behavior.
293
+ queueMicrotask(() => {
294
+ invokeHandler();
295
+ });
296
+ }
297
+ else {
298
+ // When negotiateFully=true, invoke handler synchronously so the remote has
299
+ // a chance to attach inbound handlers before the caller starts sending.
300
+ invokeHandler();
301
+ }
302
+ return outbound;
303
+ }
304
+ async _closeLocal() {
305
+ if (this.status !== "open")
306
+ return;
307
+ this.status = "closed";
308
+ this.timeline.close = Date.now();
309
+ for (const s of this.streams) {
310
+ try {
311
+ s.close?.();
312
+ }
313
+ catch { }
314
+ }
315
+ }
316
+ async close(_opts) {
317
+ if (this.pair) {
318
+ await this.pair.close(this);
319
+ return;
320
+ }
321
+ await this._closeLocal();
322
+ }
323
+ }
324
+ class InMemoryConnectionPair {
325
+ opts;
326
+ closePromise;
327
+ constructor(opts) {
328
+ this.opts = opts;
329
+ }
330
+ async close(_initiator) {
331
+ if (this.closePromise)
332
+ return this.closePromise;
333
+ this.closePromise = (async () => {
334
+ const { aConn, bConn, aOwner, bOwner, aManager, bManager, network } = this.opts;
335
+ await Promise.all([aConn._closeLocal(), bConn._closeLocal()]);
336
+ aManager._removeConnectionInstance(bOwner.peerId.toString(), aConn);
337
+ bManager._removeConnectionInstance(aOwner.peerId.toString(), bConn);
338
+ network.metrics.connectionsClosed += 1;
339
+ network.notifyDisconnect(aOwner, bOwner.peerId, [aConn]);
340
+ network.notifyDisconnect(bOwner, aOwner.peerId, [bConn]);
341
+ })();
342
+ return this.closePromise;
343
+ }
344
+ }
345
+ export class InMemoryAddressManager {
346
+ addr;
347
+ constructor(addr) {
348
+ this.addr = addr;
349
+ }
350
+ getAddresses() {
351
+ return [this.addr];
352
+ }
353
+ }
354
+ export class InMemoryPeerStore {
355
+ getAddressesForPeerId;
356
+ constructor(getAddressesForPeerId) {
357
+ this.getAddressesForPeerId = getAddressesForPeerId;
358
+ }
359
+ async get(peerId) {
360
+ const addrs = this.getAddressesForPeerId(peerId) ?? [];
361
+ return { addresses: addrs.map((multiaddr) => ({ multiaddr })) };
362
+ }
363
+ async delete(_peerId) {
364
+ // noop
365
+ }
366
+ }
367
+ export class InMemoryConnectionManager {
368
+ network;
369
+ owner;
370
+ connectionsByRemote = new Map();
371
+ dialQueue = [];
372
+ connSeqBase;
373
+ constructor(network, owner, opts) {
374
+ this.network = network;
375
+ this.owner = owner;
376
+ this.connSeqBase = opts?.connSeqBase ?? owner.peerId.toString();
377
+ }
378
+ getConnections(peerId) {
379
+ if (!peerId) {
380
+ return [...this.connectionsByRemote.values()].flat();
381
+ }
382
+ return (this.connectionsByRemote.get(peerId.toString()) ?? []);
383
+ }
384
+ getConnectionsMap() {
385
+ return {
386
+ get: (peer) => this.getConnections(peer),
387
+ };
388
+ }
389
+ getDialQueue() {
390
+ return this.dialQueue;
391
+ }
392
+ async isDialable(addr) {
393
+ if (this.network.isPeerOffline(this.owner.peerId))
394
+ return false;
395
+ const list = Array.isArray(addr) ? addr : [addr];
396
+ for (const a of list) {
397
+ const port = parseTcpPort(a);
398
+ if (port == null)
399
+ continue;
400
+ const remote = this.network.getPeerByPort(port);
401
+ if (!remote)
402
+ continue;
403
+ if (this.network.isPeerOffline(remote.peerId))
404
+ continue;
405
+ if (remote.peerId.toString() === this.owner.peerId.toString())
406
+ continue;
407
+ return true;
408
+ }
409
+ return false;
410
+ }
411
+ async openConnection(peer) {
412
+ if (this.network.isPeerOffline(this.owner.peerId)) {
413
+ throw new Error("Peer is offline");
414
+ }
415
+ const remote = (() => {
416
+ if (isPeerId(peer)) {
417
+ return this.network.getPeerById(peer.toString());
418
+ }
419
+ const list = Array.isArray(peer) ? peer : [peer];
420
+ for (const addr of list) {
421
+ const port = parseTcpPort(addr);
422
+ if (port == null)
423
+ continue;
424
+ const found = this.network.getPeerByPort(port);
425
+ if (!found)
426
+ continue;
427
+ if (found.peerId.toString() === this.owner.peerId.toString())
428
+ continue;
429
+ return found;
430
+ }
431
+ return undefined;
432
+ })();
433
+ if (!remote) {
434
+ throw new Error("No dialable address");
435
+ }
436
+ if (this.network.isPeerOffline(remote.peerId)) {
437
+ throw new Error("Peer is offline");
438
+ }
439
+ const remoteKey = remote.peerId.toString();
440
+ const existing = this.connectionsByRemote.get(remoteKey)?.find((c) => c.status === "open");
441
+ if (existing)
442
+ return existing;
443
+ const dialEntry = { peerId: remote.peerId };
444
+ this.dialQueue.push(dialEntry);
445
+ try {
446
+ if (this.network.dialDelayMs > 0) {
447
+ await new Promise((resolve) => setTimeout(resolve, this.network.dialDelayMs));
448
+ }
449
+ return this._connectTo(remote);
450
+ }
451
+ finally {
452
+ const idx = this.dialQueue.indexOf(dialEntry);
453
+ if (idx !== -1)
454
+ this.dialQueue.splice(idx, 1);
455
+ }
456
+ }
457
+ async closeConnections(peer, _options) {
458
+ const remoteId = peer.toString();
459
+ const conns = this.connectionsByRemote.get(remoteId) ?? [];
460
+ if (conns.length === 0)
461
+ return;
462
+ // Closing one side will close the paired remote connection and notify both sides.
463
+ await Promise.all([...conns].map((c) => c.close()));
464
+ }
465
+ _removeConnectionInstance(remoteId, conn) {
466
+ const conns = this.connectionsByRemote.get(remoteId);
467
+ if (!conns)
468
+ return;
469
+ const idx = conns.indexOf(conn);
470
+ if (idx === -1)
471
+ return;
472
+ conns.splice(idx, 1);
473
+ if (conns.length === 0)
474
+ this.connectionsByRemote.delete(remoteId);
475
+ }
476
+ _connectTo(remote) {
477
+ const remoteKey = remote.peerId.toString();
478
+ const existing = this.connectionsByRemote.get(remoteKey)?.find((c) => c.status === "open");
479
+ if (existing)
480
+ return existing;
481
+ const addr = remote.addressManager.getAddresses()[0];
482
+ const connIdA = `${this.connSeqBase}->${remoteKey}:${Date.now()}:${Math.random()
483
+ .toString(16)
484
+ .slice(2)}`;
485
+ const connIdB = `${remoteKey}->${this.connSeqBase}:${Date.now()}:${Math.random()
486
+ .toString(16)
487
+ .slice(2)}`;
488
+ let connA;
489
+ let connB;
490
+ connA = new InMemoryConnection({
491
+ id: connIdA,
492
+ remotePeer: remote.peerId,
493
+ remoteAddr: addr,
494
+ getLocalRegistrar: () => this.owner.registrar,
495
+ getRemoteRegistrar: () => remote.registrar,
496
+ getRemoteConnection: () => connB,
497
+ recordSend: (encoded) => this.network.recordSend(this.owner.peerId, encoded),
498
+ shouldDrop: (encoded) => this.network.shouldDrop(encoded),
499
+ recordDrop: (encoded) => this.network.recordDrop(this.owner.peerId, encoded),
500
+ recordStreamOpen: () => {
501
+ this.network.metrics.streamsOpened += 1;
502
+ },
503
+ streamHighWaterMarkBytes: this.network.streamHighWaterMarkBytes,
504
+ streamRxDelayMs: this.network.streamRxDelayMs,
505
+ });
506
+ connB = new InMemoryConnection({
507
+ id: connIdB,
508
+ remotePeer: this.owner.peerId,
509
+ remoteAddr: this.owner.addressManager.getAddresses()[0],
510
+ getLocalRegistrar: () => remote.registrar,
511
+ getRemoteRegistrar: () => this.owner.registrar,
512
+ getRemoteConnection: () => connA,
513
+ recordSend: (encoded) => this.network.recordSend(remote.peerId, encoded),
514
+ shouldDrop: (encoded) => this.network.shouldDrop(encoded),
515
+ recordDrop: (encoded) => this.network.recordDrop(remote.peerId, encoded),
516
+ recordStreamOpen: () => {
517
+ this.network.metrics.streamsOpened += 1;
518
+ },
519
+ streamHighWaterMarkBytes: this.network.streamHighWaterMarkBytes,
520
+ streamRxDelayMs: this.network.streamRxDelayMs,
521
+ });
522
+ const pair = new InMemoryConnectionPair({
523
+ network: this.network,
524
+ aOwner: this.owner,
525
+ bOwner: remote,
526
+ aManager: this,
527
+ bManager: remote.connectionManager,
528
+ aConn: connA,
529
+ bConn: connB,
530
+ });
531
+ connA._setPair(pair);
532
+ connB._setPair(pair);
533
+ {
534
+ const arr = this.connectionsByRemote.get(remoteKey) ?? [];
535
+ arr.push(connA);
536
+ this.connectionsByRemote.set(remoteKey, arr);
537
+ }
538
+ {
539
+ const arr = remote.connectionManager.connectionsByRemote.get(this.owner.peerId.toString()) ?? [];
540
+ arr.push(connB);
541
+ remote.connectionManager.connectionsByRemote.set(this.owner.peerId.toString(), arr);
542
+ }
543
+ this.network.notifyConnect(this.owner, remote.peerId, connA);
544
+ this.network.notifyConnect(remote, this.owner.peerId, connB);
545
+ this.network.metrics.dials += 1;
546
+ this.network.metrics.connectionsOpened += 1;
547
+ return connA;
548
+ }
549
+ }
550
+ export class InMemoryNetwork {
551
+ peersById = new Map();
552
+ peersByPort = new Map();
553
+ offlineUntilByPeerId = new Map();
554
+ metrics = {
555
+ dials: 0,
556
+ connectionsOpened: 0,
557
+ connectionsClosed: 0,
558
+ streamsOpened: 0,
559
+ framesSent: 0,
560
+ bytesSent: 0,
561
+ dataFramesSent: 0,
562
+ ackFramesSent: 0,
563
+ goodbyeFramesSent: 0,
564
+ otherFramesSent: 0,
565
+ framesDropped: 0,
566
+ bytesDropped: 0,
567
+ dataFramesDropped: 0,
568
+ };
569
+ peerMetricsByHash = new Map();
570
+ streamHighWaterMarkBytes;
571
+ streamRxDelayMs;
572
+ dialDelayMs;
573
+ dropDataFrameRate;
574
+ dropRng;
575
+ constructor(opts) {
576
+ this.streamHighWaterMarkBytes = opts?.streamHighWaterMarkBytes ?? 256 * 1024;
577
+ this.streamRxDelayMs = opts?.streamRxDelayMs ?? 0;
578
+ this.dialDelayMs = opts?.dialDelayMs ?? 0;
579
+ this.dropDataFrameRate = Math.max(0, Math.min(1, Number(opts?.dropDataFrameRate ?? 0)));
580
+ this.dropRng = mulberry32(Number(opts?.dropSeed ?? 1));
581
+ }
582
+ isPeerOffline(peer, now = Date.now()) {
583
+ const key = typeof peer === "string" ? peer : peer.toString();
584
+ const until = this.offlineUntilByPeerId.get(key);
585
+ if (until == null)
586
+ return false;
587
+ if (until <= now) {
588
+ this.offlineUntilByPeerId.delete(key);
589
+ return false;
590
+ }
591
+ return true;
592
+ }
593
+ setPeerOffline(peer, downMs, now = Date.now()) {
594
+ const key = typeof peer === "string" ? peer : peer.toString();
595
+ const ms = Math.max(0, Math.floor(downMs));
596
+ if (ms <= 0) {
597
+ this.offlineUntilByPeerId.delete(key);
598
+ return;
599
+ }
600
+ this.offlineUntilByPeerId.set(key, now + ms);
601
+ }
602
+ setPeerOnline(peer) {
603
+ const key = typeof peer === "string" ? peer : peer.toString();
604
+ this.offlineUntilByPeerId.delete(key);
605
+ }
606
+ async disconnectPeer(peer) {
607
+ const key = typeof peer === "string" ? peer : peer.toString();
608
+ const runtime = this.getPeerById(key);
609
+ if (!runtime)
610
+ return;
611
+ const conns = runtime.connectionManager.getConnections();
612
+ await Promise.all(conns.map((c) => c.close()));
613
+ }
614
+ registerPeer(peer, port) {
615
+ this.peersById.set(peer.peerId.toString(), peer);
616
+ this.peersByPort.set(port, peer);
617
+ }
618
+ getPeerById(id) {
619
+ return this.peersById.get(id);
620
+ }
621
+ getPeerByPort(port) {
622
+ return this.peersByPort.get(port);
623
+ }
624
+ shouldDrop(encodedFrame) {
625
+ if (this.dropDataFrameRate <= 0)
626
+ return false;
627
+ try {
628
+ const { bytes } = decodeUVarint(encodedFrame);
629
+ const base = bytes;
630
+ const variant = encodedFrame[base];
631
+ // Only drop stream "data" frames; ACK/HELLO/etc stay reliable.
632
+ if (variant !== 0)
633
+ return false;
634
+ // Drop only FanoutTree payload frames (identified by message id prefix) so
635
+ // join/repair control traffic remains reliable under loss.
636
+ if (!shouldDropByMessageIdPrefix(encodedFrame, base))
637
+ return false;
638
+ // If the message has a priority, only drop low-priority payload frames.
639
+ // This keeps control-plane traffic (join/repair) reliable so protocols can
640
+ // make progress under data loss.
641
+ //
642
+ // Layout (borsh, with length prefix stripped):
643
+ // [0]=DataMessage variant
644
+ // [1]=MessageHeader variant
645
+ // [2..33]=id (32 bytes)
646
+ // [34..41]=timestamp (u64)
647
+ // [42..49]=session (u64)
648
+ // [50..57]=expires (u64)
649
+ // [58]=priority option flag (u8)
650
+ // [59..62]=priority (u32 LE) if flag=1
651
+ const flagOffset = base + 58;
652
+ const hasPriority = encodedFrame[flagOffset];
653
+ if (hasPriority === 1) {
654
+ const prio = readU32LE(encodedFrame, flagOffset + 1);
655
+ if (prio > 1)
656
+ return false;
657
+ }
658
+ }
659
+ catch {
660
+ return false;
661
+ }
662
+ return this.dropRng() < this.dropDataFrameRate;
663
+ }
664
+ recordDrop(fromPeerId, encodedFrame) {
665
+ this.metrics.framesDropped += 1;
666
+ this.metrics.bytesDropped += encodedFrame.byteLength;
667
+ const fromHash = getPublicKeyFromPeerId(fromPeerId).hashcode();
668
+ let pm = this.peerMetricsByHash.get(fromHash);
669
+ if (!pm) {
670
+ pm = {
671
+ framesSent: 0,
672
+ bytesSent: 0,
673
+ dataFramesSent: 0,
674
+ ackFramesSent: 0,
675
+ goodbyeFramesSent: 0,
676
+ otherFramesSent: 0,
677
+ framesDropped: 0,
678
+ bytesDropped: 0,
679
+ dataFramesDropped: 0,
680
+ maxBytesPerSecond: 0,
681
+ _currentSecond: -1,
682
+ _bytesThisSecond: 0,
683
+ };
684
+ this.peerMetricsByHash.set(fromHash, pm);
685
+ }
686
+ pm.framesDropped += 1;
687
+ pm.bytesDropped += encodedFrame.byteLength;
688
+ this.metrics.dataFramesDropped += 1;
689
+ pm.dataFramesDropped += 1;
690
+ }
691
+ recordSend(fromPeerId, encodedFrame) {
692
+ this.metrics.framesSent += 1;
693
+ this.metrics.bytesSent += encodedFrame.byteLength;
694
+ const fromHash = getPublicKeyFromPeerId(fromPeerId).hashcode();
695
+ let pm = this.peerMetricsByHash.get(fromHash);
696
+ if (!pm) {
697
+ pm = {
698
+ framesSent: 0,
699
+ bytesSent: 0,
700
+ dataFramesSent: 0,
701
+ ackFramesSent: 0,
702
+ goodbyeFramesSent: 0,
703
+ otherFramesSent: 0,
704
+ framesDropped: 0,
705
+ bytesDropped: 0,
706
+ dataFramesDropped: 0,
707
+ maxBytesPerSecond: 0,
708
+ _currentSecond: -1,
709
+ _bytesThisSecond: 0,
710
+ };
711
+ this.peerMetricsByHash.set(fromHash, pm);
712
+ }
713
+ pm.framesSent += 1;
714
+ pm.bytesSent += encodedFrame.byteLength;
715
+ const sec = Math.floor(Date.now() / 1000);
716
+ if (pm._currentSecond !== sec) {
717
+ pm._currentSecond = sec;
718
+ pm._bytesThisSecond = 0;
719
+ }
720
+ pm._bytesThisSecond += encodedFrame.byteLength;
721
+ if (pm._bytesThisSecond > pm.maxBytesPerSecond) {
722
+ pm.maxBytesPerSecond = pm._bytesThisSecond;
723
+ }
724
+ try {
725
+ const { bytes } = decodeUVarint(encodedFrame);
726
+ const variant = encodedFrame[bytes];
727
+ if (variant === 0)
728
+ this.metrics.dataFramesSent += 1;
729
+ else if (variant === 1)
730
+ this.metrics.ackFramesSent += 1;
731
+ else if (variant === 3)
732
+ this.metrics.goodbyeFramesSent += 1;
733
+ else
734
+ this.metrics.otherFramesSent += 1;
735
+ if (variant === 0)
736
+ pm.dataFramesSent += 1;
737
+ else if (variant === 1)
738
+ pm.ackFramesSent += 1;
739
+ else if (variant === 3)
740
+ pm.goodbyeFramesSent += 1;
741
+ else
742
+ pm.otherFramesSent += 1;
743
+ }
744
+ catch {
745
+ this.metrics.otherFramesSent += 1;
746
+ pm.otherFramesSent += 1;
747
+ }
748
+ }
749
+ notifyConnect(owner, peerId, connection) {
750
+ const remote = this.getPeerById(peerId.toString());
751
+ for (const { protocol, topology } of owner.registrar.getTopologies()) {
752
+ if (remote && !remote.registrar.getHandler(protocol))
753
+ continue;
754
+ topology.onConnect(peerId, connection);
755
+ }
756
+ }
757
+ notifyDisconnect(owner, peerId, connections) {
758
+ const remote = this.getPeerById(peerId.toString());
759
+ for (const conn of connections) {
760
+ for (const { protocol, topology } of owner.registrar.getTopologies()) {
761
+ if (remote && !remote.registrar.getHandler(protocol))
762
+ continue;
763
+ topology.onDisconnect(peerId, conn);
764
+ }
765
+ }
766
+ }
767
+ static createPeer(opts) {
768
+ // Use deterministic-but-valid ed25519 keys (derived from index seed).
769
+ // This keeps sims reproducible while still allowing Peerbit's keychain
770
+ // to derive x25519 keys (libsodium rejects invalid points).
771
+ const seed = (() => {
772
+ const out = new Uint8Array(32);
773
+ let x = Math.imul((opts.index >>> 0) + 1, 0x9e3779b1) >>> 0;
774
+ for (let i = 0; i < out.length; i++) {
775
+ x ^= x >>> 16;
776
+ x = Math.imul(x, 0x7feb352d) >>> 0;
777
+ x ^= x >>> 15;
778
+ x = Math.imul(x, 0x846ca68b) >>> 0;
779
+ x ^= x >>> 16;
780
+ out[i] = x & 0xff;
781
+ }
782
+ return out;
783
+ })();
784
+ const generated = opts.mockKeyBytes
785
+ ? { publicKey: opts.mockKeyBytes.publicKey, privateKey: opts.mockKeyBytes.privateKey }
786
+ : sodium.crypto_sign_seed_keypair(seed);
787
+ const pub = generated.publicKey;
788
+ const priv = generated.privateKey;
789
+ const peerId = {
790
+ type: "Ed25519",
791
+ publicKey: { raw: pub },
792
+ toString: () => `sim-${opts.index}`,
793
+ equals: (other) => other?.toString?.() === `sim-${opts.index}`,
794
+ };
795
+ const privateKey = {
796
+ type: "Ed25519",
797
+ raw: priv,
798
+ publicKey: { raw: pub },
799
+ };
800
+ const addr = multiaddr(`/ip4/127.0.0.1/tcp/${opts.port}`);
801
+ const registrar = new InMemoryRegistrar();
802
+ const addressManager = new InMemoryAddressManager(addr);
803
+ const peerStore = new InMemoryPeerStore((peerId) => {
804
+ const network = opts.network;
805
+ const runtime = network?.getPeerById(peerId.toString());
806
+ return runtime?.addressManager.getAddresses() ?? [];
807
+ });
808
+ const events = new EventTarget();
809
+ // placeholder connectionManager; caller wires it after creation
810
+ const runtime = {
811
+ peerId,
812
+ privateKey,
813
+ registrar,
814
+ addressManager,
815
+ peerStore,
816
+ events,
817
+ connectionManager: undefined,
818
+ };
819
+ return { runtime: runtime, port: opts.port };
820
+ }
821
+ }
822
+ export const publicKeyHash = (peerId) => getPublicKeyFromPeerId(peerId).hashcode();
823
+ export class InMemoryLibp2p {
824
+ runtime;
825
+ status = "stopped";
826
+ peerId;
827
+ services;
828
+ components;
829
+ constructor(runtime, services) {
830
+ this.runtime = runtime;
831
+ this.peerId = runtime.peerId;
832
+ this.components = {
833
+ peerId: runtime.peerId,
834
+ privateKey: runtime.privateKey,
835
+ addressManager: runtime.addressManager,
836
+ registrar: runtime.registrar,
837
+ connectionManager: runtime.connectionManager,
838
+ peerStore: runtime.peerStore,
839
+ events: runtime.events,
840
+ };
841
+ const out = {};
842
+ if (services) {
843
+ for (const [name, factory] of Object.entries(services)) {
844
+ out[name] = factory(this.components);
845
+ }
846
+ }
847
+ this.services = out;
848
+ }
849
+ getMultiaddrs() {
850
+ return this.runtime.addressManager.getAddresses();
851
+ }
852
+ dial(addresses) {
853
+ return this.runtime.connectionManager.openConnection(addresses);
854
+ }
855
+ hangUp(peerId, options) {
856
+ return this.runtime.connectionManager.closeConnections(peerId, options);
857
+ }
858
+ getConnections(peerId) {
859
+ return this.runtime.connectionManager.getConnections(peerId);
860
+ }
861
+ async start() {
862
+ if (this.status === "started")
863
+ return;
864
+ this.status = "started";
865
+ await Promise.all(Object.values(this.services).map((svc) => svc?.start?.()));
866
+ }
867
+ async stop() {
868
+ if (this.status === "stopped")
869
+ return;
870
+ this.status = "stopped";
871
+ await Promise.all(Object.values(this.services).map((svc) => svc?.stop?.()));
872
+ const conns = this.runtime.connectionManager.getConnections();
873
+ await Promise.all(conns.map((c) => c.close()));
874
+ }
875
+ }
876
+ export class InMemorySession {
877
+ peers;
878
+ network;
879
+ constructor(opts) {
880
+ this.peers = opts.peers;
881
+ this.network = opts.network;
882
+ }
883
+ static async disconnected(n, opts) {
884
+ await sodium.ready;
885
+ const network = opts?.network ?? new InMemoryNetwork(opts?.networkOpts);
886
+ const basePort = opts?.basePort ?? 30_000;
887
+ const peers = [];
888
+ for (let i = 0; i < n; i++) {
889
+ const port = basePort + i;
890
+ const { runtime } = InMemoryNetwork.createPeer({ index: i, port, network });
891
+ runtime.connectionManager = new InMemoryConnectionManager(network, runtime);
892
+ network.registerPeer(runtime, port);
893
+ peers.push(new InMemoryLibp2p(runtime, opts?.services));
894
+ }
895
+ const session = new InMemorySession({ peers, network });
896
+ if (opts?.start !== false) {
897
+ await Promise.all(session.peers.map((p) => p.start()));
898
+ }
899
+ return session;
900
+ }
901
+ async connectFully(groups) {
902
+ const peers = groups ?? [this.peers];
903
+ const connectPromises = [];
904
+ for (const group of peers) {
905
+ for (let i = 0; i < group.length - 1; i++) {
906
+ for (let j = i + 1; j < group.length; j++) {
907
+ connectPromises.push(group[i].dial(group[j].getMultiaddrs()));
908
+ }
909
+ }
910
+ }
911
+ await Promise.all(connectPromises);
912
+ return this;
913
+ }
914
+ async stop() {
915
+ await Promise.all(this.peers.map((p) => p.stop()));
916
+ }
917
+ }
918
+ //# sourceMappingURL=inmemory-libp2p.js.map