"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.newCrossFieldKeyTable = exports.getFieldsForCrossFieldKey = exports.getParentFieldId = exports.ModularEditBuilder = exports.getChangeHandler = exports.getFieldKind = exports.rebaseRevisionMetadataFromInfo = exports.intoDelta = exports.updateRefreshers = exports.relevantRemovedRoots = exports.ModularChangeFamily = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const sorted_btree_es6_1 = require("@tylerbu/sorted-btree-es6");
const index_js_1 = require("../../core/index.js");
const index_js_2 = require("../../util/index.js");
const index_js_3 = require("../chunked-forest/index.js");
const mapTreeCursor_js_1 = require("../mapTreeCursor.js");
const memoizedIdRangeAllocator_js_1 = require("../memoizedIdRangeAllocator.js");
const crossFieldQueries_js_1 = require("./crossFieldQueries.js");
const fieldChangeHandler_js_1 = require("./fieldChangeHandler.js");
const fieldKindWithEditor_js_1 = require("./fieldKindWithEditor.js");
const genericFieldKind_js_1 = require("./genericFieldKind.js");
/**
 * Implementation of ChangeFamily which delegates work in a given field to the appropriate FieldKind
 * as determined by the schema.
 */
class ModularChangeFamily {
    constructor(fieldKinds, codecs) {
        this.codecs = codecs;
        this.fieldKinds = fieldKinds;
    }
    get rebaser() {
        return this;
    }
    /**
     * Produces an equivalent list of `FieldChangeset`s that all target the same {@link FlexFieldKind}.
     * @param changes - The list of `FieldChange`s whose `FieldChangeset`s needs to be normalized.
     * @returns An object that contains both the equivalent list of `FieldChangeset`s that all
     * target the same {@link FlexFieldKind}, and the `FieldKind` that they target.
     * The returned `FieldChangeset`s may be a shallow copy of the input `FieldChange`s.
     */
    normalizeFieldChanges(change1, change2, genId, revisionMetadata) {
        // TODO: Handle the case where changes have conflicting field kinds
        const kind = change1.fieldKind !== genericFieldKind_js_1.genericFieldKind.identifier
            ? change1.fieldKind
            : change2.fieldKind;
        if (kind === genericFieldKind_js_1.genericFieldKind.identifier) {
            // Both changes are generic
            return {
                fieldKind: genericFieldKind_js_1.genericFieldKind.identifier,
                changeHandler: genericFieldKind_js_1.genericFieldKind.changeHandler,
                change1: change1.change,
                change2: change2.change,
            };
        }
        const fieldKind = getFieldKind(this.fieldKinds, kind);
        const changeHandler = fieldKind.changeHandler;
        const normalizedChange1 = this.normalizeFieldChange(change1, changeHandler, genId, revisionMetadata);
        const normalizedChange2 = this.normalizeFieldChange(change2, changeHandler, genId, revisionMetadata);
        return {
            fieldKind: kind,
            changeHandler,
            change1: normalizedChange1,
            change2: normalizedChange2,
        };
    }
    normalizeFieldChange(fieldChange, handler, genId, revisionMetadata) {
        if (fieldChange.fieldKind !== genericFieldKind_js_1.genericFieldKind.identifier) {
            return fieldChange.change;
        }
        // The cast is based on the `fieldKind` check above
        const genericChange = fieldChange.change;
        const convertedChange = (0, genericFieldKind_js_1.convertGenericChange)(genericChange, handler, (child1, child2) => {
            (0, internal_1.assert)(child1 === undefined || child2 === undefined, 0x92f /* Should not have two changesets to compose */);
            return child1 ?? child2 ?? (0, index_js_2.fail)("Should not compose two undefined node IDs");
        }, genId, revisionMetadata);
        return convertedChange;
    }
    compose(changes) {
        const { revInfos, maxId } = getRevInfoFromTaggedChanges(changes);
        const idState = { maxId };
        if (changes.length === 0) {
            return makeModularChangeset();
        }
        return changes.reduce((change1, change2) => (0, index_js_1.makeAnonChange)(this.composePair(change1, change2, revInfos, idState))).change;
    }
    composePair(change1, change2, revInfos, idState) {
        const { fieldChanges, nodeChanges, nodeToParent, nodeAliases, crossFieldKeys } = this.composeAllFields(change1.change, change2.change, revInfos, idState);
        const { allBuilds, allDestroys, allRefreshers } = composeBuildsDestroysAndRefreshers([
            change1,
            change2,
        ]);
        return makeModularChangeset(this.pruneFieldMap(fieldChanges, nodeChanges), nodeChanges, nodeToParent, nodeAliases, crossFieldKeys, idState.maxId, revInfos, undefined, allBuilds, allDestroys, allRefreshers);
    }
    composeAllFields(change1, change2, revInfos, idState) {
        if (hasConflicts(change1) && hasConflicts(change2)) {
            return {
                fieldChanges: new Map(),
                nodeChanges: new Map(),
                nodeToParent: new Map(),
                nodeAliases: new Map(),
                crossFieldKeys: newBTree(),
            };
        }
        else if (hasConflicts(change1)) {
            return change2;
        }
        else if (hasConflicts(change2)) {
            return change1;
        }
        const genId = (0, index_js_2.idAllocatorFromState)(idState);
        const revisionMetadata = (0, index_js_1.revisionMetadataSourceFromInfo)(revInfos);
        const crossFieldTable = newComposeTable(change1, change2);
        // We merge nodeChanges, nodeToParent, and nodeAliases from the two changesets.
        // The merged tables will have correct entries for all nodes which are only referenced in one of the input changesets.
        // During composeFieldMaps and processInvalidatedElements we will find all nodes referenced in both input changesets
        // and adjust these tables as necessary.
        // Note that when merging these tables we may encounter key collisions and will arbitrarily drop values in that case.
        // A collision for a node ID means that that node is referenced in both changesets
        // (since we assume that if two changesets use the same node ID they are referring to the same node),
        // therefore all collisions will be addressed when processing the intersection of the changesets.
        const composedNodeChanges = mergeNestedMaps(change1.nodeChanges, change2.nodeChanges);
        const composedNodeToParent = mergeNestedMaps(change1.nodeToParent, change2.nodeToParent);
        const composedNodeAliases = mergeNestedMaps(change1.nodeAliases, change2.nodeAliases);
        const composedFields = this.composeFieldMaps(change1.fieldChanges, change2.fieldChanges, genId, crossFieldTable, revisionMetadata);
        this.processInvalidatedElements(crossFieldTable, composedFields, composedNodeChanges, composedNodeToParent, composedNodeAliases, genId, revisionMetadata);
        // Currently no field kinds require making changes to cross-field keys during composition, so we can just merge the two tables.
        const composedCrossFieldKeys = mergeBTrees(change1.crossFieldKeys, change2.crossFieldKeys);
        return {
            fieldChanges: composedFields,
            nodeChanges: composedNodeChanges,
            nodeToParent: composedNodeToParent,
            nodeAliases: composedNodeAliases,
            crossFieldKeys: (0, index_js_2.brand)(composedCrossFieldKeys),
        };
    }
    composeInvalidatedField(fieldChange, crossFieldTable, genId, revisionMetadata) {
        const context = crossFieldTable.fieldToContext.get(fieldChange);
        (0, internal_1.assert)(context !== undefined, 0x8cc /* Should have context for every invalidated field */);
        const { change1: fieldChange1, change2: fieldChange2, composedChange } = context;
        const rebaser = getChangeHandler(this.fieldKinds, composedChange.fieldKind).rebaser;
        const composeNodes = (child1, child2) => {
            if (child1 !== undefined &&
                child2 !== undefined &&
                (0, index_js_1.getFromChangeAtomIdMap)(crossFieldTable.newToBaseNodeId, child2) === undefined) {
                (0, index_js_1.setInChangeAtomIdMap)(crossFieldTable.newToBaseNodeId, child2, child1);
                crossFieldTable.pendingCompositions.nodeIdsToCompose.push([child1, child2]);
            }
            return child1 ?? child2 ?? (0, index_js_2.fail)("Should not compose two undefined nodes");
        };
        const amendedChange = rebaser.compose(fieldChange1, fieldChange2, composeNodes, genId, new ComposeManager(crossFieldTable, fieldChange, false), revisionMetadata);
        composedChange.change = (0, index_js_2.brand)(amendedChange);
    }
    /**
     * Updates everything in the composed output which may no longer be valid.
     * This could be due to
     * - discovering that two node changesets refer to the same node (`nodeIdsToCompose`)
     * - a previously composed field being invalidated by a cross field effect (`invalidatedFields`)
     * - a field which was copied directly from an input changeset being invalidated by a cross field effect
     * (`affectedBaseFields` and `affectedNewFields`)
     *
     * Updating an element may invalidate further elements. This function runs until there is no more invalidation.
     */
    processInvalidatedElements(table, composedFields, composedNodes, composedNodeToParent, nodeAliases, genId, metadata) {
        const pending = table.pendingCompositions;
        while (table.invalidatedFields.size > 0 ||
            pending.nodeIdsToCompose.length > 0 ||
            pending.affectedBaseFields.length > 0 ||
            pending.affectedNewFields.length > 0) {
            // Note that the call to `composeNodesById` can add entries to `crossFieldTable.nodeIdPairs`.
            for (const [id1, id2] of pending.nodeIdsToCompose) {
                this.composeNodesById(table.baseChange.nodeChanges, table.newChange.nodeChanges, composedNodes, composedNodeToParent, nodeAliases, id1, id2, genId, table, metadata);
            }
            pending.nodeIdsToCompose.length = 0;
            this.composeAffectedFields(table, table.baseChange, true, pending.affectedBaseFields, composedFields, composedNodes, genId, metadata);
            this.composeAffectedFields(table, table.newChange, false, pending.affectedNewFields, composedFields, composedNodes, genId, metadata);
            this.processInvalidatedCompositions(table, genId, metadata);
        }
    }
    processInvalidatedCompositions(table, genId, metadata) {
        const fieldsToUpdate = table.invalidatedFields;
        table.invalidatedFields = new Set();
        for (const fieldChange of fieldsToUpdate) {
            this.composeInvalidatedField(fieldChange, table, genId, metadata);
        }
    }
    /**
     * Ensures that each field in `affectedFields` has been updated in the composition output.
     * Any field which has already been composed is ignored.
     * All other fields are optimistically assumed to not have any changes in the other input changeset.
     *
     * @param change - The changeset which contains the affected fields.
     * This should be one of the two changesets being composed.
     * @param areBaseFields - Whether the affected fields are part of the base changeset.
     * If not, they are assumed to be part of the new changeset.
     * @param affectedFields - The set of fields to process.
     */
    composeAffectedFields(table, change, areBaseFields, affectedFields, composedFields, composedNodes, genId, metadata) {
        for (const fieldIdKey of affectedFields.keys()) {
            const fieldId = normalizeFieldId(fieldIdFromFieldIdKey(fieldIdKey), change.nodeAliases);
            const fieldChange = fieldChangeFromId(change.fieldChanges, change.nodeChanges, fieldId);
            if (table.fieldToContext.has(fieldChange) ||
                table.newFieldToBaseField.has(fieldChange)) {
                // This function handles fields which were not part of the intersection of the two changesets but which need to be updated anyway.
                // If we've already processed this field then either it is up to date
                // or there is pending inval which will be handled in processInvalidatedCompositions.
                continue;
            }
            const emptyChange = this.createEmptyFieldChange(fieldChange.fieldKind);
            const [change1, change2] = areBaseFields
                ? [fieldChange, emptyChange]
                : [emptyChange, fieldChange];
            const composedField = this.composeFieldChanges(change1, change2, genId, table, metadata);
            if (fieldId.nodeId === undefined) {
                composedFields.set(fieldId.field, composedField);
                continue;
            }
            const nodeId = (0, index_js_1.getFromChangeAtomIdMap)(table.newToBaseNodeId, fieldId.nodeId) ?? fieldId.nodeId;
            let nodeChangeset = nodeChangeFromId(composedNodes, nodeId);
            if (!table.composedNodes.has(nodeChangeset)) {
                nodeChangeset = cloneNodeChangeset(nodeChangeset);
                (0, index_js_1.setInChangeAtomIdMap)(composedNodes, nodeId, nodeChangeset);
            }
            if (nodeChangeset.fieldChanges === undefined) {
                nodeChangeset.fieldChanges = new Map();
            }
            nodeChangeset.fieldChanges.set(fieldId.field, composedField);
        }
        affectedFields.clear();
    }
    composeFieldMaps(change1, change2, genId, crossFieldTable, revisionMetadata) {
        const composedFields = new Map();
        if (change1 === undefined || change2 === undefined) {
            return change1 ?? change2 ?? composedFields;
        }
        for (const [field, fieldChange1] of change1) {
            const fieldChange2 = change2.get(field);
            const composedField = fieldChange2 !== undefined
                ? this.composeFieldChanges(fieldChange1, fieldChange2, genId, crossFieldTable, revisionMetadata)
                : fieldChange1;
            composedFields.set(field, composedField);
        }
        for (const [field, fieldChange2] of change2) {
            if (change1 === undefined || !change1.has(field)) {
                composedFields.set(field, fieldChange2);
            }
        }
        return composedFields;
    }
    /**
     * Returns the composition of the two input fields.
     *
     * Any nodes in this field which were modified by both changesets
     * will be added to `crossFieldTable.pendingCompositions.nodeIdsToCompose`.
     *
     * Any fields which had cross-field information sent to them as part of this field composition
     * will be added to either `affectedBaseFields` or `affectedNewFields` in `crossFieldTable.pendingCompositions`.
     *
     * Any composed `FieldChange` which is invalidated by new cross-field information will be added to `crossFieldTable.invalidatedFields`.
     */
    composeFieldChanges(change1, change2, idAllocator, crossFieldTable, revisionMetadata) {
        const { fieldKind, changeHandler, change1: change1Normalized, change2: change2Normalized, } = this.normalizeFieldChanges(change1, change2, idAllocator, revisionMetadata);
        const manager = new ComposeManager(crossFieldTable, change1);
        const composedChange = changeHandler.rebaser.compose(change1Normalized, change2Normalized, (child1, child2) => {
            if (child1 !== undefined && child2 !== undefined) {
                (0, index_js_1.setInChangeAtomIdMap)(crossFieldTable.newToBaseNodeId, child2, child1);
                crossFieldTable.pendingCompositions.nodeIdsToCompose.push([child1, child2]);
            }
            return child1 ?? child2 ?? (0, index_js_2.fail)("Should not compose two undefined nodes");
        }, idAllocator, manager, revisionMetadata);
        const composedField = {
            fieldKind,
            change: (0, index_js_2.brand)(composedChange),
        };
        crossFieldTable.fieldToContext.set(change1, {
            change1: change1Normalized,
            change2: change2Normalized,
            composedChange: composedField,
        });
        crossFieldTable.newFieldToBaseField.set(change2, change1);
        return composedField;
    }
    composeNodesById(nodeChanges1, nodeChanges2, composedNodes, composedNodeToParent, nodeAliases, id1, id2, idAllocator, crossFieldTable, revisionMetadata) {
        const nodeChangeset1 = nodeChangeFromId(nodeChanges1, id1);
        const nodeChangeset2 = nodeChangeFromId(nodeChanges2, id2);
        const composedNodeChangeset = this.composeNodeChanges(nodeChangeset1, nodeChangeset2, idAllocator, crossFieldTable, revisionMetadata);
        (0, index_js_1.setInChangeAtomIdMap)(composedNodes, id1, composedNodeChangeset);
        if (!(0, index_js_1.areEqualChangeAtomIds)(id1, id2)) {
            (0, index_js_2.deleteFromNestedMap)(composedNodes, id2.revision, id2.localId);
            (0, index_js_2.deleteFromNestedMap)(composedNodeToParent, id2.revision, id2.localId);
            (0, index_js_1.setInChangeAtomIdMap)(nodeAliases, id2, id1);
            // We need to delete id1 to avoid forming a cycle in case id1 already had an alias.
            (0, index_js_2.deleteFromNestedMap)(nodeAliases, id1.revision, id1.localId);
        }
        crossFieldTable.composedNodes.add(composedNodeChangeset);
    }
    composeNodeChanges(change1, change2, genId, crossFieldTable, revisionMetadata) {
        const nodeExistsConstraint = change1.nodeExistsConstraint ?? change2.nodeExistsConstraint;
        const composedFieldChanges = this.composeFieldMaps(change1.fieldChanges, change2.fieldChanges, genId, crossFieldTable, revisionMetadata);
        const composedNodeChange = {};
        if (composedFieldChanges.size > 0) {
            composedNodeChange.fieldChanges = composedFieldChanges;
        }
        if (nodeExistsConstraint !== undefined) {
            composedNodeChange.nodeExistsConstraint = nodeExistsConstraint;
        }
        return composedNodeChange;
    }
    /**
     * @param change - The change to invert.
     * @param isRollback - Whether the inverted change is meant to rollback a change on a branch as is the case when
     * performing a sandwich rebase.
     */
    invert(change, isRollback) {
        // Rollback changesets destroy the nodes created by the change being rolled back.
        const destroys = isRollback
            ? invertBuilds(change.change.builds, change.revision)
            : undefined;
        // Destroys only occur in rollback changesets, which are never inverted.
        (0, internal_1.assert)(change.change.destroys === undefined, 0x89a /* Unexpected destroys in change to invert */);
        if ((change.change.constraintViolationCount ?? 0) > 0) {
            return makeModularChangeset(undefined, undefined, undefined, undefined, undefined, change.change.maxId, [], undefined, undefined, destroys);
        }
        const genId = (0, index_js_2.idAllocatorFromMaxId)(change.change.maxId ?? -1);
        const invertedNodeToParent = cloneNestedMap(change.change.nodeToParent);
        const crossFieldTable = {
            ...newCrossFieldTable(),
            originalFieldToContext: new Map(),
            invertedNodeToParent,
        };
        const { revInfos } = getRevInfoFromTaggedChanges([change]);
        const revisionMetadata = (0, index_js_1.revisionMetadataSourceFromInfo)(revInfos);
        const invertedFields = this.invertFieldMap(change.change.fieldChanges, undefined, isRollback, genId, crossFieldTable, revisionMetadata);
        const invertedNodes = new Map();
        (0, index_js_2.forEachInNestedMap)(change.change.nodeChanges, (nodeChangeset, revision, localId) => {
            (0, index_js_2.setInNestedMap)(invertedNodes, revision, localId, this.invertNodeChange(nodeChangeset, { revision, localId }, isRollback, genId, crossFieldTable, revisionMetadata));
        });
        if (crossFieldTable.invalidatedFields.size > 0) {
            const fieldsToUpdate = crossFieldTable.invalidatedFields;
            crossFieldTable.invalidatedFields = new Set();
            for (const fieldChange of fieldsToUpdate) {
                const originalFieldChange = fieldChange.change;
                const context = crossFieldTable.originalFieldToContext.get(fieldChange);
                (0, internal_1.assert)(context !== undefined, 0x851 /* Should have context for every invalidated field */);
                const { invertedField, fieldId } = context;
                const amendedChange = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).rebaser.invert(originalFieldChange, isRollback, genId, new InvertManager(crossFieldTable, fieldChange, fieldId), revisionMetadata);
                invertedField.change = (0, index_js_2.brand)(amendedChange);
            }
        }
        const crossFieldKeys = this.makeCrossFieldKeyTable(invertedFields, invertedNodes);
        return makeModularChangeset(invertedFields, invertedNodes, invertedNodeToParent, change.change.nodeAliases, crossFieldKeys, genId.getMaxId(), [], change.change.constraintViolationCount, undefined, destroys);
    }
    invertFieldMap(changes, parentId, isRollback, genId, crossFieldTable, revisionMetadata) {
        const invertedFields = new Map();
        for (const [field, fieldChange] of changes) {
            const fieldId = { nodeId: parentId, field };
            const manager = new InvertManager(crossFieldTable, fieldChange, fieldId);
            const invertedChange = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).rebaser.invert(fieldChange.change, isRollback, genId, manager, revisionMetadata);
            const invertedFieldChange = {
                ...fieldChange,
                change: (0, index_js_2.brand)(invertedChange),
            };
            invertedFields.set(field, invertedFieldChange);
            crossFieldTable.originalFieldToContext.set(fieldChange, {
                fieldId,
                invertedField: invertedFieldChange,
            });
        }
        return invertedFields;
    }
    invertNodeChange(change, id, isRollback, genId, crossFieldTable, revisionMetadata) {
        const inverse = {};
        if (change.fieldChanges !== undefined) {
            inverse.fieldChanges = this.invertFieldMap(change.fieldChanges, id, isRollback, genId, crossFieldTable, revisionMetadata);
        }
        return inverse;
    }
    rebase(taggedChange, over, revisionMetadata) {
        const change = taggedChange.change;
        const maxId = Math.max(change.maxId ?? -1, over.change.maxId ?? -1);
        const idState = { maxId };
        const genId = (0, index_js_2.idAllocatorFromState)(idState);
        const crossFieldTable = {
            ...newCrossFieldTable(),
            newChange: change,
            baseChange: over.change,
            baseFieldToContext: new Map(),
            baseToRebasedNodeId: new Map(),
            rebasedFields: new Set(),
            rebasedNodeToParent: cloneNestedMap(change.nodeToParent),
            rebasedCrossFieldKeys: (0, index_js_2.brand)(change.crossFieldKeys.clone()),
            nodeIdPairs: [],
            affectedBaseFields: newBTree(),
        };
        let constraintState = newConstraintState(change.constraintViolationCount ?? 0);
        const getBaseRevisions = () => revisionInfoFromTaggedChange(over).map((info) => info.revision);
        const rebaseMetadata = {
            ...revisionMetadata,
            getRevisionToRebase: () => taggedChange.revision,
            getBaseRevisions,
        };
        const rebasedNodes = cloneNestedMap(change.nodeChanges);
        const rebasedFields = this.rebaseIntersectingFields(crossFieldTable, rebasedNodes, genId, constraintState, rebaseMetadata);
        this.rebaseFieldsWithoutNewChanges(rebasedFields, rebasedNodes, crossFieldTable, genId, rebaseMetadata);
        if (crossFieldTable.invalidatedFields.size > 0) {
            const fieldsToUpdate = crossFieldTable.invalidatedFields;
            crossFieldTable.invalidatedFields = new Set();
            constraintState = newConstraintState(change.constraintViolationCount ?? 0);
            for (const field of fieldsToUpdate) {
                const context = crossFieldTable.baseFieldToContext.get(field);
                (0, internal_1.assert)(context !== undefined, 0x852 /* Every field should have a context */);
                const { changeHandler, change1: fieldChangeset, change2: baseChangeset, } = this.normalizeFieldChanges(context.newChange, context.baseChange, genId, revisionMetadata);
                const rebaseChild = (curr, base) => {
                    if (curr !== undefined) {
                        return curr;
                    }
                    if (base !== undefined) {
                        for (const id of context.baseNodeIds) {
                            if ((0, index_js_1.areEqualChangeAtomIds)(base, id)) {
                                return base;
                            }
                        }
                    }
                    return undefined;
                };
                context.rebasedChange.change = (0, index_js_2.brand)(changeHandler.rebaser.rebase(fieldChangeset, baseChangeset, rebaseChild, genId, new RebaseManager(crossFieldTable, field, context.fieldId), rebaseMetadata));
            }
        }
        this.updateConstraintsForFields(rebasedFields, fieldChangeHandler_js_1.NodeAttachState.Attached, constraintState, rebasedNodes);
        return makeModularChangeset(this.pruneFieldMap(rebasedFields, rebasedNodes), rebasedNodes, crossFieldTable.rebasedNodeToParent, change.nodeAliases, crossFieldTable.rebasedCrossFieldKeys, idState.maxId, change.revisions, constraintState.violationCount, change.builds, change.destroys, change.refreshers);
    }
    // This performs a first pass on all fields which have both new and base changes.
    // TODO: Can we also handle additional passes in this method?
    rebaseIntersectingFields(crossFieldTable, rebasedNodes, genId, constraintState, metadata) {
        const change = crossFieldTable.newChange;
        const baseChange = crossFieldTable.baseChange;
        const rebasedFields = this.rebaseFieldMap(change.fieldChanges, baseChange.fieldChanges, undefined, genId, crossFieldTable, metadata);
        // This loop processes all fields which have both base and new changes.
        // Note that the call to `rebaseNodeChange` can add entries to `crossFieldTable.nodeIdPairs`.
        for (const [newId, baseId, _attachState] of crossFieldTable.nodeIdPairs) {
            const rebasedNode = this.rebaseNodeChange(newId, baseId, genId, crossFieldTable, metadata, constraintState);
            (0, index_js_1.setInChangeAtomIdMap)(rebasedNodes, newId, rebasedNode);
        }
        return rebasedFields;
    }
    // This processes fields which have no new changes but have been invalidated by another field.
    rebaseFieldsWithoutNewChanges(rebasedFields, rebasedNodes, crossFieldTable, genId, metadata) {
        const baseChange = crossFieldTable.baseChange;
        for (const [revision, localId, fieldKey] of crossFieldTable.affectedBaseFields.keys()) {
            const baseNodeId = localId !== undefined
                ? normalizeNodeId({ revision, localId }, baseChange.nodeAliases)
                : undefined;
            const baseFieldChange = fieldMapFromNodeId(baseChange.fieldChanges, baseChange.nodeChanges, baseNodeId).get(fieldKey);
            (0, internal_1.assert)(baseFieldChange !== undefined, 0x9c2 /* Cross field key registered for empty field */);
            if (crossFieldTable.baseFieldToContext.has(baseFieldChange)) {
                // This field has already been processed because there were changes to rebase.
                continue;
            }
            // This field has no changes in the new changeset, otherwise it would have been added to
            // `crossFieldTable.baseFieldToContext` when processing fields with both base and new changes.
            const rebaseChild = (child, baseChild, stateChange) => {
                (0, internal_1.assert)(child === undefined, 0x9c3 /* There should be no new changes in this field */);
                return undefined;
            };
            const handler = getChangeHandler(this.fieldKinds, baseFieldChange.fieldKind);
            const fieldChange = {
                ...baseFieldChange,
                change: (0, index_js_2.brand)(handler.createEmpty()),
            };
            const rebasedNodeId = baseNodeId !== undefined
                ? rebasedNodeIdFromBaseNodeId(crossFieldTable, baseNodeId)
                : undefined;
            const fieldId = { nodeId: rebasedNodeId, field: fieldKey };
            const rebasedField = handler.rebaser.rebase(fieldChange.change, baseFieldChange.change, rebaseChild, genId, new RebaseManager(crossFieldTable, baseFieldChange, fieldId), metadata);
            const rebasedFieldChange = {
                ...baseFieldChange,
                change: (0, index_js_2.brand)(rebasedField),
            };
            // TODO: Deduplicate
            crossFieldTable.baseFieldToContext.set(baseFieldChange, {
                newChange: fieldChange,
                baseChange: baseFieldChange,
                rebasedChange: rebasedFieldChange,
                fieldId,
                baseNodeIds: [],
            });
            crossFieldTable.rebasedFields.add(rebasedFieldChange);
            this.attachRebasedField(rebasedFields, rebasedNodes, crossFieldTable, rebasedFieldChange, fieldId, genId, metadata);
        }
    }
    attachRebasedField(rebasedFields, rebasedNodes, table, rebasedField, { nodeId, field: fieldKey }, idAllocator, metadata) {
        if (nodeId === undefined) {
            rebasedFields.set(fieldKey, rebasedField);
            return;
        }
        const rebasedNode = (0, index_js_1.getFromChangeAtomIdMap)(rebasedNodes, nodeId);
        if (rebasedNode !== undefined) {
            if (rebasedNode.fieldChanges === undefined) {
                rebasedNode.fieldChanges = new Map([[fieldKey, rebasedField]]);
                return;
            }
            (0, internal_1.assert)(!rebasedNode.fieldChanges.has(fieldKey), 0x9c4 /* Expected an empty field */);
            rebasedNode.fieldChanges.set(fieldKey, rebasedField);
            return;
        }
        const newNode = {
            fieldChanges: new Map([[fieldKey, rebasedField]]),
        };
        (0, index_js_1.setInChangeAtomIdMap)(rebasedNodes, nodeId, newNode);
        (0, index_js_1.setInChangeAtomIdMap)(table.baseToRebasedNodeId, nodeId, nodeId);
        const parentFieldId = getParentFieldId(table.baseChange, nodeId);
        this.attachRebasedNode(rebasedFields, rebasedNodes, table, nodeId, parentFieldId, idAllocator, metadata);
    }
    attachRebasedNode(rebasedFields, rebasedNodes, table, baseNodeId, parentFieldIdBase, idAllocator, metadata) {
        const baseFieldChange = fieldChangeFromId(table.baseChange.fieldChanges, table.baseChange.nodeChanges, parentFieldIdBase);
        const rebasedFieldId = rebasedFieldIdFromBaseId(table, parentFieldIdBase);
        (0, index_js_1.setInChangeAtomIdMap)(table.rebasedNodeToParent, baseNodeId, rebasedFieldId);
        const context = table.baseFieldToContext.get(baseFieldChange);
        if (context !== undefined) {
            // We've already processed this field.
            // The new child node can be attached when processing invalidated fields.
            context.baseNodeIds.push(baseNodeId);
            table.invalidatedFields.add(baseFieldChange);
            return;
        }
        const handler = getChangeHandler(this.fieldKinds, baseFieldChange.fieldKind);
        const fieldChange = {
            ...baseFieldChange,
            change: (0, index_js_2.brand)(handler.createEmpty()),
        };
        const rebasedChangeset = handler.rebaser.rebase(handler.createEmpty(), baseFieldChange.change, (_idNew, idBase) => idBase !== undefined && (0, index_js_1.areEqualChangeAtomIds)(idBase, baseNodeId)
            ? baseNodeId
            : undefined, idAllocator, new RebaseManager(table, baseFieldChange, rebasedFieldId), metadata);
        const rebasedField = { ...baseFieldChange, change: (0, index_js_2.brand)(rebasedChangeset) };
        table.rebasedFields.add(rebasedField);
        table.baseFieldToContext.set(baseFieldChange, {
            newChange: fieldChange,
            baseChange: baseFieldChange,
            rebasedChange: rebasedField,
            fieldId: rebasedFieldId,
            baseNodeIds: [],
        });
        this.attachRebasedField(rebasedFields, rebasedNodes, table, rebasedField, rebasedFieldId, idAllocator, metadata);
    }
    rebaseFieldMap(change, over, parentId, genId, crossFieldTable, revisionMetadata) {
        const rebasedFields = new Map();
        const rebaseChild = (child, baseChild, stateChange) => {
            if (child !== undefined && baseChild !== undefined) {
                crossFieldTable.nodeIdPairs.push([child, baseChild, stateChange]);
            }
            return child;
        };
        for (const [field, fieldChange] of change) {
            const fieldId = { nodeId: parentId, field };
            const baseChange = over.get(field);
            if (baseChange === undefined) {
                rebasedFields.set(field, fieldChange);
                continue;
            }
            const { fieldKind, changeHandler, change1: fieldChangeset, change2: baseChangeset, } = this.normalizeFieldChanges(fieldChange, baseChange, genId, revisionMetadata);
            const manager = new RebaseManager(crossFieldTable, baseChange, fieldId);
            const rebasedField = changeHandler.rebaser.rebase(fieldChangeset, baseChangeset, rebaseChild, genId, manager, revisionMetadata);
            const rebasedFieldChange = {
                fieldKind,
                change: (0, index_js_2.brand)(rebasedField),
            };
            rebasedFields.set(field, rebasedFieldChange);
            crossFieldTable.baseFieldToContext.set(baseChange, {
                baseChange,
                newChange: fieldChange,
                rebasedChange: rebasedFieldChange,
                fieldId,
                baseNodeIds: [],
            });
            crossFieldTable.rebasedFields.add(rebasedFieldChange);
        }
        return rebasedFields;
    }
    rebaseNodeChange(newId, baseId, genId, crossFieldTable, revisionMetadata, constraintState) {
        const change = nodeChangeFromId(crossFieldTable.newChange.nodeChanges, newId);
        const over = nodeChangeFromId(crossFieldTable.baseChange.nodeChanges, baseId);
        const baseMap = over?.fieldChanges ?? new Map();
        const fieldChanges = change.fieldChanges !== undefined && over.fieldChanges !== undefined
            ? this.rebaseFieldMap(change?.fieldChanges ?? new Map(), baseMap, newId, genId, crossFieldTable, revisionMetadata)
            : change.fieldChanges;
        const rebasedChange = {};
        if (fieldChanges !== undefined && fieldChanges.size > 0) {
            rebasedChange.fieldChanges = fieldChanges;
        }
        if (change?.nodeExistsConstraint !== undefined) {
            rebasedChange.nodeExistsConstraint = change.nodeExistsConstraint;
        }
        (0, index_js_1.setInChangeAtomIdMap)(crossFieldTable.baseToRebasedNodeId, baseId, newId);
        return rebasedChange;
    }
    updateConstraintsForFields(fields, parentAttachState, constraintState, nodes) {
        for (const field of fields.values()) {
            const handler = getChangeHandler(this.fieldKinds, field.fieldKind);
            for (const [nodeId, index] of handler.getNestedChanges(field.change)) {
                const isDetached = index === undefined;
                const attachState = parentAttachState === fieldChangeHandler_js_1.NodeAttachState.Detached || isDetached
                    ? fieldChangeHandler_js_1.NodeAttachState.Detached
                    : fieldChangeHandler_js_1.NodeAttachState.Attached;
                this.updateConstraintsForNode(nodeId, attachState, constraintState, nodes);
            }
        }
    }
    updateConstraintsForNode(nodeId, attachState, constraintState, nodes) {
        const node = (0, index_js_2.tryGetFromNestedMap)(nodes, nodeId.revision, nodeId.localId) ?? (0, index_js_2.fail)("Unknown node ID");
        if (node.nodeExistsConstraint !== undefined) {
            const isNowViolated = attachState === fieldChangeHandler_js_1.NodeAttachState.Detached;
            if (node.nodeExistsConstraint.violated !== isNowViolated) {
                node.nodeExistsConstraint = {
                    ...node.nodeExistsConstraint,
                    violated: isNowViolated,
                };
                constraintState.violationCount += isNowViolated ? 1 : -1;
            }
        }
        if (node.fieldChanges !== undefined) {
            this.updateConstraintsForFields(node.fieldChanges, attachState, constraintState, nodes);
        }
    }
    pruneFieldMap(changeset, nodeMap) {
        if (changeset === undefined) {
            return undefined;
        }
        const prunedChangeset = new Map();
        for (const [field, fieldChange] of changeset) {
            const handler = getChangeHandler(this.fieldKinds, fieldChange.fieldKind);
            const prunedFieldChangeset = handler.rebaser.prune(fieldChange.change, (nodeId) => this.pruneNodeChange(nodeId, nodeMap));
            if (!handler.isEmpty(prunedFieldChangeset)) {
                prunedChangeset.set(field, { ...fieldChange, change: (0, index_js_2.brand)(prunedFieldChangeset) });
            }
        }
        return prunedChangeset.size > 0 ? prunedChangeset : undefined;
    }
    pruneNodeChange(nodeId, nodeMap) {
        const changeset = nodeChangeFromId(nodeMap, nodeId);
        const prunedFields = changeset.fieldChanges !== undefined
            ? this.pruneFieldMap(changeset.fieldChanges, nodeMap)
            : undefined;
        const prunedChange = { ...changeset, fieldChanges: prunedFields };
        if (prunedChange.fieldChanges === undefined) {
            delete prunedChange.fieldChanges;
        }
        if (isEmptyNodeChangeset(prunedChange)) {
            (0, index_js_2.deleteFromNestedMap)(nodeMap, nodeId.revision, nodeId.localId);
            return undefined;
        }
        else {
            (0, index_js_1.setInChangeAtomIdMap)(nodeMap, nodeId, prunedChange);
            return nodeId;
        }
    }
    changeRevision(change, newRevision, rollbackOf) {
        const oldRevisions = new Set(change.revisions === undefined
            ? [undefined]
            : change.revisions.map((revInfo) => revInfo.revision));
        const updatedFields = this.replaceFieldMapRevisions(change.fieldChanges, oldRevisions, newRevision);
        const updatedNodes = (0, index_js_2.nestedMapFromFlatList)((0, index_js_2.nestedMapToFlatList)(change.nodeChanges).map(([revision, id, nodeChangeset]) => [
            replaceRevision(revision, oldRevisions, newRevision),
            id,
            this.replaceNodeChangesetRevisions(nodeChangeset, oldRevisions, newRevision),
        ]));
        const updatedNodeToParent = (0, index_js_2.nestedMapFromFlatList)((0, index_js_2.nestedMapToFlatList)(change.nodeToParent).map(([revision, id, fieldId]) => [
            replaceRevision(revision, oldRevisions, newRevision),
            id,
            replaceFieldIdRevision(normalizeFieldId(fieldId, change.nodeAliases), oldRevisions, newRevision),
        ]));
        const updated = {
            ...change,
            fieldChanges: updatedFields,
            nodeChanges: updatedNodes,
            nodeToParent: updatedNodeToParent,
            // We've updated all references to old node IDs, so we no longer need an alias table.
            nodeAliases: new Map(),
            crossFieldKeys: replaceCrossFieldKeyTableRevisions(change.crossFieldKeys, oldRevisions, newRevision, change.nodeAliases),
        };
        if (change.builds !== undefined) {
            updated.builds = replaceIdMapRevisions(change.builds, oldRevisions, newRevision);
        }
        if (change.destroys !== undefined) {
            updated.destroys = replaceIdMapRevisions(change.destroys, oldRevisions, newRevision);
        }
        if (change.refreshers !== undefined) {
            updated.refreshers = replaceIdMapRevisions(change.refreshers, oldRevisions, newRevision);
        }
        if (newRevision !== undefined) {
            const revInfo = { revision: newRevision };
            if (rollbackOf !== undefined) {
                revInfo.rollbackOf = rollbackOf;
            }
            updated.revisions = [revInfo];
        }
        else {
            delete updated.revisions;
        }
        return updated;
    }
    replaceNodeChangesetRevisions(nodeChangeset, oldRevisions, newRevision) {
        const updated = { ...nodeChangeset };
        if (nodeChangeset.fieldChanges !== undefined) {
            updated.fieldChanges = this.replaceFieldMapRevisions(nodeChangeset.fieldChanges, oldRevisions, newRevision);
        }
        return updated;
    }
    replaceFieldMapRevisions(fields, oldRevisions, newRevision) {
        const updatedFields = new Map();
        for (const [field, fieldChange] of fields) {
            const updatedFieldChange = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).rebaser.replaceRevisions(fieldChange.change, oldRevisions, newRevision);
            updatedFields.set(field, { ...fieldChange, change: (0, index_js_2.brand)(updatedFieldChange) });
        }
        return updatedFields;
    }
    makeCrossFieldKeyTable(fields, nodes) {
        const keys = newCrossFieldKeyTable();
        this.populateCrossFieldKeyTableForFieldMap(keys, fields, undefined);
        (0, index_js_2.forEachInNestedMap)(nodes, (node, revision, localId) => {
            if (node.fieldChanges !== undefined) {
                this.populateCrossFieldKeyTableForFieldMap(keys, node.fieldChanges, {
                    revision,
                    localId,
                });
            }
        });
        return keys;
    }
    populateCrossFieldKeyTableForFieldMap(table, fields, parent) {
        for (const [fieldKey, fieldChange] of fields) {
            const keys = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).getCrossFieldKeys(fieldChange.change);
            for (const key of keys) {
                table.set(key, { nodeId: parent, field: fieldKey });
            }
        }
    }
    buildEditor(changeReceiver) {
        return new ModularEditBuilder(this, this.fieldKinds, changeReceiver);
    }
    createEmptyFieldChange(fieldKind) {
        const emptyChange = getChangeHandler(this.fieldKinds, fieldKind).createEmpty();
        return { fieldKind, change: (0, index_js_2.brand)(emptyChange) };
    }
}
exports.ModularChangeFamily = ModularChangeFamily;
ModularChangeFamily.emptyChange = makeModularChangeset();
function replaceCrossFieldKeyTableRevisions(table, oldRevisions, newRevision, nodeAliases) {
    const updated = newBTree();
    table.forEachPair(([target, revision, id, count], field) => {
        const updatedKey = [
            target,
            replaceRevision(revision, oldRevisions, newRevision),
            id,
            count,
        ];
        const normalizedFieldId = normalizeFieldId(field, nodeAliases);
        const updatedNodeId = normalizedFieldId.nodeId !== undefined
            ? (0, index_js_1.replaceAtomRevisions)(normalizedFieldId.nodeId, oldRevisions, newRevision)
            : undefined;
        const updatedValue = {
            ...normalizedFieldId,
            nodeId: updatedNodeId,
        };
        updated.set(updatedKey, updatedValue);
    });
    return updated;
}
function replaceRevision(revision, oldRevisions, newRevision) {
    return oldRevisions.has(revision) ? newRevision : revision;
}
function replaceIdMapRevisions(map, oldRevisions, newRevision) {
    return (0, index_js_2.nestedMapFromFlatList)((0, index_js_2.nestedMapToFlatList)(map).map(([revision, id, value]) => [
        replaceRevision(revision, oldRevisions, newRevision),
        id,
        value,
    ]));
}
function composeBuildsDestroysAndRefreshers(changes) {
    const allBuilds = new Map();
    const allDestroys = new Map();
    const allRefreshers = new Map();
    for (const taggedChange of changes) {
        const revision = revisionFromTaggedChange(taggedChange);
        const change = taggedChange.change;
        if (change.builds) {
            for (const [revisionKey, innerMap] of change.builds) {
                const setRevisionKey = revisionKey ?? revision;
                const innerDstMap = (0, index_js_2.getOrAddInMap)(allBuilds, setRevisionKey, new Map());
                for (const [id, chunk] of innerMap) {
                    // Check for duplicate builds and prefer earlier ones.
                    // This can happen in compositions of commits that needed to include detached tree refreshers (e.g., undos):
                    // In that case, it's possible for the refreshers to contain different trees because the latter
                    // refresher may already reflect the changes made by the commit that includes the earlier
                    // refresher. This composition includes the changes made by the commit that includes the
                    // earlier refresher, so we need to include the build for the earlier refresher, otherwise
                    // the produced changeset will build a tree one which those changes have already been applied
                    // and also try to apply the changes again, effectively applying them twice.
                    // Note that it would in principle be possible to adopt the later build and exclude from the
                    // composition all the changes already reflected on the tree, but that is not something we
                    // care to support at this time.
                    if (!innerDstMap.has(id)) {
                        // Check for earlier destroys that this build might cancel-out with.
                        const destroyCount = (0, index_js_2.tryGetFromNestedMap)(allDestroys, setRevisionKey, id);
                        if (destroyCount === undefined) {
                            innerDstMap.set(id, chunk);
                        }
                        else {
                            (0, internal_1.assert)(destroyCount === chunk.topLevelLength, 0x89b /* Expected build and destroy to have the same length */);
                            (0, index_js_2.deleteFromNestedMap)(allDestroys, setRevisionKey, id);
                        }
                    }
                }
                if (innerDstMap.size === 0) {
                    allBuilds.delete(setRevisionKey);
                }
            }
        }
        if (change.destroys !== undefined) {
            for (const [revisionKey, innerMap] of change.destroys) {
                const setRevisionKey = revisionKey ?? revision;
                const innerDstMap = (0, index_js_2.getOrAddInMap)(allDestroys, setRevisionKey, new Map());
                for (const [id, count] of innerMap) {
                    // Check for earlier builds that this destroy might cancel-out with.
                    const chunk = (0, index_js_2.tryGetFromNestedMap)(allBuilds, setRevisionKey, id);
                    if (chunk === undefined) {
                        innerDstMap.set(id, count);
                    }
                    else {
                        (0, internal_1.assert)(count === chunk.topLevelLength, 0x89c /* Expected build and destroy to have the same length */);
                        (0, index_js_2.deleteFromNestedMap)(allBuilds, setRevisionKey, id);
                    }
                }
                if (innerDstMap.size === 0) {
                    allDestroys.delete(setRevisionKey);
                }
            }
        }
        // add all refreshers while preferring earlier ones
        if (change.refreshers) {
            (0, index_js_2.populateNestedMap)(change.refreshers, allRefreshers, false);
        }
    }
    return { allBuilds, allDestroys, allRefreshers };
}
function invertBuilds(builds, fallbackRevision) {
    if (builds !== undefined) {
        const destroys = new Map();
        for (const [revision, innerBuildMap] of builds) {
            const initializedRevision = revision ?? fallbackRevision;
            const innerDestroyMap = new Map();
            for (const [id, chunk] of innerBuildMap) {
                innerDestroyMap.set(id, chunk.topLevelLength);
            }
            destroys.set(initializedRevision, innerDestroyMap);
        }
        return destroys;
    }
    return undefined;
}
/**
 * Returns the set of removed roots that should be in memory for the given change to be applied.
 * A removed root is relevant if any of the following is true:
 * - It is being inserted
 * - It is being restored
 * - It is being edited
 * - The ID it is associated with is being changed
 *
 * May be conservative by returning more removed roots than strictly necessary.
 *
 * Will never return IDs for non-root trees, even if they are removed.
 *
 * @param change - The change to be applied.
 * @param fieldKinds - The field kinds to delegate to.
 */
