@peerbit/stream 4.0.10 → 4.1.0-81d59ee

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { TypedEventEmitter } from "@libp2p/interface";
1
+ import {
2
+ MuxerClosedError,
3
+ StreamResetError,
4
+ TypedEventEmitter,
5
+ UnsupportedProtocolError,
6
+ } from "@libp2p/interface";
2
7
  import type {
3
8
  Connection,
4
9
  Libp2pEvents,
@@ -37,6 +42,7 @@ import {
37
42
  MultiAddrinfo,
38
43
  NotStartedError,
39
44
  type PriorityOptions,
45
+ type PublicKeyFromHashResolver,
40
46
  SeekDelivery,
41
47
  SilentDelivery,
42
48
  type StreamEvents,
@@ -47,19 +53,20 @@ import {
47
53
  deliveryModeHasReceiver,
48
54
  getMsgId,
49
55
  } from "@peerbit/stream-interface";
50
- import { AbortError, TimeoutError, delay, waitFor } from "@peerbit/time";
56
+ import { AbortError, TimeoutError, delay } from "@peerbit/time";
51
57
  import { abortableSource } from "abortable-iterator";
52
58
  import * as lp from "it-length-prefixed";
53
59
  import { pipe } from "it-pipe";
54
60
  import { type Pushable, pushable } from "it-pushable";
55
61
  import type { Components } from "libp2p/components";
56
- import pDefer from "p-defer";
62
+ import pDefer, { type DeferredPromise } from "p-defer";
57
63
  import Queue from "p-queue";
58
64
  import { Uint8ArrayList } from "uint8arraylist";
59
65
  import { logger } from "./logger.js";
60
66
  import { type PushableLanes, pushableLanes } from "./pushable-lanes.js";
61
67
  import { MAX_ROUTE_DISTANCE, Routes } from "./routes.js";
62
68
  import { BandwidthTracker } from "./stats.js";
69
+ import { waitForEvent } from "./wait-for-event.js";
63
70
 
64
71
  export { logger };
65
72
 
@@ -88,6 +95,8 @@ export interface PeerStreamEvents {
88
95
  "stream:inbound": CustomEvent<never>;
89
96
  "stream:outbound": CustomEvent<never>;
90
97
  close: CustomEvent<never>;
98
+ "peer:reachable": CustomEvent<PublicSignKey>;
99
+ "peer:unreachable": CustomEvent<PublicSignKey>;
91
100
  }
92
101
 
93
102
  const SEEK_DELIVERY_TIMEOUT = 10e3;
@@ -420,7 +429,7 @@ export abstract class DirectStream<
420
429
  Events extends { [s: string]: any } = StreamEvents,
421
430
  >
422
431
  extends TypedEventEmitter<Events>
423
- implements WaitForPeer
432
+ implements WaitForPeer, PublicKeyFromHashResolver
424
433
  {
425
434
  public peerId: PeerId;
426
435
  public publicKey: PublicSignKey;
@@ -579,12 +588,19 @@ export abstract class DirectStream<
579
588
  this.outboundInflightQueue = pushable({ objectMode: true });
580
589
  pipe(this.outboundInflightQueue, async (source) => {
581
590
  for await (const { peerId, connection } of source) {
591
+ if (this.stopping || this.started === false) {
592
+ return;
593
+ }
582
594
  await this.createOutboundStream(peerId, connection);
583
595
  }
584
596
  }).catch((e) => {
585
597
  logger.error("outbound inflight queue error: " + e?.toString());
586
598
  });
587
599
 
600
+ this.closeController.signal.addEventListener("abort", () => {
601
+ this.outboundInflightQueue.return();
602
+ });
603
+
588
604
  this.routes = new Routes(this.publicKeyHash, {
589
605
  routeMaxRetentionPeriod: this.routeMaxRetentionPeriod,
590
606
  signal: this.closeController.signal,
@@ -776,11 +792,21 @@ export abstract class DirectStream<
776
792
  }
777
793
 
778
794
  try {
779
- stream = await connection.newStream(this.multicodecs);
795
+ stream = await connection.newStream(this.multicodecs, {
796
+ // TODO this property seems necessary, together with waitFor isReadable when making sure two peers are conencted before talking.
797
+ // research whether we can do without this so we can push data without beeing able to send
798
+ negotiateFully: true,
799
+ });
780
800
  if (stream.protocol == null) {
781
801
  stream.abort(new Error("Stream was not multiplexed"));
782
802
  return;
783
803
  }
804
+
805
+ if (!this.started) {
806
+ // we closed before we could create the stream
807
+ stream.abort(new Error("Closed"));
808
+ return;
809
+ }
784
810
  peer = this.addPeer(peerId, peerKey, stream.protocol!, connection.id); // TODO types
785
811
  await peer.attachOutboundStream(stream);
786
812
  } catch (error: any) {
@@ -789,10 +815,17 @@ export abstract class DirectStream<
789
815
  continue; // Retry
790
816
  }
791
817
 
818
+ if (error instanceof UnsupportedProtocolError) {
819
+ await delay(100);
820
+ continue; // Retry
821
+ }
822
+
792
823
  if (
793
824
  connection.status !== "open" ||
794
825
  error?.message === "Muxer already closed" ||
795
- error.code === "ERR_STREAM_RESET"
826
+ error.code === "ERR_STREAM_RESET" ||
827
+ error instanceof StreamResetError ||
828
+ error instanceof MuxerClosedError
796
829
  ) {
797
830
  return; // fail silenty
798
831
  }
@@ -1896,7 +1929,6 @@ export abstract class DirectStream<
1896
1929
  }
1897
1930
 
1898
1931
  sentOnce = true;
1899
-
1900
1932
  promises.push(id.waitForWrite(bytes, message.header.priority));
1901
1933
  }
1902
1934
  await Promise.all(promises);
@@ -1938,30 +1970,37 @@ export abstract class DirectStream<
1938
1970
  }
1939
1971
 
1940
1972
  async waitFor(
1941
- peer: PeerId | PublicSignKey,
1973
+ peer: PeerId | PublicSignKey | string,
1942
1974
  options?: { timeout?: number; signal?: AbortSignal; neighbour?: boolean },
1943
1975
  ) {
1944
- const hash = (
1945
- peer instanceof PublicSignKey ? peer : getPublicKeyFromPeerId(peer)
1946
- ).hashcode();
1947
- try {
1948
- await waitFor(
1949
- () => {
1950
- if (options?.neighbour && !this.peers.has(hash)) {
1951
- return false;
1952
- }
1976
+ const hash =
1977
+ typeof peer === "string"
1978
+ ? peer
1979
+ : (peer instanceof PublicSignKey
1980
+ ? peer
1981
+ : getPublicKeyFromPeerId(peer)
1982
+ ).hashcode();
1983
+ const checkIsReachable = (deferred: DeferredPromise<void>) => {
1984
+ if (options?.neighbour && !this.peers.has(hash)) {
1985
+ return;
1986
+ }
1953
1987
 
1954
- if (!this.routes.isReachable(this.publicKeyHash, hash, 0)) {
1955
- return false;
1956
- }
1988
+ if (!this.routes.isReachable(this.publicKeyHash, hash, 0)) {
1989
+ return;
1990
+ }
1957
1991
 
1958
- return true;
1959
- },
1960
- {
1961
- signal: options?.signal,
1962
- timeout: options?.timeout ?? 10 * 1000,
1963
- },
1964
- );
1992
+ deferred.resolve();
1993
+ };
1994
+ const abortSignals = [this.closeController.signal];
1995
+ if (options?.signal) {
1996
+ abortSignals.push(options.signal);
1997
+ }
1998
+
1999
+ try {
2000
+ await waitForEvent(this, ["peer:reachable"], checkIsReachable, {
2001
+ signals: abortSignals,
2002
+ timeout: options?.timeout,
2003
+ });
1965
2004
  } catch (error) {
1966
2005
  throw new Error(
1967
2006
  "Stream to " +
@@ -1974,14 +2013,25 @@ export abstract class DirectStream<
1974
2013
  this.routes.isReachable(this.publicKeyHash, hash, 0),
1975
2014
  );
1976
2015
  }
2016
+
1977
2017
  if (options?.neighbour) {
1978
2018
  const stream = this.peers.get(hash)!;
1979
2019
  try {
1980
- // Dontwait for readlable https://github.com/libp2p/js-libp2p/issues/2321
1981
- await waitFor(() => /* stream.isReadable && */ stream.isWritable, {
1982
- signal: options?.signal,
1983
- timeout: options?.timeout ?? 10 * 1000,
1984
- });
2020
+ let checkIsWritable = (pDefer: DeferredPromise<void>) => {
2021
+ // Dont wait for readlable https://github.com/libp2p/js-libp2p/issues/2321
2022
+ if (stream.isReadable && stream.isWritable) {
2023
+ pDefer.resolve();
2024
+ }
2025
+ };
2026
+ await waitForEvent(
2027
+ stream,
2028
+ ["stream:outbound", "stream:inbound"],
2029
+ checkIsWritable,
2030
+ {
2031
+ signals: abortSignals,
2032
+ timeout: options?.timeout,
2033
+ },
2034
+ );
1985
2035
  } catch (error) {
1986
2036
  throw new Error(
1987
2037
  "Stream to " +
@@ -1995,6 +2045,10 @@ export abstract class DirectStream<
1995
2045
  }
1996
2046
  }
1997
2047
 
2048
+ getPublicKey(hash: string): PublicSignKey | undefined {
2049
+ return this.peerKeyHashToPublicKey.get(hash);
2050
+ }
2051
+
1998
2052
  get pending(): boolean {
1999
2053
  return this._ackCallbacks.size > 0;
2000
2054
  }
@@ -2055,7 +2109,7 @@ export abstract class DirectStream<
2055
2109
  }
2056
2110
  }
2057
2111
 
2058
- export const waitForPeers = async (
2112
+ export const waitForReachable = async (
2059
2113
  ...libs: {
2060
2114
  waitFor: (peer: PeerId | PublicSignKey) => Promise<void>;
2061
2115
  peerId: PeerId;
@@ -2067,6 +2121,27 @@ export const waitForPeers = async (
2067
2121
  continue;
2068
2122
  }
2069
2123
  await libs[i].waitFor(libs[j].peerId);
2124
+ await libs[j].waitFor(libs[i].peerId);
2125
+ }
2126
+ }
2127
+ };
2128
+
2129
+ export const waitForNeighbour = async (
2130
+ ...libs: {
2131
+ waitFor: (
2132
+ peer: PeerId | PublicSignKey,
2133
+ options?: { neighbour?: boolean },
2134
+ ) => Promise<void>;
2135
+ peerId: PeerId;
2136
+ }[]
2137
+ ) => {
2138
+ for (let i = 0; i < libs.length; i++) {
2139
+ for (let j = 0; j < libs.length; j++) {
2140
+ if (i === j) {
2141
+ continue;
2142
+ }
2143
+ await libs[i].waitFor(libs[j].peerId, { neighbour: true });
2144
+ await libs[j].waitFor(libs[i].peerId, { neighbour: true });
2070
2145
  }
2071
2146
  }
2072
2147
  };
package/src/routes.ts CHANGED
@@ -83,13 +83,17 @@ export class Routes {
83
83
  let prev = fromMap.get(target);
84
84
  const routeDidExist = prev;
85
85
  const isNewSession = !prev || session > prev.session;
86
+ const isOldSession = prev && session < prev.session;
87
+
86
88
  if (!prev) {
87
89
  prev = { session, remoteSession, list: [] as RelayInfo[] };
88
90
  fromMap.set(target, prev);
89
91
  }
90
92
 
91
- if (neighbour === target) {
92
- if (from === this.me) {
93
+ const isRelayed = from !== this.me;
94
+ const targetIsNeighbour = neighbour === target;
95
+ if (targetIsNeighbour) {
96
+ if (!isRelayed) {
93
97
  // force distance to neighbour as targets to always favor directly sending to them
94
98
  // i.e. if target is our neighbour, always assume the shortest path to them is the direct path
95
99
  distance = -1;
@@ -105,6 +109,20 @@ export class Routes {
105
109
 
106
110
  prev.session = Math.max(session, prev.session);
107
111
 
112
+ const scheduleCleanup = () => {
113
+ return delay(this.routeMaxRetentionPeriod + 100, { signal: this.signal })
114
+ .then(() => {
115
+ this.cleanup(from, target);
116
+ })
117
+ .catch((e) => {
118
+ if (e instanceof AbortError) {
119
+ // skip
120
+ return;
121
+ }
122
+ throw e;
123
+ });
124
+ };
125
+
108
126
  // Update routes and cleanup all old routes that are older than latest session - some threshold
109
127
  if (isNewSession) {
110
128
  // Mark previous routes as old
@@ -120,18 +138,10 @@ export class Routes {
120
138
 
121
139
  // Initiate cleanup
122
140
  if (distance !== -1 && foundNodeToExpire) {
123
- delay(this.routeMaxRetentionPeriod + 100, { signal: this.signal })
124
- .then(() => {
125
- this.cleanup(from, target);
126
- })
127
- .catch((e) => {
128
- if (e instanceof AbortError) {
129
- // skip
130
- return;
131
- }
132
- throw e;
133
- });
141
+ scheduleCleanup();
134
142
  }
143
+ } else if (isOldSession) {
144
+ scheduleCleanup();
135
145
  }
136
146
 
137
147
  // Modify list for new/update route
@@ -139,16 +149,18 @@ export class Routes {
139
149
  for (const route of prev.list) {
140
150
  if (route.hash === neighbour) {
141
151
  // if route is faster or just as fast, update existing route
142
- if (route.distance > distance) {
143
- route.distance = distance;
144
- route.session = session;
145
- route.expireAt = undefined; // remove expiry since we updated
146
- prev.list.sort((a, b) => a.distance - b.distance);
147
- return isNewRemoteSession ? "restart" : "updated";
148
- } else if (route.distance === distance) {
149
- route.session = session;
150
- route.expireAt = undefined; // remove expiry since we updated
151
- return isNewRemoteSession ? "restart" : "updated";
152
+ if (isNewSession) {
153
+ if (route.distance > distance) {
154
+ route.distance = distance;
155
+ route.session = session;
156
+ route.expireAt = undefined; // remove expiry since we updated
157
+ prev.list.sort((a, b) => a.distance - b.distance);
158
+ return isNewRemoteSession ? "restart" : "updated";
159
+ } else if (route.distance === distance) {
160
+ route.session = session;
161
+ route.expireAt = undefined; // remove expiry since we updated
162
+ return isNewRemoteSession ? "restart" : "updated";
163
+ }
152
164
  }
153
165
 
154
166
  exist = true;
@@ -157,8 +169,20 @@ export class Routes {
157
169
  }
158
170
  }
159
171
 
160
- prev.list.push({ distance, session, hash: neighbour });
161
- prev.list.sort((a, b) => a.distance - b.distance);
172
+ // if not exist add new route
173
+ // else if it exist then we only end up here if the distance is longer than prev, this means that we want to keep prev while adding the new route
174
+ if (!exist || isNewSession) {
175
+ prev.list.push({
176
+ distance,
177
+ session,
178
+ hash: neighbour,
179
+ expireAt: isOldSession
180
+ ? +new Date() + this.routeMaxRetentionPeriod
181
+ : undefined,
182
+ });
183
+ prev.list.sort((a, b) => a.distance - b.distance);
184
+ }
185
+
162
186
  return exist ? (isNewRemoteSession ? "restart" : "updated") : "new";
163
187
  }
164
188
 
@@ -371,10 +395,11 @@ export class Routes {
371
395
  continue; // don't send to me or backwards
372
396
  }
373
397
 
398
+ // neighbours that are links from 'from' to 'to'
374
399
  const neighbour = this.findNeighbor(from, to);
375
400
  if (neighbour) {
376
401
  let foundClosest = false;
377
- let redundancyModified = redundancy;
402
+ let added = 0;
378
403
  for (let i = 0; i < neighbour.list.length; i++) {
379
404
  const { distance, session, expireAt } = neighbour.list[i];
380
405
 
@@ -384,7 +409,7 @@ export class Routes {
384
409
  continue;
385
410
  }
386
411
 
387
- if (distance >= redundancyModified) {
412
+ if (distance >= redundancy) {
388
413
  break; // because neighbour listis sorted
389
414
  }
390
415
 
@@ -403,12 +428,15 @@ export class Routes {
403
428
  session <= neighbour.session // (<) will never be the case, but we do add routes in the tests with later session timestamps
404
429
  ) {
405
430
  foundClosest = true;
406
-
407
431
  if (distance === -1) {
408
- // remove 1 from the expected redunancy since we got a route with negative 1 distance
409
- // if we do not do this, we would get 2 routes if redundancy = 1, {-1, 0}, while it should just be
410
- // {-1} in this case
411
- redundancyModified -= 1;
432
+ break; // dont send to more peers if we have the direct route
433
+ }
434
+ }
435
+
436
+ if (!expireAt) {
437
+ // only count non-expired routes or if we are relaying then also count expired routes
438
+ added++;
439
+ if (added >= redundancy) {
412
440
  break;
413
441
  }
414
442
  }
@@ -0,0 +1,51 @@
1
+ import type { TypedEventEmitter } from "@libp2p/interface";
2
+ import pDefer, { type DeferredPromise } from "p-defer";
3
+
4
+ export function waitForEvent<
5
+ Emitter extends TypedEventEmitter<Events>,
6
+ Events extends Record<string, any> = Emitter extends TypedEventEmitter<
7
+ infer E
8
+ >
9
+ ? E
10
+ : never,
11
+ Event extends keyof Events = keyof Events,
12
+ >(
13
+ emitter: Emitter,
14
+ events: Event[],
15
+ resolver: (deferred: DeferredPromise<void>) => void | Promise<void>,
16
+ options?: {
17
+ signals?: AbortSignal[];
18
+ timeout?: number;
19
+ },
20
+ ): Promise<void> {
21
+ const deferred = pDefer<void>();
22
+ const abortFn = () =>
23
+ deferred.reject(
24
+ new Error(
25
+ "Aborted waiting for event: " +
26
+ String(events.length > 1 ? events.join(", ") : events[0]),
27
+ ),
28
+ );
29
+
30
+ const checkIsReady = (...args: any[]) => resolver(deferred);
31
+
32
+ deferred.promise.finally(() => {
33
+ for (const event of events) {
34
+ emitter.removeEventListener(event as any, checkIsReady);
35
+ }
36
+ clearTimeout(timeout);
37
+ options?.signals?.forEach((signal) =>
38
+ signal.removeEventListener("abort", abortFn),
39
+ );
40
+ });
41
+
42
+ for (const event of events) {
43
+ emitter.addEventListener(event as any, checkIsReady);
44
+ }
45
+ let timeout = setTimeout(abortFn, options?.timeout ?? 10 * 1000);
46
+ options?.signals?.forEach((signal) =>
47
+ signal.addEventListener("abort", abortFn),
48
+ );
49
+ (resolver as any)(deferred);
50
+ return deferred.promise;
51
+ }