Skip to content

Commit

Permalink
refactor!: access-client store decoupling (#228)
Browse files Browse the repository at this point in the history
Goals:
1. Decouple store from agent
2. Simplify agent creation
3. Agent governs data format not store
4. Initialization of agent data, not store
5. DRY initialization in agent, not repeated in each store impl

~~See:
https://gist.github.com/alanshaw/ea4bd2b0ab215ade696eac1300be577d~~
outdated

```js
// In regular use:
const store = new StoreIndexedDB()
const data = await store.load()
const agent = data
  ? Agent.from(data, { store })
  : await Agent.create({}, { store })

// Then StoreMemory can be deleted, since the AgentData already stores all data in
// memory. It's equivalent to:
const agent = await Agent.create()
```
  • Loading branch information
Alan Shaw authored Dec 6, 2022
1 parent 7f50af4 commit a785278
Show file tree
Hide file tree
Showing 24 changed files with 685 additions and 863 deletions.
8 changes: 4 additions & 4 deletions packages/access-api/test/helpers/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ export async function context() {

return {
mf,
conn: await connection(
conn: connection({
principal,
// @ts-ignore
mf.dispatchFetch.bind(mf),
new URL('http://localhost:8787')
),
fetch: mf.dispatchFetch.bind(mf),
url: new URL('http://localhost:8787'),
}),
service: Signer.parse(bindings.PRIVATE_KEY),
issuer: principal,
db: new D1QB(db),
Expand Down
4 changes: 4 additions & 0 deletions packages/access-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"exports": {
".": "./src/index.js",
"./agent": "./src/agent.js",
"./drivers/*": "./src/drivers/*.js",
"./stores/*": "./src/stores/*.js",
"./types": "./src/types.js",
"./encoding": "./src/encoding.js"
Expand All @@ -40,6 +41,9 @@
"types": [
"dist/src/types"
],
"drivers/*": [
"dist/src/drivers/*"
],
"stores/*": [
"dist/src/stores/*"
],
Expand Down
145 changes: 145 additions & 0 deletions packages/access-client/src/agent-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Signer } from '@ucanto/principal'
import { Signer as EdSigner } from '@ucanto/principal/ed25519'
import { importDAG } from '@ucanto/core/delegation'
import { CID } from 'multiformats'

/** @typedef {import('./types').AgentDataModel} AgentDataModel */

/** @implements {AgentDataModel} */
export class AgentData {
/** @type {(data: import('./types').AgentDataExport) => Promise<void> | void} */
#save

/**
* @param {import('./types').AgentDataModel} data
* @param {import('./types').AgentDataOptions} [options]
*/
constructor(data, options = {}) {
this.meta = data.meta
this.principal = data.principal
this.spaces = data.spaces
this.delegations = data.delegations
this.currentSpace = data.currentSpace
this.#save = options.store ? options.store.save : () => {}
}

/**
* Create a new AgentData instance from the passed initialization data.
*
* @param {Partial<import('./types').AgentDataModel>} [init]
* @param {import('./types').AgentDataOptions} [options]
*/
static async create(init = {}, options = {}) {
const agentData = new AgentData(
{
meta: { name: 'agent', type: 'device', ...init.meta },
principal: init.principal ?? (await EdSigner.generate()),
spaces: init.spaces ?? new Map(),
delegations: init.delegations ?? new Map(),
currentSpace: init.currentSpace,
},
options
)
if (options.store) {
await options.store.save(agentData.export())
}
return agentData
}

/**
* Instantiate AgentData from previously exported data.
*
* @param {import('./types').AgentDataExport} raw
* @param {import('./types').AgentDataOptions} [options]
*/
static fromExport(raw, options) {
/** @type {import('./types').AgentDataModel['delegations']} */
const dels = new Map()

for (const [key, value] of raw.delegations) {
dels.set(key, {
delegation: importDAG(
value.delegation.map((d) => ({
cid: CID.parse(d.cid),
bytes: d.bytes,
}))
),
meta: value.meta,
})
}

return new AgentData(
{
meta: raw.meta,
// @ts-expect-error
principal: Signer.from(raw.principal),
currentSpace: raw.currentSpace,
spaces: raw.spaces,
delegations: dels,
},
options
)
}

/**
* Export data in a format safe to pass to `structuredClone()`.
*/
export() {
/** @type {import('./types').AgentDataExport} */
const raw = {
meta: this.meta,
principal: this.principal.toArchive(),
currentSpace: this.currentSpace,
spaces: this.spaces,
delegations: new Map(),
}
for (const [key, value] of this.delegations) {
raw.delegations.set(key, {
meta: value.meta,
delegation: [...value.delegation.export()].map((b) => ({
cid: b.cid.toString(),
bytes: b.bytes,
})),
})
}
return raw
}

/**
* @param {import('@ucanto/interface').DID} did
* @param {import('./types').SpaceMeta} meta
* @param {import('@ucanto/interface').Delegation} [proof]
*/
async addSpace(did, meta, proof) {
this.spaces.set(did, meta)
await (proof ? this.addDelegation(proof) : this.#save(this.export()))
}

/**
* @param {import('@ucanto/interface').DID} did
*/
async setCurrentSpace(did) {
this.currentSpace = did
await this.#save(this.export())
}

/**
* @param {import('@ucanto/interface').Delegation} delegation
* @param {import('./types').DelegationMeta} [meta]
*/
async addDelegation(delegation, meta) {
this.delegations.set(delegation.cid.toString(), {
delegation,
meta: meta ?? {},
})
await this.#save(this.export())
}

/**
* @param {import('@ucanto/interface').UCANLink} cid
*/
async removeDelegation(cid) {
this.delegations.delete(cid.toString())
await this.#save(this.export())
}
}
Loading

0 comments on commit a785278

Please sign in to comment.