@solidjs/signals 2.0.0-rc.6 → 2.0.0-rc.7

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 (65) hide show
  1. package/dist/dev.js +2033 -1291
  2. package/dist/node.cjs +1821 -2401
  3. package/dist/node.dev.cjs +13724 -0
  4. package/dist/prod/boundaries.js +3 -1
  5. package/dist/prod/core/async.js +6 -1
  6. package/dist/prod/core/attribution-hooks.js +3 -0
  7. package/dist/prod/core/constants.js +9 -1
  8. package/dist/prod/core/context.js +10 -16
  9. package/dist/prod/core/core.js +215 -139
  10. package/dist/prod/core/dev.js +2 -0
  11. package/dist/prod/core/effect.js +37 -28
  12. package/dist/prod/core/external.js +2 -2
  13. package/dist/prod/core/graph.js +35 -31
  14. package/dist/prod/core/heap.js +34 -40
  15. package/dist/prod/core/lanes.js +41 -27
  16. package/dist/prod/core/optimistic.js +42 -32
  17. package/dist/prod/core/owner.js +32 -32
  18. package/dist/prod/core/scheduler.js +140 -154
  19. package/dist/prod/core/verdict.js +31 -34
  20. package/dist/prod/index.js +0 -2
  21. package/dist/prod/map.js +104 -94
  22. package/dist/prod/signals.js +51 -34
  23. package/dist/prod/store/next/optimistic.js +153 -172
  24. package/dist/prod/store/next/reconcile.js +140 -288
  25. package/dist/prod/store/next/store.js +307 -237
  26. package/dist/prod/store/store.js +2 -2
  27. package/dist/types/core/attribution-hooks.d.ts +64 -0
  28. package/dist/types/core/attribution.d.ts +265 -9
  29. package/dist/types/core/constants.d.ts +8 -0
  30. package/dist/types/core/core.d.ts +12 -3
  31. package/dist/types/core/dev.d.ts +49 -8
  32. package/dist/types/core/heap.d.ts +5 -3
  33. package/dist/types/core/invariants.d.ts +1 -1
  34. package/dist/types/core/lanes.d.ts +10 -0
  35. package/dist/types/core/scheduler.d.ts +9 -4
  36. package/dist/types/signals.d.ts +10 -0
  37. package/dist/types/store/index.d.ts +3 -6
  38. package/dist/types/store/next/optimistic.d.ts +4 -2
  39. package/dist/types/store/next/reconcile.d.ts +5 -12
  40. package/dist/types/store/next/store.d.ts +3 -9
  41. package/dist/types/store/next/target.d.ts +20 -46
  42. package/dist/types/store/store.d.ts +14 -9
  43. package/dist/types-cjs/core/attribution-hooks.d.cts +64 -0
  44. package/dist/types-cjs/core/attribution.d.cts +265 -9
  45. package/dist/types-cjs/core/constants.d.cts +8 -0
  46. package/dist/types-cjs/core/core.d.cts +12 -3
  47. package/dist/types-cjs/core/dev.d.cts +49 -8
  48. package/dist/types-cjs/core/heap.d.cts +5 -3
  49. package/dist/types-cjs/core/invariants.d.cts +1 -1
  50. package/dist/types-cjs/core/lanes.d.cts +10 -0
  51. package/dist/types-cjs/core/scheduler.d.cts +9 -4
  52. package/dist/types-cjs/signals.d.cts +10 -0
  53. package/dist/types-cjs/store/index.d.cts +3 -6
  54. package/dist/types-cjs/store/next/optimistic.d.cts +4 -2
  55. package/dist/types-cjs/store/next/reconcile.d.cts +5 -12
  56. package/dist/types-cjs/store/next/store.d.cts +3 -9
  57. package/dist/types-cjs/store/next/target.d.cts +20 -46
  58. package/dist/types-cjs/store/store.d.cts +14 -9
  59. package/package.json +3 -2
  60. package/dist/prod/store/next/patch-hooks.js +0 -13
  61. package/dist/prod/store/next/patch.js +0 -614
  62. package/dist/types/store/next/patch-hooks.d.ts +0 -41
  63. package/dist/types/store/next/patch.d.ts +0 -91
  64. package/dist/types-cjs/store/next/patch-hooks.d.cts +0 -41
  65. package/dist/types-cjs/store/next/patch.d.cts +0 -91
@@ -4,15 +4,12 @@ export { isWrappable, $TRACK, $PROXY, $TARGET } from "./store.js";
4
4
  import type { NoFn, ProjectionOptions, Store, StoreOptions, StoreSetter } from "./store.js";
5
5
  import type { Refreshable } from "../core/index.js";
6
6
  export { createProjectionNext as createProjection } from "./next/projection.js";
7
- export { registerPatch, registerRowOps, registerSlotPatchNext as registerSlotPatch, patchableRaw } from "./next/patch.js";
8
7
  export { storeIsShallow, storeHasFamily, storeHasOptimisticFamily } from "./next/store.js";
9
8
  export { createOptimisticStoreNext as createOptimisticStore } from "./next/optimistic.js";
10
- /** Public createStore: plain form `(init, options?)` and derived writable
9
+ /** Public createStore: plain form `(initialValue, options?)` and derived writable
11
10
  * form `(fn, seed, options?)`. */
