Skip to content
This repository has been archived by the owner on Jan 8, 2024. It is now read-only.

Commit

Permalink
fix: cache IPNS entries after resolving
Browse files Browse the repository at this point in the history
After resolving one or more IPNS records, use the selector to choose
one and then cache the result.

Fixes #20
  • Loading branch information
achingbrain committed May 5, 2023
1 parent e04eca9 commit 6750d1c
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 9 deletions.
28 changes: 22 additions & 6 deletions packages/ipns/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { create, marshal, peerIdToRoutingKey, unmarshal } from 'ipns'
import type { IPNSEntry } from 'ipns'
import type { IPNSRouting, IPNSRoutingEvents } from './routing/index.js'
import { ipnsValidator } from 'ipns/validator'
import { ipnsSelector } from 'ipns/selector'
import { CID } from 'multiformats/cid'
import { resolveDnslink } from './utils/resolve-dns-link.js'
import { logger } from '@libp2p/logger'
Expand All @@ -78,6 +79,7 @@ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import type { Datastore } from 'interface-datastore'
import { localStore, LocalStore } from './routing/local-store.js'
import { CodeError } from '@libp2p/interfaces/errors'

const log = logger('helia:ipns')

Expand Down Expand Up @@ -295,16 +297,30 @@ class DefaultIPNS implements IPNS {
]
}

const unmarshaledRecord = await Promise.any(
routers.map(async (router) => {
const unmarshaledRecord = await router.get(routingKey, options)
await ipnsValidator(routingKey, unmarshaledRecord)
const records: Uint8Array[] = []

return unmarshaledRecord
await Promise.all(
routers.map(async (router) => {
try {
const record = await router.get(routingKey, options)
await ipnsValidator(routingKey, record)

records.push(record)
} catch (err) {
log.error('error finding IPNS record', err)
}
})
)

return unmarshal(unmarshaledRecord)
if (records.length === 0) {
throw new CodeError('Could not find record for routing key', 'ERR_NOT_FOUND')
}

const record = records[ipnsSelector(routingKey, records)]

await this.localStore.put(routingKey, record, options)

return unmarshal(record)
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/ipns/src/routing/local-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ export interface LocalStore extends IPNSRouting {
*/
export function localStore (datastore: Datastore): LocalStore {
return {
async put (routingKey: Uint8Array, marshaledRecord: Uint8Array, options: PutOptions = {}) {
async put (routingKey: Uint8Array, marshalledRecord: Uint8Array, options: PutOptions = {}) {
try {
const key = dhtRoutingKey(routingKey)

// Marshal to libp2p record as the DHT does
const record = new Libp2pRecord(routingKey, marshaledRecord, new Date())
const record = new Libp2pRecord(routingKey, marshalledRecord, new Date())

options.onProgress?.(new CustomProgressEvent('ipns:routing:datastore:put'))
await datastore.put(key, record.serialize(), options)
Expand Down
52 changes: 51 additions & 1 deletion packages/ipns/test/resolve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@ import { CID } from 'multiformats/cid'
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import Sinon from 'sinon'
import { StubbedInstance, stubInterface } from 'sinon-ts'
import { create, marshal, peerIdToRoutingKey } from 'ipns'
import { Datastore, Key } from 'interface-datastore'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { Libp2pRecord } from '@libp2p/record'

const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')

describe('resolve', () => {
let name: IPNS
let routing: StubbedInstance<IPNSRouting>
let datastore: Datastore

beforeEach(async () => {
const datastore = new MemoryDatastore()
datastore = new MemoryDatastore()
routing = stubInterface<IPNSRouting>()
routing.get.throws(new Error('Not found'))

Expand Down Expand Up @@ -98,4 +104,48 @@ describe('resolve', () => {

expect(onProgress).to.have.property('called', true)
})

it('should cache a record', async function () {
const peerId = await createEd25519PeerId()
const routingKey = peerIdToRoutingKey(peerId)
const dhtKey = new Key('/dht/record/' + uint8ArrayToString(routingKey, 'base32'), false)

expect(datastore.has(dhtKey)).to.be.false('already had record')

const bytes = uint8ArrayFromString(`/ipfs/${cid.toString()}`)
const record = await create(peerId, bytes, 0n, 60000)
const marshalledRecord = marshal(record)

routing.get.withArgs(routingKey).resolves(marshalledRecord)

const result = await name.resolve(peerId)
expect(result.toString()).to.equal(cid.toString(), 'incorrect record resolved')

expect(datastore.has(dhtKey)).to.be.true('did not cache record locally')
})

it('should cache the most recent record', async function () {
const peerId = await createEd25519PeerId()
const routingKey = peerIdToRoutingKey(peerId)
const dhtKey = new Key('/dht/record/' + uint8ArrayToString(routingKey, 'base32'), false)

const marshalledRecordA = marshal(await create(peerId, uint8ArrayFromString(`/ipfs/${cid.toString()}`), 0n, 60000))
const marshalledRecordB = marshal(await create(peerId, uint8ArrayFromString(`/ipfs/${cid.toString()}`), 10n, 60000))

// records should not match
expect(marshalledRecordA).to.not.equalBytes(marshalledRecordB)

// cache has older record
await datastore.put(dhtKey, marshalledRecordA)
routing.get.withArgs(routingKey).resolves(marshalledRecordB)

const result = await name.resolve(peerId)
expect(result.toString()).to.equal(cid.toString(), 'incorrect record resolved')

const cached = await datastore.get(dhtKey)
const record = Libp2pRecord.deserialize(cached)

// should have cached the updated record
expect(record.value).to.equalBytes(marshalledRecordB)
})
})

0 comments on commit 6750d1c

Please sign in to comment.