dalila 1.3.1 → 1.3.2
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/dist/context/auto-scope.d.ts +65 -49
- package/dist/context/auto-scope.js +208 -88
- package/dist/context/context.d.ts +106 -2
- package/dist/context/context.js +245 -28
- package/dist/context/index.d.ts +2 -3
- package/dist/context/index.js +2 -3
- package/dist/context/raw.d.ts +1 -1
- package/dist/context/raw.js +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/match.js +9 -3
- package/dist/core/query.js +46 -13
- package/dist/core/resource.d.ts +110 -0
- package/dist/core/resource.js +280 -39
- package/dist/core/scheduler.d.ts +31 -0
- package/dist/core/scheduler.js +26 -13
- package/dist/core/scope.d.ts +17 -1
- package/dist/core/scope.js +62 -7
- package/dist/core/signal.d.ts +22 -0
- package/dist/core/signal.js +86 -0
- package/dist/core/when.js +9 -3
- package/package.json +1 -1
package/dist/core/scope.js
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
/** Tracks disposed scopes without mutating the public interface. */
|
|
2
|
+
const disposedScopes = new WeakSet();
|
|
3
|
+
const scopeCreateListeners = new Set();
|
|
4
|
+
const scopeDisposeListeners = new Set();
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe to scope creation events.
|
|
7
|
+
* Returns an unsubscribe function.
|
|
8
|
+
*/
|
|
9
|
+
export function onScopeCreate(fn) {
|
|
10
|
+
scopeCreateListeners.add(fn);
|
|
11
|
+
return () => scopeCreateListeners.delete(fn);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Subscribe to scope disposal events.
|
|
15
|
+
* Returns an unsubscribe function.
|
|
16
|
+
*/
|
|
17
|
+
export function onScopeDispose(fn) {
|
|
18
|
+
scopeDisposeListeners.add(fn);
|
|
19
|
+
return () => scopeDisposeListeners.delete(fn);
|
|
20
|
+
}
|
|
21
|
+
/** Returns true if the given scope has been disposed. */
|
|
22
|
+
export function isScopeDisposed(scope) {
|
|
23
|
+
return disposedScopes.has(scope);
|
|
24
|
+
}
|
|
1
25
|
/**
|
|
2
26
|
* Creates a new Scope instance.
|
|
3
27
|
*
|
|
@@ -7,13 +31,13 @@
|
|
|
7
31
|
* in the same dispose pass (because we snapshot via `splice(0)`).
|
|
8
32
|
* - Parent is captured from the current scope context (set by withScope).
|
|
9
33
|
*/
|
|
10
|
-
export function createScope() {
|
|
34
|
+
export function createScope(parentOverride) {
|
|
11
35
|
const cleanups = [];
|
|
12
|
-
const parent = currentScope;
|
|
13
|
-
let disposed = false;
|
|
36
|
+
const parent = parentOverride === undefined ? currentScope : parentOverride === null ? null : parentOverride;
|
|
14
37
|
const runCleanupSafely = (fn) => {
|
|
15
38
|
try {
|
|
16
|
-
|
|
39
|
+
fn();
|
|
40
|
+
return undefined;
|
|
17
41
|
}
|
|
18
42
|
catch (err) {
|
|
19
43
|
return err;
|
|
@@ -21,7 +45,7 @@ export function createScope() {
|
|
|
21
45
|
};
|
|
22
46
|
const scope = {
|
|
23
47
|
onCleanup(fn) {
|
|
24
|
-
if (
|
|
48
|
+
if (isScopeDisposed(scope)) {
|
|
25
49
|
const error = runCleanupSafely(fn);
|
|
26
50
|
if (error) {
|
|
27
51
|
console.error('[Dalila] cleanup registered after dispose() threw:', error);
|
|
@@ -31,9 +55,9 @@ export function createScope() {
|
|
|
31
55
|
cleanups.push(fn);
|
|
32
56
|
},
|
|
33
57
|
dispose() {
|
|
34
|
-
if (
|
|
58
|
+
if (isScopeDisposed(scope))
|
|
35
59
|
return;
|
|
36
|
-
|
|
60
|
+
disposedScopes.add(scope);
|
|
37
61
|
const snapshot = cleanups.splice(0);
|
|
38
62
|
const errors = [];
|
|
39
63
|
for (const fn of snapshot) {
|
|
@@ -44,12 +68,28 @@ export function createScope() {
|
|
|
44
68
|
if (errors.length > 0) {
|
|
45
69
|
console.error('[Dalila] scope.dispose() had cleanup errors:', errors);
|
|
46
70
|
}
|
|
71
|
+
for (const listener of scopeDisposeListeners) {
|
|
72
|
+
try {
|
|
73
|
+
listener(scope);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error('[Dalila] scope.dispose() listener threw:', err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
47
79
|
},
|
|
48
80
|
parent,
|
|
49
81
|
};
|
|
50
82
|
if (parent) {
|
|
51
83
|
parent.onCleanup(() => scope.dispose());
|
|
52
84
|
}
|
|
85
|
+
for (const listener of scopeCreateListeners) {
|
|
86
|
+
try {
|
|
87
|
+
listener(scope);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.error('[Dalila] scope.create() listener threw:', err);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
53
93
|
return scope;
|
|
54
94
|
}
|
|
55
95
|
/**
|
|
@@ -61,6 +101,18 @@ let currentScope = null;
|
|
|
61
101
|
export function getCurrentScope() {
|
|
62
102
|
return currentScope;
|
|
63
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Returns the current scope hierarchy, from current scope up to the root.
|
|
106
|
+
*/
|
|
107
|
+
export function getCurrentScopeHierarchy() {
|
|
108
|
+
const scopes = [];
|
|
109
|
+
let current = currentScope;
|
|
110
|
+
while (current) {
|
|
111
|
+
scopes.push(current);
|
|
112
|
+
current = current.parent;
|
|
113
|
+
}
|
|
114
|
+
return scopes;
|
|
115
|
+
}
|
|
64
116
|
/**
|
|
65
117
|
* Sets the current active scope.
|
|
66
118
|
* Prefer using `withScope()` unless you are implementing low-level internals.
|
|
@@ -76,6 +128,9 @@ export function setCurrentScope(scope) {
|
|
|
76
128
|
* - `effect()` can auto-dispose when the scope ends
|
|
77
129
|
*/
|
|
78
130
|
export function withScope(scope, fn) {
|
|
131
|
+
if (isScopeDisposed(scope)) {
|
|
132
|
+
throw new Error('[Dalila] withScope() cannot enter a disposed scope.');
|
|
133
|
+
}
|
|
79
134
|
const prevScope = currentScope;
|
|
80
135
|
currentScope = scope;
|
|
81
136
|
try {
|
package/dist/core/signal.d.ts
CHANGED
|
@@ -5,9 +5,16 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export declare function setEffectErrorHandler(handler: (error: Error, source: string) => void): void;
|
|
7
7
|
export interface Signal<T> {
|
|
8
|
+
/** Read the current value (with dependency tracking if inside an effect). */
|
|
8
9
|
(): T;
|
|
10
|
+
/** Set a new value and notify subscribers. */
|
|
9
11
|
set(value: T): void;
|
|
12
|
+
/** Update the value using a function. */
|
|
10
13
|
update(fn: (v: T) => T): void;
|
|
14
|
+
/** Read the current value without creating a dependency (no tracking). */
|
|
15
|
+
peek(): T;
|
|
16
|
+
/** Subscribe to value changes manually (outside of effects). Returns unsubscribe function. */
|
|
17
|
+
on(callback: (value: T) => void): () => void;
|
|
11
18
|
}
|
|
12
19
|
/**
|
|
13
20
|
* Create a signal: a mutable value with automatic dependency tracking.
|
|
@@ -64,3 +71,18 @@ export declare function computed<T>(fn: () => T): Signal<T>;
|
|
|
64
71
|
* - when disposed, aborts the current run and stops future scheduling
|
|
65
72
|
*/
|
|
66
73
|
export declare function effectAsync(fn: (signal: AbortSignal) => void): () => void;
|
|
74
|
+
/**
|
|
75
|
+
* Run a function without tracking any signal reads as dependencies.
|
|
76
|
+
*
|
|
77
|
+
* Use this inside an effect when you want to read a signal's value
|
|
78
|
+
* without creating a dependency on it.
|
|
79
|
+
*
|
|
80
|
+
* Example:
|
|
81
|
+
* ```ts
|
|
82
|
+
* effect(() => {
|
|
83
|
+
* const tracked = count(); // This read is tracked
|
|
84
|
+
* const untracked = untrack(() => other()); // This read is NOT tracked
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare function untrack<T>(fn: () => T): T;
|
package/dist/core/signal.js
CHANGED
|
@@ -158,6 +158,26 @@ export function signal(initialValue) {
|
|
|
158
158
|
read.update = (fn) => {
|
|
159
159
|
read.set(fn(value));
|
|
160
160
|
};
|
|
161
|
+
read.peek = () => value;
|
|
162
|
+
read.on = (callback) => {
|
|
163
|
+
// Create a lightweight effect-like subscriber for manual subscriptions
|
|
164
|
+
const subscriber = (() => {
|
|
165
|
+
if (subscriber.disposed)
|
|
166
|
+
return;
|
|
167
|
+
callback(value);
|
|
168
|
+
});
|
|
169
|
+
subscriber.disposed = false;
|
|
170
|
+
subscribers.add(subscriber);
|
|
171
|
+
(subscriber.deps ?? (subscriber.deps = new Set())).add(subscribers);
|
|
172
|
+
// Return unsubscribe function
|
|
173
|
+
return () => {
|
|
174
|
+
if (subscriber.disposed)
|
|
175
|
+
return;
|
|
176
|
+
subscriber.disposed = true;
|
|
177
|
+
subscribers.delete(subscriber);
|
|
178
|
+
subscriber.deps?.delete(subscribers);
|
|
179
|
+
};
|
|
180
|
+
};
|
|
161
181
|
return read;
|
|
162
182
|
}
|
|
163
183
|
/**
|
|
@@ -306,6 +326,45 @@ export function computed(fn) {
|
|
|
306
326
|
read.update = () => {
|
|
307
327
|
throw new Error('Cannot update a computed signal directly. Computed signals are derived from other signals.');
|
|
308
328
|
};
|
|
329
|
+
read.peek = () => {
|
|
330
|
+
// For computed, peek still needs to compute if dirty, but without tracking
|
|
331
|
+
if (dirty) {
|
|
332
|
+
cleanupDeps();
|
|
333
|
+
const prevEffect = activeEffect;
|
|
334
|
+
const prevScope = activeScope;
|
|
335
|
+
activeEffect = markDirty;
|
|
336
|
+
activeScope = null;
|
|
337
|
+
try {
|
|
338
|
+
value = fn();
|
|
339
|
+
dirty = false;
|
|
340
|
+
if (markDirty.deps)
|
|
341
|
+
trackedDeps = new Set(markDirty.deps);
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
activeEffect = prevEffect;
|
|
345
|
+
activeScope = prevScope;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return value;
|
|
349
|
+
};
|
|
350
|
+
read.on = (callback) => {
|
|
351
|
+
const subscriber = (() => {
|
|
352
|
+
if (subscriber.disposed)
|
|
353
|
+
return;
|
|
354
|
+
// For computed, we need to get the latest value
|
|
355
|
+
callback(read.peek());
|
|
356
|
+
});
|
|
357
|
+
subscriber.disposed = false;
|
|
358
|
+
subscribers.add(subscriber);
|
|
359
|
+
(subscriber.deps ?? (subscriber.deps = new Set())).add(subscribers);
|
|
360
|
+
return () => {
|
|
361
|
+
if (subscriber.disposed)
|
|
362
|
+
return;
|
|
363
|
+
subscriber.disposed = true;
|
|
364
|
+
subscribers.delete(subscriber);
|
|
365
|
+
subscriber.deps?.delete(subscribers);
|
|
366
|
+
};
|
|
367
|
+
};
|
|
309
368
|
return read;
|
|
310
369
|
}
|
|
311
370
|
/**
|
|
@@ -363,3 +422,30 @@ export function effectAsync(fn) {
|
|
|
363
422
|
owningScope.onCleanup(dispose);
|
|
364
423
|
return dispose;
|
|
365
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Run a function without tracking any signal reads as dependencies.
|
|
427
|
+
*
|
|
428
|
+
* Use this inside an effect when you want to read a signal's value
|
|
429
|
+
* without creating a dependency on it.
|
|
430
|
+
*
|
|
431
|
+
* Example:
|
|
432
|
+
* ```ts
|
|
433
|
+
* effect(() => {
|
|
434
|
+
* const tracked = count(); // This read is tracked
|
|
435
|
+
* const untracked = untrack(() => other()); // This read is NOT tracked
|
|
436
|
+
* });
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
439
|
+
export function untrack(fn) {
|
|
440
|
+
const prevEffect = activeEffect;
|
|
441
|
+
const prevScope = activeScope;
|
|
442
|
+
activeEffect = null;
|
|
443
|
+
activeScope = null;
|
|
444
|
+
try {
|
|
445
|
+
return fn();
|
|
446
|
+
}
|
|
447
|
+
finally {
|
|
448
|
+
activeEffect = prevEffect;
|
|
449
|
+
activeScope = prevScope;
|
|
450
|
+
}
|
|
451
|
+
}
|
package/dist/core/when.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { effect } from './signal.js';
|
|
2
|
-
import { createScope, getCurrentScope, withScope } from './scope.js';
|
|
2
|
+
import { createScope, getCurrentScope, isScopeDisposed, withScope } from './scope.js';
|
|
3
3
|
import { scheduleMicrotask } from './scheduler.js';
|
|
4
4
|
/**
|
|
5
5
|
* Conditional DOM rendering with per-branch lifetime.
|
|
@@ -38,6 +38,13 @@ export function when(test, thenFn, elseFn) {
|
|
|
38
38
|
let pendingCond = undefined;
|
|
39
39
|
/** Disposal guard to prevent orphan microtasks from touching dead DOM. */
|
|
40
40
|
let disposed = false;
|
|
41
|
+
/** Parent scope captured at creation time (if any). */
|
|
42
|
+
const parentScope = getCurrentScope();
|
|
43
|
+
const resolveParentScope = () => {
|
|
44
|
+
if (!parentScope)
|
|
45
|
+
return null;
|
|
46
|
+
return isScopeDisposed(parentScope) ? null : parentScope;
|
|
47
|
+
};
|
|
41
48
|
/** Removes mounted nodes and disposes their branch scope. */
|
|
42
49
|
const clear = () => {
|
|
43
50
|
branchScope?.dispose();
|
|
@@ -56,7 +63,7 @@ export function when(test, thenFn, elseFn) {
|
|
|
56
63
|
/** Clears old branch and mounts the branch for `cond`. */
|
|
57
64
|
const swap = (cond) => {
|
|
58
65
|
clear();
|
|
59
|
-
const nextScope = createScope();
|
|
66
|
+
const nextScope = createScope(resolveParentScope());
|
|
60
67
|
try {
|
|
61
68
|
// Render inside an isolated scope so branch resources are tied to that branch.
|
|
62
69
|
const result = withScope(nextScope, () => (cond ? thenFn() : elseFn?.()));
|
|
@@ -107,7 +114,6 @@ export function when(test, thenFn, elseFn) {
|
|
|
107
114
|
});
|
|
108
115
|
});
|
|
109
116
|
// If `when()` is created inside a parent scope, dispose branch resources with it.
|
|
110
|
-
const parentScope = getCurrentScope();
|
|
111
117
|
if (parentScope) {
|
|
112
118
|
parentScope.onCleanup(() => {
|
|
113
119
|
disposed = true;
|