@waku/core 0.0.29-fd60cc2.0 → 0.0.30-682cc66.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.
@@ -1,30 +1,13 @@
1
- import { Stream } from "@libp2p/interface";
2
1
  import type { Peer } from "@libp2p/interface";
3
2
  import type { IncomingStreamData } from "@libp2p/interface-internal";
4
3
  import type {
5
- Callback,
6
4
  ContentTopic,
7
- IAsyncIterator,
8
- IDecodedMessage,
9
- IDecoder,
10
- IFilter,
11
- IProtoMessage,
12
- IReceiver,
5
+ IBaseProtocolCore,
13
6
  Libp2p,
14
7
  ProtocolCreateOptions,
15
- PubsubTopic,
16
- SingleShardInfo,
17
- Unsubscribe
8
+ PubsubTopic
18
9
  } from "@waku/interfaces";
19
- import { DefaultPubsubTopic } from "@waku/interfaces";
20
- import { messageHashStr } from "@waku/message-hash";
21
10
  import { WakuMessage } from "@waku/proto";
22
- import {
23
- ensurePubsubTopicIsConfigured,
24
- groupByContentTopic,
25
- singleShardInfoToPubsubTopic,
26
- toAsyncIterator
27
- } from "@waku/utils";
28
11
  import { Logger } from "@waku/utils";
29
12
  import all from "it-all";
30
13
  import * as lp from "it-length-prefixed";
