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 * as block from 'multiformats/block';
import * as cbor from '@ipld/dag-cbor';
import { ArrayUtility } from '../utils/array.js';
import { BlockstoreLevel } from './blockstore-level.js';
import { CID } from 'multiformats/cid';
import { createLevelDatabase } from './level-wrapper.js';
import { executeUnlessAborted } from '../utils/abort.js';
import { IndexLevel } from './index-level.js';
import { sha256 } from 'multiformats/hashes/sha2';
import { SortOrder } from '../types/message-types.js';
import { Cid, Message } from '../index.js';
/**
 * A simple implementation of {@link MessageStore} that works in both the browser and server-side.
 * Leverages LevelDB under the hood.
 */
export class MessageStoreLevel {
    /**
     * @param {MessageStoreLevelConfig} config
     * @param {string} config.blockstoreLocation - must be a directory path (relative or absolute) where
     *  LevelDB will store its files, or in browsers, the name of the
     * {@link https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase IDBDatabase} to be opened.
     * @param {string} config.indexLocation - same as config.blockstoreLocation
     */
    constructor(config = {}) {
        this.config = Object.assign({ blockstoreLocation: 'data/MESSAGESTORE', indexLocation: 'data/INDEX', createLevelDatabase }, config);
        this.blockstore = new BlockstoreLevel({
            location: this.config.blockstoreLocation,
            createLevelDatabase: this.config.createLevelDatabase,
        });
        this.index = new IndexLevel({
            location: this.config.indexLocation,
            createLevelDatabase: this.config.createLevelDatabase,
        });
    }
    open() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.blockstore.open();
            yield this.index.open();
        });
    }
    close() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.blockstore.close();
            yield this.index.close();
        });
    }
    get(tenant, cidString, options) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            (_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.throwIfAborted();
            const partition = yield executeUnlessAborted(this.blockstore.partition(tenant), options === null || options === void 0 ? void 0 : options.signal);
            const cid = CID.parse(cidString);
            const bytes = yield partition.get(cid, options);
            if (!bytes) {
                return undefined;
            }
            const decodedBlock = yield executeUnlessAborted(block.decode({ bytes, codec: cbor, hasher: sha256 }), options === null || options === void 0 ? void 0 : options.signal);
            const message = decodedBlock.value;
            return message;
        });
    }
    query(tenant, filters, messageSort, pagination, options) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            (_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.throwIfAborted();
            const messages = [];
            // note: injecting tenant into filters to allow querying with an "empty" filter.
            // if there are no other filters present it will return all the messages the tenant.
            const resultIds = yield this.index.query(tenant, filters.map(f => (Object.assign(Object.assign({}, f), { tenant }))), options);
            // as an optimization for large data sets, we are finding the message object which matches the paginationMessageCid here.
            // we can use this within the pagination function after sorting to determine the starting point of the array in a more efficient way.
            let paginationMessage;
            for (const id of resultIds) {
                const message = yield this.get(tenant, id, options);
                if (message) {
                    messages.push(message);
                }
                if ((pagination === null || pagination === void 0 ? void 0 : pagination.messageCid) && pagination.messageCid === id) {
                    paginationMessage = message;
                }
            }
            if ((pagination === null || pagination === void 0 ? void 0 : pagination.messageCid) !== undefined && paginationMessage === undefined) {
                return { messages: [] }; //if paginationMessage is not found, do not return any results
            }
            const sortedRecords = yield MessageStoreLevel.sortMessages(messages, messageSort);
            return this.paginateMessages(sortedRecords, paginationMessage, pagination);
        });
    }
    paginateMessages(messages, paginationMessage, pagination = {}) {
        return __awaiter(this, void 0, void 0, function* () {
            const { limit } = pagination;
            if (paginationMessage === undefined && limit === undefined) {
                return { messages }; // return all without pagination pointer.
            }
            // we are passing the pagination message object for an easier lookup
            // since we know this object exists within the array if passed, we can assume that it will always have a value greater than -1
            const cursorIndex = paginationMessage ? messages.indexOf(paginationMessage) : undefined;
            // the first element of the returned results is always the message immediately following the cursor.
            const start = cursorIndex === undefined ? 0 : cursorIndex + 1;
            const end = limit === undefined ? undefined : start + limit;
            const results = messages.slice(start, end);
            // we only return a paginationMessageCid cursor if there are more results
            const hasMoreResults = end !== undefined && end < messages.length;
            let paginationMessageCid;
            if (hasMoreResults) {
                // we extract the cid of the last message in the result set.
                const lastMessage = results.at(-1);
                paginationMessageCid = yield Message.getCid(lastMessage);
            }
            return { messages: results, paginationMessageCid };
        });
    }
    /**
     * Compares the chosen property of two messages in lexicographical order.
     * When the value is the same between the two objects, `messageCid` comparison is used to tiebreak.
     * tiebreaker always compares messageA to messageB
     *
     * @returns if SortOrder is Ascending:
     *            1 if the chosen property of `messageA` is larger than of `messageB`;
     *           -1 if the chosen property `messageA` is smaller/older than of `messageB`;
     *            0 otherwise
     *          if SortOrder is Descending:
     *            1 if the chosen property of `messageB` is larger than of `messageA`;
     *           -1 if the chosen property `messageB` is smaller/older than of `messageA`;
     *            0 otherwise
     */
    static lexicographicalCompare(messageA, messageB, comparedPropertyName, sortOrder) {
        return __awaiter(this, void 0, void 0, function* () {
            const a = messageA.descriptor[comparedPropertyName];
            const b = messageB.descriptor[comparedPropertyName];
            if (sortOrder === SortOrder.Ascending) {
                if (a > b) {
                    return 1;
                }
                else if (a < b) {
                    return -1;
                }
            }
            else {
                // descending order
                if (b > a) {
                    return 1;
                }
                else if (b < a) {
                    return -1;
                }
            }
            // if we reach here it means the compared properties have the same values, we need to fall back to compare the `messageCid` instead
            return yield Message.compareCid(messageA, messageB);
        });
    }
    /**
     * This is a temporary naive sort, it will eventually be done within the underlying data store.
     *
     * If sorting is based on date published, records that are not published are filtered out.
     * @param messages - Messages to be sorted if dateSort is present
     * @param sort - Sorting scheme
     * @returns Sorted Messages
     */
    static sortMessages(messages, messageSort = {}) {
        return __awaiter(this, void 0, void 0, function* () {
            const { dateCreated, datePublished, messageTimestamp } = messageSort;
            let sortOrder = SortOrder.Ascending; // default
            let messagesToSort = messages; // default
            let propertyToCompare; // `keyof MessageSort` = name of all properties of `MessageSort`
            if (dateCreated !== undefined) {
                propertyToCompare = 'dateCreated';
            }
            else if (datePublished !== undefined) {
                propertyToCompare = 'datePublished';
                messagesToSort = messages.filter(message => message.descriptor.published);
            }
            else if (messageTimestamp !== undefined) {
                propertyToCompare = 'messageTimestamp';
            }
            if (propertyToCompare !== undefined) {
                sortOrder = messageSort[propertyToCompare];
            }
            else {
                propertyToCompare = 'messageTimestamp';
            }
            const asyncComparer = (a, b) => {
                return MessageStoreLevel.lexicographicalCompare(a, b, propertyToCompare, sortOrder);
            };
            // NOTE: we needed to implement our own asynchronous sort method because Array.sort() does not take an async comparer
            return yield ArrayUtility.asyncSort(messagesToSort, asyncComparer);
        });
    }
    delete(tenant, cidString, options) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            (_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.throwIfAborted();
            const partition = yield executeUnlessAborted(this.blockstore.partition(tenant), options === null || options === void 0 ? void 0 : options.signal);
            const cid = CID.parse(cidString);
            yield partition.delete(cid, options);
            yield this.index.delete(tenant, cidString, options);
        });
    }
    put(tenant, message, indexes, options) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            (_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.throwIfAborted();
            const partition = yield executeUnlessAborted(this.blockstore.partition(tenant), options === null || options === void 0 ? void 0 : options.signal);
            const encodedMessageBlock = yield executeUnlessAborted(block.encode({ value: message, codec: cbor, hasher: sha256 }), options === null || options === void 0 ? void 0 : options.signal);
            // MessageStore data may contain `encodedData` which is not taken into account when calculating the blockCID as it is optional data.
            const messageCid = Cid.parseCid(yield Message.getCid(message));
            yield partition.put(messageCid, encodedMessageBlock.bytes, options);
            const messageCidString = messageCid.toString();
            // note: leaving the additional tenant indexing to allow for querying with an "empty" filter.
            // when querying, we also inject a filter for the specific tenant.
            // if there are no other filters present it will return all the messages for that tenant.
            const indexDocument = Object.assign(Object.assign({}, indexes), { tenant });
            yield this.index.put(tenant, messageCidString, indexDocument, options);
        });
    }
    /**
     * deletes everything in the underlying blockstore and indices.
     */
    clear() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.blockstore.clear();
            yield this.index.clear();
        });
    }
    dump() {
        var _a, _b, _c, _d;
        return __awaiter(this, void 0, void 0, function* () {
            console.group('blockstore');
            yield ((_b = (_a = this.blockstore)['dump']) === null || _b === void 0 ? void 0 : _b.call(_a));
            console.groupEnd();
            console.group('index');
            yield ((_d = (_c = this.index)['dump']) === null || _d === void 0 ? void 0 : _d.call(_c));
            console.groupEnd();
        });
    }
}
//# sourceMappingURL=message-store-level.js.map