aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/app.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/app.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/app.py31
1 files changed, 30 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/app.py b/packages/meshbay-hub/src/meshbay_hub/app.py
index 6240785..423ad03 100644
--- a/packages/meshbay-hub/src/meshbay_hub/app.py
+++ b/packages/meshbay-hub/src/meshbay_hub/app.py
@@ -35,7 +35,7 @@ from meshbay_hub.api.relay import router as relay_router
from meshbay_hub.api.signaling import router as signaling_router
from meshbay_hub.api.admin import router as admin_router
from meshbay_hub.api.notifications import router as notifications_router
-from meshbay_hub.api.webapp import router as webapp_router, STATIC_DIR
+from meshbay_hub.api.webapp import router as webapp_router, STATIC_DIR, ASSET_V
from meshbay_hub.api.middleware import limiter
@@ -145,6 +145,35 @@ def create_app(cfg: HubConfig | None = None) -> FastAPI:
response.headers.setdefault("Cache-Control", "no-cache")
return response
+ class VersionedStatics(StaticFiles):
+ """The same files under a URL that changes when they do.
+
+ `no-cache` above only binds a browser that asks. One that cached the SPA
+ before that header existed applies heuristic freshness and does not ask
+ at all, so it runs an old player against a new node — a fix that is
+ deployed, served, and not running, which looks exactly like a fix that
+ does not work. Measured: a phone kept a player without the read-ahead
+ bound and filled the browser's buffer ceiling at 106 MB, while the
+ server had been serving the bounded one for an hour.
+
+ Serving the graph under /a/<fingerprint>/ solves it for every file at
+ once, because relative imports inherit the prefix: `app.js` reaching for
+ `./i18n.js` gets the version it was built against, and never a mixture.
+ The URL changes with the content, so these may be cached hard.
+ """
+
+ async def get_response(self, path, scope):
+ response = await super().get_response(path, scope)
+ response.headers["Cache-Control"] = (
+ "public, max-age=31536000, immutable")
+ return response
+
+ # Before "/", which would otherwise swallow it.
+ app.mount(f"/a/{ASSET_V}", VersionedStatics(directory=STATIC_DIR),
+ name="static-versioned")
+ # Still served unversioned: sw.js must stay at the root or its scope stops
+ # covering the pages it intercepts downloads for, and old bookmarks of
+ # /style.css and the like should not 404.
app.mount("/", RevalidatingStatics(directory=STATIC_DIR), name="static")
return app