Baton

Recipes

Discover once, refresh by id

The pattern for external keys: look a subject up by its natural key once, then refresh any number of them through nodes(ids:).

An app whose subjects arrive by natural key, a repository's owner and name and an issue's number from a notification, cannot ask for them in one operation: operations are fixed at build time, and the count of aliases cannot vary. The pattern every such app settles on has two halves, and the GitHub samples, Swift and Kotlin, show both.

Discover once, by natural key#

The first time a subject is seen, one operation looks it up by the key the app has, and the response carries the node's id:

query IssueQuery($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    issue(number: $number) { id ...IssueDetail_issue }
  }
}

The record is keyed by that id from then on, whatever path reached it, so every later read, by any operation, finds the same record.

Refresh through nodes(ids:)#

Once the ids are known, one operation refreshes any number of them: GitHub's nodes(ids: [ID!]!) returns the records in the order asked, and the sample spreads the same row fragments its lists render:

query RefreshRowsQuery($ids: [ID!]!) {
  nodes(ids: $ids) {
    ... on Issue { id ...IssueRow_issue @alias }
    ... on PullRequest { id ...PullRequestRow_pullRequest @alias }
  }
}
let ids = rows.compactMap { $0.asIssue?.id ?? $0.asPullRequest?.id }
try await environment.fetch(RefreshRowsQuery(ids: Array(ids.prefix(50))))

In Kotlin the same two lines, fetch a suspend function; the Kotlin sample hosts the query on the function that sends it, Environment.refreshRows in its TriageScreen.kt, since no composable resolves it:

val ids = rows.mapNotNull { it.asIssue?.id ?: it.asPullRequest?.id }
environment.fetch(RefreshRowsQuery(ids = ids.take(50)))

The response commits into the records the lists already show, by identity, so every row re-renders with its new fields and nothing else changes; the list's own query is not refetched, and its connection's edges stay where they were. Fifty ids a request is GitHub's comfortable batch; Caton, the app this pattern comes from, refreshed five hundred subjects in ten requests where discovery had cost five hundred.

Why not a variable-length batch of aliases#

A compiler feature that expands issue(number:) into as many aliases as the app has keys would be a concept for one app's cold start, and the discovery cost is paid once per subject, not per launch: after it, the id is in the store and the image. nodes(ids:) is the server's batch, and Baton's identity makes the two halves meet in one record.