Skip to content

[PWA] Hoist the generated worker's importScripts above the AMD factory and pin the forced activate sweep at browser level (the #2475 'window open' red was a test observation race) #2639

Description

@Chris0Jeky

What

The forced static-cache re-sweep that PR #2416 (closes #2411) placed inside event.waitUntil on activate in frontend/taskdeck-web/public/api-cache-cleanup.js never executes in the generated worker. The activate listener is attached from inside vite-plugin-pwa's asynchronous AMD factory (the file is pulled in by importScripts inside the factory's promise continuation), so the lifecycle event has already been dispatched by the time the listener exists. The install listener attached the same way does receive its event; the activate one does not, on every run.

Consequence on current main: the install-to-activation re-poisoning window that #2411's review identified as HIGH is still open. A static-cache entry that the old (#2350-era) worker admits into taskdeck-static-assets after the evaluation-time sweep and before the replacement activates survives the migration. The evaluation-time sweep with its one-time marker is the only sweep that runs.

Evidence (2026-09-05, bounded attempt under #2475, branch issue-2475/pwa-activation-race)

  • Deterministic browser case tests/e2e/pwa-proof-strict.spec.ts (gated by TASKDECK_E2E_PWA_PREVIEW=1, playwright.pwa-proof.config.ts, self-started backend on 5031 and vite preview on 4193) holds the window open instead of racing into it: registerType: 'prompt' parks the installed replacement in waiting until the page sends taskdeck:skip-waiting, so the seed is written by the OLD worker's own CacheFirst handler from a real 200 response, strictly after the marker cache exists and while registration.waiting is non-null and the policy handshake still answers legacy-api-cache-retired.
  • Against unmodified main: 3 of 3 runs red at the final assertion, PROOF raceEntriesAfter = ["http://localhost:4193/icons/icon-192x192.png?td-race-seed=1"].
  • With a candidate hook appended to the cleanup script (self.serviceWorker statechange to activating/activated calling retireCaches({ force: true })), rebuilt: 2 of 2 runs green, raceEntriesAfter = []. The hook was reverted; the branch touches no production file.
  • Instrumented build (reverted): caches.keys() after a completed migration contained td-diag-evaluated, td-diag-state-installing, td-diag-install-entered, td-diag-state-installed, td-diag-state-activating and never td-diag-activate-entered or td-diag-activate-swept.
  • This reproduces the __proofActivateFired:false measurement recorded on PR Keep configured API paths out of PWA runtime caches #2416 on 2026-09-04; the consequence was not drawn then, and the repair for the window was placed inside the same inert listener. The generated-worker unit tests dispatch activate themselves against a fake CacheStorage, which is why they pass.
  • The case is committed with test.fail() on that branch (b51002b) so the gated lane stays green until the production repair lands; it flips the lane red the moment the case starts passing, which is the removal signal.

Ask

  1. Bind the forced re-sweep to something that actually fires in the generated worker, and prove it in a real Chromium, not with a dispatched fake event. Candidates, in order of preference: emit the cleanup script's lifecycle listeners at top level of the generated worker (workbox importScripts placed before the AMD define, or the injectManifest strategy with a hand-written worker source) so activate is attached synchronously and event.waitUntil semantics are real; otherwise the statechange hook, accepting and documenting that it cannot extend activation.
  2. Keep waitUntil semantics if at all possible: a sweep that runs after clients.claim() leaves a window where the new worker already controls the page.
  3. Adopt the branch's deterministic case as the regression (remove test.fail() once green), make the generated-worker unit tests stop dispatching activate by hand or make them run the emitted worker in a way that exercises the real attachment order, and correct docs/platform/PWA_OFFLINE_BEHAVIOR.md, the PWA row of autodoc/AGENT_INDEX.md and the comment block in public/api-cache-cleanup.js (lines 104-115 already record the inertness; the file's own doc still claims activation fails on error).
  4. Record the finding on [Security][PWA] Static-asset runtime cache admits API responses when VITE_API_BASE_URL is nested under /assets/ or /icons/ #2411 and [Security][PWA] Remove authenticated API responses from the service-worker cache #2350; the readiness view's PWA isolation claim depends on it.

Refs #2475, #2411, #2416, #2350. Priority I: this is the v0.3 PWA isolation blocker reopened.

Activity

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

    Priority IIThe active direction's next tranche: wedge capabilities, significant defects, near-term hardening.bugSomething isn't workingfrontendPrimary implementation impact in Vue/TypeScript UI and client runtime.hardeningReliability, safety, operability, and production-readiness improvements.securityAuthentication, authorization, data protection, abuse prevention, and compliance-related changes.

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions