Skip to content

Handle aggressive caching of playground #936

Open

Description

@joshunrau

Since the playground is very large, browsers very aggressively cache the code. As a result, users often report problems that are already fixed. When it is built, it should invalidate the cache without deleting local storage unless the schema change is reported

Activity

  1. changed the title [-]Handle agressive caching of playground[/-] [+]Handle aggressive caching of playground[/+] on Sep 27, 2026
  2. added
    Difficulty: MediumStraightforward change, about a day or less
    Area: Playgroundapps/playground (in-browser instrument editor)
    Area: InfrastructureDeployment, Docker, CI, logging, CLIs, build and test tooling
    on Sep 27, 2026
  3. joshunrau commented on Oct 26, 2026

    @joshunrau
    CollaboratorAuthor

    Still reproduces on main (d86740e) and on playground.opendatacapture.org. Checked today: / has Last-Modified and ETag but no Cache-Control, and /assets/does-not-exist.js and /runtime/v1/zod@3.x/does-not-exist.js both return 200 text/html with index.html's ETag.

    Since 3a798fc the image is Caddy, and apps/playground/Caddyfile:9-12 is apps/web/Caddyfile plus a /config.json handle, so this is the #1771 mechanism. Fix both files in one PR with the layout and CI check proposed on #1771: immutable only for /assets/* files that exist, no-cache for /runtime/* and the SPA fallback, and try_files inside the catch-all handle. The playground-specific parts follow.

    The image's /config.json handle never runs

    Caddy sorts site-level directives by its directive order, and try_files comes before handle. /config.json is not a file in /srv, so try_files rewrites it to /index.html before handle /config.json is evaluated, and the handle never matches. The image answers /config.json with index.html, and fetchPlaygroundConfig (src/preview/config.ts:14-20) throws on response.json(). No test sees it because the e2e suite drives the Vite dev server, whose playgroundConfig plugin serves /config.json itself. Moving try_files into the catch-all fixes this too. The CI check on /config.json should assert the JSON body as well as no-store.

    The hosted site does answer /config.json with the JSON, so it is not serving the committed Caddyfile unchanged. Whatever serves it needs the same headers, or the stale-build reports continue after the image is fixed. The note for other web servers in docs/en/2-tutorials/2.4-playground-deployment.md:105 should list the cache headers next to config.json. #1822 edits that doc and .github/workflows/ci.yaml, so whichever lands second rebases.

    Other playground notes

    • preview.html falls into the catch-all and gets no-cache, which it needs. The editor and the frame are cached on different origins, and neither $EditorMessage nor $PreviewMessage (src/preview/protocol.ts:34-50) carries a version, so a stale frame cannot tell it is out of step.
    • No vite:preloadError listener here either. The two dynamic imports, IndexPage (src/App.tsx:7) and esbuild-wasm (src/pages/IndexPage.tsx:14), both run at boot. What a tab left open across a deploy fetches later is a Monaco worker (src/components/Editor/setup.ts:20-33), created when a file of that language is first opened, and a worker that fails to load does not fire that event.

    Local storage

    Cache headers never touch IndexedDB, so the HTTP fix cannot delete saved work. A persist version change can. The store has version: 1 and no migrate (src/store/index.ts:84). On a mismatch with no migrate, zustand 5.0.15 logs a console.error and calls merge(undefined, currentState). The merge at lines 49-75 then keeps only the bundled instruments, and the next set writes that over the app entry.

    This has happened before. d04566b (June 2024) bumped version from 0 to 1 with no migrate, and zustand 4.5.2 wrote the reset state back during hydration, discarding every instrument saved before it. Then 7123b27 (November 2025) moved the store from localStorage to IndexedDB without reading the old entry, so instruments saved before that are still under app in localStorage, unread.

    #1756's suggested fix is the next version bump. Its migrate should validate the old shape. On failure it should keep the raw entry under another key and tell the user, with an en and fr string through useTranslation, instead of returning undefined. A one-time import of the old localStorage entry when IndexedDB has none can go in the same change. Cover it with a unit test in apps/playground/src/store/__tests__/ (mock idb-keyval) and a step in testing/src/specs/playground.spec.ts that seeds an old-shape entry, reloads, and finds the saved instrument.

    Separately: on the hosted site /plsb/js/script.js, the Plausible script that apps/playground/index.html:8-13 loads, also returns index.html, so the playground records no visits. Nothing in front of the files routes /plsb. That needs its own issue, like #1777 for the marketing site.

    Triage note written with Claude Code. Code references point at main @ d86740e.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: InfrastructureDeployment, Docker, CI, logging, CLIs, build and test toolingArea: Playgroundapps/playground (in-browser instrument editor)BugType: existing behavior is wrongDifficulty: MediumStraightforward change, about a day or lessPriority: MediumReal value; schedule normally

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions