@libp2p/pubsub 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +35 -0
  3. package/dist/src/errors.d.ts +39 -0
  4. package/dist/src/errors.d.ts.map +1 -0
  5. package/dist/src/errors.js +41 -0
  6. package/dist/src/errors.js.map +1 -0
  7. package/dist/src/index.d.ts +180 -0
  8. package/dist/src/index.d.ts.map +1 -0
  9. package/dist/src/index.js +467 -0
  10. package/dist/src/index.js.map +1 -0
  11. package/dist/src/message/rpc.d.ts +258 -0
  12. package/dist/src/message/rpc.js +699 -0
  13. package/dist/src/message/sign.d.ts +17 -0
  14. package/dist/src/message/sign.d.ts.map +1 -0
  15. package/dist/src/message/sign.js +84 -0
  16. package/dist/src/message/sign.js.map +1 -0
  17. package/dist/src/message/topic-descriptor.d.ts +254 -0
  18. package/dist/src/message/topic-descriptor.js +647 -0
  19. package/dist/src/peer-streams.d.ts +67 -0
  20. package/dist/src/peer-streams.d.ts.map +1 -0
  21. package/dist/src/peer-streams.js +112 -0
  22. package/dist/src/peer-streams.js.map +1 -0
  23. package/dist/src/utils.d.ts +29 -0
  24. package/dist/src/utils.d.ts.map +1 -0
  25. package/dist/src/utils.js +80 -0
  26. package/dist/src/utils.js.map +1 -0
  27. package/dist/test/emit-self.spec.d.ts +2 -0
  28. package/dist/test/emit-self.spec.d.ts.map +1 -0
  29. package/dist/test/emit-self.spec.js +63 -0
  30. package/dist/test/emit-self.spec.js.map +1 -0
  31. package/dist/test/instance.spec.d.ts +2 -0
  32. package/dist/test/instance.spec.d.ts.map +1 -0
  33. package/dist/test/instance.spec.js +50 -0
  34. package/dist/test/instance.spec.js.map +1 -0
  35. package/dist/test/lifesycle.spec.d.ts +2 -0
  36. package/dist/test/lifesycle.spec.d.ts.map +1 -0
  37. package/dist/test/lifesycle.spec.js +192 -0
  38. package/dist/test/lifesycle.spec.js.map +1 -0
  39. package/dist/test/message.spec.d.ts +2 -0
  40. package/dist/test/message.spec.d.ts.map +1 -0
  41. package/dist/test/message.spec.js +83 -0
  42. package/dist/test/message.spec.js.map +1 -0
  43. package/dist/test/pubsub.spec.d.ts +2 -0
  44. package/dist/test/pubsub.spec.d.ts.map +1 -0
  45. package/dist/test/pubsub.spec.js +310 -0
  46. package/dist/test/pubsub.spec.js.map +1 -0
  47. package/dist/test/sign.spec.d.ts +2 -0
  48. package/dist/test/sign.spec.d.ts.map +1 -0
  49. package/dist/test/sign.spec.js +93 -0
  50. package/dist/test/sign.spec.js.map +1 -0
  51. package/dist/test/topic-validators.spec.d.ts +2 -0
  52. package/dist/test/topic-validators.spec.d.ts.map +1 -0
  53. package/dist/test/topic-validators.spec.js +86 -0
  54. package/dist/test/topic-validators.spec.js.map +1 -0
  55. package/dist/test/utils/index.d.ts +22 -0
  56. package/dist/test/utils/index.d.ts.map +1 -0
  57. package/dist/test/utils/index.js +86 -0
  58. package/dist/test/utils/index.js.map +1 -0
  59. package/dist/test/utils.spec.d.ts +2 -0
  60. package/dist/test/utils.spec.d.ts.map +1 -0
  61. package/dist/test/utils.spec.js +53 -0
  62. package/dist/test/utils.spec.js.map +1 -0
  63. package/dist/tsconfig.tsbuildinfo +1 -0
  64. package/package.json +109 -4
  65. package/src/README.md +251 -0
  66. package/src/errors.ts +45 -0
  67. package/src/index.ts +610 -0
  68. package/src/message/rpc.d.ts +258 -0
  69. package/src/message/rpc.js +699 -0
  70. package/src/message/rpc.proto +20 -0
  71. package/src/message/sign.ts +101 -0
  72. package/src/message/topic-descriptor.d.ts +254 -0
  73. package/src/message/topic-descriptor.js +647 -0
  74. package/src/message/topic-descriptor.proto +30 -0
  75. package/src/peer-streams.ts +169 -0
  76. package/src/utils.ts +93 -0
