Last active
July 23, 2025 11:00
-
-
Save mary-ext/6043fac4d612fe93a3a023147639193a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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