12
- export declare function createStore<T extends object = {}>(store: NoFn<T> | Store<NoFn<T>>, options?: StoreOptions & {
13
- shallow?: boolean;
14
- }): [get: Store<T>, set: StoreSetter<T>];
15
- export declare function createStore<T extends object = {}>(fn: (store: T) => void | T | Promise<void | T> | AsyncIterable<void | T>, store: Partial<T> | Store<NoFn<T>>, options?: ProjectionOptions): [get: Refreshable<Store<T>>, set: StoreSetter<T>];
11
+ export declare function createStore<T extends object = {}>(initialValue: NoFn<T> | Store<NoFn<T>>, options?: StoreOptions): [get: Store<T>, set: StoreSetter<T>];
12
+ export declare function createStore<T extends object = {}>(fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>, seed: Partial<T> | Store<NoFn<T>>, options?: ProjectionOptions): [get: Refreshable<Store<T>>, set: StoreSetter<T>];
16
13
  export declare function reconcile<T extends U, U>(value: T, key?: string | ((item: NonNullable<any>) => any) | null): (state: U) => T;
17
14
  export declare function snapshot<T>(value: T): T;
18
15
  export declare function deep<T>(value: T): T;
@@ -1,5 +1,6 @@
1
+ import { type Refreshable } from "../../core/index.js";
1
2
  import { type Transition } from "../../core/scheduler.js";
2
- import { type NoFn, type ProjectionOptions, type Store, type StoreSetter } from "../store.js";
3
+ import { type NoFn, type ProjectionOptions, type Store, type StoreOptions, type StoreSetter } from "../store.js";
3
4
  import type { StoreNextTarget } from "./target.js";
4
5
  /** #3164 fold: a stamped truth is HELD (masked from ordinary readers until
5
6
  * the reveal) only while its transition is live AND retaining optimism —
@@ -8,7 +9,8 @@ import type { StoreNextTarget } from "./target.js";
8
9
  * staged values to converge (normal speculation). Resolves merges first:
9
10
  * merge unions optimistic nodes/stores into the target. */
10
11
  export declare function transitionHoldsOptimism(transition: Transition): boolean;
11
- export declare function createOptimisticStoreNext<T extends object = {}>(first: T | ((store: T) => void | T | Promise<void | T> | AsyncIterable<void | T>), second?: NoFn<T> | Store<NoFn<T>>, options?: ProjectionOptions): [get: Store<T>, set: StoreSetter<T>];
12
+ export declare function createOptimisticStoreNext<T extends object = {}>(initialValue: NoFn<T> | Store<NoFn<T>>, options?: StoreOptions): [get: Store<T>, set: StoreSetter<T>];
13
+ export declare function createOptimisticStoreNext<T extends object = {}>(fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>, seed: Partial<T> | Store<NoFn<T>>, options?: ProjectionOptions): [get: Refreshable<Store<T>>, set: StoreSetter<T>];
12
14
  /** Diff the draft against the current OPTIMISTIC VIEW (committed + active
13
15
  * overrides — the same view the draft was seeded from) and emit engine writes
14
16
  * for exactly the changed keys. Visible-view diffing keeps no-op writes from
@@ -1,17 +1,10 @@
1
- import type { RowOps } from "./patch.js";
2
- import { type StoreNextTarget } from "./target.js";
3
1
  type KeyFn = (item: any) => any;
4
2
  export declare function reconcileNextState(value: any, state: any, key: string | KeyFn | null | undefined, replace?: boolean): void;
3
+ /** Setter-channel row ops (the fold site calls this for array targets with
4
+ * ops consumers): structural mutation through the setter — push/splice/index
5
5
  /** Key equality for EVERY key comparison in this module (re-audit 2, P1-5):
6
- * SameValueZero, matching the Map-based matchers (buildRowOps, the adoption
7
- * window) — NaN keys are equal to themselves, so aligned NaN rows stay
8
- * aligned in the prefix walk instead of forever misaligning. Adoption and
9
- * row ops MUST agree on key equality or retained DOM rows go stale. */
6
+ * SameValueZero, matching the adoption window's Map-based matcher — NaN keys
7
+ * are equal to themselves, so aligned NaN rows stay aligned in the prefix
8
+ * walk instead of forever misaligning. */
10
9
  export declare function sameKey(a: any, b: any): boolean;
11
- export declare function emitSetterRowOps(t: StoreNextTarget, prevRows: any[], nextRows: any[]): void;
12
- /** Identity-keyed structural diff, returned rather than emitted: shared by
13
- * the setter channel (regular queue) and the OPTIMISTIC write channel (lane
14
- * queue) — same retention semantics, different dispatch timing. Returns
15
- * null when the lists are identity-aligned (no structure changed). */
16
- export declare function buildIdentityRowOps(prevRows: any[], nextRows: any[]): RowOps | null;
17
10
  export {};
@@ -1,20 +1,14 @@
1
1
  import type { Signal } from "../../core/types.js";
2
- import { type StoreNextFamily, type StoreNextTarget, type PatchChannel } from "./target.js";
3
- /** Lazily allocate the patch-channel extension (one literal shape). */
4
- export declare function pcOf(t: StoreNextTarget): PatchChannel;
2
+ import { type StoreNextFamily, type StoreNextTarget } from "./target.js";
5
3
  export declare function wrapNext<T extends Record<PropertyKey, any>>(value: T, parent?: StoreNextTarget | null, parentKey?: PropertyKey | null, fam?: StoreNextFamily | null): T;
