"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.OrderedClientElection = exports.OrderedClientCollection = void 0;
/* eslint-disable @rushstack/no-new-null */
const client_utils_1 = require("@fluid-internal/client-utils");
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const summarizerClientElection_js_1 = require("./summarizerClientElection.js");
/**
 * Tracks clients in the Quorum. It maintains their order using their join op
 * sequence numbers.
 * Internally, the collection of clients is maintained in a doubly-linked list,
 * with pointers to both the first and last nodes.
 * The first (root) node is a placeholder to simplify logic and reduce null checking.
 */
class OrderedClientCollection extends client_utils_1.TypedEventEmitter {
    get count() {
        return this.clientMap.size;
    }
    get oldestClient() {
        return this.rootNode.youngerClient;
    }
    constructor(logger, deltaManager, quorum) {
        super();
        /** Collection of ALL clients currently in the quorum, with client ids as keys. */
        this.clientMap = new Map();
        /** Placeholder head node of linked list, for simplified null checking. */
        this.rootNode = {
            sequenceNumber: -1,
            olderClient: undefined,
            youngerClient: undefined,
        };
        /** Pointer to end of linked list, for optimized client adds. */
        this._youngestClient = this.rootNode;
        this.logger = (0, internal_2.createChildLogger)({ logger, namespace: "OrderedClientCollection" });
        const members = quorum.getMembers();
        for (const [clientId, client] of members) {
            this.addClient(clientId, client);
        }
        quorum.on("addMember", (clientId, client) => {
            const newClient = this.addClient(clientId, client);
            this.emit("addClient", newClient, deltaManager.lastSequenceNumber);
        });
        quorum.on("removeMember", (clientId) => {
            const sequenceNumber = deltaManager.lastSequenceNumber;
            const removeClient = this.removeClient(clientId);
            if (removeClient === undefined) {
                this.logger.sendErrorEvent({
                    eventName: "ClientNotFound",
                    clientId,
                    sequenceNumber,
                });
            }
            else {
                this.emit("removeClient", removeClient, sequenceNumber);
            }
        });
    }
    addClient(clientId, client) {
        // Normal case is adding the latest client, which will bypass loop.
        // Find where it belongs otherwise (maybe possible during initial load?).
        (0, internal_1.assert)(client.sequenceNumber > -1, 0x1f6 /* "Negative client sequence number not allowed" */);
        let currClient = this._youngestClient;
        while (currClient.sequenceNumber > client.sequenceNumber) {
            (0, internal_1.assert)(currClient.olderClient !== undefined, 0x1f7 /* "Previous client should always be defined" */);
            // Note: If adding a client older than the elected client, it will not be automatically elected.
            currClient = currClient.olderClient;
        }
        // Now currClient is the node right before where the new client node should be.
        const newClient = {
            clientId,
            sequenceNumber: client.sequenceNumber,
            client: { ...client.client }, // shallow clone
            olderClient: currClient,
            youngerClient: currClient.youngerClient,
        };
        // Update prev node to point to this new node.
        newClient.olderClient.youngerClient = newClient;
        if (newClient.youngerClient === undefined) {
            // Update linked list end pointer to youngest client.
            this._youngestClient = newClient;
        }
        else {
            // Update next node to point back to this new node.
            newClient.youngerClient.olderClient = newClient;
        }
        this.clientMap.set(clientId, newClient);
        return newClient;
    }
    removeClient(clientId) {
        const removeClient = this.clientMap.get(clientId);
        if (removeClient === undefined) {
            return;
        }
        // Update prev node to point to next node.
        removeClient.olderClient.youngerClient = removeClient.youngerClient;
        if (removeClient.youngerClient === undefined) {
            // Update linked list end pointer to youngest client.
            this._youngestClient = removeClient.olderClient;
        }
        else {
            // Update next node to point back to previous node.
            removeClient.youngerClient.olderClient = removeClient.olderClient;
        }
        this.clientMap.delete(clientId);
        return removeClient;
    }
    /** Returns an array of all clients being tracked in order from oldest to newest. */
    getAllClients() {
        const result = [];
        let currClient = this.rootNode;
        while (currClient.youngerClient !== undefined) {
            result.push(currClient.youngerClient);
            currClient = currClient.youngerClient;
        }
        return result;
    }
}
exports.OrderedClientCollection = OrderedClientCollection;
/**
 * Adapter for OrderedClientCollection, with the purpose of deterministically maintaining
 * a currently elected client, excluding ineligible clients, in a distributed fashion.
 * This can be true as long as incrementElectedClient and resetElectedClient calls
 * are called under the same conditions for all clients.
 */
