diff --git a/packages/php-wasm/web-service-worker/src/initialize-service-worker.ts b/packages/php-wasm/web-service-worker/src/initialize-service-worker.ts index 6c6872256d..22c4d79aa5 100644 --- a/packages/php-wasm/web-service-worker/src/initialize-service-worker.ts +++ b/packages/php-wasm/web-service-worker/src/initialize-service-worker.ts @@ -16,36 +16,7 @@ import { * @param config */ export function initializeServiceWorker(config: ServiceWorkerConfiguration) { - const { version, handleRequest = defaultRequestHandler } = config; - /** - * Enable the client app to force-update the service worker - * registration. - */ - self.addEventListener('message', (event) => { - if (!event.data) { - return; - } - - if (event.data === 'skip-waiting') { - self.skipWaiting(); - } - }); - - /** - * Ensure the client gets claimed by this service worker right after the registration. - * - * Only requests from the "controlled" pages are resolved via the fetch listener below. - * However, simply registering the worker is not enough to make it the "controller" of - * the current page. The user still has to reload the page. If they don't an iframe - * pointing to /index.php will show a 404 message instead of a homepage. - * - * This activation handles saves the user reloading the page after the initial confusion. - * It immediately makes this worker the controller of any client that registers it. - */ - self.addEventListener('activate', (event) => { - // eslint-disable-next-line no-undef - event.waitUntil(self.clients.claim()); - }); + const { handleRequest = defaultRequestHandler } = config; /** * The main method. It captures the requests and loop them back to the @@ -54,24 +25,6 @@ export function initializeServiceWorker(config: ServiceWorkerConfiguration) { self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); - // Provide a custom JSON response in the special /version endpoint - // so the frontend app can know whether it's time to update the - // service worker registration. - if (url.pathname === '/version') { - event.preventDefault(); - const currentVersion = - typeof version === 'function' ? version() : version; - event.respondWith( - new Response(JSON.stringify({ version: currentVersion }), { - headers: { - 'Content-Type': 'application/json', - }, - status: 200, - }) - ); - return; - } - // Don't handle requests to the service worker script itself. if (url.pathname.startsWith(self.location.pathname)) { return; @@ -236,13 +189,6 @@ export async function broadcastMessageExpectReply(message: any, scope: string) { } interface ServiceWorkerConfiguration { - /** - * The version of the service worker – exposed via the /version endpoint. - * - * This is used by the frontend app to know whether it's time to update - * the service worker registration. - */ - version: string | (() => string); handleRequest?: (event: FetchEvent) => Promise | undefined; } diff --git a/packages/php-wasm/web/src/lib/register-service-worker.ts b/packages/php-wasm/web/src/lib/register-service-worker.ts index 54ed423b15..102a1305b4 100644 --- a/packages/php-wasm/web/src/lib/register-service-worker.ts +++ b/packages/php-wasm/web/src/lib/register-service-worker.ts @@ -14,65 +14,28 @@ import { Remote } from 'comlink'; */ export async function registerServiceWorker< Client extends Remote ->(phpApi: Client, scope: string, scriptUrl: string, expectedVersion: string) { - const sw = (navigator as any).serviceWorker; +>(phpApi: Client, scope: string, scriptUrl: string) { + const sw = navigator.serviceWorker; if (!sw) { throw new Error('Service workers are not supported in this browser.'); } - const registrations = await sw.getRegistrations(); - if (registrations.length > 0) { - const actualVersion = await getRegisteredServiceWorkerVersion(); - if (expectedVersion !== actualVersion) { - console.debug( - `[window] Reloading the currently registered Service Worker ` + - `(expected version: ${expectedVersion}, registered version: ${actualVersion})` - ); - for (const registration of registrations) { - let unregister = false; - try { - await registration.update(); - } catch (e) { - // If the worker registration cannot be updated, - // we're probably seeing a blank page in the dev - // mode. Let's unregister the worker and reload - // the page. - unregister = true; - } - const waitingWorker = - registration.waiting || registration.installing; - if (waitingWorker && !unregister) { - if (actualVersion !== null) { - // If the worker exposes a version, it supports - // a "skip-waiting" message – let's force it to - // skip waiting. - waitingWorker.postMessage('skip-waiting'); - } else { - // If the version is not exposed, we can't force - // the worker to skip waiting – let's unregister - // and reload the page. - unregister = true; - } - } - if (unregister) { - await registration.unregister(); - window.location.reload(); - } - } - } - } else { - console.debug( - `[window] Creating a Service Worker registration (version: ${expectedVersion})` - ); - await sw.register(scriptUrl, { - type: 'module', - }); - } + + console.debug(`[window][sw] Registering a Service Worker`); + const registration = await sw.register(scriptUrl, { + type: 'module', + // Always bypass HTTP cache when fetching the new Service Worker script: + updateViaCache: 'none', + }); + + // Check if there's a new service worker available and, if so, enqueue + // the update: + await registration.update(); // Proxy the service worker messages to the worker thread: navigator.serviceWorker.addEventListener( 'message', async function onMessage(event) { - console.debug('Message from ServiceWorker', event); + console.debug('[window][sw] Message from ServiceWorker', event); /** * Ignore events meant for other PHP instances to * avoid handling the same event twice. @@ -94,13 +57,3 @@ export async function registerServiceWorker< sw.startMessages(); } - -async function getRegisteredServiceWorkerVersion() { - try { - const response = await fetch('/version'); - const data = await response.json(); - return data.version; - } catch (e) { - return null; - } -} diff --git a/packages/playground/remote/service-worker.ts b/packages/playground/remote/service-worker.ts index 81ff1d62ac..f15733e3d3 100644 --- a/packages/playground/remote/service-worker.ts +++ b/packages/playground/remote/service-worker.ts @@ -13,9 +13,6 @@ import { } from '@php-wasm/web-service-worker'; import { isUploadedFilePath } from './src/lib/is-uploaded-file-path'; -// @ts-ignore -import { serviceWorkerVersion } from 'virtual:service-worker-version'; - if (!(self as any).document) { // Workaround: vite translates import.meta.url // to document.currentScript which fails inside of @@ -26,11 +23,6 @@ if (!(self as any).document) { } initializeServiceWorker({ - // Always use a random version in development to avoid caching issues. - // @ts-ignore - version: import.meta.env.DEV - ? () => Math.random() + '' - : serviceWorkerVersion, handleRequest(event) { const fullUrl = new URL(event.request.url); let scope = getURLScope(fullUrl); diff --git a/packages/playground/remote/src/lib/boot-playground-remote.ts b/packages/playground/remote/src/lib/boot-playground-remote.ts index 5ad0246719..1011be04cf 100644 --- a/packages/playground/remote/src/lib/boot-playground-remote.ts +++ b/packages/playground/remote/src/lib/boot-playground-remote.ts @@ -9,8 +9,6 @@ import { consumeAPI, recommendedWorkerBackend, } from '@php-wasm/web'; -// @ts-ignore -import { serviceWorkerVersion } from 'virtual:service-worker-version'; import type { PlaygroundWorkerEndpoint } from './worker-thread'; import type { WebClientMixin } from './playground-client'; @@ -47,6 +45,18 @@ import serviceWorkerPath from '../../service-worker.ts?worker&url'; import { LatestSupportedWordPressVersion } from './get-wordpress-module'; export const serviceWorkerUrl = new URL(serviceWorkerPath, origin); +// Prevent Vite from hot-reloading this file – it would +// cause bootPlaygroundRemote() to register another web worker +// without unregistering the previous one. The first web worker +// would then fight for service worker requests with the second +// one. It's a difficult problem to debug and HMR isn't that useful +// here anyway – let's just disable it for this file. +// @ts-ignore +if (import.meta.hot) { + // @ts-ignore + import.meta.hot.accept(() => {}); +} + const query = new URL(document.location.href).searchParams; export async function bootPlaygroundRemote() { assertNotInfiniteLoadingLoop(); @@ -158,11 +168,10 @@ export async function bootPlaygroundRemote() { await registerServiceWorker( workerApi, await workerApi.scope, - serviceWorkerUrl + '', - serviceWorkerVersion + serviceWorkerUrl + '' ); - setupPostMessageRelay(wpFrame, getOrigin(await playground.absoluteUrl)); wpFrame.src = await playground.pathToInternalUrl('/'); + setupPostMessageRelay(wpFrame, getOrigin(await playground.absoluteUrl)); setAPIReady(); diff --git a/packages/playground/remote/vite.config.ts b/packages/playground/remote/vite.config.ts index 427c6e5fd9..bdeaa545e2 100644 --- a/packages/playground/remote/vite.config.ts +++ b/packages/playground/remote/vite.config.ts @@ -5,8 +5,6 @@ import dts from 'vite-plugin-dts'; // eslint-disable-next-line @nx/enforce-module-boundaries import { remoteDevServerHost, remoteDevServerPort } from '../build-config'; // eslint-disable-next-line @nx/enforce-module-boundaries -import virtualModule from '../vite-virtual-module'; -// eslint-disable-next-line @nx/enforce-module-boundaries import { viteTsConfigPaths } from '../../vite-ts-config-paths'; const path = (filename: string) => new URL(filename, import.meta.url).pathname; @@ -19,11 +17,6 @@ const plugins = [ tsConfigFilePath: join(__dirname, 'tsconfig.lib.json'), skipDiagnostics: true, }), - virtualModule({ - name: 'service-worker-version', - // @TODO: compute a hash of the service worker chunk instead of using the build timestamp - content: `export const serviceWorkerVersion = '${Date.now()}';`, - }), ]; export default defineConfig({ assetsInclude: ['**/*.wasm', '*.data'],