"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChannelCollectionFactory = exports.detectOutboundReferences = exports.getSummaryForDatastores = exports.ChannelCollection = exports.getLocalDataStoreType = exports.wrapContext = exports.AllowTombstoneRequestHeaderKey = void 0;
const container_definitions_1 = require("@fluidframework/container-definitions");
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/datastore/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 uuid_1 = require("uuid");
const containerRuntime_js_1 = require("./containerRuntime.js");
const dataStore_js_1 = require("./dataStore.js");
const dataStoreContext_js_1 = require("./dataStoreContext.js");
const dataStoreContexts_js_1 = require("./dataStoreContexts.js");
const dataStoreRegistry_js_1 = require("./dataStoreRegistry.js");
const index_js_1 = require("./gc/index.js");
const messageTypes_js_1 = require("./messageTypes.js");
const storageServiceWithAttachBlobs_js_1 = require("./storageServiceWithAttachBlobs.js");
const index_js_2 = require("./summary/index.js");
/**
 * True if a tombstoned object should be returned without erroring
 * @legacy
 * @alpha
 */
exports.AllowTombstoneRequestHeaderKey = "allowTombstone"; // Belongs in the enum above, but avoiding the breaking change
/**
 * Creates a shallow wrapper of {@link IFluidParentContext}. The wrapper can then have its methods overwritten as needed
 */
function wrapContext(context) {
    return {
        get IFluidDataStoreRegistry() {
            return context.IFluidDataStoreRegistry;
        },
        IFluidHandleContext: context.IFluidHandleContext,
        options: context.options,
        get clientId() {
            return context.clientId;
        },
        get connected() {
            return context.connected;
        },
        deltaManager: context.deltaManager,
        storage: context.storage,
        baseLogger: context.baseLogger,
        get clientDetails() {
            return context.clientDetails;
        },
        get idCompressor() {
            return context.idCompressor;
        },
        loadingGroupId: context.loadingGroupId,
        get attachState() {
            return context.attachState;
        },
        isReadOnly: () => context.isReadOnly(),
        containerRuntime: context.containerRuntime,
        scope: context.scope,
        gcThrowOnTombstoneUsage: context.gcThrowOnTombstoneUsage,
        gcTombstoneEnforcementAllowed: context.gcTombstoneEnforcementAllowed,
        getAbsoluteUrl: async (...args) => {
            return context.getAbsoluteUrl(...args);
        },
        getQuorum: (...args) => {
            return context.getQuorum(...args);
        },
        getAudience: (...args) => {
            return context.getAudience(...args);
        },
        submitMessage: (...args) => {
            return context.submitMessage(...args);
        },
        submitSignal: (...args) => {
            return context.submitSignal(...args);
        },
        makeLocallyVisible: (...args) => {
            return context.makeLocallyVisible(...args);
        },
        uploadBlob: async (...args) => {
            return context.uploadBlob(...args);
        },
        addedGCOutboundRoute: (...args) => {
            return context.addedGCOutboundRoute(...args);
        },
        getCreateChildSummarizerNodeFn: (...args) => {
            return context.getCreateChildSummarizerNodeFn?.(...args);
        },
        deleteChildSummarizerNode: (...args) => {
            return context.deleteChildSummarizerNode(...args);
        },
        setChannelDirty: (address) => {
            return context.setChannelDirty(address);
        },
    };
}
exports.wrapContext = wrapContext;
/**
 * Creates a wrapper of a {@link IFluidParentContext} to be provided to the inner datastore channels.
 * The wrapper will have the submit methods overwritten with the appropriate id as the destination address.
 *
 * @param id - the id of the channel
 * @param parentContext - the {@link IFluidParentContext} to wrap
 * @returns A wrapped {@link IFluidParentContext}
 */
function wrapContextForInnerChannel(id, parentContext) {
    const context = wrapContext(parentContext);
    context.submitMessage = (type, content, localOpMetadata) => {
        const fluidDataStoreContent = {
            content,
            type,
        };
        const envelope = {
            address: id,
            contents: fluidDataStoreContent,
        };
        parentContext.submitMessage(messageTypes_js_1.ContainerMessageType.FluidDataStoreOp, envelope, localOpMetadata);
    };
    context.submitSignal = (type, contents, targetClientId) => {
        const envelope = {
            address: id,
            contents,
        };
        parentContext.submitSignal(type, envelope, targetClientId);
    };
    return context;
}
/**
 * Returns the type of the given local data store from its package path.
 */
function getLocalDataStoreType(localDataStore) {
    return localDataStore.packagePath[localDataStore.packagePath.length - 1];
}
exports.getLocalDataStoreType = getLocalDataStoreType;
/**
 * This class encapsulates data store handling. Currently it is only used by the container runtime,
 * but eventually could be hosted on any channel once we formalize the channel api boundary.
 * @internal
 */
