"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 context for any telemetry logs/errors emitted
     */
    constructor(summarizables, changeFamily, options, formatOptions, 
    // Base class arguments
    id, runtime, attributes, telemetryContextPrefix, schemaAndPolicy) {
        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 = schemaAndPolicy;
        (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);
                    }
                    break;
                case "replace":
                    if ((0, branch_js_1.getChangeReplaceType)(args) === "transactionCommit") {
                        this.submitCommit(args.newCommits[0]);
                    }
                    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);
    }
    // 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
     */
    submitCommit(commit, isResubmit = false) {
        // Edits should not be submitted until all transactions finish
        (0, internal_1.assert)(!this.getLocalBranch().isTransacting() || isResubmit, 0x68b /* Unexpected edit submitted during transaction */);
        // 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);
        }
        // Don't submit the op if it is not attached
        if (!this.isAttached()) {
            return;
        }
        const message = this.messageCodec.encode({
            commit,
            sessionId: this.editManager.localSessionId,
        }, {
            schema: this.schemaAndPolicy ?? undefined,
        });
        this.submitLocalMessage(message);
    }
    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.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(content, {});
        const [commit] = this.editManager.findLocalCommit(revision);
        this.submitCommit(commit, 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;
/**
 * 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