lume-js 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.d.ts CHANGED
@@ -4,186 +4,23 @@
4
4
  * Provides type safety for reactive state management
5
5
  */
6
6
 
7
- /**
8
- * Unsubscribe function returned by $subscribe
9
- */
10
- export type Unsubscribe = () => void;
11
-
12
- /**
13
- * Subscriber callback function
14
- */
15
- export type Subscriber<T> = (value: T) => void;
16
-
17
- /**
18
- * Internal unique symbol for reactive state branding
19
- * @internal
20
- */
21
- declare const lumeReactiveSymbol: unique symbol;
22
-
23
- /**
24
- * Plugin interface for extending state behavior
25
- *
26
- * All hooks are optional. Hooks execute in the order plugins are registered.
27
- *
28
- * @example
29
- * ```typescript
30
- * const debugPlugin: Plugin = {
31
- * name: 'debug',
32
- * onGet: (key, value) => {
33
- * console.log(`GET ${key}:`, value);
34
- * return value; // Return value to pass to next plugin
35
- * },
36
- * onSet: (key, newValue, oldValue) => {
37
- * console.log(`SET ${key}:`, oldValue, '→', newValue);
38
- * return newValue; // Return value to pass to next plugin
39
- * }
40
- * };
41
- *
42
- * const store = withPlugins(state({ count: 0 }), [debugPlugin]);
43
- * ```
44
- */
45
- export interface Plugin {
46
- /**
47
- * Plugin name (for debugging)
48
- */
49
- name: string;
50
-
51
- /**
52
- * Called when state object is created
53
- * Runs synchronously before Proxy is returned
54
- */
55
- onInit?(): void;
7
+ // ── Kernel types & functions ─────────────────────────────────────────────
8
+ // The DOM-free kernel (state, batch, withReadObserver and their types)
9
+ // lives in ./state.d.ts — the source of truth for the `lume-js/state`
10
+ // entry, which must type-check without lib.dom (Node/worker consumers).
11
+ // This entry re-exports the kernel and adds the DOM-flavored API.
56
12
 
57
- /**
58
- * Called when a property is accessed (before value returned)
59
- * Can transform the value by returning a new value
60
- *
61
- * Chain pattern: Each plugin receives the output of the previous plugin
62
- *
63
- * @param key - Property key being accessed
64
- * @param value - Current value (possibly transformed by previous plugins)
65
- * @returns Transformed value, or undefined to keep current value
66
- */
67
- onGet?(key: string, value: any): any;
68
-
69
- /**
70
- * Called when a property is updated (before subscribers notified)
71
- * Can transform or validate the new value
72
- *
73
- * Chain pattern: Each plugin receives the output of the previous plugin
74
- *
75
- * @param key - Property key being updated
76
- * @param newValue - New value being set (possibly transformed by previous plugins)
77
- * @param oldValue - Previous value
78
- * @returns Transformed value, or undefined to keep current value
79
- */
80
- onSet?(key: string, newValue: any, oldValue: any): any;
13
+ export type {
14
+ Unsubscribe,
15
+ Subscriber,
16
+ Plugin,
17
+ TypedPlugin,
18
+ ReactiveState,
19
+ } from './state.js';
81
20
 
82
- /**
83
- * Called when a subscriber is added
84
- * Useful for tracking active subscriptions
85
- *
86
- * @param key - Property key being subscribed to
87
- */
88
- onSubscribe?(key: string): void;
21
+ export { state, batch, withReadObserver } from './state.js';
89
22
 
90
- /**
91
- * Called when subscribers are about to be notified
92
- * Runs in microtask, before subscribers receive value
93
- *
94
- * @param key - Property key that changed
95
- * @param value - New value being notified
96
- */
97
- onNotify?(key: string, value: any): void;
98
- }
99
-
100
- /**
101
- * Type-safe plugin interface for when you know the state shape.
102
- * Provides better intellisense for key names and value types.
103
- *
104
- * @example
105
- * ```typescript
106
- * interface AppState {
107
- * count: number;
108
- * name: string;
109
- * }
110
- *
111
- * const myPlugin: TypedPlugin<AppState> = {
112
- * name: 'typed',
113
- * onSet: (key, newValue, oldValue) => {
114
- * // key is 'count' | 'name', values are properly typed
115
- * return newValue;
116
- * }
117
- * };
118
- * ```
119
- */
120
- export interface TypedPlugin<T extends object> {
121
- name: string;
122
- onInit?(): void;
123
- onGet?<K extends keyof T>(key: K, value: T[K]): T[K] | undefined;
124
- onSet?<K extends keyof T>(key: K, newValue: T[K], oldValue: T[K]): T[K] | undefined;
125
- onSubscribe?<K extends keyof T>(key: K): void;
126
- onNotify?<K extends keyof T>(key: K, value: T[K]): void;
127
- }
128
-
129
- /**
130
- * Reactive state object with $subscribe method
131
- */
132
- export type ReactiveState<T extends object> = T & {
133
- /**
134
- * Subscribe to changes on a specific property key
135
- * @param key - Property key to watch
136
- * @param callback - Function called when property changes
137
- * @returns Unsubscribe function for cleanup
138
- */
139
- $subscribe<K extends keyof T>(
140
- key: K,
141
- callback: Subscriber<T[K]>
142
- ): Unsubscribe;
143
-
144
- /**
145
- * Register a callback to run before each flush.
146
- * Dedupes duplicate function references.
147
- * @param fn - Callback function
148
- * @returns Unsubscribe function for cleanup
149
- */
150
- $beforeFlush(fn: () => void): Unsubscribe;
151
-
152
- /**
153
- * Brand to identify reactive state objects at the type level
154
- * @internal
155
- */
156
- readonly [lumeReactiveSymbol]?: true;
157
- };
158
-
159
- /**
160
- * Create a reactive state object
161
- *
162
- * @param obj - Plain object to make reactive
163
- * @returns Reactive proxy with $subscribe method
164
- * @throws {Error} If obj is not a plain object
165
- *
166
- * @example
167
- * ```typescript
168
- * const store = state({
169
- * count: 0,
170
- * user: state({
171
- * name: 'Alice'
172
- * })
173
- * });
174
- *
175
- * store.count++; // Triggers reactivity
176
- *
177
- * const unsub = store.$subscribe('count', (val) => {
178
- * console.log('Count:', val);
179
- * });
180
- *
181
- * // Cleanup
182
- * unsub();
183
- * ```
184
- *
185
- */
186
- export function state<T extends object>(obj: T): ReactiveState<T>;
23
+ import type { ReactiveState, Unsubscribe } from './state.js';
187
24
 