class OrderedClientElection extends client_utils_1.TypedEventEmitter {
    get eligibleCount() {
        return this._eligibleCount;
    }
    get electionSequenceNumber() {
        return this._electionSequenceNumber;
    }
    /**
     * OrderedClientCollection tracks electedClient and electedParent separately. This allows us to handle the case
     * where a new interactive parent client has been elected, but the summarizer is still doing work, so
     * a new summarizer should not yet be spawned. In this case, changing electedParent will cause SummaryManager
     * to stop the current summarizer, but a new summarizer will not be spawned until the old summarizer client has
     * left the quorum.
     *
     * Details:
     *
     * electedParent is the interactive client that has been elected to spawn a summarizer. It is typically the oldest
     * eligible interactive client in the quorum. Only the electedParent is permitted to spawn a summarizer.
     * Once elected, this client will remain the electedParent until it leaves the quorum or the summarizer that
     * it spawned stops producing summaries, at which point a new electedParent will be chosen.
     *
     * electedClient is the non-interactive summarizer client if one exists. If not, then electedClient is equal to
     * electedParent. If electedParent === electedClient, this is the signal for electedParent to spawn a new
     * electedClient. Once a summarizer client becomes electedClient, a new summarizer will not be spawned until
     * electedClient leaves the quorum.
     *
     * A typical sequence looks like this:
     *
     * i. Begin by electing A. electedParent === A, electedClient === A.
     *
     * ii. SummaryManager running on A spawns a summarizer client, A'. electedParent === A, electedClient === A'
     *
     * iii. A' stops producing summaries. A new parent client, B, is elected. electedParent === B, electedClient === A'
     *
     * iv. SummaryManager running on A detects the change to electedParent and tells the summarizer to stop, but A'
     * is in mid-summarization. No new summarizer is spawned, as electedParent !== electedClient.
     *
     * v. A' completes its summary, and the summarizer and backing client are torn down.
     *
     * vi. A' leaves the quorum, and B takes its place as electedClient. electedParent === B, electedClient === B
     *
     * vii. SummaryManager running on B spawns a summarizer client, B'. electedParent === B, electedClient === B'
     */
    get electedClient() {
        return this._electedClient;
    }
    get electedParent() {
        return this._electedParent;
    }
    constructor(logger, orderedClientCollection, 
    /** Serialized state from summary or current sequence number at time of load if new. */
    initialState, isEligibleFn, recordPerformanceEvents = false) {
        super();
        this.logger = logger;
        this.orderedClientCollection = orderedClientCollection;
        this.isEligibleFn = isEligibleFn;
        this.recordPerformanceEvents = recordPerformanceEvents;
        this._eligibleCount = 0;
        let initialClient;
        let initialParent;
        for (const client of orderedClientCollection.getAllClients()) {
            this.addClient(client, 0);
            if (typeof initialState !== "number") {
                if (client.clientId === initialState.electedClientId) {
                    initialClient = client;
                    if (initialState.electedParentId === undefined &&
                        client.client.details.type !== summarizerClientElection_js_1.summarizerClientType) {
                        // If there was no elected parent in the serialized data, use this one.
                        initialParent = client;
                    }
                }
                if (client.clientId === initialState.electedParentId) {
                    initialParent = client;
                }
            }
        }
        orderedClientCollection.on("addClient", (client, seq) => this.addClient(client, seq));
        orderedClientCollection.on("removeClient", (client, seq) => this.removeClient(client, seq));
        if (typeof initialState === "number") {
            this._electionSequenceNumber = initialState;
        }
        else {
            // Override the initially elected client with the initial state.
            if (initialClient?.clientId !== initialState.electedClientId) {
                // Cannot find initially elected client, so elect undefined.
                this.logger.sendErrorEvent({
                    eventName: "InitialElectedClientNotFound",
                    electionSequenceNumber: initialState.electionSequenceNumber,
                    expectedClientId: initialState.electedClientId,
                    electedClientId: initialClient?.clientId,
                    clientCount: orderedClientCollection.count,
                });
            }
            else if (initialClient !== undefined && !isEligibleFn(initialClient)) {
                // Initially elected client is ineligible, so elect next eligible client.
                initialClient = initialParent = this.findFirstEligibleParent(initialParent);
                this.logger.sendErrorEvent({
                    eventName: "InitialElectedClientIneligible",
                    electionSequenceNumber: initialState.electionSequenceNumber,
                    expectedClientId: initialState.electedClientId,
                    electedClientId: initialClient?.clientId,
                });
            }
            this._electedParent = initialParent;
            this._electedClient = initialClient;
            this._electionSequenceNumber = initialState.electionSequenceNumber;
        }
    }
    /**
     * Tries changing the elected client, raising an event if it is different.
     * Note that this function does no eligibility or suitability checks. If we get here, then
     * we will set _electedClient, and we will set _electedParent if this is an interactive client.
     */
    tryElectingClient(client, sequenceNumber, reason) {
        this.sendPerformanceEvent("TryElectingClient", client, sequenceNumber, false /* forceSend */, reason);
        let change = false;
        const isSummarizerClient = client?.client.details.type === summarizerClientElection_js_1.summarizerClientType;
        const prevClient = this._electedClient;
        if (this._electedClient !== client) {
            this.sendPerformanceEvent("ClientElected", client, sequenceNumber, true /* forceSend */, reason);
            // Changing the elected client. Record the sequence number and note that we have to fire an event.
            this._electionSequenceNumber = sequenceNumber;
            this._electedClient = client;
            change = true;
        }
        if (this._electedParent !== client && !isSummarizerClient) {
            this.sendPerformanceEvent("InteractiveClientElected", client, sequenceNumber, true /* forceSend */, reason);
            // Changing the elected parent as well.
            this._electedParent = client;
            change = true;
        }
        if (change) {
            this.emit("election", client, sequenceNumber, prevClient);
        }
    }
    tryElectingParent(client, sequenceNumber, reason) {
        this.sendPerformanceEvent("TryElectingParent", client, sequenceNumber, false /* forceSend */, reason);
        if (this._electedParent !== client) {
            this.sendPerformanceEvent("ParentElected", client, sequenceNumber, true /* forceSend */, reason);
            this._electedParent = client;
            this.emit("election", this._electedClient, sequenceNumber, this._electedClient);
        }
    }
    /**
     * Helper function to find the first eligible parent client starting with the passed in client,
     * or undefined if none are eligible.
     * @param client - client to start checking
     * @returns oldest eligible client starting with passed in client or undefined if none.
     */
    findFirstEligibleParent(client) {
        let candidateClient = client;
        while (candidateClient !== undefined &&
            (!this.isEligibleFn(candidateClient) ||
                candidateClient.client.details.type === summarizerClientElection_js_1.summarizerClientType)) {
            candidateClient = candidateClient.youngerClient;
        }
        return candidateClient;
    }
    /**
     * Updates tracking for when a new client is added to the collection.
     * Will automatically elect that new client if none is elected currently.
     * @param client - client added to the collection
     * @param sequenceNumber - sequence number when client was added
     */
    addClient(client, sequenceNumber) {
        this.sendPerformanceEvent("AddClient", client, sequenceNumber);
        if (this.isEligibleFn(client)) {
            this._eligibleCount++;
            const newClientIsSummarizer = client.client.details.type === summarizerClientElection_js_1.summarizerClientType;
            const electedClientIsSummarizer = this._electedClient?.client.details.type === summarizerClientElection_js_1.summarizerClientType;
            // Note that we allow a summarizer client to supersede an interactive client as elected client.
            if (this._electedClient === undefined ||
                (!electedClientIsSummarizer && newClientIsSummarizer)) {
                this.tryElectingClient(client, sequenceNumber, "AddClient");
            }
            else if (this._electedParent === undefined && !newClientIsSummarizer) {
                // This is an odd case. If the _electedClient is set, the _electedParent should be as well.
                this.tryElectingParent(client, sequenceNumber, "AddClient");
            }
        }
    }
    /**
     * Updates tracking for when an existing client is removed from the collection.
     * Will automatically elect next oldest client if currently elected is removed.
     * @param client - client removed from the collection
     * @param sequenceNumber - sequence number when client was removed
     */
    removeClient(client, sequenceNumber) {
        this.sendPerformanceEvent("RemoveClient", client, sequenceNumber);
        if (this.isEligibleFn(client)) {
            this._eligibleCount--;
            if (this._electedClient === client) {
                // Removing the _electedClient. There are 2 possible cases:
                if (this._electedParent !== client) {
                    // 1. The _electedClient is a summarizer that we've been allowing to finish its work.
                    // Let the _electedParent become the _electedClient so that it can start its own summarizer.
                    if (this._electedClient.client.details.type !== summarizerClientElection_js_1.summarizerClientType) {
                        throw new internal_2.UsageError("Elected client should be a summarizer client 1");
                    }
                    this.tryElectingClient(this._electedParent, sequenceNumber, "RemoveSummarizerClient");
                }
                else {
                    // 2. The _electedClient is an interactive client that has left the quorum.
                    // Automatically shift to next oldest client.
                    const nextClient = this.findFirstEligibleParent(this._electedParent?.youngerClient) ??
                        this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
                    this.tryElectingClient(nextClient, sequenceNumber, "RemoveClient");
                }
            }
            else if (this._electedParent === client) {
                // Removing the _electedParent (but not _electedClient).
                // Shift to the next oldest parent, but do not replace the _electedClient,
                // which is a summarizer that is still doing work.
                if (this._electedClient?.client.details.type !== summarizerClientElection_js_1.summarizerClientType) {
                    throw new internal_2.UsageError("Elected client should be a summarizer client 2");
                }
                const nextParent = this.findFirstEligibleParent(this._electedParent?.youngerClient) ??
                    this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
                this.tryElectingParent(nextParent, sequenceNumber, "RemoveClient");
            }
        }
    }
    getAllEligibleClients() {
        return this.orderedClientCollection.getAllClients().filter(this.isEligibleFn);
    }
    /**
     * Advance election to the next-oldest client. This is called if the current parent is leaving the quorum,
     * or if the current summarizer is not responsive and we want to stop it and spawn a new one.
     */
    incrementElectedClient(sequenceNumber) {
        const nextClient = this.findFirstEligibleParent(this._electedParent?.youngerClient) ??
            this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
        if (this._electedClient === undefined || this._electedClient === this._electedParent) {
            this.tryElectingClient(nextClient, sequenceNumber, "IncrementElectedClient");
        }
        else {
            // The _electedClient is a summarizer and should not be replaced until it leaves the quorum.
            // Changing the _electedParent will stop the summarizer.
            this.tryElectingParent(nextClient, sequenceNumber, "IncrementElectedClient");
        }
    }
    /**
     * (Re-)start election with the oldest client in the quorum. This is called if we need to summarize
     * and no client has been elected.
     */
    resetElectedClient(sequenceNumber) {
        const firstClient = this.findFirstEligibleParent(this.orderedClientCollection.oldestClient);
        if (this._electedClient === undefined || this._electedClient === this._electedParent) {
            this.tryElectingClient(firstClient, sequenceNumber, "ResetElectedClient");
        }
        else {
            // The _electedClient is a summarizer and should not be replaced until it leaves the quorum.
            // Changing the _electedParent will stop the summarizer.
            this.tryElectingParent(firstClient, sequenceNumber, "ResetElectedClient");
        }
    }
    peekNextElectedClient() {
        return (this.findFirstEligibleParent(this._electedParent?.youngerClient) ??
            this.findFirstEligibleParent(this.orderedClientCollection.oldestClient));
    }
    serialize() {
        return {
            electionSequenceNumber: this.electionSequenceNumber,
            electedClientId: this.electedClient?.clientId,
            electedParentId: this.electedParent?.clientId,
        };
    }
    sendPerformanceEvent(eventName, client, sequenceNumber, forceSend = false, reason) {
        if (this.recordPerformanceEvents || forceSend) {
            this.logger.sendPerformanceEvent({
                eventName,
                clientId: client?.clientId,
                sequenceNumber,
                electedClientId: this.electedClient?.clientId,
                electedParentId: this.electedParent?.clientId,
                isEligible: client !== undefined ? this.isEligibleFn(client) : false,
                isSummarizerClient: client?.client.details.type === summarizerClientElection_js_1.summarizerClientType,
                reason,
            });
        }
    }
}
exports.OrderedClientElection = OrderedClientElection;
//# sourceMappingURL=orderedClientElection.js.map