guts/tools/CACHE.md
type-two 1233a9effe [lane F] The stale-edge fix, prepared but not applied
The step-5 check added in the previous commit failed on its first real deploy, and it was
right: cf-cache-status HIT, age 158, cache-control max-age=14400, and the edge serving the
PREVIOUS boot.js. forum-nginx sends no Cache-Control for /gutsy/ at all, so Cloudflare applies
its 4-hour default to a game that ships un-versioned ES modules with no build step — what the
edge holds is code, not assets.

tools/forum-nginx-default.conf is the container's config with a /gutsy/ block adding
Cache-Control: no-cache (store, but revalidate — the files are etag'd, so it costs a 304).
Scoped to /gutsy/; nothing else on partly.party is touched. tools/CACHE.md has the measurement
and the two commands.

NOT APPLIED. Copying a file into the live forum-nginx container is gated for me, which is the
right gate on shared infrastructure hosting partly.party — I have not tried to route around it.
Until John runs it, a deploy reaches players when the 4-hour TTL lapses or the cache is purged.
The container has no bind mount for that config, so a force-recreate wipes the block; the file
is in the repo so re-applying is the same two commands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 22:35:05 +10:00

2.5 KiB

The edge cache, and why a green deploy could still ship nothing

The problem, measured

tools/deploy.sh step 5 compares https://partly.party/gutsy/js/boot.js, fetched the way a browser fetches it, against the bytes just shipped. On 2026-07-26 it failed for the first time and it was right:

last-modified: Sun, 26 Jul 2026 12:10:07 GMT
cache-control: max-age=14400
cf-cache-status: HIT
age: 158

The origin (forum-nginx) sends no Cache-Control at all for /gutsy/, so Cloudflare applies its default 4-hour Edge TTL to .js. GUTS has no build step and no content hashes in its filenames, so what the edge is holding is not an asset — it is code. A deploy lands on the box and players keep running the previous build for up to four hours, with nothing anywhere saying so.

Every check above step 5 appends ?v=$BUST, which is exactly what makes them bypass the edge. So six green checks were compatible with every player being served a stale game.

The fix (needs one command run by hand)

tools/forum-nginx-default.conf is the container's config with a /gutsy/ block added:

location /gutsy/ {
    root   /usr/share/nginx/html;
    add_header Cache-Control "no-cache";
}

no-cache means store it, but revalidate every time — the files are small and etag'd, so a revalidation is a 304 and costs nothing. Scoped to /gutsy/; no other path on partly.party is affected.

Apply it (the copy-into-container step is gated for agents, so this is John's to run):

scp tools/forum-nginx-default.conf humanjing@100.71.119.27:/tmp/default.conf
ssh humanjing@100.71.119.27 "docker cp /tmp/default.conf forum-nginx:/etc/nginx/conf.d/default.conf && docker exec forum-nginx nginx -t && docker exec forum-nginx nginx -s reload"

nginx -t runs before the reload, so a syntax error refuses to load rather than taking partly.party down.

Then confirm — this should report cf-cache-status: DYNAMIC or BYPASS, and deploy.sh step 5 should pass on the first try after every future deploy:

curl -sI https://partly.party/gutsy/js/boot.js | grep -iE "cf-cache-status|cache-control"

Until then

A deploy is only live to players after the 4-hour TTL expires, or after a manual purge: Cloudflare dashboard → partly.party → Caching → Configuration → Purge Everything.

Note the container has no bind mount for its nginx config, so a docker compose up --force-recreate or an nginx image bump wipes this block. That is why the file lives in the repo: re-applying it is the same two commands.