@solidjs/signals 2.0.0-beta.20 → 2.0.0-beta.22

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.
@@ -1,12 +1,12 @@
1
- import { addPendingSource, setPendingError, settlePendingSource, forEachDependent, notifyStatus } from "./core/async.js";
1
+ import { forEachDependent } from "./core/async.js";
2
2
 
3
- import { STATUS_PENDING, $REFRESH } from "./core/constants.js";
3
+ import { $REFRESH, STATUS_PENDING } from "./core/constants.js";
4
4
 
5
5
  import { NotReadyError } from "./core/error.js";
6
6
 
7
7
  import "./core/core.js";
8
8
 
9
- import { GlobalQueue, shiftAffectsMarks, globalQueue, schedule } from "./core/scheduler.js";
9
+ import { GlobalQueue, globalQueue, schedule, shiftAffectsMarks } from "./core/scheduler.js";
10
10
 
11
11
  import "./core/verdict.js";
12
12
 
@@ -17,126 +17,76 @@ import "./core/invariants.js";
17
17
  import { $TARGET, getStoreAffectsNodes } from "./store/store.js";
18
18
 
19
19
  /**
20
- * The pending-source identity of a live `affects()` mark on `node` (lazy,
21
- * one per node, shared by overlapping registrations via the refcount).
22
- *
23
- * A mark rides the SAME status rails as real in-flight async — downstream
24
- * subscribers hold the sentinel in `_pendingSources` but under its own
25
- * identity so the two channels can't clear each other:
26
- * - `_reask` is permanently `false`: a mark is by definition a declared
27
- * value change, so `quietPending` never silences a window it participates
28
- * in — even when the mark rides over an otherwise-quiet `refresh()`
29
- * re-ask of the same node (the whole point of declaring one).
30
- * - A landing on the marked node settles only the node's OWN source entry;
31
- * the sentinel entry survives until the mark's transaction releases it.
32
- * - The sentinel itself never carries `STATUS_PENDING`, so
33
- * `transitionComplete` never counts a mark as a blocker of its own
34
- * transaction (release happens AT settle — self-blocking would deadlock),
35
- * and reads of the marked node never throw (marks are value-transparent
36
- * at the source; pendingness is what propagates).
37
- */ function getAffectsSentinel(e) {
38
- return e.t ??= {
39
- o: undefined,
40
- // Brand + backref: lets the scheduler's settlement checks recognize
41
- // mark-sourced pending (which must never block its own transaction —
42
- // release happens AT settle).
43
- l: e,
44
- u: 0,
45
- i: 0,
46
- A: false,
47
- k: undefined,
48
- p: null,
49
- S: null
50
- };
51
- }
52
-
53
- /**
54
- * Push a live mark's pendingness downstream from the marked node through the
55
- * normal status rails. Runs on every registration (dedup in `notifyStatus`
56
- * stops re-descent at already-covered subscribers). Subscribers that
57
- * recompute mid-window shed this via `clearStatus` and re-acquire it through
58
- * the read path (`applyAffectsReads` for direct readers of the marked node,
59
- * `collectMarkSources` transitively) — the mark analogue of real async's
60
- * re-throw-on-read. Subscribers are deliberately NOT queued as pending nodes
61
- * (#2893): they hold no value needing a transition-scheduled commit, and
62
- * queueing would stamp the marking action's transaction onto them — from then
63
- * on ANY write dirtying one of them (including writes to unmarked signals
64
- * that merely share a downstream memo) would be captured and frozen until the
65
- * action settles.
66
- */ function propagateAffectsMark(e) {
67
- if (!e.p && !e.G) return;
68
- const t = getAffectsSentinel(e);
69
- const r = new NotReadyError(t);
70
- forEachDependent(e, e => {
71
- if (!e.m?.has(t)) {
72
- notifyStatus(e, STATUS_PENDING, r);
73
- }
74
- });
20
+ * The counting half of a mark, shared by direct registration and store-scope
21
+ * inheritance (a node created inside a live keyless mark's identity scope).
22
+ * A mark is ONLY this count: coverage of everything derived from the node is
23
+ * pull-derived by the verdict layer's `markWalk` (dep-graph reachability), so
24
+ * nothing is stored downstream and nothing can be stranded or stripped by
25
+ * mid-window recomputes.
26
+ */ function markAffects(e) {
27
+ e.t = (e.t || 0) + 1;
28
+ shiftAffectsMarks(1);
75
29
  }
76
30
 
77
31
  /**
78
- * Re-establish mark pendingness on a computed that read marked sources
79
- * during its recompute (`clearStatus` at the top of the commit path wiped
80
- * any sentinel entries it held). Called by `recompute` after the commit —
81
- * not before, because setting `_error` earlier would make the commit path
82
- * treat the node as errored and skip the value write.
83
- */ function applyAffectsReads(e, t) {
84
- let r = false;
85
- for (let f = 0; f < t.length; f++) {
86
- const o = t[f];
87
- if (!o.M) continue;
88
- const s = getAffectsSentinel(o);
89
- if (addPendingSource(e, s)) {
90
- e.i |= STATUS_PENDING;
91
- setPendingError(e, s);
92
- r = true;
32
+ * Boundary visual channel: within a transaction, a live mark holds Loading
33
+ * fallbacks / reveal ordering the way real in-flight async would — but the
34
+ * notification is tagged visibility-only at the source (`_markVisual`), so
35
+ * the root queue never registers reporters from it: marks are invisible to
36
+ * ALL completion and settlement accounting by construction. Boundaries hold
37
+ * the marked node in `_sources` while `_affectsCount` is live; the release
38
+ * sweep (finalizePureQueue re-checks boundary children after mark release)
39
+ * is the display-state update point. Ambient marks release inside the same
40
+ * flush that would surface them, before effects run — verdict-only, netting
41
+ * no visual change.
42
+ */ function notifyMarkBoundaries(e) {
43
+ if (!e.o && !e.u) return;
44
+ const r = new NotReadyError(e);
45
+ r.l = true;
46
+ const t = new Set;
47
+ const visit = e => {
48
+ if (t.has(e)) return;
49
+ t.add(e);
50
+ // Display consumers (render effects, boundary computeds) act on the
51
+ // notification; descent stops there, exactly like the status rails.
52
+ if (e.i) {
53
+ e.i(STATUS_PENDING, r);
54
+ return;
93
55
  }
94
- }
95
- if (r && GlobalQueue.P !== null) GlobalQueue.P(e);
96
- }
97
-
98
- /**
99
- * The counting half of a mark, shared by direct registration and store-scope
100
- * inheritance (a node created inside a live keyless mark's identity scope):
101
- * bumps the refcount and pokes the node's verdict companions so an
102
- * already-materialized `false` flips reactively.
103
- */ function markAffects(e) {
104
- e.M = (e.M || 0) + 1;
105
- shiftAffectsMarks(1);
106
- // Companions only exist once the verdict layer (isPending/latest) loaded;
107
- // without them there is no materialized verdict to flip.
108
- if (e.M === 1 && GlobalQueue.P !== null) GlobalQueue.P(e);
56
+ forEachDependent(e, visit);
57
+ };
58
+ forEachDependent(e, visit);
109
59
  }
110
60
 
111
61
  /**
112
62
  * Registers one `affects()` mark on a node: counts it, records the
113
63
  * registration with the current transaction (after initTransition the queue's
114
- * batch IS the active transition, mirroring `_optimisticNodes`), and
115
- * propagates STATUS_PENDING downstream on the status rails so everything
116
- * DERIVED from the marked data reads pending too. Propagation runs on every
117
- * registration (not just the first): subscribers gained since an earlier
118
- * overlapping registration get covered, and dedup stops re-descent early.
64
+ * batch IS the active transition, mirroring `_optimisticNodes`), re-derives
65
+ * every downstream verdict companion (the mark channel's only push — verdict
66
+ * pokes, not state), and notifies boundary display state. Both walks run on
67
+ * every registration (not just the first): subscribers gained since an
68
+ * earlier overlapping registration get covered, and dedup stops re-descent.
119
69
  */ function registerAffectsMark(e) {
120
70
  markAffects(e);
121
- globalQueue.N._.push(e);
122
- propagateAffectsMark(e);
71
+ globalQueue.m.A.push(e);
72
+ // Companions only exist once the verdict layer (isPending/latest) loaded;
73
+ // without them there is no materialized verdict to poke.
74
+ GlobalQueue.k !== null && GlobalQueue.k(e);
75
+ notifyMarkBoundaries(e);
123
76
  schedule();
124
77
  }
125
78
 
126
79
  /**
127
- * Releases one registration. When the node's last mark drops, settles the
128
- * mark's sentinel out of every downstream `_pendingSources` (waking blocked
129
- * nodes and re-deriving verdicts along the walk). Companion writes go through
130
- * the settlement snap (committed, not transition-scoped) so releasing a mark
131
- * can't open a fresh override window that would itself need settlement.
80
+ * Releases one registration. When the node's last mark drops, re-derives
81
+ * every downstream verdict through the settlement snap (committed, not
82
+ * transition-scoped release runs inside queue finalization, where a
83
+ * setSignal would open a fresh override window that nothing settles).
132
84
  */ function releaseAffectsMark(e) {
133
85
  shiftAffectsMarks(-1);
134
- e.M--;
135
- if (!e.M) {
136
- const t = e.t;
137
- if (t) settlePendingSource(e, t, true);
138
- GlobalQueue.R !== null && GlobalQueue.R(e);
139
- GlobalQueue.T?.(e);
86
+ e.t--;
87
+ if (!e.t) {
88
+ GlobalQueue.k !== null && GlobalQueue.k(e, true);
89
+ GlobalQueue.p?.(e);
140
90
  }
141
91
  }
142
92
 
@@ -144,73 +94,31 @@ import { $TARGET, getStoreAffectsNodes } from "./store/store.js";
144
94
  * Releases one batch of affects marks (a settling transaction's, or the
145
95
  * ambient batch at a plain flush end).
146
96
  */ function releaseAffectsMarks(e) {
147
- for (let t = 0; t < e.length; t++) releaseAffectsMark(e[t]);
97
+ for (let r = 0; r < e.length; r++) releaseAffectsMark(e[r]);
148
98
  e.length = 0;
149
99
  }
150
100
 
151
- /**
152
- * True when a node's pending status comes ONLY from affects() sentinels. A
153
- * mark is a promise of change, not an absence of value: reads of mark-pended
154
- * derived nodes stay value-transparent (verdicts report pending; the read
155
- * path must not suspend). Any real async source among the pending sources
156
- * keeps normal suspension semantics. Core's read path reaches this through
157
- * `GlobalQueue._onlyMarkPending`, gated on the `activeAffectsMarks` counter.
158
- */ function onlyMarkPending(e) {
159
- const t = e.m;
160
- if (t) {
161
- for (const e of t) if (!e.l) return false;
162
- return true;
163
- }
164
- return false;
165
- }
166
-
167
- /**
168
- * Collect the still-live marked nodes behind a pended owner's sentinel
169
- * sources into a recompute's `affectsReads`. This is the transitive half of
170
- * read-path re-establishment (#2893): real async re-establishes at every
171
- * derivation level because its re-throw on read re-registers the source, but
172
- * mark-pended reads are value-transparent — without this, pendingness dies on
173
- * the first mid-window recompute past depth one, and the isPending() probe
174
- * itself (whose prepare step recomputes retryable NotReady holders) strips
175
- * the very status it reports on. Reached through
176
- * `GlobalQueue._collectMarkSources`, gated on `activeAffectsMarks`.
177
- */ function collectMarkSources(e, t) {
178
- if (e.m) {
179
- for (const r of e.m) {
180
- const e = r.l;
181
- if (e && e.M) t.push(e);
182
- }
183
- }
184
- }
185
-
186
101
  // Late installation (same pattern as `GlobalQueue._update`): the mark engine
187
102
  // lives with the feature so graphs that never declare a mark never ship it.
188
- // Each call site is gated by state only this module creates (`markedReads`
189
- // collection under `activeAffectsMarks`, a non-empty `_affectsNodes` batch,
190
- // a live scope in the store's `affectsScopes`), so the hooks are installed
191
- // before the first time any of them can fire.
192
- GlobalQueue.j = applyAffectsReads;
103
+ // Each call site is gated by state only this module creates (a non-empty
104
+ // `_affectsNodes` batch, a live scope in the store's `affectsScopes`), so the
105
+ // hooks are installed before the first time any of them can fire.
106
+ GlobalQueue.G = releaseAffectsMarks;
193
107
 
194
- GlobalQueue.h = releaseAffectsMarks;
108
+ GlobalQueue.M = markAffects;
195
109
 
196
- GlobalQueue.D = markAffects;
110
+ GlobalQueue.h = releaseAffectsMark;
197
111
 
198
- GlobalQueue.F = releaseAffectsMark;
199
-
200
- GlobalQueue.$ = onlyMarkPending;
201
-
202
- GlobalQueue.I = collectMarkSources;
203
-
204
- function affects(e, t) {
205
- const r = e?.[$TARGET];
206
- if (r) {
207
- const e = getStoreAffectsNodes(r, t);
208
- for (let t = 0; t < e.length; t++) registerAffectsMark(e[t]);
112
+ function affects(e, r) {
113
+ const t = e?.[$TARGET];
114
+ if (t) {
115
+ const e = getStoreAffectsNodes(t, r);
116
+ for (let r = 0; r < e.length; r++) registerAffectsMark(e[r]);
209
117
  return;
210
118
  }
211
- const f = e?.[$REFRESH];
212
- if (f) {
213
- registerAffectsMark(f);
119
+ const o = e?.[$REFRESH];
120
+ if (o) {
121
+ registerAffectsMark(o);
214
122
  return;
215
123
  }
216
124
  }