"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalDetachedFluidDataStoreContext = exports.LocalFluidDataStoreContext = exports.LocalFluidDataStoreContextBase = exports.RemoteFluidDataStoreContext = exports.FluidDataStoreContext = exports.createAttributesBlob = void 0;
const client_utils_1 = require("@fluid-internal/client-utils");
const container_definitions_1 = require("@fluidframework/container-definitions");
const internal_1 = require("@fluidframework/container-definitions/internal");
const internal_2 = require("@fluidframework/core-utils/internal");
const internal_3 = require("@fluidframework/driver-utils/internal");
const internal_4 = require("@fluidframework/runtime-definitions/internal");
const internal_5 = require("@fluidframework/runtime-utils/internal");
const internal_6 = require("@fluidframework/telemetry-utils/internal");
const deltaManagerProxies_js_1 = require("./deltaManagerProxies.js");
const runtimeLayerCompatState_js_1 = require("./runtimeLayerCompatState.js");
const index_js_1 = require("./summary/index.js");
function createAttributes(pkg, isRootDataStore) {
    const stringifiedPkg = JSON.stringify(pkg);
    return {
        pkg: stringifiedPkg,
        summaryFormatVersion: 2,
        isRootDataStore,
    };
}
function createAttributesBlob(pkg, isRootDataStore) {
    const attributes = createAttributes(pkg, isRootDataStore);
    return new internal_3.BlobTreeEntry(index_js_1.dataStoreAttributesBlobName, JSON.stringify(attributes));
}
exports.createAttributesBlob = createAttributesBlob;
/**
 * Eventually we should remove the delta manger from being exposed to Datastore runtimes via the context. However to remove that exposure we need to add new
 * features, and those features themselves need forward and back compat. This proxy is here to enable that back compat. Each feature this proxy is used to
 * support should be listed below, and as layer compat support goes away for those feature, we should also remove them from this proxy, with the eventual goal
 * of completely removing this proxy.
 *
 * - Everything regarding readonly is to support older datastore runtimes which do not have the setReadonly function, so they must get their readonly state via the delta manager.
 *
 */
class ContextDeltaManagerProxy extends deltaManagerProxies_js_1.BaseDeltaManagerProxy {
    constructor(base, isReadOnly) {
        super(base, {
            onReadonly: () => {
                /* readonly is controlled from the context which calls setReadonly */
            },
        });
        this.isReadOnly = isReadOnly;
    }
    get readOnlyInfo() {
        const readonly = this.isReadOnly();
        if (readonly === this.deltaManager.readOnlyInfo.readonly) {
            return this.deltaManager.readOnlyInfo;
        }
        else {
            return readonly === true
                ? {
                    readonly,
                    forced: false,
                    permissions: undefined,
                    storageOnly: false,
                }
                : { readonly };
        }
    }
    /**
     * Called by the owning datastore context to emit the readonly
     * event on the delta manger that is projected down to the datastore
     * runtime. This state may not align with that of the true delta
     * manager if the context wishes to control the read only state
     * differently than the delta manager itself.
     */
    emitReadonly() {
        this.emit("readonly", this.isReadOnly());
    }
}
/**
 * {@link IFluidDataStoreContext} for the implementations of {@link IFluidDataStoreChannel} which powers the {@link IDataStore}s.
 */
