/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { TypedEventEmitter } from "@fluid-internal/client-utils";
import { AttachState } from "@fluidframework/container-definitions";
import { assert, LazyPromise, unreachableCase } from "@fluidframework/core-utils/internal";
import { BlobTreeEntry, isInstanceOfISnapshot, readAndParse, } from "@fluidframework/driver-utils/internal";
import { channelsTreeName, gcDataBlobKey, } from "@fluidframework/runtime-definitions/internal";
import { addBlobToSummary, isSnapshotFetchRequiredForLoadingGroupId, } from "@fluidframework/runtime-utils/internal";
import { DataCorruptionError, DataProcessingError, LoggingError, ThresholdCounter, createChildMonitoringContext, extractSafePropertiesFromMessage, generateStack, tagCodeArtifacts, } from "@fluidframework/telemetry-utils/internal";
import { sendGCUnexpectedUsageEvent } from "./gc/index.js";
import { dataStoreAttributesBlobName, getAttributesFormatVersion, getFluidDataStoreAttributes, hasIsolatedChannels, summarizerClientType, wrapSummaryInChannelsTree, } from "./summary/index.js";
function createAttributes(pkg, isRootDataStore) {
    const stringifiedPkg = JSON.stringify(pkg);
    return {
        pkg: stringifiedPkg,
        summaryFormatVersion: 2,
        isRootDataStore,
    };
}
export function createAttributesBlob(pkg, isRootDataStore) {
    const attributes = createAttributes(pkg, isRootDataStore);
    return new BlobTreeEntry(dataStoreAttributesBlobName, JSON.stringify(attributes));
}
/**
 * Represents the context for the store. This context is passed to the store runtime.
 * @internal
 */
