mirror of
https://github.com/Microsoft/vscode
synced 2024-10-05 19:02:54 +00:00
swap web.resources with service worker approach
This commit is contained in:
parent
7cbf591b90
commit
519ec86ae4
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
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 = (<HTMLElement>target).getAttribute('style');
|
||||
if (styleValue && styleValue.indexOf('vscode-remote://') >= 0) {
|
||||
this._rewriteUrls(styleValue).then(value => {
|
||||
if (value !== styleValue) {
|
||||
(<HTMLElement>target).setAttribute('style', value);
|
||||
}
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _rewriteUrls(textContent: string): Promise<string> {
|
||||
|
||||
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<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;
|
||||
}
|
||||
}
|
|
@ -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<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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string, string>();
|
||||
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 = (<HTMLElement>target).getAttribute('style');
|
||||
if (styleValue && styleValue.indexOf('vscode-remote://') >= 0) {
|
||||
this._rewriteUrls(styleValue).then(value => {
|
||||
if (value !== styleValue) {
|
||||
(<HTMLElement>target).setAttribute('style', value);
|
||||
}
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _rewriteUrls(textContent: string): Promise<string> {
|
||||
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
|
||||
(<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)));
|
||||
}
|
||||
}
|
||||
|
||||
// const url = require.toUrl('./resourceServiceWorkerMain.js');
|
||||
const url = './resourceServiceWorkerMain.js';
|
||||
Registry.as<IWorkbenchContributionsRegistry>(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);
|
||||
});
|
||||
|
||||
|
|
|
@ -4,41 +4,42 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
(function () {
|
||||
|
||||
type Handler = {
|
||||
handleFetchEvent(event: Event): void;
|
||||
handleFetchEvent(event: Event): Promise<Response | undefined>;
|
||||
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<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 => {
|
||||
|
||||
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 => {
|
||||
|
|
Loading…
Reference in a new issue