"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _TreeCheckout_events, _TreeCheckout_transaction, _TreeCheckout_validatedCommits;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TreeCheckout = exports.createTreeCheckout = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const client_utils_1 = require("@fluid-internal/client-utils");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../codec/index.js");
const index_js_2 = require("../core/index.js");
const index_js_3 = require("../feature-libraries/index.js");
const index_js_4 = require("../shared-tree-core/index.js");
const index_js_5 = require("../util/index.js");
const sharedTreeChangeFamily_js_1 = require("./sharedTreeChangeFamily.js");
const schematizingTreeView_js_1 = require("./schematizingTreeView.js");
/**
 * Creates a {@link TreeCheckout}.
 * @param args - an object containing optional components that will be used to build the view.
 * Any components not provided will be created by default.
 * @remarks This does not create a {@link SharedTree}, but rather a view with the minimal state
 * and functionality required to implement {@link ITreeCheckout}.
 */
function createTreeCheckout(idCompressor, mintRevisionTag, revisionTagCodec, args) {
    const forest = args?.forest ?? (0, index_js_3.buildForest)();
    const schema = args?.schema ?? new index_js_2.TreeStoredSchemaRepository();
    const defaultCodecOptions = { jsonValidator: index_js_1.noopValidator };
    const defaultFieldBatchVersion = 1;
    const changeFamily = args?.changeFamily ??
        new sharedTreeChangeFamily_js_1.SharedTreeChangeFamily(revisionTagCodec, args?.fieldBatchCodec ??
            (0, index_js_3.makeFieldBatchCodec)(defaultCodecOptions, defaultFieldBatchVersion), { jsonValidator: index_js_1.noopValidator }, args?.chunkCompressionStrategy, idCompressor);
    const branch = args?.branch ??
        new index_js_4.SharedTreeBranch({
            change: changeFamily.rebaser.compose([]),
            revision: "root",
        }, changeFamily, () => idCompressor.generateCompressedId());
    return new TreeCheckout(branch, false, changeFamily, schema, forest, mintRevisionTag, revisionTagCodec, idCompressor, args?.removedRoots, args?.logger, args?.breaker, args?.disposeForksAfterTransaction);
}
exports.createTreeCheckout = createTreeCheckout;
/**
 * An implementation of {@link ITreeCheckoutFork}.
 */