class FluidDataStoreContext extends client_utils_1.TypedEventEmitter {
    get packagePath() {
        (0, internal_2.assert)(this.pkg !== undefined, 0x139 /* "Undefined package path" */);
        return this.pkg;
    }
    get options() {
        return this.parentContext.options;
    }
    get clientId() {
        return this.parentContext.clientId;
    }
    get clientDetails() {
        return this.parentContext.clientDetails;
    }
    get baseLogger() {
        return this.parentContext.baseLogger;
    }
    get deltaManager() {
        return this._contextDeltaManagerProxy;
    }
    get connected() {
        return this.parentContext.connected;
    }
    get IFluidHandleContext() {
        return this.parentContext.IFluidHandleContext;
    }
    get containerRuntime() {
        return this._containerRuntime;
    }
    get isLoaded() {
        return this.loaded;
    }
    get baseSnapshot() {
        return this._baseSnapshot;
    }
    get idCompressor() {
        return this.parentContext.idCompressor;
    }
    get disposed() {
        return this._disposed;
    }
    get tombstoned() {
        return this._tombstoned;
    }
    get attachState() {
        return this._attachState;
    }
    get IFluidDataStoreRegistry() {
        return this.registry;
    }
    /**
     * The compatibility details of the Runtime layer that is exposed to the DataStore layer
     * for validating DataStore-Runtime compatibility.
     */
    get ILayerCompatDetails() {
        return runtimeLayerCompatState_js_1.runtimeCompatDetailsForDataStore;
    }
    /**
     * A datastore is considered as root if it
     * 1. is root in memory - see isInMemoryRoot
     * 2. is root as part of the base snapshot that the datastore loaded from
     * @returns whether a datastore is root
     */
    async isRoot(aliasedDataStores) {
        if (this.isInMemoryRoot()) {
            return true;
        }
        // This if is a performance optimization.
        // We know that if the base snapshot is omitted, then the isRootDataStore flag is not set.
        // That means we can skip the expensive call to getInitialSnapshotDetails for virtualized datastores,
        // and get the information from the alias map directly.
        // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
        if (aliasedDataStores !== undefined && this.baseSnapshot?.omitted === true) {
            return aliasedDataStores.has(this.id);
        }
        const snapshotDetails = await this.getInitialSnapshotDetails();
        return snapshotDetails.isRootDataStore;
    }
    /**
     * There are 3 states where isInMemoryRoot needs to be true
     * 1. when a datastore becomes aliased. This can happen for both remote and local datastores
     * 2. when a datastore is created locally as root
     * 3. when a datastore is created locally as root and is rehydrated
     * @returns whether a datastore is root in memory
     */
    isInMemoryRoot() {
        return this._isInMemoryRoot;
    }
    /**
     * Returns the count of pending messages that are stored until the data store is realized.
     */
    get pendingCount() {
        return this.pendingMessagesState?.pendingCount ?? 0;
    }
    constructor(props, existing, isLocalDataStore, makeLocallyVisibleFn) {
        super();
        this.existing = existing;
        this.isLocalDataStore = isLocalDataStore;
        this.makeLocallyVisibleFn = makeLocallyVisibleFn;
        this.isStagingMode = false;
        this.isReadOnly = () => (this.isStagingMode && this.channel?.policies?.readonlyInStagingMode === true) ||
            this.parentContext.isReadOnly();
        this._disposed = false;
        /**
         * A Tombstoned object has been unreferenced long enough that GC knows it won't be referenced again.
         * Tombstoned objects are eventually deleted by GC.
         */
        this._tombstoned = false;
        /**
         * If true, throw an error when a tombstone data store is used.
         * @deprecated NOT SUPPORTED - hardcoded to return false since it's deprecated.
         */
        this.gcThrowOnTombstoneUsage = false;
        /**
         * @deprecated NOT SUPPORTED - hardcoded to return false since it's deprecated.
         */
        this.gcTombstoneEnforcementAllowed = false;
        /**
         * If true, this means that this data store context and its children have been removed from the runtime
         */
        this.deleted = false;
        this.detachedRuntimeCreation = false;
        this.loaded = false;
        /**
         * Tracks the messages for this data store that are sent while it's not loaded
         */
        this.pendingMessagesState = {
            messageCollections: [],
            pendingCount: 0,
        };
        this._isInMemoryRoot = false;
        this._containerRuntime = props.parentContext.containerRuntime;
        this.parentContext = props.parentContext;
        this.minVersionForCollab = props.parentContext.minVersionForCollab;
        this.id = props.id;
        this.storage = props.storage;
        this.scope = props.scope;
        this.pkg = props.pkg;
        this.loadingGroupId = props.loadingGroupId;
        // URIs use slashes as delimiters. Handles use URIs.
        // Thus having slashes in types almost guarantees trouble down the road!
        (0, internal_2.assert)(!this.id.includes("/"), 0x13a /* Data store ID contains slash */);
        this._attachState =
            this.parentContext.attachState !== container_definitions_1.AttachState.Detached && this.existing
                ? this.parentContext.attachState
                : container_definitions_1.AttachState.Detached;
        this.summarizerNode = props.createSummarizerNodeFn(async (fullTree, trackState, telemetryContext) => this.summarizeInternal(fullTree, trackState, telemetryContext), async (fullGC) => this.getGCDataInternal(fullGC));
        this.mc = (0, internal_6.createChildMonitoringContext)({
            logger: this.baseLogger,
            namespace: "FluidDataStoreContext",
            properties: {
                all: (0, internal_6.tagCodeArtifacts)({
                    fluidDataStoreId: this.id,
                    // The package name is a getter because `this.pkg` may not be initialized during construction.
                    // For data stores loaded from summary, it is initialized during data store realization.
                    fullPackageName: () => this.pkg?.join("/"),
                }),
            },
        });
        this.thresholdOpsCounter = new internal_6.ThresholdCounter(FluidDataStoreContext.pendingOpsCountThreshold, this.mc.logger);
        // By default, a data store can log maximum 10 local changes telemetry in summarizer.
        this.localChangesTelemetryCount =
            this.mc.config.getNumber("Fluid.Telemetry.LocalChangesTelemetryCount") ?? 10;
        (0, internal_2.assert)((0, internal_1.isIDeltaManagerFull)(this.parentContext.deltaManager), 0xb83 /* Invalid delta manager */);
        this._contextDeltaManagerProxy = new ContextDeltaManagerProxy(this.parentContext.deltaManager, () => this.isReadOnly());
    }
    dispose() {
        if (this._disposed) {
            return;
        }
        this._disposed = true;
        // Dispose any pending runtime after it gets fulfilled
        // Errors are logged where this.channelP is consumed/generated (realizeCore(), bindRuntime())
        if (this.channelP) {
            this.channelP
                .then((runtime) => {
                runtime.dispose();
            })
                .catch((error) => { });
        }
        this._contextDeltaManagerProxy.dispose();
    }
    /**
     * When delete is called, that means that the data store is permanently removed from the runtime, and will not show up in future summaries
     * This function is called to prevent ops from being generated from this data store once it has been deleted. Furthermore, this data store
     * should not receive any ops/signals.
     */
    delete() {
        this.deleted = true;
    }
    setTombstone(tombstone) {
        if (this.tombstoned === tombstone) {
            return;
        }
        this._tombstoned = tombstone;
    }
    /**
     * Throw a {@link LoggingError} indicating that {@link factoryFromPackagePath} failed.
     */
    factoryFromPackagePathError(reason, failedPkgPath, fullPackageName) {
        throw new internal_6.LoggingError(reason, (0, internal_6.tagCodeArtifacts)({
            failedPkgPath,
            packagePath: fullPackageName?.join("/"),
        }));
    }
    async realize() {
        (0, internal_2.assert)(!this.detachedRuntimeCreation, 0x13d /* "Detached runtime creation on realize()" */);
        // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- using ??= could change behavior if value is falsy
        if (!this.channelP) {
            this.channelP = this.realizeCore(this.existing).catch((error) => {
                const errorWrapped = internal_6.DataProcessingError.wrapIfUnrecognized(error, "realizeFluidDataStoreContext");
                errorWrapped.addTelemetryProperties((0, internal_6.tagCodeArtifacts)({
                    fullPackageName: this.pkg?.join("/"),
                    fluidDataStoreId: this.id,
                }));
                this.mc.logger.sendErrorEvent({ eventName: "RealizeError" }, errorWrapped);
                throw errorWrapped;
            });
        }
        return this.channelP;
    }
    /**
     * Gets the factory that would be used to instantiate this data store by calling `instantiateDataStore` based on {@link pkg}.
     * @remarks
     * Also populates {@link registry}.
     *
     * Must be called after {@link pkg} is set, and only called once.
     *
     * @see {@link @fluidframework/container-runtime-definitions#IContainerRuntimeBase.createDataStore}.
     * @see {@link FluidDataStoreContext.pkg}.
     */
    async factoryFromPackagePath() {
        const path = this.pkg;
        if (path === undefined) {
            this.factoryFromPackagePathError("packages is undefined");
        }
        let entry;
        let registry = this.parentContext.IFluidDataStoreRegistry;
        let lastIdentifier;
        // Follow the path, looking up each identifier in the registry along the way:
        for (const identifier of path) {
            if (!registry) {
                this.factoryFromPackagePathError("No registry for package", lastIdentifier, path);
            }
            lastIdentifier = identifier;
            entry = registry.getSync?.(identifier) ?? (await registry.get(identifier));
            if (!entry) {
                this.factoryFromPackagePathError("Registry does not contain entry for the package", identifier, path);
            }
            registry = entry.IFluidDataStoreRegistry;
        }
        const factory = entry?.IFluidDataStoreFactory;
        if (factory === undefined) {
            this.factoryFromPackagePathError("Can't find factory for package", lastIdentifier, path);
        }
        (0, internal_2.assert)(this.registry === undefined, 0x157 /* "datastore registry already attached" */);
        this.registry = registry;
        return factory;
    }
    createChildDataStore(childFactory) {
        const maybe = this.registry?.getSync?.(childFactory.type);
        const isUndefined = maybe === undefined;
        const diffInstance = maybe?.IFluidDataStoreFactory !== childFactory;
        if (isUndefined || diffInstance) {
            throw new internal_6.UsageError("The provided factory instance must be synchronously available as a child of this datastore", { isUndefined, diffInstance });
        }
        if (childFactory?.createDataStore === undefined) {
            throw new internal_6.UsageError("createDataStore must exist on the provided factory", {
                noCreateDataStore: true,
            });
        }
        const context = this._containerRuntime.createDetachedDataStore([
            ...this.packagePath,
            childFactory.type,
        ]);
        (0, internal_2.assert)(context instanceof LocalDetachedFluidDataStoreContext, 0xa89 /* must be a LocalDetachedFluidDataStoreContext */);
        const created = childFactory.createDataStore(context);
        context.unsafe_AttachRuntimeSync(created.runtime);
        return created;
    }
    async realizeCore(existing) {
        const details = await this.getInitialSnapshotDetails();
        // Base snapshot is the baseline where pending ops are applied to.
        // It is important that this be in sync with the pending ops, and also
        // that it is set here, before bindRuntime is called.
        this._baseSnapshot = details.snapshot;
        this.baseSnapshotSequenceNumber = details.sequenceNumber;
        (0, internal_2.assert)(this.pkg === details.pkg, 0x13e /* "Unexpected package path" */);
        const factory = await this.factoryFromPackagePath();
        const channel = await factory.instantiateDataStore(this, existing);
        (0, internal_2.assert)(channel !== undefined, 0x140 /* "undefined channel on datastore context" */);
        await this.bindRuntime(channel, existing);
        // This data store may have been disposed before the channel is created during realization. If so,
        // dispose the channel now.
        if (this.disposed) {
            channel.dispose();
        }
        return channel;
    }
    /**
     * Notifies this object about changes in the connection state.
     * @param value - New connection state.
     * @param clientId - ID of the client. Its old ID when in disconnected state and
     * its new client ID when we are connecting or connected.
     */
    setConnectionState(connected, clientId) {
        // ConnectionState should not fail in tombstone mode as this is internally run
        this.verifyNotClosed("setConnectionState", false /* checkTombstone */);
        // Connection events are ignored if the store is not yet loaded
        if (!this.loaded) {
            return;
        }
        (0, internal_2.assert)(this.connected === connected, 0x141 /* "Unexpected connected state" */);
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        this.channel.setConnectionState(connected, clientId);
    }
    notifyReadOnlyState() {
        this.verifyNotClosed("notifyReadOnlyState", false /* checkTombstone */);
        // These two calls achieve the same purpose, and are both needed for a time for back compat
        this.channel?.notifyReadOnlyState?.(this.isReadOnly());
        this._contextDeltaManagerProxy.emitReadonly();
    }
    /**
     * Updates the readonly state of the data store based on the staging mode.
     *
     * @param staging - A boolean indicating whether the container is in staging mode.
     * If true, the data store is set to readonly unless explicitly allowed by its policies.
     */
    notifyStagingMode(staging) {
        // If the `readonlyInStagingMode` policy is not explicitly set to `false`,
        // the data store is treated as readonly in staging mode.
        const oldReadOnlyState = this.isReadOnly();
        this.isStagingMode = staging;
        if (this.isReadOnly() !== oldReadOnlyState) {
            this.notifyReadOnlyState();
        }
    }
    /**
     * Process messages for this data store. The messages here are contiguous messages for this data store in a batch.
     * @param messageCollection - The collection of messages to process.
     */
    processMessages(messageCollection) {
        const { envelope, messagesContent, local } = messageCollection;
        const safeTelemetryProps = (0, internal_6.extractSafePropertiesFromMessage)(envelope);
        // Tombstone error is logged in garbage collector. So, set "checkTombstone" to false when calling
        // "verifyNotClosed" which logs tombstone errors.
        this.verifyNotClosed("process", false /* checkTombstone */, safeTelemetryProps);
        this.summarizerNode.recordChange(envelope);
        if (this.loaded) {
            (0, internal_2.assert)(this.channel !== undefined, 0xa68 /* Channel is not loaded */);
            this.channel.processMessages(messageCollection);
        }
        else {
            (0, internal_2.assert)(!local, 0x142 /* "local store channel is not loaded" */);
            (0, internal_2.assert)(this.pendingMessagesState !== undefined, 0xa69 /* pending messages queue is undefined */);
            this.pendingMessagesState.messageCollections.push({
                ...messageCollection,
                messagesContent: [...messagesContent],
            });
            this.pendingMessagesState.pendingCount += messagesContent.length;
            this.thresholdOpsCounter.sendIfMultiple("StorePendingOps", this.pendingMessagesState.pendingCount);
        }
    }
    processSignal(message, local) {
        this.verifyNotClosed("processSignal");
        // Signals are ignored if the store is not yet loaded
        if (!this.loaded) {
            return;
        }
        this.channel?.processSignal(message, local);
    }
    getQuorum() {
        return this.parentContext.getQuorum();
    }
    getAudience() {
        return this.parentContext.getAudience();
    }
    /**
     * Returns a summary at the current sequence number.
     * @param fullTree - true to bypass optimizations and force a full summary tree
     * @param trackState - This tells whether we should track state from this summary.
     * @param telemetryContext - summary data passed through the layers for telemetry purposes
     */
    async summarize(fullTree = false, trackState = true, telemetryContext) {
        return this.summarizerNode.summarize(fullTree, trackState, telemetryContext);
    }
    async summarizeInternal(fullTree, trackState, telemetryContext) {
        await this.realize();
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const summarizeResult = await this.channel.summarize(fullTree, trackState, telemetryContext);
        // Wrap dds summaries in .channels subtree.
        (0, index_js_1.wrapSummaryInChannelsTree)(summarizeResult);
        const pathPartsForChildren = [internal_4.channelsTreeName];
        // Add data store's attributes to the summary.
        const { pkg } = await this.getInitialSnapshotDetails();
        const isRoot = await this.isRoot();
        const attributes = createAttributes(pkg, isRoot);
        (0, internal_5.addBlobToSummary)(summarizeResult, index_js_1.dataStoreAttributesBlobName, JSON.stringify(attributes));
        // If we are not referenced, mark the summary tree as unreferenced. Also, update unreferenced blob
        // size in the summary stats with the blobs size of this data store.
        if (!this.summarizerNode.isReferenced()) {
            summarizeResult.summary.unreferenced = true;
            summarizeResult.stats.unreferencedBlobSize = summarizeResult.stats.totalBlobSize;
        }
        // Add loadingGroupId to the summary
        if (this.loadingGroupId !== undefined) {
            summarizeResult.summary.groupId = this.loadingGroupId;
        }
        return {
            ...summarizeResult,
            id: this.id,
            pathPartsForChildren,
        };
    }
    /**
     * Returns the data used for garbage collection. This includes a list of GC nodes that represent this data store
     * including any of its child channel contexts. Each node has a set of outbound routes to other GC nodes in the
     * document.
     * If there is no new data in this data store since the last summary, previous GC data is used.
     * If there is new data, the GC data is generated again (by calling getGCDataInternal).
     * @param fullGC - true to bypass optimizations and force full generation of GC data.
     */
    async getGCData(fullGC = false) {
        return this.summarizerNode.getGCData(fullGC);
    }
    /**
     * Generates data used for garbage collection. This is called when there is new data since last summary. It
     * realizes the data store and calls into each channel context to get its GC data.
     * @param fullGC - true to bypass optimizations and force full generation of GC data.
     */
    async getGCDataInternal(fullGC = false) {
        await this.realize();
        (0, internal_2.assert)(this.channel !== undefined, 0x143 /* "Channel should not be undefined when running GC" */);
        return this.channel.getGCData(fullGC);
    }
    /**
     * After GC has run, called to notify the data store of routes used in it. These are used for the following:
     * 1. To identify if this data store is being referenced in the document or not.
     * 2. To determine if it needs to re-summarize in case used routes changed since last summary.
     * 3. To notify child contexts of their used routes. This is done immediately if the data store is loaded.
     * Else, it is done by the data stores's summarizer node when child summarizer nodes are created.
     *
     * @param usedRoutes - The routes that are used in this data store.
     */
    updateUsedRoutes(usedRoutes) {
        // Update the used routes in this data store's summarizer node.
        this.summarizerNode.updateUsedRoutes(usedRoutes);
        // If the channel doesn't exist yet (data store is not realized), the summarizer node will update it
        // when it creates child nodes.
        if (!this.channel) {
            return;
        }
        // Remove the route to this data store, if it exists.
        const usedChannelRoutes = usedRoutes.filter((id) => {
            return id !== "/" && id !== "";
        });
        this.channel.updateUsedRoutes(usedChannelRoutes);
    }
    /**
     * Called when a new outbound reference is added to another node. This is used by garbage collection to identify
     * all references added in the system.
     *
     * @param fromPath - The absolute path of the node that added the reference.
     * @param toPath - The absolute path of the outbound node that is referenced.
     * @param messageTimestampMs - The timestamp of the message that added the reference.
     */
    addedGCOutboundRoute(fromPath, toPath, messageTimestampMs) {
        this.parentContext.addedGCOutboundRoute(fromPath, toPath, messageTimestampMs);
    }
    submitMessage(type, content, localOpMetadata) {
        this.verifyNotClosed("submitMessage");
        (0, internal_2.assert)(!!this.channel, 0x146 /* "Channel must exist when submitting message" */);
        // Readonly clients should not submit messages.
        this.identifyLocalChangeIfReadonly("DataStoreMessageWhileReadonly", type);
        this.parentContext.submitMessage(type, content, localOpMetadata);
    }
    /**
     * This is called from a SharedSummaryBlock that does not generate ops but only wants to be part of the summary.
     * It indicates that there is data in the object that needs to be summarized.
     * We will update the latestSequenceNumber of the summary tracker of this
     * store and of the object's channel.
     *
     * @param address - The address of the channel that is dirty.
     *
     */
    setChannelDirty(address) {
        this.verifyNotClosed("setChannelDirty");
        // Get the latest sequence number.
        const latestSequenceNumber = this.deltaManager.lastSequenceNumber;
        this.summarizerNode.invalidate(latestSequenceNumber);
        const channelSummarizerNode = this.summarizerNode.getChild(address);
        if (channelSummarizerNode) {
            channelSummarizerNode.invalidate(latestSequenceNumber); // TODO: lazy load problem?
        }
    }
    submitSignal(type, content, targetClientId) {
        this.verifyNotClosed("submitSignal");
        (0, internal_2.assert)(!!this.channel, 0x147 /* "Channel must exist on submitting signal" */);
        return this.parentContext.submitSignal(type, content, targetClientId);
    }
    /**
     * This is called by the data store channel when it becomes locally visible indicating that it is ready to become
     * globally visible now.
     */
    makeLocallyVisible() {
        (0, internal_2.assert)(this.channel !== undefined, 0x2cf /* "undefined channel on datastore context" */);
        this.makeLocallyVisibleFn();
    }
    processPendingOps(channel) {
        const baseSequenceNumber = this.baseSnapshotSequenceNumber ?? -1;
        (0, internal_2.assert)(this.pendingMessagesState !== undefined, 0xa6a /* pending messages queue is undefined */);
        for (const messageCollection of this.pendingMessagesState.messageCollections) {
            // Only process ops whose seq number is greater than snapshot sequence number from which it loaded.
            if (messageCollection.envelope.sequenceNumber > baseSequenceNumber) {
                channel.processMessages(messageCollection);
            }
        }
        this.thresholdOpsCounter.send("ProcessPendingOps", this.pendingMessagesState.pendingCount);
        this.pendingMessagesState = undefined;
    }
    completeBindingRuntime(channel) {
        // Validate that the DataStore is compatible with this Runtime.
        const maybeDataStoreCompatDetails = channel;
        (0, runtimeLayerCompatState_js_1.validateDatastoreCompatibility)(maybeDataStoreCompatDetails.ILayerCompatDetails, this.dispose.bind(this), this.mc);
        // And now mark the runtime active
        this.loaded = true;
        this.channel = channel;
        // Channel does not know when it's "live" (as in - starts to receive events in the system)
        // It may read current state of the system when channel was created, but it was not getting any updates
        // through creation process and could have missed events. So update it on current state.
        // Once this.loaded is set (above), it will stat receiving events.
        channel.setConnectionState(this.connected, this.clientId);
        // Freeze the package path to ensure that someone doesn't modify it when it is
        // returned in packagePath().
        Object.freeze(this.pkg);
    }
    async bindRuntime(channel, existing) {
        if (this.channel) {
            throw new Error("Runtime already bound");
        }
        (0, internal_2.assert)(!this.detachedRuntimeCreation, 0x148 /* "Detached runtime creation on runtime bind" */);
        (0, internal_2.assert)(this.pkg !== undefined, 0x14a /* "Undefined package path" */);
        if (!existing) {
            // Execute data store's entry point to make sure that for a local (aka detached from container) data store, the
            // entryPoint initialization function is called before the data store gets attached and potentially connected to
            // the delta stream, so it gets a chance to do things while the data store is still "purely local".
            // This preserves the behavior from before we introduced entryPoints, where the instantiateDataStore method
            // of data store factories tends to construct the data object (at least kick off an async method that returns
            // it); that code moved to the entryPoint initialization function, so we want to ensure it still executes
            // before the data store is attached.
            await channel.entryPoint.get();
        }
        this.processPendingOps(channel);
        this.completeBindingRuntime(channel);
    }
    async getAbsoluteUrl(relativeUrl) {
        if (this.attachState !== container_definitions_1.AttachState.Attached) {
            return undefined;
        }
        return this.parentContext.getAbsoluteUrl(relativeUrl);
    }
    // eslint-disable-next-line jsdoc/require-description
    /**
     * @deprecated Sets the datastore as root, for aliasing purposes: #7948
     * This method should not be used outside of the aliasing context.
     * It will be removed, as the source of truth for this flag will be the aliasing blob.
     */
    setInMemoryRoot() {
        this._isInMemoryRoot = true;
    }
    // eslint-disable-next-line jsdoc/require-description
    /**
     * @deprecated The functionality to get base GC details has been moved to summarizer node.
     */
    async getBaseGCDetails() {
        return {};
    }
    reSubmit(message, localOpMetadata, squash) {
        (0, internal_2.assert)(!!this.channel, 0x14b /* "Channel must exist when resubmitting ops" */);
        this.channel.reSubmit(message.type, message.content, localOpMetadata, squash);
    }
    rollback(message, localOpMetadata) {
        if (!this.channel) {
            throw new Error("Channel must exist when rolling back ops");
        }
        if (!this.channel.rollback) {
            throw new Error("Channel doesn't support rollback");
        }
        this.channel.rollback(message.type, message.content, localOpMetadata);
    }
    async applyStashedOp(contents) {
        if (!this.channel) {
            await this.realize();
        }
        (0, internal_2.assert)(!!this.channel, 0x14c /* "Channel must exist when rebasing ops" */);
        return this.channel.applyStashedOp(contents);
    }
    verifyNotClosed(callSite, checkTombstone = true, safeTelemetryProps = {}) {
        if (this.deleted) {
            const messageString = `Context is deleted! Call site [${callSite}]`;
            const error = internal_6.DataProcessingError.create(messageString, callSite, undefined /* sequencedMessage */, safeTelemetryProps);
            this.mc.logger.sendErrorEvent({
                eventName: "GC_Deleted_DataStore_Changed",
                callSite,
            }, error);
            throw error;
        }
        if (this._disposed) {
            throw new Error(`Context is closed! Call site [${callSite}]`);
        }
        if (checkTombstone && this.tombstoned) {
            const messageString = `Context is tombstoned! Call site [${callSite}]`;
            const error = internal_6.DataProcessingError.create(messageString, callSite, undefined /* sequencedMessage */, safeTelemetryProps, 30 /* stackTraceLimit */);
            this.mc.logger.sendTelemetryEvent({
                eventName: "GC_Tombstone_DataStore_Changed",
                category: "generic",
                callSite,
            }, error);
        }
    }
    /**
     * Readonly client, including summarizer, should not have local changes. These changes can become part of the summary and can break
     * eventual consistency. For example, the next summary (say at ref seq# 100) may contain these changes whereas
     * other clients that are up-to-date till seq# 100 may not have them yet.
     */
    identifyLocalChangeIfReadonly(eventName, type) {
        if (!this.isReadOnly() || this.localChangesTelemetryCount <= 0) {
            return;
        }
        // Log a telemetry if there are local changes in readonly. This will give us data on how often
        // this is happening and which data stores do this. The eventual goal is to disallow local changes
        // in the summarizer and the data will help us plan this.
        this.mc.logger.sendTelemetryEvent({
            eventName,
            type,
            isSummaryInProgress: this.summarizerNode.isSummaryInProgress?.(),
            stack: (0, internal_6.generateStack)(30),
            readonly: this.isReadOnly(),
            isStagingMode: this.isStagingMode,
        });
        this.localChangesTelemetryCount--;
    }
    getCreateChildSummarizerNodeFn(id, createParam) {
        return (summarizeInternal, getGCDataFn) => this.summarizerNode.createChild(summarizeInternal, id, createParam, undefined /* config */, getGCDataFn);
    }
    deleteChildSummarizerNode(id) {
        this.summarizerNode.deleteChild(id);
    }
    async uploadBlob(blob, signal) {
        return this.parentContext.uploadBlob(blob, signal);
    }
    getExtension(id, requirements, ...context) {
        return this.parentContext.getExtension(id, requirements, ...context);
    }
}
exports.FluidDataStoreContext = FluidDataStoreContext;
FluidDataStoreContext.pendingOpsCountThreshold = 1000;
/**
 * @internal
 */
