"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.SharedTreeFactory = exports.defaultSharedTreeOptions = exports.ForestType = exports.SharedTree = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const codec_1 = require("../codec");
const core_1 = require("../core");
const shared_tree_core_1 = require("../shared-tree-core");
const feature_libraries_1 = require("../feature-libraries");
const events_1 = require("../events");
const util_1 = require("../util");
const class_tree_1 = require("../class-tree");
const schematizedTree_1 = require("./schematizedTree");
const treeCheckout_1 = require("./treeCheckout");
const treeView_1 = require("./treeView");
/**
 * Shared tree, configured with a good set of indexes and field kinds which will maintain compatibility over time.
 *
 * TODO: detail compatibility requirements.
 */
class SharedTree extends shared_tree_core_1.SharedTreeCore {
    constructor(id, runtime, attributes, optionsParam, telemetryContextPrefix) {
        const options = { ...exports.defaultSharedTreeOptions, ...optionsParam };
        const schema = new core_1.InMemoryStoredSchemaRepository();
        const forest = options.forest === ForestType.Optimized
            ? (0, feature_libraries_1.buildChunkedForest)((0, feature_libraries_1.makeTreeChunker)(schema, feature_libraries_1.defaultSchemaPolicy))
            : (0, feature_libraries_1.buildForest)();
        const removedRoots = (0, core_1.makeDetachedFieldIndex)("repair", options);
        const schemaSummarizer = new feature_libraries_1.SchemaSummarizer(runtime, schema, options);
        const forestSummarizer = new feature_libraries_1.ForestSummarizer(forest, schema, feature_libraries_1.defaultSchemaPolicy, options.summaryEncodeType, options);
        const removedRootsSummarizer = new feature_libraries_1.DetachedFieldIndexSummarizer(removedRoots);
        const defaultChangeFamily = new feature_libraries_1.DefaultChangeFamily(options);
        const changeFamily = (0, feature_libraries_1.makeMitigatedChangeFamily)(defaultChangeFamily, feature_libraries_1.DefaultChangeFamily.emptyChange, (error) => {
            // TODO:6344 Add telemetry for these errors.
            // Rethrowing the error has a different effect depending on the context in which the
            // ChangeFamily was invoked:
            // - If the ChangeFamily was invoked as part of incoming op processing, rethrowing the error
            // will cause the runtime to disconnect the client, log a severe error, and not reconnect.
            // This will not cause the host application to crash because it is not on the stack at that time.
            // TODO: let the host application know that the client is now disconnected.
            // - If the ChangeFamily was invoked as part of dealing with a local change, rethrowing the
            // error will cause the host application to crash. This is not ideal, but is better than
            // letting the application either send an invalid change to the server or allowing the
            // application to continue working when its local branches contain edits that cannot be
            // reflected in its views.
            // The best course of action for a host application in such a state is to restart.
            // TODO: let the host application know about this situation and provide a way to
            // programmatically reload the SharedTree container.
            throw error;
        });
        super([schemaSummarizer, forestSummarizer, removedRootsSummarizer], changeFamily, options, id, runtime, attributes, telemetryContextPrefix);
        /**
         * Creating multiple editable tree contexts for the same branch, and thus with the same underlying AnchorSet does not work due to how TreeNode caching works.
         * This flag is used to detect if one already exists for the main branch and error if creating a second.
         * THis should catch most accidental violations of this restriction but there are still ways to create two conflicting contexts (for example calling constructing one manually).
         *
         * TODO:
         * 1. API docs need to reflect this limitation or the limitation has to be removed.
         */
        this.hasView2 = false;
        this.storedSchema = new feature_libraries_1.SchemaEditor(schema, (op) => this.submitLocalMessage(op), options);
        this._events = (0, events_1.createEmitter)();
        this.view = (0, treeCheckout_1.createTreeCheckout)({
            branch: this.getLocalBranch(),
            changeFamily,
            // TODO:
            // This passes in a version of schema thats not wrapped with the editor.
            // This allows editing schema on the view without sending ops, which is incorrect behavior.
            schema,
            forest,
            events: this._events,
            removedRoots,
        });
    }
    requireSchema(schema, onSchemaIncompatible, nodeKeyManager, nodeKeyFieldKey) {
        (0, core_utils_1.assert)(this.hasView2 === false, 0x7f1 /* Cannot create second view from tree. */);
        const viewSchema = new feature_libraries_1.ViewSchema(feature_libraries_1.defaultSchemaPolicy, {}, schema);
        const compatibility = viewSchema.checkCompatibility(this.storedSchema);
        if (compatibility.write !== core_1.Compatibility.Compatible ||
            compatibility.read !== core_1.Compatibility.Compatible) {
            return undefined;
        }
        this.hasView2 = true;
        const view2 = new treeView_1.CheckoutFlexTreeView(this.view, schema, nodeKeyManager ?? (0, feature_libraries_1.createNodeKeyManager)(this.runtime.idCompressor), nodeKeyFieldKey ?? (0, util_1.brand)(feature_libraries_1.nodeKeyFieldKey), () => {
            (0, core_utils_1.assert)(this.hasView2, 0x7f2 /* unexpected dispose */);
            this.hasView2 = false;
        });
        const onSchemaChange = () => {
            const compatibilityInner = viewSchema.checkCompatibility(this.storedSchema);
            if (compatibilityInner.write !== core_1.Compatibility.Compatible ||
                compatibilityInner.read !== core_1.Compatibility.Compatible) {
                view2[util_1.disposeSymbol]();
                onSchemaIncompatible();
                return false;
            }
            else {
                return true;
            }
        };
        (0, schematizedTree_1.afterSchemaChanges)(this._events, this.storedSchema, onSchemaChange);
        return view2;
    }
    contentSnapshot() {
        const cursor = this.view.forest.allocateCursor();
        try {
            (0, core_1.moveToDetachedField)(this.view.forest, cursor);
            return {
                schema: new core_1.InMemoryStoredSchemaRepository(this.storedSchema),
                tree: (0, feature_libraries_1.jsonableTreeFromFieldCursor)(cursor),
            };
        }
        finally {
            cursor.free();
        }
    }
    schematizeInternal(config, nodeKeyManager, nodeKeyFieldKey) {
        (0, core_utils_1.assert)(this.hasView2 === false, 0x7f3 /* Cannot create second view from tree. */);
        // TODO:
        // When this becomes a more proper out of schema adapter, editing should be made lazy.
        // This will improve support for readonly documents, cross version collaboration and attribution.
        // Check for empty.
        if (this.view.forest.isEmpty && (0, core_1.schemaDataIsEmpty)(this.storedSchema)) {
            this.view.transaction.start();
            (0, schematizedTree_1.initializeContent)(this.storedSchema, config.schema, () => {
                const field = { field: core_1.rootFieldKey, parent: undefined };
                const content = (0, feature_libraries_1.normalizeNewFieldContent)({ schema: this.storedSchema }, this.storedSchema.rootFieldSchema, config.initialTree);
                switch (this.storedSchema.rootFieldSchema.kind.identifier) {
                    case feature_libraries_1.FieldKinds.optional.identifier: {
                        const fieldEditor = this.editor.optionalField(field);
                        (0, core_utils_1.assert)(content.length <= 1, 0x7f4 /* optional field content should normalize at most one item */);
                        fieldEditor.set(content.length === 0 ? undefined : content[0], true);
                        break;
                    }
                    case feature_libraries_1.FieldKinds.sequence.identifier: {
                        const fieldEditor = this.editor.sequenceField(field);
                        // TODO: should do an idempotent edit here.
                        fieldEditor.insert(0, content);
                        break;
                    }
                    default: {
                        (0, util_1.fail)("unexpected root field kind during initialize");
                    }
                }
            });
            this.view.transaction.commit();
        }
        (0, schematizedTree_1.schematize)(this.view.events, this.storedSchema, config);
        return (this.requireSchema(config.schema, () => (0, util_1.fail)("schema incompatible"), nodeKeyManager, nodeKeyFieldKey) ?? (0, util_1.fail)("Schematize failed"));
    }
    schematize(config) {
        const flexConfig = (0, class_tree_1.toFlexConfig)(config);
        const view = this.schematizeInternal(flexConfig);
        return new class_tree_1.WrapperTreeView(view);
    }
    /**
     * TODO: Shared tree needs a pattern for handling non-changeset operations.
     * Whatever pattern is adopted should probably also handle multiple versions of changeset operations.
     * A single top level enum listing all ops (including their different versions),
     * with at least fine grained enough detail to direct them to the correct subsystem would be a good approach.
     * The current use-case (with an op applying to a specific index) is a temporary hack,
     * and its not clear how it would fit into such a system if implemented in shared-tree-core:
     * maybe op dispatch is part of the shared-tree level?
     */
    processCore(message, local, localOpMetadata) {
        // TODO: Get rid of this `as any`. There should be a better way to narrow the type of message.contents.
        if (!this.storedSchema.tryHandleOp(message.contents)) {
            super.processCore(message, local, localOpMetadata);
        }
    }
    reSubmitCore(content, localOpMetadata) {
        if (!this.storedSchema.tryResubmitOp(content)) {
            super.reSubmitCore(content, localOpMetadata);
        }
    }
    applyStashedOp(content) {
        if (!this.storedSchema.tryApplyStashedOp(content)) {
            return super.applyStashedOp(content);
        }
    }
    async loadCore(services) {
        await super.loadCore(services);
        this._events.emit("afterBatch");
    }
}
exports.SharedTree = SharedTree;
/**
 * Used to distinguish between different forest types.
 * @alpha
 */