class ChannelCollection {
    constructor(baseSnapshot, parentContext, baseLogger, gcNodeUpdated, isDataStoreDeleted, aliasMap, provideEntryPoint) {
        this.baseSnapshot = baseSnapshot;
        this.parentContext = parentContext;
        this.gcNodeUpdated = gcNodeUpdated;
        this.isDataStoreDeleted = isDataStoreDeleted;
        this.aliasMap = aliasMap;
        // Stores tracked by the Domain
        this.pendingAttach = new Map();
        // 0.24 back-compat attachingBeforeSummary
        this.attachOpFiredForDataStore = new Set();
        // eslint-disable-next-line unicorn/consistent-function-scoping -- Property is defined once; no need to extract inner lambda
        this.disposeOnce = new internal_1.Lazy(() => this.contexts.dispose());
        this.pendingAliasMap = new Map();
        /**
         * For sampling. Only log once per container
         */
        this.shouldSendAttachLog = true;
        this.mc = (0, internal_6.createChildMonitoringContext)({ logger: baseLogger });
        this.contexts = new dataStoreContexts_js_1.DataStoreContexts(baseLogger);
        this.entryPoint = new internal_2.FluidObjectHandle(new internal_1.LazyPromise(async () => provideEntryPoint(this)), "", this.parentContext.IFluidHandleContext);
        this.aliasedDataStores = new Set(aliasMap.values());
        // Extract stores stored inside the snapshot
        const fluidDataStores = new Map();
        if (baseSnapshot) {
            const baseSnapshotTree = (0, internal_3.getSnapshotTree)(baseSnapshot);
            for (const [key, value] of Object.entries(baseSnapshotTree.trees)) {
                fluidDataStores.set(key, value);
            }
        }
        let unreferencedDataStoreCount = 0;
        // Create a context for each of them
        for (const [key, value] of fluidDataStores) {
            let dataStoreContext;
            // counting number of unreferenced data stores
            if (value.unreferenced) {
                unreferencedDataStoreCount++;
            }
            // If we have a detached container, then create local data store contexts.
            if (this.parentContext.attachState === container_definitions_1.AttachState.Detached) {
                if (typeof value !== "object") {
                    throw new internal_6.LoggingError("Snapshot should be there to load from!!");
                }
                const snapshotTree = value;
                dataStoreContext = new dataStoreContext_js_1.LocalFluidDataStoreContext({
                    id: key,
                    pkg: undefined,
                    parentContext: this.wrapContextForInnerChannel(key),
                    storage: this.parentContext.storage,
                    scope: this.parentContext.scope,
                    createSummarizerNodeFn: this.parentContext.getCreateChildSummarizerNodeFn(key, {
                        type: internal_4.CreateSummarizerNodeSource.FromSummary,
                    }),
                    makeLocallyVisibleFn: () => this.makeDataStoreLocallyVisible(key),
                    snapshotTree,
                });
            }
            else {
                let snapshotForRemoteFluidDatastoreContext = value;
                if ((0, internal_3.isInstanceOfISnapshot)(baseSnapshot)) {
                    snapshotForRemoteFluidDatastoreContext = {
                        ...baseSnapshot,
                        snapshotTree: value,
                    };
                }
                dataStoreContext = new dataStoreContext_js_1.RemoteFluidDataStoreContext({
                    id: key,
                    snapshot: snapshotForRemoteFluidDatastoreContext,
                    parentContext: this.wrapContextForInnerChannel(key),
                    storage: this.parentContext.storage,
                    scope: this.parentContext.scope,
                    createSummarizerNodeFn: this.parentContext.getCreateChildSummarizerNodeFn(key, {
                        type: internal_4.CreateSummarizerNodeSource.FromSummary,
                    }),
                    loadingGroupId: value.groupId,
                });
            }
            this.contexts.addBoundOrRemoted(dataStoreContext);
        }
        this.containerLoadStats = {
            containerLoadDataStoreCount: fluidDataStores.size,
            referencedDataStoreCount: fluidDataStores.size - unreferencedDataStoreCount,
        };
    }
    get aliases() {
        return this.aliasMap;
    }
    get pendingAliases() {
        return this.pendingAliasMap;
    }
    async waitIfPendingAlias(maybeAlias) {
        const pendingAliasPromise = this.pendingAliases.get(maybeAlias);
        return pendingAliasPromise ?? "Success";
    }
    wrapContextForInnerChannel(id) {
        return wrapContextForInnerChannel(id, this.parentContext);
    }
    /**
     * IFluidDataStoreChannel.makeVisibleAndAttachGraph implementation
     * Not clear when it would be called and what it should do.
     * Currently this API is called by context only for root data stores.
     */
    makeVisibleAndAttachGraph() {
        this.parentContext.makeLocallyVisible();
    }
    processAttachMessages(messageCollection) {
        const { envelope, messagesContent, local } = messageCollection;
        for (const { contents } of messagesContent) {
            const attachMessage = contents;
            // We need to process the GC Data for both local and remote attach messages
            const foundGCData = (0, internal_5.processAttachMessageGCData)(attachMessage.snapshot, (nodeId, toPath) => {
                // nodeId is the relative path under the node being attached. Always starts with "/", but no trailing "/" after an id
                const fromPath = `/${attachMessage.id}${nodeId === "/" ? "" : nodeId}`;
                this.parentContext.addedGCOutboundRoute(fromPath, toPath, envelope.timestamp);
            });
            // Only log once per container to avoid noise/cost.
            // Allows longitudinal tracking of various state (e.g. foundGCData), and some sampled details
            if (this.shouldSendAttachLog) {
                this.shouldSendAttachLog = false;
                this.mc.logger.sendTelemetryEvent({
                    eventName: "dataStoreAttachMessage_sampled",
                    ...(0, internal_6.tagCodeArtifacts)({ id: attachMessage.id, pkg: attachMessage.type }),
                    details: {
                        local,
                        snapshot: !!attachMessage.snapshot,
                        foundGCData,
                    },
                    ...(0, internal_6.extractSafePropertiesFromMessage)(envelope),
                });
            }
            // The local object has already been attached
            if (local) {
                (0, internal_1.assert)(this.pendingAttach.has(attachMessage.id), 0x15e /* "Local object does not have matching attach message id" */);
                this.contexts.get(attachMessage.id)?.setAttachState(container_definitions_1.AttachState.Attached);
                this.pendingAttach.delete(attachMessage.id);
                continue;
            }
            // If a non-local operation then go and create the object, otherwise mark it as officially attached.
            if (this.alreadyProcessed(attachMessage.id)) {
                // TODO: dataStoreId may require a different tag from PackageData #7488
                const error = new internal_6.DataCorruptionError(
                // pre-0.58 error message: duplicateDataStoreCreatedWithExistingId
                "Duplicate DataStore created with existing id", {
                    ...(0, internal_6.extractSafePropertiesFromMessage)(envelope),
                    ...(0, internal_6.tagCodeArtifacts)({ dataStoreId: attachMessage.id }),
                });
                throw error;
            }
            const flatAttachBlobs = new Map();
            let snapshot;
            if (attachMessage.snapshot) {
                snapshot = (0, internal_3.buildSnapshotTree)(attachMessage.snapshot.entries, flatAttachBlobs);
                if ((0, internal_3.isInstanceOfISnapshot)(this.baseSnapshot)) {
                    snapshot = { ...this.baseSnapshot, snapshotTree: snapshot };
                }
            }
            // Include the type of attach message which is the pkg of the store to be
            // used by RemoteFluidDataStoreContext in case it is not in the snapshot.
            const pkg = [attachMessage.type];
            const remoteFluidDataStoreContext = new dataStoreContext_js_1.RemoteFluidDataStoreContext({
                id: attachMessage.id,
                snapshot,
                parentContext: this.wrapContextForInnerChannel(attachMessage.id),
                storage: new storageServiceWithAttachBlobs_js_1.StorageServiceWithAttachBlobs(this.parentContext.storage, flatAttachBlobs),
                scope: this.parentContext.scope,
                loadingGroupId: attachMessage.snapshot?.groupId,
                createSummarizerNodeFn: this.parentContext.getCreateChildSummarizerNodeFn(attachMessage.id, {
                    type: internal_4.CreateSummarizerNodeSource.FromAttach,
                    sequenceNumber: envelope.sequenceNumber,
                    snapshot: attachMessage.snapshot ?? {
                        entries: [(0, dataStoreContext_js_1.createAttributesBlob)(pkg, true /* isRootDataStore */)],
                    },
                }),
                pkg,
            });
            this.contexts.addBoundOrRemoted(remoteFluidDataStoreContext);
        }
    }
    processAliasMessages(messageCollection) {
        const { envelope, messagesContent, local } = messageCollection;
        for (const { contents, localOpMetadata } of messagesContent) {
            const aliasMessage = contents;
            if (!(0, dataStore_js_1.isDataStoreAliasMessage)(aliasMessage)) {
                throw new internal_6.DataCorruptionError("malformedDataStoreAliasMessage", {
                    ...(0, internal_6.extractSafePropertiesFromMessage)(envelope),
                });
            }
            const resolve = localOpMetadata;
            const aliasResult = this.processAliasMessageCore(aliasMessage.internalId, aliasMessage.alias, envelope.timestamp);
            if (local) {
                resolve(aliasResult);
            }
        }
    }
    processAliasMessageCore(internalId, alias, messageTimestampMs) {
        if (this.alreadyProcessed(alias)) {
            return false;
        }
        const context = this.contexts.get(internalId);
        // If the data store has been deleted, log an error and ignore this message. This helps prevent document
        // corruption in case a deleted data store accidentally submitted a signal.
        if (this.checkAndLogIfDeleted(internalId, context, "Changed", "processAliasMessageCore")) {
            return false;
        }
        if (context === undefined) {
            this.mc.logger.sendErrorEvent({
                eventName: "AliasFluidDataStoreNotFound",
                fluidDataStoreId: internalId,
            });
            return false;
        }
        // If message timestamp doesn't exist, this is called in a detached container. Don't notify GC in that case
        // because it doesn't run in detached container and doesn't need to know about this route.
        if (messageTimestampMs) {
            this.parentContext.addedGCOutboundRoute("/", `/${internalId}`, messageTimestampMs);
        }
        this.aliasMap.set(alias, context.id);
        this.aliasedDataStores.add(context.id);
        context.setInMemoryRoot();
        return true;
    }
    alreadyProcessed(id) {
        return this.aliasMap.get(id) !== undefined || this.contexts.get(id) !== undefined;
    }
    /**
     * Package up the context's attach summary etc into an IAttachMessage
     */
    generateAttachMessage(localContext) {
        // Get the attach summary.
        const attachSummary = localContext.getAttachSummary();
        // Get the GC data and add it to the attach summary.
        const attachGCData = localContext.getAttachGCData();
        (0, internal_5.addBlobToSummary)(attachSummary, internal_4.gcDataBlobKey, JSON.stringify(attachGCData));
        // Attach message needs the summary in ITree format. Convert the ISummaryTree into an ITree.
        const snapshot = (0, internal_5.convertSummaryTreeToITree)(attachSummary.summary);
        return {
            id: localContext.id,
            snapshot,
            type: getLocalDataStoreType(localContext),
        };
    }
    /**
     * Make the data store locally visible in the container graph by moving the data store context from unbound to
     * bound list and submitting the attach message. This data store can now be reached from the root.
     * @param id - The id of the data store context to make visible.
     */
    makeDataStoreLocallyVisible(id) {
        const localContext = this.contexts.getUnbound(id);
        (0, internal_1.assert)(!!localContext, 0x15f /* "Could not find unbound context to bind" */);
        /**
         * If the container is not detached, it is globally visible to all clients. This data store should also be
         * globally visible. Move it to attaching state and send an "attach" op for it.
         * If the container is detached, this data store will be part of the summary that makes the container attached.
         */
        if (this.parentContext.attachState !== container_definitions_1.AttachState.Detached) {
            this.submitAttachChannelOp(localContext);
            localContext.setAttachState(container_definitions_1.AttachState.Attaching);
        }
        this.contexts.bind(id);
    }
    submitAttachChannelOp(localContext) {
        const message = this.generateAttachMessage(localContext);
        this.pendingAttach.set(localContext.id, message);
        this.parentContext.submitMessage(messageTypes_js_1.ContainerMessageType.Attach, message, undefined);
        this.attachOpFiredForDataStore.add(localContext.id);
    }
    /**
     * Generate compact internal DataStore ID.
     *
     * A note about namespace and name collisions:
     * This code assumes that that's the only way to generate internal IDs, and that it's Ok for this namespace to overlap with
     * user-provided alias names namespace.
     * There are two scenarios where it could cause trouble:
     * 1) Old files, where (already removed) CreateRoot*DataStore*() API was used, and thus internal name of data store
     * was provided by user. Such files may experience name collision with future data stores that receive a name generated
     * by this function.
     * 2) Much less likely, but if it happen that internal ID (generated by this function) is exactly the same as alias name
     * that user might use in the future, them ContainerRuntime.getAliasedDataStoreEntryPoint() or
     * ContainerRuntime.getDataStoreFromRequest() could return a data store with internalID matching user request, even though
     * user expected some other data store (that would receive alias later).
     * Please note that above mentioned functions have the implementation they have (allowing #2) due to #1.
     */
    createDataStoreId() {
        /**
         * Return uuid if short-ids are explicitly disabled via feature flags.
         */
        if (this.mc.config.getBoolean("Fluid.Runtime.DisableShortIds") === true) {
            return (0, uuid_1.v4)();
        }
        else {
            // We use three non-overlapping namespaces:
            // - detached state: even numbers
            // - attached state: odd numbers
            // - uuids
            // In first two cases we will encode result as strings in more compact form.
            if (this.parentContext.attachState === container_definitions_1.AttachState.Detached) {
                // container is detached, only one client observes content,  no way to hit collisions with other clients.
                return (0, internal_5.encodeCompactIdToString)(2 * this.contexts.size);
            }
            const id = this.parentContext.containerRuntime.generateDocumentUniqueId();
            if (typeof id === "number") {
                return (0, internal_5.encodeCompactIdToString)(2 * id + 1);
            }
            return id;
        }
    }
    createDetachedDataStore(pkg, loadingGroupId) {
        return this.createContext(this.createDataStoreId(), pkg, dataStoreContext_js_1.LocalDetachedFluidDataStoreContext, loadingGroupId);
    }
    createDataStoreContext(pkg, loadingGroupId) {
        return this.createContext(this.createDataStoreId(), pkg, dataStoreContext_js_1.LocalFluidDataStoreContext, loadingGroupId);
    }
    createContext(id, pkg, contextCtor, loadingGroupId) {
        (0, internal_1.assert)(loadingGroupId !== "", 0x974 /* loadingGroupId should not be the empty string */);
        const context = new contextCtor({
            id,
            pkg,
            parentContext: this.wrapContextForInnerChannel(id),
            storage: this.parentContext.storage,
            scope: this.parentContext.scope,
            createSummarizerNodeFn: this.parentContext.getCreateChildSummarizerNodeFn(id, {
                type: internal_4.CreateSummarizerNodeSource.Local,
            }),
            makeLocallyVisibleFn: () => this.makeDataStoreLocallyVisible(id),
            snapshotTree: undefined,
            loadingGroupId,
            channelToDataStoreFn: (channel) => (0, dataStore_js_1.channelToDataStore)(channel, id, this, (0, internal_6.createChildLogger)({ logger: this.parentContext.baseLogger })),
        });
        this.contexts.addUnbound(context);
        return context;
    }
    get disposed() {
        return this.disposeOnce.evaluated;
    }
    dispose() {
        return this.disposeOnce.value;
    }
    reSubmit(type, content, localOpMetadata, squash) {
        switch (type) {
            case messageTypes_js_1.ContainerMessageType.Attach:
            case messageTypes_js_1.ContainerMessageType.Alias: {
                this.parentContext.submitMessage(type, content, localOpMetadata);
                return;
            }
            case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp: {
                return this.reSubmitChannelOp(type, content, localOpMetadata, squash);
            }
            default: {
                (0, internal_1.assert)(false, 0x907 /* unknown op type */);
            }
        }
    }
    reSubmitChannelOp(type, content, localOpMetadata, squash) {
        const envelope = content;
        const context = this.contexts.get(envelope.address);
        // If the data store has been deleted, log an error and throw an error. If there are local changes for a
        // deleted data store, it can otherwise lead to inconsistent state when compared to other clients.
        if (this.checkAndLogIfDeleted(envelope.address, context, "Changed", "resubmitDataStoreOp")) {
            throw new internal_6.DataCorruptionError("Context is deleted!", {
                callSite: "resubmitDataStoreOp",
                ...(0, internal_6.tagCodeArtifacts)({ id: envelope.address }),
            });
        }
        (0, internal_1.assert)(!!context, 0x160 /* "There should be a store context for the op" */);
        const innerContents = envelope.contents;
        context.reSubmit(innerContents.type, innerContents.content, localOpMetadata, squash);
    }
    rollback(type, content, localOpMetadata) {
        (0, internal_1.assert)(type === messageTypes_js_1.ContainerMessageType.FluidDataStoreOp, 0x8e8 /* type */);
        const envelope = content;
        const context = this.contexts.get(envelope.address);
        // If the data store has been deleted, log an error and throw an error. If there are local changes for a
        // deleted data store, it can otherwise lead to inconsistent state when compared to other clients.
        if (this.checkAndLogIfDeleted(envelope.address, context, "Changed", "rollbackDataStoreOp")) {
            throw new internal_6.DataCorruptionError("Context is deleted!", {
                callSite: "rollbackDataStoreOp",
                ...(0, internal_6.tagCodeArtifacts)({ id: envelope.address }),
            });
        }
        (0, internal_1.assert)(!!context, 0x2e8 /* "There should be a store context for the op" */);
        const innerContents = envelope.contents;
        context.rollback(innerContents.type, innerContents.content, localOpMetadata);
    }
    async applyStashedOp(content) {
        const opContents = content;
        switch (opContents.type) {
            case messageTypes_js_1.ContainerMessageType.Attach: {
                return this.applyStashedAttachOp(opContents.contents);
            }
            case messageTypes_js_1.ContainerMessageType.Alias: {
                return;
            }
            case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp: {
                return this.applyStashedChannelChannelOp(opContents.contents);
            }
            default: {
                (0, internal_1.assert)(false, 0x908 /* unknon type of op */);
            }
        }
    }
    async applyStashedChannelChannelOp(envelope) {
        const context = this.contexts.get(envelope.address);
        // If the data store has been deleted, log an error and ignore this message. This helps prevent document
        // corruption in case the data store that stashed the op is deleted.
        if (this.checkAndLogIfDeleted(envelope.address, context, "Changed", "applyStashedOp")) {
            return undefined;
        }
        (0, internal_1.assert)(!!context, 0x161 /* "There should be a store context for the op" */);
        return context.applyStashedOp(envelope.contents);
    }
    async applyStashedAttachOp(message) {
        const { id, snapshot } = message;
        // build the snapshot from the summary in the attach message
        const flatAttachBlobs = new Map();
        const snapshotTree = (0, internal_3.buildSnapshotTree)(snapshot.entries, flatAttachBlobs);
        const storage = new storageServiceWithAttachBlobs_js_1.StorageServiceWithAttachBlobs(this.parentContext.storage, flatAttachBlobs);
        // create a local datastore context for the data store context,
        // which this message represents. All newly created data store
        // contexts start as a local context on the client that created
        // them, and for stashed ops, the client that applies it plays
        // the role of creating client.
        const dataStoreContext = new dataStoreContext_js_1.LocalFluidDataStoreContext({
            id,
            pkg: undefined,
            parentContext: this.wrapContextForInnerChannel(id),
            storage,
            scope: this.parentContext.scope,
            createSummarizerNodeFn: this.parentContext.getCreateChildSummarizerNodeFn(id, {
                type: internal_4.CreateSummarizerNodeSource.FromSummary,
            }),
            makeLocallyVisibleFn: () => this.makeDataStoreLocallyVisible(id),
            snapshotTree,
        });
        // add to the list of bound or remoted, as this context must be bound
        // to had an attach message sent, and is the non-detached case is remoted.
        this.contexts.addBoundOrRemoted(dataStoreContext);
        // realize the local context, as local contexts shouldn't be delay
        // loaded, as this client is playing the role of creating client,
        // and creating clients always create realized data store contexts.
        const channel = await dataStoreContext.realize();
        await channel.entryPoint.get();
        if (this.parentContext.attachState !== container_definitions_1.AttachState.Detached) {
            // if the client is not detached put in the pending attach list
            // so that on ack of the stashed op, the context is found.
            // detached client don't send ops, so should not expect and ack.
            this.pendingAttach.set(id, message);
        }
    }
    /**
     * Process messages for this channel collection. The messages here are contiguous messages in a batch.
     * @param messageCollection - The collection of messages to process.
     */
    processMessages(messageCollection) {
        switch (messageCollection.envelope.type) {
            case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp: {
                this.processChannelMessages(messageCollection);
                break;
            }
            case messageTypes_js_1.ContainerMessageType.Attach: {
                this.processAttachMessages(messageCollection);
                break;
            }
            case messageTypes_js_1.ContainerMessageType.Alias: {
                this.processAliasMessages(messageCollection);
                break;
            }
            default: {
                (0, internal_1.assert)(false, 0x8e9 /* unreached */);
            }
        }
    }
    /**
     * Process channel messages. The messages here are contiguous channel type messages in a batch. Bunch
     * of contiguous messages for a data store should be sent to it together.
     * @param messageCollection - The collection of messages to process.
     */
    processChannelMessages(messageCollection) {
        const { envelope, messagesContent, local } = messageCollection;
        let currentMessageState;
        let currentMessagesContent = [];
        // Helper that sends the current bunch of messages to the data store. It validates that the data stores exists.
        const sendBunchedMessages = () => {
            // Current message state will be undefined for the first message in the list.
            if (currentMessageState === undefined) {
                return;
            }
            const currentContext = this.contexts.get(currentMessageState.address);
            (0, internal_1.assert)(!!currentContext, 0xa66 /* Context not found */);
            currentContext.processMessages({
                envelope: { ...envelope, type: currentMessageState.type },
                messagesContent: currentMessagesContent,
                local,
            });
            currentMessagesContent = [];
        };
        /**
         * Bunch contiguous messages for the same data store and send them together.
         * This is an optimization mainly for DDSes, where it can process a bunch of ops together. DDSes
         * like merge tree or shared tree can process ops more efficiently when they are bunched together.
         */
        for (const { contents, ...restOfMessagesContent } of messagesContent) {
            const contentsEnvelope = contents;
            const address = contentsEnvelope.address;
            const context = this.contexts.get(address);
            // If the data store has been deleted, log an error and ignore this message. This helps prevent document
            // corruption in case a deleted data store accidentally submitted an op.
            if (this.checkAndLogIfDeleted(address, context, "Changed", "processFluidDataStoreOp")) {
                continue;
            }
            if (context === undefined) {
                // Former assert 0x162
                throw internal_6.DataProcessingError.create("No context for op", "processFluidDataStoreOp", envelope, {
                    local,
                    messageDetails: JSON.stringify({
                        type: envelope.type,
                        contentType: typeof contents,
                    }),
                    ...(0, internal_6.tagCodeArtifacts)({ address }),
                });
            }
            const { type: contextType, content: contextContents } = contentsEnvelope.contents;
            // If the address or type of the message changes while processing the message, send the current bunch.
            if (currentMessageState?.address !== address ||
                currentMessageState?.type !== contextType) {
                sendBunchedMessages();
            }
            currentMessagesContent.push({
                contents: contextContents,
                ...restOfMessagesContent,
            });
            currentMessageState = { address, type: contextType };
            // Notify that a GC node for the data store changed. This is used to detect if a deleted data store is
            // being used.
            this.gcNodeUpdated({
                node: { type: "DataStore", path: `/${address}` },
                reason: "Changed",
                timestampMs: envelope.timestamp,
                packagePath: context.isLoaded ? context.packagePath : undefined,
            });
            detectOutboundReferences(address, contextContents, (fromPath, toPath) => this.parentContext.addedGCOutboundRoute(fromPath, toPath, envelope.timestamp));
        }
        // Process the last bunch of messages, if any. Note that there may not be any messages in case all of them are
        // ignored because the data store is deleted.
        sendBunchedMessages();
    }
    async getDataStore(id, requestHeaderData, originalRequest) {
        const headerData = { ...containerRuntime_js_1.defaultRuntimeHeaderData, ...requestHeaderData };
        if (this.checkAndLogIfDeleted(id, this.contexts.get(id), "Requested", "getDataStore", requestHeaderData, originalRequest)) {
            // The requested data store has been deleted by gc. Create a 404 response exception.
            throw (0, internal_5.responseToException)((0, internal_5.createResponseError)(404, "DataStore was deleted", originalRequest, {
                [containerRuntime_js_1.DeletedResponseHeaderKey]: true,
            }), originalRequest);
        }
        const context = await this.contexts.getBoundOrRemoted(id, headerData.wait);
        if (context === undefined) {
            // The requested data store does not exits. Throw a 404 response exception.
            const request = { url: id };
            throw (0, internal_5.responseToException)((0, internal_5.create404Response)(request), request);
        }
        return context;
    }
    /**
     * Returns the data store requested with the given id if available. Otherwise, returns undefined.
     */
    async getDataStoreIfAvailable(id, requestHeaderData) {
        // If the data store has been deleted, log an error and return undefined.
        if (this.checkAndLogIfDeleted(id, this.contexts.get(id), "Requested", "getDataStoreIfAvailable", requestHeaderData)) {
            return undefined;
        }
        const headerData = { ...containerRuntime_js_1.defaultRuntimeHeaderData, ...requestHeaderData };
        const context = await this.contexts.getBoundOrRemoted(id, headerData.wait);
        if (context === undefined) {
            return undefined;
        }
        return context;
    }
    /**
     * Checks if the data store has been deleted by GC. If so, log an error.
     * @param id - The data store's id.
     * @param context - The data store context.
     * @param deletedLogSuffix - Whether it was Changed or Requested (will go into the eventName)
     * @param callSite - The function name this is called from.
     * @param requestHeaderData - The request header information to log if the data store is deleted.
     * @param originalRequest - The original request (could be for a child of the DataStore)
     * @returns true if the data store is deleted. Otherwise, returns false.
     */
    checkAndLogIfDeleted(id, context, deletedLogSuffix, callSite, requestHeaderData, originalRequest) {
        const dataStoreNodePath = `/${id}`;
        if (!this.isDataStoreDeleted(dataStoreNodePath)) {
            return false;
        }
        const idToLog = originalRequest === undefined ? dataStoreNodePath : (0, index_js_1.urlToGCNodePath)(originalRequest.url);
        // Log the package details asynchronously since getInitialSnapshotDetails is async
        const recentlyDeletedContext = this.contexts.getRecentlyDeletedContext(id);
        if (recentlyDeletedContext !== undefined) {
            recentlyDeletedContext
                .getInitialSnapshotDetails()
                .then((details) => {
                return details.pkg.join("/");
            })
                .then((pkg) => ({ pkg, error: undefined }), (error) => ({ pkg: undefined, error }))
                .then(({ pkg, error }) => {
                this.mc.logger.sendTelemetryEvent({
                    eventName: `GC_DeletedDataStore_PathInfo`,
                    ...(0, internal_6.tagCodeArtifacts)({
                        id: idToLog,
                        pkg,
                    }),
                    callSite,
                }, error);
            })
                .catch(() => { });
        }
        this.mc.logger.sendErrorEvent({
            eventName: `GC_Deleted_DataStore_${deletedLogSuffix}`,
            ...(0, internal_6.tagCodeArtifacts)({ id: idToLog }),
            callSite,
            headers: JSON.stringify(requestHeaderData),
            exists: context !== undefined,
            details: {
                url: originalRequest?.url,
                headers: JSON.stringify(originalRequest?.headers),
                aliased: this.aliasedDataStores.has(id),
            },
        });
        return true;
    }
    processSignal(messageArg, local) {
        const envelope = messageArg.content;
        const fluidDataStoreId = envelope.address;
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
        const message = { ...messageArg, content: envelope.contents };
        const context = this.contexts.get(fluidDataStoreId);
        // If the data store has been deleted, log an error and ignore this message. This helps prevent document
        // corruption in case a deleted data store accidentally submitted a signal.
        if (this.checkAndLogIfDeleted(fluidDataStoreId, context, "Changed", "processSignal")) {
            return;
        }
        if (!context) {
            // Attach message may not have been processed yet
            (0, internal_1.assert)(!local, 0x163 /* "Missing datastore for local signal" */);
            this.mc.logger.sendTelemetryEvent({
                eventName: "SignalFluidDataStoreNotFound",
                ...(0, internal_6.tagCodeArtifacts)({
                    fluidDataStoreId,
                }),
            });
            return;
        }
        context.processSignal(message, local);
    }
    setConnectionState(canSendOps, clientId) {
        for (const [fluidDataStoreId, context] of this.contexts) {
            try {
                context.setConnectionState(canSendOps, clientId);
            }
            catch (error) {
                this.mc.logger.sendErrorEvent({
                    eventName: "SetConnectionStateError",
                    clientId,
                    ...(0, internal_6.tagCodeArtifacts)({
                        fluidDataStoreId,
                    }),
                    details: JSON.stringify({
                        runtimeConnected: this.parentContext.connected,
                        canSendOps,
                    }),
                }, error);
            }
        }
    }
    /**
     * Enumerates the contexts and calls notifyReadOnlyState on them.
     */
    notifyReadOnlyState(readonly) {
        for (const [fluidDataStoreId, context] of this.contexts) {
            try {
                context.notifyReadOnlyState();
            }
            catch (error) {
                this.mc.logger.sendErrorEvent({
                    eventName: "SetReadOnlyStateError",
                    ...(0, internal_6.tagCodeArtifacts)({
                        fluidDataStoreId,
                    }),
                    details: {
                        runtimeReadonly: this.parentContext.isReadOnly(),
                        readonly,
                    },
                }, error);
            }
        }
    }
    /**
     * Notifies all data store contexts about the current staging mode state.
     *
     * @param staging - A boolean indicating whether the container is in staging mode.
     */
    notifyStagingMode(staging) {
        for (const [fluidDataStoreId, context] of this.contexts) {
            try {
                context.notifyStagingMode(staging);
            }
            catch (error) {
                this.mc.logger.sendErrorEvent({
                    eventName: "notifyStagingModeError",
                    ...(0, internal_6.tagCodeArtifacts)({
                        fluidDataStoreId,
                    }),
                    details: {
                        staging,
                    },
                }, error);
            }
        }
    }
    setAttachState(attachState) {
        for (const [, context] of this.contexts) {
            // Fire only for bounded stores.
            if (!this.contexts.isNotBound(context.id)) {
                context.setAttachState(attachState);
            }
        }
    }
    get size() {
        return this.contexts.size;
    }
    /**
     * Create a summary. Used when attaching or serializing a detached container.
     */
    getAttachSummary(telemetryContext) {
        const builder = new internal_5.SummaryTreeBuilder();
        this.visitLocalBoundContextsDuringAttach((contextId, context) => {
            let dataStoreSummary;
            if (context.isLoaded) {
                dataStoreSummary = context.getAttachSummary(telemetryContext);
            }
            else {
                // If this data store is not yet loaded, then there should be no changes in the snapshot from
                // which it was created as it is detached container. So just use the previous snapshot.
                (0, internal_1.assert)(!!this.baseSnapshot, 0x166 /* "BaseSnapshot should be there as detached container loaded from snapshot" */);
                dataStoreSummary = (0, internal_5.convertSnapshotTreeToSummaryTree)((0, internal_3.getSnapshotTree)(this.baseSnapshot).trees[contextId]);
            }
            builder.addWithStats(contextId, dataStoreSummary);
        });
        return builder.getSummaryTree();
    }
    /**
     * Gets the GC data. Used when attaching or serializing a detached container.
     */
    getAttachGCData(telemetryContext) {
        const builder = new internal_5.GCDataBuilder();
        this.visitLocalBoundContextsDuringAttach((contextId, context) => {
            const contextGCData = context.getAttachGCData(telemetryContext);
            // Prefix the child's id to the ids of its GC nodes so they can be identified as belonging to the child.
            // This also gradually builds the id of each node to be a path from the root.
            builder.prefixAndAddNodes(contextId, contextGCData.gcNodes);
        });
        // Get the outbound routes (aliased data stores) and add a GC node for this channel.
        builder.addNode("/", [...this.aliasedDataStores]);
        return builder.getGCData();
    }
    /**
     * Helper method for preparing to attach this channel.
     * Runs the callback for each bound context to incorporate its data however the caller specifies
     */
    visitLocalBoundContextsDuringAttach(visitor) {
        const visitedContexts = new Set();
        let visitedLength = -1;
        let notBoundContextsLength = -1;
        while (visitedLength !== visitedContexts.size &&
            notBoundContextsLength !== this.contexts.notBoundLength()) {
            // detect changes in the visitedContexts set, as on visiting a context
            // it could could make contexts available by removing other contexts
            // from the not bound context list, so we need to ensure those get processed as well.
            // only once the loop can run with no new contexts added to the visitedContexts set do we
            // know for sure all possible contexts have been visited.
            visitedLength = visitedContexts.size;
            notBoundContextsLength = this.contexts.notBoundLength();
            for (const [contextId, context] of this.contexts) {
                if (!(visitedContexts.has(contextId) ||
                    this.contexts.isNotBound(contextId) ||
                    this.attachOpFiredForDataStore.has(contextId))) {
                    visitor(contextId, context);
                    visitedContexts.add(contextId);
                }
            }
        }
    }
    /**
     * Helper method for preparing to summarize this channel.
     * Runs the callback for each bound context to incorporate its data however the caller specifies
     */
    async visitContextsDuringSummary(visitor, telemetryProps) {
        for (const [contextId, context] of this.contexts) {
            // Summarizer client and hence GC works only with clients with no local changes. A data store in
            // attaching state indicates an op was sent to attach a local data store, and the the attach op
            // had not yet round tripped back to the client.
            // Formerly assert 0x589
            if (context.attachState === container_definitions_1.AttachState.Attaching) {
                const error = internal_6.DataProcessingError.create("Local data store detected in attaching state", "summarize/getGCData");
                throw error;
            }
            if (context.attachState === container_definitions_1.AttachState.Attached) {
                // If summary / getGCData results in this data store's realization, let GC know so that it can log in
                // case the data store is not referenced. This will help identifying scenarios that we see today where
                // unreferenced data stores are being loaded.
                const contextLoadedBefore = context.isLoaded;
                const trailingOpCount = context.pendingCount;
                await visitor(contextId, context);
                if (!contextLoadedBefore && context.isLoaded) {
                    this.gcNodeUpdated({
                        node: { type: "DataStore", path: `/${context.id}` },
                        reason: "Realized",
                        packagePath: context.packagePath,
                        timestampMs: undefined, // This will be added by the parent context if needed.
                        additionalProps: { trailingOpCount, ...telemetryProps },
                    });
                }
            }
        }
    }
    async summarize(fullTree, trackState, telemetryContext) {
        const summaryBuilder = new internal_5.SummaryTreeBuilder();
        await this.visitContextsDuringSummary(async (contextId, context) => {
            const contextSummary = await context.summarize(fullTree, trackState, telemetryContext);
            summaryBuilder.addWithStats(contextId, contextSummary);
        }, { fullTree, realizedDuring: "summarize" });
        return summaryBuilder.getSummaryTree();
    }
    /**
     * Generates data used for garbage collection. It does the following:
     *
     * 1. Calls into each child data store context to get its GC data.
     *
     * 2. Prefixes the child context's id to the GC nodes in the child's GC data. This makes sure that the node can be
     * identified as belonging to the child.
     *
     * 3. Adds a GC node for this channel to the nodes received from the children. All these nodes together represent
     * the GC data of this channel.
     *
     * @param fullGC - true to bypass optimizations and force full generation of GC data.
     */
    async getGCData(fullGC = false) {
        const builder = new internal_5.GCDataBuilder();
        await this.visitContextsDuringSummary(async (contextId, context) => {
            const contextGCData = await context.getGCData(fullGC);
            // Prefix the child's id to the ids of its GC nodes so they can be identified as belonging to the child.
            // This also gradually builds the id of each node to be a path from the root.
            builder.prefixAndAddNodes(contextId, contextGCData.gcNodes);
        }, { fullGC, realizedDuring: "getGCData" });
        // Get the outbound routes and add a GC node for this channel.
        builder.addNode("/", await this.getOutboundRoutes());
        return builder.getGCData();
    }
    /**
     * After GC has run, called to notify this Container's data stores of routes that are used in it.
     * @param usedRoutes - The routes that are used in all data stores in this Container.
     */
    updateUsedRoutes(usedRoutes) {
        // Get a map of data store ids to routes used in it.
        const usedDataStoreRoutes = (0, internal_5.unpackChildNodesUsedRoutes)(usedRoutes);
        // Verify that the used routes are correct.
        for (const [id] of usedDataStoreRoutes) {
            (0, internal_1.assert)(this.contexts.has(id), 0x167 /* "Used route does not belong to any known data store" */);
        }
        // Update the used routes in each data store. Used routes is empty for unused data stores.
        for (const [contextId, context] of this.contexts) {
            context.updateUsedRoutes(usedDataStoreRoutes.get(contextId) ?? []);
        }
    }
    deleteChild(dataStoreId) {
        const dataStoreContext = this.contexts.get(dataStoreId);
        (0, internal_1.assert)(dataStoreContext !== undefined, 0x2d7 /* No data store with specified id */);
        if (dataStoreContext.isLoaded) {
            this.mc.logger.sendTelemetryEvent({
                eventName: "GC_DeletingLoadedDataStore",
                ...(0, internal_6.tagCodeArtifacts)({
                    id: `/${dataStoreId}`, // Make the id consistent with GC node path format by prefixing a slash.
                    pkg: dataStoreContext.packagePath.join("/"),
                }),
                details: {
                    aliased: this.aliasedDataStores.has(dataStoreId),
                },
            });
        }
        dataStoreContext.delete();
        // Delete the contexts of unused data stores.
        this.contexts.delete(dataStoreId);
        // Delete the summarizer node of the unused data stores.
        this.parentContext.deleteChildSummarizerNode(dataStoreId);
    }
    /**
     * Delete data stores and its objects that are sweep ready.
     * @param sweepReadyDataStoreRoutes - The routes of data stores and its objects that are sweep ready and should
     * be deleted.
     * @returns The routes of data stores and its objects that were deleted.
     */
    deleteSweepReadyNodes(sweepReadyDataStoreRoutes) {
        for (const route of sweepReadyDataStoreRoutes) {
            const pathParts = route.split("/");
            const dataStoreId = pathParts[1];
            // Ignore sub-data store routes because a data store and its sub-routes are deleted together, so, we only
            // need to delete the data store.
            // These routes will still be returned below as among the deleted routes
            if (pathParts.length > 2) {
                continue;
            }
            const dataStoreContext = this.contexts.get(dataStoreId);
            if (dataStoreContext === undefined) {
                // If the data store hasn't already been deleted, log an error because this should never happen.
                // If the data store has already been deleted, log a telemetry event. This can happen because multiple GC
                // sweep ops can contain the same data store. It would be interesting to track how often this happens.
                const alreadyDeleted = this.isDataStoreDeleted(`/${dataStoreId}`);
                this.mc.logger.sendTelemetryEvent({
                    eventName: "DeletedDataStoreNotFound",
                    category: alreadyDeleted ? "generic" : "error",
                    ...(0, internal_6.tagCodeArtifacts)({ id: dataStoreId }),
                    details: { alreadyDeleted },
                });
                continue;
            }
            this.deleteChild(dataStoreId);
        }
        return [...sweepReadyDataStoreRoutes];
    }
    /**
     * This is called to update objects whose routes are tombstones.
     *
     * A Tombstoned object has been unreferenced long enough that GC knows it won't be referenced again.
     * Tombstoned objects are eventually deleted by GC.
     *
     * @param tombstonedRoutes - The routes that are tombstones in all data stores in this Container.
     */
    updateTombstonedRoutes(tombstonedRoutes) {
        const tombstonedDataStoresSet = new Set();
        for (const route of tombstonedRoutes) {
            const pathParts = route.split("/");
            // Tombstone data store only if its route (/datastoreId) is directly in tombstoneRoutes.
            if (pathParts.length > 2) {
                continue;
            }
            const dataStoreId = pathParts[1];
            (0, internal_1.assert)(this.contexts.has(dataStoreId), 0x510 /* No data store with specified id */);
            tombstonedDataStoresSet.add(dataStoreId);
        }
        // Update the used routes in each data store. Used routes is empty for unused data stores.
        for (const [contextId, context] of this.contexts) {
            context.setTombstone(tombstonedDataStoresSet.has(contextId));
        }
    }
    /**
     * Returns the outbound routes of this channel. Only root data stores are considered referenced and their paths are
     * part of outbound routes.
     */
    async getOutboundRoutes() {
        const outboundRoutes = [];
        // Getting this information is a performance optimization that reduces network calls for virtualized datastores
        for (const [contextId, context] of this.contexts) {
            const isRootDataStore = await context.isRoot(this.aliasedDataStores);
            if (isRootDataStore) {
                outboundRoutes.push(`/${contextId}`);
            }
        }
        return outboundRoutes;
    }
    /**
     * Called by GC to retrieve the package path of a data store node with the given path.
     */
    async getDataStorePackagePath(nodePath) {
        // If the node belongs to a data store, return its package path. For DDSes, we return the package path of the
        // data store that contains it.
        const context = this.contexts.get(nodePath.split("/")[1]);
        const initialSnapshotDetails = await context?.getInitialSnapshotDetails();
        return initialSnapshotDetails?.pkg;
    }
    /**
     * Called by GC to determine if a node is for a data store or for an object within a data store (for e.g. DDS).
     * @returns the GC node type if the node belongs to a data store or object within data store, undefined otherwise.
     */
    getGCNodeType(nodePath) {
        const pathParts = nodePath.split("/");
        if (!this.contexts.has(pathParts[1])) {
            return undefined;
        }
        // Data stores paths are of the format "/dataStoreId".
        // Sub data store paths are of the format "/dataStoreId/subPath/...".
        if (pathParts.length === 2) {
            return index_js_1.GCNodeType.DataStore;
        }
        return index_js_1.GCNodeType.SubDataStore;
    }
    internalId(maybeAlias) {
        return this.aliases.get(maybeAlias) ?? maybeAlias;
    }
    async request(request) {
        const requestParser = internal_5.RequestParser.create(request);
        const id = requestParser.pathParts[0];
        // Differentiate between requesting the dataStore directly, or one of its children
        const requestForChild = requestParser.pathParts.length > 1;
        const headerData = {};
        if (typeof request.headers?.[internal_5.RuntimeHeaders.wait] === "boolean") {
            headerData.wait = request.headers[internal_5.RuntimeHeaders.wait];
        }
        if (typeof request.headers?.[internal_5.RuntimeHeaders.viaHandle] === "boolean") {
            headerData.viaHandle = request.headers[internal_5.RuntimeHeaders.viaHandle];
        }
        if (typeof request.headers?.[exports.AllowTombstoneRequestHeaderKey] === "boolean") {
            headerData.allowTombstone = request.headers[exports.AllowTombstoneRequestHeaderKey];
        }
        // We allow Tombstone requests for sub-DataStore objects
        if (requestForChild) {
            headerData.allowTombstone = true;
        }
        await this.waitIfPendingAlias(id);
        const internalId = this.internalId(id);
        const dataStoreContext = await this.getDataStore(internalId, headerData, request);
        // Get the initial snapshot details which contain the data store package path.
        const details = await dataStoreContext.getInitialSnapshotDetails();
        // When notifying GC of this node being loaded, we only indicate the DataStore itself, not the full subDataStore url if applicable.
        // This is in case the url is to a route that Fluid doesn't understand or track for GC (e.g. if suited for a custom request handler)
        this.gcNodeUpdated({
            node: { type: "DataStore", path: `/${id}` },
            reason: "Loaded",
            packagePath: details.pkg,
            request,
            headerData,
            timestampMs: undefined, // This will be added by the parent context if needed.
        });
        const dataStore = await dataStoreContext.realize();
        const subRequest = requestParser.createSubRequest(1);
        // We always expect createSubRequest to include a leading slash, but asserting here to protect against
        // unintentionally modifying the url if that changes.
        (0, internal_1.assert)(subRequest.url.startsWith("/"), 0x126 /* "Expected createSubRequest url to include a leading slash" */);
        return dataStore.request(subRequest);
    }
}
exports.ChannelCollection = ChannelCollection;
function getSummaryForDatastores(snapshot, metadata) {
    if (!snapshot) {
        return undefined;
    }
    if ((0, index_js_2.rootHasIsolatedChannels)(metadata)) {
        const datastoresSnapshot = snapshot.trees[internal_4.channelsTreeName];
        (0, internal_1.assert)(!!datastoresSnapshot, 0x168 /* Expected tree in snapshot not found */);
        return datastoresSnapshot;
    }
    else {
        // back-compat: strip out all non-datastore paths before giving to DataStores object.
        const datastoresTrees = {};
        for (const [key, value] of Object.entries(snapshot.trees)) {
            if (!index_js_2.nonDataStorePaths.includes(key)) {
                datastoresTrees[key] = value;
            }
        }
        return {
            ...snapshot,
            trees: datastoresTrees,
        };
    }
}
exports.getSummaryForDatastores = getSummaryForDatastores;
/**
 * Traverse this op's contents and detect any outbound routes that were added by this op.
 *
 * @internal
 */
