Skip to content

The console sends ETags back and patches lease renewals in place #916

Closed

Description

@V3RON

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)"]
Loading

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.renewed to 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's ttlDeadline is 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's workerId (gateway) and the worker's own lease id.
  • lastRenewedAt becomes the event's timestamp and ttlMs becomes newDeadline − timestamp (F10).
    Searches: setQueryData, ttlDeadline in ui/src/state (hits in leases.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 by id; gateway event matches workerId + workerLeaseId in ['leases'] and workerId + lease id in ['workers']; lastRenewedAt and ttlMs follow; 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/src passes 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

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 23, 2026
  2. changed the title [-]The daemon says what needs the operator: GET /v1/inbox[/-] [+]The console sends ETags back and patches lease renewals in place[/+] on Oct 23, 2026
  3. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Oct 23, 2026
  4. self-assigned this
    on Oct 23, 2026
  5. added 2 commits that reference this issue on Oct 23, 2026
    d8447b0
    ab4698e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

task:readySpec checked. An agent may implement it once its Depends on allow.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions