"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.addSubscription = addSubscription;
exports.listenToCommits = listenToCommits;
exports.whenNextTransaction = whenNextTransaction;
const harper_logger_js_1 = require("../utility/logging/harper_logger.js");
const IterableEventQueue_ts_1 = require("./IterableEventQueue.js");
const Resources_ts_1 = require("./Resources.js");
const allSubscriptions = Object.create(null); // using it as a map that doesn't change much
const allSameThreadSubscriptions = Object.create(null); // using it as a map that doesn't change much
/**
 * This module/function is responsible for the main work of tracking subscriptions and listening for new transactions
 * that have occurred on any thread, and then reading through the transaction log to notify listeners. This is
 * responsible for cleanup of subscriptions as well.
 * @param table
 * @param key
 * @param listener
 * @param startTime
 * @param options
 */
function addSubscription(table, key, listener, startTime, options) {
    const path = table.primaryStore.path;
    const tableId = table.primaryStore.tableId;
    // set up the subscriptions map. We want to just use a single map (per table) for efficient delegation
    // (rather than having every subscriber filter every transaction)
    let databaseSubscriptions;
    if (!path) {
        throw new Error('No path for table primary store');
    }
    if (options?.crossThreads === false) {
        // we are only listening for commits on our own thread, so we use a separate subscriber and sequencer tracker
        databaseSubscriptions = allSameThreadSubscriptions[path] || (allSameThreadSubscriptions[path] = []);
        listenToCommits(table.primaryStore, table.auditStore);
    }
    else {
        databaseSubscriptions = allSubscriptions[path] || (allSubscriptions[path] = []);
        const auditStore = table.auditStore;
        if (!auditStore.hasSubscriptionCommitListener) {
            let auditLogIterator;
            if (auditStore.reusableIterable) {
                // with rocksdb-js iterator we can and should not specify a start time so we just start at the end of the txn log
                // and still match older version numbers that may commit in the future. But we have to start
                // immediately so we are at the right position
                auditLogIterator = auditStore.getRange({});
            }
            auditStore.hasSubscriptionCommitListener = true;
            auditStore.on('committed', () => {
                notifyFromTransactionData(databaseSubscriptions, auditLogIterator);
            });
        }
    }
    databaseSubscriptions.auditStore = table.auditStore;
    if (databaseSubscriptions.lastTxnTime == null) {
        databaseSubscriptions.lastTxnTime = Date.now();
    }
    if (options?.scope === 'full-database') {
        return;
    }
    let tableSubscriptions = databaseSubscriptions[tableId];
    if (!tableSubscriptions) {
        tableSubscriptions = databaseSubscriptions[tableId] = new Map();
        tableSubscriptions.envs = databaseSubscriptions;
        tableSubscriptions.tableId = tableId;
        tableSubscriptions.store = table.primaryStore;
    }
    key = (0, Resources_ts_1.keyArrayToString)(key);
    const subscription = new Subscription(listener);
    subscription.startTime = startTime;
    let subscriptions = tableSubscriptions.get(key);
    if (subscriptions)
        subscriptions.push(subscription);
    else {
        tableSubscriptions.set(key, (subscriptions = [subscription]));
        subscriptions.tables = tableSubscriptions;
        subscriptions.key = key;
    }
    subscription.subscriptions = subscriptions;
    return subscription;
}
/**
 * This is the class that is returned from subscribe calls and provide the interface to set a callback, end the
 * subscription and get the initial state.
 */
