@libp2p/pubsub 1.2.3 → 1.2.7

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,13 +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
- import { RPC, IRPC } from './message/rpc.js'
9
- import { PeerStreams } from './peer-streams.js'
10
- import * as utils from './utils.js'
8
+ import { PeerStreams as PeerStreamsImpl } from './peer-streams.js'
9
+ import { toMessage, ensureArray, randomSeqno, noSignMsgId, msgId, toRpcMessage } from './utils.js'
11
10
  import {
12
11
  signMessage,
13
12
  verifySignature
@@ -15,9 +14,14 @@ import {
15
14
  import type { PeerId } from '@libp2p/interfaces/peer-id'
16
15
  import type { Registrar, IncomingStreamData } from '@libp2p/interfaces/registrar'
17
16
  import type { Connection } from '@libp2p/interfaces/connection'
18
- import type BufferList from 'bl'
19
- import type { PubSub, Message, StrictNoSign, StrictSign, PubsubOptions, PubsubEvents } from '@libp2p/interfaces/pubsub'
17
+ import type { PubSub, Message, StrictNoSign, StrictSign, PubSubOptions, PubSubEvents, RPC, PeerStreams, RPCSubscription, RPCMessage } from '@libp2p/interfaces/pubsub'
20
18
  import type { Logger } from '@libp2p/logger'
19
+ import { base58btc } from 'multiformats/bases/base58'
20
+ import { peerMap } from '@libp2p/peer-map'
21
+ import type { PeerMap } from '@libp2p/peer-map'
22
+ import { peerIdFromString } from '@libp2p/peer-id'
23
+ import type { IRPC } from './message/rpc.js'
24
+ import { RPC as RPCProto } from './message/rpc.js'
21
25
 
22
26
  export interface TopicValidator { (topic: string, message: Message): Promise<void> }
23
27
 
@@ -25,7 +29,7 @@ export interface TopicValidator { (topic: string, message: Message): Promise<voi
25
29
  * PubsubBaseProtocol handles the peers and connections logic for pubsub routers
26
30
  * and specifies the API that pubsub routers should have.
27
31
  */
28
- 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> {
29
33
  public peerId: PeerId
30
34
  public started: boolean
31
35
  /**
@@ -39,11 +43,11 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
39
43
  /**
40
44
  * Map of peer streams
41
45
  */
42
- public peers: Map<string, PeerStreams>
46
+ public peers: PeerMap<PeerStreams>
43
47
  /**
44
48
  * The signature policy to follow by default
45
49
  */
46
- public globalSignaturePolicy: StrictNoSign | StrictSign
50
+ public globalSignaturePolicy: typeof StrictNoSign | typeof StrictSign
47
51
  /**
48
52
  * If router can relay received messages, even if not subscribed
49
53
  */
@@ -61,20 +65,21 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
61
65
  public topicValidators: Map<string, TopicValidator>
62
66
  public queue: Queue
63
67
  public registrar: Registrar
68
+ public multicodecs: string[]
64
69
 
65
70
  protected log: Logger
66
- protected multicodecs: string[]
67
71
  protected _libp2p: any
68
72
  private _registrarHandlerId: string | undefined
69
73
  private _registrarTopologyId: string | undefined
70
74
 
71
- constructor (props: PubsubOptions) {
75
+ constructor (props: PubSubOptions) {
72
76
  super()
73
77
 
74
78
  const {
75
79
  debugName = 'libp2p:pubsub',
76
80
  multicodecs = [],
77
- libp2p = null,
81
+ peerId,
82
+ registrar,
78
83
  globalSignaturePolicy = 'StrictSign',
79
84
  canRelayMessage = false,
80
85
  emitSelf = false,
@@ -82,14 +87,13 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
82
87
  } = props
83
88
 
84
89
  this.log = logger(debugName)
85
- this.multicodecs = utils.ensureArray(multicodecs)
86
- this._libp2p = libp2p
87
- this.registrar = libp2p.registrar
88
- this.peerId = libp2p.peerId
90
+ this.multicodecs = ensureArray(multicodecs)
91
+ this.registrar = registrar
92
+ this.peerId = peerId
89
93
  this.started = false
90
94
  this.topics = new Map()
91
95
  this.subscriptions = new Set()
92
- this.peers = new Map()
96
+ this.peers = peerMap<PeerStreams>()
93
97
  this.globalSignaturePolicy = globalSignaturePolicy === 'StrictNoSign' ? 'StrictNoSign' : 'StrictSign'
94
98
  this.canRelayMessage = canRelayMessage
95
99
  this.emitSelf = emitSelf
@@ -148,9 +152,11 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
148
152
  }
149
153
 
150
154
  this.log('stopping')
151
- this.peers.forEach((peerStreams) => peerStreams.close())
155
+ for (const peerStreams of this.peers.values()) {
156
+ peerStreams.close()
157
+ }
152
158
 
153
- this.peers = new Map()
159
+ this.peers.clear()
154
160
  this.subscriptions = new Set()
155
161
  this.started = false
156
162
  this.log('stopped')
@@ -166,11 +172,10 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
166
172
  protected _onIncomingStream (evt: CustomEvent<IncomingStreamData>) {
167
173
  const { protocol, stream, connection } = evt.detail
168
174
  const peerId = connection.remotePeer
169
- const idB58Str = peerId.toString()
170
- const peer = this._addPeer(peerId, protocol)
175
+ const peer = this.addPeer(peerId, protocol)
171
176
  const inboundStream = peer.attachInboundStream(stream)
172
177
 
173
- this._processMessages(idB58Str, inboundStream, peer)
178
+ this.processMessages(peerId, inboundStream, peer)
174
179
  .catch(err => this.log(err))
175
180
  }
176
181
 
@@ -178,19 +183,18 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
178
183
  * Registrar notifies an established connection with pubsub protocol
179
184
  */
180
185
  protected async _onPeerConnected (peerId: PeerId, conn: Connection) {
181
- const idB58Str = peerId.toString()
182
- this.log('connected', idB58Str)
186
+ this.log('connected %p', peerId)
183
187
 
184
188
  try {
185
189
  const { stream, protocol } = await conn.newStream(this.multicodecs)
186
- const peer = this._addPeer(peerId, protocol)
190
+ const peer = this.addPeer(peerId, protocol)
187
191
  await peer.attachOutboundStream(stream)
188
192
  } catch (err: any) {
189
193
  this.log.error(err)
190
194
  }
191
195
 
192
196
  // Immediately send my own subscriptions to the newly established conn
193
- this._sendSubscriptions(idB58Str, Array.from(this.subscriptions), true)
197
+ this.send(peerId, { subscriptions: Array.from(this.subscriptions).map(sub => sub.toString()), subscribe: true })
194
198
  }
195
199
 
196
200
  /**
@@ -206,9 +210,8 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
206
210
  /**
207
211
  * Notifies the router that a peer has been connected
208
212
  */
209
- protected _addPeer (peerId: PeerId, protocol: string) {
210
- const id = peerId.toString()
211
- const existing = this.peers.get(id)
213
+ addPeer (peerId: PeerId, protocol: string): PeerStreams {
214
+ const existing = this.peers.get(peerId)
212
215
 
213
216
  // If peer streams already exists, do nothing
214
217
  if (existing != null) {
@@ -216,14 +219,14 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
216
219
  }
217
220
 
218
221
  // else create a new peer streams
219
- this.log('new peer', id)
222
+ this.log('new peer %p', peerId)
220
223
 
221
- const peerStreams = new PeerStreams({
224
+ const peerStreams: PeerStreams = new PeerStreamsImpl({
222
225
  id: peerId,
223
226
  protocol
224
227
  })
225
228
 
226
- this.peers.set(id, peerStreams)
229
+ this.peers.set(peerId, peerStreams)
227
230
  peerStreams.addEventListener('close', () => this._removePeer(peerId), {
228
231
  once: true
229
232
  })
@@ -236,7 +239,7 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
236
239
  */
237
240
  protected _removePeer (peerId: PeerId) {
238
241
  const id = peerId.toString()
239
- const peerStreams = this.peers.get(id)
242
+ const peerStreams = this.peers.get(peerId)
240
243
  if (peerStreams == null) {
241
244
  return
242
245
  }
@@ -245,8 +248,8 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
245
248
  peerStreams.close()
246
249
 
247
250
  // delete peer streams
248
- this.log('delete peer', id)
249
- this.peers.delete(id)
251
+ this.log('delete peer %p', peerId)
252
+ this.peers.delete(peerId)
250
253
 
251
254
  // remove peer from topics map
252
255
  for (const peers of this.topics.values()) {
@@ -261,20 +264,42 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
261
264
  /**
262
265
  * Responsible for processing each RPC message received by other peers.
263
266
  */
264
- async _processMessages (idB58Str: string, stream: AsyncIterable<Uint8Array|BufferList>, peerStreams: PeerStreams) {
267
+ async processMessages (peerId: PeerId, stream: AsyncIterable<Uint8Array>, peerStreams: PeerStreams) {
265
268
  try {
266
269
  await pipe(
267
270
  stream,
268
271
  async (source) => {
269
272
  for await (const data of source) {
270
- const rpcBytes = data instanceof Uint8Array ? data : data.slice()
271
- const rpcMsg = this._decodeRpc(rpcBytes)
272
-
273
- // Since _processRpc may be overridden entirely in unsafe ways,
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
+ }
291
+
292
+ // Since processRpc may be overridden entirely in unsafe ways,
274
293
  // the simplest/safest option here is to wrap in a function and capture all errors
275
294
  // to prevent a top-level unhandled exception
276
295
  // This processing of rpc messages should happen without awaiting full validation/execution of prior messages
277
- this._processRpc(idB58Str, peerStreams, rpcMsg)
296
+ this.processRpc(peerId, peerStreams, {
297
+ subscriptions: (rpcMsg.subscriptions).map(sub => ({
298
+ subscribe: Boolean(sub.subscribe),
299
+ topic: sub.topic ?? ''
300
+ })),
301
+ messages
302
+ })
278
303
  .catch(err => this.log(err))
279
304
  }
280
305
  }
@@ -287,54 +312,55 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
287
312
  /**
288
313
  * Handles an rpc request from a peer
289
314
  */
290
- async _processRpc (idB58Str: string, peerStreams: PeerStreams, rpc: RPC) {
291
- this.log('rpc from', idB58Str)
292
- const subs = rpc.subscriptions
293
- const msgs = rpc.msgs
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
+
321
+ this.log('rpc from %p', from)
322
+
323
+ const { subscriptions, messages } = rpc
324
+
325
+ if (subscriptions.length > 0) {
326
+ this.log('subscription update from %p', from)
294
327
 
295
- if (subs.length > 0) {
296
328
  // update peer subscriptions
297
- subs.forEach((subOpt) => {
298
- this._processRpcSubOpt(idB58Str, subOpt)
329
+ subscriptions.forEach((subOpt) => {
330
+ this.processRpcSubOpt(from, subOpt)
299
331
  })
300
- this.dispatchEvent(new CustomEvent('pubsub:subscription-change', {
301
- detail: { peerId: peerStreams.id, subscriptions: subs }
302
- }))
303
- }
304
332
 
305
- if (!this._acceptFrom(idB58Str)) {
306
- this.log('received message from unacceptable peer %s', idB58Str)
307
- return false
333
+ super.dispatchEvent(new CustomEvent('pubsub:subscription-change', {
334
+ detail: { peerId: peerStreams.id, subscriptions }
335
+ }))
308
336
  }
309
337
 
310
- if (msgs.length > 0) {
311
- this.queue.addAll(msgs.map(message => async () => {
312
- const topics = message.topicIDs != null ? message.topicIDs : []
313
- const hasSubscription = topics.some((topic) => this.subscriptions.has(topic))
338
+ if (messages.length > 0) {
339
+ this.log('messages from %p', from)
314
340
 
315
- if (!hasSubscription && !this.canRelayMessage) {
341
+ this.queue.addAll(messages.map(message => async () => {
342
+ if (!this.subscriptions.has(message.topic) && !this.canRelayMessage) {
316
343
  this.log('received message we didn\'t subscribe to. Dropping.')
317
344
  return
318
345
  }
319
346
 
320
347
  try {
321
- const msg = utils.normalizeInRpcMessage(message, idB58Str)
348
+ const msg = await toMessage(message)
322
349
 
323
- await this._processRpcMessage(msg)
350
+ await this.processMessage(from, msg)
324
351
  } catch (err: any) {
325
352
  this.log.error(err)
326
353
  }
327
354
  }))
328
355
  .catch(err => this.log(err))
329
356
  }
330
- return true
331
357
  }
332
358
 
333
359
  /**
334
360
  * Handles a subscription change from a peer
335
361
  */
336
- _processRpcSubOpt (id: string, subOpt: RPC.ISubOpts) {
337
- const t = subOpt.topicID
362
+ processRpcSubOpt (id: PeerId, subOpt: RPCSubscription) {
363
+ const t = subOpt.topic
338
364
 
339
365
  if (t == null) {
340
366
  return
@@ -346,20 +372,20 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
346
372
  this.topics.set(t, topicSet)
347
373
  }
348
374
 
349
- if (subOpt.subscribe === true) {
375
+ if (subOpt.subscribe) {
350
376
  // subscribe peer to new topic
351
- topicSet.add(id)
377
+ topicSet.add(id.toString())
352
378
  } else {
353
379
  // unsubscribe from existing topic
354
- topicSet.delete(id)
380
+ topicSet.delete(id.toString())
355
381
  }
356
382
  }
357
383
 
358
384
  /**
359
385
  * Handles an message from a peer
360
386
  */
361
- async _processRpcMessage (msg: Message) {
362
- if ((msg.from != null) && this.peerId.equals(msg.from) && !this.emitSelf) {
387
+ async processMessage (from: PeerId, msg: Message) {
388
+ if (this.peerId.equals(from) && !this.emitSelf) {
363
389
  return
364
390
  }
365
391
 
@@ -371,23 +397,17 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
371
397
  return
372
398
  }
373
399
 
374
- // Emit to self
375
- this._emitMessage(msg)
376
-
377
- return await this._publish(utils.normalizeOutRpcMessage(msg))
378
- }
400
+ if (this.subscriptions.has(msg.topic)) {
401
+ const isFromSelf = this.peerId.equals(from)
379
402
 
380
- /**
381
- * Emit a message from a peer
382
- */
383
- _emitMessage (message: Message) {
384
- message.topicIDs.forEach((topic) => {
385
- if (this.subscriptions.has(topic)) {
386
- this.dispatchEvent(new CustomEvent(topic, {
387
- detail: message
403
+ if (!isFromSelf || this.emitSelf) {
404
+ super.dispatchEvent(new CustomEvent(msg.topic, {
405
+ detail: msg
388
406
  }))
389
407
  }
390
- })
408
+ }
409
+
410
+ await this.publishMessage(from, msg)
391
411
  }
392
412
 
393
413
  /**
@@ -398,10 +418,17 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
398
418
  const signaturePolicy = this.globalSignaturePolicy
399
419
  switch (signaturePolicy) {
400
420
  case 'StrictSign':
401
- // @ts-expect-error seqno is optional in protobuf definition but it will exist
402
- return utils.msgId(msg.from, msg.seqno)
421
+ if (msg.seqno == null) {
422
+ throw errcode(new Error('Need seqno when signature policy is StrictSign but it was missing'), codes.ERR_MISSING_SEQNO)
423
+ }
424
+
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)
403
430
  case 'StrictNoSign':
404
- return utils.noSignMsgId(msg.data)
431
+ return noSignMsgId(msg.data)
405
432
  default:
406
433
  throw errcode(new Error('Cannot get message id: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
407
434
  }
@@ -411,7 +438,7 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
411
438
  * Whether to accept a message from a peer
412
439
  * Override to create a graylist
413
440
  */
414
- _acceptFrom (id: string) {
441
+ acceptFrom (id: PeerId) {
415
442
  return true
416
443
  }
417
444
 
@@ -419,39 +446,44 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
419
446
  * Decode Uint8Array into an RPC object.
420
447
  * This can be override to use a custom router protobuf.
421
448
  */
422
- _decodeRpc (bytes: Uint8Array) {
423
- return RPC.decode(bytes)
449
+ decodeRpc (bytes: Uint8Array) {
450
+ return RPCProto.decode(bytes)
424
451
  }
425
452
 
426
453
  /**
427
454
  * Encode RPC object into a Uint8Array.
428
455
  * This can be override to use a custom router protobuf.
429
456
  */
430
- _encodeRpc (rpc: IRPC) {
431
- return RPC.encode(rpc).finish()
457
+ encodeRpc (rpc: IRPC) {
458
+ return RPCProto.encode(rpc).finish()
432
459
  }
433
460
 
434
461
  /**
435
462
  * Send an rpc object to a peer
436
463
  */
437
- _sendRpc (id: string, rpc: IRPC) {
438
- const peerStreams = this.peers.get(id)
439
- if ((peerStreams == null) || !peerStreams.isWritable) {
440
- const msg = `Cannot send RPC to ${id} as there is no open stream to it available`
464
+ send (peer: PeerId, data: { messages?: Message[], subscriptions?: string[], subscribe?: boolean }) {
465
+ const { messages, subscriptions, subscribe } = data
441
466
 
442
- this.log.error(msg)
443
- return
444
- }
445
- peerStreams.write(this._encodeRpc(rpc))
467
+ return this.sendRpc(peer, {
468
+ subscriptions: (subscriptions ?? []).map(str => ({ topic: str, subscribe: Boolean(subscribe) })),
469
+ messages: (messages ?? []).map(toRpcMessage)
470
+ })
446
471
  }
447
472
 
448
473
  /**
449
- * Send subscriptions to a peer
474
+ * Send an rpc object to a peer
450
475
  */
451
- _sendSubscriptions (id: string, topics: string[], subscribe: boolean) {
452
- return this._sendRpc(id, {
453
- subscriptions: topics.map(t => ({ topicID: t, subscribe: subscribe }))
454
- })
476
+ sendRpc (peer: PeerId, rpc: IRPC) {
477
+ const peerStreams = this.peers.get(peer)
478
+
479
+ if (peerStreams == null || !peerStreams.isWritable) {
480
+ const msg = `Cannot send RPC to ${peer.toString(base58btc)} as there is no open stream to it available`
481
+
482
+ this.log.error(msg)
483
+ return
484
+ }
485
+
486
+ peerStreams.write(this.encodeRpc(rpc))
455
487
  }
456
488
 
457
489
  /**
@@ -462,9 +494,6 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
462
494
  const signaturePolicy = this.globalSignaturePolicy
463
495
  switch (signaturePolicy) {
464
496
  case 'StrictNoSign':
465
- if (message.from != null) {
466
- throw errcode(new Error('StrictNoSigning: from should not be present'), codes.ERR_UNEXPECTED_FROM)
467
- }
468
497
  if (message.signature != null) {
469
498
  throw errcode(new Error('StrictNoSigning: signature should not be present'), codes.ERR_UNEXPECTED_SIGNATURE)
470
499
  }
@@ -490,11 +519,10 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
490
519
  throw errcode(new Error('Cannot validate message: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
491
520
  }
492
521
 
493
- for (const topic of message.topicIDs) {
494
- const validatorFn = this.topicValidators.get(topic)
495
- if (validatorFn != null) {
496
- await validatorFn(topic, message)
497
- }
522
+ const validatorFn = this.topicValidators.get(message.topic)
523
+
524
+ if (validatorFn != null) {
525
+ await validatorFn(message.topic, message)
498
526
  }
499
527
  }
500
528
 
@@ -502,12 +530,11 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
502
530
  * Normalizes the message and signs it, if signing is enabled.
503
531
  * Should be used by the routers to create the message to send.
504
532
  */
505
- protected async _buildMessage (message: Message) {
533
+ async buildMessage (message: Message) {
506
534
  const signaturePolicy = this.globalSignaturePolicy
507
535
  switch (signaturePolicy) {
508
536
  case 'StrictSign':
509
- message.from = this.peerId.multihash.bytes
510
- message.seqno = utils.randomSeqno()
537
+ message.seqno = randomSeqno()
511
538
  return await signMessage(this.peerId, message)
512
539
  case 'StrictNoSign':
513
540
  return await Promise.resolve(message)
@@ -530,48 +557,67 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
530
557
  throw errcode(new Error('topic is required'), 'ERR_NOT_VALID_TOPIC')
531
558
  }
532
559
 
533
- const peersInTopic = this.topics.get(topic)
560
+ const peersInTopic = this.topics.get(topic.toString())
534
561
 
535
562
  if (peersInTopic == null) {
536
563
  return []
537
564
  }
538
565
 
539
- return Array.from(peersInTopic)
566
+ return Array.from(peersInTopic).map(str => peerIdFromString(str))
540
567
  }
541
568
 
542
569
  /**
543
570
  * Publishes messages to all subscribed peers
544
571
  */
545
- async publish (topic: string, message: Uint8Array) {
572
+ dispatchEvent (event: CustomEvent): boolean {
546
573
  if (!this.started) {
547
574
  throw new Error('Pubsub has not started')
548
575
  }
549
576
 
550
- this.log('publish', topic, message)
577
+ const topic = event.type
578
+ let message: Message = event.detail
551
579
 
552
- const from = this.peerId.toString()
553
- const msgObject = {
554
- receivedFrom: from,
555
- data: message,
556
- topicIDs: [topic]
580
+ if (message instanceof Uint8Array) {
581
+ message = {
582
+ from: this.peerId,
583
+ topic,
584
+ data: message
585
+ }
557
586
  }
558
587
 
559
- // ensure that the message follows the signature policy
560
- const outMsg = await this._buildMessage(msgObject)
561
- const msg = utils.normalizeInRpcMessage(outMsg)
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
+ }))
599
+
600
+ if (this.listenerCount(topic) === 0) {
601
+ this.unsubscribe(topic)
602
+ }
603
+ }
604
+ }
562
605
 
563
- // Emit to self if I'm interested and emitSelf enabled
564
- this.emitSelf && this._emitMessage(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
+ })
565
612
 
566
- // send to all the other peers
567
- await this._publish(msg)
613
+ return true
568
614
  }
569
615
 
570
616
  /**
571
617
  * Overriding the implementation of publish should handle the appropriate algorithms for the publish/subscriber implementation.
572
618
  * For example, a Floodsub implementation might simply publish each message to each topic for every peer
573
619
  */
574
- abstract _publish (message: Message): Promise<void>
620
+ abstract publishMessage (peerId: PeerId, message: Message): Promise<void>
575
621
 
576
622
  /**
577
623
  * Subscribes to a given topic.
@@ -581,23 +627,49 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
581
627
  throw new Error('Pubsub has not started')
582
628
  }
583
629
 
584
- if (!this.subscriptions.has(topic)) {
585
- this.subscriptions.add(topic)
586
- this.peers.forEach((_, id) => this._sendSubscriptions(id, [topic], true))
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)
638
+
639
+ for (const peerId of this.peers.keys()) {
640
+ this.send(peerId, { subscriptions: [topicStr], subscribe: true })
641
+ }
587
642
  }
588
643
  }
589
644
 
590
645
  /**
591
- * Unsubscribe from the given topic.
646
+ * Unsubscribe from the given topic
592
647
  */
593
648
  unsubscribe (topic: string) {
594
649
  if (!this.started) {
595
650
  throw new Error('Pubsub is not started')
596
651
  }
597
652
 
598
- if (this.subscriptions.has(topic) && this.listenerCount(topic) === 0) {
599
- this.subscriptions.delete(topic)
600
- this.peers.forEach((_, id) => this._sendSubscriptions(id, [topic], false))
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)
669
+
670
+ for (const peerId of this.peers.keys()) {
671
+ this.send(peerId, { subscriptions: [topicStr], subscribe: false })
672
+ }
601
673
  }
602
674
  }
603
675
 
@@ -611,4 +683,28 @@ export abstract class PubsubBaseProtocol<EventMap> extends EventEmitter<EventMap
611
683
 
612
684
  return Array.from(this.subscriptions)
613
685
  }
686
+
687
+ getPeers () {
688
+ if (!this.started) {
689
+ throw new Error('Pubsub is not started')
690
+ }
691
+
692
+ return Array.from(this.peers.keys())
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
+ }
614
710
  }