@nisli/core 0.54.1 → 0.55.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 +93 -3
- package/dist/component.js +146 -5
- package/dist/component.js.map +1 -1
- package/dist/diagnostics.d.ts +48 -0
- package/dist/diagnostics.js +118 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/disposable.d.ts +14 -0
- package/dist/disposable.js +8 -0
- package/dist/disposable.js.map +1 -0
- package/dist/emitter.d.ts +2 -1
- package/dist/emitter.js +2 -1
- package/dist/emitter.js.map +1 -1
- package/dist/hmr/registry.js +8 -1
- package/dist/hmr/registry.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/injector.d.ts +9 -2
- package/dist/injector.js +18 -0
- package/dist/injector.js.map +1 -1
- package/dist/lifecycle.d.ts +9 -0
- package/dist/lifecycle.js +17 -7
- package/dist/lifecycle.js.map +1 -1
- package/dist/projection.js +24 -5
- package/dist/projection.js.map +1 -1
- package/dist/query.d.ts +134 -55
- package/dist/query.js +379 -188
- package/dist/query.js.map +1 -1
- package/dist/resource.d.ts +2 -1
- package/dist/resource.js +21 -4
- package/dist/resource.js.map +1 -1
- package/dist/settle.d.ts +61 -0
- package/dist/settle.js +106 -0
- package/dist/settle.js.map +1 -0
- package/dist/signal.d.ts +42 -7
- package/dist/signal.js +248 -68
- package/dist/signal.js.map +1 -1
- package/dist/template-audit.d.ts +64 -0
- package/dist/template-audit.js +246 -0
- package/dist/template-audit.js.map +1 -0
- package/dist/template.d.ts +70 -1
- package/dist/template.js +322 -65
- package/dist/template.js.map +1 -1
- package/dist/view-transition.d.ts +70 -0
- package/dist/view-transition.js +98 -0
- package/dist/view-transition.js.map +1 -0
- package/package.json +1 -1
package/dist/resource.d.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* dependencies are tracked synchronously, while the loader runs outside the
|
|
6
6
|
* reactive observer and commits only the newest live generation.
|
|
7
7
|
*/
|
|
8
|
+
import { type DisposableResource } from './disposable.js';
|
|
8
9
|
import { type ReadonlySignal } from './signal.js';
|
|
9
|
-
export interface ResourceResult<T> {
|
|
10
|
+
export interface ResourceResult<T> extends DisposableResource {
|
|
10
11
|
/** Latest successful value. Retained while a newer load is pending. */
|
|
11
12
|
readonly data: ReadonlySignal<T | undefined>;
|
|
12
13
|
/** True while the current generation is loading. */
|
package/dist/resource.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* reactive observer and commits only the newest live generation.
|
|
7
7
|
*/
|
|
8
8
|
import { getCurrentComponent, hasContext } from './context.js';
|
|
9
|
+
import { disposable } from './disposable.js';
|
|
10
|
+
import { __enrollPending } from './settle.js';
|
|
9
11
|
import { effect, signal } from './signal.js';
|
|
10
12
|
/**
|
|
11
13
|
* Derive local async state from a synchronously tracked source.
|
|
@@ -63,32 +65,46 @@ export function resource(source, loader) {
|
|
|
63
65
|
const isStale = () => (disposed
|
|
64
66
|
|| controller.signal.aborted
|
|
65
67
|
|| currentGeneration !== generation);
|
|
68
|
+
// settle() enrollment (ADR 0030.2 T2): this generation is a pending
|
|
69
|
+
// logical request from here until COMMIT (data/error write) or ABORT
|
|
70
|
+
// (the cleanup below) — never raw loader settlement, so an
|
|
71
|
+
// abort-ignoring loader cannot wedge settle(). Idempotent.
|
|
72
|
+
const settled = __enrollPending();
|
|
66
73
|
// The microtask boundary is deliberate: loader signal reads are NOT source
|
|
67
74
|
// dependencies, and synchronous loader throws become normal rejections.
|
|
68
75
|
void Promise.resolve().then(async () => {
|
|
69
|
-
if (isStale())
|
|
76
|
+
if (isStale()) {
|
|
77
|
+
settled();
|
|
70
78
|
return;
|
|
79
|
+
}
|
|
71
80
|
try {
|
|
72
81
|
const value = await loader(enabledSource, controller.signal);
|
|
73
|
-
if (isStale())
|
|
82
|
+
if (isStale()) {
|
|
83
|
+
settled();
|
|
74
84
|
return;
|
|
85
|
+
}
|
|
75
86
|
data.value = value;
|
|
76
87
|
loading.value = false;
|
|
88
|
+
settled();
|
|
77
89
|
}
|
|
78
90
|
catch (cause) {
|
|
79
|
-
if (isStale())
|
|
91
|
+
if (isStale()) {
|
|
92
|
+
settled();
|
|
80
93
|
return;
|
|
94
|
+
}
|
|
81
95
|
error.value = toError(cause);
|
|
82
96
|
loading.value = false;
|
|
97
|
+
settled();
|
|
83
98
|
}
|
|
84
99
|
});
|
|
85
100
|
return () => {
|
|
86
101
|
if (currentGeneration === generation)
|
|
87
102
|
generation++;
|
|
88
103
|
controller.abort();
|
|
104
|
+
settled();
|
|
89
105
|
};
|
|
90
106
|
});
|
|
91
|
-
|
|
107
|
+
const result = {
|
|
92
108
|
data,
|
|
93
109
|
loading,
|
|
94
110
|
error,
|
|
@@ -98,6 +114,7 @@ export function resource(source, loader) {
|
|
|
98
114
|
},
|
|
99
115
|
dispose,
|
|
100
116
|
};
|
|
117
|
+
return disposable(result, dispose);
|
|
101
118
|
}
|
|
102
119
|
function toError(cause) {
|
|
103
120
|
if (cause instanceof Error)
|
package/dist/resource.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,MAAM,EAAuB,MAAM,aAAa,CAAC;AAelE;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CACtB,MAA2B,EAC3B,MAA0D;IAE1D,MAAM,IAAI,GAAG,MAAM,CAAgB,SAAS,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,KAAK,GAAG,MAAM,CAAe,IAAI,CAAC,CAAC;IACzC,MAAM,cAAc,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IAEjC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,UAAU,GAAe,GAAG,EAAE,GAAE,CAAC,CAAC;IAEtC,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,QAAQ;YAAE,OAAO;QACrB,QAAQ,GAAG,IAAI,CAAC;QAChB,UAAU,EAAE,CAAC;QACb,UAAU,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;IACxB,CAAC,CAAC;IAEF,wEAAwE;IACxE,4EAA4E;IAC5E,uDAAuD;IACvD,IAAI,UAAU,EAAE,EAAE,CAAC;QACjB,mBAAmB,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7C,CAAC;IAED,UAAU,GAAG,MAAM,CAAC,GAAG,EAAE;QACvB,cAAc,CAAC,KAAK,CAAC;QACrB,MAAM,iBAAiB,GAAG,EAAE,UAAU,CAAC;QAEvC,IAAI,WAA0B,CAAC;QAC/B,IAAI,CAAC;YACH,WAAW,GAAG,MAAM,EAAE,CAAC;QACzB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;YACtB,KAAK,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;YAC7B,OAAO;QACT,CAAC;QAED,sEAAsE;QACtE,sEAAsE;QACtE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;YACvB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;YACtB,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC;YACnB,OAAO;QACT,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;QACrB,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC;QACnB,MAAM,aAAa,GAAG,WAAgB,CAAC;QACvC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,CACpB,QAAQ;eACL,UAAU,CAAC,MAAM,CAAC,OAAO;eACzB,iBAAiB,KAAK,UAAU,CACpC,CAAC;QAEF,2EAA2E;QAC3E,wEAAwE;QACxE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YACrC,IAAI,OAAO,EAAE;
|
|
1
|
+
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
|
package/dist/settle.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settle.ts — Awaitable async quiescence (ADR 0030.2 T2).
|
|
3
|
+
*
|
|
4
|
+
* `await settle()` resolves when every framework-started logical async
|
|
5
|
+
* request has TERMINATED and the reactive graph is quiescent. Query runs
|
|
6
|
+
* (query.ts) and resource loads (resource.ts) enroll here when they start
|
|
7
|
+
* and deregister at *termination* — commit, terminal error after retries,
|
|
8
|
+
* or abort — never at raw promise settlement. An abort-ignoring fetcher
|
|
9
|
+
* whose run was superseded therefore cannot wedge `settle()`: the run's
|
|
10
|
+
* registry entry closed at the abort even though the underlying promise
|
|
11
|
+
* never resolves.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* mount(app);
|
|
15
|
+
* await settle();
|
|
16
|
+
* expect(screen.textContent).toContain('loaded'); // no waitFor polling
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Contract and bounds:
|
|
20
|
+
* - This is a **test/verify primitive** (and the SSG pre-snapshot barrier).
|
|
21
|
+
* Awaiting it in a live app with continuous background refetch/polling is
|
|
22
|
+
* unbounded *by design* — each commit may legally schedule the next
|
|
23
|
+
* request. A scope argument waits for a second consumer (ADR 0030.2 §8).
|
|
24
|
+
* - Each iteration awaits the pending registry, then `tick()` (signal.ts's
|
|
25
|
+
* bounded microtask-drain contract). Timer-scheduled work is NOT awaited
|
|
26
|
+
* beyond what a pending logical request itself spans.
|
|
27
|
+
* - Iteration-capped at 50 like `tick()`; hitting the cap emits diagnostic
|
|
28
|
+
* N603 and resolves — never a silent hang, never an unbounded loop.
|
|
29
|
+
*
|
|
30
|
+
* MERGE ITEM: `__resetPending()` should be folded into `resetInjector()`
|
|
31
|
+
* (injector.ts is owned by another worktree); until then test teardown
|
|
32
|
+
* calls it directly.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Enroll a logical async request. Returns its `done()` terminator: call it
|
|
36
|
+
* at commit, terminal error, or abort. Idempotent — a superseded run whose
|
|
37
|
+
* terminator was already invoked by the superseding path may safely call
|
|
38
|
+
* it again from its own settlement branch.
|
|
39
|
+
*
|
|
40
|
+
* @internal — called by query.ts runs and resource.ts loader generations.
|
|
41
|
+
*/
|
|
42
|
+
export declare function __enrollPending(): () => void;
|
|
43
|
+
/**
|
|
44
|
+
* Reset the registry. Test-teardown escape hatch: entries orphaned by a
|
|
45
|
+
* test that never terminated its fetchers would otherwise leak into the
|
|
46
|
+
* next test's `settle()`.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export declare function __resetPending(): void;
|
|
51
|
+
/** Current number of enrolled (unterminated) logical requests. @internal */
|
|
52
|
+
export declare function __pendingCount(): number;
|
|
53
|
+
/**
|
|
54
|
+
* Await async quiescence: loop `tick()` → `Promise.allSettled(pending)`
|
|
55
|
+
* until a full `tick()` leaves the pending registry empty.
|
|
56
|
+
*
|
|
57
|
+
* The `tick()` *first* ordering matters: observer effects created since
|
|
58
|
+
* the last flush are what *start* runs (and enroll them) — the registry
|
|
59
|
+
* can only be trusted empty after the reactive graph has flushed.
|
|
60
|
+
*/
|
|
61
|
+
export declare function settle(): Promise<void>;
|
package/dist/settle.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settle.ts — Awaitable async quiescence (ADR 0030.2 T2).
|
|
3
|
+
*
|
|
4
|
+
* `await settle()` resolves when every framework-started logical async
|
|
5
|
+
* request has TERMINATED and the reactive graph is quiescent. Query runs
|
|
6
|
+
* (query.ts) and resource loads (resource.ts) enroll here when they start
|
|
7
|
+
* and deregister at *termination* — commit, terminal error after retries,
|
|
8
|
+
* or abort — never at raw promise settlement. An abort-ignoring fetcher
|
|
9
|
+
* whose run was superseded therefore cannot wedge `settle()`: the run's
|
|
10
|
+
* registry entry closed at the abort even though the underlying promise
|
|
11
|
+
* never resolves.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* mount(app);
|
|
15
|
+
* await settle();
|
|
16
|
+
* expect(screen.textContent).toContain('loaded'); // no waitFor polling
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Contract and bounds:
|
|
20
|
+
* - This is a **test/verify primitive** (and the SSG pre-snapshot barrier).
|
|
21
|
+
* Awaiting it in a live app with continuous background refetch/polling is
|
|
22
|
+
* unbounded *by design* — each commit may legally schedule the next
|
|
23
|
+
* request. A scope argument waits for a second consumer (ADR 0030.2 §8).
|
|
24
|
+
* - Each iteration awaits the pending registry, then `tick()` (signal.ts's
|
|
25
|
+
* bounded microtask-drain contract). Timer-scheduled work is NOT awaited
|
|
26
|
+
* beyond what a pending logical request itself spans.
|
|
27
|
+
* - Iteration-capped at 50 like `tick()`; hitting the cap emits diagnostic
|
|
28
|
+
* N603 and resolves — never a silent hang, never an unbounded loop.
|
|
29
|
+
*
|
|
30
|
+
* MERGE ITEM: `__resetPending()` should be folded into `resetInjector()`
|
|
31
|
+
* (injector.ts is owned by another worktree); until then test teardown
|
|
32
|
+
* calls it directly.
|
|
33
|
+
*/
|
|
34
|
+
import { tick } from './signal.js';
|
|
35
|
+
// ── Pending logical-request registry ────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* One entry per in-flight logical request. The promise is a *termination*
|
|
38
|
+
* promise resolved by the enroller's `done()` callback — deliberately not
|
|
39
|
+
* the fetcher/loader promise itself (see module doc).
|
|
40
|
+
*/
|
|
41
|
+
const pending = new Set();
|
|
42
|
+
/**
|
|
43
|
+
* Enroll a logical async request. Returns its `done()` terminator: call it
|
|
44
|
+
* at commit, terminal error, or abort. Idempotent — a superseded run whose
|
|
45
|
+
* terminator was already invoked by the superseding path may safely call
|
|
46
|
+
* it again from its own settlement branch.
|
|
47
|
+
*
|
|
48
|
+
* @internal — called by query.ts runs and resource.ts loader generations.
|
|
49
|
+
*/
|
|
50
|
+
export function __enrollPending() {
|
|
51
|
+
let resolve;
|
|
52
|
+
const entry = new Promise((res) => { resolve = res; });
|
|
53
|
+
pending.add(entry);
|
|
54
|
+
let done = false;
|
|
55
|
+
return () => {
|
|
56
|
+
if (done)
|
|
57
|
+
return;
|
|
58
|
+
done = true;
|
|
59
|
+
pending.delete(entry);
|
|
60
|
+
resolve();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Reset the registry. Test-teardown escape hatch: entries orphaned by a
|
|
65
|
+
* test that never terminated its fetchers would otherwise leak into the
|
|
66
|
+
* next test's `settle()`.
|
|
67
|
+
*
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
export function __resetPending() {
|
|
71
|
+
pending.clear();
|
|
72
|
+
}
|
|
73
|
+
/** Current number of enrolled (unterminated) logical requests. @internal */
|
|
74
|
+
export function __pendingCount() {
|
|
75
|
+
return pending.size;
|
|
76
|
+
}
|
|
77
|
+
// ── Diagnostics + test isolation ────────────────────────────────────
|
|
78
|
+
// Reporting goes through the core diagnostics leaf. The pending registry
|
|
79
|
+
// self-registers with resetInjector() so test teardown clears it without
|
|
80
|
+
// an upside-down import (settle → injector, never the reverse).
|
|
81
|
+
import { diag, isDev } from './diagnostics.js';
|
|
82
|
+
import { registerResetHook } from './injector.js';
|
|
83
|
+
registerResetHook(__resetPending);
|
|
84
|
+
// ── settle() ────────────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Await async quiescence: loop `tick()` → `Promise.allSettled(pending)`
|
|
87
|
+
* until a full `tick()` leaves the pending registry empty.
|
|
88
|
+
*
|
|
89
|
+
* The `tick()` *first* ordering matters: observer effects created since
|
|
90
|
+
* the last flush are what *start* runs (and enroll them) — the registry
|
|
91
|
+
* can only be trusted empty after the reactive graph has flushed.
|
|
92
|
+
*/
|
|
93
|
+
export async function settle() {
|
|
94
|
+
for (let i = 0; i < 50; i++) {
|
|
95
|
+
await tick();
|
|
96
|
+
if (pending.size === 0)
|
|
97
|
+
return;
|
|
98
|
+
await Promise.allSettled([...pending]);
|
|
99
|
+
}
|
|
100
|
+
if (isDev())
|
|
101
|
+
diag('N603', 'settle() did not reach quiescence within 50 iterations — something '
|
|
102
|
+
+ 'keeps scheduling new async work (a self-invalidating query or a '
|
|
103
|
+
+ 'polling loop?). settle() is a test/verify primitive; under live '
|
|
104
|
+
+ 'background refetch it is unbounded by design. Returning anyway.');
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=settle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settle.js","sourceRoot":"","sources":["../src/settle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,uEAAuE;AAEvE;;;;GAIG;AACH,MAAM,OAAO,GAAG,IAAI,GAAG,EAAiB,CAAC;AAEzC;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe;IAC7B,IAAI,OAAoB,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,OAAO,CAAO,CAAC,GAAG,EAAE,EAAE,GAAG,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACnB,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,OAAO,GAAG,EAAE;QACV,IAAI,IAAI;YAAE,OAAO;QACjB,IAAI,GAAG,IAAI,CAAC;QACZ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtB,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,CAAC,KAAK,EAAE,CAAC;AAClB,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,cAAc;IAC5B,OAAO,OAAO,CAAC,IAAI,CAAC;AACtB,CAAC;AAED,uEAAuE;AACvE,yEAAyE;AACzE,yEAAyE;AACzE,gEAAgE;AAChE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,iBAAiB,CAAC,cAAc,CAAC,CAAC;AAElC,uEAAuE;AAEvE;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM;IAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,MAAM,IAAI,EAAE,CAAC;QACb,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO;QAC/B,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IACzC,CAAC;IACD,IAAI,KAAK,EAAE;QAAE,IAAI,CACf,MAAM,EACN,qEAAqE;cACnE,kEAAkE;cAClE,kEAAkE;cAClE,iEAAiE,CACpE,CAAC;AACJ,CAAC"}
|
package/dist/signal.d.ts
CHANGED
|
@@ -2,10 +2,26 @@
|
|
|
2
2
|
* signal.ts — Reactive primitives: signal(), computed(), effect()
|
|
3
3
|
*
|
|
4
4
|
* Push-pull hybrid model:
|
|
5
|
-
* - Writes push
|
|
5
|
+
* - Writes push dirty flags up the dependency graph in two flavors: a signal
|
|
6
|
+
* write pushes Dirty (the value definitely changed), a computed propagates
|
|
7
|
+
* MaybeDirty (recompute to find out). Flags never downgrade.
|
|
6
8
|
* - Reads pull fresh values lazily (computed only recalculates when read)
|
|
7
9
|
* - Multiple synchronous writes coalesce into one microtask flush
|
|
8
10
|
*
|
|
11
|
+
* Scheduler invariants (ADR 0030.2 T5):
|
|
12
|
+
* - An effect re-runs iff a value it read changed. Before a MaybeDirty
|
|
13
|
+
* effect runs, its computed sources are polled (recomputed via the
|
|
14
|
+
* node→impl back-pointer); if no source's change-epoch advanced past the
|
|
15
|
+
* effect's last completed run, the run is skipped. A computed's
|
|
16
|
+
* change-epoch also bumps on every error-state TRANSITION (enter and
|
|
17
|
+
* exit), so effects never skip across error entry or equal-value recovery
|
|
18
|
+
* (issue 0003 containment happens at the effect's own read).
|
|
19
|
+
* - Pending effects flush in creation order (monotonic index), so identical
|
|
20
|
+
* writes produce identical run order. Documented cost: a later-created
|
|
21
|
+
* writer effect costs one deterministic extra run vs. topological order.
|
|
22
|
+
* - The loop guard is clock-free: an effect that re-schedules ITSELF on
|
|
23
|
+
* MAX_EFFECT_RERUNS consecutive runs is diagnosed (N301) and disposed.
|
|
24
|
+
*
|
|
9
25
|
* Dependency tracking is automatic: reading a signal inside a computed
|
|
10
26
|
* or effect registers it as a dependency. Dependencies are re-tracked
|
|
11
27
|
* on every execution (dynamic/conditional tracking).
|
|
@@ -21,12 +37,21 @@ export interface ReadonlySignal<T> {
|
|
|
21
37
|
readonly [SIGNAL_BRAND]: true;
|
|
22
38
|
readonly value: T;
|
|
23
39
|
/** Subscribe to value changes. Returns unsubscribe function. */
|
|
24
|
-
subscribe(fn: (value: T) => void):
|
|
40
|
+
subscribe(fn: (value: T) => void): Disposer;
|
|
41
|
+
/**
|
|
42
|
+
* Read the current value WITHOUT registering a dependency.
|
|
43
|
+
* Equivalent to reading inside `untrack()`. On a computed this still
|
|
44
|
+
* pulls a fresh value (and rethrows a cached error) — it only skips
|
|
45
|
+
* dependency registration.
|
|
46
|
+
*/
|
|
47
|
+
peek(): T;
|
|
25
48
|
}
|
|
26
49
|
/** Read-write reactive container. */
|
|
27
50
|
export interface Signal<T> extends ReadonlySignal<T> {
|
|
28
51
|
value: T;
|
|
29
52
|
}
|
|
53
|
+
import { type Disposer } from './disposable.js';
|
|
54
|
+
export type { Disposer } from './disposable.js';
|
|
30
55
|
/**
|
|
31
56
|
* Create a writable reactive signal.
|
|
32
57
|
*
|
|
@@ -34,6 +59,7 @@ export interface Signal<T> extends ReadonlySignal<T> {
|
|
|
34
59
|
* const count = signal(0);
|
|
35
60
|
* count.value; // read (auto-tracks in reactive contexts)
|
|
36
61
|
* count.value = 5; // write (notifies dependents)
|
|
62
|
+
* count.peek(); // read WITHOUT tracking
|
|
37
63
|
* ```
|
|
38
64
|
*/
|
|
39
65
|
export declare function signal<T>(initialValue: T): Signal<T>;
|
|
@@ -47,12 +73,20 @@ export declare function signal<T>(initialValue: T): Signal<T>;
|
|
|
47
73
|
* ```
|
|
48
74
|
*/
|
|
49
75
|
export declare function computed<T>(fn: () => T): ReadonlySignal<T>;
|
|
76
|
+
/**
|
|
77
|
+
* Type-level rejection of async callbacks: a Promise-returning fn makes the
|
|
78
|
+
* call require an impossible second argument, so `effect(async () => {})`
|
|
79
|
+
* fails to compile. Async work belongs in resource() (ADR 0030.2 §3; N310).
|
|
80
|
+
*/
|
|
81
|
+
type SyncEffectGuard<R> = [R] extends [Promise<unknown>] ? [error: 'effect() callbacks must be synchronous — async work belongs in resource() (nisli N310)'] : [];
|
|
50
82
|
/**
|
|
51
83
|
* Create a side-effect that re-runs when its dependencies change.
|
|
52
84
|
* Returns a dispose function to stop the effect.
|
|
53
85
|
*
|
|
54
86
|
* The effect function may return a cleanup callback that runs
|
|
55
|
-
* before each re-execution and on disposal.
|
|
87
|
+
* before each re-execution and on disposal. It must be SYNCHRONOUS —
|
|
88
|
+
* async functions are rejected at the type level and diagnosed at
|
|
89
|
+
* runtime (N310); async state belongs in resource().
|
|
56
90
|
*
|
|
57
91
|
* ```ts
|
|
58
92
|
* const dispose = effect(() => {
|
|
@@ -64,7 +98,7 @@ export declare function computed<T>(fn: () => T): ReadonlySignal<T>;
|
|
|
64
98
|
* dispose(); // stop the effect
|
|
65
99
|
* ```
|
|
66
100
|
*/
|
|
67
|
-
export declare function effect(fn: () =>
|
|
101
|
+
export declare function effect<R>(fn: () => R, ..._rejectAsync: SyncEffectGuard<R>): Disposer;
|
|
68
102
|
/**
|
|
69
103
|
* Check if a value is a signal (writable or computed).
|
|
70
104
|
* Used by the template engine to detect signals in expression slots.
|
|
@@ -72,13 +106,14 @@ export declare function effect(fn: () => void | (() => void)): () => void;
|
|
|
72
106
|
export declare function isSignal(value: unknown): value is ReadonlySignal<unknown>;
|
|
73
107
|
/**
|
|
74
108
|
* Run a function with activeObserver set to null, preventing any
|
|
75
|
-
* signal reads inside `fn` from being tracked by an outer effect
|
|
109
|
+
* signal reads inside `fn` from being tracked by an outer effect,
|
|
110
|
+
* and return the function's result.
|
|
76
111
|
*
|
|
77
112
|
* Used by component.ts to isolate connectedCallback: child component
|
|
78
113
|
* setup signal reads must not leak into a parent effect's dependency
|
|
79
114
|
* set. See ADR 0008 Gap 1.
|
|
80
115
|
*/
|
|
81
|
-
export declare function untrack(fn: () =>
|
|
116
|
+
export declare function untrack<T>(fn: () => T): T;
|
|
82
117
|
/**
|
|
83
118
|
* Synchronously execute all pending effects.
|
|
84
119
|
*
|
|
@@ -102,7 +137,7 @@ export { flush as flushEffects };
|
|
|
102
137
|
* TASKS scheduled by that microtask work (e.g. a microtask calling
|
|
103
138
|
* setTimeout) may fire after tick() resolves; awaiting arbitrary future
|
|
104
139
|
* tasks is unbounded. Self-perpetuating schedulers are cut off at the
|
|
105
|
-
* iteration cap.
|
|
140
|
+
* iteration cap (loudly — N303).
|
|
106
141
|
*
|
|
107
142
|
* ```ts
|
|
108
143
|
* host.setProp('x', 1);
|