perf: bundle and fingerprint the PWA runtime (#379)
All checks were successful
CI / lint (pull_request) Successful in 42s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-09 06:59:05 +00:00
parent 56a9a0cba6
commit a919242b8f
8 changed files with 187 additions and 16 deletions

View File

@ -160,7 +160,10 @@ Every response also defines the browser execution boundary with a Content Securi
Policy that allows scripts only from the dashboard origin, denies framing and Policy that allows scripts only from the dashboard origin, denies framing and
plugins, and blocks unused browser capabilities. Keep these response headers when plugins, and blocks unused browser capabilities. Keep these response headers when
proxying; do not add inline scripts or broaden `script-src`. The dashboard bootstrap proxying; do not add inline scripts or broaden `script-src`. The dashboard bootstrap
and stylesheet are same-origin static assets included in the offline PWA shell. is assembled in source order into one content-addressed JavaScript response. Dashboard
HTML and the offline worker reference that exact fingerprint, while the runtime receives
immutable caching and HTML/worker responses remain revalidated. The stylesheet and
fingerprinted runtime are same-origin assets included atomically in the offline PWA shell.
Use **Sign out & clear this device** on shared devices; it clears Stackchain's Use **Sign out & clear this device** on shared devices; it clears Stackchain's
offline snapshots, drafts, outboxes, background IndexedDB, and PWA caches without offline snapshots, drafts, outboxes, background IndexedDB, and PWA caches without
removing unrelated forge preferences. Rotate either dashboard secret by replacing removing unrelated forge preferences. Rotate either dashboard secret by replacing

70
src/frontend_bundle.py Normal file
View File

@ -0,0 +1,70 @@
"""Build the content-addressed browser shell served by the dashboard."""
from __future__ import annotations
import hashlib
import re
from dataclasses import dataclass
from pathlib import Path
SCRIPT_TAG = re.compile(r'^<script src="(static/[^"?]+\.js)"></script>$', re.MULTILINE)
WORKER_RUNTIME_SOURCE = "static/background-issue-sync.js"
@dataclass(frozen=True)
class FrontendBuild:
dashboard_html: str
runtime_bytes: bytes
runtime_digest: str
runtime_name: str
page_sources: tuple[str, ...]
service_worker_source: str
def _bundle(frontend_dir: Path, sources: tuple[str, ...]) -> bytes:
chunks = []
for source in sources:
path = frontend_dir / source.removeprefix("static/")
chunks.append(f"/* {source} */\n".encode() + path.read_bytes() + b"\n;\n")
return b"".join(chunks)
def build_frontend(frontend_dir: Path) -> FrontendBuild:
"""Return one internally consistent HTML, runtime, and offline-worker build."""
source_html = (frontend_dir / "index.html").read_text()
sources = tuple(SCRIPT_TAG.findall(source_html))
if not sources:
raise ValueError("dashboard entry document has no page scripts")
runtime = _bundle(frontend_dir, sources)
digest = hashlib.sha256(runtime).hexdigest()[:16]
runtime_name = f"runtime-{digest}.js"
dashboard_html = SCRIPT_TAG.sub("", source_html)
dashboard_html = dashboard_html.replace(
"</body>", f'<script src="{runtime_name}"></script>\n</body>'
)
worker = (frontend_dir / "service-worker.js").read_text()
for source in sources:
if source != WORKER_RUNTIME_SOURCE:
worker = worker.replace(f" BASE + '{source}',\n", "")
worker = worker.replace(
" BASE + 'static/dashboard.css',\n",
f" BASE + 'static/dashboard.css',\n BASE + '{runtime_name}',\n",
)
worker = re.sub(
r"const CACHE = 'stackchain-dashboard-shell-v\d+';",
f"const CACHE = 'stackchain-dashboard-shell-{digest}';",
worker,
count=1,
)
return FrontendBuild(
dashboard_html=dashboard_html,
runtime_bytes=runtime,
runtime_digest=digest,
runtime_name=runtime_name,
page_sources=sources,
service_worker_source=worker,
)

View File

@ -37,7 +37,7 @@ from src.request_boundary import RequestBodyLimitMiddleware, request_body_limit
from src.suggestion_engine import compute from src.suggestion_engine import compute
from src.later_store import LaterStore from src.later_store import LaterStore
from src.today_store import TodayPlanFull, TodayStore from src.today_store import TodayPlanFull, TodayStore
from src.views import router as frontend_router from src.views import FRONTEND_BUILD, router as frontend_router
@asynccontextmanager @asynccontextmanager
async def lifespan(_app: FastAPI): async def lifespan(_app: FastAPI):
@ -588,7 +588,13 @@ async def require_operator_session(request: Request, call_next):
return await call_next(request) return await call_next(request)
public = ( public = (
path in {"/healthz", "/readyz", "/login", "/manifest.webmanifest"} path in {
"/healthz",
"/readyz",
"/login",
"/manifest.webmanifest",
"/" + FRONTEND_BUILD.runtime_name,
}
or path.startswith("/static/") or path.startswith("/static/")
or (path == "/api/v1/session" and request.method == "POST") or (path == "/api/v1/session" and request.method == "POST")
) )

View File

@ -1,12 +1,14 @@
from pathlib import Path from pathlib import Path
from fastapi import APIRouter from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse, HTMLResponse from fastapi.responses import FileResponse, HTMLResponse, Response
from src.frontend_bundle import build_frontend
router = APIRouter() router = APIRouter()
DASHBOARD_FILE = Path(__file__).resolve().parent.parent / "frontend" / "index.html" DASHBOARD_FILE = Path(__file__).resolve().parent.parent / "frontend" / "index.html"
MANIFEST_FILE = DASHBOARD_FILE.parent / "manifest.webmanifest" MANIFEST_FILE = DASHBOARD_FILE.parent / "manifest.webmanifest"
SERVICE_WORKER_FILE = DASHBOARD_FILE.parent / "service-worker.js" FRONTEND_BUILD = build_frontend(DASHBOARD_FILE.parent)
LOGIN_HTML = """<!doctype html> LOGIN_HTML = """<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@ -26,7 +28,7 @@ class RevalidatingHTMLResponse(HTMLResponse):
@router.get("/", response_class=RevalidatingHTMLResponse) @router.get("/", response_class=RevalidatingHTMLResponse)
async def dashboard() -> str: async def dashboard() -> str:
return DASHBOARD_FILE.read_text() return FRONTEND_BUILD.dashboard_html
@router.get("/login", response_class=RevalidatingHTMLResponse) @router.get("/login", response_class=RevalidatingHTMLResponse)
@ -39,10 +41,21 @@ async def web_app_manifest() -> FileResponse:
return FileResponse(MANIFEST_FILE, media_type="application/manifest+json") return FileResponse(MANIFEST_FILE, media_type="application/manifest+json")
@router.get("/service-worker.js", response_class=FileResponse) @router.get("/service-worker.js")
async def service_worker() -> FileResponse: async def service_worker() -> Response:
return FileResponse( return Response(
SERVICE_WORKER_FILE, FRONTEND_BUILD.service_worker_source,
media_type="application/javascript", media_type="application/javascript",
headers={"Service-Worker-Allowed": "/", "Cache-Control": "no-cache"}, headers={"Service-Worker-Allowed": "/", "Cache-Control": "no-cache"},
) )
@router.get("/runtime-{digest}.js")
async def runtime_bundle(digest: str) -> Response:
if digest != FRONTEND_BUILD.runtime_digest:
raise HTTPException(status_code=404, detail="Runtime revision not found")
return Response(
FRONTEND_BUILD.runtime_bytes,
media_type="application/javascript",
headers={"Cache-Control": "public, max-age=31536000, immutable"},
)

View File

@ -1,9 +1,17 @@
import re
from pathlib import Path from pathlib import Path
from src.views import dashboard as dashboard_html from src.views import dashboard as dashboard_html
FRONTEND = Path(__file__).resolve().parents[1] / "frontend" FRONTEND = Path(__file__).resolve().parents[1] / "frontend"
PAGE_SCRIPT_MANIFEST = "\n".join(
re.findall(
r'^<script src="static/[^\"]+\.js"></script>$',
(FRONTEND / "index.html").read_text(),
re.MULTILINE,
)
)
def dashboard_bundle_text() -> str: def dashboard_bundle_text() -> str:
@ -18,11 +26,12 @@ def dashboard_bundle_text() -> str:
async def dashboard() -> str: async def dashboard() -> str:
# Exercise the real view lookup before adding its separately served assets. # Exercise the real view lookup before adding its separately served bootstrap assets.
html = await dashboard_html() html = await dashboard_html()
return "\n".join( return "\n".join(
( (
html, html,
PAGE_SCRIPT_MANIFEST,
(FRONTEND / "dashboard.css").read_text(), (FRONTEND / "dashboard.css").read_text(),
(FRONTEND / "dashboard.js").read_text(), (FRONTEND / "dashboard.js").read_text(),
) )

View File

@ -7,6 +7,7 @@ import pytest
from src import main from src import main
from src.session_store import SessionStoreError from src.session_store import SessionStoreError
from src.views import FRONTEND_BUILD
@pytest.fixture @pytest.fixture
@ -956,9 +957,16 @@ async def test_public_routes_skip_session_registry_validation(access_control, mo
login = await client.get("/login") login = await client.get("/login")
manifest = await client.get("/manifest.webmanifest") manifest = await client.get("/manifest.webmanifest")
static = await client.get("/static/session.js") static = await client.get("/static/session.js")
runtime = await client.get("/" + FRONTEND_BUILD.runtime_name)
assert signed_in.status_code == 200 assert signed_in.status_code == 200
assert [login.status_code, manifest.status_code, static.status_code] == [200, 200, 200] assert [login.status_code, manifest.status_code, static.status_code, runtime.status_code] == [
200,
200,
200,
200,
]
assert runtime.headers["cache-control"] == "public, max-age=31536000, immutable"
@pytest.mark.anyio @pytest.mark.anyio

View File

@ -0,0 +1,62 @@
import gzip
import re
import shutil
from pathlib import Path
import pytest
from src.frontend_bundle import build_frontend
from src.views import dashboard, runtime_bundle, service_worker
FRONTEND = Path(__file__).resolve().parents[1] / "frontend"
PAGE_SCRIPT = re.compile(r'<script src="(static/[^"]+\.js)"></script>')
def test_page_runtime_is_one_deterministic_content_addressed_bundle(tmp_path):
first = build_frontend(FRONTEND)
second = build_frontend(FRONTEND)
assert first.runtime_name == second.runtime_name
assert first.runtime_bytes == second.runtime_bytes
assert PAGE_SCRIPT.findall(first.dashboard_html) == []
assert first.dashboard_html.count("<script src=") == 1
assert f'<script src="{first.runtime_name}"></script>' in first.dashboard_html
assert first.runtime_name.startswith("runtime-")
assert first.runtime_name.endswith(".js")
assert len(gzip.compress(first.runtime_bytes, mtime=0)) <= 100 * 1024
changed_frontend = tmp_path / "frontend"
shutil.copytree(FRONTEND, changed_frontend)
(changed_frontend / "widgets.js").write_text(
(changed_frontend / "widgets.js").read_text() + "\n// changed\n"
)
changed = build_frontend(changed_frontend)
assert changed.runtime_name != first.runtime_name
assert changed.runtime_bytes != first.runtime_bytes
def test_offline_shell_precaches_exact_runtime_without_superseded_page_modules():
build = build_frontend(FRONTEND)
assert f"BASE + '{build.runtime_name}'" in build.service_worker_source
for source in build.page_sources:
if source == "static/background-issue-sync.js":
continue
assert f"BASE + '{source}'" not in build.service_worker_source
@pytest.mark.anyio
async def test_runtime_asset_is_immutable_while_html_and_worker_revalidate():
build = build_frontend(FRONTEND)
html = await dashboard()
bundle = await runtime_bundle(build.runtime_digest)
worker = await service_worker()
assert build.runtime_name in html
assert bundle.body == build.runtime_bytes
assert bundle.headers["cache-control"] == "public, max-age=31536000, immutable"
assert worker.headers["cache-control"] == "no-cache"
assert build.runtime_name.encode() in worker.body

View File

@ -2,7 +2,7 @@ from pathlib import Path
import pytest import pytest
from src.views import dashboard from src.views import FRONTEND_BUILD, dashboard
@pytest.mark.anyio @pytest.mark.anyio
@ -28,7 +28,7 @@ async def test_dashboard_bootstrap_uses_csp_compatible_static_assets():
html = await dashboard() html = await dashboard()
assert '<link rel="stylesheet" href="static/dashboard.css" />' in html assert '<link rel="stylesheet" href="static/dashboard.css" />' in html
assert '<script src="static/dashboard.js"></script>' in html assert f'<script src="{FRONTEND_BUILD.runtime_name}"></script>' in html
assert "<style>" not in html assert "<style>" not in html
assert "<script>" not in html assert "<script>" not in html
@ -42,7 +42,7 @@ async def test_global_search_preview_is_a_phone_safe_accessible_dialog():
assert 'aria-labelledby="search-preview-title"' in html assert 'aria-labelledby="search-preview-title"' in html
assert 'id="claim-search-result"' in html assert 'id="claim-search-result"' in html
assert 'id="open-search-result-gitea"' in html assert 'id="open-search-result-gitea"' in html
assert 'src="static/search-preview.js"' in html assert "static/search-preview.js" in FRONTEND_BUILD.page_sources
assert ".search-preview-panel" in css assert ".search-preview-panel" in css
assert "height:100dvh" in css assert "height:100dvh" in css
assert "env(safe-area-inset-bottom)" in css assert "env(safe-area-inset-bottom)" in css