function detectOutboundReferences(address, contents, addedOutboundReference) {
    // These will be built up as we traverse the envelope contents
    const outboundPaths = [];
    let ddsAddress;
    function recursivelyFindHandles(obj) {
        if (typeof obj === "object" && obj !== null) {
            for (const [key, value] of Object.entries(obj)) {
                // If 'value' is a serialized IFluidHandle, it represents a new outbound route.
                if ((0, internal_5.isSerializedHandle)(value)) {
                    outboundPaths.push(value.url);
                }
                // NOTE: This is taking a hard dependency on the fact that in our DataStore implementation,
                // the address of the DDS is stored in a property called "address".  This is not ideal.
                // An alternative would be for the op envelope to include the absolute path (built up as it is submitted)
                if (key === "address" && ddsAddress === undefined) {
                    ddsAddress = value;
                }
                recursivelyFindHandles(value);
            }
        }
    }
    recursivelyFindHandles(contents);
    // GC node paths are all absolute paths, hence the "" prefix.
    // e.g. this will yield "/dataStoreId/ddsId"
    const fromPath = ["", address, ddsAddress].join("/");
    for (const toPath of outboundPaths) {
        addedOutboundReference(fromPath, toPath);
    }
}
exports.detectOutboundReferences = detectOutboundReferences;
/**
 * @internal
 */
