dalila 1.4.2 → 1.4.4

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.
Files changed (59) hide show
  1. package/dist/context/auto-scope.d.ts +167 -0
  2. package/dist/context/auto-scope.js +381 -0
  3. package/dist/context/context.d.ts +111 -0
  4. package/dist/context/context.js +283 -0
  5. package/dist/context/index.d.ts +2 -0
  6. package/dist/context/index.js +2 -0
  7. package/dist/context/raw.d.ts +2 -0
  8. package/dist/context/raw.js +2 -0
  9. package/dist/core/dev.d.ts +7 -0
  10. package/dist/core/dev.js +14 -0
  11. package/dist/core/for.d.ts +42 -0
  12. package/dist/core/for.js +311 -0
  13. package/dist/core/index.d.ts +14 -0
  14. package/dist/core/index.js +14 -0
  15. package/dist/core/key.d.ts +33 -0
  16. package/dist/core/key.js +83 -0
  17. package/dist/core/match.d.ts +22 -0
  18. package/dist/core/match.js +175 -0
  19. package/dist/core/mutation.d.ts +55 -0
  20. package/dist/core/mutation.js +128 -0
  21. package/dist/core/persist.d.ts +63 -0
  22. package/dist/core/persist.js +371 -0
  23. package/dist/core/query.d.ts +72 -0
  24. package/dist/core/query.js +184 -0
  25. package/dist/core/resource.d.ts +299 -0
  26. package/dist/core/resource.js +924 -0
  27. package/dist/core/scheduler.d.ts +111 -0
  28. package/dist/core/scheduler.js +243 -0
  29. package/dist/core/scope.d.ts +74 -0
  30. package/dist/core/scope.js +171 -0
  31. package/dist/core/signal.d.ts +88 -0
  32. package/dist/core/signal.js +451 -0
  33. package/dist/core/store.d.ts +130 -0
  34. package/dist/core/store.js +234 -0
  35. package/dist/core/virtual.d.ts +26 -0
  36. package/dist/core/virtual.js +277 -0
  37. package/dist/core/watch-testing.d.ts +13 -0
  38. package/dist/core/watch-testing.js +16 -0
  39. package/dist/core/watch.d.ts +81 -0
  40. package/dist/core/watch.js +353 -0
  41. package/dist/core/when.d.ts +23 -0
  42. package/dist/core/when.js +124 -0
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.js +4 -0
  45. package/dist/internal/watch-testing.d.ts +1 -0
  46. package/dist/internal/watch-testing.js +8 -0
  47. package/dist/router/index.d.ts +1 -0
  48. package/dist/router/index.js +1 -0
  49. package/dist/router/route.d.ts +23 -0
  50. package/dist/router/route.js +48 -0
  51. package/dist/router/router.d.ts +23 -0
  52. package/dist/router/router.js +169 -0
  53. package/dist/runtime/bind.d.ts +65 -0
  54. package/dist/runtime/bind.js +616 -0
  55. package/dist/runtime/index.d.ts +10 -0
  56. package/dist/runtime/index.js +9 -0
  57. package/dist/simple.d.ts +11 -0
  58. package/dist/simple.js +11 -0
  59. package/package.json +1 -1
