"use strict";
/**
 * This is the entry module is responsible for replicating data between nodes. It is a source for tables that are replicated
 * A typical exchange should look like:
 * 1. Node A connects to node B, and sends its node name and the database name (and the mapping of its node id to short ids?)
 * 2. Node B sends back its node name and the mapping of its node id to short ids
 * 3. Node A sends a subscription request to node B
 * 3a. Node B may also send a subscription request to node A
 * 4. Node B sends back the table names and structures
 * 5. Node B sends back the audit records
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.enabledDatabases = exports.replicationCertificateAuthorities = exports.servers = exports.startOnMainThread = void 0;
exports.buildReplicationMtlsConfig = buildReplicationMtlsConfig;
exports.start = start;
exports.monitorNodeCAs = monitorNodeCAs;
exports.disableReplication = disableReplication;
exports.setReplicator = setReplicator;
exports.sendOperationToNode = sendOperationToNode;
exports.subscribeToNode = subscribeToNode;
exports.unsubscribeFromNode = unsubscribeFromNode;
exports.urlToNodeName = urlToNodeName;
exports.iterateReplicatedDatabases = iterateReplicatedDatabases;
exports.forEachReplicatedDatabase = forEachReplicatedDatabase;
exports.replicateOperation = replicateOperation;
const databases_ts_1 = require("../core/resources/databases.js");
const Resource_ts_1 = require("../core/resources/Resource.js");
const IterableEventQueue_ts_1 = require("../core/resources/IterableEventQueue.js");
const replicationConnection_ts_1 = require("./replicationConnection.js");
const Server_ts_1 = require("../core/server/Server.js");
const environmentManager_js_1 = __importDefault(require("../core/utility/environment/environmentManager.js"));
const logger = __importStar(require("../core/utility/logging/harper_logger.js"));
const index_ts_1 = require("../core/security/certificateVerification/index.js");
var subscriptionManager_ts_1 = require("./subscriptionManager.js");
Object.defineProperty(exports, "startOnMainThread", { enumerable: true, get: function () { return subscriptionManager_ts_1.startOnMainThread; } });
const knownNodes_ts_1 = require("./knownNodes.js");
const hdbTerms_ts_1 = require("../core/utility/hdbTerms.js");
const nodeIdMapping_ts_1 = require("../core/resources/nodeIdMapping.js");
const tls = __importStar(require("node:tls"));
const hdbError_js_1 = require("../core/utility/errors/hdbError.js");
const worker_threads_1 = require("worker_threads");
const keys_js_1 = require("../core/security/keys.js");
const nodeName_1 = require("../core/server/nodeName");
// allow this to register operations
require("./setNode.js");
require("./clusterStatus.js");
require("../security/keyService.js");
require("../security/sshKeyOperations.js");
// Each active replication connection legitimately registers a small handful of updateTable / dropDatabase
// listeners on the global databaseEventsEmitter (one per subscriptionManager iterator and per per-DB WS), so a
// modest cluster blows past Node's default 10-listener cap even with every listener correctly torn down on
// disconnect. Raise the limit so we don't spam MaxListenersExceededWarning under normal multi-peer load while
// still leaving headroom small enough that a real listener leak would eventually trip the warning.
databases_ts_1.databaseEventsEmitter.setMaxListeners(1000);
let replicationDisabled;
let nextId = 1; // for request ids
exports.servers = [];
// This is the set of acceptable root certificates for replication, which includes the publicly trusted CAs if enabled
// and any CAs that have been replicated across the cluster
exports.replicationCertificateAuthorities = environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_ENABLEROOTCAS) !== false ? new Set(tls.rootCertificates) : new Set();
/**
 * Build mTLS configuration for replication server with certificate verification support
 * @param replicationOptions - Replication configuration options
 * @returns mTLS configuration object (always enabled for replication)
 */
