Why it is shaped this way
A handle keeps its fetch and derives its phase
Status: accepted, 2026-10-05. Serves The store is the UI's state and Honest data. Reopen if the bench named under Evidence fails its limits (then the fetch's value ships alone and the phase stays stored), or if a reader outside a body needs the phase pushed to it rather than computed when it asks. Superseded on the derived phase, by its own reopening line, by The phase stays stored, beside the fetch; the fetch as a value is built as decided (61c3ac4). The policy and the revalidation it left open are answered by Revalidation is one call the app makes. Its derived phase is restored on 2026-10-07 by another route, the verdict settled at the commit and kept on the root, by The verdict is the root's, and the phase is derived from it.
Status: accepted, 2026-10-05. Serves
The store is the UI's state and
Honest data. Reopen if the bench named
under Evidence fails its limits (then the fetch's value ships alone and the
phase stays stored), or if a reader outside a body needs the phase pushed to
it rather than computed when it asks. Superseded on the derived phase, by
its own reopening line, by
The phase stays stored, beside the fetch; the
fetch as a value is built as decided (61c3ac4). The policy and the
revalidation it left open are answered by
Revalidation is one call the app makes.
Its derived phase is restored on 2026-10-07 by another route, the verdict
settled at the commit and kept on the root, by
The verdict is the root's, and the phase is derived from it.
Context#
A handle has three facts: whether the store holds its operation's data,
what the operation's directives make of that data, and the fetch. As built,
it keeps them in one enum. Phase has three cases: loading, ready with the
data's lens, and failed with any Error (Operation.swift:72-76). That the
data is present is implied by the phase: ready, or failed on field errors or
on a @required field that bubbled. What the directives make of the data, a
field error under @throwOnFieldError or a root that a @required field
bubbles to, is computed by evaluate() (Operation.swift:213-222) and
stored in the phase. The fetch is spread over task, isRefreshing,
fetchTime, fetchEpoch and unplaced, the errors the last response
carried that no field holds. settle(_:) compares a new phase with the old
one by hand (Operation.swift:184-195).
Three things follow.
- A failure behind data is kept nowhere. It is returned to whoever awaited
refetch()(Operation.swift:384-394). AstoreAndNetworkattach, aninvalidate()or aretry()that fails behind data leaves no trace, and a second view of the same handle cannot tell. That is the complaint of #5. - The enum cannot say "failed, with data behind it", so the handle asks the
error for its type: eight places in
Operation.swiftcast it toFieldErrorsorRequiredFieldError(lines 188, 190, 233, 304, 348, 389, 419 and 453). - A stored phase goes stale when the store changes, so the runtime keeps a
subscription of its own, where the principle says "The runtime has no
subscriptions of its own."
noteNullsraisesnullsOrErrorsChangedon any write to or from null, any link moved to another record and any changed list of links (Store.swift:473-488), and a change to a field error or to whether a record is deleted raises it too (Store.swift:455-471). The commit then callsenvironment.reevaluate()(Store.swift:265-269), which calls every retained handle (Environment.swift:88-94), and each handle of an operation with@throwOnFieldErroror a bubbling root walks its own selection again (Operation.swift:448-458), whether or not the commit touched its data. No bench covers that walk.
Three issues ask for the value that is missing: #5 for lastError and
failedAt, #18 for a sweep
of the handles that are stale or failed, and
#12 for isReconnecting and
lastEventAt on a subscription. The answer on #5 proposed the fetch as a
readable fact beside the phase, and left open whether the failure's time is
exposed too.
Decision#
A handle stores two facts and derives the third. All of it is (planned).
- Stored: whether the store held the operation's data, at the handle's last availability check or at its own commit.
- Stored: the fetch, as a value a view reads as it is: idle, in flight, or failed, with the failure and when it failed. With it, the errors the last response carried that no field in the store holds.
- Derived, when it is read: the phase. With data, the getter reads the
verdict and not the fetch's state. The verdict is the lens's static
checks for
@throwOnFieldErrorand for a root that bubbles, through tracked reads of the records, with the last response's errors that no field holds. So a refetch behind data re-runs no body that read only the phase. Without data, the getter reads the fetch. - A body that reads the phase is invalidated by what the getter read and by
nothing else: whether data is present; with data, the fields the verdict
read, through the same channels as any lens read; without data, the
fetch. The chain through the store goes:
nullsOrErrorsChanged, the null half ofnoteNulls,reevaluateIfNeeded,Environment.reevaluate,reevaluate()on the handle protocol,settle(_:), and the eight casts. phasekeeps its three cases and its meaning: ready while data is present, unless the verdict fails it, and failed by a fetch only when there is no data to show.isRefreshingis data present and a fetch in flight.retry()with data behind a failed fetch does not pass through loading.- A fetch's failure is one of the closed set of kinds of A failure says its kind.
- A subscription's handle shows its stream's state the same way, as a
value: connecting, open, waiting to reconnect, ended. That is not a
Phasefor subscriptions, which the planning log refused as a false loading. How a stream reconnects is proposed on #12 and is not decided here. - A reader outside a tracking scope, a model or a test, computes the phase when it asks.
- It is measured before it ships. The verdict runs inside the reading
body's tracking scope, so its reads are tracked reads. Two numbers are
taken on the largest
@throwOnFieldErroroperation that the fixtures underspec/answer: what a read ofphasecosts, and how many bodies re-run, beyond today's, on a commit that changes a value the verdict read and leaves the phase equal. The limits, written before the bench runs: under 50 µs for a read, and no extra body for an operation whose own selection is spreads. If either fails, the fetch as a value ships alone, the phase stays stored, and this record is superseded on that point.
Left open, to settle when it is built: the Swift spelling of the fetch's
value and of its properties; whether the handle keeps the fetch policy it
was attached with, which the answer on #5 proposed and the planning notes
refine without a decision; how a storeOnly attach without data reads
failed when no fetch is made, which goes with the policy; and the
revalidation method #18 asks for.
Evidence#
- A spike in the planning notes, 2026-10-04, on an Apple M1 Pro with macOS 26.5.2 and Swift 6.3.3. It models the mechanism: a record whose writes are silent and whose notification follows, as in a commit, and a handle whose phase is computed from its stored facts and a tracked read of that record. It shows semantics, not cost. Three properties ran. A reader of the derived phase is invalidated by the fields the verdict read and by no others: a change to a field the verdict did not read fired 0 readers, and one it read fired 1. A refetch that failed behind data fired 0, and the failure stayed readable beside the data. A tracking scope nested in another merges its accesses into the outer one, so a verdict computed under a scope of its own still leaves the body that asked depending on the fields behind it.
- The runtime as built, by reading: the chain and the casts that Context
names. A reader outside a body already computes the verdict when it asks:
Environment.fetchreads the operation's own selection before it throws for@throwOnFieldError(Environment.swift:120-129). BENCHMARKS.md, 2026-10-04 at2ba3d18, an M1 Pro: a tracked read in a row body of eight fields costs 638 ns a field at the median, against 25.9 ns for an untracked read. The verdict walks only an operation with@throwOnFieldErroror a root that bubbles, over that operation's own selection: an error inside a spread is the fragment's to weigh, as the terminology's Throw on field error says.- The planning log of the review after 0.6.0 refused deriving the phase by
wrapping the evaluation in the handle's own
withObservationTracking: it registers every record of the selection on the main actor, re-arms on each change and fires atwillSet. Deriving in the getter, under the reader's own scope, is not that. The spike is the new evidence, and it carries no further than the semantics it ran. - Not measured yet: what a read of
phasecosts, and how many bodies a commit re-runs. Both numbers go intoBENCHMARKS.mdbefore it ships.
Not chosen#
lastErrorandfailedAtas stored properties beside a stored phase, as #5 asks, orlastErroralone, as a review of the same code proposed. Either keeps the chain and adds fields to the state machine that has the problem.- A fourth phase case, or
readycarrying the error: every switch over the phase becomes a product decision. - Deriving the phase under the handle's own observation scope: refused before, for the reasons under Evidence.
- A stored phase settled by hand after each commit, as built: a subscription the runtime keeps for itself, and a coarse one.
- A timer that refetches, or flips the phase, when a window closes: refused by An operation states its expiration in its document.
Baton