@solidjs/signals 2.0.0-beta.32 → 2.0.0-beta.34
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 +235 -87
- package/dist/node.cjs +758 -635
- package/dist/prod/core/async.js +108 -54
- package/dist/prod/core/core.js +241 -204
- 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 +39 -39
- package/dist/prod/core/owner.js +53 -44
- package/dist/prod/core/scheduler.js +76 -72
- package/dist/prod/core/verdict.js +59 -48
- package/dist/prod/map.js +6 -6
- package/dist/prod/signals.js +2 -34
- package/dist/prod/store/optimistic.js +37 -32
- 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) {
|
|
@@ -2149,9 +2240,14 @@ function handleAsync(el, result, setter) {
|
|
|
2149
2240
|
// Later iterate() calls run from asyncWrite, where rethrowing would be unhandled.
|
|
2150
2241
|
initialRead = false;
|
|
2151
2242
|
if (!hadValue && !immediatelyDone) {
|
|
2243
|
+
// Loading window: serve commit #0 (see the promise branch above).
|
|
2244
|
+
if (el._loading) return el._value;
|
|
2152
2245
|
globalQueue.initTransition(resolveTransition(el));
|
|
2153
2246
|
throw new NotReadyError(context);
|
|
2154
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;
|
|
2155
2251
|
}
|
|
2156
2252
|
return syncValue;
|
|
2157
2253
|
}
|
|
@@ -2352,6 +2448,11 @@ function recompute(el, create = false) {
|
|
|
2352
2448
|
// Re-ask classification lives in the verdict module; capture the flag before
|
|
2353
2449
|
// the recompute wipes _flags below.
|
|
2354
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;
|
|
2355
2456
|
const oldcontext = context;
|
|
2356
2457
|
context = el;
|
|
2357
2458
|
el._depsTail = null;
|
|
@@ -2408,7 +2509,14 @@ function recompute(el, create = false) {
|
|
|
2408
2509
|
const isAsyncResult = typeof fnResult === "object" && fnResult !== null;
|
|
2409
2510
|
const inFlightChanged = el._inFlight !== prevInFlight;
|
|
2410
2511
|
value = inFlightChanged || !isAsyncResult ? fnResult : handleAsync(el, fnResult);
|
|
2411
|
-
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
|
+
}
|
|
2412
2520
|
}
|
|
2413
2521
|
// On a status-free node clearStatus is a guaranteed no-op: every branch
|
|
2414
2522
|
// in its body is gated on one of these fields, and with _statusFlags === 0
|
|
@@ -2428,22 +2536,34 @@ function recompute(el, create = false) {
|
|
|
2428
2536
|
// _optimisticLane is only ever assigned by engine paths.
|
|
2429
2537
|
if (el._optimisticLane) GlobalQueue._laneAsyncSettled(el);
|
|
2430
2538
|
} catch (e) {
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
el,
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
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
|
+
}
|
|
2447
2567
|
} finally {
|
|
2448
2568
|
tracking = prevTracking;
|
|
2449
2569
|
latestReadActive = prevLatestRead;
|
|
@@ -2505,6 +2625,9 @@ function recompute(el, create = false) {
|
|
|
2505
2625
|
}
|
|
2506
2626
|
} else {
|
|
2507
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;
|
|
2508
2631
|
// Transition-held sync recompute is a write path like setSignal/asyncWrite,
|
|
2509
2632
|
// so sync derivations of held sources stay visible to isPending()/latest()
|
|
2510
2633
|
// (#2831). Both companion writes are transition-scoped (optimistic) and
|
|
@@ -2526,6 +2649,7 @@ function recompute(el, create = false) {
|
|
|
2526
2649
|
// for commit on its own transition's schedule — invisibly (A17/A18).
|
|
2527
2650
|
if (el._pendingValue === NOT_PENDING) queuePendingNode(el);
|
|
2528
2651
|
el._pendingValue = value;
|
|
2652
|
+
if (wasLoading) el._loading = true; // see the held branch above (#2990)
|
|
2529
2653
|
} else if (el._height != oldHeight) {
|
|
2530
2654
|
for (let s = el._subs; s !== null; s = s._nextSub) {
|
|
2531
2655
|
insertIntoHeapHeight(s._sub, queueFor(s._sub));
|
|
@@ -2582,6 +2706,10 @@ function updateIfNecessary(el) {
|
|
|
2582
2706
|
}
|
|
2583
2707
|
function computed(fn, options) {
|
|
2584
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;
|
|
2585
2713
|
const self = {
|
|
2586
2714
|
id: inheritId(options, transparent, context),
|
|
2587
2715
|
_config:
|
|
@@ -2598,7 +2726,7 @@ function computed(fn, options) {
|
|
|
2598
2726
|
_context: context?._context ?? defaultContext,
|
|
2599
2727
|
_childCount: 0,
|
|
2600
2728
|
_fn: fn,
|
|
2601
|
-
_value: undefined,
|
|
2729
|
+
_value: loading ? options.loadingValue : undefined,
|
|
2602
2730
|
_height: 0,
|
|
2603
2731
|
_child: null,
|
|
2604
2732
|
_nextHeap: undefined,
|
|
@@ -2613,14 +2741,16 @@ function computed(fn, options) {
|
|
|
2613
2741
|
_prevSibling: null,
|
|
2614
2742
|
_firstChild: null,
|
|
2615
2743
|
_flags: options?.lazy ? REACTIVE_LAZY : REACTIVE_NONE,
|
|
2616
|
-
|
|
2744
|
+
// A loadingValue node is born committed: commit #0 is already in _value.
|
|
2745
|
+
_statusFlags: loading ? 0 : STATUS_UNINITIALIZED,
|
|
2617
2746
|
_time: clock,
|
|
2618
2747
|
_pendingValue: NOT_PENDING,
|
|
2619
2748
|
_pendingDisposal: null,
|
|
2620
2749
|
_pendingFirstChild: null,
|
|
2621
2750
|
_inFlight: null,
|
|
2622
2751
|
_transition: null,
|
|
2623
|
-
_reask: false
|
|
2752
|
+
_reask: false,
|
|
2753
|
+
_loading: loading
|
|
2624
2754
|
};
|
|
2625
2755
|
self._name = options?.name ?? "computed";
|
|
2626
2756
|
setupComputedNode(self, options);
|
|
@@ -2671,6 +2801,7 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
|
|
|
2671
2801
|
_inFlight: null,
|
|
2672
2802
|
_transition: null,
|
|
2673
2803
|
_reask: false,
|
|
2804
|
+
_loading: false,
|
|
2674
2805
|
_modified: false,
|
|
2675
2806
|
_prevValue: undefined,
|
|
2676
2807
|
_effectFn: effectFn,
|
|
@@ -3698,6 +3829,13 @@ function quietPending(el) {
|
|
|
3698
3829
|
}
|
|
3699
3830
|
return el._reask;
|
|
3700
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.
|
|
3701
3839
|
function newQuestionInFlight(comp) {
|
|
3702
3840
|
return (
|
|
3703
3841
|
!!(comp._statusFlags & STATUS_PENDING) &&
|
|
@@ -3725,7 +3863,15 @@ function computePendingState(el) {
|
|
|
3725
3863
|
(!!(firewall._statusFlags & STATUS_PENDING) && quietPending(firewall))
|
|
3726
3864
|
);
|
|
3727
3865
|
}
|
|
3728
|
-
|
|
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
|
+
) {
|
|
3729
3875
|
if (hasActiveOverride(el))
|
|
3730
3876
|
return !el._equals || !el._equals(el._pendingValue, unwrapOverride(el._overrideValue));
|
|
3731
3877
|
return true;
|
|
@@ -4409,38 +4555,6 @@ function createSignal(first, second) {
|
|
|
4409
4555
|
registerGraph(node, getOwner());
|
|
4410
4556
|
return [accessor(node), setSignal.bind(null, node)];
|
|
4411
4557
|
}
|
|
4412
|
-
/**
|
|
4413
|
-
* Creates a readonly derived reactive memoized signal.
|
|
4414
|
-
*
|
|
4415
|
-
* ```typescript
|
|
4416
|
-
* const value = createMemo<T>(compute, options?: MemoOptions<T>);
|
|
4417
|
-
* ```
|
|
4418
|
-
* @param compute a function that receives its previous value and returns a new value used to react on a computation
|
|
4419
|
-
* @param options `MemoOptions` -- id, name, equals, unobserved, lazy
|
|
4420
|
-
*
|
|
4421
|
-
* @example
|
|
4422
|
-
* ```ts
|
|
4423
|
-
* const [first, setFirst] = createSignal("Ada");
|
|
4424
|
-
* const [last, setLast] = createSignal("Lovelace");
|
|
4425
|
-
*
|
|
4426
|
-
* const fullName = createMemo(() => `${first()} ${last()}`);
|
|
4427
|
-
*
|
|
4428
|
-
* fullName(); // "Ada Lovelace"
|
|
4429
|
-
* ```
|
|
4430
|
-
*
|
|
4431
|
-
* @example
|
|
4432
|
-
* ```ts
|
|
4433
|
-
* // Async memo — reads surface as pending inside <Loading>
|
|
4434
|
-
* const user = createMemo(async () => {
|
|
4435
|
-
* const res = await fetch(`/users/${id()}`);
|
|
4436
|
-
* return res.json();
|
|
4437
|
-
* });
|
|
4438
|
-
* ```
|
|
4439
|
-
*
|
|
4440
|
-
* @description https://docs.solidjs.com/reference/basic-reactivity/create-memo
|
|
4441
|
-
*/
|
|
4442
|
-
// NoInfer keeps the previous-value parameter from influencing T inference, so
|
|
4443
|
-
// the memo/effect result type is still driven by the compute return type.
|
|
4444
4558
|
function createMemo(compute, options) {
|
|
4445
4559
|
return accessor(computed(compute, options));
|
|
4446
4560
|
}
|
|
@@ -4478,7 +4592,7 @@ function createEffect(compute, effectFn, options) {
|
|
|
4478
4592
|
* ```
|
|
4479
4593
|
* @param compute a function that receives its previous value and returns a new value used to react on a computation
|
|
4480
4594
|
* @param effectFn a function that receives the new value and is used to perform side effects
|
|
4481
|
-
* @param options `EffectOptions` -- name, defer, schedule
|
|
4595
|
+
* @param options `EffectOptions` -- name, defer, schedule, transparent
|
|
4482
4596
|
*
|
|
4483
4597
|
* @example
|
|
4484
4598
|
* ```ts
|
|
@@ -5545,13 +5659,17 @@ function createProjectionInternal(fn, seed, options) {
|
|
|
5545
5659
|
return wrapped;
|
|
5546
5660
|
};
|
|
5547
5661
|
const wrappedStore = wrapProjection(seed);
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
);
|
|
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);
|
|
5555
5673
|
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
5556
5674
|
return { store: wrappedStore, node };
|
|
5557
5675
|
}
|
|
@@ -5623,19 +5741,48 @@ function runProjectionComputed(wrappedStore, fn, key, wrapCommit, onDraftWrite)
|
|
|
5623
5741
|
const owner = getOwner();
|
|
5624
5742
|
let settled = false;
|
|
5625
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;
|
|
5626
5761
|
const draft = new Proxy(
|
|
5627
5762
|
wrappedStore,
|
|
5628
5763
|
createWriteTraps(() => !settled || owner._inFlight === result, onDraftWrite)
|
|
5629
5764
|
);
|
|
5630
5765
|
storeSetter(draft, s => {
|
|
5631
|
-
result = fn(s);
|
|
5766
|
+
result = fn(shadow ?? s);
|
|
5632
5767
|
settled = true;
|
|
5633
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));
|
|
5634
5776
|
if (v === s || v === undefined) return;
|
|
5635
5777
|
const write = () => storeSetter(wrappedStore, s => reconcileState(v, s, key, true));
|
|
5636
5778
|
wrapCommit ? wrapCommit(write) : write();
|
|
5637
5779
|
};
|
|
5638
|
-
|
|
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);
|
|
5639
5786
|
});
|
|
5640
5787
|
return owner;
|
|
5641
5788
|
}
|
|
@@ -7205,23 +7352,24 @@ function createOptimisticProjectionInternal(fn, initialValue, options) {
|
|
|
7205
7352
|
setProjectionWriteActive(wasProjectionWriteActive);
|
|
7206
7353
|
}
|
|
7207
7354
|
};
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
}
|
|
7223
|
-
|
|
7224
|
-
|
|
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);
|
|
7225
7373
|
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
7226
7374
|
}
|
|
7227
7375
|
return { store: wrappedStore, node };
|