188
25
  /**
189
26
  * Handler interface for extending bindDom with custom reactive data-* attributes.
@@ -353,69 +190,6 @@ export function effect(fn: () => void): Unsubscribe;
353
190
  */
354
191
  export function effect(fn: () => void, deps: EffectDependency[]): Unsubscribe;
355
192
 
356
- /**
357
- * Group multiple state writes and flush them together, synchronously,
358
- * when the outermost batch() returns.
359
- *
360
- * Guarantees:
361
- * - Subscribers see only the final value of each key written in the batch.
362
- * - An effect depending on several stores mutated in the batch runs exactly
363
- * ONCE (normal microtask batching is per-state: such effects run once per
364
- * mutated store).
365
- * - Nested batch() calls are absorbed into the outermost batch.
366
- * - If fn throws, writes made before the throw still flush, then the error
367
- * propagates.
368
- *
369
- * fn must be synchronous. Writes after an `await` fall back to normal
370
- * per-state microtask flushing (a console warning is logged if fn returns
371
- * a Promise).
372
- *
373
- * @param fn - Function performing state writes
374
- * @returns The return value of fn
375
- * @throws {Error} If fn is not a function
376
- *
377
- * @example
378
- * ```typescript
379
- * import { state, effect, batch } from 'lume-js';
380
- *
381
- * const a = state({ value: 1 });
382
- * const b = state({ value: 2 });
383
- *
384
- * effect(() => {
385
- * render(a.value + b.value);
386
- * });
387
- *
388
- * batch(() => {
389
- * a.value = 10;
390
- * b.value = 20;
391
- * }); // render() ran exactly once, seeing 30
392
- * ```
393
- */
394
- export function batch<T>(fn: () => T): T;
395
-
396
- /**
397
- * Run a function with a read observer active.
398
- *
399
- * The observer receives `(proxy, key, registerEffect)` for every property read
400
- * during the synchronous execution of `fn`. Used internally by `effect()` for
401
- * auto-tracking, and exposed for building custom reactive primitives.
402
- *
403
- * @param onRead - Called on each property access inside fn
404
- * @param fn - The function to run under observation
405
- * @returns The return value of fn
406
- *
407
- * @internal
408
- */
409
- export function withReadObserver<T>(
410
- onRead: (
411
- proxy: ReactiveState<any>,
412
- key: string,
413
- registerEffect: (key: string, executeFn: () => void) => () => void
414
- ) => void,
415
- fn: () => T
416
- ): T;
417
-
418
-
419
193
  // ============================================================================
