Skip to content

Instantly share code, notes, and snippets.

@mary-ext
Last active July 23, 2025 11:00
Show Gist options
  • Save mary-ext/6043fac4d612fe93a3a023147639193a to your computer and use it in GitHub Desktop.
Save mary-ext/6043fac4d612fe93a3a023147639193a to your computer and use it in GitHub Desktop.
import { effectScope, inject, type App, type EffectScope, type InjectionKey, type Plugin } from 'vue';
interface Store {
scope: EffectScope;
value: unknown;
setup: () => unknown;
}
interface StoreManager {
stores: Map<string, Store>;
initializing: Set<string>;
}
/**
* function type for retrieving a store instance
* @param manager optional store manager instance
* @returns the store value
*/
export type UseStoreFunction<T> = (manager?: StoreManager) => 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 MANAGER_KEY: InjectionKey<StoreManager> = Symbol();
/**
* creates a new store manager instance
* @returns a store manager
*/
export const createStoreManager = (): StoreManager & Plugin => {
return {
stores: new Map(),
initializing: new Set(),
install(app: App) {
// provide the store manager to the entire application
app.provide(MANAGER_KEY, this);
},
};
};
/**
* disposes all stores in a store manager and cleans up resources
* @param manager the store manager to dispose
*/
export const disposeStoreManager = (manager: StoreManager): void => {
for (const store of manager.stores.values()) {
store.scope.stop();
}
manager.stores.clear();
manager.initializing.clear();
};
/**
* retrieves the store manager from the current Vue injection context
* @returns the store manager instance or undefined if not found
*/
export const useStoreManager = (): StoreManager | undefined => {
return inject(MANAGER_KEY, 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
*/
export const defineStore = <T>(id: string, setup: () => T): UseStoreFunction<T> => {
const useStore: UseStoreFunction<T> = (manager = useStoreManager()) => {
if (!manager) {
throw new Error(`missing store manager`);
}
let instance = manager.stores.get(id);
if (instance === undefined || instance.setup !== setup) {
instance?.scope.stop();
if (manager.initializing.has(id)) {
throw new CircularDependencyError([...manager.initializing, id]);
}
manager.initializing.add(id);
try {
const scope = effectScope(true);
const value = scope.run(setup)!;
manager.stores.set(id, (instance = { scope, setup, value }));
} finally {
manager.initializing.delete(id);
}
}
return instance.value as T;
};
return useStore;
};
if (import.meta.hot) {
const hot = import.meta.hot;
hot.accept(() => hot.invalidate());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment