/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
    var useValue = arguments.length > 2;
    for (var i = 0; i < initializers.length; i++) {
        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
    }
    return useValue ? value : void 0;
};
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
    var _, done = false;
    for (var i = decorators.length - 1; i >= 0; i--) {
        var context = {};
        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
        if (kind === "accessor") {
            if (result === void 0) continue;
            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
            if (_ = accept(result.get)) descriptor.get = _;
            if (_ = accept(result.set)) descriptor.set = _;
            if (_ = accept(result.init)) initializers.unshift(_);
        }
        else if (_ = accept(result)) {
            if (kind === "field") initializers.unshift(_);
            else descriptor[key] = _;
        }
    }
    if (target) Object.defineProperty(target, contextIn.name, descriptor);
    done = true;
};
var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
    if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
    return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
};
import { assert, fail, unreachableCase } from "@fluidframework/core-utils/internal";
import { SummaryTreeBuilder } from "@fluidframework/runtime-utils/internal";
import { createChildLogger } from "@fluidframework/telemetry-utils/internal";
import { replaceChange, RevisionTagCodec, } from "../core/index.js";
import { brand, throwIfBroken, breakingClass, } from "../util/index.js";
import { BranchCommitEnricher } from "./branchCommitEnricher.js";
import { DefaultResubmitMachine } from "./defaultResubmitMachine.js";
import { EditManager, minimumPossibleSequenceNumber } from "./editManager.js";
import { makeEditManagerCodecBuilder } from "./editManagerCodecs.js";
import { EditManagerSummarizer } from "./editManagerSummarizer.js";
import { makeMessageCodecBuilder } from "./messageCodecs.js";
import { minVersionToSharedTreeSummaryFormatVersion, summarizablesTreeKey, supportedSharedTreeSummaryFormatVersions, } from "./summaryTypes.js";
import { VersionedSummarizer } from "./versionedSummarizer.js";
/**
 * Generic shared tree, which needs to be configured with indexes, field kinds and other configuration.
 */