@@ -40,329 +23,20 @@ import {
40
23
 
41
24
  const log = new Logger("filter:v2");
42
25
 
43
- type SubscriptionCallback<T extends IDecodedMessage> = {
44
- decoders: IDecoder<T>[];
45
- callback: Callback<T>;
46
- };
47
-
48
26
  export const FilterCodecs = {
49
27
  SUBSCRIBE: "/vac/waku/filter-subscribe/2.0.0-beta1",
50
28
  PUSH: "/vac/waku/filter-push/2.0.0-beta1"
51
29
  };
52
30
 
53
- /**
54
- * A subscription object refers to a subscription to a given pubsub topic.
55
- */
56
- class Subscription {
57
- readonly peers: Peer[];
58
- private readonly pubsubTopic: PubsubTopic;
59
- private newStream: (peer: Peer) => Promise<Stream>;
60
- readonly receivedMessagesHashStr: string[] = [];
61
-
62
- private subscriptionCallbacks: Map<
63
- ContentTopic,
64
- SubscriptionCallback<IDecodedMessage>
65
- >;
66
-
31
+ export class FilterCore extends BaseProtocol implements IBaseProtocolCore {
67
32
  constructor(
68
- pubsubTopic: PubsubTopic,
69
- remotePeers: Peer[],
70
- newStream: (peer: Peer) => Promise<Stream>
33
+ private handleIncomingMessage: (
34
+ pubsubTopic: PubsubTopic,
35
+ wakuMessage: WakuMessage
36
+ ) => Promise<void>,
37
+ libp2p: Libp2p,
38
+ options?: ProtocolCreateOptions
71
39
  ) {
72
- this.peers = remotePeers;
73
- this.pubsubTopic = pubsubTopic;
74
- this.newStream = newStream;
75
- this.subscriptionCallbacks = new Map();
76
- }
77
-
78
- async subscribe<T extends IDecodedMessage>(
79
- decoders: IDecoder<T> | IDecoder<T>[],
80
- callback: Callback<T>
81
- ): Promise<void> {
82
- const decodersArray = Array.isArray(decoders) ? decoders : [decoders];
83
-
84
- // check that all decoders are configured for the same pubsub topic as this subscription
85
- decodersArray.forEach((decoder) => {
86
- if (decoder.pubsubTopic !== this.pubsubTopic) {
87
- throw new Error(
88
- `Pubsub topic not configured: decoder is configured for pubsub topic ${decoder.pubsubTopic} but this subscription is for pubsub topic ${this.pubsubTopic}. Please create a new Subscription for the different pubsub topic.`
89
- );
90
- }
91
- });
92
-
93
- const decodersGroupedByCT = groupByContentTopic(decodersArray);
94
- const contentTopics = Array.from(decodersGroupedByCT.keys());
95
-
96
- const promises = this.peers.map(async (peer) => {
97
- const stream = await this.newStream(peer);
98
-
99
- const request = FilterSubscribeRpc.createSubscribeRequest(
100
- this.pubsubTopic,
101
- contentTopics
102
- );
103
-
104
- try {
105
- const res = await pipe(
106
- [request.encode()],
107
- lp.encode,
108
- stream,
109
- lp.decode,
110
- async (source) => await all(source)
111
- );
112
-
113
- if (!res || !res.length) {
114
- throw Error(
115
- `No response received for request ${request.requestId}: ${res}`
116
- );
117
- }
118
-
119
- const { statusCode, requestId, statusDesc } =
120
- FilterSubscribeResponse.decode(res[0].slice());
121
-
122
- if (statusCode < 200 || statusCode >= 300) {
123
- throw new Error(
124
- `Filter subscribe request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
125
- );
126
- }
127
-
128
- log.info(
129
- "Subscribed to peer ",
130
- peer.id.toString(),
131
- "for content topics",
132
- contentTopics
133
- );
134
- } catch (e) {
135
- throw new Error(
136
- "Error subscribing to peer: " +
137
- peer.id.toString() +
138
- " for content topics: " +
139
- contentTopics +
140
- ": " +
141
- e
142
- );
143
- }
144
- });
145
-
146
- const results = await Promise.allSettled(promises);
147
-
148
- this.handleErrors(results, "subscribe");
149
-
150
- // Save the callback functions by content topics so they
151
- // can easily be removed (reciprocally replaced) if `unsubscribe` (reciprocally `subscribe`)
152
- // is called for those content topics
153
- decodersGroupedByCT.forEach((decoders, contentTopic) => {
154
- // Cast the type because a given `subscriptionCallbacks` map may hold
155
- // Decoder that decode to different implementations of `IDecodedMessage`
156
- const subscriptionCallback = {
157
- decoders,
158
- callback
159
- } as unknown as SubscriptionCallback<IDecodedMessage>;
160
-
161
- // The callback and decoder may override previous values, this is on
162
- // purpose as the user may call `subscribe` to refresh the subscription
163
- this.subscriptionCallbacks.set(contentTopic, subscriptionCallback);
164
- });
165
- }
166
-
167
- async unsubscribe(contentTopics: ContentTopic[]): Promise<void> {
168
- const promises = this.peers.map(async (peer) => {
169
- const stream = await this.newStream(peer);
170
- const unsubscribeRequest = FilterSubscribeRpc.createUnsubscribeRequest(
171
- this.pubsubTopic,
172
- contentTopics
173
- );
174
-
175
- try {
176
- await pipe([unsubscribeRequest.encode()], lp.encode, stream.sink);
177
- } catch (error) {
178
- throw new Error("Error unsubscribing: " + error);
179
- }
180
-
181
- contentTopics.forEach((contentTopic: string) => {
182
- this.subscriptionCallbacks.delete(contentTopic);
183
- });
184
- });
185
-
186
- const results = await Promise.allSettled(promises);
187
-
188
- this.handleErrors(results, "unsubscribe");
189
- }
190
-
191
- async ping(): Promise<void> {
192
- const promises = this.peers.map(async (peer) => {
193
- const stream = await this.newStream(peer);
194
-
195
- const request = FilterSubscribeRpc.createSubscriberPingRequest();
196
-
197
- try {
198
- const res = await pipe(
199
- [request.encode()],
200
- lp.encode,
201
- stream,
202
- lp.decode,
203
- async (source) => await all(source)
204
- );
205
-
206
- if (!res || !res.length) {
207
- throw Error(
208
- `No response received for request ${request.requestId}: ${res}`
209
- );
210
- }
211
-
212
- const { statusCode, requestId, statusDesc } =
213
- FilterSubscribeResponse.decode(res[0].slice());
214
-
215
- if (statusCode < 200 || statusCode >= 300) {
216
- throw new Error(
217
- `Filter ping request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
218
- );
219
- }
220
- log.info(`Ping successful for peer ${peer.id.toString()}`);
221
- } catch (error) {
222
- log.error("Error pinging: ", error);
223
- throw error; // Rethrow the actual error instead of wrapping it
224
- }
225
- });
226
-
227
- const results = await Promise.allSettled(promises);
228
-
229
- this.handleErrors(results, "ping");
230
- }
231
-
232
- async unsubscribeAll(): Promise<void> {
233
- const promises = this.peers.map(async (peer) => {
234
- const stream = await this.newStream(peer);
235
-
236
- const request = FilterSubscribeRpc.createUnsubscribeAllRequest(
237
- this.pubsubTopic
238
- );
239
-
240
- try {
241
- const res = await pipe(
242
- [request.encode()],
243
- lp.encode,
244
- stream,
245
- lp.decode,
246
- async (source) => await all(source)
247
- );
248
-
249
- if (!res || !res.length) {
250
- throw Error(
251
- `No response received for request ${request.requestId}: ${res}`
252
- );
253
- }
254
-
255
- const { statusCode, requestId, statusDesc } =
256
- FilterSubscribeResponse.decode(res[0].slice());
257
-
258
- if (statusCode < 200 || statusCode >= 300) {
259
- throw new Error(
260
- `Filter unsubscribe all request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
261
- );
262
- }
263
-
264
- this.subscriptionCallbacks.clear();
265
- log.info(
266
- `Unsubscribed from all content topics for pubsub topic ${this.pubsubTopic}`
267
- );
268
- } catch (error) {
269
- throw new Error(
270
- "Error unsubscribing from all content topics: " + error
271
- );
272
- }
273
- });
274
-
275
- const results = await Promise.allSettled(promises);
276
-
277
- this.handleErrors(results, "unsubscribeAll");
278
- }
279
-
280
- async processMessage(message: WakuMessage): Promise<void> {
281
- const hashedMessageStr = messageHashStr(
282
- this.pubsubTopic,
283
- message as IProtoMessage
284
- );
285
- if (this.receivedMessagesHashStr.includes(hashedMessageStr)) {
286
- log.info("Message already received, skipping");
287
- return;
288
- }
289
- this.receivedMessagesHashStr.push(hashedMessageStr);
290
-
291
- const { contentTopic } = message;
292
- const subscriptionCallback = this.subscriptionCallbacks.get(contentTopic);
293
- if (!subscriptionCallback) {
294
- log.error("No subscription callback available for ", contentTopic);
295
- return;
296
- }
297
- log.info(
298
- "Processing message with content topic ",
299
- contentTopic,
300
- " on pubsub topic ",
301
- this.pubsubTopic
302
- );
303
- await pushMessage(subscriptionCallback, this.pubsubTopic, message);
304
- }
305
-
306
- // Filter out only the rejected promises and extract & handle their reasons
307
- private handleErrors(
308
- results: PromiseSettledResult<void>[],
309
- type: "ping" | "subscribe" | "unsubscribe" | "unsubscribeAll"
310
- ): void {
311
- const errors = results
312
- .filter(
313
- (result): result is PromiseRejectedResult =>
314
- result.status === "rejected"
315
- )
316
- .map((rejectedResult) => rejectedResult.reason);
317
-
318
- if (errors.length === this.peers.length) {
319
- const errorCounts = new Map<string, number>();
320
- // TODO: streamline error logging with https://github.com/orgs/waku-org/projects/2/views/1?pane=issue&itemId=42849952
321
- errors.forEach((error) => {
322
- const message = error instanceof Error ? error.message : String(error);
323
- errorCounts.set(message, (errorCounts.get(message) || 0) + 1);
324
- });
325
-
326
- const uniqueErrorMessages = Array.from(
327
- errorCounts,
328
- ([message, count]) => `${message} (occurred ${count} times)`
329
- ).join(", ");
330
- throw new Error(`Error ${type} all peers: ${uniqueErrorMessages}`);
331
- } else if (errors.length > 0) {
332
- // TODO: handle renewing faulty peers with new peers (https://github.com/waku-org/js-waku/issues/1463)
333
- log.warn(
334
- `Some ${type} failed. These will be refreshed with new peers`,
335
- errors
336
- );
337
- } else {
338
- log.info(`${type} successful for all peers`);
339
- }
340
- }
341
- }
342
-
343
- const DEFAULT_NUM_PEERS = 3;
344
-
345
- class Filter extends BaseProtocol implements IReceiver {
346
- private activeSubscriptions = new Map<string, Subscription>();
347
-
348
- private getActiveSubscription(
349
- pubsubTopic: PubsubTopic
350
- ): Subscription | undefined {
351
- return this.activeSubscriptions.get(pubsubTopic);
352
- }
353
-
354
- private setActiveSubscription(
355
- pubsubTopic: PubsubTopic,
356
- subscription: Subscription
357
- ): Subscription {
358
- this.activeSubscriptions.set(pubsubTopic, subscription);
359
- return subscription;
360
- }
361
-
362
- //TODO: Remove when FilterCore and FilterSDK are introduced
363
- private readonly numPeersToUse: number;
364
-
365
- constructor(libp2p: Libp2p, options?: ProtocolCreateOptions) {
366
40
  super(
367
41
  FilterCodecs.SUBSCRIBE,
368
42
  libp2p.components,
@@ -371,106 +45,9 @@ class Filter extends BaseProtocol implements IReceiver {
371
45
  options
372
46
  );
373
47
 
374
- this.numPeersToUse = options?.numPeersToUse ?? DEFAULT_NUM_PEERS;
375
-
376
48
  libp2p.handle(FilterCodecs.PUSH, this.onRequest.bind(this)).catch((e) => {
377
49
  log.error("Failed to register ", FilterCodecs.PUSH, e);
378
50
  });
379
-
380
- this.activeSubscriptions = new Map();
381
- }
382
-
383
- /**
384
- * Creates a new subscription to the given pubsub topic.
385
- * The subscription is made to multiple peers for decentralization.
386
- * @param pubsubTopicShardInfo The pubsub topic to subscribe to.
387
- * @returns The subscription object.
388
- */
389
- async createSubscription(
390
- pubsubTopicShardInfo: SingleShardInfo | PubsubTopic = DefaultPubsubTopic
391
- ): Promise<Subscription> {
392
- const pubsubTopic =
393
- typeof pubsubTopicShardInfo == "string"
394
- ? pubsubTopicShardInfo
395
- : singleShardInfoToPubsubTopic(pubsubTopicShardInfo);
396
-
397
- ensurePubsubTopicIsConfigured(pubsubTopic, this.pubsubTopics);
398
-
399
- const peers = await this.getPeers({
400
- maxBootstrapPeers: 1,
401
- numPeers: this.numPeersToUse
402
- });
403
- if (peers.length === 0) {
404
- throw new Error("No peer found to initiate subscription.");
405
- }
406
-
407
- log.info(
408
- `Creating filter subscription with ${peers.length} peers: `,
409
- peers.map((peer) => peer.id.toString())
410
- );
411
-
412
- const subscription =
413
- this.getActiveSubscription(pubsubTopic) ??
414
- this.setActiveSubscription(
415
- pubsubTopic,
416
- new Subscription(pubsubTopic, peers, this.getStream.bind(this))
417
- );
418
-
419
- return subscription;
420
- }
421
-
422
- public toSubscriptionIterator<T extends IDecodedMessage>(
423
- decoders: IDecoder<T> | IDecoder<T>[]
424
- ): Promise<IAsyncIterator<T>> {
425
- return toAsyncIterator(this, decoders);
426
- }
427
-
428
- /**
429
- * This method is used to satisfy the `IReceiver` interface.
430
- *
431
- * @hidden
432
- *
433
- * @param decoders The decoders to use for the subscription.
434
- * @param callback The callback function to use for the subscription.
435
- * @param opts Optional protocol options for the subscription.
436
- *
437
- * @returns A Promise that resolves to a function that unsubscribes from the subscription.
438
- *
439
- * @remarks
440
- * This method should not be used directly.
441
- * Instead, use `createSubscription` to create a new subscription.
442
- */
443
- async subscribe<T extends IDecodedMessage>(
444
- decoders: IDecoder<T> | IDecoder<T>[],
445
- callback: Callback<T>
446
- ): Promise<Unsubscribe> {
447
- const pubsubTopics = this.getPubsubTopics(decoders);
448
-
449
- if (pubsubTopics.length === 0) {
450
- throw Error(
451
- "Failed to subscribe: no pubsubTopic found on decoders provided."
452
- );
453
- }
454
-
455
- if (pubsubTopics.length > 1) {
456
- throw Error(
457
- "Failed to subscribe: all decoders should have the same pubsub topic. Use createSubscription to be more agile."
458
- );
459
- }
460
-
461
- const subscription = await this.createSubscription(pubsubTopics[0]);
462
-
463
- await subscription.subscribe(decoders, callback);
464
-
465
- const contentTopics = Array.from(
466
- groupByContentTopic(
467
- Array.isArray(decoders) ? decoders : [decoders]
468
- ).keys()
469
- );
470
-
471
- return async () => {
472
- await subscription.unsubscribe(contentTopics);
473
- };
474
51
  }
475
52
 
476
53
  private onRequest(streamData: IncomingStreamData): void {
@@ -494,16 +71,7 @@ class Filter extends BaseProtocol implements IReceiver {
494
71
  return;
495
72
  }
496
73
 
497
- const subscription = this.getActiveSubscription(pubsubTopic);
498
-
499
- if (!subscription) {
500
- log.error(
501
- `No subscription locally registered for topic ${pubsubTopic}`
502
- );
503
- return;
504
- }
505
-
506
- await subscription.processMessage(wakuMessage);
74
+ await this.handleIncomingMessage(pubsubTopic, wakuMessage);
507
75
  }
508
76
  }).then(
509
77
  () => {
@@ -518,51 +86,117 @@ class Filter extends BaseProtocol implements IReceiver {
518
86
  }
519
87
  }
520
88
 
521
- private getPubsubTopics(decoders: IDecoder<any> | IDecoder<any>[]): string[] {
522
- if (!Array.isArray(decoders)) {
523
- return [decoders.pubsubTopic];
89
+ async subscribe(
90
+ pubsubTopic: PubsubTopic,
91
+ peer: Peer,
92
+ contentTopics: ContentTopic[]
93
+ ): Promise<void> {
94
+ const stream = await this.getStream(peer);
95
+
96
+ const request = FilterSubscribeRpc.createSubscribeRequest(
97
+ pubsubTopic,
98
+ contentTopics
99
+ );
100
+
101
+ const res = await pipe(
102
+ [request.encode()],
103
+ lp.encode,
104
+ stream,
105
+ lp.decode,
106
+ async (source) => await all(source)
107
+ );
108
+
109
+ if (!res || !res.length) {
110
+ throw Error(
111
+ `No response received for request ${request.requestId}: ${res}`
112
+ );
524
113
  }
525
114
 
526
- if (decoders.length === 0) {
527
- return [];
115
+ const { statusCode, requestId, statusDesc } =
116
+ FilterSubscribeResponse.decode(res[0].slice());
117
+
118
+ if (statusCode < 200 || statusCode >= 300) {
119
+ throw new Error(
120
+ `Filter subscribe request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
121
+ );
528
122
  }
123
+ }
529
124
 
530
- const pubsubTopics = new Set(decoders.map((d) => d.pubsubTopic));
125
+ async unsubscribe(
126
+ pubsubTopic: PubsubTopic,
127
+ peer: Peer,
128
+ contentTopics: ContentTopic[]
129
+ ): Promise<void> {
130
+ const stream = await this.getStream(peer);
131
+ const unsubscribeRequest = FilterSubscribeRpc.createUnsubscribeRequest(
132
+ pubsubTopic,
133
+ contentTopics
134
+ );
531
135
 
532
- return [...pubsubTopics];
136
+ await pipe([unsubscribeRequest.encode()], lp.encode, stream.sink);
533
137
  }
534
- }
535
138
 
536
- export function wakuFilter(
537
- init: ProtocolCreateOptions = { pubsubTopics: [] }
538
- ): (libp2p: Libp2p) => IFilter {
539
- return (libp2p: Libp2p) => new Filter(libp2p, init);
540
- }
139
+ async unsubscribeAll(pubsubTopic: PubsubTopic, peer: Peer): Promise<void> {
140
+ const stream = await this.getStream(peer);
541
141
 
542
- async function pushMessage<T extends IDecodedMessage>(
543
- subscriptionCallback: SubscriptionCallback<T>,
544
- pubsubTopic: PubsubTopic,
545
- message: WakuMessage
546
- ): Promise<void> {
547
- const { decoders, callback } = subscriptionCallback;
142
+ const request = FilterSubscribeRpc.createUnsubscribeAllRequest(pubsubTopic);
548
143
 
549
- const { contentTopic } = message;
550
- if (!contentTopic) {
551
- log.warn("Message has no content topic, skipping");
552
- return;
144
+ const res = await pipe(
145
+ [request.encode()],
146
+ lp.encode,
147
+ stream,
148
+ lp.decode,
149
+ async (source) => await all(source)
150
+ );
151
+
152
+ if (!res || !res.length) {
153
+ throw Error(
154
+ `No response received for request ${request.requestId}: ${res}`
155
+ );
156
+ }
157
+
158
+ const { statusCode, requestId, statusDesc } =
159
+ FilterSubscribeResponse.decode(res[0].slice());
160
+
161
+ if (statusCode < 200 || statusCode >= 300) {
162
+ throw new Error(
163
+ `Filter unsubscribe all request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
164
+ );
165
+ }
553
166
  }
554
167
 
555
- try {
556
- const decodePromises = decoders.map((dec) =>
557
- dec
558
- .fromProtoObj(pubsubTopic, message as IProtoMessage)
559
- .then((decoded) => decoded || Promise.reject("Decoding failed"))
560
- );
168
+ async ping(peer: Peer): Promise<void> {
169
+ const stream = await this.getStream(peer);
561
170
 
562
- const decodedMessage = await Promise.any(decodePromises);
171
+ const request = FilterSubscribeRpc.createSubscriberPingRequest();
563
172
 
564
- await callback(decodedMessage);
565
- } catch (e) {
566
- log.error("Error decoding message", e);
173
+ try {
174
+ const res = await pipe(
175
+ [request.encode()],
176
+ lp.encode,
177
+ stream,
178
+ lp.decode,
179
+ async (source) => await all(source)
180
+ );
181
+
182
+ if (!res || !res.length) {
183
+ throw Error(
184
+ `No response received for request ${request.requestId}: ${res}`
185
+ );
186
+ }
187
+
188
+ const { statusCode, requestId, statusDesc } =
189
+ FilterSubscribeResponse.decode(res[0].slice());
190
+
191
+ if (statusCode < 200 || statusCode >= 300) {
192
+ throw new Error(
193
+ `Filter ping request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
194
+ );
195
+ }
196
+ log.info(`Ping successful for peer ${peer.id.toString()}`);
197
+ } catch (error) {
198
+ log.error("Error pinging: ", error);
199
+ throw error; // Rethrow the actual error instead of wrapping it
200
+ }
567
201
  }
568
202
  }
@@ -56,7 +56,7 @@ export class KeepAliveManager {
56
56
  }
57
57
 
58
58
  try {
59
- await peerStore.patch(peerId, {
59
+ await peerStore.merge(peerId, {
60
60
  metadata: {
61
61
  ping: utf8ToBytes(ping.toString())
62
62
  }
@@ -70,7 +70,7 @@ export async function waitForRemotePeer(
70
70
  if (!waku.filter)
71
71
  throw new Error("Cannot wait for Filter peer: protocol not mounted");
72
72
  promises.push(
73
- waitForConnectedPeer(waku.filter, waku.libp2p.services.metadata)
73
+ waitForConnectedPeer(waku.filter.protocol, waku.libp2p.services.metadata)
74
74
  );
75
75
  }
76
76