function* relevantRemovedRoots(change, fieldKinds) {
    yield* relevantRemovedRootsFromFields(change.fieldChanges, change.nodeChanges, fieldKinds);
}
exports.relevantRemovedRoots = relevantRemovedRoots;
function* relevantRemovedRootsFromFields(change, nodeChanges, fieldKinds) {
    for (const [_, fieldChange] of change) {
        const handler = getChangeHandler(fieldKinds, fieldChange.fieldKind);
        const delegate = function* (node) {
            const nodeChangeset = nodeChangeFromId(nodeChanges, node);
            if (nodeChangeset.fieldChanges !== undefined) {
                yield* relevantRemovedRootsFromFields(nodeChangeset.fieldChanges, nodeChanges, fieldKinds);
            }
        };
        yield* handler.relevantRemovedRoots(fieldChange.change, delegate);
    }
}
/**
 * Adds any refreshers missing from the provided change that are relevant to the change and
 * removes any refreshers from the provided change that are not relevant to the change.
 *
 * @param change - The change that possibly has missing or superfluous refreshers. Not mutated by this function.
 * @param getDetachedNode - The function to retrieve a tree chunk from the corresponding detached node id.
 * @param removedRoots - The set of removed roots that should be in memory for the given change to be applied.
 * Can be retrieved by calling {@link relevantRemovedRoots}.
 * @param requireRefreshers - when true, this function enforces that all relevant removed roots have a
 * corresponding build or refresher.
 */
