diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/webapp.py | 52 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_asset_versioning.py | 57 |
2 files changed, 50 insertions, 59 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py index 05485e0..6ac5cdb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py @@ -21,40 +21,9 @@ STATIC_DIR = Path(__file__).parent.parent / "static" router = APIRouter(tags=["webapp"]) -# Assets the shell pulls in, in load order. Everything else is imported by -# app.js from a relative path, which inherits the `/a/<hash>/` prefix the shell -# loaded app.js under — so the whole module graph moves together. -# Every module the page loads. A file missing from here is a file whose change -# does not move the URL, so a browser holding the old one never asks for it — -# which is the failure this list exists to prevent, and it is silent. -_ASSETS = ("style.css", "keyderive.js", "crypto.js", "transport.js", "app.js", - "i18n.js", "downloads.js", "transfers.js", "zipstream.js", - "platform.js", "meshbay-m.png", - # Split out of app.js by the group-page refactor — each imported by - # app.js or group-page.js, so a change to any of them is a change - # to what the browser must fetch. - "icon.js", "file-utils.js", "hub-client.js", "apps.js", - "source-merge.js", "sticky.js", - "chat-app.js", "files-app.js", "video-player.js", "video-app.js", - "music-app.js", "music-player.js", "photos-app.js", - "group-settings.js", "group-page.js", - # The per-app settings architecture (docs/refactor-groups.md §3): - # the shared widgets, the folder picker, and one settings pane per - # app. Reached through the `apps.js` registry rather than imported - # by name anywhere, which is exactly why they have to be listed — - # nothing else would notice one of them changing. - "settings-ui.js", "folder-tree.js", - "chat-app-settings.js", "video-app-settings.js", - "music-app-settings.js", "photos-app-settings.js", - # The reference app (docs/refactor-groups.md §4.1). Hidden behind - # `?dev=1` client-side, but it is still served and still cached, so - # it participates in the hash like anything else here. - "helloworld-app.js", "helloworld-app-settings.js", - # Pages extracted from app.js — statically imported or lazy-loaded, - # but all must participate in the content hash. - "auth-page.js", "explore-page.js", "create-group-page.js", - "admin-page.js", "node-page.js", "group-name.js", - "search-page.js", "settings-page.js", "profile-page.js") +# The shell loads a few assets by name; everything else is imported by app.js +# from a relative path, which inherits the `/a/<hash>/` prefix the shell loaded +# app.js under — so the whole module graph moves together. def _asset_version() -> str: @@ -67,12 +36,19 @@ def _asset_version() -> str: for as long as that lasts, which is indistinguishable from the fix not working. Changing the URL is the only thing that reaches such a browser, and a content hash changes it exactly when the content changes. + + **Every file under the static directory**, because that is what `/a/<hash>/` + serves — with a year's `immutable`, so a file outside the hash is a file a + browser never fetches again. The fingerprint used to cover a hand-kept list + of top-level modules, and the translations and `vendor/` were not on it: a + change confined to the catalogues kept the hash, and a phone went on showing + a heading that had been rewritten and deployed. The path is hashed with the + content, so a rename or a new file moves the version too. """ h = hashlib.sha256() - for name in _ASSETS: - path = STATIC_DIR / name - if path.exists(): - h.update(path.read_bytes()) + for path in sorted(p for p in STATIC_DIR.rglob("*") if p.is_file()): + h.update(path.relative_to(STATIC_DIR).as_posix().encode() + b"\0") + h.update(path.read_bytes()) return h.hexdigest()[:12] diff --git a/packages/meshbay-hub/tests/test_asset_versioning.py b/packages/meshbay-hub/tests/test_asset_versioning.py index 3d84d99..0c93f5a 100644 --- a/packages/meshbay-hub/tests/test_asset_versioning.py +++ b/packages/meshbay-hub/tests/test_asset_versioning.py @@ -23,7 +23,7 @@ import re import pytest from fastapi.testclient import TestClient -from meshbay_hub.api.webapp import _ASSETS, ASSET_V, STATIC_DIR, _asset_version +from meshbay_hub.api.webapp import ASSET_V, STATIC_DIR, _asset_version from meshbay_hub.app import create_app @@ -103,26 +103,41 @@ def test_the_fingerprint_follows_the_content(tmp_path, monkeypatch): assert _asset_version() == before, "the fingerprint is not reproducible" -def test_every_static_script_participates_in_the_fingerprint(): +@pytest.mark.parametrize("relative", ["locales/en.js", "vendor/htm-preact.js", "style.css"]) +def test_a_change_anywhere_under_the_prefix_moves_the_fingerprint(relative): """ - `_ASSETS` is hand-maintained, and forgetting an entry fails silently: the - file is imported by the page, so the browser fetches it, but it does not - feed the content hash — so a change confined to that one file ships at the - URL a cache already holds. Nothing errors, and the symptom is a fix that - "doesn't work" on exactly the machines that visited before. + Everything under the static directory is served at `/a/<hash>/` and cached + as immutable, so everything has to feed the hash — subdirectories included. - Found by `source-merge.js`, added to the Search view and left out of the - list. It happened to be harmless that day because `search-page.js` changed - in the same commit and *is* listed — which is the worst way for this to go - unnoticed. The checklist in docs/apps.md §4 step 5 names this trap; this - enforces it instead of relying on remembering. - - `sw.js` is the one deliberate exclusion — the service worker is served - unversioned on purpose (`test_the_service_worker_is_not_versioned`). + The fingerprint used to cover a hand-kept list of top-level modules, and + the check that guarded the list globbed `*.js` at the top level only. The + catalogues and `vendor/` were on neither: a change confined to the English + catalogue shipped at the URL a phone already held, and it went on showing a + heading that had been rewritten and deployed. Pull-to-refresh fetches the + shell, which is `no-store` and was current; it does not refetch an + immutable file whose URL has not moved. """ - on_disk = {p.name for p in STATIC_DIR.glob("*.js")} - {"sw.js"} - missing = sorted(on_disk - set(_ASSETS)) - assert not missing, ( - f"static scripts missing from webapp._ASSETS: {missing}. A change to " - "one of these will not move the asset URL, so a browser that cached " - "the page keeps running the old copy") + before = _asset_version() + target = STATIC_DIR / relative + original = target.read_bytes() + try: + target.write_bytes(original + b"\n/* touched */\n") + assert _asset_version() != before, ( + f"the fingerprint did not move when {relative} did, so a browser " + "that cached it keeps the old copy for a year") + finally: + target.write_bytes(original) + assert _asset_version() == before + + +def test_a_new_file_moves_the_fingerprint(): + """A rename or an addition changes what is served even when no existing + file's bytes do.""" + before = _asset_version() + extra = STATIC_DIR / "locales" / "zz-test-only.js" + try: + extra.write_text("export default {};\n") + assert _asset_version() != before + finally: + extra.unlink() + assert _asset_version() == before |