UNPKG

signalstory

Version:

Signal-based state management for Angular that grows with your project. Explore a versatile toolbox with enriching plugins for developers at all levels.

1,826 lines 66.3 kB
import { inject, Injector, signal, runInInjectionContext, assertInInjectionContext, computed } from '@angular/core';
import { isObservable, finalize, BehaviorSubject, filter, first } from 'rxjs';

/**
 * Naively creates a deep clone of a given state object using JSON.parse and JSON.stringify.
 * This approach is simple but not optimal for performance.
 *
 * @template TState - The type of the state object.
 * @param state - The state object to be cloned.
 * @returns A deep clone of the provided state object.
 */
function naiveDeepClone(state) {
    return JSON.parse(JSON.stringify(state));
}
/**
 * Creates a deep clone of a given value object using either the `structuredClone` method if available,
 * or a naive approach using JSON.parse and JSON.stringify if `structuredClone` is not supported.
 * The naive approach is simple but may have shortcomings and is not optimal for performance.
 *
 * @template T - The type of the value object.
 * @param {T} value - The value object to be cloned.
 * @returns {T} A deep clone of the provided value object.
 */
function deepClone(state) {
    return window && 'structuredClone' in window
        ? structuredClone(state)
        : naiveDeepClone(state);
}
/**
 * Creates a shallow clone of a given value object
 * @param state - The state to be cloned.
 * @returns Shallow cloned state.
 */
function shallowClone(state) {
    if (!state || typeof state !== 'object') {
        return state;
    }
    if (Array.isArray(state))
        return [...state];
    if (state instanceof Date)
        return new Date(state);
    if (state instanceof RegExp)
        return new RegExp(state);
    if (state instanceof Set)
        return new Set(state);
    if (state instanceof Map)
        return new Map(state);
    return Object.assign({}, state);
}
/**
 * Naive implementation of an immutable update function using a clone-and-mutate approach.
 * This function serves as a placeholder that you can swap with a more optimized
 * solution like the one provided by the 'immer.js' library (https://immerjs.github.io/immer/).
 * Using 'immer.js' will result in more efficient and concise code for managing
 * immutable updates to your state.
 *
 * @template TState - The type of the state object.
 * @param currentState - The current state object to be updated.
 * @param mutation - A function that modifies a draft copy of the state.
 * @returns The new state object after applying the mutation.
 */
function naiveCloneAndMutateFunc(currentState, mutation) {
    const clone = deepClone(currentState);
    mutation(clone);
    return clone;
}

/**
 * Creates an empty Mediator registry.
 */
function createRegistry() {
    return new WeakMap();
}
/**
 * Root mediator registry instance.
 */
const rootRegistry = /*@__PURE__*/ createRegistry();
/**
 * Register an event handler for a specific event.
 *
 * @param {MediatorRegistry} registry - The mediator registry to register the handler in.
 * @param {TStore} store - The store instance associated with the event handler.
 * @param {StoreEvent<TPayload>} event - The event to register.
 * @param {(store: TStore, event: StoreEvent<TPayload>) => void} handler - The handler function to be executed when the event occurs.
 * @throws {Error} if the event name is invalid.
 */
function register(registry, store, event, handler) {
    const existingHandlers = registry.get(event) || new Set();
    existingHandlers.add({
        store: new WeakRef(store),
        handler,
    });
    registry.set(event, existingHandlers);
}
/**
 * Unregister event handlers for a specific store and events.
 *
 * @param {MediatorRegistry} registry - The mediator registry to unregister the handlers from.
 * @param {TStore} store - The store instance to remove event handlers from.
 * @param {...StoreEvent<any>[]} events - The events to remove handlers for.
 */
function unregister(registry, store, ...events) {
    for (const event of events) {
        const handlers = registry.get(event);
        if (handlers) {
            handlers.forEach(handler => {
                const handlingStore = handler.store.deref();
                if (!handlingStore || handlingStore === store) {
                    handlers.delete(handler);
                }
            });
            if (handlers.size === 0) {
                registry.delete(event);
            }
        }
    }
}
function publish(registry, event, payload) {
    const handlers = registry.get(event);
    const eventWithPayload = {
        name: event.name,
        payload: payload,
    };
    if (handlers) {
        const errors = [];
        for (const handler of handlers) {
            const store = handler.store.deref();
            if (store) {
                try {
                    handler.handler(store, eventWithPayload);
                }
                catch (error) {
                    errors.push(error);
                }
            }
            else {
                handlers.delete(handler);
            }
        }
        if (errors.length > 0) {
            throw new AggregateError(errors, `Errors in Handler for event ${eventWithPayload.name}`);
        }
    }
}
function publishStoreEvent(event, payload) {
    publish(rootRegistry, event, payload);
}

/**
 * Store registry of stores currently in scope
 */
const storeRegistry = new Set();
/**
 * Iterates over each store in the registry and executes the specified callback function.
 *
 * @param callbackFn - The callback function to be executed for each store in the registry.
 */
function forEachStoreInScope(callbackFn) {
    storeRegistry.forEach(registration => {
        const store = registration.deref();
        if (store) {
            callbackFn(store);
        }
        else {
            storeRegistry.delete(registration);
        }
    });
}
/**
 * Adds a store to the registry.
 *
 * @param store - The store to be added to the registry.
 */
function addToRegistry(store) {
    storeRegistry.add(new WeakRef(store));
}
/**
 * Cleare Store registry.
 * Only used for tests
 *
 */
function clearRegistry() {
    storeRegistry.clear();
}

/**
 * Attempts to retrieve the Angular injector or returns null if not available.
 *
 * This function utilizes the `inject` function from Angular's dependency injection system
 * to obtain an instance of the Angular `Injector`. If the injector is not available, it
 * gracefully returns null.
 *
 * @returns The Angular injector if available, otherwise null.
 */
function getInjectorOrNull() {
    try {
        return inject(Injector);
    }
    catch (_) {
        return null;
    }
}

/**
 * Checks if the provided object is a promise
 * @param obj - The object to be checked.
 * @returns True if the object is a promise, false otherwise.
 * @template T - The type of the resolved value of the promise.
 */
function isPromise(obj) {
    return (obj &&
        typeof obj.then === 'function' &&
        typeof obj.finally === 'function');
}
/**
 * Executes a side effect based on the nature of the source object
 * @param source - The source object
 * @param sideEffect - The side effect function to be executed.
 * @returns The source object with the side effect applied.
 * @template T - The type of the source object.
 */
function withSideEffect(source, sideEffect) {
    if (isObservable(source)) {
        return source.pipe(finalize(sideEffect));
    }
    else if (isPromise(source)) {
        return source.finally(sideEffect);
    }
    sideEffect();
    return source;
}

/* eslint-disable @typescript-eslint/no-explicit-any */
/**
 * Represents a signal store that manages a state and provides methods for state mutation, event handling, and more.
 * @typeparam TState The type of the store's state.
 */
