Skip to content
Merged
8 changes: 8 additions & 0 deletions .changeset/create-deferred.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@solidjs/signals": patch
"solid-js": patch
---

`createDeferred(fn, options?)`: an async memo that may lag, but never leads. Before its first answer it is an ordinary async memo (readers suspend to `<Loading>`, or `loadingValue` shows). After it, a refetch holds nothing: readers keep the previous answer until the new one lands, the rest of the screen moves on, and `isPending` reports the refetch on the node and on everything derived from it, store keys included. A refetch that read a held value lands with that hold. A rejection errors like any memo. `refresh(d)` is a quiet re-ask, and `await refresh(d)` and `until` wait for the landing. It is `createMemo` on the server and hydrates like one.

The rules: F-1 and F-2 are amended for declared lag, and D-40 / S-41 are new. The `affects()` mark core moved to its own module (`affects-marks.ts`), which `createDeferred` shares. A mark on the source of a derived store now covers that store's keys.
2 changes: 1 addition & 1 deletion documentation/solid-2.0/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,7 @@ If you need a standard Observable/AsyncIterable interface for external consumers
- **`produce`** → now the default store setter behavior (draft-first)
- **`createMutable` / `modifyMutable`** → use `createStore` with draft setters
- **`from` / `observable`** → async iterators
- **`createDeferred`** → removed; handle outside Solid
- **`createDeferred`** → same name, new meaning: an async memo that may lag but never leads (keeps its last answer while a refetch is in flight; `isPending` reports the refetch). 1.x's idle-time deferral and `timeoutMs` are gone; with a synchronous function it behaves like `createMemo`
- **`indexArray`** → use `mapArray` with `keyed: false`
- **`resetErrorBoundaries`** → no longer needed (error boundaries heal automatically)
- **`enableScheduling`** → removed
Expand Down
21 changes: 21 additions & 0 deletions packages/signals/docs/RULES-DERIVED.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,27 @@ An `<Errored>` flipping to its fallback is a boundary going back to its fallback

---

## F. Declared lag (F-1, F-2, F-7, F-9)

### D-40. A deferred source lags its readers and never leads them

**From:** F-1, F-2 (both amended 2026-10-14 for declared lag), F-7, F-9, F-4, F-12. **Scenarios:** S-41.

`createDeferred(fn)` is an async memo whose author declared that its previous answer is acceptable while the next one loads. Its first load is a first load like any: a reader suspends to a boundary or holds (F-2, F-4), and with `loadingValue` the declared first paint shows instead, verdict-quiet (D-30). After its first answer:

- **Nothing waits for it.** A change that re-asks it holds nothing on its account: its readers keep the answer they show, and the rest of the change shows now. An unready source it reads (`createDeferred(() => query())`) is the same: the node keeps its answer, nothing pends through it.
- **Its landing is a change of its own.** It shows when it lands, unless it lands into a hold (next bullet); an unrelated hold does not keep it back.
- **It never leads.** A re-ask that read a held value derived from that hold's world, so its answer lands into that hold and shows with its commit (F-7). Its readers that also read a held value see the committed one, as any reader does.
- **A held answer stays held.** Once a hold has taken its landing (a reader downstream is pending and staged it), a change that re-asks it still holds nothing: the change shows now, and the hold keeps the landing it took and shows it at its commit.
- **The verdict sees it.** `isPending` of the node, and of everything derived from it (a memo over it, a derived store's keys), is `true` from the re-ask until the landing (F-9: a new question). A `refresh()` re-ask is quiet. The verdict and the landed value change in one frame.
- **Truth readers wait for it.** `await refresh(d)` delivers the landing; `until`'s predicate does not see the stale answer of a source it derives from (the reader's own declaration: `resolve` takes the first settled value and does not wait).
- **Errors are not lag.** A rejected re-ask errors like any memo (F-12); the stale answer never masks it.
- **On the server it is `createMemo`** (everything is first load there).

**Collapses:** `docs/create-deferred.md` D1–D11 (2026-09-08, D11 ruled 2026-10-04; its lane-landing mechanism is superseded, see its §6). **Status:** standing, built 2026-10-14 (`tests/create-deferred.test.ts`, `tests/create-deferred-3710.test.ts`).

---

## Flagged — rulings that cannot cite a fundamental, stale archive text, mistakes

