@waku/core 0.0.23 → 0.0.25-a42b7be.0

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 (55) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/bundle/{base_protocol-84d9b670.js → base_protocol-4bcf7514.js} +194 -134
  3. package/bundle/{browser-bde977a3.js → browser-90197c87.js} +26 -1
  4. package/bundle/index-27b91e3b.js +31 -0
  5. package/bundle/index.js +19384 -2421
  6. package/bundle/lib/base_protocol.js +3 -2
  7. package/bundle/lib/message/version_0.js +3 -2
  8. package/bundle/lib/predefined_bootstrap_nodes.js +2 -0
  9. package/bundle/{version_0-74b4b9db.js → version_0-2f1176e3.js} +36 -24
  10. package/dist/.tsbuildinfo +1 -1
  11. package/dist/lib/connection_manager.d.ts +17 -4
  12. package/dist/lib/connection_manager.js +111 -45
  13. package/dist/lib/connection_manager.js.map +1 -1
  14. package/dist/lib/filter/index.js +53 -37
  15. package/dist/lib/filter/index.js.map +1 -1
  16. package/dist/lib/keep_alive_manager.d.ts +1 -0
  17. package/dist/lib/keep_alive_manager.js +42 -18
  18. package/dist/lib/keep_alive_manager.js.map +1 -1
  19. package/dist/lib/light_push/index.js +60 -38
  20. package/dist/lib/light_push/index.js.map +1 -1
  21. package/dist/lib/light_push/push_rpc.d.ts +1 -1
  22. package/dist/lib/light_push/push_rpc.js +2 -2
  23. package/dist/lib/message/version_0.d.ts +13 -13
  24. package/dist/lib/message/version_0.js +22 -20
  25. package/dist/lib/message/version_0.js.map +1 -1
  26. package/dist/lib/store/history_rpc.d.ts +1 -1
  27. package/dist/lib/store/history_rpc.js +1 -1
  28. package/dist/lib/store/index.d.ts +1 -1
  29. package/dist/lib/store/index.js +43 -17
  30. package/dist/lib/store/index.js.map +1 -1
  31. package/dist/lib/stream_manager.d.ts +1 -1
  32. package/dist/lib/stream_manager.js +8 -5
  33. package/dist/lib/stream_manager.js.map +1 -1
  34. package/dist/lib/wait_for_remote_peer.d.ts +2 -2
  35. package/dist/lib/wait_for_remote_peer.js +13 -11
  36. package/dist/lib/wait_for_remote_peer.js.map +1 -1
  37. package/dist/lib/waku.d.ts +4 -3
  38. package/dist/lib/waku.js +13 -11
  39. package/dist/lib/waku.js.map +1 -1
  40. package/package.json +1 -137
  41. package/src/lib/connection_manager.ts +156 -51
  42. package/src/lib/filter/index.ts +76 -40
  43. package/src/lib/keep_alive_manager.ts +53 -20
  44. package/src/lib/light_push/index.ts +74 -38
  45. package/src/lib/light_push/push_rpc.ts +2 -2
  46. package/src/lib/message/version_0.ts +25 -17
  47. package/src/lib/store/history_rpc.ts +2 -2
  48. package/src/lib/store/index.ts +60 -23
  49. package/src/lib/stream_manager.ts +12 -7
  50. package/src/lib/wait_for_remote_peer.ts +13 -11
  51. package/src/lib/waku.ts +12 -9
  52. package/dist/lib/push_or_init_map.d.ts +0 -1
  53. package/dist/lib/push_or_init_map.js +0 -9
  54. package/dist/lib/push_or_init_map.js.map +0 -1
  55. package/src/lib/push_or_init_map.ts +0 -13
@@ -1,19 +1,19 @@
1
1
  import type { PeerId } from "@libp2p/interface/peer-id";
2
2
  import type { PeerStore } from "@libp2p/interface/peer-store";
3
- import type { IRelay } from "@waku/interfaces";
3
+ import type { IRelay, PeerIdStr } from "@waku/interfaces";
4
4
  import type { KeepAliveOptions } from "@waku/interfaces";