class Store {
    /**
     * Creates a new instance of the store class.
     * @param config The configuration options for the store.
     */
    constructor(config) {
        this.config = {
            name: config.name ?? this.constructor.name,
            initialState: config.initialState,
            injector: config.injector ?? getInjectorOrNull(),
            stateEqualityFn: config.stateEqualityFn ?? null,
            plugins: config.plugins ?? [],
        };
        this._state = signal(this.config.initialState, ...(ngDevMode ? [{ debugName: "_state", equal: this.config.stateEqualityFn ?? undefined }] : [{
                equal: this.config.stateEqualityFn ?? undefined,
            }]));
        addToRegistry(this);
        this.config.plugins
            .sort((a, b) => (b.precedence ?? 0) - (a.precedence ?? 0))
            .forEach(plugin => {
            if (plugin.init) {
                this.initPostprocessor ??= [];
                this.initPostprocessor.push(plugin.init);
            }
            if (plugin.preprocessCommand) {
                this.commandPreprocessor ??= [];
                this.commandPreprocessor.push(plugin.preprocessCommand);
            }
            if (plugin.postprocessCommand) {
                this.commandPostprocessor ??= [];
                this.commandPostprocessor.unshift(plugin.postprocessCommand);
            }
            if (plugin.preprocessEffect) {
                this.effectPreprocessor ??= [];
                this.effectPreprocessor.push(plugin.preprocessEffect);
            }
            if (plugin.postprocessEffect) {
                this.effectPostprocessor ??= [];
                this.effectPostprocessor.unshift(plugin.postprocessEffect);
            }
        });
        this.initPostprocessor?.forEach(p => p(this));
    }
    /**
     * Gets the name of the store
     */
    get name() {
        return this.config.name;
    }
    /**
     * Gets the signal representing the store's current state.
     */
    get state() {
        return this._state.asReadonly();
    }
    /**
     * Sets the store's state to the provided state, with an optional command name.
     * @param newState The new state of the store.
     * @param commandName The name of the command associated with the state change.
     */
    set(newState, commandName) {
        this.commandPreprocessor?.forEach(p => p(this, commandName));
        this._state.set(newState);
        this.commandPostprocessor?.forEach(p => p(this, commandName));
    }
    /**
     * Updates the store's state based on the current state, with an optional command name.
     * @param updateFn A function that updates the current state.
     * @param commandName The name of the command associated with the state change.
     */
    update(updateFn, commandName) {
        this.commandPreprocessor?.forEach(p => p(this, commandName));
        this._state.update(state => updateFn(state));
        this.commandPostprocessor?.forEach(p => p(this, commandName));
    }
    /**
     * Mutates the store's state using the provided mutator function, with an optional command name.
     * @param mutator A function that mutates the current state.
     * @param commandName The name of the command associated with the state mutation.
     */
    mutate(mutator, commandName) {
        this.commandPreprocessor?.forEach(p => p(this, commandName));
        this._state.update(state => {
            const cloned = shallowClone(state);
            mutator(cloned);
            return cloned;
        });
        this.commandPostprocessor?.forEach(p => p(this, commandName));
    }
    /**
     * Registers a handler for the specified event in the store's mediator.
     * @param event The event to register the handler for.
     * @param handler The handler function to be executed when the event is published.
     */
    registerHandler(event, handler) {
        register(rootRegistry, this, event, handler);
    }
    unregisterHandler(...events) {
        unregister(rootRegistry, this, ...events);
    }
    /**
     * Runs an effect with the provided arguments and returns the result.
     * The effect may be associated with the store itself but it may also be unrelated
     * @typeparam TStore The types of the effect's target store.
     * @typeparam TArgs The types of the effect's arguments.
     * @typeparam TResult The type of the effect's result.
     * @param effect The store effect to run.
     * @param args The arguments to pass to the effect.
     * @returns The result of the effect.
     */
    runEffect(effect, ...args) {
        const invocationId = performance.now() + Math.random();
        this.effectPreprocessor?.forEach(p => p(this, effect, invocationId));
        const effectResult = effect.config.withInjectionContext && this.config.injector
            ? runInInjectionContext(this.config.injector, () => effect.func(this, ...args))
            : effect.func(this, ...args);
        return !this.effectPostprocessor
            ? effectResult
            : withSideEffect(effectResult, () => {
                this.effectPostprocessor?.forEach(action => action(this, effect, effectResult, invocationId));
            });
    }
    /**
     * Runs a store query potentially targeting many differnt stores with the provided arguments and returns the result.
     * @typeparam TResult The type of the query's result.
     * @typeparam TStores The types of the stores used in the query.
     * @typeparam TArgs The type of the query's arguments.
     * @param storeQuery The store query to run.
     * @param args The arguments to pass to the query.
     * @returns The result of the query as computed signal.
     */
    runQuery(storeQuery, ...args) {
        if (!this.config.injector) {
            assertInInjectionContext(this.runQuery);
        }
        return runInInjectionContext(this.config.injector ?? inject(Injector), () => {
            const queryArgs = [
                ...storeQuery.stores.map(x => x === this.constructor ? this : inject(x)),
                ...args,
            ];
            return computed(() => storeQuery.query(...queryArgs));
        });
    }
}

/**
 * Implementation of the createEffect function.
 */
function createEffect(name, func, arg) {
    return {
        name,
        func,
        config: {
            withInjectionContext: !arg || arg === true || (arg.withInjectionContext ?? true),
            setLoadingStatus: arg?.setLoadingStatus ?? false,
            setInitializedStatus: arg?.setInitializedStatus ?? false,
        },
    };
}

/**
 * Creates a store event blueprint with the provided name.
 * @param name The name of the event.
 * @returns A store event blueprint object.
 */
function createEvent(name) {
    return { name };
}

/**
 * Represents a store that holds an immutable state, allowing mutation through controlled operations.
 *
 * @typeparam TState The type of the immutable state held by the store.
 */
class ImmutableStore extends Store {
    constructor(config) {
        super(config);
        this.cloneAndMutateFunc =
            config.mutationProducerFn ?? naiveCloneAndMutateFunc;
    }
    mutate(mutator, commandName) {
        this.update(state => this.cloneAndMutateFunc(state, mutator), commandName);
    }
}

/**
 * Memoizes a function by caching its result and returning the cached result on subsequent calls.
 * @param fn - The function to be memoized.
 * @returns A memoized version of the input function.
 */
function memoize(fn) {
    let cachedResult;
    return () => {
        if (cachedResult === undefined) {
            cachedResult = fn();
        }
        return cachedResult;
    };
}

/**
 * Creates a memoized function for feature detection.
 *
 * @param detectionFn - Function that performs the feature detection.
 * @returns Memoized function for feature detection.
 */
function makeFeatureDetector(detectionFn) {
    return memoize(() => {
        try {
            return detectionFn();
        }
        catch {
            return false;
        }
    });
}
/**
 * Feature detection for IndexedDB availability.
 */
const isIndexedDbAvailable = /*@__PURE__*/ makeFeatureDetector(() => !!indexedDB);
/**
 * Feature detection for Local Storage availability.
 */
const isLocalStorageAvailable = /*@__PURE__*/ makeFeatureDetector(() => !!localStorage);
/**
 * Feature detection for Session Storage availability.
 */
const isSessionStorageAvailable = /*@__PURE__*/ makeFeatureDetector(() => !!sessionStorage);
/**
 * Feature detection for Redux DevTools availability.
 */
const isDevtoolsAvailable = /*@__PURE__*/ makeFeatureDetector(() => window && '__REDUX_DEVTOOLS_EXTENSION__' in window);
/**
 * Feature detection for setTimeout availability.
 */
const isSetTimeoutAvailable = /*@__PURE__*/ makeFeatureDetector(() => !!setTimeout);

/* eslint-disable @typescript-eslint/no-explicit-any */
/**
 * Type guard to check if an item is a undo item.
 * @param item The item to be checked.
 */
