Something went wrong. Try again.
singleton stores in Vue, just like Pinia, but much simpler
Something went wrong. Try again.
3.2 kB · 143 lines · Created 1y ago
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144import { 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<string, Store>; initializing: Set<string>;}
/** * function type for retrieving a store instance * @param depot optional depot instance * @returns the store value */export type UseStoreFunction<T> = (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<Depot> = 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 = <T>(id: string, setup: () => T): UseStoreFunction<T> => { const useStore: UseStoreFunction<T> = (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());}