class RemoteFluidDataStoreContext extends FluidDataStoreContext {
    constructor(props) {
        super(props, true /* existing */, false /* isLocalDataStore */, () => {
            throw new Error("Already attached");
        });
        // eslint-disable-next-line unicorn/consistent-function-scoping -- Property is defined once; no need to extract inner lambda
        this.initialSnapshotDetailsP = new internal_2.LazyPromise(async () => {
            // Sequence number of the snapshot.
            let sequenceNumber;
            // Check whether we need to fetch the snapshot first to load. The snapshot should be in new format to see
            // whether we want to evaluate to fetch snapshot or not for loadingGroupId. Otherwise, the snapshot
            // will contain all the blobs.
            if (this.snapshotFetchRequired === undefined &&
                this._baseSnapshot?.groupId !== undefined &&
                this.isSnapshotInISnapshotFormat) {
                (0, internal_2.assert)(this.blobContents !== undefined, 0x97a /* Blob contents should be present to evaluate */);
                (0, internal_2.assert)(this._baseSnapshot !== undefined, 0x97b /* snapshotTree should be present to evaluate */);
                this.snapshotFetchRequired = (0, internal_5.isSnapshotFetchRequiredForLoadingGroupId)(this._baseSnapshot, this.blobContents);
            }
            if (this.snapshotFetchRequired === true) {
                (0, internal_2.assert)(this.loadingGroupId !== undefined, 0x8f5 /* groupId should be present to fetch snapshot */);
                const snapshot = await this.runtime.getSnapshotForLoadingGroupId([this.loadingGroupId], [this.id]);
                this._baseSnapshot = snapshot.snapshotTree;
                sequenceNumber = snapshot.sequenceNumber;
                this.snapshotFetchRequired = false;
            }
            let tree = this.baseSnapshot;
            let isRootDataStore = true;
            if (!!tree && tree.blobs[index_js_1.dataStoreAttributesBlobName] !== undefined) {
                // Need to get through snapshot and use that to populate extraBlobs
                // eslint-disable-next-line import-x/no-deprecated
                const attributes = await (0, internal_3.readAndParse)(this.storage, tree.blobs[index_js_1.dataStoreAttributesBlobName]);
                let pkgFromSnapshot;
                // Use the snapshotFormatVersion to determine how the pkg is encoded in the snapshot.
                // For snapshotFormatVersion = "0.1" (1) or above, pkg is jsonified, otherwise it is just a string.
                const formatVersion = (0, index_js_1.getAttributesFormatVersion)(attributes);
                if (formatVersion < 1) {
                    pkgFromSnapshot =
                        attributes.pkg.startsWith('["') && attributes.pkg.endsWith('"]')
                            ? JSON.parse(attributes.pkg)
                            : [attributes.pkg];
                }
                else {
                    pkgFromSnapshot = JSON.parse(attributes.pkg);
                }
                this.pkg = pkgFromSnapshot;
                /**
                 * If there is no isRootDataStore in the attributes blob, set it to true. This will ensure that
                 * data stores in older documents are not garbage collected incorrectly. This may lead to additional
                 * roots in the document but they won't break.
                 */
                isRootDataStore = attributes.isRootDataStore ?? true;
                if ((0, index_js_1.hasIsolatedChannels)(attributes)) {
                    tree = tree.trees[internal_4.channelsTreeName];
                    (0, internal_2.assert)(tree !== undefined, 0x1fe /* "isolated channels subtree should exist in remote datastore snapshot" */);
                }
            }
            (0, internal_2.assert)(this.pkg !== undefined, 0x8f6 /* The datastore context package should be defined */);
            return {
                pkg: this.pkg,
                isRootDataStore,
                snapshot: tree,
                sequenceNumber,
            };
        });
        this.runtime = props.parentContext.containerRuntime;
        if ((0, internal_3.isInstanceOfISnapshot)(props.snapshot)) {
            this.blobContents = props.snapshot.blobContents;
            this._baseSnapshot = props.snapshot.snapshotTree;
            this.isSnapshotInISnapshotFormat = true;
        }
        else {
            this._baseSnapshot = props.snapshot;
            this.isSnapshotInISnapshotFormat = false;
        }
    }
    /**
     * This API should not be called for RemoteFluidDataStoreContext. But here is one scenario where it's not the case:
     * The scenario (hit by stashedOps.spec.ts, "resends attach op" UT is the following (as far as I understand):
     *
     * 1. data store is being attached in attached container
     *
     * 2. container state is serialized (stashed ops feature)
     *
     * 3. new container instance is rehydrated (from stashed ops) -
     * As result, we create RemoteFluidDataStoreContext for this data store that is actually in "attaching" state  * (as of # 2).
     * But its state is set to attached when loading container from stashed ops.
     *
     * 4. attach op for this data store is processed - setAttachState() is called.
     */
    setAttachState(attachState) { }
    async getInitialSnapshotDetails() {
        return this.initialSnapshotDetailsP;
    }
    /**
     * {@inheritDoc FluidDataStoreContext.getAttachSummary}
     */
    getAttachSummary() {
        throw new Error("Cannot attach remote store");
    }
    /**
     * {@inheritDoc FluidDataStoreContext.getAttachGCData}
     */
    getAttachGCData(telemetryContext) {
        throw new Error("Cannot attach remote store");
    }
}
exports.RemoteFluidDataStoreContext = RemoteFluidDataStoreContext;
/**
 * Base class for detached & attached context classes
 * @internal
 */
