"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RocksTransactionLogStore = void 0;
const rocksdb_js_1 = require("@harperfast/rocksdb-js");
const extended_iterable_1 = require("@harperfast/extended-iterable");
const nodeIdMapping_ts_1 = require("./nodeIdMapping.js");
const auditStore_ts_1 = require("./auditStore.js");
const node_worker_threads_1 = require("node:worker_threads");
const node_events_1 = require("node:events");
const lmdb_1 = require("lmdb");
if (!process.env.HARPER_NO_FLUSH_ON_EXIT && node_worker_threads_1.isMainThread) {
    // we want to be able to test log replay
    process.on('exit', () => (0, rocksdb_js_1.shutdown)());
}
// reserving 0x80000000 for future use if we need a flag to indicate 64-bits of flag bits for more flags
const HAS_PREVIOUS_RESIDENCY_ID = 0x40000000;
const HAS_PREVIOUS_VERSION = 0x20000000;
/**
 * Represents a transaction log store backed by RocksDB.
 * This class provides methods that conform to a standard store interface
 * to manage and interact with transaction logs, including querying logs,
 * adding entries, and loading logs for multiple nodes or purposes.
 */
class RocksTransactionLogStore extends node_events_1.EventEmitter {
    log;
    nodeLogs; // whatever the type of the read logger
    logByName = new Map();
    updates = 0; // the number of updates to the list of logs that have occurred
    rootStore;
    reusableIterable = true; // flag indicating that iterable can be reused to resume iterating through audit log
    constructor(rootDatabase) {
        super();
        this.log = rootDatabase.useLog('local');
        this.rootStore = rootDatabase;
    }
    /**
     * Translate a put to an addEntry
     * @param suggestedKey - ignored, only used by LMDB
     * @param auditRecord - Audit record to save
     * @param options - Options for save
     */
    put(suggestedKey, auditRecord, options) {
        if (options.transaction.isRetry) {
            // do not record transaction entries on retry
            return;
        }
        const log = this.logById(options.nodeId) ?? this.logById(options.viaNodeId) ?? this.log;
        let entryBinary;
        if (auditRecord instanceof Uint8Array)
            entryBinary = auditRecord;
        else {
            const flagAndStructureVersion = (auditRecord.previousVersion ? HAS_PREVIOUS_VERSION : 0) |
                (auditRecord.previousResidencyId ? HAS_PREVIOUS_RESIDENCY_ID : 0) |
                auditRecord.structureVersion;
            auditStore_ts_1.ENTRY_DATAVIEW.setUint32(0, flagAndStructureVersion);
            let position = 4;
            if (auditRecord.previousResidencyId) {
                auditStore_ts_1.ENTRY_DATAVIEW.setUint32(4, auditRecord.previousResidencyId);
                position = 8;
            }
            if (auditRecord.previousNodeId) {
                auditStore_ts_1.ENTRY_DATAVIEW.setUint32(position, auditRecord.previousNodeId);
                position += 4;
            }
            entryBinary = (0, auditStore_ts_1.createAuditEntry)(auditRecord, position);
        }
        if (this.listenerCount('aftercommit')) {
            if (!options.transaction.logEntries) {
                options.transaction.logEntries = [];
                options.transaction.onCommit = () => {
                    this.emit('aftercommit', options.transaction.logEntries);
                };
            }
            options.transaction.logEntries.push(auditRecord);
        }
        log.addEntry(entryBinary, options.transaction.id);
    }
    logById(nodeId) {
        return nodeId > -1 ? (this.nodeLogs?.[nodeId] ?? this.loadLogs()[nodeId]) : undefined;
    }
    putSync(suggestedKey, value, options) {
        if (typeof suggestedKey === 'symbol') {
            this.rootStore.putSync(suggestedKey, (0, lmdb_1.asBinary)(value), options);
        }
        else {
            this.put(suggestedKey, value, options);
        }
    }
    get(key, tableId, recordId, nodeId) {
        return this.getSync(key, tableId, recordId, nodeId);
    }
    getSync(key, tableId, recordId, nodeId) {
        if (typeof key === 'number') {
            if (typeof tableId !== 'number')
                throw new Error('tableId must be a number');
            if (recordId === undefined) {
                throw new Error('recordId must be provided');
            }
            // this a request for a transaction log entry by a timestamp
            for (const entry of this.getRange({ start: key, exactStart: true, log: nodeId })) {
                if (entry.recordId === recordId && entry.tableId === tableId) {
                    return entry;
                }
                if (entry.version !== key)
                    return; // no longer in this transaction
            }
        }
        else {
            // Harper puts some metadata in the database, we will just put this in the root store instead
            return this.rootStore.getSync(key);
        }
    }
    getBinary(key) {
        if (typeof key === 'number') {
            throw new Error('Unsupported binary access by number');
        }
        return this.rootStore.getBinarySync(key);
    }
    getEntry() {
        throw new Error('Not implemented');
    }
    addLogToMaps(logName, log) {
        const nodeId = ((0, nodeIdMapping_ts_1.getIdOfRemoteNode)(logName, this) ?? 0);
        if (this.nodeLogs) {
            this.nodeLogs[nodeId] ??= log;
        }
        this.updates++;
        this.logByName.set(logName, log);
        return nodeId;
    }
    loadLogs() {
        if (this.nodeLogs) {
            // listLogs should only be called one time, and then listen for changes to update
            return this.nodeLogs;
        }
        this.nodeLogs = [];
        for (const logName of this.rootStore.listLogs()) {
            const log = this.rootStore.useLog(logName);
            this.addLogToMaps(logName, log);
        }
        this.rootStore.on('new-transaction-log', (logName) => {
            if (this.logByName.has(logName))
                return; // already added
            // Add this to our logs
            const log = this.rootStore.useLog(logName);
            this.addLogToMaps(logName, log);
        });
        return this.nodeLogs;
    }
    ensureLogExists(logName) {
        if (this.logByName.has(logName))
            return;
        const log = this.rootStore.useLog(logName);
        return this.addLogToMaps(logName, log);
    }
    /**
     * Get all entries matching the range, from all the transaction logs, sorted by timestamp
     * @param options
     */
    getRange(options) {
        let iterable = new extended_iterable_1.ExtendedIterable();
        let aggregateIterator;
        if (options.log !== undefined) {
            let log = typeof options.log === 'number' ? this.nodeLogs?.[options.log] : this.logByName.get(options.log);
            if (!log) {
                this.loadLogs();
                if (typeof options.log === 'number') {
                    log = this.nodeLogs?.[options.log];
                }
                else {
                    log = this.logByName.get(options.log);
                }
                if (!log) {
                    log = this.rootStore.useLog(options.log);
                }
            }
            const queryIterator = log.query(options);
            iterable.iterate = () => queryIterator;
        }
        else {
            const onlyKeys = options.onlyKeys;
            let logs = [];
            // holds the queue of next entries from each iterator
            let nextEntries;
            let latestUpdates;
            const iterators = [];
            const updateIterators = () => {
                if (latestUpdates !== this.updates) {
                    const latestLogs = (this.nodeLogs || this.loadLogs()).filter((log) => !options.excludeLogs?.includes(log.name));
                    for (let log of latestLogs) {
                        if (!logs.includes(log)) {
                            logs.push(log);
                            let queryOptions = options;
                            if (options.startByLog) {
                                // if the startByLog is provided, we use that
                                queryOptions = { ...options, start: options.startByLog.get(log.name) ?? 0 };
                            }
                            else if (latestUpdates >= 0) {
                                // if this is not the first update, that means that this is a brand new log and if start wasn't specified
                                // that means we are taking all future requests, so we need to start at zero so we don't introduce a race
                                // condition of potentially missing an initial update
                                queryOptions = { ...options, start: options.start ?? 0 };
                            }
                            iterators.push(log.query(queryOptions));
                        }
                    }
                    latestUpdates = this.updates;
                    if (logs.length > latestLogs.length) {
                        for (let i = 0; i < logs.length; i++) {
                            let log = logs[i];
                            if (!latestLogs.includes(log)) {
                                logs.splice(i, 1);
                                iterators.splice(i--, 1);
                            }
                        }
                    }
                }
                nextEntries = iterators.map((iterator) => iterator.next());
            };
            updateIterators();
            aggregateIterator = {
                next() {
                    if (nextEntries.length === 0) {
                        // on the first iteration and any time we finished all the iterators, we re-retrieve all
                        // the next entries (in case we are resuming after being done)
                        updateIterators();
                    }
                    let earliest;
                    let earliestIndex = -1;
                    for (let i = 0; i < nextEntries.length; i++) {
                        const result = nextEntries[i];
                        // skip any that are done
                        if (result.done) {
                            continue;
                        }
                        // find the earliest one that is not done
                        const next = result.value;
                        if (!earliest || earliest.timestamp > next.timestamp) {
                            earliest = next;
                            earliestIndex = i;
                        }
                    }
                    if (earliestIndex >= 0) {
                        // replace the entry with the next one from the iterator we pulled from
                        nextEntries[earliestIndex] = iterators[earliestIndex].next();
                        return {
                            value: onlyKeys ? earliest.timestamp : earliest,
                            done: false,
                        };
                    } // else we are done
                    nextEntries.length = 0; // reset so if this iterator is restarted, we can re-query
                    return { value: undefined, done: true };
                },
                addLog(logName) {
                    let index = options.excludeLogs?.indexOf(logName);
                    if (index >= 0) {
                        options.excludeLogs.splice(index, 1);
                    }
                },
                removeLog: (logName) => {
                    const log = this.logByName.get(logName);
                    if (!log)
                        return; // not found
                    const index = logs.findIndex((l) => l === log);
                    if (index >= 0) {
                        logs.splice(index, 1);
                        iterators.splice(index, 1);
                        nextEntries.splice(index, 1);
                        options.excludeLogs.push(logName);
                    }
                },
            };
            iterable.iterate = () => aggregateIterator;
        }
        const mappedAggregateIterable = iterable.map(({ timestamp, data, endTxn }) => {
            const decoder = new auditStore_ts_1.Decoder(data.buffer, data.byteOffset, data.byteLength);
            data.dataView = decoder;
            // This represents the data that shouldn't be transferred for replication
            let structureVersion = decoder.getUint32(0);
            let position = 4;
            let previousResidencyId;
            let previousVersion;
            if (structureVersion & HAS_PREVIOUS_RESIDENCY_ID) {
                previousResidencyId = decoder.getUint32(position);
                position += 4;
            }
            if (structureVersion & HAS_PREVIOUS_VERSION) {
                // does previous residency id and version actually require separate flags?
                previousVersion = decoder.getFloat64(position);
                position += 8;
            }
            const auditRecord = (0, auditStore_ts_1.readAuditEntry)(data, position, undefined, true);
            auditRecord.version = timestamp;
            auditRecord.endTxn = endTxn;
            auditRecord.previousResidencyId = previousResidencyId;
            auditRecord.previousVersion = previousVersion;
            auditRecord.structureVersion = structureVersion & 0x00ffffff;
            return auditRecord;
        });
        // Add methods to the mapped iterable if we have an aggregate iterator
        if (aggregateIterator?.addLog) {
            mappedAggregateIterable.addLog = aggregateIterator.addLog;
            mappedAggregateIterable.removeLog = aggregateIterator.removeLog;
        }
        return mappedAggregateIterable;
    }
    getKeys(_options) {
        return []; // TODO: implement this
        // options.onlyKeys = true;
        // return this.getRange(options);
    }
    getStats() {
        let totalSize = 0;
        const logs = [];
        for (const log of this.loadLogs()) {
            if (!log)
                continue;
            const size = log.getLogFileSize();
            totalSize += size;
            logs.push({ name: log.name, size });
        }
        return {
            logs,
            totalSize,
        };
    }
    getUserSharedBuffer(key, defaultBuffer, options) {
        return this.rootStore.getUserSharedBuffer(key, defaultBuffer, options);
    }
    on(eventName, listener) {
        if (eventName === 'aftercommit') {
            return super.on('aftercommit', listener);
        }
        else {
            return this.rootStore.on(eventName, listener);
        }
    }
    tryLock(key, onUnlocked) {
        return this.rootStore.tryLock(key, onUnlocked);
    }
    unlock(key) {
        this.rootStore.unlock(key);
    }
    get path() {
        return this.rootStore.path;
    }
    async remove() {
        // TODO: this function can likely be removed once the call to purgeLogs()
        // is added in `resources/Table.ts`
    }
}
exports.RocksTransactionLogStore = RocksTransactionLogStore;
//# sourceMappingURL=RocksTransactionLogStore.js.map