@helia/ipns 0.0.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 (44) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +59 -0
  3. package/dist/index.min.js +25 -0
  4. package/dist/src/index.d.ts +124 -0
  5. package/dist/src/index.d.ts.map +1 -0
  6. package/dist/src/index.js +192 -0
  7. package/dist/src/index.js.map +1 -0
  8. package/dist/src/routing/dht.d.ts +18 -0
  9. package/dist/src/routing/dht.d.ts.map +1 -0
  10. package/dist/src/routing/dht.js +65 -0
  11. package/dist/src/routing/dht.js.map +1 -0
  12. package/dist/src/routing/index.d.ts +17 -0
  13. package/dist/src/routing/index.d.ts.map +1 -0
  14. package/dist/src/routing/index.js +3 -0
  15. package/dist/src/routing/index.js.map +1 -0
  16. package/dist/src/routing/local-store.d.ts +15 -0
  17. package/dist/src/routing/local-store.d.ts.map +1 -0
  18. package/dist/src/routing/local-store.js +48 -0
  19. package/dist/src/routing/local-store.js.map +1 -0
  20. package/dist/src/routing/pubsub.d.ts +20 -0
  21. package/dist/src/routing/pubsub.d.ts.map +1 -0
  22. package/dist/src/routing/pubsub.js +150 -0
  23. package/dist/src/routing/pubsub.js.map +1 -0
  24. package/dist/src/utils/resolve-dns-link.browser.d.ts +6 -0
  25. package/dist/src/utils/resolve-dns-link.browser.d.ts.map +1 -0
  26. package/dist/src/utils/resolve-dns-link.browser.js +46 -0
  27. package/dist/src/utils/resolve-dns-link.browser.js.map +1 -0
  28. package/dist/src/utils/resolve-dns-link.d.ts +3 -0
  29. package/dist/src/utils/resolve-dns-link.d.ts.map +1 -0
  30. package/dist/src/utils/resolve-dns-link.js +54 -0
  31. package/dist/src/utils/resolve-dns-link.js.map +1 -0
  32. package/dist/src/utils/tlru.d.ts +15 -0
  33. package/dist/src/utils/tlru.d.ts.map +1 -0
  34. package/dist/src/utils/tlru.js +39 -0
  35. package/dist/src/utils/tlru.js.map +1 -0
  36. package/package.json +191 -0
  37. package/src/index.ts +296 -0
  38. package/src/routing/dht.ts +85 -0
  39. package/src/routing/index.ts +26 -0
  40. package/src/routing/local-store.ts +63 -0
  41. package/src/routing/pubsub.ts +195 -0
  42. package/src/utils/resolve-dns-link.browser.ts +61 -0
  43. package/src/utils/resolve-dns-link.ts +65 -0
  44. package/src/utils/tlru.ts +52 -0