function buildReplicationMtlsConfig(replicationOptions) {
    // mTLS is ALWAYS enabled for replication (required for security)
    // It cannot be disabled - only certificate verification can be configured
    // If mtls config exists and is an object, use it for certificate verification settings
    if (replicationOptions?.mtls && typeof replicationOptions.mtls === 'object') {
        // Preserve certificate verification settings
        return replicationOptions.mtls;
    }
    // If mtls is explicitly set to false, override it - mTLS is required for replication
    // Default: mTLS enabled, certificate verification disabled
    return true;
}
/**
 * Start the replication server. This will start a WebSocket server that will accept replication requests from other nodes.
 * @param options
 */
function start(options) {
    logger.notify('Starting replication server');
    if (options.hostname && !environmentManager_js_1.default.get('node_hostname')) {
        // for back-compat, carry this over
        environmentManager_js_1.default.setProperty('node_hostname');
        (0, nodeName_1.clearThisNodeName)();
    }
    if (options.url && !environmentManager_js_1.default.get('node_url')) {
        // for back-compat, carry this over
        environmentManager_js_1.default.setProperty('node_url');
        (0, nodeName_1.clearThisNodeName)();
    }
    if (!options.port && !options.securePort) {
        // if no replication ports are specified at all, default to using operations API ports
        options.port = environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_PORT);
        options.securePort = environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_SECUREPORT);
    }
    logger.notify('replication server hostname', Server_ts_1.server.hostname);
    if (!Server_ts_1.server.hostname)
        throw new Error('Can not load replication without a url (see replication.url in the config)');
    const routeByHostname = new Map();
    for (const node of (0, knownNodes_ts_1.iterateRoutes)(options)) {
        routeByHostname.set(node.name, node);
    }
    assignReplicationSource(options);
    // Build mTLS configuration with certificate verification support
    // mTLS is always enabled for replication (required for security)
    const mtlsConfig = buildReplicationMtlsConfig(options);
    options = {
        // We generally expect this to use the same settings as the operations API port
        usageType: 'replication', // indicate usage type for cert selection
        maxPayload: 10 * 1024 * 1024 * 1024, // 10 GB max payload, primarily to support replicating applications
        ...options,
        mtls: mtlsConfig, // mTLS with optional certificate verification (always overrides)
    };
    // noinspection JSVoidFunctionReturnValueUsed
    // @ts-expect-error
    const wsServers = Server_ts_1.server.ws(async (ws, request, chainCompletion, next) => {
        logger.debug('Incoming WS connection received ' + request.url);
        if (request.headers.get('sec-websocket-protocol') !== 'harperdb-replication-v1') {
            return next(ws, request, chainCompletion);
        }
        ws._socket.unref(); // we don't want the socket to keep the thread alive
        (0, replicationConnection_ts_1.replicateOverWS)(ws, options, chainCompletion.then(() => request?.user));
        ws.on('error', (error) => {
            if (error.code !== 'ECONNREFUSED')
                logger.error('Error in connection to ' + this.url, error.message);
        });
    }, options);
    options.runFirst = true;
    // now setup authentication for the replication server, authorizing by certificate
    // or IP address and then falling back to standard authorization, we set up an http middleware listener
    Server_ts_1.server.http(async (request, nextHandler) => {
        if (request.isWebSocket && request.headers.get('Sec-WebSocket-Protocol') === 'harperdb-replication-v1') {
            logger.debug('Incoming replication WS connection received, authorized: ' + request.authorized);
            const authorizationError = request._nodeRequest.socket.authorizationError;
            if (authorizationError) {
                if (authorizationError === 'CERT_HAS_EXPIRED') {
                    logger.error(`Incoming client connection from ${request.peerCertificate?.subjectaltname ?? request.ip} has an expired certificate`, authorizationError);
                }
                else {
                    logger.error(`Incoming client connection from ${request.peerCertificate?.subjectaltname ?? request.ip} did not have valid certificate, you may need turn on enableRootCAs in the config if you are using a publicly signed certificate, or add the CA to the server's trusted CAs`, authorizationError, 'certificate issuer', request.peerCertificate.issuer, 'did not match any available CAs', Array.from(Array.from(wsServers[0].secureContexts.values())[0].options.availableCAs.keys()));
                }
            }
            const hdbNodesStore = (0, knownNodes_ts_1.getHDBNodeTable)().primaryStore;
            // attempt to authorize by certificate common name, this is the most common means of auth
            if (request.authorized && request.peerCertificate.subjectaltname) {
                const hostnames = (0, keys_js_1.getHostnamesFromCertificate)(request.peerCertificate);
                let node;
                for (const hostname of hostnames) {
                    node = hostname && (hdbNodesStore.get(hostname) || routeByHostname.get(hostname));
                    if (node)
                        break;
                }
                if (node) {
                    // Perform certificate verification
                    // Pass the mtls config (which may contain certificateVerification settings)
                    const verificationResult = await (0, index_ts_1.verifyCertificate)(request.peerCertificate, options.mtls);
                    if (!verificationResult.valid) {
                        logger.warn('Certificate verification failed:', verificationResult.status, 'for node', node.name, 'certificate serial number', request.peerCertificate.serialNumber);
                        return;
                    }
                    // Keep manual revocation check as a fallback
                    if (node?.revoked_certificates?.includes(request.peerCertificate.serialNumber)) {
                        logger.warn('Revoked certificate used in attempt to connect to node', node.name, 'certificate serial number', request.peerCertificate.serialNumber);
                        return;
                    }
                    else {
                        request.user = node;
                    }
                }
                else {
                    // technically if there are credentials, we could still allow the connection, but give a warning, because we don't usually do that
                    logger.warn(`No node found for certificate common name/SANs: ${hostnames}, available nodes are ${Array.from(hdbNodesStore
                        .getRange({})
                        .filter(({ value }) => value)
                        .map(({ key }) => key)).join(', ')} and routes ${Array.from(routeByHostname.keys()).join(', ')}, connection will require credentials.`);
                }
            }
            else if (request.ip) {
                // try by IP address
                const node = hdbNodesStore.get(request.ip) || routeByHostname.get(request.ip);
                if (node) {
                    request.user = node;
                }
                else if (!authorizationError) {
                    logger.info(`No node found for IP address ${request.ip}, available nodes are ${Array.from(new Set([...hdbNodesStore.getKeys(), ...routeByHostname.keys()])).join(', ')}, connection will require credentials.`);
                }
            }
        }
        return nextHandler(request);
    }, options);
    // we need to keep track of the servers so we can update the secure contexts
    const contextUpdaters = [];
    // @ts-expect-error
    for (const wsServer of wsServers) {
        if (wsServer.secureContexts) {
            // we have secure contexts, so we can update the replication variants with the replication CAs
            const updateContexts = () => {
                // on any change to the list of replication CAs or the certificates, we update the replication security contexts
                // note that we do not do this for the main security contexts, because all the CAs
                // add a big performance penalty on connection setup
                const contextsToUpdate = new Set(wsServer.secureContexts.values());
                if (wsServer.defaultContext)
                    contextsToUpdate.add(wsServer.defaultContext);
                for (const context of contextsToUpdate) {
                    try {
                        const ca = Array.from(exports.replicationCertificateAuthorities);
                        // add the replication CAs (and root CAs) to any existing CAs for the context
                        if (context.options.availableCAs)
                            ca.push(...context.options.availableCAs.values());
                        const tlsOptions = 
                        // make sure we use the overriden tls.createSecureContext
                        // create a new security context with the extra CAs
                        { ...context.options, ca };
                        context.updatedContext = tls.createSecureContext(tlsOptions);
                    }
                    catch (error) {
                        logger.error('Error creating replication TLS config', error);
                    }
                }
            };
            wsServer.secureContextsListeners.push(updateContexts);
            // we need to stay up-to-date with any CAs that have been replicated across the cluster
            contextUpdaters.push(updateContexts);
            if (environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_ENABLEROOTCAS) !== false) {
                // if we are using root CAs, then we need to at least update the contexts for this even if none of the nodes have (explicit) CAs
                updateContexts();
            }
        }
    }
    // we always need to monitor for node changes, because this also does the essential step of setting up the server.shards
    monitorNodeCAs(() => {
        for (const updateContexts of contextUpdaters)
            updateContexts();
    });
}
function monitorNodeCAs(listener) {
    let lastCaCount = 0;
    (0, knownNodes_ts_1.subscribeToNodeUpdates)((node) => {
        logger.debug('Adding node CA', node?.ca?.slice(0, 60));
        if (node?.ca) {
            // we only care about nodes that have a CA
            exports.replicationCertificateAuthorities.add(node.ca);
            // created a set of all the CAs that have been replicated, if changed, update the secure context
            if (exports.replicationCertificateAuthorities.size !== lastCaCount) {
                lastCaCount = exports.replicationCertificateAuthorities.size;
                listener?.();
            }
        }
    });
}
function disableReplication(disabled = true) {
    replicationDisabled = disabled;
}
/**
 * Replication functions by acting as a "source" for tables. With replicated tables, the local tables are considered
 * a "cache" of the cluster's data. The tables don't resolve gets to the cluster, but they do propagate
 * writes and subscribe to the cluster.
 * This function will assign the NATS replicator as a source to all tables don't have an otherwise defined source (basically
 * any tables that aren't caching tables for another source).
 */