class Subscription extends IterableEventQueue_ts_1.IterableEventQueue {
    listener;
    subscriptions;
    startTime;
    includeDescendants;
    supportsTransactions;
    onlyChildren;
    constructor(listener) {
        super();
        this.listener = listener;
        this.on('close', () => this.end());
    }
    end() {
        // cleanup
        if (!this.subscriptions)
            return;
        this.subscriptions.splice(this.subscriptions.indexOf(this), 1);
        if (this.subscriptions.length === 0) {
            const tableSubscriptions = this.subscriptions.tables;
            if (tableSubscriptions) {
                // TODO: Handle cleanup of wildcard
                const key = this.subscriptions.key;
                tableSubscriptions.delete(key);
                if (tableSubscriptions.size === 0) {
                    const envSubscriptions = tableSubscriptions.envs;
                    const dbi = tableSubscriptions.dbi;
                    delete envSubscriptions[dbi];
                }
            }
        }
        this.subscriptions = null;
    }
    toJSON() {
        return { name: 'subscription' };
    }
}
const ACTIONS_OF_INTEREST = ['put', 'patch', 'delete', 'message', 'invalidate'];
function notifyFromTransactionData(subscriptions, auditLogIterable) {
    if (!subscriptions)
        return; // if no subscriptions to this env path, don't need to read anything
    const auditStore = subscriptions.auditStore;
    auditStore.resetReadTxn?.();
    nextTransaction(subscriptions.auditStore);
    let subscribersWithTxns;
    if (!auditLogIterable) {
        // rocksdb will pass this in, but with lmdb, we have to re-create the iterable
        auditLogIterable = auditStore.getRange({
            start: subscriptions.lastTxnTime,
            exclusiveStart: true,
        });
    }
    for (const auditRecord of auditLogIterable) {
        const timestamp = auditRecord.localTime ?? auditRecord.version;
        subscriptions.lastTxnTime = timestamp;
        if (!ACTIONS_OF_INTEREST.includes(auditRecord.type))
            continue;
        const tableSubscriptions = subscriptions[auditRecord.tableId];
        if (!tableSubscriptions)
            continue;
        const recordId = auditRecord.recordId;
        // TODO: How to handle invalidation
        let matchingKey = (0, Resources_ts_1.keyArrayToString)(recordId);
        let ancestorLevel = 0;
        do {
            // we iterate through the key hierarchy, notifying all subscribers for each key,
            // so for an id like resource/foo/bar, we notify subscribers for resource/foo/bar, resource/foo/, resource/foo, resource/, and resource
            // this allows for efficient subscriptions to children ids/topics
            const keySubscriptions = tableSubscriptions.get(matchingKey);
            if (keySubscriptions) {
                for (const subscription of keySubscriptions) {
                    if (ancestorLevel > 0 && // only ancestors if the subscription is for ancestors (and apply onlyChildren filtering as necessary)
                        !(subscription.includeDescendants && !(subscription.onlyChildren && ancestorLevel > 1)))
                        continue;
                    if (subscription.startTime >= timestamp) {
                        continue;
                    }
                    try {
                        let beginTxn;
                        if (subscription.supportsTransactions && subscription.txnInProgress !== auditRecord.version) {
                            // if the subscriber supports transactions, we mark this as the beginning of a new transaction
                            // tracking the subscription so that we can delimit the transaction on next transaction
                            // (with a beginTxn flag, which may be on an endTxn event)
                            beginTxn = true;
                            if (!subscription.txnInProgress) {
                                // if first txn for subscriber of this cycle, add to the transactional subscribers that we are tracking
                                if (!subscribersWithTxns)
                                    subscribersWithTxns = [subscription];
                                else
                                    subscribersWithTxns.push(subscription);
                            }
                            // the version defines the extent of a transaction, all audit records with the same version
                            // are part of the same transaction, and when the version changes, we know it is a new
                            // transaction
                            subscription.txnInProgress = auditRecord.version;
                        }
                        subscription.listener(recordId, auditRecord, timestamp, beginTxn);
                    }
                    catch (error) {
                        (0, harper_logger_js_1.warn)(error);
                    }
                }
            }
            if (matchingKey == null)
                break;
            const lastSlash = matchingKey.lastIndexOf?.('/', matchingKey.length - 2);
            if (lastSlash !== matchingKey.length - 1) {
                ancestorLevel++; // don't increase the ancestor level for this going from resource/ to resource
            }
            if (lastSlash > -1) {
                matchingKey = matchingKey.slice(0, lastSlash + 1);
            }
            else
                matchingKey = null;
        } while (true);
    }
    if (subscribersWithTxns) {
        // any subscribers with open transactions need to have an event to indicate that their transaction has been ended
        for (const subscription of subscribersWithTxns) {
            subscription.txnInProgress = null; // clean up
            subscription.listener(null, { type: 'end_txn' }, subscriptions.lastTxnTime, true);
        }
    }
}
/**
 * Interface with database to listen for commits and traverse the audit log only on the same thread.
 * @param primaryStore
 * @param auditStore
 */
function listenToCommits(primaryStore, auditStore) {
    const store = auditStore || primaryStore;
    const path = primaryStore.path;
    const lmdbEnv = store.env;
    if (!lmdbEnv.hasAfterCommitListener) {
        lmdbEnv.hasAfterCommitListener = true;
        store.on('aftercommit', (logEntries) => {
            const subscriptions = allSameThreadSubscriptions[path]; // there is a different set of subscribers for same-thread subscriptions
            if (!subscriptions)
                return;
            // With RocksTransactionLog, we actually have direct access to the list of log entries:
            if (Array.isArray(logEntries)) {
                return notifyFromTransactionData(subscriptions, logEntries);
            }
            // we want each thread to do this mutually exclusively so that we don't have multiple threads trying to process the same data (the intended purpose of crossThreads=false)
            const acquiredLock = () => {
                // we have the lock, so we can now read the last sequence/local write time and continue to read the audit log from there
                if (!store.threadLocalWrites)
                    // initiate the shared buffer if needed
                    store.threadLocalWrites = new Float64Array(store.getUserSharedBuffer('last-thread-local-write', new ArrayBuffer(8)));
                subscriptions.txnTime = store.threadLocalWrites[0] || Date.now(); // start from last one
                try {
                    notifyFromTransactionData(subscriptions);
                }
                finally {
                    store.threadLocalWrites[0] = subscriptions.lastTxnTime; // update shared buffer
                    store.unlock('thread-local-writes'); // and release the lock
                }
            };
            // try to get lock or wait for it
            if (!store.tryLock('thread-local-writes', acquiredLock))
                return;
            acquiredLock();
        });
    }
}
function nextTransaction(auditStore) {
    auditStore.nextTransaction?.resolve();
    let nextResolve;
    auditStore.nextTransaction = new Promise((resolve) => {
        nextResolve = resolve;
    });
    auditStore.nextTransaction.resolve = nextResolve;
}
function whenNextTransaction(auditStore) {
    if (!auditStore.nextTransaction) {
        addSubscription({
            primaryStore: auditStore,
            auditStore,
        }, null, null, 0, { scope: 'full-database' });
        nextTransaction(auditStore);
    }
    return auditStore.nextTransaction;
}
//# sourceMappingURL=transactionBroadcast.js.map