class ChannelCollectionFactory {
    constructor(registryEntries, 
    // ADO:7302 We need a better type here
    provideEntryPoint) {
        this.provideEntryPoint = provideEntryPoint;
        this.type = "ChannelCollectionChannel";
        this.IFluidDataStoreRegistry = new dataStoreRegistry_js_1.FluidDataStoreRegistry(registryEntries);
    }
    get IFluidDataStoreFactory() {
        return this;
    }
    async instantiateDataStore(context, _existing) {
        // when work is done to complete nested datastores
        // this class should move to a new package
        // and IFluidDataStoreContext will need to support
        // all the cross layer needs of the channel context,
        // and the below assert should be removed.
        //
        // for now this will continue to work if both
        // this factory and the container runtime are
        // from the same package.
        (0, internal_1.assert)(context instanceof dataStoreContext_js_1.FluidDataStoreContext, 0xb8f /* we don't support the layer boundary here today */);
        const runtime = new ChannelCollection(context.baseSnapshot, context, // parentContext
        context.baseLogger, () => { }, // gcNodeUpdated
        (_nodePath) => false, // isDataStoreDeleted
        new Map(), // aliasMap
        this.provideEntryPoint);
        return runtime;
    }
}
exports.ChannelCollectionFactory = ChannelCollectionFactory;
//# sourceMappingURL=channelCollection.js.map