function updateRefreshers(change, getDetachedNode, removedRoots, requireRefreshers = true) {
    const refreshers = new Map();
    const chunkLengths = new Map();
    if (change.builds !== undefined) {
        for (const [major, buildsMap] of change.builds) {
            const lengthTree = (0, index_js_2.getOrAddInMap)(chunkLengths, major, new sorted_btree_es6_1.BTree());
            for (const [id, chunk] of buildsMap) {
                lengthTree.set(id, chunk.topLevelLength);
            }
        }
    }
    for (const root of removedRoots) {
        if (change.builds !== undefined) {
            const lengthTree = chunkLengths.get(root.major);
            if (lengthTree !== undefined) {
                const lengthPair = lengthTree.getPairOrNextLower(root.minor);
                if (lengthPair !== undefined) {
                    const [firstMinor, length] = lengthPair;
                    // if the root minor is within the length of the minor of the retrieved pair
                    // then there's no need to check for the detached node
                    if (root.minor < firstMinor + length) {
                        continue;
                    }
                }
            }
        }
        const node = getDetachedNode(root);
        if (node === undefined) {
            (0, internal_1.assert)(!requireRefreshers, 0x8cd /* detached node should exist */);
        }
        else {
            (0, index_js_2.setInNestedMap)(refreshers, root.major, root.minor, node);
        }
    }
    const { fieldChanges, nodeChanges, maxId, revisions, constraintViolationCount, builds, destroys, } = change;
    return makeModularChangeset(fieldChanges, nodeChanges, change.nodeToParent, change.nodeAliases, change.crossFieldKeys, maxId, revisions, constraintViolationCount, builds, destroys, refreshers);
}
exports.updateRefreshers = updateRefreshers;
/**
 * @param change - The change to convert into a delta.
 * @param fieldKinds - The field kinds to delegate to.
 */