6
4
  /** Unwrap our own proxies to their current backing; leave everything else. */
7
5
  export declare function unwrapValue(v: any): any;
8
- export declare function getNode(target: StoreNextTarget, key: PropertyKey, current: any): Signal<any>;
6
+ export declare function getNode(target: StoreNextTarget, key: PropertyKey, current: any, accKnown?: -1 | 0 | 1): Signal<any>;
9
7
  export declare function getHasNode(target: StoreNextTarget, key: PropertyKey, present: boolean): Signal<boolean>;
10
8
  export declare function getKeySetNode(target: StoreNextTarget): Signal<number>;
11
9
  /** Deep-witness bump: any value/shape change on a record with a live deep()
12
10
  * subscriber notifies it. One null check when unused. */
13
11
  export declare function bumpDeep(t: StoreNextTarget): void;
14
- /** Scanned plainness for patch admission (patchableRaw): runs the one-time
15
- * accessor scan if it hasn't happened yet — the sticky `a` flag alone is not
16
- * trustworthy before a scan (it starts false and is discovered lazily). */
17
- export declare function targetIsPlain(target: StoreNextTarget): boolean;
18
12
  /** Downgrade a prototype-overlay pending backing to the clone path: builds
19
13
  * the real container (committed + overlay writes − deletes) that fold will
20
14
  * SWAP in as the committed backing, exactly as if the draft had started on
@@ -90,7 +84,7 @@ export type SetStoreNextFunction<T> = (fn: (draft: T) => T | void) => void;
90
84
  * replacements as adoptions. `guard=false` skips the owned-scope dev guard —
91
85
  * projection recomputes legitimately write from inside their computed. */
92
86
  export declare function storeSetterNext<T>(proxy: T, fn: (draft: T) => T | void, guard?: boolean): void;
93
- export declare function createStoreNext<T extends Record<PropertyKey, any>>(init: T, shallow?: boolean): [T, SetStoreNextFunction<T>];
87
+ export declare function createStoreNext<T extends Record<PropertyKey, any>>(initialValue: T, shallow?: boolean): [T, SetStoreNextFunction<T>];
94
88
  /** True when `proxy` is a SHALLOW store (children served verbatim, slots
95
89
  * replaced by reference — #2932). The list driver uses this to choose the
96
90
  * slot-patch channel (collected row bodies) over per-record registration. */
@@ -12,7 +12,7 @@
12
12
  * entry per read-through object; zero layer slots; nodes, has-nodes, and the
13
13
  * key-set node are lazy, materialized only by subscription.
14
14
  */
15
- import type { Computed, Owner, Signal } from "../../core/types.js";
15
+ import type { Computed, Signal } from "../../core/types.js";
16
16
  /** Projection family (§7b): children wrap into the family's own map (writes
17
17
  * land in the projection, never the source family), and every node created
18
18
  * under the family carries the projection computed as its firewall. */
@@ -53,40 +53,6 @@ export interface StoreNextFamily {
53
53
  node: Computed<any> | null;
54
54
  shallow?: boolean;
55
55
  }
