lume-js 2.0.0-beta.2 → 2.0.0

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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Creates a cleanup group that can collect and dispose multiple
3
+ * cleanup/unsubscribe functions at once.
4
+ *
5
+ * @returns {CleanupGroup}
6
+ *
7
+ * @example
8
+ * ```js
9
+ * import { createCleanupGroup } from 'lume-js/addons';
10
+ *
11
+ * const group = createCleanupGroup();
12
+ * group.add(bindDom(root, store));
13
+ * group.add(effect(() => { ... }));
14
+ * group.add(store.$subscribe('key', fn));
15
+ *
16
+ * // Dispose everything at once
17
+ * group.dispose();
18
+ * ```
19
+ */
20
+ export function createCleanupGroup() {
21
+ const cleanups = [];
22
+
23
+ return {
24
+ /**
25
+ * Add a cleanup function to the group.
26
+ * @param {Function} fn - Cleanup/unsubscribe function
27
+ */
28
+ add(fn) {
29
+ if (typeof fn === 'function') {
30
+ cleanups.push(fn);
31
+ }
32
+ },
33
+
34
+ /**
35
+ * Run all collected cleanup functions and clear the group.
36
+ */
37
+ dispose() {
38
+ while (cleanups.length) {
39
+ const fn = cleanups.pop();
40
+ try { fn(); } catch (e) { /* ignore cleanup errors */ }
41
+ }
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Reads initial state from a `<script type="application/json">` element
3
+ * embedded in the server-rendered HTML. Useful for SSR / hydration patterns.
4
+ *
5
+ * @param {string} [selector='#__LUME_DATA__'] - CSS selector for the script element
6
+ * @returns {object} Parsed JSON object, or empty object if not found / invalid
7
+ *
8
+ * @example
9
+ * ```html
10
+ * <script id="__LUME_DATA__" type="application/json">
11
+ * {"title": "Welcome", "count": 42}
12
+ * </script>
13
+ * ```
14
+ *
15
+ * ```js
16
+ * import { state } from 'lume-js';
17
+ * import { hydrateState } from 'lume-js/addons';
18
+ *
19
+ * const store = state(hydrateState());
20
+ * ```
21
+ */
22
+ export function hydrateState(selector = '#__LUME_DATA__') {
23
+ const el = typeof document !== 'undefined' ? document.querySelector(selector) : null;
24
+ if (!el) return {};
25
+ try {
26
+ return JSON.parse(el.textContent);
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
@@ -320,11 +320,11 @@ export interface DebugPlugin {
320
320
  * @example
321
321
  * ```typescript
322
322
  * import { state } from 'lume-js';
323
- * import { createDebugPlugin } from 'lume-js/addons';
324
- *
325
- * const store = state({ count: 0 }, {
326
- * plugins: [createDebugPlugin({ label: 'counter' })]
327
- * });
323
+ * import { createDebugPlugin, withPlugins } from 'lume-js/addons';
324
+ *
325
+ * const store = withPlugins(state({ count: 0 }), [
326
+ * createDebugPlugin({ label: 'counter' })
327
+ * ]);
328
328
  * ```
329
329
  */
330
330
  export function createDebugPlugin(options?: DebugPluginOptions): DebugPlugin;
@@ -434,3 +434,51 @@ export interface Plugin {
434
434
  */
435
435
  export function withPlugins<T extends object>(store: ReactiveState<T>, plugins: Plugin[]): ReactiveState<T>;
436
436
 
437
+ /**
438
+ * A group that collects cleanup/unsubscribe functions and can dispose them all at once.
439
+ *
440
+ * @example
441
+ * ```typescript
442
+ * import { createCleanupGroup } from 'lume-js/addons';
443
+ *
444
+ * const group = createCleanupGroup();
445
+ * group.add(bindDom(root, store));
446
+ * group.add(effect(() => { ... }));
447
+ * group.add(store.$subscribe('key', fn));
448
+ *
449
+ * // Dispose everything at once
450
+ * group.dispose();
451
+ * ```
452
+ */
453
+ export interface CleanupGroup {
454
+ /** Add a cleanup/unsubscribe function to the group */
455
+ add(fn: () => void): void;
456
+ /** Run all collected cleanup functions and clear the group */
457
+ dispose(): void;
458
+ }
459
+
460
+ /**
461
+ * Creates a cleanup group that can collect and dispose multiple
462
+ * cleanup/unsubscribe functions at once.
463
+ *
464
+ * @returns A CleanupGroup instance
465
+ */
466
+ export function createCleanupGroup(): CleanupGroup;
467
+
468
+ /**
469
+ * Reads initial state from a `<script type="application/json">` element
470
+ * embedded in the server-rendered HTML.
471
+ *
472
+ * @param selector - CSS selector for the script element (default: '#__LUME_DATA__')
473
+ * @returns Parsed JSON object, or empty object if not found or invalid
474
+ *
475
+ * @example
476
+ * ```typescript
477
+ * import { state } from 'lume-js';
478
+ * import { hydrateState } from 'lume-js/addons';
479
+ *
480
+ * const store = state(hydrateState());
481
+ * ```
482
+ */
483
+ export function hydrateState(selector?: string): object;
484
+
@@ -3,6 +3,8 @@ export { watch } from "./watch.js";
3
3
  export { repeat, defaultFocusPreservation, defaultScrollPreservation } from "./repeat.js";
4
4
  export { createDebugPlugin, debug } from "./debug.js";
5
5
  export { withPlugins } from "./withPlugins.js";
6
+ export { createCleanupGroup } from "./cleanupGroup.js";
7
+ export { hydrateState } from "./hydrateState.js";
6
8
 
7
9
  /**
8
10
  * Returns true if the value is a Lume reactive proxy created by state().
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Lume-JS List Rendering (Addon)
3
- * @experimental
4
3
  *
5
4
  * Renders lists with automatic subscription and element reuse by key.
6
5
  *
@@ -60,13 +60,22 @@ export function withPlugins(store, plugins = []) {
60
60
  pendingNotifications.clear();
61
61
  }
62
62
 
63
- // Register once on the underlying state; hook survives for lifetime of store.
63
+ // Register once on the underlying state; capture unsubscribe for cleanup.
64
+ let flushUnsub;
64
65
  if (typeof store.$beforeFlush === 'function') {
65
- store.$beforeFlush(runNotifyHooks);
66
+ flushUnsub = store.$beforeFlush(runNotifyHooks);
66
67
  }
67
68
 
68
69
  return new Proxy(store, {
69
70
  get(target, key) {
71
+ // $dispose — remove the beforeFlush hook and clear pending state
72
+ if (key === '$dispose') {
73
+ return () => {
74
+ if (flushUnsub) flushUnsub();
75
+ pendingNotifications.clear();
76
+ };
77
+ }
78
+
70
79
  // Pass $-prefixed meta methods through without interception
71
80
  if (typeof key === 'string' && key.startsWith('$')) {
72
81
  const method = target[key];
@@ -120,8 +120,7 @@ export function effect(fn, deps) {
120
120
  // Save previous subscriptions instead of cleaning immediately.
121
121
  // If fn() doesn't read any state (early return / error), we restore
122
122
  // them so the effect stays reactive.
123
- const oldCleanups = [...cleanups];
124
- cleanups.length = 0;
123
+ const oldCleanups = cleanups.splice(0);
125
124
 
126
125
  // Create effect context for tracking
127
126
  const myContext = {
package/src/index.d.ts CHANGED
@@ -14,6 +14,12 @@ export type Unsubscribe = () => void;
14
14
  */
15
15
  export type Subscriber<T> = (value: T) => void;
16
16
 
17
+ /**
18
+ * Internal unique symbol for reactive state branding
19
+ * @internal
20
+ */
21
+ declare const lumeReactiveSymbol: unique symbol;
22
+
17
23
  /**
18
24
  * Plugin interface for extending state behavior
19
25
  *
@@ -147,7 +153,7 @@ export type ReactiveState<T extends object> = T & {
147
153
  * Brand to identify reactive state objects at the type level
148
154
  * @internal
149
155
  */
150
- readonly [Symbol('lume.reactive')]?: true;
156
+ readonly [lumeReactiveSymbol]?: true;
151
157
  };
152
158
 
153
159
  /**
@@ -347,6 +353,28 @@ export function effect(fn: () => void): Unsubscribe;
347
353
  */
348
354
  export function effect(fn: () => void, deps: EffectDependency[]): Unsubscribe;
349
355
 
356
+ /**
357
+ * Run a function with a read observer active.
358
+ *
359
+ * The observer receives `(proxy, key, registerEffect)` for every property read
360
+ * during the synchronous execution of `fn`. Used internally by `effect()` for
361
+ * auto-tracking, and exposed for building custom reactive primitives.
362
+ *
363
+ * @param onRead - Called on each property access inside fn
364
+ * @param fn - The function to run under observation
365
+ * @returns The return value of fn
366
+ *
367
+ * @internal
368
+ */
369
+ export function withReadObserver<T>(
370
+ onRead: (
371
+ proxy: ReactiveState<any>,
372
+ key: string,
373
+ registerEffect: (key: string, executeFn: () => void) => () => void
374
+ ) => void,
375
+ fn: () => T
376
+ ): T;
377
+
350
378
 
351
379
  // ============================================================================
352
380
  // Utility Types
package/src/index.js CHANGED
@@ -5,11 +5,12 @@
5
5
  * - state(): create reactive state
6
6
  * - bindDom(): zero-runtime DOM binding
7
7
  * - effect(): reactive effect with automatic dependency tracking
8
+ * - withReadObserver(): advanced API for custom reactive primitives
8
9
  *
9
10
  * Usage:
10
11
  * import { state, bindDom, effect } from "lume-js";
11
12
  */
12
13
 
13
- export { state } from "./core/state.js";
14
+ export { state, withReadObserver } from "./core/state.js";
14
15
  export { bindDom } from "./core/bindDom.js";
15
16
  export { effect } from "./core/effect.js";