feat(kit): tell somebody the API is waking, instead of failing at them - #65
Merged
Conversation
`src/lib` was byte for byte identical in both React starters and nothing kept it that way. The kit already depends on it, since `useCollection` imports `../../lib/api`, so a change to what `apiFetch` throws is a change to what the kit has to handle, and maintaining that across two unlinked copies is a drift waiting to happen. The manifest now lists `lib`, so `npm run validate` fails on an edit made to a template's copy instead of to the source, the same as for the kit. No file contents change; this is a move plus one line of manifest.
Scaling an idle application's task to zero is the largest cost lever the product has, and the price is a first request that takes tens of seconds. The front end had nothing to say about that: a sleeping API produced whatever a failed fetch produces, which to a visitor is indistinguishable from a broken link. The person who meets it is never the owner, whose first view happens while the task is still warm. It is somebody who was sent a link days later and has never seen the application before. apiFetch now throws an ApiError carrying the status and a `waking` flag. A 502, 503 or 504 is a load balancer with nothing healthy behind it yet, and so is a fetch the browser rejected outright, because a load balancer's own 503 carries none of the API's CORS headers and so arrives as no answer rather than as a status. Being offline is ruled out, and a missing origin stays the plain configuration error it was, since waiting patiently for a misconfigured build is a wait that never ends. useCollection waits a wake out. It enters a new `waking` state and retries with backoff, capping at eight seconds and giving up after about a minute and a half, so it recovers on its own with nobody pressing anything. Retries keep the waking state rather than flashing the skeleton, the pending retry is cancelled on unmount and on a path change, and giving up resets the window so a retry button buys the whole wait again. LoadState gains `waking`, and RecordList, DataTable and a new WakingState render it: a calm panel saying the application sleeps while nobody is using it, that this takes a few seconds, and that there is nothing to do.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #60.
This is the front end half of scaling an idle application's task to zero. The infrastructure half is tracked in
seamless-terraform-service; this is the half that decides whether the trade is acceptable to a person.The problem, restated
An application that scales to zero is woken by the first request after an idle spell, and that takes tens of seconds. The front end had nothing to say about it, so a sleeping API produced whatever a failed fetch produces. To a non-technical visitor that is a broken link.
The person who meets the cold path is never the owner. The owner's first view happens seconds after the build, while the task is warm. The cold path belongs to somebody who was sent a link days later, has never seen the application before, and is judging the whole thing on that one screen.
Telling a wake from a fault
apiFetchnow throws anApiErrorwith thestatusand awakingflag.wakingnavigator.onLine === false)VITE_API_URLApiErrorat allThe second row is the one worth reading twice. A load balancer's own 503 is generated by the load balancer, so it carries none of the API's CORS headers, and the browser refuses to let the page read it. It arrives here as a rejected fetch with no status, indistinguishable from no answer at all. Treating only the status codes as a wake would have missed the most likely shape of the real thing.
The last row matters just as much in the other direction: if a missing origin were dressed up as an API that is about to answer, a misconfigured build would sit on a patient waiting screen forever. That case stays the plain error it always was, and there is a test pinning it.
Waiting it out
A first load that fails as a wake puts the collection into a new
wakingstate and retries with backoff (1s, 2s, 4s, then 8s), giving up after about a minute and a half, which is comfortably longer than a cold start. Nobody presses anything; it recovers on its own.Three details that are easy to get wrong and are each pinned by a test:
What it looks like
LoadStategainswaking;RecordListandDataTablerender a newWakingState. Not a skeleton, which promises "nearly there", and not an error, because nothing is wrong:It is a
role="status"witharia-live="polite", and it takes no props on purpose: every generated application should say this the same way, and the reader has no idea what a cold start is.Checked in the browser in both palettes, desktop and at 375px, since this screen is the first impression of the whole company for every non-owner.
The first commit:
src/libjoins the shared source of truthThis PR has to change
apiFetch, which lives insrc/lib. That directory was byte for byte identical in both React starters with nothing keeping it that way, and the kit already imports it (useCollectionreaches for../../lib/api), so a change to whatapiFetchthrows is a change to what the kit has to handle. Maintaining that across two unlinked copies is drift waiting to happen.The first commit is a pure move plus one line of
sync.json, sonpm run validatenow fails on an edit made to a template's copy, the same as for the kit. No file contents change in it. Reviewing it separately from the feature should be quick.Tests
19 new assertions across three files: the
ApiErrorclassification table above, the hook's wake behaviour (waits rather than fails, retries without being asked, keeps saying waking while it retries, gives up eventually, stops on unmount, and goes straight to an error for a 404), andRecordListrendering the notice rather than a skeleton or a fault.They were mutation checked rather than only run green. Making nothing classify as waking fails four of them; never closing the wake window, not cancelling the retry on unmount, and letting a retry reset the state to
loadingeach fail exactly one. That last mutation initially passed, which is why the "keeps saying it is waking while it retries" test exists.Checks
npm run validatepasses, andnpm run checkis green in both templates (react-vite30 tests,react-oauth34 tests).