56
- /** Write-side patch-channel state (stage 2), grouped off the target's named
57
- * fields — see the shape rule on `StoreNextTarget.pc`. One literal shape,
58
- * allocated by `pcOf` on first use. */
59
- export interface PatchChannel {
60
- /** Slot-patch hooks for shallow arrays — the reconcile walk emits
61
- * (i, next, prev) for key-aligned value-replaced slots through the patch
62
- * apply queue (records are raw, no per-record targets exist).
63
- * MULTI-CONSUMER (external audit): one array can drive several lists. */
64
- sp: {
65
- fn: (index: number, next: any, prev: any) => void;
66
- owner: Owner | null;
67
- }[] | null;
68
- /** Patch-channel consumers (next/patch.ts): per-record compiled patch
69
- * entries, multi-consumer. null when unpatched (the common case). */
70
- p: object[] | null;
71
- /** Same-batch coalescing stamp (re-audit 2/3): the container array this
72
- * channel last pushed a non-forced SELF entry into, plus that entry. A
73
- * later same-batch emission UPDATES the queued entry's `next` in place
74
- * (latest state wins — adoption REPLACES the captured object, so dropping
75
- * the later emission would apply stale state) while `prev` stays the
76
- * batch's earliest. The drain clears both stamps so a quiet record
77
- * retains nothing from its last batch. */
78
- qa: unknown;
79
- qe: unknown;
80
- /** Row-ops consumers (next/patch.ts, PR-B): structural list ops —
81
- * (nextRows, { prefix, sources, removed }) at apply timing. */
82
- ro: object[] | null;
83
- /** Keys written through the traps since the last fold commit. Bounds the
84
- * setter notify/hold-check to O(written) instead of O(subscribed nodes) —
85
- * a record with thousands of per-key subscriptions (selection maps) would
86
- * otherwise pay a full node scan on every write. null = no trap writes
87
- * this batch (bulk paths fall back to the full scan). */
88
- wk: Set<PropertyKey> | null;
89
- }
90
56
  export interface StoreNextTarget {
91
57
  /** Committed backing: source object (shared) or owned clone. */
92
58
  v: Record<PropertyKey, any>;
@@ -102,15 +68,19 @@ export interface StoreNextTarget {
102
68
  h: Record<PropertyKey, Signal<boolean>> | null;
103
69
  /** Lazy key-set node: membership/iteration/$TRACK subscriptions (§6). */
104
70
  k: Signal<number> | null;
105
- /** Patch-channel extension (lazily allocated on first use): groups the
106
- * write-side stage-2 fields so they never widen the TARGET's own named
107
- * field count. LOAD-BEARING SHAPE RULE: array proxy targets carry their
108
- * fields as named properties on a real array, and V8 normalizes an array
109
- * to dictionary properties as the named count grows (empirically at
110
- * counts ≡ 0 mod 3 from 18 up on V8 13.x) — every trap field read then
111
- * becomes a hash lookup (~15% uibench, tree suites worst). New
112
- * patch-channel state MUST go inside this object, not on the target. */
113
- pc: PatchChannel | null;
71
+ /** Keys written through the traps since the last fold commit. Bounds the
72
+ * setter notify/hold-check to O(written) instead of O(subscribed nodes) —
73
+ * a record with thousands of per-key subscriptions (selection maps) would
74
+ * otherwise pay a full node scan on every write. null = no trap writes
75
+ * this batch (bulk paths fall back to the full scan); WK_ALL = bound
76
+ * unusable (array length write). LOAD-BEARING SHAPE RULE: array proxy
77
+ * targets carry their fields as named properties on a real array, and V8
78
+ * normalizes an array to dictionary properties as the named count grows
79
+ * (empirically at counts ≡ 0 mod 3 from 18 up on V8 13.x) — every trap
80
+ * field read then becomes a hash lookup (~15% uibench, tree suites
81
+ * worst). Future write-side state MUST ride an extension object, not new
82
+ * named fields. */
83
+ wk: Set<PropertyKey> | null;
114
84
  /** Lazy deep-witness node: `deep()` subscribes ONE node per record instead
115
85
  * of one per path; write paths bump it only when it exists. Separate from
116
86
  * `k` so $TRACK/mapArray never rerun on leaf value changes (R9). */
@@ -130,8 +100,12 @@ export interface StoreNextTarget {
130
100
  /** Accessor scan performed (scan-once on first trap read; adopted data is
131
101
  * not rescanned — legacy-parity behavior). */
132
102
  sc: boolean;
133
- /** Backing was swapped by adoption this batch (fold diff-notifies it). */
134
- adopted: boolean;
103
+ /** Adoption diff base, non-null when the backing was swapped by adoption
104
+ * this batch: the view the nodes were LAST TOLD — the pre-batch committed
105
+ * backing, or the draft's pending backing when a draft preceded the
106
+ * adoption (its setter-exit notifications already moved the nodes, #3296).
107
+ * The deferred fold diffs incoming against this, never against committed. */
108
+ ab: Record<PropertyKey, any> | null;
135
109
  /** Pending backing is a prototype-chain OVERLAY of the committed backing
136
110
  * (`Object.create(v)` — own keys are this batch's writes, everything else
137
111
  * reads through). O(written) per flush instead of O(container) clones
@@ -1,7 +1,7 @@
1
1
  import { type Signal } from "../core/index.js";
2
2
  import type { Refreshable } from "../core/index.js";
3
- /** A read-only view of a store's value as seen by consumers. Mutate it via the paired `StoreSetter`. */
4
- export type Store<T> = Readonly<T>;
3
+ /** A reactive view of a store's value. Update it through the paired `StoreSetter`. */
4
+ export type Store<T> = T;
5
5
  /**
6
6
  * A store setter. The callback receives a writable **draft** of the store.
7
7
  *
@@ -14,25 +14,30 @@ export type Store<T> = Readonly<T>;
14
14
  *
15
15
  * The setter does **not** perform keyed reconciliation. If you need surviving
16
16
  * items to keep their store identity across full-array replacement, use the
17
- * projection form — `createStore(fn, seed, { key })` or `createProjection` —
18
- * whose derive function reconciles its return by `options.key`.
17
+ * projection form — `createStore(fn, seed, { key })` or
18
+ * `createProjection(fn, seed, { key })` — whose derive function reconciles
19
+ * its return by `options.key`.
19
20
  */
20
21
  export type StoreSetter<T> = (fn: (state: T) => T | void) => void;
21
- /** Tuple returned by the plain `createStore(initialValue)` form. */
22
+ /** Tuple returned by the plain `createStore(initialValue, options?)` form. */
22
23
  export type StoreReturn<T> = [get: Store<T>, set: StoreSetter<T>];
23
24
  /** Tuple returned by the derived `createStore(fn, seed, options?)` form. */
24
25
  export type ProjectionStoreReturn<T> = [get: Refreshable<Store<T>>, set: StoreSetter<T>];
25
- /** Base options for store primitives. */
26
+ /** Options shared by all store primitives. */
26
27
  export interface StoreOptions {
27
28
  /** Debug name (dev mode only) */
28
29
  name?: string;
30
+ /** Single-layer store: root keys reactive, values raw records replaced by reference */
31
+ shallow?: boolean;
29
32
  }
30
- /** Options for derived/projected stores created with `createStore(fn)`, `createProjection`, or `createOptimisticStore(fn)`. */
33
+ /**
34
+ * Options for derived/projected stores created with
35
+ * `createStore(fn, seed, options?)`, `createProjection(fn, seed, options?)`,
36
+ * or `createOptimisticStore(fn, seed, options?)`.
37
+ */
31
38
  export interface ProjectionOptions extends StoreOptions {
32
39
  /** Key property name or function for reconciliation identity; `null` merges positionally */
33
40
  key?: string | ((item: NonNullable<any>) => any) | null;
34
- /** Single-layer store: root keys reactive, values raw records replaced by reference */
35
- shallow?: boolean;
36
41
  /**
37
42
  * Treat the seed as commit #0: the store is born committed with the seed's
38
43
  * contents, shown until the derive's first real answer lands. While that
@@ -1,3 +1,4 @@
1
+ import type { Transition } from "./scheduler.cjs";
1
2
  import type { Computed, Signal } from "./types.cjs";
2
3
  /**
3
4
  * Dev-only observability hook points for the reactive core.
@@ -58,6 +59,69 @@ export interface AttributionHooks {
58
59
  * from the node's state against its asyncStart snapshot.
59
60
  */
60
61
  asyncEnd(el: Computed<any>, prev: unknown, value: unknown, direct: boolean): void;
62
+ /**
63
+ * An effect's imperative half (its effect callback) is about to run /
64
+ * has run. Both fire outside the run's try; `effectRunEnd` fires whether
65
+ * or not the callback threw. Writes between the two are the effect's.
66
+ */
67
+ effectRunStart(el: Computed<any>): void;
68
+ effectRunEnd(el: Computed<any>): void;
69
+ /**
70
+ * One synchronous step of an `action()` generator is about to run / has
71
+ * run (`it.next()`/`it.throw()` up to the next yield). `it` is the
72
+ * invocation's iterator — stable identity across its steps; `name` the
73
+ * generator function's name. Writes between the two are the action's.
74
+ */
75
+ actionStepStart(it: object, name: string | undefined): void;
76
+ actionStepEnd(it: object): void;
77
+ /**
78
+ * A flush found `t` incomplete (transitionComplete's false verdict): its
79
+ * writes stay staged and its queues are about to be parked. Fired BEFORE
80
+ * this flush's lane effects (the visible acknowledgers — isPending
81
+ * companions, optimistic values) run; `holdEnd` fires from the root
82
+ * stashQueues call after them, so effect runs between the two are runs that
83
+ * painted *during* the hold.
84
+ */
85
+ holdStart(t: Transition): void;
86
+ holdEnd(): void;
87
+ /**
88
+ * `t` was judged complete (transitionComplete's true verdict, before `_done`
89
+ * flips). Fired before its held writes commit, so `t._pendingNodes` still
90
+ * lists what was staged.
91
+ */
92
+ transitionSettled(t: Transition): void;
93
+ /** `outgoing` was folded into `target` (`outgoing._done = target`). */
94
+ transitionMerged(target: Transition, outgoing: Transition): void;
95
+ /**
96
+ * A store setter batch replaced the container at `path` (e.g. `store.user`)
97
+ * with a different one (both non-null, same array-ness, not the same
98
+ * logical slot), and this is the leaf census of the new container against
99
+ * the old: `total` leaves (own keys, or items) in the new one, `unchanged`
100
+ * of which are the same value as before (identity, judged on unwrapped
101
+ * values — object keys compared by key, array items by membership), and
102
+ * `prevTotal` leaves in the old one. Containers above 64 leaves are not
103
+ * announced. Fired per written key from the write channel's notify. The
104
+ * engine decides whether the replacement was a spread-copy worth a
105
+ * diagnostic.
106
+ */
107
+ storeReplaced(path: string, isArray: boolean, total: number, unchanged: number, prevTotal: number): void;
108
+ /**
109
+ * A `mapArray` update both disposed and created rows: `removed` are the
110
+ * items whose rows were disposed, `created` the items that got new rows,
111
+ * `newLen` the list's new length, `keyed` whether a key function is in use
112
+ * (false = identity or by-index). Fired after commit. The engine judges
113
+ * whether the churn replaced equivalent records (unstable identity).
114
+ */
115
+ listChurn(el: Computed<any>, removed: unknown[], created: unknown[], newLen: number, keyed: boolean): void;
116
+ /**
117
+ * A loading boundary started (`shown` true) or stopped showing its
118
+ * fallback. `boundary` is the boundary's queue (stable identity); `tree`
119
+ * its bound subtree computed when already constructed — the first show can
120
+ * fire while the subtree is still being built — whose owner chain names
121
+ * the boundary. Fired at the source-set transitions (first pending source
122
+ * registers / last one clears), not per flush.
123
+ */
124
+ boundaryFallback(boundary: object, tree: Computed<any> | undefined, shown: boolean): void;
61
125
  }
62
126
  export declare let attrHooks: AttributionHooks | null;
63
127
  export declare function setAttributionHooks(hooks: AttributionHooks | null): void;
@@ -22,6 +22,45 @@ import type { Computed } from "./types.cjs";
22
22
  * front-ends.
23
23
  */
24
24
  export type ChangeKind = "write" | "derived" | "async" | "refresh";
25
+ /**
26
+ * Provenance of a root change: the imperative frame that performed it.
27
+ *
28
+ * - `interaction` — a user event handler (the web runtime marks dispatch via
29
+ * `withInteraction`). `name` is the event type, `target` the element hit
30
+ * (`button#next "Next →"`), `at` the dispatch time on the `performance.now()`
31
+ * clock — the base every feedback-latency number is measured from.
32
+ * - `effect` — an effect callback (`name` = the effect's name; `run` = the
33
+ * compute run whose effect phase performed the write, when that run was
34
+ * recorded — so a write can be joined to the re-run that produced it).
35
+ * - `action` — a step of an `action()` generator (`name` = the generator's
36
+ * name, when it has one). Writes after an `await` (not a `yield`) run in a
37
+ * bare microtask and stamp `external` — the documented escape.
38
+ * - `async` — an async landing (`name` = the node whose flight landed).
39
+ * - `external` — none of the above: timers, sockets, promise callbacks, setup.
40
+ *
41
+ * `interaction` on a non-interaction frame is the user event the frame runs
42
+ * under — an action started by a click, an effect whose run was caused by a
43
+ * click's write, a landing whose flight a click started. It is what lets
44
+ * every downstream cost be keyed by the interaction that paid for it.
45
+ */
46
+ export interface ChangeOrigin {
47
+ kind: "interaction" | "effect" | "action" | "async" | "external";
48
+ name?: string;
49
+ target?: string;
50
+ at?: number;
51
+ interaction?: ChangeOrigin;
52
+ /** `effect` only: the `RerunEvent.run` of the compute run this callback belongs to. */
53
+ run?: number;
54
+ }
55
+ /** A user interaction, as the web runtime describes it to `withInteraction`. */
56
+ export interface InteractionRef {
57
+ /** Event type — `click`, `keydown`, `input`… */
58
+ type: string;
59
+ /** The element hit, e.g. `button#next "Next →"`. */
60
+ target?: string;
61
+ /** Dispatch time on the `performance.now()` clock; defaults to now. */
62
+ at?: number;
63
+ }
25
64
  export interface ChangeRecord {
26
65
  /** Global monotonic change sequence — orders causes across the app. */
27
66
  seq: number;
@@ -34,6 +73,10 @@ export interface ChangeRecord {
34
73
  stack?: string[];
35
74
  /** For derived changes: the upstream changes that produced this one. */
36
75
  causes?: ChangeRecord[];
76
+ /** Root changes only: who performed the write. */
77
+ origin?: ChangeOrigin;
78
+ /** Root changes only: when the write was stamped (`performance.now()` clock). */
79
+ at?: number;
37
80
  }
38
81
  export interface RerunEvent {
39
82
  /** Global monotonic run sequence. */
@@ -74,19 +117,21 @@ export interface RerunEvent {
74
117
  changed: boolean;
75
118
  /**
76
119
  * Which posture this run executed under. "optimistic" = under an
77
- * optimistic lane (overlay recompute); "transition" = a transition was
78
- * active or owns the node (the run may be replayed/settled later);
79
- * "plain" = an ordinary committed run. Overlay runs are real work (they
80
- * count toward time budgets) but are never blamed as waste, and costs()
81
- * reports their time separately as `overlayMs`.
120
+ * optimistic lane (overlay recompute); "held" = a hold was open or owns
121
+ * the node (the run may be replayed/settled later); "plain" = an ordinary
122
+ * committed run. Overlay runs are real work (they count toward time
123
+ * budgets) but are never blamed as waste, and costs() reports their time
124
+ * separately as `overlayMs`.
82
125
  */
83
- phase: "plain" | "transition" | "optimistic";
126
+ phase: "plain" | "held" | "optimistic";
84
127
  /**
85
- * The changed value was held in `_pendingValue` (a transition hold) rather
86
- * than committed directly; its reveal happens on the transition's own
87
- * schedule. Held runs are excluded from waste accounting.
128
+ * The changed value was parked in `_pendingValue` (held) rather than
129
+ * committed directly; its reveal happens on the hold's own schedule. Held
130
+ * runs are excluded from waste accounting.
88
131
  */
89
132
  held: boolean;
133
+ /** The user interaction this run traces back to through its causes, if any. */
134
+ interaction?: ChangeOrigin;
90
135
  }
91
136
  export interface AttributionOptions {
92
137
  /** Pretty-print each re-run to the console (default true). */
@@ -161,6 +206,42 @@ export interface AttributionOptions {
161
206
  waterfalls?: {
162
207
  minFlightMs: number;
163
208
  } | false;
209
+ /**
210
+ * Silent-hold warning: emit a diagnostic when a user's writes were held
211
+ * behind async work for at least `infoMs` (default 100ms — RAIL's "feels
212
+ * instant" ceiling) and the screen never acknowledged the wait — no
213
+ * `isPending()`/`latest()` reader downstream of the held writes or their
214
+ * blockers, no optimistic value, no `affects()` mark, and no lane effect
215
+ * painted while held. Below `warnMs`
216
+ * (default 200ms — the INP "good" ceiling) the event is advisory
217
+ * (structured channel only); at or above it the console gets the finding.
218
+ * The engine measures to the commit, not the paint, so every number is a
219
+ * floor on what the user saw; the thresholds sit at the strict end of the
220
+ * band on purpose. Holds that staged no root write (initial loads, bare
221
+ * `refresh()`) are never judged: nothing the user did went unanswered.
222
+ * `false` disables hold tracking altogether (`longHolds` included).
223
+ */
224
+ holds?: {
225
+ infoMs: number;
226
+ warnMs: number;
227
+ } | false;
228
+ /**
229
+ * Long-hold warning: emit a diagnostic when a hold's quiescent tail — the
230
+ * time from the LAST write to join it until it committed — reached
231
+ * `infoMs` (default 500ms), `warn` from `warnMs` (default 1000ms, where
232
+ * RAIL says the user loses the thread). Measured from the last join so a
233
+ * hold that keeps taking input (typing) is judged by each wait, not by its
234
+ * lifetime. A hold this long is past what a stale screen should carry,
235
+ * acknowledged or not: the honest UI is a fallback, which a `Loading`
236
+ * boundary gives only when it has not revealed yet or its `on` prop
237
+ * changed. Reported as LONG_HOLD when the hold was acknowledged; a silent
238
+ * long hold stays one SILENT_HOLD with the boundary repair appended.
239
+ * `false` disables.
240
+ */
241
+ longHolds?: {
242
+ infoMs: number;
243
+ warnMs: number;
244
+ } | false;
164
245
  }
165
246
  export interface ScopeCost {
166
247
  name: string;
@@ -185,6 +266,15 @@ export interface WriteCost {
185
266
  /** Summed self-time of every downstream re-run it caused. */
186
267
  downstreamMs: number;
187
268
  }
269
+ /**
270
+ * Run `fn` as the handler of a user interaction: every root write it performs
271
+ * (and every action step or effect the write causes) carries the interaction
272
+ * as provenance. The web runtime wraps event dispatch in this; it is dev-only
273
+ * and safe to call with no engine enabled.
274
+ */
275
+ export declare function withInteraction<T>(ref: InteractionRef, fn: () => T): T;
276
+ /** `click on button#next "Next →"`, `effect "syncTitle"`, `action "save"`, … */
277
+ export declare function formatOrigin(origin: ChangeOrigin): string;
188
278
  export declare function formatRerun(event: RerunEvent): string;
189
279
  export interface Attribution {
190
280
  enable(opts?: AttributionOptions): void;
@@ -211,6 +301,26 @@ export interface Attribution {
211
301
  * thresholded view of the same data.
212
302
  */
213
303
  waterfalls(): readonly WaterfallRecord[];
304
+ /**
305
+ * Every settled transition hold that staged at least one root write since
306
+ * enable() (ring-buffered like history()). Facts, not verdicts: recorded
307
+ * regardless of duration or acknowledgment — the SILENT_HOLD diagnostic is
308
+ * the thresholded, unacknowledged subset.
309
+ */
310
+ holds(): readonly HoldEvent[];
311
+ /**
312
+ * What the user waited on, folded from holds() and the interaction on each
313
+ * re-run: `sources` ranks async sources by the silent time writes spent
314
+ * held behind them (with which affordances answered, how often, and which
315
+ * interactions were held); `interactions` ranks user events by the total
316
+ * time they cost — re-run work caused (long-flush hazard) beside time held
317
+ * (silent-hold hazard). Facts at every duration; SILENT_HOLD is the
318
+ * thresholded verdict. Two more tables round out the picture: `flights`
319
+ * counts each async source's flights and how many were abandoned before
320
+ * landing (the re-ask storm), and `fallbacks` measures how long each
321
+ * loading boundary showed its fallback and how often that was a flash.
322
+ */
323
+ feedback(): AttributionFeedbackTables;
214
324
  /**
215
325
  * Cooperative preload declaration: stamp a flight object (promise or async
216
326
  * iterable) with its true kickoff time BEFORE the reactive graph sees it.
@@ -223,7 +333,15 @@ export interface Attribution {
223
333
  * later enable()). Dev-only, like the whole DEV surface.
224
334
  */
225
335
  markFlight(flight: object, startedAt?: number): void;
336
+ /**
337
+ * Run `fn` as a user interaction's handler: root writes inside stamp it as
338
+ * their origin, and actions/effects/flights it causes carry it. The web
339
+ * runtime wraps every event dispatch in this; custom renderers and test
340
+ * harnesses call it themselves. Callable while attribution is disabled.
341
+ */
342
+ withInteraction: typeof withInteraction;
226
343
  format: typeof formatRerun;
344
+ formatOrigin: typeof formatOrigin;
227
345
  }
228
346
  /** One landed flight: its node name, wall duration, and upstream chain. */
229
347
  export interface FlightLink {
@@ -236,4 +354,142 @@ export interface WaterfallRecord {
236
354
  /** Summed wall time of the chain — the serialized cost. */
237
355
  sequentialMs: number;
238
356
  }
357
+ export interface HeldWrite {
358
+ name: string;
359
+ prev?: string;
360
+ value?: string;
361
+ origin?: ChangeOrigin;
362
+ }
363
+ export interface HoldEvent {
364
+ /**
365
+ * Wall time the user waited: from the interaction that performed the held
366
+ * writes when one is known (`interaction.at`) or the first flush that
367
+ * parked them, whichever is earlier, to the commit.
368
+ */
369
+ holdMs: number;
370
+ /**
371
+ * The quiescent tail: from the LAST held write to join (the user's final
372
+ * input) to the commit. Equal to `holdMs` for a single write; shorter when
373
+ * the hold kept taking input. The LONG_HOLD measure.
374
+ */
375
+ tailMs: number;
376
+ /** The user interaction whose writes were held, when the stamp is known. */
377
+ interaction?: ChangeOrigin;
378
+ /** Flushes that ended with the hold still open. */
379
+ flushes: number;
380
+ /** Root signal writes staged behind the hold (the user's unanswered input). */
381
+ heldWrites: HeldWrite[];
382
+ /** Async nodes the hold waited on (union across its parked flushes). */
383
+ blockers: string[];
384
+ /**
385
+ * Feedback the graph provably rendered for this hold, as `"<kind>:<node>"`
386
+ * — `isPending:posts`, `latest:page`, `optimistic:todos`, `affects:list`.
387
+ * Empty and `paintedDuringHold === 0` is the SILENT_HOLD signature.
388
+ */
389
+ acknowledgedBy: string[];
390
+ /**
391
+ * Effect callbacks that ran inside the hold's parked flushes. Mainline
392
+ * effects are stashed while a hold is open, so these are lane effects —
393
+ * readers of optimistic values and of `isPending()`/`latest()` companions,
394
+ * i.e. the screen changing in response to the hold. An unrelated effect
395
+ * cannot land here: it waits with everything else.
396
+ */
397
+ paintedDuringHold: number;
398
+ /** The hold was opened (or joined) by an `action()`. */
399
+ action: boolean;
400
+ }
401
+ export interface FeedbackSource {
402
+ /** The async nodes the holds waited on; empty when an action alone kept them open. */
403
+ sources: string[];
404
+ holds: number;
405
+ /** Summed wait across the holds (ms). */
406
+ heldMs: number;
407
+ worstMs: number;
408
+ /** Holds with no acknowledgment at all — the SILENT_HOLD signature, at any duration. */
409
+ silent: number;
410
+ silentMs: number;
411
+ /** Holds whose only acknowledgment was a `latest()` shadow: the input showed, nothing said "loading". */
412
+ latestOnly: number;
413
+ /**
414
+ * Holds whose quiescent tail (last write to join → commit) reached
415
+ * `longHolds.infoMs`, acknowledged or not — the LONG_HOLD signature at the
416
+ * table level. The affordance is not the whole answer there: a fallback
417
+ * (`Loading` keyed with `on`), a preload, a cache, or a faster source is.
418
+ * `longMs` sums the tails.
419
+ */
420
+ long: number;
421
+ longMs: number;
422
+ /** Which affordances answered, and in how many holds — ranked. */
423
+ acknowledgedBy: {
424
+ by: string;
425
+ holds: number;
426
+ }[];
427
+ /** Interactions whose writes were held here, ranked by holds. */
428
+ interactions: {
429
+ interaction: string;
430
+ holds: number;
431
+ }[];
432
+ /** Distinct root writes that were held. */
433
+ writes: string[];
434
+ /** Holds an action opened or joined. */
435
+ actions: number;
436
+ }
437
+ export interface FeedbackInteraction {
438
+ /** `click on button#next "Next →"` — type and target; repeated dispatches fold together. */
439
+ interaction: string;
440
+ /** Distinct dispatches seen (by dispatch time). */
441
+ dispatches: number;
442
+ /** Re-runs traced back to this interaction, and their summed self-time. */
443
+ runs: number;
444
+ selfMs: number;
445
+ /** The most re-run self-time a single dispatch caused — the long-flush hazard. */
446
+ worstDispatchMs: number;
447
+ /** Holds this interaction's writes waited in — the silent-hold hazard. */
448
+ holds: number;
449
+ heldMs: number;
450
+ silentMs: number;
451
+ worstHoldMs: number;
452
+ }
453
+ /** Per async source: how many flights it started, and how many it threw away. */
454
+ export interface FlightStats {
455
+ source: string;
456
+ /** Flights registered (a recompute that produced a new promise/iterable). */
457
+ flights: number;
458
+ /** Flights that landed (whether or not the value changed). */
459
+ landed: number;
460
+ /**
461
+ * Flights superseded by a newer one before landing — the search-as-you-type
462
+ * signature when large: every keystroke asked, most answers were discarded.
463
+ * A debounced/equality-gated derivation between input and fetch is the repair.
464
+ */
465
+ abandoned: number;
466
+ /** Summed and worst wall time of landed flights (ms). */
467
+ landedMs: number;
468
+ worstMs: number;
469
+ }
470
+ /** Per loading boundary: how long, and how briefly, it showed its fallback. */
471
+ export interface FallbackStats {
472
+ /** The boundary's owner path (`<App> › <Feed>`), or `boundary` when unnamed. */
473
+ boundary: string;
474
+ /** Times the fallback was shown. */
475
+ shows: number;
476
+ /** Summed and worst fallback duration (ms) across completed shows. */
477
+ shownMs: number;
478
+ worstMs: number;
479
+ /**
480
+ * Shows shorter than the flash window (default 150ms): a spinner that
481
+ * appeared and vanished — the other end of the SILENT_HOLD spectrum, too
482
+ * much feedback for too little wait. A preload, a cache, or lifting the
483
+ * fetch above the boundary removes the flash.
484
+ */
485
+ flashes: number;
486
+ }
487
+ export interface AttributionFeedbackTables {
488
+ sources: FeedbackSource[];
489
+ interactions: FeedbackInteraction[];
490
+ /** Async sources ranked by abandoned flights, then by flights. */
491
+ flights: FlightStats[];
492
+ /** Loading boundaries ranked by flashes, then by time shown. */
493
+ fallbacks: FallbackStats[];
494
+ }
239
495
  export declare const attribution: Attribution;