@peerbit/stream 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,1556 @@
1
+ import { EventEmitter, CustomEvent } from "@libp2p/interfaces/events";
2
+ import { pipe } from "it-pipe";
3
+ import Queue from "p-queue";
4
+ import type { PeerId } from "@libp2p/interface-peer-id";
5
+ import type { Connection } from "@libp2p/interface-connection";
6
+ import type { Pushable } from "it-pushable";
7
+ import { pushable } from "it-pushable";
8
+ import type { Stream } from "@libp2p/interface-connection";
9
+ import { Uint8ArrayList } from "uint8arraylist";
10
+ import { abortableSource } from "abortable-iterator";
11
+ import * as lp from "it-length-prefixed";
12
+ import { Routes } from "./routes.js";
13
+ import { multiaddr } from "@multiformats/multiaddr";
14
+ import { PeerMap } from "./peer-map.js";
15
+ import type {
16
+ IncomingStreamData,
17
+ Registrar,
18
+ } from "@libp2p/interface-registrar";
19
+ import { ConnectionManager } from "@libp2p/interface-connection-manager";
20
+ import { PeerStore } from "@libp2p/interface-peer-store";
21
+
22
+ import { delay, TimeoutError, waitFor } from "@peerbit/time";
23
+ import {
24
+ Ed25519PublicKey,
25
+ getKeypairFromPeerId,
26
+ getPublicKeyFromPeerId,
27
+ PublicSignKey,
28
+ SignatureWithKey,
29
+ } from "@peerbit/crypto";
30
+
31
+ export type SignaturePolicy = "StictSign" | "StrictNoSign";
32
+ import type { AddressManager } from "@libp2p/interface-address-manager";
33
+
34
+ import { logger } from "./logger.js";
35
+ import { Cache } from "@peerbit/cache";
36
+ import { createTopology } from "./topology.js";
37
+ export { logger };
38
+ import type { Libp2pEvents } from "@libp2p/interface-libp2p";
39
+ import {
40
+ PeerStreamEvents,
41
+ Message as Message,
42
+ Goodbye,
43
+ Hello,
44
+ DataMessage,
45
+ Ping,
46
+ PingPong,
47
+ Pong,
48
+ getMsgId,
49
+ WaitForPeer,
50
+ } from "@peerbit/stream-interface";
51
+ export interface PeerStreamsInit {
52
+ peerId: PeerId;
53
+ publicKey: PublicSignKey;
54
+ protocol: string;
55
+ connId: string;
56
+ }
57
+
58
+ const isWebsocketConnection = (c: Connection) =>
59
+ c.remoteAddr.protoNames().find((x) => x === "ws" || x === "wss");
60
+
61
+ /**
62
+ * Thin wrapper around a peer's inbound / outbound pubsub streams
63
+ */
64
+ export class PeerStreams extends EventEmitter<PeerStreamEvents> {
65
+ public counter = 0;
66
+ public readonly peerId: PeerId;
67
+ public readonly publicKey: PublicSignKey;
68
+ public readonly protocol: string;
69
+ /**
70
+ * Write stream - it's preferable to use the write method
71
+ */
72
+ public outboundStream?: Pushable<Uint8ArrayList>;
73
+ /**
74
+ * Read stream
75
+ */
76
+ public inboundStream?: AsyncIterable<Uint8ArrayList>;
77
+ /**
78
+ * The raw outbound stream, as retrieved from conn.newStream
79
+ */
80
+ public _rawOutboundStream?: Stream;
81
+ /**
82
+ * The raw inbound stream, as retrieved from the callback from libp2p.handle
83
+ */
84
+ public _rawInboundStream?: Stream;
85
+ /**
86
+ * An AbortController for controlled shutdown of the treams
87
+ */
88
+ private readonly inboundAbortController: AbortController;
89
+
90
+ private closed: boolean;
91
+
92
+ public pingJob: { resolve: () => void; abort: () => void };
93
+ public pingLatency: number | undefined;
94
+
95
+ public connId: string;
96
+ constructor(init: PeerStreamsInit) {
97
+ super();
98
+
99
+ this.peerId = init.peerId;
100
+ this.publicKey = init.publicKey;
101
+ this.protocol = init.protocol;
102
+ this.inboundAbortController = new AbortController();
103
+ this.closed = false;
104
+ this.connId = init.connId;
105
+ this.counter = 1;
106
+ }
107
+
108
+ /**
109
+ * Do we have a connection to read from?
110
+ */
111
+ get isReadable() {
112
+ return Boolean(this.inboundStream);
113
+ }
114
+
115
+ /**
116
+ * Do we have a connection to write on?
117
+ */
118
+ get isWritable() {
119
+ return Boolean(this.outboundStream);
120
+ }
121
+
122
+ /**
123
+ * Send a message to this peer.
124
+ * Throws if there is no `stream` to write to available.
125
+ */
126
+ write(data: Uint8Array | Uint8ArrayList) {
127
+ if (this.outboundStream == null) {
128
+ logger.error("No writable connection to " + this.peerId.toString());
129
+ throw new Error("No writable connection to " + this.peerId.toString());
130
+ }
131
+ this.outboundStream.push(
132
+ data instanceof Uint8Array ? new Uint8ArrayList(data) : data
133
+ );
134
+ }
135
+
136
+ async waitForWrite(bytes: Uint8Array | Uint8ArrayList) {
137
+ if (!this.isWritable) {
138
+ // Catch the event where the outbound stream is attach, but also abort if we shut down
139
+ const outboundPromise = new Promise<void>((rs, rj) => {
140
+ const resolve = () => {
141
+ this.removeEventListener("stream:outbound", listener);
142
+ clearTimeout(timer);
143
+ rs();
144
+ };
145
+ const reject = (err: Error) => {
146
+ this.removeEventListener("stream:outbound", listener);
147
+ clearTimeout(timer);
148
+ rj(err);
149
+ };
150
+ const timer = setTimeout(() => {
151
+ reject(new Error("Timed out"));
152
+ }, 3 * 1000); // TODO if this timeout > 10s we run into issues in the tests when running in CI
153
+ const abortHandler = () => {
154
+ this.removeEventListener("close", abortHandler);
155
+ reject(new Error("Aborted"));
156
+ };
157
+ this.addEventListener("close", abortHandler);
158
+
159
+ const listener = () => {
160
+ resolve();
161
+ };
162
+ this.addEventListener("stream:outbound", listener);
163
+ if (this.isWritable) {
164
+ resolve();
165
+ }
166
+ });
167
+
168
+ await outboundPromise
169
+ .then(() => {
170
+ this.write(bytes);
171
+ })
172
+ .catch((error) => {
173
+ logger.error(
174
+ "Failed to send to stream: " +
175
+ this.peerId +
176
+ ". " +
177
+ (error?.message || error?.toString())
178
+ );
179
+ });
180
+ } else {
181
+ this.write(bytes);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Attach a raw inbound stream and setup a read stream
187
+ */
188
+ attachInboundStream(stream: Stream) {
189
+ // Create and attach a new inbound stream
190
+ // The inbound stream is:
191
+ // - abortable, set to only return on abort, rather than throw
192
+ // - transformed with length-prefix transform
193
+ this._rawInboundStream = stream;
194
+ this.inboundStream = abortableSource(
195
+ pipe(this._rawInboundStream, (source) =>
196
+ lp.decode(source, { maxDataLength: 100001000 })
197
+ ),
198
+ this.inboundAbortController.signal,
199
+ {
200
+ returnOnAbort: true,
201
+ onReturnError: (err) => {
202
+ logger.error("Inbound stream error", err?.message);
203
+ },
204
+ }
205
+ );
206
+
207
+ this.dispatchEvent(new CustomEvent("stream:inbound"));
208
+ return this.inboundStream;
209
+ }
210
+
211
+ /**
212
+ * Attach a raw outbound stream and setup a write stream
213
+ */
214
+
215
+ async attachOutboundStream(stream: Stream) {
216
+ // If an outbound stream already exists, gently close it
217
+ const _prevStream = this.outboundStream;
218
+ this._rawOutboundStream = stream;
219
+ this.pingJob?.abort();
220
+
221
+ this.outboundStream = pushable<Uint8ArrayList>({
222
+ objectMode: true,
223
+ onEnd: () => {
224
+ stream.close();
225
+ if (this._rawOutboundStream === stream) {
226
+ this.dispatchEvent(new CustomEvent("close"));
227
+ this._rawOutboundStream = undefined;
228
+ this.outboundStream = undefined;
229
+ }
230
+ },
231
+ });
232
+
233
+ pipe(
234
+ this.outboundStream,
235
+ (source) => lp.encode(source),
236
+ this._rawOutboundStream
237
+ ).catch((err: Error) => {
238
+ logger.error(err);
239
+ });
240
+
241
+ // Emit if the connection is new
242
+ this.dispatchEvent(new CustomEvent("stream:outbound"));
243
+
244
+ if (_prevStream != null) {
245
+ // End the stream without emitting a close event
246
+ await _prevStream.end();
247
+ }
248
+ return this.outboundStream;
249
+ }
250
+
251
+ /**
252
+ * Closes the open connection to peer
253
+ */
254
+ close() {
255
+ if (this.closed) {
256
+ return;
257
+ }
258
+
259
+ this.closed = true;
260
+
261
+ // End the outbound stream
262
+ if (this.outboundStream != null) {
263
+ this.outboundStream.return();
264
+ this._rawOutboundStream?.close();
265
+ }
266
+ // End the inbound stream
267
+ if (this.inboundStream != null) {
268
+ this.inboundAbortController.abort();
269
+ this._rawInboundStream?.close();
270
+ }
271
+
272
+ this.pingJob?.abort();
273
+ this.pingLatency = undefined;
274
+
275
+ //this.dispatchEvent(new CustomEvent('close'))
276
+ this._rawOutboundStream = undefined;
277
+ this.outboundStream = undefined;
278
+ this._rawInboundStream = undefined;
279
+ this.inboundStream = undefined;
280
+ }
281
+ }
282
+
283
+ export interface PeerEvents {
284
+ "peer:reachable": CustomEvent<PublicSignKey>;
285
+ "peer:unreachable": CustomEvent<PublicSignKey>;
286
+ }
287
+
288
+ export interface MessageEvents {
289
+ message: CustomEvent<Message>;
290
+ }
291
+
292
+ export interface StreamEvents extends PeerEvents, MessageEvents {
293
+ data: CustomEvent<DataMessage>;
294
+ }
295
+
296
+ export type ConnectionManagerOptions = {
297
+ autoDial?: boolean;
298
+ retryDelay?: number;
299
+ };
300
+ export type DirectStreamOptions = {
301
+ canRelayMessage?: boolean;
302
+ emitSelf?: boolean;
303
+ messageProcessingConcurrency?: number;
304
+ maxInboundStreams?: number;
305
+ maxOutboundStreams?: number;
306
+ signaturePolicy?: SignaturePolicy;
307
+ pingInterval?: number;
308
+ connectionManager?: ConnectionManagerOptions;
309
+ };
310
+
311
+ export interface DirectStreamComponents {
312
+ peerId: PeerId;
313
+ addressManager: AddressManager;
314
+ registrar: Registrar;
315
+ connectionManager: ConnectionManager;
316
+ peerStore: PeerStore;
317
+ events: EventEmitter<Libp2pEvents>;
318
+ }
319
+
320
+ export abstract class DirectStream<
321
+ Events extends { [s: string]: any } = StreamEvents
322
+ >
323
+ extends EventEmitter<Events>
324
+ implements WaitForPeer
325
+ {
326
+ public peerId: PeerId;
327
+ public peerIdStr: string;
328
+ public publicKey: PublicSignKey;
329
+ public publicKeyHash: string;
330
+ public sign: (bytes: Uint8Array) => Promise<SignatureWithKey>;
331
+
332
+ public started: boolean;
333
+
334
+ /**
335
+ * Map of peer streams
336
+ */
337
+ public peers: PeerMap<PeerStreams>;
338
+ public peerKeyHashToPublicKey: Map<string, PublicSignKey>;
339
+ public peerIdToPublicKey: Map<string, PublicSignKey>;
340
+ public routes: Routes;
341
+ /**
342
+ * If router can relay received messages, even if not subscribed
343
+ */
344
+ public canRelayMessage: boolean;
345
+ /**
346
+ * if publish should emit to self, if subscribed
347
+ */
348
+
349
+ public signaturePolicy: SignaturePolicy;
350
+ public emitSelf: boolean;
351
+ public queue: Queue;
352
+ public multicodecs: string[];
353
+ public seenCache: Cache;
354
+ public earlyGoodbyes: Map<string, Goodbye>;
355
+ public helloMap: Map<string, Map<string, Hello>>; // key is hash of publicKey, value is map whey key is hash of signature bytes, and value is latest Hello
356
+ public multiaddrsMap: Map<string, string[]>;
357
+ private _registrarTopologyIds: string[] | undefined;
358
+ private readonly maxInboundStreams?: number;
359
+ private readonly maxOutboundStreams?: number;
360
+ private topology: any;
361
+ private pingJobPromise: any;
362
+ private pingJob: any;
363
+ private pingInterval: number;
364
+ private connectionManagerOptions: ConnectionManagerOptions;
365
+ private recentDials: Cache<string>;
366
+
367
+ constructor(
368
+ readonly components: DirectStreamComponents,
369
+ multicodecs: string[],
370
+ options?: DirectStreamOptions
371
+ ) {
372
+ super();
373
+ const {
374
+ canRelayMessage = false,
375
+ emitSelf = false,
376
+ messageProcessingConcurrency = 10,
377
+ pingInterval = 10 * 1000,
378
+ maxInboundStreams,
379
+ maxOutboundStreams,
380
+ signaturePolicy = "StictSign",
381
+ connectionManager = { autoDial: true },
382
+ } = options || {};
383
+
384
+ const signKey = getKeypairFromPeerId(components.peerId);
385
+ this.sign = signKey.sign.bind(signKey);
386
+ this.peerId = components.peerId;
387
+ this.peerIdStr = components.peerId.toString();
388
+ this.publicKey = signKey.publicKey;
389
+ this.publicKeyHash = signKey.publicKey.hashcode();
390
+ this.multicodecs = multicodecs;
391
+ this.started = false;
392
+ this.peers = new Map<string, PeerStreams>();
393
+ this.helloMap = new Map();
394
+ this.multiaddrsMap = new Map();
395
+ this.routes = new Routes(this.publicKeyHash);
396
+ this.canRelayMessage = canRelayMessage;
397
+ this.emitSelf = emitSelf;
398
+ this.queue = new Queue({ concurrency: messageProcessingConcurrency });
399
+ this.earlyGoodbyes = new Map();
400
+ this.maxInboundStreams = maxInboundStreams;
401
+ this.maxOutboundStreams = maxOutboundStreams;
402
+ this.seenCache = new Cache({ max: 1e3, ttl: 10 * 60 * 1e3 });
403
+ this.peerKeyHashToPublicKey = new Map();
404
+ this.peerIdToPublicKey = new Map();
405
+ this.pingInterval = pingInterval;
406
+ this._onIncomingStream = this._onIncomingStream.bind(this);
407
+ this.onPeerConnected = this.onPeerConnected.bind(this);
408
+ this.onPeerDisconnected = this.onPeerDisconnected.bind(this);
409
+ this.signaturePolicy = signaturePolicy;
410
+ this.connectionManagerOptions = connectionManager;
411
+ this.recentDials = new Cache({
412
+ ttl: connectionManager.retryDelay || 60 * 1000,
413
+ max: 1e3,
414
+ });
415
+ }
416
+
417
+ async start() {
418
+ if (this.started) {
419
+ return;
420
+ }
421
+
422
+ logger.debug("starting");
423
+
424
+ this.topology = createTopology({
425
+ onConnect: this.onPeerConnected.bind(this),
426
+ onDisconnect: this.onPeerDisconnected.bind(this),
427
+ });
428
+
429
+ // register protocol with topology
430
+ // Topology callbacks called on connection manager changes
431
+ this._registrarTopologyIds = await Promise.all(
432
+ this.multicodecs.map((multicodec) =>
433
+ this.components.registrar.register(multicodec, this.topology)
434
+ )
435
+ );
436
+
437
+ // Incoming streams
438
+ // Called after a peer dials us
439
+ await Promise.all(
440
+ this.multicodecs.map((multicodec) =>
441
+ this.components.registrar.handle(multicodec, this._onIncomingStream, {
442
+ maxInboundStreams: this.maxInboundStreams,
443
+ maxOutboundStreams: this.maxOutboundStreams,
444
+ })
445
+ )
446
+ );
447
+
448
+ this.started = true;
449
+
450
+ // All existing connections are like new ones for us. To deduplication on remotes so we only resuse one connection for this protocol (we could be connected with many connections)
451
+ const peerToConnections: Map<string, Connection[]> = new Map();
452
+ const connections = this.components.connectionManager.getConnections();
453
+ for (const conn of connections) {
454
+ let arr = peerToConnections.get(conn.remotePeer.toString());
455
+ if (!arr) {
456
+ arr = [];
457
+ peerToConnections.set(conn.remotePeer.toString(), arr);
458
+ }
459
+ arr.push(conn);
460
+ }
461
+ for (const [_peer, arr] of peerToConnections) {
462
+ let conn = arr[0]; // TODO choose TCP when both websocket and tcp exist
463
+ for (const c of arr) {
464
+ if (!isWebsocketConnection(c)) {
465
+ // TODO what is correct connection prioritization?
466
+ conn = c; // always favor non websocket address
467
+ break;
468
+ }
469
+ }
470
+
471
+ await this.onPeerConnected(conn.remotePeer, conn, true);
472
+ }
473
+
474
+ const pingJob = async () => {
475
+ // TODO don't use setInterval but waitFor previous done to be done
476
+ await this.pingJobPromise;
477
+ const promises: Promise<any>[] = [];
478
+ this.peers.forEach((peer) => {
479
+ promises.push(
480
+ this.ping(peer).catch((e) => {
481
+ if (e instanceof TimeoutError) {
482
+ // Ignore
483
+ } else {
484
+ logger.error(e);
485
+ }
486
+ })
487
+ );
488
+ });
489
+
490
+ promises.push(this.hello()); // Repetedly say hello to everyone to create traces in the network to measure latencies
491
+ this.pingJobPromise = Promise.all(promises)
492
+ .catch((e) => {
493
+ logger.error(e?.message);
494
+ })
495
+ .finally(() => {
496
+ if (!this.started) {
497
+ return;
498
+ }
499
+ this.pingJob = setTimeout(pingJob, this.pingInterval);
500
+ });
501
+ };
502
+ pingJob();
503
+ }
504
+
505
+ /**
506
+ * Unregister the pubsub protocol and the streams with other peers will be closed.
507
+ */
508
+ async stop() {
509
+ if (!this.started) {
510
+ return;
511
+ }
512
+ this.started = false;
513
+
514
+ clearTimeout(this.pingJob);
515
+ await this.pingJobPromise;
516
+
517
+ await Promise.all(
518
+ this.multicodecs.map((x) => this.components.registrar.unhandle(x))
519
+ );
520
+
521
+ logger.debug("stopping");
522
+ for (const peerStreams of this.peers.values()) {
523
+ peerStreams.close();
524
+ }
525
+
526
+ // unregister protocol and handlers
527
+ if (this._registrarTopologyIds != null) {
528
+ this._registrarTopologyIds?.map((id) =>
529
+ this.components.registrar.unregister(id)
530
+ );
531
+ }
532
+
533
+ this.queue.clear();
534
+ this.helloMap.clear();
535
+ this.multiaddrsMap.clear();
536
+ this.earlyGoodbyes.clear();
537
+ this.peers.clear();
538
+ this.seenCache.clear();
539
+ this.routes.clear();
540
+ this.peerKeyHashToPublicKey.clear();
541
+ this.peerIdToPublicKey.clear();
542
+ logger.debug("stopped");
543
+ }
544
+
545
+ isStarted() {
546
+ return this.started;
547
+ }
548
+
549
+ /**
550
+ * On an inbound stream opened
551
+ */
552
+
553
+ protected async _onIncomingStream(data: IncomingStreamData) {
554
+ if (!this.isStarted()) {
555
+ return;
556
+ }
557
+
558
+ const { stream, connection } = data;
559
+ const peerId = connection.remotePeer;
560
+ if (stream.stat.protocol == null) {
561
+ stream.abort(new Error("Stream was not multiplexed"));
562
+ return;
563
+ }
564
+
565
+ const publicKey = getPublicKeyFromPeerId(peerId);
566
+ const peer = this.addPeer(
567
+ peerId,
568
+ publicKey,
569
+ stream.stat.protocol,
570
+ connection.id
571
+ );
572
+ const inboundStream = peer.attachInboundStream(stream);
573
+ this.processMessages(peerId, inboundStream, peer).catch((err) => {
574
+ logger.error(err);
575
+ });
576
+ }
577
+
578
+ /**
579
+ * Registrar notifies an established connection with protocol
580
+ */
581
+ public async onPeerConnected(
582
+ peerId: PeerId,
583
+ conn: Connection,
584
+ fromExisting?: boolean
585
+ ) {
586
+ if (!this.isStarted() || conn.stat.status !== "OPEN") {
587
+ return;
588
+ }
589
+ try {
590
+ const peerKey = getPublicKeyFromPeerId(peerId);
591
+ const peerKeyHash = peerKey.hashcode();
592
+
593
+ for (const existingStreams of conn.streams) {
594
+ if (
595
+ existingStreams.stat.protocol &&
596
+ this.multicodecs.includes(existingStreams.stat.protocol) &&
597
+ existingStreams.stat.direction === "outbound"
598
+ ) {
599
+ return;
600
+ }
601
+ }
602
+
603
+ // This condition seem to work better than the one above, for some reason.
604
+ // The reason we need this at all is because we will connect to existing connection and recieve connection that
605
+ // some times, yields a race connections where connection drop each other by reset
606
+
607
+ let stream: Stream = undefined as any; // TODO types
608
+ let tries = 0;
609
+ let peer: PeerStreams = undefined as any;
610
+ while (tries <= 3) {
611
+ tries++;
612
+ if (!this.started) {
613
+ return;
614
+ }
615
+
616
+ try {
617
+ stream = await conn.newStream(this.multicodecs);
618
+ if (stream.stat.protocol == null) {
619
+ stream.abort(new Error("Stream was not multiplexed"));
620
+ return;
621
+ }
622
+ peer = this.addPeer(peerId, peerKey, stream.stat.protocol!, conn.id); // TODO types
623
+ await peer.attachOutboundStream(stream);
624
+
625
+ // TODO do we want to do this?
626
+ /* if (!peer.inboundStream) {
627
+ const inboundStream = conn.streams.find(
628
+ (x) =>
629
+ x.stat.protocol &&
630
+ this.multicodecs.includes(x.stat.protocol) &&
631
+ x.stat.direction === "inbound"
632
+ );
633
+ if (inboundStream) {
634
+ this._onIncomingStream({
635
+ connection: conn,
636
+ stream: inboundStream,
637
+ });
638
+ }
639
+ } */
640
+ } catch (error: any) {
641
+ if (error.code === "ERR_UNSUPPORTED_PROTOCOL") {
642
+ await delay(100);
643
+ continue; // Retry
644
+ }
645
+
646
+ if (
647
+ conn.stat.status !== "OPEN" ||
648
+ error?.message === "Muxer already closed" ||
649
+ error.code === "ERR_STREAM_RESET"
650
+ ) {
651
+ return; // fail silenty
652
+ }
653
+
654
+ throw error;
655
+ }
656
+ break;
657
+ }
658
+ if (!stream) {
659
+ return;
660
+ }
661
+
662
+ if (fromExisting) {
663
+ return; // we return here because we will enter this method once more once the protocol has been registered for the remote peer
664
+ }
665
+
666
+ // Add connection with assumed large latency
667
+ this.peerIdToPublicKey.set(peerId.toString(), peerKey);
668
+ const promises: Promise<any>[] = [];
669
+
670
+ /* if (!existingStream) */ {
671
+ this.addRouteConnection(
672
+ this.publicKey,
673
+ peerKey,
674
+ Number.MAX_SAFE_INTEGER
675
+ );
676
+
677
+ // Get accurate latency
678
+ promises.push(this.ping(peer));
679
+
680
+ // Say hello
681
+ promises.push(
682
+ this.publishMessage(
683
+ this.components.peerId,
684
+ await new Hello({
685
+ multiaddrs: this.components.addressManager
686
+ .getAddresses()
687
+ .map((x) => x.toString()),
688
+ }).sign(this.sign),
689
+ [peer]
690
+ )
691
+ );
692
+ // Send my goodbye early if I disconnect for some reason, (so my peer can say goodbye for me)
693
+ // TODO add custom condition fn for doing below
694
+ promises.push(
695
+ this.publishMessage(
696
+ this.components.peerId,
697
+ await new Goodbye({ early: true }).sign(this.sign),
698
+ [peer]
699
+ )
700
+ );
701
+
702
+ // replay all hellos
703
+ for (const [sender, hellos] of this.helloMap) {
704
+ if (sender === peerKeyHash) {
705
+ // Don't say hellos from sender to same sender (uneccessary)
706
+ continue;
707
+ }
708
+ for (const [key, hello] of hellos) {
709
+ if (!hello.header.verify()) {
710
+ hellos.delete(key);
711
+ }
712
+
713
+ promises.push(
714
+ this.publishMessage(this.components.peerId, hello, [peer])
715
+ );
716
+ }
717
+ }
718
+ }
719
+ const resolved = await Promise.all(promises);
720
+ return resolved;
721
+ } catch (err: any) {
722
+ logger.error(err);
723
+ }
724
+ }
725
+
726
+ private addRouteConnection(
727
+ from: PublicSignKey,
728
+ to: PublicSignKey,
729
+ latency: number
730
+ ) {
731
+ this.peerKeyHashToPublicKey.set(from.hashcode(), from);
732
+ this.peerKeyHashToPublicKey.set(to.hashcode(), to);
733
+ const links = this.routes.addLink(from.hashcode(), to.hashcode(), latency);
734
+ for (const added of links) {
735
+ const key = this.peerKeyHashToPublicKey.get(added);
736
+ if (key?.equals(this.publicKey) === false) {
737
+ this.onPeerReachable(key!);
738
+ }
739
+ }
740
+ }
741
+
742
+ removeRouteConnection(from: PublicSignKey, to: PublicSignKey) {
743
+ const has = this.routes.hasNode(to.hashcode());
744
+ if (!has) {
745
+ this.onPeerUnreachable(to);
746
+ } else {
747
+ const links = this.routes.deleteLink(from.hashcode(), to.hashcode());
748
+ for (const deleted of links) {
749
+ const key = this.peerKeyHashToPublicKey.get(deleted)!;
750
+ this.peerKeyHashToPublicKey.delete(deleted);
751
+ if (key?.equals(this.publicKey) === false) {
752
+ this.onPeerUnreachable(key!);
753
+ }
754
+ }
755
+ }
756
+ }
757
+
758
+ /**
759
+ * Registrar notifies a closing connection with pubsub protocol
760
+ */
761
+ protected async onPeerDisconnected(peerId: PeerId, conn?: Connection) {
762
+ // PeerId could be me, if so, it means that I am disconnecting
763
+ const peerKey = getPublicKeyFromPeerId(peerId);
764
+ const peerKeyHash = peerKey.hashcode();
765
+ const connections = this.components.connectionManager
766
+ .getConnectionsMap()
767
+ .get(peerId);
768
+ if (
769
+ conn?.id &&
770
+ connections &&
771
+ connections.length > 0 &&
772
+ !this.components.connectionManager
773
+ .getConnectionsMap()
774
+ .get(peerId)
775
+ ?.find(
776
+ (x) => x.id === conn.id
777
+ ) /* TODO this should work but does not? peer?.connId !== conn.id */
778
+ ) {
779
+ return;
780
+ }
781
+
782
+ if (!this.publicKey.equals(peerKey)) {
783
+ this._removePeer(peerKey);
784
+ this.removeRouteConnection(this.publicKey, peerKey);
785
+
786
+ // Notify network
787
+ const earlyGoodBye = this.earlyGoodbyes.get(peerKeyHash);
788
+ if (earlyGoodBye) {
789
+ earlyGoodBye.early = false;
790
+ await earlyGoodBye.sign(this.sign);
791
+ await this.publishMessage(this.components.peerId, earlyGoodBye);
792
+ this.earlyGoodbyes.delete(peerKeyHash);
793
+ }
794
+ this.peerIdToPublicKey.delete(peerId.toString());
795
+ }
796
+
797
+ logger.debug("connection ended:" + peerKey.toString());
798
+ }
799
+
800
+ /**
801
+ * invoked when a new peer becomes reachable
802
+ * @param publicKeyHash
803
+ */
804
+ public onPeerReachable(publicKey: PublicSignKey) {
805
+ // override this fn
806
+ this.dispatchEvent(
807
+ new CustomEvent("peer:reachable", { detail: publicKey })
808
+ );
809
+ }
810
+
811
+ /**
812
+ * invoked when a new peer becomes unreachable
813
+ * @param publicKeyHash
814
+ */
815
+ public onPeerUnreachable(publicKey: PublicSignKey) {
816
+ // override this fn
817
+ this.helloMap.delete(publicKey.hashcode());
818
+ this.multiaddrsMap.delete(publicKey.hashcode());
819
+
820
+ this.dispatchEvent(
821
+ new CustomEvent("peer:unreachable", { detail: publicKey })
822
+ );
823
+ }
824
+
825
+ /**
826
+ * Notifies the router that a peer has been connected
827
+ */
828
+ addPeer(
829
+ peerId: PeerId,
830
+ publicKey: PublicSignKey,
831
+ protocol: string,
832
+ connId: string
833
+ ): PeerStreams {
834
+ const publicKeyHash = publicKey.hashcode();
835
+ const existing = this.peers.get(publicKeyHash);
836
+
837
+ // If peer streams already exists, do nothing
838
+ if (existing != null) {
839
+ existing.counter += 1;
840
+ existing.connId = connId;
841
+ return existing;
842
+ }
843
+
844
+ // else create a new peer streams
845
+ const peerIdStr = peerId.toString();
846
+ logger.debug("new peer" + peerIdStr);
847
+
848
+ const peerStreams: PeerStreams = new PeerStreams({
849
+ peerId,
850
+ publicKey,
851
+ protocol,
852
+ connId,
853
+ });
854
+
855
+ this.peers.set(publicKeyHash, peerStreams);
856
+ peerStreams.addEventListener("close", () => this._removePeer(publicKey), {
857
+ once: true,
858
+ });
859
+ return peerStreams;
860
+ }
861
+
862
+ /**
863
+ * Notifies the router that a peer has been disconnected
864
+ */
865
+ protected _removePeer(publicKey: PublicSignKey) {
866
+ const hash = publicKey.hashcode();
867
+ const peerStreams = this.peers.get(hash);
868
+
869
+ if (peerStreams == null) {
870
+ return;
871
+ }
872
+
873
+ // close peer streams
874
+ peerStreams.close();
875
+
876
+ // delete peer streams
877
+ logger.debug("delete peer" + publicKey.toString());
878
+ this.peers.delete(hash);
879
+ return peerStreams;
880
+ }
881
+
882
+ // MESSAGE METHODS
883
+
884
+ /**
885
+ * Responsible for processing each RPC message received by other peers.
886
+ */
887
+ async processMessages(
888
+ peerId: PeerId,
889
+ stream: AsyncIterable<Uint8ArrayList>,
890
+ peerStreams: PeerStreams
891
+ ) {
892
+ try {
893
+ await pipe(stream, async (source) => {
894
+ for await (const data of source) {
895
+ const msgId = await getMsgId(data);
896
+ if (this.seenCache.has(msgId)) {
897
+ // we got message that WE sent?
898
+
899
+ /**
900
+ * Most propobable reason why we arrive here is a race condition/issue
901
+
902
+ ┌─┐
903
+ │0│
904
+ └△┘
905
+ ┌▽┐
906
+ │1│
907
+ └△┘
908
+ ┌▽┐
909
+ │2│
910
+ └─┘
911
+
912
+ from 2s perspective,
913
+
914
+ if everyone conents to each other at the same time, then 0 will say hello to 1 and 1 will save that hello to resend to 2 if 2 ever connects
915
+ but two is already connected by onPeerConnected has not been invoked yet, so the hello message gets forwarded,
916
+ and later onPeerConnected gets invoked on 1, and the same message gets resent to 2
917
+ */
918
+
919
+ continue;
920
+ }
921
+
922
+ this.seenCache.add(msgId);
923
+ this.processRpc(peerId, peerStreams, data).catch((err) =>
924
+ logger.warn(err)
925
+ );
926
+ }
927
+ });
928
+ } catch (err: any) {
929
+ logger.error(
930
+ "error on processing messages to id: " +
931
+ peerStreams.peerId.toString() +
932
+ ". " +
933
+ err?.message
934
+ );
935
+ this.onPeerDisconnected(peerStreams.peerId);
936
+ }
937
+ }
938
+
939
+ /**
940
+ * Handles an rpc request from a peer
941
+ */
942
+ async processRpc(
943
+ from: PeerId,
944
+ peerStreams: PeerStreams,
945
+ message: Uint8ArrayList
946
+ ): Promise<boolean> {
947
+ if (!this.acceptFrom(from)) {
948
+ logger.debug("received message from unacceptable peer %p", from);
949
+ return false;
950
+ }
951
+
952
+ // logger.debug("rpc from " + from + ", " + this.peerIdStr);
953
+
954
+ if (message.length > 0) {
955
+ // logger.debug("messages from " + from);
956
+ await this.queue
957
+ .add(async () => {
958
+ try {
959
+ await this.processMessage(from, peerStreams, message);
960
+ } catch (err: any) {
961
+ logger.error(err);
962
+ }
963
+ })
964
+ .catch((err) => logger.warn(err));
965
+ }
966
+
967
+ return true;
968
+ }
969
+
970
+ /**
971
+ * Handles a message from a peer
972
+ */
973
+ async processMessage(
974
+ from: PeerId,
975
+ peerStream: PeerStreams,
976
+ msg: Uint8ArrayList
977
+ ) {
978
+ if (!from.publicKey) {
979
+ return;
980
+ }
981
+
982
+ if (this.components.peerId.equals(from) && !this.emitSelf) {
983
+ return;
984
+ }
985
+
986
+ // Ensure the message is valid before processing it
987
+ const message: Message | undefined = Message.deserialize(msg);
988
+
989
+ this.dispatchEvent(
990
+ new CustomEvent("message", {
991
+ detail: message,
992
+ })
993
+ );
994
+
995
+ if (message instanceof DataMessage) {
996
+ await this.onDataMessage(from, peerStream, message);
997
+ } else if (message instanceof Hello) {
998
+ await this.onHello(from, peerStream, message);
999
+ } else if (message instanceof Goodbye) {
1000
+ await this.onGoodbye(from, peerStream, message);
1001
+ } else if (message instanceof PingPong) {
1002
+ await this.onPing(from, peerStream, message);
1003
+ } else {
1004
+ throw new Error("Unsupported");
1005
+ }
1006
+ }
1007
+
1008
+ async onDataMessage(
1009
+ from: PeerId,
1010
+ peerStream: PeerStreams,
1011
+ message: DataMessage
1012
+ ) {
1013
+ const isFromSelf = this.components.peerId.equals(from);
1014
+ if (!isFromSelf || this.emitSelf) {
1015
+ const isForAll = message.to.length === 0;
1016
+ const isForMe =
1017
+ !isForAll && message.to.find((x) => x === this.publicKeyHash);
1018
+ if (isForAll || isForMe) {
1019
+ if (
1020
+ this.signaturePolicy === "StictSign" &&
1021
+ !(await message.verify(this.signaturePolicy === "StictSign"))
1022
+ ) {
1023
+ // we don't verify messages we don't dispatch because of the performance penalty // TODO add opts for this
1024
+ logger.warn("Recieved message with invalid signature or timestamp");
1025
+ return false;
1026
+ }
1027
+
1028
+ this.dispatchEvent(
1029
+ new CustomEvent("data", {
1030
+ detail: message,
1031
+ })
1032
+ );
1033
+ }
1034
+ if (isForMe && message.to.length === 1) {
1035
+ // dont forward this message anymore because it was meant ONLY for me
1036
+ return true;
1037
+ }
1038
+ }
1039
+
1040
+ // Forward
1041
+ await this.relayMessage(from, message);
1042
+ return true;
1043
+ }
1044
+
1045
+ async onHello(from: PeerId, peerStream: PeerStreams, message: Hello) {
1046
+ if (!(await message.verify(false))) {
1047
+ const a = message.header.verify();
1048
+ const b =
1049
+ message.networkInfo.pingLatencies.length ===
1050
+ message.signatures.signatures.length - 1;
1051
+ logger.warn(
1052
+ `Recieved hello message that did not verify. Header: ${a}, Ping info ${b}, Signatures ${
1053
+ a && b
1054
+ }`
1055
+ );
1056
+ return false;
1057
+ }
1058
+
1059
+ const sender = message.sender?.hashcode();
1060
+ if (!sender) {
1061
+ logger.warn("Recieved hello without sender");
1062
+ return false;
1063
+ }
1064
+
1065
+ const signatures = message.signatures;
1066
+ for (let i = 0; i < signatures.signatures.length - 1; i++) {
1067
+ this.addRouteConnection(
1068
+ signatures.signatures[i].publicKey,
1069
+ signatures.signatures[i + 1].publicKey,
1070
+ message.networkInfo.pingLatencies[i]
1071
+ );
1072
+ }
1073
+
1074
+ message.networkInfo.pingLatencies.push(
1075
+ peerStream.pingLatency || 4294967295
1076
+ ); // TODO don't propagate if latency is high?
1077
+
1078
+ await message.sign(this.sign); // sign it so othere peers can now I have seen it (and can build a network graph from trace info)
1079
+
1080
+ let hellos = this.helloMap.get(sender);
1081
+ if (!hellos) {
1082
+ hellos = new Map();
1083
+ this.helloMap.set(sender, hellos);
1084
+ }
1085
+
1086
+ this.multiaddrsMap.set(sender, message.multiaddrs);
1087
+
1088
+ const helloSignaturHash = await message.signatures.hashPublicKeys();
1089
+ const existingHello = hellos.get(helloSignaturHash);
1090
+ if (existingHello) {
1091
+ if (existingHello.header.expires < message.header.expires) {
1092
+ hellos.set(helloSignaturHash, message);
1093
+ }
1094
+ } else {
1095
+ hellos.set(helloSignaturHash, message);
1096
+ }
1097
+
1098
+ // Forward
1099
+ await this.relayMessage(from, message);
1100
+ return true;
1101
+ }
1102
+
1103
+ async onGoodbye(from: PeerId, peerStream: PeerStreams, message: Goodbye) {
1104
+ if (!(await message.verify(false))) {
1105
+ logger.warn("Recieved message with invalid signature or timestamp");
1106
+ return false;
1107
+ }
1108
+
1109
+ const sender = message.sender?.hashcode();
1110
+ if (!sender) {
1111
+ logger.warn("Recieved hello without sender");
1112
+ return false;
1113
+ }
1114
+
1115
+ const peerKey = getPublicKeyFromPeerId(from);
1116
+ const peerKeyHash = peerKey.hashcode();
1117
+ if (message.early) {
1118
+ this.earlyGoodbyes.set(peerKeyHash, message);
1119
+ } else {
1120
+ const signatures = message.signatures;
1121
+ /* TODO Should we update routes on goodbye?
1122
+ for (let i = 1; i < signatures.signatures.length - 1; i++) {
1123
+ this.addRouteConnection(
1124
+ signatures.signatures[i].publicKey,
1125
+ signatures.signatures[i + 1].publicKey
1126
+ );
1127
+ }
1128
+ */
1129
+
1130
+ //let neighbour = message.trace[1] || this.peerIdStr;
1131
+ this.removeRouteConnection(
1132
+ signatures.signatures[0].publicKey,
1133
+ signatures.signatures[1].publicKey || this.publicKey
1134
+ );
1135
+
1136
+ const relayToPeers: PeerStreams[] = [];
1137
+ for (const stream of this.peers.values()) {
1138
+ if (stream.peerId.equals(from)) {
1139
+ continue;
1140
+ }
1141
+ relayToPeers.push(stream);
1142
+ }
1143
+ await message.sign(this.sign); // sign it so othere peers can now I have seen it (and can build a network graph from trace info)
1144
+
1145
+ const hellos = this.helloMap.get(sender);
1146
+ if (hellos) {
1147
+ const helloSignaturHash = await message.signatures.hashPublicKeys();
1148
+ hellos.delete(helloSignaturHash);
1149
+ }
1150
+
1151
+ // Forward
1152
+ await this.relayMessage(from, message);
1153
+ }
1154
+ return true;
1155
+ }
1156
+
1157
+ async onPing(from: PeerId, peerStream: PeerStreams, message: PingPong) {
1158
+ if (message instanceof Ping) {
1159
+ // respond with pong
1160
+ await this.publishMessage(
1161
+ this.components.peerId,
1162
+ new Pong(message.pingBytes),
1163
+ [peerStream]
1164
+ );
1165
+ } else if (message instanceof Pong) {
1166
+ // Let the (waiting) thread know that we have recieved the pong
1167
+ peerStream.pingJob?.resolve();
1168
+ } else {
1169
+ throw new Error("Unsupported");
1170
+ }
1171
+ }
1172
+
1173
+ async ping(stream: PeerStreams): Promise<number | undefined> {
1174
+ return new Promise<number | undefined>((resolve, reject) => {
1175
+ stream.pingJob?.abort();
1176
+ const ping = new Ping();
1177
+ const start = +new Date();
1178
+ const timeout = setTimeout(() => {
1179
+ reject(new TimeoutError("Ping timed out"));
1180
+ }, 10000);
1181
+ const resolver = () => {
1182
+ const end = +new Date();
1183
+ clearTimeout(timeout);
1184
+
1185
+ // TODO what happens if a peer send a ping back then leaves? Any problems?
1186
+ const latency = end - start;
1187
+ stream.pingLatency = latency;
1188
+ this.addRouteConnection(this.publicKey, stream.publicKey, latency);
1189
+ resolve(undefined);
1190
+ };
1191
+ stream.pingJob = {
1192
+ resolve: resolver,
1193
+ abort: () => {
1194
+ clearTimeout(timeout);
1195
+ resolve(undefined);
1196
+ },
1197
+ };
1198
+ this.publishMessage(this.components.peerId, ping, [stream]).catch(
1199
+ (err) => {
1200
+ clearTimeout(timeout);
1201
+ reject(err);
1202
+ }
1203
+ );
1204
+ });
1205
+ }
1206
+
1207
+ /**
1208
+ * Whether to accept a message from a peer
1209
+ * Override to create a graylist
1210
+ */
1211
+ acceptFrom(id: PeerId) {
1212
+ return true;
1213
+ }
1214
+
1215
+ /**
1216
+ * Publishes messages to all peers
1217
+ */
1218
+ async publish(
1219
+ data: Uint8Array | Uint8ArrayList,
1220
+ options?: { to?: (string | PublicSignKey | PeerId)[] | Set<string> }
1221
+ ): Promise<Uint8Array> {
1222
+ if (!this.started) {
1223
+ throw new Error("Not started");
1224
+ }
1225
+
1226
+ // dispatch the event if we are interested
1227
+ let toHashes: string[];
1228
+ if (options?.to) {
1229
+ if (options.to instanceof Set) {
1230
+ toHashes = new Array(options.to.size);
1231
+ } else {
1232
+ toHashes = new Array(options.to.length);
1233
+ }
1234
+
1235
+ let i = 0;
1236
+ for (const to of options.to) {
1237
+ toHashes[i++] =
1238
+ to instanceof PublicSignKey
1239
+ ? to.hashcode()
1240
+ : typeof to === "string"
1241
+ ? to
1242
+ : getPublicKeyFromPeerId(to).hashcode();
1243
+ }
1244
+ } else {
1245
+ toHashes = [];
1246
+ }
1247
+ const message = new DataMessage({
1248
+ data: data instanceof Uint8ArrayList ? data.subarray() : data,
1249
+ to: toHashes,
1250
+ });
1251
+ if (this.signaturePolicy === "StictSign") {
1252
+ await message.sign(this.sign);
1253
+ }
1254
+
1255
+ if (this.emitSelf) {
1256
+ super.dispatchEvent(
1257
+ new CustomEvent("data", {
1258
+ detail: message,
1259
+ })
1260
+ );
1261
+ }
1262
+
1263
+ // send to all the other peers
1264
+ await this.publishMessage(this.components.peerId, message, undefined);
1265
+ return message.id;
1266
+ }
1267
+
1268
+ public async hello(data?: Uint8Array): Promise<void> {
1269
+ if (!this.started) {
1270
+ return;
1271
+ }
1272
+
1273
+ // send to all the other peers
1274
+ await this.publishMessage(
1275
+ this.components.peerId,
1276
+ await new Hello({
1277
+ multiaddrs: this.components.addressManager
1278
+ .getAddresses()
1279
+ .map((x) => x.toString()),
1280
+ data,
1281
+ }).sign(this.sign.bind(this))
1282
+ );
1283
+ }
1284
+
1285
+ public async relayMessage(
1286
+ from: PeerId,
1287
+ message: Message,
1288
+ to?: PeerStreams[] | PeerMap<PeerStreams>
1289
+ ) {
1290
+ if (this.canRelayMessage) {
1291
+ return this.publishMessage(from, message, to, true);
1292
+ } else {
1293
+ logger.debug("received message we didn't subscribe to. Dropping.");
1294
+ }
1295
+ }
1296
+ public async publishMessage(
1297
+ from: PeerId,
1298
+ message: Message,
1299
+ to?: PeerStreams[] | PeerMap<PeerStreams>,
1300
+ relayed?: boolean
1301
+ ): Promise<void> {
1302
+ if (message instanceof DataMessage && !to) {
1303
+ // message.to can be distant peers, but "to" are neighbours
1304
+ const fanoutMap = new Map<string, string[]>();
1305
+
1306
+ // Message to > 0
1307
+ if (message.to.length > 0) {
1308
+ const missingPathsFor: string[] = [];
1309
+ for (const to of message.to) {
1310
+ const fromKey = this.peerIdToPublicKey
1311
+ .get(from.toString())
1312
+ ?.hashcode();
1313
+ if (to === this.publicKeyHash || fromKey === to) {
1314
+ continue; // don't send to me or backwards
1315
+ }
1316
+
1317
+ const directStream = this.peers.get(to);
1318
+ if (directStream) {
1319
+ // always favor direct stream, even path seems longer
1320
+ const fanout = fanoutMap.get(to);
1321
+ if (!fanout) {
1322
+ fanoutMap.set(to, [to]);
1323
+ } else {
1324
+ fanout.push(to);
1325
+ }
1326
+ continue;
1327
+ } else {
1328
+ const fromMe = from.equals(this.components.peerId);
1329
+ const block = !fromMe ? fromKey : undefined;
1330
+ const path = this.routes.getPath(this.publicKeyHash, to, {
1331
+ block, // prevent send message backwards
1332
+ });
1333
+
1334
+ if (path && path.length > 0) {
1335
+ const fanout = fanoutMap.get(path[1]);
1336
+ if (!fanout) {
1337
+ fanoutMap.set(path[1], [to]);
1338
+ if (
1339
+ this.connectionManagerOptions.autoDial &&
1340
+ path.length >= 3
1341
+ ) {
1342
+ await this.maybeConnectDirectly(path).catch((e) => {
1343
+ logger.error(
1344
+ "Failed to request direct connection: " + e.message
1345
+ );
1346
+ });
1347
+ }
1348
+ continue;
1349
+ } else {
1350
+ fanout.push(to);
1351
+ continue;
1352
+ }
1353
+ } else {
1354
+ missingPathsFor.push(to);
1355
+ }
1356
+ }
1357
+
1358
+ // we can't find path, send message to all peers
1359
+ fanoutMap.clear();
1360
+ break;
1361
+ }
1362
+
1363
+ // update to's
1364
+ let sentOnce = false;
1365
+ if (missingPathsFor.length === 0) {
1366
+ if (fanoutMap.size > 0) {
1367
+ for (const [neighbour, distantPeers] of fanoutMap) {
1368
+ message.to = distantPeers;
1369
+ const bytes = message.serialize();
1370
+ if (!sentOnce) {
1371
+ // if relayed = true, we have already added it to seenCache
1372
+ if (!relayed) {
1373
+ this.seenCache.add(await getMsgId(bytes));
1374
+ }
1375
+ sentOnce = true;
1376
+ }
1377
+
1378
+ const stream = this.peers.get(neighbour);
1379
+ await stream!.waitForWrite(bytes);
1380
+ }
1381
+ return; // we are done sending the message in all direction with updates 'to' lists
1382
+ }
1383
+ return;
1384
+ }
1385
+
1386
+ // else send to all (fallthrough to code below)
1387
+ }
1388
+ }
1389
+
1390
+ // We fils to send the message directly, instead fallback to floodsub
1391
+ const peers: PeerStreams[] | PeerMap<PeerStreams> = to || this.peers;
1392
+ if (
1393
+ peers == null ||
1394
+ (Array.isArray(peers) && peers.length === 0) ||
1395
+ (peers instanceof Map && peers.size === 0)
1396
+ ) {
1397
+ logger.debug("no peers are subscribed");
1398
+ return;
1399
+ }
1400
+
1401
+ const bytes = message.serialize();
1402
+ let sentOnce = false;
1403
+ for (const stream of peers.values()) {
1404
+ const id = stream as PeerStreams;
1405
+ if (id.peerId.equals(from)) {
1406
+ continue;
1407
+ }
1408
+
1409
+ if (!sentOnce) {
1410
+ sentOnce = true;
1411
+ if (!relayed) {
1412
+ // if relayed = true, we have already added it to seenCache
1413
+ const msgId = await getMsgId(bytes);
1414
+ this.seenCache.add(msgId);
1415
+ }
1416
+ }
1417
+ await id.waitForWrite(bytes);
1418
+ }
1419
+ if (!sentOnce && !relayed) {
1420
+ throw new Error("Message did not have any valid recievers. ");
1421
+ }
1422
+ }
1423
+
1424
+ async maybeConnectDirectly(path: string[]) {
1425
+ if (path.length < 3) {
1426
+ return;
1427
+ }
1428
+
1429
+ const toHash = path[path.length - 1];
1430
+
1431
+ if (this.peers.has(toHash)) {
1432
+ return; // TODO, is this expected, or are we to dial more addresses?
1433
+ }
1434
+
1435
+ // Try to either connect directly
1436
+ if (!this.recentDials.has(toHash)) {
1437
+ this.recentDials.add(toHash);
1438
+ const addrs = this.multiaddrsMap.get(toHash);
1439
+ try {
1440
+ if (addrs && addrs.length > 0) {
1441
+ await this.components.connectionManager.openConnection(
1442
+ addrs.map((x) => multiaddr(x))
1443
+ );
1444
+ return;
1445
+ }
1446
+ } catch (error) {
1447
+ // continue regardless of error
1448
+ }
1449
+ }
1450
+
1451
+ // Connect through a closer relay that maybe does holepunch for us
1452
+ const nextToHash = path[path.length - 2];
1453
+ const routeKey = nextToHash + toHash;
1454
+ if (!this.recentDials.has(routeKey)) {
1455
+ this.recentDials.add(routeKey);
1456
+ const to = this.peerKeyHashToPublicKey.get(toHash)! as Ed25519PublicKey;
1457
+ const toPeerId = await to.toPeerId();
1458
+ const addrs = this.multiaddrsMap.get(path[path.length - 2]);
1459
+ if (addrs && addrs.length > 0) {
1460
+ const addressesToDial = addrs.sort((a, b) => {
1461
+ if (a.includes("/wss/")) {
1462
+ if (b.includes("/wss/")) {
1463
+ return 0;
1464
+ }
1465
+ return -1;
1466
+ }
1467
+ if (a.includes("/ws/")) {
1468
+ if (b.includes("/ws/")) {
1469
+ return 0;
1470
+ }
1471
+ if (b.includes("/wss/")) {
1472
+ return 1;
1473
+ }
1474
+ return -1;
1475
+ }
1476
+ return 0;
1477
+ });
1478
+
1479
+ for (const addr of addressesToDial) {
1480
+ const circuitAddress = multiaddr(
1481
+ addr + "/p2p-circuit/webrtc/p2p/" + toPeerId.toString()
1482
+ );
1483
+ try {
1484
+ await this.components.connectionManager.openConnection(
1485
+ circuitAddress
1486
+ );
1487
+ return;
1488
+ } catch (error: any) {
1489
+ logger.error(
1490
+ "Failed to connect directly to: " +
1491
+ circuitAddress.toString() +
1492
+ ". " +
1493
+ error?.message
1494
+ );
1495
+ }
1496
+ }
1497
+ }
1498
+ }
1499
+ }
1500
+
1501
+ async waitFor(peer: PeerId | PublicSignKey) {
1502
+ const hash = (
1503
+ peer instanceof PublicSignKey ? peer : getPublicKeyFromPeerId(peer)
1504
+ ).hashcode();
1505
+ try {
1506
+ await waitFor(() => {
1507
+ if (!this.peers.has(hash)) {
1508
+ return false;
1509
+ }
1510
+ if (!this.routes.hasLink(this.publicKeyHash, hash)) {
1511
+ return false;
1512
+ }
1513
+
1514
+ return true;
1515
+ });
1516
+ } catch (error) {
1517
+ throw new Error(
1518
+ "Stream to " +
1519
+ hash +
1520
+ " does not exist. Connection exist: " +
1521
+ this.peers.has(hash) +
1522
+ ". Route exist: " +
1523
+ this.routes.hasLink(this.publicKeyHash, hash)
1524
+ );
1525
+ }
1526
+ const stream = this.peers.get(hash)!;
1527
+ try {
1528
+ await waitFor(() => stream.isReadable && stream.isWritable);
1529
+ } catch (error) {
1530
+ throw new Error(
1531
+ "Stream to " +
1532
+ stream.publicKey.hashcode() +
1533
+ " not ready. Readable: " +
1534
+ stream.isReadable +
1535
+ ". Writable " +
1536
+ stream.isWritable
1537
+ );
1538
+ }
1539
+ }
1540
+ }
1541
+
1542
+ export const waitForPeers = async (
1543
+ ...libs: {
1544
+ waitFor: (peer: PeerId | PublicSignKey) => Promise<void>;
1545
+ peerId: PeerId;
1546
+ }[]
1547
+ ) => {
1548
+ for (let i = 0; i < libs.length; i++) {
1549
+ for (let j = 0; j < libs.length; j++) {
1550
+ if (i === j) {
1551
+ continue;
1552
+ }
1553
+ await libs[i].waitFor(libs[j].peerId);
1554
+ }
1555
+ }
1556
+ };