Start here
One screen, end to end
A screen followed from the fragment beside its view to the frame it renders: the operation the compiler assembles, the request, the response, the records, the reads, the one view that re-renders, and the image on disk.
A character's header: a name, a status, a species, a picture, and where the character is from. Small enough to follow every step, and every step on this page is program output. The compiler compiled the documents below when the page was built; the response is one the specification recorded from the API; the records are dumps the runtime wrote under the specification's fixtures, and the notifications and the phases are what the specification requires of both runtimes.
1. A fragment beside the view#
The view declares the fields it reads, next to its body, and takes a lens as its one input. It cannot read a field it did not declare, because the lens has no accessor for one.
CharacterHeader.swift
import Baton
import SwiftUI
struct CharacterHeader: View {
@Fragment("""
fragment CharacterHeader_character on Character {
name
status
species
image
origin { name }
}
""")
var character: CharacterHeader_character
var body: some View {
VStack {
Avatar(url: character.image)
Text(character.name ?? "Unknown").font(.largeTitle)
Text("\(character.status ?? "?") · \(character.species ?? "?")")
if let origin = character.origin {
Text("From \(origin.name ?? "somewhere")")
}
}
}
}CharacterHeader.kt
package app
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import baton.Fragment
@Fragment("""
fragment CharacterHeader_character on Character {
name
status
species
image
origin { name }
}
""")
@Composable
fun CharacterHeader(character: CharacterHeader_character) {
Column {
Avatar(url = character.image)
Text(character.name ?: "Unknown", style = MaterialTheme.typography.headlineLarge)
Text("${character.status ?: "?"} · ${character.species ?: "?"}")
character.origin?.let { origin ->
Text("From ${origin.name ?: "somewhere"}")
}
}
}origin { name } reads through a link to another record, and the lens makes that a nested lens.2. One query for the screen#
The screen declares one query and spreads the header's fragment into it. The parent never lists the header's fields; it hands the header its lens through the spread's accessor, characterHeader.
CharacterScreen.swift
import Baton
import SwiftUI
struct CharacterScreen: View {
@Query("""
query CharacterQuery($id: ID!) @cacheExpiration(seconds: 300) {
character(id: $id) { ...CharacterHeader_character }
}
""")
var character: CharacterQuery
var body: some View {
switch character.phase {
case .ready(let data):
if let character = data.character {
CharacterHeader(character: character.characterHeader)
}
case .loading:
ProgressView()
case .failed(let error):
ErrorView(error) { character.retry() }
}
}
}CharacterScreen.kt
package app
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import baton.Phase
import baton.Query
import baton.rememberQuery
@Query($$"""
query CharacterQuery($id: ID!) @cacheExpiration(seconds: 300) {
character(id: $id) { ...CharacterHeader_character }
}
""")
@Composable
fun CharacterScreen(id: String) {
val query = rememberQuery(CharacterQuery(id = id))
when (val phase = query.phase) {
is Phase.Ready -> phase.data.character?.let { character ->
CharacterHeader(character.characterHeader)
}
Phase.Loading -> CircularProgressIndicator()
is Phase.Failed -> ErrorView(phase.error) { query.retry() }
}
}@cacheExpiration(seconds: 300) says data older than five minutes reads as stale.3. What the compiler makes of them#
The compiler reads the schema and every document in the module, validates them, assembles the operation with the fragments it reaches, and adds the fields identity needs. This is the text the app will send, printed on one line as the compiler prints it:
- query
CharacterQuery- variables
$id: ID!- spreads
CharacterHeader_character- the text
query CharacterQuery($id:ID!){character(id:$id){...CharacterHeader_character,id}}fragment CharacterHeader_character on Character{name,status,species,image,origin{name,id}}
id to the character and under origin, because both types are keyed by id and the store needs the keys to make the records; the directive on the query stays out of the text, since it is the client's business.For each fragment and each operation it emits a lens: one accessor per declared field, typed as the schema types it.
The lens of CharacterHeader_character, a fragment, reads a record of type Character:
| accessor | reads | as |
|---|---|---|
name | name | a scalar, optional |
status | status | a scalar, optional |
species | species | a scalar, optional |
image | image | a scalar, optional |
origin | origin | a lens of Location, optional |
name | name | a scalar, optional |
name is optional because the schema says so; origin is a nested lens; nothing else exists on the type.And it writes the code. The generated Swift is one line per field plus the data tables the runtime walks a response by; there is no model, no decoder and no cache policy in it.
CharacterHeader.baton.swift
// Generated by batonc. Do not edit.
import Foundation
@_spi(Generated) import Baton
/// Lens for `fragment CharacterHeader_character on Character`.
nonisolated public struct CharacterHeader_character: Baton.Lens {
@_spi(Generated) public let anchor: Baton.Anchor
@_spi(Generated) public init(anchor: Baton.Anchor) { self.anchor = anchor }
@MainActor public var name: String? { anchor.string(Slots.Character.name) }
@MainActor public var status: String? { anchor.string(Slots.Character.status) }
@MainActor public var species: String? { anchor.string(Slots.Character.species) }
@MainActor public var image: String? { anchor.string(Slots.Character.image) }
@MainActor public var origin: Origin? { anchor.linked(Slots.Character.origin).map(Origin.init(anchor:)) }
nonisolated public struct Origin: Baton.Lens {
@_spi(Generated) public let anchor: Baton.Anchor
@_spi(Generated) public init(anchor: Baton.Anchor) { self.anchor = anchor }
@MainActor public var name: String? { anchor.string(Slots.Location.name) }
}
}CharacterScreen.baton.swift
// Generated by batonc. Do not edit.
import Foundation
@_spi(Generated) import Baton
/// Operation value for `query CharacterQuery`.
nonisolated public struct CharacterQuery: Baton.Query {
public var id: String
@_spi(Generated) public var resolution: Baton.Resolution<Baton.OperationHandle<Self>> = .unresolved
public init(id: String) {
self.id = id
}
public static let name = "CharacterQuery"
public static let document: Baton.Document = .text(#"query CharacterQuery($id:ID!){character(id:$id){...CharacterHeader_character,id}}fragment CharacterHeader_character on Character{name,status,species,image,origin{name,id}}"#)
@_spi(Generated) public static let cacheExpiration: Swift.Duration? = .seconds(300)
public var variables: Baton.Variables {
Baton.Variables(["id": Baton.Variable(self.id)])
}
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
@_spi(Generated) public static let plan = Baton.Plan(root: selection0)
private static let selection0: Baton.Selection = Baton.Selection(type: Types.Query, key: [], abstract: false, fields: [
.linked("character", key: .dynamic(Slots.Query.character_bca4f9), plural: false, lookup: Baton.Lookup(type: Types.Character, key: [.variable("id")]), selection: selection1),
])
private static let selection1: Baton.Selection = Baton.Selection(type: Types.Character, key: ["id"], abstract: false, fields: [
.scalar("name", key: .fixed(Slots.Character.name), kind: .string, list: false),
.scalar("status", key: .fixed(Slots.Character.status), kind: .string, list: false),
.scalar("species", key: .fixed(Slots.Character.species), kind: .string, list: false),
.scalar("image", key: .fixed(Slots.Character.image), kind: .string, list: false),
.linked("origin", key: .fixed(Slots.Character.origin), plural: false, selection: selection2),
.scalar("id", key: .fixed(Slots.Character.id), kind: .string, list: false),
])
private static let selection2: Baton.Selection = Baton.Selection(type: Types.Location, key: ["id"], abstract: false, fields: [
.scalar("name", key: .fixed(Slots.Location.name), kind: .string, list: false),
.scalar("id", key: .fixed(Slots.Location.id), kind: .string, list: false),
])
nonisolated public struct Data: Baton.Lens {
@_spi(Generated) public let anchor: Baton.Anchor
@_spi(Generated) public init(anchor: Baton.Anchor) { self.anchor = anchor }
@MainActor public var character: Character? { anchor.linked(anchor.owner.slot(Slots.Query.character_bca4f9)).map(Character.init(anchor:)) }
nonisolated public struct Character: Baton.Lens {
@_spi(Generated) public let anchor: Baton.Anchor
@_spi(Generated) public init(anchor: Baton.Anchor) { self.anchor = anchor }
@MainActor public var characterHeader: CharacterHeader_character { .init(anchor: anchor.entering()) }
}
}
}CharacterHeader.baton.kt
// Generated by batonc. Do not edit.
@file:OptIn(baton.Generated::class)
package app
import androidx.compose.runtime.Stable
import baton.Anchor
import baton.Lens
/** Lens for `fragment CharacterHeader_character on Character`. */
@Stable
class CharacterHeader_character(override val anchor: Anchor) : Lens {
val name: String? get() = anchor.string(Slots.Character.name)
val status: String? get() = anchor.string(Slots.Character.status)
val species: String? get() = anchor.string(Slots.Character.species)
val image: String? get() = anchor.string(Slots.Character.image)
val origin: Origin? get() = anchor.linked(Slots.Character.origin)?.let(::Origin)
override fun equals(other: Any?): Boolean = other is CharacterHeader_character && other.anchor == anchor
override fun hashCode(): Int = anchor.hashCode()
@Stable
class Origin(override val anchor: Anchor) : Lens {
val name: String? get() = anchor.string(Slots.Location.name)
override fun equals(other: Any?): Boolean = other is CharacterHeader_character.Origin && other.anchor == anchor
override fun hashCode(): Int = anchor.hashCode()
}
}CharacterScreen.baton.kt
// Generated by batonc. Do not edit.
@file:OptIn(baton.Generated::class)
package app
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import baton.Anchor
import baton.Document
import baton.Lens
import baton.Lookup
import baton.OperationKind
import baton.OperationType
import baton.Plan
import baton.PlanField
import baton.QueryOperation
import baton.QueryType
import baton.ScalarKind
import baton.Selection
import baton.StorageKey
import baton.Variable
import baton.Variables
/** Operation value for `query CharacterQuery`. */
@Immutable
class CharacterQuery(val id: String) : QueryOperation<CharacterQuery.Data> {
override val variables: Variables
get() = Variables.of("id" to Variable.of(id))
override val type: OperationType<Data> get() = Companion
override fun equals(other: Any?): Boolean = other is CharacterQuery && other.id == id
override fun hashCode(): Int = listOf(id).hashCode()
companion object : QueryType<CharacterQuery, CharacterQuery.Data> {
override val name = "CharacterQuery"
override val document: Document = Document.Text("query CharacterQuery(\$id:ID!){character(id:\$id){...CharacterHeader_character,id}}fragment CharacterHeader_character on Character{name,status,species,image,origin{name,id}}")
override val kind = OperationKind.QUERY
override val cacheExpirationSeconds: Double? = 300.0
override val plan: Plan by lazy { Plan(root = `CharacterQuery-plan`.selection0) }
override fun data(anchor: Anchor): Data = Data(anchor)
}
@Stable
class Data(override val anchor: Anchor) : Lens {
val character: Character? get() = anchor.linked(anchor.owner.slot(Slots.Query.character_bca4f9))?.let(::Character)
override fun equals(other: Any?): Boolean = other is CharacterQuery.Data && other.anchor == anchor
override fun hashCode(): Int = anchor.hashCode()
@Stable
class Character(override val anchor: Anchor) : Lens {
val characterHeader: CharacterHeader_character get() = CharacterHeader_character(anchor.entering())
override fun equals(other: Any?): Boolean = other is CharacterQuery.Data.Character && other.anchor == anchor
override fun hashCode(): Int = anchor.hashCode()
}
}
}
private object `CharacterQuery-plan` {
val selection0: Selection by lazy {
Selection(type = Types.Query, key = listOf(), isAbstract = false, fields = listOf(
PlanField.linked("character", key = StorageKey.Dynamic(Slots.Query.character_bca4f9), plural = false, lookup = Lookup(type = Types.Character, key = listOf(Lookup.Key.Variable("id"))), selection = selection1),
))
}
val selection1: Selection by lazy {
Selection(type = Types.Character, key = listOf("id"), isAbstract = false, fields = listOf(
PlanField.scalar("name", key = StorageKey.Fixed(Slots.Character.name), kind = ScalarKind.STRING, list = false),
PlanField.scalar("status", key = StorageKey.Fixed(Slots.Character.status), kind = ScalarKind.STRING, list = false),
PlanField.scalar("species", key = StorageKey.Fixed(Slots.Character.species), kind = ScalarKind.STRING, list = false),
PlanField.scalar("image", key = StorageKey.Fixed(Slots.Character.image), kind = ScalarKind.STRING, list = false),
PlanField.linked("origin", key = StorageKey.Fixed(Slots.Character.origin), plural = false, selection = selection2),
PlanField.scalar("id", key = StorageKey.Fixed(Slots.Character.id), kind = ScalarKind.STRING, list = false),
))
}
val selection2: Selection by lazy {
Selection(type = Types.Location, key = listOf("id"), isAbstract = false, fields = listOf(
PlanField.scalar("name", key = StorageKey.Fixed(Slots.Location.name), kind = ScalarKind.STRING, list = false),
PlanField.scalar("id", key = StorageKey.Fixed(Slots.Location.id), kind = ScalarKind.STRING, list = false),
))
}
}4. The request#
The view appears, and its handle checks the store. Nothing is there yet, so the environment sends the operation through the transport: the text above, with {"id": "9"} as the variables, as a standard GraphQL-over-HTTP request. Under persistConfig it would send the id of the text instead, and the server would look the text up.
While the request is out, the view reads phase as loading. Here is what a handle shows at each moment, from the specification's script of phases:
A view attaches a handle for TestHeaderQuery(id: "5") under storeOrNetwork, named jerry; the fetch stays in flight.
Phase {"handle":"jerry","phase":"loading","isRefreshing":false}
Fetch {"handle":"jerry","fetch":"inFlight"}
The fetch of jerry is answered with tests/character-header-5.json.
tests/character-header-5.json
{
"data": {
"character": {
"name": "Jerry Smith",
"status": "Alive",
"species": "Human",
"image": "https://rickandmortyapi.com/api/character/avatar/5.jpeg",
"origin": {
"name": "Earth (Replacement Dimension)",
"id": "20"
},
"id": "5"
}
}
}Phase {"handle":"jerry","phase":"ready","isRefreshing":false}
Fetch {"handle":"jerry","fetch":"idle"}
A view attaches a handle for TestHeaderQuery(id: "5") under storeOrNetwork, named quiet; the fetch stays in flight.
Phase {"handle":"quiet","phase":"ready","isRefreshing":false}
Fetch {"handle":"quiet","fetch":"idle"}
A view attaches a handle for TestHeaderQuery(id: "5") under storeAndNetwork, named eager; the fetch stays in flight.
Phase {"handle":"eager","phase":"ready","isRefreshing":true}
Fetch {"handle":"eager","fetch":"inFlight"}
The fetch of eager fails with a transport failure.
Phase {"handle":"eager","phase":"ready","isRefreshing":false}
Fetch {"handle":"eager","fetch":{"failed":"transport"}}
storeAndNetwork is ready and refreshing; when its refresh fails, the data stays and the fetch records the failure.5. The response#
The server answers in the document's shape:
spec/rickandmorty/character-header-9.json
{
"data": {
"character": {
"name": "Agency Director",
"status": "Dead",
"species": "Human",
"image": "https://rickandmortyapi.com/api/character/avatar/9.jpeg",
"origin": {
"name": "Earth (Replacement Dimension)",
"id": "20"
},
"id": "9"
}
}
}id: "9". Five values, two objects, and the two ids the compiler asked for.6. The records#
The response's bytes are decoded straight into record slots, off the main actor, by the plan the compiler emitted, and committed on the main actor as one batch. Hover a field to see where it went:
The response
{
"data": {
"character": {
"name": "Agency Director",
"status": "Dead",
"species": "Human",
"image": "https://rickandmortyapi.com/api/character/avatar/9.jpeg",
"origin": {
"name": "Earth (Replacement Dimension)",
"id": "20"
},
"id": "9"
}
}
}The records
| id | "9" |
|---|---|
| image | "https://rickandmortyapi.com/api/character/avatar/9.jpeg" |
| name | "Agency Director" |
| origin | Location:20 |
| species | "Human" |
| status | "Dead" |
| id | "20" |
|---|---|
| name | "Earth (Replacement Dimension)" |
| character(id:"9") | Character:9 |
|---|
Character:9, Location:20, and the root that links to the character under its storage key, character(id:"9"). The origin slot holds a link, not a copy.7. The reads#
The specification's manifest says what the lens must read at each path after that commit, and both runtimes are held to it:
After the response to TestHeaderQuery(id: "9") is committed, the lens the compiler generated must read:
| path | value |
|---|---|
character.name | "Agency Director" |
character.status | "Dead" |
character.species | "Human" |
character.origin.name | "Earth (Replacement Dimension)" |
and, under an optimistic response that overrides character.name, read "Director" there until the layer is replaced or reverted.
The view's body runs. character.name loads a slot; so do status, species and image; origin follows the link and origin.name loads a slot of the other record. Each read registers with Observation, so the body now depends on exactly six slots of those two records: five on the character, the origin link among them, and one on the location.
8. A field changes#
Later a refetch, another screen's query, a mutation's payload or a subscription's event changes a record. The commit compares each slot before it writes, and notifies only the slots that changed. The specification scripts this on a character with notes:
A view attaches a handle for TestNotesQuery(id: "1") under storeOrNetwork, named notes; the transport answers its fetch with tests/notes-page-1.json.
tests/notes-page-1.json
{
"data": {
"character": {
"id": "1",
"name": "Rick Sanchez",
"notes": {
"totalCount": 5,
"edges": [
{
"cursor": "c1",
"node": {
"__typename": "Note",
"id": "n1",
"text": "Wubba lubba dub dub"
}
},
{
"cursor": "c2",
"node": {
"__typename": "Note",
"id": "n2",
"text": "Portal gun needs charging"
}
}
],
"pageInfo": {
"endCursor": "c2",
"hasNextPage": true
}
}
}
}
}Notified nothing: no observed field changed, so no body runs
The store after the step, tests/notes-page-1.store.json
| __TestNotes_notes_connection | Character:1:__TestNotes_notes_connection |
|---|---|
| id | "1" |
| name | "Rick Sanchez" |
| notes(first:2) | Character:1:notes(first:2) |
| edges | [ Character:1:notes(first:2):edges:0, Character:1:notes(first:2):edges:1 ] |
|---|---|
| pageInfo | Character:1:__TestNotes_notes_connection:pageInfo |
| totalCount | 5 |
| endCursor | "c2" |
|---|---|
| hasNextPage | true |
| edges | [ Character:1:notes(first:2):edges:0, Character:1:notes(first:2):edges:1 ] |
|---|---|
| pageInfo | Character:1:notes(first:2):pageInfo |
| totalCount | 5 |
| cursor | "c1" |
|---|---|
| node | Note:n1 |
| cursor | "c2" |
|---|---|
| node | Note:n2 |
| endCursor | "c2" |
|---|---|
| hasNextPage | true |
| id | "n1" |
|---|---|
| text | "Wubba lubba dub dub" |
| id | "n2" |
|---|---|
| text | "Portal gun needs charging" |
| character(id:"1") | Character:1 |
|---|
The server answers TestNotesQuery(id: "1") with tests/notes-page-1.json, and the environment commits it as a server batch.
tests/notes-page-1.json
{
"data": {
"character": {
"id": "1",
"name": "Rick Sanchez",
"notes": {
"totalCount": 5,
"edges": [
{
"cursor": "c1",
"node": {
"__typename": "Note",
"id": "n1",
"text": "Wubba lubba dub dub"
}
},
{
"cursor": "c2",
"node": {
"__typename": "Note",
"id": "n2",
"text": "Portal gun needs charging"
}
}
],
"pageInfo": {
"endCursor": "c2",
"hasNextPage": true
}
}
}
}
}Notified nothing: no observed field changed, so no body runs
The server answers TestNotesQuery(id: "1") with tests/notes-page-1-renamed.json, and the environment commits it as a server batch.
tests/notes-page-1-renamed.json
{
"data": {
"character": {
"id": "1",
"name": "Rick C-137",
"notes": {
"totalCount": 5,
"edges": [
{
"cursor": "c1",
"node": {
"__typename": "Note",
"id": "n1",
"text": "Wubba lubba dub dub"
}
},
{
"cursor": "c2",
"node": {
"__typename": "Note",
"id": "n2",
"text": "Portal gun needs charging"
}
}
],
"pageInfo": {
"endCursor": "c2",
"hasNextPage": true
}
}
}
}
}Notified Character:1.name
Character:1.name, and the bodies that read it run; a body that read only the notes does not.That is the whole re-render story. No watcher re-executes the query, no snapshot is diffed; a body that read a field is invalidated when that field of that record changes, and at no other time.
9. A write, seen at once#
A mutation's optimistic response is a typed value that goes through the same ingest as a server's answer and is applied as a layer. The layer shows in the turn of the call; the server's answer replaces it; a failure reverts it, and the store notifies only what differs in the end.
The optimistic response tests/rename-1.json for TestRename(id: "1", name: "Rick Prime") is applied as a layer named rename, before any server answers.
tests/rename-1.json
{
"data": {
"rename": {
"character": {
"id": "1",
"name": "Rick Prime"
}
}
}
}Notified Character:1.name
| A lens reads | and finds |
|---|---|
notes · character.name | Rick Prime |
The server's answer tests/rename-1.json replaces the layer rename in one batch.
tests/rename-1.json
{
"data": {
"rename": {
"character": {
"id": "1",
"name": "Rick Prime"
}
}
}
}Notified nothing: no observed field changed, so no body runs
| A lens reads | and finds |
|---|---|
notes · character.name | Rick Prime |
The store after the step, scripts/optimistic-resolved.store.json
| __TestNotes_notes_connection | Character:1:__TestNotes_notes_connection |
|---|---|
| id | "1" |
| name | "Rick Prime" |
| notes(first:2) | Character:1:notes(first:2) |
| edges | [ Character:1:notes(first:2):edges:0, Character:1:notes(first:2):edges:1 ] |
|---|---|
| pageInfo | Character:1:__TestNotes_notes_connection:pageInfo |
| totalCount | 6 |
| endCursor | "c2" |
|---|---|
| hasNextPage | true |
| edges | [ Character:1:notes(first:2):edges:0, Character:1:notes(first:2):edges:1 ] |
|---|---|
| pageInfo | Character:1:notes(first:2):pageInfo |
| totalCount | 6 |
| cursor | "c1" |
|---|---|
| node | Note:n1 |
| cursor | "c2" |
|---|---|
| node | Note:n2 |
| endCursor | "c2" |
|---|---|
| hasNextPage | true |
| id | "n1" |
|---|---|
| text | "Wubba lubba dub dub" |
| id | "n2" |
|---|---|
| text | "Portal gun needs charging" |
| character(id:"1") | Character:1 |
|---|
| rename | client:root:mutation:rename |
|---|
| character | Character:1 |
|---|
The optimistic response tests/set-favorite-1.json for TestSetFavorite(id: "1", favorite: true) is applied as a layer named favorite, before any server answers.
tests/set-favorite-1.json
{
"data": {
"setFavorite": {
"character": {
"id": "1",
"name": "Rick Sanchez",
"favorite": true
}
}
}
}Notified Character:1.name
| A lens reads | and finds |
|---|---|
notes · character.name | Rick Sanchez |
The layer favorite is reverted, as a failed mutation's is.
Notified Character:1.favorite Character:1.name client:root:mutation.setFavorite client:root:mutation:setFavorite.character
| A lens reads | and finds |
|---|---|
notes · character.name | Rick Prime |
The store after the step, scripts/optimistic-resolved.store.json
| __TestNotes_notes_connection | Character:1:__TestNotes_notes_connection |
|---|---|
| id | "1" |
| name | "Rick Prime" |
| notes(first:2) | Character:1:notes(first:2) |
| edges | [ Character:1:notes(first:2):edges:0, Character:1:notes(first:2):edges:1 ] |
|---|---|
| pageInfo | Character:1:__TestNotes_notes_connection:pageInfo |
| totalCount | 6 |
| endCursor | "c2" |
|---|---|
| hasNextPage | true |
| edges | [ Character:1:notes(first:2):edges:0, Character:1:notes(first:2):edges:1 ] |
|---|---|
| pageInfo | Character:1:notes(first:2):pageInfo |
| totalCount | 6 |
| cursor | "c1" |
|---|---|
| node | Note:n1 |
| cursor | "c2" |
|---|---|
| node | Note:n2 |
| endCursor | "c2" |
|---|---|
| hasNextPage | true |
| id | "n1" |
|---|---|
| text | "Wubba lubba dub dub" |
| id | "n2" |
|---|---|
| text | "Portal gun needs charging" |
| character(id:"1") | Character:1 |
|---|
| rename | client:root:mutation:rename |
|---|
| character | Character:1 |
|---|
name; the server's answer resolves it with no further notification, since the value is the same; a second layer applied and reverted leaves the resolved name in place.10. The image, and the next launch#
Every server batch is written behind to an image, one SQLite file, off the main actor. When the app launches again, the handle's availability check reads what memory lacks from the image before the first body runs, so the screen the user left is the screen they get back, before any request.
The server answers TestQuotesQuery() with tests/quote-list.json, and the environment commits it as a server batch.
tests/quote-list.json
{
"data": {
"quotes": [
{
"rate": 1.5,
"base": "BTC",
"quote": "USD"
},
{
"rate": 0.9,
"base": "EUR",
"quote": "USD"
},
{
"rate": 2,
"base": "A:B",
"quote": "C\\D"
}
]
}
}The store after the step, tests/quote-list.store.json
| base | "A:B" |
|---|---|
| quote | "C\D" |
| rate | 2.0 |
| base | "BTC" |
|---|---|
| quote | "USD" |
| rate | 1.5 |
| base | "EUR" |
|---|---|
| quote | "USD" |
| rate | 0.9 |
| quotes | [ Quote:BTC:USD, Quote:EUR:USD, Quote:A\:B:C\\D ] |
|---|
The availability check runs for TestQuoteQuery(base: "BTC", quote: "USD").
The check answers memory
| A lens reads | and finds |
|---|---|
quote.rate | 1.5 |
The store after the step, scripts/check-bound.store.json
| base | "A:B" |
|---|---|
| quote | "C\D" |
| rate | 2.0 |
| base | "BTC" |
|---|---|
| quote | "USD" |
| rate | 1.5 |
| base | "EUR" |
|---|---|
| quote | "USD" |
| rate | 0.9 |
| quote(base:"BTC",quote:"USD") | Quote:BTC:USD |
|---|---|
| quotes | [ Quote:BTC:USD, Quote:EUR:USD, Quote:A\:B:C\\D ] |
The availability check runs for TestQuoteQuery(base: "BTC", quote: "EUR").
The check answers miss
| A lens reads | and finds |
|---|---|
quote.rate | null |
The environment ends and a second store opens over the same image, as the next launch does.
Records held client:root client:root:mutation client:root:subscription
The availability check runs for TestQuoteQuery(base: "BTC", quote: "USD").
The check answers image
| A lens reads | and finds |
|---|---|
quote.rate | 1.5 |
The availability check runs for TestQuoteQuery(base: "BTC", quote: "EUR").
The check answers miss
memory, and for one it lacks, miss; after a relaunch the first check answers image, and the lens reads the same value, while the miss is still a miss, which a handle would fetch.The image is a cache, not a database. An image written under another schema, another format or another identity configuration starts again; a record that goes a whole launch unread is dropped at the next; optimistic layers never reach it.
11. Signing out#
The environment is the session. Ending it cancels what it started, closes the image, and leaves every handle failed as gone; a new environment makes its own store, on the same file or another.
A view attaches a handle for TestConditions(id: "1", withOrigin: true, hideStatus: false) under storeOrNetwork, named rick; the transport answers its fetch with tests/conditions-included.json.
tests/conditions-included.json
{
"data": {
"character": {
"name": "Rick Sanchez",
"origin": {
"name": "Earth (C-137)",
"dimension": "Dimension C-137",
"id": "1"
},
"species": "Human",
"status": "Alive",
"id": "1"
}
}
}A view attaches a handle for TestHeaderQuery(id: "11") under storeOrNetwork, named einstein; the fetch stays in flight.
Fetch {"handle":"einstein","fetch":"inFlight"}
The optimistic response tests/rename-1.json for TestRename(id: "1", name: "Rick Prime") is applied as a layer named rename, before any server answers.
tests/rename-1.json
{
"data": {
"rename": {
"character": {
"id": "1",
"name": "Rick Prime"
}
}
}
}| A lens reads | and finds |
|---|---|
rick · character.name | Rick Prime |
The environment ends.
Phase [{"handle":"rick","phase":{"failed":"gone"}},{"handle":"einstein","phase":{"failed":"gone"}}]
Fetch [{"handle":"rick","fetch":{"failed":"environment"}},{"handle":"einstein","fetch":{"failed":"environment"}}]
Records held client:root client:root:mutation client:root:subscription
Logged nothing
| A lens reads | and finds |
|---|---|
character.name | null |
await environment.end() and a new environment.Run it#
The screen on this page is a trimmed copy of the Rick and Morty sample, which is in the repository:
swift run RickAndMortyQuit it, launch it again, and the characters are there before the network answers. Then change the header's fragment, add type, and build: the lens gains an accessor, the operation's text gains a field, and nothing else in the app moves. Or do the same without leaving this site, in the playground.
Baton