5
+ import { Logger } from "@waku/utils";
5
6
  import { utf8ToBytes } from "@waku/utils/bytes";
6
- import debug from "debug";
7
7
  import type { PingService } from "libp2p/ping";
8
8
 
9
9
  import { createEncoder } from "./message/version_0.js";
10
10
 
11
11
  export const RelayPingContentTopic = "/relay-ping/1/ping/null";
12
- const log = debug("waku:keep-alive");
12
+ const log = new Logger("keep-alive");
13
13
 
14
14
  export class KeepAliveManager {
15
15
  private pingKeepAliveTimers: Map<string, ReturnType<typeof setInterval>>;
16
- private relayKeepAliveTimers: Map<PeerId, ReturnType<typeof setInterval>>;
16
+ private relayKeepAliveTimers: Map<PeerId, ReturnType<typeof setInterval>[]>;
17
17
  private options: KeepAliveOptions;
18
18
  private relay?: IRelay;
19
19
 
@@ -37,14 +37,24 @@ export class KeepAliveManager {
37
37
 
38
38
  const peerIdStr = peerId.toString();
39
39
 
40
+ // Ping the peer every pingPeriodSecs seconds
41
+ // if pingPeriodSecs is 0, don't ping the peer
40
42
  if (pingPeriodSecs !== 0) {
41
43
  const interval = setInterval(() => {
42
44
  void (async () => {
45
+ let ping: number;
43
46
  try {
44
47
  // ping the peer for keep alive
45
48
  // also update the peer store with the latency
46
- const ping = await libp2pPing.ping(peerId);
47
- log(`Ping succeeded (${peerIdStr})`, ping);
49
+ try {
50
+ ping = await libp2pPing.ping(peerId);
51
+ log.info(`Ping succeeded (${peerIdStr})`, ping);
52
+ } catch (error) {
53
+ log.error(`Ping failed for peer (${peerIdStr}).
54
+ Next ping will be attempted in ${pingPeriodSecs} seconds.
55
+ `);
56
+ return;
57
+ }
48
58
 
49
59
  try {
50
60
  await peerStore.patch(peerId, {
@@ -53,10 +63,10 @@ export class KeepAliveManager {
53
63
  }
54
64
  });
55
65
  } catch (e) {
56
- log("Failed to update ping", e);
66
+ log.error("Failed to update ping", e);
57
67
  }
58
68
  } catch (e) {
59
- log(`Ping failed (${peerIdStr})`, e);
69
+ log.error(`Ping failed (${peerIdStr})`, e);
60
70
  }
61
71
  })();
62
72
  }, pingPeriodSecs * 1000);
@@ -66,17 +76,12 @@ export class KeepAliveManager {
66
76
 
67
77
  const relay = this.relay;
68
78
  if (relay && relayPeriodSecs !== 0) {
69
- const encoder = createEncoder({
70
- contentTopic: RelayPingContentTopic,
71
- ephemeral: true
72
- });
73
- const interval = setInterval(() => {
74
- log("Sending Waku Relay ping message");
75
- relay
76
- .send(encoder, { payload: new Uint8Array([1]) })
77
- .catch((e) => log("Failed to send relay ping", e));
78
- }, relayPeriodSecs * 1000);
79
- this.relayKeepAliveTimers.set(peerId, interval);
79
+ const intervals = this.scheduleRelayPings(
80
+ relay,
81
+ relayPeriodSecs,
82
+ peerId.toString()
83
+ );
84
+ this.relayKeepAliveTimers.set(peerId, intervals);
80
85
  }
81
86
  }
82
87
 
@@ -89,7 +94,7 @@ export class KeepAliveManager {
89
94
  }
90
95
 
91
96
  if (this.relayKeepAliveTimers.has(peerId)) {
92
- clearInterval(this.relayKeepAliveTimers.get(peerId));
97
+ this.relayKeepAliveTimers.get(peerId)?.map(clearInterval);
93
98
  this.relayKeepAliveTimers.delete(peerId);
94
99
  }
95
100
  }
@@ -105,4 +110,32 @@ export class KeepAliveManager {
105
110
  this.pingKeepAliveTimers.clear();
106
111
  this.relayKeepAliveTimers.clear();
107
112
  }
113
+
114
+ private scheduleRelayPings(
115
+ relay: IRelay,
116
+ relayPeriodSecs: number,
117
+ peerIdStr: PeerIdStr
118
+ ): NodeJS.Timeout[] {
119
+ // send a ping message to each PubSubTopic the peer is part of
120
+ const intervals: NodeJS.Timeout[] = [];
121
+ for (const topic of relay.pubsubTopics) {
122
+ const meshPeers = relay.getMeshPeers(topic);
123
+ if (!meshPeers.includes(peerIdStr)) continue;
124
+
125
+ const encoder = createEncoder({
126
+ pubsubTopic: topic,
127
+ contentTopic: RelayPingContentTopic,
128
+ ephemeral: true
129
+ });
130
+ const interval = setInterval(() => {
131
+ log.info("Sending Waku Relay ping message");
132
+ relay
133
+ .send(encoder, { payload: new Uint8Array([1]) })
134
+ .catch((e) => log.error("Failed to send relay ping", e));
135
+ }, relayPeriodSecs * 1000);
136
+ intervals.push(interval);
137
+ }
138
+
139
+ return intervals;
140
+ }
108
141
  }
@@ -1,3 +1,4 @@
1
+ import type { Stream } from "@libp2p/interface/connection";
1
2
  import type { PeerId } from "@libp2p/interface/peer-id";
2
3
  import {
3
4
  IEncoder,
@@ -5,12 +6,16 @@ import {
5
6
  IMessage,
6
7
  Libp2p,
7
8
  ProtocolCreateOptions,
9
+ PubSubTopic,
8
10
  SendError,
9
11
  SendResult
10
12
  } from "@waku/interfaces";
11
13
  import { PushResponse } from "@waku/proto";
12
- import { isSizeValid } from "@waku/utils";
13
- import debug from "debug";
14
+ import {
15
+ ensurePubsubTopicIsConfigured,
16
+ isMessageSizeUnderCap
17
+ } from "@waku/utils";
18
+ import { Logger } from "@waku/utils";
14
19
  import all from "it-all";
15
20
  import * as lp from "it-length-prefixed";
16
21
  import { pipe } from "it-pipe";
@@ -21,7 +26,7 @@ import { DefaultPubSubTopic } from "../constants.js";
21
26
 
22
27
  import { PushRpc } from "./push_rpc.js";
23
28
 
24
- const log = debug("waku:light-push");
29
+ const log = new Logger("light-push");
25
30
 
26
31
  export const LightPushCodec = "/vac/waku/lightpush/2.0.0-beta1";
27
32
  export { PushResponse };
@@ -40,38 +45,43 @@ type PreparePushMessageResult =
40
45
  * Implements the [Waku v2 Light Push protocol](https://rfc.vac.dev/spec/19/).
41
46
  */
42
47
  class LightPush extends BaseProtocol implements ILightPush {
43
- options: ProtocolCreateOptions;
48
+ private readonly pubsubTopics: PubSubTopic[];
44
49
  private readonly NUM_PEERS_PROTOCOL = 1;
45
50
 
46
51
  constructor(libp2p: Libp2p, options?: ProtocolCreateOptions) {
47
52
  super(LightPushCodec, libp2p.components);
48
- this.options = options || {};
53
+ this.pubsubTopics = options?.pubsubTopics ?? [DefaultPubSubTopic];
49
54
  }
50
55
 
51
56
  private async preparePushMessage(
52
57
  encoder: IEncoder,
53
58
  message: IMessage,
54
- pubSubTopic: string
59
+ pubsubTopic: string
55
60
  ): Promise<PreparePushMessageResult> {
56
61
  try {
57
- if (!isSizeValid(message.payload)) {
58
- log("Failed to send waku light push: message is bigger than 1MB");
62
+ if (!message.payload || message.payload.length === 0) {
63
+ log.error("Failed to send waku light push: payload is empty");
64
+ return { query: null, error: SendError.EMPTY_PAYLOAD };
65
+ }
66
+
67
+ if (!(await isMessageSizeUnderCap(encoder, message))) {
68
+ log.error("Failed to send waku light push: message is bigger than 1MB");
59
69
  return { query: null, error: SendError.SIZE_TOO_BIG };
60
70
  }
61
71
 
62
72
  const protoMessage = await encoder.toProtoObj(message);
63
73
  if (!protoMessage) {
64
- log("Failed to encode to protoMessage, aborting push");
74
+ log.error("Failed to encode to protoMessage, aborting push");
65
75
  return {
66
76
  query: null,
67
77
  error: SendError.ENCODE_FAILED
68
78
  };
69
79
  }
70
80
 
71
- const query = PushRpc.createRequest(protoMessage, pubSubTopic);
81
+ const query = PushRpc.createRequest(protoMessage, pubsubTopic);
72
82
  return { query, error: null };
73
83
  } catch (error) {
74
- log("Failed to prepare push message", error);
84
+ log.error("Failed to prepare push message", error);
75
85
 
76
86
  return {
77
87
  query: null,
@@ -81,13 +91,15 @@ class LightPush extends BaseProtocol implements ILightPush {
81
91
  }
82
92
 
83
93
  async send(encoder: IEncoder, message: IMessage): Promise<SendResult> {
84
- const { pubSubTopic = DefaultPubSubTopic } = this.options;
94
+ const { pubsubTopic } = encoder;
95
+ ensurePubsubTopicIsConfigured(pubsubTopic, this.pubsubTopics);
96
+
85
97
  const recipients: PeerId[] = [];
86
98
 
87
99
  const { query, error: preparationError } = await this.preparePushMessage(
88
100
  encoder,
89
101
  message,
90
- pubSubTopic
102
+ pubsubTopic
91
103
  );
92
104
 
93
105
  if (preparationError || !query) {
@@ -97,48 +109,72 @@ class LightPush extends BaseProtocol implements ILightPush {
97
109
  };
98
110
  }
99
111
 
112
+ //TODO: get a relevant peer for the topic/shard
100
113
  const peers = await this.getPeers({
101
114
  maxBootstrapPeers: 1,
102
115
  numPeers: this.NUM_PEERS_PROTOCOL
103
116
  });
104
117
 
118
+ if (!peers.length) {
119
+ return {
120
+ recipients,
121
+ errors: [SendError.NO_PEER_AVAILABLE]
122
+ };
123
+ }
124
+
105
125
  const promises = peers.map(async (peer) => {
106
- let error: SendError | undefined;
107
- const stream = await this.getStream(peer);
126
+ let stream: Stream | undefined;
127
+ try {
128
+ stream = await this.getStream(peer);
129
+ } catch (err) {
130
+ log.error(
131
+ `Failed to get a stream for remote peer${peer.id.toString()}`,
132
+ err
133
+ );
134
+ return { recipients, error: SendError.REMOTE_PEER_FAULT };
135
+ }
108
136
 
137
+ let res: Uint8ArrayList[] | undefined;
109
138
  try {
110
- const res = await pipe(
139
+ res = await pipe(
111
140
  [query.encode()],
112
141
  lp.encode,
113
142
  stream,
114
143
  lp.decode,
115
144
  async (source) => await all(source)
116
145
  );
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;
135
- }
136
146
  } catch (err) {
137
- log("Failed to send waku light push request", err);
138
- error = SendError.GENERIC_FAIL;
147
+ log.error("Failed to send waku light push request", err);
148
+ return { recipients, error: SendError.GENERIC_FAIL };
149
+ }
150
+
151
+ const bytes = new Uint8ArrayList();
152
+ res.forEach((chunk) => {
153
+ bytes.append(chunk);
154
+ });
155
+
156
+ let response: PushResponse | undefined;
157
+ try {
158
+ response = PushRpc.decode(bytes).response;
159
+ } catch (err) {
160
+ log.error("Failed to decode push reply", err);
161
+ return { recipients, error: SendError.DECODE_FAILED };
139
162
  }
140
163
 
141
- return { recipients, error };
164
+ if (!response) {
165
+ log.error("Remote peer fault: No response in PushRPC");
166
+ return { recipients, error: SendError.REMOTE_PEER_FAULT };
167
+ }
168
+
169
+ if (!response.isSuccess) {
170
+ log.error("Remote peer rejected the message: ", response.info);
171
+ return { recipients, error: SendError.REMOTE_PEER_REJECTED };
172
+ }
173
+
174
+ recipients.some((recipient) => recipient.equals(peer.id)) ||
175
+ recipients.push(peer.id);
176
+
177
+ return { recipients };
142
178
  });
143
179
 
144
180
  const results = await Promise.allSettled(promises);
@@ -7,13 +7,13 @@ export class PushRpc {
7
7
 
8
8
  static createRequest(
9
9
  message: proto.WakuMessage,
10
- pubSubTopic: string
10
+ pubsubTopic: string
11
11
  ): PushRpc {
12
12
  return new PushRpc({
13
13
  requestId: uuid(),
14
14
  request: {
15
15
  message: message,
16
- pubsubTopic: pubSubTopic
16
+ pubsubTopic: pubsubTopic
17
17
  },
18
18
  response: undefined
19
19
  });
@@ -6,12 +6,15 @@ import type {
6
6
  IMessage,
7
7
  IMetaSetter,
8
8
  IProtoMessage,
9
- IRateLimitProof
9
+ IRateLimitProof,
10
+ PubSubTopic
10
11
  } from "@waku/interfaces";
11
12
  import { proto_message as proto } from "@waku/proto";
12
- import debug from "debug";
13
+ import { Logger } from "@waku/utils";
13
14
 
14
- const log = debug("waku:message:version-0");
15
+ import { DefaultPubSubTopic } from "../constants.js";
16
+
17
+ const log = new Logger("message:version-0");
15
18
  const OneMillion = BigInt(1_000_000);
16
19
 
17
20
  export const Version = 0;
@@ -19,7 +22,7 @@ export { proto };
19
22
 
20
23
  export class DecodedMessage implements IDecodedMessage {
21
24
  constructor(
22
- public pubSubTopic: string,
25
+ public pubsubTopic: string,
23
26
  protected proto: proto.WakuMessage
24
27
  ) {}
25
28
 
@@ -73,6 +76,7 @@ export class Encoder implements IEncoder {
73
76
  constructor(
74
77
  public contentTopic: string,
75
78
  public ephemeral: boolean = false,
79
+ public pubsubTopic: PubSubTopic,
76
80
  public metaSetter?: IMetaSetter
77
81
  ) {
78
82
  if (!contentTopic || contentTopic === "") {
@@ -109,22 +113,25 @@ export class Encoder implements IEncoder {
109
113
  /**
110
114
  * Creates an encoder that encode messages without Waku level encryption or signature.
111
115
  *
112
- * An encoder is used to encode messages in the [`14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/)
116
+ * An encoder is used to encode messages in the [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/)
113
117
  * format to be sent over the Waku network. The resulting encoder can then be
114
- * pass to { @link @waku/interfaces.LightPush.push } or
115
- * { @link @waku/interfaces.Relay.send } to automatically encode outgoing
118
+ * pass to { @link @waku/interfaces!ISender.send } to automatically encode outgoing
116
119
  * messages.
117
120
  */
118
121
  export function createEncoder({
122
+ pubsubTopic = DefaultPubSubTopic,
119
123
  contentTopic,
120
124
  ephemeral,
121
125
  metaSetter
122
126
  }: EncoderOptions): Encoder {
123
- return new Encoder(contentTopic, ephemeral, metaSetter);
127
+ return new Encoder(contentTopic, ephemeral, pubsubTopic, metaSetter);
124
128
  }
125
129
 
126
130
  export class Decoder implements IDecoder<DecodedMessage> {
127
- constructor(public contentTopic: string) {
131
+ constructor(
132
+ public pubsubTopic: PubSubTopic,
133
+ public contentTopic: string
134
+ ) {
128
135
  if (!contentTopic || contentTopic === "") {
129
136
  throw new Error("Content topic must be specified");
130
137
  }
@@ -132,7 +139,6 @@ export class Decoder implements IDecoder<DecodedMessage> {
132
139
 
133
140
  fromWireToProtoObj(bytes: Uint8Array): Promise<IProtoMessage | undefined> {
134
141
  const protoMessage = proto.WakuMessage.decode(bytes);
135
- log("Message decoded", protoMessage);
136
142
  return Promise.resolve({
137
143
  payload: protoMessage.payload,
138
144
  contentTopic: protoMessage.contentTopic,
@@ -145,13 +151,13 @@ export class Decoder implements IDecoder<DecodedMessage> {
145
151
  }
146
152
 
147
153
  async fromProtoObj(
148
- pubSubTopic: string,
154
+ pubsubTopic: string,
149
155
  proto: IProtoMessage
150
156
  ): Promise<DecodedMessage | undefined> {
151
157
  // https://rfc.vac.dev/spec/14/
152
158
  // > If omitted, the value SHOULD be interpreted as version 0.
153
159
  if (proto.version ?? 0 !== Version) {
154
- log(
160
+ log.error(
155
161
  "Failed to decode due to incorrect version, expected:",
156
162
  Version,
157
163
  ", actual:",
@@ -160,7 +166,7 @@ export class Decoder implements IDecoder<DecodedMessage> {
160
166
  return Promise.resolve(undefined);
161
167
  }
162
168
 
163
- return new DecodedMessage(pubSubTopic, proto);
169
+ return new DecodedMessage(pubsubTopic, proto);
164
170
  }
165
171
  }
166
172
 
@@ -169,12 +175,14 @@ export class Decoder implements IDecoder<DecodedMessage> {
169
175
  *
170
176
  * A decoder is used to decode messages from the [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/)
171
177
  * format when received from the Waku network. The resulting decoder can then be
172
- * pass to { @link @waku/interfaces.Filter.subscribe } or
173
- * { @link @waku/interfaces.Relay.subscribe } to automatically decode incoming
178
+ * pass to { @link @waku/interfaces!IReceiver.subscribe } to automatically decode incoming
174
179
  * messages.
175
180
  *
176
181
  * @param contentTopic The resulting decoder will only decode messages with this content topic.
177
182
  */
178
- export function createDecoder(contentTopic: string): Decoder {
179
- return new Decoder(contentTopic);
183
+ export function createDecoder(
184
+ contentTopic: string,
185
+ pubsubTopic: PubSubTopic = DefaultPubSubTopic
186
+ ): Decoder {
187
+ return new Decoder(pubsubTopic, contentTopic);
180
188
  }
@@ -11,7 +11,7 @@ export enum PageDirection {
11
11
 
12
12
  export interface Params {
13
13
  contentTopics: string[];
14
- pubSubTopic: string;
14
+ pubsubTopic: string;
15
15
  pageDirection: PageDirection;
16
16
  pageSize: number;
17
17
  startTime?: Date;
@@ -59,7 +59,7 @@ export class HistoryRpc {
59
59
  return new HistoryRpc({
60
60
  requestId: uuid(),
61
61
  query: {
62
- pubsubTopic: params.pubSubTopic,
62
+ pubsubTopic: params.pubsubTopic,
63
63
  contentFilters,
64
64
  pagingInfo,
65
65
  startTime,
@@ -6,12 +6,13 @@ import {
6
6
  IDecoder,
7
7
  IStore,
8
8
  Libp2p,
9
- ProtocolCreateOptions
9
+ ProtocolCreateOptions,
10
+ PubSubTopic
10
11
  } from "@waku/interfaces";
11
12
  import { proto_store as proto } from "@waku/proto";
12
- import { isDefined } from "@waku/utils";
13
+ import { ensurePubsubTopicIsConfigured, isDefined } from "@waku/utils";
14
+ import { Logger } from "@waku/utils";
13
15
  import { concat, utf8ToBytes } from "@waku/utils/bytes";
14
- import debug from "debug";
15
16
  import all from "it-all";
16
17
  import * as lp from "it-length-prefixed";
17
18
  import { pipe } from "it-pipe";
@@ -25,7 +26,7 @@ import { HistoryRpc, PageDirection, Params } from "./history_rpc.js";
25
26
 
26
27
  import HistoryError = proto.HistoryResponse.HistoryError;
27
28
 
28
- const log = debug("waku:store");
29
+ const log = new Logger("store");
29
30
 
30
31
  export const StoreCodec = "/vac/waku/store/2.0.0-beta4";
31
32
 
@@ -74,12 +75,12 @@ export interface QueryOptions {
74
75
  * The Waku Store protocol can be used to retrieved historical messages.
75
76
  */
76
77
  class Store extends BaseProtocol implements IStore {
77
- options: ProtocolCreateOptions;
78
+ private readonly pubsubTopics: PubSubTopic[];
78
79
  private readonly NUM_PEERS_PROTOCOL = 1;
79
80
 
80
81
  constructor(libp2p: Libp2p, options?: ProtocolCreateOptions) {
81
82
  super(StoreCodec, libp2p.components);
82
- this.options = options ?? {};
83
+ this.pubsubTopics = options?.pubsubTopics ?? [DefaultPubSubTopic];
83
84
  }
84
85
 
85
86
  /**
@@ -206,12 +207,20 @@ class Store extends BaseProtocol implements IStore {
206
207
  * @throws If not able to reach a Waku Store peer to query,
207
208
  * or if an error is encountered when processing the reply,
208
209
  * or if two decoders with the same content topic are passed.
210
+ *
211
+ * This API only supports querying a single pubsub topic at a time.
212
+ * If multiple decoders are provided, they must all have the same pubsub topic.
213
+ * @throws If multiple decoders with different pubsub topics are provided.
214
+ * @throws If no decoders are provided.
215
+ * @throws If no decoders are found for the provided pubsub topic.
209
216
  */
210
217
  async *queryGenerator<T extends IDecodedMessage>(
211
218
  decoders: IDecoder<T>[],
212
219
  options?: QueryOptions
213
220
  ): AsyncGenerator<Promise<T | undefined>[]> {
214
- const { pubSubTopic = DefaultPubSubTopic } = this.options;
221
+ if (decoders.length === 0) {
222
+ throw new Error("No decoders provided");
223
+ }
215
224
 
216
225
  let startTime, endTime;
217
226
 
@@ -220,6 +229,33 @@ class Store extends BaseProtocol implements IStore {
220
229
  endTime = options.timeFilter.endTime;
221
230
  }
222
231
 
232
+ // convert array to set to remove duplicates
233
+ const uniquePubSubTopicsInQuery = Array.from(
234
+ new Set(decoders.map((decoder) => decoder.pubsubTopic))
235
+ );
236
+
237
+ // If multiple pubsub topics are provided, throw an error
238
+ if (uniquePubSubTopicsInQuery.length > 1) {
239
+ throw new Error(
240
+ "API does not support querying multiple pubsub topics at once"
241
+ );
242
+ }
243
+
244
+ // we can be certain that there is only one pubsub topic in the query
245
+ const pubSubTopicForQuery = uniquePubSubTopicsInQuery[0];
246
+
247
+ ensurePubsubTopicIsConfigured(pubSubTopicForQuery, this.pubsubTopics);
248
+
249
+ // check that the pubsubTopic from the Cursor and Decoder match
250
+ if (
251
+ options?.cursor?.pubsubTopic &&
252
+ options.cursor.pubsubTopic !== pubSubTopicForQuery
253
+ ) {
254
+ throw new Error(
255
+ `Cursor pubsub topic (${options?.cursor?.pubsubTopic}) does not match decoder pubsub topic (${pubSubTopicForQuery})`
256
+ );
257
+ }
258
+
223
259
  const decodersAsMap = new Map();
224
260
  decoders.forEach((dec) => {
225
261
  if (decodersAsMap.has(dec.contentTopic)) {
@@ -230,11 +266,17 @@ class Store extends BaseProtocol implements IStore {
230
266
  decodersAsMap.set(dec.contentTopic, dec);
231
267
  });
232
268
 
233
- const contentTopics = decoders.map((dec) => dec.contentTopic);
269
+ const contentTopics = decoders
270
+ .filter((decoder) => decoder.pubsubTopic === pubSubTopicForQuery)
271
+ .map((dec) => dec.contentTopic);
272
+
273
+ if (contentTopics.length === 0) {
274
+ throw new Error("No decoders found for topic " + pubSubTopicForQuery);
275
+ }
234
276
 
235
277
  const queryOpts = Object.assign(
236
278
  {
237
- pubSubTopic: pubSubTopic,
279
+ pubsubTopic: pubSubTopicForQuery,
238
280
  pageDirection: PageDirection.BACKWARD,
239
281
  pageSize: DefaultPageSize
240
282
  },
@@ -242,8 +284,6 @@ class Store extends BaseProtocol implements IStore {
242
284
  { contentTopics, startTime, endTime }
243
285
  );
244
286
 
245
- log("Querying history with the following options", options);
246
-
247
287
  const peer = (
248
288
  await this.getPeers({
249
289
  numPeers: this.NUM_PEERS_PROTOCOL,
@@ -283,9 +323,9 @@ async function* paginate<T extends IDecodedMessage>(
283
323
 
284
324
  const historyRpcQuery = HistoryRpc.createQuery(queryOpts);
285
325
 
286
- log(
326
+ log.info(
287
327
  "Querying store peer",
288
- `for (${queryOpts.pubSubTopic})`,
328
+ `for (${queryOpts.pubsubTopic})`,
289
329
  queryOpts.contentTopics
290
330
  );
291
331
 
@@ -307,7 +347,7 @@ async function* paginate<T extends IDecodedMessage>(
307
347
  const reply = historyRpcQuery.decode(bytes);
308
348
 
309
349
  if (!reply.response) {
310
- log("Stopping pagination due to store `response` field missing");
350
+ log.warn("Stopping pagination due to store `response` field missing");
311
351
  break;
312
352
  }
313
353
 
@@ -318,13 +358,13 @@ async function* paginate<T extends IDecodedMessage>(
318
358
  }
319
359
 
320
360
  if (!response.messages || !response.messages.length) {
321
- log(
361
+ log.warn(
322
362
  "Stopping pagination due to store `response.messages` field missing or empty"
323
363
  );
324
364
  break;
325
365
  }
326
366
 
327
- log(`${response.messages.length} messages retrieved from store`);
367
+ log.error(`${response.messages.length} messages retrieved from store`);
328
368
 
329
369
  yield response.messages.map((protoMsg) => {
330
370
  const contentTopic = protoMsg.contentTopic;
@@ -332,7 +372,7 @@ async function* paginate<T extends IDecodedMessage>(
332
372
  const decoder = decoders.get(contentTopic);
333
373
  if (decoder) {
334
374
  return decoder.fromProtoObj(
335
- queryOpts.pubSubTopic,
375
+ queryOpts.pubsubTopic,
336
376
  toProtoMessage(protoMsg)
337
377
  );
338
378
  }
@@ -344,7 +384,7 @@ async function* paginate<T extends IDecodedMessage>(
344
384
  if (typeof nextCursor === "undefined") {
345
385
  // If the server does not return cursor then there is an issue,
346
386
  // Need to abort, or we end up in an infinite loop
347
- log(
387
+ log.warn(
348
388
  "Stopping pagination due to `response.pagingInfo.cursor` missing from store response"
349
389
  );
350
390
  break;
@@ -365,10 +405,7 @@ async function* paginate<T extends IDecodedMessage>(
365
405
  }
366
406
  }
367
407
 
368
- export async function createCursor(
369
- message: IDecodedMessage,
370
- pubsubTopic: string = DefaultPubSubTopic
371
- ): Promise<Cursor> {
408
+ export async function createCursor(message: IDecodedMessage): Promise<Cursor> {
372
409
  if (
373
410
  !message ||
374
411
  !message.timestamp ||
@@ -386,7 +423,7 @@ export async function createCursor(
386
423
 
387
424
  return {
388
425
  digest,
389
- pubsubTopic,
426
+ pubsubTopic: message.pubsubTopic,
390
427
  senderTime: messageTime,
391
428
  receiverTime: messageTime
392
429
  };