@@ -0,0 +1,283 @@
1
+ import { getCurrentScope } from "../core/scope.js";
2
+ import { isInDevMode } from "../core/dev.js";
3
+ /**
4
+ * Create a new context token.
5
+ *
6
+ * Notes:
7
+ * - Tokens are identity-based: the same token instance is the key.
8
+ * - `name` is for developer-facing errors and debugging only.
9
+ * - `defaultValue` is returned by inject/tryInject when no provider is found.
10
+ */
11
+ export function createContext(name, defaultValue) {
12
+ return { name, defaultValue };
13
+ }
14
+ let globalRevision = 0;
15
+ let warnedDeepHierarchy = false;
16
+ /**
17
+ * Configurable deep hierarchy warning threshold.
18
+ */
19
+ let deepHierarchyWarnDepth = 50;
20
+ /**
21
+ * Configure the depth at which context lookup warns about deep hierarchies.
22
+ * Set to Infinity to disable the warning.
23
+ */
24
+ export function setDeepHierarchyWarnDepth(depth) {
25
+ deepHierarchyWarnDepth = depth;
26
+ }
27
+ function bumpRevision() {
28
+ globalRevision++;
29
+ }
30
+ function maybeWarnDeepHierarchy(depth) {
31
+ if (!isInDevMode())
32
+ return;
33
+ if (warnedDeepHierarchy)
34
+ return;
35
+ if (depth < deepHierarchyWarnDepth)
36
+ return;
37
+ warnedDeepHierarchy = true;
38
+ console.warn(`[Dalila] Context lookup traversed ${depth} parent scopes. ` +
39
+ `Consider flattening scope hierarchy or caching static contexts.`);
40
+ }
41
+ /**
42
+ * Per-scope registry that stores context values and links to a parent registry.
43
+ *
44
+ * Lookup:
45
+ * - `get()` checks current registry first.
46
+ * - If not found, it delegates to the parent registry (if any).
47
+ */
48
+ class ContextRegistry {
49
+ constructor(ownerScope, parent) {
50
+ this.contexts = new Map();
51
+ this.resolveCache = new Map();
52
+ this.ownerScope = ownerScope;
53
+ this.parent = parent;
54
+ }
55
+ set(token, value) {
56
+ this.contexts.set(token, { token, value });
57
+ this.resolveCache.delete(token);
58
+ }
59
+ get(token) {
60
+ const res = this.resolve(token);
61
+ return res ? res.value : undefined;
62
+ }
63
+ resolve(token) {
64
+ const cached = this.resolveCache.get(token);
65
+ if (cached && cached.rev === globalRevision)
66
+ return cached.res;
67
+ const hit = this.contexts.get(token);
68
+ if (hit !== undefined) {
69
+ const res = { value: hit.value, ownerScope: this.ownerScope, depth: 0 };
70
+ this.resolveCache.set(token, { rev: globalRevision, res });
71
+ return res;
72
+ }
73
+ if (this.parent) {
74
+ const parentRes = this.parent.resolve(token);
75
+ if (parentRes) {
76
+ const res = {
77
+ value: parentRes.value,
78
+ ownerScope: parentRes.ownerScope,
79
+ depth: parentRes.depth + 1,
80
+ };
81
+ this.resolveCache.set(token, { rev: globalRevision, res });
82
+ return res;
83
+ }
84
+ }
85
+ this.resolveCache.set(token, { rev: globalRevision, res: undefined });
86
+ return undefined;
87
+ }
88
+ listTokens(maxPerLevel) {
89
+ const tokens = [];
90
+ for (const entry of this.contexts.values()) {
91
+ tokens.push(entry.token);
92
+ if (maxPerLevel != null && tokens.length >= maxPerLevel)
93
+ break;
94
+ }
95
+ return tokens;
96
+ }
97
+ /**
98
+ * Release references eagerly.
99
+ *
100
+ * Safety:
101
+ * - Scopes in Dalila cascade: disposing a parent scope disposes children,
102
+ * so no child scope should outlive a parent registry.
103
+ */
104
+ clear() {
105
+ bumpRevision();
106
+ this.contexts.clear();
107
+ this.parent = undefined;
108
+ this.resolveCache.clear();
109
+ }
110
+ }
111
+ /**
112
+ * Registry storage keyed by Scope.
113
+ *
114
+ * Why WeakMap:
115
+ * - Avoid keeping scopes alive through registry bookkeeping.
116
+ */
117
+ const scopeRegistries = new WeakMap();
118
+ /**
119
+ * Get (or lazily create) the registry for a given scope.
120
+ *
121
+ * Key detail:
122
+ * - Parent linkage is derived from `scope.parent` (captured at createScope time),
123
+ * not from `getCurrentScope()`. This makes the hierarchy stable and explicit.
124
+ *
125
+ * Cleanup:
126
+ * - When the scope is disposed, we clear the registry and remove it from the WeakMap.
127
+ */
128
+ function getScopeRegistry(scope) {
129
+ let registry = scopeRegistries.get(scope);
130
+ if (registry)
131
+ return registry;
132
+ const parentScope = scope.parent;
133
+ const parentRegistry = parentScope ? getScopeRegistry(parentScope) : undefined;
134
+ registry = new ContextRegistry(scope, parentRegistry);
135
+ scopeRegistries.set(scope, registry);
136
+ const registryRef = registry;
137
+ scope.onCleanup(() => {
138
+ registryRef.clear();
139
+ scopeRegistries.delete(scope);
140
+ });
141
+ return registry;
142
+ }
143
+ /**
144
+ * Provide a context value in the current scope.
145
+ *
146
+ * Rules:
147
+ * - Must be called inside a scope.
148
+ * - Overrides any existing value for the same token in this scope.
149
+ */
150
+ export function provide(token, value) {
151
+ const scope = getCurrentScope();
152
+ if (!scope) {
153
+ throw new Error("[Dalila] provide() must be called within a scope. " +
154
+ "Use withScope(createScope(), () => provide(...)) or the auto-scope API.");
155
+ }
156
+ const registry = getScopeRegistry(scope);
157
+ bumpRevision();
158
+ registry.set(token, value);
159
+ }
160
+ /**
161
+ * Inject a context value from the current scope hierarchy.
162
+ *
163
+ * Rules:
164
+ * - Must be called inside a scope.
165
+ * - Walks up the parent chain until it finds the token.
166
+ * - If not found and token has a defaultValue, returns that.
167
+ * - Throws a descriptive error if not found and no default.
168
+ */
169
+ export function inject(token) {
170
+ const scope = getCurrentScope();
171
+ if (!scope) {
172
+ throw new Error("[Dalila] inject() must be called within a scope. " +
173
+ "Wrap your code in withScope(...) or use the auto-scope API.");
174
+ }
175
+ const registry = getScopeRegistry(scope);
176
+ const res = registry.resolve(token);
177
+ if (res) {
178
+ maybeWarnDeepHierarchy(res.depth);
179
+ return res.value;
180
+ }
181
+ // Check for default value
182
+ if (token.defaultValue !== undefined) {
183
+ return token.defaultValue;
184
+ }
185
+ const name = token.name || "unnamed";
186
+ let message = `[Dalila] Context "${name}" not found in scope hierarchy.`;
187
+ if (isInDevMode()) {
188
+ const levels = debugListAvailableContexts(8);
189
+ if (levels.length > 0) {
190
+ message += `\n\nAvailable contexts by depth:\n`;
191
+ for (const level of levels) {
192
+ const names = level.tokens.map((t) => t.name).join(", ") || "(none)";
193
+ message += ` depth ${level.depth}: ${names}\n`;
194
+ }
195
+ }
196
+ }
197
+ throw new Error(message);
198
+ }
199
+ /**
200
+ * Inject a context value from the current scope hierarchy if present.
201
+ *
202
+ * Rules:
203
+ * - Must be called inside a scope.
204
+ * - Returns { found: true, value } when found.
205
+ * - Returns { found: false, value: undefined } when not found (or uses defaultValue if available).
206
+ */
207
+ export function tryInject(token) {
208
+ const scope = getCurrentScope();
209
+ if (!scope) {
210
+ throw new Error("[Dalila] tryInject() must be called within a scope. " +
211
+ "Wrap your code in withScope(...) or use the auto-scope API.");
212
+ }
213
+ const registry = getScopeRegistry(scope);
214
+ const res = registry.resolve(token);
215
+ if (res) {
216
+ maybeWarnDeepHierarchy(res.depth);
217
+ return { found: true, value: res.value };
218
+ }
219
+ // Check for default value
220
+ if (token.defaultValue !== undefined) {
221
+ return { found: true, value: token.defaultValue };
222
+ }
223
+ return { found: false, value: undefined };
224
+ }
225
+ /**
226
+ * Inject a context value and return metadata about where it was resolved.
227
+ */
228
+ export function injectMeta(token) {
229
+ const scope = getCurrentScope();
230
+ if (!scope) {
231
+ throw new Error("[Dalila] injectMeta() must be called within a scope. " +
232
+ "Wrap your code in withScope(...) or use the auto-scope API.");
233
+ }
234
+ const registry = getScopeRegistry(scope);
235
+ const res = registry.resolve(token);
236
+ if (!res) {
237
+ const name = token.name || "unnamed";
238
+ let message = `[Dalila] Context "${name}" not found in scope hierarchy.`;
239
+ if (isInDevMode()) {
240
+ const levels = debugListAvailableContexts(8);
241
+ if (levels.length > 0) {
242
+ message += `\n\nAvailable contexts by depth:\n`;
243
+ for (const level of levels) {
244
+ const names = level.tokens.map((t) => t.name).join(", ") || "(none)";
245
+ message += ` depth ${level.depth}: ${names}\n`;
246
+ }
247
+ }
248
+ }
249
+ throw new Error(message);
250
+ }
251
+ maybeWarnDeepHierarchy(res.depth);
252
+ return { value: res.value, ownerScope: res.ownerScope, depth: res.depth };
253
+ }
254
+ /**
255
+ * Debug helper: list available context tokens per depth.
256
+ */
257
+ export function debugListAvailableContexts(maxPerLevel) {
258
+ const scope = getCurrentScope();
259
+ if (!scope)
260
+ return [];
261
+ const levels = [];
262
+ let depth = 0;
263
+ let current = scope;
264
+ while (current) {
265
+ const registry = scopeRegistries.get(current);
266
+ const tokens = registry
267
+ ? registry.listTokens(maxPerLevel).map((token) => ({
268
+ name: token.name || "unnamed",
269
+ token,
270
+ }))
271
+ : [];
272
+ levels.push({ depth, tokens });
273
+ current = current.parent;
274
+ depth++;
275
+ }
276
+ return levels;
277
+ }
278
+ /**
279
+ * Alias for debugListAvailableContexts (public helper name).
280
+ */
281
+ export function listAvailableContexts(maxPerLevel) {
282
+ return debugListAvailableContexts(maxPerLevel);
283
+ }
@@ -0,0 +1,2 @@
1
+ export { createContext, setDeepHierarchyWarnDepth, listAvailableContexts, type ContextToken, type TryInjectResult, } from "./context.js";
2
+ export { provide, inject, tryInject, scope, createProvider, createSignalContext, provideGlobal, injectGlobal, setAutoScopePolicy, hasGlobalScope, getGlobalScope, resetGlobalScope, type SignalContext, } from "./auto-scope.js";
@@ -0,0 +1,2 @@
1
+ export { createContext, setDeepHierarchyWarnDepth, listAvailableContexts, } from "./context.js";
2
+ export { provide, inject, tryInject, scope, createProvider, createSignalContext, provideGlobal, injectGlobal, setAutoScopePolicy, hasGlobalScope, getGlobalScope, resetGlobalScope, } from "./auto-scope.js";
@@ -0,0 +1,2 @@
1
+ export { createContext, setDeepHierarchyWarnDepth, type ContextToken, type TryInjectResult, } from "./context.js";
2
+ export { provide, inject, tryInject, injectMeta, debugListAvailableContexts, listAvailableContexts, } from "./context.js";
@@ -0,0 +1,2 @@
1
+ export { createContext, setDeepHierarchyWarnDepth, } from "./context.js";
2
+ export { provide, inject, tryInject, injectMeta, debugListAvailableContexts, listAvailableContexts, } from "./context.js";
@@ -0,0 +1,7 @@
1
+ export declare function setDevMode(enabled: boolean): void;
2
+ export declare function isInDevMode(): boolean;
3
+ /**
4
+ * Initialize dev tools. Currently just enables dev mode.
5
+ * Returns a promise for future async initialization support.
6
+ */
7
+ export declare function initDevTools(): Promise<void>;
@@ -0,0 +1,14 @@
1
+ let isDevMode = true;
2
+ export function setDevMode(enabled) {
3
+ isDevMode = enabled;
4
+ }
5
+ export function isInDevMode() {
6
+ return isDevMode;
7
+ }
8
+ /**
9
+ * Initialize dev tools. Currently just enables dev mode.
10
+ * Returns a promise for future async initialization support.
11
+ */
12
+ export async function initDevTools() {
13
+ setDevMode(true);
14
+ }
@@ -0,0 +1,42 @@
1
+ interface DisposableFragment extends DocumentFragment {
2
+ dispose(): void;
3
+ }
4
+ /**
5
+ * Low-level keyed list rendering with fine-grained reactivity.
6
+ *
7
+ * Uses keyed diffing to efficiently update only changed items.
8
+ * Each item gets its own scope for automatic cleanup.
9
+ *
10
+ * @param items - Signal or function returning array of items
11
+ * @param template - Function that renders each item (receives item and reactive index)
12
+ * @param keyFn - Optional function to extract unique key from item (defaults to index)
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const todos = signal([
17
+ * { id: 1, text: 'Learn Dalila' },
18
+ * { id: 2, text: 'Build app' }
19
+ * ]);
20
+ *
21
+ * forEach(
22
+ * () => todos(),
23
+ * (todo, index) => {
24
+ * const li = document.createElement('li');
25
+ * li.textContent = `${index()}: ${todo.text}`;
26
+ * return li;
27
+ * },
28
+ * (todo) => todo.id.toString()
29
+ * );
30
+ * ```
31
+ *
32
+ * @internal Prefer createList() for most use cases
33
+ */
34
+ export declare function forEach<T>(items: () => T[], template: (item: T, index: () => number) => Node | Node[], keyFn?: (item: T, index: number) => string): DisposableFragment;
35
+ /**
36
+ * Stable API for rendering keyed lists.
37
+ *
38
+ * Renders a reactive list with automatic updates when items change.
39
+ * Only re-renders items that actually changed (keyed diffing).
40
+ */
41
+ export declare function createList<T>(items: () => T[], template: (item: T, index: number) => Node | Node[], keyFn?: (item: T, index: number) => string): DisposableFragment;
42
+ export {};
@@ -0,0 +1,311 @@
1
+ import { effect, signal } from './signal.js';
2
+ import { isInDevMode } from './dev.js';
3
+ import { createScope, withScope, getCurrentScope } from './scope.js';
4
+ const autoDisposeByDocument = new WeakMap();
5
+ const getMutationObserverCtor = (doc) => {
6
+ if (doc.defaultView?.MutationObserver)
7
+ return doc.defaultView.MutationObserver;
8
+ if (typeof MutationObserver !== 'undefined')
9
+ return MutationObserver;
10
+ return null;
11
+ };
12
+ const registerAutoDispose = (start, end, cleanup) => {
13
+ const doc = start.ownerDocument;
14
+ const MutationObserverCtor = getMutationObserverCtor(doc);
15
+ if (!MutationObserverCtor)
16
+ return () => { };
17
+ let ctx = autoDisposeByDocument.get(doc);
18
+ if (!ctx) {
19
+ const entries = new Set();
20
+ const observer = new MutationObserverCtor(() => {
21
+ entries.forEach(entry => {
22
+ const connected = entry.start.isConnected && entry.end.isConnected;
23
+ if (!entry.attached && connected) {
24
+ entry.attached = true;
25
+ return;
26
+ }
27
+ if (entry.attached && !connected)
28
+ entry.cleanup();
29
+ });
30
+ if (entries.size === 0) {
31
+ observer.disconnect();
32
+ autoDisposeByDocument.delete(doc);
33
+ }
34
+ });
35
+ observer.observe(doc, { childList: true, subtree: true });
36
+ ctx = { observer, entries };
37
+ autoDisposeByDocument.set(doc, ctx);
38
+ }
39
+ const entry = {
40
+ start,
41
+ end,
42
+ cleanup,
43
+ attached: start.isConnected && end.isConnected
44
+ };
45
+ ctx.entries.add(entry);
46
+ return () => {
47
+ ctx?.entries.delete(entry);
48
+ if (ctx && ctx.entries.size === 0) {
49
+ ctx.observer.disconnect();
50
+ autoDisposeByDocument.delete(doc);
51
+ }
52
+ };
53
+ };
54
+ /**
55
+ * Low-level keyed list rendering with fine-grained reactivity.
56
+ *
57
+ * Uses keyed diffing to efficiently update only changed items.
58
+ * Each item gets its own scope for automatic cleanup.
59
+ *
60
+ * @param items - Signal or function returning array of items
61
+ * @param template - Function that renders each item (receives item and reactive index)
62
+ * @param keyFn - Optional function to extract unique key from item (defaults to index)
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const todos = signal([
67
+ * { id: 1, text: 'Learn Dalila' },
68
+ * { id: 2, text: 'Build app' }
69
+ * ]);
70
+ *
71
+ * forEach(
72
+ * () => todos(),
73
+ * (todo, index) => {
74
+ * const li = document.createElement('li');
75
+ * li.textContent = `${index()}: ${todo.text}`;
76
+ * return li;
77
+ * },
78
+ * (todo) => todo.id.toString()
79
+ * );
80
+ * ```
81
+ *
82
+ * @internal Prefer createList() for most use cases
83
+ */
84
+ export function forEach(items, template, keyFn) {
85
+ const start = document.createComment('for:start');
86
+ const end = document.createComment('for:end');
87
+ let currentItems = [];
88
+ let disposeEffect = null;
89
+ const parentScope = getCurrentScope();
90
+ let disposed = false;
91
+ let stopAutoDispose = null;
92
+ const getKey = (item, index) => {
93
+ if (keyFn)
94
+ return keyFn(item, index);
95
+ // Using index as key is an anti-pattern for dynamic lists
96
+ // but we allow it as a fallback. Items will be re-rendered on reorder.
97
+ return `__idx_${index}`;
98
+ };
99
+ const removeNode = (node) => {
100
+ if (node.parentNode)
101
+ node.parentNode.removeChild(node);
102
+ };
103
+ const removeRange = (keyedItem) => {
104
+ // Dispose scope first to cleanup effects/listeners
105
+ if (keyedItem.scope) {
106
+ keyedItem.scope.dispose();
107
+ keyedItem.scope = null;
108
+ }
109
+ // Remove DOM nodes (including markers)
110
+ let node = keyedItem.start;
111
+ while (node) {
112
+ const next = node.nextSibling;
113
+ removeNode(node);
114
+ if (node === keyedItem.end)
115
+ break;
116
+ node = next;
117
+ }
118
+ };
119
+ const clearBetween = (startNode, endNode) => {
120
+ let node = startNode.nextSibling;
121
+ while (node && node !== endNode) {
122
+ const next = node.nextSibling;
123
+ removeNode(node);
124
+ node = next;
125
+ }
126
+ };
127
+ const moveRangeBefore = (startNode, endNode, referenceNode) => {
128
+ const parent = referenceNode.parentNode;
129
+ if (!parent)
130
+ return;
131
+ let node = startNode;
132
+ while (node) {
133
+ const next = node.nextSibling;
134
+ parent.insertBefore(node, referenceNode);
135
+ if (node === endNode)
136
+ break;
137
+ node = next;
138
+ }
139
+ };
140
+ let hasValidatedOnce = false;
141
+ const throwDuplicateKey = (key, scheduleFatal) => {
142
+ const error = new Error(`[Dalila] Duplicate key "${key}" detected in forEach. ` +
143
+ `Keys must be unique within the same list. Check your keyFn implementation.`);
144
+ if (scheduleFatal) {
145
+ queueMicrotask(() => {
146
+ throw error;
147
+ });
148
+ }
149
+ throw error;
150
+ };
151
+ const validateNoDuplicateKeys = (arr) => {
152
+ if (!isInDevMode())
153
+ return;
154
+ const scheduleFatal = hasValidatedOnce;
155
+ hasValidatedOnce = true;
156
+ const seenKeys = new Set();
157
+ arr.forEach((item, index) => {
158
+ const key = getKey(item, index);
159
+ if (seenKeys.has(key)) {
160
+ throwDuplicateKey(key, scheduleFatal);
161
+ }
162
+ seenKeys.add(key);
163
+ });
164
+ };
165
+ const disposeItemScope = (item) => {
166
+ if (!item.scope)
167
+ return;
168
+ item.scope.dispose();
169
+ item.scope = null;
170
+ };
171
+ const cleanup = () => {
172
+ if (disposed)
173
+ return;
174
+ disposed = true;
175
+ stopAutoDispose?.();
176
+ stopAutoDispose = null;
177
+ disposeEffect?.();
178
+ disposeEffect = null;
179
+ currentItems.forEach(item => {
180
+ removeRange(item);
181
+ });
182
+ currentItems = [];
183
+ removeNode(start);
184
+ removeNode(end);
185
+ };
186
+ // Validate first render synchronously (let throw escape in dev)
187
+ validateNoDuplicateKeys(items());
188
+ const update = () => {
189
+ if (disposed)
190
+ return;
191
+ const newItems = items();
192
+ // Validate again on updates (will be caught by effect error handler)
193
+ validateNoDuplicateKeys(newItems);
194
+ const oldMap = new Map();
195
+ currentItems.forEach(item => oldMap.set(item.key, item));
196
+ const nextItems = [];
197
+ const itemsToUpdate = new Set();
198
+ const seenNextKeys = new Set();
199
+ // Phase 1: Build next list + detect updates/new
200
+ newItems.forEach((item, index) => {
201
+ const key = getKey(item, index);
202
+ if (seenNextKeys.has(key))
203
+ return; // prod-mode: ignore dup keys silently
204
+ seenNextKeys.add(key);
205
+ const existing = oldMap.get(key);
206
+ if (existing) {
207
+ if (existing.value !== item) {
208
+ itemsToUpdate.add(key);
209
+ existing.value = item;
210
+ }
211
+ nextItems.push(existing);
212
+ }
213
+ else {
214
+ itemsToUpdate.add(key);
215
+ nextItems.push({
216
+ key,
217
+ value: item,
218
+ start: document.createComment(`for:${key}:start`),
219
+ end: document.createComment(`for:${key}:end`),
220
+ scope: null,
221
+ indexSignal: signal(index)
222
+ });
223
+ }
224
+ });
225
+ // Phase 2: Remove items no longer present
226
+ const nextKeys = new Set(nextItems.map(i => i.key));
227
+ currentItems.forEach(item => {
228
+ if (!nextKeys.has(item.key))
229
+ removeRange(item);
230
+ });
231
+ // Phase 3: Move/insert items to correct positions
232
+ const parent = end.parentNode;
233
+ if (parent) {
234
+ let cursor = start;
235
+ nextItems.forEach(item => {
236
+ const nextSibling = cursor.nextSibling;
237
+ const inDom = item.start.parentNode === parent;
238
+ if (!inDom) {
239
+ const referenceNode = nextSibling || end;
240
+ referenceNode.before(item.start, item.end);
241
+ }
242
+ else if (nextSibling !== item.start) {
243
+ const referenceNode = nextSibling || end;
244
+ moveRangeBefore(item.start, item.end, referenceNode);
245
+ }
246
+ cursor = item.end;
247
+ });
248
+ }
249
+ // Phase 4: Dispose scopes and clear content for changed items
250
+ nextItems.forEach(item => {
251
+ if (!itemsToUpdate.has(item.key))
252
+ return;
253
+ disposeItemScope(item);
254
+ clearBetween(item.start, item.end);
255
+ });
256
+ // Phase 5: Update reactive indices for ALL items
257
+ nextItems.forEach((item, index) => {
258
+ item.indexSignal.set(index);
259
+ });
260
+ // Phase 6: Render changed items
261
+ nextItems.forEach(item => {
262
+ if (!itemsToUpdate.has(item.key))
263
+ return;
264
+ item.scope = createScope();
265
+ withScope(item.scope, () => {
266
+ const indexGetter = () => item.indexSignal();
267
+ const templateResult = template(item.value, indexGetter);
268
+ const nodes = Array.isArray(templateResult) ? templateResult : [templateResult];
269
+ item.end.before(...nodes);
270
+ });
271
+ });
272
+ currentItems = nextItems;
273
+ };
274
+ // IMPORTANT: append markers BEFORE creating the effect,
275
+ // so a synchronous effect run can still render into the fragment safely.
276
+ const frag = document.createDocumentFragment();
277
+ frag.append(start, end);
278
+ frag.dispose = cleanup;
279
+ // Run update reactively and capture dispose
280
+ disposeEffect = effect(() => {
281
+ if (disposed)
282
+ return;
283
+ update();
284
+ });
285
+ // Cleanup on parent scope disposal
286
+ if (parentScope) {
287
+ parentScope.onCleanup(cleanup);
288
+ }
289
+ else {
290
+ stopAutoDispose = registerAutoDispose(start, end, cleanup);
291
+ if (isInDevMode()) {
292
+ console.warn('[Dalila] forEach() called outside of a scope. ' +
293
+ 'The effect will not be tied to a scope. ' +
294
+ 'It will auto-dispose when removed from the DOM, ' +
295
+ 'or call fragment.dispose() for manual cleanup if needed.');
296
+ }
297
+ }
298
+ return frag;
299
+ }
300
+ /**
301
+ * Stable API for rendering keyed lists.
302
+ *
303
+ * Renders a reactive list with automatic updates when items change.
304
+ * Only re-renders items that actually changed (keyed diffing).
305
+ */
306
+ export function createList(items, template, keyFn) {
307
+ return forEach(items, (item, index) => {
308
+ const idx = index();
309
+ return template(item, idx);
310
+ }, keyFn);
311
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./scope.js";
2
+ export * from "./signal.js";
3
+ export * from "./watch.js";
4
+ export * from "./when.js";
5
+ export * from "./match.js";
6
+ export * from "./for.js";
7
+ export * from "./virtual.js";
8
+ export * from "./dev.js";
9
+ export * from "./key.js";
10
+ export * from "./resource.js";
11
+ export * from "./query.js";
12
+ export * from "./mutation.js";
13
+ export { batch, measure, mutate, configureScheduler, getSchedulerConfig } from "./scheduler.js";
14
+ export { persist, createJSONStorage, clearPersisted, createPreloadScript, createThemeScript, type StateStorage, type PersistOptions, type Serializer, type PreloadScriptOptions } from "./persist.js";