diff --git a/dist-web/app.js b/dist-web/app.js
index f9150ccebd..943ff345cc 100644
--- a/dist-web/app.js
+++ b/dist-web/app.js
@@ -39,14 +39,17 @@
// src/web/library.js
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, 50));
- async function registerServiceWorker(url, onRequest) {
+ async function registerServiceWorker({ url, onRequest, scope }) {
if (!navigator.serviceWorker) {
alert("Service workers are not supported in this browser.");
throw new Exception("Service workers are not supported in this browser.");
}
await navigator.serviceWorker.register(url);
- const serviceWorkerChannel = new BroadcastChannel("wordpress-service-worker");
+ const serviceWorkerChannel = new BroadcastChannel(`wordpress-service-worker`);
serviceWorkerChannel.addEventListener("message", async function onMessage(event) {
+ if (scope && event.data.scope !== scope) {
+ return;
+ }
console.debug(`[Main] "${event.data.type}" message received from a service worker`);
let result;
if (event.data.type === "request" || event.data.type === "httpRequest") {
@@ -67,12 +70,8 @@
navigator.serviceWorker.startMessages();
await sleep(0);
const wordPressDomain = new URL(url).origin;
- const response = await fetch(`${wordPressDomain}/wp-admin/atomlib.php`);
- if (!response.ok) {
- window.location.reload();
- }
}
- async function createWordPressWorker({ backend, wordPressSiteUrl: wordPressSiteUrl2 }) {
+ async function createWordPressWorker({ backend, wordPressSiteUrl: wordPressSiteUrl2, scope }) {
while (true) {
try {
await backend.sendMessage({ type: "is_alive" }, 50);
@@ -81,11 +80,17 @@
}
await sleep(50);
}
+ if (scope) {
+ wordPressSiteUrl2 += `/scope:${scope}`;
+ }
await backend.sendMessage({
type: "initialize_wordpress",
siteURL: wordPressSiteUrl2
});
return {
+ urlFor(path) {
+ return `${wordPressSiteUrl2}${path}`;
+ },
async HTTPRequest(request) {
return await backend.sendMessage({
type: "request",
@@ -153,20 +158,21 @@
// src/web/app.mjs
async function init() {
console.log("[Main] Initializing the workers");
- const wasmWorker = await createWordPressWorker(
- {
- backend: getWorkerBackend(wasmWorkerBackend, wasmWorkerUrl),
- wordPressSiteUrl
- }
- );
- await registerServiceWorker(
- serviceWorkerUrl,
- async (request) => {
+ const tabScope = Math.random().toFixed(16);
+ const wasmWorker = await createWordPressWorker({
+ backend: getWorkerBackend(wasmWorkerBackend, wasmWorkerUrl),
+ wordPressSiteUrl,
+ scope: tabScope
+ });
+ await registerServiceWorker({
+ url: serviceWorkerUrl,
+ onRequest: async (request) => {
return await wasmWorker.HTTPRequest(request);
- }
- );
+ },
+ scope: tabScope
+ });
console.log("[Main] Workers are ready");
- document.querySelector("#wp").src = "/wp-login.php";
+ document.querySelector("#wp").src = wasmWorker.urlFor(`/wp-login.php`);
}
init();
})();
diff --git a/dist-web/service-worker.js b/dist-web/service-worker.js
index 9cc5bdb782..91ce8ed7de 100644
--- a/dist-web/service-worker.js
+++ b/dist-web/service-worker.js
@@ -31,56 +31,92 @@
}
// src/web/service-worker.js
- var broadcastChannel = new BroadcastChannel("wordpress-service-worker");
+ var broadcastChannel = new BroadcastChannel(`wordpress-service-worker`);
self.addEventListener("activate", (event) => {
event.waitUntil(clients.claim());
});
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
const isWpOrgRequest = url.hostname.includes("api.wordpress.org");
- const isPHPRequest = url.pathname.endsWith("/") && url.pathname !== "/" || url.pathname.endsWith(".php");
- if (isWpOrgRequest || !isPHPRequest) {
+ if (isWpOrgRequest) {
console.log(`[ServiceWorker] Ignoring request: ${url.pathname}`);
- return;
}
- event.preventDefault();
- return event.respondWith(
- new Promise(async (accept) => {
- console.log(`[ServiceWorker] Serving request: ${url.pathname}?${url.search}`);
- console.log({ isWpOrgRequest, isPHPRequest });
- const post = await parsePost(event.request);
- const requestHeaders = {};
- for (const pair of event.request.headers.entries()) {
- requestHeaders[pair[0]] = pair[1];
- }
- let wpResponse;
- try {
- const message = {
- type: "httpRequest",
- request: {
- path: url.pathname + url.search,
- method: event.request.method,
- _POST: post,
- headers: requestHeaders
- }
- };
- console.log("[ServiceWorker] Forwarding a request to the main app", { message });
- const messageId = postMessageExpectReply(broadcastChannel, message);
- wpResponse = await awaitReply(broadcastChannel, messageId);
- console.log("[ServiceWorker] Response received from the main app", { wpResponse });
- } catch (e) {
- console.error(e);
- throw e;
- }
- accept(new Response(
- wpResponse.body,
- {
- headers: wpResponse.headers
+ const isScopedRequest = url.pathname.startsWith(`/scope:`);
+ const scope = isScopedRequest ? url.pathname.split("/")[1].split(":")[1] : null;
+ const isPHPRequest = url.pathname.endsWith("/") && url.pathname !== "/" || url.pathname.endsWith(".php");
+ if (isPHPRequest) {
+ event.preventDefault();
+ return event.respondWith(
+ new Promise(async (accept) => {
+ console.log(`[ServiceWorker] Serving request: ${url.pathname}?${url.search}`);
+ console.log({ isWpOrgRequest, isPHPRequest });
+ const post = await parsePost(event.request);
+ const requestHeaders = {};
+ for (const pair of event.request.headers.entries()) {
+ requestHeaders[pair[0]] = pair[1];
}
- ));
- })
- );
+ let wpResponse;
+ try {
+ const message = {
+ type: "httpRequest",
+ scope,
+ request: {
+ path: url.pathname + url.search,
+ method: event.request.method,
+ _POST: post,
+ headers: requestHeaders
+ }
+ };
+ console.log("[ServiceWorker] Forwarding a request to the main app", { message });
+ const messageId = postMessageExpectReply(broadcastChannel, message);
+ wpResponse = await awaitReply(broadcastChannel, messageId);
+ console.log("[ServiceWorker] Response received from the main app", { wpResponse });
+ } catch (e) {
+ console.error(e);
+ throw e;
+ }
+ accept(new Response(
+ wpResponse.body,
+ {
+ headers: wpResponse.headers
+ }
+ ));
+ })
+ );
+ }
+ const isScopedStaticFileRequest = isScopedRequest;
+ if (isScopedStaticFileRequest) {
+ const scopedUrl = url + "";
+ url.pathname = "/" + url.pathname.split("/").slice(2).join("/");
+ console.log(`[ServiceWorker] Rerouting static request from ${scopedUrl} to ${serverUrl}`);
+ event.preventDefault();
+ return event.respondWith(
+ new Promise(async (accept) => {
+ const newRequest = await cloneRequest(event.request, {
+ url: serverUrl
+ });
+ accept(fetch(newRequest));
+ })
+ );
+ }
+ console.log(`[ServiceWorker] Ignoring a request to ${event.request.url}`);
});
+ async function cloneRequest(request, overrides) {
+ const body = ["GET", "HEAD"].includes(request.method) || "body" in overrides ? void 0 : await r.blob();
+ return new Request(overrides.url || request.url, {
+ body,
+ method: request.method,
+ headers: request.headers,
+ referrer: request.referrer,
+ referrerPolicy: request.referrerPolicy,
+ mode: request.mode,
+ credentials: request.credentials,
+ cache: request.cache,
+ redirect: request.redirect,
+ integrity: request.integrity,
+ ...overrides
+ });
+ }
async function parsePost(request) {
if (request.method !== "POST") {
return void 0;
diff --git a/dist-web/wasm-worker.js b/dist-web/wasm-worker.js
index bb5d944d69..2d145fa2c0 100644
--- a/dist-web/wasm-worker.js
+++ b/dist-web/wasm-worker.js
@@ -96,7 +96,8 @@
SCHEMA = "http";
HOSTNAME = "localhost";
PORT = 80;
- HOST = ``;
+ HOST = "";
+ PATHNAME = "";
ABSOLUTE_URL = ``;
constructor(php) {
this.php = php;
@@ -111,7 +112,8 @@
this.PORT = url.port ? url.port : url.protocol === "https:" ? 443 : 80;
this.SCHEMA = (url.protocol || "").replace(":", "");
this.HOST = `${this.HOSTNAME}:${this.PORT}`;
- this.ABSOLUTE_URL = `${this.SCHEMA}://${this.HOSTNAME}:${this.PORT}`;
+ this.PATHNAME = url.pathname.replace(/\/+$/, "");
+ this.ABSOLUTE_URL = `${this.SCHEMA}://${this.HOSTNAME}:${this.PORT}${this.PATHNAME}`;
await this.php.refresh();
const result = await this.php.run(` {
+ onRequest: async (request) => {
return await wasmWorker.HTTPRequest(request);
- }
- );
+ },
+ scope: tabScope
+ });
console.log("[Main] Workers are ready")
- document.querySelector('#wp').src = '/wp-login.php';
+ document.querySelector('#wp').src = wasmWorker.urlFor(`/wp-login.php`);
}
init();
diff --git a/src/web/library.js b/src/web/library.js
index 3d6d4a26f3..3da9d568d4 100644
--- a/src/web/library.js
+++ b/src/web/library.js
@@ -4,14 +4,24 @@ const sleep = ms => new Promise(resolve => setTimeout(resolve, 50));
//
// Register the service worker and handle any HTTP WordPress requests it provides us:
-export async function registerServiceWorker(url, onRequest) {
+export async function registerServiceWorker({ url, onRequest, scope }) {
if ( ! navigator.serviceWorker ) {
alert('Service workers are not supported in this browser.');
throw new Exception('Service workers are not supported in this browser.');
}
await navigator.serviceWorker.register(url);
- const serviceWorkerChannel = new BroadcastChannel('wordpress-service-worker');
+ const serviceWorkerChannel = new BroadcastChannel(`wordpress-service-worker`);
serviceWorkerChannel.addEventListener('message', async function onMessage(event) {
+ /**
+ * Ignore events meant for other WordPress instances to
+ * avoid handling the same event twice.
+ *
+ * This is important because BroadcastChannel transmits
+ * events to all the listeners across all browser tabs.
+ */
+ if (scope && event.data.scope !== scope) {
+ return;
+ }
console.debug(`[Main] "${event.data.type}" message received from a service worker`);
let result;
@@ -39,16 +49,16 @@ export async function registerServiceWorker(url, onRequest) {
await sleep(0);
const wordPressDomain = new URL(url).origin;
- const response = await fetch(`${wordPressDomain}/wp-admin/atomlib.php`);
- if (!response.ok) {
- // The service worker did not claim this page for some reason. Let's reload.
- window.location.reload();
- }
+ // const response = await fetch(`${wordPressDomain}/wp-admin/atomlib.php`);
+ // if (!response.ok) {
+ // // The service worker did not claim this page for some reason. Let's reload.
+ // window.location.reload();
+ // }
}
//
//
-export async function createWordPressWorker({ backend, wordPressSiteUrl }) {
+export async function createWordPressWorker({ backend, wordPressSiteUrl, scope }) {
// Keep asking if the worker is alive until we get a response
while (true) {
try {
@@ -60,6 +70,17 @@ export async function createWordPressWorker({ backend, wordPressSiteUrl }) {
await sleep(50);
}
+ /**
+ * Scoping a WordPress instances means hosting it on a
+ * path starting with `/scope:`. This helps WASM workers
+ * avoid rendering any requests meant for other WASM workers.
+ *
+ * @see registerServiceWorker for more details
+ */
+ if (scope) {
+ wordPressSiteUrl += `/scope:${scope}`;
+ }
+
// Now that the worker is up and running, let's ask it to initialize
// WordPress:
await backend.sendMessage({
@@ -68,6 +89,9 @@ export async function createWordPressWorker({ backend, wordPressSiteUrl }) {
});
return {
+ urlFor(path) {
+ return `${wordPressSiteUrl}${path}`;
+ },
async HTTPRequest(request) {
return await backend.sendMessage({
type: 'request',
diff --git a/src/web/service-worker.js b/src/web/service-worker.js
index c4d4043018..fde86a7795 100644
--- a/src/web/service-worker.js
+++ b/src/web/service-worker.js
@@ -1,6 +1,6 @@
import { postMessageExpectReply, awaitReply } from '../shared/messaging.mjs';
-const broadcastChannel = new BroadcastChannel( 'wordpress-service-worker' );
+const broadcastChannel = new BroadcastChannel( `wordpress-service-worker` );
/**
* Ensure the client gets claimed by this service worker right after the registration.
@@ -16,61 +16,129 @@ const broadcastChannel = new BroadcastChannel( 'wordpress-service-worker' );
self.addEventListener("activate", (event) => {
event.waitUntil(clients.claim());
});
-
+
+const urlMap = {}
+
/**
* The main method. It captures the requests and loop them back to the main
* application using the Loopback request
*/
-self.addEventListener( 'fetch', ( event ) => {
+self.addEventListener('fetch', (event) => {
// @TODO A more involved hostname check
- const url = new URL( event.request.url );
- const isWpOrgRequest = url.hostname.includes( 'api.wordpress.org' );
- const isPHPRequest = ( url.pathname.endsWith( '/' ) && url.pathname !== '/' ) || url.pathname.endsWith( '.php' );
- if ( isWpOrgRequest || ! isPHPRequest ) {
- console.log( `[ServiceWorker] Ignoring request: ${ url.pathname }` );
- return;
+ const url = new URL(event.request.url);
+ const isWpOrgRequest = url.hostname.includes('api.wordpress.org');
+ if (isWpOrgRequest) {
+ console.log(`[ServiceWorker] Ignoring request: ${url.pathname}`);
}
- event.preventDefault();
- return event.respondWith(
- new Promise( async ( accept ) => {
- console.log( `[ServiceWorker] Serving request: ${ url.pathname }?${ url.search }` );
- console.log( { isWpOrgRequest, isPHPRequest } );
- const post = await parsePost( event.request );
- const requestHeaders = {};
- for ( const pair of event.request.headers.entries() ) {
- requestHeaders[ pair[ 0 ] ] = pair[ 1 ];
- }
-
- let wpResponse;
- try {
- const message = {
- type: 'httpRequest',
- request: {
- path: url.pathname + url.search,
- method: event.request.method,
- _POST: post,
- headers: requestHeaders,
+ /**
+ * Detect scoped requests – their url starts with `/scope:`
+ *
+ * We need this mechanics because BroadcastChannel transmits
+ * events to all the listeners across all browser tabs. Scopes
+ * helps WASM workers ignore requests meant for other WASM workers.
+ */
+ const isScopedRequest = url.pathname.startsWith(`/scope:`);
+ const scope = isScopedRequest ? url.pathname.split('/')[1].split(':')[1] : null;
+
+ const isPHPRequest = (url.pathname.endsWith('/') && url.pathname !== '/') || url.pathname.endsWith('.php');
+ if (isPHPRequest) {
+ event.preventDefault();
+ return event.respondWith(
+ new Promise(async (accept) => {
+ console.log(`[ServiceWorker] Serving request: ${url.pathname}?${url.search}`);
+ console.log({ isWpOrgRequest, isPHPRequest });
+ const post = await parsePost(event.request);
+ const requestHeaders = {};
+ for (const pair of event.request.headers.entries()) {
+ requestHeaders[pair[0]] = pair[1];
+ }
+
+ let wpResponse;
+ try {
+ const message = {
+ type: 'httpRequest',
+ scope,
+ request: {
+ path: url.pathname + url.search,
+ method: event.request.method,
+ _POST: post,
+ headers: requestHeaders,
+ },
+ };
+ console.log('[ServiceWorker] Forwarding a request to the main app', { message });
+ const messageId = postMessageExpectReply(broadcastChannel, message);
+ wpResponse = await awaitReply(broadcastChannel, messageId);
+ console.log('[ServiceWorker] Response received from the main app', { wpResponse });
+ } catch (e) {
+ console.error(e);
+ throw e;
+ }
+
+ accept(new Response(
+ wpResponse.body,
+ {
+ headers: wpResponse.headers,
},
- };
- console.log( '[ServiceWorker] Forwarding a request to the main app', { message } );
- const messageId = postMessageExpectReply( broadcastChannel, message );
- wpResponse = await awaitReply( broadcastChannel, messageId );
- console.log( '[ServiceWorker] Response received from the main app', { wpResponse } );
- } catch ( e ) {
- console.error( e );
- throw e;
- }
-
- accept( new Response(
- wpResponse.body,
- {
- headers: wpResponse.headers,
- },
- ) );
- } ),
- );
-} );
+ ));
+ }),
+ );
+ }
+
+ const isScopedStaticFileRequest = isScopedRequest;
+ if (isScopedStaticFileRequest) {
+ const scopedUrl = url + '';
+ url.pathname = '/' + url.pathname.split('/').slice(2).join('/');
+ console.log(`[ServiceWorker] Rerouting static request from ${scopedUrl} to ${serverUrl}`);
+
+ event.preventDefault();
+ return event.respondWith(
+ new Promise(async (accept) => {
+ const newRequest = await cloneRequest(event.request, {
+ url: serverUrl
+ });
+ accept(fetch(newRequest));
+ })
+ );
+ }
+
+ console.log(`[ServiceWorker] Ignoring a request to ${event.request.url}`);
+});
+
+/**
+ * Copy a request with custom overrides.
+ *
+ * This function is only needed because Request properties
+ * are read-only. The only way to change e.g. a URL is to
+ * create an entirely new request:
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/API/Request
+ *
+ * @param {Request} request
+ * @param {Object} overrides
+ * @returns Request
+ */
+async function cloneRequest(request, overrides) {
+ const body =
+ ['GET', 'HEAD'].includes(request.method)
+ || 'body' in overrides
+ ? undefined
+ : await r.blob()
+ ;
+ return new Request(overrides.url || request.url, {
+ body,
+ method: request.method,
+ headers: request.headers,
+ referrer: request.referrer,
+ referrerPolicy: request.referrerPolicy,
+ mode: request.mode,
+ credentials: request.credentials,
+ cache: request.cache,
+ redirect: request.redirect,
+ integrity: request.integrity,
+ ...overrides
+ });
+}
async function parsePost( request ) {
if ( request.method !== 'POST' ) {