package/src/errors.ts ADDED
@@ -0,0 +1,45 @@
1
+
2
+ export const codes = {
3
+ /**
4
+ * Signature policy is invalid
5
+ */
6
+ ERR_INVALID_SIGNATURE_POLICY: 'ERR_INVALID_SIGNATURE_POLICY',
7
+ /**
8
+ * Signature policy is unhandled
9
+ */
10
+ ERR_UNHANDLED_SIGNATURE_POLICY: 'ERR_UNHANDLED_SIGNATURE_POLICY',
11
+
12
+ // Strict signing codes
13
+
14
+ /**
15
+ * Message expected to have a `signature`, but doesn't
16
+ */
17
+ ERR_MISSING_SIGNATURE: 'ERR_MISSING_SIGNATURE',
18
+ /**
19
+ * Message expected to have a `seqno`, but doesn't
20
+ */
21
+ ERR_MISSING_SEQNO: 'ERR_MISSING_SEQNO',
22
+ /**
23
+ * Message `signature` is invalid
24
+ */
25
+ ERR_INVALID_SIGNATURE: 'ERR_INVALID_SIGNATURE',
26
+
27
+ // Strict no-signing codes
28
+
29
+ /**
30
+ * Message expected to not have a `from`, but does
31
+ */
32
+ ERR_UNEXPECTED_FROM: 'ERR_UNEXPECTED_FROM',
33
+ /**
34
+ * Message expected to not have a `signature`, but does
35
+ */
36
+ ERR_UNEXPECTED_SIGNATURE: 'ERR_UNEXPECTED_SIGNATURE',
37
+ /**
38
+ * Message expected to not have a `key`, but does
39
+ */
40
+ ERR_UNEXPECTED_KEY: 'ERR_UNEXPECTED_KEY',
41
+ /**
42
+ * Message expected to not have a `seqno`, but does
43
+ */
44
+ ERR_UNEXPECTED_SEQNO: 'ERR_UNEXPECTED_SEQNO'
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,610 @@
1
+ import debug from 'debug'
2
+ import { EventEmitter } from 'events'
3
+ import errcode from 'err-code'
4
+ import { pipe } from 'it-pipe'
5
+ import Queue from 'p-queue'
6
+ import { MulticodecTopology } from '@libp2p/topology/multicodec-topology'
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'
11
+ import type { PeerId } from '@libp2p/interfaces/peer-id'
12
+ import type { Registrar, IncomingStreamEvent } from '@libp2p/interfaces/registrar'
13
+ import type { Connection } from '@libp2p/interfaces/connection'
14
+ import type BufferList from 'bl'
15
+ import {
16
+ signMessage,
17
+ verifySignature
18
+ } from './message/sign.js'
19
+ import type { PubSub, Message, StrictNoSign, StrictSign, PubsubOptions } from '@libp2p/interfaces/pubsub'
20
+ import type { Startable } from '@libp2p/interfaces'
21
+
22
+ export interface TopicValidator { (topic: string, message: Message): Promise<void> }
23
+
24
+ /**
25
+ * PubsubBaseProtocol handles the peers and connections logic for pubsub routers
26
+ * and specifies the API that pubsub routers should have.
27
+ */
28
+ export abstract class PubsubBaseProtocol extends EventEmitter implements PubSub, Startable {
29
+ public peerId: PeerId
30
+ public started: boolean
31
+ /**
32
+ * Map of topics to which peers are subscribed to
33
+ */
34
+ public topics: Map<string, Set<string>>
35
+ /**
36
+ * List of our subscriptions
37
+ */
38
+ public subscriptions: Set<string>
39
+ /**
40
+ * Map of peer streams
41
+ */
42
+ public peers: Map<string, PeerStreams>
43
+ /**
44
+ * The signature policy to follow by default
45
+ */
46
+ public globalSignaturePolicy: StrictNoSign | StrictSign
47
+ /**
48
+ * If router can relay received messages, even if not subscribed
49
+ */
50
+ public canRelayMessage: boolean
51
+ /**
52
+ * if publish should emit to self, if subscribed
53
+ */
54
+ public emitSelf: boolean
55
+ /**
56
+ * Topic validator map
57
+ *
58
+ * Keyed by topic
59
+ * Topic validators are functions with the following input:
60
+ */
61
+ public topicValidators: Map<string, TopicValidator>
62
+ public queue: Queue
63
+ public registrar: Registrar
64
+
65
+ protected log: debug.Debugger & { err: debug.Debugger }
66
+ protected multicodecs: string[]
67
+ protected _libp2p: any
68
+ private _registrarId: string | undefined
69
+
70
+ constructor (props: PubsubOptions) {
71
+ super()
72
+
73
+ const {
74
+ debugName = 'libp2p:pubsub',
75
+ multicodecs = [],
76
+ libp2p = null,
77
+ globalSignaturePolicy = 'StrictSign',
78
+ canRelayMessage = false,
79
+ emitSelf = false,
80
+ messageProcessingConcurrency = 10
81
+ } = props
82
+
83
+ this.log = Object.assign(debug(debugName), {
84
+ err: debug(`${debugName}:error`)
85
+ })
86
+
87
+ this.multicodecs = utils.ensureArray(multicodecs)
88
+ this._libp2p = libp2p
89
+ this.registrar = libp2p.registrar
90
+ this.peerId = libp2p.peerId
91
+ this.started = false
92
+ this.topics = new Map()
93
+ this.subscriptions = new Set()
94
+ this.peers = new Map()
95
+ this.globalSignaturePolicy = globalSignaturePolicy === 'StrictNoSign' ? 'StrictNoSign' : 'StrictSign'
96
+ this.canRelayMessage = canRelayMessage
97
+ this.emitSelf = emitSelf
98
+ this.topicValidators = new Map()
99
+ this.queue = new Queue({ concurrency: messageProcessingConcurrency })
100
+
101
+ this._onIncomingStream = this._onIncomingStream.bind(this)
102
+ this._onPeerConnected = this._onPeerConnected.bind(this)
103
+ this._onPeerDisconnected = this._onPeerDisconnected.bind(this)
104
+ }
105
+
106
+ // LIFECYCLE METHODS
107
+
108
+ /**
109
+ * Register the pubsub protocol onto the libp2p node.
110
+ *
111
+ * @returns {void}
112
+ */
113
+ start () {
114
+ if (this.started) {
115
+ return
116
+ }
117
+
118
+ this.log('starting')
119
+
120
+ // Incoming streams
121
+ // Called after a peer dials us
122
+ this.registrar.handle(this.multicodecs, this._onIncomingStream)
123
+
124
+ // register protocol with topology
125
+ // Topology callbacks called on connection manager changes
126
+ const topology = new MulticodecTopology({
127
+ multicodecs: this.multicodecs,
128
+ handlers: {
129
+ onConnect: this._onPeerConnected,
130
+ onDisconnect: this._onPeerDisconnected
131
+ }
132
+ })
133
+ this._registrarId = this.registrar.register(topology)
134
+
135
+ this.log('started')
136
+ this.started = true
137
+ }
138
+
139
+ /**
140
+ * Unregister the pubsub protocol and the streams with other peers will be closed.
141
+ *
142
+ * @returns {void}
143
+ */
144
+ stop () {
145
+ if (!this.started) {
146
+ return
147
+ }
148
+
149
+ // unregister protocol and handlers
150
+ if (this._registrarId != null) {
151
+ this.registrar.unregister(this._registrarId)
152
+ }
153
+
154
+ this.log('stopping')
155
+ this.peers.forEach((peerStreams) => peerStreams.close())
156
+
157
+ this.peers = new Map()
158
+ this.subscriptions = new Set()
159
+ this.started = false
160
+ this.log('stopped')
161
+ }
162
+
163
+ isStarted () {
164
+ return this.started
165
+ }
166
+
167
+ /**
168
+ * On an inbound stream opened
169
+ */
170
+ protected _onIncomingStream ({ protocol, stream, connection }: IncomingStreamEvent) {
171
+ const peerId = connection.remotePeer
172
+ const idB58Str = peerId.toString()
173
+ const peer = this._addPeer(peerId, protocol)
174
+ const inboundStream = peer.attachInboundStream(stream)
175
+
176
+ this._processMessages(idB58Str, inboundStream, peer)
177
+ .catch(err => this.log(err))
178
+ }
179
+
180
+ /**
181
+ * Registrar notifies an established connection with pubsub protocol
182
+ */
183
+ protected async _onPeerConnected (peerId: PeerId, conn: Connection) {
184
+ const idB58Str = peerId.toString()
185
+ this.log('connected', idB58Str)
186
+
187
+ try {
188
+ const { stream, protocol } = await conn.newStream(this.multicodecs)
189
+ const peer = this._addPeer(peerId, protocol)
190
+ await peer.attachOutboundStream(stream)
191
+ } catch (err: any) {
192
+ this.log.err(err)
193
+ }
194
+
195
+ // Immediately send my own subscriptions to the newly established conn
196
+ this._sendSubscriptions(idB58Str, Array.from(this.subscriptions), true)
197
+ }
198
+
199
+ /**
200
+ * Registrar notifies a closing connection with pubsub protocol
201
+ */
202
+ protected _onPeerDisconnected (peerId: PeerId, conn?: Connection) {
203
+ const idB58Str = peerId.toString()
204
+
205
+ this.log('connection ended', idB58Str)
206
+ this._removePeer(peerId)
207
+ }
208
+
209
+ /**
210
+ * Notifies the router that a peer has been connected
211
+ */
212
+ protected _addPeer (peerId: PeerId, protocol: string) {
213
+ const id = peerId.toString()
214
+ const existing = this.peers.get(id)
215
+
216
+ // If peer streams already exists, do nothing
217
+ if (existing != null) {
218
+ return existing
219
+ }
220
+
221
+ // else create a new peer streams
222
+ this.log('new peer', id)
223
+
224
+ const peerStreams = new PeerStreams({
225
+ id: peerId,
226
+ protocol
227
+ })
228
+
229
+ this.peers.set(id, peerStreams)
230
+ peerStreams.once('close', () => this._removePeer(peerId))
231
+
232
+ return peerStreams
233
+ }
234
+
235
+ /**
236
+ * Notifies the router that a peer has been disconnected
237
+ */
238
+ protected _removePeer (peerId: PeerId) {
239
+ const id = peerId.toString()
240
+ const peerStreams = this.peers.get(id)
241
+ if (peerStreams == null) return
242
+
243
+ // close peer streams
244
+ peerStreams.removeAllListeners()
245
+ peerStreams.close()
246
+
247
+ // delete peer streams
248
+ this.log('delete peer', id)
249
+ this.peers.delete(id)
250
+
251
+ // remove peer from topics map
252
+ for (const peers of this.topics.values()) {
253
+ peers.delete(id)
254
+ }
255
+
256
+ return peerStreams
257
+ }
258
+
259
+ // MESSAGE METHODS
260
+
261
+ /**
262
+ * Responsible for processing each RPC message received by other peers.
263
+ */
264
+ async _processMessages (idB58Str: string, stream: AsyncIterable<Uint8Array|BufferList>, peerStreams: PeerStreams) {
265
+ try {
266
+ await pipe(
267
+ stream,
268
+ async (source) => {
269
+ 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,
274
+ // the simplest/safest option here is to wrap in a function and capture all errors
275
+ // to prevent a top-level unhandled exception
276
+ // This processing of rpc messages should happen without awaiting full validation/execution of prior messages
277
+ this._processRpc(idB58Str, peerStreams, rpcMsg)
278
+ .catch(err => this.log(err))
279
+ }
280
+ }
281
+ )
282
+ } catch (err: any) {
283
+ this._onPeerDisconnected(peerStreams.id, err)
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Handles an rpc request from a peer
289
+ */
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
294
+
295
+ if (subs.length > 0) {
296
+ // update peer subscriptions
297
+ subs.forEach((subOpt) => {
298
+ this._processRpcSubOpt(idB58Str, subOpt)
299
+ })
300
+ this.emit('pubsub:subscription-change', { peerId: peerStreams.id, subscriptions: subs })
301
+ }
302
+
303
+ if (!this._acceptFrom(idB58Str)) {
304
+ this.log('received message from unacceptable peer %s', idB58Str)
305
+ return false
306
+ }
307
+
308
+ if (msgs.length > 0) {
309
+ this.queue.addAll(msgs.map(message => async () => {
310
+ const topics = message.topicIDs != null ? message.topicIDs : []
311
+ const hasSubscription = topics.some((topic) => this.subscriptions.has(topic))
312
+
313
+ if (!hasSubscription && !this.canRelayMessage) {
314
+ this.log('received message we didn\'t subscribe to. Dropping.')
315
+ return
316
+ }
317
+
318
+ try {
319
+ const msg = utils.normalizeInRpcMessage(message, idB58Str)
320
+
321
+ await this._processRpcMessage(msg)
322
+ } catch (err: any) {
323
+ this.log.err(err)
324
+ }
325
+ }))
326
+ .catch(err => this.log(err))
327
+ }
328
+ return true
329
+ }
330
+
331
+ /**
332
+ * Handles a subscription change from a peer
333
+ */
334
+ _processRpcSubOpt (id: string, subOpt: RPC.ISubOpts) {
335
+ const t = subOpt.topicID
336
+
337
+ if (t == null) {
338
+ return
339
+ }
340
+
341
+ let topicSet = this.topics.get(t)
342
+ if (topicSet == null) {
343
+ topicSet = new Set()
344
+ this.topics.set(t, topicSet)
345
+ }
346
+
347
+ if (subOpt.subscribe === true) {
348
+ // subscribe peer to new topic
349
+ topicSet.add(id)
350
+ } else {
351
+ // unsubscribe from existing topic
352
+ topicSet.delete(id)
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Handles an message from a peer
358
+ */
359
+ async _processRpcMessage (msg: Message) {
360
+ if ((msg.from != null) && this.peerId.equals(msg.from) && !this.emitSelf) {
361
+ return
362
+ }
363
+
364
+ // Ensure the message is valid before processing it
365
+ try {
366
+ await this.validate(msg)
367
+ } catch (err: any) {
368
+ this.log('Message is invalid, dropping it. %O', err)
369
+ return
370
+ }
371
+
372
+ // Emit to self
373
+ this._emitMessage(msg)
374
+
375
+ return await this._publish(utils.normalizeOutRpcMessage(msg))
376
+ }
377
+
378
+ /**
379
+ * Emit a message from a peer
380
+ */
381
+ _emitMessage (message: Message) {
382
+ message.topicIDs.forEach((topic) => {
383
+ if (this.subscriptions.has(topic)) {
384
+ this.emit(topic, message)
385
+ }
386
+ })
387
+ }
388
+
389
+ /**
390
+ * The default msgID implementation
391
+ * Child class can override this.
392
+ */
393
+ getMsgId (msg: Message) {
394
+ const signaturePolicy = this.globalSignaturePolicy
395
+ switch (signaturePolicy) {
396
+ case 'StrictSign':
397
+ // @ts-expect-error seqno is optional in protobuf definition but it will exist
398
+ return utils.msgId(msg.from, msg.seqno)
399
+ case 'StrictNoSign':
400
+ return utils.noSignMsgId(msg.data)
401
+ default:
402
+ throw errcode(new Error('Cannot get message id: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Whether to accept a message from a peer
408
+ * Override to create a graylist
409
+ */
410
+ _acceptFrom (id: string) {
411
+ return true
412
+ }
413
+
414
+ /**
415
+ * Decode Uint8Array into an RPC object.
416
+ * This can be override to use a custom router protobuf.
417
+ */
418
+ _decodeRpc (bytes: Uint8Array) {
419
+ return RPC.decode(bytes)
420
+ }
421
+
422
+ /**
423
+ * Encode RPC object into a Uint8Array.
424
+ * This can be override to use a custom router protobuf.
425
+ */
426
+ _encodeRpc (rpc: IRPC) {
427
+ return RPC.encode(rpc).finish()
428
+ }
429
+
430
+ /**
431
+ * Send an rpc object to a peer
432
+ */
433
+ _sendRpc (id: string, rpc: IRPC) {
434
+ const peerStreams = this.peers.get(id)
435
+ if ((peerStreams == null) || !peerStreams.isWritable) {
436
+ const msg = `Cannot send RPC to ${id} as there is no open stream to it available`
437
+
438
+ this.log.err(msg)
439
+ return
440
+ }
441
+ peerStreams.write(this._encodeRpc(rpc))
442
+ }
443
+
444
+ /**
445
+ * Send subscriptions to a peer
446
+ */
447
+ _sendSubscriptions (id: string, topics: string[], subscribe: boolean) {
448
+ return this._sendRpc(id, {
449
+ subscriptions: topics.map(t => ({ topicID: t, subscribe: subscribe }))
450
+ })
451
+ }
452
+
453
+ /**
454
+ * Validates the given message. The signature will be checked for authenticity.
455
+ * Throws an error on invalid messages
456
+ */
457
+ async validate (message: Message) { // eslint-disable-line require-await
458
+ const signaturePolicy = this.globalSignaturePolicy
459
+ switch (signaturePolicy) {
460
+ case 'StrictNoSign':
461
+ if (message.from != null) {
462
+ throw errcode(new Error('StrictNoSigning: from should not be present'), codes.ERR_UNEXPECTED_FROM)
463
+ }
464
+ if (message.signature != null) {
465
+ throw errcode(new Error('StrictNoSigning: signature should not be present'), codes.ERR_UNEXPECTED_SIGNATURE)
466
+ }
467
+ if (message.key != null) {
468
+ throw errcode(new Error('StrictNoSigning: key should not be present'), codes.ERR_UNEXPECTED_KEY)
469
+ }
470
+ if (message.seqno != null) {
471
+ throw errcode(new Error('StrictNoSigning: seqno should not be present'), codes.ERR_UNEXPECTED_SEQNO)
472
+ }
473
+ break
474
+ case 'StrictSign':
475
+ if (message.signature == null) {
476
+ throw errcode(new Error('StrictSigning: Signing required and no signature was present'), codes.ERR_MISSING_SIGNATURE)
477
+ }
478
+ if (message.seqno == null) {
479
+ throw errcode(new Error('StrictSigning: Signing required and no seqno was present'), codes.ERR_MISSING_SEQNO)
480
+ }
481
+ if (!(await verifySignature(message))) {
482
+ throw errcode(new Error('StrictSigning: Invalid message signature'), codes.ERR_INVALID_SIGNATURE)
483
+ }
484
+ break
485
+ default:
486
+ throw errcode(new Error('Cannot validate message: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
487
+ }
488
+
489
+ for (const topic of message.topicIDs) {
490
+ const validatorFn = this.topicValidators.get(topic)
491
+ if (validatorFn != null) {
492
+ await validatorFn(topic, message)
493
+ }
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Normalizes the message and signs it, if signing is enabled.
499
+ * Should be used by the routers to create the message to send.
500
+ */
501
+ protected async _buildMessage (message: Message) {
502
+ const signaturePolicy = this.globalSignaturePolicy
503
+ switch (signaturePolicy) {
504
+ case 'StrictSign':
505
+ message.from = this.peerId.multihash.bytes
506
+ message.seqno = utils.randomSeqno()
507
+ return await signMessage(this.peerId, message)
508
+ case 'StrictNoSign':
509
+ return await Promise.resolve(message)
510
+ default:
511
+ throw errcode(new Error('Cannot build message: unhandled signature policy'), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
512
+ }
513
+ }
514
+
515
+ // API METHODS
516
+
517
+ /**
518
+ * Get a list of the peer-ids that are subscribed to one topic.
519
+ */
520
+ getSubscribers (topic: string) {
521
+ if (!this.started) {
522
+ throw errcode(new Error('not started yet'), 'ERR_NOT_STARTED_YET')
523
+ }
524
+
525
+ if (topic == null) {
526
+ throw errcode(new Error('topic is required'), 'ERR_NOT_VALID_TOPIC')
527
+ }
528
+
529
+ const peersInTopic = this.topics.get(topic)
530
+
531
+ if (peersInTopic == null) {
532
+ return []
533
+ }
534
+
535
+ return Array.from(peersInTopic)
536
+ }
537
+
538
+ /**
539
+ * Publishes messages to all subscribed peers
540
+ */
541
+ async publish (topic: string, message: Uint8Array) {
542
+ if (!this.started) {
543
+ throw new Error('Pubsub has not started')
544
+ }
545
+
546
+ this.log('publish', topic, message)
547
+
548
+ const from = this.peerId.toString()
549
+ const msgObject = {
550
+ receivedFrom: from,
551
+ data: message,
552
+ topicIDs: [topic]
553
+ }
554
+
555
+ // ensure that the message follows the signature policy
556
+ const outMsg = await this._buildMessage(msgObject)
557
+ const msg = utils.normalizeInRpcMessage(outMsg)
558
+
559
+ // Emit to self if I'm interested and emitSelf enabled
560
+ this.emitSelf && this._emitMessage(msg)
561
+
562
+ // send to all the other peers
563
+ await this._publish(msg)
564
+ }
565
+
566
+ /**
567
+ * Overriding the implementation of publish should handle the appropriate algorithms for the publish/subscriber implementation.
568
+ * For example, a Floodsub implementation might simply publish each message to each topic for every peer
569
+ */
570
+ abstract _publish (message: Message): Promise<void>
571
+
572
+ /**
573
+ * Subscribes to a given topic.
574
+ */
575
+ subscribe (topic: string) {
576
+ if (!this.started) {
577
+ throw new Error('Pubsub has not started')
578
+ }
579
+
580
+ if (!this.subscriptions.has(topic)) {
581
+ this.subscriptions.add(topic)
582
+ this.peers.forEach((_, id) => this._sendSubscriptions(id, [topic], true))
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Unsubscribe from the given topic.
588
+ */
589
+ unsubscribe (topic: string) {
590
+ if (!this.started) {
591
+ throw new Error('Pubsub is not started')
592
+ }
593
+
594
+ if (this.subscriptions.has(topic) && this.listenerCount(topic) === 0) {
595
+ this.subscriptions.delete(topic)
596
+ this.peers.forEach((_, id) => this._sendSubscriptions(id, [topic], false))
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Get the list of topics which the peer is subscribed to.
602
+ */
603
+ getTopics () {
604
+ if (!this.started) {
605
+ throw new Error('Pubsub is not started')
606
+ }
607
+
608
+ return Array.from(this.subscriptions)
609
+ }
610
+ }