function assignReplicationSource(options) {
    if (replicationDisabled)
        return;
    (0, databases_ts_1.getDatabases)();
    exports.enabledDatabases = options.databases;
    // we need to set up the replicator as a source for each database that is replicated
    forEachReplicatedDatabase(options, (database, databaseName) => {
        if (!database) {
            // if no database, then the notification means the database was removed
            const dbSubscriptions = options.databaseSubscriptions || replicationConnection_ts_1.databaseSubscriptions;
            for (const [_url, dbConnections] of connections) {
                const dbConnection = dbConnections.get(databaseName);
                if (dbConnection) {
                    dbConnection.subscribe([], false);
                    dbConnections.delete(databaseName);
                }
            }
            dbSubscriptions.delete(databaseName);
            return;
        }
        for (const tableName in database) {
            const Table = database[tableName];
            setReplicator(databaseName, Table, options);
            replicationConnection_ts_1.tableUpdateListeners.get(Table)?.forEach((listener) => listener(Table));
        }
    });
}
/**
 * Get/create a replication resource that can be assigned as a source to tables
 * @param tableName
 * @param dbName
 */
function setReplicator(dbName, table, options) {
    if (!table) {
        return console.error(`Attempt to replicate non-existent table ${table.name} from database ${dbName}`);
    }
    if (table.replicate === false || table.sources?.some((source) => source.isReplicator))
        return;
    // We may try to consult this to get the other nodes for back-compat
    // const { hub_routes } = getClusteringRoutes();
    table.sourcedFrom(class Replicator extends Resource_ts_1.Resource {
        /**
         * This subscribes to the other nodes. Subscription events are notifications rather than
         * requests for data changes, so they circumvent the validation and replication layers
         * of the table classes.
         */
        static connection;
        static subscription;
        static async subscribe() {
            const dbSubscriptions = options.databaseSubscriptions || replicationConnection_ts_1.databaseSubscriptions;
            let subscription = dbSubscriptions.get(dbName);
            const tableById = subscription?.tableById || [];
            tableById[table.tableId] = table;
            const resolve = subscription?.ready;
            logger.trace('Setting up replicator subscription to database', dbName);
            if (!subscription?.auditStore) {
                // if and only if we are the first table for the database, then we set up the subscription.
                // We only need one subscription for the database
                // TODO: Eventually would be nice to have a real database subscription that delegated each specific table
                // event to each table
                this.subscription = subscription = new IterableEventQueue_ts_1.IterableEventQueue();
                dbSubscriptions.set(dbName, subscription);
                subscription.tableById = tableById;
                subscription.auditStore = table.auditStore;
                subscription.dbisDB = table.dbisDB;
                subscription.databaseName = dbName;
                if (resolve)
                    resolve(subscription);
                return subscription;
            }
            this.subscription = subscription;
        }
        static subscribeOnThisThread(_workerIndex, _totalWorkers) {
            // we need a subscription on every thread because we could get subscription requests from any
            // incoming TCP connection
            return true;
        }
        /**
         * This should be called when there is a local invalidated entry, or an entry that is known to be available
         * elsewhere on the cluster, and will retrieve from the appropriate node
         * @param query
         */
        static async load(entry) {
            if (entry) {
                const residencyId = entry.residencyId;
                const residency = entry.residency || table.dbisDB.get([Symbol.for('residency_by_id'), residencyId]);
                if (residency) {
                    let firstError;
                    const attemptedNodes = new Set();
                    do {
                        // This loop is for trying multiple nodes if the first one fails. With each iteration, we add the node to the attemptedConnections,
                        // so after fails we progressively try the next best node each time.
                        let bestConnection;
                        let bestNode = '';
                        let bestLatency = Infinity;
                        for (const nodeName of residency) {
                            if (attemptedNodes.has(nodeName))
                                continue;
                            if (nodeName === Server_ts_1.server.hostname)
                                continue; // don't both connecting to ourselves
                            const connection = getRetrievalConnectionByName(nodeName, Replicator.subscription, dbName);
                            // find a connection, needs to be connected and we haven't tried it yet
                            if (connection?.isConnected) {
                                // is connected and not ourselves
                                const latency = (0, knownNodes_ts_1.getReplicationSharedStatus)(table.auditStore, dbName, nodeName)[replicationConnection_ts_1.LATENCY_POSITION];
                                // choose this as the best connection if latency is lower (or hasn't been tested yet)
                                if (!bestConnection || latency < bestLatency) {
                                    bestConnection = connection;
                                    bestNode = nodeName;
                                    bestLatency = latency;
                                }
                            }
                        }
                        // if there are no connections left, throw an error
                        if (!bestConnection)
                            throw (firstError || new hdbError_js_1.ServerError(`No connection to any other nodes are available: ${residency}`, 502));
                        const request = {
                            requestId: nextId++,
                            table,
                            entry,
                            id: entry.key,
                        };
                        attemptedNodes.add(bestNode);
                        try {
                            return await bestConnection.getRecord(request);
                        }
                        catch (error) {
                            // if we are still connected, must be a non-network error
                            if (bestConnection.isConnected)
                                throw error;
                            // if we got a network error, record it and try the next node (continuing through the loop)
                            logger.warn('Error in load from node', bestNode, error);
                            if (!firstError)
                                firstError = error;
                        }
                    } while (true);
                }
            }
        }
        static isReplicator = true;
    }, { intermediateSource: true });
}
const connections = new Map();
/**
 * Get or create a connection to the specified node
 * @param url
 * @param subscription
 * @param dbName
 */
