import { effectScope, hasInjectionContext, inject, type App, type EffectScope, type InjectionKey, type Plugin, } from 'vue'; interface Store { scope: EffectScope; value: unknown; setup: () => unknown; } export interface Depot { stores: Map; initializing: Set; } /** * function type for retrieving a store instance * @param depot optional depot instance * @returns the store value */ export type UseStoreFunction = (depot?: Depot) => T; /** * error thrown when circular dependencies are detected during store initialization */ export class CircularDependencyError extends Error { override readonly name = 'CircularDependencyError'; /** * creates a new circular dependency error * @param path array of store ids that form the circular dependency */ constructor(path: string[]) { super(`circular dependency detected: ${path.join(' -> ')}`); } } const DEPOT_KEY: InjectionKey = Symbol(); let activeDepot: Depot | undefined; /** * sets the active depot for use outside of vue's injection context * @param depot the depot to set as active, or undefined to clear */ export const setActiveDepot = (depot: Depot | undefined): void => { activeDepot = depot; }; /** * gets the currently active depot * @returns the active depot or undefined */ /*#__NO_SIDE_EFFECTS__*/ export const getActiveDepot = (): Depot | undefined => { if (hasInjectionContext()) { return inject(DEPOT_KEY, activeDepot); } return activeDepot; }; /** * creates a new depot instance * @returns a depot */ /*#__NO_SIDE_EFFECTS__*/ export const createDepot = (): Depot & Plugin => { return { stores: new Map(), initializing: new Set(), install(app: App) { app.provide(DEPOT_KEY, this); setActiveDepot(this); }, }; }; /** * disposes all stores in a depot and cleans up resources * @param depot the depot to dispose */ export const disposeDepot = (depot: Depot): void => { for (const store of depot.stores.values()) { store.scope.stop(); } depot.stores.clear(); depot.initializing.clear(); if (activeDepot === depot) { activeDepot = undefined; } }; /** * defines a new store with the given id and setup function * @param id unique identifier for the store * @param setup function that returns the store value * @returns a function to retrieve the store instance */ /*#__NO_SIDE_EFFECTS__*/ export const defineStore = (id: string, setup: () => T): UseStoreFunction => { const useStore: UseStoreFunction = (depot = getActiveDepot()) => { if (!depot) { throw new Error(`missing depot`); } let instance = depot.stores.get(id); if (instance === undefined) { if (depot.initializing.has(id)) { throw new CircularDependencyError([...depot.initializing, id]); } depot.initializing.add(id); setActiveDepot(depot); try { const scope = effectScope(true); const value = scope.run(setup)!; depot.stores.set(id, (instance = { scope, setup, value })); } finally { depot.initializing.delete(id); } } return instance.value as T; }; return useStore; }; if (import.meta.hot) { const hot = import.meta.hot; hot.accept(() => hot.invalidate()); }