@solidjs/signals 2.0.0-beta.31 → 2.0.0-beta.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev.js +250 -89
- package/dist/node.cjs +775 -637
- package/dist/prod/core/async.js +131 -62
- package/dist/prod/core/core.js +231 -194
- package/dist/prod/core/effect.js +27 -27
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +32 -32
- package/dist/prod/core/heap.js +36 -36
- package/dist/prod/core/lanes.js +14 -14
- package/dist/prod/core/optimistic.js +37 -37
- package/dist/prod/core/owner.js +53 -44
- package/dist/prod/core/scheduler.js +74 -70
- package/dist/prod/core/verdict.js +57 -46
- package/dist/prod/map.js +6 -6
- package/dist/prod/signals.js +2 -34
- package/dist/prod/store/optimistic.js +35 -30
- package/dist/prod/store/projection.js +41 -6
- package/dist/prod/store/store.js +7 -7
- package/dist/types/core/async.d.ts +9 -0
- package/dist/types/core/types.d.ts +24 -0
- package/dist/types/signals.d.ts +60 -4
- package/dist/types/store/store.d.ts +14 -0
- package/dist/types-cjs/core/async.d.cts +9 -0
- package/dist/types-cjs/core/types.d.cts +24 -0
- package/dist/types-cjs/signals.d.cts +60 -4
- package/dist/types-cjs/store/store.d.cts +14 -0
- package/package.json +1 -1
package/dist/dev.js
CHANGED
|
@@ -980,6 +980,10 @@ function commitPendingNode(n) {
|
|
|
980
980
|
// Set _modified for effects, but not for tracked effects (they handle their own scheduling)
|
|
981
981
|
if (n._type && n._type !== EFFECT_TRACKED) n._modified = true;
|
|
982
982
|
}
|
|
983
|
+
// The committed hold is the first observable answer for a loading-window
|
|
984
|
+
// node — the window closes here, not at compute time (#2990). Unconditional
|
|
985
|
+
// store to an always-present computed slot.
|
|
986
|
+
c._loading = false;
|
|
983
987
|
c._flags &= ~REACTIVE_MANUAL_WRITE;
|
|
984
988
|
if (!(c._statusFlags & STATUS_PENDING)) c._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
985
989
|
if (c._pendingFirstChild !== null || c._pendingDisposal !== null)
|
|
@@ -1327,6 +1331,10 @@ function markDisposal(el) {
|
|
|
1327
1331
|
}
|
|
1328
1332
|
}
|
|
1329
1333
|
function dispose(node) {
|
|
1334
|
+
// Leave every scheduler heap on disposal (mirrors `unobserved`): a node
|
|
1335
|
+
// still queued here would be recomputed by the next flush, and recompute()
|
|
1336
|
+
// rewriting `_flags` would clear REACTIVE_DISPOSED — resurrecting it (#2983).
|
|
1337
|
+
deleteFromHeap(node, queueFor(node));
|
|
1330
1338
|
let toRemove = node._deps;
|
|
1331
1339
|
while (toRemove !== null) {
|
|
1332
1340
|
toRemove = unlinkSubs(toRemove);
|
|
@@ -1353,9 +1361,14 @@ function disposeChildren(node, self = false, zombie) {
|
|
|
1353
1361
|
let child = zombie ? node._pendingFirstChild : node._firstChild;
|
|
1354
1362
|
while (child) {
|
|
1355
1363
|
const nextChild = child._nextSibling;
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1364
|
+
const n = child;
|
|
1365
|
+
// Heap removal must not be gated on `_deps`: a dependency-free
|
|
1366
|
+
// computation queued by refresh() has a null dep list but still sits in
|
|
1367
|
+
// the dirty heap, and left there the post-disposal flush recomputes it —
|
|
1368
|
+
// recompute() rewriting `_flags` clears REACTIVE_DISPOSED and the node
|
|
1369
|
+
// comes back to life (post-unmount runs, leaked cleanups, #2983).
|
|
1370
|
+
if (n._flags & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT)) deleteFromHeap(n, queueFor(n));
|
|
1371
|
+
if (n._deps) {
|
|
1359
1372
|
let toRemove = n._deps;
|
|
1360
1373
|
do {
|
|
1361
1374
|
toRemove = unlinkSubs(toRemove);
|
|
@@ -1736,6 +1749,34 @@ function clearPendingSources(el) {
|
|
|
1736
1749
|
el._pendingSources?.clear();
|
|
1737
1750
|
el._pendingSources = undefined;
|
|
1738
1751
|
}
|
|
1752
|
+
// A rejection-pending only resolves through the settle sweep over the
|
|
1753
|
+
// SOURCE's subscribers, so it is retryable iff a tracked read created that
|
|
1754
|
+
// edge: a dep that IS the source, or one whose own pending chain carries it
|
|
1755
|
+
// (pending sources propagate the origin node, so this covers any depth).
|
|
1756
|
+
// Dev-only caller — tree-shaken from prod builds.
|
|
1757
|
+
function retryReaches(el, source) {
|
|
1758
|
+
for (let d = el._deps; d; d = d._nextDep) {
|
|
1759
|
+
const dep = d._dep._firewall || d._dep;
|
|
1760
|
+
if (dep === source || dep._pendingSources?.has(source)) return true;
|
|
1761
|
+
}
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* A loading-window node hit an unready source (sync throw in recompute, or a
|
|
1766
|
+
* NotReadyError-rejected flight): register for the source's settle — the
|
|
1767
|
+
* settlePendingSource walk runs off `_pendingSources` + `_blocked` alone —
|
|
1768
|
+
* with NO read-visible pending status, no downstream propagation, no
|
|
1769
|
+
* transition, no lane registration. Commit #0 keeps serving.
|
|
1770
|
+
*/
|
|
1771
|
+
function parkLoadingWindow(el, e) {
|
|
1772
|
+
el._blocked = true;
|
|
1773
|
+
if (e.source) addPendingSource(el, e.source);
|
|
1774
|
+
// A settled error is the node's answer ("the error stays the answer until
|
|
1775
|
+
// this retry can actually run") — the park must not replace it: reads
|
|
1776
|
+
// throw `_error` while STATUS_ERROR is set, and overwriting it here leaks
|
|
1777
|
+
// a pending-class NotReadyError from a read-invisible park (#2989).
|
|
1778
|
+
if (!(el._statusFlags & STATUS_ERROR)) setPendingError(el, e.source, e);
|
|
1779
|
+
}
|
|
1739
1780
|
function setPendingError(el, source, error) {
|
|
1740
1781
|
if (!source) {
|
|
1741
1782
|
el._error = null;
|
|
@@ -1832,12 +1873,18 @@ function settlePendingSource(el) {
|
|
|
1832
1873
|
visited.add(node);
|
|
1833
1874
|
node._time = clock;
|
|
1834
1875
|
const remaining = node._pendingSources?.values().next().value;
|
|
1876
|
+
// STATUS_ERROR + pending sources only coexist via an errored loading
|
|
1877
|
+
// window's park (notifyStatus(STATUS_ERROR) clears pending sources
|
|
1878
|
+
// otherwise): the settled error stays the answer through the settle —
|
|
1879
|
+
// nulling it here would have reads throw `null` until the re-enqueued
|
|
1880
|
+
// retry lands, or lose it entirely if that retry parks again (#2989).
|
|
1881
|
+
const errored = node._statusFlags & STATUS_ERROR;
|
|
1835
1882
|
if (remaining) {
|
|
1836
|
-
setPendingError(node, remaining);
|
|
1883
|
+
if (!errored) setPendingError(node, remaining);
|
|
1837
1884
|
updateCompanions !== null && updateCompanions(node);
|
|
1838
1885
|
} else {
|
|
1839
1886
|
node._statusFlags &= ~STATUS_PENDING;
|
|
1840
|
-
setPendingError(node);
|
|
1887
|
+
if (!errored) setPendingError(node);
|
|
1841
1888
|
updateCompanions !== null && updateCompanions(node);
|
|
1842
1889
|
if (node._blocked) {
|
|
1843
1890
|
enqueueSub(node);
|
|
@@ -1872,6 +1919,8 @@ function handleAsync(el, result, setter) {
|
|
|
1872
1919
|
}
|
|
1873
1920
|
if (!thenable && !iterator) {
|
|
1874
1921
|
el._inFlight = null;
|
|
1922
|
+
// A sync landing is the first real answer for a loadingValue node.
|
|
1923
|
+
el._loading = false;
|
|
1875
1924
|
return result;
|
|
1876
1925
|
}
|
|
1877
1926
|
// Dev-only contract enforcement for `sync: true` nodes. In production these
|
|
@@ -1924,9 +1973,36 @@ function handleAsync(el, result, setter) {
|
|
|
1924
1973
|
};
|
|
1925
1974
|
const handleError = error => {
|
|
1926
1975
|
if (el._inFlight !== result) return;
|
|
1927
|
-
settleTransition();
|
|
1928
1976
|
// NotReadyError from rejected promises should be treated as pending, not error
|
|
1929
|
-
|
|
1977
|
+
let stillPending = error instanceof NotReadyError;
|
|
1978
|
+
// Dev-only authorship diagnostic (#2987): no edge means a post-`await`
|
|
1979
|
+
// FIRST read — untracked, so the source's settle sweep can never find
|
|
1980
|
+
// this node and "pending" wedges it (and its boundary) forever while
|
|
1981
|
+
// isPending reads false. Fail loud in dev; prod pays no bytes for the
|
|
1982
|
+
// forbidden pattern (the wedge stands there, caught during development).
|
|
1983
|
+
// Runs BEFORE the loading-window parking below: a non-retryable read is
|
|
1984
|
+
// a real error, and the window must not silently park a wedge that can
|
|
1985
|
+
// never settle.
|
|
1986
|
+
if (stillPending && !retryReaches(el, error.source)) {
|
|
1987
|
+
stillPending = false;
|
|
1988
|
+
error = new Error(
|
|
1989
|
+
"Read of an unresolved async source after an `await`. Reads inside async " +
|
|
1990
|
+
"computations only register as dependencies before the first `await`; a source " +
|
|
1991
|
+
"first read after it cannot retry when it settles. Read it before the first " +
|
|
1992
|
+
"`await` (or restructure so the value is an input)."
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
if (stillPending && el._loading) {
|
|
1996
|
+
// Loading window: the flight died waiting on an unready source. Keep
|
|
1997
|
+
// serving commit #0 — same parking as recompute's catch for sync
|
|
1998
|
+
// dependency throws. The dead flight is released so the clock-gated
|
|
1999
|
+
// error-retry pull (updateIfNecessary) can also re-ask.
|
|
2000
|
+
el._inFlight = null;
|
|
2001
|
+
parkLoadingWindow(el, error);
|
|
2002
|
+
el._time = clock;
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
settleTransition();
|
|
1930
2006
|
notifyStatus(el, stillPending ? STATUS_PENDING : STATUS_ERROR, error);
|
|
1931
2007
|
el._time = clock;
|
|
1932
2008
|
// A real error settles derivatively-pending dependents (notifyStatus
|
|
@@ -2002,6 +2078,13 @@ function handleAsync(el, result, setter) {
|
|
|
2002
2078
|
notifyStatus(el, STATUS_ERROR, e);
|
|
2003
2079
|
}
|
|
2004
2080
|
}
|
|
2081
|
+
// First real answer landing: the window closes when the answer becomes
|
|
2082
|
+
// OBSERVABLE. A direct commit is observable now; a transition-held write
|
|
2083
|
+
// (`_pendingValue` set above or inside setSignal) is not — the verdict's
|
|
2084
|
+
// held-value branch is window-gated, and commitPendingNode closes the
|
|
2085
|
+
// window when the hold commits, so no one-frame isPending pulse can leak
|
|
2086
|
+
// to live observers between the landing and its commit (#2990).
|
|
2087
|
+
if (el._pendingValue === NOT_PENDING) el._loading = false;
|
|
2005
2088
|
settlePendingSource(el);
|
|
2006
2089
|
schedule();
|
|
2007
2090
|
flush();
|
|
@@ -2054,8 +2137,16 @@ function handleAsync(el, result, setter) {
|
|
|
2054
2137
|
handleError(syncError);
|
|
2055
2138
|
throw syncError;
|
|
2056
2139
|
} else if (!resolved) {
|
|
2140
|
+
// Loading window: serve commit #0 instead of suspending. No transition
|
|
2141
|
+
// is opened — first-flight work on a loadingValue node is loading-class
|
|
2142
|
+
// (invisible to boundaries and transitions); the flight itself is
|
|
2143
|
+
// already registered in _inFlight and lands through asyncWrite.
|
|
2144
|
+
if (el._loading) return el._value;
|
|
2057
2145
|
globalQueue.initTransition(resolveTransition(el));
|
|
2058
2146
|
throw new NotReadyError(context);
|
|
2147
|
+
} else {
|
|
2148
|
+
// Synchronously-resolved promise: the first real answer landed.
|
|
2149
|
+
el._loading = false;
|
|
2059
2150
|
}
|
|
2060
2151
|
}
|
|
2061
2152
|
if (iterator) {
|
|
@@ -2083,9 +2174,22 @@ function handleAsync(el, result, setter) {
|
|
|
2083
2174
|
resolved = false,
|
|
2084
2175
|
rejected = false,
|
|
2085
2176
|
isSync = true;
|
|
2086
|
-
|
|
2177
|
+
// Protocol tolerance, matching `for await`: `await` unwraps whatever
|
|
2178
|
+
// next() returns — a thenable OR a bare IteratorResult. Real producers
|
|
2179
|
+
// use the bare form as a promise-free fast path when a value is already
|
|
2180
|
+
// buffered (seroval's deserialized streams do), so a bare result is
|
|
2181
|
+
// assimilated as an already-settled step instead of crashing on `.then`.
|
|
2182
|
+
const step = it.next();
|
|
2183
|
+
const settled = isThenable(step) ? step : { then: onSettle => void onSettle(step) };
|
|
2184
|
+
settled.then(
|
|
2087
2185
|
r => {
|
|
2088
|
-
|
|
2186
|
+
// The sync stash only serves the INITIAL drain (handleAsync's caller
|
|
2187
|
+
// consumes syncValue / throws NotReady from it). A sync-settled step
|
|
2188
|
+
// after an async gap — seroval buffering values between pulls, a
|
|
2189
|
+
// sync-thenable producer mid-stream — has no caller reading the
|
|
2190
|
+
// stash: it must write through the async path or the value is
|
|
2191
|
+
// silently dropped.
|
|
2192
|
+
if (isSync && initialRead) {
|
|
2089
2193
|
syncResult = r;
|
|
2090
2194
|
resolved = true;
|
|
2091
2195
|
if (r.done) completed = true;
|
|
@@ -2136,9 +2240,14 @@ function handleAsync(el, result, setter) {
|
|
|
2136
2240
|
// Later iterate() calls run from asyncWrite, where rethrowing would be unhandled.
|
|
2137
2241
|
initialRead = false;
|
|
2138
2242
|
if (!hadValue && !immediatelyDone) {
|
|
2243
|
+
// Loading window: serve commit #0 (see the promise branch above).
|
|
2244
|
+
if (el._loading) return el._value;
|
|
2139
2245
|
globalQueue.initTransition(resolveTransition(el));
|
|
2140
2246
|
throw new NotReadyError(context);
|
|
2141
2247
|
}
|
|
2248
|
+
// A sync first yield (or immediate empty completion) is the first real
|
|
2249
|
+
// answer; async yields clear inside asyncWrite.
|
|
2250
|
+
el._loading = false;
|
|
2142
2251
|
}
|
|
2143
2252
|
return syncValue;
|
|
2144
2253
|
}
|
|
@@ -2339,6 +2448,11 @@ function recompute(el, create = false) {
|
|
|
2339
2448
|
// Re-ask classification lives in the verdict module; capture the flag before
|
|
2340
2449
|
// the recompute wipes _flags below.
|
|
2341
2450
|
const hadReask = (el._flags & REACTIVE_REASK) !== 0;
|
|
2451
|
+
// Captured before the compute clears it on a sync landing: if that landing
|
|
2452
|
+
// is transition-held below, the window must stay open until the hold
|
|
2453
|
+
// commits (commitPendingNode) — a closed window plus a held value reads as
|
|
2454
|
+
// a pending frame to live observers of the verdict (#2990).
|
|
2455
|
+
const wasLoading = el._loading;
|
|
2342
2456
|
const oldcontext = context;
|
|
2343
2457
|
context = el;
|
|
2344
2458
|
el._depsTail = null;
|
|
@@ -2395,7 +2509,14 @@ function recompute(el, create = false) {
|
|
|
2395
2509
|
const isAsyncResult = typeof fnResult === "object" && fnResult !== null;
|
|
2396
2510
|
const inFlightChanged = el._inFlight !== prevInFlight;
|
|
2397
2511
|
value = inFlightChanged || !isAsyncResult ? fnResult : handleAsync(el, fnResult);
|
|
2398
|
-
if (!inFlightChanged && !isAsyncResult)
|
|
2512
|
+
if (!inFlightChanged && !isAsyncResult) {
|
|
2513
|
+
el._inFlight = null;
|
|
2514
|
+
// A sync (non-object) return is the first real answer; async-shaped
|
|
2515
|
+
// results clear inside handleAsync at their own landing points, and a
|
|
2516
|
+
// self-registered flight (inFlightChanged — projections) clears when
|
|
2517
|
+
// its internal handleAsync lands.
|
|
2518
|
+
el._loading = false;
|
|
2519
|
+
}
|
|
2399
2520
|
}
|
|
2400
2521
|
// On a status-free node clearStatus is a guaranteed no-op: every branch
|
|
2401
2522
|
// in its body is gated on one of these fields, and with _statusFlags === 0
|
|
@@ -2415,22 +2536,34 @@ function recompute(el, create = false) {
|
|
|
2415
2536
|
// _optimisticLane is only ever assigned by engine paths.
|
|
2416
2537
|
if (el._optimisticLane) GlobalQueue._laneAsyncSettled(el);
|
|
2417
2538
|
} catch (e) {
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
el,
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2539
|
+
const notReady = e instanceof NotReadyError;
|
|
2540
|
+
if (notReady && el._loading) {
|
|
2541
|
+
// Loading window with an unready sync dependency: register for the
|
|
2542
|
+
// source's settle (the settlePendingSource walk runs off
|
|
2543
|
+
// _pendingSources + _blocked alone) but take NO read-visible pending
|
|
2544
|
+
// status, no downstream propagation, no transition, no lane
|
|
2545
|
+
// registration — the committed loading value keeps serving. If the
|
|
2546
|
+
// node is currently errored the error stays the answer until this
|
|
2547
|
+
// retry can actually run.
|
|
2548
|
+
parkLoadingWindow(el, e);
|
|
2549
|
+
} else {
|
|
2550
|
+
// Track pending async in the lane (not the lane's source — it creates the lane
|
|
2551
|
+
// but doesn't belong to it). Set lane BEFORE notifyStatus for downstream propagation.
|
|
2552
|
+
if (notReady && currentOptimisticLane) GlobalQueue._laneAsyncPending(el);
|
|
2553
|
+
let reaskChanged = false;
|
|
2554
|
+
if (notReady) {
|
|
2555
|
+
el._blocked = true;
|
|
2556
|
+
if (GlobalQueue._applyReask !== null) reaskChanged = GlobalQueue._applyReask(el, hadReask);
|
|
2557
|
+
}
|
|
2558
|
+
notifyStatus(
|
|
2559
|
+
el,
|
|
2560
|
+
notReady ? STATUS_PENDING : STATUS_ERROR,
|
|
2561
|
+
e,
|
|
2562
|
+
undefined,
|
|
2563
|
+
notReady ? el._optimisticLane : undefined
|
|
2564
|
+
);
|
|
2565
|
+
if (reaskChanged) GlobalQueue._repollVerdicts(el);
|
|
2566
|
+
}
|
|
2434
2567
|
} finally {
|
|
2435
2568
|
tracking = prevTracking;
|
|
2436
2569
|
latestReadActive = prevLatestRead;
|
|
@@ -2492,6 +2625,9 @@ function recompute(el, create = false) {
|
|
|
2492
2625
|
}
|
|
2493
2626
|
} else {
|
|
2494
2627
|
el._pendingValue = value;
|
|
2628
|
+
// A window landing that gets held re-opens the window until the hold
|
|
2629
|
+
// commits — the verdict's held-value branch is window-gated (#2990).
|
|
2630
|
+
if (wasLoading) el._loading = true;
|
|
2495
2631
|
// Transition-held sync recompute is a write path like setSignal/asyncWrite,
|
|
2496
2632
|
// so sync derivations of held sources stay visible to isPending()/latest()
|
|
2497
2633
|
// (#2831). Both companion writes are transition-scoped (optimistic) and
|
|
@@ -2513,6 +2649,7 @@ function recompute(el, create = false) {
|
|
|
2513
2649
|
// for commit on its own transition's schedule — invisibly (A17/A18).
|
|
2514
2650
|
if (el._pendingValue === NOT_PENDING) queuePendingNode(el);
|
|
2515
2651
|
el._pendingValue = value;
|
|
2652
|
+
if (wasLoading) el._loading = true; // see the held branch above (#2990)
|
|
2516
2653
|
} else if (el._height != oldHeight) {
|
|
2517
2654
|
for (let s = el._subs; s !== null; s = s._nextSub) {
|
|
2518
2655
|
insertIntoHeapHeight(s._sub, queueFor(s._sub));
|
|
@@ -2569,6 +2706,10 @@ function updateIfNecessary(el) {
|
|
|
2569
2706
|
}
|
|
2570
2707
|
function computed(fn, options) {
|
|
2571
2708
|
const transparent = options?.transparent ?? false;
|
|
2709
|
+
// `in` (not `!== undefined`): an explicit `loadingValue: undefined` on a
|
|
2710
|
+
// `T | undefined` node is a real commit #0. The typeof guard tolerates
|
|
2711
|
+
// non-object option values that older call shapes force through `as any`.
|
|
2712
|
+
const loading = options !== null && typeof options === "object" && "loadingValue" in options;
|
|
2572
2713
|
const self = {
|
|
2573
2714
|
id: inheritId(options, transparent, context),
|
|
2574
2715
|
_config:
|
|
@@ -2585,7 +2726,7 @@ function computed(fn, options) {
|
|
|
2585
2726
|
_context: context?._context ?? defaultContext,
|
|
2586
2727
|
_childCount: 0,
|
|
2587
2728
|
_fn: fn,
|
|
2588
|
-
_value: undefined,
|
|
2729
|
+
_value: loading ? options.loadingValue : undefined,
|
|
2589
2730
|
_height: 0,
|
|
2590
2731
|
_child: null,
|
|
2591
2732
|
_nextHeap: undefined,
|
|
@@ -2600,14 +2741,16 @@ function computed(fn, options) {
|
|
|
2600
2741
|
_prevSibling: null,
|
|
2601
2742
|
_firstChild: null,
|
|
2602
2743
|
_flags: options?.lazy ? REACTIVE_LAZY : REACTIVE_NONE,
|
|
2603
|
-
|
|
2744
|
+
// A loadingValue node is born committed: commit #0 is already in _value.
|
|
2745
|
+
_statusFlags: loading ? 0 : STATUS_UNINITIALIZED,
|
|
2604
2746
|
_time: clock,
|
|
2605
2747
|
_pendingValue: NOT_PENDING,
|
|
2606
2748
|
_pendingDisposal: null,
|
|
2607
2749
|
_pendingFirstChild: null,
|
|
2608
2750
|
_inFlight: null,
|
|
2609
2751
|
_transition: null,
|
|
2610
|
-
_reask: false
|
|
2752
|
+
_reask: false,
|
|
2753
|
+
_loading: loading
|
|
2611
2754
|
};
|
|
2612
2755
|
self._name = options?.name ?? "computed";
|
|
2613
2756
|
setupComputedNode(self, options);
|
|
@@ -2658,6 +2801,7 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
|
|
|
2658
2801
|
_inFlight: null,
|
|
2659
2802
|
_transition: null,
|
|
2660
2803
|
_reask: false,
|
|
2804
|
+
_loading: false,
|
|
2661
2805
|
_modified: false,
|
|
2662
2806
|
_prevValue: undefined,
|
|
2663
2807
|
_effectFn: effectFn,
|
|
@@ -3685,6 +3829,13 @@ function quietPending(el) {
|
|
|
3685
3829
|
}
|
|
3686
3830
|
return el._reask;
|
|
3687
3831
|
}
|
|
3832
|
+
// NOTE: a loadingValue node's open loading window (_loading) is verdict-quiet
|
|
3833
|
+
// on purpose: commit #0 answers the question by declaration, so the window
|
|
3834
|
+
// reads NOT pending — first-load affordances live in the value channel
|
|
3835
|
+
// (null / skeleton provenance the author encoded), and isPending stays what
|
|
3836
|
+
// it always was: refetch truth for an answered question. This keeps the
|
|
3837
|
+
// verdict fully correlated with transition-class machinery and keeps server
|
|
3838
|
+
// (always false) and client hydration trivially consistent.
|
|
3688
3839
|
function newQuestionInFlight(comp) {
|
|
3689
3840
|
return (
|
|
3690
3841
|
!!(comp._statusFlags & STATUS_PENDING) &&
|
|
@@ -3712,7 +3863,15 @@ function computePendingState(el) {
|
|
|
3712
3863
|
(!!(firewall._statusFlags & STATUS_PENDING) && quietPending(firewall))
|
|
3713
3864
|
);
|
|
3714
3865
|
}
|
|
3715
|
-
|
|
3866
|
+
// `!comp._loading`: a hold created while the loading window is still open is
|
|
3867
|
+
// the window's own landing in flight to its commit — verdict-quiet like the
|
|
3868
|
+
// rest of the window (the UNINITIALIZED check suppresses exactly this frame
|
|
3869
|
+
// for windowless first loads; born-committed nodes need their own gate, #2990).
|
|
3870
|
+
if (
|
|
3871
|
+
el._pendingValue !== NOT_PENDING &&
|
|
3872
|
+
!(comp._statusFlags & STATUS_UNINITIALIZED) &&
|
|
3873
|
+
!comp._loading
|
|
3874
|
+
) {
|
|
3716
3875
|
if (hasActiveOverride(el))
|
|
3717
3876
|
return !el._equals || !el._equals(el._pendingValue, unwrapOverride(el._overrideValue));
|
|
3718
3877
|
return true;
|
|
@@ -4396,38 +4555,6 @@ function createSignal(first, second) {
|
|
|
4396
4555
|
registerGraph(node, getOwner());
|
|
4397
4556
|
return [accessor(node), setSignal.bind(null, node)];
|
|
4398
4557
|
}
|
|
4399
|
-
/**
|
|
4400
|
-
* Creates a readonly derived reactive memoized signal.
|
|
4401
|
-
*
|
|
4402
|
-
* ```typescript
|
|
4403
|
-
* const value = createMemo<T>(compute, options?: MemoOptions<T>);
|
|
4404
|
-
* ```
|
|
4405
|
-
* @param compute a function that receives its previous value and returns a new value used to react on a computation
|
|
4406
|
-
* @param options `MemoOptions` -- id, name, equals, unobserved, lazy
|
|
4407
|
-
*
|
|
4408
|
-
* @example
|
|
4409
|
-
* ```ts
|
|
4410
|
-
* const [first, setFirst] = createSignal("Ada");
|
|
4411
|
-
* const [last, setLast] = createSignal("Lovelace");
|
|
4412
|
-
*
|
|
4413
|
-
* const fullName = createMemo(() => `${first()} ${last()}`);
|
|
4414
|
-
*
|
|
4415
|
-
* fullName(); // "Ada Lovelace"
|
|
4416
|
-
* ```
|
|
4417
|
-
*
|
|
4418
|
-
* @example
|
|
4419
|
-
* ```ts
|
|
4420
|
-
* // Async memo — reads surface as pending inside <Loading>
|
|
4421
|
-
* const user = createMemo(async () => {
|
|
4422
|
-
* const res = await fetch(`/users/${id()}`);
|
|
4423
|
-
* return res.json();
|
|
4424
|
-
* });
|
|
4425
|
-
* ```
|
|
4426
|
-
*
|
|
4427
|
-
* @description https://docs.solidjs.com/reference/basic-reactivity/create-memo
|
|
4428
|
-
*/
|
|
4429
|
-
// NoInfer keeps the previous-value parameter from influencing T inference, so
|
|
4430
|
-
// the memo/effect result type is still driven by the compute return type.
|
|
4431
4558
|
function createMemo(compute, options) {
|
|
4432
4559
|
return accessor(computed(compute, options));
|
|
4433
4560
|
}
|
|
@@ -4465,7 +4592,7 @@ function createEffect(compute, effectFn, options) {
|
|
|
4465
4592
|
* ```
|
|
4466
4593
|
* @param compute a function that receives its previous value and returns a new value used to react on a computation
|
|
4467
4594
|
* @param effectFn a function that receives the new value and is used to perform side effects
|
|
4468
|
-
* @param options `EffectOptions` -- name, defer, schedule
|
|
4595
|
+
* @param options `EffectOptions` -- name, defer, schedule, transparent
|
|
4469
4596
|
*
|
|
4470
4597
|
* @example
|
|
4471
4598
|
* ```ts
|
|
@@ -5532,13 +5659,17 @@ function createProjectionInternal(fn, seed, options) {
|
|
|
5532
5659
|
return wrapped;
|
|
5533
5660
|
};
|
|
5534
5661
|
const wrappedStore = wrapProjection(seed);
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
);
|
|
5662
|
+
// seedLoadingValue: the firewall is born committed (the seed is commit #0);
|
|
5663
|
+
// the internal handleAsync serves it during the derive's first flight. The
|
|
5664
|
+
// node's own value channel is void, so the loading value itself is
|
|
5665
|
+
// `undefined` — presence of the key is what flips the mode.
|
|
5666
|
+
let nodeOptions;
|
|
5667
|
+
if (options?.seedLoadingValue) nodeOptions = { loadingValue: undefined };
|
|
5668
|
+
if (options?.name) nodeOptions = { ...nodeOptions, name: options.name };
|
|
5669
|
+
node = computed(() => {
|
|
5670
|
+
if (!node) node = getOwner();
|
|
5671
|
+
runProjectionComputed(wrappedStore, fn, options?.key === undefined ? "id" : options.key);
|
|
5672
|
+
}, nodeOptions);
|
|
5542
5673
|
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
5543
5674
|
return { store: wrappedStore, node };
|
|
5544
5675
|
}
|
|
@@ -5610,19 +5741,48 @@ function runProjectionComputed(wrappedStore, fn, key, wrapCommit, onDraftWrite)
|
|
|
5610
5741
|
const owner = getOwner();
|
|
5611
5742
|
let settled = false;
|
|
5612
5743
|
let result;
|
|
5744
|
+
// Open loading window (seedLoadingValue): the observable store IS commit #0
|
|
5745
|
+
// for the whole first flight, so the derive works a detached shadow of the
|
|
5746
|
+
// seed — draft writes (pre-await, or between yields) land on the shadow and
|
|
5747
|
+
// cannot tear through to readers (#2988; store reads resolve from the live
|
|
5748
|
+
// backing, and the born-committed firewall removed the status gate that hid
|
|
5749
|
+
// windowless drafts). Every commit point — sync return, each yield, the
|
|
5750
|
+
// async landing — reconciles the shadow through the normal commit path, so
|
|
5751
|
+
// a fully-sync derive still lands immediately (commit #0 superseded before
|
|
5752
|
+
// any observer runs, same as a sync answer superseding loadingValue). The
|
|
5753
|
+
// JSON round-trip matches the server's frozen-seed copy (seedLock): a
|
|
5754
|
+
// loading-window seed is renderable data by contract. Optimistic note:
|
|
5755
|
+
// onDraftWrite (override clearing) shifts from write-time to commit-time
|
|
5756
|
+
// for the shadow run — an invisible draft write must not clobber a visible
|
|
5757
|
+
// optimistic override mid-window.
|
|
5758
|
+
const shadow = owner._loading
|
|
5759
|
+
? JSON.parse(JSON.stringify(wrappedStore[$TARGET][STORE_VALUE]))
|
|
5760
|
+
: null;
|
|
5613
5761
|
const draft = new Proxy(
|
|
5614
5762
|
wrappedStore,
|
|
5615
5763
|
createWriteTraps(() => !settled || owner._inFlight === result, onDraftWrite)
|
|
5616
5764
|
);
|
|
5617
5765
|
storeSetter(draft, s => {
|
|
5618
|
-
result = fn(s);
|
|
5766
|
+
result = fn(shadow ?? s);
|
|
5619
5767
|
settled = true;
|
|
5620
5768
|
const commit = v => {
|
|
5769
|
+
// Shadow run: a void/self return is the mutation form — the shadow
|
|
5770
|
+
// carries the writes and is what commits. Commit a detached snapshot,
|
|
5771
|
+
// never the shadow itself: reconcile adopts a new root value by
|
|
5772
|
+
// identity, and handing it the live shadow would fuse the draft to the
|
|
5773
|
+
// observable store — later shadow writes would mutate the backing
|
|
5774
|
+
// silently and the next yield would diff the shadow against itself.
|
|
5775
|
+
if (shadow && (v === undefined || v === shadow)) v = JSON.parse(JSON.stringify(shadow));
|
|
5621
5776
|
if (v === s || v === undefined) return;
|
|
5622
5777
|
const write = () => storeSetter(wrappedStore, s => reconcileState(v, s, key, true));
|
|
5623
5778
|
wrapCommit ? wrapCommit(write) : write();
|
|
5624
5779
|
};
|
|
5625
|
-
|
|
5780
|
+
const sync = handleAsync(owner, result, commit);
|
|
5781
|
+
// A still-open window after handleAsync means the return was the
|
|
5782
|
+
// commit-#0 fall-through, not a landing — real landings arrive through
|
|
5783
|
+
// the setter. A closed one is a genuine sync landing (windowless nodes
|
|
5784
|
+
// were never open); commit it.
|
|
5785
|
+
if (!owner._loading) commit(sync);
|
|
5626
5786
|
});
|
|
5627
5787
|
return owner;
|
|
5628
5788
|
}
|
|
@@ -7192,23 +7352,24 @@ function createOptimisticProjectionInternal(fn, initialValue, options) {
|
|
|
7192
7352
|
setProjectionWriteActive(wasProjectionWriteActive);
|
|
7193
7353
|
}
|
|
7194
7354
|
};
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
}
|
|
7210
|
-
|
|
7211
|
-
|
|
7355
|
+
// seedLoadingValue: born-committed firewall, same as createProjection.
|
|
7356
|
+
let nodeOptions;
|
|
7357
|
+
if (options?.seedLoadingValue) nodeOptions = { loadingValue: undefined };
|
|
7358
|
+
if (options?.name) nodeOptions = { ...nodeOptions, name: options.name };
|
|
7359
|
+
node = computed(() => {
|
|
7360
|
+
setProjectionWriteActive(true);
|
|
7361
|
+
try {
|
|
7362
|
+
runProjectionComputed(
|
|
7363
|
+
wrappedStore,
|
|
7364
|
+
fn,
|
|
7365
|
+
options?.key === undefined ? "id" : options.key,
|
|
7366
|
+
wrapCommit,
|
|
7367
|
+
clearProjectionOverride
|
|
7368
|
+
);
|
|
7369
|
+
} finally {
|
|
7370
|
+
setProjectionWriteActive(false);
|
|
7371
|
+
}
|
|
7372
|
+
}, nodeOptions);
|
|
7212
7373
|
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
7213
7374
|
}
|
|
7214
7375
|
return { store: wrappedStore, node };
|