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

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 (134) hide show
  1. package/dist/dev-shared.js +6357 -0
  2. package/dist/dev.attribution.js +2485 -0
  3. package/dist/dev.js +2851 -7027
  4. package/dist/observe/affects.js +129 -0
  5. package/dist/observe/attribution.js +7 -0
  6. package/dist/observe/boundaries.js +641 -0
  7. package/dist/observe/core/action.js +177 -0
  8. package/dist/observe/core/async.js +846 -0
  9. package/dist/observe/core/attribution-costs.js +66 -0
  10. package/dist/observe/core/attribution-feedback.js +282 -0
  11. package/dist/observe/core/attribution-hooks.js +82 -0
  12. package/dist/observe/core/attribution-queries.js +28 -0
  13. package/dist/observe/core/attribution.js +2095 -0
  14. package/dist/observe/core/constants.js +248 -0
  15. package/dist/{types-cjs/core/context.d.cts → observe/core/context.js} +37 -12
  16. package/dist/observe/core/core.js +1789 -0
  17. package/dist/observe/core/dev.js +293 -0
  18. package/dist/observe/core/effect.js +195 -0
  19. package/dist/observe/core/error-hooks.js +71 -0
  20. package/dist/observe/core/error.js +80 -0
  21. package/dist/observe/core/external.js +98 -0
  22. package/dist/observe/core/graph.js +155 -0
  23. package/dist/observe/core/heap.js +147 -0
  24. package/dist/observe/core/invariants.js +45 -0
  25. package/dist/observe/core/lanes.js +205 -0
  26. package/dist/observe/core/optimistic.js +585 -0
  27. package/dist/observe/core/owner.js +322 -0
  28. package/dist/observe/core/scheduler.js +1428 -0
  29. package/dist/observe/core/verdict.js +669 -0
  30. package/dist/observe/index.js +59 -0
  31. package/dist/observe/map.js +484 -0
  32. package/dist/{types-cjs/signals.d.cts → observe/signals.js} +311 -317
  33. package/dist/observe/store/index.js +40 -0
  34. package/dist/observe/store/next/optimistic.js +660 -0
  35. package/dist/observe/store/next/projection.js +255 -0
  36. package/dist/observe/store/next/reconcile.js +375 -0
  37. package/dist/observe/store/next/store.js +2327 -0
  38. package/dist/observe/store/next/target.js +51 -0
  39. package/dist/observe/store/store.js +325 -0
  40. package/dist/observe/store/storePath.js +103 -0
  41. package/dist/observe/store/utils.js +1023 -0
  42. package/dist/prod/attribution.js +41 -0
  43. package/dist/prod/boundaries.js +128 -76
  44. package/dist/prod/core/action.js +65 -37
  45. package/dist/prod/core/async.js +324 -172
  46. package/dist/prod/core/constants.js +57 -1
  47. package/dist/prod/core/context.js +3 -3
  48. package/dist/prod/core/core.js +1003 -364
  49. package/dist/prod/core/dev.js +17 -1
  50. package/dist/prod/core/effect.js +76 -42
  51. package/dist/prod/core/error-hooks.js +71 -0
  52. package/dist/prod/core/external.js +4 -4
  53. package/dist/prod/core/graph.js +37 -37
  54. package/dist/prod/core/heap.js +61 -56
  55. package/dist/prod/core/lanes.js +107 -63
  56. package/dist/prod/core/optimistic.js +374 -93
  57. package/dist/prod/core/owner.js +62 -46
  58. package/dist/prod/core/scheduler.js +653 -286
  59. package/dist/prod/core/verdict.js +245 -127
  60. package/dist/prod/index.js +15 -5
  61. package/dist/prod/map.js +292 -141
  62. package/dist/prod/signals.js +29 -16
  63. package/dist/prod/store/next/optimistic.js +77 -55
  64. package/dist/prod/store/next/projection.js +31 -20
  65. package/dist/prod/store/next/reconcile.js +87 -84
  66. package/dist/prod/store/next/store.js +718 -401
  67. package/dist/prod/store/next/target.js +32 -10
  68. package/dist/prod/store/store.js +12 -14
  69. package/dist/prod/store/utils.js +950 -130
  70. package/dist/types/attribution.d.ts +20 -0
  71. package/dist/types/attribution.prod.d.ts +22 -0
  72. package/dist/types/boundaries.d.ts +10 -1
  73. package/dist/types/core/action.d.ts +12 -5
  74. package/dist/types/core/async.d.ts +1 -1
  75. package/dist/types/core/attribution-costs.d.ts +35 -0
  76. package/dist/types/core/attribution-feedback.d.ts +133 -0
  77. package/dist/types/core/attribution-hooks.d.ts +147 -9
  78. package/dist/types/core/attribution-queries.d.ts +10 -0
  79. package/dist/types/core/attribution.d.ts +216 -182
  80. package/dist/types/core/constants.d.ts +56 -0
  81. package/dist/types/core/core.d.ts +190 -2
  82. package/dist/types/core/dev.d.ts +246 -28
  83. package/dist/types/core/effect.d.ts +8 -1
  84. package/dist/types/core/error-hooks.d.ts +71 -0
  85. package/dist/types/core/index.d.ts +4 -1
  86. package/dist/types/core/invariants.d.ts +4 -0
  87. package/dist/types/core/lanes.d.ts +39 -5
  88. package/dist/types/core/scheduler.d.ts +153 -0
  89. package/dist/types/core/types.d.ts +32 -9
  90. package/dist/types/index.d.ts +10 -3
  91. package/dist/types/map.d.ts +2 -0
  92. package/dist/types/signals.d.ts +8 -0
  93. package/dist/types/store/index.d.ts +2 -0
  94. package/dist/types/store/next/optimistic.d.ts +1 -1
  95. package/dist/types/store/next/store.d.ts +11 -5
  96. package/dist/types/store/next/target.d.ts +37 -10
  97. package/dist/types/store/utils.d.ts +178 -0
  98. package/package.json +21 -17
  99. package/dist/node.cjs +0 -10541
  100. package/dist/node.dev.cjs +0 -13724
  101. package/dist/types-cjs/affects.d.cts +0 -47
  102. package/dist/types-cjs/boundaries.d.cts +0 -175
  103. package/dist/types-cjs/core/action.d.cts +0 -64
  104. package/dist/types-cjs/core/async.d.cts +0 -23
  105. package/dist/types-cjs/core/attribution-hooks.d.cts +0 -127
  106. package/dist/types-cjs/core/attribution.d.cts +0 -495
  107. package/dist/types-cjs/core/constants.d.cts +0 -159
  108. package/dist/types-cjs/core/core.d.cts +0 -185
  109. package/dist/types-cjs/core/dev.d.cts +0 -136
  110. package/dist/types-cjs/core/effect.d.cts +0 -30
  111. package/dist/types-cjs/core/error.d.cts +0 -56
  112. package/dist/types-cjs/core/external.d.cts +0 -15
  113. package/dist/types-cjs/core/graph.d.cts +0 -28
  114. package/dist/types-cjs/core/heap.d.cts +0 -24
  115. package/dist/types-cjs/core/index.d.cts +0 -13
  116. package/dist/types-cjs/core/invariants.d.cts +0 -59
  117. package/dist/types-cjs/core/lanes.d.cts +0 -54
  118. package/dist/types-cjs/core/optimistic.d.cts +0 -6
  119. package/dist/types-cjs/core/owner.d.cts +0 -124
  120. package/dist/types-cjs/core/scheduler.d.cts +0 -236
  121. package/dist/types-cjs/core/types.d.cts +0 -204
  122. package/dist/types-cjs/core/verdict.d.cts +0 -2
  123. package/dist/types-cjs/index.d.cts +0 -10
  124. package/dist/types-cjs/map.d.cts +0 -69
  125. package/dist/types-cjs/package.json +0 -3
  126. package/dist/types-cjs/store/index.d.cts +0 -18
  127. package/dist/types-cjs/store/next/optimistic.d.cts +0 -25
  128. package/dist/types-cjs/store/next/projection.d.cts +0 -8
  129. package/dist/types-cjs/store/next/reconcile.d.cts +0 -10
  130. package/dist/types-cjs/store/next/store.d.cts +0 -121
  131. package/dist/types-cjs/store/next/target.d.cts +0 -166
  132. package/dist/types-cjs/store/store.d.cts +0 -143
  133. package/dist/types-cjs/store/storePath.d.cts +0 -58
  134. package/dist/types-cjs/store/utils.d.cts +0 -74