function intoDelta(taggedChange, fieldKinds) {
    const change = taggedChange.change;
    const idAllocator = memoizedIdRangeAllocator_js_1.MemoizedIdRangeAllocator.fromNextId();
    const rootDelta = {};
    if ((change.constraintViolationCount ?? 0) === 0) {
        // If there are no constraint violations, then tree changes apply.
        const fieldDeltas = intoDeltaImpl(change.fieldChanges, change.nodeChanges, idAllocator, fieldKinds);
        if (fieldDeltas.size > 0) {
            rootDelta.fields = fieldDeltas;
        }
    }
    // Constraint violations should not prevent nodes from being built
    if (change.builds && change.builds.size > 0) {
        rootDelta.build = copyDetachedNodes(change.builds);
    }
    if (change.destroys !== undefined && change.destroys.size > 0) {
        const destroys = [];
        (0, index_js_2.forEachInNestedMap)(change.destroys, (count, major, minor) => {
            destroys.push({
                id: (0, index_js_1.makeDetachedNodeId)(major, minor),
                count,
            });
        });
        rootDelta.destroy = destroys;
    }
    if (change.refreshers && change.refreshers.size > 0) {
        rootDelta.refreshers = copyDetachedNodes(change.refreshers);
    }
    return rootDelta;
}
exports.intoDelta = intoDelta;
function copyDetachedNodes(detachedNodes) {
    const copiedDetachedNodes = [];
    (0, index_js_2.forEachInNestedMap)(detachedNodes, (chunk, major, minor) => {
        if (chunk.topLevelLength > 0) {
            const trees = (0, index_js_1.mapCursorField)(chunk.cursor(), (c) => (0, mapTreeCursor_js_1.cursorForMapTreeNode)((0, mapTreeCursor_js_1.mapTreeFromCursor)(c)));
            copiedDetachedNodes.push({
                id: (0, index_js_1.makeDetachedNodeId)(major, minor),
                trees,
            });
        }
    });
    return copiedDetachedNodes.length > 0 ? copiedDetachedNodes : undefined;
}
/**
 * @param change - The change to convert into a delta.
 */
