Repository navigation
0.7.0 Split Time
The ground before the spine: the module's boundaries held by a check, the
compiler refusing what the runtime cannot hold, the plugin telling the truth
about its inputs and outputs, and the numbers the next steps are measured
against, taken before any of them moves anything. The first release that
publishes the compiler's bundle, so a package can depend on Baton by its
tag.
- The bench suite measures what the next steps move, so that each has its
number before it moves anything: a collection pass over one root that
reaches 50,000 records, over 300 roots, and the pass that clears a store
of 50,000 records because no root is left; the re-evaluation a commit
runs today for a retained@throwOnFieldErrorhandle, against the verdict
a phase read would compute in a body's own tracking scope; and a commit
with the three report closures set. The numbers are inBENCHMARKS.md.
No iPhone 12-class device was at hand for the suite, and the three
decision records whose reopening lines wait for one now say so. - Small truths.
Persistence(name:)resolves under the app's bundle
identifier, or the process's name when it has none, so two apps on a Mac
that both name their image"Main"no longer share one file; an app
upgrading finds an empty image at the new path, which is a cache's lot.
The public error types areLocalizedErrors, solocalizedDescription
shows the text they carry. Three names that never passed the terminology
leave the public surface, to go with the steps that remove them:
Environment.collect(),OperationHandle.settle()and
OperationHandle.isComplete. The docs tell the truth again where they had
not:onErrorisbaton.json's, not the environment's; a record's
type-membership bits, configured identity and an introspection command
are not built, and are marked so or dropped; the lookup entry links the
decision that stands;baton.jsontakes Relay's key names, and
relay.config.jsonis not read. - The collector takes a root's entries with the records it sweeps. A root
field rendered from variables,character(id:"7")or a page after a
cursor, kept its entry on the root with a blank value after its record
was collected, so a long session's root held one per id ever looked up;
the entry now goes with the record, and writing a missing value to such
a key takes its entry out the same way. The long-session bench looks up
50,000 ids, where it looked up 2,000, and reports what the process's
table of keys grows by. - The hostile-name sweep is a check of the repository:
scripts/hostile-name-sweep.pytype-checks, one document at a time, the
names of fragments, operations and refetch queries the corpus tests prove
accepted but cannot compile, and every spread form of them, against the
module just built. CI runs it in the Swift job; it is among the local
checks. - The build plugin tells the truth about its inputs and outputs. The
compiler is among the build command's inputs, so a rebuilt compiler
regenerates; the compiler removes from its output directory the
generated files it did not write in this run, so a source renamed or
removed leaves none behind; and generated code and the runtime share a
format number,Types.formatnamingBaton.Format1, so code of another
format fails to compile at one line that says which side is behind,
rather than at every line that names the runtime. - A field whose type is a list of lists,
[[Int!]!]!, is a compile error
at the field. The plan says of a type that it is a list or not, so such
a field was lowered to a flat list and read wrong; the refusal stands
until the plan carries a type that can say the depth. A fragment spread
that reaches the normalization program, which Relay inlines, is an
internal error rather than a silent skip. RecordedTransportandSilentTransportmove toBatonTesting, a
library product of the package for an app's tests and previews, so that
neither ships in the app. A test or a preview that uses them adds
import BatonTesting.- The boundaries inside the runtime module are checked:
scripts/check-boundaries.sh, in CI and among the local checks, holds
the module decision's rules (one file imports SwiftUI, one imports
SQLite, the record, the plan and the ingest do not name the store, the
store's files do not name the environment or a transport, and the runtime
imports nothing else) as a ratchet whose list of tolerated violations
can only shrink. SwiftUI's part of the runtime, the environment value,
the storages behind the marker macros and theForEachinitializers,
now sits in one file. - A custom scalar is its text: a string's contents, or the bytes of any
other token exactly as the server wrote it, so1.50, an integer past
2^53 and an object or array all read back unchanged. Before, numbers were
rounded throughDouble, and objects and arrays were stored as null. - Ingest errors instead of wrong values or traps: an
Intfield given a
fraction, an exponent or a value outsideIntfails the response with an
IngestError(it wrapped, rounded, or trapped), andInt.minreads. A
null inside a list of scalars is stored as a null element; it failed the
whole response. The generated readers, typed as lists of non-optional
values, still leave such elements out. A\uescape cut short by the end of a string, or a high
surrogate followed by an escape that is not a low surrogate, reads as
U+FFFD; the first read past the string and the second trapped. - Storage keys are built from the arguments, not parsed from text. A
string argument holding$(price(format: "$0.00")) was read as a
variable, a lookup argument holding a comma was cut at it, and a list or
input object with a variable inside was stored under its own text; an
input object's keys were written unquoted and unsorted. Floats in keys are
written as the runtime renders a variable. A lookup inbaton.jsonwhose
argument the selection does not pass is a compile error. - Plans know types and conditions. The normalization plan was flat: every
field of every type condition was expected on every record, so the
availability check failed right after an operation's own response for any
selection on an interface or union, and the ingest bound a response key to
the first field of that name, storing a Location'slabel: dimensionin
itsname. The compiler now decides, for each abstract selection, the
fields each group of concrete types reads, and turns@includeand
@skipinto guards the plan settles once per set of variables; the
ingest reads an object by its type's variant, and the check, collection
and deferred parts follow the same variants. - Lenses follow types and conditions. A type condition on an interface
emittedasNodebehind a test of the record's concrete type, so it was
always nil; it is now tested against the set of types that satisfy it,
emitted once in the shared file, and folds into the parent when every
type the parent admits satisfies it. An accessor under@includeor
@skipis optional and reads nil, reporting nothing missing, when its
condition does not select;totalCount @include(if: $x)read 0 and
reported missing data. A field selected twice, or a fragment spread
twice, emits one accessor: the file did not compile. An aliased spread
of a fragment on an interface or union is tested against the types that
satisfy it; it was always nil. - A field no variables can select, such as one under
@include(if: $x)
and@skip(if: $x)at once, is fetched and read under no variables; it
was planned as always selected, so the check waited for a field the
server never sends. A field the initial part and a deferred one both
select is read from the initial payload by its own selection; the
deferred copy could come first, and the initial fields under it were
dropped. - One write path. Everything a batch does, field errors and deletion
included, is in its undo log and its net notification: a failed
optimistic write to a field no longer loses the server's error on it, an
optimistic response that revived a deleted record no longer leaves it
revived when it fails, and a server commit under a layer that deletes a
record no longer fires every channel of it twice. - A deletion is announced to the bodies that hold it. A body that read
only a list, or a connection'snodes, kept a row for a record
@deleteRecordremoved, because the slot holding the link did not
change; the commit that changes whether a record is deleted now notifies
every slot that links to it, in one pass over the store (2.2 ms for a
commit that deletes one record from 8,965 on an M1 Pro, against 7 µs
without the pass). A read of a deleted record's field reports nothing
missing. - Identity is the key alone. The store indexed entities by bare id as well,
last created wins across types, and@deleteRecord,@deleteEdgeand
lookups without a type resolved through it: in the Rick and Morty data
Character:1,Location:1andEpisode:1coexist, and the index named
the episode. The index is gone.@deleteRecorddeletes the one live
record of any type with the id, and when several types have it deletes
nothing and calls the newStore.reportAmbiguousIdentity(debug builds
print);@deleteEdgedrops the edges whose node has the id; a lookup
without a type probes the field's possible types with the same rule. A
lookup the image cannot answer no longer leaves an empty record behind.
Store.existing(id:)is removed. An interface is keyed by id when the
types that implement it have one, as a union is. An object under an
interface or union that is keyed by its path is a record per concrete
type; before, a payload of another type at the same path wrote its fields
into the first type's record. The image's format moved to 2, so an image
an earlier version wrote is discarded at the next launch. baton.jsonis checked: each lookup's field, argument andtypeagainst
the schema, and an unknown key is an error.- A superseded fetch does not commit. A response that arrived, or was still
being read, after a refetch replaced its fetch landed after the newer one
when the transport did not hear the cancellation. Ingest now runs in the
fetch's own task, so cancellation and priority reach it. A mutation's
request runs apart from its caller's task, and its payload commits
whoever stopped waiting, because the server applied it; with
URLSessionTransporta cancelled caller cancelled the request, and the
payload, and an optimistic layer with it, was lost. refetch()on an operation value and on its handle isasync throws:
a refetch that fails throws its error, and the data on screen stays. It
was dropped before. Breaking: a call site needstry.phasechanges only when it changes. A fetch that changed nothing
assigned an equal.readyand re-ran every body that read the phase; a
ready handle now stays as it is, and so does a failure on the same field
errors. A@throwOnFieldErroror bubbling operation's phase follows any
commit that changes a field error, a null, a link or a deletion in the
store, not only its own fetch, and a parked one is settled again when a
view attaches it. Both read the same errors: the operation's own
selection's, as Relay's reader of the operation does (an error inside a
spread is the fragment's to weigh), and those its last response carried
that no field holds..networkOnlyno longer sends a handle another
view shows back to.loading, nor reads the store, and the image, to
decide. A preload's fetch serves the first attach while its data is
fresh; the attach made a second request when the preload had finished.- A field error under a parent the server nulled lands on that parent. The
walk that places an error left aswitchwhere it meant to stop, so an
error atcharacter.origin.namewithoriginnull was stored on
character.name. An error keeps its whole path, and one with no path, or
a path that names nothing selected, is kept in
ChangeSet.unplacedErrorsand counts as uncaught for
@throwOnFieldError; before, it was dropped. - Generated code states its isolation:
Types,Slotsand every generated
type arenonisolated, readers stay@MainActor, so a target built with
the Xcode template's default of main-actor isolation compiles them (it
failed onstatic let plan). - The compiler comes with the package. A package that depends on Baton
downloads the compiler bundle its release published, named by checksum in
Package.swift; a checkout that built its own with
scripts/build-compiler.shruns that one (BATON_COMPILER=localor
releaseoverrides the choice). The release workflow builds the bundle
for both Mac architectures and writes the release commit. - A handle outlives its environment: a view that releases its handle after
the environment is gone no longer traps; the release does nothing. - A damaged image is a miss, never a crash. A file damaged under the open
connection could leave a read stepping a statement already finalized; a
row with a name id past any table, a link that names no type, or lists
nested in lists trapped or recursed without bound. Such a row is used as
far as it reads. - Each slot is its own invalidation channel. A record had sixteen, so a
body woke for a change to a field sixteen slots from one it read; on the
query root, where each field with arguments is a slot, a screen woke for
root fields other screens fetched (four wakes of an unrelated root-field
reader in the bench, now none). A tracked read costs about 620 ns against
560 on an M1 Pro; an untracked read is unchanged at 29 ns. - A schema whose root types have other names, such as
QueryRootor
query_root, works: the compiler interns them by the names the store's
root records have,Query,MutationandSubscription, as Relay's
root record is a__Rootin any schema. Before, the root fields' slots
belonged to the schema's type and were written into a record of another.
A schema that renames a root and also has a type of that root's store
name, or whose renamed root implements an interface or belongs to a
union, is an error.Store(rootType:mutationType:subscriptionType:)is
removed. - Reads never write. A lens read of a root field that was never fetched
resolved its lookup and wrote the link, notifying, inside the body that
read it. The availability check binds a lookup before a handle is ready,
as it did; a lens read of a missing link now reads nil and reports it.
Thelookup:parameters ofAnchor.linked,requiredLinkedand
throwingLinkedare removed. See
the decision. - Readers say what they could not read. A
required*reader that finds a
null reports it through the newStore.reportUnexpected, and one that
finds no value reports the miss; both still return the zero value. A
value of another kind than the reader's reads nil and is reported; it
was nil silently. A@requiredfield the store never received is
reported missing before its lens bubbles. A non-null link without a
record reads one placeholder per type, so the fields below it report
nothing a second time; a record was allocated per read. A@catchon a
non-null list reports a null as the other readers do. - A field error inside a type condition,
... on Character { name }under
an interface, counts for@throwOnFieldErrorand@catchwhen the
record is of the type; the lens's error scan skipped the condition. - A
@requiredlink to a record@deleteRecordremoved is null, as every
other read of the link is: the lens bubbles, aTHROWcollects the
error, and an operation that bubbles to its root fails. The lens read a
blank record and the operation stayed ready. - A connection's
nodesbuilds its lenses in one pass instead of an array
of anchors mapped into a second one: 110 µs for 2,100 nodes against 124
µs on an M1 Pro. - A field selected on an interface or union reads through an
AbstractSlot, which resolves its key once per concrete type: 22 ns per
untracked read against 56 ns, which took the registry's lock and hashed
the key on every read. Thekey:readers ofAnchorare removed. - Keys with variables and fragment arguments are resolved once per owner,
Relay's fragment owner: the scope a lens reads in, which a handle makes
once and keeps. A root field with a variable argument rendered its key,
took the registry's lock and hashed it on every read, 232 ns; it reads
in 28 ns, against 25 ns for the untracked read of a field with a constant
key. A spread with@argumentsbuilt two dictionaries per read and gave
its child a scope that never compared equal to the last: 453 ns for the
read and one variable of the child's scope, 45 ns now, and the spread
alone makes its lens in 10 ns. An
Anchoris a record, an owner and the record it was reached from, and
two are equal when those are the same objects. Breaking for code that
builds anchors:Anchor(record:owner:parent:)replaces theparent:
form, andbindingtakes the spread'sArgumentSite. - The ingest keeps the last value per field. An entity the response names
at many paths was written once per appearance and the commit picked the
winners on the main actor, taking the registry's lock per record; the
ingest now groups the change set by record, one entry per slot, off the
main actor. On an M1 Pro, a commit of the fixture's unchanged payload
takes 152 µs against 182, a commit that changes one field 154 µs against
185, and the ingest 2.95 ms against 2.78. Placing field errors scans the
record's entries instead of indexing every entry. - A commit compares a list where it is stored before building the new
one, so a list that did not change allocates nothing: the fixture's
unchanged payload commits in 133 µs against 152. - A plan resolves once where it can. A handle's fetch uses the resolution
the handle holds, a selection with no variable below it resolves once
and keeps the result, and each field's response key bytes and fixed key
are taken when the static plan is built. Resolving the fixture's plan for
another page takes 0.69 µs against 8.5 µs without the kept resolutions. - A subscription frame, an incremental part's envelope and the
errors
array are read by a scanner of the response bytes alone; each built the
plan-driven cursor and its 48 scratch buffers. A 69-byte frame reads in
375 ns against 2.21 µs. - A small response costs what it is. The change set reserved room for
32,768 entries whatever the response, the cursor made 48 scratch buffers
before reading a byte, and the change set the ingest returned was copied
on its first append. Reservations now follow the response's size, a
buffer is made when the walk first reaches its depth, and the change set
is built where it is filled: a 64-byte mutation payload ingests in 2.4 µs
against 4.5 µs. - A floating-point number is read where it lies in the response; each one
was copied into a new array first, and a number the plan skips was
parsed. - A mutation's root fields are keyed without their arguments. Each
distinct input numbered a permanent slot onMutation, named by the
input's text; now a field is keyed by its name, or by its alias when it
has one. The store dumps underspec/changed accordingly. The data a
mutatereturns is the latest payload of its field. - The multipart reader drops a preamble, which it returned as a first
part, lets go of each part once its delimiter is read instead of keeping
the whole response, and reads a part without headers. It scans the
chunks it is given rather than a byte at a time: 978 KB of 20 parts in
16 KB chunks parse in 0.50 ms against 14.6 ms. - The compiler emits
Types.schemaDigest, the MD5 of the schema's text, for
an app to pass as its image'sversion: an image written under another
schema is discarded. The version is the app's to pass, because generated
constants are made on first use and nothing has made one when the file
opens. - A deferred fragment the image holds only half reads absent, and its
operation fetches. A record read from the image holds every cell of its
row, a deferred fragment's link among them, while the records behind it
may be gone, and the check passes over deferred fields: the fragment
read present and empty. The deferred fields are now checked apart, in
memory and then in the image; one whose records are not whole is
cleared, unless the initial part selects the same field, whose data
stays, and a store-or-network attach fetches while the initial part
renders. - An image that lost a batch, written in vain or dropped while the file
could not open, is discarded at the next open; it served rows older
than memory had known, a deleted record among them. - An edit the store cannot make in memory makes the image forget what it
would have changed. An edge directive on a connection the store held
only in the image was dropped, and an insert into the empty record a
link had made wrote a connection of one edge over the image's; a
@deleteRecordof a record only the image held left it there, to come
back at the next launch. The connection, or every record with the id,
is dropped from the image and read as missing until a response gives
it again, so the screen fetches. - A read of the image writes nothing first. The availability check wrote
the writer's whole queue on the main actor before reading, and every
launch's first reads queued stamps that the next check then wrote there.
A batch being written lands before a read takes the file, and the
records of a batch still queued, with those its root fields link to, are
kept by the collector until written, so a read never meets a row older
than memory held. - Data read every launch keeps its age. A fetch time was kept only by the
fetch that wrote it, so data an app read from the image at every launch
without fetching went stale at every second launch; a launch that reads
a fetch time now keeps it for the next. Store.checksays where its answer came from:.memory,.imageor
.miss. A handle took the image's part from a change in a global
counter, which missed the root's fields and records an earlier check had
filled, so data from the image without a fetch time could read as fresh.
Store.hydratedRecordsis no longer public. Breaking:checkreturned a
Bool.removeAll()deletes the image's file. It queued deletes that a failed
open dropped and a failed batch rolled back, and it kept the interned
names, which hold argument values; work queued before it is dropped
too. A sign-out releases the old environment's handles, removes the
image and makes a new environment over it. A store made before the
removal reads, writes and dates nothing in the image after it, so a
response that lands late for the user who signed out reaches neither
the file nor the next user.Persistence.close()writes what is queued and closes the file, so a new
image can take it over. The new image counts as a launch, though the
process is the same: the rows the closed one wrote that it does not read
age out a launch sooner. A sign-out keeps one image: it removes it and
hands it to the next environment.- Opening the image scans nothing. The rows no launch has read since the
one before last were deleted at open, three scans the first frame
waited for; a read now treats them as gone and the writer's first batch
deletes them. A database of another kind at the image's path turns the
image off for the process instead of being opened again every second.
The generation moves once per image, not per connection, names are
written with a plain insert, so a second connection that took an id
fails its batch instead of renaming every row written with it, and an
image past 65,536 names starts again. - One
invalidate:Environment.invalidate().Store.invalidate(), which
marked memory stale but left the image's fetch times, so data read back
from the image counted as fresh, is internal and does both. - Diagnostics point where they are. An error in the schema is positioned
in the schema file; it printed1:1. Related places print asnote:
lines, and a message Relay writes over several lines prints on one. A
state of Relay's programs the lowering relies on never meeting is an
internal error at the place it was met, where it lowered into an empty
plan. - A linked field named
type,self,protocoloranygets a nested
lens withLensafter its name; it emittedstruct Typeorstruct Self. A selection named or aliasedanchororrecordID, which every
lens has for itself, is a compile error at the name that asks to alias
the field or to choose another alias. - Generated slots are nested per type,
Slots.Character.name; a type's
name and a field's ran together, soA_b.candA.b_cwere both
Slots.A_b_c. - A variable named like a Swift keyword,
$whereor$in, is escaped in
the operation value; it emittedpublic var where. - A directive Baton gives no meaning to is a compile error at the
directive, by place:@inline,@relay(plural:),@relay(mask: false),
@raw_response_type,@preloadableand@streamcompiled through
Relay's transforms and did nothing, or, formask: false, read unmasked
data that Baton has no word for. A marker holds exactly one definition
of its own kind:@Fragment("query …")and two fragments under one
@Querycompiled. - A persisted id is the MD5 of the operation's text as the app holds it.
The hash took the printed text with its trailing line break, which the
emittedtextdrops, so no text the app held matched its id. The ids
change. onErroris decided at compile time:"onError"inbaton.json, sent
with every operation the target compiles. UnderNULLthe fields the
schema types non-null are typed by their semantic nullability, so an
accessor no longer reads""or0for a field an error nulled.
Environment.errorBehavior, a switch at run time that changed what the
compiled types meant, is removed.- The default fetch policy is
FetchPolicy.default,.storeOrNetwork, as
Relay's queries default to: the store answers when it can. It was
.storeAndNetwork, spelled in five places, so every attach of every
screen made a request. Breaking for code that relied on that default:
name.storeAndNetwork.Environment.releaseBufferSizeis set at init
and fixed after. - The data a mutation returns stays readable. Nothing kept its payload
alive, so after the next collection it read nil; the environment now
keeps the completed mutation as a root, and its data lives until later
mutations push it out. - Kinds are types. An operation value conforms to
Query,Mutationor
Subscription, each refiningOperation, and each API takes only its
kind:handle(for:),preload,fetchand@Queryqueries,mutate
and@Mutationmutations,subscriptionHandleand@Subscription
subscriptions. A@Queryholding a mutation compiled, ran on attach and
wrote the mutation root's slots into the query root. A subscription
value carries its handle as a query value does; it found it in a table
of the whole process, keyed by the value, which equal values in two
environments shared. Breaking:OperationKindandkindare removed,
and code generic over operations names the kind it needs. - A subscription survives a bad event. An event with errors and no data
ended the subscription for good; it now setserrorand the stream goes
on, and the next good event clears it.retry()opens a stream the
server or the socket ended. A stream that a newer one replaced no longer
closes the newer one's state when it ends. The WebSocket transport reads
anerrorframe's GraphQL errors as the messages, where it showed the
frame's text, and closes the socket when its last subscription ends. - Incremental delivery reads all of the format. A part's
subPathplaces
its data below the announced path, its ownerrorsland on the fields
they name, and an announced part thatcompletedwith errors puts them
on the fields it would have filled, where@catchreads them; all were
skipped. A part withhasNext: falseends the stream, where the fetch
waited for the connection to close. Later parts are parsed and
normalized off the main actor, and the first part's announcements are
read in the pass that ingests it rather than parsed a second time on
the main actor. - A deferred response is fetched when its stream completes, and its fetch
time is stamped then; it was stamped at the first part, as if the whole
response had come. A stream that breaks after the first part leaves no
fetch time, and the deferred parts it lacks make the next store-or-network
attach fetch again; the first part still renders at once. URLSessionTransportreads an incremental response from its data task's
own delegate, in the chunks the loading system delivers; it iterated the
body a byte at a time. The bench's 978 KB response reads in 1.2 ms
against 6.1 ms.@deferin a mutation or a subscription is a compile error. It compiled,
and the response was read as one part.- The ingest takes a type's name once per selection rather than from the
registry, under its lock, for every entity and path key, and reads a
list of links into a buffer kept per depth rather than a new array per
list. - The compiler reads a marker qualified by the module,
@Baton.Query,
and raw string literals (#"""to"""#), in which only a backslash
followed by the literal's hashes is an escape. A bare@Querywhose
first argument is not a string literal, such as SwiftData's
@Query(sort:)or@Query(FetchDescriptor<Item>()), is that other macro
and is left alone; it was an error. The other markers and@Baton.Query
still want a literal. The plugin hands the compiler every file that names
a marker. - A
.graphqlor.gqlfile in a target writes its own output; its
documents compiled and their lenses were never written. Outputs are named
by the source's path in the target, soThing.swiftandThing.graphql,
or two files of one name in two directories, no longer write one file;
batonc generate --outnames them the same way. ABaton.swiftat the
target's root that declares GraphQL, whose output would be the shared
Baton.baton.swift, is an error naming it; the shared file overwrote its
output, or the build failed on two producers of one file. A document with
an error writes nothing, where every output was overwritten with a stub,
and a file holding GraphQL that no output is named for is an error. - Each
batonccommand takes only its own options, and any other is an
error naming the ones it takes:--schem xwas ignored, and the schema
then came from wherever else it could. - The check that a marker's property is typed as its document's generated
type finds the definition by its file and its place among the file's
documents. It searched the document's text for the longest known name, so
a query that spreadHomeDetail_characterwas taken for the operation
HomeDetail. A subscription's property typed.Actionis now warned
about; only a mutation's may be. - A parked
@throwOnFieldErroror bubbling handle that was ready is
settled again when a view attaches it. A commit made while it was parked
that put a field error or a null into its selection left it ready; only
a failed one was settled. - The first part of a deferred response settles the phase by the errors
with no path that part carried. It read the last response's, so a
@throwOnFieldErroroperation whose first part carried one rendered
ready until the stream completed, and stayed ready when the stream broke
after it; one whose last response carried one stayed failed until a
clean response completed. - A
@throwOnFieldErroroperation failed by an error with no path fetches
again when a view attaches it understoreOrNetwork. No record holds
such an error, so no commit could clear it, and the failure stayed until
retry(). A failure on field errors or a@requirednull, whose data is
in the store, goes stale as ready data does, soinvalidate()and the
expiration refetch it, and a refetch that fails at the transport leaves
it as it leaves ready data;isStalewas false for every failure. Environment.fetch(_:)of a@throwOnFieldErroroperation throws the
field errors its handle fails on. It threw for every uncaught error the
response placed, one inside a spread among them, so the fetch threw where
the handle was ready.- A stream a retry replaced leaves the new one's
erroralone. A bad
event the old stream was still reading whenretry()ran set its errors
on the handle, and the new stream showed them until its first good event. - A subscription whose transport ends its stream with a
CancellationError
of its own ends:isActiveis false and the nextretain()opens it
again. The handle stayed active over a stream that had ended, and no
retain could reopen it short ofretry()or a full release. - Completed mutations take no place in the release buffer. Each one
took a place of its own, so ten mutations pushed out the query of a
screen the user had left, and going back to it loaded and fetched again.
The environment keeps them apart, one per operation value and as many as
releaseBufferSize, and one pushed out is collected at once; its records
stayed until some unrelated release scheduled a collection. - A subscription that opens the WebSocket just after the last one closed
it keeps its socket. The closed socket's read, failing as it closed,
ended whichever socket was current, and the new subscription failed with
a cancellation. Each socket's frames and end now concern that socket
alone, a socket is not closed while a subscription is opening it or
starting on it, and the one that opens it no longer waits for an
acknowledgement that came, or a socket that failed, while its
connection_initwas on the way. - A subscription whose reader goes away before the server acknowledges
the connection leaves nothing behind: it stops waiting for the
acknowledgement, and the socket closes when no other subscription is on
it or starting. The socket stayed open with no subscription on it until a
later one ended, and without an acknowledgement the subscription's start
waited for good. - A
@required(action: LOG)field below a placeholder logs nothing. Reads
under a placeholder report nothing, since the non-null link above it was
reported missing, but a LOG field there still told
Environment.requiredFieldMissing, naming the placeholder's key. - Floats read as JSON writes them whatever the locale. They were read by
the thread's locale, in place in the response, so under one with a
decimal comma0.25read as 0,[1,5]as[1.5, 5], and a response cut
off in digits was read past its end; they are read in the C locale now,
and never past the number the scan found. - A deferred part the server could not deliver fails an operation the
same however many errors it sent. Each error after the first was
unplaced, so with two a part whose fields are all under@catchfailed a
@throwOnFieldErrorfetch and handle, and a spread's part failed a
handle though the spread's fragment weighs its own errors. The errors
count as uncaught now only when a field the part would have filled is
under no@catch; a part with no field on its record's type leaves them
all unplaced, where it dropped the first. - A fragment on the mutation type reads the payload. Its fields kept their
arguments in their keys while the mutation wrote them without, so a
mutation that spread it read nil; they are keyed as the mutation's own
root fields are. - Of two fields whose lenses would take one name, the second is checked
through its own lens. Its lens is numbered,TypesLens2beside a
TypesLens, butfieldErrorsandsatisfiednamed it again without the
number and checked the first field's lens, so a field error or a missing
@requiredfield in the second went unseen. - A linked field named
mainActor,doubleoroptionalgets a nested
lens withLensafter its name, astypeandstringdo. Its lens hid
the attribute on every accessor, theDoubleaFloatfield reads as,
or theOptionala caught spread is wrapped in, and the generated code
did not compile. The names held back are every type and attribute a
lens spells unqualified, and Swift's own. - An edge directive's commit looks no key up by name. Each edit took the
registry's lock and hashededges,node,cursorand
__connection_next_edge_indexseveral times; it now edits the
connection by the slots its plans resolved, which the registry keeps
under the connection's type. A directive that names a record no
connection field made, or inserts an edge of another type than the
connection's edges, leaves it alone; it wrote edges into the record, or
an edge the connection's readers read by another type's slots. - The docs say what runs on the main actor: reads, commits, the
availability check with its reads of the image, collection, and the
normalization of an optimistic response. The vision, the store principle
and the README said collection and all normalization ran off it; a
decision record now says why they do not, with the numbers that would
move the check and collection. - A view's storage releases its handle as SwiftUI drops the view's state,
on the main actor. It released it from a task started for each
teardown, so the handle stayed retained, and a root, until the task ran. - A request with nothing to send it fails with
EnvironmentError, which
says what is missing: a view's environment, a lens's, the one that made a
handle and is gone, or a subscription transport. It failed with a
TransportErrorwhose status code was 0 and whose description read as
an HTTP status. - An operation whose root a
@requiredfield bubbled to fails with a
RequiredFieldErrorthat names it inoperationNameand says the root
bubbled. Its path was the operation's name, so the error described a
null field of that name. - A preloaded operation is settled on its first attach. When the preload's
fetch had finished with fresh data, the attach returned before reading
the phase again, so a commit that put a field error or a null into the
selection while the handle waited for a view left it ready. - A deferred part the server could not deliver lists each of its errors
once among the uncaught ones. Its first error is placed on every field
the part would have filled, and was counted once for each such field
under no@catch, so a part of two of them listed it twice. - A spread alone under an aliased
@catch,... @alias(as: "x") @catch { ...F }, compiles whatever the fragment's error policy and reads the
field errors in the fragment: a failure underRESULT, nil underNULL.
It calledF.caught, which only a fragment with@throwOnFieldErrorhad,
so the generated code did not compile for any other fragment, and under
@catch(to: NULL)such a fragment's accessor threw instead of reading
nil. - Every name the generated code declares comes from one allocator per
scope, which knows the names the scope spells unqualified and the
program's fragments and operations, and a name the document spells never
moves. A field aliasedasCharacterbeside... on Characterdeclared
asCharacterandAsCharactertwice; the condition's accessor and lens
take the next number,asCharacter2andAsCharacter2. A field named
like a fragment or an operation the lens names, such as
testNotes_character, got a lens that hid it; the lens takesLensafter
the name. A spread's accessor named like a field takes the fragment's
whole name. A connection'snodesreadEdges.Nodewhatever names those
lenses took. An optimistic builder for a payload field namedtype,
self,stringorsendablewasstruct Type, hidStringor
conformed to itself; it takesResponseafter the name. A name a scope
would still declare twice is an internal error naming both declarations,
where the Swift did not compile. - A variable named
selfis a parameter, a property and a request
variable of that name. The value's initializer,variablesandhash
read the instance itself in its place, and the generated code did not
compile. A variable namedhasher, orcommitin a mutation, compiles
too:hash(into:)combined its own parameter in its place, and the
action called the parameter for itscommit. - A plan's edge directive is an
Edit, as the change set's
ChangeSet.Editit becomes, so "handle" means only the operation handle.
Breaking for a plan built by hand:HandleisEdit,ResolvedHandle
isResolvedEdit, thehandleproperties ofPlanFieldand
ResolvedFieldareedit, and.scalarand.linkedtakeedit:. - A mutation's action passes a variable named like a Swift keyword, such
as$self, by its bare label: Swift warns about an escaped label at a
call site, and a build that treats warnings as errors refused the
generated code. - A key rendered from variables no longer widens the records of its type.
Each cursor and each id renders a key the process keeps, and keys were
numbered in one sequence per type, so a field first used after a long
session was numbered past all of them and every record given it made
room for each. In the bench's session of 2,000 lookups and 500 pages,
2,000 characters given such a field took 28.2 MB and 5.86 ms to commit;
they take 1.1 MB and 1.71 ms. A key the compiler emits as a constant,
with arguments or without, keeps its place among the type's slots; a
record keeps the rendered keys written to it in a list of its own
sorted by key, which a read of one searches by halves: a root field
with a variable argument reads in 31.6 ns against 28.8 ns, and the
newest of the session's 2,000 keys in 50.7 ns. A list of 5,000 rows,
each holding three fields with a variable, commits in 4.76 ms against
4.08 ms for three constants, and each row holds 96 bytes more. A key
keeps the kind it is first met as: a constant whose text was rendered
first, or that the image named first, is read through the search.
Slot.indexis negative for a rendered key, andRegistry.slotCount
counts both kinds. - One image in a process holds its file. A second
Persistencemade on a
file another holds, under any spelling of its path (/tmpand
/private/tmp, before the file exists and after), runs without the image,
as over a database of another kind, and stops a debug build where it is
made; it opened a second connection, which moved the generation again and
could fail the first's batches on a name both interned.close()and the
image's end hand the file over; a closed image whose file another has
taken reads, writes and removes nothing there. RecordedTransport.requestsis read under the lockexecuteappends
under. It was read without it, so reading it while a request arrived
off the main actor was a data race.- A
TransportErrorwith no response behind it, from a WebSocket that
closed under a subscription or aRecordedTransportwith nothing
recorded for the operation, describes itself by what went wrong; it
readHTTP 0: .... ItsstatusCodeis still 0, now documented as no
response. isRefreshingis true while an operation that failed on field errors or a
@requirednull, with its data in the store, fetches again, andretry()
leaves such a failure in place, its data visible, asrefetch()does.
isRefreshingwas set only behind ready data, so a view showing that data
and the failure did not see the refetch;retry()showed loading over the
data, and when its fetch failed at the transport the failure became the
transport's, which no later commit could clear. A handle that shows
loading is not refreshing: anetworkOnlyview that attached while a
refetch behind ready data was in flight, and a fetch started behind it,
keptisRefreshingtrue over a screen that showed nothing.- A name the document chose that the generated code needs is an error at
that name, which says what it clashes with and asks for an alias or a
rename: a variable namedvariables, orresolutionin a query or a
subscription; a mutation's payload field namedvariable, which its
optimistic builder declares; an inline fragment@alias(as:)names
anchororrecordID, which every lens has; a field named or aliased
hasNext,hasPrevious,isLoadingNext,isLoadingPrevious,
connectionIDornodesin a connection; and a fragment or operation
namedTypes,SlotsorBaton, a refetch query among them. Each was an
internal error without a position that asked to report it, and a fragment
namedBatonhid the runtime's module from the generated code. A fragment
or operation namedSwift,SelforAny, or like a standard library
name the generated code spells (String,Int,Double,Bool,
Optional,Result,MainActor,HasherorSendable), is such an
error too: it hid that name from the whole module, and the generated code
did not compile. So is a field namedTypes,Slots,AbstractSlotsor
Siteswhere its lens, or a lens nested in it, reads through that shared
enum, which the field's accessor hid; a variable namedBaton,Typesor
Slots, orAbstractSlotsorSiteswhere the operation's lenses read
through them, which the variable hid from the operation's code; and a
mutation's variable namedoptimistic, which its action takes as a
parameter of its own. A clash between two names the compiler chose stays
an internal error. - A field named
Batoncompiles in any lens. A lens under@catchor
@throwOnFieldError, a refetchable fragment's and a connection's named
the runtime's module in expressions, which the field's accessor hid; a
lens now names it only in types. - An operation's
textis a raw literal delimited by one#more than the
longest run of them in the text. A document in a raw literal of two or
more hashes can hold\#, as insearch(name: "\\#1"), which the
literal of one hash read as an escape, and the generated code did not
compile. - A schema type named
Baton,Type,ProtocolorAnyisBaton_,
Type_,Protocol_orAny_inTypes, at its constant and at every
reference, as inSlots, where a field namedAnyisAny_too.
Batonreferred to itself in its own initializer and hid the runtime's
module from the other constants, Swift readTypes.Typeand
Types.Protocolas metatypes, and Swift lets no member be namedAny,
so the shared file did not compile. A field or a variable namedAnyis
escaped where the generated code declares it, asTypeis. The shared
file's sets of types areSwift.Set, which a type of the module named
Setno longer hides. - The
RequiredFieldErrorof a root that a@requiredfield bubbled to
has the path of that field, the first required field that is null, as
character.origin, and says it: "TestRequiredOrigin: the @required field
character.origin is null and bubbled to the root". The generated root
lens, and each lens its check recurses into, hasmissingRequiredField,
whichLensdeclares and which reads and reports whatsatisfieddoes;
satisfiedis unchanged. - The store's writes, the ingest and the store's counters belong to the
package, not to an app:Store.commit,commit(_:replacingOptimistic:),
applyOptimistic,revertOptimistic, the availability checkcheck,
optimisticLayers,existing,invalidationEpoch, the root records
and their keys,ChangeSet,Ingest, the resolved plan
(Plan.resolve,ResolvedSelectionand its parts),Store.count,
Environment.collections,rootCount,unconfiguredandresolve,
the handles'retainCount,MutationStateand the storage-key
renderings ofVariableandVariablesarepackage. The tests and
benchmarks reach them; an app writes through operations and reads
through lenses. Breaking for code that committed or ingested by hand. - The interface generated code calls is SPI, and generated files open with
@_spi(Generated) import Baton.Anchor,Owner,Registry,
AbstractSlot,DynamicKey,ArgumentSite, the plan types, the
numbers insideTypeIDandSlot,Record.read,errorandis, and
Lens'sanchor,init(anchor:)and static checks are
@_spi(Generated), as are an operation'splanand its flags; a lens's
anchor,init(anchor:),connectionandrefetchableare generated
as SPI too. An app reads through accessors and cannot rebuild one
fragment's lens as another's from another module. Theanchor,
init(anchor:)andplanthat batonc generates for every lens and
operation default to a trap, so Baton builds with library evolution, as
a framework built for distribution builds it; a lens or an operation
written by hand that leaves them out compiles and traps where it is
read.Slot.storageKeyandTypeID.namestay public: the store's
reports hand an app a slot and a record. Breaking for code that read a
record, built an anchor or a lens, or named a plan: it needs
@_spi(Generated) import Baton, as the tests and benchmarks have. - A mutation's payload field named or aliased
Batoncompiles. Its
optimistic builder wrote each scalar asBaton.Variable(value), which
the field's property hid in the builder and in every builder nested in
it; a builder now names the runtime's module only in types and writes a
scalar as.init(value). - A spread's accessor compiles where a member is named like its fragment:
a field aliased like the fragment it is spread beside, as
TestRow_character: name ...TestRow_character, a variable of the
operation, or the accessor itself, which a fragment named in lower case,
asfragment row, gives its name. The accessor spelled the fragment in
expressions, asrow(anchor: anchor)androw.satisfied(anchor), which
the member hid; it now builds the lens as.init(anchor:)and calls the
fragment's checks through a local alias of its type. refetch(), and a connection'sloadNextandloadPrevious, compile
where a field is named like the fragment or its refetch query, in the
fragment's lens or in the connection's. They named both in expressions,
asanchor.refetch(TestNotesPaginationQuery.self, TestNotes_character.refetchable), which the field hid;refetch()now
reads the descriptor asSelf.refetchable, and the query, and in a
connection the fragment, are named through local aliases of their
types.- A mutation's payload field that is a list of floats or of booleans
compiles in its optimistic builder, and so does a variable of either
list type:Variablehas initializers from[Double]?and[Bool]?.
With those from[String]?, which lists of strings, IDs, enums and
custom scalars take, and from[Int]?, every list of scalars a builder
or a variable writes has one. - A variable that is a list of input objects, such as
$filters: [FilterCharacter!]!, compiles:Variablehas an initializer
from a list of variables, which the operation value'svariablescalls. - A spread that binds sixteen or more of a fragment's arguments compiles,
and so does the refetch query of a fragment that declares as many. The
closure that binds them returned a dictionary whose type Swift inferred
from the literal, which took twice as long with each argument, and Swift
gave up on it at sixteen; the closure now states its type. - A spread argument that is a list or an input object holding a variable
beside a constant, as@arguments(ids: [$id, "2"]), compiles. The
binding defaulted every item to null,.string("2") ?? .null, which
Swift warns about, and a build that treats warnings as errors refused the
generated code; only a variable, which the scope may lack, is defaulted. - A field, a selection, an argument or a variable named
rethrows,
fallthrough,precedencegroupor_is escaped as Swift's other
keywords are; the generated code declaredpublic var rethrowsand
var _, which Swift refuses. A mutation's action passes a variable
named$_by an escaped label, since Swift reads a bare_:as no label. - A variable, or a field of a mutation's payload, named
awaitcompiles.
Its value was read bare, as inself.await = awaitandif let await,
where Swift reads the keyword; it is escaped wherever it is declared or
read. - A mutation's variable named
$varor$letcompiles in a build that
treats warnings as errors. Its action passed it by an escaped label,
which Swift, 6.2 as much as 6.3, warns needs no backticks; at a call site
onlyinoutand_take them. - A variable named
hashValue, orphase,isRefreshingorisStalein
a query, a refetchable fragment's arguments among them, orsubscription
in a subscription, is an error at the variable that asks to rename it.
Each became a property of the value that took the place of the one the
runtime gives every such value:isStaleread the variable where a view
meant the handle's state, without a word when both wereBool, and
hashValuestood besideHashable's, so the generated code did not
compile. Swift tellsrefetch()andretry()from a property by the
call, so variables of those names still compile. - A mutation's payload field named
fieldscompiles. Its optimistic
builder collected the response in a localfields, whichif let fields
hid; the local takes a name none of the builder's fields binds,
fields2. - A field or a variable named
Selfcompiles beside@throwOnFieldError
and@catch. Thecaughtcheck they give a lens built the lens as
Self(anchor:), which the member hid from that lens and from every lens
nested in it; it builds it as.init(anchor:). Where a body still
reaches a lens's own static member throughSelf, as a refetchable
fragment'srefetch()and a connection's members do, a field named
Selfin that lens or in one around it, or a variable of that name, is
an error at the name. - A fragment, a query, a subscription or a refetch query named like a
Swift keyword, asfragment classor@refetchable(queryName: "each"),
compiles. Its name was written bare wherever it stood for a type,
public struct classortypealias Query = each; a type a document
names is escaped wherever the generated code spells it, as a property
is. - A mutation named like a Swift keyword, or like what its action declares
or calls (Op,callAsFunction,commitoroptimistic), compiles,
and so does a mutation with a variable named like it, as
mutation Favorite($Favorite: ID!). The action extended
MutationAction where Op == Favoriteand spelled the mutation in its
signature and its call,Favorite(Favorite: Favorite), where the
parameter, the action's own members or its type parameter took the
name's place. It extendsFavorite.Action, so the mutation is named
once, where only types are looked up, and its body writesOp.Dataand
self.commit(.init(Favorite: Favorite)). - The spread of a fragment named from an underscore, as
..._hidden,
compiles. Its accessor took the owner's prefix before the first
underscore, which is empty, and declaredvar : _hidden; it takes the
fragment's whole name,_hidden. - A fragment named
Datais an error at its name where an operation
spreads it, and so is one namedActionorOptimisticResponsewhere a
mutation does. Inside the operation value those names are its own types,
so the spread's accessor read the fragment as the operation's root lens,
which compiled and returned the wrong lens, or as the mutation's action
or builder, which did not compile.