function isHistoryUndoItem(item) {
    return !!item && 'undoneCommandIndex' in item;
}
/**
 * Type guard to check if an item is a redo item.
 * @param item The item to be checked.
 */
function isHistoryRedoItem(item) {
    return !!item && 'redoneCommandIndex' in item;
}
/**
 * Creates a tracker which immediately tracks the history of the specifiedd stores.
 * @param maxLength Maximum number of commands to retain in the history.
 * @param store Initial store to be tracked in the history.
 * @param stores Additional stores to be tracked in the history.
 * @returns An instance of `HistoryTracker`.
 *
 * @remark At least one store has to be specifieid
 */
function trackHistory(maxLength, store, ...stores) {
    stores ??= [];
    stores.push(store);
    return new HistoryTrackerBase(new Set(stores.map(x => new WeakRef(x))), maxLength);
}
const PRUNE_FRACTION = 0.25;
const UNDO_COMMAND = '_UNDO_';
const REDO_COMMAND = '_REDO_';
const UNSPECIFIED_COMMAND = 'Unspecified';
class HistoryTrackerBase {
    constructor(stores, maxLength) {
        this.addToHistoryRef = this.addToHistory.bind(this);
        this.stores = stores;
        this.maxLength = Math.floor(maxLength * (1 + PRUNE_FRACTION));
        this.pool = new WeakMap();
        this._history = [];
        this.activeTransactions = 0;
        this.lastCommand = signal(undefined, ...(ngDevMode ? [{ debugName: "lastCommand" }] : []));
        this.foreachStore(store => {
            if (store instanceof ImmutableStore) {
                store['commandPreprocessor'] ??= [];
                store['commandPreprocessor'].push(this.addToHistoryRef);
                this.pool.set(store, new WeakRef(store));
            }
            else {
                throw new Error(`${store.name} is not immutable: HistoryTracker does only support ImmutableStores`);
            }
        });
    }
    foreachStore(callbackFn) {
        this.stores.forEach(storeRef => {
            const store = storeRef.deref();
            if (store) {
                callbackFn(store);
            }
            else {
                this.stores.delete(storeRef);
            }
        });
    }
    pushToHistory(historyItem) {
        this._history.push(historyItem);
        this.lastCommand.set(historyItem);
        if (this._history.length > this.maxLength) {
            if (isSetTimeoutAvailable()) {
                setTimeout(this.prune.bind(this), 0);
            }
            else {
                this.prune();
            }
        }
    }
    popFromHistory() {
        const poped = this._history.pop();
        this.lastCommand.set(this._history[this._history.length - 1]);
        return poped;
    }
    addToHistory(store, command) {
        if (this.activeTransactions === 0 &&
            command !== UNDO_COMMAND &&
            command !== REDO_COMMAND) {
            this.pushToHistory({
                command: command ?? UNSPECIFIED_COMMAND,
                before: store.state(),
                store: this.pool.get(store) ?? new WeakRef(store),
            });
        }
    }
    collectCurrentStates() {
        const values = new WeakMap();
        this.foreachStore(store => values.set(store, store.state()));
        return values;
    }
    getCommandToUndo() {
        let toBeUndoneCommandIndex = this._history.length - 1;
        let toBeUndoneCommand = this.lastCommand();
        while (toBeUndoneCommandIndex >= 0 &&
            isHistoryUndoItem(toBeUndoneCommand)) {
            toBeUndoneCommandIndex = toBeUndoneCommand.undoneCommandIndex - 1;
            toBeUndoneCommand =
                toBeUndoneCommandIndex >= 0
                    ? this._history[toBeUndoneCommandIndex]
                    : undefined;
        }
        return toBeUndoneCommandIndex >= 0
            ? [toBeUndoneCommandIndex, toBeUndoneCommand]
            : [-1, undefined];
    }
    getCommandToRedo() {
        let toBeRedoneCommandIndex = this._history.length - 1;
        let toBeRedoneCommand = this.lastCommand();
        while (toBeRedoneCommandIndex >= 0 &&
            isHistoryRedoItem(toBeRedoneCommand)) {
            toBeRedoneCommandIndex = toBeRedoneCommand.redoneCommandIndex - 1;
            toBeRedoneCommand =
                toBeRedoneCommandIndex >= 0
                    ? this._history[toBeRedoneCommandIndex]
                    : undefined;
        }
        return toBeRedoneCommandIndex >= 0 && isHistoryUndoItem(toBeRedoneCommand)
            ? [toBeRedoneCommandIndex, toBeRedoneCommand]
            : [-1, undefined];
    }
    prune() {
        const deleteCount = Math.floor(this._history.length * PRUNE_FRACTION);
        if (deleteCount > 0) {
            this._history.splice(0, deleteCount);
            this._history.forEach(command => {
                if (isHistoryRedoItem(command)) {
                    command.redoneCommandIndex -= deleteCount;
                }
                else if (isHistoryUndoItem(command)) {
                    command.undoneCommandIndex -= deleteCount;
                }
            });
        }
    }
    get canUndo() {
        return computed(() => !!this.lastCommand() && this.getCommandToUndo()[0] >= 0);
    }
    get canRedo() {
        return computed(() => !!this.lastCommand() &&
            this.activeTransactions === 0 &&
            this.getCommandToRedo()[0] >= 0);
    }
    getHistory() {
        const scopedStores = Array.from(this.stores);
        return this._history.map(x => {
            const stores = 'store' in x ? [x.store] : scopedStores;
            const before = stores.map(storeRef => {
                const store = storeRef.deref();
                return [
                    store,
                    x.before instanceof WeakMap
                        ? store
                            ? x.before.get(store)
                            : undefined
                        : x.before,
                ];
            });
            return {
                command: x.command,
                before,
            };
        });
    }
    destroy() {
        this.foreachStore(store => {
            if (store instanceof ImmutableStore) {
                const trackerRefIndex = store['commandPreprocessor']?.indexOf(this.addToHistoryRef);
                if (trackerRefIndex !== undefined && trackerRefIndex > -1) {
                    store['commandPreprocessor'].splice(trackerRefIndex, 1);
                }
            }
        });
    }
    beginTransaction(tag) {
        if (this.activeTransactions === 0) {
            this.pushToHistory({
                command: tag ?? UNSPECIFIED_COMMAND,
                before: this.collectCurrentStates(),
            });
        }
        this.activeTransactions++;
    }
    endTransaction() {
        if (this.activeTransactions > 0) {
            this.activeTransactions--;
        }
    }
    undo() {
        if (this.activeTransactions > 0) {
            const toBeUndone = this.popFromHistory();
            if (toBeUndone && toBeUndone.before instanceof WeakMap) {
                this.foreachStore(store => store.set(toBeUndone.before.get(store), UNDO_COMMAND));
            }
            this.activeTransactions = 0;
            return true;
        }
        const [toBeUndoneCommandIndex, toBeUndoneCommand] = this.getCommandToUndo();
        if (toBeUndoneCommand) {
            const newState = toBeUndoneCommand.before;
            if (newState instanceof WeakMap) {
                this.pushToHistory({
                    command: UNDO_COMMAND,
                    before: this.collectCurrentStates(),
                    undoneCommandIndex: toBeUndoneCommandIndex,
                });
                this.foreachStore(store => store.set(newState.get(store), UNDO_COMMAND));
            }
            else if ('store' in toBeUndoneCommand) {
                const store = toBeUndoneCommand.store.deref();
                if (store) {
                    this.pushToHistory({
                        command: UNDO_COMMAND,
                        store: toBeUndoneCommand.store,
                        before: store.state(),
                        undoneCommandIndex: toBeUndoneCommandIndex,
                    });
                    store.set(newState, UNDO_COMMAND);
                }
            }
            return true;
        }
        return false;
    }
    redo() {
        if (this.activeTransactions > 0) {
            return false;
        }
        const [toBeRedoneCommandIndex, toBeRedoneCommand] = this.getCommandToRedo();
        if (toBeRedoneCommand) {
            const newState = toBeRedoneCommand.before;
            if (newState instanceof WeakMap) {
                this.pushToHistory({
                    command: REDO_COMMAND,
                    before: this.collectCurrentStates(),
                    redoneCommandIndex: toBeRedoneCommandIndex,
                });
                this.foreachStore(store => store.set(newState.get(store), REDO_COMMAND));
            }
            else if ('store' in toBeRedoneCommand) {
                const store = toBeRedoneCommand.store.deref();
                if (store) {
                    this.pushToHistory({
                        command: REDO_COMMAND,
                        store: toBeRedoneCommand.store,
                        before: store.state(),
                        redoneCommandIndex: toBeRedoneCommandIndex,
                    });
                    store.set(newState, REDO_COMMAND);
                }
            }
            return true;
        }
        return false;
    }
}

