Repository navigation
The console sends ETags back and patches lease renewals in place #916
Description
Part of #905.
Scope
The console sends back the ETag of each route's last answer, so an unchanged answer costs no download and no re-render. A lease renewal updates the lease's deadline in place, with no read.
In short
An idle page's polls mostly come back 304 Not Modified. A renewal moves the lease's "ends in" time at once on the Leases page and on its worker's page, with no request. When that lease was in the Inbox as ending, or its drawer is open, that data is read once, in the same burst window as any event.
flowchart LR
Q[query read] --> A["ApiClient.get + If-None-Match (new)"]
A -->|304| K[keep last data]
A -->|200| N[new data + tag]
E[lease.renewed] --> P["patch ttlDeadline (new)"]
If it goes wrong: a lease shows an old deadline, or a page shows data older than the server's.
Technical spec
Facts
| # | Fact | Source |
|---|---|---|
| F1 | ApiClient.get parses response.json() and returns { body, date }; a non-ok status throws through refuseError. A 304 is not ok. |
ui/src/data/api.ts:147-151, 194-204 |
| F2 | The server's ETag rules. | #905 Shared values "ETag" (#919) |
| F3 | lease.renewed payload { leaseId, newDeadline }; newDeadline is the worker's ttlDeadline (epoch ms). A gateway relays it with the worker's leaseId and adds workerId. |
src/bus/index.ts:42; src/leasing/lease-lifecycle.ts:131-141; src/gateway/worker-link.ts:556-564 |
| F4 | Lease records carry ttlDeadline, workerId (gateway) and, after #913, workerLeaseId. |
src/contract/schemas.ts:303, 1383-1386; #905 Shared values |
| F5 | The Inbox ending item embeds its lease record (#915). |
#905 Shared values inbox.get |
| F6 | The worker page's device rows show a lease's "expires" and "renewed" from ['workers'] (worker.leases), whose records carry the worker's own lease id. |
ui/src/model/worker-page.ts:353-366 |
| F7 | A gateway relays a worker's event before it refreshes that worker's view, and lease.list answers from views, so a read right after a renewal can return the old deadline. |
src/gateway/worker-link.ts:556-575; src/gateway/AGENTS.md Reads |
| F8 | Each read corrects the clock offset from Date and marks a starting daemon answered; after #914 this sits in connection.read(path). A 304 carries ETag (Hono) and Date (node:http adds it to every response through @hono/node-server). |
ui/src/data/live/connection.ts:187-194; node_modules/hono/dist/middleware/etag/index.js:56-66; src/http/server.ts:7 |
| F9 | Console browser stubs fulfill on top of route.fetch(), which forwards the console's headers, so they would receive the daemon's 304. |
e2e/console/workers-answers.ts:37-47; e2e/console/long-lists.spec.ts:119-127; grep -l "route.fetch" e2e/console |
| F10 | A renewal also moves lastRenewedAt and may change ttlMs; the Leases meter uses ttlMs. |
src/leasing/lease-lifecycle.ts:131-141; ui/src/model/leases.ts:190 |
Modules touched
ui/src/data/api.ts (get(path, { tag }): with a tag it returns { notModified: true, date } on 304 or { body, date, tag } on 200; without one its return type stays ApiResponse<T> so getJson and mode.ts are unchanged), ui/src/data/live/connection.ts (read passes the tag; a 304 corrects the offset from its Date and counts as the daemon answering, F8), the ApiClient fakes (ui/src/state/test-support.tsx, ui/src/data/live/test-support.ts), every console browser stub that calls route.fetch() (F9: it drops If-None-Match before fetching, so the stub always edits a full answer), ui/src/state/queries.ts (keeps the last tag per key; on 304 returns the cached data object), ui/src/model/renewal.ts (new, pure: the match rule, proven through the query-layer tests), ui/src/state/queries.ts event handling for lease.renewed, api.test.ts and queries.test.ts. Searches: api.get(, getJson( (the latter keeps no tag), lease.renewed in ui/src (none today).
Contract and event changes
None. Reads If-None-Match/ETag (#919) and workerLeaseId (#913).
Other code on the same state
- The event map (The console reads per page through a query cache, and events mark data stale #914) maps
lease.renewedto no key; this task adds the patch beside it. - A read of
['leases']or['workers']can land after a patch with the old deadline (F7). The console keeps each renewal's deadline for 60 s and applies it to every later answer: a record'sttlDeadlineis the later of the answer's and the kept one. - The worker page reads leases from
['workers'](F6): the patch applies there too, matched by the event'sworkerId(gateway) and the worker's own lease id. lastRenewedAtbecomes the event's timestamp andttlMsbecomesnewDeadline − timestamp(F10).
Searches:setQueryData,ttlDeadlineinui/src/state(hits inleases.ts,inbox.ts).
Failure modes
| Call | Outcome | What happens | Limit, counted from |
|---|---|---|---|
| GET with tag | 304 | cached data kept, same object, no re-render | 10 s timeout from send |
| GET with tag | 200 | new data and tag | same |
| GET with tag | 304 but no cached data (dropped meanwhile) | read again without a tag | same |
| renewal | no matching record | nothing; the next poll brings it | 10 s poll |
| renewal | ending item in the inbox, or ['lease', id] observed |
marked stale through #914's burst window: one read per burst, however many renewals | 300 ms / 1 s window |
Rules in play
- ADR 0044 §2 (renewals) and §3 (ETag): complies.
Tests
Seams: ui/src/data/api.test.ts for the client; the query layer of #914 (ui/src/state/queries.test.ts) for the cache, its observers built with the same options as useRouteQuery.
- A re-read of an unchanged route: the second read sends the first tag; 304 → the observer gets the same data object and no update; 200 → new data; 304 after the data was dropped → one read without a tag.
- A renewal patches its lease: worker event (no
workerId) matches byid; gateway event matchesworkerId+workerLeaseIdin['leases']andworkerId+ lease id in['workers'];lastRenewedAtandttlMsfollow; a later answer with the old deadline keeps the new one; no match → no change; 20 renewals of ending leases in 1 s → one inbox read;['lease', id]observed → one read; otherwise no request.
Done when
pnpm vitest run ui/srcpasses with the new tests.- The console browser lane passes with the stubs of F9 dropping
If-None-Match.
Out of scope
Patching any other event's data in place.
Depends on
- Unchanged JSON answers under /v1 are 304 with an ETag #919
- One lease is read directly, and gateway leases name the worker's lease id #913
- The console reads per page through a query cache, and events mark data stale #914 (its query layer, event map and burst window) (decided in delivery, accepted by the maintainer 2026-10-23)
- The daemon says what needs the operator: GET /v1/inbox #915 (its inbox with the
endingitem) (decided in delivery, accepted by the maintainer 2026-10-23) - The daemon says what needs the operator: GET /v1/inbox #915
Written by an agent.
Activity
[-]The daemon says what needs the operator: GET /v1/inbox[/-][+]The console sends ETags back and patches lease renewals in place[/+]test: a re-read of an unchanged route keeps data on a 304, replaces i…
feat(ui): a 304 after the data was dropped is read again without a tag (
test: a lease renewal moves the lease's deadline in place with no read (
feat(ui): a lease.renewed event moves the lease's deadline in the lea…
test: a lease renewal is matched by worker in a gateway and its deadl…
feat(ui): renewals match a gateway's leases and the worker page's, an…
docs(ui): the manual describes the ETag round trip and in-place renew…
refactor(ui): keep the conditional read within the complexity bound (#…
chore: renewal helpers stay private and the console's react-query is …
test: a renewal marks only what it leaves stale (#916)
feat(ui): a renewal marks its inbox ending item and its open lease pa…
refactor(ui): keep the event handler within the complexity bound (#916)
refactor(ui): the renewal patch is its own step (#916)
test: the renewal test straddles the kept window, tells a worker's le…
fix(ui): a renewal that moves no lease leaves the poll alone, and a r…
fix(ui): a patched renewal leaves the poll's timer running, only a re…
test: the toast test's table is shown before time freezes, and an act…
feat(ui): the console sends ETags back and patches lease renewals in …
test(console): an idle console on a 50-worker fleet stays under 40 re…