From b6e0ff082b5ba6ea9b682aeb51cda19dbe3c9426 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 6 Dec 2022 11:20:26 +0000 Subject: [PATCH] feat: add static agent constructor methods --- packages/access-client/src/agent-data.js | 15 +++-- packages/access-client/src/agent.js | 66 ++++++++++++++----- .../access-client/src/stores/store-conf.js | 13 +--- .../src/stores/store-indexeddb.js | 18 ++--- packages/access-client/src/stores/types.ts | 8 +-- packages/access-client/src/types.ts | 4 +- packages/access-client/test/agent.test.js | 35 +++------- .../access-client/test/awake.node.test.js | 7 +- 8 files changed, 85 insertions(+), 81 deletions(-) diff --git a/packages/access-client/src/agent-data.js b/packages/access-client/src/agent-data.js index e3c58657b..11639753f 100644 --- a/packages/access-client/src/agent-data.js +++ b/packages/access-client/src/agent-data.js @@ -5,9 +5,7 @@ import { CID } from 'multiformats' /** @typedef {import('./types').AgentDataModel} AgentDataModel */ -/** - * @implements {AgentDataModel} - */ +/** @implements {AgentDataModel} */ export class AgentData { /** @type {(data: import('./types').AgentDataExport) => Promise | void} */ #save @@ -26,6 +24,8 @@ export class AgentData { } /** + * Create a new AgentData instance from the passed initialization data. + * * @param {Partial} [init] * @param {import('./types').AgentDataOptions} [options] */ @@ -67,6 +67,8 @@ export class AgentData { } /** + * Instantiate AgentData from previously exported data. + * * @param {import('./types').AgentDataExport} raw * @param {import('./types').AgentDataOptions} [options] */ @@ -99,6 +101,9 @@ export class AgentData { ) } + /** + * Export data in a format safe to pass to `structuredClone()`. + */ export() { /** @type {import('./types').AgentDataExport} */ const raw = { @@ -127,7 +132,7 @@ export class AgentData { */ async addSpace(did, meta, proof) { this.spaces.set(did, meta) - await (proof ? this.addDelegation(proof) : this.#save(this.export())); + await (proof ? this.addDelegation(proof) : this.#save(this.export())) } /** @@ -145,7 +150,7 @@ export class AgentData { async addDelegation(delegation, meta) { this.delegations.set(delegation.cid.toString(), { delegation, - ...(meta ? { meta } : {}), + meta: meta ?? {}, }) await this.#save(this.export()) } diff --git a/packages/access-client/src/agent.js b/packages/access-client/src/agent.js index 8f354dbc8..de18cbc87 100644 --- a/packages/access-client/src/agent.js +++ b/packages/access-client/src/agent.js @@ -22,6 +22,9 @@ import { validate, canDelegateCapability, } from './delegations.js' +import { AgentData } from './agent-data.js' + +export { AgentData } const HOST = 'https://w3access-staging.protocol-labs.workers.dev' const PRINCIPAL = DID.parse( @@ -80,16 +83,47 @@ export class Agent { */ constructor(data, options = {}) { this.url = options.url ?? new URL(HOST) - this.connection = options.connection ?? connection({ - principal: options.servicePrincipal, - url: this.url - }) + this.connection = + options.connection ?? + connection({ + principal: options.servicePrincipal, + url: this.url, + }) this.#data = data this.#service = undefined } + /** + * Create a new Agent instance, optionally with the passed initialization data. + * + * @param {Partial} [init] + * @param {import('./types').AgentOptions & { store?: import('./types').IStore }} [options] + */ + static async create(init, options = {}) { + const { store } = options + if (store) await store.open() + const data = await AgentData.create(init, { store }) + return new Agent(data, options) + } + + /** + * Create a new Agent instance from pre-exported agent data. + * + * @param {import('./types').AgentDataExport} raw + * @param {import('./types').AgentOptions & { store?: import('./types').IStore }} [options] + */ + static async from(raw, options = {}) { + const { store } = options + const data = AgentData.fromExport(raw, { store }) + return new Agent(data, options) + } + get issuer() { - return this.data.principal + return this.#data.principal + } + + get meta() { + return this.#data.meta } async service() { @@ -103,7 +137,7 @@ export class Agent { } did() { - return this.data.principal.did() + return this.#data.principal.did() } /** @@ -118,7 +152,7 @@ export class Agent { checkAudience: this.issuer, checkIsExpired: true, }) - await this.data.addDelegation(delegation, { audience: this.meta }) + await this.#data.addDelegation(delegation, { audience: this.meta }) } /** @@ -128,7 +162,7 @@ export class Agent { */ async *#delegations(caps) { const _caps = new Set(caps) - for (const [, value] of this.data.delegations) { + for (const [, value] of this.#data.delegations) { // check expiration if (!isExpired(value.delegation)) { // check if delegation can be used @@ -147,7 +181,7 @@ export class Agent { } } else { // delete any expired delegation - await this.data.removeDelegation(value.delegation.cid) + await this.#data.removeDelegation(value.delegation.cid) } } } @@ -210,7 +244,7 @@ export class Agent { }) const meta = { name, isRegistered: false } - await this.data.addSpace(signer.did(), meta, proof) + await this.#data.addSpace(signer.did(), meta, proof) return { did: signer.did(), @@ -312,7 +346,7 @@ export class Agent { throw new Error(`Agent has no proofs for ${space}.`) } - await this.data.setCurrentSpace(space) + await this.#data.setCurrentSpace(space) return space } @@ -321,14 +355,14 @@ export class Agent { * Get current space DID */ currentSpace() { - return this.data.currentSpace + return this.#data.currentSpace } /** * Get current space DID, proofs and abilities */ async currentSpaceWithMeta() { - if (!this.data.currentSpace) { + if (!this.#data.currentSpace) { return } @@ -426,8 +460,8 @@ export class Agent { spaceMeta.isRegistered = true - this.data.addSpace(space, spaceMeta) - this.data.removeDelegation(voucherRedeem.cid) + this.#data.addSpace(space, spaceMeta) + this.#data.removeDelegation(voucherRedeem.cid) } /** @@ -492,7 +526,7 @@ export class Agent { ...options, }) - await this.data.addDelegation(delegation, { + await this.#data.addDelegation(delegation, { audience: options.audienceMeta, }) diff --git a/packages/access-client/src/stores/store-conf.js b/packages/access-client/src/stores/store-conf.js index 256066408..50f2d452d 100644 --- a/packages/access-client/src/stores/store-conf.js +++ b/packages/access-client/src/stores/store-conf.js @@ -38,12 +38,7 @@ export class StoreConf { this.path = this.#config.path } - /** - * @returns {Promise>} - */ - async open() { - return this - } + async open() {} async close() {} @@ -51,13 +46,9 @@ export class StoreConf { this.#config.clear() } - /** - * @param {T} data - * @returns {Promise>} - */ + /** @param {T} data */ async save(data) { this.#config.set(data) - return this } /** @returns {Promise} */ diff --git a/packages/access-client/src/stores/store-indexeddb.js b/packages/access-client/src/stores/store-indexeddb.js index d5d6e1faa..965f4b36e 100644 --- a/packages/access-client/src/stores/store-indexeddb.js +++ b/packages/access-client/src/stores/store-indexeddb.js @@ -45,14 +45,11 @@ export class StoreIndexedDB { this.#dbStoreName = options.dbStoreName ?? STORE_NAME } - /** - * @returns {Promise>} - */ async open() { const db = this.#db - if (db) return this + if (db) return - /** @type {import('p-defer').DeferredPromise>} */ + /** @type {import('p-defer').DeferredPromise} */ const { resolve, reject, promise } = defer() const openReq = indexedDB.open(this.#dbName, this.#dbVersion) @@ -63,7 +60,7 @@ export class StoreIndexedDB { openReq.addEventListener('success', () => { this.#db = openReq.result - resolve(this) + resolve() }) openReq.addEventListener('error', () => reject(openReq.error)) @@ -79,10 +76,7 @@ export class StoreIndexedDB { this.#db = undefined } - /** - * @param {T} data - * @returns {Promise>} - */ + /** @param {T} data */ async save(data) { const db = this.#db if (!db) throw new Error('Store is not open') @@ -92,10 +86,10 @@ export class StoreIndexedDB { 'readwrite', this.#dbStoreName, async (store) => { - /** @type {import('p-defer').DeferredPromise>} */ + /** @type {import('p-defer').DeferredPromise} */ const { resolve, reject, promise } = defer() const putReq = store.put({ id: DATA_ID, ...data }) - putReq.addEventListener('success', () => resolve(this)) + putReq.addEventListener('success', () => resolve()) putReq.addEventListener('error', () => reject(new Error('failed to query DB', { cause: putReq.error })) ) diff --git a/packages/access-client/src/stores/types.ts b/packages/access-client/src/stores/types.ts index 7ef22b7c1..188003b16 100644 --- a/packages/access-client/src/stores/types.ts +++ b/packages/access-client/src/stores/types.ts @@ -5,21 +5,19 @@ export interface IStore { /** * Open store */ - open: () => Promise> + open: () => Promise /** * Clean up and close store */ close: () => Promise /** * Persist data to the store's backend - * - * @param data */ - save: (data: T) => Promise> + save: (data: T) => Promise /** * Loads data from the store's backend */ - load: () => Promise + load: () => Promise /** * Clean all the data in the store's backend */ diff --git a/packages/access-client/src/types.ts b/packages/access-client/src/types.ts index f1a233079..cba58145a 100644 --- a/packages/access-client/src/types.ts +++ b/packages/access-client/src/types.ts @@ -96,7 +96,7 @@ export type AgentDataExport = Pick< delegations: Map< CIDString, { - meta?: DelegationMeta + meta: DelegationMeta delegation: Array<{ cid: CIDString; bytes: Uint8Array }> } > @@ -122,7 +122,7 @@ export interface DelegationMeta { * Audience metadata to be easier to build UIs with human readable data * Normally used with delegations issued to third parties or other devices. */ - audience: AgentMeta + audience?: AgentMeta } /** diff --git a/packages/access-client/test/agent.test.js b/packages/access-client/test/agent.test.js index 278bde9b2..302ce99a6 100644 --- a/packages/access-client/test/agent.test.js +++ b/packages/access-client/test/agent.test.js @@ -1,23 +1,19 @@ import assert from 'assert' import { URI } from '@ucanto/validator' import { Agent, connection } from '../src/agent.js' -import { AgentData } from '../src/agent-data.js' import * as Space from '@web3-storage/capabilities/space' import { createServer } from './helpers/utils.js' import * as fixtures from './helpers/fixtures.js' describe('Agent', function () { it('should return did', async function () { - const data = await AgentData.create() - const agent = new Agent(data) + const agent = await Agent.create() assert.ok(agent.did()) }) it('should create space', async function () { - const data = await AgentData.create() - const agent = new Agent(data) - + const agent = await Agent.create() const space = await agent.createSpace('test-create') assert(typeof space.did === 'string') @@ -25,20 +21,15 @@ describe('Agent', function () { }) it('should add proof when creating acccount', async function () { - const data = await AgentData.create() - const agent = new Agent(data) - + const agent = await Agent.create() const space = await agent.createSpace('test-add') - const delegations = await agent.proofs() assert.equal(space.proof.cid, delegations[0].cid) }) it('should set current space', async function () { - const data = await AgentData.create() - const agent = new Agent(data) - + const agent = await Agent.create() const space = await agent.createSpace('test') await agent.setCurrentSpace(space.did) @@ -53,8 +44,7 @@ describe('Agent', function () { }) it('fails set current space with no proofs', async function () { - const data = await AgentData.create() - const agent = new Agent(data) + const agent = await Agent.create() await assert.rejects( () => { @@ -67,8 +57,7 @@ describe('Agent', function () { }) it('should invoke and execute', async function () { - const data = await AgentData.create() - const agent = new Agent(data, { + const agent = await Agent.create(undefined, { connection: connection({ channel: createServer() }), }) @@ -90,8 +79,7 @@ describe('Agent', function () { }) it('should execute', async function () { - const data = await AgentData.create() - const agent = new Agent(data, { + const agent = await Agent.create(undefined, { connection: connection({ channel: createServer() }), }) @@ -126,8 +114,7 @@ describe('Agent', function () { }) it('should fail execute with no proofs', async function () { - const data = await AgentData.create() - const agent = new Agent(data, { + const agent = await Agent.create(undefined, { connection: connection({ channel: createServer() }), }) @@ -149,8 +136,7 @@ describe('Agent', function () { it('should get space info', async function () { const server = createServer() - const data = await AgentData.create() - const agent = new Agent(data, { + const agent = await Agent.create(undefined, { connection: connection({ channel: server }), }) @@ -174,8 +160,7 @@ describe('Agent', function () { it('should delegate', async function () { const server = createServer() - const data = await AgentData.create() - const agent = new Agent(data, { + const agent = await Agent.create(undefined, { connection: connection({ channel: server }), }) diff --git a/packages/access-client/test/awake.node.test.js b/packages/access-client/test/awake.node.test.js index 15eb776b7..f31348366 100644 --- a/packages/access-client/test/awake.node.test.js +++ b/packages/access-client/test/awake.node.test.js @@ -7,7 +7,6 @@ import PQueue from 'p-queue' import delay from 'delay' import pWaitFor from 'p-wait-for' import { Agent } from '../src/agent.js' -import { AgentData } from '../src/agent-data.js' describe('awake', function () { const host = new URL('ws://127.0.0.1:8788/connect') @@ -38,14 +37,12 @@ describe('awake', function () { }) it('should send msgs', async function () { - const data1 = await AgentData.create() - const agent1 = new Agent(data1, { + const agent1 = await Agent.create(undefined, { url: new URL('http://127.0.0.1:8787'), }) const space = await agent1.createSpace('responder') await agent1.setCurrentSpace(space.did) - const data2 = await AgentData.create() - const agent2 = new Agent(data2, { + const agent2 = await Agent.create(undefined, { url: new URL('http://127.0.0.1:8787'), }) const responder = agent1.peer(ws1)