@libp2p/floodsub 10.1.46 → 11.0.0-55b7e5fea

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.
@@ -0,0 +1,794 @@
1
+ import { InvalidMessageError, NotStartedError, InvalidParametersError, serviceCapabilities, serviceDependencies } from '@libp2p/interface'
2
+ import { PeerMap, PeerSet } from '@libp2p/peer-collections'
3
+ import { pipe } from 'it-pipe'
4
+ import { TypedEventEmitter } from 'main-event'
5
+ import Queue from 'p-queue'
6
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
7
+ import { SimpleTimeCache } from './cache.js'
8
+ import { pubSubSymbol } from './constants.ts'
9
+ import { RPC } from './message/rpc.js'
10
+ import { PeerStreams, PeerStreams as PeerStreamsImpl } from './peer-streams.js'
11
+ import { signMessage, verifySignature } from './sign.js'
12
+ import { toMessage, ensureArray, noSignMsgId, msgId, toRpcMessage, randomSeqno } from './utils.js'
13
+ import { protocol, StrictNoSign, TopicValidatorResult, StrictSign } from './index.js'
14
+ import type { FloodSubComponents, FloodSubEvents, FloodSubInit, FloodSub as FloodSubInterface, Message, PublishResult, SubscriptionChangeData, TopicValidatorFn } from './index.js'
15
+ import type { Logger, Connection, PeerId, Stream, Topology } from '@libp2p/interface'
16
+ import type { Uint8ArrayList } from 'uint8arraylist'
17
+
18
+ export interface PubSubRPCMessage {
19
+ from?: Uint8Array
20
+ topic?: string
21
+ data?: Uint8Array
22
+ sequenceNumber?: Uint8Array
23
+ signature?: Uint8Array
24
+ key?: Uint8Array
25
+ }
26
+
27
+ export interface PubSubRPCSubscription {
28
+ subscribe?: boolean
29
+ topic?: string
30
+ }
31
+
32
+ export interface PubSubRPC {
33
+ subscriptions: PubSubRPCSubscription[]
34
+ messages: PubSubRPCMessage[]
35
+ }
36
+
37
+ /**
38
+ * PubSubBaseProtocol handles the peers and connections logic for pubsub routers
39
+ * and specifies the API that pubsub routers should have.
40
+ */
41
+ export class FloodSub extends TypedEventEmitter<FloodSubEvents> implements FloodSubInterface {
42
+ protected log: Logger
43
+
44
+ public started: boolean
45
+ /**
46
+ * Map of topics to which peers are subscribed to
47
+ */
48
+ public topics: Map<string, PeerSet>
49
+ /**
50
+ * List of our subscriptions
51
+ */
52
+ public subscriptions: Set<string>
53
+ /**
54
+ * Map of peer streams
55
+ */
56
+ public peers: PeerMap<PeerStreams>
57
+ /**
58
+ * The signature policy to follow by default
59
+ */
60
+ public globalSignaturePolicy: typeof StrictNoSign | typeof StrictSign
61
+ /**
62
+ * If router can relay received messages, even if not subscribed
63
+ */
64
+ public canRelayMessage: boolean
65
+ /**
66
+ * if publish should emit to self, if subscribed
67
+ */
68
+ public emitSelf: boolean
69
+ /**
70
+ * Topic validator map
71
+ *
72
+ * Keyed by topic
73
+ * Topic validators are functions with the following input:
74
+ */
75
+ public topicValidators: Map<string, TopicValidatorFn>
76
+ public queue: Queue
77
+ public protocols: string[]
78
+ public components: FloodSubComponents
79
+
80
+ private _registrarTopologyIds: string[] | undefined
81
+ private readonly maxInboundStreams: number
82
+ private readonly maxOutboundStreams: number
83
+ public seenCache: SimpleTimeCache<boolean>
84
+
85
+ constructor (components: FloodSubComponents, init: FloodSubInit) {
86
+ super()
87
+
88
+ this.log = components.logger.forComponent('libp2p:floodsub')
89
+ this.components = components
90
+ this.protocols = ensureArray(init.protocols ?? protocol)
91
+ this.started = false
92
+ this.topics = new Map()
93
+ this.subscriptions = new Set()
94
+ this.peers = new PeerMap<PeerStreams>()
95
+ this.globalSignaturePolicy = init.globalSignaturePolicy === 'StrictNoSign' ? 'StrictNoSign' : 'StrictSign'
96
+ this.canRelayMessage = init.canRelayMessage ?? true
97
+ this.emitSelf = init.emitSelf ?? false
98
+ this.topicValidators = new Map()
99
+ this.queue = new Queue({
100
+ concurrency: init.messageProcessingConcurrency ?? 10
101
+ })
102
+ this.maxInboundStreams = init.maxInboundStreams ?? 1
103
+ this.maxOutboundStreams = init.maxOutboundStreams ?? 1
104
+ this.seenCache = new SimpleTimeCache<boolean>({
105
+ validityMs: init?.seenTTL ?? 30000
106
+ })
107
+
108
+ this._onIncomingStream = this._onIncomingStream.bind(this)
109
+ this._onPeerConnected = this._onPeerConnected.bind(this)
110
+ this._onPeerDisconnected = this._onPeerDisconnected.bind(this)
111
+ }
112
+
113
+ readonly [pubSubSymbol] = true
114
+
115
+ readonly [Symbol.toStringTag] = '@libp2p/floodsub'
116
+
117
+ readonly [serviceCapabilities]: string[] = [
118
+ '@libp2p/pubsub'
119
+ ]
120
+
121
+ readonly [serviceDependencies]: string[] = [
122
+ '@libp2p/identify'
123
+ ]
124
+
125
+ // LIFECYCLE METHODS
126
+
127
+ /**
128
+ * Register the pubsub protocol onto the libp2p node.
129
+ */
130
+ async start (): Promise<void> {
131
+ if (this.started) {
132
+ return
133
+ }
134
+
135
+ this.log('starting')
136
+
137
+ const registrar = this.components.registrar
138
+ // Incoming streams
139
+ // Called after a peer dials us
140
+ await Promise.all(this.protocols.map(async multicodec => {
141
+ await registrar.handle(multicodec, this._onIncomingStream, {
142
+ maxInboundStreams: this.maxInboundStreams,
143
+ maxOutboundStreams: this.maxOutboundStreams
144
+ })
145
+ }))
146
+
147
+ // register protocol with topology
148
+ // Topology callbacks called on connection manager changes
149
+ const topology: Topology = {
150
+ onConnect: this._onPeerConnected,
151
+ onDisconnect: this._onPeerDisconnected
152
+ }
153
+ this._registrarTopologyIds = await Promise.all(this.protocols.map(async multicodec => registrar.register(multicodec, topology)))
154
+
155
+ this.log('started')
156
+ this.started = true
157
+ }
158
+
159
+ /**
160
+ * Unregister the pubsub protocol and the streams with other peers will be closed.
161
+ */
162
+ async stop (): Promise<void> {
163
+ if (!this.started) {
164
+ return
165
+ }
166
+
167
+ const registrar = this.components.registrar
168
+
169
+ // unregister protocol and handlers
170
+ if (this._registrarTopologyIds != null) {
171
+ this._registrarTopologyIds?.forEach(id => {
172
+ registrar.unregister(id)
173
+ })
174
+ }
175
+
176
+ await Promise.all(this.protocols.map(async multicodec => {
177
+ await registrar.unhandle(multicodec)
178
+ }))
179
+
180
+ this.log('stopping')
181
+ for (const peerStreams of this.peers.values()) {
182
+ peerStreams.close()
183
+ }
184
+
185
+ this.peers.clear()
186
+ this.subscriptions = new Set()
187
+ this.started = false
188
+ this.log('stopped')
189
+ }
190
+
191
+ isStarted (): boolean {
192
+ return this.started
193
+ }
194
+
195
+ /**
196
+ * On an inbound stream opened
197
+ */
198
+ protected _onIncomingStream (stream: Stream, connection: Connection): void {
199
+ const peerId = connection.remotePeer
200
+
201
+ if (stream.protocol == null) {
202
+ stream.abort(new Error('Stream was not multiplexed'))
203
+ return
204
+ }
205
+
206
+ const peer = this.addPeer(peerId, stream.protocol)
207
+ const inboundStream = peer.attachInboundStream(stream)
208
+
209
+ this.processMessages(peerId, inboundStream, peer)
210
+ .catch(err => { this.log(err) })
211
+ }
212
+
213
+ /**
214
+ * Registrar notifies an established connection with pubsub protocol
215
+ */
216
+ protected async _onPeerConnected (peerId: PeerId, conn: Connection): Promise<void> {
217
+ this.log('connected %p', peerId)
218
+
219
+ // if this connection is already in use for pubsub, ignore it
220
+ if (conn.streams.find(stream => stream.direction === 'outbound' && stream.protocol != null && this.protocols.includes(stream.protocol)) != null) {
221
+ this.log('outbound pubsub streams already present on connection from %p', peerId)
222
+ return
223
+ }
224
+
225
+ const stream = await conn.newStream(this.protocols)
226
+
227
+ if (stream.protocol == null) {
228
+ stream.abort(new Error('Stream was not multiplexed'))
229
+ return
230
+ }
231
+
232
+ const peer = this.addPeer(peerId, stream.protocol)
233
+ await peer.attachOutboundStream(stream)
234
+
235
+ // Immediately send my own subscriptions to the newly established conn
236
+ this.send(peerId, { subscriptions: Array.from(this.subscriptions).map(sub => sub.toString()), subscribe: true })
237
+ }
238
+
239
+ /**
240
+ * Registrar notifies a closing connection with pubsub protocol
241
+ */
242
+ protected _onPeerDisconnected (peerId: PeerId, conn?: Connection): void {
243
+ this.log('connection ended %p', peerId)
244
+ this._removePeer(peerId)
245
+ }
246
+
247
+ /**
248
+ * Notifies the router that a peer has been connected
249
+ */
250
+ addPeer (peerId: PeerId, protocol: string): PeerStreams {
251
+ const existing = this.peers.get(peerId)
252
+
253
+ // If peer streams already exists, do nothing
254
+ if (existing != null) {
255
+ return existing
256
+ }
257
+
258
+ // else create a new peer streams
259
+ this.log('new peer %p', peerId)
260
+
261
+ const peerStreams: PeerStreams = new PeerStreamsImpl(this.components, {
262
+ id: peerId,
263
+ protocol
264
+ })
265
+
266
+ this.peers.set(peerId, peerStreams)
267
+ peerStreams.addEventListener('close', () => this._removePeer(peerId), {
268
+ once: true
269
+ })
270
+
271
+ return peerStreams
272
+ }
273
+
274
+ /**
275
+ * Notifies the router that a peer has been disconnected
276
+ */
277
+ protected _removePeer (peerId: PeerId): PeerStreams | undefined {
278
+ const peerStreams = this.peers.get(peerId)
279
+ if (peerStreams == null) {
280
+ return
281
+ }
282
+
283
+ // close peer streams
284
+ peerStreams.close()
285
+
286
+ // delete peer streams
287
+ this.log('delete peer %p', peerId)
288
+ this.peers.delete(peerId)
289
+
290
+ // remove peer from topics map
291
+ for (const peers of this.topics.values()) {
292
+ peers.delete(peerId)
293
+ }
294
+
295
+ return peerStreams
296
+ }
297
+
298
+ // MESSAGE METHODS
299
+
300
+ /**
301
+ * Responsible for processing each RPC message received by other peers.
302
+ */
303
+ async processMessages (peerId: PeerId, stream: AsyncIterable<Uint8ArrayList>, peerStreams: PeerStreams): Promise<void> {
304
+ try {
305
+ await pipe(
306
+ stream,
307
+ async (source) => {
308
+ for await (const data of source) {
309
+ const rpcMsg = this.decodeRpc(data)
310
+ const messages: PubSubRPCMessage[] = []
311
+
312
+ for (const msg of (rpcMsg.messages ?? [])) {
313
+ if (msg.from == null || msg.data == null || msg.topic == null) {
314
+ this.log('message from %p was missing from, data or topic fields, dropping', peerId)
315
+ continue
316
+ }
317
+
318
+ messages.push({
319
+ from: msg.from,
320
+ data: msg.data,
321
+ topic: msg.topic,
322
+ sequenceNumber: msg.sequenceNumber ?? undefined,
323
+ signature: msg.signature ?? undefined,
324
+ key: msg.key ?? undefined
325
+ })
326
+ }
327
+
328
+ // Since processRpc may be overridden entirely in unsafe ways,
329
+ // the simplest/safest option here is to wrap in a function and capture all errors
330
+ // to prevent a top-level unhandled exception
331
+ // This processing of rpc messages should happen without awaiting full validation/execution of prior messages
332
+ this.processRpc(peerId, peerStreams, {
333
+ subscriptions: (rpcMsg.subscriptions ?? []).map(sub => ({
334
+ subscribe: Boolean(sub.subscribe),
335
+ topic: sub.topic ?? ''
336
+ })),
337
+ messages
338
+ })
339
+ .catch(err => { this.log(err) })
340
+ }
341
+ }
342
+ )
343
+ } catch (err: any) {
344
+ this._onPeerDisconnected(peerStreams.id, err)
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Handles an rpc request from a peer
350
+ */
351
+ async processRpc (from: PeerId, peerStreams: PeerStreams, rpc: PubSubRPC): Promise<boolean> {
352
+ if (!this.acceptFrom(from)) {
353
+ this.log('received message from unacceptable peer %p', from)
354
+ return false
355
+ }
356
+
357
+ this.log('rpc from %p', from)
358
+
359
+ const { subscriptions, messages } = rpc
360
+
361
+ if (subscriptions != null && subscriptions.length > 0) {
362
+ this.log('subscription update from %p', from)
363
+
364
+ // update peer subscriptions
365
+ subscriptions.forEach((subOpt) => {
366
+ this.processRpcSubOpt(from, subOpt)
367
+ })
368
+
369
+ super.dispatchEvent(new CustomEvent<SubscriptionChangeData>('subscription-change', {
370
+ detail: {
371
+ peerId: peerStreams.id,
372
+ subscriptions: subscriptions.map(({ topic, subscribe }) => ({
373
+ topic: `${topic ?? ''}`,
374
+ subscribe: Boolean(subscribe)
375
+ }))
376
+ }
377
+ }))
378
+ }
379
+
380
+ if (messages != null && messages.length > 0) {
381
+ this.log('messages from %p', from)
382
+
383
+ this.queue.addAll(messages.map(message => async () => {
384
+ if (message.topic == null || (!this.subscriptions.has(message.topic) && !this.canRelayMessage)) {
385
+ this.log('received message we didn\'t subscribe to. Dropping.')
386
+ return false
387
+ }
388
+
389
+ try {
390
+ const msg = await toMessage(message)
391
+
392
+ await this.processMessage(from, msg)
393
+ } catch (err: any) {
394
+ this.log.error(err)
395
+ }
396
+ }))
397
+ .catch(err => { this.log(err) })
398
+ }
399
+
400
+ return true
401
+ }
402
+
403
+ /**
404
+ * Handles a subscription change from a peer
405
+ */
406
+ processRpcSubOpt (id: PeerId, subOpt: PubSubRPCSubscription): void {
407
+ const t = subOpt.topic
408
+
409
+ if (t == null) {
410
+ return
411
+ }
412
+
413
+ let topicSet = this.topics.get(t)
414
+ if (topicSet == null) {
415
+ topicSet = new PeerSet()
416
+ this.topics.set(t, topicSet)
417
+ }
418
+
419
+ if (subOpt.subscribe === true) {
420
+ // subscribe peer to new topic
421
+ topicSet.add(id)
422
+ } else {
423
+ // unsubscribe from existing topic
424
+ topicSet.delete(id)
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Handles a message from a peer
430
+ */
431
+ async processMessage (from: PeerId, msg: Message): Promise<void> {
432
+ if (this.components.peerId.equals(from) && !this.emitSelf) {
433
+ return
434
+ }
435
+
436
+ // Check if I've seen the message, if yes, ignore
437
+ const seqno = await this.getMsgId(msg)
438
+ const msgIdStr = uint8ArrayToString(seqno, 'base64')
439
+
440
+ if (this.seenCache.has(msgIdStr)) {
441
+ return
442
+ }
443
+
444
+ this.seenCache.put(msgIdStr, true)
445
+
446
+ // Ensure the message is valid before processing it
447
+ try {
448
+ await this.validate(from, msg)
449
+ } catch (err: any) {
450
+ this.log('Message is invalid, dropping it. %O', err)
451
+ return
452
+ }
453
+
454
+ if (this.subscriptions.has(msg.topic)) {
455
+ const isFromSelf = this.components.peerId.equals(from)
456
+
457
+ if (!isFromSelf || this.emitSelf) {
458
+ super.dispatchEvent(new CustomEvent<Message>('message', {
459
+ detail: msg
460
+ }))
461
+ }
462
+ }
463
+
464
+ await this.publishMessage(from, msg)
465
+ }
466
+
467
+ /**
468
+ * The default msgID implementation
469
+ * Child class can override this.
470
+ */
471
+ getMsgId (msg: Message): Promise<Uint8Array> | Uint8Array {
472
+ const signaturePolicy = this.globalSignaturePolicy
473
+ switch (signaturePolicy) {
474
+ case 'StrictSign':
475
+ if (msg.type !== 'signed') {
476
+ throw new InvalidMessageError('Message type should be "signed" when signature policy is StrictSign but it was not')
477
+ }
478
+
479
+ if (msg.sequenceNumber == null) {
480
+ throw new InvalidMessageError('Need sequence number when signature policy is StrictSign but it was missing')
481
+ }
482
+
483
+ if (msg.key == null) {
484
+ throw new InvalidMessageError('Need key when signature policy is StrictSign but it was missing')
485
+ }
486
+
487
+ return msgId(msg.key, msg.sequenceNumber)
488
+ case 'StrictNoSign':
489
+ return noSignMsgId(msg.data)
490
+ default:
491
+ throw new InvalidMessageError('Cannot get message id: unhandled signature policy')
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Whether to accept a message from a peer
497
+ * Override to create a gray list
498
+ */
499
+ acceptFrom (id: PeerId): boolean {
500
+ return true
501
+ }
502
+
503
+ /**
504
+ * Decode Uint8Array into an RPC object.
505
+ * This can be override to use a custom router protobuf.
506
+ */
507
+ decodeRpc (bytes: Uint8Array | Uint8ArrayList): PubSubRPC {
508
+ return RPC.decode(bytes)
509
+ }
510
+
511
+ /**
512
+ * Encode RPC object into a Uint8Array.
513
+ * This can be override to use a custom router protobuf.
514
+ */
515
+ encodeRpc (rpc: PubSubRPC): Uint8Array {
516
+ return RPC.encode(rpc)
517
+ }
518
+
519
+ /**
520
+ * Encode RPC object into a Uint8Array.
521
+ * This can be override to use a custom router protobuf.
522
+ */
523
+ encodeMessage (rpc: PubSubRPCMessage): Uint8Array {
524
+ return RPC.Message.encode(rpc)
525
+ }
526
+
527
+ /**
528
+ * Send an rpc object to a peer
529
+ */
530
+ send (peer: PeerId, data: { messages?: Message[], subscriptions?: string[], subscribe?: boolean }): void {
531
+ const { messages, subscriptions, subscribe } = data
532
+
533
+ this.sendRpc(peer, {
534
+ subscriptions: (subscriptions ?? []).map(str => ({ topic: str, subscribe: Boolean(subscribe) })),
535
+ messages: (messages ?? []).map(toRpcMessage)
536
+ })
537
+ }
538
+
539
+ /**
540
+ * Send an rpc object to a peer
541
+ */
542
+ sendRpc (peer: PeerId, rpc: PubSubRPC): void {
543
+ const peerStreams = this.peers.get(peer)
544
+
545
+ if (peerStreams == null) {
546
+ this.log.error('Cannot send RPC to %p as there are no streams to it available', peer)
547
+
548
+ return
549
+ }
550
+
551
+ if (!peerStreams.isWritable) {
552
+ this.log.error('Cannot send RPC to %p as there is no outbound stream to it available', peer)
553
+
554
+ return
555
+ }
556
+
557
+ peerStreams.write(this.encodeRpc(rpc))
558
+ }
559
+
560
+ /**
561
+ * Validates the given message. The signature will be checked for authenticity.
562
+ * Throws an error on invalid messages
563
+ */
564
+ async validate (from: PeerId, message: Message): Promise<void> {
565
+ const signaturePolicy = this.globalSignaturePolicy
566
+ switch (signaturePolicy) {
567
+ case 'StrictNoSign':
568
+ if (message.type !== 'unsigned') {
569
+ throw new InvalidMessageError('Message type should be "unsigned" when signature policy is StrictNoSign but it was not')
570
+ }
571
+
572
+ // @ts-expect-error should not be present
573
+ if (message.signature != null) {
574
+ throw new InvalidMessageError('StrictNoSigning: signature should not be present')
575
+ }
576
+
577
+ // @ts-expect-error should not be present
578
+ if (message.key != null) {
579
+ throw new InvalidMessageError('StrictNoSigning: key should not be present')
580
+ }
581
+
582
+ // @ts-expect-error should not be present
583
+ if (message.sequenceNumber != null) {
584
+ throw new InvalidMessageError('StrictNoSigning: seqno should not be present')
585
+ }
586
+ break
587
+ case 'StrictSign':
588
+ if (message.type !== 'signed') {
589
+ throw new InvalidMessageError('Message type should be "signed" when signature policy is StrictSign but it was not')
590
+ }
591
+
592
+ if (message.signature == null) {
593
+ throw new InvalidMessageError('StrictSigning: Signing required and no signature was present')
594
+ }
595
+
596
+ if (message.sequenceNumber == null) {
597
+ throw new InvalidMessageError('StrictSigning: Signing required and no sequenceNumber was present')
598
+ }
599
+
600
+ if (!(await verifySignature(message, this.encodeMessage.bind(this)))) {
601
+ throw new InvalidMessageError('StrictSigning: Invalid message signature')
602
+ }
603
+
604
+ break
605
+ default:
606
+ throw new InvalidMessageError('Cannot validate message: unhandled signature policy')
607
+ }
608
+
609
+ const validatorFn = this.topicValidators.get(message.topic)
610
+ if (validatorFn != null) {
611
+ const result = await validatorFn(from, message)
612
+ if (result === TopicValidatorResult.Reject || result === TopicValidatorResult.Ignore) {
613
+ throw new InvalidMessageError('Message validation failed')
614
+ }
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Normalizes the message and signs it, if signing is enabled.
620
+ * Should be used by the routers to create the message to send.
621
+ */
622
+ async buildMessage (message: { from: PeerId, topic: string, data: Uint8Array, sequenceNumber: bigint }): Promise<Message> {
623
+ const signaturePolicy = this.globalSignaturePolicy
624
+ switch (signaturePolicy) {
625
+ case 'StrictSign':
626
+ return signMessage(this.components.privateKey, message, this.encodeMessage.bind(this))
627
+ case 'StrictNoSign':
628
+ return Promise.resolve({
629
+ type: 'unsigned',
630
+ ...message
631
+ })
632
+ default:
633
+ throw new InvalidMessageError('Cannot build message: unhandled signature policy')
634
+ }
635
+ }
636
+
637
+ // API METHODS
638
+
639
+ /**
640
+ * Get a list of the peer-ids that are subscribed to one topic.
641
+ */
642
+ getSubscribers (topic: string): PeerId[] {
643
+ if (!this.started) {
644
+ throw new NotStartedError('not started yet')
645
+ }
646
+
647
+ if (topic == null) {
648
+ throw new InvalidParametersError('Topic is required')
649
+ }
650
+
651
+ const peersInTopic = this.topics.get(topic.toString())
652
+
653
+ if (peersInTopic == null) {
654
+ return []
655
+ }
656
+
657
+ return Array.from(peersInTopic.values())
658
+ }
659
+
660
+ /**
661
+ * Publishes messages to all subscribed peers
662
+ */
663
+ async publish (topic: string, data?: Uint8Array): Promise<PublishResult> {
664
+ if (!this.started) {
665
+ throw new Error('Pubsub has not started')
666
+ }
667
+
668
+ const message = {
669
+ from: this.components.peerId,
670
+ topic,
671
+ data: data ?? new Uint8Array(0),
672
+ sequenceNumber: randomSeqno()
673
+ }
674
+
675
+ this.log('publish topic: %s from: %p data: %m', topic, message.from, message.data)
676
+
677
+ const rpcMessage = await this.buildMessage(message)
678
+ let emittedToSelf = false
679
+
680
+ // dispatch the event if we are interested
681
+ if (this.emitSelf) {
682
+ if (this.subscriptions.has(topic)) {
683
+ emittedToSelf = true
684
+ super.dispatchEvent(new CustomEvent<Message>('message', {
685
+ detail: rpcMessage
686
+ }))
687
+ }
688
+ }
689
+
690
+ // send to all the other peers
691
+ const result = await this.publishMessage(this.components.peerId, rpcMessage)
692
+
693
+ if (emittedToSelf) {
694
+ result.recipients = [...result.recipients, this.components.peerId]
695
+ }
696
+
697
+ return result
698
+ }
699
+
700
+ /**
701
+ * Overriding the implementation of publish should handle the appropriate algorithms for the publish/subscriber implementation.
702
+ * For example, a Floodsub implementation might simply publish each message to each topic for every peer.
703
+ *
704
+ * `sender` might be this peer, or we might be forwarding a message on behalf of another peer, in which case sender
705
+ * is the peer we received the message from, which may not be the peer the message was created by.
706
+ */
707
+ async publishMessage (from: PeerId, message: Message): Promise<PublishResult> {
708
+ const peers = this.getSubscribers(message.topic)
709
+ const recipients: PeerId[] = []
710
+
711
+ if (peers == null || peers.length === 0) {
712
+ this.log('no peers are subscribed to topic %s', message.topic)
713
+ return { recipients }
714
+ }
715
+
716
+ peers.forEach(id => {
717
+ if (this.components.peerId.equals(id)) {
718
+ this.log('not sending message on topic %s to myself', message.topic)
719
+ return
720
+ }
721
+
722
+ if (id.equals(from)) {
723
+ this.log('not sending message on topic %s to sender %p', message.topic, id)
724
+ return
725
+ }
726
+
727
+ this.log('publish msgs on topics %s %p', message.topic, id)
728
+
729
+ recipients.push(id)
730
+ this.send(id, { messages: [message] })
731
+ })
732
+
733
+ return { recipients }
734
+ }
735
+
736
+ /**
737
+ * Subscribes to a given topic.
738
+ */
739
+ subscribe (topic: string): void {
740
+ if (!this.started) {
741
+ throw new Error('Pubsub has not started')
742
+ }
743
+
744
+ this.log('subscribe to topic: %s', topic)
745
+
746
+ if (!this.subscriptions.has(topic)) {
747
+ this.subscriptions.add(topic)
748
+
749
+ for (const peerId of this.peers.keys()) {
750
+ this.send(peerId, { subscriptions: [topic], subscribe: true })
751
+ }
752
+ }
753
+ }
754
+
755
+ /**
756
+ * Unsubscribe from the given topic
757
+ */
758
+ unsubscribe (topic: string): void {
759
+ if (!this.started) {
760
+ throw new Error('Pubsub is not started')
761
+ }
762
+
763
+ const wasSubscribed = this.subscriptions.has(topic)
764
+
765
+ this.log('unsubscribe from %s - am subscribed %s', topic, wasSubscribed)
766
+
767
+ if (wasSubscribed) {
768
+ this.subscriptions.delete(topic)
769
+
770
+ for (const peerId of this.peers.keys()) {
771
+ this.send(peerId, { subscriptions: [topic], subscribe: false })
772
+ }
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Get the list of topics which the peer is subscribed to.
778
+ */
779
+ getTopics (): string[] {
780
+ if (!this.started) {
781
+ throw new Error('Pubsub is not started')
782
+ }
783
+
784
+ return Array.from(this.subscriptions)
785
+ }
786
+
787
+ getPeers (): PeerId[] {
788
+ if (!this.started) {
789
+ throw new Error('Pubsub is not started')
790
+ }
791
+
792
+ return Array.from(this.peers.keys())
793
+ }
794
+ }