Baton

Why it is shaped this way

What earns a concept

Baton does not try to solve every problem a data layer meets. It does more with less: few concepts, each with a clear design and a careful implementation. A concept sits in the vocabulary only when real screens demand it and no composition of the existing concepts expresses it. Both clauses. Everything else is a directive the compiler already understands, a value on a handle, or an example.

Baton does not try to solve every problem a data layer meets. It does more with less: few concepts, each with a clear design and a careful implementation. A concept sits in the vocabulary only when real screens demand it and no composition of the existing concepts expresses it. Both clauses. Everything else is a directive the compiler already understands, a value on a handle, or an example.

Why#

GraphQL clients die of configuration. Each real complaint becomes an option: a cache policy here, a type policy there, a merge function, a link in a chain, a flag on codegen. Ten years later the client has four codegen modes, a dozen cache policies and a documentation site explaining which combinations work. The complaints were real; the answers multiplied the surface instead of fixing the mechanism.

Breadth also costs depth. Each concept has to be designed, measured, proved against the oracle and built once per runtime; a client that covers twice the ground does each part half as well, and the half shows in the first frame and in the bugs.

The opposite failure is a client so pure it cannot do the job. Pagination, optimistic updates and error handling are real; a store that refuses them is a demo.

The idea#

The server answers TestAddNote(characterId: "1", text: "Appended", connections: ["Character:1:__TestNotes_notes_connection"]) with tests/add-note-n9.json, and the environment commits it as a server batch.

tests/add-note-n9.json
{
  "data": {
    "addNote": {
      "noteEdge": {
        "cursor": "c9",
        "node": {
          "id": "n9",
          "text": "Appended"
        }
      }
    }
  }
}
A lens readsand finds
notes · character.notes.edges.2.node.textAppended
notes · character.notes.edges.3.node.textnull

The store after the step, scripts/connections-inserted.store.json

Character:1Character
__TestNotes_notes_connectionCharacter:1:__TestNotes_notes_connection
id"1"
name"Rick Sanchez"
notes(after:"c2",first:2)Character:1:notes(after:"c2",first:2)
notes(first:2)Character:1:notes(first:2)
Character:1:__TestNotes_notes_connectionNoteConnection
__connection_next_edge_index1
edges[ Character:1:notes(first:2):edges:0, Character:1:notes(first:2):edges:1, Character:1:__TestNotes_notes_connection:edges:0 ]
pageInfoCharacter:1:__TestNotes_notes_connection:pageInfo
totalCount5
Character:1:__TestNotes_notes_connection:edges:0NoteEdge
cursor"c9"
nodeNote:n9
Character:1:__TestNotes_notes_connection:pageInfoPageInfo
endCursor"c2"
hasNextPagetrue
Character:1:notes(after:"c2",first:2):edges:0NoteEdge
cursor"c3"
nodeNote:n3
Character:1:notes(after:"c2",first:2):edges:1NoteEdge
cursor"c4"
nodeNote:n4
Character:1:notes(after:"c2",first:2):pageInfoPageInfo
endCursor"c4"
hasNextPagetrue
Character:1:notes(first:2)NoteConnection
edges[ Character:1:notes(first:2):edges:0, Character:1:notes(first:2):edges:1 ]
pageInfoCharacter:1:notes(first:2):pageInfo
totalCount5
Character:1:notes(first:2):edges:0NoteEdge
cursor"c1"
nodeNote:n1
Character:1:notes(first:2):edges:1NoteEdge
cursor"c2"
nodeNote:n2
Character:1:notes(first:2):pageInfoPageInfo
endCursor"c2"
hasNextPagetrue
Note:n1Note
id"n1"
text"Wubba lubba dub dub"
Note:n2Note
id"n2"
text"Portal gun needs charging"
Note:n3Note
id"n3"
text"Get Schwifty"
Note:n4Note
id"n4"
text"Avoid the Citadel"
Note:n9Note
id"n9"
text"Appended"
client:rootQuery
character(id:"1")Character:1
node(id:"1")Character:1
client:root:mutationMutation
addNoteclient:root:mutation:addNote
client:root:mutation:addNoteAddNotePayload
noteEdgeclient:root:mutation:addNote:noteEdge
client:root:mutation:addNote:noteEdgeNoteEdge
cursor"c9"
nodeNote:n9
Plate 1. A list edited by a directive on a payload: a composition of connection, plan and store, not a concept of its own.

The concept inventory is closed and short: schema, document, directive, lens, record, store, plan, operation value, connection, environment, transport, phase. A feature must be a composition of these, or a Relay directive the compiler compiles into a plan. A new concept must pay for itself across many features, and it enters through this test:

  • Real screens demand it. Screens people build, not screens a completeness argument names.
  • No composition of the existing concepts expresses it.

When the test passes, the concept gets a name in the terminology before it gets a type in code, and a principle file if it constrains the others.

Consequences#

  • Subtraction is a contribution. A concept that stops paying for itself is removed before 1.0.
  • Leaving a problem unsolved is a fair answer: the app, an example or the watch list can own it. A half-built feature in the library is not.
  • Requests that fail the test become examples: here is how to do that with what exists.
  • Page-based pagination, imperative store updaters, a binary wire format and offline sync are on a watch list with triggers, not in the plan.
  • The terminology is the gate: nothing ships that is not named there.

Not this#

  • A codegen option to fix an ergonomic complaint.
  • A per-type or per-field policy object.
  • Two ways to do the same thing because the second is more convenient.
  • A feature "for completeness."

See Relay's words for where the names come from.

Spelled today#

The inventory is closed as listed above, twelve concepts. The terminology marks each of its entries with its place: the concept it is or is a form of, or the concepts it is a composition of. The watch list, each item with the trigger that would promote it, lives in the project's planning notes, which are not published. This section may rot; the rest must not.