class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
    constructor(props) {
        super(props, props.snapshotTree !== undefined /* existing */, true /* isLocalDataStore */, props.makeLocallyVisibleFn);
        // eslint-disable-next-line unicorn/consistent-function-scoping -- Property is defined once; no need to extract inner lambda
        this.initialSnapshotDetailsP = new internal_2.LazyPromise(async () => {
            let snapshot = this.snapshotTree;
            // eslint-disable-next-line import-x/no-deprecated
            let attributes;
            let isRootDataStore = false;
            if (snapshot !== undefined) {
                // Get the dataStore attributes.
                // Note: storage can be undefined in special case while detached.
                attributes = await (0, index_js_1.getFluidDataStoreAttributes)(this.storage, snapshot);
                if ((0, index_js_1.hasIsolatedChannels)(attributes)) {
                    snapshot = snapshot.trees[internal_4.channelsTreeName];
                    (0, internal_2.assert)(snapshot !== undefined, 0x1ff /* "isolated channels subtree should exist in local datastore snapshot" */);
                }
                if (this.pkg === undefined) {
                    this.pkg = JSON.parse(attributes.pkg);
                    // If there is no isRootDataStore in the attributes blob, set it to true. This ensures that data
                    // stores in older documents are not garbage collected incorrectly. This may lead to additional
                    // roots in the document but they won't break.
                    if (attributes.isRootDataStore ?? true) {
                        isRootDataStore = true;
                        this.setInMemoryRoot();
                    }
                }
            }
            (0, internal_2.assert)(this.pkg !== undefined, 0x152 /* "pkg should be available in local data store" */);
            return {
                pkg: this.pkg,
                isRootDataStore,
                snapshot,
            };
        });
        // Summarizer client should not create local data stores.
        this.identifyLocalChangeIfReadonly("DataStoreCreatedWhileReadonly");
        this.snapshotTree = props.snapshotTree;
    }
    setAttachState(attachState) {
        switch (attachState) {
            case container_definitions_1.AttachState.Attaching: {
                (0, internal_2.assert)(this.attachState === container_definitions_1.AttachState.Detached, 0x14d /* "Should move from detached to attaching" */);
                this._attachState = container_definitions_1.AttachState.Attaching;
                if (this.channel?.setAttachState) {
                    this.channel.setAttachState(attachState);
                }
                else if (this.channel) {
                    // back-compat! To be removed in the future
                    // Added in "2.0.0-rc.2.0.0" timeframe.
                    this.emit("attaching");
                }
                break;
            }
            case container_definitions_1.AttachState.Attached: {
                // We can get called into here twice, as result of both container and data store being attached, if
                // those processes overlapped, for example, in a flow like that one:
                // 1. Container attach started
                // 2. data store attachment started
                // 3. container attached
                // 4. data store attached.
                if (this.attachState !== container_definitions_1.AttachState.Attached) {
                    (0, internal_2.assert)(this.attachState === container_definitions_1.AttachState.Attaching, 0x14e /* "Should move from attaching to attached" */);
                    this._attachState = container_definitions_1.AttachState.Attached;
                    this.channel?.setAttachState?.(attachState);
                    if (this.channel?.setAttachState) {
                        this.channel.setAttachState(attachState);
                    }
                    else if (this.channel) {
                        // back-compat! To be removed in the future
                        // Added in "2.0.0-rc.2.0.0" timeframe.
                        this.emit("attached");
                    }
                }
                break;
            }
            default: {
                (0, internal_2.unreachableCase)(attachState, "unreached");
            }
        }
    }
    /**
     * {@inheritDoc FluidDataStoreContext.getAttachSummary}
     */
    getAttachSummary(telemetryContext) {
        (0, internal_2.assert)(this.channel !== undefined, 0x14f /* "There should be a channel when generating attach message" */);
        (0, internal_2.assert)(this.pkg !== undefined, 0x150 /* "pkg should be available in local data store context" */);
        const attachSummary = this.channel.getAttachSummary(telemetryContext);
        // Wrap dds summaries in .channels subtree.
        (0, index_js_1.wrapSummaryInChannelsTree)(attachSummary);
        // Add data store's attributes to the summary.
        const attributes = createAttributes(this.pkg, this.isInMemoryRoot());
        (0, internal_5.addBlobToSummary)(attachSummary, index_js_1.dataStoreAttributesBlobName, JSON.stringify(attributes));
        // Add loadingGroupId to the summary
        if (this.loadingGroupId !== undefined) {
            attachSummary.summary.groupId = this.loadingGroupId;
        }
        return attachSummary;
    }
    /**
     * {@inheritDoc FluidDataStoreContext.getAttachGCData}
     */
    getAttachGCData(telemetryContext) {
        (0, internal_2.assert)(this.channel !== undefined, 0x9a6 /* There should be a channel when generating attach GC data */);
        return this.channel.getAttachGCData(telemetryContext);
    }
    async getInitialSnapshotDetails() {
        return this.initialSnapshotDetailsP;
    }
    /**
     * A context should only be marked as deleted when its a remote context.
     * Session Expiry at the runtime level should have closed the container creating the local data store context
     * before delete is even possible. Session Expiry is at 30 days, and sweep is done 36+ days later from the time
     * it was unreferenced. Thus the sweeping container should have loaded from a snapshot and thus creating a remote
     * context.
     */
    delete() {
        // TODO: GC:Validation - potentially prevent this from happening or asserting. Maybe throw here.
        this.mc.logger.sendErrorEvent({
            eventName: "GC_Deleted_DataStore_Unexpected_Delete",
            message: "Unexpected deletion of a local data store context",
        });
        super.delete();
    }
}
exports.LocalFluidDataStoreContextBase = LocalFluidDataStoreContextBase;
/**
 * context implementation for "attached" data store runtime.
 * Various workflows (snapshot creation, requests) result in .realize() being called
 * on context, resulting in instantiation and attachment of runtime.
 * Runtime is created using data store factory that is associated with this context.
 * @internal
 */
