nandi/frqpublic Fork 0
562900f
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

An hour of cache is a day of debugging

`main.dart.js` was given `max-age=3600` on the theory that a repeat
visit should cost no round trips. Flutter web does not hash its
filenames, so that pinned one build in every browser that had loaded
the app: `index.html` revalidated and truthfully said it had not
changed, while the JS it pulls in was an hour old. Three deploys were
then tested against code that was never running, and two of those
tests were reported as failures of the fix rather than of the header.

So the per-build files revalidate now -- `no-cache`, a 304 with no
body when they match -- and only `canvaskit/` keeps a max-age, since
it tracks the Flutter SDK and not this app. Anything unrecognised
revalidates too: being wrong that way costs a round trip, and being
wrong the other way costs an hour of serving code that no longer
exists.

gzip is untouched, which is where the bytes were actually saved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-18T15:35:33-07:00 Browse files
562900f parent: 8d6f6cb
modified .modal/flutter-web/serve.py +26 -7
@@ -46,21 +46,33 @@ PORT = 8080
4646 # pasted images under /api/v1/media, and those are `:image` in the same chat.
4747 PROXY_HOSTS = ("cdn.bsky.app", "irc.freeq.at", "video.bsky.app")
4848
49-# Files whose name is stable but whose *content* changes with every build, so a
50-# browser must ask before reusing one. Everything else -- `main.dart.js`, the
51-# canvaskit wasm, the assets -- gets a bounded max-age below: an hour of
52-# staleness in exchange for a repeat visit that costs no round trips.
49+# Files a browser must ask about before reusing. `main.dart.js` is on this list
50+# the hard way: it was given `max-age=3600` on the theory that an hour of
51+# staleness was a fair price for a repeat visit with no round trips, and the
52+# next three deploys were then tested against a bundle the browser had pinned
53+# in cache. `index.html` revalidated and said nothing had changed, because
54+# nothing in *it* had; the JS it pulls in was an hour old. Two debugging
55+# sessions went into code that was never running.
5356 #
54-# Why an hour and not `immutable`: Flutter web does not hash its filenames, so
55-# there is no URL that is safe to cache forever. The bound is the honest answer.
57+# Flutter web does not hash its filenames, so there is no URL here whose
58+# content is fixed, and `no-cache` -- revalidate every time, 304 with no body
59+# when it matches -- is the only honest header for a file that changes on
60+# every build. Only `canvaskit/` keeps a max-age: it changes with the Flutter
61+# SDK and not with this app, and it is the largest thing on the page.
5662 REVALIDATE = (
5763 "index.html",
64+ "main.dart.js",
65+ "flutter.js",
5866 "flutter_bootstrap.js",
5967 "flutter_service_worker.js",
68+ "manifest.json",
6069 "version.json",
6170 "client-metadata.json",
6271 )
6372
73+# Prefixes that keep the bounded max-age, by published path.
74+LONG_CACHE = ("canvaskit/",)
75+
6476 MAX_AGE = 3600
6577
6678 # Content types worth compressing. gzip on a PNG spends CPU to add bytes; gzip
@@ -165,6 +177,7 @@ def web():
165177 f"HOSTS = {PROXY_HOSTS!r}\n"
166178 f"PORT = {PORT}\n"
167179 f"REVALIDATE = {REVALIDATE!r}\n"
180+ f"LONG_CACHE = {LONG_CACHE!r}\n"
168181 f"MAX_AGE = {MAX_AGE}\n"
169182 f"COMPRESSIBLE = {COMPRESSIBLE!r}\n"
170183 )
@@ -201,7 +214,13 @@ class H(http.server.SimpleHTTPRequestHandler):
201214 def cache_control(self, path):
202215 if os.path.basename(path) in REVALIDATE:
203216 return "no-cache"
204- return f"public, max-age={MAX_AGE}"
217+ rel = os.path.relpath(path, ROOT).replace(os.sep, "/")
218+ if rel.startswith(LONG_CACHE):
219+ return f"public, max-age={MAX_AGE}"
220+ # Everything unrecognised revalidates too. Being wrong in this
221+ # direction costs a round trip; being wrong the other way costs an
222+ # hour of serving code that no longer exists.
223+ return "no-cache"
205224
206225 def entry(self, path):
207226 """(content-type, gzipped body) for a file, compressed at most once."""
@@ -46,21 +46,33 @@ PORT = 8080
46 # pasted images under /api/v1/media, and those are `:image` in the same chat.46 # pasted images under /api/v1/media, and those are `:image` in the same chat.
47 PROXY_HOSTS = ("cdn.bsky.app", "irc.freeq.at", "video.bsky.app")47 PROXY_HOSTS = ("cdn.bsky.app", "irc.freeq.at", "video.bsky.app")
48 48
49-# Files whose name is stable but whose *content* changes with every build, so a49+# Files a browser must ask about before reusing. `main.dart.js` is on this list
50-# browser must ask before reusing one. Everything else -- `main.dart.js`, the50+# the hard way: it was given `max-age=3600` on the theory that an hour of
51-# canvaskit wasm, the assets -- gets a bounded max-age below: an hour of51+# staleness was a fair price for a repeat visit with no round trips, and the
52-# staleness in exchange for a repeat visit that costs no round trips.52+# next three deploys were then tested against a bundle the browser had pinned
53+# in cache. `index.html` revalidated and said nothing had changed, because
54+# nothing in *it* had; the JS it pulls in was an hour old. Two debugging
55+# sessions went into code that was never running.
53 #56 #
54-# Why an hour and not `immutable`: Flutter web does not hash its filenames, so57+# Flutter web does not hash its filenames, so there is no URL here whose
55-# there is no URL that is safe to cache forever. The bound is the honest answer.58+# content is fixed, and `no-cache` -- revalidate every time, 304 with no body
59+# when it matches -- is the only honest header for a file that changes on
60+# every build. Only `canvaskit/` keeps a max-age: it changes with the Flutter
61+# SDK and not with this app, and it is the largest thing on the page.
56 REVALIDATE = (62 REVALIDATE = (
57 "index.html",63 "index.html",
64+ "main.dart.js",
65+ "flutter.js",
58 "flutter_bootstrap.js",66 "flutter_bootstrap.js",
59 "flutter_service_worker.js",67 "flutter_service_worker.js",
68+ "manifest.json",
60 "version.json",69 "version.json",
61 "client-metadata.json",70 "client-metadata.json",
62 )71 )
63 72
73+# Prefixes that keep the bounded max-age, by published path.
74+LONG_CACHE = ("canvaskit/",)
75+
64 MAX_AGE = 360076 MAX_AGE = 3600
65 77
66 # Content types worth compressing. gzip on a PNG spends CPU to add bytes; gzip78 # Content types worth compressing. gzip on a PNG spends CPU to add bytes; gzip
@@ -165,6 +177,7 @@ def web():
165 f"HOSTS = {PROXY_HOSTS!r}\n"177 f"HOSTS = {PROXY_HOSTS!r}\n"
166 f"PORT = {PORT}\n"178 f"PORT = {PORT}\n"
167 f"REVALIDATE = {REVALIDATE!r}\n"179 f"REVALIDATE = {REVALIDATE!r}\n"
180+ f"LONG_CACHE = {LONG_CACHE!r}\n"
168 f"MAX_AGE = {MAX_AGE}\n"181 f"MAX_AGE = {MAX_AGE}\n"
169 f"COMPRESSIBLE = {COMPRESSIBLE!r}\n"182 f"COMPRESSIBLE = {COMPRESSIBLE!r}\n"
170 )183 )
@@ -201,7 +214,13 @@ class H(http.server.SimpleHTTPRequestHandler):
201 def cache_control(self, path):214 def cache_control(self, path):
202 if os.path.basename(path) in REVALIDATE:215 if os.path.basename(path) in REVALIDATE:
203 return "no-cache"216 return "no-cache"
204- return f"public, max-age={MAX_AGE}"217+ rel = os.path.relpath(path, ROOT).replace(os.sep, "/")
218+ if rel.startswith(LONG_CACHE):
219+ return f"public, max-age={MAX_AGE}"
220+ # Everything unrecognised revalidates too. Being wrong in this
221+ # direction costs a round trip; being wrong the other way costs an
222+ # hour of serving code that no longer exists.
223+ return "no-cache"
205 224
206 def entry(self, path):225 def entry(self, path):
207 """(content-type, gzipped body) for a file, compressed at most once."""226 """(content-type, gzipped body) for a file, compressed at most once."""