lume-js 0.3.0 → 0.4.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.
@@ -1,53 +1,136 @@
1
1
  /**
2
- * computed - creates a derived value based on state
2
+ * Lume-JS Computed Addon
3
3
  *
4
- * NOTE: This is a basic implementation. For production use,
5
- * consider more robust solutions with automatic dependency tracking.
4
+ * Creates computed values that automatically update when dependencies change.
5
+ * Uses core effect() for automatic dependency tracking.
6
6
  *
7
- * @param {Function} fn - function that computes value from state
8
- * @returns {Object} - { value, recompute, subscribe }
7
+ * Usage:
8
+ * import { computed } from "lume-js/addons/computed";
9
+ *
10
+ * const doubled = computed(() => store.count * 2);
11
+ * console.log(doubled.value); // Auto-updates when store.count changes
12
+ *
13
+ * Features:
14
+ * - Automatic dependency tracking (no manual recompute)
15
+ * - Cached values (only recomputes when dependencies change)
16
+ * - Subscribe to changes
17
+ * - Cleanup with dispose()
18
+ *
19
+ * @module addons/computed
20
+ */
21
+
22
+ import { effect } from '../core/effect.js';
23
+
24
+ /**
25
+ * Creates a computed value with automatic dependency tracking
26
+ *
27
+ * The computation function runs immediately and tracks which state
28
+ * properties are accessed. When any dependency changes, the value
29
+ * is automatically recomputed.
30
+ *
31
+ * @param {function} fn - Function that computes the value
32
+ * @returns {object} Object with .value property and methods
9
33
  *
10
34
  * @example
11
- * const store = state({ count: 0 });
35
+ * const store = state({ count: 5 });
36
+ *
12
37
  * const doubled = computed(() => store.count * 2);
38
+ * console.log(doubled.value); // 10
39
+ *
40
+ * store.count = 10;
41
+ * // After microtask:
42
+ * console.log(doubled.value); // 20 (auto-updated)
13
43
  *
44
+ * @example
14
45
  * // Subscribe to changes
15
- * doubled.subscribe(val => console.log('Doubled:', val));
46
+ * const unsub = doubled.subscribe(value => {
47
+ * console.log('Doubled changed to:', value);
48
+ * });
16
49
  *
17
- * // Manually trigger recomputation after state changes
18
- * store.$subscribe('count', () => doubled.recompute());
50
+ * @example
51
+ * // Cleanup
52
+ * doubled.dispose();
19
53
  */
