"use strict";
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.connectedToNode = exports.disconnectedFromNode = void 0;
exports.startOnMainThread = startOnMainThread;
exports.requestClusterStatus = requestClusterStatus;
exports.ensureNode = ensureNode;
/**
 * This module is responsible for managing the subscriptions for replication. It determines the connections and
 * subscriptions that are needed and delegates them to the available threads. It also manages when connections are
 * lost and delegating subscriptions through other nodes
 */
const databases_ts_1 = require("../core/resources/databases.js");
const manageThreads_js_1 = require("../core/server/threads/manageThreads.js");
const nodeIdMapping_ts_1 = require("../core/resources/nodeIdMapping.js");
const replicator_ts_1 = require("./replicator.js");
const nodeName_ts_1 = require("../core/server/nodeName.js");
const worker_threads_1 = require("worker_threads");
const knownNodes_ts_1 = require("./knownNodes.js");
const logger = __importStar(require("../core/utility/logging/harper_logger.js"));
const lodash_1 = __importDefault(require("lodash"));
const { cloneDeep } = lodash_1.default;
const environmentManager_js_1 = __importDefault(require("../core/utility/environment/environmentManager.js"));
const hdbTerms_ts_1 = require("../core/utility/hdbTerms.js");
const crypto_1 = require("crypto");
const minimist_1 = __importDefault(require("minimist"));
const cliArgs = (0, minimist_1.default)(process.argv);
const NODE_SUBSCRIBE_DELAY = 200; // delay before sending node subscribe to other nodes, so operations can complete first
// When a worker dies it may have been holding subscriptions for many (database, node) pairs.
// All of those pairs fire onDatabase reassignments in the same tick, which would otherwise
// slam a fresh worker with a burst of catchup connections and is the kind of pressure that
// caused the OOM in the first place. We stagger the re-subscriptions in time so the new
// worker(s) absorb them gradually.
const WORKER_EXIT_REASSIGN_STAGGER_MS = 100;
let nextWorkerExitReassignAt = 0;
const connectionReplicationMap = new Map();
const nodeMap = new Map(); // this is a map of all nodes that are available to connect to
const selfCatchupOfDatabase = new Map(); // this is a map of databases that need to catch up to themselves, and the time of the last audit entry (to start from)
const routes = [];
async function startOnMainThread(options) {
    // we do all of the main management of tracking connections and subscriptions on the main thread and delegate
    // the actual work to the worker threads
    let nextWorkerIndex = 0;
    const databases = (0, databases_ts_1.getDatabases)();
    // find all the databases last recorded audit entry so that we can inquire from the first node for self catch-up
    // of any records that may have been missed
    for (const dbName of Object.getOwnPropertyNames(databases)) {
        const database = databases[dbName];
        for (const tableName in database) {
            const table = database[tableName];
            if (table.auditStore) {
                selfCatchupOfDatabase.set(dbName, (0, nodeIdMapping_ts_1.lastTimeInAuditStore)(table.auditStore));
                break;
            }
        }
    }
    // we need to wait for the threads to start before we can start adding nodes
    // but don't await this because this start function has to finish before the threads can start
    manageThreads_js_1.whenThreadsStarted.then(async () => {
        const nodes = [];
        // if we are getting notified of system table updates, hdbNodes could be absent
        for await (const node of databases.system.hdb_nodes?.search([]) || []) {
            nodes.push(node);
        }
        const thisName = (0, nodeName_ts_1.getThisNodeName)();
        function ensureThisNode() {
            // If it doesn't exist and or needs to be updated.
            const existing = (0, knownNodes_ts_1.getHDBNodeTable)().primaryStore.get(thisName);
            if (existing !== null) {
                // if this was null it has previously been deleted, and we don't want to recreate nodes for deleted nodes
                const url = options.url ?? (0, nodeName_ts_1.getThisNodeUrl)();
                if (existing === undefined || existing.url !== url || existing.shard !== options.shard) {
                    return ensureNode(thisName, {
                        name: thisName,
                        url,
                        shard: options.shard,
                        replicates: true,
                    });
                }
            }
        }
        if ((0, knownNodes_ts_1.getHDBNodeTable)().primaryStore.get(thisName))
            ensureThisNode(); // if this node record already exists, check for config changes
        for (const route of (0, knownNodes_ts_1.iterateRoutes)(options)) {
            try {
                const replicateAll = !route.subscriptions;
                if (replicateAll) {
                    await ensureThisNode();
                }
                if (replicateAll) {
                    if (route.replicates == undefined)
                        route.replicates = true;
                }
                routes.push(route);
                if (nodes.find((node) => node.name === route.name))
                    continue;
                // just tentatively add this node to the list of nodes in memory
                onNodeUpdate(route);
            }
            catch (error) {
                console.error(error);
            }
        }
        (0, knownNodes_ts_1.subscribeToNodeUpdates)(onNodeUpdate);
    });
    let isFullyReplicating;
    /**
     * This is called when a new node is added to the hdbNodes table
     * @param node
     */
    function onNodeUpdate(node, hostname = node?.name) {
        const isSelf = ((0, nodeName_ts_1.getThisNodeName)() && hostname === (0, nodeName_ts_1.getThisNodeName)()) || ((0, nodeName_ts_1.getThisNodeUrl)() && node?.url === (0, nodeName_ts_1.getThisNodeUrl)());
        if (isSelf) {
            // this is just this node, we don't need to connect to ourselves, but if we get removed, we need to remove all fully replicating connections,
            // so we update each one
            const shouldFullyReplicate = Boolean(node?.replicates);
            if (isFullyReplicating !== undefined && isFullyReplicating !== shouldFullyReplicate) {
                for (const node of (0, knownNodes_ts_1.getHDBNodeTable)().search([])) {
                    if (node.replicates && node.name !== hostname)
                        onNodeUpdate(node, node.name);
                }
            }
            isFullyReplicating = shouldFullyReplicate;
        }
        logger.info('Setting up node replication for', node);
        if (!node) {
            // deleted node
            for (const [url, dbReplicationWorkers] of connectionReplicationMap) {
                let foundNode;
                for (const [_database, { nodes }] of dbReplicationWorkers) {
                    const node = nodes[0];
                    if (!node)
                        continue;
                    if (node.name == hostname) {
                        foundNode = true;
                        for (const [database, { worker }] of dbReplicationWorkers) {
                            dbReplicationWorkers.delete(database);
                            logger.warn('Node was deleted, unsubscribing from node', hostname, database, url);
                            worker?.postMessage({ type: 'unsubscribe-from-node', node: hostname, nodes, database, url });
                        }
                        break;
                    }
                }
                if (foundNode) {
                    const dbReplicationWorkers = connectionReplicationMap.get(url);
                    dbReplicationWorkers.iterator.remove();
                    connectionReplicationMap.delete(url);
                    return;
                }
            }
            return;
        }
        if (isSelf)
            return;
        let dbReplicationWorkers = connectionReplicationMap.get((0, knownNodes_ts_1.getNodeURL)(node));
        if (dbReplicationWorkers)
            dbReplicationWorkers.iterator.remove(); // we need to remove the old iterator so we can create a new one
        if (!(node.replicates === true || node.replicates?.sends) && !node.subscriptions?.length && !dbReplicationWorkers)
            return; // we don't have any subscriptions and we haven't connected yet, so just return
        logger.info(`Added node ${node.name} at ${(0, knownNodes_ts_1.getNodeURL)(node)} for process ${(0, nodeName_ts_1.getThisNodeName)()}`);
        if (node.replicates && node.subscriptions) {
            node = { ...node, subscriptions: null }; // if we have replicates flag set and have subscriptions, remove the subscriptions, they are just there for NATS
        }
        if (node.name) {
            // don't add to a map if we don't have a name (yet)
            // replace any node with same url
            for (const [key, existingNode] of nodeMap) {
                if (node.url === existingNode.url) {
                    nodeMap.delete(key);
                    break;
                }
            }
            nodeMap.set(node.name, node);
        }
        const databases = (0, databases_ts_1.getDatabases)();
        if (!dbReplicationWorkers) {
            dbReplicationWorkers = new Map();
            connectionReplicationMap.set((0, knownNodes_ts_1.getNodeURL)(node), dbReplicationWorkers);
        }
        dbReplicationWorkers.iterator = (0, replicator_ts_1.forEachReplicatedDatabase)(options, (database, databaseName, replicateByDefault) => {
            if (replicateByDefault) {
                onDatabase(databaseName, true);
            }
            else {
                onDatabase(databaseName, false);
            }
        });
        // check to see if there are any explicit subscriptions to databases that don't exist yet
        if (node.subscriptions) {
            // if we can't find any more granular subscriptions, then we skip this database
            // check to see if we have any explicit node subscriptions
            for (const sub of node.subscriptions) {
                const databaseName = sub.database || sub.schema;
                if (!databases[databaseName]) {
                    logger.warn(`Database ${databaseName} not found for node ${node.name}, making a subscription anyway`);
                    onDatabase(databaseName, false);
                }
            }
        }
        function onDatabase(databaseName, tablesReplicateByDefault) {
            logger.trace('Setting up replication for database', databaseName, 'on node', node.name);
            const existingEntry = dbReplicationWorkers.get(databaseName);
            let worker;
            const nodes = [{ replicateByDefault: tablesReplicateByDefault, ...node }];
            // Self catchup is done in case we have replicated any records that weren't actually written to our storage
            // before a crash.
            if (selfCatchupOfDatabase.has(databaseName) && environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_FAILOVER)) {
                // if we have a self catchup (only do if we have failover enabled), we need to add this node to the list of nodes that need to catch up
                // and then we will remove it when it is done
                nodes.push({
                    replicateByDefault: tablesReplicateByDefault,
                    name: (0, nodeName_ts_1.getThisNodeName)(),
                    startTime: selfCatchupOfDatabase.get(databaseName),
                    endTime: Date.now(),
                    replicates: true,
                });
                selfCatchupOfDatabase.delete(databaseName);
            }
            const shouldSubscribe = (0, knownNodes_ts_1.shouldReplicateFromNode)(node, databaseName);
            const httpWorkers = manageThreads_js_1.workers.filter((worker) => worker.name === 'http');
            if (existingEntry) {
                worker = existingEntry.worker;
                existingEntry.nodes = nodes;
                if (shouldSubscribe) {
                    // already subscribed, don't need to do anything
                    return;
                }
            }
            else if (shouldSubscribe) {
                nextWorkerIndex = nextWorkerIndex % httpWorkers.length; // wrap around as necessary
                worker = httpWorkers[nextWorkerIndex++];
                if (!worker) {
                    logger.warn('No http workers available to subscribe to node', node.name, (0, knownNodes_ts_1.getNodeURL)(node));
                }
                dbReplicationWorkers.set(databaseName, {
                    worker,
                    nodes,
                    url: (0, knownNodes_ts_1.getNodeURL)(node),
                });
                worker?.on('exit', () => {
                    // when a worker exits, we need to remove the entry from the map, and then reassign the subscriptions
                    if (dbReplicationWorkers.get(databaseName)?.worker === worker) {
                        // first verify it is still the worker
                        dbReplicationWorkers.delete(databaseName);
                        const now = Date.now();
                        nextWorkerExitReassignAt = Math.max(now, nextWorkerExitReassignAt) + WORKER_EXIT_REASSIGN_STAGGER_MS;
                        const delay = nextWorkerExitReassignAt - now;
                        setTimeout(() => onDatabase(databaseName, tablesReplicateByDefault), delay).unref();
                    }
                });
            }
            if (shouldSubscribe) {
                let leaderUrl = cliArgs.HDB_LEADER_URL ?? // first see if there was a leader explicitly specified
                    process.env.HDB_LEADER_URL ??
                    routes[0]?.url; // if we have routes, use the first one
                let leaderName = leaderUrl
                    ? new URL(leaderUrl).hostname
                    : Array.from((0, knownNodes_ts_1.getHDBNodeTable)()
                        .primaryStore.getKeys({})
                        .filter((nodeName) => nodeName !== (0, nodeName_ts_1.getThisNodeName)()) // find the first node that is not this one
                    )[0]; // try to find the first node
                const nodeName = nodes[0].name ?? (nodes[0].url && new URL(nodes[0].url).hostname);
                logger.warn(`Setting up subscription with leader ${leaderName} for node ${nodeName}`);
                nodes[0].isLeader = !leaderName || nodeName === leaderName;
                nodes[0].url ??= (0, knownNodes_ts_1.getNodeURL)(nodes[0]);
                setTimeout(() => {
                    const request = {
                        ...nodes[0],
                        type: 'subscribe-to-node',
                        database: databaseName,
                        nodes,
                    };
                    if (worker) {
                        worker.postMessage(request);
                    }
                    else
                        (0, replicator_ts_1.subscribeToNode)(request);
                }, NODE_SUBSCRIBE_DELAY);
            }
            else {
                logger.info('Node no longer should be used, unsubscribing from node', {
                    replicates: node.replicates,
                    databaseName,
                    node,
                    subscriptions: node.subscriptions,
                    hasDatabase: !!databases[databaseName],
                    thisReplicates: (0, knownNodes_ts_1.getHDBNodeTable)().primaryStore.get((0, nodeName_ts_1.getThisNodeName)())?.replicates,
                });
                if (!(0, knownNodes_ts_1.getHDBNodeTable)().primaryStore.get((0, nodeName_ts_1.getThisNodeName)())?.replicates) {
                    // if we are not fully replicating because it is turned off, make sure we set this
                    // flag so that we actually turn on subscriptions if full replication is turned on
                    isFullyReplicating = false;
                    logger.info('Disabling replication, this node name', (0, nodeName_ts_1.getThisNodeName)(), (0, knownNodes_ts_1.getHDBNodeTable)().primaryStore.get((0, nodeName_ts_1.getThisNodeName)()), databaseName);
                }
                const request = {
                    type: 'unsubscribe-from-node',
                    database: databaseName,
                    url: (0, knownNodes_ts_1.getNodeURL)(node),
                    name: node.name,
                    nodes,
                };
                if (worker) {
                    worker.postMessage(request);
                }
                else
                    (0, replicator_ts_1.unsubscribeFromNode)(request);
            }
        }
    }
    // only assign these if we are on the main thread
    exports.disconnectedFromNode = function (connection) {
        // if a node is disconnected, we need to reassign the subscriptions to another node
        // we try to do this in a deterministic way so that we don't end up with a cycle that short circuits
        // a node that may have more recent updates, so we try to go to the next node in the list, using
        // a sorted list of node names that all nodes should have and use.
        try {
            logger.info('Disconnected from node', connection.name, connection.url, 'finished', !!connection.finished);
            const nodeMapKeys = Array.from(nodeMap.keys());
            const nodeNames = nodeMapKeys.sort();
            const existingIndex = nodeNames.indexOf(connection.name || (0, replicator_ts_1.urlToNodeName)(connection.url));
            if (existingIndex === -1) {
                logger.warn('Disconnected node not found in node map', connection.name, nodeMapKeys);
                return;
            }
            let dbReplicationWorkers = connectionReplicationMap.get(connection.url);
            const existingWorkerEntry = dbReplicationWorkers?.get(connection.database);
            if (!existingWorkerEntry) {
                logger.warn('Disconnected node not found in replication map', connection.database, dbReplicationWorkers);
                return;
            }
            existingWorkerEntry.connected = false;
            if (connection.finished) {
                return;
            } // intentionally closed connection
            if (!environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_FAILOVER)) {
                // if failover is disabled, immediately return
                return;
            }
            const mainNode = existingWorkerEntry.nodes[0];
            if (!(mainNode.replicates === true || mainNode.replicates?.sends || mainNode.subscriptions?.length)) {
                // no replication, so just return
                return;
            }
            const shard = mainNode.shard;
            let nextIndex = (existingIndex + 1) % nodeNames.length;
            while (existingIndex !== nextIndex) {
                const nextNodeName = nodeNames[nextIndex];
                const nextNode = nodeMap.get(nextNodeName);
                dbReplicationWorkers = connectionReplicationMap.get((0, knownNodes_ts_1.getNodeURL)(nextNode));
                const failoverWorkerEntry = dbReplicationWorkers?.get(connection.database);
                if (!failoverWorkerEntry ||
                    failoverWorkerEntry.connected === false ||
                    failoverWorkerEntry.nodes[0].shard !== shard) {
                    // try the next node if this isn't connected or isn't in the same shard
                    nextIndex = (nextIndex + 1) % nodeNames.length;
                    continue;
                }
                const { nodes } = failoverWorkerEntry;
                // record which node we are now redirecting to
                let hasMovedNodes = false;
                for (const node of existingWorkerEntry.nodes) {
                    if (nodes.some((n) => n.name === node.name)) {
                        logger.info(`Disconnected node is already failing over to ${nextNodeName} for ${connection.database}`);
                        continue;
                    }
                    if (node.endTime < Date.now())
                        continue; // already expired
                    nodes.push(node);
                    logger.info(`Failing over ${connection.database} from ${connection.name} to ${nextNodeName}`);
                    connectToNextWorker(node, connection.database, failoverWorkerEntry.nodes[0]);
                    hasMovedNodes = true;
                }
                existingWorkerEntry.nodes = [existingWorkerEntry.nodes[0]]; // only keep our own subscription
                if (!hasMovedNodes) {
                    logger.info(`Disconnected node ${connection.name} has no nodes to fail over to ${nextNodeName}`);
                }
                return;
            }
            logger.warn('Unable to find any other node to fail over to', connection.name, connection.url);
        }
        catch (error) {
            logger.error('Error failing over node', error);
        }
    };
    exports.connectedToNode = function (connection) {
        // Basically undo what we did in disconnectedFromNode and also update the latency
        const dbReplicationWorkers = connectionReplicationMap.get(connection.url);
        const mainWorkerEntry = dbReplicationWorkers?.get(connection.database);
        if (!mainWorkerEntry) {
            logger.warn('Connected node not found in replication map, this may be because the node is being removed', connection.database, dbReplicationWorkers);
            return;
        }
        mainWorkerEntry.connected = true;
        mainWorkerEntry.latency = connection.latency;
        const restoredNode = mainWorkerEntry.nodes[0];
        if (!restoredNode) {
            logger.warn('Newly connected node has no node subscriptions', connection.database, mainWorkerEntry);
            return;
        }
        if (!restoredNode.name) {
            logger.debug('Connected node is not named yet', connection.database, mainWorkerEntry.url);
            return;
        }
        if (!environmentManager_js_1.default.get(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_FAILOVER)) {
            // if failover is disabled, immediately return, we don't need to restore anything
            return;
        }
        mainWorkerEntry.nodes = [restoredNode]; // restart with just our own connection
        for (const nodeWorkers of connectionReplicationMap.values()) {
            const failOverConnections = nodeWorkers.get(connection.database);
            if (!failOverConnections || failOverConnections == mainWorkerEntry)
                continue;
            const { nodes: failOverNodes, connected } = failOverConnections;
            if (!failOverNodes)
                continue;
            if (connected === false && failOverNodes[0].shard === restoredNode.shard && connection.url === restoredNode.url) {
                // if it is not connected and has extra nodes, grab them
                for (let node of failOverNodes) {
                    connectToNextWorker(node, connection.database);
                }
            }
            else {
                // remove the restored node from any other connections list of node
                const filtered = failOverNodes.filter((node) => {
                    if (node) {
                        if (node.name === restoredNode.name && node.worker) {
                            node.worker.postMessage({
                                type: 'unsubscribe-to-node',
                                database: connection.database,
                                url: connection.url,
                                nodes: [node],
                            });
                            return false;
                        }
                        return true;
                    }
                });
                if (filtered.length < failOverNodes.length) {
                    // if we were in the list, reset the nodes list
                    failOverConnections.nodes = filtered;
                }
            }
        }
    };
    function connectToNextWorker(node, database, connectingNode = node) {
        const httpWorkers = manageThreads_js_1.workers.filter((worker) => worker.name === 'http');
        nextWorkerIndex = nextWorkerIndex % httpWorkers.length; // wrap around as necessary
        const worker = httpWorkers[nextWorkerIndex++];
        // not enumerable property, we don't want this to be serialized in the postMessage
        Object.defineProperty(node, 'worker', { value: worker, configurable: true });
        if (worker) {
            worker.postMessage({
                url: (0, knownNodes_ts_1.getNodeURL)(connectingNode),
                name: connectingNode.name,
                type: 'subscribe-to-node',
                database,
                nodes: [node],
            });
        }
        else
            (0, replicator_ts_1.subscribeToNode)({ url: (0, knownNodes_ts_1.getNodeURL)(connectingNode), name: connectingNode.name, database, nodes: [node] });
    }
    (0, manageThreads_js_1.onMessageByType)('disconnected-from-node', exports.disconnectedFromNode);
    (0, manageThreads_js_1.onMessageByType)('connected-to-node', exports.connectedToNode);
    (0, manageThreads_js_1.onMessageByType)('request-cluster-status', requestClusterStatus);
}
/**
 * This is called when a request is made to get the cluster status. This should be executed only on the main thread
 * and will return the status of all replication connections (for each database)
 * @param message
 * @param port
 */
