@web5/agent
Version:
320 lines • 16.2 kB
JavaScript
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