Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register service worker before spawning the worker thread #1606

Merged
merged 9 commits into from
Jul 15, 2024
2 changes: 1 addition & 1 deletion packages/php-wasm/web/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type { LoaderOptions as PHPWebLoaderOptions } from './load-runtime';

export { loadWebRuntime } from './load-runtime';
export { getPHPLoaderModule } from './get-php-loader-module';
export { registerServiceWorker } from './register-service-worker';
export { registerServiceWorker, setPhpApi } from './register-service-worker';
export { setupPostMessageRelay } from './setup-post-message-relay';

export { spawnPHPWorkerThread } from './worker-thread/spawn-php-worker-thread';
Expand Down
44 changes: 34 additions & 10 deletions packages/php-wasm/web/src/lib/register-service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,43 @@ import { PhpWasmError } from '@php-wasm/util';
import { responseTo } from '@php-wasm/web-service-worker';
import { Remote } from 'comlink';

export interface Client extends Remote<PHPWorker> {}

/**
* Resolves when the PHP API client is set.
*
* This allows us to wait for the PHP API client to be set before proxying service worker messages to the web worker.
*/
let resolvePhpApi: (api: Client) => void;
export const phpApiPromise = new Promise<Client>((resolve) => {
resolvePhpApi = resolve;
});

/**
* Sets the PHP API client.
*
* @param {Client} api The PHP API client.
*
*/
export function setPhpApi(api: Client) {
if (!api) {
throw new PhpWasmError('PHP API client must be a valid client object.');
}
resolvePhpApi(api);
}

/**
* Run this in the main application to register the service worker or
* reload the registered worker if the app expects a different version
* than the currently registered one.
*
* @param {string} scriptUrl The URL of the service worker script.
* @param {string} expectedVersion The expected version of the service worker script. If
* mismatched with the actual version, the service worker
* will be re-registered.
* @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<Client extends Remote<PHPWorker>>(
phpApi: Client,
scope: string,
scriptUrl: string
) {
export async function registerServiceWorker(scope: string, scriptUrl: string) {
const sw = navigator.serviceWorker;
if (!sw) {
/**
Expand Down Expand Up @@ -60,8 +82,10 @@ export async function registerServiceWorker<Client extends Remote<PHPWorker>>(
return;
}

const args = event.data.args || [];
// 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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export async function spawnPHPWorkerThread(
workerUrl: string,
startupOptions: Record<string, string | string[]> = {}
) {
workerUrl = addQueryParams(workerUrl, startupOptions);
const worker = new Worker(workerUrl, { type: 'module' });
return new Promise<Worker>((resolve, reject) => {
worker.onerror = (e) => {
Expand All @@ -21,6 +20,10 @@ export async function spawnPHPWorkerThread(
(error as any).filename = e.filename;
reject(error);
};
worker.postMessage({
type: 'startup-options',
startupOptions,
});
// There is no way to know when the worker script has started
// executing, so we use a message to signal that.
function onStartup(event: { data: string }) {
Expand All @@ -32,23 +35,3 @@ export async function spawnPHPWorkerThread(
worker.addEventListener('message', onStartup);
});
}

function addQueryParams(
url: string | URL,
searchParams: Record<string, string | string[]>
): string {
if (!Object.entries(searchParams).length) {
return url + '';
}
const urlWithOptions = new URL(url);
for (const [key, value] of Object.entries(searchParams)) {
if (Array.isArray(value)) {
for (const item of value) {
urlWithOptions.searchParams.append(key, item);
}
} else {
urlWithOptions.searchParams.set(key, value);
}
}
return urlWithOptions.toString();
}
38 changes: 21 additions & 17 deletions packages/playground/remote/src/lib/boot-playground-remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '@php-wasm/universal';
import {
registerServiceWorker,
setPhpApi,
spawnPHPWorkerThread,
exposeAPI,
consumeAPI,
Expand Down Expand Up @@ -71,7 +72,11 @@ export async function bootPlaygroundRemote() {
);
const withNetworking = query.get('networking') === 'yes';
const sapiName = query.get('sapi-name') || undefined;
const workerApi = consumeAPI<PlaygroundWorkerEndpoint>(

const scope = Math.random().toFixed(16);
await registerServiceWorker(scope, serviceWorkerUrl + '');

const phpApi = consumeAPI<PlaygroundWorkerEndpoint>(
await spawnPHPWorkerThread(workerUrl, {
wpVersion,
phpVersion,
Expand All @@ -80,25 +85,28 @@ export async function bootPlaygroundRemote() {
storage: query.get('storage') || '',
...(sapiName ? { sapiName } : {}),
'site-slug': query.get('site-slug') || 'wordpress',
scope,
})
);
// Set PHP API in the service worker
setPhpApi(phpApi);

const wpFrame = document.querySelector('#wp') as HTMLIFrameElement;
const webApi: WebClientMixin = {
async onDownloadProgress(fn) {
return workerApi.onDownloadProgress(fn);
return phpApi.onDownloadProgress(fn);
},
async journalFSEvents(root: string, callback) {
return workerApi.journalFSEvents(root, callback);
return phpApi.journalFSEvents(root, callback);
},
async replayFSJournal(events: FilesystemOperation[]) {
return workerApi.replayFSJournal(events);
return phpApi.replayFSJournal(events);
},
async addEventListener(event, listener) {
return await workerApi.addEventListener(event, listener);
return await phpApi.addEventListener(event, listener);
},
async removeEventListener(event, listener) {
return await workerApi.removeEventListener(event, listener);
return await phpApi.removeEventListener(event, listener);
},
async setProgress(options: ProgressBarOptions) {
if (!bar) {
Expand Down Expand Up @@ -183,7 +191,7 @@ export async function bootPlaygroundRemote() {
* @returns
*/
async onMessage(callback: MessageListener) {
return await workerApi.onMessage(callback);
return await phpApi.onMessage(callback);
},
/**
* Ditto for this function.
Expand All @@ -195,11 +203,11 @@ export async function bootPlaygroundRemote() {
options: BindOpfsOptions,
onProgress?: SyncProgressCallback
) {
return await workerApi.bindOpfs(options, onProgress);
return await phpApi.bindOpfs(options, onProgress);
},
};

await workerApi.isConnected();
await phpApi.isConnected();

// If onDownloadProgress is not explicitly re-exposed here,
// Comlink will throw an error and claim the callback
Expand All @@ -208,22 +216,18 @@ export async function bootPlaygroundRemote() {
// https://github.com/GoogleChromeLabs/comlink/issues/426#issuecomment-578401454
// @TODO: Handle the callback conversion automatically and don't explicitly re-expose
// the onDownloadProgress method
const [setAPIReady, setAPIError, playground] = exposeAPI(webApi, workerApi);
const [setAPIReady, setAPIError, playground] = exposeAPI(webApi, phpApi);

try {
await workerApi.isReady();
await registerServiceWorker(
workerApi,
await workerApi.scope,
serviceWorkerUrl + ''
);
await phpApi.isReady();

setupPostMessageRelay(
wpFrame,
getOrigin((await playground.absoluteUrl)!)
);
setupMountListener(playground);
if (withNetworking) {
await setupFetchNetworkTransport(workerApi);
await setupFetchNetworkTransport(phpApi);
}

setAPIReady();
Expand Down
10 changes: 9 additions & 1 deletion packages/playground/remote/src/lib/worker-thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
downloadMonitor,
spawnHandlerFactory,
createPhpRuntime,
setStartupOptions,
waitForStartupOptions,
} from './worker-utils';
import {
FilesystemOperation,
Expand All @@ -43,7 +45,13 @@ import {
import { wpVersionToStaticAssetsDirectory } from '@wp-playground/wordpress-builds';
import { logger } from '@php-wasm/logger';

const scope = Math.random().toFixed(16);
/**
* Startup options are received from spawnPHPWorkerThread using a message event.
* We need to wait for startup options to be received to setup the worker thread.
*/
setStartupOptions(await waitForStartupOptions());

