Skip to content

Commit

Permalink
[Website] Use site slug as a stable scope
Browse files Browse the repository at this point in the history
 ## 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 `<img src="">` and `<a href="">` 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 `<img src="/scope:98742/wp-uploads/img.jpg">`
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
  • Loading branch information
adamziel committed Oct 3, 2024
1 parent b7bab1a commit a7b0d5d
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 51 deletions.
14 changes: 7 additions & 7 deletions packages/php-wasm/scopes/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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'
);
});

Expand Down
18 changes: 9 additions & 9 deletions packages/php-wasm/scopes/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/**
* 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.
*
* 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.
*/
Expand All @@ -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'));
Expand All @@ -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'));
Expand All @@ -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')
Expand Down Expand Up @@ -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'));
Expand Down
68 changes: 39 additions & 29 deletions packages/php-wasm/web/src/lib/register-service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
/**
Expand Down Expand Up @@ -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();
},
};
}
9 changes: 9 additions & 0 deletions packages/playground/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ export interface StartPlaygroundOptions {
onBeforeBlueprint?: () => Promise<void>;
mounts?: Array<MountDescriptor>;
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;
}

/**
Expand All @@ -88,6 +95,7 @@ export async function startPlaygroundWeb({
sapiName,
onBeforeBlueprint,
mounts,
scope,
shouldInstallWordPress,
}: StartPlaygroundOptions): Promise<PlaygroundClient> {
assertValidRemote(remoteUrl);
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions packages/playground/remote/src/lib/boot-playground-remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlaygroundWorkerEndpoint>(
await spawnPHPWorkerThread(workerUrl)
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/remote/src/lib/playground-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export interface WebClientMixin extends ProgressReceiver {

unmountOpfs(mountpoint: string): Promise<void>;

boot(options: Omit<WorkerBootOptions, 'scope'>): Promise<void>;
boot(options: WorkerBootOptions): Promise<void>;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit a7b0d5d

Please sign in to comment.