@peerbit/stream 1.0.20 → 2.0.2

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/lib/esm/index.js CHANGED
@@ -1,23 +1,40 @@
1
- import { EventEmitter, CustomEvent } from "@libp2p/interface/events";
1
+ import { TypedEventEmitter, CustomEvent } from "@libp2p/interface";
2
2
  import { pipe } from "it-pipe";
3
3
  import Queue from "p-queue";
4
4
  import { pushable } from "it-pushable";
5
5
  import { Uint8ArrayList } from "uint8arraylist";
6
6
  import { abortableSource } from "abortable-iterator";
7
7
  import * as lp from "it-length-prefixed";
8
- import { Routes } from "./routes.js";
8
+ import { MAX_ROUTE_DISTANCE, Routes } from "./routes.js";
9
+ import pDefer from "p-defer";
10
+ import { AbortError, delay, TimeoutError, waitFor } from "@peerbit/time";
11
+ import { getKeypairFromPeerId, getPublicKeyFromPeerId, PublicSignKey, sha256Base64, toBase64 } from "@peerbit/crypto";
9
12
  import { multiaddr } from "@multiformats/multiaddr";
10
- import { delay, TimeoutError, waitFor, waitForAsync } from "@peerbit/time";
11
- import { getKeypairFromPeerId, getPublicKeyFromPeerId, PublicSignKey } from "@peerbit/crypto";
12
13
  import { logger } from "./logger.js";
13
- import { Cache } from "@peerbit/cache";
14
14
  export { logger };
15
- import { Message as Message, Goodbye, Hello, DataMessage, Ping, PingPong, Pong, getMsgId } from "@peerbit/stream-interface";
15
+ import { Cache } from "@peerbit/cache";
16
+ import { ready } from "@peerbit/crypto";
17
+ import { Message as Message, DataMessage, getMsgId, ACK, SeekDelivery, AcknowledgeDelivery, SilentDelivery, MessageHeader, Goodbye, TracedDelivery, AnyWhere } from "@peerbit/stream-interface";
18
+ import { MultiAddrinfo } from "@peerbit/stream-interface";
19
+ import { MovingAverageTracker } from "./metrics.js";
20
+ const logError = (e) => {
21
+ return logger.error(e?.message);
22
+ };
23
+ const DEFAULT_SEEK_MESSAGE_REDUDANCY = 2;
24
+ const DEFAULT_SILENT_MESSAGE_REDUDANCY = 1;
16
25
  const isWebsocketConnection = (c) => c.remoteAddr.protoNames().find((x) => x === "ws" || x === "wss");
26
+ const SEEK_DELIVERY_TIMEOUT = 15e3;
27
+ const MAX_DATA_LENGTH = 1e7 + 1000; // 10 mb and some metadata
28
+ const MAX_QUEUED_BYTES = MAX_DATA_LENGTH * 50;
29
+ const DEFAULT_PRUNE_CONNECTIONS_INTERVAL = 2e4;
30
+ const DEFAULT_MIN_CONNECTIONS = 2;
31
+ const DEFAULT_MAX_CONNECTIONS = 300;
32
+ const DEFAULT_PRUNED_CONNNECTIONS_TIMEOUT = 30 * 1000;
33
+ const ROUTE_UPDATE_DELAY_FACTOR = 3e4;
17
34
  /**
18
35
  * Thin wrapper around a peer's inbound / outbound pubsub streams
19
36
  */
