From 7cbf591b90c0c2669d11ea12738e8b8618e60e7a Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 1 Jul 2019 18:39:57 +0200 Subject: [PATCH 1/2] wip --- resourceServiceWorkerMain.js | 40 ++++++++++++++++ src/vs/workbench/browser/web.resources.ts | 8 ++++ .../browser/resourceServiceWorker.ts | 11 +++++ .../browser/resourceServiceWorkerClient.ts | 29 +++++++++++ .../browser/resourceServiceWorkerMain.ts | 48 +++++++++++++++++++ src/vs/workbench/workbench.web.main.ts | 5 ++ 6 files changed, 141 insertions(+) create mode 100644 resourceServiceWorkerMain.js create mode 100644 src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts create mode 100644 src/vs/workbench/contrib/resources/browser/resourceServiceWorkerClient.ts create mode 100644 src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts diff --git a/resourceServiceWorkerMain.js b/resourceServiceWorkerMain.js new file mode 100644 index 00000000000..38278c36043 --- /dev/null +++ b/resourceServiceWorkerMain.js @@ -0,0 +1,40 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +(function () { + let handler; + self.addEventListener('fetch', event => { + console.log('FETCH', event); + if (handler) { + handler.handleFetchEvent(event); + } + else { + //@ts-ignore + event.respondWith(fetch(event.request)); + } + }); + self.addEventListener('install', event => { + let loadPromise = new Promise((resolve, reject) => { + // load loader + const monacoBaseUrl = './out/'; + importScripts(monacoBaseUrl + 'vs/loader.js'); + require.config({ + baseUrl: monacoBaseUrl, + catchError: true + }); + require(['vs/workbench/contrib/resources/browser/resourceServiceWorker'], module => { + handler = module; + resolve(); + }, reject); + }); + //@ts-ignore + event.waitUntil(Promise.all([loadPromise, self.skipWaiting()])); + }); + self.addEventListener('activate', event => { + //@ts-ignore + event.waitUntil(self.clients.claim()); // Become available to all pages + }); +})(); +//# sourceMappingURL=resourceServiceWorkerMain.js.map diff --git a/src/vs/workbench/browser/web.resources.ts b/src/vs/workbench/browser/web.resources.ts index ca6c82ef787..2b9adf61c5e 100644 --- a/src/vs/workbench/browser/web.resources.ts +++ b/src/vs/workbench/browser/web.resources.ts @@ -79,6 +79,14 @@ export class WebResources { private async _rewriteUrls(textContent: string): Promise { + return textContent.replace(/url\(('|")?(vscode-remote:\/\/(.*?))\1\)/ig, function (_m, quote, url) { + return `url("http://localhost:9888/out/vs/workbench/contrib/resources/browser/foo?${encodeURIComponent(url)}")`; + // return `url(${quote}${location.href}out/vs/workbench/contrib/resources/browser/?${encodeURIComponent(url)}${quote})`; + }); + } + + private async _rewriteUrls2(textContent: string): Promise { + const positions: number[] = []; const promises: Promise[] = []; diff --git a/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts new file mode 100644 index 00000000000..d311f8d8676 --- /dev/null +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +export function handleFetchEvent(event) { + + console.log('FETCH', event); +} + diff --git a/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerClient.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerClient.ts new file mode 100644 index 00000000000..b4222bf304d --- /dev/null +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerClient.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IFileService } from 'vs/platform/files/common/files'; + +export class ResourceServiceWorkerService { + + constructor( + @IFileService private readonly _fileService: IFileService, + ) { + console.log(this._fileService); + } +} + +// const url = require.toUrl('./resourceServiceWorkerMain.js'); +const url = './resourceServiceWorkerMain.js'; + +navigator.serviceWorker.register( + url, + // { scope: './out/vs/workbench/contrib/resources/browser/' } +).then(value => { + console.log(value); + console.log(navigator.serviceWorker.controller); +}).catch(err => { + console.error(err); +}); + diff --git a/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts new file mode 100644 index 00000000000..997a74fbfc5 --- /dev/null +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * 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): void; + }; + let handler: Handler | undefined; + + self.addEventListener('fetch', event => { + console.log('FETCH', event); + if (handler) { + handler.handleFetchEvent(event); + } else { + //@ts-ignore + event.respondWith(fetch(event.request)); + } + }); + self.addEventListener('install', event => { + + let loadPromise = new Promise((resolve, reject) => { + + // load loader + const monacoBaseUrl = '../../../../../'; + importScripts(monacoBaseUrl + 'vs/loader.js'); + require.config({ + baseUrl: monacoBaseUrl, + catchError: true + }); + + require(['vs/workbench/contrib/resources/browser/resourceServiceWorker'], module => { + handler = module; + resolve(); + }, reject); + }); + + //@ts-ignore + event.waitUntil(Promise.all([loadPromise, self.skipWaiting()])); + }); + + self.addEventListener('activate', event => { + //@ts-ignore + event.waitUntil(self.clients.claim()); // Become available to all pages + }); +})(); diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index c28adc0ad98..bb96b82c27c 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -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 From 519ec86ae421afd2a53146c101a69b5a06762c8a Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 2 Jul 2019 15:16:34 +0200 Subject: [PATCH 2/2] swap web.resources with service worker approach --- resourceServiceWorkerMain.js | 40 ----- src/vs/workbench/browser/web.main.ts | 6 +- src/vs/workbench/browser/web.resources.ts | 124 --------------- .../browser/resourceServiceWorker.ts | 67 ++++++++- .../browser/resourceServiceWorkerClient.ts | 142 ++++++++++++++++-- .../browser/resourceServiceWorkerMain.ts | 59 ++++---- 6 files changed, 225 insertions(+), 213 deletions(-) delete mode 100644 resourceServiceWorkerMain.js delete mode 100644 src/vs/workbench/browser/web.resources.ts diff --git a/resourceServiceWorkerMain.js b/resourceServiceWorkerMain.js deleted file mode 100644 index 38278c36043..00000000000 --- a/resourceServiceWorkerMain.js +++ /dev/null @@ -1,40 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -(function () { - let handler; - self.addEventListener('fetch', event => { - console.log('FETCH', event); - if (handler) { - handler.handleFetchEvent(event); - } - else { - //@ts-ignore - event.respondWith(fetch(event.request)); - } - }); - self.addEventListener('install', event => { - let loadPromise = new Promise((resolve, reject) => { - // load loader - const monacoBaseUrl = './out/'; - importScripts(monacoBaseUrl + 'vs/loader.js'); - require.config({ - baseUrl: monacoBaseUrl, - catchError: true - }); - require(['vs/workbench/contrib/resources/browser/resourceServiceWorker'], module => { - handler = module; - resolve(); - }, reject); - }); - //@ts-ignore - event.waitUntil(Promise.all([loadPromise, self.skipWaiting()])); - }); - self.addEventListener('activate', event => { - //@ts-ignore - event.waitUntil(self.clients.claim()); // Become available to all pages - }); -})(); -//# sourceMappingURL=resourceServiceWorkerMain.js.map diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index f87f57f07dc..9519bdb5158 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -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(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(); -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/web.resources.ts b/src/vs/workbench/browser/web.resources.ts deleted file mode 100644 index 2b9adf61c5e..00000000000 --- a/src/vs/workbench/browser/web.resources.ts +++ /dev/null @@ -1,124 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IFileService } from 'vs/platform/files/common/files'; -import { URI } from 'vs/base/common/uri'; -import { getMediaMime } from 'vs/base/common/mime'; - -export class WebResources { - - private readonly _regexp = /url\(('|")?(vscode-remote:\/\/.*?)\1\)/g; - private readonly _urlCache = new Map(); - private readonly _requestCache = new Map>(); - private readonly _observer: MutationObserver; - - constructor(@IFileService private readonly _fileService: IFileService) { - // todo@joh add observer to more than head-element - // todo@joh explore alternative approach - this._observer = new MutationObserver(r => this._handleMutation(r)); - this._observer.observe(document, { - subtree: true, - childList: true, - attributes: true, - attributeFilter: ['style'] - }); - } - - dispose(): void { - this._observer.disconnect(); - this._urlCache.forEach(value => URL.revokeObjectURL(value)); - } - - private _handleMutation(records: MutationRecord[]): void { - for (const record of records) { - if (record.target.nodeName === 'STYLE') { - // style-element directly modified - this._handleStyleNode(record.target); - - } else if (record.target.nodeName === 'HEAD' && record.type === 'childList') { - // style-element added to head - record.addedNodes.forEach(node => { - if (node.nodeName === 'STYLE') { - this._handleStyleNode(node); - } - }); - } else if (record.type === 'attributes') { - // style-attribute - this._handleAttrMutation(record.target); - } - } - } - - private _handleStyleNode(target: Node): void { - if (target.textContent && target.textContent.indexOf('vscode-remote://') >= 0) { - const content = target.textContent; - this._rewriteUrls(content).then(value => { - if (content === target.textContent) { - target.textContent = value; - } - }).catch(e => { - console.error(e); - }); - } - } - - private _handleAttrMutation(target: Node): void { - const styleValue = (target).getAttribute('style'); - if (styleValue && styleValue.indexOf('vscode-remote://') >= 0) { - this._rewriteUrls(styleValue).then(value => { - if (value !== styleValue) { - (target).setAttribute('style', value); - } - }).catch(e => { - console.error(e); - }); - } - } - - private async _rewriteUrls(textContent: string): Promise { - - return textContent.replace(/url\(('|")?(vscode-remote:\/\/(.*?))\1\)/ig, function (_m, quote, url) { - return `url("http://localhost:9888/out/vs/workbench/contrib/resources/browser/foo?${encodeURIComponent(url)}")`; - // return `url(${quote}${location.href}out/vs/workbench/contrib/resources/browser/?${encodeURIComponent(url)}${quote})`; - }); - } - - private async _rewriteUrls2(textContent: string): Promise { - - const positions: number[] = []; - const promises: Promise[] = []; - - 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; - } -} diff --git a/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts index d311f8d8676..c2163194783 100644 --- a/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts @@ -3,9 +3,72 @@ * 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'; -export function handleFetchEvent(event) { +const cacheName = 'vscode-resources'; - console.log('FETCH', event); +declare const clients: { get(s: string): Promise }; + + +const _pending = new Map(); + +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 { + + 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(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 }); + } } diff --git a/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerClient.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerClient.ts index b4222bf304d..592b17e9a8a 100644 --- a/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerClient.ts +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerClient.ts @@ -4,26 +4,142 @@ *--------------------------------------------------------------------------------------------*/ import { IFileService } from 'vs/platform/files/common/files'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +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 ResourceServiceWorkerService { +// todo@joh explore alternative, explicit approach +class ResourcesMutationObserver { + + private readonly _urlCache = new Map(); + private readonly _observer: MutationObserver; + + private readonly _regexp = /url\(('|")?(vscode-remote:\/\/(.*?))\1\)/ig; + + constructor() { + this._observer = new MutationObserver(r => this._handleMutation(r)); + this._observer.observe(document, { + subtree: true, + childList: true, + attributes: true, + attributeFilter: ['style'] + }); + this.scan(); + } + + scan(): void { + document.querySelectorAll('style').forEach(value => this._handleStyleNode(value)); + // todo@joh more! + } + + dispose(): void { + this._observer.disconnect(); + this._urlCache.forEach(value => URL.revokeObjectURL(value)); + } + + private _handleMutation(records: MutationRecord[]): void { + for (const record of records) { + if (record.target.nodeName === 'STYLE') { + // style-element directly modified + this._handleStyleNode(record.target); + + } else if (record.target.nodeName === 'HEAD' && record.type === 'childList') { + // style-element added to head + record.addedNodes.forEach(node => { + if (node.nodeName === 'STYLE') { + this._handleStyleNode(node); + } + }); + } else if (record.type === 'attributes') { + // style-attribute + this._handleAttrMutation(record.target); + } + } + } + + private _handleStyleNode(target: Node): void { + if (target.textContent && target.textContent.indexOf('vscode-remote://') >= 0) { + const content = target.textContent; + this._rewriteUrls(content).then(value => { + if (content === target.textContent) { + target.textContent = value; + } + }).catch(e => { + console.error(e); + }); + } + } + + private _handleAttrMutation(target: Node): void { + const styleValue = (target).getAttribute('style'); + if (styleValue && styleValue.indexOf('vscode-remote://') >= 0) { + this._rewriteUrls(styleValue).then(value => { + if (value !== styleValue) { + (target).setAttribute('style', value); + } + }).catch(e => { + console.error(e); + }); + } + } + + private async _rewriteUrls(textContent: string): Promise { + 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, ) { - console.log(this._fileService); + 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 + (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))); } } -// const url = require.toUrl('./resourceServiceWorkerMain.js'); -const url = './resourceServiceWorkerMain.js'; +Registry.as(Extensions.Workbench).registerWorkbenchContribution( + ResourceServiceWorker, + LifecyclePhase.Starting +); -navigator.serviceWorker.register( - url, - // { scope: './out/vs/workbench/contrib/resources/browser/' } -).then(value => { - console.log(value); - console.log(navigator.serviceWorker.controller); -}).catch(err => { - console.error(err); -}); diff --git a/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts index 997a74fbfc5..d2c9220fad5 100644 --- a/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts @@ -4,41 +4,42 @@ *--------------------------------------------------------------------------------------------*/ (function () { - type Handler = { - handleFetchEvent(event: Event): void; + handleFetchEvent(event: Event): Promise; + handleMessageEvent(event: MessageEvent): void; }; - let handler: Handler | undefined; - self.addEventListener('fetch', event => { - console.log('FETCH', event); - if (handler) { - handler.handleFetchEvent(event); - } else { - //@ts-ignore - event.respondWith(fetch(event.request)); - } + const handlerPromise = new Promise((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 => { - - let loadPromise = new Promise((resolve, reject) => { - - // load loader - const monacoBaseUrl = '../../../../../'; - importScripts(monacoBaseUrl + 'vs/loader.js'); - require.config({ - baseUrl: monacoBaseUrl, - catchError: true - }); - - require(['vs/workbench/contrib/resources/browser/resourceServiceWorker'], module => { - handler = module; - resolve(); - }, reject); - }); - //@ts-ignore - event.waitUntil(Promise.all([loadPromise, self.skipWaiting()])); + event.waitUntil(self.skipWaiting()); }); self.addEventListener('activate', event => {