function getSubscriptionConnection(subscriptionUrl, connectingUrl, subscription, dbName, nodeName, authorization) {
    const connectionKey = connectingUrl + '-' + subscriptionUrl;
    let dbConnections = connections.get(connectionKey);
    if (!dbConnections) {
        dbConnections = new Map();
        connections.set(connectionKey, dbConnections);
    }
    let connection = dbConnections.get(dbName);
    if (connection)
        return connection;
    if (subscription) {
        dbConnections.set(dbName, (connection = new replicationConnection_ts_1.NodeReplicationConnection(connectingUrl, subscription, dbName, nodeName, authorization)));
        connection.connect();
        connection.once('finished', () => dbConnections.delete(dbName));
        return connection;
    }
}
const nodeNameToRetrievalConnections = new Map();
/**
 * Get connection by node name, using caching
 * */
function getRetrievalConnectionByName(nodeName, subscription, dbName) {
    let dbConnections = nodeNameToRetrievalConnections.get(nodeName);
    if (!dbConnections) {
        dbConnections = new Map();
        nodeNameToRetrievalConnections.set(nodeName, dbConnections);
    }
    let connection = dbConnections.get(dbName);
    if (connection)
        return connection;
    const node = (0, knownNodes_ts_1.getHDBNodeTable)().primaryStore.get(nodeName);
    if (node?.url) {
        connection = new replicationConnection_ts_1.NodeReplicationConnection((0, knownNodes_ts_1.getNodeURL)(node), subscription, dbName, nodeName, node.authorization);
        // cache the connection
        dbConnections.set(dbName, connection);
        connection.connect();
        connection.once('finished', () => dbConnections.delete(dbName));
    }
    return connection;
}
async function sendOperationToNode(node, operation, options) {
    if (!options)
        options = {};
    options.serverName = node.name;
    const socket = await (0, replicationConnection_ts_1.createWebSocket)((0, knownNodes_ts_1.getNodeURL)(node), options);
    const session = (0, replicationConnection_ts_1.replicateOverWS)(socket, {}, {});
    return new Promise((resolve, reject) => {
        socket.on('open', () => {
            logger.debug('Sending operation connection to ' + (0, knownNodes_ts_1.getNodeURL)(node) + ' opened', operation);
            resolve(session.sendOperation(operation));
        });
        socket.on('error', (error) => {
            reject(error);
        });
        socket.on('close', (error) => {
            logger.info('Sending operation connection to ' + (0, knownNodes_ts_1.getNodeURL)(node) + ' closed', error);
        });
    }).finally(() => {
        socket.close();
    });
}
/**
 * Subscribe to a node for a database, getting the necessary connection and subscription and signaling the start of the subscription
 * @param request
 */
