Repository navigation
feat: createDeferred, an async memo that may lag but never leads (D-40) - #4067
Conversation
🦋 Changeset detectedLatest commit: c037364 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Size (brotli, eager entry chunk)
|
|
The Size check is red, but the cause is already on Generated by Claude Code |
Coverage Report for CI Build 38664875775Coverage decreased (-0.005%) to 77.548%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 11.48%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | memo + sync render effect only (reference) |
30.6 ms | 26.7 ms | +14.63% |
| ⚡ | projection derive: write one NESTED field (reference) |
2.4 ms | 2.1 ms | +13.08% |
| ⚡ | build + consume |
741.9 µs | 694.1 µs | +6.89% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing claude/create-deferred-d40 (c037364) with next (eb4e019)
|
CodSpeed flags two benchmarks:
I'm reading them as runner noise and haven't changed anything for them. Generated by Claude Code |
Amends F-1/F-2 with a declared-lag exception: once a createDeferred node has answered, its refetches hold nothing. Readers keep the committed value, isPending reports the flight (on the node, its derivations and derived store leaves), refresh() is a quiet re-ask, and until/await refresh wait for the landing. A refetch that read a held input lands with that hold. Built on the loading window, CONFIG_REDERIVE and a mark per flight; the mark core moves out of affects into affects-marks.ts so createDeferred can reuse it. Server createDeferred is createMemo; hydration goes through the signal-like adapter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2HrYgWeGJX6hKw7bD5LQN
… (D11) Ports #3710's signals tests (D1-D11, mizulu's graph), its web and hydration specs and the deferred-settled-element harness scenario, and makes them pass: - D11: an answered deferred node carries CONFIG_LAGS; recompute's T4 arm does not join its held re-pass to the hold, and the wrapper serves the staged landing as the pass's value, so the write commits mainline and the hold keeps the landing it took. - until over a derivation is released by an equal-value landing: the mark's repoll re-runs truth readers too (not through a derived store, whose leaves re-run them at the derive's write). - The lag walk hops a store slot to its family's derive. The disposal pin is rewritten to this PR's behaviour (the mark drops at disposal). Treeshake pins cover the deferred module and the mark core. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2HrYgWeGJX6hKw7bD5LQN
…n the mark core The projection derive now carries `fam` (the object) instead of a closure, and the repoll iterates the family's live nodes itself. That moves the iteration off the store floor into affects-marks.ts, which only createDeferred and affects() load: -12 B minified on every store scenario. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2HrYgWeGJX6hKw7bD5LQN
Server createDeferred is createMemo, so the recording is async-settled-element's under its own name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2HrYgWeGJX6hKw7bD5LQN
2bf1ac4 to
ee141b9
Compare
|
After the rebase onto
Even without D11 the floors would land 1 B past the 20 B allowance. Getting this green needs a Generated by Claude Code |
Size-Exception accepted by the maintainer 2026-10-14: the D11 check in recompute's held-node arm (12 B) and the REACTIVE_REASK mask (2 B), on floors that next already left 18-19 B over their recorded minified. Core floor 7.48 -> 7.50 KB, render + one signal 9.98 -> 10.01 KB (9,995 B, under 10 KB), + isPending/latest 9.77 -> 9.79 KB, compiled floor 10.17 -> 10.21 KB; recorded minified at this head. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S2HrYgWeGJX6hKw7bD5LQN
…d-d40 # Conflicts: # packages/signals/docs/RULES-INDEX.md
…ateDeferred) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
…d-d40 # Conflicts: # scripts/size/scenarios.js
createDeferred (#4067) grew the signals core: + until/refresh goes over its cap (8.07 -> 8.11 KB, 22,195 -> 22,255 B minified), and the four SC page scenarios sit within 1-6 B of the minified allowance. Re-record them at the merged tree's measurement: frozen page floors 106,772 -> 106,786 and 118,990 -> 119,009, router pages 130,653 -> 130,667 and 144,190 -> 144,205. Brotli caps on the pages are unchanged. Accepted by the maintainer (2026-10-15, "Ratchet up"). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZkrATgvFJjgFRdDB4Drcq
… follows (#4084) * fix(frames): morph drops a stale text node instead of relocating what follows An empty text hole renders `<!--$--><!--/-->` with no text node. When a hole went from text to empty, reconcileChildren met the old text node against the new closing comment, inserted the comment, then relocated the next element (found by lookahead) in front of the stale text. Every emptied cell re-inserted its following subtree. A text node holds no client state, so when the new side has no text at that position the reconcile now drops it and lines up again. server-dbmon, 400 rows, 10% changing, 4x CPU: 28 popover subtrees re-inserted per tick -> 0; main-thread busy 771-910 -> 571-705 ms/s. Size: +71 B minified on the frames client and the server-component pages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZkrATgvFJjgFRdDB4Drcq * chore(size): note the maintainer's acceptance of the base page floor raise Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZkrATgvFJjgFRdDB4Drcq * chore(size): ratchet up caps to what next measures after #4067 createDeferred (#4067) grew the signals core: + until/refresh goes over its cap (8.07 -> 8.11 KB, 22,195 -> 22,255 B minified), and the four SC page scenarios sit within 1-6 B of the minified allowance. Re-record them at the merged tree's measurement: frozen page floors 106,772 -> 106,786 and 118,990 -> 119,009, router pages 130,653 -> 130,667 and 144,190 -> 144,205. Brotli caps on the pages are unchanged. Accepted by the maintainer (2026-10-15, "Ratchet up"). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZkrATgvFJjgFRdDB4Drcq --------- Co-authored-by: Claude <noreply@anthropic.com>
Conflicts: D-41/S-42/S-43 (errors) and next's D-40/S-41 (createDeferred, #4067) both sit after D-39/S-40; kept both, D-41 in section E and the new section F after it. Size caps re-raised from next's caps against next @ 4a1ed17 (same Size-Exception). CONFIG_STAGED_ERROR moves to 1 << 21: createDeferred's CONFIG_LAGS took 1 << 19, and a lagging node read as a staged error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBzTHZM41ipGacdiFva1ju
Requested by Ryan · project thread
Summary
Supersedes #3710: it carries that PR's pins (signals D1–D11 incl. mizulu's graph, the web and hydration specs) on a smaller core footprint.
Before: under the 2.0 hold model, every refetch of an async memo holds whatever reads it. An independent widget (a dashboard panel, a feed, search results) can't keep showing its last answer with a "refreshing" indicator while the rest of the screen moves on. This was the migration gap raised in #3910.
After:
createDeferred(fn, options?)is an async memo that may lag but never leads. Before its first answer it acts like any async memo, so readers suspend to<Loading>. After that, a refetch holds nothing:isPendingreports the flight on the node, on its derivations and on derived store leaves.refresh(d)is a quiet re-ask.await refresh(d)anduntilwait for the landing, not the stale value.loadingValuefor a node that never suspends.Rules: F-1 and F-2 gain a declared-lag exception (amended 2026-10-14). D-40 is the new derived section, S-41 is the new scenario table, and RULES-INDEX is regenerated. The design and as-built notes are in
packages/signals/docs/create-deferred.md.How: it is built from parts the engine already has.
CONFIG_REDERIVEturns an upstream pending into a re-derive, not a pushed status.affectsmark per flight carries the verdict. The mark core moves out ofaffects.tsintoaffects-marks.tsso both can use it, and the mark walk now reaches derived-store leaves through afamilyhook on the projection derive.holdNodelands a refetch into a held input's transaction.CONFIG_LAGS;recompute's T4 arm does not join its held re-pass to the hold, and the wrapper serves the staged landing as the pass's value (D11).GlobalQueue._laggingcheck inwatchmakes truth readers (refresh's waiter,until) wait out a flight.core.tskeepsREACTIVE_REASKvisible through the recompute pass so a re-ask stays quiet. On the server,createDeferrediscreateMemo. Hydration goes through the signal-like adapter.Size (minified vs
next@ 0109a56): every client scenario grows +14/+15 B, and the store scenarios +17 B. Of that, 12 B is the D11 check inrecompute's held-node arm and 2 B keepsREACTIVE_REASKthrough the pass. The rest is property-mangler reshuffle. #3710 measured +24 / +54. ImportingcreateDeferredcosts about 1.6 KB minified.Size-Exception: createDeferred (D-40) — the D11 check in
recompute(a held deferred re-pass joins nothing, 12 B) and theREACTIVE_REASKmask (2 B) add +14 B minified on the floors.nextalready sat 18–19 B over their recorded minified, so the floors are re-recorded at this head's measurement: core floor 7.48 → 7.50 KB (20,436 → 20,469), render + one signal 9.98 → 10.01 KB (27,945 → 27,977; measured 9,995 B, still under 10 KB), plus scenarios.js for + isPending/latest 9.77 → 9.79 KB (27,447 → 27,480) and compiled floor 10.17 → 10.21 KB (28,475 → 28,507). Each cap is set at measured + 10 B, rounded up to 0.01 KB. Accepted by the maintainer 2026-10-14.After merging
next@ eb4e019 (2026-10-15): #4073's seedless derived stores and #4075's still-streaming branch each sat inside the 20 B minified allowance alone, and together with this PR they pass it. Re-recorded, each at measured + 10 B: + createStore 14.94 → 15.00 KB (45,365 → 45,395), hydrating + every store family 29.69 → 29.76 KB (93,753 → 93,770), compiled hydrating recorded minified 101,369 → 101,387 (cap unchanged), and one more frozen floor, hydrating (no stores) 18.09 → 18.11 KB (53,350 → 53,386), covered by the Size-Exception above.MIGRATION.mdnow sayscreateDeferredreturns with new semantics instead of being removed.How did you test this change?
packages/signals:npx vitest rungives 5282 passed, and treeshake pins cover the deferred module and the mark core.tests/create-deferred-3710.test.tscarries feat(signals,solid): createDeferred — an async memo that may lag, but never leads #3710's 36 pins, with the disposal pin rewritten to this PR's behaviour (the mark drops at disposal). The newtests/create-deferred.test.tshas 15 tests: panels, first load, isPending through derivations and store leaves, error boundary, quiet refresh, landing into a hold, unrelated hold, wrapping form, Loading fallback only on first load, until, streams, disposal, and loadingValue.packages/solid: 862 passed, including the newcreate-deferred-hydration.spec.ts(serialized first answer, then a lagging refetch) and a server test.packages/web: feat(signals,solid): createDeferred — an async memo that may lag, but never leads #3710'screate-deferred-mizulu-3710.spec.tsx,hydration/create-deferred-lag.spec.tsxand thedeferred-settled-elementharness scenario are ported. Those and the compiled-size scenarios need the compiler binary, which isn't available locally, so CI covers those.🤖 Generated with Claude Code
https://claude.ai/code/session_01S2HrYgWeGJX6hKw7bD5LQN
Generated by Claude Code
Generated by Claude Code