@nisli/core 0.54.1 → 0.56.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.
Files changed (47) hide show
  1. package/README.md +119 -4
  2. package/dist/component.js +146 -5
  3. package/dist/component.js.map +1 -1
  4. package/dist/diagnostics.d.ts +48 -0
  5. package/dist/diagnostics.js +118 -0
  6. package/dist/diagnostics.js.map +1 -0
  7. package/dist/disposable.d.ts +14 -0
  8. package/dist/disposable.js +8 -0
  9. package/dist/disposable.js.map +1 -0
  10. package/dist/emitter.d.ts +2 -1
  11. package/dist/emitter.js +2 -1
  12. package/dist/emitter.js.map +1 -1
  13. package/dist/hmr/registry.js +8 -1
  14. package/dist/hmr/registry.js.map +1 -1
  15. package/dist/index.d.ts +6 -3
  16. package/dist/index.js +9 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/injector.d.ts +9 -2
  19. package/dist/injector.js +18 -0
  20. package/dist/injector.js.map +1 -1
  21. package/dist/lifecycle.d.ts +9 -0
  22. package/dist/lifecycle.js +17 -7
  23. package/dist/lifecycle.js.map +1 -1
  24. package/dist/projection.js +24 -5
  25. package/dist/projection.js.map +1 -1
  26. package/dist/query.d.ts +134 -55
  27. package/dist/query.js +379 -188
  28. package/dist/query.js.map +1 -1
  29. package/dist/resource.d.ts +2 -1
  30. package/dist/resource.js +21 -4
  31. package/dist/resource.js.map +1 -1
  32. package/dist/settle.d.ts +61 -0
  33. package/dist/settle.js +106 -0
  34. package/dist/settle.js.map +1 -0
  35. package/dist/signal.d.ts +42 -7
  36. package/dist/signal.js +248 -68
  37. package/dist/signal.js.map +1 -1
  38. package/dist/template-audit.d.ts +64 -0
  39. package/dist/template-audit.js +246 -0
  40. package/dist/template-audit.js.map +1 -0
  41. package/dist/template.d.ts +69 -12
  42. package/dist/template.js +349 -66
  43. package/dist/template.js.map +1 -1
  44. package/dist/view-transition.d.ts +70 -0
  45. package/dist/view-transition.js +98 -0
  46. package/dist/view-transition.js.map +1 -0
  47. package/package.json +1 -1
@@ -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
- return {
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)
@@ -1 +1 @@
1
- {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
@@ -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 "dirty" flags up the dependency graph
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): () => 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: () => void | (() => void)): () => void;
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: () => void): void;
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);