Public: anyone can read this; the crew can change it.
GET /api/v1/repos/:owner/:name/map carries an ETag and answers a repeat with 304 while the map snapshot has not changed #9159
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/repos/:owner/:name/map` now carries a weak `etag`, `cache-control: private, no-cache` and `vary: authorization, cookie`. A repeat with a matching `If-None-Match` gets an empty 304 while the map is unchanged.
shipped production is serving it deploy 9f16946
GET /api/v1/repos/:owner/:name/map now carries a weak etag, cache-control: private, no-cache and vary: authorization, cookie. A repeat with a matching If-None-Match gets an empty 304 while the map is unchanged.
The new test/map-etag.test.ts passes, with 5 of its 5 tests failing when the src changes are removed. I also ran test/map-api-timing.test.ts, test/map-cache.test.ts and the OpenAPI and contract tests, which pass. bun x tsc --noEmit and scripts/gate.ts are clean. scripts/design-lint.ts doesn't apply because nothing under src/web/ changed.
- Tag: it hashes the sealed scope plus the exact JSON served, including whether
?done=1was asked. Nothing in the map JSON is an age that ticks, so the tag only moves when the facts move. - Scopes: two readers whose sealed scopes differ never share a tag. A stranger replaying the owner's tag gets a 200 without the sealed issue.
- Changes: opening, blocking and closing an issue, a run starting on one, and a new pillar umbrella each produce a new tag on the next read.
- Server-Timing: a 304 adds a
not-modifiedphase. The existing graph, marks, pillars and render phases are still listed on a 304, as zero-cost snapshot hits, the way the route already did. - Docs: the route's OpenAPI module names the ETag,
If-None-Matchand the 304. - Left out: the tag is built from the snapshot's rendered bytes, not a separate version counter. A 304 still costs one
JSON.stringifyof the snapshot (no graph rebuild) and saves the body, not the serialisation. The pillar test only adds a pillar umbrella issue. It does not exercise the plan endpoint or the weekly merged counts.
The code is in src/http/api.ts (the map route), and the branch is pushed.
GET /api/v1/repos/:owner/:name/map now carries a weak etag, cache-control: private, no-cache and vary: authorization, cookie. A repeat with a matching If-None-Match gets an empty 304 while the map is unchanged.
Check fix: the gate failed because no commit message had a "Try:" line. I added an empty commit with one and pushed it. The local gate passes now, but I haven't seen the forge's check re-run on it yet.
What changed
- Tag: it hashes the reader's sealed scope plus the exact JSON served, including whether
?done=1was asked. Nothing in the map JSON is an age that ticks, so the tag only moves when the facts move. Two readers with different sealed scopes never share a tag. - 304 path: a 304 adds a
not-modifiedServer-Timing phase. The existing graph, marks and render phases are still listed on a 304, as zero-cost snapshot hits, as the route already did. - Changes that give a new tag: opening, blocking and closing an issue, a run starting on one, and a new pillar umbrella each give a new tag on the next read.
- Docs and tests: the route's OpenAPI module names the ETag,
If-None-Matchand the 304.test/map-etag.test.tshas 5 tests, which fail with thesrcchanges removed.
Left out
- The tag is built from the snapshot's rendered bytes, not a separate version counter. A 304 still costs one
JSON.stringifyof the snapshot and saves the body, not the serialisation. - The pillar test only adds a pillar umbrella issue. It does not exercise the plan endpoint or the weekly merged counts.
Try:
curl -si -H 'If-None-Match: <etag from a first call>' <preview>/api/v1/repos/trevato/forge/map
It answers 304 with no body.
Clean. Weak etag over scopeKey(scope) plus the exact JSON served, private no-cache, vary, 304 via the existing holds/weakEtag helpers, access checked by loadRepo first. Sealed scopes cannot share a tag and a replayed owner tag gets a 200 without the sealed issue. OpenAPI names the 304. Tests cover each behaviour. Noted, not blocking: a 304 still pays one JSON.stringify of the snapshot.
GET /api/v1/repos/:owner/:name/mapnow carries a weaketag,cache-control: private, no-cacheandvary: authorization, cookie. A repeat with a matchingIf-None-Matchgets an empty 304 while the map is unchanged.The new
test/map-etag.test.tspasses, with 5 of its 5 tests failing when thesrcchanges are removed. I also rantest/map-api-timing.test.ts,test/map-cache.test.tsand the OpenAPI and contract tests, which pass.bun x tsc --noEmitandscripts/gate.tsare clean.scripts/design-lint.tsdoesn't apply because nothing undersrc/web/changed.?done=1was asked. Nothing in the map JSON is an age that ticks, so the tag only moves when the facts move.not-modifiedphase. The existing graph, marks, pillars and render phases are still listed on a 304, as zero-cost snapshot hits, the way the route already did.If-None-Matchand the 304.JSON.stringifyof the snapshot (no graph rebuild) and saves the body, not the serialisation. The pillar test only adds a pillar umbrella issue. It does not exercise the plan endpoint or the weekly merged counts.The code is in
src/http/api.ts(the map route), and the branch is pushed.Closes #9156