/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, Deferred, Lazy } from "@fluidframework/core-utils/internal";
import { createChildLogger, } from "@fluidframework/telemetry-utils/internal";
/**
 * Manages the collection of data store contexts, tracking their bound/unbound state.
 *
 * @remarks
 * A context is "unbound" when it's created locally but not yet made visible (reachable from root).
 * A context is "bound" once it's made locally visible, regardless of the Container's attach state.
 * In attached containers, binding a context immediately sends an attach op and transitions it to Attaching state.
 *
 * @internal
 */
export class DataStoreContexts {
    constructor(baseLogger) {
        /**
         * Set of IDs for contexts that are unbound (not yet made locally visible).
         * These contexts exist locally but aren't known to other clients (even in an attached container).
         */
        this.notBoundContexts = new Set();
        /**
         * Map of all data store contexts (both bound and unbound).
         */
        this._contexts = new Map();
        /**
         * List of pending context waiting either to be bound or to arrive from another client.
         * This covers the case where a local context has been created but not yet bound,
         * or the case where a client knows a store will exist (e.g. by alias) and is waiting on its creation,
         * so that a caller may await the deferred's promise until such a time as the context is fully ready.
         * This is a superset of _contexts, since contexts remain here once the Deferred resolves.
         */
        this.deferredContexts = new Map();
        /**
         * Lazy disposal logic that disposes all contexts when called.
         */
        // eslint-disable-next-line unicorn/consistent-function-scoping -- Property is defined once; no need to extract inner lambda
        this.disposeOnce = new Lazy(() => {
            // close/stop all store contexts
            for (const [fluidDataStoreId, contextD] of this.deferredContexts) {
                contextD.promise
                    .then((context) => {
                    context.dispose();
                })
                    .catch((contextError) => {
                    this._logger.sendErrorEvent({
                        eventName: "FluidDataStoreContextDisposeError",
                        fluidDataStoreId,
                    }, contextError);
                });
            }
        });
        this.dispose = () => this.disposeOnce.value;
        /**
         * Map of recently deleted contexts for diagnostic purposes for GC.
         * Allows retrieval of context information even after deletion for logging/telemetry.
         */
        this._recentlyDeletedContexts = new Map();
        this._logger = createChildLogger({ logger: baseLogger });
    }
    [Symbol.iterator]() {
        return this._contexts.entries();
    }
    get size() {
        return this._contexts.size;
    }
    get disposed() {
        return this.disposeOnce.evaluated;
    }
    /**
     * Returns the count of unbound contexts (i.e. local-only on this client)
     */
    notBoundLength() {
        return this.notBoundContexts.size;
    }
    /**
     * Returns true if the given ID corresponds to an unbound context. (i.e. local-only on this client)
     */
    isNotBound(id) {
        return this.notBoundContexts.has(id);
    }
    /**
     * Returns true if a context with the given ID exists (bound or unbound).
     */
    has(id) {
        return this._contexts.has(id);
    }
    /**
     * Returns the context with the given ID, or undefined if not found.
     * This returns both bound and unbound contexts.
     */
    get(id) {
        return this._contexts.get(id);
    }
    /**
     * Deletes the context with the given ID from all internal maps.
     * @returns True if the context was found and deleted, false otherwise.
     */
    delete(id) {
        this.deferredContexts.delete(id);
        this.notBoundContexts.delete(id);
        // Stash the context here in case it's requested in this session, we can log some details about it
        const context = this._contexts.get(id);
        this._recentlyDeletedContexts.set(id, context);
        return this._contexts.delete(id);
    }
    /**
     * Returns a recently deleted context by ID, or undefined if not found.
     * Used for diagnostic logging for GC, when a deleted context is referenced.
     */
    getRecentlyDeletedContext(id) {
        return this._recentlyDeletedContexts.get(id);
    }
    /**
     * Returns the unbound local context with the given ID.
     * @returns The unbound context, or undefined if not found or not unbound.
     */
    getUnbound(id) {
        const context = this._contexts.get(id);
        if (context === undefined || !this.notBoundContexts.has(id)) {
            return undefined;
        }
        return context;
    }
    /**
     * Adds the given context to the collection, marking it as unbound (not yet locally visible).
     * Asserts that no context with this ID already exists.
     */
    addUnbound(context) {
        const id = context.id;
        assert(!this._contexts.has(id), 0x158 /* "Creating store with existing ID" */);
        this._contexts.set(id, context);
        this.notBoundContexts.add(id);
        this.ensureDeferred(id);
    }
    /**
     * Get the context with the given id, once it exists locally and is attached.
     * e.g. If created locally, it must be bound, or if created remotely then it's fine as soon as it's sync'd in.
     * @param id - The id of the context to get
     * @param wait - If false, return undefined if the context isn't present and ready now. Otherwise, wait for it.
     */
    async getBoundOrRemoted(id, wait) {
        const deferredContext = this.ensureDeferred(id);
        if (!wait && !deferredContext.isCompleted) {
            return undefined;
        }
        return deferredContext.promise;
    }
    /**
     * Gets or creates a deferred promise for the given context ID.
     * Used to allow waiting for contexts that don't exist yet.
     */
    ensureDeferred(id) {
        const deferred = this.deferredContexts.get(id);
        if (deferred) {
            return deferred;
        }
        const newDeferred = new Deferred();
        this.deferredContexts.set(id, newDeferred);
        return newDeferred;
    }
    /**
     * Marks the context with the given ID as bound (locally visible).
     * Removes it from the unbound set and resolves its deferred promise.
     */
    bind(id) {
        const removed = this.notBoundContexts.delete(id);
        assert(removed, 0x159 /* "The given id was not found in notBoundContexts to delete" */);
        this.resolveDeferred(id);
    }
    /**
     * Triggers the deferred to resolve, indicating the context is not local-only
     * @param id - The id of the context to resolve to
     */
    resolveDeferred(id) {
        const context = this._contexts.get(id);
        assert(!!context, 0x15a /* "Cannot find context to resolve to" */);
        assert(!this.notBoundContexts.has(id), 0x15b /* "Expected this id to already be removed from notBoundContexts" */);
        const deferred = this.deferredContexts.get(id);
        assert(!!deferred, 0x15c /* "Cannot find deferred to resolve" */);
        deferred.resolve(context);
    }
    /**
     * Adds the given context to the collection as already bound or from a remote client.
     * This is used when:
     * - Adding a local context that's already been bound via the bind() method, OR
     * - Adding a remote context that was created by another client.
     * The context's deferred promise is resolved immediately.
     */
    addBoundOrRemoted(context) {
        const id = context.id;
        assert(!this._contexts.has(id), 0x15d /* "Creating store with existing ID" */);
        this._contexts.set(id, context);
        // Resolve the deferred immediately since this context is not unbound
        this.ensureDeferred(id);
        this.resolveDeferred(id);
    }
}
//# sourceMappingURL=dataStoreContexts.js.map