From 1f7c5311aa4803c1ac2c4ccbb8ae8f9b63017a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 3 Oct 2024 21:27:27 +0200 Subject: [PATCH] [Website] Use site slug as a stable scope ## Change details With this PR, Playground sites will use a stable path such as `/scope:my-site-slug/` instead of a volatile, randomly generated one such as `/scope:0.489625368/`. ## Motivation WordPress stores URLs such as `` and `` in the post content in a raw form that includes the site path. However, Playground assigns each site a random one-time scope. This is a problem for persistent sites where you can upload an image today and have it referenced in a post as, say `` and still expect to have it working correctly tomorrow when the site URL changes to `/scope:1987578`. ## Testing instructions * Create a new site * Save it in the browser * Upload an image in a post * Save the post * Refresh the browser tab * Confirm that image still renders correctly * Do it again, but now upload that image before saving the site in the browser With the introduction of multiple persistent Playgrounds, the users will get to interact with the same site multiple times --- packages/php-wasm/scopes/src/index.spec.ts | 14 ++-- packages/php-wasm/scopes/src/index.ts | 18 ++--- .../web/src/lib/register-service-worker.ts | 68 +++++++++++-------- packages/playground/client/src/index.ts | 9 +++ .../remote/src/lib/boot-playground-remote.ts | 10 +-- .../remote/src/lib/playground-client.ts | 2 +- .../src/lib/state/redux/boot-site-client.ts | 1 + 7 files changed, 71 insertions(+), 51 deletions(-) diff --git a/packages/php-wasm/scopes/src/index.spec.ts b/packages/php-wasm/scopes/src/index.spec.ts index fbb0056e66..d7472711f8 100644 --- a/packages/php-wasm/scopes/src/index.spec.ts +++ b/packages/php-wasm/scopes/src/index.spec.ts @@ -2,8 +2,8 @@ import { getURLScope, isURLScoped, removeURLScope, setURLScope } from './index'; describe('getURLScope', () => { it('should return the scope from a scoped URL', () => { - const url = new URL('http://localhost/scope:12345/index.php'); - expect(getURLScope(url)).toBe('12345'); + const url = new URL('http://localhost/scope:scope-12345/index.php'); + expect(getURLScope(url)).toBe('scope-12345'); }); it('should return null from a non-scoped URL', () => { @@ -39,15 +39,15 @@ describe('removeURLScope', () => { describe('setURLScope', () => { it('should add the scope to a non-scoped URL', () => { const url = new URL('http://localhost/index.php'); - expect(setURLScope(url, '12345').href).toBe( - 'http://localhost/scope:12345/index.php' + expect(setURLScope(url, 'new-scope').href).toBe( + 'http://localhost/scope:new-scope/index.php' ); }); it('should replace the scope in a scoped URL', () => { - const url = new URL('http://localhost/scope:12345/index.php'); - expect(setURLScope(url, '67890').href).toBe( - 'http://localhost/scope:67890/index.php' + const url = new URL('http://localhost/scope:old-scope/index.php'); + expect(setURLScope(url, 'new-scope').href).toBe( + 'http://localhost/scope:new-scope/index.php' ); }); diff --git a/packages/php-wasm/scopes/src/index.ts b/packages/php-wasm/scopes/src/index.ts index ec187efa48..0b39b64dfe 100644 --- a/packages/php-wasm/scopes/src/index.ts +++ b/packages/php-wasm/scopes/src/index.ts @@ -1,5 +1,5 @@ /** - * Scopes are unique strings, like `96253`, used to uniquely brand + * Scopes are unique strings, like `my-site`, used to uniquely brand * the outgoing HTTP traffic from each browser tab. This helps the * main thread distinguish between the relevant and irrelevant * messages received from the Service Worker. @@ -7,7 +7,7 @@ * Scopes are included in the `PHPRequestHandler.absoluteUrl` as follows: * * An **unscoped** URL: http://localhost:8778/wp-login.php - * A **scoped** URL: http://localhost:8778/scope:96253/wp-login.php + * A **scoped** URL: http://localhost:8778/scope:my-site/wp-login.php * * For more information, see the README section on scopes. */ @@ -17,7 +17,7 @@ * * @example * ```js - * isURLScoped(new URL('http://localhost/scope:96253/index.php')); + * isURLScoped(new URL('http://localhost/scope:my-site/index.php')); * // true * * isURLScoped(new URL('http://localhost/index.php')); @@ -36,7 +36,7 @@ export function isURLScoped(url: URL): boolean { * * @example * ```js - * getScopeFromURL(new URL('http://localhost/scope:96253/index.php')); + * getScopeFromURL(new URL('http://localhost/scope:my-site/index.php')); * // '96253' * * getScopeFromURL(new URL('http://localhost/index.php')); @@ -58,11 +58,11 @@ export function getURLScope(url: URL): string | null { * * @example * ```js - * setURLScope(new URL('http://localhost/index.php'), '96253'); - * // URL('http://localhost/scope:96253/index.php') + * setURLScope(new URL('http://localhost/index.php'), 'my-site'); + * // URL('http://localhost/scope:my-site/index.php') * - * setURLScope(new URL('http://localhost/scope:96253/index.php'), '12345'); - * // URL('http://localhost/scope:12345/index.php') + * setURLScope(new URL('http://localhost/scope:my-site/index.php'), 'my-site'); + * // URL('http://localhost/scope:my-site/index.php') * * setURLScope(new URL('http://localhost/index.php'), null); * // URL('http://localhost/index.php') @@ -96,7 +96,7 @@ export function setURLScope(url: URL | string, scope: string | null): URL { * * @example * ```js - * removeURLScope(new URL('http://localhost/scope:96253/index.php')); + * removeURLScope(new URL('http://localhost/scope:my-site/index.php')); * // URL('http://localhost/index.php') * * removeURLScope(new URL('http://localhost/index.php')); 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 dd6cee85c6..4b6eddce43 100644 --- a/packages/php-wasm/web/src/lib/register-service-worker.ts +++ b/packages/php-wasm/web/src/lib/register-service-worker.ts @@ -35,13 +35,9 @@ export function setPhpInstanceUsedByServiceWorker(api: Client) { * reload the registered worker if the app expects a different version * than the currently registered one. * - * @param scope The numeric value used in the path prefix of the site - * this service worker is meant to serve. E.g. for a prefix - * like `/scope:793/`, the scope would be `793`. See the - * `@php-wasm/scopes` package for more details. * @param scriptUrl The URL of the service worker script. */ -export async function registerServiceWorker(scope: string, scriptUrl: string) { +export async function registerServiceWorker(scriptUrl: string) { const sw = navigator.serviceWorker; if (!sw) { /** @@ -77,30 +73,44 @@ export async function registerServiceWorker(scope: string, scriptUrl: string) { logger.error('Failed to update service worker.', e); } - // Proxy the service worker messages to the web worker: - navigator.serviceWorker.addEventListener( - 'message', - async function onMessage(event) { - /** - * Ignore events meant for other PHP instances to - * avoid handling the same event twice. - * - * This is important because the service worker posts the - * same message to all application instances across all browser tabs. - */ - if (scope && event.data.scope !== scope) { - return; - } + return { + /** + * Establishes the communication bridge between the service worker and the web worker + * where the current site is running. + * + * @param scope The string prefix used in the site URL served by the currently + * running remote.html. E.g. for a prefix like `/scope:playground/`, + * the scope would be `playground`. See the `@php-wasm/scopes` package + * for more details. + */ + startServiceWorkerCommunicationBridge({ scope }: { scope: string }) { + // Proxy the service worker messages to the web worker: + navigator.serviceWorker.addEventListener( + 'message', + async function onMessage(event) { + /** + * Ignore events meant for other PHP instances to + * avoid handling the same event twice. + * + * This is important because the service worker posts the + * same message to all application instances across all browser tabs. + */ + if (scope && event.data.scope !== scope) { + return; + } - // Wait for the PHP API client to be set by bootPlaygroundRemote - const phpApi = await phpApiPromise; + // Wait for the PHP API client to be set by bootPlaygroundRemote + const phpApi = await phpApiPromise; - const args = event.data.args || []; - const method = event.data.method as keyof Client; - const result = await (phpApi[method] as Function)(...args); - event.source!.postMessage(responseTo(event.data.requestId, result)); - } - ); - - sw.startMessages(); + const args = event.data.args || []; + const method = event.data.method as keyof Client; + const result = await (phpApi[method] as Function)(...args); + event.source!.postMessage( + responseTo(event.data.requestId, result) + ); + } + ); + sw.startMessages(); + }, + }; } diff --git a/packages/playground/client/src/index.ts b/packages/playground/client/src/index.ts index 26b2de4b3c..d559877f16 100644 --- a/packages/playground/client/src/index.ts +++ b/packages/playground/client/src/index.ts @@ -68,6 +68,13 @@ export interface StartPlaygroundOptions { onBeforeBlueprint?: () => Promise; mounts?: Array; shouldInstallWordPress?: boolean; + /** + * The string prefix used in the site URL served by the currently + * running remote.html. E.g. for a prefix like `/scope:playground/`, + * the scope would be `playground`. See the `@php-wasm/scopes` package + * for more details. + */ + scope?: string; } /** @@ -88,6 +95,7 @@ export async function startPlaygroundWeb({ sapiName, onBeforeBlueprint, mounts, + scope, shouldInstallWordPress, }: StartPlaygroundOptions): Promise { assertValidRemote(remoteUrl); @@ -128,6 +136,7 @@ export async function startPlaygroundWeb({ await playground.boot({ mounts, sapiName, + scope: scope ?? Math.random().toFixed(16), shouldInstallWordPress, phpVersion: compiled.versions.php, wpVersion: compiled.versions.wp, diff --git a/packages/playground/remote/src/lib/boot-playground-remote.ts b/packages/playground/remote/src/lib/boot-playground-remote.ts index 02ca085a50..17c72a0a6f 100644 --- a/packages/playground/remote/src/lib/boot-playground-remote.ts +++ b/packages/playground/remote/src/lib/boot-playground-remote.ts @@ -57,8 +57,8 @@ export async function bootPlaygroundRemote() { document.body.prepend(bar.element); } - const scope = Math.random().toFixed(16); - await registerServiceWorker(scope, serviceWorkerUrl + ''); + const { startServiceWorkerCommunicationBridge } = + await registerServiceWorker(serviceWorkerUrl + ''); const phpWorkerApi = consumeAPI( await spawnPHPWorkerThread(workerUrl) @@ -208,9 +208,9 @@ export async function bootPlaygroundRemote() { }, async boot(options) { - await phpWorkerApi.boot({ - ...options, - scope, + await phpWorkerApi.boot(options); + startServiceWorkerCommunicationBridge({ + scope: options.scope, }); try { diff --git a/packages/playground/remote/src/lib/playground-client.ts b/packages/playground/remote/src/lib/playground-client.ts index a49f56f6b5..22b36e26c8 100644 --- a/packages/playground/remote/src/lib/playground-client.ts +++ b/packages/playground/remote/src/lib/playground-client.ts @@ -68,7 +68,7 @@ export interface WebClientMixin extends ProgressReceiver { unmountOpfs(mountpoint: string): Promise; - boot(options: Omit): Promise; + boot(options: WorkerBootOptions): Promise; } /** diff --git a/packages/playground/website/src/lib/state/redux/boot-site-client.ts b/packages/playground/website/src/lib/state/redux/boot-site-client.ts index aabb8c3bbc..7df006b8bf 100644 --- a/packages/playground/website/src/lib/state/redux/boot-site-client.ts +++ b/packages/playground/website/src/lib/state/redux/boot-site-client.ts @@ -115,6 +115,7 @@ export function bootSiteClient( playground = await startPlaygroundWeb({ iframe: iframe!, remoteUrl: getRemoteUrl().toString(), + scope: site.slug, blueprint, // Intercept the Playground client even if the // Blueprint fails.