Baton

Reference

The specification

The fixtures both runtimes are held to: schemas, documents, recorded responses, the records a store must hold, and the scripts over time.

What a Baton runtime is held to, in files no language owns: the schemas, the documents, the recorded responses, the records a store must hold after them, what a generated lens must read, and the rules that produce all of it, in runtime.md, the contract. The Swift runtime passes these files; a second runtime passes the same files. The principles are The response is the oracle and Two runtimes, one compiler.

The fixtures are server responses, not tests: a change to the store, the tokenizer or the record layout is frozen under them, and none is edited to fit code. A new case adds files; a reviewed change to identity or layout rewrites the dumps (BATON_BLESS=1 swift test writes .store.json files and the documents; BATON_BLESS=1 cargo test in compiler/ writes sources/ from the markers).

Layout#

  • rickandmorty/: the public API's schema, three documents and their responses.
  • fate/: responses recorded from the server of fate's GraphQL template, with the schema it exports, proven by the Swift test target BatonFateTests and by the Kotlin harness's jvmFateTest; fate/README.md says how they were made.
  • tests/: the responses the test documents read, recorded or shaped by hand to say one thing each, with the dump beside each one the oracle reads, the schema and its client extensions, and baton.json, the configuration the authors' documents compile with: identity, lookups, transient types and fields, and the custom scalars' types by language.
  • sources/: the authors' documents, one file for each document the Swift test target writes, named for its first operation or fragment, as the author wrote it, client directives included. Compiled with tests/baton.json, they give the lenses every case and script reads.
  • tokenizer/: responses that exercise the tokenizer, and malformed.json, the responses that are not well formed, each with the outcome it must have.
  • documents/: the text of every operation the manifest names, with its fragments, as the compiler emits it, compact on one line; written from the generated code and checked against it, so the two cannot drift. This is the text a server receives: the client directives the authors wrote, @catch, @required, @connection, @cacheExpiration, are not in it. The authors' documents a second runtime compiles its lenses from are under sources/, checked against the Swift test target's markers byte for byte by the compiler's tests, which also prove that they plan what the markers plan.
  • scripts/: the scripts, below: steps over time and what each leaves.
  • relay/: cases harvested from Relay's own store tests, with Relay's test schema, its own manifest in this format and the cases where Baton parts from Relay, run by the Swift test target BatonRelayTests and by the Kotlin harness's jvmRelayTest, case for case; relay/README.md says how they are made and what the harvest kept.
  • manifest.json: the sources, the cases and the scripts, below.
  • runtime.md: the contract, one paragraph a rule, each ending with the fixture that holds it or the word unheld.

The manifest#

manifest.json lists the cases. Its fields:

FieldMeaning
formatThe manifest's own version, raised when a field changes meaning: 3. Format 3 adds sources.
sourcesThe authors' documents: directory, the directory of .graphql files, and config, the baton.json they compile with, both under spec/. A runtime's harness compiles its lenses by running batonc over the directory with the configuration.
casesThe cases, below.
scriptsThe scripts' files under spec/, in the order they run, below.

Each case has:

FieldMeaning
nameThe case's name: the path of its response under spec/ without the extension, which also names its dump.
operationThe operation's name in its document.
kindquery, mutation or subscription: which root the response is committed under.
documentThe operation's text with its fragments, under spec/.
variablesThe variables the operation is run with, as JSON. Omitted means none.
responsesThe responses in the order the server sent them: one file, or the parts of an incremental response.
recordsThe store's dump after the responses: every record by key, a map from storage key to value, links as Relay writes them ({"__ref": key}, {"__refs": [key]}), the type as __typename, field errors under __errors, a deleted record as null; keys sorted, one record a line.
completeWhether the responses answer every field the document selects, so the availability check passes on them. Omitted means true.
overrideOptional: the leaves an optimistic response overrides, and the value they read under it; several paths when they are aliases of one storage key.
readsWhat a generated lens reads at each path after the responses: path is the response keys and list indices from the root joined by dots, value the JSON the lens yields, with null for a field it reads as absent. A row's value is the response's leaf unless a rule of the runtime reads it otherwise, a @required null that bubbles or a @throwOnFieldError fragment that throws; such a row carries a note saying which. A custom scalar the test target's baton.json maps to a Swift type is a row of the response's text, compared as the mapped type's value, so 1.50 is the decimal the lens reads as 1.5; a text the type cannot hold reads as null, with its note, as does an element of a list whose elements are nullable. An enum is a row of the response's text too, compared as the generated enum's value, so a value the schema does not declare is the generated enum's case for an undeclared value with that text: Swift's unknown, Kotlin's Undeclared. Rows name only fields the document selects on the object's type; a list of records with null entries has no rows below it, since a lens shows the records. A row may say instead what the read throws, "throws": "requiredField" for a @required(action: THROW) field that is null or "fieldErrors" for a @throwOnFieldError read with errors, or what a @catch read's result is, "result": {"ok": true, "value": …} (the value left out for a lens) or {"ok": false, "errors": [path, …]} with the dotted path of each error; such a row needs no value. A row at the empty path is the operation's own outcome: what it fails with, requiredField when a @required field bubbles to its root or fieldErrors under @throwOnFieldError; without one the operation reads. The cases under relay/ use both; the main cases do not yet.

