@waku/core 0.0.22 → 0.0.23

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bundle/base_protocol-84d9b670.js +1198 -0
  3. package/bundle/index.js +1132 -1251
  4. package/bundle/lib/base_protocol.js +2 -116
  5. package/bundle/lib/message/version_0.js +1 -1
  6. package/bundle/lib/predefined_bootstrap_nodes.js +6 -6
  7. package/bundle/{version_0-86411fdf.js → version_0-74b4b9db.js} +875 -794
  8. package/dist/.tsbuildinfo +1 -0
  9. package/dist/index.d.ts +5 -4
  10. package/dist/index.js +4 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/base_protocol.d.ts +18 -5
  13. package/dist/lib/base_protocol.js +25 -8
  14. package/dist/lib/base_protocol.js.map +1 -1
  15. package/dist/lib/connection_manager.js +10 -10
  16. package/dist/lib/connection_manager.js.map +1 -1
  17. package/dist/lib/filter/filter_rpc.js +4 -4
  18. package/dist/lib/filter/index.d.ts +4 -0
  19. package/dist/lib/filter/index.js +15 -11
  20. package/dist/lib/filter/index.js.map +1 -1
  21. package/dist/lib/filterPeers.d.ts +10 -0
  22. package/dist/lib/filterPeers.js +31 -0
  23. package/dist/lib/filterPeers.js.map +1 -0
  24. package/dist/lib/keep_alive_manager.d.ts +3 -2
  25. package/dist/lib/keep_alive_manager.js +27 -8
  26. package/dist/lib/keep_alive_manager.js.map +1 -1
  27. package/dist/lib/light_push/index.js +62 -33
  28. package/dist/lib/light_push/index.js.map +1 -1
  29. package/dist/lib/light_push/push_rpc.js +2 -2
  30. package/dist/lib/message/version_0.d.ts +1 -1
  31. package/dist/lib/message/version_0.js +3 -3
  32. package/dist/lib/message/version_0.js.map +1 -1
  33. package/dist/lib/predefined_bootstrap_nodes.js +6 -6
  34. package/dist/lib/store/history_rpc.js +3 -3
  35. package/dist/lib/store/index.d.ts +0 -5
  36. package/dist/lib/store/index.js +54 -37
  37. package/dist/lib/store/index.js.map +1 -1
  38. package/dist/lib/stream_manager.d.ts +15 -0
  39. package/dist/lib/stream_manager.js +53 -0
  40. package/dist/lib/stream_manager.js.map +1 -0
  41. package/dist/lib/to_proto_message.js +1 -1
  42. package/dist/lib/waku.d.ts +2 -2
  43. package/dist/lib/waku.js +1 -1
  44. package/package.json +15 -21
  45. package/src/index.ts +6 -9
  46. package/src/lib/base_protocol.ts +49 -18
  47. package/src/lib/connection_manager.ts +17 -13
  48. package/src/lib/filter/filter_rpc.ts +4 -4
  49. package/src/lib/filter/index.ts +21 -22
  50. package/src/lib/filterPeers.ts +43 -0
  51. package/src/lib/keep_alive_manager.ts +33 -9
  52. package/src/lib/light_push/index.ts +103 -47
  53. package/src/lib/light_push/push_rpc.ts +2 -2
  54. package/src/lib/message/version_0.ts +8 -5
  55. package/src/lib/predefined_bootstrap_nodes.ts +7 -7
  56. package/src/lib/store/history_rpc.ts +4 -4
  57. package/src/lib/store/index.ts +70 -51
  58. package/src/lib/stream_manager.ts +69 -0
  59. package/src/lib/to_proto_message.ts +1 -1
  60. package/src/lib/wait_for_remote_peer.ts +1 -1
  61. package/src/lib/waku.ts +3 -3
@@ -1,10 +1,12 @@
1
- import type { PeerId } from "@libp2p/interface-peer-id";
1
+ import type { PeerId } from "@libp2p/interface/peer-id";
2
+ import type { PeerStore } from "@libp2p/interface/peer-store";
2
3
  import type { IRelay } from "@waku/interfaces";