package/src/index.ts ADDED
@@ -0,0 +1,296 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * IPNS operations using a Helia node
5
+ *
6
+ * @example
7
+ *
8
+ * ```typescript
9
+ * import { gossipsub } from '@chainsafe/libp2p'
10
+ * import { kadDHT } from '@libp2p/kad-dht'
11
+ * import { createLibp2p } from 'libp2p'
12
+ * import { createHelia } from 'helia'
13
+ * import { ipns, ipnsValidator, ipnsSelector } from '@helia/ipns'
14
+ * import { dht, pubsub } from '@helia/ipns/routing'
15
+ * import { unixfs } from '@helia/unixfs
16
+ *
17
+ * const libp2p = await createLibp2p({
18
+ * dht: kadDHT({
19
+ * validators: {
20
+ * ipns: ipnsValidator
21
+ * },
22
+ * selectors: {
23
+ * ipns: ipnsSelector
24
+ * }
25
+ * }),
26
+ * pubsub: gossipsub()
27
+ * })
28
+ *
29
+ * const helia = await createHelia({
30
+ * libp2p,
31
+ * //.. other options
32
+ * })
33
+ * const name = ipns(helia, [
34
+ * dht(helia)
35
+ * pubsub(helia)
36
+ * ])
37
+ *
38
+ * // create a public key to publish as an IPNS name
39
+ * const keyInfo = await helia.libp2p.keychain.createKey('my-key')
40
+ * const peerId = await helia.libp2p.keychain.exportPeerId(keyInfo.name)
41
+ *
42
+ * // store some data to publish
43
+ * const fs = unixfs(helia)
44
+ * const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4]))
45
+ *
46
+ * // publish the name
47
+ * await name.publish(peerId, cid)
48
+ *
49
+ * // resolve the name
50
+ * const cid = name.resolve(peerId)
51
+ * ```
52
+ *
53
+ * @example
54
+ *
55
+ * ```typescript
56
+ * // resolve a CID from a TXT record in a DNS zone file, eg:
57
+ * // > dig ipfs.io TXT
58
+ * // ;; ANSWER SECTION:
59
+ * // ipfs.io. 435 IN TXT "dnslink=/ipfs/Qmfoo"
60
+ *
61
+ * const cid = name.resolveDns('ipfs.io')
62
+ * ```
63
+ */
64
+
65
+ import type { AbortOptions } from '@libp2p/interfaces'
66
+ import { isPeerId, PeerId } from '@libp2p/interface-peer-id'
67
+ import { create, marshal, peerIdToRoutingKey, unmarshal } from 'ipns'
68
+ import type { IPNSEntry } from 'ipns'
69
+ import type { IPNSRouting, IPNSRoutingEvents } from './routing/index.js'
70
+ import { ipnsValidator } from 'ipns/validator'
71
+ import { CID } from 'multiformats/cid'
72
+ import { resolveDnslink } from './utils/resolve-dns-link.js'
73
+ import { logger } from '@libp2p/logger'
74
+ import { peerIdFromString } from '@libp2p/peer-id'
75
+ import type { ProgressEvent, ProgressOptions } from 'progress-events'
76
+ import { CustomProgressEvent } from 'progress-events'
77
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
78
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
79
+ import type { Datastore } from 'interface-datastore'
80
+ import { localStore, LocalStore } from './routing/local-store.js'
81
+
82
+ const log = logger('helia:ipns')
83
+
84
+ const MINUTE = 60 * 1000
85
+ const HOUR = 60 * MINUTE
86
+
87
+ const DEFAULT_LIFETIME_MS = 24 * HOUR
88
+ const DEFAULT_REPUBLISH_INTERVAL_MS = 23 * HOUR
89
+
90
+ export type PublishProgressEvents =
91
+ ProgressEvent<'ipns:publish:start'> |
92
+ ProgressEvent<'ipns:publish:success', IPNSEntry> |
93
+ ProgressEvent<'ipns:publish:error', Error>
94
+
95
+ export type ResolveProgressEvents =
96
+ ProgressEvent<'ipns:resolve:start', unknown> |
97
+ ProgressEvent<'ipns:resolve:success', IPNSEntry> |
98
+ ProgressEvent<'ipns:resolve:error', Error>
99
+
100
+ export type RepublishProgressEvents =
101
+ ProgressEvent<'ipns:republish:start', unknown> |
102
+ ProgressEvent<'ipns:republish:success', IPNSEntry> |
103
+ ProgressEvent<'ipns:republish:error', { record: IPNSEntry, err: Error }>
104
+
105
+ export interface PublishOptions extends AbortOptions, ProgressOptions<PublishProgressEvents | IPNSRoutingEvents> {
106
+ /**
107
+ * Time duration of the record in ms
108
+ */
109
+ lifetime?: number
110
+ }
111
+
112
+ export interface ResolveOptions extends AbortOptions, ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents> {
113
+ /**
114
+ * do not use cached entries
115
+ */
116
+ nocache?: boolean
117
+ }
118
+
119
+ export interface RepublishOptions extends AbortOptions, ProgressOptions<RepublishProgressEvents | IPNSRoutingEvents> {
120
+ /**
121
+ * The republish interval in ms (default: 24hrs)
122
+ */
123
+ interval?: number
124
+ }
125
+
126
+ export interface IPNS {
127
+ /**
128
+ * Creates an IPNS record signed by the passed PeerId that will resolve to the passed value
129
+ *
130
+ * If the valid is a PeerId, a recursive IPNS record will be created.
131
+ */
132
+ publish: (key: PeerId, value: CID | PeerId, options?: PublishOptions) => Promise<IPNSEntry>
133
+
134
+ /**
135
+ * Accepts a public key formatted as a libp2p PeerID and resolves the IPNS record
136
+ * corresponding to that public key until a value is found
137
+ */
138
+ resolve: (key: PeerId, options?: ResolveOptions) => Promise<CID>
139
+
140
+ /**
141
+ * Resolve a CID from a dns-link style IPNS record
142
+ */
143
+ resolveDns: (domain: string, options?: ResolveOptions) => Promise<CID>
144
+
145
+ /**
146
+ * Periodically republish all IPNS records found in the datastore
147
+ */
148
+ republish: (options?: RepublishOptions) => void
149
+ }
150
+
151
+ export type { IPNSRouting } from './routing/index.js'
152
+
153
+ export interface IPNSComponents {
154
+ datastore: Datastore
155
+ }
156
+
157
+ class DefaultIPNS implements IPNS {
158
+ private readonly routers: IPNSRouting[]
159
+ private readonly localStore: LocalStore
160
+ private timeout?: ReturnType<typeof setTimeout>
161
+
162
+ constructor (components: IPNSComponents, routers: IPNSRouting[] = []) {
163
+ this.routers = routers
164
+ this.localStore = localStore(components.datastore)
165
+ }
166
+
167
+ async publish (key: PeerId, value: CID | PeerId, options: PublishOptions = {}): Promise<IPNSEntry> {
168
+ try {
169
+ let sequenceNumber = 1n
170
+ const routingKey = peerIdToRoutingKey(key)
171
+
172
+ if (await this.localStore.has(routingKey, options)) {
173
+ // if we have published under this key before, increment the sequence number
174
+ const buf = await this.localStore.get(routingKey, options)
175
+ const existingRecord = unmarshal(buf)
176
+ sequenceNumber = existingRecord.sequence + 1n
177
+ }
178
+
179
+ let str
180
+
181
+ if (isPeerId(value)) {
182
+ str = `/ipns/${value.toString()}`
183
+ } else {
184
+ str = `/ipfs/${value.toString()}`
185
+ }
186
+
187
+ const bytes = uint8ArrayFromString(str)
188
+
189
+ // create record
190
+ const record = await create(key, bytes, sequenceNumber, options.lifetime ?? DEFAULT_LIFETIME_MS)
191
+ const marshaledRecord = marshal(record)
192
+
193
+ await this.localStore.put(routingKey, marshaledRecord, options)
194
+
195
+ // publish record to routing
196
+ await Promise.all(this.routers.map(async r => { await r.put(routingKey, marshaledRecord, options) }))
197
+
198
+ return record
199
+ } catch (err: any) {
200
+ options.onProgress?.(new CustomProgressEvent<Error>('ipns:publish:error', err))
201
+ throw err
202
+ }
203
+ }
204
+
205
+ async resolve (key: PeerId, options: ResolveOptions = {}): Promise<CID> {
206
+ const routingKey = peerIdToRoutingKey(key)
207
+ const record = await this.#findIpnsRecord(routingKey, options)
208
+ const str = uint8ArrayToString(record.value)
209
+
210
+ return await this.#resolve(str)
211
+ }
212
+
213
+ async resolveDns (domain: string, options: ResolveOptions = {}): Promise<CID> {
214
+ const dnslink = await resolveDnslink(domain, options)
215
+
216
+ return await this.#resolve(dnslink)
217
+ }
218
+
219
+ republish (options: RepublishOptions = {}): void {
220
+ if (this.timeout != null) {
221
+ throw new Error('Republish is already running')
222
+ }
223
+
224
+ options.signal?.addEventListener('abort', () => {
225
+ clearTimeout(this.timeout)
226
+ })
227
+
228
+ async function republish (): Promise<void> {
229
+ const startTime = Date.now()
230
+
231
+ options.onProgress?.(new CustomProgressEvent('ipns:republish:start'))
232
+
233
+ const finishType = Date.now()
234
+ const timeTaken = finishType - startTime
235
+ let nextInterval = DEFAULT_REPUBLISH_INTERVAL_MS - timeTaken
236
+
237
+ if (nextInterval < 0) {
238
+ nextInterval = options.interval ?? DEFAULT_REPUBLISH_INTERVAL_MS
239
+ }
240
+
241
+ setTimeout(() => {
242
+ republish().catch(err => {
243
+ log.error('error republishing', err)
244
+ })
245
+ }, nextInterval)
246
+ }
247
+
248
+ this.timeout = setTimeout(() => {
249
+ republish().catch(err => {
250
+ log.error('error republishing', err)
251
+ })
252
+ }, options.interval ?? DEFAULT_REPUBLISH_INTERVAL_MS)
253
+ }
254
+
255
+ async #resolve (ipfsPath: string): Promise<CID> {
256
+ const parts = ipfsPath.split('/')
257
+
258
+ if (parts.length === 3) {
259
+ const scheme = parts[1]
260
+
261
+ if (scheme === 'ipns') {
262
+ return await this.resolve(peerIdFromString(parts[2]))
263
+ } else if (scheme === 'ipfs') {
264
+ return CID.parse(parts[2])
265
+ }
266
+ }
267
+
268
+ log.error('invalid ipfs path %s', ipfsPath)
269
+ throw new Error('Invalid value')
270
+ }
271
+
272
+ async #findIpnsRecord (routingKey: Uint8Array, options: AbortOptions): Promise<IPNSEntry> {
273
+ const routers = [
274
+ this.localStore,
275
+ ...this.routers
276
+ ]
277
+
278
+ const unmarshaledRecord = await Promise.any(
279
+ routers.map(async (router) => {
280
+ const unmarshaledRecord = await router.get(routingKey, options)
281
+ await ipnsValidator(routingKey, unmarshaledRecord)
282
+
283
+ return unmarshaledRecord
284
+ })
285
+ )
286
+
287
+ return unmarshal(unmarshaledRecord)
288
+ }
289
+ }
290
+
291
+ export function ipns (components: IPNSComponents, routers: IPNSRouting[] = []): IPNS {
292
+ return new DefaultIPNS(components, routers)
293
+ }
294
+
295
+ export { ipnsValidator }
296
+ export { ipnsSelector } from 'ipns/selector'
@@ -0,0 +1,85 @@
1
+ import { logger } from '@libp2p/logger'
2
+ import type { IPNSRouting } from '../index.js'
3
+ import type { DHT, QueryEvent } from '@libp2p/interface-dht'
4
+ import type { GetOptions, PutOptions } from './index.js'
5
+ import { CustomProgressEvent, ProgressEvent } from 'progress-events'
6
+
7
+ const log = logger('helia:ipns:routing:dht')
8
+
9
+ export interface DHTRoutingComponents {
10
+ libp2p: {
11
+ dht: DHT
12
+ }
13
+ }
14
+
15
+ export type DHTProgressEvents =
16
+ ProgressEvent<'ipns:routing:dht:query', QueryEvent> |
17
+ ProgressEvent<'ipns:routing:dht:error', Error>
18
+
19
+ export class DHTRouting implements IPNSRouting {
20
+ private readonly dht: DHT
21
+
22
+ constructor (components: DHTRoutingComponents) {
23
+ this.dht = components.libp2p.dht
24
+ }
25
+
26
+ async put (routingKey: Uint8Array, marshaledRecord: Uint8Array, options: PutOptions = {}): Promise<void> {
27
+ let putValue = false
28
+
29
+ try {
30
+ for await (const event of this.dht.put(routingKey, marshaledRecord, options)) {
31
+ logEvent('DHT put event', event)
32
+
33
+ options.onProgress?.(new CustomProgressEvent<QueryEvent>('ipns:routing:dht:query', event))
34
+
35
+ if (event.name === 'PEER_RESPONSE' && event.messageName === 'PUT_VALUE') {
36
+ putValue = true
37
+ }
38
+ }
39
+ } catch (err: any) {
40
+ options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:dht:error', err))
41
+ }
42
+
43
+ if (!putValue) {
44
+ throw new Error('Could not put value to DHT')
45
+ }
46
+ }
47
+
48
+ async get (routingKey: Uint8Array, options: GetOptions = {}): Promise<Uint8Array> {
49
+ try {
50
+ for await (const event of this.dht.get(routingKey, options)) {
51
+ logEvent('DHT get event', event)
52
+
53
+ options.onProgress?.(new CustomProgressEvent<QueryEvent>('ipns:routing:dht:query', event))
54
+
55
+ if (event.name === 'VALUE') {
56
+ return event.value
57
+ }
58
+ }
59
+ } catch (err: any) {
60
+ options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:dht:error', err))
61
+ }
62
+
63
+ throw new Error('Not found')
64
+ }
65
+ }
66
+
67
+ function logEvent (prefix: string, event: QueryEvent): void {
68
+ if (event.name === 'SENDING_QUERY') {
69
+ log(prefix, event.name, event.messageName, '->', event.to.toString())
70
+ } else if (event.name === 'PEER_RESPONSE') {
71
+ log(prefix, event.name, event.messageName, '<-', event.from.toString())
72
+ } else if (event.name === 'FINAL_PEER') {
73
+ log(prefix, event.name, event.peer.id.toString())
74
+ } else if (event.name === 'QUERY_ERROR') {
75
+ log(prefix, event.name, event.error.message)
76
+ } else if (event.name === 'PROVIDER') {
77
+ log(prefix, event.name, event.providers.map(p => p.id.toString()).join(', '))
78
+ } else {
79
+ log(prefix, event.name)
80
+ }
81
+ }
82
+
83
+ export function dht (components: DHTRoutingComponents): IPNSRouting {
84
+ return new DHTRouting(components)
85
+ }
@@ -0,0 +1,26 @@
1
+ import type { ProgressOptions } from 'progress-events'
2
+ import type { AbortOptions } from '@libp2p/interfaces'
3
+ import type { DHTProgressEvents } from './dht.js'
4
+ import type { DatastoreProgressEvents } from './local-store.js'
5
+ import type { PubSubProgressEvents } from './pubsub.js'
6
+
7
+ export interface PutOptions extends AbortOptions, ProgressOptions {
8
+
9
+ }
10
+
11
+ export interface GetOptions extends AbortOptions, ProgressOptions {
12
+
13
+ }
14
+
15
+ export interface IPNSRouting {
16
+ put: (routingKey: Uint8Array, marshaledRecord: Uint8Array, options?: PutOptions) => Promise<void>
17
+ get: (routingKey: Uint8Array, options?: GetOptions) => Promise<Uint8Array>
18
+ }
19
+
20
+ export type IPNSRoutingEvents =
21
+ DatastoreProgressEvents |
22
+ DHTProgressEvents |
23
+ PubSubProgressEvents
24
+
25
+ export { dht } from './dht.js'
26
+ export { pubsub } from './pubsub.js'
@@ -0,0 +1,63 @@
1
+ import { CustomProgressEvent, ProgressEvent } from 'progress-events'
2
+ import type { AbortOptions } from '@libp2p/interfaces'
3
+ import { Libp2pRecord } from '@libp2p/record'
4
+ import { Datastore, Key } from 'interface-datastore'
5
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
6
+ import type { GetOptions, IPNSRouting, PutOptions } from '../routing'
7
+
8
+ function dhtRoutingKey (key: Uint8Array): Key {
9
+ return new Key('/dht/record/' + uint8ArrayToString(key, 'base32'), false)
10
+ }
11
+
12
+ export type DatastoreProgressEvents =
13
+ ProgressEvent<'ipns:routing:datastore:put'> |
14
+ ProgressEvent<'ipns:routing:datastore:get'> |
15
+ ProgressEvent<'ipns:routing:datastore:error', Error>
16
+
17
+ export interface LocalStore extends IPNSRouting {
18
+ has: (routingKey: Uint8Array, options?: AbortOptions) => Promise<boolean>
19
+ }
20
+
21
+ /**
22
+ * Returns an IPNSRouting implementation that reads/writes IPNS records to the
23
+ * datastore as DHT records. This lets us publish IPNS records offline then
24
+ * serve them to the network later in response to DHT queries.
25
+ */
26
+ export function localStore (datastore: Datastore): LocalStore {
27
+ return {
28
+ async put (routingKey: Uint8Array, marshaledRecord: Uint8Array, options: PutOptions = {}) {
29
+ try {
30
+ const key = dhtRoutingKey(routingKey)
31
+
32
+ // Marshal to libp2p record as the DHT does
33
+ const record = new Libp2pRecord(routingKey, marshaledRecord, new Date())
34
+
35
+ options.onProgress?.(new CustomProgressEvent('ipns:routing:datastore:put'))
36
+ await datastore.put(key, record.serialize(), options)
37
+ } catch (err: any) {
38
+ options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:datastore:error', err))
39
+ throw err
40
+ }
41
+ },
42
+ async get (routingKey: Uint8Array, options: GetOptions = {}): Promise<Uint8Array> {
43
+ try {
44
+ const key = dhtRoutingKey(routingKey)
45
+
46
+ options.onProgress?.(new CustomProgressEvent('ipns:routing:datastore:get'))
47
+ const buf = await datastore.get(key, options)
48
+
49
+ // Unmarshal libp2p record as the DHT does
50
+ const record = Libp2pRecord.deserialize(buf)
51
+
52
+ return record.value
53
+ } catch (err: any) {
54
+ options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:datastore:error', err))
55
+ throw err
56
+ }
57
+ },
58
+ async has (routingKey: Uint8Array, options: AbortOptions = {}): Promise<boolean> {
59
+ const key = dhtRoutingKey(routingKey)
60
+ return await datastore.has(key, options)
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,195 @@
1
+ import { peerIdToRoutingKey } from 'ipns'
2
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
3
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
4
+ import { logger } from '@libp2p/logger'
5
+ import type { PeerId } from '@libp2p/interface-peer-id'
6
+ import type { Message, PublishResult, PubSub } from '@libp2p/interface-pubsub'
7
+ import type { Datastore } from 'interface-datastore'
8
+ import type { GetOptions, IPNSRouting, PutOptions } from './index.js'
9
+ import { CodeError } from '@libp2p/interfaces/errors'
10
+ import { localStore, LocalStore } from './local-store.js'
11
+ import { ipnsValidator } from 'ipns/validator'
12
+ import { ipnsSelector } from 'ipns/selector'
13
+ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
14
+ import { CustomProgressEvent, ProgressEvent } from 'progress-events'
15
+
16
+ const log = logger('helia:ipns:routing:pubsub')
17
+
18
+ export interface PubsubRoutingComponents {
19
+ datastore: Datastore
20
+ libp2p: {
21
+ peerId: PeerId
22
+ pubsub: PubSub
23
+ }
24
+ }
25
+
26
+ export type PubSubProgressEvents =
27
+ ProgressEvent<'ipns:pubsub:publish', { topic: string, result: PublishResult }> |
28
+ ProgressEvent<'ipns:pubsub:subscribe', { topic: string }> |
29
+ ProgressEvent<'ipns:pubsub:error', Error>
30
+
31
+ /**
32
+ * This IPNS routing receives IPNS record updates via dedicated
33
+ * pubsub topic.
34
+ *
35
+ * Note we must first be subscribed to the topic in order to receive
36
+ * updated records, so the first call to `.get` should be expected
37
+ * to fail!
38
+ */
39
+ class PubSubRouting implements IPNSRouting {
40
+ private subscriptions: string[]
41
+ private readonly localStore: LocalStore
42
+ private readonly peerId: PeerId
43
+ private readonly pubsub: PubSub
44
+
45
+ constructor (components: PubsubRoutingComponents) {
46
+ this.subscriptions = []
47
+ this.localStore = localStore(components.datastore)
48
+ this.peerId = components.libp2p.peerId
49
+ this.pubsub = components.libp2p.pubsub
50
+
51
+ this.pubsub.addEventListener('message', (evt) => {
52
+ const message = evt.detail
53
+
54
+ if (!this.subscriptions.includes(message.topic)) {
55
+ return
56
+ }
57
+
58
+ this.#processPubSubMessage(message).catch(err => {
59
+ log.error('Error processing message', err)
60
+ })
61
+ })
62
+ }
63
+
64
+ async #processPubSubMessage (message: Message): Promise<void> {
65
+ log('message received for topic', message.topic)
66
+
67
+ if (message.type !== 'signed') {
68
+ log.error('unsigned message received, this module can only work with signed messages')
69
+ return
70
+ }
71
+
72
+ if (message.from.equals(this.peerId)) {
73
+ log('not storing record from self')
74
+ return
75
+ }
76
+
77
+ const routingKey = topicToKey(message.topic)
78
+
79
+ await ipnsValidator(routingKey, message.data)
80
+
81
+ if (await this.localStore.has(routingKey)) {
82
+ const currentRecord = await this.localStore.get(routingKey)
83
+
84
+ if (uint8ArrayEquals(currentRecord, message.data)) {
85
+ log('not storing record as we already have it')
86
+ return
87
+ }
88
+
89
+ const records = [currentRecord, message.data]
90
+ const index = ipnsSelector(routingKey, records)
91
+
92
+ if (index === 0) {
93
+ log('not storing record as the one we have is better')
94
+ return
95
+ }
96
+ }
97
+
98
+ await this.localStore.put(routingKey, message.data)
99
+ }
100
+
101
+ /**
102
+ * Put a value to the pubsub datastore indexed by the received key properly encoded
103
+ */
104
+ async put (routingKey: Uint8Array, marshaledRecord: Uint8Array, options: PutOptions = {}): Promise<void> {
105
+ try {
106
+ const topic = keyToTopic(routingKey)
107
+
108
+ log('publish value for topic %s', topic)
109
+ const result = await this.pubsub.publish(topic, marshaledRecord)
110
+
111
+ log('published record on topic %s to %d recipients', topic, result.recipients)
112
+ options.onProgress?.(new CustomProgressEvent('ipns:pubsub:publish', { topic, result }))
113
+ } catch (err: any) {
114
+ options.onProgress?.(new CustomProgressEvent<Error>('ipns:pubsub:error', err))
115
+ throw err
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Get a value from the pubsub datastore indexed by the received key properly encoded.
121
+ * Also, the identifier topic is subscribed to and the pubsub datastore records will be
122
+ * updated once new publishes occur
123
+ */
124
+ async get (routingKey: Uint8Array, options: GetOptions = {}): Promise<Uint8Array> {
125
+ try {
126
+ const topic = keyToTopic(routingKey)
127
+
128
+ // ensure we are subscribed to topic
129
+ if (!this.pubsub.getTopics().includes(topic)) {
130
+ log('add subscription for topic', topic)
131
+ this.pubsub.subscribe(topic)
132
+ this.subscriptions.push(topic)
133
+
134
+ options.onProgress?.(new CustomProgressEvent('ipns:pubsub:subscribe', { topic }))
135
+ }
136
+
137
+ // chain through to local store
138
+ return await this.localStore.get(routingKey, options)
139
+ } catch (err: any) {
140
+ options.onProgress?.(new CustomProgressEvent<Error>('ipns:pubsub:error', err))
141
+ throw err
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Get pubsub subscriptions related to ipns
147
+ */
148
+ getSubscriptions (): string[] {
149
+ return this.subscriptions
150
+ }
151
+
152
+ /**
153
+ * Cancel pubsub subscriptions related to ipns
154
+ */
155
+ cancel (key: PeerId): void {
156
+ const routingKey = peerIdToRoutingKey(key)
157
+ const topic = keyToTopic(routingKey)
158
+
159
+ // Not found topic
160
+ if (!this.subscriptions.includes(topic)) {
161
+ return
162
+ }
163
+
164
+ this.pubsub.unsubscribe(topic)
165
+ this.subscriptions = this.subscriptions.filter(t => t !== topic)
166
+ }
167
+ }
168
+
169
+ const PUBSUB_NAMESPACE = '/record/'
170
+
171
+ /**
172
+ * converts a binary record key to a pubsub topic key
173
+ */
174
+ function keyToTopic (key: Uint8Array): string {
175
+ const b64url = uint8ArrayToString(key, 'base64url')
176
+
177
+ return `${PUBSUB_NAMESPACE}${b64url}`
178
+ }
179
+
180
+ /**
181
+ * converts a pubsub topic key to a binary record key
182
+ */
183
+ function topicToKey (topic: string): Uint8Array {
184
+ if (topic.substring(0, PUBSUB_NAMESPACE.length) !== PUBSUB_NAMESPACE) {
185
+ throw new CodeError('topic received is not from a record', 'ERR_TOPIC_IS_NOT_FROM_RECORD_NAMESPACE')
186
+ }
187
+
188
+ const key = topic.substring(PUBSUB_NAMESPACE.length)
189
+
190
+ return uint8ArrayFromString(key, 'base64url')
191
+ }
192
+
193
+ export function pubsub (components: PubsubRoutingComponents): IPNSRouting {
194
+ return new PubSubRouting(components)
195
+ }