Bound a Supply run toggle's session load and CSRF refresh by its deadline (Feature 17)
CI / ci (push) Successful in 4m58s
CI / ci (pull_request) Successful in 4m59s

- loadMe takes an optional abort signal; AuthBootstrap calls it with none.
- Each toggle try loads GET /api/v1/me itself under its deadline when the
  session is not loaded, instead of joining a /me query already in flight
  that may never answer.
- The /me refresh a csrf 403 makes is bounded by the request's signal: a
  request whose signal aborts stops waiting at once and the refresh is no
  longer shared, so the next csrf 403 starts a fresh /me. Requests without
  a signal still share it, and it is aborted only once no request waits.
- Tests: a hung /me and a hung csrf refresh during a toggle exchange, both
  recovered on the next try in order; the client's refresh sharing and abort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 11:27:18 -04:00
co-authored by Claude Opus 5.5
parent 6d45f3ed0d
commit 54b59965ed
5 changed files with 239 additions and 34 deletions
+8 -12
View File
@@ -228,8 +228,9 @@ export type Toggles = {
}
// TOGGLE_DEADLINE is how long, in milliseconds, one try of a toggle — its
// GET /api/v1/me and its PATCH — may take before it counts as the server not
// answering. Only toggles have a deadline: the API client sets none.
// GET /api/v1/me, its PATCH and the /me refresh a csrf 403 to it makes — may
// take before it counts as the server not answering. Only toggles have a
// deadline: the API client sets none.
export const TOGGLE_DEADLINE = 10_000
// RETRY_FIRST and RETRY_CEILING bound, in milliseconds, how long a toggle
@@ -258,14 +259,6 @@ function reachable(network: NetworkStatus, delay: number): Promise<void> {
})
}
// beforeDeadline is promise, rejected with signal's reason once it aborts.
function beforeDeadline<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
return new Promise((resolve, reject) => {
signal.addEventListener('abort', () => reject(signal.reason as Error), { once: true })
promise.then(resolve, reject)
})
}
// toggleOptions is the mutation every toggle of the week's Supply run is,
// made on the Supply run screen (useToggles) or replayed from the queue saved
// on this device (useSupplyRunSync).
@@ -313,8 +306,11 @@ function toggleOptions(
const deadline = new AbortController()
const timer = setTimeout(() => deadline.abort(new DOMException(`${where}: no answer in time`, 'TimeoutError')), TOGGLE_DEADLINE)
try {
const me = queryClient.ensureQueryData({ queryKey: meQueryKey, queryFn: api.loadMe, networkMode: 'always' })
await beforeDeadline(me, deadline.signal)
// Each try loads /me itself, under its deadline, rather than share
// a /me already in flight that may never answer.
if (queryClient.getQueryData(meQueryKey) === undefined) {
queryClient.setQueryData(meQueryKey, await api.loadMe(deadline.signal))
}
body = await api.apiFetch<unknown>(path, { method: 'PATCH', body: { needed }, signal: deadline.signal })
break
} catch (error) {