export class FluidDataStoreContext extends TypedEventEmitter {
    existing;
    isLocalDataStore;
    makeLocallyVisibleFn;
    get packagePath() {
        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.parentContext.deltaManager;
    }
    get connected() {
        return this.parentContext.connected;
    }
    get IFluidHandleContext() {
        return this.parentContext.IFluidHandleContext;
    }
    get containerRuntime() {
        return this._containerRuntime;
    }
    // back-compat, to be removed in 2.0
    ensureNoDataModelChanges(callback) {
        return this.parentContext.ensureNoDataModelChanges(callback);
    }
    get isLoaded() {
        return this.loaded;
    }
    get baseSnapshot() {
        return this._baseSnapshot;
    }
    get idCompressor() {
        return this.parentContext.idCompressor;
    }
    _disposed = false;
    get disposed() {
        return this._disposed;
    }
    /**
     * A Tombstoned object has been unreferenced long enough that GC knows it won't be referenced again.
     * Tombstoned objects are eventually deleted by GC.
     */
    _tombstoned = false;
    get tombstoned() {
        return this._tombstoned;
    }
    /** If true, throw an error when a tombstone data store is used. */
    gcThrowOnTombstoneUsage;
    gcTombstoneEnforcementAllowed;
    /** If true, this means that this data store context and its children have been removed from the runtime */
    deleted = false;
    get attachState() {
        return this._attachState;
    }
    get IFluidDataStoreRegistry() {
        return this.registry;
    }
    baseSnapshotSequenceNumber;
    /**
     * 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.
        if (aliasedDataStores !== undefined && this.baseSnapshot?.omitted === true) {
            return aliasedDataStores.has(this.id);
        }
        return (await this.getInitialSnapshotDetails()).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;
    }
    registry;
    detachedRuntimeCreation = false;
    channel;
    loaded = false;
    pending = [];
    channelP;
    _baseSnapshot;
    _attachState;
    _isInMemoryRoot = false;
    summarizerNode;
    mc;
    thresholdOpsCounter;
    static pendingOpsCountThreshold = 1000;
    /**
     * If the summarizer makes local changes, a telemetry event is logged. This has the potential to be very noisy.
     * So, adding a count of how many telemetry events are logged per data store context. This can be
     * controlled via feature flags.
     */
    localChangesTelemetryCount;
    // The used routes of this node as per the last GC run. This is used to update the used routes of the channel
    // if it realizes after GC is run.
    lastUsedRoutes;
    id;
    _containerRuntime;
    parentContext;
    storage;
    scope;
    // Represents the group to which the data store belongs too.
    loadingGroupId;
    pkg;
    constructor(props, existing, isLocalDataStore, makeLocallyVisibleFn) {
        super();
        this.existing = existing;
        this.isLocalDataStore = isLocalDataStore;
        this.makeLocallyVisibleFn = makeLocallyVisibleFn;
        this._containerRuntime = props.parentContext.containerRuntime;
        this.parentContext = props.parentContext;
        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!
        assert(!this.id.includes("/"), 0x13a /* Data store ID contains slash */);
        this._attachState =
            this.parentContext.attachState !== AttachState.Detached && this.existing
                ? this.parentContext.attachState
                : AttachState.Detached;
        const thisSummarizeInternal = async (fullTree, trackState, telemetryContext) => this.summarizeInternal(fullTree, trackState, telemetryContext);
        this.summarizerNode = props.createSummarizerNodeFn(thisSummarizeInternal, async (fullGC) => this.getGCDataInternal(fullGC));
        this.mc = createChildMonitoringContext({
            logger: this.baseLogger,
            namespace: "FluidDataStoreContext",
            properties: {
                all: 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 ThresholdCounter(FluidDataStoreContext.pendingOpsCountThreshold, this.mc.logger);
        this.gcThrowOnTombstoneUsage = this.parentContext.gcThrowOnTombstoneUsage;
        this.gcTombstoneEnforcementAllowed = this.parentContext.gcTombstoneEnforcementAllowed;
        // By default, a data store can log maximum 10 local changes telemetry in summarizer.
        this.localChangesTelemetryCount =
            this.mc.config.getNumber("Fluid.Telemetry.LocalChangesTelemetryCount") ?? 10;
    }
    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) => { });
        }
    }
    /**
     * 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;
    }
    rejectDeferredRealize(reason, failedPkgPath, fullPackageName) {
        throw new LoggingError(reason, tagCodeArtifacts({
            failedPkgPath,
            packagePath: fullPackageName?.join("/"),
        }));
    }
    async realize() {
        assert(!this.detachedRuntimeCreation, 0x13d /* "Detached runtime creation on realize()" */);
        if (!this.channelP) {
            this.channelP = this.realizeCore(this.existing).catch((error) => {
                const errorWrapped = DataProcessingError.wrapIfUnrecognized(error, "realizeFluidDataStoreContext");
                errorWrapped.addTelemetryProperties(tagCodeArtifacts({
                    fullPackageName: this.pkg?.join("/"),
                    fluidDataStoreId: this.id,
                }));
                this.mc.logger.sendErrorEvent({ eventName: "RealizeError" }, errorWrapped);
                throw errorWrapped;
            });
        }
        return this.channelP;
    }
    async factoryFromPackagePath() {
        const packages = this.pkg;
        if (packages === undefined) {
            this.rejectDeferredRealize("packages is undefined");
        }
        let entry;
        let registry = this.parentContext.IFluidDataStoreRegistry;
        let lastPkg;
        for (const pkg of packages) {
            if (!registry) {
                this.rejectDeferredRealize("No registry for package", lastPkg, packages);
            }
            lastPkg = pkg;
            entry = await registry.get(pkg);
            if (!entry) {
                this.rejectDeferredRealize("Registry does not contain entry for the package", pkg, packages);
            }
            registry = entry.IFluidDataStoreRegistry;
        }
        const factory = entry?.IFluidDataStoreFactory;
        if (factory === undefined) {
            this.rejectDeferredRealize("Can't find factory for package", lastPkg, packages);
        }
        assert(this.registry === undefined, 0x157 /* "datastore registry already attached" */);
        this.registry = registry;
        return factory;
    }
    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;
        assert(this.pkg === details.pkg, 0x13e /* "Unexpected package path" */);
        const factory = await this.factoryFromPackagePath();
        const channel = await factory.instantiateDataStore(this, existing);
        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;
        }
        assert(this.connected === connected, 0x141 /* "Unexpected connected state" */);
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        this.channel.setConnectionState(connected, clientId);
    }
    process(message, local, localOpMetadata) {
        const safeTelemetryProps = extractSafePropertiesFromMessage(message);
        // On op process, tombstone error is logged in garbage collector. So, set "checkTombstone" to false when calling
        // "verifyNotClosed" which logs tombstone errors. Throw error if tombstoned and throwing on load is configured.
        this.verifyNotClosed("process", false /* checkTombstone */, safeTelemetryProps);
        if (this.tombstoned && this.gcThrowOnTombstoneUsage) {
            throw new DataCorruptionError("Context is tombstoned! Call site [process]", safeTelemetryProps);
        }
        this.summarizerNode.recordChange(message);
        if (this.loaded) {
            return this.channel?.process(message, local, localOpMetadata);
        }
        else {
            assert(!local, 0x142 /* "local store channel is not loaded" */);
            assert(this.pending !== undefined, 0x23d /* "pending is undefined" */);
            this.pending.push(message);
            this.thresholdOpsCounter.sendIfMultiple("StorePendingOps", this.pending.length);
        }
    }
    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.
        wrapSummaryInChannelsTree(summarizeResult);
        const pathPartsForChildren = [channelsTreeName];
        // Add data store's attributes to the summary.
        const { pkg } = await this.getInitialSnapshotDetails();
        const isRoot = await this.isRoot();
        const attributes = createAttributes(pkg, isRoot);
        addBlobToSummary(summarizeResult, 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();
        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. These are added to the summary generated by the data store.
     *
     * 4. To notify child contexts of their used routes. This is done immediately if the data store is loaded.
     * Else, it is done when realizing the data store.
     *
     * 5. To update the timestamp when this data store or any children are marked as unreferenced.
     *
     * @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);
        /**
         * Store the used routes to update the channel if the data store is not loaded yet. If the used routes changed
         * since the previous run, the data store will be loaded during summarize since the used state changed. So, it's
         * safe to only store the last used routes.
         */
        this.lastUsedRoutes = usedRoutes;
        // If we are loaded, call the channel so it can update the used routes of the child contexts.
        // If we are not loaded, we will update this when we are realized.
        if (this.loaded) {
            this.updateChannelUsedRoutes();
        }
    }
    /**
     * 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.
     */
    addedGCOutboundRoute(fromPath, toPath) {
        this.parentContext.addedGCOutboundRoute(fromPath, toPath);
    }
    /**
     * Updates the used routes of the channel and its child contexts. The channel must be loaded before calling this.
     * It is called in these two scenarios:
     * 1. When the used routes of the data store is updated and the data store is loaded.
     * 2. When the data store is realized. This updates the channel's used routes as per last GC run.
     */
    updateChannelUsedRoutes() {
        assert(this.loaded, 0x144 /* "Channel should be loaded when updating used routes" */);
        assert(this.channel !== undefined, 0x145 /* "Channel should be present when data store is loaded" */);
        // If there is no lastUsedRoutes, GC has not run up until this point.
        if (this.lastUsedRoutes === undefined) {
            return;
        }
        // Remove the route to this data store, if it exists.
        const usedChannelRoutes = this.lastUsedRoutes.filter((id) => {
            return id !== "/" && id !== "";
        });
        this.channel.updateUsedRoutes(usedChannelRoutes);
    }
    /**
     * @deprecated 0.18.Should call request on the runtime directly
     */
    async request(request) {
        const runtime = await this.realize();
        return runtime.request(request);
    }
    submitMessage(type, content, localOpMetadata) {
        this.verifyNotClosed("submitMessage");
        assert(!!this.channel, 0x146 /* "Channel must exist when submitting message" */);
        // Summarizer clients should not submit messages.
        this.identifyLocalChangeInSummarizer("DataStoreMessageSubmittedInSummarizer", 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?
        }
    }
    /**
     * Submits the signal to be sent to other clients.
     * @param type - Type of the signal.
     * @param content - Content of the signal. Should be a JSON serializable object or primitive.
     * @param targetClientId - When specified, the signal is only sent to the provided client id.
     */
    submitSignal(type, content, targetClientId) {
        this.verifyNotClosed("submitSignal");
        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() {
        assert(this.channel !== undefined, 0x2cf /* "undefined channel on datastore context" */);
        this.makeLocallyVisibleFn();
    }
    processPendingOps(channel) {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const pending = this.pending;
        // Apply all pending ops
        for (const op of pending) {
            // Only process ops whose seq number is greater than snapshot sequence number from which it loaded.
            const seqNumber = this.baseSnapshotSequenceNumber ?? -1;
            if (op.sequenceNumber > seqNumber) {
                channel.process(op, false, undefined /* localOpMetadata */);
            }
        }
        this.pending = undefined;
        this.thresholdOpsCounter.send("ProcessPendingOps", pending.length);
    }
    completeBindingRuntime(channel) {
        // 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);
        /**
         * Update the used routes of the channel. If GC has run before this data store was realized, we will have
         * the used routes saved. So, this will ensure that all the child contexts have up-to-date used routes as
         * per the last time GC was run.
         * Also, this data store may have been realized during summarize. In that case, the child contexts need to
         * have their used routes updated to determine if its needs to summarize again and to add it to the summary.
         */
        this.updateChannelUsedRoutes();
    }
    async bindRuntime(channel, existing) {
        if (this.channel) {
            throw new Error("Runtime already bound");
        }
        assert(!this.detachedRuntimeCreation, 0x148 /* "Detached runtime creation on runtime bind" */);
        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 !== AttachState.Attached) {
            return undefined;
        }
        return this.parentContext.getAbsoluteUrl(relativeUrl);
    }
    /**
     * @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;
    }
    /**
     * @deprecated The functionality to get base GC details has been moved to summarizer node.
     */
    async getBaseGCDetails() {
        return {};
    }
    reSubmit(type, contents, localOpMetadata) {
        assert(!!this.channel, 0x14b /* "Channel must exist when resubmitting ops" */);
        this.channel.reSubmit(type, contents, localOpMetadata);
    }
    rollback(type, contents, 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(type, contents, localOpMetadata);
    }
    async applyStashedOp(contents) {
        if (!this.channel) {
            await this.realize();
        }
        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 = new DataCorruptionError(messageString, 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 = new DataCorruptionError(messageString, safeTelemetryProps);
            sendGCUnexpectedUsageEvent(this.mc, {
                eventName: "GC_Tombstone_DataStore_Changed",
                category: this.gcThrowOnTombstoneUsage ? "error" : "generic",
                gcTombstoneEnforcementAllowed: this.gcTombstoneEnforcementAllowed,
                callSite,
            }, this.pkg, error);
            if (this.gcThrowOnTombstoneUsage) {
                throw error;
            }
        }
    }
    /**
     * Summarizer client 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.
     */
    identifyLocalChangeInSummarizer(eventName, type) {
        if (this.clientDetails.type !== summarizerClientType ||
            this.localChangesTelemetryCount <= 0) {
            return;
        }
        // Log a telemetry if there are local changes in the summarizer. 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: generateStack(),
        });
        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);
    }
}
/** @internal */
export class RemoteFluidDataStoreContext extends FluidDataStoreContext {
    // Tells whether we need to fetch the snapshot before use. This is to support Data Virtualization.
    snapshotFetchRequired;
    runtime;
    blobContents;
    constructor(props) {
        super(props, true /* existing */, false /* isLocalDataStore */, () => {
            throw new Error("Already attached");
        });
        this.runtime = props.parentContext.containerRuntime;
        if (isInstanceOfISnapshot(props.snapshot)) {
            this.blobContents = props.snapshot.blobContents;
            this._baseSnapshot = props.snapshot.snapshotTree;
        }
        else {
            this._baseSnapshot = props.snapshot;
        }
        if (this._baseSnapshot !== undefined) {
            this.summarizerNode.updateBaseSummaryState(this._baseSnapshot);
        }
    }
    /*
    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) { }
    initialSnapshotDetailsP = new LazyPromise(async () => {
        // Sequence number of the snapshot.
        let sequenceNumber;
        // Check whether we need to fetch the snapshot first to load.
        if (this.snapshotFetchRequired === undefined && this._baseSnapshot?.groupId !== undefined) {
            assert(this.blobContents !== undefined, 0x97a /* Blob contents should be present to evaluate */);
            assert(this._baseSnapshot !== undefined, 0x97b /* snapshotTree should be present to evaluate */);
            this.snapshotFetchRequired = isSnapshotFetchRequiredForLoadingGroupId(this._baseSnapshot, this.blobContents);
        }
        if (this.snapshotFetchRequired) {
            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[dataStoreAttributesBlobName] !== undefined) {
            // Need to get through snapshot and use that to populate extraBlobs
            // eslint-disable-next-line import/no-deprecated
            const attributes = await readAndParse(this.storage, tree.blobs[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 = 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 (hasIsolatedChannels(attributes)) {
                tree = tree.trees[channelsTreeName];
                assert(tree !== undefined, 0x1fe /* "isolated channels subtree should exist in remote datastore snapshot" */);
            }
        }
        assert(this.pkg !== undefined, 0x8f6 /* The datastore context package should be defined */);
        return {
            pkg: this.pkg,
            isRootDataStore,
            snapshot: tree,
            sequenceNumber,
        };
    });
    async getInitialSnapshotDetails() {
        return this.initialSnapshotDetailsP;
    }
    /**
     * @see FluidDataStoreContext.getAttachData
     */
    getAttachData(includeGCData) {
        throw new Error("Cannot attach remote store");
    }
}
/**
 * Base class for detached & attached context classes
 * @internal
 */
