"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.commitsAwaitingReplication = void 0;
exports.getHDBNodeTable = getHDBNodeTable;
exports.getReplicationSharedStatus = getReplicationSharedStatus;
exports.subscribeToNodeUpdates = subscribeToNodeUpdates;
exports.shouldReplicateFromNode = shouldReplicateFromNode;
exports.iterateRoutes = iterateRoutes;
exports.getNodeURL = getNodeURL;
/**
 * This module is responsible for managing the list of known nodes in the network. This also tracks replication confirmation
 * when we want to ensure that a transaction has been replicated to multiple nodes before we confirm it.
 */
const databases_ts_1 = require("../core/resources/databases.js");
const replicator_ts_1 = require("./replicator.js");
const nodeName_ts_1 = require("../core/server/nodeName.js");
const DatabaseTransaction_ts_1 = require("../core/resources/DatabaseTransaction.js");
const worker_threads_1 = require("worker_threads");
const hdbError_js_1 = require("../core/utility/errors/hdbError.js");
const environmentManager_js_1 = __importDefault(require("../core/utility/environment/environmentManager.js"));
const hdbTerms_ts_1 = require("../core/utility/hdbTerms.js");
const logger_ts_1 = require("../core/utility/logging/logger.js");
let hdbNodeTable;
server.nodes = [];
function getHDBNodeTable() {
    return (hdbNodeTable ||
        (hdbNodeTable = (0, databases_ts_1.table)({
            table: 'hdb_nodes',
            database: 'system',
            attributes: [
                {
                    name: 'name',
                    isPrimaryKey: true,
                },
                {
                    attribute: 'subscriptions',
                },
                {
                    attribute: 'system_info',
                },
                {
                    attribute: 'url',
                },
                {
                    attribute: 'routes',
                },
                {
                    attribute: 'ca',
                },
                {
                    attribute: 'ca_info',
                },
                {
                    attribute: 'replicates',
                },
                {
                    attribute: 'revoked_certificates',
                },
                {
                    attribute: '__createdtime__',
                },
                {
                    attribute: '__updatedtime__',
                },
            ],
        })));
}
function getReplicationSharedStatus(auditStore, databaseName, node_name, callback) {
    return new Float64Array(auditStore.getUserSharedBuffer(['replicated', databaseName, node_name], new ArrayBuffer(64), callback && { callback }));
}
function subscribeToNodeUpdates(listener) {
    getHDBNodeTable()
        .subscribe({})
        .then(async (events) => {
        for await (const event of events) {
            // remove any nodes that have been updated or deleted
            const node_name = event?.value?.name;
            logger_ts_1.logger.debug?.('adding node', node_name, 'on  node', (0, nodeName_ts_1.getThisNodeName)(), ' on process', process.pid);
            server.nodes = server.nodes.filter((node) => node.name !== node_name);
            if (event.type === 'put' && node_name !== (0, nodeName_ts_1.getThisNodeName)()) {
                // add any new nodes
                if (event.value)
                    server.nodes.push(event.value);
                else {
                    console.error('Invalid node update event', event);
                }
            }
            const shards = new Map();
            for await (const node of getHDBNodeTable().search({})) {
                if (node.shard != undefined) {
                    let nodesForShard = shards.get(node.shard);
                    if (!nodesForShard) {
                        shards.set(node.shard, (nodesForShard = []));
                    }
                    nodesForShard.push(node);
                }
            }
            server.shards = shards;
            if (event.type === 'put' || event.type === 'delete') {
                listener(event.value, event.id);
            }
        }
    });
    server.nodes = [];
    server.shards = new Map();
    for (let entry of getHDBNodeTable().primaryStore.getRange({})) {
        const { value: node, key } = entry;
        server.nodes.push(node);
        if (node.shard != undefined) {
            let nodesForShard = server.shards.get(node.shard);
            if (!nodesForShard) {
                server.shards.set(node.shard, (nodesForShard = []));
            }
            nodesForShard.push(node);
        }
        listener(node, key);
    }
    logger_ts_1.logger.debug?.('Known nodes at startup', server.nodes.map((node) => node.name));
}
function shouldReplicateFromNode(node, databaseName) {
    const databaseReplications = environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_DATABASES);
    return (((typeof node.replicates === 'object'
        ? node.replicates?.sends ||
            node.replicates?.sendsTo?.some?.((sendsTo) => typeof sendsTo === 'object'
                ? (!sendsTo.target || sendsTo.target === (0, nodeName_ts_1.getThisNodeName)()) &&
                    (!sendsTo.database || sendsTo.database === databaseName)
                : sendsTo === (0, nodeName_ts_1.getThisNodeName)())
        : node.replicates) &&
        databases[databaseName] &&
        (!databaseReplications ||
            databaseReplications === '*' ||
            (Array.isArray(databaseReplications) &&
                databaseReplications.find?.((dbReplication) => {
                    return typeof dbReplication === 'string'
                        ? dbReplication === databaseName
                        : dbReplication.name === databaseName &&
                            (!dbReplication.sharded || node.shard === environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_SHARD));
                }))) &&
        getHDBNodeTable().primaryStore.get((0, nodeName_ts_1.getThisNodeName)())?.replicates) ||
        node.subscriptions?.some((sub) => (sub.database || sub.schema) === databaseName && sub.subscribe));
}
const replicationConfirmationFloat64s = new Map();
(0, DatabaseTransaction_ts_1.replicationConfirmation)((databaseName, txnTime, confirmationCount) => {
    if (confirmationCount > server.nodes.length) {
        let nodesInTable = Array.from(databases.system.hdb_nodes.primaryStore.getKeys());
        throw new hdbError_js_1.ClientError(`Cannot confirm replication to more nodes (${confirmationCount}) than are in the network (${server.nodes.length} nodes: ${server.nodes.map((node) => node.name).join(', ')}, all in table ${nodesInTable.join(', ')})`);
    }
    if (!exports.commitsAwaitingReplication) {
        exports.commitsAwaitingReplication = new Map();
        startSubscriptionToReplications();
    }
    let awaiting = exports.commitsAwaitingReplication.get(databaseName);
    if (!awaiting) {
        awaiting = [];
        exports.commitsAwaitingReplication.set(databaseName, awaiting);
    }
    return new Promise((resolve) => {
        let count = 0;
        awaiting.push({
            txnTime,
            onConfirm: () => {
                if (++count === confirmationCount)
                    resolve();
            },
        });
    });
});
// Per-node confirmation watchers. Previously the per-node-update callback below called
// forEachReplicatedDatabase and discarded the returned remove handle, so every hdb_nodes
// update added two listeners (updateTable + dropDatabase) on the global databaseEventsEmitter
// and they accumulated unboundedly across the lifetime of the process — tripping
// MaxListenersExceededWarning once a cluster had ~5+ peers (or after a few node-table churn
// events). We need the future-DB watching aspect of forEachReplicatedDatabase here so that
// new databases added after this node started up still get replicationConfirmation tracking
// wired up; without it, commits on those databases would never receive their onConfirm()
// callback and hang. Keep one watcher per node and replace it on each node update so the
// callback closes over the latest nodeRecord rather than a stale one.
const confirmationWatchersByNode = new Map();
function startSubscriptionToReplications() {
    subscribeToNodeUpdates((nodeRecord, nodeId) => {
        // subscribeToNodeUpdates fires the listener for both 'put' and 'delete' events; on delete
        // the value is undefined but the id (node name) is still passed. Tear down any existing
        // watcher in both cases so we don't accumulate stale watchers when nodes are removed.
        const nodeNameAtUpdate = nodeRecord?.name ?? nodeId;
        if (!nodeNameAtUpdate)
            return;
        confirmationWatchersByNode.get(nodeNameAtUpdate)?.remove();
        confirmationWatchersByNode.delete(nodeNameAtUpdate);
        if (!nodeRecord) {
            // Node was removed — release its shared-buffer entries so the map doesn't accumulate
            // stale node entries across long-running clusters where nodes churn.
            replicationConfirmationFloat64s.delete(nodeNameAtUpdate);
            return;
        }
        const handle = (0, replicator_ts_1.forEachReplicatedDatabase)({}, (database, databaseName) => {
            let confirmationsForNode = replicationConfirmationFloat64s.get(nodeNameAtUpdate);
            if (!confirmationsForNode) {
                replicationConfirmationFloat64s.set(nodeNameAtUpdate, (confirmationsForNode = new Map()));
            }
            if (!database) {
                // dropDatabase notification — clear the entry so a later create-then-recreate of the
                // same databaseName will re-register confirmation tracking instead of being skipped
                // by the .has() guard below.
                confirmationsForNode.delete(databaseName);
                return;
            }
            if (confirmationsForNode.has(databaseName))
                return;
            let auditStore;
            for (const tableName in database) {
                const table = database[tableName];
                auditStore = table.auditStore;
                if (auditStore)
                    break;
            }
            if (auditStore) {
                const replicatedTime = getReplicationSharedStatus(auditStore, databaseName, nodeNameAtUpdate, () => {
                    const updatedTime = replicatedTime[0];
                    const lastTime = replicatedTime.lastTime;
                    for (const { txnTime, onConfirm } of exports.commitsAwaitingReplication.get(databaseName) || []) {
                        if (txnTime > lastTime && txnTime <= updatedTime) {
                            onConfirm();
                        }
                    }
                    replicatedTime.lastTime = updatedTime;
                });
                replicatedTime.lastTime = 0;
                confirmationsForNode.set(databaseName, replicatedTime);
            }
        });
        confirmationWatchersByNode.set(nodeNameAtUpdate, handle);
    });
}
function* iterateRoutes(options) {
    for (const route of options.routes || []) {
        let url = route.url;
        let host;
        if (typeof route === 'string') {
            // a plain route string can be a url or hostname (or host)
            if (route.includes('://'))
                url = route;
            else
                host = route;
        }
        else
            host = route.hostname ?? route.host;
        if (!host && url) {
            host = new URL(url).hostname;
        }
        else if (!url && !host) {
            if (worker_threads_1.isMainThread)
                console.error('Invalid route, must specify a url or host (with port)');
            continue;
        }
        yield {
            replicates: route.replicates ?? !route.subscriptions, // if there is not a list of subscriptions, then this node is authorized to fully replicate
            name: host,
            url,
            port: route.port,
            subscription: route.subscriptions,
            routes: route.routes,
            startTime: route.startTime,
            revoked_certificates: route.revokedCertificates,
        };
    }
}
function getNodeURL(node) {
    if (node.url)
        return node.url;
    const host = node.name;
    const securePort = environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_SECUREPORT);
    let port;
    // if the host includes a port, use that port
    if ((port = host.match(/:(\d+)$/)?.[1]))
        host = host.slice(0, -port[0].length - 1);
    else if (node.port)
        port = node.port; // could be in the routes config
    // otherwise use the default port for the service
    else
        port = securePort || environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_PORT) || 9933;
    const lastColon = port?.lastIndexOf?.(':');
    if (lastColon > 0)
        port = +port.slice(lastColon + 1).replace(/[[\]]/g, '');
    return (securePort ? 'wss://' : 'ws://') + host + ':' + port; // now construct the full url
}
//# sourceMappingURL=knownNodes.js.map