/**
 * Deeply freezes an object and its properties, making it immutable at runtime.
 * @template T - The type of the object.
 * @param {T} obj - The object to be deeply frozen.
 * @returns {T} The deeply frozen object.
 */
function deepFreeze(obj) {
    if (obj) {
        Object.freeze(obj);
        const oIsFunction = typeof obj === 'function';
        const hasOwnProp = Object.prototype.hasOwnProperty;
        Object.getOwnPropertyNames(obj).forEach(function (prop) {
            if (hasOwnProp.call(obj, prop) &&
                (oIsFunction
                    ? prop !== 'caller' && prop !== 'callee' && prop !== 'arguments'
                    : true)) {
                const propValue = obj[prop];
                if (propValue !== null &&
                    (typeof propValue === 'object' || typeof propValue === 'function') &&
                    !Object.isFrozen(propValue)) {
                    deepFreeze(propValue);
                }
            }
        });
    }
    return obj;
}

/**
 * Enables Storeplugin that deep freezes the state after each command
 * This middleware introduces some overhead
 *
 * @returns DeepFreeze Storeplugin.
 */
function useDeepFreeze() {
    return {
        postprocessCommand(store) {
            deepFreeze(store.state());
        },
    };
}

/**
 * Registry for store with attached redux devtools monitoring
 * It maps the store name to a Weak reference of the store
 */
const registry = new Map();
/**
 * Retrieves a store from the registry by name.
 * @param name Name of the store.
 * @returns The store, if found.
 */
function getStore(name) {
    return registry.get(name)?.deref();
}
/**
 * The DevTools extension instance.
 */
let devtools;
/**
 * Initializes the Redux DevTools extension.
 * @param options DevTools options.
 */
function initDevtools(options = {}) {
    devtools = window.__REDUX_DEVTOOLS_EXTENSION__.connect(options);
    devtools.subscribe(handleDevtoolsMessage);
}
/**
 * Scavenges and retrieves a snapshot of registered stores.
 * @returns Snapshot of registered stores.
 * @modifies registry - Deletes references to garbage collected stores
 */
function scavengeAndGetStoresSnapshot() {
    const stores = {};
    registry.forEach((storeRef, name) => {
        const store = storeRef.deref();
        if (store) {
            stores[store.name] = store.state();
        }
        else {
            registry.delete(name);
            sendToDevtools({ type: `[${name}] - @Removal` });
        }
    });
    return stores;
}
/**
 * Sends an action to the Redux DevTools extension.
 * @param action Action to send.
 */
function sendToDevtools(action) {
    devtools?.send(action, scavengeAndGetStoresSnapshot());
}
/**
 * Handles messages received from the Redux DevTools extension.
 * @param message DevTools message.
 */
function handleDevtoolsMessage(message) {
    if (devtools) {
        if (message.type === 'DISPATCH') {
            const payloadType = message.payload.type;
            if (payloadType === 'COMMIT') {
                devtools.init(scavengeAndGetStoresSnapshot());
                return;
            }
            if (payloadType === 'JUMP_TO_STATE' || payloadType === 'JUMP_TO_ACTION') {
                const state = JSON.parse(message.state);
                for (const [name, value] of Object.entries(state)) {
                    const store = getStore(name);
                    if (store) {
                        store['_state'].set(value);
                    }
                }
            }
        }
    }
}
/**
 * Registers a store for Devtools monitoring.
 *
 * @param store - Store to be registered.
 */
function registerForDevtools(store) {
    if (!devtools) {
        initDevtools({});
    }
    registry.set(store.name, new WeakRef(store));
    sendToDevtools({ type: `[${store.name}] - @Init` });
}
/**
 * Removes a store from Devtools monitoring.
 *
 * @param store - Store to be removed.
 */
function removeFromDevtools(store) {
    registry.delete(store.name);
    sendToDevtools({ type: `[${store.name}] - @Removal` });
}
/**
 * Enables Storeplugin that links the store activity with the Redux DevTools extension.
 * @returns Devtools Storeplugin
 */
function useDevtools() {
    if (!isDevtoolsAvailable()) {
        return {};
    }
    return {
        init(store) {
            registerForDevtools(store);
        },
        postprocessCommand(store, command) {
            sendToDevtools({
                type: `[${store.name}] - ${command ?? 'Command'}`,
            });
        },
    };
}

/**
 * Enables StorePlugin that logs command and effect execution
 * @returns A StorePlugin instance for logging.
 */
function useLogger(options = {}) {
    const plugin = {
        name: 'StoreLogger',
        log: options.logFunction ?? console.log,
    };
    plugin.init = store => plugin.log(`[${store.name}->Init]`, store.state());
    plugin.postprocessCommand = (store, command) => plugin.log(`[${store.name}->Command] ${command ?? 'Unspecified'}`, store.state());
    plugin.preprocessEffect = (store, effect) => plugin.log(`[${store.name}->Effect STARTED] ${effect.name ?? 'Unspecified'}`, store.state());
    plugin.postprocessEffect = (store, effect, _, invocationId) => plugin.log(`[${store.name}->Effect FINNISHED in ${Math.floor(performance.now() - invocationId)} ms] ${effect.name ?? 'Unspecified'}`, store.state());
    return plugin;
}

/**
 * PerformanceCounter class is used to measure and analyze the performance of a process.
 */
