lume-js 2.2.0 → 2.3.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.
- package/README.md +90 -19
- package/dist/addons.min.mjs +1 -1
- package/dist/addons.mjs +160 -6
- package/dist/addons.mjs.map +1 -1
- package/dist/handlers.min.mjs +1 -1
- package/dist/handlers.mjs +38 -2
- package/dist/handlers.mjs.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.mjs +4 -141
- package/dist/index.mjs.map +1 -1
- package/dist/lume.global.js +1 -1
- package/dist/lume.global.js.map +1 -1
- package/dist/shared-Bk_gndPJ.mjs +232 -0
- package/dist/shared-Bk_gndPJ.mjs.map +1 -0
- package/dist/shared-DNe4ez8V.mjs +249 -0
- package/dist/shared-DNe4ez8V.mjs.map +1 -0
- package/dist/shared-DmpHYKx7.mjs +15 -0
- package/dist/shared-DmpHYKx7.mjs.map +1 -0
- package/dist/state.min.mjs +1 -0
- package/dist/state.mjs +7 -0
- package/dist/state.mjs.map +1 -0
- package/package.json +5 -1
- package/src/addons/computed.js +5 -0
- package/src/addons/hydrateState.js +32 -4
- package/src/addons/index.d.ts +70 -2
- package/src/addons/index.js +13 -2
- package/src/addons/persist.js +152 -0
- package/src/addons/repeat.js +120 -5
- package/src/addons/withPlugins.js +7 -1
- package/src/core/batch.js +139 -0
- package/src/core/bindDom.js +12 -3
- package/src/core/effect.js +34 -5
- package/src/core/state.js +124 -68
- package/src/handlers/index.d.ts +24 -0
- package/src/handlers/index.js +1 -0
- package/src/handlers/on.js +60 -0
- package/src/handlers/stringAttr.js +14 -2
- package/src/index.d.ts +40 -0
- package/src/index.js +3 -1
- package/src/state.d.ts +17 -0
- package/src/state.js +25 -0
- package/dist/shared-Dcokqj5a.mjs +0 -249
- package/dist/shared-Dcokqj5a.mjs.map +0 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { a as logWarn, l as logError } from "./shared-DmpHYKx7.mjs";
|
|
2
|
+
const MAX_FLUSH_ITERATIONS = 100;
|
|
3
|
+
let batchDepth = 0;
|
|
4
|
+
const batchedStates = /* @__PURE__ */ new Set();
|
|
5
|
+
function enqueueIfBatching(handle) {
|
|
6
|
+
if (batchDepth === 0) return false;
|
|
7
|
+
batchedStates.add(handle);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
function flushBatchedStates() {
|
|
11
|
+
let iterations = 0;
|
|
12
|
+
while (batchedStates.size > 0 && iterations < MAX_FLUSH_ITERATIONS) {
|
|
13
|
+
iterations++;
|
|
14
|
+
const wave = Array.from(batchedStates);
|
|
15
|
+
batchedStates.clear();
|
|
16
|
+
const effects = /* @__PURE__ */ new Set();
|
|
17
|
+
for (const s of wave) {
|
|
18
|
+
s.runBeforeFlushHooks();
|
|
19
|
+
s.notifySubscribers();
|
|
20
|
+
for (const fx of s.takeEffects()) effects.add(fx);
|
|
21
|
+
}
|
|
22
|
+
for (const fx of effects) {
|
|
23
|
+
try {
|
|
24
|
+
fx();
|
|
25
|
+
} catch (err) {
|
|
26
|
+
logError("[Lume.js state] Error in effect:", err);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (iterations >= MAX_FLUSH_ITERATIONS) {
|
|
31
|
+
batchedStates.clear();
|
|
32
|
+
logError(
|
|
33
|
+
"[Lume.js state] Maximum batch flush iterations reached (100). This usually indicates an infinite loop caused by an effect or computed mutating state it depends on."
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function batch(fn) {
|
|
38
|
+
if (typeof fn !== "function") {
|
|
39
|
+
throw new Error("batch() requires a function");
|
|
40
|
+
}
|
|
41
|
+
if (batchDepth > 0) return fn();
|
|
42
|
+
batchDepth++;
|
|
43
|
+
let result;
|
|
44
|
+
try {
|
|
45
|
+
result = fn();
|
|
46
|
+
if (result && typeof result.then === "function") {
|
|
47
|
+
logWarn(
|
|
48
|
+
"[Lume.js batch] batch() received an async function. Only writes before the first await are batched; later writes flush via normal microtasks."
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
} finally {
|
|
53
|
+
try {
|
|
54
|
+
flushBatchedStates();
|
|
55
|
+
} finally {
|
|
56
|
+
batchDepth--;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const readers = /* @__PURE__ */ new Set();
|
|
61
|
+
const REACTIVE_BRAND = Symbol.for("lume.reactive");
|
|
62
|
+
function withReadObserver(onRead, fn) {
|
|
63
|
+
readers.add(onRead);
|
|
64
|
+
try {
|
|
65
|
+
return fn();
|
|
66
|
+
} finally {
|
|
67
|
+
readers.delete(onRead);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function state(obj) {
|
|
71
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
72
|
+
throw new Error("state() requires a plain object");
|
|
73
|
+
}
|
|
74
|
+
if (Object.isFrozen(obj) || Object.isSealed(obj)) {
|
|
75
|
+
throw new Error("state() requires a mutable plain object");
|
|
76
|
+
}
|
|
77
|
+
const listeners = /* @__PURE__ */ Object.create(null);
|
|
78
|
+
const pendingNotifications = /* @__PURE__ */ new Map();
|
|
79
|
+
const pendingEffects = /* @__PURE__ */ new Set();
|
|
80
|
+
const beforeFlushHooks = [];
|
|
81
|
+
let flushScheduled = false;
|
|
82
|
+
function runBeforeFlushHooks() {
|
|
83
|
+
for (let i = 0; i < beforeFlushHooks.length; i++) {
|
|
84
|
+
try {
|
|
85
|
+
beforeFlushHooks[i]();
|
|
86
|
+
} catch (err) {
|
|
87
|
+
logError("[Lume.js state] Error in beforeFlush hook:", err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function notifySubscribers() {
|
|
92
|
+
for (const [key, value] of pendingNotifications) {
|
|
93
|
+
if (listeners[key]) {
|
|
94
|
+
const subs = listeners[key];
|
|
95
|
+
let i = 0;
|
|
96
|
+
while (i < subs.length) {
|
|
97
|
+
const fn = subs[i];
|
|
98
|
+
try {
|
|
99
|
+
fn(value);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
logError(`[Lume.js state] Error notifying subscriber for key "${String(key)}":`, err);
|
|
102
|
+
}
|
|
103
|
+
if (subs[i] === fn) i++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
pendingNotifications.clear();
|
|
108
|
+
}
|
|
109
|
+
function takeEffects() {
|
|
110
|
+
const effects = Array.from(pendingEffects);
|
|
111
|
+
pendingEffects.clear();
|
|
112
|
+
return effects;
|
|
113
|
+
}
|
|
114
|
+
const batchHandle = { runBeforeFlushHooks, notifySubscribers, takeEffects };
|
|
115
|
+
function scheduleFlush() {
|
|
116
|
+
if (enqueueIfBatching(batchHandle)) return;
|
|
117
|
+
if (flushScheduled) return;
|
|
118
|
+
flushScheduled = true;
|
|
119
|
+
queueMicrotask(() => {
|
|
120
|
+
let iterations = 0;
|
|
121
|
+
try {
|
|
122
|
+
while ((pendingNotifications.size > 0 || pendingEffects.size > 0) && iterations < MAX_FLUSH_ITERATIONS) {
|
|
123
|
+
iterations++;
|
|
124
|
+
runBeforeFlushHooks();
|
|
125
|
+
notifySubscribers();
|
|
126
|
+
const effects = takeEffects();
|
|
127
|
+
for (let i = 0; i < effects.length; i++) {
|
|
128
|
+
try {
|
|
129
|
+
effects[i]();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
logError("[Lume.js state] Error in effect:", err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
flushScheduled = false;
|
|
137
|
+
}
|
|
138
|
+
if (iterations >= MAX_FLUSH_ITERATIONS) {
|
|
139
|
+
logError(
|
|
140
|
+
"[Lume.js state] Maximum flush iterations reached (100). This usually indicates an infinite loop caused by an effect or computed mutating state it depends on."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
Object.defineProperty(obj, REACTIVE_BRAND, { value: true });
|
|
146
|
+
const MAX_SUBSCRIBERS = 1e3;
|
|
147
|
+
const noopUnsubscribe = () => {
|
|
148
|
+
};
|
|
149
|
+
function addListener(key, fn, kind) {
|
|
150
|
+
if (!listeners[key]) listeners[key] = [];
|
|
151
|
+
if (listeners[key].length >= MAX_SUBSCRIBERS) {
|
|
152
|
+
logError(
|
|
153
|
+
`[Lume.js state] Subscriber limit (${MAX_SUBSCRIBERS}) reached for key "${String(key)}". ${kind} ignored — it will NOT receive updates. This usually means subscriptions are created in a loop without cleanup.`
|
|
154
|
+
);
|
|
155
|
+
return noopUnsubscribe;
|
|
156
|
+
}
|
|
157
|
+
listeners[key].push(fn);
|
|
158
|
+
return () => {
|
|
159
|
+
if (listeners[key]) {
|
|
160
|
+
const idx = listeners[key].indexOf(fn);
|
|
161
|
+
if (idx !== -1) {
|
|
162
|
+
listeners[key].splice(idx, 1);
|
|
163
|
+
if (listeners[key].length === 0) delete listeners[key];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const registerEffect = (key, executeFn) => {
|
|
169
|
+
const callback = () => {
|
|
170
|
+
pendingEffects.add(executeFn);
|
|
171
|
+
};
|
|
172
|
+
return addListener(key, callback, "Effect subscription");
|
|
173
|
+
};
|
|
174
|
+
const BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
175
|
+
const proxy = new Proxy(obj, {
|
|
176
|
+
get(target, key) {
|
|
177
|
+
if (typeof key === "string" && key.startsWith("$")) {
|
|
178
|
+
return target[key];
|
|
179
|
+
}
|
|
180
|
+
const value = target[key];
|
|
181
|
+
if (readers.size > 0) {
|
|
182
|
+
for (const reader of readers) {
|
|
183
|
+
reader(proxy, key, registerEffect);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
},
|
|
188
|
+
set(target, key, value) {
|
|
189
|
+
if (typeof key === "string" && BLOCKED_KEYS.has(key)) {
|
|
190
|
+
logWarn(`[Lume.js state] Blocked write to reserved key "${key}"`);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
const oldValue = target[key];
|
|
194
|
+
if (Object.is(oldValue, value)) return true;
|
|
195
|
+
target[key] = value;
|
|
196
|
+
pendingNotifications.set(key, value);
|
|
197
|
+
scheduleFlush();
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
obj.$beforeFlush = (fn) => {
|
|
202
|
+
if (typeof fn !== "function") {
|
|
203
|
+
throw new Error("$beforeFlush requires a function");
|
|
204
|
+
}
|
|
205
|
+
if (beforeFlushHooks.indexOf(fn) === -1) {
|
|
206
|
+
beforeFlushHooks.push(fn);
|
|
207
|
+
}
|
|
208
|
+
return () => {
|
|
209
|
+
const idx = beforeFlushHooks.indexOf(fn);
|
|
210
|
+
if (idx !== -1) {
|
|
211
|
+
beforeFlushHooks.splice(idx, 1);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
obj.$subscribe = (key, fn) => {
|
|
216
|
+
if (typeof fn !== "function") {
|
|
217
|
+
throw new Error("Subscriber must be a function");
|
|
218
|
+
}
|
|
219
|
+
const unsubscribe = addListener(key, fn, "New subscriber");
|
|
220
|
+
if (unsubscribe === noopUnsubscribe) return unsubscribe;
|
|
221
|
+
fn(proxy[key]);
|
|
222
|
+
return unsubscribe;
|
|
223
|
+
};
|
|
224
|
+
return proxy;
|
|
225
|
+
}
|
|
226
|
+
export {
|
|
227
|
+
REACTIVE_BRAND as R,
|
|
228
|
+
batch as b,
|
|
229
|
+
state as s,
|
|
230
|
+
withReadObserver as w
|
|
231
|
+
};
|
|
232
|
+
//# sourceMappingURL=shared-Bk_gndPJ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { w as withReadObserver } from "./shared-Bk_gndPJ.mjs";
|
|
2
|
+
import { a as logWarn, l as logError } from "./shared-DmpHYKx7.mjs";
|
|
3
|
+
const boolHandler = (name) => ({
|
|
4
|
+
attr: `data-${name}`,
|
|
5
|
+
apply(el, val) {
|
|
6
|
+
el[name] = Boolean(val);
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
const ariaHandler = (name) => ({
|
|
10
|
+
attr: `data-${name}`,
|
|
11
|
+
apply(el, val) {
|
|
12
|
+
el.setAttribute(name, val ? "true" : "false");
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
const DEFAULT_HANDLERS = [
|
|
16
|
+
boolHandler("hidden"),
|
|
17
|
+
boolHandler("disabled"),
|
|
18
|
+
boolHandler("checked"),
|
|
19
|
+
boolHandler("required"),
|
|
20
|
+
ariaHandler("aria-expanded"),
|
|
21
|
+
ariaHandler("aria-hidden")
|
|
22
|
+
];
|
|
23
|
+
function mergeHandlers(defaults, userHandlers) {
|
|
24
|
+
if (!userHandlers.length) return defaults;
|
|
25
|
+
const merged = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const h of defaults) merged.set(h.attr, h);
|
|
27
|
+
for (const h of userHandlers.flat()) merged.set(h.attr, h);
|
|
28
|
+
return [...merged.values()];
|
|
29
|
+
}
|
|
30
|
+
function bindDom(root, store, options = {}) {
|
|
31
|
+
if (!(root instanceof HTMLElement)) {
|
|
32
|
+
throw new Error("bindDom() requires a valid HTMLElement as root");
|
|
33
|
+
}
|
|
34
|
+
if (!store || typeof store !== "object") {
|
|
35
|
+
throw new Error("bindDom() requires a reactive state object");
|
|
36
|
+
}
|
|
37
|
+
const { immediate = false, handlers: userHandlers = [] } = options;
|
|
38
|
+
const handlers = mergeHandlers(DEFAULT_HANDLERS, userHandlers);
|
|
39
|
+
const performBinding = () => {
|
|
40
|
+
const cleanups = [];
|
|
41
|
+
const bindingMap = /* @__PURE__ */ new WeakMap();
|
|
42
|
+
const selector = ["[data-bind]", ...handlers.map((h) => `[${h.attr}]`)].join(",");
|
|
43
|
+
const elements = root.querySelectorAll(selector);
|
|
44
|
+
for (const el of elements) {
|
|
45
|
+
if (el.hasAttribute("data-bind")) {
|
|
46
|
+
const c = handleDataBind(el, store, el.getAttribute("data-bind"), bindingMap);
|
|
47
|
+
if (c) cleanups.push(c);
|
|
48
|
+
}
|
|
49
|
+
for (const handler of handlers) {
|
|
50
|
+
if (el.hasAttribute(handler.attr)) {
|
|
51
|
+
const c = applyHandler(el, store, el.getAttribute(handler.attr), handler);
|
|
52
|
+
if (c) cleanups.push(c);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const inputHandler = (e) => {
|
|
57
|
+
const binding = bindingMap.get(e.target);
|
|
58
|
+
if (binding) binding.target[binding.key] = getInputValue(e.target);
|
|
59
|
+
};
|
|
60
|
+
root.addEventListener("input", inputHandler);
|
|
61
|
+
cleanups.push(() => root.removeEventListener("input", inputHandler));
|
|
62
|
+
return () => cleanups.forEach((c) => c());
|
|
63
|
+
};
|
|
64
|
+
if (!immediate && document.readyState === "loading") {
|
|
65
|
+
let cleanup = null;
|
|
66
|
+
const onReady = () => {
|
|
67
|
+
cleanup = performBinding();
|
|
68
|
+
};
|
|
69
|
+
document.addEventListener("DOMContentLoaded", onReady, { once: true });
|
|
70
|
+
return () => cleanup ? cleanup() : document.removeEventListener("DOMContentLoaded", onReady);
|
|
71
|
+
}
|
|
72
|
+
return performBinding();
|
|
73
|
+
}
|
|
74
|
+
function applyHandler(el, store, path, handler) {
|
|
75
|
+
const result = resolveProp(store, path);
|
|
76
|
+
if (!result) return null;
|
|
77
|
+
const { target, key } = result;
|
|
78
|
+
return target.$subscribe(key, (val) => handler.apply(el, val));
|
|
79
|
+
}
|
|
80
|
+
function handleDataBind(el, store, path, bindingMap) {
|
|
81
|
+
const result = resolveProp(store, path);
|
|
82
|
+
if (!result) return null;
|
|
83
|
+
const { target, key } = result;
|
|
84
|
+
const unsub = target.$subscribe(key, (val) => applyBindValue(el, val));
|
|
85
|
+
if (isFormInput(el)) {
|
|
86
|
+
bindingMap.set(el, { target, key });
|
|
87
|
+
}
|
|
88
|
+
return unsub;
|
|
89
|
+
}
|
|
90
|
+
function resolvePath(obj, pathArr) {
|
|
91
|
+
if (!pathArr || pathArr.length === 0) {
|
|
92
|
+
return obj;
|
|
93
|
+
}
|
|
94
|
+
let current = obj;
|
|
95
|
+
for (let i = 0; i < pathArr.length; i++) {
|
|
96
|
+
const key = pathArr[i];
|
|
97
|
+
if (current === null || current === void 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (!(key in current)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
current = current[key];
|
|
104
|
+
}
|
|
105
|
+
return current;
|
|
106
|
+
}
|
|
107
|
+
function resolveProp(store, path) {
|
|
108
|
+
if (!path) return null;
|
|
109
|
+
const pathArr = path.split(".");
|
|
110
|
+
const key = pathArr.pop();
|
|
111
|
+
const target = resolvePath(store, pathArr);
|
|
112
|
+
if (target === null || target === void 0) {
|
|
113
|
+
logWarn(`[Lume.js] Invalid path "${path}"`);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (!target?.$subscribe) {
|
|
117
|
+
logWarn(`[Lume.js] Target for "${path}" is not reactive`);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return { target, key };
|
|
121
|
+
}
|
|
122
|
+
function applyBindValue(el, val) {
|
|
123
|
+
if (el.tagName === "INPUT") {
|
|
124
|
+
if (el.type === "checkbox") el.checked = Boolean(val);
|
|
125
|
+
else if (el.type === "radio") el.checked = el.value === String(val);
|
|
126
|
+
else el.value = val ?? "";
|
|
127
|
+
} else if (el.tagName === "TEXTAREA" || el.tagName === "SELECT") {
|
|
128
|
+
el.value = val ?? "";
|
|
129
|
+
} else {
|
|
130
|
+
el.textContent = val ?? "";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function getInputValue(el) {
|
|
134
|
+
if (el.type === "checkbox") return el.checked;
|
|
135
|
+
if (el.type === "number" || el.type === "range") return el.valueAsNumber;
|
|
136
|
+
return el.value;
|
|
137
|
+
}
|
|
138
|
+
function isFormInput(el) {
|
|
139
|
+
return el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT";
|
|
140
|
+
}
|
|
141
|
+
let currentEffect = null;
|
|
142
|
+
function effect(fn, deps) {
|
|
143
|
+
if (typeof fn !== "function") {
|
|
144
|
+
throw new Error("effect() requires a function");
|
|
145
|
+
}
|
|
146
|
+
const cleanups = [];
|
|
147
|
+
let isRunning = false;
|
|
148
|
+
const execute = () => {
|
|
149
|
+
if (isRunning) return;
|
|
150
|
+
isRunning = true;
|
|
151
|
+
try {
|
|
152
|
+
fn();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logError("[Lume.js effect] Error in effect:", error);
|
|
155
|
+
throw error;
|
|
156
|
+
} finally {
|
|
157
|
+
isRunning = false;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
if (Array.isArray(deps)) {
|
|
161
|
+
let scheduled = false;
|
|
162
|
+
let disposed = false;
|
|
163
|
+
cleanups.push(() => {
|
|
164
|
+
disposed = true;
|
|
165
|
+
});
|
|
166
|
+
const scheduleExecute = () => {
|
|
167
|
+
if (scheduled) return;
|
|
168
|
+
scheduled = true;
|
|
169
|
+
queueMicrotask(() => {
|
|
170
|
+
scheduled = false;
|
|
171
|
+
if (disposed) return;
|
|
172
|
+
try {
|
|
173
|
+
execute();
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
for (const dep of deps) {
|
|
179
|
+
if (Array.isArray(dep) && dep.length >= 2) {
|
|
180
|
+
const [store, ...keys] = dep;
|
|
181
|
+
if (store && typeof store.$subscribe === "function") {
|
|
182
|
+
for (const key of keys) {
|
|
183
|
+
let isFirst = true;
|
|
184
|
+
const unsub = store.$subscribe(key, () => {
|
|
185
|
+
if (isFirst) {
|
|
186
|
+
isFirst = false;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
scheduleExecute();
|
|
190
|
+
});
|
|
191
|
+
cleanups.push(unsub);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
execute();
|
|
197
|
+
} else {
|
|
198
|
+
const executeWithTracking = () => {
|
|
199
|
+
if (isRunning) return;
|
|
200
|
+
const oldCleanups = cleanups.splice(0);
|
|
201
|
+
const myContext = {
|
|
202
|
+
fn,
|
|
203
|
+
cleanups,
|
|
204
|
+
execute: executeWithTracking,
|
|
205
|
+
tracking: /* @__PURE__ */ new WeakMap()
|
|
206
|
+
};
|
|
207
|
+
const previousEffect = currentEffect;
|
|
208
|
+
currentEffect = myContext;
|
|
209
|
+
isRunning = true;
|
|
210
|
+
try {
|
|
211
|
+
const onRead = (proxy, key, registerEffect) => {
|
|
212
|
+
if (currentEffect !== myContext) return;
|
|
213
|
+
let keys = myContext.tracking.get(proxy);
|
|
214
|
+
if (!keys) {
|
|
215
|
+
keys = /* @__PURE__ */ new Set();
|
|
216
|
+
myContext.tracking.set(proxy, keys);
|
|
217
|
+
}
|
|
218
|
+
if (keys.has(key)) return;
|
|
219
|
+
keys.add(key);
|
|
220
|
+
myContext.cleanups.push(registerEffect(key, myContext.execute));
|
|
221
|
+
};
|
|
222
|
+
withReadObserver(onRead, fn);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
cleanups.length = 0;
|
|
225
|
+
cleanups.push(...oldCleanups);
|
|
226
|
+
logError("[Lume.js effect] Error in effect:", error);
|
|
227
|
+
throw error;
|
|
228
|
+
} finally {
|
|
229
|
+
currentEffect = previousEffect;
|
|
230
|
+
isRunning = false;
|
|
231
|
+
}
|
|
232
|
+
if (cleanups.length > 0) {
|
|
233
|
+
for (const cleanup of oldCleanups) cleanup();
|
|
234
|
+
} else {
|
|
235
|
+
cleanups.push(...oldCleanups);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
executeWithTracking();
|
|
239
|
+
}
|
|
240
|
+
return () => {
|
|
241
|
+
while (cleanups.length) cleanups.pop()();
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
export {
|
|
245
|
+
applyBindValue as a,
|
|
246
|
+
bindDom as b,
|
|
247
|
+
effect as e
|
|
248
|
+
};
|
|
249
|
+
//# sourceMappingURL=shared-DNe4ez8V.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-DNe4ez8V.mjs","sources":["../src/core/bindDom.js","../src/core/effect.js"],"sourcesContent":["// src/core/bindDom.js\n/**\n * Lume-JS DOM Binding\n *\n * Binds reactive state to DOM elements using data-* attributes.\n *\n * Built-in attributes (always available):\n * data-bind=\"key\" → Two-way binding for inputs, textContent for others\n * data-hidden=\"key\" → Toggles hidden (truthy = hidden)\n * data-disabled=\"key\" → Toggles disabled (truthy = disabled)\n * data-checked=\"key\" → Toggles checked (for checkboxes/radios)\n * data-required=\"key\" → Toggles required (truthy = required)\n * data-aria-expanded=\"key\" → Sets aria-expanded to \"true\"/\"false\"\n * data-aria-hidden=\"key\" → Sets aria-hidden to \"true\"/\"false\"\n *\n * Extensible via handlers option:\n * import { show, classToggle } from 'lume-js/handlers';\n * bindDom(root, store, { handlers: [show, classToggle('active')] });\n *\n * Custom handlers:\n * const tooltip = { attr: 'data-tooltip', apply(el, val) { el.title = val ?? ''; } };\n * bindDom(root, store, { handlers: [tooltip] });\n *\n * Usage:\n * import { bindDom } from \"lume-js\";\n * const cleanup = bindDom(document.body, store);\n *\n * @security `data-bind` attribute values are resolved once at bind time and\n * trusted as state path expressions. If an attacker can inject `data-bind`\n * attributes into the DOM, they can subscribe to any reachable reactive state.\n * Ensure your HTML is trusted or sanitize it before calling bindDom().\n */\n\nimport { logWarn } from '../utils/log.js';\n\n// --- Default Handlers (always active, backwards compatible) ---\n\nconst boolHandler = (name) => ({\n attr: `data-${name}`,\n apply(el, val) { el[name] = Boolean(val); }\n});\n\nconst ariaHandler = (name) => ({\n attr: `data-${name}`,\n apply(el, val) { el.setAttribute(name, val ? 'true' : 'false'); }\n});\n\nconst DEFAULT_HANDLERS = [\n boolHandler('hidden'),\n boolHandler('disabled'),\n boolHandler('checked'),\n boolHandler('required'),\n ariaHandler('aria-expanded'),\n ariaHandler('aria-hidden'),\n];\n\n/**\n * Merge default and user handlers.\n * User handlers override defaults with same attr (Map deduplicates).\n * User handler arrays are flattened one level (supports classToggle()).\n */\nfunction mergeHandlers(defaults, userHandlers) {\n if (!userHandlers.length) return defaults;\n const merged = new Map();\n for (const h of defaults) merged.set(h.attr, h);\n for (const h of userHandlers.flat()) merged.set(h.attr, h);\n return [...merged.values()];\n}\n\n/**\n * DOM binding for reactive state\n */\nexport function bindDom(root, store, options = {}) {\n if (!(root instanceof HTMLElement)) {\n throw new Error('bindDom() requires a valid HTMLElement as root');\n }\n if (!store || typeof store !== 'object') {\n throw new Error('bindDom() requires a reactive state object');\n }\n\n const { immediate = false, handlers: userHandlers = [] } = options;\n const handlers = mergeHandlers(DEFAULT_HANDLERS, userHandlers);\n\n const performBinding = () => {\n const cleanups = [];\n const bindingMap = new WeakMap();\n\n // Build compiled selector: data-bind (always) + all handler attrs\n const selector = ['[data-bind]', ...handlers.map(h => `[${h.attr}]`)].join(',');\n const elements = root.querySelectorAll(selector);\n\n for (const el of elements) {\n // data-bind (two-way) — always in core, special handling\n if (el.hasAttribute('data-bind')) {\n const c = handleDataBind(el, store, el.getAttribute('data-bind'), bindingMap);\n if (c) cleanups.push(c);\n }\n\n // All registered handlers (default + user)\n for (const handler of handlers) {\n if (el.hasAttribute(handler.attr)) {\n const c = applyHandler(el, store, el.getAttribute(handler.attr), handler);\n if (c) cleanups.push(c);\n }\n }\n }\n\n // Event delegation for two-way bindings\n const inputHandler = e => {\n const binding = bindingMap.get(e.target);\n if (binding) binding.target[binding.key] = getInputValue(e.target);\n };\n root.addEventListener(\"input\", inputHandler);\n cleanups.push(() => root.removeEventListener(\"input\", inputHandler));\n\n return () => cleanups.forEach(c => c());\n };\n\n // Auto-wait for DOM if needed\n if (!immediate && document.readyState === 'loading') {\n let cleanup = null;\n const onReady = () => { cleanup = performBinding(); };\n document.addEventListener('DOMContentLoaded', onReady, { once: true });\n return () => cleanup ? cleanup() : document.removeEventListener('DOMContentLoaded', onReady);\n }\n\n return performBinding();\n}\n\n/**\n * Apply a handler to an element via subscription.\n * Resolves the state path and subscribes to changes.\n */\nfunction applyHandler(el, store, path, handler) {\n const result = resolveProp(store, path);\n if (!result) return null;\n const { target, key } = result;\n return target.$subscribe(key, val => handler.apply(el, val));\n}\n\n/**\n * Handle data-bind (two-way for inputs, textContent for others)\n */\nfunction handleDataBind(el, store, path, bindingMap) {\n const result = resolveProp(store, path);\n if (!result) return null;\n\n const { target, key } = result;\n const unsub = target.$subscribe(key, val => applyBindValue(el, val));\n\n if (isFormInput(el)) {\n bindingMap.set(el, { target, key });\n }\n\n return unsub;\n}\n\n/**\n * Resolve a nested path in an object.\n * Example: resolvePath(obj, ['user', 'address']) returns obj.user.address\n */\nfunction resolvePath(obj, pathArr) {\n if (!pathArr || pathArr.length === 0) {\n return obj;\n }\n let current = obj;\n for (let i = 0; i < pathArr.length; i++) {\n const key = pathArr[i];\n if (current === null || current === undefined) {\n return null;\n }\n if (!(key in current)) {\n return null;\n }\n current = current[key];\n }\n return current;\n}\n\n/**\n * Resolve path to target and key.\n *\n * ⚠️ Path bindings are resolved once at bind time. If an intermediate\n * object in the path is null/undefined at bindDom call time, the binding\n * is permanently dead and will not self-heal when the path later becomes valid.\n */\nfunction resolveProp(store, path) {\n if (!path) return null;\n\n const pathArr = path.split(\".\");\n const key = pathArr.pop();\n const target = resolvePath(store, pathArr);\n\n if (target === null || target === undefined) {\n logWarn(`[Lume.js] Invalid path \"${path}\"`);\n return null;\n }\n\n if (!target?.$subscribe) {\n logWarn(`[Lume.js] Target for \"${path}\" is not reactive`);\n return null;\n }\n\n return { target, key };\n}\n\n/**\n * Update element with value (for data-bind).\n *\n * Exported for internal reuse (e.g. the repeat addon's template bindings)\n * so addon and core data-bind semantics never drift. Not part of the\n * public package API.\n */\nexport function applyBindValue(el, val) {\n if (el.tagName === \"INPUT\") {\n if (el.type === \"checkbox\") el.checked = Boolean(val);\n else if (el.type === \"radio\") el.checked = el.value === String(val);\n else el.value = val ?? '';\n } else if (el.tagName === \"TEXTAREA\" || el.tagName === \"SELECT\") {\n el.value = val ?? '';\n } else {\n el.textContent = val ?? '';\n }\n}\n\n/**\n * Get value from input\n */\nfunction getInputValue(el) {\n if (el.type === \"checkbox\") return el.checked;\n if (el.type === \"number\" || el.type === \"range\") return el.valueAsNumber;\n return el.value;\n}\n\n/**\n * Check if element is form input\n */\nfunction isFormInput(el) {\n return el.tagName === \"INPUT\" || el.tagName === \"TEXTAREA\" || el.tagName === \"SELECT\";\n}","import { withReadObserver } from './state.js';\nimport { logError } from '../utils/log.js';\n\n/**\n * Lume-JS Effect\n *\n * Reactive effects with two modes:\n * 1. Auto-tracking (default): Tracks dependencies automatically via withReadObserver\n * 2. Explicit deps: You specify exactly what triggers re-runs\n *\n * Auto-tracking uses scope-based read observation — state.js has zero permanent\n * dependency on this module. Read tracking is only active during the synchronous\n * execution of an effect's body.\n *\n * Usage:\n * import { effect } from \"lume-js\";\n *\n * // Auto-tracking mode (existing behavior)\n * effect(() => {\n * console.log('Count is:', store.count);\n * // Automatically re-runs when store.count changes\n * });\n *\n * // Explicit deps mode (new - no magic)\n * effect(() => {\n * console.log('Count is:', store.count);\n * }, [[store, 'count']]); // Only re-runs when store.count changes\n *\n * Features:\n * - Automatic dependency collection via withReadObserver scope (default)\n * - Explicit dependencies for side-effects\n * - Explicit-deps notifications are coalesced: one run per microtask,\n * no matter how many tracked keys (or stores) changed in the same tick\n * - Returns cleanup function\n * - Compatible with per-state batching\n */\n\n// Module-scoped effect context (prevents third-party spoofing via globalThis)\nlet currentEffect = null;\n\n// withReadObserver is used below to scope read tracking to synchronous effect execution.\n\n/**\n * Creates an effect that runs reactively\n *\n * @param {function} fn - Function to run reactively\n * @param {Array<[object, string]>} [deps] - Optional explicit dependencies as [store, key] tuples\n * @returns {function} Cleanup function to stop the effect\n *\n * @example\n * // Auto-tracking (default)\n * const store = state({ count: 0 });\n * effect(() => {\n * document.title = `Count: ${store.count}`;\n * });\n * \n * @example\n * // Explicit deps (no magic)\n * effect(() => {\n * analytics.log(store.count); // Won't track store.count automatically\n * }, [[store, 'count']]); // Explicit: only re-run on store.count\n */\n// eslint-disable-next-line sonarjs/cognitive-complexity -- handles both auto-tracking and explicit-deps modes with cleanup; splitting would require exporting internal state\nexport function effect(fn, deps) {\n if (typeof fn !== 'function') {\n throw new Error('effect() requires a function');\n }\n\n const cleanups = [];\n let isRunning = false;\n\n /**\n * Execute the effect function\n */\n const execute = () => {\n /* v8 ignore next -- re-entry guard: unreachable because $subscribe fires via microtask after isRunning resets in finally */\n if (isRunning) return;\n isRunning = true;\n\n try {\n fn();\n } catch (error) {\n logError('[Lume.js effect] Error in effect:', error);\n throw error;\n } finally {\n isRunning = false;\n }\n };\n\n // EXPLICIT DEPS MODE: deps array provided\n if (Array.isArray(deps)) {\n // Coalesce notifications: when several tracked keys change in the same\n // flush (or several stores flush in the same tick), run the effect once\n // per microtask instead of once per changed key. This matches the\n // dedupe guarantee auto-tracking mode gets from the per-state effect queue.\n let scheduled = false;\n let disposed = false;\n cleanups.push(() => { disposed = true; });\n\n const scheduleExecute = () => {\n if (scheduled) return;\n scheduled = true;\n queueMicrotask(() => {\n scheduled = false;\n if (disposed) return;\n // execute() logs and re-throws; swallow here so a throwing effect\n // doesn't become an uncaught error inside the microtask (same\n // containment the state flush loop provides for subscribers).\n try { execute(); } catch { /* already logged by execute() */ }\n });\n };\n\n // Subscribe to each [store, key1, key2, ...] tuple explicitly\n for (const dep of deps) {\n if (Array.isArray(dep) && dep.length >= 2) {\n const [store, ...keys] = dep;\n if (store && typeof store.$subscribe === 'function') {\n // Subscribe to each key in this tuple\n for (const key of keys) {\n // $subscribe calls immediately, then on changes\n // We want: call execute immediately once, then on changes\n let isFirst = true;\n const unsub = store.$subscribe(key, () => {\n if (isFirst) {\n isFirst = false;\n return; // Skip first call, we'll run execute() below\n }\n scheduleExecute();\n });\n cleanups.push(unsub);\n }\n }\n }\n }\n // Run immediately\n execute();\n }\n // AUTO-TRACKING MODE: no deps (existing behavior)\n else {\n const executeWithTracking = () => {\n /* v8 ignore next -- defensive guard: synchronous re-entry is unreachable through the public API */\n if (isRunning) return;\n\n // Save previous subscriptions instead of cleaning immediately.\n // If fn() doesn't read any state (early return / error), we restore\n // them so the effect stays reactive.\n const oldCleanups = cleanups.splice(0);\n\n // Tracking is keyed per store proxy (WeakMap<proxy, Set<key>>) so the\n // same key name on two different stores creates two subscriptions.\n const myContext = {\n fn,\n cleanups,\n execute: executeWithTracking,\n tracking: new WeakMap()\n };\n\n // Set as current effect (for state.js to detect)\n // Save previous context to support nested effects/computed\n const previousEffect = currentEffect;\n currentEffect = myContext;\n isRunning = true;\n\n try {\n const onRead = (proxy, key, registerEffect) => {\n // Only the currently active effect (not a nested one) creates subscriptions\n if (currentEffect !== myContext) return;\n let keys = myContext.tracking.get(proxy);\n if (!keys) {\n keys = new Set();\n myContext.tracking.set(proxy, keys);\n }\n if (keys.has(key)) return;\n keys.add(key);\n myContext.cleanups.push(registerEffect(key, myContext.execute));\n };\n withReadObserver(onRead, fn);\n } catch (error) {\n // On error, restore old subscriptions so the effect stays reactive\n cleanups.length = 0;\n cleanups.push(...oldCleanups);\n logError('[Lume.js effect] Error in effect:', error);\n throw error;\n } finally {\n // Restore previous context (not undefined) to support nesting\n currentEffect = previousEffect;\n isRunning = false;\n }\n\n // If fn() created new subscriptions, clean old ones.\n // If it didn't (e.g., early return), keep old subscriptions intact.\n if (cleanups.length > 0) {\n for (const cleanup of oldCleanups) cleanup();\n } else {\n cleanups.push(...oldCleanups);\n }\n };\n\n // Run immediately to collect initial dependencies\n executeWithTracking();\n }\n\n // Return cleanup function\n return () => {\n // while/pop is faster than forEach\n while (cleanups.length) cleanups.pop()();\n };\n}"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function logWarn(msg, ...rest) {
|
|
2
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
3
|
+
console.warn(msg, ...rest);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
function logError(msg, ...rest) {
|
|
7
|
+
if (typeof console !== "undefined" && typeof console.error === "function") {
|
|
8
|
+
console.error(msg, ...rest);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
logWarn as a,
|
|
13
|
+
logError as l
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=shared-DmpHYKx7.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-DmpHYKx7.mjs","sources":["../src/utils/log.js"],"sourcesContent":["/**\n * Environment-safe logging utilities for constrained runtimes\n * (e.g. service workers, embedded engines, SSR environments).\n *\n * All core and addon files should import these instead of\n * calling console.* directly to avoid ReferenceError when\n * console is not defined.\n */\n\nexport function logWarn(msg, ...rest) {\n if (typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn(msg, ...rest);\n }\n}\n\nexport function logError(msg, ...rest) {\n if (typeof console !== 'undefined' && typeof console.error === 'function') {\n console.error(msg, ...rest);\n }\n}\n"],"names":[],"mappings":"AASO,SAAS,QAAQ,QAAQ,MAAM;AACpC,MAAI,OAAO,YAAY,eAAe,OAAO,QAAQ,SAAS,YAAY;AACxE,YAAQ,KAAK,KAAK,GAAG,IAAI;AAAA,EAC3B;AACF;AAEO,SAAS,SAAS,QAAQ,MAAM;AACrC,MAAI,OAAO,YAAY,eAAe,OAAO,QAAQ,UAAU,YAAY;AACzE,YAAQ,MAAM,KAAK,GAAG,IAAI;AAAA,EAC5B;AACF;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e,...t){void 0!==console&&"function"==typeof console.warn&&console.warn(e,...t)}function t(e,...t){void 0!==console&&"function"==typeof console.error&&console.error(e,...t)}const r=100;let o=0;const n=new Set;function s(e){return 0!==o&&(n.add(e),!0)}function i(){let e=0;for(;n.size>0&&r>e;){e++;const r=Array.from(n);n.clear();const o=new Set;for(const e of r){e.runBeforeFlushHooks(),e.notifySubscribers();for(const t of e.takeEffects())o.add(t)}for(const e of o)try{e()}catch(e){t("[Lume.js state] Error in effect:",e)}}r>e||(n.clear(),t("[Lume.js state] Maximum batch flush iterations reached (100). This usually indicates an infinite loop caused by an effect or computed mutating state it depends on."))}function c(t){if("function"!=typeof t)throw Error("batch() requires a function");if(o>0)return t();let r;o++;try{return r=t(),r&&"function"==typeof r.then&&e("[Lume.js batch] batch() received an async function. Only writes before the first await are batched; later writes flush via normal microtasks."),r}finally{try{i()}finally{o--}}}const f=new Set,u=Symbol.for("lume.reactive");function a(e,t){f.add(e);try{return t()}finally{f.delete(e)}}function l(o){if(!o||"object"!=typeof o||Array.isArray(o))throw Error("state() requires a plain object");if(Object.isFrozen(o)||Object.isSealed(o))throw Error("state() requires a mutable plain object");const n=Object.create(null),i=new Map,c=new Set,a=[];let l=!1;function y(){for(let e=0;e<a.length;e++)try{a[e]()}catch(e){t("[Lume.js state] Error in beforeFlush hook:",e)}}function h(){for(const[e,r]of i)if(n[e]){const o=n[e];let s=0;for(;s<o.length;){const n=o[s];try{n(r)}catch(r){t(`[Lume.js state] Error notifying subscriber for key "${e+""}":`,r)}o[s]===n&&s++}}i.clear()}function b(){const e=Array.from(c);return c.clear(),e}const d={runBeforeFlushHooks:y,notifySubscribers:h,takeEffects:b};Object.defineProperty(o,u,{value:!0});const p=()=>{};function m(e,r,o){return n[e]||(n[e]=[]),1e3>n[e].length?(n[e].push(r),()=>{if(n[e]){const t=n[e].indexOf(r);-1!==t&&(n[e].splice(t,1),0===n[e].length&&delete n[e])}}):(t(`[Lume.js state] Subscriber limit (1000) reached for key "${e+""}". ${o} ignored — it will NOT receive updates. This usually means subscriptions are created in a loop without cleanup.`),p)}const w=(e,t)=>m(e,()=>{c.add(t)},"Effect subscription"),j=new Set(["__proto__","constructor","prototype"]),g=new Proxy(o,{get(e,t){if("string"==typeof t&&t.startsWith("$"))return e[t];const r=e[t];if(f.size>0)for(const e of f)e(g,t,w);return r},set(o,n,f){if("string"==typeof n&&j.has(n))return e(`[Lume.js state] Blocked write to reserved key "${n}"`),!0;const u=o[n];return Object.is(u,f)||(o[n]=f,i.set(n,f),s(d)||l||(l=!0,queueMicrotask(()=>{let e=0;try{for(;(i.size>0||c.size>0)&&r>e;){e++,y(),h();const r=b();for(let e=0;e<r.length;e++)try{r[e]()}catch(e){t("[Lume.js state] Error in effect:",e)}}}finally{l=!1}r>e||t("[Lume.js state] Maximum flush iterations reached (100). This usually indicates an infinite loop caused by an effect or computed mutating state it depends on.")}))),!0}});return o.$beforeFlush=e=>{if("function"!=typeof e)throw Error("$beforeFlush requires a function");return-1===a.indexOf(e)&&a.push(e),()=>{const t=a.indexOf(e);-1!==t&&a.splice(t,1)}},o.$subscribe=(e,t)=>{if("function"!=typeof t)throw Error("Subscriber must be a function");const r=m(e,t,"New subscriber");return r===p||t(g[e]),r},g}export{c as batch,l as state,a as withReadObserver};
|
package/dist/state.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lume-js",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"import": "./dist/index.mjs",
|
|
15
15
|
"types": "./src/index.d.ts"
|
|
16
16
|
},
|
|
17
|
+
"./state": {
|
|
18
|
+
"import": "./dist/state.mjs",
|
|
19
|
+
"types": "./src/state.d.ts"
|
|
20
|
+
},
|
|
17
21
|
"./addons": {
|
|
18
22
|
"import": "./dist/addons.mjs",
|
|
19
23
|
"types": "./src/addons/index.d.ts"
|
package/src/addons/computed.js
CHANGED
|
@@ -33,6 +33,11 @@ import { logError } from '../utils/log.js';
|
|
|
33
33
|
* mutates a state property it depends on, the flush triggered by that
|
|
34
34
|
* mutation is skipped to prevent an infinite microtask loop.
|
|
35
35
|
*
|
|
36
|
+
* @security After each computation, a re-entry guard stays active until the
|
|
37
|
+
* next microtask. If a dependency changes synchronously during this window,
|
|
38
|
+
* the computed will not recompute until a subsequent microtask. This is
|
|
39
|
+
* intentional — it prevents infinite loops from self-mutating computeds.
|
|
40
|
+
*
|
|
36
41
|
* @param {function} fn - Function that computes the value
|
|
37
42
|
* @returns {object} Object with .value property and methods
|
|
38
43
|
*
|