420
194
  // Utility Types
421
195
  // ============================================================================
package/src/state.d.ts CHANGED
@@ -1,17 +1,252 @@
1
1
  /**
2
2
  * Lume.js Universal State Entry — TypeScript Definitions
3
3
  *
4
- * The DOM-free kernel: state() + batch() + withReadObserver().
5
- * Import from "lume-js/state" in Node, Deno, workers, and CLI tools.
6
- * Types are shared with the full core entry.
4
+ * Source of truth for the DOM-free kernel: state(), batch(),
5
+ * withReadObserver() and their types. This file must type-check WITHOUT
6
+ * lib.dom it is what `lume-js/state` consumers in Node, Deno, and
7
+ * workers resolve (the full `lume-js` entry re-exports everything here
8
+ * and adds the DOM-flavored API on top).
7
9
  */
8
10
 
9
- export type {
10
- Unsubscribe,
11
- Subscriber,
12
- Plugin,
13
- TypedPlugin,
14
- ReactiveState,
15
- } from './index.js';
11
+ /**
12
+ * Unsubscribe function returned by $subscribe
13
+ */
14
+ export type Unsubscribe = () => void;
15
+
16
+ /**
17
+ * Subscriber callback function
18
+ */
19
+ export type Subscriber<T> = (value: T) => void;
20
+
21
+ /**
22
+ * Internal unique symbol for reactive state branding
23
+ * @internal
24
+ */
25
+ declare const lumeReactiveSymbol: unique symbol;
26
+
27
+ /**
28
+ * Plugin interface for extending state behavior
29
+ *
30
+ * All hooks are optional. Hooks execute in the order plugins are registered.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const debugPlugin: Plugin = {
35
+ * name: 'debug',
36
+ * onGet: (key, value) => {
37
+ * console.log(`GET ${key}:`, value);
38
+ * return value; // Return value to pass to next plugin
39
+ * },
40
+ * onSet: (key, newValue, oldValue) => {
41
+ * console.log(`SET ${key}:`, oldValue, '→', newValue);
42
+ * return newValue; // Return value to pass to next plugin
43
+ * }
44
+ * };
45
+ *
46
+ * const store = withPlugins(state({ count: 0 }), [debugPlugin]);
47
+ * ```
48
+ */
49
+ export interface Plugin {
50
+ /**
51
+ * Plugin name (for debugging)
52
+ */
53
+ name: string;
54
+
55
+ /**
56
+ * Called when state object is created
57
+ * Runs synchronously before Proxy is returned
58
+ */
59
+ onInit?(): void;
60
+
61
+ /**
62
+ * Called when a property is accessed (before value returned)
63
+ * Can transform the value by returning a new value
64
+ *
65
+ * Chain pattern: Each plugin receives the output of the previous plugin
66
+ *
67
+ * @param key - Property key being accessed
68
+ * @param value - Current value (possibly transformed by previous plugins)
69
+ * @returns Transformed value, or undefined to keep current value
70
+ */
71
+ onGet?(key: string, value: any): any;
72
+
73
+ /**
74
+ * Called when a property is updated (before subscribers notified)
75
+ * Can transform or validate the new value
76
+ *
77
+ * Chain pattern: Each plugin receives the output of the previous plugin
78
+ *
79
+ * @param key - Property key being updated
80
+ * @param newValue - New value being set (possibly transformed by previous plugins)
81
+ * @param oldValue - Previous value
82
+ * @returns Transformed value, or undefined to keep current value
83
+ */
84
+ onSet?(key: string, newValue: any, oldValue: any): any;
85
+
86
+ /**
87
+ * Called when a subscriber is added
88
+ * Useful for tracking active subscriptions
89
+ *
90
+ * @param key - Property key being subscribed to
91
+ */
92
+ onSubscribe?(key: string): void;
93
+
94
+ /**
95
+ * Called when subscribers are about to be notified
96
+ * Runs in microtask, before subscribers receive value
97
+ *
98
+ * @param key - Property key that changed
99
+ * @param value - New value being notified
100
+ */
101
+ onNotify?(key: string, value: any): void;
102
+ }
103
+
104
+ /**
105
+ * Type-safe plugin interface for when you know the state shape.
106
+ * Provides better intellisense for key names and value types.
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * interface AppState {
111
+ * count: number;
112
+ * name: string;
113
+ * }
114
+ *
115
+ * const myPlugin: TypedPlugin<AppState> = {
116
+ * name: 'typed',
117
+ * onSet: (key, newValue, oldValue) => {
118
+ * // key is 'count' | 'name', values are properly typed
119
+ * return newValue;
120
+ * }
121
+ * };
122
+ * ```
123
+ */
124
+ export interface TypedPlugin<T extends object> {
125
+ name: string;
126
+ onInit?(): void;
127
+ onGet?<K extends keyof T>(key: K, value: T[K]): T[K] | undefined;
128
+ onSet?<K extends keyof T>(key: K, newValue: T[K], oldValue: T[K]): T[K] | undefined;
129
+ onSubscribe?<K extends keyof T>(key: K): void;
130
+ onNotify?<K extends keyof T>(key: K, value: T[K]): void;
131
+ }
132
+
133
+ /**
134
+ * Reactive state object with $subscribe method
135
+ */
136
+ export type ReactiveState<T extends object> = T & {
137
+ /**
138
+ * Subscribe to changes on a specific property key
139
+ * @param key - Property key to watch
140
+ * @param callback - Function called when property changes
141
+ * @returns Unsubscribe function for cleanup
142
+ */
143
+ $subscribe<K extends keyof T>(
144
+ key: K,
145
+ callback: Subscriber<T[K]>
146
+ ): Unsubscribe;
147
+
148
+ /**
149
+ * Register a callback to run before each flush.
150
+ * Dedupes duplicate function references.
151
+ * @param fn - Callback function
152
+ * @returns Unsubscribe function for cleanup
153
+ */
154
+ $beforeFlush(fn: () => void): Unsubscribe;
155
+
156
+ /**
157
+ * Brand to identify reactive state objects at the type level
158
+ * @internal
159
+ */
160
+ readonly [lumeReactiveSymbol]?: true;
161
+ };
162
+
163
+ /**
164
+ * Create a reactive state object
165
+ *
166
+ * @param obj - Plain object to make reactive
167
+ * @returns Reactive proxy with $subscribe method
168
+ * @throws {Error} If obj is not a plain object
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * const store = state({
173
+ * count: 0,
174
+ * user: state({
175
+ * name: 'Alice'
176
+ * })
177
+ * });
178
+ *
179
+ * store.count++; // Triggers reactivity
180
+ *
181
+ * const unsub = store.$subscribe('count', (val) => {
182
+ * console.log('Count:', val);
183
+ * });
184
+ *
185
+ * // Cleanup
186
+ * unsub();
187
+ * ```
188
+ *
189
+ */
190
+ export function state<T extends object>(obj: T): ReactiveState<T>;
16
191
 