3
4
  import type { KeepAliveOptions } from "@waku/interfaces";
5
+ import { utf8ToBytes } from "@waku/utils/bytes";
4
6
  import debug from "debug";
5
7
  import type { PingService } from "libp2p/ping";
6
8
 
7
- import { createEncoder } from "../index.js";
9
+ import { createEncoder } from "./message/version_0.js";
8
10
 
9
11
  export const RelayPingContentTopic = "/relay-ping/1/ping/null";
10
12
  const log = debug("waku:keep-alive");
@@ -22,8 +24,12 @@ export class KeepAliveManager {
22
24
  this.relay = relay;
23
25
  }
24
26
 
25
- public start(peerId: PeerId, libp2pPing: PingService): void {
26
- // Just in case a timer already exist for this peer
27
+ public start(
28
+ peerId: PeerId,
29
+ libp2pPing: PingService,
30
+ peerStore: PeerStore
31
+ ): void {
32
+ // Just in case a timer already exists for this peer
27
33
  this.stop(peerId);
28
34
 
29
35
  const { pingKeepAlive: pingPeriodSecs, relayKeepAlive: relayPeriodSecs } =
@@ -33,10 +39,28 @@ export class KeepAliveManager {
33
39
 
34
40
  if (pingPeriodSecs !== 0) {
35
41
  const interval = setInterval(() => {
36
- libp2pPing.ping(peerId).catch((e) => {
37
- log(`Ping failed (${peerIdStr})`, e);
38
- });
42
+ void (async () => {
43
+ try {
44
+ // ping the peer for keep alive
45
+ // also update the peer store with the latency
46
+ const ping = await libp2pPing.ping(peerId);
47
+ log(`Ping succeeded (${peerIdStr})`, ping);
48
+
49
+ try {
50
+ await peerStore.patch(peerId, {
51
+ metadata: {
52
+ ping: utf8ToBytes(ping.toString())
53
+ }
54
+ });
55
+ } catch (e) {
56
+ log("Failed to update ping", e);
57
+ }
58
+ } catch (e) {
59
+ log(`Ping failed (${peerIdStr})`, e);
60
+ }
61
+ })();
39
62
  }, pingPeriodSecs * 1000);
63
+
40
64
  this.pingKeepAliveTimers.set(peerIdStr, interval);
41
65
  }
42
66
 
@@ -44,7 +68,7 @@ export class KeepAliveManager {
44
68
  if (relay && relayPeriodSecs !== 0) {
45
69
  const encoder = createEncoder({
46
70
  contentTopic: RelayPingContentTopic,
47
- ephemeral: true,
71
+ ephemeral: true
48
72
  });
49
73
  const interval = setInterval(() => {
50
74
  log("Sending Waku Relay ping message");
@@ -73,7 +97,7 @@ export class KeepAliveManager {
73
97
  public stopAll(): void {
74
98
  for (const timer of [
75
99
  ...Object.values(this.pingKeepAliveTimers),
76
- ...Object.values(this.relayKeepAliveTimers),
100
+ ...Object.values(this.relayKeepAliveTimers)
77
101
  ]) {
78
102
  clearInterval(timer);
79
103
  }
@@ -1,13 +1,12 @@
1
- import type { PeerId } from "@libp2p/interface-peer-id";
1
+ import type { PeerId } from "@libp2p/interface/peer-id";
2
2
  import {
3
3
  IEncoder,
4
4
  ILightPush,
5
5
  IMessage,
6
6
  Libp2p,
7
7
  ProtocolCreateOptions,
8
- ProtocolOptions,
9
8
  SendError,
10
- SendResult,
9
+ SendResult
11
10
  } from "@waku/interfaces";
12
11
  import { PushResponse } from "@waku/proto";
13
12
  import { isSizeValid } from "@waku/utils";
@@ -27,80 +26,137 @@ const log = debug("waku:light-push");
27
26
  export const LightPushCodec = "/vac/waku/lightpush/2.0.0-beta1";
28
27
  export { PushResponse };
29
28
 
29
+ type PreparePushMessageResult =
30
+ | {
31
+ query: PushRpc;
32
+ error: null;
33
+ }
34
+ | {
35
+ query: null;
36
+ error: SendError;
37
+ };
38
+
30
39
  /**
31
40
  * Implements the [Waku v2 Light Push protocol](https://rfc.vac.dev/spec/19/).
32
41
  */
33
42
  class LightPush extends BaseProtocol implements ILightPush {
34
43
  options: ProtocolCreateOptions;
44
+ private readonly NUM_PEERS_PROTOCOL = 1;
35
45
 
36
46
  constructor(libp2p: Libp2p, options?: ProtocolCreateOptions) {
37
47
  super(LightPushCodec, libp2p.components);
38
48
  this.options = options || {};
39
49
  }
40
50
 
41
- async send(
51
+ private async preparePushMessage(
42
52
  encoder: IEncoder,
43
53
  message: IMessage,
44
- opts?: ProtocolOptions
45
- ): Promise<SendResult> {
46
- const { pubSubTopic = DefaultPubSubTopic } = this.options;
47
-
48
- const peer = await this.getPeer(opts?.peerId);
49
- const stream = await this.newStream(peer);
50
-
51
- const recipients: PeerId[] = [];
52
- let error: undefined | SendError = undefined;
53
-
54
+ pubSubTopic: string
55
+ ): Promise<PreparePushMessageResult> {
54
56
  try {
55
57
  if (!isSizeValid(message.payload)) {
56
- log("Failed to send waku light push: message is bigger that 1MB");
57
- return {
58
- recipients,
59
- error: SendError.SIZE_TOO_BIG,
60
- };
58
+ log("Failed to send waku light push: message is bigger than 1MB");
59
+ return { query: null, error: SendError.SIZE_TOO_BIG };
61
60
  }
62
61
 
63
62
  const protoMessage = await encoder.toProtoObj(message);
64
63
  if (!protoMessage) {
65
64
  log("Failed to encode to protoMessage, aborting push");
66
65
  return {
67
- recipients,
68
- error: SendError.ENCODE_FAILED,
66
+ query: null,
67
+ error: SendError.ENCODE_FAILED
69
68
  };
70
69
  }
70
+
71
71
  const query = PushRpc.createRequest(protoMessage, pubSubTopic);
72
- const res = await pipe(
73
- [query.encode()],
74
- lp.encode,
75
- stream,
76
- lp.decode,
77
- async (source) => await all(source)
78
- );
72
+ return { query, error: null };
73
+ } catch (error) {
74
+ log("Failed to prepare push message", error);
75
+
76
+ return {
77
+ query: null,
78
+ error: SendError.GENERIC_FAIL
79
+ };
80
+ }
81
+ }
82
+
83
+ async send(encoder: IEncoder, message: IMessage): Promise<SendResult> {
84
+ const { pubSubTopic = DefaultPubSubTopic } = this.options;
85
+ const recipients: PeerId[] = [];
86
+
87
+ const { query, error: preparationError } = await this.preparePushMessage(
88
+ encoder,
89
+ message,
90
+ pubSubTopic
91
+ );
92
+
93
+ if (preparationError || !query) {
94
+ return {
95
+ recipients,
96
+ errors: [preparationError]
97
+ };
98
+ }
99
+
100
+ const peers = await this.getPeers({
101
+ maxBootstrapPeers: 1,
102
+ numPeers: this.NUM_PEERS_PROTOCOL
103
+ });
104
+
105
+ const promises = peers.map(async (peer) => {
106
+ let error: SendError | undefined;
107
+ const stream = await this.getStream(peer);
108
+
79
109
  try {
80
- const bytes = new Uint8ArrayList();
81
- res.forEach((chunk) => {
82
- bytes.append(chunk);
83
- });
84
-
85
- const response = PushRpc.decode(bytes).response;
86
-
87
- if (response?.isSuccess) {
88
- recipients.push(peer.id);
89
- } else {
90
- log("No response in PushRPC");
91
- error = SendError.NO_RPC_RESPONSE;
110
+ const res = await pipe(
111
+ [query.encode()],
112
+ lp.encode,
113
+ stream,
114
+ lp.decode,
115
+ async (source) => await all(source)
116
+ );
117
+ try {
118
+ const bytes = new Uint8ArrayList();
119
+ res.forEach((chunk) => {
120
+ bytes.append(chunk);
121
+ });
122
+
123
+ const response = PushRpc.decode(bytes).response;
124
+
125
+ if (response?.isSuccess) {
126
+ recipients.some((recipient) => recipient.equals(peer.id)) ||
127
+ recipients.push(peer.id);
128
+ } else {
129
+ log("No response in PushRPC");
130
+ error = SendError.NO_RPC_RESPONSE;
131
+ }
132
+ } catch (err) {
133
+ log("Failed to decode push reply", err);
134
+ error = SendError.DECODE_FAILED;
92
135
  }
93
136
  } catch (err) {
94
- log("Failed to decode push reply", err);
95
- error = SendError.DECODE_FAILED;
137
+ log("Failed to send waku light push request", err);
138
+ error = SendError.GENERIC_FAIL;
96
139
  }
97
- } catch (err) {
98
- log("Failed to send waku light push request", err);
99
- error = SendError.GENERIC_FAIL;
100
- }
140
+
141
+ return { recipients, error };
142
+ });
143
+
144
+ const results = await Promise.allSettled(promises);
145
+ const errors = results
146
+ .filter(
147
+ (
148
+ result
149
+ ): result is PromiseFulfilledResult<{
150
+ recipients: PeerId[];
151
+ error: SendError | undefined;
152
+ }> => result.status === "fulfilled"
153
+ )
154
+ .map((result) => result.value.error)
155
+ .filter((error) => error !== undefined) as SendError[];
156
+
101
157
  return {
102
- error,
103
158
  recipients,
159
+ errors
104
160
  };
105
161
  }
106
162
  }
@@ -13,9 +13,9 @@ export class PushRpc {
13
13
  requestId: uuid(),
14
14
  request: {
15
15
  message: message,
16
- pubsubTopic: pubSubTopic,
16
+ pubsubTopic: pubSubTopic
17
17
  },
18
- response: undefined,
18
+ response: undefined
19
19
  });
20
20
  }
21
21
 
@@ -6,7 +6,7 @@ import type {
6
6
  IMessage,
7
7
  IMetaSetter,
8
8
  IProtoMessage,
9
- IRateLimitProof,
9
+ IRateLimitProof
10
10
  } from "@waku/interfaces";
11
11
  import { proto_message as proto } from "@waku/proto";
12
12
  import debug from "debug";
@@ -18,7 +18,10 @@ export const Version = 0;
18
18
  export { proto };
19
19
 
20
20
  export class DecodedMessage implements IDecodedMessage {
21
- constructor(public pubSubTopic: string, protected proto: proto.WakuMessage) {}
21
+ constructor(
22
+ public pubSubTopic: string,
23
+ protected proto: proto.WakuMessage
24
+ ) {}
22
25
 
23
26
  get ephemeral(): boolean {
24
27
  return Boolean(this.proto.ephemeral);
@@ -91,7 +94,7 @@ export class Encoder implements IEncoder {
91
94
  timestamp: BigInt(timestamp.valueOf()) * OneMillion,
92
95
  meta: undefined,
93
96
  rateLimitProof: message.rateLimitProof,
94
- ephemeral: this.ephemeral,
97
+ ephemeral: this.ephemeral
95
98
  };
96
99
 
97
100
  if (this.metaSetter) {
@@ -115,7 +118,7 @@ export class Encoder implements IEncoder {
115
118
  export function createEncoder({
116
119
  contentTopic,
117
120
  ephemeral,
118
- metaSetter,
121
+ metaSetter
119
122
  }: EncoderOptions): Encoder {
120
123
  return new Encoder(contentTopic, ephemeral, metaSetter);
121
124
  }
@@ -137,7 +140,7 @@ export class Decoder implements IDecoder<DecodedMessage> {
137
140
  timestamp: protoMessage.timestamp ?? undefined,
138
141
  meta: protoMessage.meta ?? undefined,
139
142
  rateLimitProof: protoMessage.rateLimitProof ?? undefined,
140
- ephemeral: protoMessage.ephemeral ?? false,
143
+ ephemeral: protoMessage.ephemeral ?? false
141
144
  });
142
145
  }
143
146
 
@@ -4,7 +4,7 @@ export const DefaultWantedNumber = 1;
4
4
 
5
5
  export enum Fleet {
6
6
  Prod = "prod",
7
- Test = "test",
7
+ Test = "test"
8
8
  }
9
9
 
10
10
  /**
@@ -51,8 +51,8 @@ export const fleets = {
51
51
  "node-01.do-ams3.wakuv2.prod":
52
52
  "/dns4/node-01.do-ams3.wakuv2.prod.statusim.net/tcp/8000/wss/p2p/16Uiu2HAmL5okWopX7NqZWBUKVqW8iUxCEmd5GMHLVPwCgzYzQv3e",
53
53
  "node-01.gc-us-central1-a.wakuv2.prod":
54
- "/dns4/node-01.gc-us-central1-a.wakuv2.prod.statusim.net/tcp/8000/wss/p2p/16Uiu2HAmVkKntsECaYfefR1V2yCR79CegLATuTPE6B9TxgxBiiiA",
55
- },
54
+ "/dns4/node-01.gc-us-central1-a.wakuv2.prod.statusim.net/tcp/8000/wss/p2p/16Uiu2HAmVkKntsECaYfefR1V2yCR79CegLATuTPE6B9TxgxBiiiA"
55
+ }
56
56
  },
57
57
  "wakuv2.test": {
58
58
  "waku-websocket": {
@@ -61,8 +61,8 @@ export const fleets = {
61
61
  "node-01.do-ams3.wakuv2.test":
62
62
  "/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/8000/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ",
63
63
  "node-01.gc-us-central1-a.wakuv2.test":
64
- "/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/8000/wss/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS",
65
- },
66
- },
67
- },
64
+ "/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/8000/wss/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS"
65
+ }
66
+ }
67
+ }
68
68
  };
@@ -6,7 +6,7 @@ const OneMillion = BigInt(1_000_000);
6
6
 
7
7
  export enum PageDirection {
8
8
  BACKWARD = "backward",
9
- FORWARD = "forward",
9
+ FORWARD = "forward"
10
10
  }
11
11
 
12
12
  export interface Params {
@@ -43,7 +43,7 @@ export class HistoryRpc {
43
43
  const pagingInfo = {
44
44
  pageSize: BigInt(params.pageSize),
45
45
  cursor: params.cursor,
46
- direction,
46
+ direction
47
47
  } as proto.PagingInfo;
48
48
 
49
49
  let startTime, endTime;
@@ -63,9 +63,9 @@ export class HistoryRpc {
63
63
  contentFilters,
64
64
  pagingInfo,
65
65
  startTime,
66
- endTime,
66
+ endTime
67
67
  },
68
- response: undefined,
68
+ response: undefined
69
69
  });
70
70
  }
71
71
 
@@ -1,5 +1,4 @@
1
- import type { Stream } from "@libp2p/interface-connection";
2
- import type { PeerId } from "@libp2p/interface-peer-id";
1
+ import type { Stream } from "@libp2p/interface/connection";
3
2
  import { sha256 } from "@noble/hashes/sha256";
4
3
  import {
5
4
  Cursor,
@@ -7,7 +6,7 @@ import {
7
6
  IDecoder,
8
7
  IStore,
9
8
  Libp2p,
10
- ProtocolCreateOptions,
9
+ ProtocolCreateOptions
11
10
  } from "@waku/interfaces";
12
11
  import { proto_store as proto } from "@waku/proto";
13
12
  import { isDefined } from "@waku/utils";
@@ -40,10 +39,6 @@ export interface TimeFilter {
40
39
  }
41
40
 
42
41
  export interface QueryOptions {
43
- /**
44
- * The peer to query. If undefined, a pseudo-random peer is selected from the connected Waku Store peers.
45
- */
46
- peerId?: PeerId;
47
42
  /**
48
43
  * The direction in which pages are retrieved:
49
44
  * - { @link PageDirection.BACKWARD }: Most recent page first.
@@ -80,12 +75,61 @@ export interface QueryOptions {
80
75
  */
81
76
  class Store extends BaseProtocol implements IStore {
82
77
  options: ProtocolCreateOptions;
78
+ private readonly NUM_PEERS_PROTOCOL = 1;
83
79
 
84
80
  constructor(libp2p: Libp2p, options?: ProtocolCreateOptions) {
85
81
  super(StoreCodec, libp2p.components);
86
82
  this.options = options ?? {};
87
83
  }
88
84
 
85
+ /**
86
+ * Processes messages based on the provided callback and options.
87
+ * @private
88
+ */
89
+ private async processMessages<T extends IDecodedMessage>(
90
+ messages: Promise<T | undefined>[],
91
+ callback: (message: T) => Promise<void | boolean> | boolean | void,
92
+ options?: QueryOptions
93
+ ): Promise<boolean> {
94
+ let abort = false;
95
+ const messagesOrUndef: Array<T | undefined> = await Promise.all(messages);
96
+ let processedMessages: Array<T> = messagesOrUndef.filter(isDefined);
97
+
98
+ if (this.shouldReverseOrder(options)) {
99
+ processedMessages = processedMessages.reverse();
100
+ }
101
+
102
+ await Promise.all(
103
+ processedMessages.map(async (msg) => {
104
+ if (msg && !abort) {
105
+ abort = Boolean(await callback(msg));
106
+ }
107
+ })
108
+ );
109
+
110
+ return abort;
111
+ }
112
+
113
+ /**
114
+ * Determines whether to reverse the order of messages based on the provided options.
115
+ *
116
+ * Messages in pages are ordered from oldest (first) to most recent (last).
117
+ * https://github.com/vacp2p/rfc/issues/533
118
+ *
119
+ * @private
120
+ */
121
+ private shouldReverseOrder(options?: QueryOptions): boolean {
122
+ return (
123
+ typeof options?.pageDirection === "undefined" ||
124
+ options?.pageDirection === PageDirection.BACKWARD
125
+ );
126
+ }
127
+
128
+ /**
129
+ * @deprecated Use `queryWithOrderedCallback` instead
130
+ **/
131
+ queryOrderedCallback = this.queryWithOrderedCallback;
132
+
89
133
  /**
90
134
  * Do a query to a Waku Store to retrieve historical/missed messages.
91
135
  *
@@ -103,42 +147,20 @@ class Store extends BaseProtocol implements IStore {
103
147
  * or if an error is encountered when processing the reply,
104
148
  * or if two decoders with the same content topic are passed.
105
149
  */
106
- async queryOrderedCallback<T extends IDecodedMessage>(
150
+ async queryWithOrderedCallback<T extends IDecodedMessage>(
107
151
  decoders: IDecoder<T>[],
108
152
  callback: (message: T) => Promise<void | boolean> | boolean | void,
109
153
  options?: QueryOptions
110
154
  ): Promise<void> {
111
- let abort = false;
112
155
  for await (const promises of this.queryGenerator(decoders, options)) {
113
- if (abort) break;
114
- const messagesOrUndef: Array<T | undefined> = await Promise.all(promises);
115
-
116
- let messages: Array<T> = messagesOrUndef.filter(isDefined);
117
-
118
- // Messages in pages are ordered from oldest (first) to most recent (last).
119
- // https://github.com/vacp2p/rfc/issues/533
120
- if (
121
- typeof options?.pageDirection === "undefined" ||
122
- options?.pageDirection === PageDirection.BACKWARD
123
- ) {
124
- messages = messages.reverse();
125
- }
126
-
127
- await Promise.all(
128
- messages.map(async (msg) => {
129
- if (msg && !abort) {
130
- abort = Boolean(await callback(msg));
131
- }
132
- })
133
- );
156
+ if (await this.processMessages(promises, callback, options)) break;
134
157
  }
135
158
  }
136
159
 
137
160
  /**
138
161
  * Do a query to a Waku Store to retrieve historical/missed messages.
139
- *
140
162
  * The callback function takes a `Promise<WakuMessage>` in input,
141
- * useful if messages needs to be decrypted and performance matters.
163
+ * useful if messages need to be decrypted and performance matters.
142
164
  *
143
165
  * The order of the messages passed to the callback is as follows:
144
166
  * - within a page, messages are expected to be ordered from oldest to most recent
@@ -152,7 +174,7 @@ class Store extends BaseProtocol implements IStore {
152
174
  * or if an error is encountered when processing the reply,
153
175
  * or if two decoders with the same content topic are passed.
154
176
  */
155
- async queryCallbackOnPromise<T extends IDecodedMessage>(
177
+ async queryWithPromiseCallback<T extends IDecodedMessage>(
156
178
  decoders: IDecoder<T>[],
157
179
  callback: (
158
180
  message: Promise<T | undefined>
@@ -160,17 +182,15 @@ class Store extends BaseProtocol implements IStore {
160
182
  options?: QueryOptions
161
183
  ): Promise<void> {
162
184
  let abort = false;
163
- let promises: Promise<void>[] = [];
164
185
  for await (const page of this.queryGenerator(decoders, options)) {
165
- const _promises = page.map(async (msg) => {
166
- if (!abort) {
167
- abort = Boolean(await callback(msg));
168
- }
186
+ const _promises = page.map(async (msgPromise) => {
187
+ if (abort) return;
188
+ abort = Boolean(await callback(msgPromise));
169
189
  });
170
190
 
171
- promises = promises.concat(_promises);
191
+ await Promise.all(_promises);
192
+ if (abort) break;
172
193
  }
173
- await Promise.all(promises);
174
194
  }
175
195
 
176
196
  /**
@@ -183,9 +203,6 @@ class Store extends BaseProtocol implements IStore {
183
203
  * as follows:
184
204
  * - within a page, messages SHOULD be ordered from oldest to most recent
185
205
  * - pages direction depends on { @link QueryOptions.pageDirection }
186
- *
187
- * However, there is no way to guarantee the behavior of the remote node.
188
- *
189
206
  * @throws If not able to reach a Waku Store peer to query,
190
207
  * or if an error is encountered when processing the reply,
191
208
  * or if two decoders with the same content topic are passed.
@@ -219,21 +236,23 @@ class Store extends BaseProtocol implements IStore {
219
236
  {
220
237
  pubSubTopic: pubSubTopic,
221
238
  pageDirection: PageDirection.BACKWARD,
222
- pageSize: DefaultPageSize,
239
+ pageSize: DefaultPageSize
223
240
  },
224
241
  options,
225
242
  { contentTopics, startTime, endTime }
226
243
  );
227
244
 
228
- log("Querying history with the following options", {
229
- ...options,
230
- peerId: options?.peerId?.toString(),
231
- });
245
+ log("Querying history with the following options", options);
232
246
 
233
- const peer = await this.getPeer(options?.peerId);
247
+ const peer = (
248
+ await this.getPeers({
249
+ numPeers: this.NUM_PEERS_PROTOCOL,
250
+ maxBootstrapPeers: 1
251
+ })
252
+ )[0];
234
253
 
235
254
  for await (const messages of paginate<T>(
236
- this.newStream.bind(this, peer),
255
+ this.getStream.bind(this, peer),
237
256
  queryOpts,
238
257
  decodersAsMap,
239
258
  options?.cursor
@@ -369,7 +388,7 @@ export async function createCursor(
369
388
  digest,
370
389
  pubsubTopic,
371
390
  senderTime: messageTime,
372
- receiverTime: messageTime,
391
+ receiverTime: messageTime
373
392
  };
374
393
  }
375
394
 
@@ -0,0 +1,69 @@
1
+ import type { PeerUpdate } from "@libp2p/interface";
2
+ import type { Stream } from "@libp2p/interface/connection";
3
+ import { Peer } from "@libp2p/interface/peer-store";
4
+ import { Libp2p } from "@waku/interfaces";
5
+ import { selectConnection } from "@waku/utils/libp2p";
6
+ import debug from "debug";
7
+
8
+ export class StreamManager {
9
+ private streamPool: Map<string, Promise<Stream>>;
10
+ private log: debug.Debugger;
11
+
12
+ constructor(
13
+ public multicodec: string,
14
+ public getConnections: Libp2p["getConnections"],
15
+ public addEventListener: Libp2p["addEventListener"]
16
+ ) {
17
+ this.log = debug(`waku:stream-manager:${multicodec}`);
18
+ this.addEventListener(
19
+ "peer:update",
20
+ this.handlePeerUpdateStreamPool.bind(this)
21
+ );
22
+ this.getStream = this.getStream.bind(this);
23
+ this.streamPool = new Map();
24
+ }
25
+
26
+ public async getStream(peer: Peer): Promise<Stream> {
27
+ const peerIdStr = peer.id.toString();
28
+ const streamPromise = this.streamPool.get(peerIdStr);
29
+
30
+ if (!streamPromise) {
31
+ return this.newStream(peer); // fallback by creating a new stream on the spot
32
+ }
33
+
34
+ // We have the stream, let's remove it from the map
35
+ this.streamPool.delete(peerIdStr);
36
+
37
+ this.prepareNewStream(peer);
38
+
39
+ const stream = await streamPromise;
40
+
41
+ if (stream.status === "closed") {
42
+ return this.newStream(peer); // fallback by creating a new stream on the spot
43
+ }
44
+
45
+ return stream;
46
+ }
47
+
48
+ private async newStream(peer: Peer): Promise<Stream> {
49
+ const connections = this.getConnections(peer.id);
50
+ const connection = selectConnection(connections);
51
+ if (!connection) {
52
+ throw new Error("Failed to get a connection to the peer");
53
+ }
54
+ return connection.newStream(this.multicodec);
55
+ }
56
+
57
+ private prepareNewStream(peer: Peer): void {
58
+ const streamPromise = this.newStream(peer);
59
+ this.streamPool.set(peer.id.toString(), streamPromise);
60
+ }
61
+
62
+ private handlePeerUpdateStreamPool = (evt: CustomEvent<PeerUpdate>): void => {
63
+ const peer = evt.detail.peer;
64
+ if (peer.protocols.includes(this.multicodec)) {
65
+ this.log(`Preemptively opening a stream to ${peer.id.toString()}`);
66
+ this.prepareNewStream(peer);
67
+ }
68
+ };
69
+ }
@@ -8,7 +8,7 @@ const EmptyMessage: IProtoMessage = {
8
8
  timestamp: undefined,
9
9
  meta: undefined,
10
10
  rateLimitProof: undefined,
11
- ephemeral: undefined,
11
+ ephemeral: undefined
12
12
  };
13
13
 
14
14
  export function toProtoMessage(wire: WakuMessageProto): IProtoMessage {