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

Switch webview service-worker to use message channel #138811

Merged
merged 2 commits into from Dec 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion product.json
Expand Up @@ -25,7 +25,7 @@
"licenseFileName": "LICENSE.txt",
"reportIssueUrl": "https://github.com/microsoft/vscode/issues/new",
"urlProtocol": "code-oss",
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/insider/9acd320edad7cea2c062d339fa04822c5eeb9e1d/out/vs/workbench/contrib/webview/browser/pre/",
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/insider/69df0500a8963fc469161c038a14a39384d5a303/out/vs/workbench/contrib/webview/browser/pre/",
"extensionAllowedProposedApi": [
"ms-vscode.vscode-js-profile-flame",
"ms-vscode.vscode-js-profile-table",
Expand Down
45 changes: 7 additions & 38 deletions src/vs/workbench/contrib/webview/browser/pre/main.js
Expand Up @@ -204,40 +204,28 @@ const workerReady = new Promise((resolve, reject) => {
return reject(new Error('Service Workers are not enabled. Webviews will not work. Try disabling private/incognito mode.'));
}

const swPath = `service-worker.js?vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}`;
const swPath = `service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}`;

navigator.serviceWorker.register(swPath).then(
async registration => {
await navigator.serviceWorker.ready;

/**
* @param {MessageEvent} event
*/
const versionHandler = async (event) => {
if (event.data.channel !== 'version') {
if (event.data.channel !== 'init') {
return;
}

navigator.serviceWorker.removeEventListener('message', versionHandler);
if (event.data.version === expectedWorkerVersion) {
return resolve();
} else {
console.log(`Found unexpected service worker version. Found: ${event.data.version}. Expected: ${expectedWorkerVersion}`);
console.log(`Attempting to reload service worker`);

// If we have the wrong version, try once (and only once) to unregister and re-register
// Note that `.update` doesn't seem to work desktop electron at the moment so we use
// `unregister` and `register` here.
return registration.unregister()
.then(() => navigator.serviceWorker.register(swPath))
.then(() => navigator.serviceWorker.ready)
.finally(() => { resolve(); });
}

// Forward the port back to VS Code
hostMessaging.onMessage('did-init-service-worker', () => resolve());
hostMessaging.postMessage('init-service-worker', {}, event.ports);
};
navigator.serviceWorker.addEventListener('message', versionHandler);

const postVersionMessage = () => {
assertIsDefined(navigator.serviceWorker.controller).postMessage({ channel: 'version' });
assertIsDefined(navigator.serviceWorker.controller).postMessage({ channel: 'init' });
};

// At this point, either the service worker is ready and
Expand Down Expand Up @@ -388,26 +376,7 @@ const initData = {
themeName: undefined,
};

hostMessaging.onMessage('did-load-resource', (_event, data) => {
navigator.serviceWorker.ready.then(registration => {
assertIsDefined(registration.active).postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []);
});
});

hostMessaging.onMessage('did-load-localhost', (_event, data) => {
navigator.serviceWorker.ready.then(registration => {
assertIsDefined(registration.active).postMessage({ channel: 'did-load-localhost', data });
});
});

navigator.serviceWorker.addEventListener('message', event => {
switch (event.data.channel) {
case 'load-resource':
case 'load-localhost':
hostMessaging.postMessage(event.data.channel, event.data);
return;
}
});
/**
* @param {HTMLDocument?} document
* @param {HTMLElement?} body
Expand Down
157 changes: 53 additions & 104 deletions src/vs/workbench/contrib/webview/browser/pre/service-worker.js
Expand Up @@ -9,13 +9,12 @@

const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {any} */ (self));

const VERSION = 2;
const VERSION = 3;

const resourceCacheName = `vscode-resource-cache-${VERSION}`;

const rootPath = sw.location.pathname.replace(/\/service-worker.js$/, '');


const searchParams = new URL(location.toString()).searchParams;

/**
Expand Down Expand Up @@ -98,11 +97,16 @@ class RequestStore {
}
}

/**
* @typedef {{ readonly status: 200; id: number; path: string; mime: string; data: Uint8Array; etag: string | undefined; mtime: number | undefined; }
* | { readonly status: 304; id: number; path: string; mime: string; mtime: number | undefined }
* | { readonly status: 401; id: number; path: string }
* | { readonly status: 404; id: number; path: string }} ResourceResponse
*/

/**
* Map of requested paths to responses.
* @typedef {{ type: 'response', body: Uint8Array, mime: string, etag: string | undefined, mtime: number | undefined } |
* { type: 'not-modified', mime: string, mtime: number | undefined } |
* undefined} ResourceResponse
*
* @type {RequestStore<ResourceResponse>}
*/
const resourceRequestStore = new RequestStore();
Expand All @@ -120,48 +124,41 @@ const notFound = () =>
const methodNotAllowed = () =>
new Response('Method Not Allowed', { status: 405, });

sw.addEventListener('message', async (event) => {
const vscodeMessageChannel = new MessageChannel();

sw.addEventListener('message', event => {
switch (event.data.channel) {
case 'version':
case 'init':
{
const source = /** @type {Client} */ (event.source);
sw.clients.get(source.id).then(client => {
if (client) {
client.postMessage({
channel: 'version',
version: VERSION
});
}
client?.postMessage({
channel: 'init',
version: VERSION
}, [vscodeMessageChannel.port2]);
});
return;
}
}

console.log('Unknown message');
});

vscodeMessageChannel.port1.onmessage = (event) => {
switch (event.data.channel) {
case 'did-load-resource':
{
/** @type {ResourceResponse} */
let response = undefined;

const data = event.data.data;
switch (data.status) {
case 200:
{
response = { type: 'response', body: data.data, mime: data.mime, etag: data.etag, mtime: data.mtime };
break;
}
case 304:
{
response = { type: 'not-modified', mime: data.mime, mtime: data.mtime };
break;
}
}

if (!resourceRequestStore.resolve(data.id, response)) {
console.log('Could not resolve unknown resource', data.path);
/** @type {ResourceResponse} */
const response = event.data;
if (!resourceRequestStore.resolve(response.id, response)) {
console.log('Could not resolve unknown resource', response.path);
}
return;
}
case 'did-load-localhost':
{
const data = event.data.data;
const data = event.data;
if (!localhostRequestStore.resolve(data.id, data.location)) {
console.log('Could not resolve unknown localhost', data.origin);
}
Expand All @@ -170,7 +167,7 @@ sw.addEventListener('message', async (event) => {
}

console.log('Unknown message');
});
};

sw.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
Expand All @@ -192,7 +189,7 @@ sw.addEventListener('fetch', (event) => {
});

sw.addEventListener('install', (event) => {
event.waitUntil(sw.skipWaiting()); // Activate worker immediately
event.waitUntil(sw.skipWaiting());
});

sw.addEventListener('activate', (event) => {
Expand All @@ -210,35 +207,29 @@ async function processResourceRequest(event, requestUrl) {
return notFound();
}

const webviewId = getWebviewIdForClient(client);
if (!webviewId) {
console.error('Could not resolve webview id');
return notFound();
}

const shouldTryCaching = (event.request.method === 'GET');

/**
* @param {ResourceResponse} entry
* @param {Response | undefined} cachedResponse
*/
async function resolveResourceEntry(entry, cachedResponse) {
if (!entry) {
return notFound();
}

if (entry.type === 'not-modified') {
if (entry.status === 304) { // Not modified
if (cachedResponse) {
return cachedResponse.clone();
} else {
throw new Error('No cache found');
}
}

if (entry.status !== 200) {
return notFound();
}

/** @type {Record<string, string>} */
const headers = {
'Content-Type': entry.mime,
'Content-Length': entry.body.byteLength.toString(),
'Content-Length': entry.data.byteLength.toString(),
'Access-Control-Allow-Origin': '*',
};
if (entry.etag) {
Expand All @@ -248,7 +239,7 @@ async function processResourceRequest(event, requestUrl) {
if (entry.mtime) {
headers['Last-Modified'] = new Date(entry.mtime).toUTCString();
}
const response = new Response(entry.body, {
const response = new Response(entry.data, {
status: 200,
headers
});
Expand All @@ -261,12 +252,6 @@ async function processResourceRequest(event, requestUrl) {
return response.clone();
}

const parentClients = await getOuterIframeClient(webviewId);
if (!parentClients.length) {
console.log('Could not find parent client for request');
return notFound();
}

/** @type {Response | undefined} */
let cached;
if (shouldTryCaching) {
Expand All @@ -280,17 +265,15 @@ async function processResourceRequest(event, requestUrl) {
const scheme = firstHostSegment.split('+', 1)[0];
const authority = firstHostSegment.slice(scheme.length + 1); // may be empty

for (const parentClient of parentClients) {
parentClient.postMessage({
channel: 'load-resource',
id: requestId,
path: requestUrl.pathname,
scheme,
authority,
query: requestUrl.search.replace(/^\?/, ''),
ifNoneMatch: cached?.headers.get('ETag'),
});
}
vscodeMessageChannel.port1.postMessage({
channel: 'load-resource',
id: requestId,
path: requestUrl.pathname,
scheme,
authority,
query: requestUrl.search.replace(/^\?/, ''),
ifNoneMatch: cached?.headers.get('ETag'),
});

return promise.then(entry => resolveResourceEntry(entry, cached));
}
Expand All @@ -307,11 +290,6 @@ async function processLocalhostRequest(event, requestUrl) {
// that are not spawned by vs code
return fetch(event.request);
}
const webviewId = getWebviewIdForClient(client);
if (!webviewId) {
console.error('Could not resolve webview id');
return fetch(event.request);
}

const origin = requestUrl.origin;

Expand All @@ -332,42 +310,13 @@ async function processLocalhostRequest(event, requestUrl) {
});
};

const parentClients = await getOuterIframeClient(webviewId);
if (!parentClients.length) {
console.log('Could not find parent client for request');
return notFound();
}

const { requestId, promise } = localhostRequestStore.create();
for (const parentClient of parentClients) {
parentClient.postMessage({
channel: 'load-localhost',
origin: origin,
id: requestId,
});
}

return promise.then(resolveRedirect);
}

/**
* @param {Client} client
* @returns {string | null}
*/
function getWebviewIdForClient(client) {
const requesterClientUrl = new URL(client.url);
return requesterClientUrl.searchParams.get('id');
}

/**
* @param {string} webviewId
* @returns {Promise<Client[]>}
*/
async function getOuterIframeClient(webviewId) {
const allClients = await sw.clients.matchAll({ includeUncontrolled: true });
return allClients.filter(client => {
const clientUrl = new URL(client.url);
const hasExpectedPathName = (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html`);
return hasExpectedPathName && clientUrl.searchParams.get('id') === webviewId;
vscodeMessageChannel.port1.postMessage({
channel: 'load-localhost',
origin: origin,
id: requestId,
});

return promise.then(resolveRedirect);
}