17
- export { state, batch, withReadObserver } from './index.js';
192
+ /**
193
+ * Group multiple state writes and flush them together, synchronously,
194
+ * when the outermost batch() returns.
195
+ *
196
+ * Guarantees:
197
+ * - Subscribers see only the final value of each key written in the batch.
198
+ * - An effect depending on several stores mutated in the batch runs exactly
199
+ * ONCE (normal microtask batching is per-state: such effects run once per
200
+ * mutated store).
201
+ * - Nested batch() calls are absorbed into the outermost batch.
202
+ * - If fn throws, writes made before the throw still flush, then the error
203
+ * propagates.
204
+ *
205
+ * fn must be synchronous. Writes after an `await` fall back to normal
206
+ * per-state microtask flushing (a console warning is logged if fn returns
207
+ * a Promise).
208
+ *
209
+ * @param fn - Function performing state writes
210
+ * @returns The return value of fn
211
+ * @throws {Error} If fn is not a function
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * import { state, effect, batch } from 'lume-js';
216
+ *
217
+ * const a = state({ value: 1 });
218
+ * const b = state({ value: 2 });
219
+ *
220
+ * effect(() => {
221
+ * render(a.value + b.value);
222
+ * });
223
+ *
224
+ * batch(() => {
225
+ * a.value = 10;
226
+ * b.value = 20;
227
+ * }); // render() ran exactly once, seeing 30
228
+ * ```
229
+ */
230
+ export function batch<T>(fn: () => T): T;
231
+
232
+ /**
233
+ * Run a function with a read observer active.
234
+ *
235
+ * The observer receives `(proxy, key, registerEffect)` for every property read
236
+ * during the synchronous execution of `fn`. Used internally by `effect()` for
237
+ * auto-tracking, and exposed for building custom reactive primitives.
238
+ *
239
+ * @param onRead - Called on each property access inside fn
240
+ * @param fn - The function to run under observation
241
+ * @returns The return value of fn
242
+ *
243
+ * @internal
244
+ */
245
+ export function withReadObserver<T>(
246
+ onRead: (
247
+ proxy: ReactiveState<any>,
248
+ key: string,
249
+ registerEffect: (key: string, executeFn: () => void) => () => void
250
+ ) => void,
251
+ fn: () => T
252
+ ): T;
@@ -1 +0,0 @@
1
- {"version":3,"file":"shared-Bk_gndPJ.mjs","sources":["../src/core/batch.js","../src/core/state.js"],"sourcesContent":["/**\n * Lume-JS Cross-Store Batching\n *\n * While batchDepth > 0, states skip their microtask flush and enqueue a\n * small flush handle here instead (via enqueueIfBatching, called from\n * state.js's scheduler); batch() drains the set synchronously when the\n * outermost batch ends. Effects collected from all enqueued states run\n * from one Set per wave, so an effect depending on several mutated stores\n * runs exactly once per batch instead of once per store.\n *\n * This module never imports state.js — state.js imports from here — so\n * there is no cycle, no global scheduler object, and no import side effect.\n */\n\nimport { logError, logWarn } from '../utils/log.js';\n\n// Cap for cascading flush waves (effects mutating state that re-triggers\n// effects). Shared with the per-state microtask flush in state.js.\nexport const MAX_FLUSH_ITERATIONS = 100;\n\nlet batchDepth = 0;\nconst batchedStates = new Set();\n\n/**\n * Called by state.js when a write is scheduled. Returns true if a batch is\n * active and the state's flush handle was captured (the caller must then\n * skip its own microtask scheduling).\n *\n * Internal API between core modules — not exported from the package root.\n *\n * @param {{runBeforeFlushHooks: function, notifySubscribers: function, takeEffects: function}} handle\n * @returns {boolean}\n */\nexport function enqueueIfBatching(handle) {\n if (batchDepth === 0) return false;\n batchedStates.add(handle);\n return true;\n}\n\nfunction flushBatchedStates() {\n let iterations = 0;\n while (batchedStates.size > 0 && iterations < MAX_FLUSH_ITERATIONS) {\n iterations++;\n const wave = Array.from(batchedStates);\n batchedStates.clear();\n\n // Notify each state's subscribers, collecting effects into one\n // deduplicated set (the same effect queued by N stores runs once).\n const effects = new Set();\n for (const s of wave) {\n s.runBeforeFlushHooks();\n s.notifySubscribers();\n for (const fx of s.takeEffects()) effects.add(fx);\n }\n\n // Effects run after all subscribers of the wave. Writes they make\n // re-enter batchedStates (depth is still held) → next iteration.\n for (const fx of effects) {\n try { fx(); }\n catch (err) { logError('[Lume.js state] Error in effect:', err); }\n }\n }\n if (iterations >= MAX_FLUSH_ITERATIONS) {\n // Drop the runaway wave so a future, unrelated batch doesn't inherit\n // it. Nothing is lost permanently: the states keep their queued work\n // and flush it on their next write via the normal microtask path.\n batchedStates.clear();\n logError(\n '[Lume.js state] Maximum batch flush iterations reached (100). ' +\n 'This usually indicates an infinite loop caused by an effect or computed mutating state it depends on.'\n );\n }\n}\n\n/**\n * Group multiple state writes and flush them together, synchronously,\n * when the outermost batch() returns.\n *\n * Guarantees:\n * - Subscribers see only the final value of each key (intermediate writes\n * within the batch are coalesced, as with microtask batching).\n * - An effect that depends on several stores mutated in the batch runs\n * exactly ONCE — unlike microtask batching, which is per-state and runs\n * such effects once per store.\n * - Nested batch() calls are absorbed: everything flushes when the\n * outermost batch ends.\n * - If fn throws, writes made before the throw still flush, then the\n * error propagates. State scheduling is left clean either way.\n *\n * `fn` must be synchronous — writes after an `await` happen outside the\n * batch and fall back to normal per-state microtask flushing (a console\n * warning is logged if fn returns a Promise).\n *\n * @param {function} fn - Function performing state writes\n * @returns {*} The return value of fn\n *\n * @example\n * import { state, effect, batch } from 'lume-js';\n *\n * const a = state({ value: 1 });\n * const b = state({ value: 2 });\n * effect(() => render(a.value + b.value));\n *\n * batch(() => {\n * a.value = 10;\n * b.value = 20;\n * }); // render() ran exactly once, seeing 30\n */\nexport function batch(fn) {\n if (typeof fn !== 'function') {\n throw new Error('batch() requires a function');\n }\n\n // Nested batch: let the outermost batch flush everything\n if (batchDepth > 0) return fn();\n\n batchDepth++;\n let result;\n try {\n result = fn();\n if (result && typeof result.then === 'function') {\n logWarn(\n '[Lume.js batch] batch() received an async function. Only writes before the first await are batched; ' +\n 'later writes flush via normal microtasks.'\n );\n }\n return result;\n } finally {\n // Flush while depth is still held so cascading writes from\n // subscribers/effects keep collecting into batchedStates (deduped),\n // then release. Runs on success AND when fn throws (writes made\n // before the throw are committed, then the error propagates).\n try {\n flushBatchedStates();\n } finally {\n batchDepth--;\n }\n }\n}\n","/**\n * Lume-JS Reactive State Core\n *\n * Provides minimal reactive state with standard JavaScript.\n * Features automatic microtask batching for performance.\n * Read tracking is opt-in via withReadObserver — state.js has zero permanent\n * dependency on effect.js or any other module.\n *\n * Features:\n * - Lightweight and Go-style\n * - Explicit nested states\n * - $subscribe for listening to key changes\n * - Cleanup with unsubscribe\n * - Per-state microtask batching for writes\n * - batch() for grouping writes across states with cross-store effect dedupe\n * - Scope-based read tracking via withReadObserver (multi-observer safe)\n *\n * Usage:\n * import { state } from \"lume-js\";\n *\n * const store = state({ count: 0 });\n * const unsub = store.$subscribe(\"count\", val => console.log(val));\n * unsub(); // cleanup\n */\n\nimport { logError, logWarn } from '../utils/log.js';\nimport { enqueueIfBatching, MAX_FLUSH_ITERATIONS } from './batch.js';\n\n// Per-state batching – each state object maintains its own microtask flush.\n// This keeps effects simple and aligned with Lume's minimal philosophy.\n\n/**\n * Creates a reactive state object.\n *\n * @param {Object} obj - Initial state object (must be plain object)\n * @returns {Proxy} Reactive proxy with $subscribe method\n *\n * @example\n * const store = state({ count: 0 });\n */\n\n// Active read observers — only populated during withReadObserver scopes.\n// This keeps state.js pure: tracking only happens when someone explicitly\n// asks to observe reads within a synchronous function call.\n//\n// Note: This Set is module-level, so all reactive state instances and effects\n// within the SAME module instance share it. This is standard behavior for\n// auto-tracking reactive libraries (Vue, MobX, Solid, etc.). Multiple copies\n// of the lume-js module (e.g. from different bundled chunks) each get their\n// own independent Set via ES module / CommonJS isolation.\nconst readers = new Set();\n\n/**\n * Brand symbol stamped on every object passed to state().\n *\n * Uses the global symbol registry (Symbol.for) so independent copies of\n * lume-js on the same page (e.g. a CDN build next to a bundled chunk)\n * agree on the same brand. This is a type tag for reliable detection\n * (see isReactive in addons), not a security boundary — any code can\n * stamp it.\n *\n * Internal API — exported for addons; not re-exported from the package root.\n */\nexport const REACTIVE_BRAND = Symbol.for('lume.reactive');\n\n// batch() lives in ./batch.js (which never imports this module — no cycle).\n// state.js participates through enqueueIfBatching in scheduleFlush below.\n\n/**\n * Run a function with a read observer active.\n * The observer receives (proxy, key, registerEffect) for every property read.\n * Multiple observers can be active simultaneously (nested effects, devtools, etc.)\n *\n * Internal API — used by effect.js for auto-tracking. May be stabilized\n * for third-party addons in a future release.\n *\n * @security The observer sees reads from ALL state instances within the same\n * module instance, including nested scopes. Only pass trusted observer functions.\n * A future scoped variant (e.g., scopedReadObserver(store, fn)) may limit\n * observation to a single state instance.\n *\n * @param {function} onRead - Called on each property access inside fn\n * @param {function} fn - The function to run under observation\n */\nexport function withReadObserver(onRead, fn) {\n readers.add(onRead);\n try {\n return fn();\n } finally {\n readers.delete(onRead);\n }\n}\n\nexport function state(obj) {\n // Validate input\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {\n throw new Error('state() requires a plain object');\n }\n if (Object.isFrozen(obj) || Object.isSealed(obj)) {\n throw new Error('state() requires a mutable plain object');\n }\n\n // Object.create(null) - no prototype chain lookups\n const listeners = Object.create(null);\n const pendingNotifications = new Map(); // Per-state pending changes\n const pendingEffects = new Set(); // Dedupe effects per state\n const beforeFlushHooks = [];\n let flushScheduled = false;\n\n // ── Flush steps ──────────────────────────────────────────────────────\n // Named pieces shared by the per-state microtask flush and batch().\n\n function runBeforeFlushHooks() {\n for (let i = 0; i < beforeFlushHooks.length; i++) {\n try {\n beforeFlushHooks[i]();\n } catch (err) {\n logError('[Lume.js state] Error in beforeFlush hook:', err);\n }\n }\n }\n\n function notifySubscribers() {\n for (const [key, value] of pendingNotifications) {\n if (listeners[key]) {\n const subs = listeners[key];\n let i = 0;\n while (i < subs.length) {\n const fn = subs[i];\n try {\n fn(value);\n } catch (err) {\n logError(`[Lume.js state] Error notifying subscriber for key \"${String(key)}\":`, err);\n }\n // Only advance if fn wasn't removed (something shifted into its place)\n if (subs[i] === fn) i++;\n }\n }\n }\n pendingNotifications.clear();\n }\n\n /** Drain queued effects (Set deduplicates) into an array. */\n function takeEffects() {\n const effects = Array.from(pendingEffects);\n pendingEffects.clear();\n return effects;\n }\n\n // Handle this state gives batch() — flush steps only, no live queues.\n const batchHandle = { runBeforeFlushHooks, notifySubscribers, takeEffects };\n\n /**\n * Schedule a single microtask flush for this state object.\n *\n * Flush order per state:\n * 1) Notify subscribers for changed keys (key → subscribers)\n * 2) Run each queued effect exactly once (Set-based dedupe)\n * 3) Repeat up to 100 iterations to handle cascading updates,\n * then log an error to prevent infinite loops.\n *\n * Notes:\n * - Batching is per state; effects that depend on multiple states\n * may run once per state that changed (by design). Use batch() to\n * group writes across states and run such effects once.\n * - Inside batch(), the microtask is skipped: the state enqueues\n * itself for the synchronous flush at the end of the batch.\n */\n function scheduleFlush() {\n // Inside batch(): the batch captures this state's flush handle and\n // flushes synchronously at the end — skip the microtask.\n if (enqueueIfBatching(batchHandle)) return;\n\n if (flushScheduled) return;\n\n flushScheduled = true;\n queueMicrotask(() => {\n let iterations = 0;\n\n try {\n while ((pendingNotifications.size > 0 || pendingEffects.size > 0) && iterations < MAX_FLUSH_ITERATIONS) {\n iterations++;\n runBeforeFlushHooks();\n notifySubscribers();\n const effects = takeEffects();\n for (let i = 0; i < effects.length; i++) {\n try {\n effects[i]();\n } catch (err) {\n logError('[Lume.js state] Error in effect:', err);\n }\n }\n }\n } finally {\n flushScheduled = false;\n }\n\n if (iterations >= MAX_FLUSH_ITERATIONS) {\n logError(\n '[Lume.js state] Maximum flush iterations reached (100). ' +\n 'This usually indicates an infinite loop caused by an effect or computed mutating state it depends on.'\n );\n }\n });\n }\n\n // Stamp the shared brand (non-enumerable: spreads/Object.assign copies\n // of a store do not inherit the brand and won't masquerade as reactive).\n Object.defineProperty(obj, REACTIVE_BRAND, { value: true });\n\n const MAX_SUBSCRIBERS = 1000;\n const noopUnsubscribe = () => {};\n\n /**\n * Shared listener registration with a per-key cap (subscriber DoS\n * protection). Applied identically to $subscribe callbacks and effect\n * subscriptions so both paths degrade the same way: a loud console\n * error and a no-op unsubscribe.\n */\n function addListener(key, fn, kind) {\n if (!listeners[key]) listeners[key] = [];\n if (listeners[key].length >= MAX_SUBSCRIBERS) {\n logError(\n `[Lume.js state] Subscriber limit (${MAX_SUBSCRIBERS}) reached for key \"${String(key)}\". ` +\n `${kind} ignored — it will NOT receive updates. ` +\n 'This usually means subscriptions are created in a loop without cleanup.'\n );\n return noopUnsubscribe;\n }\n listeners[key].push(fn);\n return () => {\n if (listeners[key]) {\n const idx = listeners[key].indexOf(fn);\n if (idx !== -1) {\n listeners[key].splice(idx, 1);\n if (listeners[key].length === 0) delete listeners[key];\n }\n }\n };\n }\n\n // Defined once per state instance — not per property read — to avoid per-read closure allocation.\n const registerEffect = (key, executeFn) => {\n const callback = () => {\n pendingEffects.add(executeFn);\n };\n return addListener(key, callback, 'Effect subscription');\n };\n\n const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\n const proxy = new Proxy(obj, {\n get(target, key) {\n // Skip effect tracking for internal meta methods (e.g. $subscribe)\n if (typeof key === 'string' && key.startsWith('$')) {\n return target[key];\n }\n\n const value = target[key];\n\n // Notify active read observers (effects, devtools, etc.)\n if (readers.size > 0) {\n for (const reader of readers) {\n reader(proxy, key, registerEffect);\n }\n }\n\n return value;\n },\n\n set(target, key, value) {\n if (typeof key === 'string' && BLOCKED_KEYS.has(key)) {\n logWarn(`[Lume.js state] Blocked write to reserved key \"${key}\"`);\n return true;\n }\n\n const oldValue = target[key];\n\n // Skip update if value unchanged - Object.is() handles NaN and -0 correctly\n if (Object.is(oldValue, value)) return true;\n\n target[key] = value;\n\n // Batch notifications at the state level (per-state, not global)\n pendingNotifications.set(key, value);\n scheduleFlush();\n\n return true;\n }\n });\n\n /**\n * Subscribe to changes for a specific key.\n * Calls the callback immediately with the current value.\n * Returns an unsubscribe function for cleanup.\n *\n * @param {string} key - Property key to watch\n * @param {function} fn - Callback function\n * @returns {function} Unsubscribe function\n */\n // Set on obj (not proxy) to avoid triggering the set trap.\n // The get trap already returns target[key] directly for $-prefixed keys.\n /**\n * Register a callback to run before each flush.\n * Returns an unsubscribe function.\n */\n obj.$beforeFlush = (fn) => {\n if (typeof fn !== 'function') {\n throw new Error('$beforeFlush requires a function');\n }\n if (beforeFlushHooks.indexOf(fn) === -1) {\n beforeFlushHooks.push(fn);\n }\n return () => {\n const idx = beforeFlushHooks.indexOf(fn);\n if (idx !== -1) {\n beforeFlushHooks.splice(idx, 1);\n }\n };\n };\n\n obj.$subscribe = (key, fn) => {\n if (typeof fn !== 'function') {\n throw new Error('Subscriber must be a function');\n }\n\n const unsubscribe = addListener(key, fn, 'New subscriber');\n\n // Over the cap: listener was not added, skip the immediate call too\n if (unsubscribe === noopUnsubscribe) return unsubscribe;\n\n // Call immediately with current value (NOT batched)\n fn(proxy[key]);\n\n return unsubscribe;\n };\n\n return proxy;\n}\n"],"names":[],"mappings":"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"}