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

Commit

Permalink
feat: allow publish/resolve using only local datastore (#15)
Browse files Browse the repository at this point in the history
Adds an `offline` option to publish and resolve that causes this
module to only use the local datastore instead of any configured
routers.
  • Loading branch information
achingbrain authored Mar 29, 2023
1 parent a51fbe3 commit 43e32a2
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 20 deletions.
3 changes: 2 additions & 1 deletion packages/ipns/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@
"@libp2p/peer-id-factory": "^2.0.1",
"aegir": "^38.1.0",
"datastore-core": "^9.0.3",
"sinon": "^15.0.1"
"sinon": "^15.0.1",
"sinon-ts": "^1.0.0"
},
"browser": {
"./dist/src/utils/resolve-dns-link.js": "./dist/src/utils/resolve-dns-link.browser.js"
Expand Down
46 changes: 33 additions & 13 deletions packages/ipns/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,33 @@ export type RepublishProgressEvents =

export interface PublishOptions extends AbortOptions, ProgressOptions<PublishProgressEvents | IPNSRoutingEvents> {
/**
* Time duration of the record in ms
* Time duration of the record in ms (default: 24hrs)
*/
lifetime?: number

/**
* Only publish to a local datastore (default: false)
*/
offline?: boolean
}

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

export interface ResolveDNSOptions extends ResolveOptions {
/**
* Do not use cached DNS entries (default: false)
*/
nocache?: boolean
}

export interface RepublishOptions extends AbortOptions, ProgressOptions<RepublishProgressEvents | IPNSRoutingEvents> {
/**
* The republish interval in ms (default: 24hrs)
* The republish interval in ms (default: 23hrs)
*/
interval?: number
}
Expand All @@ -140,7 +152,7 @@ export interface IPNS {
/**
* Resolve a CID from a dns-link style IPNS record
*/
resolveDns: (domain: string, options?: ResolveOptions) => Promise<CID>
resolveDns: (domain: string, options?: ResolveDNSOptions) => Promise<CID>

/**
* Periodically republish all IPNS records found in the datastore
Expand Down Expand Up @@ -192,8 +204,10 @@ class DefaultIPNS implements IPNS {

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

// publish record to routing
await Promise.all(this.routers.map(async r => { await r.put(routingKey, marshaledRecord, options) }))
if (options.offline !== true) {
// publish record to routing
await Promise.all(this.routers.map(async r => { await r.put(routingKey, marshaledRecord, options) }))
}

return record
} catch (err: any) {
Expand All @@ -207,13 +221,13 @@ class DefaultIPNS implements IPNS {
const record = await this.#findIpnsRecord(routingKey, options)
const str = uint8ArrayToString(record.value)

return await this.#resolve(str)
return await this.#resolve(str, options)
}

async resolveDns (domain: string, options: ResolveOptions = {}): Promise<CID> {
async resolveDns (domain: string, options: ResolveDNSOptions = {}): Promise<CID> {
const dnslink = await resolveDnslink(domain, options)

return await this.#resolve(dnslink)
return await this.#resolve(dnslink, options)
}

republish (options: RepublishOptions = {}): void {
Expand Down Expand Up @@ -252,14 +266,14 @@ class DefaultIPNS implements IPNS {
}, options.interval ?? DEFAULT_REPUBLISH_INTERVAL_MS)
}

async #resolve (ipfsPath: string): Promise<CID> {
async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise<CID> {
const parts = ipfsPath.split('/')

if (parts.length === 3) {
const scheme = parts[1]

if (scheme === 'ipns') {
return await this.resolve(peerIdFromString(parts[2]))
return await this.resolve(peerIdFromString(parts[2]), options)
} else if (scheme === 'ipfs') {
return CID.parse(parts[2])
}
Expand All @@ -269,12 +283,18 @@ class DefaultIPNS implements IPNS {
throw new Error('Invalid value')
}

async #findIpnsRecord (routingKey: Uint8Array, options: AbortOptions): Promise<IPNSEntry> {
const routers = [
async #findIpnsRecord (routingKey: Uint8Array, options: ResolveOptions = {}): Promise<IPNSEntry> {
let routers = [
this.localStore,
...this.routers
]

if (options.offline === true) {
routers = [
this.localStore
]
}

const unmarshaledRecord = await Promise.any(
routers.map(async (router) => {
const unmarshaledRecord = await router.get(routingKey, options)
Expand Down
19 changes: 16 additions & 3 deletions packages/ipns/test/publish.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@

import { expect } from 'aegir/chai'
import { MemoryDatastore } from 'datastore-core'
import type { IPNS } from '../src/index.js'
import type { IPNS, IPNSRouting } from '../src/index.js'
import { ipns } from '../src/index.js'
import { CID } from 'multiformats/cid'
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import Sinon from 'sinon'
import { StubbedInstance, stubInterface } from 'sinon-ts'

const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')

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

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

name = ipns({ datastore })
name = ipns({ datastore }, [routing])
})

it('should publish an IPNS record with the default params', async function () {
Expand All @@ -38,6 +42,15 @@ describe('publish', () => {
expect(ipnsEntry).to.have.property('ttl', BigInt(lifetime) * 100000n)
})

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

expect(routing.put.called).to.be.false()
})

it('should emit progress events', async function () {
const key = await createEd25519PeerId()
const onProgress = Sinon.stub()
Expand Down
29 changes: 26 additions & 3 deletions packages/ipns/test/resolve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@

import { expect } from 'aegir/chai'
import { MemoryDatastore } from 'datastore-core'
import type { IPNS } from '../src/index.js'
import type { IPNS, IPNSRouting } from '../src/index.js'
import { ipns } from '../src/index.js'
import { CID } from 'multiformats/cid'
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import Sinon from 'sinon'
import { StubbedInstance, stubInterface } from 'sinon-ts'

const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')

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

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

name = ipns({ datastore })
name = ipns({ datastore }, [routing])
})

it('should resolve a record', async () => {
Expand All @@ -32,6 +36,25 @@ describe('resolve', () => {
expect(resolvedValue.toString()).to.equal(cid.toString())
})

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

expect(routing.put.called).to.be.true()

const resolvedValue = await name.resolve(key, {
offline: true
})

expect(routing.get.called).to.be.false()

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

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

it('should resolve a recursive record', async () => {
const key1 = await createEd25519PeerId()
const key2 = await createEd25519PeerId()
Expand Down

0 comments on commit 43e32a2

Please sign in to comment.