export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
    snapshotTree;
    /**
     * @deprecated 0.16 Issue #1635, #3631
     */
    createProps;
    constructor(props) {
        super(props, props.snapshotTree !== undefined /* existing */, true /* isLocalDataStore */, props.makeLocallyVisibleFn);
        // Summarizer client should not create local data stores.
        this.identifyLocalChangeInSummarizer("DataStoreCreatedInSummarizer");
        this.snapshotTree = props.snapshotTree;
        this.createProps = props.createProps;
    }
    setAttachState(attachState) {
        switch (attachState) {
            case AttachState.Attaching:
                assert(this.attachState === AttachState.Detached, 0x14d /* "Should move from detached to attaching" */);
                this._attachState = 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 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 !== AttachState.Attached) {
                    assert(this.attachState === AttachState.Attaching, 0x14e /* "Should move from attaching to attached" */);
                    this._attachState = 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:
                unreachableCase(attachState, "unreached");
        }
    }
    /**
     * @see FluidDataStoreContext.getAttachData
     */
    getAttachData(includeGCData, telemetryContext) {
        assert(this.channel !== undefined, 0x14f /* "There should be a channel when generating attach message" */);
        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.
        wrapSummaryInChannelsTree(attachSummary);
        // Add data store's attributes to the summary.
        const attributes = createAttributes(this.pkg, this.isInMemoryRoot());
        addBlobToSummary(attachSummary, dataStoreAttributesBlobName, JSON.stringify(attributes));
        if (includeGCData) {
            const gcData = this.channel.getAttachGCData?.(telemetryContext);
            if (gcData !== undefined) {
                addBlobToSummary(attachSummary, gcDataBlobKey, JSON.stringify(gcData));
            }
        }
        // Add loadingGroupId to the summary
        if (this.loadingGroupId !== undefined) {
            attachSummary.summary.groupId = this.loadingGroupId;
        }
        return {
            attachSummary,
            type: this.pkg[this.pkg.length - 1],
        };
    }
    initialSnapshotDetailsP = new LazyPromise(async () => {
        let snapshot = this.snapshotTree;
        // eslint-disable-next-line import/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 getFluidDataStoreAttributes(this.storage, snapshot);
            if (hasIsolatedChannels(attributes)) {
                snapshot = snapshot.trees[channelsTreeName];
                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();
                }
            }
        }
        assert(this.pkg !== undefined, 0x152 /* "pkg should be available in local data store" */);
        return {
            pkg: this.pkg,
            isRootDataStore,
            snapshot,
        };
    });
    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.
        sendGCUnexpectedUsageEvent(this.mc, {
            eventName: "GC_Deleted_DataStore_Unexpected_Delete",
            message: "Unexpected deletion of a local data store context",
            category: "error",
            gcTombstoneEnforcementAllowed: undefined,
        }, this.pkg);
        super.delete();
    }
}
/**
 * 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
 */
export class LocalFluidDataStoreContext extends LocalFluidDataStoreContextBase {
    constructor(props) {
        super(props);
    }
}
/**
 * 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
 */
export class LocalDetachedFluidDataStoreContext extends LocalFluidDataStoreContextBase {
    constructor(props) {
        super(props);
        this.detachedRuntimeCreation = true;
        this.channelToDataStoreFn = props.channelToDataStoreFn;
    }
    channelToDataStoreFn;
    async attachRuntime(registry, dataStoreChannel) {
        assert(this.detachedRuntimeCreation, 0x154 /* "runtime creation is already attached" */);
        this.detachedRuntimeCreation = false;
        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();
            assert(factory2 === factory, 0x156 /* "Unexpected factory for package path" */);
            await super.bindRuntime(dataStoreChannel, false /* existing */);
            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);
    }
    async getInitialSnapshotDetails() {
        if (this.detachedRuntimeCreation) {
            throw new Error("Detached Fluid Data Store context can't be realized! Please attach runtime first!");
        }
        return super.getInitialSnapshotDetails();
    }
}
//# sourceMappingURL=dataStoreContext.js.map