class PerformanceCounter {
    constructor() {
        this.count = 0;
        this.totalDurationMs = 0;
        this.maxDurationMs = 0;
        this.sumSquares = 0;
        this.currentTimer = null;
    }
    /**
     * Indicates whether the timer is currently running.
     */
    get isRunning() {
        return !!this.currentTimer;
    }
    /**
     * Adds the specified duration to the counter, updating relevant statistics.
     * @param duration - The duration to be added in milliseconds.
     */
    addDuration(duration) {
        this.count++;
        this.totalDurationMs += duration;
        this.maxDurationMs = Math.max(this.maxDurationMs, duration);
        this.sumSquares += duration * duration;
    }
    /**
     * Starts the timer, recording the current timestamp.
     */
    startTimer() {
        this.currentTimer = performance.now();
    }
    /**
     * Stops the timer, calculates the duration, and adds it to the counter.
     */
    stopTimer() {
        const duration = performance.now() - this.currentTimer;
        this.addDuration(duration);
        this.currentTimer = null;
    }
    /**
     * Toggles the timer between start and stop states.
     */
    toggleTimer() {
        if (this.isRunning) {
            this.stopTimer();
        }
        else {
            this.startTimer();
        }
    }
    /**
     * Calculates and returns the average duration based on the recorded intervals.
     * @returns The average duration in milliseconds.
     */
    getAverageDuration() {
        return this.count === 0 ? 0 : this.totalDurationMs / this.count;
    }
    /**
     * Calculates and returns the standard deviation of durations.
     * @returns The standard deviation
     */
    getStandardDeviation() {
        if (this.count === 0) {
            return 0;
        }
        const count = this.count;
        const meanSquared = (this.totalDurationMs / count) ** 2;
        const variance = (this.sumSquares - meanSquared * count) / count;
        return Math.sqrt(variance);
    }
    /**
     * Generates and returns a performance report with various statistics.
     * @returns An object containing performance statistics.
     */
    getReport() {
        return {
            count: this.count,
            maxDurationMs: this.maxDurationMs,
            averageDurationMs: this.getAverageDuration(),
            standardDeviation: this.getStandardDeviation(),
        };
    }
}

// Counters
const globalCommandCounter = 
/*@__PURE__*/ new PerformanceCounter();
const globalEffectCounter = 
/*@__PURE__*/ new PerformanceCounter();
const commandCounters = [];
const effectCounters = [];
/**
 * Toggles the timer for a specific command, updating the associated counter.
 * @param store - The name of the store.
 * @param command - The name of the command.
 */
function toggleCommandTimer(store, command) {
    const commandName = command ?? 'Unspecified';
    const counterRegistration = commandCounters.find(c => c.store === store && c.name === commandName);
    const counter = counterRegistration?.counter ?? new PerformanceCounter();
    if (!counterRegistration) {
        commandCounters.push({
            name: commandName,
            store,
            counter,
        });
    }
    counter.toggleTimer();
}
/**
 * Adds the duration of an effect to the associated counter.
 * @param store - The name of the store.
 * @param effect - The name of the effect.
 * @param duration - The duration of the effect execution.
 */
function addEffectDuration(store, effect, duration) {
    const effectName = effect ?? 'Unspecified';
    const counterRegistration = effectCounters.find(e => e.store === store && e.name === effectName);
    const counter = counterRegistration?.counter ?? new PerformanceCounter();
    if (!counterRegistration) {
        effectCounters.push({
            name: effectName,
            store,
            counter,
        });
    }
    counter.addDuration(duration);
}
/**
 * Generates and returns a performance report with various metrics.
 * @returns An object containing performance statistics.
 */
function getReport() {
    const globalCommandReport = globalCommandCounter.getReport();
    const globalEffectReport = globalEffectCounter.getReport();
    return {
        totalCommandCount: globalCommandReport.count,
        averageCommandDurationMs: globalCommandReport.averageDurationMs,
        commandDurationStandartDeviation: globalCommandReport.standardDeviation,
        totalEffectCount: globalEffectReport.count,
        averageEffectDurationMs: globalEffectReport.averageDurationMs,
        effectDurationStandartDeviation: globalEffectReport.standardDeviation,
        commands: commandCounters
            .map(c => ({
            name: c.name,
            store: c.store,
            ...c.counter.getReport(),
        }))
            .sort((a, b) => b.averageDurationMs - a.averageDurationMs),
        effects: effectCounters
            .map(e => ({
            name: e.name,
            store: e.store,
            ...e.counter.getReport(),
        }))
            .sort((a, b) => b.averageDurationMs - a.averageDurationMs),
    };
}
/**
 * Store-Like object for registering the performance counters in redux devtools
 */
const counterStore = {
    name: '@signalstory/performance-counter',
    state() {
        return getReport();
    },
};
/**
 * Returns a StorePlugin that includes initialization and hooks for tracking command and effect performance.
 * @returns The StorePlugin for performance tracking.
 */
function usePerformanceCounter() {
    return {
        precedence: 11, // should come early in initialization
        init() {
            if (isDevtoolsAvailable() && !registry.has(counterStore.name)) {
                registry.set(counterStore.name, new WeakRef(counterStore));
            }
        },
        preprocessCommand(store, command) {
            globalCommandCounter.toggleTimer();
            toggleCommandTimer(store.name, command);
        },
        postprocessCommand(store, command) {
            globalCommandCounter.toggleTimer();
            toggleCommandTimer(store.name, command);
        },
        postprocessEffect(store, effect, _, invocationId) {
            const duration = Math.floor(performance.now() - invocationId);
            globalEffectCounter.addDuration(duration);
            addEffectDuration(store.name, effect.name, duration);
        },
    };
}

/**
 * Class for configuring indexedb object stores and their corresponding miggrations
 */
class IndexedDbStoreMigrator {
    constructor() {
        this.migrations = [];
    }
    /**
     * Creates an object store if it does not exist
     * @param objectStoreName - The name of the object store
     * @returns The IndexedDbStoreRegistrator instance for chaining
     */
    createStore(objectStoreName) {
        this.migrations.push([objectStoreName, undefined]);
        return this;
    }
    /**
     * Creates an object store if it does not exist.
     * If it does exists, the current object store value is cleared
     * @param objectStoreName - The name of the object store
     * @returns The IndexedDbStoreRegistrator instance for chaining
     */
    createStoreOrClearState(objectStoreName) {
        this.migrations.push([objectStoreName, 'CLEAR']);
        return this;
    }
    /**
     * Creates an object store if it does not exist.
     * If it does exists, the current object store value can be transformed using the passed transformation function
     * @param objectStoreName - The name of the object store
     * @param transformation - Custom transformation function for update
     * @returns The IndexedDbStoreRegistrator instance for chaining
     */
    createStoreOrTransform(objectStoreName, transformation) {
        this.migrations.push([objectStoreName, transformation]);
        return this;
    }
    /**
     * Creates an object store if it does not exist.
     * If it does exists, the current object store value can be transformed using the passed transformation function
     * @param objectStoreName - The name of the object store
     * @param transformation - Custom transformation function for update
     * @returns The IndexedDbStoreRegistrator instance for chaining
     */
    createStoreOrMigrateRecords(objectStoreName, migration) {
        const recordMigration = migration(new IndexedDbStoreMigrator());
        this.migrations.push([objectStoreName, recordMigration]);
        return this;
    }
    /**
     * Deletes the object store if it does exist.
     * @param objectStoreName - The name of the object store
     * @returns The IndexedDbStoreRegistrator instance for chaining
     */
    dropStore(objectStoreName) {
        this.migrations.push([objectStoreName, 'DROP']);
        return this;
    }
    /**
     * Get all registrations
     */
    getMigrations() {
        return this.migrations;
    }
}
/**
 * Registered indexedDB migrations per databasename
 */
const idbMigrations = new Map();
/**
 * Retrieves the registered migration for a specific IndexedDB database.
 * @param dbName - The name of the IndexedDB database.
 * @returns The registered migration or undefined if not found.
 */
