UNPKG

@web5/agent

Version:
320 lines 16.2 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import ms from 'ms'; import { Convert, NodeStream, TtlCache } from '@web5/common'; import { TENANT_SEPARATOR } from './utils-internal.js'; import { getDataStoreTenant } from './utils-internal.js'; import { DwnInterface } from './types/dwn.js'; export class DwnDataStore { constructor() { this.name = 'DwnDataStore'; /** * Cache of Store Objects referenced by DWN record ID to Store Objects. * * Up to 100 entries are retained for 15 minutes. */ this._cache = new TtlCache({ ttl: ms('15 minutes'), max: 100 }); /** * Index for mappings from Store Identifier to DWN record ID. * Since these values don't change, we can use a long TTL. * * Up to 1,000 entries are retained for 21 days. * NOTE: The maximum number for the ttl is 2^31 - 1 milliseconds (24.8 days), setting to 21 days to be safe. */ this._index = new TtlCache({ ttl: ms('21 days'), max: 1000 }); /** * Cache of tenant DIDs that have been initialized with the protocol. * This is used to avoid redundant protocol initialization requests. * * Since these are default protocols and unlikely to change, we can use a long TTL. */ this._protocolInitializedCache = new TtlCache({ ttl: ms('21 days'), max: 1000 }); /** * Properties to use when writing and querying records with the DWN store. */ this._recordProperties = { dataFormat: 'application/json', }; } delete({ id, agent, tenant }) { return __awaiter(this, void 0, void 0, function* () { // Determine the tenant identifier (DID) for the delete operation. const tenantDid = yield getDataStoreTenant({ agent, tenant, didUri: id }); // Look up the DWN record ID of the object in the store with the given `id`. let matchingRecordId = yield this.lookupRecordId({ id, tenantDid, agent }); // Return false if the given ID was not found in the store. if (!matchingRecordId) return false; // If a record for the given ID was found, attempt to delete it. const { reply: { status } } = yield agent.dwn.processRequest({ author: tenantDid, target: tenantDid, messageType: DwnInterface.RecordsDelete, messageParams: { recordId: matchingRecordId } }); // If the record was successfully deleted, update the index/cache and return true; if (status.code === 202) { this._index.delete(`${tenantDid}${TENANT_SEPARATOR}${id}`); this._cache.delete(matchingRecordId); return true; } // If the Delete operation failed, throw an error. throw new Error(`${this.name}: Failed to delete '${id}' from store: (${status.code}) ${status.detail}`); }); } get({ id, agent, tenant, useCache = false }) { return __awaiter(this, void 0, void 0, function* () { // Determine the tenant identifier (DID) for the list operation. const tenantDid = yield getDataStoreTenant({ agent, tenant, didUri: id }); // Look up the DWN record ID of the object in the store with the given `id`. let matchingRecordId = yield this.lookupRecordId({ id, tenantDid, agent }); // Return undefined if no matches were found. if (!matchingRecordId) return undefined; // Retrieve and return the stored object. return yield this.getRecord({ recordId: matchingRecordId, tenantDid, agent, useCache }); }); } list({ agent, tenant }) { return __awaiter(this, void 0, void 0, function* () { // Determine the tenant identifier (DID) for the list operation. const tenantDid = yield getDataStoreTenant({ tenant, agent }); // Query the DWN for all stored record objects. const storedRecords = yield this.getAllRecords({ agent, tenantDid }); return storedRecords; }); } set({ id, data, tenant, agent, preventDuplicates = true, updateExisting = false, useCache = false }) { return __awaiter(this, void 0, void 0, function* () { // Determine the tenant identifier (DID) for the set operation. const tenantDid = yield getDataStoreTenant({ agent, tenant, didUri: id }); // initialize the storage protocol if not already done yield this.initialize({ tenant: tenantDid, agent }); const messageParams = Object.assign({}, this._recordProperties); if (updateExisting) { // Look up the DWN record ID of the object in the store with the given `id`. const matchingRecordEntry = yield this.getExistingRecordEntry({ id, tenantDid, agent }); if (!matchingRecordEntry) { throw new Error(`${this.name}: Update failed due to missing entry for: ${id}`); } // set the recordId in the messageParams to update the existing record // set the dateCreated to the existing dateCreated as this is an immutable property messageParams.recordId = matchingRecordEntry.recordsWrite.recordId; messageParams.dateCreated = matchingRecordEntry.recordsWrite.descriptor.dateCreated; } else if (preventDuplicates) { // Look up the DWN record ID of the object in the store with the given `id`. const matchingRecordId = yield this.lookupRecordId({ id, tenantDid, agent }); if (matchingRecordId) { throw new Error(`${this.name}: Import failed due to duplicate entry for: ${id}`); } } // Convert the store object to a byte array, which will be the data payload of the DWN record. const dataBytes = Convert.object(data).toUint8Array(); // Store the record in the DWN. const { message, reply: { status } } = yield agent.dwn.processRequest({ author: tenantDid, target: tenantDid, messageType: DwnInterface.RecordsWrite, messageParams: Object.assign(Object.assign({}, this._recordProperties), messageParams), dataStream: new Blob([dataBytes], { type: 'application/json' }) }); // If the write fails, throw an error. if (!(message && status.code === 202)) { throw new Error(`${this.name}: Failed to write data to store for ${id}: ${status.detail}`); } // Add the ID of the newly created record to the index. this._index.set(`${tenantDid}${TENANT_SEPARATOR}${id}`, message.recordId); // If caching is enabled, add the store object to the cache. if (useCache) { this._cache.set(message.recordId, data); } }); } /** * Initialize the relevant protocol for the given tenant. * This confirms that the storage protocol is configured, otherwise it will be installed. */ initialize({ tenant, agent }) { return __awaiter(this, void 0, void 0, function* () { const tenantDid = yield getDataStoreTenant({ agent, tenant }); if (this._protocolInitializedCache.has(tenantDid)) { return; } const { reply: { status, entries } } = yield agent.dwn.processRequest({ author: tenantDid, target: tenantDid, messageType: DwnInterface.ProtocolsQuery, messageParams: { filter: { protocol: this._recordProtocolDefinition.protocol } }, }); if (status.code !== 200) { throw new Error(`Failed to query for protocols: ${status.code} - ${status.detail}`); } if ((entries === null || entries === void 0 ? void 0 : entries.length) === 0) { // protocol is not installed, install it yield this.installProtocol(tenantDid, agent); } this._protocolInitializedCache.set(tenantDid, true); }); } getAllRecords(_params) { return __awaiter(this, void 0, void 0, function* () { throw new Error('Not implemented: Classes extending DwnDataStore must implement getAllRecords()'); }); } getRecord({ recordId, tenantDid, agent, useCache }) { var _a; return __awaiter(this, void 0, void 0, function* () { // If caching is enabled, check the cache for the record ID. if (useCache) { const record = this._cache.get(recordId); // If the record ID was present in the cache, return the associated store object. if (record) return record; // Otherwise, continue to read from the store. } // Read the record from the store. const { reply: readReply } = yield agent.dwn.processRequest({ author: tenantDid, target: tenantDid, messageType: DwnInterface.RecordsRead, messageParams: { filter: { recordId } } }); if (!((_a = readReply.entry) === null || _a === void 0 ? void 0 : _a.data)) { throw new Error(`${this.name}: Failed to read data from DWN for: ${recordId}`); } // If the record was found, convert back to store object format. const storeObject = yield NodeStream.consumeToJson({ readable: readReply.entry.data }); // If caching is enabled, add the store object to the cache. if (useCache) { this._cache.set(recordId, storeObject); } return storeObject; }); } /** * Install the protocol for the given tenant using a `ProtocolsConfigure` message. */ installProtocol(tenant, agent) { return __awaiter(this, void 0, void 0, function* () { const { reply: { status } } = yield agent.dwn.processRequest({ author: tenant, target: tenant, messageType: DwnInterface.ProtocolsConfigure, messageParams: { definition: this._recordProtocolDefinition }, }); if (status.code !== 202) { throw new Error(`Failed to install protocol: ${status.code} - ${status.detail}`); } }); } lookupRecordId({ id, tenantDid, agent }) { return __awaiter(this, void 0, void 0, function* () { // Check the index for a matching ID and extend the index TTL. let recordId = this._index.get(`${tenantDid}${TENANT_SEPARATOR}${id}`, { updateAgeOnGet: true }); // If no matching record ID was found in the index... if (!recordId) { // Query the DWN for all stored objects, which rebuilds the index. yield this.getAllRecords({ agent, tenantDid }); // Check the index again for a matching ID. recordId = this._index.get(`${tenantDid}${TENANT_SEPARATOR}${id}`); } return recordId; }); } getExistingRecordEntry({ id, tenantDid, agent }) { return __awaiter(this, void 0, void 0, function* () { // Look up the DWN record ID of the object in the store with the given `id`. const recordId = yield this.lookupRecordId({ id, tenantDid, agent }); if (recordId) { // Read the record from the store. const { reply: readReply } = yield agent.dwn.processRequest({ author: tenantDid, target: tenantDid, messageType: DwnInterface.RecordsRead, messageParams: { filter: { recordId } } }); return readReply.entry; } }); } } export class InMemoryDataStore { constructor() { this.name = 'InMemoryDataStore'; /** * A private field that contains the Map used as the in-memory data store. */ this.store = new Map(); } delete({ id, agent, tenant }) { return __awaiter(this, void 0, void 0, function* () { // Determine the tenant identifier (DID) for the delete operation. const tenantDid = yield getDataStoreTenant({ agent, tenant, didUri: id }); if (this.store.has(`${tenantDid}${TENANT_SEPARATOR}${id}`)) { // Record with given identifier exists so proceed with delete. this.store.delete(`${tenantDid}${TENANT_SEPARATOR}${id}`); return true; } // Record with given identifier not present so delete operation not possible. return false; }); } get({ id, agent, tenant }) { return __awaiter(this, void 0, void 0, function* () { // Determine the tenant identifier (DID) for the get operation. const tenantDid = yield getDataStoreTenant({ agent, tenant, didUri: id }); return this.store.get(`${tenantDid}${TENANT_SEPARATOR}${id}`); }); } list({ agent, tenant }) { return __awaiter(this, void 0, void 0, function* () { // Determine the tenant identifier (DID) for the list operation. const tenantDid = yield getDataStoreTenant({ tenant, agent }); const result = []; for (const [key, storedRecord] of this.store.entries()) { if (key.startsWith(`${tenantDid}${TENANT_SEPARATOR}`)) { result.push(storedRecord); } } return result; }); } set({ id, data, tenant, agent, preventDuplicates, updateExisting }) { return __awaiter(this, void 0, void 0, function* () { // Determine the tenant identifier (DID) for the set operation. const tenantDid = yield getDataStoreTenant({ agent, tenant, didUri: id }); // If enabled, check if a record with the given `id` is already present in the store. if (updateExisting) { // Look up the DWN record ID of the object in the store with the given `id`. if (!this.store.has(`${tenantDid}${TENANT_SEPARATOR}${id}`)) { throw new Error(`${this.name}: Update failed due to missing entry for: ${id}`); } // set the recordId in the messageParams to update the existing record } else if (preventDuplicates) { const duplicateFound = this.store.has(`${tenantDid}${TENANT_SEPARATOR}${id}`); if (duplicateFound) { throw new Error(`${this.name}: Import failed due to duplicate entry for: ${id}`); } } // Make a deep copy so that the object stored does not share the same references as the input. const clonedData = structuredClone(data); this.store.set(`${tenantDid}${TENANT_SEPARATOR}${id}`, clonedData); }); } } //# sourceMappingURL=store-data.js.map