class LocalFluidDataStoreContext extends LocalFluidDataStoreContextBase {
    constructor(props) {
        super(props);
    }
}
exports.LocalFluidDataStoreContext = LocalFluidDataStoreContext;
/**
 * Detached context. Data Store runtime will be attached to it by attachRuntime() call
 * Before attachment happens, this context is not associated with particular type of runtime
 * or factory, i.e. it's package path is undefined.
 * Attachment process provides all missing parts - package path, data store runtime, and data store factory
 */
class LocalDetachedFluidDataStoreContext extends LocalFluidDataStoreContextBase {
    constructor(props) {
        super(props);
        this.detachedRuntimeCreation = true;
        this.channelToDataStoreFn = props.channelToDataStoreFn;
    }
    async attachRuntime(registry, dataStoreChannel) {
        (0, internal_2.assert)(this.detachedRuntimeCreation, 0x154 /* "runtime creation is already attached" */);
        this.detachedRuntimeCreation = false;
        (0, internal_2.assert)(this.channelP === undefined, 0x155 /* "channel deferral is already set" */);
        this.channelP = Promise.resolve()
            .then(async () => {
            const factory = registry.IFluidDataStoreFactory;
            const factory2 = await this.factoryFromPackagePath();
            (0, internal_2.assert)(factory2 === factory, 0x156 /* "Unexpected factory for package path" */);
            await super.bindRuntime(dataStoreChannel, false /* existing */);
            (0, internal_2.assert)(!(await this.isRoot()), 0x8f7 /* there are no more createRootDataStore() kind of APIs! */);
            return dataStoreChannel;
        })
            .catch((error) => {
            this.mc.logger.sendErrorEvent({ eventName: "AttachRuntimeError" }, error);
            // The following two lines result in same exception thrown.
            // But we need to ensure that this.channelDeferred.promise is "observed", as otherwise
            // out UT reports unhandled exception
            throw error;
        });
        return this.channelToDataStoreFn(await this.channelP);
    }
    /**
     * This method provides a synchronous path for binding a runtime to the context.
     *
     * Due to its synchronous nature, it is unable to validate that the runtime
     * represents a datastore which is instantiable by remote clients. This could
     * happen if the runtime's package path does not return a factory when looked up
     * in the container runtime's registry, or if the runtime's entrypoint is not
     * properly initialized. As both of these validation's are asynchronous to preform.
     *
     * If used incorrectly, this function can result in permanent data corruption.
     */
    unsafe_AttachRuntimeSync(channel) {
        this.channelP = Promise.resolve(channel);
        this.processPendingOps(channel);
        this.completeBindingRuntime(channel);
        return this.channelToDataStoreFn(channel);
    }
    async getInitialSnapshotDetails() {
        if (this.detachedRuntimeCreation) {
            throw new Error("Detached Fluid Data Store context can't be realized! Please attach runtime first!");
        }
        return super.getInitialSnapshotDetails();
    }
}
exports.LocalDetachedFluidDataStoreContext = LocalDetachedFluidDataStoreContext;
//# sourceMappingURL=dataStoreContext.js.map