const scope = startupOptions.scope;

// post message to parent
self.postMessage('worker-script-started');
Expand Down
67 changes: 40 additions & 27 deletions packages/playground/remote/src/lib/worker-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type ReceivedStartupOptions = {
storage?: string;
phpExtensions?: string[];
siteSlug?: string;
scope?: string;
};

export type ParsedStartupOptions = {
Expand All @@ -30,36 +31,48 @@ export type ParsedStartupOptions = {
storage: string;
phpExtensions: string[];
siteSlug: string;
scope: string;
};

export const receivedParams: ReceivedStartupOptions = {};
const url = self?.location?.href;
if (typeof url !== 'undefined') {
const params = new URL(self.location.href).searchParams;
receivedParams.wpVersion = params.get('wpVersion') || undefined;
receivedParams.phpVersion = params.get('phpVersion') || undefined;
receivedParams.storage = params.get('storage') || undefined;
// Default to CLI to support the WP-CLI Blueprint step
receivedParams.sapiName = params.get('sapiName') || 'cli';
receivedParams.phpExtensions = params.getAll('php-extension');
receivedParams.siteSlug = params.get('site-slug') || undefined;
}
export let requestedWPVersion: string;
export let startupOptions: ParsedStartupOptions;

export const setStartupOptions = (receivedParams: ReceivedStartupOptions) => {
requestedWPVersion = receivedParams.wpVersion || '';
startupOptions = {
wpVersion: SupportedWordPressVersionsList.includes(requestedWPVersion)
? requestedWPVersion
: LatestSupportedWordPressVersion,
phpVersion: SupportedPHPVersionsList.includes(
receivedParams.phpVersion || ''
)
? (receivedParams.phpVersion as SupportedPHPVersion)
: '8.0',
sapiName: receivedParams.sapiName || 'cli',
storage: receivedParams.storage || 'local',
phpExtensions: receivedParams.phpExtensions || [],
siteSlug: receivedParams.siteSlug,
scope: receivedParams.scope || '',
} as ParsedStartupOptions;
};

export const requestedWPVersion = receivedParams.wpVersion || '';
export const startupOptions = {
wpVersion: SupportedWordPressVersionsList.includes(requestedWPVersion)
? requestedWPVersion
: LatestSupportedWordPressVersion,
phpVersion: SupportedPHPVersionsList.includes(
receivedParams.phpVersion || ''
)
? (receivedParams.phpVersion as SupportedPHPVersion)
: '8.0',
sapiName: receivedParams.sapiName || 'cli',
storage: receivedParams.storage || 'local',
phpExtensions: receivedParams.phpExtensions || [],
siteSlug: receivedParams.siteSlug,
} as ParsedStartupOptions;
export const waitForStartupOptions = async () => {
return new Promise<ReceivedStartupOptions>((resolve) => {
self.addEventListener('message', async (event) => {
if (event.data.type === 'startup-options') {
resolve({
wpVersion: event.data.startupOptions.wpVersion,
phpVersion: event.data.startupOptions.phpVersion,
storage: event.data.startupOptions.storage,
sapiName: event.data.startupOptions.sapiName,
phpExtensions: event.data.startupOptions['php-extension'],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to make these names consistent one day

siteSlug: event.data.startupOptions['site-slug'],
scope: event.data.startupOptions.scope,
});
}
});
});
};

export const downloadMonitor = new EmscriptenDownloadMonitor();

Expand Down
Loading