class TreeCheckout {
    constructor(branch, 
    /** True if and only if this checkout is for a forked branch and not the "main branch" of the tree. */
    isBranch, changeFamily, storedSchema, forest, mintRevisionTag, revisionTagCodec, idCompressor, removedRoots = (0, index_js_2.makeDetachedFieldIndex)("repair", revisionTagCodec, idCompressor), 
    /** Optional logger for telemetry. */
    logger, breaker = new index_js_5.Breakable("TreeCheckout"), disposeForksAfterTransaction = true) {
        this.isBranch = isBranch;
        this.changeFamily = changeFamily;
        this.storedSchema = storedSchema;
        this.forest = forest;
        this.mintRevisionTag = mintRevisionTag;
        this.revisionTagCodec = revisionTagCodec;
        this.idCompressor = idCompressor;
        this.removedRoots = removedRoots;
        this.logger = logger;
        this.breaker = breaker;
        this.disposeForksAfterTransaction = disposeForksAfterTransaction;
        this.disposed = false;
        this.views = new Set();
        /**
         * Set of revertibles maintained for automatic disposal
         */
        this.revertibles = new Set();
        /**
         * Each branch's head commit corresponds to a revertible commit.
         * Maintaining a whole branch ensures the commit graph is not pruned in a way that would prevent the commit from
         * being reverted.
         */
        this.revertibleCommitBranches = new Map();
        _TreeCheckout_events.set(this, (0, client_utils_1.createEmitter)());
        this.events = __classPrivateFieldGet(this, _TreeCheckout_events, "f");
        this.onAfterChange = (event) => {
            this.editLock.lock();
            __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("beforeBatch", event);
            if (event.change !== undefined) {
                const revision = event.type === "rebase"
                    ? __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.getHead().revision
                    : event.change.revision;
                // Conflicts due to schema will be empty and thus are not applied.
                for (const change of event.change.change.changes) {
                    if (change.type === "data") {
                        const delta = (0, index_js_3.intoDelta)((0, index_js_2.tagChange)(change.innerChange, revision));
                        this.withCombinedVisitor((visitor) => {
                            (0, index_js_2.visitDelta)(delta, visitor, this.removedRoots, revision);
                        });
                    }
                    else if (change.type === "schema") {
                        // Schema changes from a current to a new schema are expected to be backwards compatible.
                        // This guarantees that all data in the forest (which is valid before the schema change)
                        // is also valid under the new schema.
                        // Note however, that such schema changes may in some cases be rolled back:
                        // Case 1: A transaction with a schema change may be aborted.
                        // The transaction may have made some data changes that would render some trees invalid
                        // under the old schema, but these changes will also be rolled back, thereby putting the forest
                        // back in the state before the transaction, which is valid under the original (reinstated) schema.
                        // Case 2: A branch with a schema change may be rebased such that the schema change (because
                        // of a constraint) is no longer applied.
                        // Such a branch may contain data changes that would render some trees invalid under the
                        // original schema. These data changes may not necessarily be rolled back.
                        // They will however be rebased over the rollback of the schema change. This rebasing will
                        // ensure that these data changes are muted if they would render some trees invalid under the
                        // original (reinstated) schema.
                        this.storedSchema.apply(change.innerChange.schema.new);
                    }
                    else {
                        (0, index_js_5.fail)("Unknown Shared Tree change type.");
                    }
                }
            }
            __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("afterBatch");
            this.editLock.unlock();
            if (event.type === "append") {
                event.newCommits.forEach((commit) => this.validateCommit(commit));
            }
        };
        this.onAncestryTrimmed = (revisions) => {
            // When the branch is trimmed, we can garbage collect any repair data whose latest relevant revision is one of the
            // trimmed revisions.
            this.withCombinedVisitor((visitor) => {
                revisions.forEach((revision) => {
                    // get all the roots last created or used by the revision
                    const roots = this.removedRoots.getRootsLastTouchedByRevision(revision);
                    // get the detached field for the root and delete it from the removed roots
                    for (const root of roots) {
                        visitor.destroy(this.removedRoots.toFieldKey(root), 1);
                    }
                    this.removedRoots.deleteRootsLastTouchedByRevision(revision);
                });
            });
        };
        /**
         * The {@link Transactor} for this checkout.
         * @remarks In the context of a checkout, transactions allow edits to be batched into atomic units.
         * Edits made during a transaction will update the local state of the tree immediately, but will be squashed into a single edit when the transaction is committed.
         * If the transaction is aborted, the local state will be reset to what it was before the transaction began.
         * Transactions may nest, meaning that a transaction may be started while a transaction is already ongoing.
         *
         * To avoid updating observers of the view state with intermediate results during a transaction,
         * use {@link ITreeCheckout#branch} and {@link ISharedTreeFork#merge}.
         */
        _TreeCheckout_transaction.set(this, void 0);
        // #region Commit Validation
        /** Used to maintain the contract of {@link onCommitValid}(). */
        _TreeCheckout_validatedCommits.set(this, new WeakMap());
        __classPrivateFieldSet(this, _TreeCheckout_transaction, new index_js_4.SquashingTransactionStack(branch, (commits) => {
            const revision = this.mintRevisionTag();
            for (const transactionStep of commits) {
                this.removedRoots.updateMajor(transactionStep.revision, revision);
            }
            const squashedChange = this.changeFamily.rebaser.compose(commits);
            const change = this.changeFamily.rebaser.changeRevision(squashedChange, revision);
            return (0, index_js_2.tagChange)(change, revision);
        }, () => {
            const disposeForks = this.disposeForksAfterTransaction
                ? trackForksForDisposal(this)
                : undefined;
            // When each transaction is started, take a snapshot of the current state of removed roots
            const removedRootsSnapshot = this.removedRoots.clone();
            return (result) => {
                switch (result) {
                    case index_js_4.TransactionResult.Abort:
                        this.removedRoots = removedRootsSnapshot;
                        break;
                    case index_js_4.TransactionResult.Commit:
                        if (!this.transaction.isInProgress()) {
                            // The changes in a transaction squash commit have already applied to the checkout and are known to be valid, so we can validate the squash commit automatically.
                            this.validateCommit(__classPrivateFieldGet(this, _TreeCheckout_transaction, "f").branch.getHead());
                        }
                        break;
                    default:
                        (0, internal_1.unreachableCase)(result);
                }
                disposeForks?.();
            };
        }), "f");
        this.editLock = new EditLock(__classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranchEditor);
        branch.events.on("afterChange", (event) => {
            // The following logic allows revertibles to be generated for the change.
            // Currently only appends (including merges and transaction commits) are supported.
            if (event.type === "append") {
                // TODO:#20949: When the SharedTree is detached, these commits will already have been garbage collected.
                //       Figure out a way to generate revertibles before the commits are garbage collected.
                for (const commit of event.newCommits) {
                    const kind = event.type === "append" ? event.kind : index_js_2.CommitKind.Default;
                    const { change, revision } = commit;
                    const getRevertible = (0, sharedTreeChangeFamily_js_1.hasSchemaChange)(change)
                        ? undefined
                        : (onRevertibleDisposed) => {
                            if (!withinEventContext) {
                                throw new internal_2.UsageError("Cannot get a revertible outside of the context of a changed event.");
                            }
                            if (this.revertibleCommitBranches.get(revision) !== undefined) {
                                throw new internal_2.UsageError("Cannot generate the same revertible more than once. Note that this can happen when multiple changed event listeners are registered.");
                            }
                            const revertible = this.createRevertible(revision, kind, this, onRevertibleDisposed);
                            this.revertibleCommitBranches.set(revision, __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.fork(commit));
                            this.revertibles.add(revertible);
                            return revertible;
                        };
                    let withinEventContext = true;
                    __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("changed", { isLocal: true, kind }, getRevertible);
                    withinEventContext = false;
                }
            }
            else if (this.isRemoteChangeEvent(event)) {
                // TODO: figure out how to plumb through commit kind info for remote changes
                __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("changed", { isLocal: false, kind: index_js_2.CommitKind.Default });
            }
        });
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranchEvents.on("afterChange", this.onAfterChange);
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranchEvents.on("ancestryTrimmed", this.onAncestryTrimmed);
    }
    withCombinedVisitor(fn) {
        const anchorVisitor = this.forest.anchors.acquireVisitor();
        const combinedVisitor = (0, index_js_2.combineVisitors)([this.forest.acquireVisitor(), anchorVisitor], [anchorVisitor]);
        fn(combinedVisitor);
        combinedVisitor.free();
    }
    checkNotDisposed(usageError) {
        if (this.disposed) {
            if (usageError !== undefined) {
                throw new internal_2.UsageError(usageError);
            }
            (0, internal_1.assert)(false, 0x911 /* Invalid operation on a disposed TreeCheckout */);
        }
    }
    /**
     * Creates a {@link RevertibleAlpha} object that can undo a specific change in the tree's history.
     * Revision must exist in the given {@link TreeCheckout}'s branch.
     *
     * @param revision - The revision tag identifying the change to be made revertible.
     * @param kind - The {@link CommitKind} that produced this revertible (e.g., Default, Undo, Redo).
     * @param checkout - The {@link TreeCheckout} instance this revertible belongs to.
     * @param onRevertibleDisposed - Callback function that will be called when the revertible is disposed.
     * @returns - {@link RevertibleAlpha}
     */
    createRevertible(revision, kind, checkout, onRevertibleDisposed) {
        const commitBranches = checkout.revertibleCommitBranches;
        const revertible = {
            get status() {
                const revertibleCommit = commitBranches.get(revision);
                return revertibleCommit === undefined
                    ? index_js_2.RevertibleStatus.Disposed
                    : index_js_2.RevertibleStatus.Valid;
            },
            revert: (release = true) => {
                if (revertible.status === index_js_2.RevertibleStatus.Disposed) {
                    throw new internal_2.UsageError("Unable to revert a revertible that has been disposed.");
                }
                const revertMetrics = checkout.revertRevertible(revision, kind);
                checkout.logger?.sendTelemetryEvent({
                    eventName: TreeCheckout.revertTelemetryEventName,
                    ...revertMetrics,
                });
                if (release) {
                    revertible.dispose();
                }
            },
            clone: (targetBranch) => {
                // TODO:#23442: When a revertible is cloned for a forked branch, optimize to create a fork of a revertible branch once per revision NOT once per revision per checkout.
                const targetCheckout = (0, schematizingTreeView_js_1.getCheckout)(targetBranch);
                const revertibleBranch = this.revertibleCommitBranches.get(revision);
                if (revertibleBranch === undefined) {
                    throw new internal_2.UsageError("Unable to clone a revertible that has been disposed.");
                }
                const commitToRevert = revertibleBranch.getHead();
                const activeBranchHead = __classPrivateFieldGet(targetCheckout, _TreeCheckout_transaction, "f").activeBranch.getHead();
                if ((0, index_js_2.isAncestor)(commitToRevert, activeBranchHead, true) === false) {
                    throw new internal_2.UsageError("Cannot clone revertible for a commit that is not present on the given branch.");
                }
                targetCheckout.revertibleCommitBranches.set(revision, revertibleBranch.fork());
                return this.createRevertible(revision, kind, targetCheckout, onRevertibleDisposed);
            },
            dispose: () => {
                if (revertible.status === index_js_2.RevertibleStatus.Disposed) {
                    throw new internal_2.UsageError("Unable to dispose a revertible that has already been disposed.");
                }
                checkout.disposeRevertible(revertible, revision);
                onRevertibleDisposed?.(revertible);
            },
        };
        return revertible;
    }
    viewWith(config) {
        const view = new schematizingTreeView_js_1.SchematizingSimpleTreeView(this, config, (0, index_js_3.createNodeKeyManager)(this.idCompressor), this.breaker, () => {
            this.views.delete(view);
        });
        this.views.add(view);
        return view;
    }
    get rootEvents() {
        return this.forest.anchors.events;
    }
    get editor() {
        this.checkNotDisposed();
        return this.editLock.editor;
    }
    locate(anchor) {
        this.checkNotDisposed();
        return this.forest.anchors.locate(anchor);
    }
    get transaction() {
        return __classPrivateFieldGet(this, _TreeCheckout_transaction, "f");
    }
    branch() {
        this.checkNotDisposed("The parent branch has already been disposed and can no longer create new branches.");
        this.editLock.checkUnlocked("Branching");
        const anchors = new index_js_2.AnchorSet();
        const branch = __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.fork();
        const storedSchema = this.storedSchema.clone();
        const forest = this.forest.clone(storedSchema, anchors);
        const checkout = new TreeCheckout(branch, true, this.changeFamily, storedSchema, forest, this.mintRevisionTag, this.revisionTagCodec, this.idCompressor, this.removedRoots.clone(), this.logger, this.breaker, this.disposeForksAfterTransaction);
        __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("fork", checkout);
        return checkout;
    }
    rebase(checkout) {
        this.checkNotDisposed("The target of the branch rebase has been disposed and cannot be rebased.");
        checkout.checkNotDisposed("The source of the branch rebase has been disposed and cannot be rebased.");
        this.editLock.checkUnlocked("Rebasing");
        (0, internal_1.assert)(!checkout.transaction.isInProgress(), 0x9af /* A view cannot be rebased while it has a pending transaction */);
        (0, internal_1.assert)(checkout.isBranch, 0xa5d /* The main branch cannot be rebased onto another branch. */);
        __classPrivateFieldGet(checkout, _TreeCheckout_transaction, "f").activeBranch.rebaseOnto(__classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch);
    }
    rebaseOnto(checkout) {
        this.checkNotDisposed("The target of the branch rebase has been disposed and cannot be rebased.");
        checkout.rebase(this);
    }
    merge(checkout, disposeMerged = true) {
        this.checkNotDisposed("The target of the branch merge has been disposed and cannot be merged.");
        checkout.checkNotDisposed("The source of the branch merge has been disposed and cannot be merged.");
        this.editLock.checkUnlocked("Merging");
        (0, internal_1.assert)(!this.transaction.isInProgress(), 0x9b0 /* Views cannot be merged into a view while it has a pending transaction */);
        while (checkout.transaction.isInProgress()) {
            checkout.transaction.commit();
        }
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.merge(__classPrivateFieldGet(checkout, _TreeCheckout_transaction, "f").activeBranch);
        if (disposeMerged && checkout.isBranch) {
            // Dispose the merged checkout unless it is the main branch.
            checkout[index_js_5.disposeSymbol]();
        }
    }
    updateSchema(newSchema) {
        this.checkNotDisposed();
        this.editor.schema.setStoredSchema(this.storedSchema.clone(), newSchema);
    }
    dispose() {
        this.editLock.checkUnlocked("Disposing a view");
        this[index_js_5.disposeSymbol]();
    }
    [(_TreeCheckout_events = new WeakMap(), _TreeCheckout_transaction = new WeakMap(), _TreeCheckout_validatedCommits = new WeakMap(), index_js_5.disposeSymbol)]() {
        this.checkNotDisposed("The branch has already been disposed and cannot be disposed again.");
        this.disposed = true;
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").branch.dispose();
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").dispose();
        this.purgeRevertibles();
        for (const view of this.views) {
            view.dispose();
        }
        __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("dispose");
    }
    getRemovedRoots() {
        this.assertNoUntrackedRoots();
        const trees = [];
        const cursor = this.forest.allocateCursor("getRemovedRoots");
        for (const { id, root } of this.removedRoots.entries()) {
            const parentField = this.removedRoots.toFieldKey(root);
            this.forest.moveCursorToPath({ parent: undefined, parentField, parentIndex: 0 }, cursor);
            const tree = (0, index_js_3.jsonableTreeFromCursor)(cursor);
            // This method is used for tree consistency comparison.
            const { major, minor } = id;
            const finalizedMajor = major !== undefined ? this.revisionTagCodec.encode(major) : major;
            trees.push([finalizedMajor, minor, tree]);
        }
        cursor.free();
        return trees;
    }
    /**
     * This must be called on the root/main checkout after loading from a summary.
     * @remarks This pattern is necessary because the EditManager skips the normal process of applying commits to branches when loading a summary.
     * Instead, it simply {@link SharedTreeBranch#setHead | mutates} the branches directly which does not propagate the typical events throughout the rest of the system.
     */
    load() {
        // Set the tip revision as the latest relevant revision for any removed roots that are loaded from a summary - this allows them to be garbage collected later.
        // When a load happens, the head of the trunk and the head of the local/main branch must be the same (this is enforced by SharedTree).
        this.removedRoots.setRevisionsForLoadedData(__classPrivateFieldGet(this, _TreeCheckout_transaction, "f").branch.getHead().revision);
        // The content of the checkout (e.g. the forest) has (maybe) changed, so fire an afterBatch event.
        __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("afterBatch");
    }
    purgeRevertibles() {
        for (const revertible of this.revertibles) {
            revertible.dispose();
        }
    }
    disposeRevertible(revertible, revision) {
        this.revertibleCommitBranches.get(revision)?.dispose();
        this.revertibleCommitBranches.delete(revision);
        this.revertibles.delete(revertible);
    }
    revertRevertible(revision, kind) {
        if (this.transaction.isInProgress()) {
            throw new internal_2.UsageError("Undo is not yet supported during transactions.");
        }
        const revertibleBranch = this.revertibleCommitBranches.get(revision);
        (0, internal_1.assert)(revertibleBranch !== undefined, 0x7cc /* expected to find a revertible commit */);
        const commitToRevert = revertibleBranch.getHead();
        const revisionForInvert = this.mintRevisionTag();
        let change = (0, index_js_2.tagChange)(this.changeFamily.rebaser.invert(commitToRevert, false, revisionForInvert), revisionForInvert);
        const headCommit = __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.getHead();
        // Rebase the inverted change onto any commits that occurred after the undoable commits.
        if (commitToRevert !== headCommit) {
            change = (0, index_js_2.tagChange)((0, index_js_2.rebaseChange)(this.changeFamily.rebaser, change, commitToRevert, headCommit, this.mintRevisionTag).change, revisionForInvert);
        }
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.apply(change, kind === index_js_2.CommitKind.Default || kind === index_js_2.CommitKind.Redo
            ? index_js_2.CommitKind.Undo
            : index_js_2.CommitKind.Redo);
        // Derive some stats about the reversion to return to the caller.
        let revertAge = 0;
        let currentCommit = headCommit;
        while (commitToRevert.revision !== currentCommit.revision) {
            revertAge++;
            const parentCommit = currentCommit.parent;
            (0, internal_1.assert)(parentCommit !== undefined, 0x9a9 /* expected to find a parent commit */);
            currentCommit = parentCommit;
        }
        return { age: revertAge };
    }
    assertNoUntrackedRoots() {
        const cursor = this.forest.getCursorAboveDetachedFields();
        const rootFields = new Set([index_js_2.rootFieldKey]);
        for (const { root } of this.removedRoots.entries()) {
            rootFields.add(this.removedRoots.toFieldKey(root));
        }
        if (!cursor.firstField()) {
            return;
        }
        do {
            const field = cursor.getFieldKey();
            (0, internal_1.assert)(rootFields.has(field), 0xa22 /* Forest has a root field which is unknown to the detached field index */);
            rootFields.delete(field);
        } while (cursor.nextField());
    }
    /**
     * `true` iff the given branch change event is due to a remote change
     */
    isRemoteChangeEvent(event) {
        return (
        // Remote changes are only ever applied to the main branch
        !this.isBranch &&
            // Remote changes are applied to the main branch by rebasing it onto the trunk.
            // No other rebases are allowed on the main branch, so we can use this to detect remote changes.
            event.type === "rebase");
    }
    /**
     * Registers a function to be called when the given commit is validated.
     * @remarks A commit is validated by the checkout after it has been applied to the checkout's state (e.g. it has an effect on the forest).
     * If the commit applies successfully (i.e. it does not raise any unexpected errors), the commit is considered valid and the registered function is called.
     * If the commit does not apply successfully (because it causes an unexpected error), the function is not called (and the checkout will left in an error state).
     *
     * If the commit has already been validated when this function is called, the function is called immediately and this function returns `true`.
     * Otherwise, the function is registered to be called later and this function returns `false`.
     */
    onCommitValid(commit, fn) {
        const validated = (0, index_js_5.getOrCreate)(__classPrivateFieldGet(this, _TreeCheckout_validatedCommits, "f"), commit, () => []);
        if (validated === true) {
            fn(commit);
            return true;
        }
        validated.push(fn);
        return false;
    }
    /** Mark the given commit as "validated" according to the contract of {@link onCommitValid}(). */
    validateCommit(commit) {
        const validated = (0, index_js_5.getOrCreate)(__classPrivateFieldGet(this, _TreeCheckout_validatedCommits, "f"), commit, () => []);
        if (validated !== true) {
            validated.forEach((fn) => fn(commit));
            __classPrivateFieldGet(this, _TreeCheckout_validatedCommits, "f").set(commit, true);
        }
    }
}
exports.TreeCheckout = TreeCheckout;
/**
 * The name of the telemetry event logged for calls to {@link TreeCheckout.revertRevertible}.
 * @privateRemarks Exposed for testing purposes.
 */