@@ -1,8 +1,10 @@
1
1
  import { type OptimisticLane } from "./lanes.js";
2
+ import type { Transition } from "./scheduler.js";
2
3
  import type { Computed, FirewallSignal, NodeExtension, NodeOptions, Owner, Signal } from "./types.js";
3
4
  export declare const PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE = "[PRIMITIVE_IN_FORBIDDEN_SCOPE] Cannot create reactive primitives inside createTrackedEffect or owner-backed onSettled";
4
5
  export declare const REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE: string;
5
6
  export declare const REACTIVE_WRITE_IN_OWNED_SCOPE_REFRESH_MESSAGE: string;
7
+ export declare const ASYNC_STORE_SETTER_MESSAGE: string;
6
8
  export declare let tracking: boolean;
7
9
  /** @internal verdict-module glue */
8
10
  export declare function setPendingCheckActive(v: boolean): void;
@@ -60,6 +62,14 @@ export declare function signal<T>(v: T, options?: NodeOptions<T>, firewall?: Com
60
62
  export declare let slotUnobservedHook: (node: Signal<any>) => void;
61
63
  /** Install the shared slot-node unobserved handler (store module, once). */
62
64
  export declare function setSlotUnobserved(fn: (node: Signal<any>) => void): void;
65
+ /** Release a firewall child the store no longer addresses (unobserved sweep
66
+ * dropped it from its target's cache): unlink it from the chain so the
67
+ * projection stops retaining it and its last value. The node keeps its own
68
+ * `_nextChild` so a walk that is mid-chain on it still terminates. It also
69
+ * leaves `_companionChildren` (#3503): the companions themselves are
70
+ * permanent on the node, but the node is unreachable through the store, so
71
+ * the set would only retain it and its last value. */
72
+ export declare function unlinkFirewallChild(node: Signal<any>): void;
63
73
  export declare function slotSignal<T>(v: T, equals: (a: T, b: T) => boolean, host: object, key: PropertyKey, acc: boolean, firewall?: Computed<unknown> | null): Signal<T>;
64
74
  export declare function optimisticSignal<T>(v: T, options?: NodeOptions<T>): Signal<T>;
65
75
  export declare function optimisticComputed<T>(fn: (prev?: T) => T | PromiseLike<T> | AsyncIterable<T>, options?: NodeOptions<T>): Computed<T>;
@@ -93,6 +103,23 @@ export declare function setStrictRead(v: string | false): string | false;
93
103
  * ```
94
104
  */
95
105
  export declare function untrack<T>(fn: () => T, strictReadLabel?: string | false): T;
106
+ /**
107
+ * Set while runtime bookkeeping evaluates a user accessor inside another
108
+ * node's pass (a loading boundary's `on` key, read from `notify` while the
109
+ * node that went pending is still recomputing). `context` is that node, but
110
+ * the read is nobody's: the value is compared, never derived from, so the
111
+ * untracked-pending re-run link (`read`, `!tracking`) must not be recorded on
112
+ * it — it made the key's source a dependency of an unrelated async memo
113
+ * (#3528: `isPending(m2)` through a memo in `on()` linked the memo into `m2`,
114
+ * a cycle that re-derived `m2` on every pending mark and never converged).
115
+ */
116
+ export declare let spectating: boolean;
117
+ /**
118
+ * Evaluates `fn` untracked and without recording a dependency for the
119
+ * untracked-pending re-run rule on the current `context`. For bookkeeping
120
+ * reads made mid-propagation on behalf of no node — see `spectating`.
121
+ */
122
+ export declare function spectate<T>(fn: () => T): T;
96
123
  /**
97
124
  * Bring a computed to a readable state: lazy/disposed nodes are (re)computed;
98
125
  * an isPending() probe (`refresh`) additionally pulls the node fully up to
@@ -128,16 +155,177 @@ export declare function notifyAuthoritativeObservers(el: Signal<any> | Computed<
128
155
  * the gating bit is only ever set by such a read, so the `!` call sites are
129
156
  * safe once every setter installs, #3303). */
130
157
  export declare function installAuthoritativeRead(): void;
158
+ /**
159
+ * Stale-reader term of the value selections below: a render effect reading a
160
+ * node some OTHER live transaction has staged sees the committed value. The
161
+ * commit is silent — the staging walk was the notification — so a reader
162
+ * that linked AFTER that walk (an effect created during the hold, a store
163
+ * key first read under it) would show the old value past the reveal: record
164
+ * it for the transaction's commit replay (the `_gatedSubs` contract lanes
165
+ * already use). An effect the transaction itself computed re-derives at its
166
+ * commit on its own (parked run, or the contested re-derive, #3322) and is
167
+ * not recorded — replaying it too would publish the frame twice.
168
+ *
169
+ * Flight twin (the pending-branch carve-out): the reader is served the
170
+ * node's committed, pre-flight value and now observes that flight — A15:
171
+ * async work observed by a reader settles as one unit with the writes that
172
+ * asked it — so it joins the transaction's reporters for the node. The
173
+ * reporter the transaction recorded when the flight started may be gone (a
174
+ * keyed remount disposed it, #3374); a completion check that found no live
175
+ * reporter committed the writes ahead of the answer, tearing the new
176
+ * reader's frame (`Count: 1` beside `Details: 0`). Joins an entry the
177
+ * transaction already holds; a flight nobody had observed yet has none (the
178
+ * reader is its first observer — a conditional that just revealed it, #3458)
179
+ * and is notified up the reader's own queue chain under that transaction,
180
+ * the one sanctioned registration site (INV-3): a collecting boundary above
181
+ * the reader consumes it as it would any pending, an unboundaried reader
182
+ * opens the entry — and the transaction, judged complete on its other
183
+ * flights, revealed the inputs beside the reader's pre-flight value
184
+ * otherwise (`Count: 1 | A: 1` beside `B: 0`). A staged signal or a settled
185
+ * node registers nothing. Every reporter dies with its reader
186
+ * (reporterBlocksSource: the read linked it as a dep). The node's own entry
187
+ * is the only one that can matter: a chain's intermediate memo is re-pulled
188
+ * by the read (updateIfNecessary's retry) and enters the transaction, so the
189
+ * reader holds through the normal path; a node with its own flight that is
190
+ * also pending on an upstream re-ask blocks through that flight until it
191
+ * lands, and its landing re-runs the reader into the normal path.
192
+ */
193
+ /** The replay half of the stale-of-foreign clause (A15 / A26): a stale reader
194
+ * served the committed value because `txn` holds what it read re-runs at
195
+ * txn's commit, when the value it was denied becomes the frame — unless its
196
+ * own last value already came from that transaction. One registration for
197
+ * the node path (heldFromStale) and the store's backing paths, which have
198
+ * no node to carry the hold (heldFromReader, the adoption hold view). */
199
+ export declare function recordStaleReplay(txn: Transition, c: Computed<any>): void;
200
+ /**
201
+ * The ownership relation (DESIGN-CONSOLIDATION §6, ruled 2026-09-17): is
202
+ * `hold` part of the running pass's world? A plain reader's world is the
203
+ * transaction it runs under, through merges. A lane reader's world is its
204
+ * lane AND the transition that owns the lane — the one asymmetry between a
205
+ * lane and a separate transaction (a lane sees what lands from its parent as
206
+ * its own; a separate transaction would wait for the parent to settle) —
207
+ * see `ownsLane` in lanes.ts, built on this. One relation for the
208
+ * stale-of-foreign clause (heldFromStale), the lane arm (readsHeldCommitted)
209
+ * and the store's backing holds (foreignHold); `serve` has no lane arm of
210
+ * its own, the lane's extra visibility lives here.
211
+ */
212
+ export declare function ownsHold(hold: Transition): boolean;
213
+ export declare function enterStagedRead(el: Signal<any> | Computed<any> | null, t?: Transition | null | undefined): void;
214
+ /**
215
+ * Rule 1 (value selection), the full arm: does this reader see a STAGED
216
+ * node's COMMITTED value? One implementation of the rule the fast paths
217
+ * (readNodeFast, read's fast block) carry as their trivial ternary and that
218
+ * every slow site — read's tail, the store's backing selection, the lane and
219
+ * verdict arms — used to restate by hand (docs/DESIGN-CONSOLIDATION.md, move 3b). In order:
220
+ * - no reader at all (an untracked read) — the committed frame;
221
+ * - a reader under an optimistic lane the engine says reads committed
222
+ * (laneReadsCommitted: another lane's hold, #3460);
223
+ * - nothing staged;
224
+ * - a children-forbidden reader (createTrackedEffect / onSettled: the frame,
225
+ * never the graph — A32);
226
+ * - a stale reader (render effect) of a FOREIGN transaction's staged write —
227
+ * committed, no entanglement (heldFromStale registers the replay; a node
228
+ * born held has no committed frame to fall back to, `noCommitted`);
229
+ * - A17 for HELD truth (#3164, CONFIG_HELD_TRUTH): staged confirming truth —
230
+ * fold-staged onto an armed family, or entangle-stolen by an awaited
231
+ * until() — is masked from ordinary readers until its transaction's
232
+ * reveal, the retaining transaction's own speculative recomputes included
233
+ * (partial override coverage would otherwise compose override + staged
234
+ * truth into a state no timeline contains). Authoritative readers
235
+ * (until()'s predicate) and latest() see the staged truth — the tunnel that
236
+ * keeps the hold deadlock-free.
237
+ * False means the reader derives from the staged value and enters its
238
+ * transaction (enterStagedRead, A29).
239
+ */
240
+ export declare function readerSeesCommitted(el: Signal<any> | Computed<any>, c: Computed<any> | null, owner: Signal<any> | Computed<any>, noCommitted: boolean): boolean;
241
+ /** A28 — set when a node is staged (queuePendingNode) or a held node rewritten
242
+ * (stashHeldRewrite) OUTSIDE a flush; cleared when the next flush begins. The
243
+ * read sites test this one module boolean instead of `globalQueue._running`:
244
+ * inside a flush it is false and the A28 arm costs nothing; outside, only a
245
+ * tick with unflushed writes pays the staged-node check. */
246
+ export declare let unflushedStaged: boolean;
247
+ export declare function markUnflushedStaged(): void;
248
+ /** A28 — a write becomes visible at flush. Outside a flush, a node holding an
249
+ * AMBIENT staged value (no transaction stamp) was written since the last
250
+ * flush: ambient staging commits at flush end, so nothing else leaves a node
251
+ * in this state; a stamped value is the flushed held world, which A28 says
252
+ * latest() serves. Inside a flush the rule does not apply (A28 (4): promoted
253
+ * within the round). Structural — no marker on the write path. */
254
+ export declare function unflushed(el: Signal<any> | Computed<any>): boolean;
255
+ /** The value an unflushed node serves — the committed value for an ambient
256
+ * write, the flushed staged value for a rewrite of a held node — or
257
+ * NOT_PENDING when nothing is unflushed. Exempt: owned-write nodes (A28 (4):
258
+ * a write issued inside a recompute is promoted at that recompute's end —
259
+ * boundary and loading machinery, until()'s internals, signals declared for
260
+ * in-computation writes) and engine companions (the isPending() verdict
261
+ * signal, the latest() shadow: the system's own writes, made at the source's
262
+ * write to mirror it, installing eagerly — A28, A8). */
263
+ export declare function unflushedValue(el: Signal<any> | Computed<any>, committed?: any): unknown;
264
+ /** A28 (5): an optimistic write becomes the ACTIVE override at the flush that
265
+ * carries it. `_overrideTime` is stamped with `clock` at the write and `clock`
266
+ * advances after every flush, so "this tick, outside a flush" is unflushed. */
267
+ export declare function unflushedOverride(el: Signal<any> | Computed<any>): boolean;
268
+ /** Active optimistic override on an armed node (an armed slot idles at
269
+ * NOT_PENDING; undefined = unarmed plain node). The writer's own channels —
270
+ * the draft, `in`/keys inside the setter — compose on this regardless of
271
+ * flush state. */
272
+ export declare function hasActiveOverride(el: Signal<any> | Computed<any>): boolean;
273
+ /** The override a READER sees: installed, and carried by a flush (A28 (5) —
274
+ * an optimistic write is a write; until its flush no reader sees it). One
275
+ * implementation for read()'s override arm, the verdict channels
276
+ * (latestRead, computePendingState) and the store's selection
277
+ * (docs/DESIGN-CONSOLIDATION.md, move 3b). */
278
+ export declare function visibleOverride(el: Signal<any> | Computed<any>): boolean;
279
+ /** A derivation served the committed value because of an unflushed write
280
+ * (A28) must run again in the flush that carries it — the late-linker case
281
+ * (#3337's reason to defer the walk): it linked after the write walked. */
282
+ export declare function markLateLinker(c: Computed<any>): true;
283
+ /** Companion-bearing nodes written outside a flush (setSignal); the flush
284
+ * that carries their writes re-syncs their companions (A28). */
285
+ export declare const unflushedCompanions: Array<Signal<any> | Computed<any>>;
286
+ export declare function resyncUnflushedCompanions(): void;
131
287
  export declare function readNodeFast<T>(el: Signal<T>): T | typeof READ_SLOW;
132
288
  export declare function read<T>(el: Signal<T> | Computed<T>): T;
289
+ /**
290
+ * Rule 1, the one slow implementation (DESIGN-CONSOLIDATION move 3b, step
291
+ * 6c): the value a reader `c` (null = untracked, no pass) is served from
292
+ * `el`, whose committed value is `committed` — the node's own `_value` for
293
+ * a signal or memo, the BACKING for a store property node (single-home rule,
294
+ * O6: committed truth lives in the backing and a node's `_value` is never
295
+ * served for one). Called by read()'s slow tail and by the store's untracked
296
+ * node path (nodeValue); the fast paths (readNodeFast, read's fast block)
297
+ * keep their trivial ternary by design (perf, see the doc). Arms, in order:
298
+ * - the override (A17), routed through the engine for a tracked reader under
299
+ * a lane or a supersession (A18), an authoritative reader marked instead;
300
+ * - the lane entanglement gate (committed, recorded for replay);
301
+ * - a node born held has nothing for an untracked reader (A19 exception 1);
302
+ * - an unflushed write serves committed and re-runs the reader in the
303
+ * carrying flush (A28);
304
+ * - readerSeesCommitted, else the staged value and the transaction (A29).
305
+ */
306
+ export declare function serve(el: Signal<any> | Computed<any>, c: Computed<any> | null, owner: Signal<any> | Computed<any>, committed: unknown): unknown;
133
307
  /**
134
308
  * Store-rewrite setter guard: the rewrite parks writes in a pending backing
135
309
  * (no setSignal at write time), so the owned-scope write protection must
136
- * fire at the setter entry instead. Mirrors setSignal's guard condition
310
+ * fire at the setter entry instead. Exactly setSignal's guard condition
137
311
  * minus the node-specific exemptions (ownedWrite/firewall), which don't
138
- * apply to plain store setters.
312
+ * apply to plain store setters. Roots are NOT exempt (#3500): a root body is
313
+ * tree construction — every dev component body, every context Provider, the
314
+ * top of `render()`, and the whole SSR pass run directly under one — and a
315
+ * write there re-runs what already read the old value (or on the server,
316
+ * can't). Same rule as setSignal, which never exempted roots.
139
317
  */
140
318
  export declare function devGuardStoreSetterWrite(): void;
319
+ /**
320
+ * Store setter result guard: the callback's return has one meaning — a
321
+ * replacement root to adopt — and a thenable can never be that. It is the
322
+ * signature of `setStore(async d => …)` (or a sync arrow whose helper is
323
+ * async): only the writes before the first `await` were in the transaction;
324
+ * the rest land on a closed draft and vanish. Setters are synchronous
325
+ * transactions; async orchestration is `action()`'s job. Store-specific —
326
+ * a signal may legitimately hold a promise, so its setter has no such rule.
327
+ */
328
+ export declare function devGuardStoreSetterResult(result: unknown): void;
141
329
  export declare function setSignal<T>(el: Signal<T> | Computed<T>, v: T | ((prev: T) => T)): T;
142
330
  /**
143
331
  * Suppresses automatic recomputation of `el` until the scheduler drains. Used
@@ -1,5 +1,6 @@
1
- import { type Attribution } from "./attribution.js";
2
- import type { Computed, Link, Owner, Signal } from "./types.js";
1
+ import { type AttributionHooks, type InteractionRef, type OriginRef } from "./attribution-hooks.js";
2
+ import type { ChangeOrigin, RerunEvent } from "./attribution.js";
3
+ import type { Computed, Owner, Signal } from "./types.js";
3
4
  export interface DevHooks {
4
5
  onOwner?: (owner: Owner) => void;
5
6
  onGraph?: (value: any, owner: Owner | null) => void;
@@ -13,13 +14,19 @@ export interface DevHooks {
13
14
  * `warn`/`error` as failures unless they opt in to `info`.
14
15
  */
15
16
  export type DiagnosticSeverity = "info" | "warn" | "error";
16
- export type DiagnosticCode = "STRICT_READ_UNTRACKED" | "PENDING_ASYNC_UNTRACKED_READ" | "PENDING_ASYNC_FORBIDDEN_SCOPE" | "REACTIVE_WRITE_IN_OWNED_SCOPE" | "ACTION_CALLED_IN_OWNED_SCOPE" | "RUN_WITH_DISPOSED_OWNER" | "NO_OWNER_CLEANUP" | "CLEANUP_IN_FORBIDDEN_SCOPE" | "SETTLED_CLEANUP_UNOWNED" | "SETTLE_WALK_UNINITIALIZED_SOURCE" | "FLUSH_IN_EFFECT_CALLBACK" | "PRIMITIVE_IN_FORBIDDEN_SCOPE" | "NO_OWNER_EFFECT" | "NO_OWNER_BOUNDARY" | "ASYNC_OUTSIDE_LOADING_BOUNDARY" | "INVALID_REFRESH_TARGET" | "INVALID_AFFECTS_TARGET" | "MISSING_EFFECT_FN" | "SYNC_NODE_RECEIVED_ASYNC" | "REACTIVITY_HALTED" | "INVARIANT_VIOLATION" | "HUGE_FAN_OUT" | "HUGE_FAN_IN" | "HOT_SCOPE_RERUNS" | "HOT_SCOPE_TIME" | "WIDE_SCOPE_DEPS" | "UNSTABLE_MEMO_OUTPUT" | "WIDE_WRITE" | "ASYNC_WATERFALL" | "HOT_SCOPE_FANOUT" | "SILENT_HOLD" | "LONG_HOLD" | "EFFECT_WRITES_OWN_SOURCE" | "EFFECT_RELAY_TEAR" | "IMMUTABLE_UPDATE_IN_STORE" | "UNSTABLE_LIST_IDENTITY";
17
+ export type DiagnosticCode = "STRICT_READ_UNTRACKED" | "PENDING_ASYNC_UNTRACKED_READ" | "PENDING_ASYNC_FORBIDDEN_SCOPE" | "REACTIVE_WRITE_IN_OWNED_SCOPE" | "ASYNC_STORE_SETTER" | "ACTION_CALLED_IN_OWNED_SCOPE" | "RUN_WITH_DISPOSED_OWNER" | "NO_OWNER_CLEANUP" | "CLEANUP_IN_FORBIDDEN_SCOPE" | "SETTLED_CLEANUP_UNOWNED" | "SETTLE_WALK_UNINITIALIZED_SOURCE" | "FLUSH_IN_EFFECT_CALLBACK" | "PRIMITIVE_IN_FORBIDDEN_SCOPE" | "NO_OWNER_EFFECT" | "NO_OWNER_BOUNDARY" | "ASYNC_OUTSIDE_LOADING_BOUNDARY" | "INVALID_REFRESH_TARGET" | "INVALID_AFFECTS_TARGET" | "MISSING_EFFECT_FN" | "SYNC_NODE_RECEIVED_ASYNC" | "REACTIVITY_HALTED" | "INVARIANT_VIOLATION" | "HUGE_FAN_OUT" | "HUGE_FAN_IN" | "HOT_SCOPE_RERUNS" | "HOT_SCOPE_TIME" | "WIDE_SCOPE_DEPS" | "UNSTABLE_MEMO_OUTPUT" | "WIDE_WRITE" | "ASYNC_WATERFALL" | "HOT_SCOPE_FANOUT" | "SILENT_HOLD" | "LONG_HOLD" | "EFFECT_WRITES_OWN_SOURCE" | "EFFECT_RELAY_TEAR" | "IMMUTABLE_UPDATE_IN_STORE" | "UNSTABLE_LIST_IDENTITY" | "SSR_RENDER_ERROR_CONTAINED" | "SSR_SUBTREE_ABANDONED" | "SSR_STREAM_ABANDONED" | "SSR_CLIENT_CONTENT_MASKED" | "LATE_HEADER_WRITE" | "SERVER_FN_ERROR_SANITIZED" | "SSR_ERROR_SANITIZED" | "SERVER_WRITE" | "REVEAL_IN_RENDER_TO_STRING" | "LAZY_ASSET_UNMAPPED" | "PRELOAD_DESCRIPTOR_INVALID" | "HEAD_TAG_INVALID" | "UNRECOGNIZED_INSERT_VALUE" | "BEHAVIOR_CLAIM_DROPPED" | "FRAME_MARKER_CORRUPTED";
17
18
  export type DiagnosticKind = "strict-read" | "async" | "write" | "lifecycle" | "owner" | "error" | "perf" | "graph"
18
19
  /** Perceived responsiveness: the runtime behaved correctly but the user saw no feedback. */
19
- | "responsiveness";
20
- /** First warning when a node's live edge count reaches this size. */
20
+ | "responsiveness"
21
+ /** Server rendering: boundaries, fragments, the stream, the server-function wire. */
22
+ | "ssr"
23
+ /** The response head: `<head>` tags, preload descriptors, HTTP headers. */
24
+ | "head"
25
+ /** The renderer's insert positions, on either platform: a value it has no rendering for. */
26
+ | "render";
27
+ /** First warning when a change reaches (or a pass tracks) this many edges. */
21
28
  export declare const GRAPH_SIZE_WARN_AT = 2000;
22
- /** Repeat the warning at this interval after the first. */
29
+ /** Re-warn once the count has grown by this much since the last warning. */
23
30
  export declare const GRAPH_SIZE_WARN_EVERY = 500;
24
31
  export interface DiagnosticEvent {
25
32
  sequence: number;
@@ -51,27 +58,218 @@ export interface Diagnostics {
51
58
  subscribe(listener: DiagnosticListener): () => void;
52
59
  capture(): DiagnosticCapture;
53
60
  /**
54
- * Registers a console footer appended to the first console report of
55
- * each diagnostic code a discovery pointer to deeper guidance (e.g.
56
- * solid-js registers its shipped repair skill). Reported events carry
57
- * it as trailing lines of the same console entry; events that surface as
58
- * a thrown error instead get it as a follow-up line. Returning undefined
59
- * for an event suppresses the footer. Passing undefined unregisters and
60
- * resets the once-per-code memory.
61
+ * Records an event on the channel from outside the reactive core a host
62
+ * runtime reporting its own findings (hydration mismatches, server render
63
+ * faults) so consumers see one stream. `subject` locates it like the
64
+ * internal sites do; a host whose owners are not signals' owners passes
65
+ * `ownerPath` on the event instead and it is used as-is.
61
66
  */
62
- setConsoleFooter(footer: ((event: DiagnosticEvent) => string | undefined) | undefined): void;
67
+ emit(event: Omit<DiagnosticEvent, "sequence">, subject?: DiagnosticSubject | null): DiagnosticEvent;
68
+ }
69
+ /**
70
+ * The core's side of attribution: the hook slot an engine installs into, and
71
+ * the interaction frame the rendering runtime opens around event dispatch.
72
+ * The engine itself — "why did this run", costs, holds, feedback — is
73
+ * `@solidjs/signals/attribution`, a separate entry so an observe build pays
74
+ * for it only when something imports it.
75
+ */
76
+ export interface AttributionSlot {
77
+ /**
78
+ * Installs `hooks` as the engine the core reports facts to (`null`
79
+ * uninstalls). One engine at a time; the built-in engine's `enable()` calls
80
+ * this, and an external consumer (devtools) may install its own instead.
81
+ */
82
+ install(hooks: AttributionHooks | null): void;
83
+ /** The installed engine's hooks, or `null` when none is installed. */
84
+ readonly installed: AttributionHooks | null;
85
+ /**
86
+ * Run `fn` as a user interaction's handler: root writes inside stamp it as
87
+ * their origin, and actions/effects/flights it causes carry it. The web
88
+ * runtime wraps every event dispatch in this; custom renderers and test
89
+ * harnesses call it themselves. `fn()` when no engine is installed.
90
+ */
91
+ withInteraction<T>(ref: InteractionRef, fn: () => T): T;
92
+ /**
93
+ * Run `fn` as a declared unit of work — a router's navigation, described
94
+ * by the parametrized route it matched: root writes inside are attributed
95
+ * to it (under the enclosing interaction, if any), so the hold behind the
96
+ * route's data, the re-runs and the verdicts carry the route's name. Any
97
+ * router calls this around its location write; nothing else is
98
+ * router-specific. `fn()` when no engine is installed.
99
+ */
100
+ withOrigin<T>(ref: OriginRef, fn: () => T): T;
101
+ /**
102
+ * The provenance a root write performed now would be stamped with — the
103
+ * interaction whose handler is running, the navigation or effect or action
104
+ * frame open, or inside a recompute the origin of the change that caused
105
+ * it — as the installed engine sees it; `undefined` with no engine, or when
106
+ * nothing is in effect (external). For a runtime recording a fact of its
107
+ * own beside the engine's records: `@solidjs/web` stamps its `"call"`
108
+ * record with this, so a server-function call joins the interaction or
109
+ * navigation it ran for by the identity of the object, not by time.
110
+ */
111
+ currentOrigin(): ChangeOrigin | undefined;
112
+ }
113
+ /**
114
+ * The records the runtimes deliver on `OBSERVE.records`, by type — each
115
+ * entry `{ event, live }`: the serializable record and the live handles
116
+ * (a thrown error, a request) an in-process consumer may want beside it.
117
+ * The core emits none and declares none; the runtimes that emit declare
118
+ * theirs by augmentation, and the union of record types is whatever the
119
+ * loaded runtimes declared. `solid-js` augments THIS interface (its
120
+ * `"boundary"` record); the runtimes above it — `@solidjs/web`'s
121
+ * `"invocation"`, `"frame"` and `"call"`, a router's — augment
122
+ * `HostRecordTypes`, reached through the `solid-js` re-export, which this
123
+ * interface extends so the channel sees one catalogue.
124
+ *
125
+ * Two interfaces, one augmenter each, by design: TypeScript merges an
126
+ * augmentation into a re-exported interface by following the alias, and
127
+ * two augmentations reaching the same interface through DIFFERENT aliases
128
+ * (`"@solidjs/signals"` from solid-js, `"solid-js"` from web) merge
129
+ * order-dependently — one set is lost. So each layer augments an interface
130
+ * of its own, through one module name.
131
+ */
132
+ export interface RecordTypes extends HostRecordTypes {
133
+ }
134
+ /** The record types host runtimes declare — see `RecordTypes`. */
135
+ export interface HostRecordTypes {
136
+ }
137
+ export type RecordType = keyof RecordTypes & string;
138
+ export type RecordEvent<K extends RecordType> = RecordTypes[K] extends {
139
+ event: infer E;
140
+ } ? E : never;
141
+ export type RecordLive<K extends RecordType> = RecordTypes[K] extends {
142
+ live: infer L;
143
+ } ? L : never;
144
+ export type RecordListener<K extends RecordType> = (event: RecordEvent<K>, live: RecordLive<K>) => void;
145
+ /**
146
+ * The records channel — `OBSERVE.records`, on either platform: one place a
147
+ * consumer (an APM adapter's `init()`, devtools, the diagnostics harness)
148
+ * subscribes to the completed, serializable summaries of the things the
149
+ * runtimes did — a `<Loading>` boundary that waited on the server, a
150
+ * server-function execution or call, a frame stream produced or applied —
151
+ * each delivered synchronously the moment it is complete, with its live
152
+ * handles passed BESIDE it. Any number of listeners; none can alter what it
153
+ * observes; one that throws is reported and the rest run. (Reactive
154
+ * attribution — re-runs, holds, interactions — is the attribution engine's
155
+ * `subscribe`, a separate entry the observe build pays for only when
156
+ * imported.)
157
+ *
158
+ * The object is created once per PROCESS under a registered symbol, so a
159
+ * subscription made before the emitting runtime has loaded, or from a
160
+ * second bundled copy of the core, reaches the same listener set. Absent in
161
+ * prod with the rest of `OBSERVE`.
162
+ */
163
+ export interface Records {
164
+ /** Deliver `type` records as they complete; returns the unsubscribe. */
165
+ subscribe<K extends RecordType>(type: K, listener: RecordListener<K>): () => void;
166
+ /**
167
+ * Whether anything is subscribed to `type` — an emitter's pre-check, so
168
+ * a record nobody will hear costs nothing to not build (no clock read).
169
+ */
170
+ observed(type: RecordType): boolean;
171
+ /**
172
+ * Delivers a completed record to `type`'s listeners, synchronously: how a
173
+ * runtime publishes. Snapshot iteration — a listener unsubscribing
174
+ * mid-delivery neither skips nor double-calls anyone this round.
175
+ */
176
+ emit<K extends RecordType>(type: K, event: RecordEvent<K>, live: RecordLive<K>): void;
177
+ }
178
+ /**
179
+ * The server runtime's observe surface — where a server-side consumer
180
+ * installs what only the server has: the trace-context provider slot.
181
+ * Declared EMPTY here and typed by the runtime that owns the surface:
182
+ * `solid-js`'s server entry augments this interface with `trace:
183
+ * ServerTrace`, an interface of its own that `@solidjs/web`'s server
184
+ * entries fill in (`provide`) — so the core never learns that shape and
185
+ * the consumer still finds it on the one `OBSERVE`. One augmenter per
186
+ * interface: see `RecordTypes` for why.
187
+ *
188
+ * The OBJECT behind it is not the core's either: the core has one artifact
189
+ * per tier for both platforms, and the client would carry it for nothing.
190
+ * `solid-js`'s server entry replaces this empty literal with the
191
+ * process-wide slot the moment it evaluates (see `serverSlots` in
192
+ * solid-js/src/server/observe.ts), so a consumer that imports only
193
+ * `solid-js` can provide before the web runtime that reads it has loaded,
194
+ * and from a second copy when a host bundles one. On the client this stays
195
+ * `{}`.
196
+ */
197
+ export interface ServerObserve {
63
198
  }
199
+ /**
200
+ * The observe tier: the structured channel and the attribution wiring —
201
+ * everything a production observability consumer needs, and nothing that
202
+ * assumes a developer at a console. Present in dev and observe builds
203
+ * (`__OBSERVE__`); `undefined` in prod.
204
+ */
205
+ export interface Observe {
206
+ diagnostics: Diagnostics;
207
+ /** Completed records from the runtimes, by type — see `Records`. */
208
+ records: Records;
209
+ /** The attribution hook slot and interaction frame — see `AttributionSlot`. */
210
+ attribution: AttributionSlot;
211
+ /** The server runtime's surface — see `ServerObserve`. */
212
+ server: ServerObserve;
213
+ /**
214
+ * The live node an emitted record was about, when the emitter knew it.
215
+ * Records are serializable and never carry the node — a diagnostic event
216
+ * names its subject by `ownerPath`/`nodeName`, a re-run record by
217
+ * `nodeId` — so consumers that run in-process (devtools, the console
218
+ * reporter, `attribution.subscriptions(OBSERVE.subjectOf(run))`) look the
219
+ * node up here. Answers for `DiagnosticEvent`s and the attribution
220
+ * engine's `RerunEvent`s; `undefined` for anything else, and for a record
221
+ * that has left the process and come back.
222
+ */
223
+ subjectOf(record: DiagnosticEvent | RerunEvent): DiagnosticSubject | undefined;
224
+ /**
225
+ * Marks `owner`'s subtree as the observer's own. A consumer that renders
226
+ * inside the app it watches — an APM adapter's panel, devtools — would
227
+ * otherwise see its own effects, stores and holds reported as findings about
228
+ * the app. Under an excluded owner: diagnostics whose subject sits in the
229
+ * subtree are neither delivered nor reported (the entry is still built, so
230
+ * a site that throws its message still throws), and the attribution engine
231
+ * records no runs for its computations. Mark the root as it is created
232
+ * (`createRoot(() => { OBSERVE.exclude(getOwner()!); … })`); the signals
233
+ * and stores created under it are excluded subjects wherever their writes
234
+ * come from (a click handler, an adapter callback), so writes need no
235
+ * `runWithOwner` — and must not use one: a write under an owner is a write
236
+ * in an owned scope (REACTIVE_WRITE_IN_OWNED_SCOPE). Irrevocable for the
237
+ * owner's lifetime.
238
+ */
239
+ exclude(owner: Owner): void;
240
+ /** Whether `subject` sits under an excluded owner (itself included). */
241
+ isExcluded(subject: DiagnosticSubject | null | undefined): boolean;
242
+ }
243
+ /**
244
+ * The dev tier: devtools hooks, graph traversal, and the console face of the
245
+ * diagnostics channel. Present only in dev builds (`__DEV__`).
246
+ */
64
247
  export interface Dev {
65
248
  hooks: DevHooks;
66
- diagnostics: Diagnostics;
67
- /** "Why did this run" re-run attribution — see attribution.ts. */
68
- attribution: Attribution;
69
249
  getChildren: typeof getChildren;
70
250
  getSignals: typeof getSignals;
71
251
  getParent: typeof getParent;
72
252
  getSources: typeof getSources;
73
253
  getObservers: typeof getObservers;
254
+ /** Console face of an emitted event — see `reportDiagnostic`. */
255
+ report(entry: DiagnosticEvent): void;
256
+ /**
257
+ * Registers a console footer appended to the first console report of
258
+ * each diagnostic code — a discovery pointer to deeper guidance (e.g.
259
+ * solid-js registers its shipped repair skill). Reported events carry
260
+ * it as trailing lines of the same console entry; events that surface as
261
+ * a thrown error instead get it as a follow-up line. Returning undefined
262
+ * for an event suppresses the footer. Passing undefined unregisters and
263
+ * resets the once-per-code memory.
264
+ */
265
+ setConsoleFooter(footer: ((event: DiagnosticEvent) => string | undefined) | undefined): void;
74
266
  }
267
+ export declare const OBSERVE: Observe;
268
+ export declare function isExcluded(subject: DiagnosticSubject | null | undefined): boolean;
269
+ /** For engines that cache the verdict per node: is anything excluded at all? */
270
+ export declare function anyExcluded(): boolean;
271
+ /** Was `entry` built for an excluded subject? Once-per-key reporters must not spend their slot on it. */
272
+ export declare function isSuppressed(entry: DiagnosticEvent): boolean;
75
273
  export declare const DEV: Dev;
76
274
  /**
77
275
  * Dev-mode internal consistency check. A failure means the reactive system
@@ -96,9 +294,15 @@ export declare function ownerPath(subject: DiagnosticSubject | null | undefined)
96
294
  * default (right for the synchronous rule checks — they fire inside the
97
295
  * scope that misbehaved); pass the node for scheduler-time findings whose
98
296
  * ambient context is the flush, or `null` for events that have no location
99
- * by nature. Console output is a separate step see `reportDiagnostic`.
297
+ * by nature. An `ownerPath` already on the event wins over the subject walk
298
+ * (hosts whose owners are not signals' owners compute their own). Console
299
+ * output is a separate, dev-tier step — see `reportDiagnostic`.
100
300
  */
101
- export declare function emitDiagnostic(event: Omit<DiagnosticEvent, "sequence" | "ownerPath">, subject?: DiagnosticSubject | null | undefined): DiagnosticEvent;
301
+ export declare function emitDiagnostic(event: Omit<DiagnosticEvent, "sequence">, subject?: DiagnosticSubject | null | undefined): DiagnosticEvent;
302
+ /** Register `subject` as what `record` was about — see `Observe.subjectOf`. */
303
+ export declare function recordSubject(record: object, subject: DiagnosticSubject): void;
304
+ /** The live subject `record` was about, if its emitter registered one. */
305
+ export declare function subjectOf(record: object): DiagnosticSubject | undefined;
102
306
  /**
103
307
  * The console face of a diagnostic — ONE entry per finding: the message, the
104
308
  * owner path (`in <App> › <TodoRow> › effect`) so a human can locate it, the
@@ -107,7 +311,9 @@ export declare function emitDiagnostic(event: Omit<DiagnosticEvent, "sequence" |
107
311
  * second console argument (hover highlights it, click jumps to Elements).
108
312
  * Severity picks the console method. Call sites report the entry
109
313
  * `emitDiagnostic` returned so the structured and console channels never
110
- * disagree.
314
+ * disagree. Dev-tier: in an observe build this is a no-op, so wiring paths
315
+ * that both emit and report (graph-size warnings) reach the channel only —
316
+ * production observability never writes to the console.
111
317
  */
112
318
  export declare function reportDiagnostic(entry: DiagnosticEvent): void;
113
319
  /**
@@ -117,6 +323,11 @@ export declare function reportDiagnostic(entry: DiagnosticEvent): void;
117
323
  */
118
324
  export declare function throwPendingUntrackedRead(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence" | "data" | "ownerPath">>): never;
119
325
  export declare function warnStrictReadUntracked(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence" | "ownerPath">>): void;
326
+ /**
327
+ * Observe-tier: stamp a signal with its creating owner so `ownerPath` can
328
+ * locate signal subjects. The per-owner `_signals` list and the devtools
329
+ * `onGraph` hook are dev-tier — the observe build pays one property write.
330
+ */
120
331
  export declare function registerGraph(value: any, owner: Owner | null): void;
121
332
  export declare function clearSignals(node: Owner): void;
122
333
  export declare function getChildren(owner: Owner): Owner[];
@@ -125,12 +336,19 @@ export declare function getParent(owner: Owner): Owner | null;
125
336
  export declare function getSources(computation: Computed<any>): (Signal<any> | Computed<any>)[];
126
337
  export declare function getObservers(node: Signal<any> | Computed<any>): Computed<any>[];
127
338
  /**
128
- * DEV-only: bump live edge counts after a new graph link and warn when a
129
- * node grows an unusually large fan-out (many subscribers on one source) or
130
- * fan-in (many sources on one computation). Repeat-reads that `link()`
131
- * dedupes never reach here. Always-on in dev unlike the opt-in attribution
132
- * engine, a graph-size pathology should surface without asking.
339
+ * Observe-tier: a committed change on `node` is about to re-run `count`
340
+ * subscribers (the notify walk in `insertSubs` counted them as it went
341
+ * fan-out costs exactly one local increment in a loop that already visits
342
+ * every edge, and nothing at link time). Fires from GRAPH_SIZE_WARN_AT up,
343
+ * on the write rather than the link: a fan-out that is never written costs
344
+ * nothing, and one that is re-runs every subscriber this flush. Always-on
345
+ * wherever the channel exists — unlike the opt-in attribution engine, a
346
+ * graph-size pathology should surface without asking.
347
+ */
348
+ export declare function noteFanOut(node: Signal<any> | Computed<any>, count: number): void;
349
+ /**
350
+ * Observe-tier: a recompute pass of `node` tracked `count` distinct sources
351
+ * (its trimmed dep list, walked once at the end of the pass — see recompute;
352
+ * no per-link work, no pass bracket). Fires from GRAPH_SIZE_WARN_AT up.
133
353
  */
134
- export declare function noteGraphLink(dep: Signal<any> | Computed<any>, sub: Computed<any>): void;
135
- /** DEV-only: drop live edge counts when a link is removed. */
136
- export declare function unnoteGraphLink(link: Link): void;
354
+ export declare function noteFanIn(node: Computed<any>, count: number): void;
@@ -1,3 +1,4 @@
1
+ import { type Transition } from "./scheduler.js";
1
2
  import type { Computed, NodeOptions, Owner } from "./types.js";
2
3
  export interface Effect<T> extends Computed<T>, Owner {
3
4
  _effectFn: (val: T, prev: T | undefined) => void | (() => void);
@@ -5,7 +6,13 @@ export interface Effect<T> extends Computed<T>, Owner {
5
6
  _modified: boolean;
6
7
  _prevValue: T | undefined;
7
8
  _type: number;
8
- _boundRunEffect?: () => void;
9
+ _boundRunEffect?: (type: number) => void;
10
+ /** The transaction whose staged view produced `_value` (null = committed
11
+ * view). Effects have one value slot and do not entangle transactions, so
12
+ * a second transaction recomputing the same effect overwrites a value the
13
+ * first one still owes a run for; see the contested-effect arm of recompute
14
+ * (#3322). */
15
+ _valueTransition: Transition | null;
9
16
  }
10
17
  /**
11
18
  * Effects are the leaf nodes of our reactive graph. When their sources change, they are