-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor!: access-client store decoupling (#228)
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
Showing
24 changed files
with
685 additions
and
863 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
Oops, something went wrong.