function getRegisteredMigration(dbName) {
    return idbMigrations.get(dbName);
}
/**
 * Redeems the migration for a specific IndexedDB database.
 * @param dbName - The name of the IndexedDB database.
 * @param dbVersion - Optional parameter specifying the database version.
 * @returns A function to handle the IDBVersionChangeEvent or undefined if the migration is not found.
 * @throws Error if attempting to open a connection with a version different from the registered migration.
 */
function redeemMigration(dbName, dbVersion) {
    const migration = idbMigrations.get(dbName);
    if (!migration) {
        return undefined;
    }
    if (dbVersion && dbVersion !== migration.dbVersion) {
        throw new Error(`getMigrationForDb: Attempted to open a connection to IndexedDb ${dbName} with the version ${dbVersion}, but a migration for the version ${migration.dbVersion} has been specified. Please use only one version for a specific db.`);
    }
    idbMigrations.delete(dbName);
    return (event) => {
        const target = event.target;
        const db = target.result;
        const transaction = target.transaction;
        const oldVersion = event.oldVersion;
        migration.migrations.forEach(([store, op]) => {
            if (!db.objectStoreNames.contains(store)) {
                if (op !== 'DROP') {
                    db.createObjectStore(store);
                }
            }
            else if (op) {
                if (op === 'DROP') {
                    db.deleteObjectStore(store);
                }
                else {
                    const objectStore = transaction.objectStore(store);
                    if (op === 'CLEAR') {
                        objectStore.clear();
                    }
                    else {
                        if (op instanceof IndexedDbStoreMigrator) {
                            // Migrate records of objectStore (one-objectstore-for-mulitple-stores approach)
                            op.getMigrations().forEach(([recordName, recordOp]) => {
                                objectStore.get(recordName).onsuccess = (event) => {
                                    if (recordOp === 'DROP') {
                                        objectStore.delete(recordName);
                                    }
                                    else if (recordOp === 'CLEAR') {
                                        objectStore.put(undefined, recordName);
                                    }
                                    else if (typeof recordOp === 'function') {
                                        const existingData = event.target.result;
                                        if (existingData) {
                                            const newData = recordOp(oldVersion, existingData);
                                            objectStore.put(newData, recordName);
                                        }
                                    }
                                };
                            });
                        }
                        else if (typeof op === 'function') {
                            // Migrate objectStore
                            objectStore.openCursor().onsuccess = (event) => {
                                const cursor = event.target.result;
                                if (cursor) {
                                    const existingData = cursor.value;
                                    const newData = op(oldVersion, existingData);
                                    objectStore.put(newData, cursor.primaryKey);
                                }
                            };
                        }
                    }
                }
            }
        });
    };
}
/**
 * Configures the IndexedDB with specified store registrations and database migration.
 * @param dbName - The name of the IndexedDB database.
 * @param dbVersion - The version of the IndexedDB database.
 * @param migration - A function defining store registrations and migration operations.
 * @remarks Migrations are registered lazily and applied upon the first use of the database.
 * @throws Throws an error if no stores are registered for migration.
 * @throws Throws an error if a migration for the specified database already exists.
 */
function migrateIndexedDb(dbName, dbVersion, migration) {
    const migrations = migration(new IndexedDbStoreMigrator()).getMigrations();
    if (!migrations || migrations.length === 0) {
        throw new Error('migrateIndexedDb: Please register at least one Store');
    }
    if (idbMigrations.has(dbName)) {
        throw new Error(`migrateIndexedDb: A migration for ${dbName} has already been specified`);
    }
    idbMigrations.set(dbName, {
        dbVersion,
        migrations,
    });
}

/**
 * Type guard for IDBDatabase based on IndexedDbPoolEntryState.
 * @param entry The IndexedDB pool entry to be checked.
 * @returns True if the entry is an IDBDatabase, false otherwise.
 */
function isIDBDatabase(entry) {
    return typeof entry === 'object' && 'name' in IDBDatabase;
}
/**
 * Represents a mapping of database names to their corresponding cached IndexedDB pool entries.
 */
const dbPool = new Map();
/**
 * Represents an entry in the IndexedDB pool.
 */
class IndexedDbPoolEntry {
    constructor(dbName, dbVersion) {
        this.dbName = dbName;
        this.dbVersion = dbVersion;
        this._db = new BehaviorSubject(undefined);
    }
    get db() {
        return this._db.asObservable();
    }
    updateEntryState(dbState) {
        this._db.next(dbState);
    }
}
/**
 * Retrieves an existing or opens a new connection to an IndexedDB.
 * @param dbName - The name of the database.
 * @param dbVersion - The version of the database.
 * @param migration - A callback function to perform database migration during upgrade.
 * @returns An observable stream representing the state of the IndexedDB entry.
 * @throws Throws an error if attempting to open a connection to a database with conflicting versions.
 */
function getOrOpenDb(dbName, dbVersion, migration) {
    const cachedDbEntry = dbPool.get(dbName);
    if (cachedDbEntry) {
        if (dbVersion && cachedDbEntry.dbVersion !== dbVersion) {
            throw new Error(`getOrOpenDb: Attempted to open a connection to IndexedDb ${dbName} with the version ${dbVersion}, but another connection to the same db with the version ${cachedDbEntry.dbVersion} is already open. Please use only one version for a specific db.`);
        }
        return cachedDbEntry.db;
    }
    else {
        dbVersion ??= getRegisteredMigration(dbName)?.dbVersion;
        if (!dbVersion) {
            throw new Error(`getOrOpenDb: No db version specified. If you want to let the db pool infer the version, you have to setup db migration first using a specific version`);
        }
        const dbPoolEntry = new IndexedDbPoolEntry(dbName, dbVersion);
        dbPool.set(dbName, dbPoolEntry);
        const request = indexedDB.open(dbName, dbVersion);
        request.onupgradeneeded = event => {
            const db = event.target?.result;
            if (db) {
                const registeredMigration = redeemMigration(db.name, db.version);
                registeredMigration?.(event);
                migration?.(event);
            }
        };
        request.onsuccess = event => {
            dbPoolEntry.updateEntryState(event.target?.result);
        };
        request.onblocked = () => {
            dbPoolEntry.updateEntryState('Blocked');
        };
        request.onerror = () => {
            dbPoolEntry.updateEntryState('InitError');
        };
        return dbPoolEntry.db;
    }
}

/**
 * configures connection to an IndexedDb
 * @param options - The configuration options for IndexedDB.
 * @returns Store persistence plugin options.
 */
function configureIndexedDb(options) {
    return {
        persistenceStorage: new IndexedDbAdapter(options.dbName, options.dbVersion, options.objectStoreName, options.key, options.handlers),
        projection: options.projection,
    };
}
/**
 * Represents an adapter for interacting with IndexedDB, implementing AsyncStorage.
 */
