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