function intoDeltaImpl(change, nodeChanges, idAllocator, fieldKinds) {
    const delta = new Map();
    for (const [field, fieldChange] of change) {
        const deltaField = getChangeHandler(fieldKinds, fieldChange.fieldKind).intoDelta(fieldChange.change, (childChange) => {
            const nodeChange = nodeChangeFromId(nodeChanges, childChange);
            return deltaFromNodeChange(nodeChange, nodeChanges, idAllocator, fieldKinds);
        }, idAllocator);
        if (!(0, index_js_1.isEmptyFieldChanges)(deltaField)) {
            delta.set(field, deltaField);
        }
    }
    return delta;
}
function deltaFromNodeChange(change, nodeChanges, idAllocator, fieldKinds) {
    if (change.fieldChanges !== undefined) {
        return intoDeltaImpl(change.fieldChanges, nodeChanges, idAllocator, fieldKinds);
    }
    // TODO: update the API to allow undefined to be returned here
    return new Map();
}
/**
 * @internal
 * @param revInfos - This should describe the revision being rebased and all revisions in the rebase path,
 * even if not part of the current base changeset.
 * For example, when rebasing change B from a local branch [A, B, C] over a branch [X, Y], the `revInfos` must include
 * the changes [A⁻¹ X, Y, A, B] for each rebase step of B.
 * @param revisionToRebase - The revision of the changeset which is being rebased.
 * @param baseRevisions - The set of revisions in the changeset being rebased over.
 * For example, when rebasing change B from a local branch [A, B, C] over a branch [X, Y], the `baseRevisions` must include
 * revisions [A⁻¹ X, Y, A] if rebasing over the composition of all those changes, or
 * revision [A⁻¹] for the first rebase, then [X], etc. if rebasing over edits individually.
 * @returns - RebaseRevisionMetadata to be passed to `FieldChangeRebaser.rebase`*
 */