class IndexedDbAdapter {
    constructor(dbName, dbVersion, _objectStoreName, _key, handlers) {
        this.dbName = dbName;
        this.dbVersion = dbVersion;
        this._objectStoreName = _objectStoreName;
        this._key = _key;
        this.handlers = handlers;
    }
    get objectStoreName() {
        return this._objectStoreName;
    }
    get key() {
        return this._key;
    }
    initAsync(storeName, callback) {
        this._objectStoreName ??= storeName;
        this._key ??= storeName;
        const db = getOrOpenDb(this.dbName, this.dbVersion, event => {
            const db = event.target?.result;
            if (db) {
                if (!db.objectStoreNames.contains(this._objectStoreName)) {
                    db.createObjectStore(this._objectStoreName);
                }
            }
        });
        db.pipe(filter(entry => !!entry), first()).subscribe(entry => {
            if (isIDBDatabase(entry)) {
                this.db = entry;
                this.dbVersion = entry.version;
                this.handlers?.onSuccess?.();
                callback?.();
            }
            else if (entry === 'Blocked') {
                this.handlers?.onBlocked?.();
            }
            else if (entry === 'InitError') {
                this.handlers?.onInitializationError?.();
            }
        });
    }
    getItemAsync(_, callback) {
        const request = this.db
            ?.transaction([this.objectStoreName])
            ?.objectStore(this.objectStoreName)
            ?.get(this.key);
        if (request) {
            request.onsuccess = (event) => {
                callback(event.target.result);
            };
        }
    }
    setItemAsync(_, value, callback) {
        const request = this.db
            ?.transaction([this.objectStoreName], 'readwrite')
            ?.objectStore(this.objectStoreName)
            ?.put(value, this.key);
        if (request && callback) {
            request.onsuccess = callback;
        }
    }
    removeItemAsync(_, callback) {
        const request = this.db
            ?.transaction([this.objectStoreName], 'readwrite')
            ?.objectStore(this.objectStoreName)
            ?.clear();
        if (request && callback) {
            request.onsuccess = callback;
        }
    }
}

/* eslint-disable @typescript-eslint/no-explicit-any */
/**
 * Type guard to check if an object implements the `PersistenceStorageAsynchronous` interface.
 * @param obj - The object to check.
 * @returns True if the object implements the `PersistenceStorageAsynchronous` interface, false otherwise.
 */
function isAsyncStorage(obj) {
    return (typeof obj === 'object' &&
        typeof obj.initAsync === 'function' &&
        typeof obj.getItemAsync === 'function' &&
        typeof obj.setItemAsync === 'function' &&
        typeof obj.removeItemAsync === 'function');
}

/**
 * Type guard to check if an object implements the `PersistenceStorageSynchronous` interface.
 * @param obj - The object to check.
 * @returns True if the object implements the `PersistenceStorageSynchronous` interface, false otherwise.
 */
function isSyncStorage(obj) {
    return (typeof obj === 'object' &&
        typeof obj.getItem === 'function' &&
        typeof obj.setItem === 'function' &&
        typeof obj.removeItem === 'function');
}
/**
 * Loads a value from storage.
 *
 * @template TState - The type of state to load.
 * @param store - The store instance.
 * @returns The loaded value if available and successfully parsed, otherwise undefined.
 */
function loadFromStorage(persistenceStorage, persistenceKey) {
    const value = persistenceStorage.getItem(persistenceKey);
    try {
        return value ? JSON.parse(value) : undefined;
    }
    catch {
        return undefined;
    }
}
/**
 * Saves a value to local storage.
 *
 * @template TState - The type of state to save.
 * @param store - The store instance.
 * @param value - The store value to store.
 */
function saveToStorage(persistenceStorage, persistenceKey, value) {
    persistenceStorage.setItem(persistenceKey, JSON.stringify(value));
}

/**
 * typeguard for StorePersistencePlugin.
 * @param obj - The object to check.
 * @returns True if the object is a StorePersistencePlugin, otherwise false.
 */
function isStorePersistencePlugin(obj) {
    return (obj &&
        typeof obj === 'object' &&
        'name' in obj &&
        obj['name'] === 'StorePersistence');
}
/**
 * Clears the value associated with the provided key from local storage.
 *
 * @template TState - The type of state to clear from local storage.
 * @param store - The store instance.
 *
 */
function clearStoreStorage(store) {
    const plugin = store.config.plugins.find(isStorePersistencePlugin);
    if (plugin) {
        if (isSyncStorage(plugin.storage)) {
            plugin.storage.removeItem(plugin.persistenceKey);
        }
        else if (isAsyncStorage(plugin.storage)) {
            plugin.storage.removeItemAsync(plugin.persistenceKey);
        }
    }
    else {
        throw new Error(`Store persistence plugin is not enabled for store ${store.config.name}`);
    }
}
function configureSyncStorage(plugin, projection) {
    plugin.init = store => {
        if (!plugin.persistenceKey) {
            plugin.persistenceKey = `_persisted_state_of_${store.config.name}`;
        }
        const persistedState = loadFromStorage(plugin.storage, plugin.persistenceKey);
        if (persistedState) {
            store.set(projection
                ? projection.onLoad(persistedState)
                : persistedState, 'Load state from storage');
        }
    };
    plugin.postprocessCommand = projection
        ? store => saveToStorage(plugin.storage, plugin.persistenceKey, projection.onWrite(store.state()))
        : store => saveToStorage(plugin.storage, plugin.persistenceKey, store.state());
    return plugin;
}
function configureAsyncStorage(plugin, projection) {
    plugin.init = store => {
        if (!plugin.persistenceKey) {
            plugin.persistenceKey = `_persisted_state_of_${store.config.name}`;
        }
        plugin.storage.initAsync(store.name, () => {
            plugin.storage.getItemAsync(plugin.persistenceKey, persistedState => {
                if (persistedState) {
                    store.set(projection
                        ? projection.onLoad(persistedState)
                        : persistedState, 'Load state from storage');
                }
            });
        });
    };
    plugin.postprocessCommand = projection
        ? store => plugin.storage.setItemAsync(plugin.persistenceKey, projection.onWrite(store.state()))
        : store => plugin.storage.setItemAsync(plugin.persistenceKey, store.state());
    return plugin;
}
/**
 * Enables Storeplugin that persists the store state to a storage (e.g. local storage).
 * State changes are automatically synced with the storage.
 * @param options - Options for configuring the StorePersistencePlugin.
 * @returns A StorePersistencePlugin instance.
 */
function useStorePersistence(options = {}) {
    const storageProvider = options.persistenceStorage ?? 'LOCAL_STORAGE';
    if ((storageProvider === 'LOCAL_STORAGE' && !isLocalStorageAvailable()) ||
        (storageProvider === 'SESSION_STORAGE' && !isSessionStorageAvailable()) ||
        (storageProvider instanceof IndexedDbAdapter && !isIndexedDbAvailable())) {
        return {};
    }
    const storage = storageProvider === 'LOCAL_STORAGE'
        ? localStorage
        : storageProvider === 'SESSION_STORAGE'
            ? sessionStorage
            : storageProvider;
    const plugin = {
        name: 'StorePersistence',
        storage,
        persistenceKey: options.persistenceKey ?? '',
    };
    return isAsyncStorage(storage)
        ? configureAsyncStorage(plugin, options.projection)
        : configureSyncStorage(plugin, options.projection);
}

/* eslint-disable @typescript-eslint/no-explicit-any */
const storeStatusMap = new WeakMap();
let _runningEffects;
function getRunningEffects() {
    if (!_runningEffects) {
        _runningEffects = signal([]);
    }
    return _runningEffects;
}
/**
 * Returns a Signal indicating whether the provided store has been modified.
 *
 * @note A store is initially considered unmodified. Any command (`set`, `update`, `mutate`) applied to the store
 * will mark it as modified. Additionally, an effect created with the `setInitializedStatus` flag will reset the store's
 * modification status to unmodified.
 *
 * @param store - The store to check for modification status.
 * @returns Signal<boolean> - A signal indicating whether the store has been modified.
 */