var ForestType;
(function (ForestType) {
    /**
     * The "ObjectForest" forest type.
     */
    ForestType[ForestType["Reference"] = 0] = "Reference";
    /**
     * The "ChunkedForest" forest type.
     */
    ForestType[ForestType["Optimized"] = 1] = "Optimized";
})(ForestType || (exports.ForestType = ForestType = {}));
// TODO: The default summaryEncodeType is set to Uncompressed as there are many out of schema tests that break when using Compressed.
// This should eventually be changed to use Compressed as the default tree compression strategy so production gets the compressed format.
exports.defaultSharedTreeOptions = {
    jsonValidator: codec_1.noopValidator,
    forest: ForestType.Reference,
    summaryEncodeType: feature_libraries_1.TreeCompressionStrategy.Uncompressed,
};
/**
 * A channel factory that creates {@link ISharedTree}s.
 * @alpha
 */
class SharedTreeFactory {
    constructor(options = {}) {
        this.options = options;
        this.type = "https://graph.microsoft.com/types/tree";
        this.attributes = {
            type: this.type,
            snapshotFormatVersion: "0.0.0",
            packageVersion: "0.0.0",
        };
    }
    async load(runtime, id, services, channelAttributes) {
        const tree = new SharedTree(id, runtime, channelAttributes, this.options, "SharedTree");
        await tree.load(services);
        return tree;
    }
    create(runtime, id) {
        const tree = new SharedTree(id, runtime, this.attributes, this.options, "SharedTree");
        tree.initializeLocal();
        return tree;
    }
}
exports.SharedTreeFactory = SharedTreeFactory;
//# sourceMappingURL=sharedTree.js.map