| item | finding | disposition |
Expand Down
15 changes: 8 additions & 7 deletions packages/signals/docs/RULES-FUNDAMENTALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,27 @@ These are the only words the fundamentals use. Internal names (transaction, stag
- **First load.** The wait before a source has its first answer. A first load is _loading_, not a change: there is no previous content to keep. A memo created during a hold that reads the held change (_born held_, D-2) is loading in the same sense: it has no value the screen can show until that hold commits. A read of something loading suspends — to a boundary, or into the hold (F-2, F-4) — and `isPending` of it gives no verdict, because there is nothing shown to be stale (F-9).
- **Question / re-ask.** A source's inputs are its question. An input changing asks a new question; `refresh()`, polling, an action's confirming refetch re-ask the same one.
- **Display-ahead reader.** `latest(x)`, `isPending(x)`, an optimistic value: a reader that shows a held change (or the verdict about it) before the hold reveals it.
- **Declared lag.** `createDeferred(fn)`: a source whose author has declared that its previous answer is an acceptable answer while the next one loads. After its first answer, its own re-asks hold nothing and its readers keep the answer they show until the new one lands (D-40). It is the counterpart of a display-ahead reader: that one shows a held change early, this one shows a change's consequence late, and neither is a hold.
- **Lane.** The world a display-ahead read opens: the screen plus what it shows ahead. What is rendered from a display-ahead read lives in its lane, and the lane holds _itself_ on its own async — a hold of its own, separate from the hold it reads ahead of (F-6).
- **Flush.** The unit of visibility. Writes become visible at the flush that carries them; a manual `flush()` ends a tick.

---

## F-1. The screen is whole

**Immutable.** Every frame the screen shows is consistent: a value and everything on screen derived from it changed in the same frame. The screen never shows a new value beside a stale derivation of it, and never a derivation ahead of its input. This holds for every kind of on-screen reader — a plain binding, a memo between, a boundary's content, a `latest()` view and what is rendered from it — and in every world the screen can show: the committed one, a held one, an optimistic one.
**Immutable.** Every frame the screen shows is consistent: a value and everything on screen derived from it changed in the same frame. The screen never shows a new value beside a stale derivation of it, and never a derivation ahead of its input. This holds for every kind of on-screen reader — a plain binding, a memo between, a boundary's content, a `latest()` view and what is rendered from it — and in every world the screen can show: the committed one, a held one, an optimistic one. The one exception is the author's declaration of lag (amended 2026-10-14): a `createDeferred` node keeps showing its previous answer beside the change that re-asked it, and so does what is rendered from it, until its own answer lands. It may lag the screen; it never leads it (F-7, D-40).

**What it does not say.** It does not say that two _unrelated_ changes must land together (F-2 decides what waits for what). It does not say that imperative, untracked reads are part of a frame — they read the screen as it stands (D-29, D-34). It does not say control and content must agree when the content is _absent_ by the author's choice (a fallback is content: F-4).
**What it does not say.** It does not say that two _unrelated_ changes must land together (F-2 decides what waits for what). It does not say that imperative, untracked reads are part of a frame — they read the screen as it stands (D-29, D-34). It does not say control and content must agree when the content is _absent_ by the author's choice (a fallback is content: F-4). It does not say content the author declared may lag agrees with the control that re-asked it (D-40).

**Collapses:** A10 (`[isPending(x), x()]` atomic), A15's no-tearing core, A17's "no-tearing is an effect-level concern", A29/#3408 (`Panel: 1` beside `Count: 0` was the violation), #3479 "the frame is whole", #3851 (`content 1` beside `x = 0`), fuzzer finding F6 (#3811, the disarmed half — a lane revealing ahead of content a disarmed boundary is still showing), OL-R26, OS-R9, #3761 (ruled against), N1 (reverted). **Derived:** D-1–D-3, D-9, D-10, D-25–D-28, D-34. **Scenarios:** S-2–S-4, S-11, S-30, S-31, S-39.
**Collapses:** A10 (`[isPending(x), x()]` atomic), A15's no-tearing core, A17's "no-tearing is an effect-level concern", A29/#3408 (`Panel: 1` beside `Count: 0` was the violation), #3479 "the frame is whole", #3851 (`content 1` beside `x = 0`), fuzzer finding F6 (#3811, the disarmed half — a lane revealing ahead of content a disarmed boundary is still showing), OL-R26, OS-R9, #3761 (ruled against), N1 (reverted). **Derived:** D-1–D-3, D-9, D-10, D-25–D-28, D-34, D-40. **Scenarios:** S-2–S-4, S-11, S-30, S-31, S-39, S-41.

## F-2. A change that needs async to be shown is held; the screen keeps what it showed

**Immutable.** When showing a change would put a render effect on screen that consumes async that has not answered, the change is _held_: nothing of it shows, the screen keeps the content it had, and when every such async has landed, the change and all of it reveal as one frame. Which async counts: any new async that render effect consumes, whatever its source, unless a boundary catches the render effect (F-3, F-4). A first load in a branch the change mounts counts, and so does a memo created during the hold, when a render effect outside a boundary consumes it (D-2, D-6): the branch does not attach until they have answered, and the screen never shows it with an empty slot. `<Loading>` is how a change escapes the hold. This is the navigation model: click, page B's data loads, page B appears whole. An `action` is the explicit form: its writes are held until its body has returned and everything they put in flight has landed.
**Immutable.** When showing a change would put a render effect on screen that consumes async that has not answered, the change is _held_: nothing of it shows, the screen keeps the content it had, and when every such async has landed, the change and all of it reveal as one frame. Which async counts: any new async that render effect consumes, whatever its source, unless a boundary catches the render effect (F-3, F-4), or the async is a declared-lag source re-asking after its first answer (`createDeferred`, amended 2026-10-14): its readers keep the answer they show and hold nothing (D-40). A first load in a branch the change mounts counts, and so does a memo created during the hold, when a render effect outside a boundary consumes it (D-2, D-6): the branch does not attach until they have answered, and the screen never shows it with an empty slot. `<Loading>` is how a change escapes the hold. This is the navigation model: click, page B's data loads, page B appears whole. An `action` is the explicit form: its writes are held until its body has returned and everything they put in flight has landed.

**What it does not say.** It does not say that what an armed boundary catches makes the change wait (F-4: the catch is the exception, by design). It does not say a new memo joins a hold by being created — it joins by reading a held value, sync or async (D-2); a memo that reads nothing held or pending shows now. It does not say a display-ahead reader holds (F-6). It does not say the hold never waits on work that has never committed: that was the direction rule's time half (2026-10-06, #3820; drafted as F-8, withdrawn 2026-10-09 — the ID is not reused). A first load a render effect consumes outside a boundary holds the change like any other async.
**What it does not say.** It does not say that what an armed boundary catches makes the change wait (F-4: the catch is the exception, by design). It does not say a new memo joins a hold by being created — it joins by reading a held value, sync or async (D-2); a memo that reads nothing held or pending shows now. It does not say a display-ahead reader holds (F-6), or that a declared-lag source's re-ask does (D-40); its first load does, like any. It does not say the hold never waits on work that has never committed: that was the direction rule's time half (2026-10-06, #3820; drafted as F-8, withdrawn 2026-10-09 — the ID is not reused). A first load a render effect consumes outside a boundary holds the change like any other async.

**Collapses:** A15 (entanglement), A29 (the read entry, born held, ruling A), A26 (actions), A34 (1) (a write to a held slot joins), the tick ruling (2026-10-04), #3800's direction, the #2933 navigation model, recovery 2.1/3.1/3.2/4.2, the 2026-10-07 words "in general transitions can hold on any unready read". **Derived:** D-1–D-4, D-6–D-11, D-28. **Scenarios:** S-1–S-13, S-39.
**Collapses:** A15 (entanglement), A29 (the read entry, born held, ruling A), A26 (actions), A34 (1) (a write to a held slot joins), the tick ruling (2026-10-04), #3800's direction, the #2933 navigation model, recovery 2.1/3.1/3.2/4.2, the 2026-10-07 words "in general transitions can hold on any unready read". **Derived:** D-1–D-4, D-6–D-11, D-28, D-40. **Scenarios:** S-1–S-13, S-39, S-41.

## F-3. A reader holds only while it is the display

Expand Down Expand Up @@ -129,7 +130,7 @@ These are the only words the fundamentals use. Internal names (transaction, stag

## Reading the fundamentals together

- **What waits:** F-2 says a change waits for the async its on-screen content needs. F-3 removes a reader that is no longer the display. F-4 removes what an armed boundary catches from it — a transition's reads, or a lane's own async — and the boundary shows a fallback in its place. F-6 removes display-ahead readers from it.
- **What waits:** F-2 says a change waits for the async its on-screen content needs. F-3 removes a reader that is no longer the display. F-4 removes what an armed boundary catches from it — a transition's reads, or a lane's own async — and the boundary shows a fallback in its place. F-6 removes display-ahead readers from it. A declared-lag source's re-asks never enter it (F-1, F-2, D-40).
- **What shows meanwhile:** F-1 says whatever shows is a whole frame. F-5 says a disarmed boundary's old content is that frame's. F-6 says display-ahead readers are the one way to show the change early; they bring their own derivations with them and hold themselves, in their own lane, on their own async. F-7 says nothing else from the change shows early.
- **Verdicts and visibility:** F-9 says when the screen admits it is stale. F-10 says when a write exists at all. F-11 says what an optimistic value is and for how long. F-12 says what an error is and is not.

Expand Down
Loading
Loading