a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1/**
2 * Global reactive store for cross-scope state management
3 *
4 * The store holds signals that are accessible from any scope via $store.
5 * Supports both programmatic registration and declarative initialization.
6 */
7
8import type { Optional } from "$types/helpers";
9import type { GlobalStore, Signal } from "$types/volt";
10import { signal } from "./signal";
11
12/**
13 * Internal signal registry for the global store
14 */
15const storeSignals = new Map<string, Signal<unknown>>();
16
17/**
18 * Global store singleton with helper methods and direct signal access
19 */
20const store: GlobalStore = {
21 _signals: storeSignals,
22
23 get<T = unknown>(key: string): Optional<T> {
24 const sig = storeSignals.get(key);
25 return sig ? (sig.get() as T) : undefined;
26 },
27
28 set<T = unknown>(key: string, value: T): void {
29 const sig = storeSignals.get(key);
30 if (sig) {
31 sig.set(value);
32 } else {
33 storeSignals.set(key, signal(value));
34 }
35 },
36
37 has(key: string): boolean {
38 return storeSignals.has(key);
39 },
40};
41
42/**
43 * Register state in the global store.
44 *
45 * Accepts either:
46 * - An object of signals: { theme: signal('dark') }
47 * - An object of values: { count: 0 } (auto-wrapped in signals)
48 *
49 * @param state - Object containing signals or raw values to register globally
50 *
51 * @example
52 * ```ts
53 * // With signals
54 * registerStore({
55 * theme: signal('dark'),
56 * user: signal({ name: 'Alice' })
57 * });
58 *
59 * // With raw values (auto-wrapped)
60 * registerStore({
61 * count: 0,
62 * isLoggedIn: false
63 * });
64 * ```
65 */
66export function registerStore(state: Record<string, unknown>): void {
67 for (const [key, value] of Object.entries(state)) {
68 if (value && typeof value === "object" && "get" in value && "set" in value && "subscribe" in value) {
69 storeSignals.set(key, value as Signal<unknown>);
70 Object.defineProperty(store, key, { get: () => value, enumerable: true, configurable: true });
71 } else {
72 const sig = signal(value);
73 storeSignals.set(key, sig);
74 Object.defineProperty(store, key, { get: () => sig, enumerable: true, configurable: true });
75 }
76 }
77}
78
79/**
80 * Get the global store instance.
81 *
82 * The store provides:
83 * - Direct signal access: `$store.theme` returns the signal
84 * - Helper methods: `$store.get('theme')` returns the unwrapped value
85 * - Signal creation: `$store.set('newKey', value)` creates a new signal
86 *
87 * @returns The global store singleton
88 *
89 * @example
90 * ```ts
91 * const store = getStore();
92 *
93 * // Access signal directly
94 * const themeSignal = store.theme;
95 *
96 * // Get unwrapped value
97 * const currentTheme = store.get('theme');
98 *
99 * // Set value (creates signal if doesn't exist)
100 * store.set('theme', 'light');
101 * ```
102 */
103export function getStore(): GlobalStore {
104 return store;
105}