Skip to content

Commit

Permalink
feat: add cache control to IPNS (#473)
Browse files Browse the repository at this point in the history
* feat: add cache control to IPNS

Adds a `nocache` option when resolving IPNS records similar to when
resolving DNSLink records.

The record will be resolved from the local datastore and returned if
it is valid (e.g. non-expired, correct key, etc).

If the record is not present it will be fetched from the routing and
stored in the local datastore if it is valid.

- `nocache` will skip the datastore and use the routing.
- `offline` only uses the datastore and skips the routing.

* chore: add missing dep

* chore: apply suggestions from code review

Co-authored-by: Daniel Norman <[email protected]>

---------

Co-authored-by: Daniel Norman <[email protected]>
  • Loading branch information
achingbrain and 2color authored Apr 2, 2024
1 parent 9ac5909 commit b00f682
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 37 deletions.
1 change: 1 addition & 0 deletions packages/ipns/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
"@types/dns-packet": "^5.6.5",
"aegir": "^42.2.5",
"datastore-core": "^9.2.9",
"it-drain": "^3.0.5",
"sinon": "^17.0.1",
"sinon-ts": "^2.0.0"
},
Expand Down
53 changes: 38 additions & 15 deletions packages/ipns/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,18 @@ export interface PublishOptions extends AbortOptions, ProgressOptions<PublishPro

export interface ResolveOptions extends AbortOptions, ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents> {
/**
* Do not query the network for the IPNS record (default: false)
* Do not query the network for the IPNS record
*
* @default false
*/
offline?: boolean

/**
* Do not use cached IPNS Record entries
*
* @default false
*/
nocache?: boolean
}