20
- export class PeerStreams extends EventEmitter {
37
+ export class PeerStreams extends TypedEventEmitter {
21
38
  counter = 0;
22
39
  peerId;
23
40
  publicKey;
@@ -43,9 +60,9 @@ export class PeerStreams extends EventEmitter {
43
60
  */
44
61
  inboundAbortController;
45
62
  closed;
46
- pingJob;
47
- pingLatency;
48
63
  connId;
64
+ seekedOnce;
65
+ usedBandWidthTracker;
49
66
  constructor(init) {
50
67
  super();
51
68
  this.peerId = init.peerId;
@@ -55,6 +72,7 @@ export class PeerStreams extends EventEmitter {
55
72
  this.closed = false;
56
73
  this.connId = init.connId;
57
74
  this.counter = 1;
75
+ this.usedBandWidthTracker = new MovingAverageTracker();
58
76
  }
59
77
  /**
60
78
  * Do we have a connection to read from?
@@ -68,6 +86,9 @@ export class PeerStreams extends EventEmitter {
68
86
  get isWritable() {
69
87
  return Boolean(this.outboundStream);
70
88
  }
89
+ get usedBandwidth() {
90
+ return this.usedBandWidthTracker.value;
91
+ }
71
92
  /**
72
93
  * Send a message to this peer.
73
94
  * Throws if there is no `stream` to write to available.
@@ -77,9 +98,14 @@ export class PeerStreams extends EventEmitter {
77
98
  logger.error("No writable connection to " + this.peerId.toString());
78
99
  throw new Error("No writable connection to " + this.peerId.toString());
79
100
  }
80
- this.outboundStream.push(data instanceof Uint8Array ? new Uint8ArrayList(data) : data);
101
+ this.usedBandWidthTracker.add(data.byteLength);
102
+ this.outboundStream.push(data instanceof Uint8Array ? data : data.subarray());
81
103
  }
82
104
  async waitForWrite(bytes) {
105
+ if (this.closed) {
106
+ logger.error("Failed to send to stream: " + this.peerId + ". Closed");
107
+ return;
108
+ }
83
109
  if (!this.isWritable) {
84
110
  // Catch the event where the outbound stream is attach, but also abort if we shut down
85
111
  const outboundPromise = new Promise((rs, rj) => {
@@ -133,7 +159,7 @@ export class PeerStreams extends EventEmitter {
133
159
  // - abortable, set to only return on abort, rather than throw
134
160
  // - transformed with length-prefix transform
135
161
  this._rawInboundStream = stream;
136
- this.inboundStream = abortableSource(pipe(this._rawInboundStream, (source) => lp.decode(source, { maxDataLength: 100001000 })), this.inboundAbortController.signal, {
162
+ this.inboundStream = abortableSource(pipe(this._rawInboundStream, (source) => lp.decode(source, { maxDataLength: MAX_DATA_LENGTH })), this.inboundAbortController.signal, {
137
163
  returnOnAbort: true,
138
164
  onReturnError: (err) => {
139
165
  logger.error("Inbound stream error", err?.message);
@@ -149,9 +175,8 @@ export class PeerStreams extends EventEmitter {
149
175
  // If an outbound stream already exists, gently close it
150
176
  const _prevStream = this.outboundStream;
151
177
  this._rawOutboundStream = stream;
152
- this.pingJob?.abort();
153
178
  this.outboundStream = pushable({
154
- objectMode: true,
179
+ objectMode: false,
155
180
  onEnd: () => {
156
181
  return stream.close().then(() => {
157
182
  if (this._rawOutboundStream === stream) {
@@ -162,9 +187,7 @@ export class PeerStreams extends EventEmitter {
162
187
  });
163
188
  }
164
189
  });
165
- pipe(this.outboundStream, (source) => lp.encode(source), this._rawOutboundStream).catch((err) => {
166
- logger.error(err);
167
- });
190
+ pipe(this.outboundStream, (source) => lp.encode(source), this._rawOutboundStream).catch(logError);
168
191
  // Emit if the connection is new
169
192
  this.dispatchEvent(new CustomEvent("stream:outbound"));
170
193
  if (_prevStream != null) {
@@ -191,19 +214,16 @@ export class PeerStreams extends EventEmitter {
191
214
  this.inboundAbortController.abort();
192
215
  await this._rawInboundStream?.close();
193
216
  }
194
- this.pingJob?.abort();
195
- this.pingLatency = undefined;
196
- //this.dispatchEvent(new CustomEvent('close'))
217
+ this.dispatchEvent(new CustomEvent("close"));
197
218
  this._rawOutboundStream = undefined;
198
219
  this.outboundStream = undefined;
199
220
  this._rawInboundStream = undefined;
200
221
  this.inboundStream = undefined;
201
222
  }
202
223
  }
203
- export class DirectStream extends EventEmitter {
224
+ export class DirectStream extends TypedEventEmitter {
204
225
  components;
205
226
  peerId;
206
- peerIdStr;
207
227
  publicKey;
208
228
  publicKeyHash;
209
229
  sign;
@@ -213,7 +233,6 @@ export class DirectStream extends EventEmitter {
213
233
  */
214
234
  peers;
215
235
  peerKeyHashToPublicKey;
216
- peerIdToPublicKey;
217
236
  routes;
218
237
  /**
219
238
  * If router can relay received messages, even if not subscribed
@@ -223,82 +242,110 @@ export class DirectStream extends EventEmitter {
223
242
  * if publish should emit to self, if subscribed
224
243
  */
225
244
  signaturePolicy;
226
- emitSelf;
227
245
  queue;
228
246
  multicodecs;
229
247
  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
248
  _registrarTopologyIds;
234
249
  maxInboundStreams;
235
250
  maxOutboundStreams;
236
- topology;
237
- pingJobPromise;
238
- pingJob;
239
- pingInterval;
240
251
  connectionManagerOptions;
241
252
  recentDials;
253
+ healthChecks;
254
+ pruneConnectionsTimeout;
255
+ prunedConnectionsCache;
256
+ routeSeekInterval;
257
+ seekTimeout;
258
+ closeController;
259
+ _ackCallbacks = new Map();
242
260
  constructor(components, multicodecs, options) {
243
261
  super();
244
262
  this.components = components;
245
- const { canRelayMessage = false, emitSelf = false, messageProcessingConcurrency = 10, pingInterval = 10 * 1000, maxInboundStreams, maxOutboundStreams, signaturePolicy = "StictSign", connectionManager = { autoDial: true } } = options || {};
263
+ const { canRelayMessage = false, messageProcessingConcurrency = 10, maxInboundStreams, maxOutboundStreams, signaturePolicy = "StictSign", connectionManager, routeSeekInterval = ROUTE_UPDATE_DELAY_FACTOR, seekTimeout = SEEK_DELIVERY_TIMEOUT } = options || {};
246
264
  const signKey = getKeypairFromPeerId(components.peerId);
265
+ this.seekTimeout = seekTimeout;
247
266
  this.sign = signKey.sign.bind(signKey);
248
267
  this.peerId = components.peerId;
249
- this.peerIdStr = components.peerId.toString();
250
268
  this.publicKey = signKey.publicKey;
251
269
  this.publicKeyHash = signKey.publicKey.hashcode();
252
270
  this.multicodecs = multicodecs;
253
271
  this.started = false;
254
272
  this.peers = new Map();
255
- this.helloMap = new Map();
256
- this.multiaddrsMap = new Map();
257
273
  this.routes = new Routes(this.publicKeyHash);
258
274
  this.canRelayMessage = canRelayMessage;
259
- this.emitSelf = emitSelf;
275
+ this.healthChecks = new Map();
276
+ this.routeSeekInterval = routeSeekInterval;
260
277
  this.queue = new Queue({ concurrency: messageProcessingConcurrency });
261
- this.earlyGoodbyes = new Map();
262
278
  this.maxInboundStreams = maxInboundStreams;
263
279
  this.maxOutboundStreams = maxOutboundStreams;
264
- this.seenCache = new Cache({ max: 1e3, ttl: 10 * 60 * 1e3 });
280
+ this.seenCache = new Cache({ max: 1e6, ttl: 10 * 60 * 1e3 });
265
281
  this.peerKeyHashToPublicKey = new Map();
266
- this.peerIdToPublicKey = new Map();
267
- this.pingInterval = pingInterval;
268
282
  this._onIncomingStream = this._onIncomingStream.bind(this);
269
283
  this.onPeerConnected = this.onPeerConnected.bind(this);
270
284
  this.onPeerDisconnected = this.onPeerDisconnected.bind(this);
271
285
  this.signaturePolicy = signaturePolicy;
272
- this.connectionManagerOptions = connectionManager;
273
- this.recentDials = new Cache({
274
- ttl: connectionManager.retryDelay || 60 * 1000,
275
- max: 1e3
276
- });
286
+ if (connectionManager === false || connectionManager === null) {
287
+ this.connectionManagerOptions = {
288
+ maxConnections: Number.MAX_SAFE_INTEGER,
289
+ minConnections: 0,
290
+ dialer: undefined,
291
+ pruner: undefined
292
+ };
293
+ }
294
+ else {
295
+ this.connectionManagerOptions = {
296
+ maxConnections: DEFAULT_MAX_CONNECTIONS,
297
+ minConnections: DEFAULT_MIN_CONNECTIONS,
298
+ ...connectionManager,
299
+ dialer: connectionManager?.dialer !== false &&
300
+ connectionManager?.dialer !== null
301
+ ? { retryDelay: 60 * 1000, ...connectionManager?.dialer }
302
+ : undefined,
303
+ pruner: connectionManager?.pruner !== false &&
304
+ connectionManager?.pruner !== null
305
+ ? {
306
+ connectionTimeout: DEFAULT_PRUNED_CONNNECTIONS_TIMEOUT,
307
+ interval: DEFAULT_PRUNE_CONNECTIONS_INTERVAL,
308
+ maxBuffer: MAX_QUEUED_BYTES,
309
+ ...connectionManager?.pruner
310
+ }
311
+ : undefined
312
+ };
313
+ }
314
+ this.recentDials = this.connectionManagerOptions.dialer
315
+ ? new Cache({
316
+ ttl: this.connectionManagerOptions.dialer.retryDelay,
317
+ max: 1e3
318
+ })
319
+ : undefined;
320
+ this.prunedConnectionsCache = this.connectionManagerOptions.pruner
321
+ ? new Cache({
322
+ max: 1e6,
323
+ ttl: this.connectionManagerOptions.pruner.connectionTimeout
324
+ })
325
+ : undefined;
277
326
  }
278
327
  async start() {
279
328
  if (this.started) {
280
329
  return;
281
330
  }
331
+ await ready;
332
+ this.closeController = new AbortController();
333
+ this.started = true;
282
334
  logger.debug("starting");
283
- // register protocol with topology
284
- // Topology callbacks called on connection manager changes
285
- this._registrarTopologyIds = await Promise.all(this.multicodecs.map((multicodec) => this.components.registrar.register(multicodec, {
286
- onConnect: this.onPeerConnected.bind(this),
287
- onDisconnect: this.onPeerDisconnected.bind(this)
288
- })));
289
335
  // Incoming streams
290
336
  // Called after a peer dials us
291
337
  await Promise.all(this.multicodecs.map((multicodec) => this.components.registrar.handle(multicodec, this._onIncomingStream, {
292
338
  maxInboundStreams: this.maxInboundStreams,
293
- maxOutboundStreams: this.maxOutboundStreams
339
+ maxOutboundStreams: this.maxOutboundStreams,
340
+ runOnTransientConnection: false
341
+ })));
342
+ // register protocol with topology
343
+ // Topology callbacks called on connection manager changes
344
+ this._registrarTopologyIds = await Promise.all(this.multicodecs.map((multicodec) => this.components.registrar.register(multicodec, {
345
+ onConnect: this.onPeerConnected.bind(this),
346
+ onDisconnect: this.onPeerDisconnected.bind(this),
347
+ notifyOnTransient: false
294
348
  })));
295
- // TODO remove/modify when https://github.com/libp2p/js-libp2p/issues/2036 is resolved
296
- this.components.events.addEventListener("connection:open", (e) => {
297
- if (e.detail.multiplexer === "/webrtc") {
298
- this.onPeerConnected(e.detail.remotePeer, e.detail);
299
- }
300
- });
301
- this.started = true;
302
349
  // 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)
303
350
  const peerToConnections = new Map();
304
351
  const connections = this.components.connectionManager.getConnections();
@@ -319,35 +366,24 @@ export class DirectStream extends EventEmitter {
319
366
  break;
320
367
  }
321
368
  }
322
- await this.onPeerConnected(conn.remotePeer, conn, { fromExisting: true });
323
- }
324
- const pingJob = async () => {
325
- // TODO don't use setInterval but waitFor previous done to be done
326
- await this.pingJobPromise;
327
- const promises = [];
328
- this.peers.forEach((peer) => {
329
- promises.push(this.ping(peer).catch((e) => {
330
- if (e instanceof TimeoutError) {
331
- // Ignore
332
- }
333
- else {
334
- logger.error(e);
335
- }
336
- }));
337
- });
338
- promises.push(this.hello()); // Repetedly say hello to everyone to create traces in the network to measure latencies
339
- this.pingJobPromise = Promise.all(promises)
340
- .catch((e) => {
341
- logger.error(e?.message);
342
- })
343
- .finally(() => {
344
- if (!this.started || !this.pingInterval) {
369
+ await this.onPeerConnected(conn.remotePeer, conn);
370
+ }
371
+ if (this.connectionManagerOptions.pruner) {
372
+ const pruneConnectionsLoop = () => {
373
+ if (!this.connectionManagerOptions.pruner) {
345
374
  return;
346
375
  }
347
- this.pingJob = setTimeout(pingJob, this.pingInterval);
348
- });
349
- };
350
- pingJob();
376
+ this.pruneConnectionsTimeout = setTimeout(() => {
377
+ this.maybePruneConnections().finally(() => {
378
+ if (!this.started) {
379
+ return;
380
+ }
381
+ pruneConnectionsLoop();
382
+ });
383
+ }, this.connectionManagerOptions.pruner.interval);
384
+ };
385
+ pruneConnectionsLoop();
386
+ }
351
387
  }
352
388
  /**
353
389
  * Unregister the pubsub protocol and the streams with other peers will be closed.
@@ -357,26 +393,31 @@ export class DirectStream extends EventEmitter {
357
393
  return;
358
394
  }
359
395
  this.started = false;
360
- clearTimeout(this.pingJob);
361
- await this.pingJobPromise;
396
+ this.closeController.abort();
397
+ clearTimeout(this.pruneConnectionsTimeout);
362
398
  await Promise.all(this.multicodecs.map((x) => this.components.registrar.unhandle(x)));
363
399
  logger.debug("stopping");
364
400
  for (const peerStreams of this.peers.values()) {
365
401
  await peerStreams.close();
366
402
  }
403
+ for (const [k, v] of this.healthChecks) {
404
+ clearTimeout(v);
405
+ }
406
+ this.healthChecks.clear();
407
+ this.prunedConnectionsCache?.clear();
367
408
  // unregister protocol and handlers
368
409
  if (this._registrarTopologyIds != null) {
369
410
  this._registrarTopologyIds?.map((id) => this.components.registrar.unregister(id));
370
411
  }
371
412
  this.queue.clear();
372
- this.helloMap.clear();
373
- this.multiaddrsMap.clear();
374
- this.earlyGoodbyes.clear();
375
413
  this.peers.clear();
376
414
  this.seenCache.clear();
377
415
  this.routes.clear();
378
416
  this.peerKeyHashToPublicKey.clear();
379
- this.peerIdToPublicKey.clear();
417
+ for (const [k, v] of this._ackCallbacks) {
418
+ clearTimeout(v.timeout);
419
+ }
420
+ this._ackCallbacks.clear();
380
421
  logger.debug("stopped");
381
422
  }
382
423
  isStarted() {
@@ -396,65 +437,32 @@ export class DirectStream extends EventEmitter {
396
437
  return;
397
438
  }
398
439
  const publicKey = getPublicKeyFromPeerId(peerId);
440
+ if (this.prunedConnectionsCache?.has(publicKey.hashcode())) {
441
+ await connection.close();
442
+ await this.components.peerStore.delete(peerId);
443
+ return;
444
+ }
399
445
  const peer = this.addPeer(peerId, publicKey, stream.protocol, connection.id);
400
446
  const inboundStream = peer.attachInboundStream(stream);
401
- this.processMessages(peerId, inboundStream, peer).catch((err) => {
402
- logger.error(err);
403
- });
447
+ this.processMessages(peer.publicKey, inboundStream, peer).catch(logError);
404
448
  }
405
449
  /**
406
450
  * Registrar notifies an established connection with protocol
407
451
  */
408
- async onPeerConnected(peerId, conn, properties) {
409
- if (conn.transient) {
410
- return;
411
- }
412
- if (!this.isStarted() || conn.status !== "open") {
452
+ async onPeerConnected(peerId, connection) {
453
+ if (!this.isStarted() ||
454
+ connection.transient ||
455
+ connection.status !== "open") {
413
456
  return;
414
457
  }
415
- try {
416
- // TODO remove/modify when https://github.com/libp2p/js-libp2p/issues/2036 is resolved
417
- let closeFn = undefined;
418
- const close = () => {
419
- closeFn?.();
420
- };
421
- this.addEventListener("close", close);
422
- const result = await waitForAsync(async () => {
423
- try {
424
- const hasProtocol = await this.components.peerStore
425
- .get(peerId)
426
- .then((x) => this.multicodecs.find((y) => x.protocols.includes(y)));
427
- if (!hasProtocol) {
428
- return;
429
- }
430
- }
431
- catch (error) {
432
- if (error.code === "ERR_NOT_FOUND") {
433
- return;
434
- }
435
- throw error;
436
- }
437
- return true;
438
- }, {
439
- delayInterval: 100,
440
- timeout: 1e4,
441
- stopperCallback: (cb) => {
442
- closeFn = cb;
443
- }
444
- }).finally(() => {
445
- this.removeEventListener("close", close);
446
- });
447
- if (!result) {
448
- return;
449
- }
450
- }
451
- catch (error) {
452
- return;
458
+ const peerKey = getPublicKeyFromPeerId(peerId);
459
+ if (this.prunedConnectionsCache?.has(peerKey.hashcode())) {
460
+ await connection.close();
461
+ await this.components.peerStore.delete(peerId);
462
+ return; // we recently pruned this connect, dont allow it to connect for a while
453
463
  }
454
464
  try {
455
- const peerKey = getPublicKeyFromPeerId(peerId);
456
- const peerKeyHash = peerKey.hashcode();
457
- for (const existingStreams of conn.streams) {
465
+ for (const existingStreams of connection.streams) {
458
466
  if (existingStreams.protocol &&
459
467
  this.multicodecs.includes(existingStreams.protocol) &&
460
468
  existingStreams.direction === "outbound") {
@@ -473,35 +481,20 @@ export class DirectStream extends EventEmitter {
473
481
  return;
474
482
  }
475
483
  try {
476
- stream = await conn.newStream(this.multicodecs);
484
+ stream = await connection.newStream(this.multicodecs);
477
485
  if (stream.protocol == null) {
478
486
  stream.abort(new Error("Stream was not multiplexed"));
479
487
  return;
480
488
  }
481
- peer = this.addPeer(peerId, peerKey, stream.protocol, conn.id); // TODO types
489
+ peer = this.addPeer(peerId, peerKey, stream.protocol, connection.id); // TODO types
482
490
  await peer.attachOutboundStream(stream);
483
- // TODO do we want to do this?
484
- /* if (!peer.inboundStream) {
485
- const inboundStream = conn.streams.find(
486
- (x) =>
487
- x.stat.protocol &&
488
- this.multicodecs.includes(x.stat.protocol) &&
489
- x.stat.direction === "inbound"
490
- );
491
- if (inboundStream) {
492
- this._onIncomingStream({
493
- connection: conn,
494
- stream: inboundStream,
495
- });
496
- }
497
- } */
498
491
  }
499
492
  catch (error) {
500
493
  if (error.code === "ERR_UNSUPPORTED_PROTOCOL") {
501
494
  await delay(100);
502
495
  continue; // Retry
503
496
  }
504
- if (conn.status !== "open" ||
497
+ if (connection.status !== "open" ||
505
498
  error?.message === "Muxer already closed" ||
506
499
  error.code === "ERR_STREAM_RESET") {
507
500
  return; // fail silenty
@@ -513,80 +506,12 @@ export class DirectStream extends EventEmitter {
513
506
  if (!stream) {
514
507
  return;
515
508
  }
516
- if (properties?.fromExisting) {
517
- return; // we return here because we will enter this method once more once the protocol has been registered for the remote peer
518
- }
519
- // Add connection with assumed large latency
520
- this.peerIdToPublicKey.set(peerId.toString(), peerKey);
521
- const promises = [];
522
- /* if (!existingStream) */ {
523
- this.addRouteConnection(this.publicKey, peerKey, Number.MAX_SAFE_INTEGER);
524
- // Get accurate latency
525
- promises.push(this.ping(peer));
526
- // Say hello
527
- promises.push(this.publishMessage(this.components.peerId, await new Hello({
528
- multiaddrs: this.components.addressManager
529
- .getAddresses()
530
- .map((x) => x.toString())
531
- }).sign(this.sign), [peer]));
532
- // Send my goodbye early if I disconnect for some reason, (so my peer can say goodbye for me)
533
- // TODO add custom condition fn for doing below
534
- promises.push(this.publishMessage(this.components.peerId, await new Goodbye({ early: true }).sign(this.sign), [peer]));
535
- // replay all hellos
536
- for (const [sender, hellos] of this.helloMap) {
537
- if (sender === peerKeyHash) {
538
- // Don't say hellos from sender to same sender (uneccessary)
539
- continue;
540
- }
541
- outer: for (const [key, hello] of hellos) {
542
- if (!hello.header.verify()) {
543
- hellos.delete(key);
544
- }
545
- for (const signer of hello.signatures.publicKeys) {
546
- if (!this.routes.hasNode(signer.hashcode())) {
547
- // purge this hello since it has travelled a path that no longer exist
548
- hellos.delete(key);
549
- continue outer;
550
- }
551
- }
552
- promises.push(this.publishMessage(this.components.peerId, hello, [peer]));
553
- }
554
- }
555
- }
556
- const resolved = await Promise.all(promises);
557
- return resolved;
509
+ this.addRouteConnection(this.publicKeyHash, peerKey.hashcode(), peerKey, 0, +new Date());
558
510
  }
559
511
  catch (err) {
560
512
  logger.error(err);
561
513
  }
562
514
  }
563
- addRouteConnection(from, to, latency) {
564
- this.peerKeyHashToPublicKey.set(from.hashcode(), from);
565
- this.peerKeyHashToPublicKey.set(to.hashcode(), to);
566
- const links = this.routes.addLink(from.hashcode(), to.hashcode(), latency);
567
- for (const added of links) {
568
- const key = this.peerKeyHashToPublicKey.get(added);
569
- if (key?.equals(this.publicKey) === false) {
570
- this.onPeerReachable(key);
571
- }
572
- }
573
- }
574
- removeRouteConnection(from, to) {
575
- const has = this.routes.hasNode(to.hashcode());
576
- if (!has) {
577
- this.onPeerUnreachable(to);
578
- }
579
- else {
580
- const links = this.routes.deleteLink(from.hashcode(), to.hashcode());
581
- for (const deleted of links) {
582
- const key = this.peerKeyHashToPublicKey.get(deleted);
583
- this.peerKeyHashToPublicKey.delete(deleted);
584
- if (key?.equals(this.publicKey) === false) {
585
- this.onPeerUnreachable(key);
586
- }
587
- }
588
- }
589
- }
590
515
  /**
591
516
  * Registrar notifies a closing connection with pubsub protocol
592
517
  */
@@ -608,19 +533,51 @@ export class DirectStream extends EventEmitter {
608
533
  }
609
534
  if (!this.publicKey.equals(peerKey)) {
610
535
  await this._removePeer(peerKey);
611
- this.removeRouteConnection(this.publicKey, peerKey);
612
536
  // Notify network
613
- const earlyGoodBye = this.earlyGoodbyes.get(peerKeyHash);
614
- if (earlyGoodBye) {
615
- earlyGoodBye.early = false;
616
- await earlyGoodBye.sign(this.sign);
617
- await this.publishMessage(this.components.peerId, earlyGoodBye);
618
- this.earlyGoodbyes.delete(peerKeyHash);
537
+ const dependent = this.routes.getDependent(peerKeyHash);
538
+ this.removeRouteConnection(peerKeyHash, true);
539
+ if (dependent.length > 0) {
540
+ await this.publishMessage(this.publicKey, await new Goodbye({
541
+ leaving: [peerKeyHash],
542
+ header: new MessageHeader({
543
+ mode: new SilentDelivery({ to: dependent, redundancy: 2 })
544
+ })
545
+ }).sign(this.sign));
619
546
  }
620
- this.peerIdToPublicKey.delete(peerId.toString());
621
547
  }
622
548
  logger.debug("connection ended:" + peerKey.toString());
623
549
  }
550
+ removeRouteConnection(hash, neigbour) {
551
+ const unreachable = neigbour
552
+ ? this.routes.removeNeighbour(hash)
553
+ : this.routes.removeTarget(hash);
554
+ for (const node of unreachable) {
555
+ this.onPeerUnreachable(node); // TODO types
556
+ this.peerKeyHashToPublicKey.delete(node);
557
+ }
558
+ }
559
+ addRouteConnection(from, neighbour, target, distance, session, pending = false) {
560
+ const targetHash = typeof target === "string" ? target : target.hashcode();
561
+ const wasReachable = from === this.publicKeyHash
562
+ ? this.routes.isReachable(from, targetHash)
563
+ : true;
564
+ if (pending) {
565
+ this.routes.addPendingRouteConnection(session, {
566
+ distance,
567
+ from,
568
+ neighbour,
569
+ target: targetHash
570
+ });
571
+ }
572
+ else {
573
+ this.routes.add(from, neighbour, targetHash, distance, session);
574
+ }
575
+ const newPeer = wasReachable === false && this.routes.isReachable(from, targetHash);
576
+ if (newPeer) {
577
+ this.peerKeyHashToPublicKey.set(target.hashcode(), target);
578
+ this.onPeerReachable(target); // TODO types
579
+ }
580
+ }
624
581
  /**
625
582
  * invoked when a new peer becomes reachable
626
583
  * @param publicKeyHash
@@ -633,17 +590,24 @@ export class DirectStream extends EventEmitter {
633
590
  * invoked when a new peer becomes unreachable
634
591
  * @param publicKeyHash
635
592
  */
636
- onPeerUnreachable(publicKey) {
593
+ onPeerUnreachable(hash) {
637
594
  // override this fn
638
- this.helloMap.delete(publicKey.hashcode());
639
- this.multiaddrsMap.delete(publicKey.hashcode());
640
- this.dispatchEvent(new CustomEvent("peer:unreachable", { detail: publicKey }));
595
+ this.dispatchEvent(
596
+ // TODO types
597
+ new CustomEvent("peer:unreachable", {
598
+ detail: this.peerKeyHashToPublicKey.get(hash)
599
+ }));
641
600
  }
642
601
  /**
643
602
  * Notifies the router that a peer has been connected
644
603
  */
645
604
  addPeer(peerId, publicKey, protocol, connId) {
646
605
  const publicKeyHash = publicKey.hashcode();
606
+ const hc = this.healthChecks.get(publicKeyHash);
607
+ if (hc) {
608
+ clearTimeout(hc);
609
+ this.healthChecks.delete(publicKeyHash);
610
+ }
647
611
  const existing = this.peers.get(publicKeyHash);
648
612
  // If peer streams already exists, do nothing
649
613
  if (existing != null) {
@@ -690,32 +654,7 @@ export class DirectStream extends EventEmitter {
690
654
  try {
691
655
  await pipe(stream, async (source) => {
692
656
  for await (const data of source) {
693
- const msgId = await getMsgId(data);
694
- if (this.seenCache.has(msgId)) {
695
- // we got message that WE sent?
696
- /**
697
- * Most propobable reason why we arrive here is a race condition/issue
698
-
699
- ┌─┐
700
- │0│
701
- └△┘
702
- ┌▽┐
703
- │1│
704
- └△┘
705
- ┌▽┐
706
- │2│
707
- └─┘
708
-
709
- from 2s perspective,
710
-
711
- 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
712
- but two is already connected by onPeerConnected has not been invoked yet, so the hello message gets forwarded,
713
- and later onPeerConnected gets invoked on 1, and the same message gets resent to 2
714
- */
715
- continue;
716
- }
717
- this.seenCache.add(msgId);
718
- this.processRpc(peerId, peerStreams, data).catch((err) => logger.warn(err));
657
+ this.processRpc(peerId, peerStreams, data).catch(logError);
719
658
  }
720
659
  });
721
660
  }
@@ -731,10 +670,6 @@ export class DirectStream extends EventEmitter {
731
670
  * Handles an rpc request from a peer
732
671
  */
733
672
  async processRpc(from, peerStreams, message) {
734
- if (!this.acceptFrom(from)) {
735
- logger.debug("received message from unacceptable peer %p", from);
736
- return false;
737
- }
738
673
  // logger.debug("rpc from " + from + ", " + this.peerIdStr);
739
674
  if (message.length > 0) {
740
675
  // logger.debug("messages from " + from);
@@ -747,18 +682,21 @@ export class DirectStream extends EventEmitter {
747
682
  logger.error(err);
748
683
  }
749
684
  })
750
- .catch((err) => logger.warn(err));
685
+ .catch(logError);
751
686
  }
752
687
  return true;
753
688
  }
689
+ async modifySeenCache(message, getIdFn = getMsgId) {
690
+ const msgId = await getIdFn(message);
691
+ const seen = this.seenCache.get(msgId);
692
+ this.seenCache.add(msgId, seen ? seen + 1 : 1);
693
+ return seen || 0;
694
+ }
754
695
  /**
755
696
  * Handles a message from a peer
756
697
  */
757
698
  async processMessage(from, peerStream, msg) {
758
- if (!from.publicKey) {
759
- return;
760
- }
761
- if (this.components.peerId.equals(from) && !this.emitSelf) {
699
+ if (!this.started) {
762
700
  return;
763
701
  }
764
702
  // Ensure the message is valid before processing it
@@ -767,208 +705,226 @@ export class DirectStream extends EventEmitter {
767
705
  detail: message
768
706
  }));
769
707
  if (message instanceof DataMessage) {
770
- await this.onDataMessage(from, peerStream, message);
708
+ // DONT await this since it might introduce a dead-lock
709
+ this._onDataMessage(from, peerStream, msg, message).catch(logError);
771
710
  }
772
- else if (message instanceof Hello) {
773
- await this.onHello(from, peerStream, message);
711
+ else {
712
+ if (message instanceof ACK) {
713
+ this.onAck(from, peerStream, msg, message).catch(logError);
714
+ }
715
+ else if (message instanceof Goodbye) {
716
+ this.onGoodBye(from, peerStream, msg, message).catch(logError);
717
+ }
718
+ else {
719
+ throw new Error("Unsupported message type");
720
+ }
774
721
  }
775
- else if (message instanceof Goodbye) {
776
- await this.onGoodbye(from, peerStream, message);
722
+ }
723
+ shouldIgnore(message, seenBefore) {
724
+ const fromMe = message.header.signatures?.publicKeys.find((x) => x.equals(this.publicKey));
725
+ if (fromMe) {
726
+ return true;
727
+ }
728
+ if ((seenBefore > 0 &&
729
+ message.header.mode instanceof SeekDelivery === false) ||
730
+ (message.header.mode instanceof SeekDelivery &&
731
+ seenBefore >= message.header.mode.redundancy)) {
732
+ return true;
733
+ }
734
+ return false;
735
+ }
736
+ async onDataMessage(from, peerStream, message, seenBefore) {
737
+ if (this.shouldIgnore(message, seenBefore)) {
738
+ return false;
777
739
  }
778
- else if (message instanceof PingPong) {
779
- await this.onPing(from, peerStream, message);
740
+ let isForMe = false;
741
+ if (message.header.mode instanceof AnyWhere) {
742
+ isForMe = true;
780
743
  }
781
744
  else {
782
- throw new Error("Unsupported");
745
+ const isFromSelf = this.publicKey.equals(from);
746
+ if (!isFromSelf) {
747
+ isForMe =
748
+ message.header.mode.to == null ||
749
+ message.header.mode.to.find((x) => x === this.publicKeyHash) != null;
750
+ }
783
751
  }
784
- }
785
- async onDataMessage(from, peerStream, message) {
786
- const isFromSelf = this.components.peerId.equals(from);
787
- if (!isFromSelf || this.emitSelf) {
788
- const isForAll = message.to.length === 0;
789
- const isForMe = !isForAll && message.to.find((x) => x === this.publicKeyHash);
790
- if (isForAll || isForMe) {
791
- if (this.signaturePolicy === "StictSign" &&
792
- !(await message.verify(this.signaturePolicy === "StictSign"))) {
793
- // we don't verify messages we don't dispatch because of the performance penalty // TODO add opts for this
794
- logger.warn("Recieved message with invalid signature or timestamp");
795
- return false;
796
- }
752
+ if (isForMe) {
753
+ if ((await this.maybeVerifyMessage(message)) === false) {
754
+ // we don't verify messages we don't dispatch because of the performance penalty // TODO add opts for this
755
+ logger.warn("Recieved message with invalid signature or timestamp");
756
+ return false;
757
+ }
758
+ await this.acknowledgeMessage(peerStream, message, seenBefore);
759
+ if (seenBefore === 0 && message.data) {
797
760
  this.dispatchEvent(new CustomEvent("data", {
798
761
  detail: message
799
762
  }));
800
763
  }
801
- if (isForMe && message.to.length === 1) {
764
+ }
765
+ if (message.header.mode instanceof SilentDelivery ||
766
+ message.header.mode instanceof AcknowledgeDelivery) {
767
+ if (message.header.mode.to &&
768
+ message.header.mode.to.length === 1 &&
769
+ message.header.mode.to[0] === this.publicKeyHash) {
802
770
  // dont forward this message anymore because it was meant ONLY for me
803
771
  return true;
804
772
  }
805
773
  }
806
774
  // Forward
807
- await this.relayMessage(from, message);
808
- return true;
809
- }
810
- async onHello(from, peerStream, message) {
811
- if (!(await message.verify(false))) {
812
- const a = message.header.verify();
813
- const b = message.networkInfo.pingLatencies.length ===
814
- message.signatures.signatures.length - 1;
815
- logger.warn(`Recieved hello message that did not verify. Header: ${a}, Ping info ${b}, Signatures ${a && b}`);
816
- return false;
817
- }
818
- const sender = message.sender?.hashcode();
819
- if (!sender) {
820
- logger.warn("Recieved hello without sender");
821
- return false;
822
- }
823
- const signatures = message.signatures;
824
- for (let i = 0; i < signatures.signatures.length - 1; i++) {
825
- this.addRouteConnection(signatures.signatures[i].publicKey, signatures.signatures[i + 1].publicKey, message.networkInfo.pingLatencies[i]);
826
- }
827
- message.networkInfo.pingLatencies.push(peerStream.pingLatency || 4294967295); // TODO don't propagate if latency is high?
828
- 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)
829
- let hellos = this.helloMap.get(sender);
830
- if (!hellos) {
831
- hellos = new Map();
832
- this.helloMap.set(sender, hellos);
833
- }
834
- this.multiaddrsMap.set(sender, message.multiaddrs);
835
- const helloSignaturHash = await message.signatures.hashPublicKeys();
836
- const existingHello = hellos.get(helloSignaturHash);
837
- if (existingHello) {
838
- if (existingHello.header.expires < message.header.expires) {
839
- hellos.set(helloSignaturHash, message);
775
+ if (message.header.mode instanceof SeekDelivery || seenBefore === 0) {
776
+ // DONT await this since it might introduce a dead-lock
777
+ if (message.header.mode instanceof SeekDelivery) {
778
+ if (seenBefore < message.header.mode.redundancy) {
779
+ const to = [...this.peers.values()].filter((x) => !message.header.signatures?.publicKeys.find((y) => y.equals(x.publicKey)) && x != peerStream);
780
+ if (to.length > 0) {
781
+ this.relayMessage(from, message, to);
782
+ }
783
+ }
784
+ }
785
+ else {
786
+ this.relayMessage(from, message);
840
787
  }
841
788
  }
842
- else {
843
- hellos.set(helloSignaturHash, message);
789
+ }
790
+ async maybeVerifyMessage(message) {
791
+ return message.verify(this.signaturePolicy === "StictSign");
792
+ }
793
+ async acknowledgeMessage(peerStream, message, seenBefore) {
794
+ if ((message.header.mode instanceof SeekDelivery ||
795
+ message.header.mode instanceof AcknowledgeDelivery) &&
796
+ seenBefore < message.header.mode.redundancy) {
797
+ await this.publishMessage(this.publicKey, await new ACK({
798
+ messageIdToAcknowledge: message.id,
799
+ seenCounter: seenBefore,
800
+ // TODO only give origin info to peers we want to connect to us
801
+ header: new MessageHeader({
802
+ mode: new TracedDelivery(message.header.signatures.publicKeys.map((x) => x.hashcode())),
803
+ // include our origin if message is SeekDelivery and we have not recently pruned a connection to this peer
804
+ origin: message.header.mode instanceof SeekDelivery &&
805
+ !message.header.signatures.publicKeys.find((x) => this.prunedConnectionsCache?.has(x.hashcode()))
806
+ ? new MultiAddrinfo(this.components.addressManager
807
+ .getAddresses()
808
+ .map((x) => x.toString()))
809
+ : undefined
810
+ })
811
+ }).sign(this.sign), [peerStream]);
844
812
  }
845
- // Forward
846
- await this.relayMessage(from, message);
847
- return true;
848
813
  }
849
- async onGoodbye(from, peerStream, message) {
850
- if (!(await message.verify(false))) {
851
- logger.warn("Recieved message with invalid signature or timestamp");
814
+ async _onDataMessage(from, peerStream, messageBytes, message) {
815
+ const seenBefore = await this.modifySeenCache(messageBytes instanceof Uint8ArrayList
816
+ ? messageBytes.subarray()
817
+ : messageBytes);
818
+ return this.onDataMessage(from, peerStream, message, seenBefore);
819
+ }
820
+ async onAck(publicKey, peerStream, messageBytes, message) {
821
+ const seenBefore = await this.modifySeenCache(messageBytes instanceof Uint8Array
822
+ ? messageBytes
823
+ : messageBytes.subarray(), (bytes) => sha256Base64(bytes));
824
+ if (seenBefore > 1) {
825
+ logger.debug("Received message already seen of type: " + message.constructor.name);
852
826
  return false;
853
827
  }
854
- const sender = message.sender?.hashcode();
855
- if (!sender) {
856
- logger.warn("Recieved hello without sender");
828
+ if (!(await message.verify(true))) {
829
+ logger.warn(`Recieved ACK message that did not verify`);
857
830
  return false;
858
831
  }
859
- const peerKey = getPublicKeyFromPeerId(from);
860
- const peerKeyHash = peerKey.hashcode();
861
- if (message.early) {
862
- this.earlyGoodbyes.set(peerKeyHash, message);
832
+ const messageIdString = toBase64(message.messageIdToAcknowledge);
833
+ const myIndex = message.header.mode.trace.findIndex((x) => x === this.publicKeyHash);
834
+ const next = message.header.mode.trace[myIndex - 1];
835
+ const nextStream = next ? this.peers.get(next) : undefined;
836
+ this._ackCallbacks
837
+ .get(messageIdString)
838
+ ?.callback(message, peerStream, nextStream);
839
+ // relay ACK ?
840
+ // send exactly backwards same route we got this message
841
+ if (nextStream) {
842
+ await this.publishMessage(this.publicKey, message, [nextStream], true);
863
843
  }
864
844
  else {
865
- const signatures = message.signatures;
866
- /* TODO Should we update routes on goodbye?
867
- for (let i = 1; i < signatures.signatures.length - 1; i++) {
868
- this.addRouteConnection(
869
- signatures.signatures[i].publicKey,
870
- signatures.signatures[i + 1].publicKey
871
- );
845
+ // if origin exist (we can connect to remote peer) && we have autodialer turned on
846
+ if (message.header.origin && this.connectionManagerOptions.dialer) {
847
+ this.maybeConnectDirectly(message.header.signatures.publicKeys[0].hashcode(), message.header.origin);
872
848
  }
873
- */
874
- //let neighbour = message.trace[1] || this.peerIdStr;
875
- this.removeRouteConnection(signatures.signatures[0].publicKey, signatures.signatures[1].publicKey || this.publicKey);
876
- const relayToPeers = [];
877
- for (const stream of this.peers.values()) {
878
- if (stream.peerId.equals(from)) {
879
- continue;
880
- }
881
- relayToPeers.push(stream);
882
- }
883
- 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)
884
- const hellos = this.helloMap.get(sender);
885
- if (hellos) {
886
- const helloSignaturHash = await message.signatures.hashPublicKeys();
887
- hellos.delete(helloSignaturHash);
888
- }
889
- // Forward
890
- await this.relayMessage(from, message);
891
849
  }
892
- return true;
893
850
  }
894
- async onPing(from, peerStream, message) {
895
- if (message instanceof Ping) {
896
- // respond with pong
897
- await this.publishMessage(this.components.peerId, new Pong(message.pingBytes), [peerStream]);
898
- }
899
- else if (message instanceof Pong) {
900
- // Let the (waiting) thread know that we have received the pong
901
- peerStream.pingJob?.resolve();
851
+ async onGoodBye(publicKey, peerStream, messageBytes, message) {
852
+ const seenBefore = await this.modifySeenCache(messageBytes instanceof Uint8Array
853
+ ? messageBytes
854
+ : messageBytes.subarray());
855
+ if (seenBefore > 0) {
856
+ logger.debug("Received message already seen of type: " + message.constructor.name);
857
+ return;
902
858
  }
903
- else {
904
- throw new Error("Unsupported");
859
+ if (!(await message.verify(true))) {
860
+ logger.warn(`Recieved ACK message that did not verify`);
861
+ return false;
905
862
  }
906
- }
907
- async ping(stream) {
908
- return new Promise((resolve, reject) => {
909
- stream.pingJob?.abort();
910
- const ping = new Ping();
911
- const start = +new Date();
912
- const timeout = setTimeout(() => {
913
- reject(new TimeoutError("Ping timed out"));
914
- }, 10000);
915
- const resolver = () => {
916
- const end = +new Date();
917
- clearTimeout(timeout);
918
- // TODO what happens if a peer send a ping back then leaves? Any problems?
919
- const latency = end - start;
920
- stream.pingLatency = latency;
921
- this.addRouteConnection(this.publicKey, stream.publicKey, latency);
922
- resolve(undefined);
923
- };
924
- stream.pingJob = {
925
- resolve: resolver,
926
- abort: () => {
927
- clearTimeout(timeout);
928
- resolve(undefined);
863
+ const filteredLeaving = message.leaving.filter((x) => this.routes.hasTarget(x));
864
+ if (filteredLeaving.length > 0) {
865
+ this.publish(new Uint8Array(0), {
866
+ mode: new SeekDelivery({
867
+ to: filteredLeaving,
868
+ redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY
869
+ })
870
+ }).catch((e) => {
871
+ if (e instanceof TimeoutError || e instanceof AbortError) {
872
+ // peer left or closed
929
873
  }
930
- };
931
- this.publishMessage(this.components.peerId, ping, [stream]).catch((err) => {
932
- clearTimeout(timeout);
933
- reject(err);
934
- });
935
- });
936
- }
937
- /**
938
- * Whether to accept a message from a peer
939
- * Override to create a graylist
940
- */
941
- acceptFrom(id) {
942
- return true;
874
+ else {
875
+ throw e;
876
+ }
877
+ }); // this will remove the target if it is still not reable
878
+ }
879
+ for (const target of message.leaving) {
880
+ // relay message to every one who previously talked to this peer
881
+ const dependent = this.routes.getDependent(target);
882
+ message.header.mode.to = [...message.header.mode.to, ...dependent];
883
+ message.header.mode.to = message.header.mode.to.filter((x) => x !== this.publicKeyHash);
884
+ if (message.header.mode.to.length > 0) {
885
+ await this.publishMessage(publicKey, message, undefined, true);
886
+ }
887
+ }
943
888
  }
944
889
  async createMessage(data, options) {
945
890
  // dispatch the event if we are interested
946
- let toHashes;
947
- if (options?.to) {
948
- if (options.to instanceof Set) {
949
- toHashes = new Array(options.to.size);
950
- }
951
- else {
952
- toHashes = new Array(options.to.length);
953
- }
954
- let i = 0;
955
- for (const to of options.to) {
956
- toHashes[i++] =
957
- to instanceof PublicSignKey
958
- ? to.hashcode()
959
- : typeof to === "string"
960
- ? to
961
- : getPublicKeyFromPeerId(to).hashcode();
891
+ let mode = options.mode
892
+ ? options.mode
893
+ : new SilentDelivery({
894
+ to: options.to,
895
+ redundancy: DEFAULT_SILENT_MESSAGE_REDUDANCY
896
+ });
897
+ if (mode instanceof AcknowledgeDelivery ||
898
+ mode instanceof SilentDelivery ||
899
+ mode instanceof SeekDelivery) {
900
+ if (mode.to?.find((x) => x === this.publicKeyHash)) {
901
+ mode.to = mode.to.filter((x) => x !== this.publicKeyHash);
962
902
  }
963
903
  }
964
- else {
965
- toHashes = [];
904
+ if (mode instanceof AcknowledgeDelivery || mode instanceof SilentDelivery) {
905
+ const now = +new Date();
906
+ for (const hash of mode.to) {
907
+ const neighbourRoutes = this.routes.routes
908
+ .get(this.publicKeyHash)
909
+ ?.get(hash);
910
+ if (!neighbourRoutes ||
911
+ now - neighbourRoutes.session >
912
+ neighbourRoutes.list.length * this.routeSeekInterval) {
913
+ mode = new SeekDelivery({
914
+ to: mode.to,
915
+ redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY
916
+ });
917
+ break;
918
+ }
919
+ }
966
920
  }
967
921
  const message = new DataMessage({
968
922
  data: data instanceof Uint8ArrayList ? data.subarray() : data,
969
- to: toHashes
923
+ header: new MessageHeader({ mode })
970
924
  });
971
- if (this.signaturePolicy === "StictSign") {
925
+ if (this.signaturePolicy === "StictSign" ||
926
+ mode instanceof SeekDelivery ||
927
+ mode instanceof AcknowledgeDelivery) {
972
928
  await message.sign(this.sign);
973
929
  }
974
930
  return message;
@@ -976,122 +932,195 @@ export class DirectStream extends EventEmitter {
976
932
  /**
977
933
  * Publishes messages to all peers
978
934
  */
979
- async publish(data, options) {
935
+ async publish(data, options = {
936
+ mode: new SeekDelivery({ redundancy: DEFAULT_SEEK_MESSAGE_REDUDANCY })
937
+ }) {
980
938
  if (!this.started) {
981
939
  throw new Error("Not started");
982
940
  }
983
941
  const message = await this.createMessage(data, options);
984
- if (this.emitSelf) {
985
- super.dispatchEvent(new CustomEvent("data", {
986
- detail: message
987
- }));
988
- }
989
- // send to all the other peers
990
- await this.publishMessage(this.components.peerId, message, undefined);
942
+ await this.publishMessage(this.publicKey, message, undefined);
991
943
  return message.id;
992
944
  }
993
- async hello(data) {
994
- if (!this.started) {
995
- return;
996
- }
997
- // send to all the other peers
998
- await this.publishMessage(this.components.peerId, await new Hello({
999
- multiaddrs: this.components.addressManager
1000
- .getAddresses()
1001
- .map((x) => x.toString()),
1002
- data
1003
- }).sign(this.sign.bind(this)));
1004
- }
1005
945
  async relayMessage(from, message, to) {
1006
946
  if (this.canRelayMessage) {
947
+ if (message instanceof DataMessage) {
948
+ if (message.header.mode instanceof AcknowledgeDelivery ||
949
+ message.header.mode instanceof SeekDelivery) {
950
+ await message.sign(this.sign);
951
+ }
952
+ }
1007
953
  return this.publishMessage(from, message, to, true);
1008
954
  }
1009
955
  else {
1010
- logger.debug("received message we didn't subscribe to. Dropping.");
956
+ logger.debug("Received a message to relay but canRelayMessage is false");
1011
957
  }
1012
958
  }
1013
- async publishMessage(from, message, to, relayed) {
1014
- if (message instanceof DataMessage && !to) {
1015
- // message.to can be distant peers, but "to" are neighbours
1016
- const fanoutMap = new Map();
1017
- // Message to > 0
1018
- if (message.to.length > 0) {
1019
- const missingPathsFor = [];
1020
- for (const to of message.to) {
1021
- const fromKey = this.peerIdToPublicKey
1022
- .get(from.toString())
1023
- ?.hashcode();
1024
- if (to === this.publicKeyHash || fromKey === to) {
1025
- continue; // don't send to me or backwards
1026
- }
1027
- const directStream = this.peers.get(to);
1028
- if (directStream) {
1029
- // always favor direct stream, even path seems longer
1030
- const fanout = fanoutMap.get(to);
1031
- if (!fanout) {
1032
- fanoutMap.set(to, [to]);
1033
- }
1034
- else {
1035
- fanout.push(to);
1036
- }
1037
- continue;
1038
- }
1039
- else {
1040
- const fromMe = from.equals(this.components.peerId);
1041
- const block = !fromMe ? fromKey : undefined;
1042
- const path = this.routes.getPath(this.publicKeyHash, to, {
1043
- block // prevent send message backwards
1044
- });
1045
- if (path && path.length > 0) {
1046
- const fanout = fanoutMap.get(path[1]);
1047
- if (!fanout) {
1048
- fanoutMap.set(path[1], [to]);
1049
- if (this.connectionManagerOptions.autoDial &&
1050
- path.length >= 3) {
1051
- // Dont await this even if it is async since this method can fail
1052
- // and might take some time to run
1053
- this.maybeConnectDirectly(path).catch((e) => {
1054
- logger.error("Failed to request direct connection: " + e.message);
1055
- });
1056
- }
1057
- continue;
1058
- }
1059
- else {
1060
- fanout.push(to);
1061
- continue;
959
+ async createDeliveryPromise(from, message, relayed) {
960
+ if (message.header.mode instanceof AnyWhere) {
961
+ return { promise: Promise.resolve() };
962
+ }
963
+ const idString = toBase64(message.id);
964
+ const existing = this._ackCallbacks.get(idString);
965
+ if (existing) {
966
+ return { promise: existing.promise };
967
+ }
968
+ const deliveryDeferredPromise = pDefer();
969
+ const fastestNodesReached = new Map();
970
+ const messageToSet = new Set();
971
+ if (message.header.mode.to) {
972
+ for (const to of message.header.mode.to) {
973
+ if (to === from.hashcode()) {
974
+ continue;
975
+ }
976
+ messageToSet.add(to);
977
+ if (!relayed && !this.healthChecks.has(to)) {
978
+ this.healthChecks.set(to, setTimeout(() => {
979
+ this.removeRouteConnection(to, false);
980
+ }, this.seekTimeout + 5000));
981
+ }
982
+ }
983
+ }
984
+ if (messageToSet.size === 0) {
985
+ deliveryDeferredPromise.resolve(); // we dont know how many answer to expect, just resolve immediately
986
+ }
987
+ const willGetAllAcknowledgements = !relayed; // Only the origin will get all acks
988
+ // Expected to receive at least 'filterMessageForSeenCounter' acknowledgements from each peer
989
+ const filterMessageForSeenCounter = relayed
990
+ ? undefined
991
+ : message.header.mode instanceof SeekDelivery
992
+ ? Math.min(this.peers.size, message.header.mode.redundancy)
993
+ : 1; /* message.deliveryMode instanceof SeekDelivery ? Math.min(this.peers.size - (relayed ? 1 : 0), message.deliveryMode.redundancy) : 1 */
994
+ const finalize = () => {
995
+ this._ackCallbacks.delete(idString);
996
+ if (message.header.mode instanceof SeekDelivery) {
997
+ this.routes.commitPendingRouteConnection(session);
998
+ }
999
+ };
1000
+ const uniqueAcks = new Set();
1001
+ const session = +new Date();
1002
+ const timeout = setTimeout(async () => {
1003
+ let hasAll = true;
1004
+ finalize();
1005
+ // peer not reachable (?)!
1006
+ for (const to of messageToSet) {
1007
+ let foundNode = false;
1008
+ if (fastestNodesReached.has(to)) {
1009
+ foundNode = true;
1010
+ break;
1011
+ }
1012
+ if (!foundNode && !relayed) {
1013
+ hasAll = false;
1014
+ }
1015
+ }
1016
+ if (!hasAll && willGetAllAcknowledgements) {
1017
+ deliveryDeferredPromise.reject(new TimeoutError(`${this.publicKeyHash} Failed to get message ${idString} ${filterMessageForSeenCounter} ${[
1018
+ ...messageToSet
1019
+ ]} delivery acknowledges from all nodes (${fastestNodesReached.size}/${messageToSet.size}). Mode: ${message.header.mode.constructor.name}`));
1020
+ }
1021
+ else {
1022
+ deliveryDeferredPromise.resolve();
1023
+ }
1024
+ }, this.seekTimeout);
1025
+ this._ackCallbacks.set(idString, {
1026
+ promise: deliveryDeferredPromise.promise,
1027
+ callback: (ack, messageThrough, messageFrom) => {
1028
+ const messageTarget = ack.header.signatures.publicKeys[0];
1029
+ const messageTargetHash = messageTarget.hashcode();
1030
+ const seenCounter = ack.seenCounter;
1031
+ // remove the automatic removal of route timeout since we have observed lifesigns of a peer
1032
+ const timer = this.healthChecks.get(messageTargetHash);
1033
+ clearTimeout(timer);
1034
+ this.healthChecks.delete(messageTargetHash);
1035
+ // if the target is not inside the original message to, we still ad the target to our routes
1036
+ // this because a relay might modify the 'to' list and we might receive more answers than initially set
1037
+ if (message.header.mode instanceof SeekDelivery) {
1038
+ this.addRouteConnection(messageFrom?.publicKey.hashcode() || this.publicKeyHash, messageThrough.publicKey.hashcode(), messageTarget, seenCounter, session, true); // we assume the seenCounter = distance. The more the message has been seen by the target the longer the path is to the target
1039
+ }
1040
+ if (messageToSet.has(messageTargetHash)) {
1041
+ // Only keep track of relevant acks
1042
+ if (filterMessageForSeenCounter == null ||
1043
+ seenCounter < filterMessageForSeenCounter) {
1044
+ // TODO set limit correctly
1045
+ if (seenCounter < MAX_ROUTE_DISTANCE) {
1046
+ let arr = fastestNodesReached.get(messageTargetHash);
1047
+ if (!arr) {
1048
+ arr = [];
1049
+ fastestNodesReached.set(messageTargetHash, arr);
1062
1050
  }
1063
- }
1064
- else {
1065
- missingPathsFor.push(to);
1051
+ arr.push(seenCounter);
1052
+ uniqueAcks.add(messageTargetHash + seenCounter);
1066
1053
  }
1067
1054
  }
1068
- // we can't find path, send message to all peers
1069
- fanoutMap.clear();
1070
- break;
1071
1055
  }
1072
- // update to's
1073
- let sentOnce = false;
1074
- if (missingPathsFor.length === 0) {
1075
- if (fanoutMap.size > 0) {
1076
- for (const [neighbour, distantPeers] of fanoutMap) {
1077
- message.to = distantPeers;
1078
- const bytes = message.bytes();
1079
- if (!sentOnce) {
1080
- // if relayed = true, we have already added it to seenCache
1081
- if (!relayed) {
1082
- this.seenCache.add(await getMsgId(bytes));
1083
- }
1084
- sentOnce = true;
1085
- }
1056
+ // This if clause should never enter for relayed connections, since we don't
1057
+ // know how many ACKs we will get
1058
+ if (filterMessageForSeenCounter != null &&
1059
+ uniqueAcks.size >= messageToSet.size * filterMessageForSeenCounter) {
1060
+ if (messageToSet.size > 0) {
1061
+ // this statement exist beacuse if we do SEEK and have to = [], then it means we try to reach as many as possible hence we never want to delete this ACK callback
1062
+ // only remove callback function if we actually expected a expected amount of responses
1063
+ clearTimeout(timeout);
1064
+ finalize();
1065
+ }
1066
+ deliveryDeferredPromise.resolve();
1067
+ }
1068
+ },
1069
+ timeout
1070
+ });
1071
+ return deliveryDeferredPromise;
1072
+ }
1073
+ async publishMessage(from, message, to, relayed) {
1074
+ let delivereyPromise = undefined;
1075
+ if ((!message.header.signatures ||
1076
+ message.header.signatures.publicKeys.length === 0) &&
1077
+ message instanceof DataMessage &&
1078
+ message.header.mode instanceof SilentDelivery === false) {
1079
+ throw new Error("Missing signature");
1080
+ }
1081
+ /**
1082
+ * Logic for handling acknowledge messages when we receive them (later)
1083
+ */
1084
+ if (message instanceof DataMessage &&
1085
+ message.header.mode instanceof SeekDelivery &&
1086
+ !relayed) {
1087
+ to = this.peers; // seek delivery will not work unless we try all possible paths
1088
+ }
1089
+ if (message instanceof AcknowledgeDelivery) {
1090
+ to = undefined;
1091
+ }
1092
+ if (message instanceof DataMessage &&
1093
+ (message.header.mode instanceof SeekDelivery ||
1094
+ message.header.mode instanceof AcknowledgeDelivery)) {
1095
+ delivereyPromise = (await this.createDeliveryPromise(from, message, relayed)).promise;
1096
+ }
1097
+ const bytes = message.bytes();
1098
+ if (!relayed) {
1099
+ const bytesArray = bytes instanceof Uint8Array ? bytes : bytes.subarray();
1100
+ await this.modifySeenCache(bytesArray);
1101
+ }
1102
+ /**
1103
+ * For non SEEKing message delivery modes, use routing
1104
+ */
1105
+ if (message instanceof DataMessage) {
1106
+ if ((message.header.mode instanceof AcknowledgeDelivery ||
1107
+ message.header.mode instanceof SilentDelivery) &&
1108
+ !to &&
1109
+ message.header.mode.to) {
1110
+ const fanout = this.routes.getFanout(from, message.header.mode.to, message.header.mode.redundancy);
1111
+ if (fanout) {
1112
+ if (fanout.size > 0) {
1113
+ for (const [neighbour, _distantPeers] of fanout) {
1086
1114
  const stream = this.peers.get(neighbour);
1087
1115
  stream?.waitForWrite(bytes).catch((e) => {
1088
1116
  logger.error("Failed to publish message: " + e.message);
1089
1117
  });
1090
1118
  }
1091
- return; // we are done sending the message in all direction with updates 'to' lists
1119
+ return delivereyPromise; // we are done sending the message in all direction with updates 'to' lists
1092
1120
  }
1093
- return;
1121
+ return; // we defintely that we should not forward the message anywhere
1094
1122
  }
1123
+ return;
1095
1124
  // else send to all (fallthrough to code below)
1096
1125
  }
1097
1126
  }
@@ -1100,108 +1129,64 @@ export class DirectStream extends EventEmitter {
1100
1129
  if (peers == null ||
1101
1130
  (Array.isArray(peers) && peers.length === 0) ||
1102
1131
  (peers instanceof Map && peers.size === 0)) {
1103
- logger.debug("no peers are subscribed");
1132
+ logger.debug("No peers to send to");
1104
1133
  return;
1105
1134
  }
1106
- const bytes = message.bytes();
1107
1135
  let sentOnce = false;
1108
1136
  for (const stream of peers.values()) {
1109
1137
  const id = stream;
1110
- if (id.peerId.equals(from)) {
1138
+ if (id.publicKey.equals(from)) {
1111
1139
  continue;
1112
1140
  }
1113
- if (!sentOnce) {
1114
- sentOnce = true;
1115
- if (!relayed) {
1116
- // if relayed = true, we have already added it to seenCache
1117
- const msgId = await getMsgId(bytes);
1118
- this.seenCache.add(msgId);
1119
- }
1120
- }
1141
+ sentOnce = true;
1121
1142
  id.waitForWrite(bytes).catch((e) => {
1122
1143
  logger.error("Failed to publish message: " + e.message);
1123
1144
  });
1124
1145
  }
1125
- if (!sentOnce && !relayed) {
1126
- throw new Error("Message did not have any valid receivers. ");
1146
+ if (!sentOnce) {
1147
+ if (!relayed) {
1148
+ throw new Error("Message did not have any valid receivers");
1149
+ }
1127
1150
  }
1151
+ return delivereyPromise;
1128
1152
  }
1129
- async maybeConnectDirectly(path) {
1130
- if (path.length < 3) {
1131
- return;
1132
- }
1133
- const toHash = path[path.length - 1];
1134
- if (this.peers.has(toHash)) {
1153
+ async maybeConnectDirectly(toHash, origin) {
1154
+ if (this.peers.has(toHash) || this.prunedConnectionsCache?.has(toHash)) {
1135
1155
  return; // TODO, is this expected, or are we to dial more addresses?
1136
1156
  }
1137
- // Try to either connect directly
1138
- if (!this.recentDials.has(toHash)) {
1139
- this.recentDials.add(toHash);
1140
- const addrs = this.multiaddrsMap.get(toHash);
1157
+ const addresses = origin.multiaddrs
1158
+ .filter((x) => {
1159
+ const ret = !this.recentDials.has(x);
1160
+ this.recentDials.add(x);
1161
+ return ret;
1162
+ })
1163
+ .map((x) => multiaddr(x));
1164
+ if (addresses.length > 0) {
1141
1165
  try {
1142
- if (addrs && addrs.length > 0) {
1143
- await this.components.connectionManager.openConnection(addrs.map((x) => multiaddr(x)));
1144
- return;
1145
- }
1166
+ await this.components.connectionManager.openConnection(addresses);
1146
1167
  }
1147
1168
  catch (error) {
1148
- // continue regardless of error
1149
- }
1150
- }
1151
- // Connect through a closer relay that maybe does holepunch for us
1152
- const nextToHash = path[path.length - 2];
1153
- const routeKey = nextToHash + toHash;
1154
- if (!this.recentDials.has(routeKey)) {
1155
- this.recentDials.add(routeKey);
1156
- const to = this.peerKeyHashToPublicKey.get(toHash);
1157
- const toPeerId = await to.toPeerId();
1158
- const addrs = this.multiaddrsMap.get(path[path.length - 2]);
1159
- if (addrs && addrs.length > 0) {
1160
- const addressesToDial = addrs.sort((a, b) => {
1161
- if (a.includes("/wss/")) {
1162
- if (b.includes("/wss/")) {
1163
- return 0;
1164
- }
1165
- return -1;
1166
- }
1167
- if (a.includes("/ws/")) {
1168
- if (b.includes("/ws/")) {
1169
- return 0;
1170
- }
1171
- if (b.includes("/wss/")) {
1172
- return 1;
1173
- }
1174
- return -1;
1175
- }
1176
- return 0;
1177
- });
1178
- for (const addr of addressesToDial) {
1179
- const circuitAddress = multiaddr(addr + "/p2p-circuit/webrtc/p2p/" + toPeerId.toString());
1180
- try {
1181
- await this.components.connectionManager.openConnection(circuitAddress);
1182
- return;
1183
- }
1184
- catch (error) {
1185
- logger.error("Failed to connect directly to: " +
1186
- circuitAddress.toString() +
1187
- ". " +
1188
- error?.message);
1189
- }
1190
- }
1169
+ logger.info("Failed to connect directly to: " +
1170
+ JSON.stringify(addresses.map((x) => x.toString())) +
1171
+ ". " +
1172
+ error?.message);
1191
1173
  }
1192
1174
  }
1193
1175
  }
1194
- async waitFor(peer) {
1176
+ async waitFor(peer, options) {
1195
1177
  const hash = (peer instanceof PublicSignKey ? peer : getPublicKeyFromPeerId(peer)).hashcode();
1196
1178
  try {
1197
1179
  await waitFor(() => {
1198
1180
  if (!this.peers.has(hash)) {
1199
1181
  return false;
1200
1182
  }
1201
- if (!this.routes.hasLink(this.publicKeyHash, hash)) {
1183
+ if (!this.routes.isReachable(this.publicKeyHash, hash)) {
1202
1184
  return false;
1203
1185
  }
1204
1186
  return true;
1187
+ }, {
1188
+ signal: options?.signal,
1189
+ timeout: 10 * 1000
1205
1190
  });
1206
1191
  }
1207
1192
  catch (error) {
@@ -1210,11 +1195,15 @@ export class DirectStream extends EventEmitter {
1210
1195
  " does not exist. Connection exist: " +
1211
1196
  this.peers.has(hash) +
1212
1197
  ". Route exist: " +
1213
- this.routes.hasLink(this.publicKeyHash, hash));
1198
+ this.routes.isReachable(this.publicKeyHash, hash));
1214
1199
  }
1215
1200
  const stream = this.peers.get(hash);
1216
1201
  try {
1217
- await waitFor(() => stream.isReadable && stream.isWritable);
1202
+ // Dontwait for readlable https://github.com/libp2p/js-libp2p/issues/2321
1203
+ await waitFor(() => /* stream.isReadable && */ stream.isWritable, {
1204
+ signal: options?.signal,
1205
+ timeout: 10 * 1000
1206
+ });
1218
1207
  }
1219
1208
  catch (error) {
1220
1209
  throw new Error("Stream to " +
@@ -1225,6 +1214,58 @@ export class DirectStream extends EventEmitter {
1225
1214
  stream.isWritable);
1226
1215
  }
1227
1216
  }
1217
+ get pending() {
1218
+ return this._ackCallbacks.size > 0;
1219
+ }
1220
+ lastQueuedBytes = 0;
1221
+ // make this into a job? run every few ms
1222
+ maybePruneConnections() {
1223
+ if (this.connectionManagerOptions.pruner) {
1224
+ if (this.connectionManagerOptions.pruner.bandwidth != null) {
1225
+ let usedBandwidth = 0;
1226
+ for (const [_k, v] of this.peers) {
1227
+ usedBandwidth += v.usedBandwidth;
1228
+ }
1229
+ usedBandwidth /= this.peers.size;
1230
+ if (usedBandwidth > this.connectionManagerOptions.pruner.bandwidth) {
1231
+ // prune
1232
+ return this.pruneConnections();
1233
+ }
1234
+ }
1235
+ else if (this.connectionManagerOptions.pruner.maxBuffer != null) {
1236
+ const queuedBytes = this.getQueuedBytes();
1237
+ if (queuedBytes > this.connectionManagerOptions.pruner.maxBuffer) {
1238
+ // prune
1239
+ return this.pruneConnections();
1240
+ }
1241
+ }
1242
+ }
1243
+ return Promise.resolve();
1244
+ }
1245
+ async pruneConnections() {
1246
+ // TODO sort by bandwidth
1247
+ if (this.peers.size <= this.connectionManagerOptions.minConnections) {
1248
+ return;
1249
+ }
1250
+ const sorted = [...this.peers.values()]
1251
+ .sort((x, y) => x.usedBandwidth - y.usedBandwidth)
1252
+ .map((x) => x.publicKey.hashcode());
1253
+ const prunables = this.routes.getPrunable(sorted);
1254
+ if (prunables.length === 0) {
1255
+ return;
1256
+ }
1257
+ const stream = this.peers.get(prunables[0]);
1258
+ this.prunedConnectionsCache.add(stream.publicKey.hashcode());
1259
+ await this.onPeerDisconnected(stream.peerId);
1260
+ return this.components.connectionManager.closeConnections(stream.peerId);
1261
+ }
1262
+ getQueuedBytes() {
1263
+ let sum = 0;
1264
+ for (const peer of this.peers) {
1265
+ sum += peer[1].outboundStream?.readableLength || 0;
1266
+ }
1267
+ return sum;
1268
+ }
1228
1269
  }
1229
1270
  export const waitForPeers = async (...libs) => {
1230
1271
  for (let i = 0; i < libs.length; i++) {