function subscribeToNode(request) {
    try {
        if (worker_threads_1.isMainThread) {
            logger.warn(`Subscribing on main thread (should not happen in multi-threaded instance)`, request.nodes[0].url, request.database);
        }
        let subscriptionToTable = replicationConnection_ts_1.databaseSubscriptions.get(request.database);
        if (!subscriptionToTable) {
            // Wait for it to be created
            let ready;
            subscriptionToTable = new Promise((resolve) => {
                logger.info('Waiting for subscription to database ' + request.database);
                ready = resolve;
            });
            subscriptionToTable.ready = ready;
            replicationConnection_ts_1.databaseSubscriptions.set(request.database, subscriptionToTable);
        }
        const connection = getSubscriptionConnection(request.nodes[0].url, request.url, subscriptionToTable, request.database, request.name, request.nodes[0].authorization);
        if (request.nodes[0].name === undefined) {
            // we don't have the node name yet
            connection.tentativeNode = request.nodes[0];
        }
        else {
            connection.nodeName = request.nodes[0].name;
        }
        connection.subscribe(request.nodes.filter((node) => {
            return (0, knownNodes_ts_1.shouldReplicateFromNode)(node, request.database);
        }), request.replicateByDefault);
    }
    catch (error) {
        logger.error('Error in subscription to node', request.nodes[0]?.url, error);
    }
}
async function unsubscribeFromNode({ url, nodes, database }) {
    logger.trace('Unsubscribing from node', url, database, 'nodes', Array.from((0, knownNodes_ts_1.getHDBNodeTable)().primaryStore.getRange({})));
    const connectionKey = url + '-' + (nodes[0]?.url ?? url);
    const dbConnections = connections.get(connectionKey);
    if (dbConnections) {
        const connection = dbConnections.get(database);
        if (connection) {
            connection.unsubscribe();
            dbConnections.delete(database);
        }
    }
}
Server_ts_1.server.replication = {
    exportIdMapping: nodeIdMapping_ts_1.exportIdMapping,
    getIdOfRemoteNode: nodeIdMapping_ts_1.getIdOfRemoteNode,
};
function urlToNodeName(nodeUrl) {
    if (nodeUrl)
        return new URL(nodeUrl).hostname; // this the part of the URL that is the node name, as we want it to match common name in the certificate
}
/**
 * Iterate through all the databases and tables that are currently replicated, calling the callback for each.
 * Use this when you only need a one-shot pass over the present state — it does not register any listeners
 * on databaseEventsEmitter, so callers can invoke it in a hot path (e.g. inside a per-node-update handler)
 * without leaking listeners and tripping MaxListenersExceededWarning.
 * @param options
 * @param callback
 */