function rebaseRevisionMetadataFromInfo(revInfos, revisionToRebase, baseRevisions) {
    const filteredRevisions = [];
    for (const revision of baseRevisions) {
        if (revision !== undefined) {
            filteredRevisions.push(revision);
        }
    }
    const getBaseRevisions = () => filteredRevisions;
    return {
        ...(0, index_js_1.revisionMetadataSourceFromInfo)(revInfos),
        getRevisionToRebase: () => revisionToRebase,
        getBaseRevisions,
    };
}
exports.rebaseRevisionMetadataFromInfo = rebaseRevisionMetadataFromInfo;
function isEmptyNodeChangeset(change) {
    return change.fieldChanges === undefined && change.nodeExistsConstraint === undefined;
}
function getFieldKind(fieldKinds, kind) {
    if (kind === genericFieldKind_js_1.genericFieldKind.identifier) {
        return genericFieldKind_js_1.genericFieldKind;
    }
    const fieldKind = fieldKinds.get(kind);
    (0, internal_1.assert)(fieldKind !== undefined, 0x3ad /* Unknown field kind */);
    return (0, fieldKindWithEditor_js_1.withEditor)(fieldKind);
}
exports.getFieldKind = getFieldKind;
function getChangeHandler(fieldKinds, kind) {
    return getFieldKind(fieldKinds, kind).changeHandler;
}
exports.getChangeHandler = getChangeHandler;
function newComposeTable(baseChange, newChange) {
    return {
        ...newCrossFieldTable(),
        baseChange,
        newChange,
        fieldToContext: new Map(),
        newFieldToBaseField: new Map(),
        newToBaseNodeId: new Map(),
        composedNodes: new Set(),
        pendingCompositions: {
            nodeIdsToCompose: [],
            affectedBaseFields: newBTree(),
            affectedNewFields: newBTree(),
        },
    };
}
function newCrossFieldTable() {
    return {
        srcTable: new Map(),
        dstTable: new Map(),
        srcDependents: new Map(),
        dstDependents: new Map(),
        invalidatedFields: new Set(),
    };
}
function newConstraintState(violationCount) {
    return {
        violationCount,
    };
}
class CrossFieldManagerI {
    constructor(crossFieldTable, currentFieldKey, allowInval = true) {
        this.crossFieldTable = crossFieldTable;
        this.currentFieldKey = currentFieldKey;
        this.allowInval = allowInval;
    }
    set(target, revision, id, count, newValue, invalidateDependents) {
        if (invalidateDependents && this.allowInval) {
            const lastChangedId = id + count - 1;
            let firstId = id;
            while (firstId <= lastChangedId) {
                const dependentEntry = (0, crossFieldQueries_js_1.getFirstFromCrossFieldMap)(this.getDependents(target), revision, firstId, lastChangedId - firstId + 1);
                if (dependentEntry.value !== undefined) {
                    this.crossFieldTable.invalidatedFields.add(dependentEntry.value);
                }
                firstId = (0, index_js_2.brand)(firstId + dependentEntry.length);
            }
        }
        (0, crossFieldQueries_js_1.setInCrossFieldMap)(this.getMap(target), revision, id, count, newValue);
    }
    get(target, revision, id, count, addDependency) {
        if (addDependency) {
            // We assume that if there is already an entry for this ID it is because
            // a field handler has called compose on the same node multiple times.
            // In this case we only want to update the latest version, so we overwrite the dependency.
            (0, crossFieldQueries_js_1.setInCrossFieldMap)(this.getDependents(target), revision, id, count, this.currentFieldKey);
        }
        return (0, crossFieldQueries_js_1.getFirstFromCrossFieldMap)(this.getMap(target), revision, id, count);
    }
    getMap(target) {
        return target === crossFieldQueries_js_1.CrossFieldTarget.Source
            ? this.crossFieldTable.srcTable
            : this.crossFieldTable.dstTable;
    }
    getDependents(target) {
        return target === crossFieldQueries_js_1.CrossFieldTarget.Source
            ? this.crossFieldTable.srcDependents
            : this.crossFieldTable.dstDependents;
    }
}
class InvertManager extends CrossFieldManagerI {
    constructor(table, field, fieldId, allowInval = true) {
        super(table, field, allowInval);
        this.fieldId = fieldId;
    }
    onMoveIn(id) {
        (0, index_js_1.setInChangeAtomIdMap)(this.table.invertedNodeToParent, id, this.fieldId);
    }
    moveKey(target, revision, id, count) {
        (0, internal_1.assert)(false, 0x9c5 /* Keys should not be moved manually during invert */);
    }
    get table() {
        return this.crossFieldTable;
    }
}
class RebaseManager extends CrossFieldManagerI {
    constructor(table, currentField, fieldId, allowInval = true) {
        super(table, currentField, allowInval);
        this.fieldId = fieldId;
    }
    set(target, revision, id, count, newValue, invalidateDependents) {
        if (invalidateDependents && this.allowInval) {
            const newFieldIds = getFieldsForCrossFieldKey(this.table.newChange, [
                target,
                revision,
                id,
                count,
            ]);
            (0, internal_1.assert)(newFieldIds.length === 0, 0x9c6 /* TODO: Modifying a cross-field key from the new changeset is currently unsupported */);
            const baseFieldIds = getFieldsForCrossFieldKey(this.table.baseChange, [
                target,
                revision,
                id,
                count,
            ]);
            (0, internal_1.assert)(baseFieldIds.length > 0, 0x9c7 /* Cross field key not registered in base or new change */);
            for (const baseFieldId of baseFieldIds) {
                this.table.affectedBaseFields.set([baseFieldId.nodeId?.revision, baseFieldId.nodeId?.localId, baseFieldId.field], true);
            }
        }
        super.set(target, revision, id, count, newValue, invalidateDependents);
    }
    onMoveIn(id) {
        (0, index_js_1.setInChangeAtomIdMap)(this.table.rebasedNodeToParent, id, this.fieldId);
    }
    moveKey(target, revision, id, count) {
        setInCrossFieldKeyTable(this.table.rebasedCrossFieldKeys, target, revision, id, count, this.fieldId);
    }
    get table() {
        return this.crossFieldTable;
    }
}
// TODO: Deduplicate this with RebaseTable
class ComposeManager extends CrossFieldManagerI {
    constructor(table, currentField, allowInval = true) {
        super(table, currentField, allowInval);
    }
    set(target, revision, id, count, newValue, invalidateDependents) {
        if (invalidateDependents && this.allowInval) {
            const newFieldIds = getFieldsForCrossFieldKey(this.table.newChange, [
                target,
                revision,
                id,
                count,
            ]);
            if (newFieldIds.length > 0) {
                for (const newFieldId of newFieldIds) {
                    this.table.pendingCompositions.affectedNewFields.set([newFieldId.nodeId?.revision, newFieldId.nodeId?.localId, newFieldId.field], true);
                }
            }
            else {
                const baseFieldIds = getFieldsForCrossFieldKey(this.table.baseChange, [
                    target,
                    revision,
                    id,
                    count,
                ]);
                (0, internal_1.assert)(baseFieldIds.length > 0, 0x9c8 /* Cross field key not registered in base or new change */);
                for (const baseFieldId of baseFieldIds) {
                    this.table.pendingCompositions.affectedBaseFields.set([baseFieldId.nodeId?.revision, baseFieldId.nodeId?.localId, baseFieldId.field], true);
                }
            }
        }
        super.set(target, revision, id, count, newValue, invalidateDependents);
    }
    onMoveIn(id) {
        throw new Error("Method not implemented.");
    }
    moveKey(target, revision, id, count) {
        throw new Error("Method not implemented.");
    }
    get table() {
        return this.crossFieldTable;
    }
}
function makeModularChangeset(fieldChanges = undefined, nodeChanges = undefined, nodeToParent = undefined, nodeAliases = undefined, crossFieldKeys = undefined, maxId = -1, revisions = undefined, constraintViolationCount = undefined, builds, destroys, refreshers) {
    const changeset = {
        fieldChanges: fieldChanges ?? new Map(),
        nodeChanges: nodeChanges ?? new Map(),
        nodeToParent: nodeToParent ?? new Map(),
        nodeAliases: nodeAliases ?? new Map(),
        crossFieldKeys: crossFieldKeys ?? newCrossFieldKeyTable(),
    };
    if (revisions !== undefined && revisions.length > 0) {
        changeset.revisions = revisions;
    }
    if (maxId >= 0) {
        changeset.maxId = (0, index_js_2.brand)(maxId);
    }
    if (constraintViolationCount !== undefined && constraintViolationCount > 0) {
        changeset.constraintViolationCount = constraintViolationCount;
    }
    if (builds !== undefined && builds.size > 0) {
        changeset.builds = builds;
    }
    if (destroys !== undefined && destroys.size > 0) {
        changeset.destroys = destroys;
    }
    if (refreshers !== undefined && refreshers.size > 0) {
        changeset.refreshers = refreshers;
    }
    return changeset;
}
class ModularEditBuilder extends index_js_1.EditBuilder {
    constructor(family, fieldKinds, changeReceiver) {
        super(family, changeReceiver);
        this.fieldKinds = fieldKinds;
        this.transactionDepth = 0;
        this.idAllocator = (0, index_js_2.idAllocatorFromMaxId)();
    }
    enterTransaction() {
        this.transactionDepth += 1;
        if (this.transactionDepth === 1) {
            this.idAllocator = (0, index_js_2.idAllocatorFromMaxId)();
        }
    }
    exitTransaction() {
        (0, internal_1.assert)(this.transactionDepth > 0, 0x5b9 /* Cannot exit inexistent transaction */);
        this.transactionDepth -= 1;
        if (this.transactionDepth === 0) {
            this.idAllocator = (0, index_js_2.idAllocatorFromMaxId)();
        }
    }
    /**
     * @param firstId - The ID to associate with the first node
     * @param content - The node(s) to build. Can be in either Field or Node mode.
     * @returns A description of the edit that can be passed to `submitChanges`.
     */
    buildTrees(firstId, content) {
        if (content.mode === 1 /* CursorLocationType.Fields */ && content.getFieldLength() === 0) {
            return { type: "global" };
        }
        const builds = new Map();
        const innerMap = new Map();
        builds.set(undefined, innerMap);
        const chunk = content.mode === 1 /* CursorLocationType.Fields */
            ? (0, index_js_3.chunkFieldSingle)(content, index_js_3.defaultChunkPolicy)
            : (0, index_js_3.chunkTree)(content, index_js_3.defaultChunkPolicy);
        innerMap.set(firstId, chunk);
        return {
            type: "global",
            builds,
        };
    }
    /**
     * Adds a change to the edit builder
     * @param field - the field which is being edited
     * @param fieldKind - the kind of the field
     * @param change - the change to the field
     * @param maxId - the highest `ChangesetLocalId` used in this change
     */
    submitChange(field, fieldKind, change) {
        const crossFieldKeys = getChangeHandler(this.fieldKinds, fieldKind).getCrossFieldKeys(change);
        const modularChange = buildModularChangesetFromField(field, { fieldKind, change }, new Map(), new Map(), newCrossFieldKeyTable(), this.idAllocator, crossFieldKeys);
        this.applyChange(modularChange);
    }
    submitChanges(changes) {
        const modularChange = this.buildChanges(changes);
        this.applyChange(modularChange);
    }
    buildChanges(changes) {
        const changeMaps = changes.map((change) => (0, index_js_1.makeAnonChange)(change.type === "global"
            ? makeModularChangeset(undefined, undefined, undefined, undefined, undefined, this.idAllocator.getMaxId(), undefined, undefined, change.builds)
            : buildModularChangesetFromField(change.field, {
                fieldKind: change.fieldKind,
                change: change.change,
            }, new Map(), new Map(), newCrossFieldKeyTable(), this.idAllocator, getChangeHandler(this.fieldKinds, change.fieldKind).getCrossFieldKeys(change.change))));
        const composedChange = this.changeFamily.rebaser.compose(changeMaps);
        const maxId = (0, index_js_2.brand)(this.idAllocator.getMaxId());
        if (maxId >= 0) {
            composedChange.maxId = maxId;
        }
        return composedChange;
    }
    generateId(count) {
        return (0, index_js_2.brand)(this.idAllocator.allocate(count));
    }
    addNodeExistsConstraint(path) {
        const nodeChange = {
            nodeExistsConstraint: { violated: false },
        };
        this.applyChange(buildModularChangesetFromNode(path, nodeChange, new Map(), new Map(), newCrossFieldKeyTable(), this.idAllocator));
    }
}
exports.ModularEditBuilder = ModularEditBuilder;
function buildModularChangesetFromField(path, fieldChange, nodeChanges, nodeToParent, crossFieldKeys, idAllocator = (0, index_js_2.idAllocatorFromMaxId)(), localCrossFieldKeys = [], childId = undefined) {
    const fieldChanges = new Map([[path.field, fieldChange]]);
    if (path.parent === undefined) {
        for (const key of localCrossFieldKeys) {
            crossFieldKeys.set(key, { nodeId: undefined, field: path.field });
        }
        if (childId !== undefined) {
            (0, index_js_1.setInChangeAtomIdMap)(nodeToParent, childId, {
                nodeId: undefined,
                field: path.field,
            });
        }
        return makeModularChangeset(fieldChanges, nodeChanges, nodeToParent, undefined, crossFieldKeys, idAllocator.getMaxId());
    }
    const nodeChangeset = {
        fieldChanges,
    };
    const parentId = { localId: (0, index_js_2.brand)(idAllocator.allocate()) };
    for (const key of localCrossFieldKeys) {
        crossFieldKeys.set(key, { nodeId: parentId, field: path.field });
    }
    if (childId !== undefined) {
        (0, index_js_1.setInChangeAtomIdMap)(nodeToParent, childId, {
            nodeId: parentId,
            field: path.field,
        });
    }
    return buildModularChangesetFromNode(path.parent, nodeChangeset, nodeChanges, nodeToParent, crossFieldKeys, idAllocator, parentId);
}
function buildModularChangesetFromNode(path, nodeChange, nodeChanges, nodeToParent, crossFieldKeys, idAllocator, nodeId = { localId: (0, index_js_2.brand)(idAllocator.allocate()) }) {
    (0, index_js_1.setInChangeAtomIdMap)(nodeChanges, nodeId, nodeChange);
    const fieldChangeset = genericFieldKind_js_1.genericFieldKind.changeHandler.editor.buildChildChange(path.parentIndex, nodeId);
    const fieldChange = {
        fieldKind: genericFieldKind_js_1.genericFieldKind.identifier,
        change: fieldChangeset,
    };
    return buildModularChangesetFromField({ parent: path.parent, field: path.parentField }, fieldChange, nodeChanges, nodeToParent, crossFieldKeys, idAllocator, [], nodeId);
}
function getRevInfoFromTaggedChanges(changes) {
    let maxId = -1;
    const revInfos = [];
    for (const taggedChange of changes) {
        const change = taggedChange.change;
        maxId = Math.max(change.maxId ?? -1, maxId);
        revInfos.push(...revisionInfoFromTaggedChange(taggedChange));
    }
    const revisions = new Set();
    const rolledBackRevisions = [];
    for (const info of revInfos) {
        revisions.add(info.revision);
        if (info.rollbackOf !== undefined) {
            rolledBackRevisions.push(info.rollbackOf);
        }
    }
    rolledBackRevisions.reverse();
    for (const revision of rolledBackRevisions) {
        if (!revisions.has(revision)) {
            revInfos.push({ revision });
        }
    }
    return { maxId: (0, index_js_2.brand)(maxId), revInfos };
}
function revisionInfoFromTaggedChange(taggedChange) {
    const revInfos = [];
    if (taggedChange.change.revisions !== undefined) {
        revInfos.push(...taggedChange.change.revisions);
    }
    else if (taggedChange.revision !== undefined) {
        const info = { revision: taggedChange.revision };
        if (taggedChange.rollbackOf !== undefined) {
            info.rollbackOf = taggedChange.rollbackOf;
        }
        revInfos.push(info);
    }
    return revInfos;
}
function revisionFromTaggedChange(change) {
    return change.revision ?? revisionFromRevInfos(change.change.revisions);
}
function revisionFromRevInfos(revInfos) {
    if (revInfos === undefined || revInfos.length !== 1) {
        return undefined;
    }
    return revInfos[0].revision;
}
function mergeBTrees(tree1, tree2) {
    const result = tree1.clone();
    tree2.forEachPair((k, v) => {
        result.set(k, v);
    });
    return result;
}
function mergeNestedMaps(map1, map2) {
    const merged = new Map();
    (0, index_js_2.populateNestedMap)(map1, merged, true);
    (0, index_js_2.populateNestedMap)(map2, merged, true);
    return merged;
}
function fieldChangeFromId(fields, nodes, id) {
    const fieldMap = fieldMapFromNodeId(fields, nodes, id.nodeId);
    return fieldMap.get(id.field) ?? (0, index_js_2.fail)("No field exists for the given ID");
}
function fieldMapFromNodeId(rootFieldMap, nodes, nodeId) {
    if (nodeId === undefined) {
        return rootFieldMap;
    }
    const node = nodeChangeFromId(nodes, nodeId);
    (0, internal_1.assert)(node.fieldChanges !== undefined, 0x9c9 /* Expected node to have field changes */);
    return node.fieldChanges;
}
function rebasedFieldIdFromBaseId(table, baseId) {
    if (baseId.nodeId === undefined) {
        return baseId;
    }
    return { ...baseId, nodeId: rebasedNodeIdFromBaseNodeId(table, baseId.nodeId) };
}
function rebasedNodeIdFromBaseNodeId(table, baseId) {
    return (0, index_js_1.getFromChangeAtomIdMap)(table.baseToRebasedNodeId, baseId) ?? baseId;
}
function nodeChangeFromId(nodes, id) {
    const node = (0, index_js_1.getFromChangeAtomIdMap)(nodes, id);
    (0, internal_1.assert)(node !== undefined, 0x9ca /* Unknown node ID */);
    return node;
}
function fieldIdFromFieldIdKey([revision, localId, field]) {
    const nodeId = localId !== undefined ? { revision, localId } : undefined;
    return { nodeId, field };
}
function cloneNodeChangeset(nodeChangeset) {
    if (nodeChangeset.fieldChanges !== undefined) {
        return { ...nodeChangeset, fieldChanges: new Map(nodeChangeset.fieldChanges) };
    }
    return { ...nodeChangeset };
}
function replaceFieldIdRevision(fieldId, oldRevisions, newRevision) {
    if (fieldId.nodeId === undefined) {
        return fieldId;
    }
    return {
        ...fieldId,
        nodeId: (0, index_js_1.replaceAtomRevisions)(fieldId.nodeId, oldRevisions, newRevision),
    };
}
function getParentFieldId(changeset, nodeId) {
    const parentId = (0, index_js_1.getFromChangeAtomIdMap)(changeset.nodeToParent, nodeId);
    (0, internal_1.assert)(parentId !== undefined, 0x9cb /* Parent field should be defined */);
    return normalizeFieldId(parentId, changeset.nodeAliases);
}
exports.getParentFieldId = getParentFieldId;
function getFieldsForCrossFieldKey(changeset, [target, revision, id, count]) {
    let firstLocalId = id;
    const lastLocalId = id + count - 1;
    const fields = [];
    // eslint-disable-next-line no-constant-condition
    while (true) {
        const entry = getFirstIntersectingCrossFieldEntry(changeset.crossFieldKeys, [
            target,
            revision,
            (0, index_js_2.brand)(firstLocalId),
            count,
        ]);
        if (entry === undefined) {
            return fields;
        }
        const [[_target, _revision, entryId, entryCount], fieldId] = entry;
        fields.push(normalizeFieldId(fieldId, changeset.nodeAliases));
        const entryLastId = entryId + entryCount - 1;
        if (entryLastId >= lastLocalId) {
            return fields;
        }
        firstLocalId = entryLastId + 1;
    }
}
exports.getFieldsForCrossFieldKey = getFieldsForCrossFieldKey;
function getFirstIntersectingCrossFieldEntry(table, [target, revision, id, count]) {
    const entry = table.nextLowerPair([target, revision, id, Infinity]);
    if (entry === undefined) {
        return undefined;
    }
    const [entryTarget, entryRevision, entryId, entryCount] = entry[0];
    if (entryTarget !== target || entryRevision !== revision) {
        return undefined;
    }
    const lastQueryId = id + count - 1;
    const entryLastId = entryId + entryCount - 1;
    if (entryId > lastQueryId || entryLastId < id) {
        return undefined;
    }
    return entry;
}
function setInCrossFieldKeyTable(table, target, revision, id, count, value) {
    let entry = getFirstIntersectingCrossFieldEntry(table, [target, revision, id, count]);
    const lastQueryId = id + count - 1;
    while (entry !== undefined) {
        const [entryKey, entryValue] = entry;
        table.delete(entryKey);
        const [_target, _revision, entryId, entryCount] = entryKey;
        if (entryId < id) {
            table.set([target, revision, entryId, id - entryId], entryValue);
        }
        const lastEntryId = entryId + entryCount - 1;
        if (lastEntryId > lastQueryId) {
            table.set([target, revision, (0, index_js_2.brand)(lastQueryId + 1), lastEntryId - lastQueryId], entryValue);
            break;
        }
        const nextId = (0, index_js_2.brand)(lastEntryId + 1);
        entry = getFirstIntersectingCrossFieldEntry(table, [
            target,
            revision,
            nextId,
            lastQueryId - nextId + 1,
        ]);
    }
    table.set([target, revision, id, count], value);
}
function normalizeFieldId(fieldId, nodeAliases) {
    return fieldId.nodeId !== undefined
        ? { ...fieldId, nodeId: normalizeNodeId(fieldId.nodeId, nodeAliases) }
        : fieldId;
}
/**
 * @returns The canonical form of nodeId, according to nodeAliases
 */
function normalizeNodeId(nodeId, nodeAliases) {
    let currentId = nodeId;
    // eslint-disable-next-line no-constant-condition
    while (true) {
        const dealiased = (0, index_js_1.getFromChangeAtomIdMap)(nodeAliases, currentId);
        if (dealiased === undefined) {
            return currentId;
        }
        currentId = dealiased;
    }
}
function hasConflicts(change) {
    return (change.constraintViolationCount ?? 0) > 0;
}
function newCrossFieldKeyTable() {
    return newBTree();
}
exports.newCrossFieldKeyTable = newCrossFieldKeyTable;
function newBTree() {
    return (0, index_js_2.brand)(new sorted_btree_es6_1.BTree(undefined, compareTuples));
}
// This assumes that the arrays are the same length.
function compareTuples(arrayA, arrayB) {
    for (let i = 0; i < arrayA.length; i++) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const a = arrayA[i];
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const b = arrayB[i];
        if (a < b) {
            return -1;
        }
        else if (a > b) {
            return 1;
        }
    }
    return 0;
}
function cloneNestedMap(map) {
    const cloned = new Map();
    (0, index_js_2.populateNestedMap)(map, cloned, true);
    return cloned;
}
//# sourceMappingURL=modularChangeFamily.js.map