function modified(store) {
    const status = storeStatusMap.get(store);
    if (!status) {
        throw new Error(`StatusPlugin has not been activated for store ${store.name}`);
    }
    return computed(() => status().hasBeenModified);
}
/**
 * Returns a Signal indicating whether the provided store has been initialized by an initializing effect.
 *
 * @note A store is initially considered as deinitialized. An effect created with the `setInitializedStatus` flag will set the store's
 * initialization status to `true`.
 *
 * @param store - The store to check for initialization status.
 * @returns Signal<boolean> - A signal indicating whether the store has been initialized.
 */
function initialized(store) {
    const status = storeStatusMap.get(store);
    if (!status) {
        throw new Error(`StatusPlugin has not been activated for store ${store.name}`);
    }
    return computed(() => status().hasBeenInitialized);
}
/**
 * Manually resets the status indicators for the provided store, marking it as deinitialized and unmodified.
 * This means that both `unmodified()` and `initialized()` will return false. For manual reset of the loading status,
 * use `markAsHavingNoRunningEffects`.
 *
 * @note This method is intended for exceptional cases.
 *
 * @param store - The store to manually reset the status.
 * @returns void
 */
function resetStoreStatus(store) {
    storeStatusMap.get(store)?.set({
        hasBeenInitialized: false,
        hasBeenModified: false,
    });
}
/**
 * Returns a Signal indicating whether any of the provided stores is in a loading state.
 * If no stores are provided, the returned signal indicates if any store is in a loading state.
 *
 * @note An effect created with `setLoadingStatus` will mark the associated store as loading while the effect is running.
 *
 * @param stores - Stores to check for loading status. If no stores are provided, the signal checks all stores.
 * @returns Signal<boolean> - A signal indicating whether any store is loading.
 */
function isLoading(...stores) {
    if (!stores || stores.length === 0) {
        return computed(() => getRunningEffects()().some(effect => effect[1].config.setLoadingStatus));
    }
    else {
        return computed(() => getRunningEffects()().some(runningEffect => {
            const affectedStore = runningEffect[0].deref();
            return (affectedStore &&
                runningEffect[1].config.setLoadingStatus &&
                stores.some(store => store === affectedStore));
        }));
    }
}
/**
 * Manually marks the provided store as not having any running effects.
 *
 * @note This method is intended for exceptional cases, specifically when you observe
 * that an effect is not removed from the running state by signalstory automatically.
 * If you encounter such a situation, use this method as a temporary workaround and
 * be sure to file an issue on GitHub for further investigation and resolution.
 *
 * @param store - The store to manually mark as not having running effects.
 * @returns void
 */
function markAsHavingNoRunningEffects(store) {
    getRunningEffects().update(state => state.filter(runningEffect => runningEffect[0].deref() !== store));
}
/**
 * Returns a Signal indicating whether any effect is currently running for any of the provided stores.
 * If no Store is provided the returned signal indicates if any store has an effect running
 * @param stores - Stores to check for running effects.
 * @returns Signal<boolean> - A signal indicating whether any effect is running for any store.
 */
function isAnyEffectRunning(...stores) {
    if (!stores || stores.length === 0) {
        return computed(() => getRunningEffects()().length > 0);
    }
    else {
        return computed(() => getRunningEffects()().some(runningEffect => {
            const affectedStore = runningEffect[0].deref();
            return affectedStore && stores.some(store => store === affectedStore);
        }));
    }
}
/**
 * Returns a Signal indicating whether the specified effect is currently running for any of the provided stores.
 * If no Store is provided the returned signal indicates if any store has the given effect running
 * @param effect - The effect to check for.
 * @param stores - Stores to check for the specified effect.
 * @returns Signal<boolean> - A signal indicating whether the specified effect is running for any store.
 */
function isEffectRunning(effect, ...stores) {
    if (!stores || stores.length === 0) {
        return computed(() => getRunningEffects()().some(x => x[1] === effect));
    }
    else {
        return computed(() => getRunningEffects()()
            .filter(runningEffect => runningEffect[1] === effect)
            .some(runningEffect => {
            const affectedStore = runningEffect[0].deref();
            return affectedStore && stores.some(store => store === affectedStore);
        }));
    }
}
/**
 * Enables StorePlugin that tracks the loading and modification status of a store.
 * @returns A StorePlugin instance for loading and modification status tracking.
 */
function useStoreStatus() {
    return {
        init(store) {
            storeStatusMap.set(store, signal({
                hasBeenInitialized: false,
                hasBeenModified: false,
            }));
        },
        postprocessCommand(store) {
            const status = storeStatusMap.get(store);
            if (status && !status().hasBeenModified) {
                storeStatusMap.get(store)?.update(state => ({
                    ...state,
                    hasBeenModified: true,
                }));
            }
        },
        preprocessEffect(store, effect, invocationId) {
            getRunningEffects().update(effects => [
                ...effects,
                [new WeakRef(store), effect, invocationId],
            ]);
        },
        postprocessEffect(store, effect, _, invocationId) {
            getRunningEffects().update(effects => effects.filter(x => x[2] !== invocationId));
            if (effect.config.setInitializedStatus) {
                storeStatusMap.get(store)?.set({
                    hasBeenInitialized: true,
                    hasBeenModified: false,
                });
            }
        },
    };
}

function createQuery(stores, query) {
    return {
        stores: stores,
        query: query,
    };
}

/**
 * Represents the snapshot restore command.
 */
const RestoreCommand = '_SNAPSHOT_RESTORE_';
/**
 * Base implementation of the StateSnapshot interface.
 */
class StateSnapshotBase {
    constructor(storesWithState) {
        this.storesWithState = storesWithState;
        this.timestamp = performance.now();
    }
    restore() {
        forEachStoreInScope(store => {
            if (this.storesWithState.has(store)) {
                const snapshotValue = this.storesWithState.get(store);
                if (snapshotValue !== store.state()) {
                    store.set(snapshotValue, RestoreCommand);
                }
            }
        });
    }
}
/**
 * Creates a state snapshot either for specified stores or all stores in scope.
 *
 * If no stores are provided, the snapshot will include all stores currently in scope.
 * Stores can be specified either as instances of Store or as ProviderTokens representing
 * Store classes.
 *
 * @param stores - The stores for which to create a snapshot. Can be either instances
 * of Store or ProviderTokens representing Store classes.
 * @returns A StateSnapshot instance representing the application state snapshot.
 */
function createSnapshot(...stores) {
    const storesWithState = new WeakMap();
    stores ??= [];
    forEachStoreInScope(store => {
        if (stores.length === 0 ||
            stores.some(x => x === store || store.constructor === x)) {
            const stateSnapshot = store instanceof ImmutableStore
                ? store.state()
                : deepClone(store.state());
            storesWithState.set(store, stateSnapshot);
        }
    });
    return new StateSnapshotBase(storesWithState);
}

/*
 * Public API Surface of signalstory
 */

/**
 * Generated bundle index. Do not edit.
 */

export { ImmutableStore, Store, clearStoreStorage, configureIndexedDb, createEffect, createEvent, createQuery, createSnapshot, getReport, initialized, isAnyEffectRunning, isEffectRunning, isLoading, markAsHavingNoRunningEffects, migrateIndexedDb, modified, publishStoreEvent, resetStoreStatus, trackHistory, useDeepFreeze, useDevtools, useLogger, usePerformanceCounter, useStorePersistence, useStoreStatus };
//# sourceMappingURL=signalstory.mjs.map