TreeCheckout.revertTelemetryEventName = "RevertRevertible";
/**
 * A helper class that assists {@link TreeCheckout} in preventing functionality from being used while the tree is in the middle of being edited.
 */
class EditLock {
    /**
     * @param editor - an editor which will be used to create a new editor that is monitored to determine if any changes are happening to the tree.
     * Use {@link EditLock.editor} in place of the original editor to ensure that changes are monitored.
     */
    constructor(editor) {
        this.locked = false;
        const checkLock = () => this.checkUnlocked("Editing the tree");
        this.editor = {
            get schema() {
                return editor.schema;
            },
            valueField(...fieldArgs) {
                const valueField = editor.valueField(...fieldArgs);
                return {
                    set(...editArgs) {
                        checkLock();
                        valueField.set(...editArgs);
                    },
                };
            },
            optionalField(...fieldArgs) {
                const optionalField = editor.optionalField(...fieldArgs);
                return {
                    set(...editArgs) {
                        checkLock();
                        optionalField.set(...editArgs);
                    },
                };
            },
            sequenceField(...fieldArgs) {
                const sequenceField = editor.sequenceField(...fieldArgs);
                return {
                    insert(...editArgs) {
                        checkLock();
                        sequenceField.insert(...editArgs);
                    },
                    remove(...editArgs) {
                        checkLock();
                        sequenceField.remove(...editArgs);
                    },
                };
            },
            move(...moveArgs) {
                checkLock();
                editor.move(...moveArgs);
            },
            addNodeExistsConstraint(path) {
                editor.addNodeExistsConstraint(path);
            },
            addNodeExistsConstraintOnRevert(path) {
                editor.addNodeExistsConstraintOnRevert(path);
            },
        };
    }
    /**
     * Prevent further changes from being made to {@link EditLock.editor} until {@link EditLock.unlock} is called.
     * @remarks May only be called when the lock is not already locked.
     */
    lock() {
        if (this.locked) {
            debugger;
        }
        (0, internal_1.assert)(!this.locked, 0xaa7 /* Checkout has already been locked */);
        this.locked = true;
    }
    /**
     * Throws an error if the lock is currently locked.
     * @param action - The current action being performed by the user.
     * This must start with a capital letter, as it shows up as the first part of the error message and we want it to look nice.
     */
    checkUnlocked(action) {
        if (this.locked) {
            // These type assertions ensure that the event name strings used here match the actual event names
            const nodeChanged = "nodeChanged";
            const treeChanged = "treeChanged";
            throw new internal_2.UsageError(`${action} is forbidden during a ${nodeChanged} or ${treeChanged} event`);
        }
    }
    /**
     * Allow changes to be made to {@link EditLock.editor} again.
     * @remarks May only be called when the lock is currently locked.
     */
    unlock() {
        (0, internal_1.assert)(this.locked, 0xaa8 /* Checkout has not been locked */);
        this.locked = false;
    }
}
/**
 * Keeps track of all new forks created until the returned function is invoked, which will dispose all of those for.
 * The returned function may only be called once.
 *
 * @param checkout - The tree checkout for which you want to monitor forks for disposal.
 * @returns a function which can be called to dispose all of the tracked forks.
 */
function trackForksForDisposal(checkout) {
    const forks = new Set();
    const onDisposeUnSubscribes = [];
    const onForkUnSubscribe = (0, index_js_4.onForkTransitive)(checkout, (fork) => {
        forks.add(fork);
        onDisposeUnSubscribes.push(fork.events.on("dispose", () => forks.delete(fork)));
    });
    let disposed = false;
    return () => {
        (0, internal_1.assert)(!disposed, 0xaa9 /* Forks may only be disposed once */);
        forks.forEach((fork) => fork.dispose());
        onDisposeUnSubscribes.forEach((unsubscribe) => unsubscribe());
        onForkUnSubscribe();
        disposed = true;
    };
}
//# sourceMappingURL=treeCheckout.js.map