Repository navigation
0.8.0 Back Straight
The spine: the architecture the decision records describe, built. A
handle's fetch and a subscription's stream are values beside its phase,
and a failure says its kind; every write is a batch through one door; the
store owns the roots that keep records alive, stamps their ages at the
commit and collects from them alone; an environment ends; and the keys a
session renders are its store's. Around it, the shapes a production schema
has read as Swift types (enums, input objects, mapped scalars, configured
identity, client fields), the transport has one verb, the image evicts by
launch and keeps the rows a partial response did not name, and the
compiler validates, prints and reports what it compiled.
- The runtime reads a plural link out as values:
values,requiredValues,
caughtValuesandcaughtRequiredValueson an anchor build one value per
linked record at the read, for the@inlinefragment the compiler is
learning to emit.FieldErrorsand everyMappedScalarareHashable,
so a value holding a caught field or a mapped scalar can be. A mapped
scalar type of the app's own that was notHashablemust become it. - A response with a few of a record's fields no longer empties the
record's row in the image of the rest. The writer replaced every row with
the commit's snapshot of the record, which for a record memory had not
read from the image held only what this launch's responses wrote: a
header fetched before a screen left the screen's next check a miss. The
snapshot of a record memory has not read is now merged into its row, the
response's cells over the row's; a record the check has read replaces
its row as before, and so does a deleted one. Recorded in
docs/decisions/the-image-is-sqlite.md. - An image's
close()is final. A commit or a read that reached a closed
image opened its file again and took it back from the next environment's
image, which then ran without one or, in a debug build, tripped the
assertion that one image holds a file. A closed image drops what is
queued after and misses every read;removeAll()still deletes the
file. Recorded indocs/decisions/an-image-belongs-to-one-store.md. - An operation's text is printed compact, with Relay's printer's own
option: no newline, indentation or optional space, a comma between
items, strings as they are. The test target's 115 operations hold
61,797 bytes of text where they held 83,885, and the deepest realistic
one 47% of what it held; the 65 KB operation of issue 36 that a server
refused goes out at about half. Every persisted id changes with the
text, so a team with registered ids regenerates the file and registers
again;batonc printandspec/documentsshow the compact text.
Recorded indocs/decisions/operation-text-is-printed-compact.md. - A connection on the query root comes back from the image. The
availability check hydrates the root a waited field at a time, and the
connection's client link, Relay's handle key on the root, was never among
them, so a reopened store read the connection as ready and empty while the
same connection under a record came back whole; the check now reads the
root's cell for the link before walking the connection. Issue 35. - An operation's plan declares each distinct selection once, as a static
member with its type stated, where it was one nested expression that
copied a fragment at every spread. A union inside a union no longer
exhausts the Swift compiler: the case of issue 34 that was killed at
12 GB compiles in 1.7 s and 0.24 GB, and its plan holds 82 selections
where it held 2,258. Recorded in
docs/decisions/a-plan-declares-each-selection-once.md. - An object under an interface or union takes its type from its
__typenameby a byte comparison with the names the plan lists, where
the ingest made a string and took the registry's lock for every object;
an escaped or unlisted name still asks the registry. A page of 899 union
results ingests in 508 µs against 541, inBENCHMARKS.md. - The compiler's tests fence the bytes of generated code per accessor line
at 120 over the goldens, the budget 0.1.0 named and never enforced; the
goldens stand at 106.1. - A part of a deferred response that names a place no earlier part created,
or a label the plan does not know, is logged aspartDroppedwith its
response path, where it was dropped without a word. - An image over its size limit evicts instead of starting over: the rows
of launches before the last go first, then the last launch's, and the
file shrinks; only a file still over the limit with nothing left to evict
starts again. Recency is the launch's, which the rows already record; no
rule per type. Recorded indocs/decisions/the-image-evicts-by-launch.md. - CI compares the benchmark suite's deterministic counts, the notifications
a commit path fires and the events a commit logs, with
benchmarks/counts.txt:swift run -c release BatonBenchmarks --counts
prints them, onecount <name> <value>a line, and a change to them is a
diff to review where a timing would be noise. - A recipe,
docs/recipes/discover-once.md: a subject discovered once by
its natural key and refreshed by id throughnodes(ids:), the pattern an
app with external keys needs; the GitHub sample refreshes its rows that
way from the toolbar. - A recipe for derived state outside views,
docs/recipes/derived-state.md:
a model derives its value inside anObservationsclosure over the
lenses it reads, and no commit signal is added; recorded in
docs/decisions/derived-state-is-observed-not-signaled.md. List.empty, the value a view substitutes for a nullable list it reads
as empty:fragment.reviewRequests?.nodes ?? .empty. A nullable list
still reads asList?, since the server's null and its empty list
differ.Record,Value,Slot,TypeID,OwnerandMembersare generated
code's interface, behind@_spi(Generated), now that no hook hands them
out; an app's own files see lenses, handles, the environment, the log,
transports and persistence.- The GitHub sample keeps its store in an image across launches and signs
out from the toolbar: the environment ends, the image's file is removed,
and a new environment takes over, as the README describes. - Two recipes:
docs/recipes/uikit.md, a handle held by a view controller
and rendered throughObservations, with a cell over a lens; and
docs/recipes/porting-from-relay.md, Relay's words beside Baton's, what
differs on purpose, and what is not ported with its reason. - A recipe for previews and tests,
docs/recipes/testing.md: a fixture
committed as a response, recorded responses, a held mutation and a driven
subscription, the log in tests, and a bug report's dump as a fixture. Environment.fetchhas one spelling, the operation value's. The one by
type and variables, which returned the uncaught field errors as an array
beside the other's throwing under@throwOnFieldError, is gone: a
fetch's field errors are read where a view reads them, from the data,
and each one no@catchhandled is afieldErrorevent of the log.- The environment logs.
Environment.logis one closure called with each
LogEvent, a value-free enum of names and counts: a fetch started,
completed with its duration or failed with its failure's kind; a commit
with its kind and the slots it changed in records that existed; a field error a fetch's response
carried that no@catchhandled, by operation and response path, as
Relay's field logger reports them; the image opened, unavailable,
written with its batches or failed; a field read and never fetched, a value
a reader's type cannot hold, an id naming records of several types, a
@required(action: LOG)field that is null, each by type and field. It
replaces the four hooks,reportMissing,reportUnexpected,
reportAmbiguousIdentityandrequiredFieldMissing, which handed out
records, slots and values. Debug builds print the missing-data cases
untillogis set. - A query or subscription value's
resolution, the handle a view resolved
it to, is the mechanism's: declared behind@_spi(Generated)on the
protocols and in generated code, read throughphase,fetch,
isStaleand the rest as before. Lens.typeNameis gone: a line of generated code per lens and a public
requirement, read by nothing. Format 15.- A fragment no operation reaches, directly or through another fragment,
is a warning at its definition: nothing can read its lens, and the code
generated for it is dead. The test target's fragments now all reach an
operation. batonc validate, the same compilation with no output, for an editor or a
hook;batonc print <Name>, one operation's text and id as the app sends
them; andbatonc generate --check, which writes nothing and names every
output on disk that differs from what it would write, for a team that
commits its generated code. The command's contract, with a Bazel
genruleover it, isdocs/recipes/batonc.md.BatonInspector, a third product of the package for a debug menu:
StoreInspector(environment)is a view over the store, its counts, its
records by type searchable by key, each record's slots with their values
and field errors, and a share button that exports the store in the dump
formatspec/freezes, so a bug report can become a fixture. It reads
and never writes.- The compiler writes a report of what it compiled for a target:
batonc generate --report <file>, andBaton.report.jsonin the build's
output directory under the plugin. Every operation with its kind, source,
id, variables, the fragments it reaches and its text; every fragment with
its type, source, the operations that reach it and its printed
definition; the schema's digest. Deterministic and by name, so a diff of
two reports is the contract's change. The report and the persisted
documents file sit beside the generated Swift in the plugin's output
directory and are not outputs the build bundles, so neither reaches the
app. - A recipe,
docs/recipes/exchange.md, and its sample,examples/Exchange,
for what a production endpoint needs around the transport's one verb: one
replay of an authorization challenge, a bounded retry with a jittered
doubling backoff over a 5xx or a lost connection, a deadline across every
attempt and its waits, and never a second send of a mutation or of a
stream that delivered. The GitHub sample sends through it. BatonTestinggainsScriptedTransport, for an app's tests: answers
from fixtures by operation name or through a responder, mutations held
until the test replies or refuses, subscriptions driven by hand, and the
requests sent listed by kind; andwait(until:), which waits on the main
actor for a handle or a store to settle.- Subscriptions over HTTP, with
graphql-ssein its distinct-connections
mode:URLSessionTransportasks a subscription fortext/event-stream
and yields the payload of eachnextevent untilcomplete, so the
environment'ssubscriptionsmay be the HTTP transport where a gateway
holds no sockets.EventStreamParsersplits the events beside
MultipartParser's parts, whatever the chunking. The single-connection
mode waits for a gateway that cannot do HTTP/2. - A subscription reconnects in its handle. A stream that ends by a failure
while the handle is retained waits and opens again, by a fixed backoff: a
step doubling from one second to thirty, jittered, reset by an event; the
stream's value sayswaiting(until:)meanwhile, andretry()skips the
wait; a request error, the server's refusal of the operation, ends the
stream instead. The handle countsresumptions, the times the stream was opened
again, so an owner that observes it refetches its baseline. The
environment'sisActive, set from the app's scene phase, parks every
retained subscription while false and resumes them when true. - The transport has one verb.
Transport.sendtakes a request and yields
a stream of payloads: one for a query, the parts of a deferred response,
the events of a subscription, so a wrapper wraps one method;payload
reads the one payload of a request that answers once.execute,stream
andSubscriptionTransportare gone, and the environment's
subscriptionsis aTransport. ARequestsays itskindand carries
the operation'sdocument, text or id and never both; the operation's
textandpersistedIDare itsdocument, withtextan optional
convenience. OneEncodingturns a request into the JSON a server
receives, for the HTTP body and the socket's payload alike; the standard
one writesqueryordocumentId, and the built-in transports take
another. They also readcredentialsper attempt, from a closure. Under
Relay'spersistConfiginbaton.jsonthe artifact carries the id,
hashed withMD5,SHA256orSHA1, and no text, andbatonc generate
writes the map from id to text; without it the text and no id. Generated
code of this shape is format 14. - A concrete type's lens under an interface or union sees the conditions
on the interfaces and unions its type satisfies:... on Characterreads
the fields... on Namedselected beside it, under that condition's
@includeand@skip, as Relay's generated types give each concrete
variant every field a matching condition selected. The set condition
keeps its own lens for the types the document does not name. - A variable of an input object type takes a Swift struct generated for
the type, declared once per module in the shared file: a property per
field typed as the schema types it, an initializer with a parameter per
field, nil for a field left absent, andvariable, the object the
request carries. A field name is checked by the compiler where it was
checked by the server before. Generated code of this shape is format 13. - What may reach the image is configured.
baton.json'stransientblock
names types whose records are never written and root fields, as
Query.search, whose cells, storage keys and fetch stamps never are,
since those carry the arguments and variables they were asked with. A
slot linking to a transient record is left out of its row; the next
launch misses on it and fetches. Memory is unaffected. The lists join the
schema's digest, so an image written under another list starts again.
Generated code of this shape is format 12. - Client schema extensions, under Relay's key
schemaExtensionsin
baton.json: files, or directories of.graphqlfiles, that give server
types client fields or declare types the server does not have. A client
field is written bycommitPayloadfor an operation that selects it,
read by a lens like any field, left out of the text and the id a server
receives, and neither waited for by the availability check nor healed.
The plan marks each client field; the extensions fold into the schema's
digest. A non-null client field is refused at its line, and an operation
of client fields alone at its name. The SwiftPM plugin regenerates when
an extension changes. Generated code of this shape is format 11. - A schema enum reads as a Swift enum generated for it, declared once per
module in the shared file: a case per value the build knows, spelled as
the schema spells it, andunknown(String)for a value it does not, so a
schema's growth never fails a read. The conversion cannot fail, so the
accessor keeps the schema's nullability; a null on a non-null field reads
asunknown("")and is reported. Variables and optimistic responses of
an enum type take the enum. A schema enum named like a fragment or an
operation is a name error at the document; one named like a shared enum
or a standard library type takesEnumafter its name. Generated code of
this shape is format 10. - A custom scalar reads as the Swift type
baton.jsonmaps it to, under
Relay's keycustomScalarTypes:"Decimal": "Foundation.Decimal". The
type conforms toMappedScalar, an initializer from the scalar's text
and the text back;Decimal,Date,URLandUUIDconform, each with
one format. The store keeps the text; the accessor converts at the read
and says the conversion can fail: optional wherever the schema puts the
field, non-optional and throwing under@requiredor
@throwOnFieldError, aResultunder@catchwhose failure carries the
conversion's error. A value that does not convert is reported as
unexpected and never reads as a zero. A variable of a mapped type takes
the Swift type and is sent as its text. Generated files import
Foundation. Generated code of this shape is format 9. - A lookup takes several arguments.
baton.json'slookupsname the
arguments that carry a composite key,"arguments": ["base", "quote"],
one per field of the type's key in its order;argumentstays the
spelling for a key of one field. The compiler refuses a lookup whose
arguments do not match the type's key, and a lookup without a type probes
only the types one value keys. The runtime composes the arguments' values
into the record's key as the ingest composes the key fields, so a cached
Quotesatisfiesquote(base:, quote:)before it is fetched. Generated
code of this shape is format 8. - Identity is configured.
baton.json'sidentitynames the fields that
key a record of each type: adefaultlist,["id"]unless written, and
typesentries for a type or for an interface, whose implementers take
it. A key is own scalar fields, in order, several of them for a composite
key,Quote:base:quote, each value escaped so that no two lists of values
meet; a key does not rename, since it is the values at the write. The
compiler selects the key fields wherever the type is read, as it selects
id, refuses a configuration naming a field a type lacks or cannot key
by, and the plan names each type's key; the ingest keys a record once
every key field is read, looking ahead past a link as it did forid. A
configuration other than the default joins the schema's digest, so an
image keyed the old way is a miss and not a merge. What names a record by
one value,@deleteRecord,@deleteEdge, a lookup without a type and
the image's forget, reaches single-field keys only. Generated code of this
shape is format 7. - An owner settles an
@includeor@skipcondition once, as it resolves
a key with variables once. Generated code declares each condition as a
constant,Guards.<variable>_<value>, that the plan's fields and the
lenses' reads both name, and an accessor's read tests the owner's answer
rather than looking the variable up by name. Generated code of this shape
is format 6.Guardsjoins the names the compiler keeps: a fragment or
operation of that name is an error when the module has a condition, a
field of that name where its lens tests one, and a variable of that name
where its operation tests one, as withSites; a nested lens that would
take the name isGuardsLens. - A resolved variant carries the lists its walks need, made once at the
resolution: the fields a response is read by and the ones a complete
response must carry, the fields the availability check waits for, the
connections' client links the check walks for merged pages, and the
links the collector follows. A walk tests no field for what it is; a
field's origin, the server, a@deferlabel or the client, is one
attribute of it. - An anchor's third word is the record its fragment starts at. A fragment
spread enters its record as the anchor's origin, and a connection below
it, however many links down, paginates with that record's id, as its
fragment's query takes it; before, it passed the id of the record it
hung from, which a connection one link below its fragment's type got
wrong. Generated code of this shape is format 5. The test schema's
Notegainsauthor: Characterfor the fixture. - Membership comes from the response. Whether a type satisfies an
interface or union condition is a table by the type's number, filled
from the sets the build compiled and from what responses say in Relay's
__isXfields, which the plan now carries; a type condition in a lens
is an array load where it was a hash per read. A record of a concrete
type the build did not list, a type the schema gained after the build,
takes its variant from the answers: the fields under each condition the
response says it satisfies, with the ones every type reads, settled once
per type and per set of conditions. A condition every compiled member
satisfies is a condition still, since it says nothing of a type the
build did not list. The image keeps what responses taught for the next
launch. A lookup without a type probes the members the build compiled.
Generated code of this shape is format 4; the image's format is 6. - The plan names the field that keys a record of a type,
id, and the
ingest reads that key and knows no field by name; a refetch reads the
owner's id from the slot its descriptor names. A storage key leaves a
null argument out, as Relay's does:notes(after:null,first:2)is
notes(first:2), whether the null is a constant in the document or a
variable given null; an argument that is an object or a list keeps its
nulls inside. Generated code of this shape is format 3, and an image is
format 5: both start again. The decision is
A storage key leaves a null argument out. - A change set carries where each record's id starts in its key, so the
store makes the record from it and asks the registry nothing per record
it creates; an entity's key,Type:id, is built in one place, and what
names a record by a bare value, a deletion, a lookup or the image's
forget, reads the value part the one way it was written. - The plan IR carries a field's and a variable's type as one recursive
shape, a named type or a list of a type with nullability at every level,
built once in the lowering for the reader side and the normalization
side alike. With it, a list whose elements the schema types nullable
reads as an array of optionals,[String?], and a null element reads as
nil where it was dropped before; a list of non-null elements reports an
element it cannot hold, a null or a value of another type, once, as a
scalar reports a value it cannot hold, and leaves it out. A variable
typed as such a list is a property of the same shape. Generated code of
this shape is format 2: code of format 1 fails to compile at its marker,
with a message that says to rebuild. The decision is
A list's null elements are typed as the schema says. spec/manifest.jsonlists the cases a runtime is held to: each a
document, its variables, the responses in order, the store's dump after
them, and what a generated lens reads at each of a set of paths, in a
language-neutral formspec/README.mdspells out. The documents the
manifest names are underspec/documents/, written from the generated
code and checked against it. The oracle tests run the manifest's cases
and read every row through the generated lens, beside the plan walk they
ran before.- The keys a session renders from its variables, one per id looked up and
per cursor paged past, are numbered by the store that renders them, in
Keys, where the process's registry numbered them for its own life: the
registry numbers the keys the build names, and nothing a session produces
is kept in a table of the process. A text has one slot in a store: a
rendering whose text the build names as a constant takes the constant's
slot, and a constant the build names after the store rendered its text is
adopted when the store next resolves, checks or commits: the two slots
become twins, the records' values are copied across, and every write
lands in both. A plan is resolved for a store,Plan.resolve(_:in:), and a change
set made from the resolution is committed into that store; the image's
writer names a row's slots through the committing store's keys.
Slot.storageKeyis gone: a report names a slot through
Store.storageKey(of:). - The image sweeps its names with its rows: at a launch's first batch,
after the rows that aged out go, the names no row uses any more are
deleted and their ids used again, so the file's table of names is
bounded by its rows, and the limit of 65,536 names, past which an image
started again, is gone. The image's format is 4; a file of format 3 is a
miss and starts again. - The collector frees the keys a session rendered once nothing can name
them: a resolution and a lens's scope hold the numbers they took while
they live, an optimistic layer and a row waiting for the image keep
theirs, and the rest go with the records the pass sweeps, their numbers
used again for the next renderings, lowest first, and the image told to
forget their names. A key a record's row was read under stays numbered
while the image lives, as the record reads its row once; a root field's
is freed with the rest and read from the image again. After a long
session whose roots left, the store's table is the size it was at the
start. - The image's file is protected at creation:
Persistence(url:version:sizeLimit:protection:)and thename:form take
aFileProtectionType, which Apple's SQLite gives the file and its
write-ahead log; an image made under another class starts again. A file
that cannot be taken, locked or full, is waited for, not discarded: the
writer keeps its work for the next commit or read, where a lost batch
marked the image behind memory and the next open emptied the cache, so
every background refresh on a locked device would have emptied it. Work
that outgrows 50,000 rows is dropped and the image starts again. A marker
beside the file has the next open finish a removal a crash interrupted or
a locked device put off; the log is deleted before the database file. - What the image was told to forget and has not yet dropped is kept behind
the image's interface, as one value the store holds, where three sets of
the store's tracked it. - The image's row is one codec: the tags, the writing of a record's row and
a root field's cell, and the reading back, in one type over bytes, where
the writer was the disk's and the reader the hydration's. The disk keeps
SQLite and knows nothing of the layout; a record's snapshot is the
record's own type, so the record names the image no more. - An image belongs to one store. It is made for the store and lives as long
as it: the environment's end closes it and gives the file back, and the
next environment makes its own, on that file or another. The count of
removals a store noted when it was made, which eight of the image's
functions took to fence a store from before a sign-out, goes: an ended
store commits nothing, which fences the same with no count.
removeAll()stays as the deletion of the file after the end, which is
hygiene; what keeps one account's rows from the next is the image's
identity, the account in its path or in itsversion. - An environment ends.
await environment.end()ends the session once and
for good: it cancels every fetch and stream the environment started,
drops the roots, clears every record and closes the image, giving its
file back. An ended store commits nothing, checked at the one door a
payload takes, so a response that lands after the end, a fetch awaited in
a task of the app's own or a mutation the server applied, reaches neither
memory nor the image; a handle still held reads
.failed(EnvironmentError.gone)and tells its observers; every later call
on the environment throws the same error. A store dropped without an end
clears its records when it is deallocated, so records that link to each
other are freed with it. - A list of links that only grew, a page appended or prepended to a
connection, drops no link, so it schedules no collection pass of its
own; a list that changed otherwise still does. The lifetime step's
numbers are inBENCHMARKS.md: a pass over 50,000 records takes 2.8 ms,
and a page costs about a millisecond more from the eleventh on, the pass
that follows the root the release buffer pushes out. - The heal. A read that finds a slot the store never received, which a
retargeted link leaves behind, marks the owning operation stale and
refetches it if a holder allows the network, once per fetch of that
operation; a field still missing after the heal's own refetch is reported
as unexpected and healed no further. Missing data was reported and left
to the next attach. - The fetch policy is the holder's. An attach's policy stays with the
retention it makes, so a fetch the runtime starts later asks whether any
holder allows the network:invalidate()no longer refetches an
operation attachedstoreOnly.Environment.revalidate()refetches the
retained operations that are stale or whose last fetch failed, where a
holder allows the network, and marks nothing: for an app's return to the
foreground or a connection regained. - The commit stamps an operation's age. Every server write dates its
operation, whoever asked for it: a handle's fetch, a refetch, a page,
Environment.fetchorcommitPayload, and a deferred response when its
stream completes; the age is the root's, in the store, and a handle reads
it asfetchTime. A query just written that nothing retains waits in the
release buffer, as Relay's does. Data with no known age is stale wherever
an expiration applies, in memory as it already was from the image: an
operation never fetched whose data another operation brought refetches
under astoreOrNetworkattach when a cache expiration applies. The store
no longer names its environment: a lens fetches through its owner, and
the phases a commit settles are told through a hook. - The store owns what keeps records alive. A root, an operation's selection
and the record it starts from, is the store's, with how many hold it;
retain()returns aRetention, a token whose end releases, and
release()is gone:@Query's storage holds the token for the view's
life, and a model or a view controller holds one in a property. The
release buffer isStore(releaseBufferSize:), andcollect(),
rootCountandcollectionsare the store's;Environmenttakes no
buffer size. The collector marks from the roots, the optimistic layers and
the image's write queue, and from nothing else, and runs when a root left
or a commit dropped a link: a screen that stays up and refetches is
collected without another view going away, and a release that only moves
a root into the buffer runs no pass. - A query states how old its data may be in its document:
@cacheExpiration(seconds:), the first directive that is not Relay's,
which the compiler emits as a constant of the operation and leaves out of
the text a server receives. The handle reads it with the operation's age;
an operation that states none takesStore(cacheExpiration:), the
default given when the store is made. The settable
Environment.queryCacheExpirationis gone: a handle has no one owner, so
a number set at run time was whoever set it last. The directive is
refused on a mutation or a subscription, and with a variable. - A response that omits a field the operation selected fails the fetch as
malformed, anIngestErrornaming the field and its type, where before
the handle settled ready and the first read of the field reported missing
data: a server answers every field it was asked for, so the plan was not
made for the response. A deferred field is expected in its part, and an
optimistic response or a payload committed by hand may carry part of the
selection, as before. Environment.commitPayload(operation, payload)commits a payload for an
operation that some other road delivered, a REST response, a socket's
tick, a preview's fixture or a test's seed, through the same door a
fetch's response takes: the records merge, the connections and the edge
directives apply, and the image is written. A payload may carry part of
what the operation selects.- One door from a payload to slots. A query's fetch, a deferred stream, a
mutation, a subscription's event and a page each normalized and
committed on their own; every payload now passes through one function of
the environment, which reads it by the plan off the main actor and
commits it as a server batch, and each entrance keeps only its rule for a
caller cancelled on the way: a query checks before the commit, a mutation
does not, since the server applied it, a subscription commits until its
task ends. An ended store will refuse at that door, the commit will stamp
the operation's age there, and a report will be raised there. - The ingest returns what it normalized and no more: a change set holds
what the store writes, and the parts an incremental response announces,
whether more follow, and the errors of a part the server could not
deliver are read beside it rather than carried through the commit. The
assembly of a stream of parts into change sets, by announced id or by
path and label, leaves the environment's fetch for a delivery of its
own; the socket's frame reader leaves the ingest for the transport. - Nothing outside a batch writes a record. A commit is a batch of one of
three kinds: server, written to the image; optimistic, whose undo stays
with its layer; and local, the runtime's own writes, a page's loading
flag, a lookup's link bound, a link repaired, a cell filled from the
image, a deferred slot cleared, which were written one by one and
notified as they happened. The availability check's writes are one local
batch notified once the walk is over, so an observer that asks the store
a question from inside a notification sees a whole walk, never one in
progress; the store no longer keeps the walk's state to guard against
that. The connection merge and the edge edits are functions over a batch
in a file of their own. - A subscription handle's stream is a value beside its events.
stream
reads.idle,.connectinguntil the first event,.open, or
.ended(failure), withnilfor the server's completion and aFailure
for an error that ended it;isActiveis connecting or open. A
subscription has no loading, since it has no data of its own to wait for,
so this is not a phase. - An error keeps its
extensions.FieldError.extensionsis the server's
extensionsfor the error as a JSON value (Variable), kept in memory
and in the image, so an app branches on the server's code and never on
its message;GraphQLErrors.errorscarries a request error's errors
whole, withmessagesstill readable. The image's row format moves to
3, so an image written before starts again, as a cache does. - A handle's fetch is a value beside its phase.
fetchon a query value
and onOperationHandlereads.idle,.inFlight, or
.failed(failure, at:)with the failure and when it failed, so a fetch
that fails behind data is seen while the phase stays ready, by every view
of the handle; the next response replaces it.isRefreshingis data
present and a fetch in flight. The failure is aFailure, one of a closed
set of kinds: the transport's, carrying what the transport threw
unchanged; a request error (GraphQLErrors); a malformed response
(IngestError); the environment's (EnvironmentError). Field errors are
not among them. Whatphase's failed case andrefetch()carry is the
error as thrown, as before. Deriving the phase when it is read was decided
behind a gate, and the gate failed on the bench, so the phase stays
stored.