function requestClusterStatus(message, port) {
    const connections = [];
    for (const [node_name, node] of nodeMap) {
        try {
            const dbReplicationMap = connectionReplicationMap.get((0, knownNodes_ts_1.getNodeURL)(node));
            logger.info('Getting cluster status for', node_name, (0, knownNodes_ts_1.getNodeURL)(node), 'has dbs', dbReplicationMap?.size);
            const databases = [];
            if (dbReplicationMap) {
                for (const [database, { worker, connected, nodes, latency }] of dbReplicationMap) {
                    databases.push({
                        database,
                        connected,
                        latency,
                        threadId: worker?.threadId,
                        nodes: nodes.filter((node) => !(node.endTime < Date.now())).map((node) => node.name),
                    });
                }
                const res = cloneDeep(node);
                res.database_sockets = databases;
                delete res.ca;
                delete res.node_name;
                delete res.__updatedtime__;
                delete res.__createdtime__;
                connections.push(res);
            }
        }
        catch (error) {
            logger.warn('Error getting cluster status for', node?.url, error);
        }
    }
    port?.postMessage({
        type: 'cluster-status',
        connections,
    });
    return { connections };
}
if (worker_threads_1.parentPort) {
    exports.disconnectedFromNode = (connection) => {
        worker_threads_1.parentPort.postMessage({ type: 'disconnected-from-node', ...connection });
    };
    exports.connectedToNode = (connection) => {
        worker_threads_1.parentPort.postMessage({ type: 'connected-to-node', ...connection });
    };
    (0, manageThreads_js_1.onMessageByType)('subscribe-to-node', (message) => {
        (0, replicator_ts_1.subscribeToNode)(message);
    });
    (0, manageThreads_js_1.onMessageByType)('unsubscribe-from-node', (message) => {
        (0, replicator_ts_1.unsubscribeFromNode)(message);
    });
}
async function ensureNode(name, node) {
    const table = (0, knownNodes_ts_1.getHDBNodeTable)();
    name = name ?? (0, replicator_ts_1.urlToNodeName)(node.url);
    node.name = name;
    try {
        if (node.ca) {
            const cert = new crypto_1.X509Certificate(node.ca);
            node.ca_info = {
                issuer: cert.issuer.replace(/\n/g, ' '),
                subject: cert.subject.replace(/\n/g, ' '),
                subjectAltName: cert.subjectAltName,
                serialNumber: cert.serialNumber,
                validFrom: cert.validFrom,
                validTo: cert.validTo,
            };
        }
    }
    catch (err) {
        logger.error('Error parsing replication CA info for hdb_nodes table', err.message);
    }
    const existing = table.primaryStore.get(name);
    logger.debug(`Ensuring node ${name} at ${(0, knownNodes_ts_1.getNodeURL)(node)}, existing record:`, existing, 'new record:', node);
    if (!existing) {
        await table.patch(node);
    }
    else {
        if (Array.isArray(node.revoked_certificates)) {
            const existingRevoked = existing.revoked_certificates || [];
            node.revoked_certificates = [...new Set([...existingRevoked, ...node.revoked_certificates])];
        }
        logger.info(`Updating node ${name} at ${(0, knownNodes_ts_1.getNodeURL)(node)}`);
        await table.patch(node);
    }
}
//# sourceMappingURL=subscriptionManager.js.map