Merge pull request #76465 from microsoft/joh/sw

Fetch vscode-remote resources with web worker
This commit is contained in:
Johannes Rieken 2019-07-02 15:50:02 +02:00 committed by GitHub
commit 46b449b5e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 199 additions and 46 deletions

View File

@ -29,7 +29,6 @@ import { URI } from 'vs/base/common/uri';
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
import { ConfigurationCache } from 'vs/workbench/services/configuration/browser/configurationCache';
import { WebResources } from 'vs/workbench/browser/web.resources';
import { ISignService } from 'vs/platform/sign/common/sign';
import { SignService } from 'vs/platform/sign/browser/signService';
import { hash } from 'vs/base/common/hash';
@ -68,9 +67,6 @@ class CodeRendererMain extends Disposable {
// Layout
this._register(addDisposableListener(window, EventType.RESIZE, () => this.workbench.layout()));
// Resource Loading
this._register(new WebResources(<IFileService>services.serviceCollection.get(IFileService)));
// Workbench Lifecycle
this._register(this.workbench.onShutdown(() => this.dispose()));
@ -199,4 +195,4 @@ export function main(domElement: HTMLElement, options: IWorkbenchConstructionOpt
const renderer = new CodeRendererMain(domElement, options);
return renderer.open();
}
}

View File

@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { getMediaMime } from 'vs/base/common/mime';
const cacheName = 'vscode-resources';
declare const clients: { get(s: string): Promise<any> };
const _pending = new Map<string, Function>();
export function handleMessageEvent(event: MessageEvent): void {
const fn = _pending.get(event.data.token);
if (fn) {
fn(event.data.data);
_pending.delete(event.data.token);
}
}
export async function handleFetchEvent(event: any): Promise<Response | undefined> {
const url = URI.parse(event.request.url);
if (url.path !== '/vscode-resources/fetch') {
return undefined;
}
if (!event.clientId) {
return undefined;
}
const cachedValue = await caches.open(cacheName).then(cache => cache.match(event.request));
if (cachedValue) {
return cachedValue;
}
// console.log('fetch', url.query);
try {
const token = generateUuid();
return new Promise<Response>(async resolve => {
const handle = setTimeout(() => {
resolve(new Response(undefined, { status: 500, statusText: 'timeout' }));
_pending.delete(token);
}, 5000);
_pending.set(token, (data: ArrayBuffer) => {
clearTimeout(handle);
const res = new Response(data, {
status: 200,
headers: { 'Content-Type': getMediaMime(URI.parse(url.query).path) || 'text/plain' }
});
caches.open(cacheName).then(cache => {
cache.put(event.request, res.clone());
resolve(res);
});
});
const client = await clients.get(event.clientId);
client.postMessage({ uri: url.query, token });
});
} catch (err) {
console.error(err);
return new Response(err, { status: 500 });
}
}

View File