function iterateReplicatedDatabases(options, callback) {
    for (const databaseName of Object.getOwnPropertyNames(databases_ts_1.databases)) {
        forReplicatedDatabase(databaseName, options, callback);
    }
}
/**
 * Iterate through all the databases and tables that are replicated, both those that exist now, and future databases that
 * are added or removed, calling the callback for each. Returns a handle whose remove() unsubscribes both watchers — the
 * caller must call remove() when done, otherwise listeners accumulate on databaseEventsEmitter for the life of the process.
 * @param options
 * @param callback
 */
function forEachReplicatedDatabase(options, callback) {
    iterateReplicatedDatabases(options, callback);
    // Both listeners must be returned through the same handle, otherwise callers that
    // .remove() the result still leak the dropDatabase listener forever — which over time
    // trips MaxListenersExceededWarning on the global databaseEventsEmitter.
    const removedListener = (0, databases_ts_1.onRemovedDB)((databaseName) => {
        forReplicatedDatabase(databaseName, options, callback);
    });
    const updatedListener = (0, databases_ts_1.onUpdatedTable)((Table) => {
        forReplicatedDatabase(Table.databaseName, options, callback);
    });
    return {
        remove() {
            removedListener.remove();
            updatedListener.remove();
        },
    };
}
function forReplicatedDatabase(databaseName, options, callback) {
    const database = databases_ts_1.databases[databaseName];
    logger.trace('Checking replication status of ', databaseName, options?.databases);
    if (options?.databases === undefined ||
        options.databases === '*' ||
        options.databases.includes(databaseName) ||
        options.databases.some?.((dbConfig) => dbConfig.name === databaseName) ||
        !database)
        callback(database, databaseName, true);
    else if (hasExplicitlyReplicatedTable(databaseName))
        callback(database, databaseName, false);
}
function hasExplicitlyReplicatedTable(databaseName) {
    const database = databases_ts_1.databases[databaseName];
    for (const tableName in database) {
        const table = database[tableName];
        if (table.replicate)
            return true;
    }
}
async function replicateOperation(req) {
    const response = { message: '' };
    if (req.replicated !== false) {
        req.replicated = false; // don't send a replicated flag to the nodes we are sending to
        logger.trace?.('Replicating operation', req.operation, 'to nodes', Server_ts_1.server.nodes.map((node) => node.name));
        const replicatedResults = await Promise.allSettled(Server_ts_1.server.nodes.map((node) => {
            // do all the nodes in parallel
            return sendOperationToNode(node, req);
        }));
        // map the settled results to the response
        response.replicated = replicatedResults.map((settledResult, index) => {
            const result = settledResult.status === 'rejected'
                ? { status: 'failed', reason: settledResult.reason.toString() }
                : settledResult.value;
            result.node = Server_ts_1.server.nodes[index]?.name; // add the node to the result so we know which node succeeded/failed
            return result;
        });
    }
    return response;
}
Server_ts_1.server.replication.replicateOperation = replicateOperation;
Server_ts_1.server.replication.monitorNodeCAs = monitorNodeCAs;
Server_ts_1.server.replication.sendOperationToNode = sendOperationToNode;
//# sourceMappingURL=replicator.js.map