"use strict";
/*!
 * 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 });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SharedTreeCore = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/runtime-utils/internal");
const internal_3 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../core/index.js");
const index_js_2 = require("../util/index.js");
const branchCommitEnricher_js_1 = require("./branchCommitEnricher.js");
const changeEnricher_js_1 = require("./changeEnricher.js");
const defaultResubmitMachine_js_1 = require("./defaultResubmitMachine.js");
const editManager_js_1 = require("./editManager.js");
const editManagerCodecs_js_1 = require("./editManagerCodecs.js");
const editManagerSummarizer_js_1 = require("./editManagerSummarizer.js");
const messageCodecs_js_1 = require("./messageCodecs.js");
// TODO: Organize this to be adjacent to persisted types.
const summarizablesTreeKey = "indexes";
/**
 * Generic shared tree, which needs to be configured with indexes, field kinds and other configuration.
 */
let SharedTreeCore = (() => {
    let _classDecorators = [index_js_2.breakingClass];
    let _classDescriptor;
    let _classExtraInitializers = [];
    let _classThis;
    let _instanceExtraInitializers = [];
    let _summarizeCore_decorators;
    var SharedTreeCore = _classThis = class {
        /**
         * @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, formatOptions, idCompressor, schema, schemaPolicy, resubmitMachine, enricher, getEditor = () => this.getLocalBranch().editor) {
            this.breaker = (__runInitializers(this, _instanceExtraInitializers), breaker);
            this.sharedObject = sharedObject;
            this.serializer = serializer;
            this.submitLocalMessage = submitLocalMessage;
            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 = editManager_js_1.minimumPossibleSequenceNumber;
            this.schemaAndPolicy = {
                schema,
                policy: schemaPolicy,
            };
            const rebaseLogger = (0, internal_3.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_js_1.EditManager(changeFamily, localSessionId, this.mintRevisionTag, rebaseLogger);
            this.editManager.localBranch.events.on("beforeChange", (change) => {
                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.commitEnricher.processChange(change);
                }
                if (change.type === "append") {
                    for (const commit of change.newCommits) {
                        this.submitCommit(commit, this.schemaAndPolicy, false);
                    }
                }
            });
            const revisionTagCodec = new index_js_1.RevisionTagCodec(idCompressor);
            const editManagerCodec = (0, editManagerCodecs_js_1.makeEditManagerCodec)(this.editManager.changeFamily.codecs, revisionTagCodec, options, formatOptions.editManager);
            this.summarizables = [
                new editManagerSummarizer_js_1.EditManagerSummarizer(this.editManager, editManagerCodec, this.idCompressor, this.schemaAndPolicy),
                ...summarizables,
            ];
            (0, internal_1.assert)(new Set(this.summarizables.map((e) => e.key)).size === this.summarizables.length, 0x350 /* Index summary element keys must be unique */);
            this.messageCodec = (0, messageCodecs_js_1.makeMessageCodec)(changeFamily.codecs, new index_js_1.RevisionTagCodec(idCompressor), options, formatOptions.message);
            const changeEnricher = enricher ?? new changeEnricher_js_1.NoOpChangeEnricher();
            this.resubmitMachine =
                resubmitMachine ??
                    new defaultResubmitMachine_js_1.DefaultResubmitMachine((change) => changeFamily.rebaser.invert(change, true, this.mintRevisionTag()), changeEnricher);
            this.commitEnricher = new branchCommitEnricher_js_1.BranchCommitEnricher(changeFamily.rebaser, changeEnricher);
        }
        // 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.
        summarizeCore(serializer, telemetryContext, incrementalSummaryContext, fullTree) {
            const builder = new internal_2.SummaryTreeBuilder();
            const summarizableBuilder = new internal_2.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: (contents) => serializer.stringify(contents, this.sharedObject.handle),
                    fullTree,
                    telemetryContext,
                    incrementalSummaryContext: childIncrementalSummaryContext,
                }));
            }
            builder.addWithStats(summarizablesTreeKey, summarizableBuilder.getSummaryTree());
            return builder.getSummaryTree();
        }
        async loadCore(services) {
            (0, internal_1.assert)(this.editManager.localBranch.getHead() === this.editManager.getTrunkHead(), 0xaaa /* All local changes should be applied to the trunk before loading from summary */);
            const [editManagerSummarizer, ...summarizables] = this.summarizables;
            const loadEditManager = this.loadSummarizable(editManagerSummarizer, services);
            const loadSummarizables = summarizables.map(async (s) => this.loadSummarizable(s, services));
            if (this.detachedRevision !== undefined) {
                // 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;
                // Find the most recent detached revision in the summary trunk...
                let latestDetachedSequenceNumber;
                (0, index_js_1.findAncestor)(this.editManager.getTrunkHead(), (c) => {
                    const sequenceNumber = this.editManager.getSequenceNumber(c);
                    if (sequenceNumber !== undefined && sequenceNumber < 0) {
                        latestDetachedSequenceNumber = sequenceNumber;
                        return true;
                    }
                    return false;
                });
                // ...and set our detached revision to be as it would be if we had been already created that revision.
                this.detachedRevision = latestDetachedSequenceNumber ?? this.detachedRevision;
                await Promise.all(loadSummarizables);
            }
            else {
                await Promise.all([loadEditManager, ...loadSummarizables]);
            }
        }
        async loadSummarizable(summarizable, services) {
            return summarizable.load(scopeStorageService(services, summarizablesTreeKey, summarizable.key), (contents) => this.serializer.parse(contents));
        }
        /**
         * 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(commit, schemaAndPolicy, isResubmit) {
            (0, internal_1.assert)(this.sharedObject.isAttached() === (this.detachedRevision === undefined), 0x95a /* Detached revision should only be set when not attached */);
            const enrichedCommit = this.detachedRevision === undefined && !isResubmit
                ? this.commitEnricher.enrich(commit)
                : commit;
            // 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 = (0, index_js_2.brand)(this.detachedRevision + 1);
                this.detachedRevision = newRevision;
                this.editManager.addSequencedChanges([enrichedCommit], this.editManager.localSessionId, newRevision, this.detachedRevision);
                this.editManager.advanceMinimumSequenceNumber(newRevision, false);
                return undefined;
            }
            const message = this.messageCodec.encode({
                commit: enrichedCommit,
                sessionId: this.editManager.localSessionId,
            }, {
                idCompressor: this.idCompressor,
                schema: schemaAndPolicy,
            });
            this.submitLocalMessage(message, {
                // Clone the schema to ensure that during resubmit the schema has not been mutated by later changes
                schema: schemaAndPolicy.schema.clone(),
                policy: schemaAndPolicy.policy,
            });
            this.resubmitMachine.onCommitSubmitted(enrichedCommit);
        }
        /**
         * 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;
            // 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 { commit, sessionId } = this.messageCodec.decode(messageContent.contents, {
                    idCompressor: this.idCompressor,
                });
                commits.push(commit);
                if (messagesSessionId !== undefined) {
                    (0, internal_1.assert)(messagesSessionId === sessionId, 0xad9 /* All messages in a bunch must have the same session ID */);
                }
                messagesSessionId = sessionId;
            }
            (0, internal_1.assert)(messagesSessionId !== undefined, 0xada /* Messages must have a session ID */);
            this.editManager.addSequencedChanges(commits, messagesSessionId, (0, index_js_2.brand)(envelope.sequenceNumber), (0, index_js_2.brand)(envelope.referenceSequenceNumber));
            // Update the resubmit machine for each commit applied.
            for (const _ of messagesContent) {
                this.resubmitMachine.onSequencedCommitApplied(local);
            }
            this.editManager.advanceMinimumSequenceNumber((0, index_js_2.brand)(envelope.minimumSequenceNumber));
        }
        getLocalBranch() {
            return this.editManager.localBranch;
        }
        didAttach() {
            this.detachedRevision = undefined;
        }
        reSubmitCore(content, localOpMetadata) {
            // Empty context object is passed in, as our decode function is schema-agnostic.
            const { commit: { revision }, } = this.messageCodec.decode(this.serializer.decode(content), {
                idCompressor: this.idCompressor,
            });
            // If a resubmit phase is not already in progress, then this must be the first commit of a new resubmit phase.
            if (this.resubmitMachine.isInResubmitPhase === false) {
                const localCommits = this.editManager.getLocalCommits();
                const revisionIndex = localCommits.findIndex((c) => c.revision === revision);
                (0, internal_1.assert)(revisionIndex >= 0, 0xbdb /* revision must exist in local commits */);
                const toResubmit = localCommits.slice(revisionIndex);
                this.resubmitMachine.prepareForResubmit(toResubmit);
            }
            (0, internal_1.assert)(isClonableSchemaPolicy(localOpMetadata), 0x95e /* Local metadata must contain schema and policy. */);
            (0, internal_1.assert)(this.resubmitMachine.isInResubmitPhase !== false, 0x984 /* Invalid resubmit outside of resubmit phase */);
            const enrichedCommit = this.resubmitMachine.peekNextCommit();
            this.submitCommit(enrichedCommit, localOpMetadata, true);
        }
        rollback(content, localOpMetadata) {
            // Empty context object is passed in, as our decode function is schema-agnostic.
            const { commit: { revision }, } = this.messageCodec.decode(this.serializer.decode(content), {
                idCompressor: this.idCompressor,
            });
            const [commit] = this.editManager.findLocalCommit(revision);
            const { parent } = commit;
            (0, internal_1.assert)(parent !== undefined, 0xbdc /* must have parent */);
            const [precedingCommit] = this.editManager.findLocalCommit(parent.revision);
            this.editManager.localBranch.removeAfter(precedingCommit);
            this.resubmitMachine.onCommitRollback(commit);
        }
        applyStashedOp(content) {
            // Empty context object is passed in, as our decode function is schema-agnostic.
            const { commit: { revision, change }, } = this.messageCodec.decode(content, { idCompressor: this.idCompressor });
            this.editManager.localBranch.apply({ change, revision });
        }
    };
    __setFunctionName(_classThis, "SharedTreeCore");
    (() => {
        const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
        _summarizeCore_decorators = [index_js_2.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;
})();
exports.SharedTreeCore = 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}`);
        },
    };
}
//# sourceMappingURL=sharedTreeCore.js.map