@ -4,19 +4,21 @@
*--------------------------------------------------------------------------------------------*/
import { IFileService } from 'vs/platform/files/common/files';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { getMediaMime } from 'vs/base/common/mime';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
export class WebResources {
// todo@joh explore alternative, explicit approach
class ResourcesMutationObserver {
private readonly _regexp = /url\(('|")?(vscode-remote:\/\/.*?)\1\)/g;
private readonly _urlCache = new Map<string, string>();
private readonly _requestCache = new Map<string, Promise<any>>();
private readonly _observer: MutationObserver;
constructor(@IFileService private readonly _fileService: IFileService) {
// todo@joh add observer to more than head-element
// todo@joh explore alternative approach
private readonly _regexp = /url\(('|")?(vscode-remote:\/\/(.*?))\1\)/ig;
constructor() {
this._observer = new MutationObserver(r => this._handleMutation(r));
this._observer.observe(document, {
subtree: true,
@ -24,6 +26,12 @@ export class WebResources {
attributes: true,
attributeFilter: ['style']
});
this.scan();
}
scan(): void {
document.querySelectorAll('style').forEach(value => this._handleStyleNode(value));
// todo@joh more!
}
dispose(): void {
@ -78,39 +86,60 @@ export class WebResources {
}
private async _rewriteUrls(textContent: string): Promise<string> {
const positions: number[] = [];
const promises: Promise<any>[] = [];
let match: RegExpMatchArray | null = null;
while (match = this._regexp.exec(textContent)) {
const remoteUrl = match[2];
positions.push(match.index! + 'url('.length + (typeof match[1] === 'string' ? match[1].length : 0));
positions.push(remoteUrl.length);
if (!this._urlCache.has(remoteUrl)) {
let request = this._requestCache.get(remoteUrl);
if (!request) {
const uri = URI.parse(remoteUrl, true);
request = this._fileService.readFile(uri).then(file => {
const blobUrl = URL.createObjectURL(new Blob([file.value.buffer], { type: getMediaMime(uri.path) }));
this._urlCache.set(remoteUrl, blobUrl);
});
this._requestCache.set(remoteUrl, request);
}
promises.push(request);
}
}
let content = textContent;
await Promise.all(promises);
for (let i = positions.length - 1; i >= 0; i -= 2) {
const start = positions[i - 1];
const len = positions[i];
const url = this._urlCache.get(content.substr(start, len));
content = content.substring(0, start) + url + content.substring(start + len);
}
return content;
return textContent.replace(this._regexp, function (_m, quote, url) {
return `url(${quote}${location.href}vscode-resources/fetch?${encodeURIComponent(url)}${quote})`;
});
}
}
class ResourceServiceWorker {
private readonly _disposables = new DisposableStore();
constructor(
@IFileService private readonly _fileService: IFileService,
) {
this._initServiceWorker();
this._initFetchHandler();
}
dispose(): void {
this._disposables.dispose();
}
private _initServiceWorker(): void {
const url = './resourceServiceWorkerMain.js';
navigator.serviceWorker.register(url).then(() => {
// console.log('registered');
return navigator.serviceWorker.ready;
}).then(() => {
// console.log('ready');
this._disposables.add(new ResourcesMutationObserver());
}).catch(err => {
console.error(err);
});
}
private _initFetchHandler(): void {
const fetchListener: (this: ServiceWorkerContainer, ev: MessageEvent) => void = event => {
const uri = URI.parse(event.data.uri);
this._fileService.readFile(uri).then(file => {
// todo@joh typings
(<any>event.source).postMessage({
token: event.data.token,
data: file.value.buffer.buffer
}, [file.value.buffer.buffer]);
});
};
navigator.serviceWorker.addEventListener('message', fetchListener);
this._disposables.add(toDisposable(() => navigator.serviceWorker.removeEventListener('message', fetchListener)));
}
}
Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench).registerWorkbenchContribution(
ResourceServiceWorker,
LifecyclePhase.Starting
);

View File

@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
(function () {
type Handler = {
handleFetchEvent(event: Event): Promise<Response | undefined>;
handleMessageEvent(event: MessageEvent): void;
};
const handlerPromise = new Promise<Handler>((resolve, reject) => {
// load loader
const baseUrl = './out/';
importScripts(baseUrl + 'vs/loader.js');
require.config({
baseUrl,
catchError: true
});
require(['vs/workbench/contrib/resources/browser/resourceServiceWorker'], resolve, reject);
});
self.addEventListener('message', event => {
handlerPromise.then(handler => {
handler.handleMessageEvent(event);
});
});
self.addEventListener('fetch', (event: any) => {
event.respondWith(handlerPromise.then(handler => {
return handler.handleFetchEvent(event).then(value => {
if (value instanceof Response) {
return value;
} else {
return fetch(event.request);
}
});
}));
});
self.addEventListener('install', event => {
//@ts-ignore
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', event => {
//@ts-ignore
event.waitUntil(self.clients.claim()); // Become available to all pages
});
})();

View File

@ -28,6 +28,11 @@ import 'vs/workbench/browser/parts/quickinput/quickInputActions';
//#endregion
//#region --- Remote Resource loading
import 'vs/workbench/contrib/resources/browser/resourceServiceWorkerClient';
//#endregion
//#region --- API Extension Points