export interface ResolveDNSLinkOptions extends AbortOptions, ProgressOptions<ResolveDNSLinkProgressEvents> {
Expand Down Expand Up @@ -523,22 +532,40 @@ class DefaultIPNS implements IPNS {
}

async #findIpnsRecord (routingKey: Uint8Array, options: ResolveOptions = {}): Promise<IPNSRecord> {
let routers = [
this.localStore,
...this.routers
]
const records: Uint8Array[] = []
const cached = await this.localStore.has(routingKey, options)

if (cached) {
log('record is present in the cache')

if (options.nocache !== true) {
// check the local cache first
const record = await this.localStore.get(routingKey, options)

try {
await ipnsValidator(routingKey, record)

this.log('record successfully retrieved from cache')

return unmarshal(record)
} catch (err) {
this.log('cached record was invalid', err)
}
} else {
log('ignoring local cache due to nocache=true option')
}
}

if (options.offline === true) {
routers = [
this.localStore
]
throw new CodeError('Record was not present in the cache or has expired', 'ERR_NOT_FOUND')
}

const records: Uint8Array[] = []
log('did not have record locally')

let foundInvalid = 0

await Promise.all(
routers.map(async (router) => {
this.routers.map(async (router) => {
let record: Uint8Array

try {
Expand All @@ -547,11 +574,7 @@ class DefaultIPNS implements IPNS {
validate: false
})
} catch (err: any) {
if (router === this.localStore && err.code === 'ERR_NOT_FOUND') {
log('did not have record locally')
} else {
log.error('error finding IPNS record', err)
}
log.error('error finding IPNS record', err)

return
}
Expand Down
8 changes: 4 additions & 4 deletions packages/ipns/test/resolve-dnslink.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe('resolveDNSLink', () => {

await name.publish(key, cid)

const result = await name.resolveDNSLink('foobar.baz', { nocache: true })
const result = await name.resolveDNSLink('foobar.baz')

if (result == null) {
throw new Error('Did not resolve entry')
Expand Down Expand Up @@ -186,7 +186,7 @@ describe('resolveDNSLink', () => {

await name.publish(key, cid)

const result = await name.resolveDNSLink('foobar.baz', { nocache: true })
const result = await name.resolveDNSLink('foobar.baz')

if (result == null) {
throw new Error('Did not resolve entry')
Expand All @@ -213,7 +213,7 @@ describe('resolveDNSLink', () => {

await name.publish(key, cid)

const result = await name.resolveDNSLink('foobar.baz', { nocache: true })
const result = await name.resolveDNSLink('foobar.baz')

if (result == null) {
throw new Error('Did not resolve entry')
Expand All @@ -235,7 +235,7 @@ describe('resolveDNSLink', () => {

await name.publish(key, cid)

const result = await name.resolveDNSLink('foobar.baz', { nocache: true })
const result = await name.resolveDNSLink('foobar.baz')

if (result == null) {
throw new Error('Did not resolve entry')
Expand Down
54 changes: 36 additions & 18 deletions packages/ipns/test/resolve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { expect } from 'aegir/chai'
import { MemoryDatastore } from 'datastore-core'
import { type Datastore, Key } from 'interface-datastore'
import { create, marshal, peerIdToRoutingKey, unmarshal } from 'ipns'
import drain from 'it-drain'
import { CID } from 'multiformats/cid'
import Sinon from 'sinon'
import { type StubbedInstance, stubInterface } from 'sinon-ts'
Expand Down Expand Up @@ -46,14 +47,14 @@ describe('resolve', () => {

it('should resolve a record', async () => {
const key = await createEd25519PeerId()
await name.publish(key, cid)
const record = await name.publish(key, cid)

const resolvedValue = await name.resolve(key)
// empty the datastore to ensure we resolve using the routing
await drain(datastore.deleteMany(datastore.queryKeys({})))

if (resolvedValue == null) {
throw new Error('Did not resolve entry')
}
heliaRouting.get.resolves(marshal(record))

const resolvedValue = await name.resolve(key)
expect(resolvedValue.cid.toString()).to.equal(cid.toV1().toString())

expect(heliaRouting.get.called).to.be.true()
Expand All @@ -70,15 +71,42 @@ describe('resolve', () => {
const resolvedValue = await name.resolve(key, {
offline: true
})
expect(resolvedValue.cid.toString()).to.equal(cid.toV1().toString())

expect(heliaRouting.get.called).to.be.false()
expect(customRouting.get.called).to.be.false()
})

it('should skip the local cache when resolving a record', async () => {
const cacheGetSpy = Sinon.spy(datastore, 'get')

const key = await createEd25519PeerId()
const record = await name.publish(key, cid)

heliaRouting.get.resolves(marshal(record))

const resolvedValue = await name.resolve(key, {
nocache: true
})
expect(resolvedValue.cid.toString()).to.equal(cid.toV1().toString())

expect(heliaRouting.get.called).to.be.true()
expect(customRouting.get.called).to.be.true()
expect(cacheGetSpy.called).to.be.false()
})

if (resolvedValue == null) {
throw new Error('Did not resolve entry')
}
it('should retrieve from local cache when resolving a record', async () => {
const cacheGetSpy = Sinon.spy(datastore, 'get')

const key = await createEd25519PeerId()
await name.publish(key, cid)

const resolvedValue = await name.resolve(key)
expect(resolvedValue.cid.toString()).to.equal(cid.toV1().toString())

expect(heliaRouting.get.called).to.be.false()
expect(customRouting.get.called).to.be.false()
expect(cacheGetSpy.called).to.be.true()
})

it('should resolve a recursive record', async () => {
Expand All @@ -88,11 +116,6 @@ describe('resolve', () => {
await name.publish(key1, key2)

const resolvedValue = await name.resolve(key1)

if (resolvedValue == null) {
throw new Error('Did not resolve entry')
}

expect(resolvedValue.cid.toString()).to.equal(cid.toV1().toString())
})

Expand All @@ -103,11 +126,6 @@ describe('resolve', () => {
await name.publish(key1, key2)

const resolvedValue = await name.resolve(key1)

if (resolvedValue == null) {
throw new Error('Did not resolve entry')
}

expect(resolvedValue.cid.toString()).to.equal(cid.toV1().toString())
})

Expand Down

0 comments on commit b00f682

Please sign in to comment.