"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
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/shared-object-base/internal");
const index_js_1 = require("../core/index.js");
const index_js_2 = require("../util/index.js");
const branch_js_1 = require("./branch.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 a history policy to be used.
 *
 * TODO: actually implement
 * TODO: is history policy a detail of what indexes are used, or is there something else to it?
 */
class SharedTreeCore extends internal_3.SharedObject {
    /**
     * 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.
     */
    get editor() {
        return this.getLocalBranch().editor;
    }
    /**
     * @param summarizables - Summarizers for all indexes used by this tree
     * @param changeFamily - The change family
     * @param editManager - The edit manager
     * @param id - The id of the shared object
     * @param runtime - The IFluidDataStoreRuntime which contains the shared object
     * @param attributes - Attributes of the shared object
     * @param telemetryContextPrefix - The property prefix for telemetry pertaining to this object. See {@link ITelemetryContext}
     */
    constructor(summarizables, changeFamily, options, formatOptions, 
    // Base class arguments
    id, runtime, attributes, telemetryContextPrefix, schema, schemaPolicy, enricher) {
        super(id, runtime, attributes, telemetryContextPrefix);
        /**
         * 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,
        };
        (0, internal_1.assert)(runtime.idCompressor !== undefined, 0x886 /* IdCompressor must be enabled to use SharedTree */);
        this.idCompressor = runtime.idCompressor;
        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 = runtime.idCompressor.localSessionId;
        this.editManager = new editManager_js_1.EditManager(changeFamily, localSessionId, this.mintRevisionTag);
        this.editManager.localBranch.on("afterChange", (args) => {
            if (this.getLocalBranch().isTransacting()) {
                // Avoid submitting ops for changes that are part of a transaction.
                return;
            }
            switch (args.type) {
                case "append":
                    for (const c of args.newCommits) {
                        this.submitCommit(c, this.schemaAndPolicy);
                    }
                    break;
                case "replace":
                    if ((0, branch_js_1.getChangeReplaceType)(args) === "transactionCommit") {
                        this.submitCommit(args.newCommits[0], this.schemaAndPolicy);
                    }
                    break;
                default:
                    break;
            }
        });
        const revisionTagCodec = new index_js_1.RevisionTagCodec(runtime.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.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(runtime.idCompressor), options, formatOptions.message);
        this.commitEnricher = enricher;
    }
    // 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) {
        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) {
            summarizableBuilder.addWithStats(s.key, s.getAttachSummary((contents) => serializer.stringify(contents, this.handle), undefined, undefined, telemetryContext, incrementalSummaryContext));
        }
        builder.addWithStats(summarizablesTreeKey, summarizableBuilder.getSummaryTree());
        return builder.getSummaryTree();
    }
    async loadCore(services) {
        const loadSummaries = this.summarizables.map(async (summaryElement) => summaryElement.load(scopeStorageService(services, summarizablesTreeKey, summaryElement.key), (contents) => this.serializer.parse(contents)));
        await Promise.all(loadSummaries);
    }
    /**
     * 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 = false) {
        (0, internal_1.assert)(
        // Edits should not be submitted until all transactions finish
        !this.getLocalBranch().isTransacting() || isResubmit, 0x68b /* Unexpected edit submitted during transaction */);
        (0, internal_1.assert)(this.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 = (0, index_js_2.brand)(this.detachedRevision + 1);
            this.detachedRevision = newRevision;
            this.editManager.addSequencedChange({ ...commit, sessionId: this.editManager.localSessionId }, newRevision, this.detachedRevision);
            this.editManager.advanceMinimumSequenceNumber(newRevision);
            return undefined;
        }
        let enrichedCommit;
        if (this.commitEnricher !== undefined) {
            if (isResubmit) {
                (0, internal_1.assert)(this.commitEnricher.isInResubmitPhase, 0x95b /* Invalid resubmit outside of resubmit phase */);
            }
            else {
                (0, internal_1.assert)(!this.commitEnricher.isInResubmitPhase, 0x95c /* Invalid enrichment call during resubmit phase */);
            }
            enrichedCommit = this.commitEnricher.enrichCommit(commit);
        }
        else {
            enrichedCommit = commit;
        }
        const message = this.messageCodec.encode({
            commit: enrichedCommit,
            sessionId: this.editManager.localSessionId,
        }, {
            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,
        });
        return enrichedCommit;
    }
    processCore(message, local, localOpMetadata) {
        // Empty context object is passed in, as our decode function is schema-agnostic.
        const { commit, sessionId } = this.messageCodec.decode(message.contents, {});
        this.editManager.addSequencedChange({ ...commit, sessionId }, (0, index_js_2.brand)(message.sequenceNumber), (0, index_js_2.brand)(message.referenceSequenceNumber));
        this.commitEnricher?.onSequencedCommitApplied(local);
        this.editManager.advanceMinimumSequenceNumber((0, index_js_2.brand)(message.minimumSequenceNumber));
    }
    /**
     * @returns the head commit of the root local branch
     */
    getLocalBranch() {
        return this.editManager.localBranch;
    }
    onDisconnect() { }
    didAttach() {
        if (this.detachedRevision !== undefined) {
            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), {});
        const [commit] = this.editManager.findLocalCommit(revision);
        // If a resubmit phase is not already in progress, then this must be the first commit of a new resubmit phase.
        if (this.commitEnricher?.isInResubmitPhase === false) {
            const toResubmit = this.editManager.getLocalCommits();
            (0, internal_1.assert)(commit === toResubmit[0], 0x95d /* Resubmit phase should start with the oldest local commit */);
            this.commitEnricher.prepareForResubmit(toResubmit);
        }
        (0, internal_1.assert)(isClonableSchemaPolicy(localOpMetadata), 0x95e /* Local metadata must contain schema and policy. */);
        this.submitCommit(commit, localOpMetadata, true);
    }
    applyStashedOp(content) {
        (0, internal_1.assert)(!this.getLocalBranch().isTransacting(), 0x674 /* Unexpected transaction is open while applying stashed ops */);
        // Empty context object is passed in, as our decode function is schema-agnostic.
        const { commit: { revision, change }, } = this.messageCodec.decode(content, {});
        this.editManager.localBranch.apply(change, revision);
    }
    getGCData(fullGC) {
        const gcNodes = super.getGCData(fullGC).gcNodes;
        for (const s of this.summarizables) {
            for (const [id, routes] of Object.entries(s.getGCData(fullGC).gcNodes)) {
                gcNodes[id] ?? (gcNodes[id] = []);
                for (const route of routes) {
                    gcNodes[id].push(route);
                }
            }
        }
        return {
            gcNodes,
        };
    }
}
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