From 43e32a20f44fffd533531a57e6d60883cebc55ca Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Wed, 29 Mar 2023 16:01:29 +0100 Subject: [PATCH] feat: allow publish/resolve using only local datastore (#15) Adds an `offline` option to publish and resolve that causes this module to only use the local datastore instead of any configured routers. --- packages/ipns/package.json | 3 +- packages/ipns/src/index.ts | 46 +++++++++++++++++++++--------- packages/ipns/test/publish.spec.ts | 19 ++++++++++-- packages/ipns/test/resolve.spec.ts | 29 +++++++++++++++++-- 4 files changed, 77 insertions(+), 20 deletions(-) diff --git a/packages/ipns/package.json b/packages/ipns/package.json index c97ef61..427857c 100644 --- a/packages/ipns/package.json +++ b/packages/ipns/package.json @@ -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" diff --git a/packages/ipns/src/index.ts b/packages/ipns/src/index.ts index 5991e1e..7e2345d 100644 --- a/packages/ipns/src/index.ts +++ b/packages/ipns/src/index.ts @@ -104,21 +104,33 @@ export type RepublishProgressEvents = export interface PublishOptions extends AbortOptions, ProgressOptions { /** - * 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 { /** - * 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 { /** - * The republish interval in ms (default: 24hrs) + * The republish interval in ms (default: 23hrs) */ interval?: number } @@ -140,7 +152,7 @@ export interface IPNS { /** * Resolve a CID from a dns-link style IPNS record */ - resolveDns: (domain: string, options?: ResolveOptions) => Promise + resolveDns: (domain: string, options?: ResolveDNSOptions) => Promise /** * Periodically republish all IPNS records found in the datastore @@ -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) { @@ -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 { + async resolveDns (domain: string, options: ResolveDNSOptions = {}): Promise { const dnslink = await resolveDnslink(domain, options) - return await this.#resolve(dnslink) + return await this.#resolve(dnslink, options) } republish (options: RepublishOptions = {}): void { @@ -252,14 +266,14 @@ class DefaultIPNS implements IPNS { }, options.interval ?? DEFAULT_REPUBLISH_INTERVAL_MS) } - async #resolve (ipfsPath: string): Promise { + async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise { 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]) } @@ -269,12 +283,18 @@ class DefaultIPNS implements IPNS { throw new Error('Invalid value') } - async #findIpnsRecord (routingKey: Uint8Array, options: AbortOptions): Promise { - const routers = [ + async #findIpnsRecord (routingKey: Uint8Array, options: ResolveOptions = {}): Promise { + 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) diff --git a/packages/ipns/test/publish.spec.ts b/packages/ipns/test/publish.spec.ts index 2ff3ea4..3e8b7aa 100644 --- a/packages/ipns/test/publish.spec.ts +++ b/packages/ipns/test/publish.spec.ts @@ -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 - before(async () => { + beforeEach(async () => { const datastore = new MemoryDatastore() + routing = stubInterface() + 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 () { @@ -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() diff --git a/packages/ipns/test/resolve.spec.ts b/packages/ipns/test/resolve.spec.ts index e489127..70bec01 100644 --- a/packages/ipns/test/resolve.spec.ts +++ b/packages/ipns/test/resolve.spec.ts @@ -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 - before(async () => { + beforeEach(async () => { const datastore = new MemoryDatastore() + routing = stubInterface() + routing.get.throws(new Error('Not found')) - name = ipns({ datastore }) + name = ipns({ datastore }, [routing]) }) it('should resolve a record', async () => { @@ -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()