@libp2p/pubsub 1.2.5 → 1.2.8

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,12 +1,12 @@
1
1
  import { logger } from '@libp2p/logger'
2
- import { EventEmitter, CustomEvent } from '@libp2p/interfaces'
2
+ import { EventEmitter, CustomEvent, EventHandler } from '@libp2p/interfaces'
3
3
  import errcode from 'err-code'
4
4
  import { pipe } from 'it-pipe'
5
5
  import Queue from 'p-queue'
6
6
  import { Topology } from '@libp2p/topology'
7
7
  import { codes } from './errors.js'
8
8
  import { PeerStreams as PeerStreamsImpl } from './peer-streams.js'
9
- import { toRpcMessage, toMessage, ensureArray, randomSeqno, noSignMsgId, msgId } from './utils.js'
9
+ import { toMessage, ensureArray, randomSeqno, noSignMsgId, msgId, toRpcMessage } from './utils.js'
10
10
  import {
11
11
  signMessage,
12
12
  verifySignature
@@ -14,7 +14,7 @@ import {
14
14
  import type { PeerId } from '@libp2p/interfaces/peer-id'
15
15
  import type { Registrar, IncomingStreamData } from '@libp2p/interfaces/registrar'
16
16
  import type { Connection } from '@libp2p/interfaces/connection'
17
- import type { PubSub, Message, StrictNoSign, StrictSign, PubSubOptions, PubSubEvents, RPCMessage, RPC, PeerStreams, RPCSubscription } from '@libp2p/interfaces/pubsub'
17
+ import type { PubSub, Message, StrictNoSign, StrictSign, PubSubOptions, PubSubEvents, RPC, PeerStreams, RPCSubscription, RPCMessage } from '@libp2p/interfaces/pubsub'
18
18
  import type { Logger } from '@libp2p/logger'
19
19
  import { base58btc } from 'multiformats/bases/base58'
20
20
  import { peerMap } from '@libp2p/peer-map'
@@ -29,7 +29,7 @@ export interface TopicValidator { (topic: string, message: Message): Promise<voi
29
29
  * PubsubBaseProtocol handles the peers and connections logic for pubsub routers
30
30
  * and specifies the API that pubsub routers should have.
31
31
  */
32
- export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap & PubSubEvents> implements PubSub<EventMap & PubSubEvents> {
32
+ export abstract class PubsubBaseProtocol<EventMap extends PubSubEvents = PubSubEvents> extends EventEmitter<EventMap & PubSubEvents> implements PubSub<EventMap & PubSubEvents> {
33
33
  public peerId: PeerId
34
34
  public started: boolean
35
35
  /**
@@ -172,10 +172,10 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
172
172
  protected _onIncomingStream (evt: CustomEvent<IncomingStreamData>) {
173
173
  const { protocol, stream, connection } = evt.detail
174
174
  const peerId = connection.remotePeer
175
- const peer = this._addPeer(peerId, protocol)
175
+ const peer = this.addPeer(peerId, protocol)
176
176
  const inboundStream = peer.attachInboundStream(stream)
177
177
 
178
- this._processMessages(peerId, inboundStream, peer)
178
+ this.processMessages(peerId, inboundStream, peer)
179
179
  .catch(err => this.log(err))
180
180
  }
181
181
 
@@ -187,14 +187,14 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
187
187
 
188
188
  try {
189
189
  const { stream, protocol } = await conn.newStream(this.multicodecs)
190
- const peer = this._addPeer(peerId, protocol)
190
+ const peer = this.addPeer(peerId, protocol)
191
191
  await peer.attachOutboundStream(stream)
192
192
  } catch (err: any) {
193
193
  this.log.error(err)
194
194
  }
195
195
 
196
196
  // Immediately send my own subscriptions to the newly established conn
197
- this._sendSubscriptions(peerId, Array.from(this.subscriptions), true)
197
+ this.send(peerId, { subscriptions: Array.from(this.subscriptions).map(sub => sub.toString()), subscribe: true })
198
198
  }
199
199
 
200
200
  /**
@@ -210,7 +210,7 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
210
210
  /**
211
211
  * Notifies the router that a peer has been connected
212
212
  */
213
- protected _addPeer (peerId: PeerId, protocol: string): PeerStreams {
213
+ addPeer (peerId: PeerId, protocol: string): PeerStreams {
214
214
  const existing = this.peers.get(peerId)
215
215
 
216
216
  // If peer streams already exists, do nothing
@@ -264,31 +264,41 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
264
264
  /**
265
265
  * Responsible for processing each RPC message received by other peers.
266
266
  */
267
- async _processMessages (peerId: PeerId, stream: AsyncIterable<Uint8Array>, peerStreams: PeerStreams) {
267
+ async processMessages (peerId: PeerId, stream: AsyncIterable<Uint8Array>, peerStreams: PeerStreams) {
268
268
  try {
269
269
  await pipe(
270
270
  stream,
271
271
  async (source) => {
272
272
  for await (const data of source) {
273
- const rpcMsg = this._decodeRpc(data)
273
+ const rpcMsg = this.decodeRpc(data)
274
+ const messages: RPCMessage[] = []
275
+
276
+ for (const msg of (rpcMsg.messages ?? [])) {
277
+ if (msg.from == null || msg.data == null || msg.topic == null) {
278
+ this.log('message from %p was missing from, data or topic fields, dropping', peerId)
279
+ continue
280
+ }
281
+
282
+ messages.push({
283
+ from: msg.from,
284
+ data: msg.data,
285
+ topic: msg.topic,
286
+ seqno: msg.seqno ?? undefined,
287
+ signature: msg.signature ?? undefined,
288
+ key: msg.key ?? undefined
289
+ })
290
+ }
274
291
 
275
- // Since _processRpc may be overridden entirely in unsafe ways,
292
+ // Since processRpc may be overridden entirely in unsafe ways,
276
293
  // the simplest/safest option here is to wrap in a function and capture all errors
277
294
  // to prevent a top-level unhandled exception
278
295
  // This processing of rpc messages should happen without awaiting full validation/execution of prior messages
279
296
  this.processRpc(peerId, peerStreams, {
280
297
  subscriptions: (rpcMsg.subscriptions).map(sub => ({
281
298
  subscribe: Boolean(sub.subscribe),
282
- topicID: sub.topicID ?? ''
299
+ topic: sub.topic ?? ''
283
300
  })),
284
- msgs: (rpcMsg.msgs ?? []).map(msg => ({
285
- from: msg.from ?? peerId.multihash.bytes,
286
- data: msg.data ?? new Uint8Array(0),
287
- topicIDs: msg.topicIDs ?? [],
288
- seqno: msg.seqno ?? undefined,
289
- signature: msg.signature ?? undefined,
290
- key: msg.key ?? undefined
291
- }))
301
+ messages
292
302
  })
293
303
  .catch(err => this.log(err))
294
304
  }
@@ -303,56 +313,54 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
303
313
  * Handles an rpc request from a peer
304
314
  */
305
315
  async processRpc (from: PeerId, peerStreams: PeerStreams, rpc: RPC) {
316
+ if (!this.acceptFrom(from)) {
317
+ this.log('received message from unacceptable peer %p', from)
318
+ return
319
+ }
320
+
306
321
  this.log('rpc from %p', from)
307
- const subs = rpc.subscriptions
308
- const msgs = rpc.msgs
309
322
 
310
- if (subs.length > 0) {
323
+ const { subscriptions, messages } = rpc
324
+
325
+ if (subscriptions.length > 0) {
326
+ this.log('subscription update from %p', from)
327
+
311
328
  // update peer subscriptions
312
- subs.forEach((subOpt) => {
313
- this._processRpcSubOpt(from, subOpt)
329
+ subscriptions.forEach((subOpt) => {
330
+ this.processRpcSubOpt(from, subOpt)
314
331
  })
315
- this.dispatchEvent(new CustomEvent('pubsub:subscription-change', {
316
- detail: { peerId: peerStreams.id, subscriptions: subs }
317
- }))
318
- }
319
332
 
320
- if (!this._acceptFrom(from)) {
321
- this.log('received message from unacceptable peer %p', from)
322
- return false
333
+ super.dispatchEvent(new CustomEvent('pubsub:subscription-change', {
334
+ detail: { peerId: peerStreams.id, subscriptions }
335
+ }))
323
336
  }
324
337
 
325
- if (msgs.length > 0) {
326
- this.queue.addAll(msgs.map(message => async () => {
327
- const topics = message.topicIDs != null ? message.topicIDs : []
328
- const hasSubscription = topics.some((topic) => this.subscriptions.has(topic))
338
+ if (messages.length > 0) {
339
+ this.log('messages from %p', from)
329
340
 
330
- if (!hasSubscription && !this.canRelayMessage) {
341
+ this.queue.addAll(messages.map(message => async () => {
342
+ if (!this.subscriptions.has(message.topic) && !this.canRelayMessage) {
331
343
  this.log('received message we didn\'t subscribe to. Dropping.')
332
344
  return
333
345
  }
334
346
 
335
347
  try {
336
- const msg = toMessage({
337
- ...message,
338
- from: from.multihash.bytes
339
- })
348
+ const msg = await toMessage(message)
340
349
 
341
- await this._processMessage(msg)
350
+ await this.processMessage(from, msg)
342
351
  } catch (err: any) {
343
352
  this.log.error(err)
344
353
  }
345
354
  }))
346
355
  .catch(err => this.log(err))
347
356
  }
348
- return true
349
357
  }
350
358
 
351
359
  /**
352
360
  * Handles a subscription change from a peer
353
361
  */
354
- _processRpcSubOpt (id: PeerId, subOpt: RPCSubscription) {
355
- const t = subOpt.topicID
362
+ processRpcSubOpt (id: PeerId, subOpt: RPCSubscription) {
363
+ const t = subOpt.topic
356
364
 
357
365
  if (t == null) {
358
366
  return
@@ -376,8 +384,8 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
376
384
  /**
377
385
  * Handles an message from a peer
378
386
  */
379
- async _processMessage (msg: Message) {
380
- if (this.peerId.equals(msg.from) && !this.emitSelf) {
387
+ async processMessage (from: PeerId, msg: Message) {
388
+ if (this.peerId.equals(from) && !this.emitSelf) {
381
389
  return
382
390
  }
383
391
 
@@ -389,23 +397,17 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
389
397
  return
390
398
  }
391
399
 
392
- // Emit to self
393
- this.emitMessage(msg)
394
-
395
- return await this._publish(toRpcMessage(msg))
396
- }
400
+ if (this.subscriptions.has(msg.topic)) {
401
+ const isFromSelf = this.peerId.equals(from)
397
402
 
398
- /**
399
- * Emit a message from a peer
400
- */
401
- emitMessage (message: Message) {
402
- message.topicIDs.forEach((topic) => {
403
- if (this.subscriptions.has(topic)) {
404
- this.dispatchEvent(new CustomEvent(topic, {
405
- detail: message
403
+ if (!isFromSelf || this.emitSelf) {
404
+ super.dispatchEvent(new CustomEvent(msg.topic, {
405
+ detail: msg
406
406
  }))
407
407
  }
408
- })
408
+ }
409
+
410
+ await this.publishMessage(from, msg)
409
411
  }
410
412
 
411
413
  /**
@@ -420,7 +422,11 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
420
422
  throw errcode(new Error('Need seqno when signature policy is StrictSign but it was missing'), codes.ERR_MISSING_SEQNO)
421
423
  }
422
424
 
423
- return msgId(msg.from, msg.seqno)
425
+ if (msg.key == null) {
426
+ throw errcode(new Error('Need key when signature policy is StrictSign but it was missing'), codes.ERR_MISSING_KEY)
427
+ }
428
+
429
+ return msgId(msg.key, msg.seqno)
424
430
  case 'StrictNoSign':
425
431
  return noSignMsgId(msg.data)
426
432
  default:
@@ -432,7 +438,7 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
432
438
  * Whether to accept a message from a peer
433
439
  * Override to create a graylist
434
440
  */
435
- _acceptFrom (id: PeerId) {
441
+ acceptFrom (id: PeerId) {
436
442
  return true
437
443
  }
438
444
 
@@ -440,7 +446,7 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
440
446
  * Decode Uint8Array into an RPC object.
441
447
  * This can be override to use a custom router protobuf.
442
448
  */
443
- _decodeRpc (bytes: Uint8Array) {
449
+ decodeRpc (bytes: Uint8Array) {
444
450
  return RPCProto.decode(bytes)
445
451
  }
446
452
 
@@ -448,14 +454,26 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
448
454
  * Encode RPC object into a Uint8Array.
449
455
  * This can be override to use a custom router protobuf.
450
456
  */
451
- _encodeRpc (rpc: IRPC) {
457
+ encodeRpc (rpc: IRPC) {
452
458
  return RPCProto.encode(rpc).finish()
453
459
  }
454
460
 
455
461
  /**
456
462
  * Send an rpc object to a peer
457
463
  */
458
- _sendRpc (peer: PeerId, rpc: IRPC) {
464
+ send (peer: PeerId, data: { messages?: Message[], subscriptions?: string[], subscribe?: boolean }) {
465
+ const { messages, subscriptions, subscribe } = data
466
+
467
+ return this.sendRpc(peer, {
468
+ subscriptions: (subscriptions ?? []).map(str => ({ topic: str, subscribe: Boolean(subscribe) })),
469
+ messages: (messages ?? []).map(toRpcMessage)
470
+ })
471
+ }
472
+
473
+ /**
474
+ * Send an rpc object to a peer
475
+ */
476
+ sendRpc (peer: PeerId, rpc: IRPC) {
459
477
  const peerStreams = this.peers.get(peer)
460
478
 
461
479
  if (peerStreams == null || !peerStreams.isWritable) {
@@ -465,16 +483,7 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
465
483
  return
466
484
  }
467
485
 
468
- peerStreams.write(this._encodeRpc(rpc))
469
- }
470
-
471
- /**
472
- * Send subscriptions to a peer
473
- */
474
- _sendSubscriptions (id: PeerId, topics: string[], subscribe: boolean) {
475
- return this._sendRpc(id, {
476
- subscriptions: topics.map(t => ({ topicID: t, subscribe: subscribe }))
477
- })
486
+ peerStreams.write(this.encodeRpc(rpc))
478
487
  }
479
488
 
480
489
  /**
@@ -510,11 +519,10 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
510
519
  throw errcode(new Error('Cannot validate message: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
511
520
  }
512
521
 
513
- for (const topic of message.topicIDs) {
514
- const validatorFn = this.topicValidators.get(topic)
515
- if (validatorFn != null) {
516
- await validatorFn(topic, message)
517
- }
522
+ const validatorFn = this.topicValidators.get(message.topic)
523
+
524
+ if (validatorFn != null) {
525
+ await validatorFn(message.topic, message)
518
526
  }
519
527
  }
520
528
 
@@ -522,7 +530,7 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
522
530
  * Normalizes the message and signs it, if signing is enabled.
523
531
  * Should be used by the routers to create the message to send.
524
532
  */
525
- protected async _maybeSignMessage (message: Message) {
533
+ async buildMessage (message: Message) {
526
534
  const signaturePolicy = this.globalSignaturePolicy
527
535
  switch (signaturePolicy) {
528
536
  case 'StrictSign':
@@ -549,7 +557,7 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
549
557
  throw errcode(new Error('topic is required'), 'ERR_NOT_VALID_TOPIC')
550
558
  }
551
559
 
552
- const peersInTopic = this.topics.get(topic)
560
+ const peersInTopic = this.topics.get(topic.toString())
553
561
 
554
562
  if (peersInTopic == null) {
555
563
  return []
@@ -561,34 +569,55 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
561
569
  /**
562
570
  * Publishes messages to all subscribed peers
563
571
  */
564
- async publish (topic: string, message: Uint8Array) {
572
+ dispatchEvent (event: CustomEvent): boolean {
565
573
  if (!this.started) {
566
574
  throw new Error('Pubsub has not started')
567
575
  }
568
576
 
569
- this.log('publish', topic, message)
577
+ const topic = event.type
578
+ let message: Message = event.detail
570
579
 
571
- const msgObject = {
572
- from: this.peerId,
573
- data: message,
574
- topicIDs: [topic]
580
+ if (message instanceof Uint8Array) {
581
+ message = {
582
+ from: this.peerId,
583
+ topic,
584
+ data: message
585
+ }
575
586
  }
576
587
 
577
- // ensure that the message follows the signature policy
578
- const msg = await this._maybeSignMessage(msgObject)
588
+ this.log('publish', topic, message)
589
+
590
+ Promise.resolve().then(async () => {
591
+ message = await this.buildMessage(message)
592
+
593
+ // dispatch the event if we are interested
594
+ if (this.emitSelf) {
595
+ if (this.subscriptions.has(topic)) {
596
+ super.dispatchEvent(new CustomEvent(topic, {
597
+ detail: message
598
+ }))
579
599
 
580
- // Emit to self if I'm interested and emitSelf enabled
581
- this.emitSelf && this.emitMessage(msg)
600
+ if (this.listenerCount(topic) === 0) {
601
+ this.unsubscribe(topic)
602
+ }
603
+ }
604
+ }
582
605
 
583
- // send to all the other peers
584
- await this._publish(toRpcMessage(msg))
606
+ // send to all the other peers
607
+ await this.publishMessage(this.peerId, message)
608
+ })
609
+ .catch(err => {
610
+ this.log.error(err)
611
+ })
612
+
613
+ return true
585
614
  }
586
615
 
587
616
  /**
588
617
  * Overriding the implementation of publish should handle the appropriate algorithms for the publish/subscriber implementation.
589
618
  * For example, a Floodsub implementation might simply publish each message to each topic for every peer
590
619
  */
591
- abstract _publish (message: RPCMessage): Promise<void>
620
+ abstract publishMessage (peerId: PeerId, message: Message): Promise<void>
592
621
 
593
622
  /**
594
623
  * Subscribes to a given topic.
@@ -598,28 +627,48 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
598
627
  throw new Error('Pubsub has not started')
599
628
  }
600
629
 
601
- if (!this.subscriptions.has(topic)) {
602
- this.subscriptions.add(topic)
630
+ const topicStr = topic.toString()
631
+
632
+ if (topicStr === 'pubsub:subscription-change') {
633
+ return
634
+ }
635
+
636
+ if (!this.subscriptions.has(topicStr)) {
637
+ this.subscriptions.add(topicStr)
603
638
 
604
639
  for (const peerId of this.peers.keys()) {
605
- this._sendSubscriptions(peerId, [topic], true)
640
+ this.send(peerId, { subscriptions: [topicStr], subscribe: true })
606
641
  }
607
642
  }
608
643
  }
609
644
 
610
645
  /**
611
- * Unsubscribe from the given topic.
646
+ * Unsubscribe from the given topic
612
647
  */
613
648
  unsubscribe (topic: string) {
614
649
  if (!this.started) {
615
650
  throw new Error('Pubsub is not started')
616
651
  }
617
652
 
618
- if (this.subscriptions.has(topic) && this.listenerCount(topic) === 0) {
619
- this.subscriptions.delete(topic)
653
+ // @ts-expect-error topic should be a key of the event map
654
+ super.removeEventListener(topic)
655
+
656
+ const topicStr = topic.toString()
657
+
658
+ if (topicStr === 'pubsub:subscription-change') {
659
+ return
660
+ }
661
+
662
+ const wasSubscribed = this.subscriptions.has(topicStr)
663
+ const listeners = this.listenerCount(topicStr)
664
+
665
+ this.log('unsubscribe from %s - am subscribed %s, listeners %d', topic, wasSubscribed, listeners)
666
+
667
+ if (wasSubscribed && listeners === 0) {
668
+ this.subscriptions.delete(topicStr)
620
669
 
621
670
  for (const peerId of this.peers.keys()) {
622
- this._sendSubscriptions(peerId, [topic], false)
671
+ this.send(peerId, { subscriptions: [topicStr], subscribe: false })
623
672
  }
624
673
  }
625
674
  }
@@ -642,4 +691,20 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
642
691
 
643
692
  return Array.from(this.peers.keys())
644
693
  }
694
+
695
+ addEventListener<U extends keyof EventMap> (type: U, callback: EventHandler<EventMap[U]>, options?: AddEventListenerOptions | boolean) {
696
+ this.subscribe(type.toString())
697
+
698
+ super.addEventListener(type, callback, options)
699
+ }
700
+
701
+ removeEventListener<U extends keyof EventMap> (type: U, callback: EventHandler<EventMap[U]> | undefined, options?: EventListenerOptions | boolean) {
702
+ super.removeEventListener(type, callback, options)
703
+
704
+ const topicStr = type.toString()
705
+
706
+ if (this.listenerCount(topicStr) === 0) {
707
+ this.unsubscribe(topicStr)
708
+ }
709
+ }
645
710
  }