"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 core_utils_1 = require("@fluidframework/core-utils");
const runtime_utils_1 = require("@fluidframework/runtime-utils");
const shared_object_base_1 = require("@fluidframework/shared-object-base");
const util_1 = require("../util");
const branch_1 = require("./branch");
const editManagerSummarizer_1 = require("./editManagerSummarizer");
const editManager_1 = require("./editManager");
const messageCodecs_1 = require("./messageCodecs");
// TODO: How should the format version be determined?
const formatVersion = 0;
// 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 shared_object_base_1.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, 
    // Base class arguments
    id, runtime, attributes, telemetryContextPrefix) {
        super(id, runtime, attributes, telemetryContextPrefix);
        /** Iff false, calls to `submitOp` will have no effect */
        this.submitOps = true;
        /**
         * The sequence number that this instance is at.
         * This is 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_1.minimumPossibleSequenceNumber;
        /**
         * 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.
         */
        // TODO: Change this type to be the Session ID type provided by the IdCompressor when available.
        const localSessionId = (0, util_1.generateStableId)();
        this.editManager = new editManager_1.EditManager(changeFamily, localSessionId);
        this.editManager.localBranch.on("afterChange", (args) => {
            const { type } = args;
            switch (type) {
                case "append":
                    for (const c of args.newCommits) {
                        if (!this.getLocalBranch().isTransacting()) {
                            this.submitCommit(c);
                        }
                    }
                    break;
                case "replace":
                    if ((0, branch_1.getChangeReplaceType)(args) === "transactionCommit") {
                        if (!this.getLocalBranch().isTransacting()) {
                            this.submitCommit(args.newCommits[0]);
                        }
                    }
                    break;
                default:
                    break;
            }
        });
        this.summarizables = [
            new editManagerSummarizer_1.EditManagerSummarizer(this.editManager, options),
            ...summarizables,
        ];
        (0, core_utils_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_1.makeMessageCodec)(changeFamily.codecs.resolve(formatVersion).json, options);
    }
    // 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) {
        const builder = new runtime_utils_1.SummaryTreeBuilder();
        const summarizableBuilder = new runtime_utils_1.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));
        }
        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) {
        if (!this.submitOps) {
            return;
        }
        // Edits should not be submitted until all transactions finish
        (0, core_utils_1.assert)(!this.getLocalBranch().isTransacting(), 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, util_1.brand)(this.detachedRevision + 1);
            this.detachedRevision = newRevision;
            this.editManager.addSequencedChange({ ...commit, sessionId: this.editManager.localSessionId }, newRevision, this.detachedRevision);
        }
        const message = this.messageCodec.encode({
            commit,
            sessionId: this.editManager.localSessionId,
        });
        this.submitLocalMessage(this.serializer.encode(message, this.handle));
    }
    processCore(message, local, localOpMetadata) {
        const contents = this.serializer.decode(message.contents);
        const { commit, sessionId } = this.messageCodec.decode(contents);
        this.editManager.addSequencedChange({ ...commit, sessionId }, (0, util_1.brand)(message.sequenceNumber), (0, util_1.brand)(message.referenceSequenceNumber));
        this.editManager.advanceMinimumSequenceNumber((0, util_1.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) {
        const { commit: { revision }, } = this.messageCodec.decode(content);
        const [commit] = this.editManager.findLocalCommit(revision);
        this.submitCommit(commit);
    }
    applyStashedOp(content) {
        (0, core_utils_1.assert)(!this.getLocalBranch().isTransacting(), 0x674 /* Unexpected transaction is open while applying stashed ops */);
        const { commit: { revision, change }, } = this.messageCodec.decode(content);
        this.submitOps = false;
        this.editManager.localBranch.apply(change, revision);
        this.submitOps = true;
        return;
    }
    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