Skip to content

Commit 66b6adf

Browse files
authored
Switch webview service-worker to use message channel (#138811)
* Switch webview service-worker to use message channel This change hooks the service worker used for loading webview resources directly up to the main VS Code process over a message channel. Previously this communication had to go through an extra hop through the webview This simplifies the logic somewhat (although this change required adding extra logic to exchange the message port). It also improves performance a little * Update webview content commit version
1 parent 86ee106 commit 66b6adf

File tree

6 files changed

+164
-207
lines changed

6 files changed

+164
-207
lines changed

product.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"licenseFileName": "LICENSE.txt",
2626
"reportIssueUrl": "https://github.com/microsoft/vscode/issues/new",
2727
"urlProtocol": "code-oss",
28-
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/insider/9acd320edad7cea2c062d339fa04822c5eeb9e1d/out/vs/workbench/contrib/webview/browser/pre/",
28+
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/insider/69df0500a8963fc469161c038a14a39384d5a303/out/vs/workbench/contrib/webview/browser/pre/",
2929
"extensionAllowedProposedApi": [
3030
"ms-vscode.vscode-js-profile-flame",
3131
"ms-vscode.vscode-js-profile-table",

src/vs/workbench/contrib/webview/browser/pre/main.js

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -204,40 +204,28 @@ const workerReady = new Promise((resolve, reject) => {
204204
return reject(new Error('Service Workers are not enabled. Webviews will not work. Try disabling private/incognito mode.'));
205205
}
206206

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

209209
navigator.serviceWorker.register(swPath).then(
210210
async registration => {
211211
await navigator.serviceWorker.ready;
212-
213212
/**
214213
* @param {MessageEvent} event
215214
*/
216215
const versionHandler = async (event) => {
217-
if (event.data.channel !== 'version') {
216+
if (event.data.channel !== 'init') {
218217
return;
219218
}
220-
221219
navigator.serviceWorker.removeEventListener('message', versionHandler);
222-
if (event.data.version === expectedWorkerVersion) {
223-
return resolve();
224-
} else {
225-
console.log(`Found unexpected service worker version. Found: ${event.data.version}. Expected: ${expectedWorkerVersion}`);
226-
console.log(`Attempting to reload service worker`);
227-
228-
// If we have the wrong version, try once (and only once) to unregister and re-register
229-
// Note that `.update` doesn't seem to work desktop electron at the moment so we use
230-
// `unregister` and `register` here.
231-
return registration.unregister()
232-
.then(() => navigator.serviceWorker.register(swPath))
233-
.then(() => navigator.serviceWorker.ready)
234-
.finally(() => { resolve(); });
235-
}
220+
221+
// Forward the port back to VS Code
222+
hostMessaging.onMessage('did-init-service-worker', () => resolve());
223+
hostMessaging.postMessage('init-service-worker', {}, event.ports);
236224
};
237225
navigator.serviceWorker.addEventListener('message', versionHandler);
238226

239227
const postVersionMessage = () => {
240-
assertIsDefined(navigator.serviceWorker.controller).postMessage({ channel: 'version' });
228+
assertIsDefined(navigator.serviceWorker.controller).postMessage({ channel: 'init' });
241229
};
242230

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

391-
hostMessaging.onMessage('did-load-resource', (_event, data) => {
392-
navigator.serviceWorker.ready.then(registration => {
393-
assertIsDefined(registration.active).postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []);
394-
});
395-
});
396-
397-
hostMessaging.onMessage('did-load-localhost', (_event, data) => {
398-
navigator.serviceWorker.ready.then(registration => {
399-
assertIsDefined(registration.active).postMessage({ channel: 'did-load-localhost', data });
400-
});
401-
});
402379

403-
navigator.serviceWorker.addEventListener('message', event => {
404-
switch (event.data.channel) {
405-
case 'load-resource':
406-
case 'load-localhost':
407-
hostMessaging.postMessage(event.data.channel, event.data);
408-
return;
409-
}
410-
});
411380
/**
412381
* @param {HTMLDocument?} document
413382
* @param {HTMLElement?} body

src/vs/workbench/contrib/webview/browser/pre/service-worker.js

Lines changed: 53 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@
99

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

12-
const VERSION = 2;
12+
const VERSION = 3;
1313

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

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

18-
1918
const searchParams = new URL(location.toString()).searchParams;
2019

2120
/**
@@ -98,11 +97,16 @@ class RequestStore {
9897
}
9998
}
10099

100+
/**
101+
* @typedef {{ readonly status: 200; id: number; path: string; mime: string; data: Uint8Array; etag: string | undefined; mtime: number | undefined; }
102+
* | { readonly status: 304; id: number; path: string; mime: string; mtime: number | undefined }
103+
* | { readonly status: 401; id: number; path: string }
104+
* | { readonly status: 404; id: number; path: string }} ResourceResponse
105+
*/
106+
101107
/**
102108
* Map of requested paths to responses.
103-
* @typedef {{ type: 'response', body: Uint8Array, mime: string, etag: string | undefined, mtime: number | undefined } |
104-
* { type: 'not-modified', mime: string, mtime: number | undefined } |
105-
* undefined} ResourceResponse
109+
*
106110
* @type {RequestStore<ResourceResponse>}
107111
*/
108112
const resourceRequestStore = new RequestStore();
@@ -120,48 +124,41 @@ const notFound = () =>
120124
const methodNotAllowed = () =>
121125
new Response('Method Not Allowed', { status: 405, });
122126

123-
sw.addEventListener('message', async (event) => {
127+
const vscodeMessageChannel = new MessageChannel();
128+
129+
sw.addEventListener('message', event => {
124130
switch (event.data.channel) {
125-
case 'version':
131+
case 'init':
126132
{
127133
const source = /** @type {Client} */ (event.source);
128134
sw.clients.get(source.id).then(client => {
129-
if (client) {
130-
client.postMessage({
131-
channel: 'version',
132-
version: VERSION
133-
});
134-
}
135+
client?.postMessage({
136+
channel: 'init',
137+
version: VERSION
138+
}, [vscodeMessageChannel.port2]);
135139
});
136140
return;
137141
}
142+
}
143+
144+
console.log('Unknown message');
145+
});
146+
147+
vscodeMessageChannel.port1.onmessage = (event) => {
148+
switch (event.data.channel) {
138149
case 'did-load-resource':
139150
{
140-
/** @type {ResourceResponse} */
141-
let response = undefined;
142-
143-
const data = event.data.data;
144-
switch (data.status) {
145-
case 200:
146-
{
147-
response = { type: 'response', body: data.data, mime: data.mime, etag: data.etag, mtime: data.mtime };
148-
break;
149-
}
150-
case 304:
151-
{
152-
response = { type: 'not-modified', mime: data.mime, mtime: data.mtime };
153-
break;
154-
}
155-
}
156151

157-
if (!resourceRequestStore.resolve(data.id, response)) {
158-
console.log('Could not resolve unknown resource', data.path);
152+
/** @type {ResourceResponse} */
153+
const response = event.data;
154+
if (!resourceRequestStore.resolve(response.id, response)) {
155+
console.log('Could not resolve unknown resource', response.path);
159156
}
160157
return;
161158
}
162159
case 'did-load-localhost':
163160
{
164-
const data = event.data.data;
161+
const data = event.data;
165162
if (!localhostRequestStore.resolve(data.id, data.location)) {
166163
console.log('Could not resolve unknown localhost', data.origin);
167164
}
@@ -170,7 +167,7 @@ sw.addEventListener('message', async (event) => {
170167
}
171168

172169
console.log('Unknown message');
173-
});
170+
};
174171

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

194191
sw.addEventListener('install', (event) => {
195-
event.waitUntil(sw.skipWaiting()); // Activate worker immediately
192+
event.waitUntil(sw.skipWaiting());
196193
});
197194

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

213-
const webviewId = getWebviewIdForClient(client);
214-
if (!webviewId) {
215-
console.error('Could not resolve webview id');
216-
return notFound();
217-
}
218-
219210
const shouldTryCaching = (event.request.method === 'GET');
220211

221212
/**
222213
* @param {ResourceResponse} entry
223214
* @param {Response | undefined} cachedResponse
224215
*/
225216
async function resolveResourceEntry(entry, cachedResponse) {
226-
if (!entry) {
227-
return notFound();
228-
}
229-
230-
if (entry.type === 'not-modified') {
217+
if (entry.status === 304) { // Not modified
231218
if (cachedResponse) {
232219
return cachedResponse.clone();
233220
} else {
234221
throw new Error('No cache found');
235222
}
236223
}
237224

225+
if (entry.status !== 200) {
226+
return notFound();
227+
}
228+
238229
/** @type {Record<string, string>} */
239230
const headers = {
240231
'Content-Type': entry.mime,
241-
'Content-Length': entry.body.byteLength.toString(),
232+
'Content-Length': entry.data.byteLength.toString(),
242233
'Access-Control-Allow-Origin': '*',
243234
};
244235
if (entry.etag) {
@@ -248,7 +239,7 @@ async function processResourceRequest(event, requestUrl) {
248239
if (entry.mtime) {
249240
headers['Last-Modified'] = new Date(entry.mtime).toUTCString();
250241
}
251-
const response = new Response(entry.body, {
242+
const response = new Response(entry.data, {
252243
status: 200,
253244
headers
254245
});
@@ -261,12 +252,6 @@ async function processResourceRequest(event, requestUrl) {
261252
return response.clone();
262253
}
263254

264-
const parentClients = await getOuterIframeClient(webviewId);
265-
if (!parentClients.length) {
266-
console.log('Could not find parent client for request');
267-
return notFound();
268-
}
269-
270255
/** @type {Response | undefined} */
271256
let cached;
272257
if (shouldTryCaching) {
@@ -280,17 +265,15 @@ async function processResourceRequest(event, requestUrl) {
280265
const scheme = firstHostSegment.split('+', 1)[0];
281266
const authority = firstHostSegment.slice(scheme.length + 1); // may be empty
282267

283-
for (const parentClient of parentClients) {
284-
parentClient.postMessage({
285-
channel: 'load-resource',
286-
id: requestId,
287-
path: requestUrl.pathname,
288-
scheme,
289-
authority,
290-
query: requestUrl.search.replace(/^\?/, ''),
291-
ifNoneMatch: cached?.headers.get('ETag'),
292-
});
293-
}
268+
vscodeMessageChannel.port1.postMessage({
269+
channel: 'load-resource',
270+
id: requestId,
271+
path: requestUrl.pathname,
272+
scheme,
273+
authority,
274+
query: requestUrl.search.replace(/^\?/, ''),
275+
ifNoneMatch: cached?.headers.get('ETag'),
276+
});
294277

295278
return promise.then(entry => resolveResourceEntry(entry, cached));
296279
}
@@ -307,11 +290,6 @@ async function processLocalhostRequest(event, requestUrl) {
307290
// that are not spawned by vs code
308291
return fetch(event.request);
309292
}
310-
const webviewId = getWebviewIdForClient(client);
311-
if (!webviewId) {
312-
console.error('Could not resolve webview id');
313-
return fetch(event.request);
314-
}
315293

316294
const origin = requestUrl.origin;
317295

@@ -332,42 +310,13 @@ async function processLocalhostRequest(event, requestUrl) {
332310
});
333311
};
334312

335-
const parentClients = await getOuterIframeClient(webviewId);
336-
if (!parentClients.length) {
337-
console.log('Could not find parent client for request');
338-
return notFound();
339-
}
340-
341313
const { requestId, promise } = localhostRequestStore.create();
342-
for (const parentClient of parentClients) {
343-
parentClient.postMessage({
344-
channel: 'load-localhost',
345-
origin: origin,
346-
id: requestId,
347-
});
348-
}
349-
350-
return promise.then(resolveRedirect);
351-
}
352-
353-
/**
354-
* @param {Client} client
355-
* @returns {string | null}
356-
*/
357-
function getWebviewIdForClient(client) {
358-
const requesterClientUrl = new URL(client.url);
359-
return requesterClientUrl.searchParams.get('id');
360-
}
361314

362-
/**
363-
* @param {string} webviewId
364-
* @returns {Promise<Client[]>}
365-
*/
366-
async function getOuterIframeClient(webviewId) {
367-
const allClients = await sw.clients.matchAll({ includeUncontrolled: true });
368-
return allClients.filter(client => {
369-
const clientUrl = new URL(client.url);
370-
const hasExpectedPathName = (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html`);
371-
return hasExpectedPathName && clientUrl.searchParams.get('id') === webviewId;
315+
vscodeMessageChannel.port1.postMessage({
316+
channel: 'load-localhost',
317+
origin: origin,
318+
id: requestId,
372319
});
320+
321+
return promise.then(resolveRedirect);
373322
}

0 commit comments

Comments
 (0)