Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/action-await-yield-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

Docs: inside an `action`, a bare `yield` is required after an `await` before anything that creates a reader — `until()`, `latest()`, a memo or effect, a mount — not only before writes. The `until()` docstring's own example had `await` straight into `yield until(...)`; the `until(...)` expression is evaluated in the post-`await` continuation, outside the transaction, and its predicate reader is born held there (#3482). Example corrected.
2 changes: 1 addition & 1 deletion packages/signals/docs/RULES-INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:67` | scheduler.ts×1 | action-await-contract.test.ts×2 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… |
| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:233` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… |
| A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:51` | constants.ts×1 core.ts×18 optimistic.ts×1 scheduler.ts×3 types.ts×1 verdict.ts×6 optimistic.ts×3 store.ts×1 | createOptimistic.test.ts×5 latest-held-till-flush.test.ts×1 optimistic-store-layer-scope.test.ts×1 posture-store-parity.test.ts×3 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 shallow.test.ts×1 treeshake.test.ts×2 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) \*\*A write becomes visible at flush — to every chan… |
| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:75` | core.ts×5 effect.ts×1 optimistic.ts×1 | body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 direct-commit-readers-posture.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 latest-held-till-flush.test.ts×2 posture-store-parity.test.ts×1 treeshake.test.ts×1 visibility-oracle-store.states.ts×3 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … |
| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:75` | action.ts×1 core.ts×5 effect.ts×1 optimistic.ts×1 signals.ts×1 | body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 direct-commit-readers-posture.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 latest-held-till-flush.test.ts×2 posture-store-parity.test.ts×1 treeshake.test.ts×1 visibility-oracle-store.states.ts×3 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … |
| A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:207` | async.ts×1 attribution.ts×1 core.ts×1 effect.ts×1 scheduler.ts×3 | async-landing-deps-3461.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 treeshake.test.ts×1 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… |
| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:83` | core.ts×2 | ispending-combined-atomic-3442.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … |
| A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:91` | — | visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… |
Expand Down
17 changes: 12 additions & 5 deletions packages/signals/src/core/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,22 @@ function restoreTransition<T>(seq: number, transition: Transition, fn: () => T):
* `yield` is the transaction-safe suspension point: the action waits for a
* yielded promise and re-enters the transaction before running the code after
* it. A plain `await` does NOT — the runtime has no hook into an async
* generator's internal await continuations, so writes to fresh signals
* between an `await` and the next `yield` escape the transaction and commit
* immediately. `await` is still the ergonomic choice for typed results; just
* put a bare `yield` before any writes that follow it:
* generator's internal await continuations, so code between an `await` and
* the next `yield` runs OUTSIDE the transaction: writes to fresh signals
* commit immediately, and anything that creates a reader there — `until()`,
* `latest()`, a memo or effect, a mount — is created mainline, where a read of
* this action's held state makes it born held (A29): staged with the
* transaction and replayed at its commit. For `until()` that commit is the
* settle its own promise holds open (#3482). `await` is still the ergonomic
* choice for typed results; just put a bare `yield` before any write or
* reader creation that follows it — including the expression of the next
* `yield`, which is evaluated before the step re-enters:
*
* ```ts
* const saved = await api.createTodo(text); // typed result
* yield; // re-enter the transaction before writing
* yield; // re-enter the transaction before writing or reading
* setTodos(t => { ... });
* yield until(() => todos.some(t => t.id === saved.id));
* ```
*
* (For the same reason, don't call `flush()` inside an action body — it
Expand Down
8 changes: 8 additions & 0 deletions packages/signals/src/signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -932,12 +932,20 @@ export interface UntilOptions {
*
* Must be called *outside* a tracking scope.
*
* Inside an action, call it from a step: after an `await`, put a bare `yield`
* before `yield until(...)`. The runtime cannot hook an async generator's
* `await` continuation, so the `until(...)` expression — which CREATES the
* predicate's reader — would otherwise run outside the transaction; created
* there it is born held (A29) and replays only at the commit its own promise
* holds open (#3482). See {@link action}.
*
* @example
* ```ts
* const send = action(async function* (text: string) {
* const clientId = crypto.randomUUID();
* setMessages(m => { m.push({ clientId, text, pending: true }); }); // optimistic
* await socket.send({ clientId, text }); // fire-and-forget transport
* yield; // re-enter the transaction after the await
* // Hold until the live source echoes the write (authoritative view —
* // the optimistic row above cannot satisfy this):
* yield until(() => messages.some(m => m.clientId === clientId), { timeout: 10_000 });
Expand Down
Loading