A runtime proves a case by committing the responses in order under the case's root, comparing its store to records, reading every reads row through the lens its compiler generated for the document, and, where override is given, applying the overriding response as an optimistic layer and reading the overridden leaves. A runtime that cannot bind a case or a read fails the case; it does not skip it.

Scripts#

A case starts from an empty store and commits once. What happens over time, a second page merging, a layer reverted, a root released and collected, a clock advanced past an expiration, a phase moving, is a script: a file under scripts/, listed in the manifest's scripts, holding steps and, after any step, the facts the runtime must then show. Format 2 of the manifest adds them; the cases are unchanged.

A script has a name, the name of its file; whether it runs with an image (an image on a file of the harness's choosing, so that relaunch can open a second store over it); the store's release buffer (ten when omitted) and default expiration in seconds (none when omitted); and steps, each an object with one key naming the step, whose value is an object of the step's arguments ({} for none), and any of the expectation fields beside it. A response is a path under spec/; variables is omitted for none.

StepArgumentsWhat the runtime does
commitoperation, variables, response (or responses, the parts of an incremental response)Commits the response as a server's response to the operation, through the door, under the root of the operation's kind: complete, so an omitted field is malformed. A query is fetched by the environment and a mutation committed by it, the transport answering with the response; a subscription's events come by event.
payloadoperation, variables, responseCommits the response as a payload committed by hand: part of the selection may be absent.
optimisticoperation, variables, response, asApplies the response as an optimistic layer of the mutation, named as for later steps.
resolvelayer, responseThe server's answer to the layer's mutation replaces the layer in one batch.
revertlayerReverts the layer, as a failed mutation does.
attachoperation, variables, policy, asMakes the operation's handle in the script's environment with the fetch policy (storeOrNetwork, the default, storeAndNetwork, networkOnly, storeOnly), retains it, and names it as. The transport answers the fetch this makes from response when the step gives one, and fails it with failure (transport, request, malformed) when the step gives that; with neither the fetch stays in flight.
answerhandle, response or failureAnswers the handle's fetch in flight, or fails it.
refetchhandle, response or failureRefetches the handle, answered or failed as answer is.
retryhandle, and response or failure or neitherRetries the handle after a failure, answered or failed as attach is.
releasehandleEnds the handle's retention.
collectRuns a collection pass now.
advancesecondsAdvances the store's clock, and the wall clock the image keeps ages by.
invalidateMarks everything stale, as Environment.invalidate() does; the refetches it starts stay in flight for answer.
revalidateAs Environment.revalidate(); its refetches stay in flight as well.
checkoperation, variablesRuns the availability check for the operation.
relaunchEnds the environment and opens a second store over the same image, in the same process; later steps run in it.
eventhandle, response or failure or complete (true)A subscription handle's stream delivers an event, fails, or is completed by the server.
activevalueSets the environment's activity.
endEnds the environment.

The failures are a transport's own (transport), the server's errors and no data (request), and a response with neither data nor errors (malformed). The expectations, each optional, each compared after the step and after a collection pass it scheduled has run:

FieldMeaning
recordsThe path of the store's dump, as a case's.
readsRows as a case's, each with the handle whose data the lens reads, or with the operation and variables of a lens made by hand over the root of the operation's kind.
notifiedThe fields the step's batches notified, as [{"record": key, "field": storage key}], and nothing else was notified, among the fields that held a value before the step. [] says the step notified nothing.
phaseA handle's phase: {"handle": name, "phase": "loading" or "ready" or {"failed": kind}}, where kind is a failure's kind, fieldErrors, requiredField, missingData or gone; with isRefreshing and isStale beside it when they matter. A list of such objects compares several handles; so for fetch and stream.
fetchA handle's fetch: {"handle": name, "fetch": "idle" or "inFlight" or {"failed": kind}}.
streamA subscription handle's stream: {"handle": name, "stream": state}, the state idle, connecting, open, waiting, or {"ended": kind or null}; with events (the count) and resumptions beside it when they matter.
answerBeside a check step, the check's answer: memory, image or miss. As a word it is this expectation; as an object it is the answer step.
records_heldThe record keys the store holds, the roots among them, sorted.
eventsThe log's events during the step, its expectations' reads included, as their names in order, with the value-free fields a name carries ({"fetchFailed": {"operation": name, "kind": kind}}); a bare name compares the name alone. The image's events are left out, since their timing is the writer's.
sentThe requests the transport received during the step, in order and no others: [{"operation": name, "body": text}], the body the standard encoding writes for each, compared as text byte for byte.
errorWhat the step threw, as a failure's kind or fieldErrors or gone, when the step is expected to throw (a refetch failed; a fetch after the end). A step that throws without it fails the script, except a revert, whose failure is the point. A storeOnly attach without data throws nothing: its phase reads {"failed": "missingData"}.

A runtime proves a script by running its steps in order in one environment over one store, through a transport the harness scripts from the steps, and comparing every expectation as it comes. The clock is the store's: a runtime whose store cannot be told the time cannot run advance and fails the script. Timing that is not the store's, a subscription's backoff, is not scripted: a waiting stream is compared as waiting, not at an instant. A runtime that cannot bind a step fails the script; it does not skip it. The rules a script holds are named in runtime.md by the script's name.