let SharedTreeCore = (() => {
    let _classDecorators = [breakingClass];
    let _classDescriptor;
    let _classExtraInitializers = [];
    let _classThis;
    let _classSuper = VersionedSummarizer;
    let _instanceExtraInitializers = [];
    let _summarizeCore_decorators;
    var SharedTreeCore = _classThis = class extends _classSuper {
        /**
         * @param summarizables - Summarizers for all indexes used by this tree
         * @param changeFamily - The change family
         * @param editManager - The edit manager
         * @param runtime - The IFluidDataStoreRuntime which contains the shared object
         * @param editor - Used to edit the state of the tree. Edits will be immediately applied locally to the tree.
         * If there is no transaction currently ongoing, then the edits will be submitted to Fluid immediately as well.
         */
        constructor(breaker, sharedObject, serializer, submitLocalMessage, logger, summarizables, changeFamily, options, changeFormatVersionForEditManager, changeFormatVersionForMessage, idCompressor, schema, schemaPolicy, enrichmentConfig, getEditor = () => this.getLocalBranch().editor) {
            super(summarizablesTreeKey, minVersionToSharedTreeSummaryFormatVersion(options.minVersionForCollab), supportedSharedTreeSummaryFormatVersions, true /* supportPreVersioningFormat */);
            this.breaker = (__runInitializers(this, _instanceExtraInitializers), breaker);
            this.sharedObject = sharedObject;
            this.serializer = serializer;
            this.submitLocalMessage = submitLocalMessage;
            this.changeFamily = changeFamily;
            this.idCompressor = idCompressor;
            this.getEditor = getEditor;
            /**
             * The sequence number that this instance is at.
             * This number is artificial in that it is made up by this instance as opposed to being provided by the runtime.
             * Is `undefined` after (and only after) this instance is attached.
             */
            this.detachedRevision = minimumPossibleSequenceNumber;
            this.enrichers = new Map();
            this.schemaAndPolicy = {
                schema,
                policy: schemaPolicy,
            };
            const rebaseLogger = createChildLogger({
                logger,
                namespace: "Rebase",
            });
            this.mintRevisionTag = () => this.idCompressor.generateCompressedId();
            /**
             * A random ID that uniquely identifies this client in the collab session.
             * This is sent alongside every op to identify which client the op originated from.
             * This is used rather than the Fluid client ID because the Fluid client ID is not stable across reconnections.
             */
            const localSessionId = idCompressor.localSessionId;
            this.editManager = new EditManager(changeFamily, localSessionId, this.mintRevisionTag, (branchId) => this.registerSharedBranch(branchId), rebaseLogger);
            this.registerSharedBranch("main");
            const revisionTagCodec = new RevisionTagCodec(idCompressor);
            const editManagerCodec = makeEditManagerCodecBuilder().build({
                ...options,
                changeCodecs: this.editManager.changeFamily.codecs,
                dependentChangeFormatVersion: changeFormatVersionForEditManager,
                revisionTagCodec,
            });
            this.summarizables = [
                new EditManagerSummarizer(this.editManager, editManagerCodec, this.idCompressor, options.minVersionForCollab, this.schemaAndPolicy),
                ...summarizables,
            ];
            assert(new Set(this.summarizables.map((e) => e.key)).size === this.summarizables.length, 0x350 /* Index summary element keys must be unique */);
            this.messageCodec = makeMessageCodecBuilder().build({
                ...options,
                changeCodecs: changeFamily.codecs,
                dependentChangeFormatVersion: changeFormatVersionForMessage,
                revisionTagCodec: new RevisionTagCodec(idCompressor),
            });
            if (enrichmentConfig !== undefined) {
                this.registerSharedBranchForEditing("main", enrichmentConfig.enricher, enrichmentConfig.resubmitMachine);
            }
        }
        // TODO: SharedObject's merging of the two summary methods into summarizeCore is not what we want here:
        // We might want to not subclass it, or override/reimplement most of its functionality.
        summarizeInternal(props) {
            const { stringify, fullTree, telemetryContext, incrementalSummaryContext, builder } = props;
            const summarizableBuilder = new SummaryTreeBuilder();
            // Merge the summaries of all summarizables together under a single ISummaryTree
            for (const s of this.summarizables) {
                // Add the summarizable's path in the summary tree to the incremental summary context's
                // summary path, so that the summarizable can use it to generate incremental summaries.
                const childIncrementalSummaryContext = incrementalSummaryContext === undefined
                    ? undefined
                    : {
                        ...incrementalSummaryContext,
                        summaryPath: `${incrementalSummaryContext.summaryPath}/${summarizablesTreeKey}/${s.key}`,
                    };
                summarizableBuilder.addWithStats(s.key, s.summarize({
                    stringify,
                    fullTree,
                    telemetryContext,
                    incrementalSummaryContext: childIncrementalSummaryContext,
                }));
            }
            builder.addWithStats(summarizablesTreeKey, summarizableBuilder.getSummaryTree());
        }
        async loadInternal(services, parse) {
            const [editManagerSummarizer, ...summarizables] = this.summarizables;
            const loadEditManager = this.loadSummarizable(editManagerSummarizer, services, parse);
            const loadSummarizables = summarizables.map(async (s) => this.loadSummarizable(s, services, parse));
            if (this.detachedRevision === undefined) {
                await Promise.all([loadEditManager, ...loadSummarizables]);
            }
            else {
                // If we are detached but loading from a summary, then we need to update our detached revision to ensure that it is ahead of all detached revisions in the summary.
                // First, finish loading the edit manager so that we can inspect the sequence numbers of the commits on the trunk.
                await loadEditManager;
                const latestDetachedSequenceNumber = this.editManager.getLatestSequenceNumber();
                // When we load a summary for a tree that was never attached,
                // latestDetachedSequenceNumber is either undefined (no commits in summary) or negative (all commits in summary were made while detached).
                // We only need to update `this.detachedRevision` in the latter case.
                if (latestDetachedSequenceNumber !== undefined && latestDetachedSequenceNumber < 0) {
                    this.detachedRevision = latestDetachedSequenceNumber;
                }
                await Promise.all(loadSummarizables);
            }
        }
        summarizeCore(serializer, telemetryContext, incrementalSummaryContext, fullTree) {
            return this.summarize({
                stringify: (contents) => serializer.stringify(contents, this.sharedObject.handle),
                fullTree,
                telemetryContext,
                incrementalSummaryContext,
            });
        }
        async loadCore(services) {
            assert(this.getLocalBranch().getHead() === this.editManager.getTrunkHead("main"), 0xaaa /* All local changes should be applied to the trunk before loading from summary */);
            await super.load(services, (contents) => this.serializer.parse(contents));
        }
        registerSharedBranch(branchId) {
            const branch = this.editManager.getLocalBranch(branchId);
            branch.events.on("beforeChange", (change) => {
                if (change.type === "append") {
                    if (this.detachedRevision === undefined) {
                        // Commit enrichment is only necessary for changes that will be submitted as ops, and changes issued while detached are not submitted.
                        this.getCommitEnricher(branchId).prepareChanges(change.newCommits);
                    }
                    for (const commit of change.newCommits) {
                        this.submitCommit(branchId, commit, this.schemaAndPolicy, false);
                    }
                }
            });
        }
        async loadSummarizable(summarizable, services, parse) {
            return summarizable.load(scopeStorageService(services, summarizablesTreeKey, summarizable.key), parse);
        }
        /**
         * Submits an op to the Fluid runtime containing the given commit
         * @param commit - the commit to submit
         * @returns the submitted commit. This is undefined if the underlying `SharedObject` is not attached,
         * and may differ from `commit` due to enrichments like detached tree refreshers.
         */
        submitCommit(branchId, commit, schemaAndPolicy, isResubmit) {
            assert(this.sharedObject.isAttached() === (this.detachedRevision === undefined), 0x95a /* Detached revision should only be set when not attached */);
            // Edits submitted before the first attach are treated as sequenced because they will be included
            // in the attach summary that is uploaded to the service.
            // Until this attach workflow happens, this instance essentially behaves as a centralized data structure.
            if (this.detachedRevision !== undefined) {
                const newRevision = brand(this.detachedRevision + 1);
                this.detachedRevision = newRevision;
                this.editManager.addSequencedChanges([commit], this.editManager.localSessionId, newRevision, this.detachedRevision, branchId);
                this.editManager.advanceMinimumSequenceNumber(newRevision, false);
                return undefined;
            }
            const enrichedCommit = isResubmit
                ? commit
                : replaceChange(commit, this.getCommitEnricher(branchId).retrieveChange(commit));
            this.submitMessage({
                type: "commit",
                commit: enrichedCommit,
                sessionId: this.editManager.localSessionId,
                branchId,
            }, schemaAndPolicy);
            this.getResubmitMachine(branchId).onCommitSubmitted(enrichedCommit);
        }
        submitBranchCreation(branchId) {
            this.submitMessage({ type: "branch", sessionId: this.editManager.localSessionId, branchId }, this.schemaAndPolicy);
        }
        submitMessage(message, schemaAndPolicy) {
            const encodedMessage = this.messageCodec.encode(message, {
                idCompressor: this.idCompressor,
                schema: schemaAndPolicy,
            });
            this.submitLocalMessage(encodedMessage, {
                // Clone the schema to ensure that during resubmit the schema has not been mutated by later changes
                schema: schemaAndPolicy.schema.clone(),
                policy: schemaAndPolicy.policy,
            });
        }
        /**
         * Process a bunch of messages from the runtime. SharedObject will call this method with a bunch of messages.
         */
        processMessagesCore(messagesCollection) {
            const { envelope, local, messagesContent } = messagesCollection;
            const commits = [];
            let messagesSessionId;
            let branchId;
            const processBunch = (branch) => {
                assert(messagesSessionId !== undefined, 0xada /* Messages must have a session ID */);
                this.processCommits(messagesSessionId, brand(envelope.sequenceNumber), brand(envelope.referenceSequenceNumber), local, branch, commits);
                commits.length = 0;
                branchId = undefined;
            };
            // Get a list of all the commits from the messages.
            for (const messageContent of messagesContent) {
                // Empty context object is passed in, as our decode function is schema-agnostic.
                const message = this.messageCodec.decode(messageContent.contents, {
                    idCompressor: this.idCompressor,
                });
                if (messagesSessionId !== undefined) {
                    assert(messagesSessionId === message.sessionId, 0xad9 /* All messages in a bunch must have the same session ID */);
                }
                messagesSessionId = message.sessionId;
                const type = message.type;
                switch (type) {
                    case "commit": {
                        if (branchId !== undefined && message.branchId !== branchId) {
                            processBunch(branchId);
                        }
                        branchId = message.branchId;
                        commits.push(message.commit);
                        break;
                    }
                    case "branch": {
                        if (branchId !== undefined) {
                            processBunch(branchId);
                        }
                        this.editManager.sequenceBranchCreation(messagesSessionId, brand(envelope.referenceSequenceNumber), message.branchId);
                        break;
                    }
                    default: {
                        unreachableCase(type);
                    }
                }
            }
            if (branchId !== undefined) {
                processBunch(branchId);
            }
            this.editManager.advanceMinimumSequenceNumber(brand(envelope.minimumSequenceNumber));
        }
        processCommits(sessionId, sequenceNumber, referenceSequenceNumber, isLocal, branchId, commits) {
            this.editManager.addSequencedChanges(commits, sessionId, sequenceNumber, referenceSequenceNumber, branchId);
            // Update the resubmit machine for each commit applied.
            for (const commit of commits) {
                this.tryGetResubmitMachine(branchId)?.onSequencedCommitApplied(commit.revision, isLocal);
            }
        }
        getLocalBranch() {
            return this.editManager.getLocalBranch("main");
        }
        getSharedBranchIds() {
            return this.editManager
                .getSharedBranchIds()
                .filter((id) => id !== "main")
                .map((id) => this.idCompressor.decompress(id));
        }
        createSharedBranch() {
            const branchId = this.idCompressor.generateCompressedId();
            this.addBranch(branchId);
            this.submitBranchCreation(branchId);
            return this.idCompressor.decompress(branchId);
        }
        addBranch(branchId) {
            this.editManager.addNewBranch(branchId);
        }
        getSharedBranch(branchId) {
            return this.editManager.getLocalBranch(branchId);
        }
        didAttach() {
            this.detachedRevision = undefined;
        }
        reSubmitCore(content, localOpMetadata) {
            // Empty context object is passed in, as our decode function is schema-agnostic.
            const message = this.messageCodec.decode(this.serializer.decode(content), {
                idCompressor: this.idCompressor,
            });
            const type = message.type;
            switch (type) {
                case "commit": {
                    const { commit: { revision }, branchId, } = message;
                    const resubmitMachine = this.getResubmitMachine(branchId);
                    const getLocalCommits = () => {
                        const localCommits = this.editManager.getLocalCommits(branchId);
                        const revisionIndex = localCommits.findIndex((c) => c.revision === revision);
                        assert(revisionIndex >= 0, 0xbdb /* revision must exist in local commits */);
                        return localCommits.slice(revisionIndex);
                    };
                    assert(isClonableSchemaPolicy(localOpMetadata), 0x95e /* Local metadata must contain schema and policy. */);
                    const enrichedCommit = resubmitMachine.getEnrichedCommit(revision, getLocalCommits);
                    if (enrichedCommit !== undefined) {
                        this.submitCommit(branchId, enrichedCommit, localOpMetadata, true);
                    }
                    break;
                }
                case "branch": {
                    this.submitBranchCreation(message.branchId);
                    break;
                }
                default: {
                    unreachableCase(type);
                }
            }
        }
        rollback(content, localOpMetadata) {
            // Empty context object is passed in, as our decode function is schema-agnostic.
            const message = this.messageCodec.decode(this.serializer.decode(content), {
                idCompressor: this.idCompressor,
            });
            const type = message.type;
            switch (type) {
                case "commit": {
                    const { commit: { revision }, branchId, } = message;
                    const branch = this.editManager.getLocalBranch(branchId);
                    const head = branch.getHead();
                    assert(head.revision === revision, 0xc6b /* Can only rollback latest commit */);
                    const newHead = head.parent ?? fail(0xc6c /* must have parent */);
                    branch.removeAfter(newHead);
                    this.getResubmitMachine(branchId).onCommitRollback(head);
                    break;
                }
                case "branch": {
                    this.editManager.removeBranch(message.branchId);
                    break;
                }
                default: {
                    unreachableCase(type);
                }
            }
        }
        applyStashedOp(content) {
            // Empty context object is passed in, as our decode function is schema-agnostic.
            const message = this.messageCodec.decode(this.serializer.decode(content), {
                idCompressor: this.idCompressor,
            });
            const type = message.type;
            switch (type) {
                case "commit": {
                    const { commit: { revision, change }, branchId, } = message;
                    this.editManager.getLocalBranch(branchId).apply({ change, revision });
                    break;
                }
                case "branch": {
                    this.editManager.addNewBranch(message.branchId);
                    break;
                }
                default: {
                    unreachableCase(type);
                }
            }
        }
        registerSharedBranchForEditing(branchId, enricher, resubmitMachine) {
            const commitEnricher = new BranchCommitEnricher(enricher);
            assert(!this.enrichers.has(branchId), 0xc6d /* Branch already registered */);
            this.enrichers.set(branchId, {
                enricher: commitEnricher,
                resubmitMachine: resubmitMachine ?? new DefaultResubmitMachine(enricher),
            });
        }
        getResubmitMachine(branchId) {
            return this.getEnricherState(branchId).resubmitMachine;
        }
        tryGetResubmitMachine(branchId) {
            return this.tryGetEnricherState(branchId)?.resubmitMachine;
        }
        getCommitEnricher(branchId) {
            return this.getEnricherState(branchId).enricher;
        }
        getEnricherState(branchId) {
            return (this.tryGetEnricherState(branchId) ??
                fail(0xc6e /* Expected to have a resubmit machine for this branch */));
        }
        tryGetEnricherState(branchId) {
            return this.enrichers.get(branchId);
        }
    };
    __setFunctionName(_classThis, "SharedTreeCore");
    (() => {
        const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
        _summarizeCore_decorators = [throwIfBroken];
        __esDecorate(_classThis, null, _summarizeCore_decorators, { kind: "method", name: "summarizeCore", static: false, private: false, access: { has: obj => "summarizeCore" in obj, get: obj => obj.summarizeCore }, metadata: _metadata }, null, _instanceExtraInitializers);
        __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
        SharedTreeCore = _classThis = _classDescriptor.value;
        if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        __runInitializers(_classThis, _classExtraInitializers);
    })();
    return SharedTreeCore = _classThis;
})();
export { SharedTreeCore };
function isClonableSchemaPolicy(maybeSchemaPolicy) {
    const schemaAndPolicy = maybeSchemaPolicy;
    return schemaAndPolicy.schema !== undefined && schemaAndPolicy.policy !== undefined;
}
/**
 * Compose an {@link IChannelStorageService} which prefixes all paths before forwarding them to the original service
 */
function scopeStorageService(service, ...pathElements) {
    const scope = `${pathElements.join("/")}/`;
    return {
        async readBlob(path) {
            return service.readBlob(`${scope}${path}`);
        },
        async contains(path) {
            return service.contains(`${scope}${path}`);
        },
        async list(path) {
            return service.list(`${scope}${path}`);
        },
        getSnapshotTree() {
            const snapshotTree = service.getSnapshotTree();
            if (snapshotTree === undefined) {
                return undefined;
            }
            let scopedTree = snapshotTree;
            for (const element of pathElements) {
                const tree = scopedTree.trees[element];
                assert(tree !== undefined, 0xc20 /* snapshot tree not found for one of tree's summarizables */);
                scopedTree = tree;
            }
            return scopedTree;
        },
    };
}
//# sourceMappingURL=sharedTreeCore.js.map