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, Stream, TtlCache } from '@enbox/common';
import { Protocols } from '@enbox/dwn-sdk-js';
import { DwnInterface } from './types/dwn.js';
import { getDataStoreTenant, TENANT_SEPARATOR } from './utils-internal.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.
         *
         * Uses TtlCache with a 1-hour TTL rather than a permanent Set so that
         * protocol state changes by another agent/process (protocol upgrade,
         * reinstall, or tenant clear) are eventually re-detected. Both this
         * and `_tenantEncryptionActive` must share the same TTL so they expire
         * together — otherwise `initialize()` could return early while the
         * encryption state is stale.
         */
        this._protocolInitializedCache = new TtlCache({ ttl: ms('1 hour'), max: 1000 });
        /**
         * Per-tenant encryption resolution cache. Populated during `initialize()`:
         * `true` if the tenant supports encryption (has X25519 keyAgreement).
         * When any type in `_recordProtocolDefinition` has `encryptionRequired: true`,
         * this will always be `true` after successful initialization (since
         * installation fails if encryption is not possible).
         *
         * Uses the same 1-hour TTL as `_protocolInitializedCache` so both
         * caches expire and re-derive together. See comment above.
         */
        this._tenantEncryptionActive = new TtlCache({ ttl: ms('1 hour'), max: 1000 });
        /**
         * Properties to use when writing and querying records with the DWN store.
         * Subclasses MUST override this to include `protocol` and `protocolPath`.
         */
        this._recordProperties = {
            dataFormat: 'application/json',
            protocol: '', // overridden by subclass
            protocolPath: '', // overridden by subclass
        };
    }
    delete(_a) {
        return __awaiter(this, arguments, void 0, function* ({ id, agent, tenant }) {
            // 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`.
            const 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(_a) {
        return __awaiter(this, arguments, void 0, function* ({ id, agent, tenant, useCache = false }) {
            // 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`.
            const 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(_a) {
        return __awaiter(this, arguments, void 0, function* ({ agent, tenant }) {
            // 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(_a) {
        return __awaiter(this, arguments, void 0, function* ({ id, data, tenant, agent, preventDuplicates = true, updateExisting = false, useCache = false }) {
            // 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. If encryption is active for this tenant,
            // the agent's auto-encryption pipeline handles DEK generation and key wrapping.
            const encryptionActive = this.isEncryptionActive(tenantDid);
            const { message, reply: { status } } = yield agent.dwn.processRequest(Object.assign({ author: tenantDid, target: tenantDid, messageType: DwnInterface.RecordsWrite, messageParams: Object.assign(Object.assign({}, this._recordProperties), messageParams), dataStream: new Blob([dataBytes], { type: 'application/json' }) }, (encryptionActive ? { encryption: true } : {})));
            // 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(_a) {
        return __awaiter(this, arguments, void 0, function* ({ tenant, agent }) {
            var _b, _c, _d;
            const tenantDid = yield getDataStoreTenant({ agent, tenant });
            // Short-circuit only when both caches are present. If the encryption
            // cache has expired or was cleared while the init cache survived,
            // re-derive instead of silently dropping encryption.
            if (this._protocolInitializedCache.has(tenantDid)
                && (!this.encryptionRequired || this._tenantEncryptionActive.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}`);
            }
            let encryptionActive = false;
            if ((entries === null || entries === void 0 ? void 0 : entries.length) === 0) {
                // protocol is not installed, install it
                const installed = yield this.installProtocol(tenantDid, agent);
                encryptionActive = installed.encryptionActive;
            }
            else if (this.encryptionRequired) {
                // Protocol already installed — determine if it has $encryption keys
                // by inspecting the installed definition.
                const definition = (_b = entries[0].descriptor) === null || _b === void 0 ? void 0 : _b.definition;
                const firstType = Object.keys((_c = definition === null || definition === void 0 ? void 0 : definition.structure) !== null && _c !== void 0 ? _c : {})[0];
                encryptionActive = !!(firstType && ((_d = definition === null || definition === void 0 ? void 0 : definition.structure[firstType]) === null || _d === void 0 ? void 0 : _d.$encryption));
            }
            // Set both caches atomically so they always expire together. This
            // prevents a stale-encryption window where one cache expires before
            // the other and initialize() short-circuits on the surviving entry.
            this._tenantEncryptionActive.set(tenantDid, encryptionActive);
            this._protocolInitializedCache.set(tenantDid, true);
        });
    }
    /**
     * Returns `true` if any type in the protocol definition has `encryptionRequired: true`.
     * Cached after first computation since the protocol definition is immutable.
     */
    get encryptionRequired() {
        if (this._encryptionRequired === undefined) {
            this._encryptionRequired = Object.values(this._recordProtocolDefinition.types)
                .some((type) => type.encryptionRequired === true);
        }
        return this._encryptionRequired;
    }
    /**
     * Returns `true` if DWN record-level encryption is active for the given tenant.
     * This is resolved during `initialize()` and cached.
     */
    isEncryptionActive(tenantDid) {
        return this._tenantEncryptionActive.get(tenantDid) === true;
    }
    getAllRecords(_params) {
        return __awaiter(this, void 0, void 0, function* () {
            throw new Error('Not implemented: Classes extending DwnDataStore must implement getAllRecords()');
        });
    }
    getRecord(_a) {
        return __awaiter(this, arguments, void 0, function* ({ recordId, tenantDid, agent, useCache }) {
            var _b;
            // 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. If encryption is active for this tenant,
            // the agent's auto-decryption pipeline handles ECIES key unwrapping and AES decryption.
            //
            // When the protocol definition declares `encryptionRequired: true`, always
            // request decryption — even if `initialize()` has not been called for this
            // tenant yet. This covers delegate sessions where key records arrive via
            // replicated sync rather than `set()`, leaving the per-tenant
            // `_tenantEncryptionActive` cache cold after an agent restart.
            const encryptionActive = this.encryptionRequired || this.isEncryptionActive(tenantDid);
            const { reply: readReply } = yield agent.dwn.processRequest(Object.assign({ author: tenantDid, target: tenantDid, messageType: DwnInterface.RecordsRead, messageParams: { filter: { recordId } } }, (encryptionActive ? { encryption: true } : {})));
            if (!((_b = readReply.entry) === null || _b === void 0 ? void 0 : _b.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 Stream.consumeToJson({ readableStream: 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.
     * When any type in the protocol definition has `encryptionRequired: true`,
     * `$encryption` keys are derived and injected into the protocol definition.
     * If the tenant DID lacks an X25519 keyAgreement key, the error propagates
     * — plaintext fallback is not allowed.
     */
    installProtocol(tenant, agent) {
        return __awaiter(this, void 0, void 0, function* () {
            let definition = this._recordProtocolDefinition;
            let encryptionActive = false;
            if (this.encryptionRequired) {
                // Derive encryption keys — requires the tenant DID to have an X25519
                // keyAgreement key. If it does not, the error propagates to the caller.
                const keyDeriver = yield agent.dwn.getEncryptionKeyDeriver(tenant);
                definition = yield Protocols.deriveAndInjectPublicEncryptionKeys(definition, keyDeriver);
                encryptionActive = true;
            }
            const { reply: { status } } = yield agent.dwn.processRequest({
                author: tenant,
                target: tenant,
                messageType: DwnInterface.ProtocolsConfigure,
                messageParams: { definition },
            });
            if (status.code !== 202) {
                throw new Error(`Failed to install protocol: ${status.code} - ${status.detail}`);
            }
            // When the protocol has encrypted types, install the KeyDeliveryProtocol
            // proactively. Replication admission requires it to be present before
            // encrypted records can be committed (pull) or pushed.
            // Without this, there's a race: postWriteKeyDelivery installs it lazily
            // after the first encrypted write, but the DWN event fires before that
            // completes and the sync engine sees a missing dependency.
            if (encryptionActive) {
                yield agent.dwn.ensureKeyDeliveryProtocol(tenant);
            }
            return { encryptionActive };
        });
    }
    lookupRecordId(_a) {
        return __awaiter(this, arguments, void 0, function* ({ id, tenantDid, agent }) {
            // 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(_a) {
        return __awaiter(this, arguments, void 0, function* ({ id, tenantDid, agent }) {
            // 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(_a) {
        return __awaiter(this, arguments, void 0, function* ({ id, agent, tenant }) {
            // 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(_a) {
        return __awaiter(this, arguments, void 0, function* ({ id, agent, tenant }) {
            // 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(_a) {
        return __awaiter(this, arguments, void 0, function* ({ agent, tenant }) {
            // 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(_a) {
        return __awaiter(this, arguments, void 0, function* ({ id, data, tenant, agent, preventDuplicates, updateExisting }) {
            // 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