"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EditManager = exports.minimumPossibleSequenceNumber = void 0;
const sorted_btree_1 = __importDefault(require("sorted-btree"));
const core_utils_1 = require("@fluidframework/core-utils");
const util_1 = require("../util");
const core_1 = require("../core");
const branch_1 = require("./branch");
const editManagerFormat_1 = require("./editManagerFormat");
exports.minimumPossibleSequenceNumber = (0, util_1.brand)(Number.MIN_SAFE_INTEGER);
const minimumPossibleSequenceId = {
    sequenceNumber: exports.minimumPossibleSequenceNumber,
};
/**
 * Represents a local branch of a document and interprets the effect on the document of adding sequenced changes,
 * which were based on a given session's branch, to the document history
 */
// TODO: Try to reduce this to a single type parameter
class EditManager {
    /**
     * @param changeFamily - the change family of changes on the trunk and local branch
     * @param localSessionId - the id of the local session that will be used for local commits
     */
    constructor(changeFamily, 
    // TODO: Change this type to be the Session ID type provided by the IdCompressor when available.
    localSessionId) {
        this.changeFamily = changeFamily;
        this.localSessionId = localSessionId;
        /**
         * Records extra data associated with commits in the {@link trunk}.
         * This does not include an entry for the {@link trunkBase}.
         */
        this.trunkMetadata = new Map();
        /**
         * A map from a sequence id to the commit in the {@link trunk} which has that sequence id.
         * This also includes an entry for the {@link trunkBase} which always has the lowest key in the map.
         */
        this.sequenceMap = new sorted_btree_1.default(undefined, editManagerFormat_1.sequenceIdComparator);
        /**
         * Branches are maintained to represent the local change list that the issuing client had
         * at the time of submitting the latest known edit on the branch.
         * This means the head commit of each branch is always in its original (non-rebased) form.
         */
        // TODO:#4593: Add test to ensure that peer branches are never initialized with a repairDataStoreProvider
        this.peerLocalBranches = new Map();
        /**
         * Tracks where on the trunk all registered branches are based. Each key is the sequence id of a commit on
         * the trunk, and the value is the set of all branches who have that commit as their common ancestor with the trunk.
         */
        this.trunkBranches = new sorted_btree_1.default(undefined, editManagerFormat_1.sequenceIdComparator);
        /**
         * The sequence number of the newest commit on the trunk that has been received by all peers.
         * Defaults to {@link minimumPossibleSequenceNumber} if no commits have been received.
         *
         * @remarks If there are more than one commit with the same sequence number we assume this refers to the last commit in the batch.
         */
        this.minimumSequenceNumber = exports.minimumPossibleSequenceNumber;
        this.trunkBase = {
            revision: (0, core_1.assertIsRevisionTag)("00000000-0000-4000-8000-000000000000"),
            change: changeFamily.rebaser.compose([]),
        };
        this.sequenceMap.set(minimumPossibleSequenceId, this.trunkBase);
        this.trunk = new branch_1.SharedTreeBranch(this.trunkBase, changeFamily);
        this.localBranch = new branch_1.SharedTreeBranch(this.trunk.getHead(), changeFamily);
        this.localBranch.on("revertibleDispose", this.onRevertibleDisposed());
        // Track all forks of the local branch for purposes of trunk eviction. Unlike the local branch, they have
        // an unknown lifetime and rebase frequency, so we can not make any assumptions about which trunk commits
        // they require and therefore we monitor them explicitly.
        (0, branch_1.onForkTransitive)(this.localBranch, (fork) => this.registerBranch(fork));
    }
    /**
     * Make the given branch known to the `EditManager`. The `EditManager` will ensure that all registered
     * branches remain usable even as the minimum sequence number advances.
     */
    registerBranch(branch) {
        const trackBranch = (b) => {
            const trunkCommit = (0, core_1.findCommonAncestor)(this.trunk.getHead(), b.getHead()) ??
                (0, util_1.fail)("Expected branch to be related to trunk");
            const sequenceId = this.trunkMetadata.get(trunkCommit.revision)?.sequenceId ??
                minimumPossibleSequenceId;
            const branches = (0, util_1.getOrCreate)(this.trunkBranches, sequenceId, () => new Set());
            (0, core_utils_1.assert)(!branches.has(b), 0x670 /* Branch was registered more than once */);
            branches.add(b);
            return sequenceId;
        };
        const untrackBranch = (b, sequenceId) => {
            const branches = this.trunkBranches.get(sequenceId) ?? (0, util_1.fail)("Expected branch to be tracked");
            (0, core_utils_1.assert)(branches.delete(b), 0x671 /* Expected branch to be tracked */);
            if (branches.size === 0) {
                this.trunkBranches.delete(sequenceId);
            }
        };
        // Record the sequence id of the branch's base commit on the trunk
        const trunkBase = { sequenceId: trackBranch(branch) };
        // Whenever the branch is rebased, update our record of its base trunk commit
        const offRebase = branch.on("afterChange", (args) => {
            if (args.type === "replace" && (0, branch_1.getChangeReplaceType)(args) === "rebase") {
                untrackBranch(branch, trunkBase.sequenceId);
                trunkBase.sequenceId = trackBranch(branch);
                this.trimTrunk();
            }
        });
        // When the branch is disposed, update our branch set and trim the trunk
        const offDispose = branch.on("dispose", () => {
            untrackBranch(branch, trunkBase.sequenceId);
            this.trimTrunk();
            offRebase();
            offDispose();
        });
    }
    /**
     * Returns the sequence id of the oldest sequenced revertible commit on the local branch.
     *
     * TODO: may be more performant to maintain the oldest revertible on the branches themselves
     * this should be tested and revisited once branches are supported
     */
    getOldestRevertibleSequenceId() {
        if (this._oldestRevertibleSequenceId === undefined) {
            let oldest;
            for (const revision of this.localBranch.revertibleCommits()) {
                if (oldest === undefined) {
                    oldest = this.trunkMetadata.get(revision)?.sequenceId;
                }
                else {
                    const current = this.trunkMetadata.get(revision)?.sequenceId;
                    if (current !== undefined) {
                        oldest = (0, editManagerFormat_1.minSequenceId)(oldest, current);
                    }
                }
            }
            this._oldestRevertibleSequenceId = oldest;
        }
        return this._oldestRevertibleSequenceId;
    }
    onRevertibleDisposed() {
        return (revision) => {
            const metadata = this.trunkMetadata.get(revision);
            // if this revision hasn't been sequenced, it won't be evicted
            if (metadata !== undefined) {
                const { sequenceId: id } = metadata;
                // if this revision corresponds with the current oldest revertible sequence id, replace it with the new oldest
                if (id === this._oldestRevertibleSequenceId) {
                    this._oldestRevertibleSequenceId = undefined;
                }
            }
        };
    }
    /**
     * Advances the minimum sequence number, and removes all commits from the trunk which lie outside the collaboration window.
     * @param minimumSequenceNumber - the sequence number of the newest commit that all peers (including this one) have received and applied to their trunks.
     *
     * @remarks If there are more than one commit with the same sequence number we assume this refers to the last commit in the batch.
     */
    advanceMinimumSequenceNumber(minimumSequenceNumber) {
        if (minimumSequenceNumber === this.minimumSequenceNumber) {
            return;
        }
        (0, core_utils_1.assert)(minimumSequenceNumber > this.minimumSequenceNumber, 0x476 /* number must be larger or equal to current minimumSequenceNumber. */);
        this.minimumSequenceNumber = minimumSequenceNumber;
        this.trimTrunk();
    }
    /**
     * Examines the latest known minimum sequence number and the trunk bases of any registered branches to determine
     * if any commits on the trunk are unreferenced and unneeded for future computation; those found are evicted from the trunk.
     * @returns the number of commits that were removed from the trunk
     */
    trimTrunk() {
        /** The sequence id of the oldest commit on the trunk that will be retained */
        let trunkTailSequenceId = {
            sequenceNumber: this.minimumSequenceNumber,
            indexInBatch: Number.POSITIVE_INFINITY,
        };
        // If there are any outstanding registered branches, get the one that is the oldest (has the "most behind" trunk base)
        const minimumBranchBaseSequenceId = this.trunkBranches.minKey();
        if (minimumBranchBaseSequenceId !== undefined) {
            // If that branch is behind the minimum sequence id, we only want to evict commits older than it,
            // even if those commits are behind the minimum sequence id
            trunkTailSequenceId = (0, editManagerFormat_1.minSequenceId)(trunkTailSequenceId, minimumBranchBaseSequenceId);
        }
        // TODO get the oldest revertible sequence id from all registered branches, not just the local branch
        const oldestRevertibleSequenceId = this.getOldestRevertibleSequenceId();
        if (oldestRevertibleSequenceId !== undefined) {
            // use a smaller sequence number so that the oldest revertible is not trimmed
            const sequenceIdBeforeOldestRevertible = {
                sequenceNumber: (0, util_1.brand)(oldestRevertibleSequenceId.sequenceNumber - 1),
            };
            trunkTailSequenceId = (0, editManagerFormat_1.minSequenceId)(trunkTailSequenceId, sequenceIdBeforeOldestRevertible);
        }
        const [sequenceId, latestEvicted] = this.getClosestTrunkCommit(trunkTailSequenceId);
        // Don't do any work if the commit found by the search is already the tail of the trunk
        if (latestEvicted !== this.trunkBase) {
            // The minimum sequence number informs us that all peer branches are at least caught up to the tail commit,
            // so rebase them accordingly. This is necessary to prevent peer branches from referencing any evicted commits.
            for (const [, branch] of this.peerLocalBranches) {
                branch.rebaseOnto(this.trunk, latestEvicted);
            }
            // This mutation is a performance hack. If commits are truly immutable, then changing the trunk's tail requires
            // regenerating the entire commit graph. Instead, we can simply chop off the tail like this if we're certain
            // that there are no outstanding references to any of the commits being removed (other than the references via
            // the trunk). The peer branches have been rebased to the head of the trunk, the local branch is already rebased
            // to the head of the trunk, and all other branches are tracked by `trunkBranches` and known to be ahead of or at
            // `newTrunkBase`. Therefore, no branches should have unique references to any of the commits being evicted here.
            // We mutate the most recent of the evicted commits to become the new trunk base. That way, any other commits that
            // have parent pointers to the latest evicted commit will stay linked, even though that it is no longer part of the trunk.
            const newTrunkBase = latestEvicted;
            // The metadata for new trunk base revision needs to be deleted before modifying it.
            this.trunkMetadata.delete(newTrunkBase.revision);
            // Copying the revision of the old trunk base into the new trunk base means we don't need to write out the original
            // revision to summaries. All clients agree that the trunk base always has the same hardcoded revision.
            newTrunkBase.revision = this.trunkBase.revision;
            // Overwriting the change is not strictly necessary, but done here for consistency (so all trunk bases are deeply equal).
            newTrunkBase.change = this.trunkBase.change;
            // Dropping the parent field removes (transitively) all references to the evicted commits so they can be garbage collected.
            delete newTrunkBase.parent;
            this.trunkBase = newTrunkBase;
            // Update any state that is derived from trunk commits
            this.sequenceMap.editRange(minimumPossibleSequenceId, sequenceId, true, (s, { revision }) => {
                // Cleanup look-aside data for each evicted commit
                this.trunkMetadata.delete(revision);
                // Delete all evicted commits from `sequenceMap` except for the latest one, which is the new `trunkBase`
                if ((0, editManagerFormat_1.equalSequenceIds)(s, sequenceId)) {
                    (0, core_utils_1.assert)(revision === newTrunkBase.revision, 0x729 /* Expected last evicted commit to be new trunk base */);
                }
                else {
                    return { delete: true };
                }
            });
            const trunkSize = getPathFromBase(this.trunk.getHead(), this.trunkBase).length;
            (0, core_utils_1.assert)(this.sequenceMap.size === trunkSize + 1, 0x744 /* The size of the sequenceMap must have one element more than the trunk */);
            (0, core_utils_1.assert)(this.trunkMetadata.size === trunkSize, 0x745 /* The size of the trunkMetadata must be the same as the trunk */);
        }
    }
    isEmpty() {
        return (this.trunk.getHead() === this.trunkBase &&
            this.peerLocalBranches.size === 0 &&
            this.localBranch.getHead() === this.trunk.getHead() &&
            this.minimumSequenceNumber === exports.minimumPossibleSequenceNumber);
    }
    getSummaryData() {
        // The assert below is acceptable at present because summarization only ever occurs on a client with no
        // local/in-flight changes.
        // In the future we may wish to relax this constraint. For that to work, the current implementation of
        // `EditManager` would have to be amended in one of two ways:
        // A) Changes made by the local session should be represented by a branch in `EditManager.branches`.
        // B) The contents of such a branch should be computed on demand based on the trunk.
        // Note that option (A) would be a simple change to `addSequencedChange` whereas (B) would likely require
        // rebasing trunk changes over the inverse of trunk changes.
        (0, core_utils_1.assert)(this.localBranch.getHead() === this.trunk.getHead(), 0x428 /* Clients with local changes cannot be used to generate summaries */);
        const trunk = getPathFromBase(this.trunk.getHead(), this.trunkBase).map((c) => {
            const metadata = this.trunkMetadata.get(c.revision) ?? (0, util_1.fail)("Expected metadata for trunk commit");
            const commit = {
                change: c.change,
                revision: c.revision,
                sequenceNumber: metadata.sequenceId.sequenceNumber,
                sessionId: metadata.sessionId,
            };
            if (metadata.sequenceId.indexInBatch !== undefined) {
                commit.indexInBatch = metadata.sequenceId.indexInBatch;
            }
            return commit;
        });
        const branches = new Map((0, util_1.mapIterable)(this.peerLocalBranches.entries(), ([sessionId, branch]) => {
            const branchPath = [];
            const ancestor = (0, core_1.findCommonAncestor)([branch.getHead(), branchPath], this.trunk.getHead()) ??
                (0, util_1.fail)("Expected branch to be based on trunk");
            return [
                sessionId,
                {
                    base: ancestor.revision,
                    commits: branchPath.map((c) => {
                        const commit = {
                            change: c.change,
                            revision: c.revision,
                            sessionId,
                        };
                        return commit;
                    }),
                },
            ];
        }));
        return { trunk, branches };
    }
    loadSummaryData(data) {
        (0, core_utils_1.assert)(this.isEmpty(), 0x68a /* Attempted to load from summary after edit manager was already mutated */);
        // Record the tags of each trunk commit as we generate the trunk so they can be looked up quickly
        // when hydrating the peer branches below
        const trunkRevisionCache = new Map();
        trunkRevisionCache.set(this.trunkBase.revision, this.trunkBase);
        this.trunk.setHead(data.trunk.reduce((base, c) => {
            const sequenceId = c.indexInBatch === undefined
                ? {
                    sequenceNumber: c.sequenceNumber,
                }
                : {
                    sequenceNumber: c.sequenceNumber,
                    indexInBatch: c.indexInBatch,
                };
            const commit = (0, core_1.mintCommit)(base, c);
            this.sequenceMap.set(sequenceId, commit);
            this.trunkMetadata.set(c.revision, {
                sequenceId,
                sessionId: c.sessionId,
            });
            trunkRevisionCache.set(c.revision, commit);
            return commit;
        }, this.trunkBase));
        this.localBranch.setHead(this.trunk.getHead());
        for (const [sessionId, branch] of data.branches) {
            const commit = trunkRevisionCache.get(branch.base) ??
                (0, util_1.fail)("Expected summary branch to be based off of a revision in the trunk");
            this.peerLocalBranches.set(sessionId, new branch_1.SharedTreeBranch(branch.commits.reduce(core_1.mintCommit, commit), this.changeFamily));
        }
    }
    getTrunkChanges() {
        return getPathFromBase(this.trunk.getHead(), this.trunkBase).map((c) => c.change);
    }
    getTrunkHead() {
        return this.trunk.getHead();
    }
    getLocalChanges() {
        return getPathFromBase(this.localBranch.getHead(), this.trunk.getHead()).map((c) => c.change);
    }
    /**
     * @returns The length of the longest branch maintained by this EditManager.
     * This may be the length of a peer branch or the local branch.
     * The length is counted from the lowest common ancestor with the trunk such that a fully sequenced branch would
     * have length zero.
     */
    getLongestBranchLength() {
        let max = 0;
        const trunkHead = this.trunk.getHead();
        for (const branch of this.peerLocalBranches.values()) {
            const branchPath = getPathFromBase(branch.getHead(), trunkHead);
            if (branchPath.length > max) {
                max = branchPath.length;
            }
        }
        const localPath = getPathFromBase(this.localBranch.getHead(), trunkHead);
        return Math.max(max, localPath.length);
    }
    addSequencedChange(newCommit, sequenceNumber, referenceSequenceNumber) {
        (0, core_utils_1.assert)(sequenceNumber > this.minimumSequenceNumber, 0x713 /* Expected change sequence number to exceed the last known minimum sequence number */);
        const commitsSequenceNumber = this.getBatch(sequenceNumber);
        const sequenceId = commitsSequenceNumber.length === 0
            ? {
                sequenceNumber,
            }
            : {
                sequenceNumber,
                indexInBatch: commitsSequenceNumber.length,
            };
        if (newCommit.sessionId === this.localSessionId) {
            const [firstLocalCommit] = getPathFromBase(this.localBranch.getHead(), this.trunk.getHead());
            (0, core_utils_1.assert)(firstLocalCommit !== undefined, 0x6b5 /* Received a sequenced change from the local session despite having no local changes */);
            // The first local branch commit is already rebased over the trunk, so we can push it directly to the trunk.
            this.pushToTrunk(sequenceId, { ...firstLocalCommit, sessionId: this.localSessionId }, true);
            this.localBranch.rebaseOnto(this.trunk);
            return;
        }
        // Get the revision that the remote change is based on
        const [, baseRevisionInTrunk] = this.getClosestTrunkCommit(referenceSequenceNumber);
        // Rebase that branch over the part of the trunk up to the base revision
        // This will be a no-op if the sending client has not advanced since the last time we received an edit from it
        const peerLocalBranch = (0, util_1.getOrCreate)(this.peerLocalBranches, newCommit.sessionId, () => new branch_1.SharedTreeBranch(baseRevisionInTrunk, this.changeFamily));
        peerLocalBranch.rebaseOnto(this.trunk, baseRevisionInTrunk);
        if (peerLocalBranch.getHead() === this.trunk.getHead()) {
            // If the branch is fully caught up and empty after being rebased, then push to the trunk directly
            this.pushToTrunk(sequenceId, newCommit);
            peerLocalBranch.setHead(this.trunk.getHead());
        }
        else {
            // Otherwise, rebase the change over the trunk and append it, and append the original change to the peer branch.
            const newChangeFullyRebased = (0, core_1.rebaseChange)(this.changeFamily.rebaser, newCommit.change, peerLocalBranch.getHead(), this.trunk.getHead());
            peerLocalBranch.apply(newCommit.change, newCommit.revision);
            this.pushToTrunk(sequenceId, {
                ...newCommit,
                change: newChangeFullyRebased,
            });
        }
        this.localBranch.rebaseOnto(this.trunk);
    }
    findLocalCommit(revision) {
        const commits = [];
        const commit = (0, core_1.findAncestor)([this.localBranch.getHead(), commits], (c) => c.revision === revision);
        (0, core_utils_1.assert)(commit !== undefined, 0x599 /* Expected local branch to contain revision */);
        return [commit, commits];
    }
    pushToTrunk(sequenceId, commit, local = false) {
        const mintedCommit = (0, core_1.mintCommit)(this.trunk.getHead(), commit);
        this.trunk.setHead(mintedCommit);
        this.localBranch.updateRevertibleCommit(mintedCommit);
        const trunkHead = this.trunk.getHead();
        this.sequenceMap.set(sequenceId, trunkHead);
        this.trunkMetadata.set(trunkHead.revision, { sequenceId, sessionId: commit.sessionId });
    }
    getClosestTrunkCommit(searchBy) {
        const sequenceId = typeof searchBy === "number"
            ? {
                // This is to make sure that the correct commit is selected in this 2 scenarios:
                // 1) The commit is unique for that sequence number
                // 2) There are more than one commit for the same sequence number, in this case we need to select the last one.
                sequenceNumber: searchBy,
                indexInBatch: Number.POSITIVE_INFINITY,
            }
            : searchBy;
        const commit = this.sequenceMap.getPairOrNextLower(sequenceId);
        (0, core_utils_1.assert)(commit !== undefined, 0x746 /* sequence id has been evicted */);
        return commit;
    }
    getBatch(sequenceNumber) {
        const startSequenceId = {
            sequenceNumber,
        };
        const endSequenceId = {
            sequenceNumber: (0, util_1.brand)(sequenceNumber + 1),
        };
        return this.sequenceMap.getRange(startSequenceId, endSequenceId, false);
    }
}
exports.EditManager = EditManager;
/**
 * @returns the path from the base of a branch to its head
 */
function getPathFromBase(branchHead, baseBranchHead) {
    const path = [];
    (0, core_utils_1.assert)((0, core_1.findCommonAncestor)([branchHead, path], baseBranchHead) !== undefined, 0x573 /* Expected branches to be related */);
    return path;
}
//# sourceMappingURL=editManager.js.map