Skip to content

Commit

Permalink
fix: cache IPNS entries after resolving (#35)
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 authored May 9, 2023
1 parent a4edce7 commit 704b413
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 704b413

Please sign in to comment.