20
54
  export function computed(fn) {
21
- let value;
22
- let dirty = true;
23
- const subscribers = new Set();
55
+ if (typeof fn !== 'function') {
56
+ throw new Error('computed() requires a function');
57
+ }
24
58
 
25
- const recalc = () => {
59
+ let cachedValue;
60
+ let isInitialized = false;
61
+ const subscribers = [];
62
+
63
+ // Use effect to automatically track dependencies
64
+ const cleanupEffect = effect(() => {
26
65
  try {
27
- value = fn();
28
- } catch (err) {
29
- console.error("[computed] Error computing value:", err);
30
- value = undefined;
66
+ const newValue = fn();
67
+
68
+ // Check if value actually changed
69
+ if (!isInitialized || newValue !== cachedValue) {
70
+ cachedValue = newValue;
71
+ isInitialized = true;
72
+
73
+ // Notify all subscribers
74
+ subscribers.forEach(callback => callback(cachedValue));
75
+ }
76
+ } catch (error) {
77
+ console.error('[Lume.js computed] Error in computation:', error);
78
+ // Set to undefined on error, mark as initialized
79
+ if (!isInitialized || cachedValue !== undefined) {
80
+ cachedValue = undefined;
81
+ isInitialized = true;
82
+
83
+ // Notify subscribers of error state
84
+ subscribers.forEach(callback => callback(cachedValue));
85
+ }
31
86
  }
32
- dirty = false;
33
- subscribers.forEach(cb => cb(value));
34
- };
35
-
87
+ });
88
+
36
89
  return {
90
+ /**
91
+ * Get the current computed value
92
+ */
37
93
  get value() {
38
- if (dirty) recalc();
39
- return value;
94
+ if (!isInitialized) {
95
+ throw new Error('Computed value accessed before initialization');
96
+ }
97
+ return cachedValue;
40
98
  },
41
- recompute: () => {
42
- dirty = true;
43
- recalc();
44
- },
45
- subscribe: cb => {
46
- subscribers.add(cb);
47
- // Immediately notify subscriber with current value
48
- if (!dirty) cb(value);
49
- else recalc(); // Compute first time
50
- return () => subscribers.delete(cb); // unsubscribe function
99
+
100
+ /**
101
+ * Subscribe to changes in computed value
102
+ *
103
+ * @param {function} callback - Called when value changes
104
+ * @returns {function} Unsubscribe function
105
+ */
106
+ subscribe(callback) {
107
+ if (typeof callback !== 'function') {
108
+ throw new Error('subscribe() requires a function');
109
+ }
110
+
111
+ subscribers.push(callback);
112
+
113
+ // Call immediately with current value
114
+ if (isInitialized) {
115
+ callback(cachedValue);
116
+ }
117
+
118
+ // Return unsubscribe function
119
+ return () => {
120
+ const index = subscribers.indexOf(callback);
121
+ if (index > -1) {
122
+ subscribers.splice(index, 1);
123
+ }
124
+ };
51
125
  },
126
+
127
+ /**
128
+ * Clean up computed value and stop tracking
129
+ */
130
+ dispose() {
131
+ cleanupEffect();
132
+ subscribers.length = 0;
133
+ isInitialized = false;
134
+ }
52
135
  };
53
136
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Lume.js Addons TypeScript Definitions
3
+ *
4
+ * Optional utilities for advanced reactive patterns.
5
+ * Import from "lume-js/addons" for tree-shaking.
6
+ */
7
+
8
+ import type { Unsubscribe, Subscriber, ReactiveState } from '../index.js';
9
+
10
+ /**
11
+ * Computed value container returned by computed().
12
+ * T is the computed result type; when an error occurs the value becomes undefined.
13
+ */
14
+ export interface Computed<T> {
15
+ /** Current computed value (undefined if computation threw). */
16
+ readonly value: T | undefined;
17
+ /** Subscribe to changes; immediate invocation with current value (may be undefined). */
18
+ subscribe(callback: Subscriber<T | undefined>): Unsubscribe;
19
+ /** Dispose computed value and stop tracking dependencies. */
20
+ dispose(): void;
21
+ }
22
+
23
+ /**
24
+ * Create a computed value that automatically re-evaluates when accessed reactive state keys change.
25
+ * Uses effect() internally for dependency tracking.
26
+ *
27
+ * @param fn - Pure function that derives a value from reactive state.
28
+ * @returns Computed value container with .value, .subscribe(), and .dispose()
29
+ * @throws {Error} If fn is not a function
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * import { state } from 'lume-js';
34
+ * import { computed } from 'lume-js/addons';
35
+ *
36
+ * const store = state({ count: 5 });
37
+ * const doubled = computed(() => store.count * 2);
38
+ *
39
+ * console.log(doubled.value); // 10
40
+ *
41
+ * store.count = 10;
42
+ * // After microtask:
43
+ * console.log(doubled.value); // 20 (auto-updated)
44
+ *
45
+ * // Subscribe to changes
46
+ * const unsub = doubled.subscribe(value => {
47
+ * console.log('Doubled:', value);
48
+ * });
49
+ *
50
+ * // Cleanup
51
+ * doubled.dispose();
52
+ * unsub();
53
+ * ```
54
+ */
55
+ export function computed<T>(fn: () => T): Computed<T>;
56
+
57
+ /**
58
+ * Watch a single key on a reactive state object; convenience wrapper around $subscribe.
59
+ *
60
+ * @param store - Reactive state object created with state().
61
+ * @param key - Property key to observe.
62
+ * @param callback - Invoked immediately and on subsequent changes.
63
+ * @returns Unsubscribe function for cleanup
64
+ * @throws {Error} If store is not a reactive state object
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * import { state } from 'lume-js';
69
+ * import { watch } from 'lume-js/addons';
70
+ *
71
+ * const store = state({ count: 0 });
72
+ *
73
+ * const unwatch = watch(store, 'count', (value) => {
74
+ * console.log('Count is now:', value);
75
+ * });
76
+ *
77
+ * // Cleanup
78
+ * unwatch();
79
+ * ```
80
+ */
81
+ export function watch<T extends object, K extends keyof T>(
82
+ store: ReactiveState<T>,
83
+ key: K,
84
+ callback: Subscriber<T[K]>
85
+ ): Unsubscribe;
@@ -4,10 +4,19 @@
4
4
  *
5
5
  * Binds reactive state to DOM elements using [data-bind].
6
6
  * Supports two-way binding for INPUT/TEXTAREA/SELECT.
7
+ *
8
+ * Automatically waits for DOMContentLoaded if the document is still loading,
9
+ * ensuring safe binding regardless of when the function is called.
7
10
  *
8
11
  * Usage:
9
12
  * import { bindDom } from "lume-js";
13
+ *
14
+ * // Default: Auto-waits for DOM (safe anywhere)
10
15
  * const cleanup = bindDom(document.body, store);
16
+ *
17
+ * // Advanced: Force immediate binding (no auto-wait)
18
+ * const cleanup = bindDom(myElement, store, { immediate: true });
19
+ *
11
20
  * // Later: cleanup();
12
21
  *
13
22
  * HTML:
@@ -23,9 +32,11 @@ import { resolvePath } from "./utils.js";
23
32
  *
24
33
  * @param {HTMLElement} root - Root element to scan for [data-bind]
25
34
  * @param {object} store - Reactive state object
35
+ * @param {object} [options] - Optional configuration
36
+ * @param {boolean} [options.immediate=false] - Skip auto-wait, bind immediately
26
37
  * @returns {function} Cleanup function to remove all bindings
27
38
  */
28
- export function bindDom(root, store) {
39
+ export function bindDom(root, store, options = {}) {
29
40
  if (!(root instanceof HTMLElement)) {
30
41
  throw new Error('bindDom() requires a valid HTMLElement as root');
31
42
  }
@@ -34,52 +45,79 @@ export function bindDom(root, store) {
34
45
  throw new Error('bindDom() requires a reactive state object');
35
46
  }
36
47
 
37
- const nodes = root.querySelectorAll("[data-bind]");
38
- const unsubscribers = [];
48
+ const { immediate = false } = options;
39
49
 
40
- nodes.forEach(el => {
41
- const bindPath = el.getAttribute("data-bind");
42
-
43
- if (!bindPath) {
44
- console.warn('[Lume.js] Empty data-bind attribute found', el);
45
- return;
46
- }
50
+ // Core binding logic extracted to separate function
51
+ const performBinding = () => {
52
+ const nodes = root.querySelectorAll("[data-bind]");
53
+ const cleanups = [];
47
54
 
48
- const pathArr = bindPath.split(".");
49
- const lastKey = pathArr.pop();
55
+ nodes.forEach(el => {
56
+ const bindPath = el.getAttribute("data-bind");
57
+
58
+ if (!bindPath) {
59
+ console.warn('[Lume.js] Empty data-bind attribute found', el);
60
+ return;
61
+ }
50
62
 
51
- let target;
52
- try {
53
- target = resolvePath(store, pathArr);
54
- } catch (err) {
55
- console.warn(`[Lume.js] Invalid binding path "${bindPath}":`, err.message);
56
- return;
57
- }
63
+ const pathArr = bindPath.split(".");
64
+ const lastKey = pathArr.pop();
58
65
 
59
- if (!target || typeof target.$subscribe !== 'function') {
60
- console.warn(`[Lume.js] Target for "${bindPath}" is not a reactive state object`);
61
- return;
62
- }
63
-
64
- // Subscribe to changes
65
- const unsub = target.$subscribe(lastKey, val => {
66
- updateElement(el, val);
67
- });
66
+ let target;
67
+ try {
68
+ target = resolvePath(store, pathArr);
69
+ } catch (err) {
70
+ console.warn(`[Lume.js] Invalid binding path "${bindPath}":`, err.message);
71
+ return;
72
+ }
68
73
 
69
- unsubscribers.push(unsub);
74
+ if (!target || typeof target.$subscribe !== 'function') {
75
+ console.warn(`[Lume.js] Target for "${bindPath}" is not a reactive state object`);
76
+ return;
77
+ }
70
78
 
71
- // Two-way binding for form inputs
72
- if (isFormInput(el)) {
73
- el.addEventListener("input", e => {
74
- target[lastKey] = getInputValue(e.target);
79
+ // Subscribe to changes - receives already-batched notifications
80
+ const unsubscribe = target.$subscribe(lastKey, val => {
81
+ updateElement(el, val);
75
82
  });
76
- }
77
- });
83
+ cleanups.push(unsubscribe);
84
+
85
+ // Two-way binding for form inputs
86
+ if (isFormInput(el)) {
87
+ const handler = e => {
88
+ target[lastKey] = getInputValue(e.target);
89
+ };
90
+ el.addEventListener("input", handler);
91
+ cleanups.push(() => el.removeEventListener("input", handler));
92
+ }
93
+ });
78
94
 
79
- // Return cleanup function
80
- return () => {
81
- unsubscribers.forEach(unsub => unsub());
95
+ return () => {
96
+ cleanups.forEach(cleanup => cleanup());
97
+ };
82
98
  };
99
+
100
+ // Auto-wait for DOM if needed (unless immediate flag is set)
101
+ if (!immediate && document.readyState === 'loading') {
102
+ let cleanup = null;
103
+ const onReady = () => {
104
+ cleanup = performBinding();
105
+ };
106
+ document.addEventListener('DOMContentLoaded', onReady, { once: true });
107
+
108
+ // Return cleanup function that handles both cases
109
+ return () => {
110
+ if (cleanup) {
111
+ cleanup();
112
+ } else {
113
+ // If cleanup hasn't been created yet, remove the event listener
114
+ document.removeEventListener('DOMContentLoaded', onReady);
115
+ }
116
+ };
117
+ }
118
+
119
+ // Immediate binding (DOM already ready or immediate flag set)
120
+ return performBinding();
83
121
  }
84
122
 
85
123
  /**
@@ -89,8 +127,13 @@ export function bindDom(root, store) {
89
127
  function updateElement(el, val) {
90
128
  if (el.tagName === "INPUT") {
91
129
  if (el.type === "checkbox") {
130
+ // Single checkbox: bind to boolean value
131
+ // For multiple checkboxes, use nested state objects:
132
+ // <input data-bind="tags.javascript"> → state({ tags: state({ javascript: true }) })
92
133
  el.checked = Boolean(val);
93
134
  } else if (el.type === "radio") {
135
+ // Radio: checked when el.value matches state value
136
+ // String() handles null/undefined gracefully (no radio selected)
94
137
  el.checked = el.value === String(val);
95
138
  } else {
96
139
  el.value = val ?? '';
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Lume-JS Effect
3
+ *
4
+ * Automatic dependency tracking for reactive effects.
5
+ * Tracks which state properties are accessed during execution
6
+ * and automatically re-runs when those properties change.
7
+ *
8
+ * Part of core because it's fundamental to modern reactivity.
9
+ *
10
+ * Usage:
11
+ * import { effect } from "lume-js";
12
+ *
13
+ * effect(() => {
14
+ * console.log('Count is:', store.count);
15
+ * // Automatically re-runs when store.count changes
16
+ * });
17
+ *
18
+ * Features:
19
+ * - Automatic dependency collection
20
+ * - Dynamic dependencies (tracks what you actually access)
21
+ * - Returns cleanup function
22
+ * - Plays nicely with per-state batching (no global scheduler)
23
+ *
24
+ */
25
+
26
+ /**
27
+ * Creates an effect that automatically tracks dependencies
28
+ *
29
+ * The effect runs immediately and collects dependencies by tracking
30
+ * which state properties are accessed. When any dependency changes,
31
+ * the effect re-runs automatically.
32
+ *
33
+ * @param {function} fn - Function to run reactively
34
+ * @returns {function} Cleanup function to stop the effect
35
+ *
36
+ * @example
37
+ * const store = state({ count: 0, name: 'Alice' });
38
+ *
39
+ * const cleanup = effect(() => {
40
+ * // Only tracks 'count' (name not accessed)
41
+ * document.title = `Count: ${store.count}`;
42
+ * });
43
+ *
44
+ * store.count = 5; // Effect re-runs
45
+ * store.name = 'Bob'; // Effect does NOT re-run
46
+ *
47
+ * cleanup(); // Stop tracking
48
+ */
49
+ export function effect(fn) {
50
+ if (typeof fn !== 'function') {
51
+ throw new Error('effect() requires a function');
52
+ }
53
+
54
+ const cleanups = [];
55
+ let isRunning = false; // Prevent infinite recursion
56
+
57
+ /**
58
+ * Execute the effect function and collect dependencies
59
+ *
60
+ * The execution re-tracks accessed keys on every run. Subscriptions
61
+ * are cleaned up and re-established so the effect always reflects
62
+ * current dependencies.
63
+ */
64
+ const execute = () => {
65
+ if (isRunning) return; // Prevent re-entry
66
+
67
+ // Clean up previous subscriptions
68
+ cleanups.forEach(cleanup => cleanup());
69
+ cleanups.length = 0;
70
+
71
+ // Create effect context for tracking
72
+ const effectContext = {
73
+ fn,
74
+ cleanups,
75
+ execute, // Reference to this execute function
76
+ tracking: {} // Map of tracked keys
77
+ };
78
+
79
+ // Set as current effect (for state.js to detect)
80
+ globalThis.__LUME_CURRENT_EFFECT__ = effectContext;
81
+ isRunning = true;
82
+
83
+ try {
84
+ // Run the effect function (this triggers state getters)
85
+ fn();
86
+ } catch (error) {
87
+ console.error('[Lume.js effect] Error in effect:', error);
88
+ throw error;
89
+ } finally {
90
+ // Always clean up, even if error
91
+ globalThis.__LUME_CURRENT_EFFECT__ = undefined;
92
+ isRunning = false;
93
+ }
94
+ };
95
+
96
+ // Run immediately to collect initial dependencies
97
+ execute();
98
+
99
+ // Return cleanup function
100
+ return () => {
101
+ cleanups.forEach(cleanup => cleanup());
102
+ cleanups.length = 0;
103
+ };
104
+ }
package/src/core/state.js CHANGED
@@ -1,14 +1,17 @@
1
- // src/core/state.js
2
1
  /**
3
2
  * Lume-JS Reactive State Core
4
3
  *
5
4
  * Provides minimal reactive state with standard JavaScript.
5
+ * Features automatic microtask batching for performance.
6
+ * Supports automatic dependency tracking for effects.
6
7
  *
7
8
  * Features:
8
9
  * - Lightweight and Go-style
9
10
  * - Explicit nested states
10
11
  * - $subscribe for listening to key changes
11
12
  * - Cleanup with unsubscribe
13
+ * - Per-state microtask batching for writes
14
+ * - Effect dependency tracking support (deduped per state flush)
12
15
  *
13
16
  * Usage:
14
17
  * import { state } from "lume-js";
@@ -17,12 +20,18 @@
17
20
  * unsub(); // cleanup
18
21
  */
19
22
 
23
+ // Per-state batching – each state object maintains its own microtask flush.
24
+ // This keeps effects simple and aligned with Lume's minimal philosophy.
25
+
20
26
  /**
21
27
  * Creates a reactive state object.
22
28
  *
23
29
  * @param {Object} obj - Initial state object
24
30
  * @returns {Proxy} Reactive proxy with $subscribe method
25
31
  */
32
+ // Internal symbol used to mark reactive proxies (non-enumerable via Proxy trap)
33
+ const REACTIVE_MARKER = Symbol('__LUME_REACTIVE__');
34
+
26
35
  export function state(obj) {
27
36
  // Validate input
28
37
  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
@@ -30,26 +39,101 @@ export function state(obj) {
30
39
  }
31
40
 
32
41
  const listeners = {};
42
+ const pendingNotifications = new Map(); // Per-state pending changes
43
+ const pendingEffects = new Set(); // Dedupe effects per state
44
+ let flushScheduled = false;
33
45
 
34
- // Notify subscribers of a key
35
- function notify(key, val) {
36
- if (listeners[key]) {
37
- listeners[key].forEach(fn => fn(val));
38
- }
46
+ /**
47
+ * Schedule a single microtask flush for this state object.
48
+ *
49
+ * Flush order per state:
50
+ * 1) Notify subscribers for changed keys (key → subscribers)
51
+ * 2) Run each queued effect exactly once (Set-based dedupe)
52
+ *
53
+ * Notes:
54
+ * - Batching is per state; effects that depend on multiple states
55
+ * may run once per state that changed (by design).
56
+ */
57
+ function scheduleFlush() {
58
+ if (flushScheduled) return;
59
+
60
+ flushScheduled = true;
61
+ queueMicrotask(() => {
62
+ flushScheduled = false;
63
+
64
+ // Notify all subscribers of changed keys
65
+ // Snapshot listeners array to handle unsubscribes during iteration
66
+ for (const [key, value] of pendingNotifications) {
67
+ if (listeners[key]) {
68
+ const subscribersSnapshot = Array.from(listeners[key]);
69
+ subscribersSnapshot.forEach(fn => fn(value));
70
+ }
71
+ }
72
+
73
+ pendingNotifications.clear();
74
+
75
+ // Run each effect exactly once (Set deduplicates)
76
+ const effects = Array.from(pendingEffects);
77
+ pendingEffects.clear();
78
+ effects.forEach(effect => effect());
79
+ });
39
80
  }
40
81
 
41
82
  const proxy = new Proxy(obj, {
42
83
  get(target, key) {
84
+ // Reactive marker check (avoid tracking for internal symbol)
85
+ if (key === REACTIVE_MARKER) return true;
86
+ // Skip effect tracking for internal meta methods (e.g. $subscribe)
87
+ if (typeof key === 'string' && key.startsWith('$')) {
88
+ return target[key];
89
+ }
90
+ // Support effect tracking
91
+ // Check if we're inside an effect context
92
+ if (typeof globalThis.__LUME_CURRENT_EFFECT__ !== 'undefined') {
93
+ const currentEffect = globalThis.__LUME_CURRENT_EFFECT__;
94
+
95
+ if (currentEffect && !currentEffect.tracking[key]) {
96
+ // Mark as tracked
97
+ currentEffect.tracking[key] = true;
98
+
99
+ // Subscribe to changes for this key (skip initial call for effects)
100
+ const unsubscribe = (() => {
101
+ if (!listeners[key]) listeners[key] = [];
102
+
103
+ const effectFn = () => {
104
+ // Queue effect in this state's pending set
105
+ // Set deduplicates - effect runs once even if multiple keys change
106
+ pendingEffects.add(currentEffect.execute);
107
+ };
108
+
109
+ listeners[key].push(effectFn);
110
+
111
+ // Return unsubscribe function (no initial call for effects)
112
+ return () => {
113
+ if (listeners[key]) {
114
+ listeners[key] = listeners[key].filter(subscriber => subscriber !== effectFn);
115
+ }
116
+ };
117
+ })();
118
+
119
+ // Store cleanup function
120
+ currentEffect.cleanups.push(unsubscribe);
121
+ }
122
+ }
123
+
43
124
  return target[key];
44
125
  },
126
+
45
127
  set(target, key, value) {
46
128
  const oldValue = target[key];
129
+ if (oldValue === value) return true;
130
+
47
131
  target[key] = value;
48
132
 
49
- // Only notify if value actually changed
50
- if (oldValue !== value) {
51
- notify(key, value);
52
- }
133
+ // Batch notifications at the state level (per-state, not global)
134
+ pendingNotifications.set(key, value);
135
+ scheduleFlush();
136
+
53
137
  return true;
54
138
  }
55
139
  });
@@ -71,7 +155,7 @@ export function state(obj) {
71
155
  if (!listeners[key]) listeners[key] = [];
72
156
  listeners[key].push(fn);
73
157
 
74
- // Call immediately with current value
158
+ // Call immediately with current value (NOT batched)
75
159
  fn(proxy[key]);
76
160
 
77
161
  // Return unsubscribe function
@@ -83,4 +167,14 @@ export function state(obj) {
83
167
  };
84
168
 
85
169
  return proxy;
170
+ }
171
+
172
+ /**
173
+ * Determine if an object is a Lume reactive proxy.
174
+ * Defensive: ensures object and marker presence.
175
+ * @param {any} obj
176
+ * @returns {boolean}
177
+ */
178
+ export function isReactive(obj) {
179
+ return !!(obj && typeof obj === 'object' && obj[REACTIVE_MARKER]);
86
180
  }