Introduces IWebWorkerService to allow the monaco editor to customize web worker handling via service injection

This commit is contained in:
Henning Dieterichs
2025-11-12 00:41:33 +01:00
committed by Henning Dieterichs
parent 77a0f670d3
commit 32b7a94b60
21 changed files with 335 additions and 271 deletions

View File

@@ -3,8 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
WebWorkerDescriptor.useBundlerLocationRef();
registerSingleton(IWebWorkerService, StandaloneWebWorkerService, InstantiationType.Eager);
import { WebWorkerDescriptor } from '../../src/vs/base/browser/webWorkerFactory.js';
import '../../src/vs/code/browser/workbench/workbench.ts';
import { InstantiationType, registerSingleton } from '../../src/vs/platform/instantiation/common/extensions.ts';
import { IWebWorkerService } from '../../src/vs/platform/webWorker/browser/webWorkerService.ts';
// eslint-disable-next-line local/code-no-standalone-editor
import { StandaloneWebWorkerService } from '../../src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts';

View File

@@ -206,7 +206,6 @@ export default tseslint.config(
'src/vs/base/browser/dom.ts',
'src/vs/base/browser/markdownRenderer.ts',
'src/vs/base/browser/touch.ts',
'src/vs/base/browser/webWorkerFactory.ts',
'src/vs/base/common/async.ts',
'src/vs/base/common/desktopEnvironmentInfo.ts',
'src/vs/base/common/objects.ts',

View File

@@ -18,7 +18,7 @@
"vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts"
],
"ban-worker-calls": [
"vs/base/browser/webWorkerFactory.ts",
"vs/platform/webWorker/browser/webWorkerServiceImpl.ts",
"vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts"
],
"ban-worker-importscripts": [

View File

@@ -1,236 +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 { createTrustedTypesPolicy } from './trustedTypes.js';
import { onUnexpectedError } from '../common/errors.js';
import { COI } from '../common/network.js';
import { URI } from '../common/uri.js';
import { IWebWorker, IWebWorkerClient, Message, WebWorkerClient } from '../common/worker/webWorker.js';
import { Disposable, toDisposable } from '../common/lifecycle.js';
import { coalesce } from '../common/arrays.js';
import { getNLSLanguage, getNLSMessages } from '../../nls.js';
import { Emitter } from '../common/event.js';
import { getMonacoEnvironment } from './browser.js';
type WorkerGlobalWithPolicy = typeof globalThis & {
workerttPolicy?: ReturnType<typeof createTrustedTypesPolicy>;
};
// Reuse the trusted types policy defined from worker bootstrap
// when available.
// Refs https://github.com/microsoft/vscode/issues/222193
let ttPolicy: ReturnType<typeof createTrustedTypesPolicy>;
const workerGlobalThis = globalThis as WorkerGlobalWithPolicy;
if (typeof self === 'object' && self.constructor && self.constructor.name === 'DedicatedWorkerGlobalScope' && workerGlobalThis.workerttPolicy !== undefined) {
ttPolicy = workerGlobalThis.workerttPolicy;
} else {
ttPolicy = createTrustedTypesPolicy('defaultWorkerFactory', { createScriptURL: value => value });
}
export function createBlobWorker(blobUrl: string, options?: WorkerOptions): Worker {
if (!blobUrl.startsWith('blob:')) {
throw new URIError('Not a blob-url: ' + blobUrl);
}
return new Worker(ttPolicy ? ttPolicy.createScriptURL(blobUrl) as unknown as string : blobUrl, { ...options, type: 'module' });
}
function getWorker(descriptor: WebWorkerDescriptor, id: number): Worker | Promise<Worker> {
const label = descriptor.label || 'anonymous' + id;
// Option for hosts to overwrite the worker script (used in the standalone editor)
const monacoEnvironment = getMonacoEnvironment();
if (monacoEnvironment) {
if (typeof monacoEnvironment.getWorker === 'function') {
const w = monacoEnvironment.getWorker('workerMain.js', label);
if (w !== undefined) {
return w;
}
}
if (typeof monacoEnvironment.getWorkerUrl === 'function') {
const workerUrl = monacoEnvironment.getWorkerUrl('workerMain.js', label);
if (workerUrl !== undefined) {
return new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrl) as unknown as string : workerUrl, { name: label, type: 'module' });
}
}
}
const esmWorkerLocation = descriptor.getUrl();
if (esmWorkerLocation) {
const workerUrl = getWorkerBootstrapUrl(label, esmWorkerLocation);
const worker = new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrl) as unknown as string : workerUrl, { name: label, type: 'module' });
return whenESMWorkerReady(worker);
}
throw new Error(`You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker`);
}
function getWorkerBootstrapUrl(label: string, workerScriptUrl: string): string {
if (/^((http:)|(https:)|(file:))/.test(workerScriptUrl) && workerScriptUrl.substring(0, globalThis.origin.length) !== globalThis.origin) {
// this is the cross-origin case
// i.e. the webpage is running at a different origin than where the scripts are loaded from
} else {
const start = workerScriptUrl.lastIndexOf('?');
const end = workerScriptUrl.lastIndexOf('#', start);
const params = start > 0
? new URLSearchParams(workerScriptUrl.substring(start + 1, ~end ? end : undefined))
: new URLSearchParams();
COI.addSearchParam(params, true, true);
const search = params.toString();
if (!search) {
workerScriptUrl = `${workerScriptUrl}#${label}`;
} else {
workerScriptUrl = `${workerScriptUrl}?${params.toString()}#${label}`;
}
}
// In below blob code, we are using JSON.stringify to ensure the passed
// in values are not breaking our script. The values may contain string
// terminating characters (such as ' or ").
const blob = new Blob([coalesce([
`/*${label}*/`,
`globalThis._VSCODE_NLS_MESSAGES = ${JSON.stringify(getNLSMessages())};`,
`globalThis._VSCODE_NLS_LANGUAGE = ${JSON.stringify(getNLSLanguage())};`,
`globalThis._VSCODE_FILE_ROOT = ${JSON.stringify(globalThis._VSCODE_FILE_ROOT)};`,
`const ttPolicy = globalThis.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value });`,
`globalThis.workerttPolicy = ttPolicy;`,
`await import(ttPolicy?.createScriptURL(${JSON.stringify(workerScriptUrl)}) ?? ${JSON.stringify(workerScriptUrl)});`,
`globalThis.postMessage({ type: 'vscode-worker-ready' });`,
`/*${label}*/`
]).join('')], { type: 'application/javascript' });
return URL.createObjectURL(blob);
}
function whenESMWorkerReady(worker: Worker): Promise<Worker> {
return new Promise<Worker>((resolve, reject) => {
worker.onmessage = function (e) {
if (e.data.type === 'vscode-worker-ready') {
worker.onmessage = null;
resolve(worker);
}
};
worker.onerror = reject;
});
}
function isPromiseLike<T>(obj: unknown): obj is PromiseLike<T> {
return !!obj && typeof (obj as PromiseLike<T>).then === 'function';
}
/**
* A worker that uses HTML5 web workers so that is has
* its own global scope and its own thread.
*/
class WebWorker extends Disposable implements IWebWorker {
private static LAST_WORKER_ID = 0;
private readonly id: number;
private worker: Promise<Worker> | null;
private readonly _onMessage = this._register(new Emitter<Message>());
public readonly onMessage = this._onMessage.event;
private readonly _onError = this._register(new Emitter<MessageEvent | ErrorEvent>());
public readonly onError = this._onError.event;
constructor(descriptorOrWorker: WebWorkerDescriptor | Worker | Promise<Worker>) {
super();
this.id = ++WebWorker.LAST_WORKER_ID;
const workerOrPromise = (
descriptorOrWorker instanceof Worker
? descriptorOrWorker :
'then' in descriptorOrWorker ? descriptorOrWorker
: getWorker(descriptorOrWorker, this.id)
);
if (isPromiseLike(workerOrPromise)) {
this.worker = workerOrPromise;
} else {
this.worker = Promise.resolve(workerOrPromise);
}
this.postMessage('-please-ignore-', []); // TODO: Eliminate this extra message
const errorHandler = (ev: ErrorEvent) => {
this._onError.fire(ev);
};
this.worker.then((w) => {
w.onmessage = (ev) => {
this._onMessage.fire(ev.data);
};
w.onmessageerror = (ev) => {
this._onError.fire(ev);
};
if (typeof w.addEventListener === 'function') {
w.addEventListener('error', errorHandler);
}
});
this._register(toDisposable(() => {
this.worker?.then(w => {
w.onmessage = null;
w.onmessageerror = null;
w.removeEventListener('error', errorHandler);
w.terminate();
});
this.worker = null;
}));
}
public getId(): number {
return this.id;
}
public postMessage(message: unknown, transfer: Transferable[]): void {
this.worker?.then(w => {
try {
w.postMessage(message, transfer);
} catch (err) {
onUnexpectedError(err);
onUnexpectedError(new Error(`FAILED to post message to worker`, { cause: err }));
}
});
}
}
export class WebWorkerDescriptor {
private static _useBundlerLocationRef = false;
/** TODO @hediet: Use web worker service! */
public static useBundlerLocationRef() {
WebWorkerDescriptor._useBundlerLocationRef = true;
}
public readonly esmModuleLocation: URI | (() => URI) | undefined;
public readonly esmModuleLocationBundler: URL | (() => URL) | undefined;
public readonly label: string | undefined;
constructor(args: {
/** The location of the esm module after transpilation */
esmModuleLocation?: URI | (() => URI);
/** The location of the esm module when used in a bundler environment. Refer to the typescript file in the src folder and use `?worker`. */
esmModuleLocationBundler?: URL | (() => URL);
label?: string;
}) {
this.esmModuleLocation = args.esmModuleLocation;
this.esmModuleLocationBundler = args.esmModuleLocationBundler;
this.label = args.label;
}
getUrl(): string | undefined {
if (WebWorkerDescriptor._useBundlerLocationRef) {
if (this.esmModuleLocationBundler) {
const esmWorkerLocation = typeof this.esmModuleLocationBundler === 'function' ? this.esmModuleLocationBundler() : this.esmModuleLocationBundler;
return esmWorkerLocation.toString();
}
} else if (this.esmModuleLocation) {
const esmWorkerLocation = typeof this.esmModuleLocation === 'function' ? this.esmModuleLocation() : this.esmModuleLocation;
return esmWorkerLocation.toString(true);
}
return undefined;
}
}
export function createWebWorker<T extends object>(workerDescriptor: WebWorkerDescriptor | Worker | Promise<Worker>): IWebWorkerClient<T> {
return new WebWorkerClient<T>(new WebWorker(workerDescriptor));
}

View File

@@ -7,7 +7,8 @@ import { timeout } from '../../../base/common/async.js';
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
import { URI } from '../../../base/common/uri.js';
import { logOnceWebWorkerWarning, IWebWorkerClient, Proxied } from '../../../base/common/worker/webWorker.js';
import { createWebWorker, WebWorkerDescriptor } from '../../../base/browser/webWorkerFactory.js';
import { WebWorkerDescriptor } from '../../../platform/webWorker/browser/webWorkerDescriptor.js';
import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js';
import { Position } from '../../common/core/position.js';
import { IRange, Range } from '../../common/core/range.js';
import { ITextModel } from '../../common/model.js';
@@ -67,6 +68,7 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ
@ILogService logService: ILogService,
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
@IWebWorkerService private readonly _webWorkerService: IWebWorkerService,
) {
super();
this._modelService = modelService;
@@ -77,7 +79,7 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ
label: 'editorWorkerService'
});
this._workerManager = this._register(new WorkerManager(workerDescriptor, this._modelService));
this._workerManager = this._register(new WorkerManager(workerDescriptor, this._modelService, this._webWorkerService));
this._logService = logService;
// register default link-provider and default completions-provider
@@ -333,15 +335,18 @@ class WordBasedCompletionItemProvider implements languages.CompletionItemProvide
class WorkerManager extends Disposable {
private readonly _modelService: IModelService;
private readonly _webWorkerService: IWebWorkerService;
private _editorWorkerClient: EditorWorkerClient | null;
private _lastWorkerUsedTime: number;
constructor(
private readonly _workerDescriptor: WebWorkerDescriptor,
@IModelService modelService: IModelService
@IModelService modelService: IModelService,
@IWebWorkerService webWorkerService: IWebWorkerService
) {
super();
this._modelService = modelService;
this._webWorkerService = webWorkerService;
this._editorWorkerClient = null;
this._lastWorkerUsedTime = (new Date()).getTime();
@@ -393,7 +398,7 @@ class WorkerManager extends Disposable {
public withWorker(): Promise<EditorWorkerClient> {
this._lastWorkerUsedTime = (new Date()).getTime();
if (!this._editorWorkerClient) {
this._editorWorkerClient = new EditorWorkerClient(this._workerDescriptor, false, this._modelService);
this._editorWorkerClient = new EditorWorkerClient(this._workerDescriptor, false, this._modelService, this._webWorkerService);
}
return Promise.resolve(this._editorWorkerClient);
}
@@ -428,6 +433,7 @@ export interface IEditorWorkerClient {
export class EditorWorkerClient extends Disposable implements IEditorWorkerClient {
private readonly _modelService: IModelService;
private readonly _webWorkerService: IWebWorkerService;
private readonly _keepIdleModels: boolean;
private _worker: IWebWorkerClient<EditorWorker> | null;
private _modelManager: WorkerTextModelSyncClient | null;
@@ -437,9 +443,11 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien
private readonly _workerDescriptorOrWorker: WebWorkerDescriptor | Worker | Promise<Worker>,
keepIdleModels: boolean,
@IModelService modelService: IModelService,
@IWebWorkerService webWorkerService: IWebWorkerService
) {
super();
this._modelService = modelService;
this._webWorkerService = webWorkerService;
this._keepIdleModels = keepIdleModels;
this._worker = null;
this._modelManager = null;
@@ -453,7 +461,7 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien
private _getOrCreateWorker(): IWebWorkerClient<EditorWorker> {
if (!this._worker) {
try {
this._worker = this._register(createWebWorker<EditorWorker>(this._workerDescriptorOrWorker));
this._worker = this._register(this._webWorkerService.createWorkerClient<EditorWorker>(this._workerDescriptorOrWorker));
EditorWorkerHost.setChannel(this._worker, this._createEditorWorkerHost());
} catch (err) {
logOnceWebWorkerWarning(err);

View File

@@ -9,9 +9,6 @@ import { createMonacoEditorAPI } from './standalone/browser/standaloneEditor.js'
import { createMonacoLanguagesAPI } from './standalone/browser/standaloneLanguages.js';
import { FormattingConflicts } from './contrib/format/browser/format.js';
import { getMonacoEnvironment } from '../base/browser/browser.js';
import { WebWorkerDescriptor } from '../base/browser/webWorkerFactory.js';
WebWorkerDescriptor.useBundlerLocationRef();
// Set defaults for standalone editor
EditorOptions.wrappingIndent.defaultValue = WrappingIndent.None;

View File

@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getMonacoEnvironment } from '../../../../base/browser/browser.js';
import { WebWorkerDescriptor } from '../../../../platform/webWorker/browser/webWorkerDescriptor.js';
import { WebWorkerService } from '../../../../platform/webWorker/browser/webWorkerServiceImpl.js';
export class StandaloneWebWorkerService extends WebWorkerService {
protected override _createWorker(descriptor: WebWorkerDescriptor): Promise<Worker> {
const monacoEnvironment = getMonacoEnvironment();
if (monacoEnvironment) {
if (typeof monacoEnvironment.getWorker === 'function') {
const worker = monacoEnvironment.getWorker('workerMain.js', descriptor.label);
if (worker !== undefined) {
return Promise.resolve(worker);
}
}
}
return super._createWorker(descriptor);
}
protected override _getUrl(descriptor: WebWorkerDescriptor): string {
const monacoEnvironment = getMonacoEnvironment();
if (monacoEnvironment) {
if (typeof monacoEnvironment.getWorkerUrl === 'function') {
const workerUrl = monacoEnvironment.getWorkerUrl('workerMain.js', descriptor.label);
if (workerUrl !== undefined) {
return workerUrl;
}
}
}
if (!descriptor.esmModuleLocationBundler) {
throw new Error(`You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker`);
}
const url = typeof descriptor.esmModuleLocationBundler === 'function' ? descriptor.esmModuleLocationBundler() : descriptor.esmModuleLocationBundler;
const urlStr = url.toString();
return urlStr;
}
}

View File

@@ -39,6 +39,7 @@ import { IKeybindingService } from '../../../platform/keybinding/common/keybindi
import { IMarker, IMarkerData, IMarkerService } from '../../../platform/markers/common/markers.js';
import { IOpenerService } from '../../../platform/opener/common/opener.js';
import { MultiDiffEditorWidget } from '../../browser/widget/multiDiffEditor/multiDiffEditorWidget.js';
import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js';
/**
* Create a new editor under `domElement`.
@@ -332,7 +333,7 @@ export function onDidChangeModelLanguage(listener: (e: { readonly model: ITextMo
* Specify an AMD module to load that will `create` an object that will be proxied.
*/
export function createWebWorker<T extends object>(opts: IInternalWebWorkerOptions): MonacoWebWorker<T> {
return actualCreateWebWorker<T>(StandaloneServices.get(IModelService), opts);
return actualCreateWebWorker<T>(StandaloneServices.get(IModelService), StandaloneServices.get(IWebWorkerService), opts);
}
/**

View File

@@ -96,6 +96,8 @@ import { ResourceMap } from '../../../base/common/map.js';
import { ITreeSitterLibraryService } from '../../common/services/treeSitter/treeSitterLibraryService.js';
import { StandaloneTreeSitterLibraryService } from './standaloneTreeSitterLibraryService.js';
import { IDataChannelService, NullDataChannelService } from '../../../platform/dataChannel/common/dataChannel.js';
import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js';
import { StandaloneWebWorkerService } from './services/standaloneWebWorkerService.js';
class SimpleModel implements IResolvedTextEditorModel {
@@ -1110,6 +1112,7 @@ export interface IEditorOverrideServices {
}
registerSingleton(IWebWorkerService, StandaloneWebWorkerService, InstantiationType.Eager);
registerSingleton(ILogService, StandaloneLogService, InstantiationType.Eager);
registerSingleton(IConfigurationService, StandaloneConfigurationService, InstantiationType.Eager);
registerSingleton(ITextResourceConfigurationService, StandaloneResourceConfigurationService, InstantiationType.Eager);

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { URI } from '../../../base/common/uri.js';
import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js';
import { EditorWorkerClient } from '../../browser/services/editorWorkerService.js';
import { IModelService } from '../../common/services/model.js';
@@ -11,8 +12,8 @@ import { IModelService } from '../../common/services/model.js';
* Create a new web worker that has model syncing capabilities built in.
* Specify an AMD module to load that will `create` an object that will be proxied.
*/
export function createWebWorker<T extends object>(modelService: IModelService, opts: IInternalWebWorkerOptions): MonacoWebWorker<T> {
return new MonacoWebWorkerImpl<T>(modelService, opts);
export function createWebWorker<T extends object>(modelService: IModelService, webWorkerService: IWebWorkerService, opts: IInternalWebWorkerOptions): MonacoWebWorker<T> {
return new MonacoWebWorkerImpl<T>(modelService, webWorkerService, opts);
}
/**
@@ -55,8 +56,8 @@ class MonacoWebWorkerImpl<T extends object> extends EditorWorkerClient implement
private readonly _foreignModuleHost: { [method: string]: Function } | null;
private _foreignProxy: Promise<T>;
constructor(modelService: IModelService, opts: IInternalWebWorkerOptions) {
super(opts.worker, opts.keepIdleModels || false, modelService);
constructor(modelService: IModelService, webWorkerService: IWebWorkerService, opts: IInternalWebWorkerOptions) {
super(opts.worker, opts.keepIdleModels || false, modelService, webWorkerService);
this._foreignModuleHost = opts.host || null;
this._foreignProxy = this._getProxy().then(proxy => {
return new Proxy({}, {

View File

@@ -4,11 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import { createWebWorker, WebWorkerDescriptor } from '../../../base/browser/webWorkerFactory.js';
import { WebWorkerDescriptor } from '../../webWorker/browser/webWorkerDescriptor.js';
import { URI } from '../../../base/common/uri.js';
import { Proxied } from '../../../base/common/worker/webWorker.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IWebWorkerService } from '../../webWorker/browser/webWorkerService.js';
import { ILogService } from '../../log/common/log.js';
import { IV8Profile } from '../common/profiling.js';
import { BottomUpSample } from '../common/profilingModel.js';
@@ -44,11 +45,12 @@ class ProfileAnalysisWorkerService implements IProfileAnalysisWorkerService {
constructor(
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@ILogService private readonly _logService: ILogService,
@IWebWorkerService private readonly _webWorkerService: IWebWorkerService,
) { }
private async _withWorker<R>(callback: (worker: Proxied<IProfileAnalysisWorker>) => Promise<R>): Promise<R> {
const worker = createWebWorker<IProfileAnalysisWorker>(
const worker = this._webWorkerService.createWorkerClient<IProfileAnalysisWorker>(
new WebWorkerDescriptor({
esmModuleLocation: FileAccess.asBrowserUri('vs/platform/profiling/electron-browser/profileAnalysisWorkerMain.js'),
label: 'CpuProfileAnalysisWorker'

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* 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 '../../../base/common/uri.js';
export class WebWorkerDescriptor {
public readonly esmModuleLocation: URI | (() => URI) | undefined;
public readonly esmModuleLocationBundler: URL | (() => URL) | undefined;
public readonly label: string;
constructor(args: {
/** The location of the esm module after transpilation */
esmModuleLocation?: URI | (() => URI);
/** The location of the esm module when used in a bundler environment. Refer to the typescript file in the src folder and use `?worker`. */
esmModuleLocationBundler?: URL | (() => URL);
label: string;
}) {
this.esmModuleLocation = args.esmModuleLocation;
this.esmModuleLocationBundler = args.esmModuleLocationBundler;
this.label = args.label;
}
}

View File

@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IWebWorkerClient } from '../../../base/common/worker/webWorker.js';
import { WebWorkerDescriptor } from './webWorkerDescriptor.js';
export const IWebWorkerService = createDecorator<IWebWorkerService>('IWebWorkerService');
export interface IWebWorkerService {
readonly _serviceBrand: undefined;
createWorkerClient<T extends object>(workerDescriptor: WebWorkerDescriptor | Worker | Promise<Worker>): IWebWorkerClient<T>;
}

View File

@@ -0,0 +1,181 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createTrustedTypesPolicy } from '../../../base/browser/trustedTypes.js';
import { coalesce } from '../../../base/common/arrays.js';
import { onUnexpectedError } from '../../../base/common/errors.js';
import { Emitter } from '../../../base/common/event.js';
import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
import { COI } from '../../../base/common/network.js';
import { IWebWorker, IWebWorkerClient, Message, WebWorkerClient } from '../../../base/common/worker/webWorker.js';
import { getNLSLanguage, getNLSMessages } from '../../../nls.js';
import { WebWorkerDescriptor } from './webWorkerDescriptor.js';
import { IWebWorkerService } from './webWorkerService.js';
export class WebWorkerService implements IWebWorkerService {
private static _workerIdPool: number = 0;
declare readonly _serviceBrand: undefined;
createWorkerClient<T extends object>(workerDescriptor: WebWorkerDescriptor | Worker | Promise<Worker>): IWebWorkerClient<T> {
let worker: Worker | Promise<Worker>;
const id = ++WebWorkerService._workerIdPool;
if (workerDescriptor instanceof Worker || isPromiseLike<Worker>(workerDescriptor)) {
worker = Promise.resolve(workerDescriptor);
} else {
worker = this._createWorker(workerDescriptor);
}
return new WebWorkerClient<T>(new WebWorker(worker, id));
}
protected _createWorker(descriptor: WebWorkerDescriptor): Promise<Worker> {
const workerRunnerUrl = this._getUrl(descriptor);
const workerUrl = getWorkerBootstrapUrl(descriptor.label, workerRunnerUrl);
const worker = new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrl) as unknown as string : workerUrl, { name: descriptor.label, type: 'module' });
return whenESMWorkerReady(worker);
}
protected _getUrl(descriptor: WebWorkerDescriptor): string {
if (!descriptor.esmModuleLocation) {
throw new Error('Missing esmModuleLocation in WebWorkerDescriptor');
}
const uri = typeof descriptor.esmModuleLocation === 'function' ? descriptor.esmModuleLocation() : descriptor.esmModuleLocation;
const urlStr = uri.toString(true);
return urlStr;
}
}
const ttPolicy = ((): ReturnType<typeof createTrustedTypesPolicy> => {
type WorkerGlobalWithPolicy = typeof globalThis & {
workerttPolicy?: ReturnType<typeof createTrustedTypesPolicy>;
};
// Reuse the trusted types policy defined from worker bootstrap
// when available.
// Refs https://github.com/microsoft/vscode/issues/222193
const workerGlobalThis = globalThis as WorkerGlobalWithPolicy;
if (typeof self === 'object' && self.constructor && self.constructor.name === 'DedicatedWorkerGlobalScope' && workerGlobalThis.workerttPolicy !== undefined) {
return workerGlobalThis.workerttPolicy;
} else {
return createTrustedTypesPolicy('defaultWorkerFactory', { createScriptURL: value => value });
}
})();
export function createBlobWorker(blobUrl: string, options?: WorkerOptions): Worker {
if (!blobUrl.startsWith('blob:')) {
throw new URIError('Not a blob-url: ' + blobUrl);
}
return new Worker(ttPolicy ? ttPolicy.createScriptURL(blobUrl) as unknown as string : blobUrl, { ...options, type: 'module' });
}
function getWorkerBootstrapUrl(label: string, workerScriptUrl: string): string {
if (/^((http:)|(https:)|(file:))/.test(workerScriptUrl) && workerScriptUrl.substring(0, globalThis.origin.length) !== globalThis.origin) {
// this is the cross-origin case
// i.e. the webpage is running at a different origin than where the scripts are loaded from
} else {
const start = workerScriptUrl.lastIndexOf('?');
const end = workerScriptUrl.lastIndexOf('#', start);
const params = start > 0
? new URLSearchParams(workerScriptUrl.substring(start + 1, ~end ? end : undefined))
: new URLSearchParams();
COI.addSearchParam(params, true, true);
const search = params.toString();
if (!search) {
workerScriptUrl = `${workerScriptUrl}#${label}`;
} else {
workerScriptUrl = `${workerScriptUrl}?${params.toString()}#${label}`;
}
}
// In below blob code, we are using JSON.stringify to ensure the passed
// in values are not breaking our script. The values may contain string
// terminating characters (such as ' or ").
const blob = new Blob([coalesce([
`/*${label}*/`,
`globalThis._VSCODE_NLS_MESSAGES = ${JSON.stringify(getNLSMessages())};`,
`globalThis._VSCODE_NLS_LANGUAGE = ${JSON.stringify(getNLSLanguage())};`,
`globalThis._VSCODE_FILE_ROOT = ${JSON.stringify(globalThis._VSCODE_FILE_ROOT)};`,
`const ttPolicy = globalThis.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value });`,
`globalThis.workerttPolicy = ttPolicy;`,
`await import(ttPolicy?.createScriptURL(${JSON.stringify(workerScriptUrl)}) ?? ${JSON.stringify(workerScriptUrl)});`,
`globalThis.postMessage({ type: 'vscode-worker-ready' });`,
`/*${label}*/`
]).join('')], { type: 'application/javascript' });
return URL.createObjectURL(blob);
}
function whenESMWorkerReady(worker: Worker): Promise<Worker> {
return new Promise<Worker>((resolve, reject) => {
worker.onmessage = function (e) {
if (e.data.type === 'vscode-worker-ready') {
worker.onmessage = null;
resolve(worker);
}
};
worker.onerror = reject;
});
}
function isPromiseLike<T>(obj: unknown): obj is PromiseLike<T> {
return !!obj && typeof (obj as PromiseLike<T>).then === 'function';
}
export class WebWorker extends Disposable implements IWebWorker {
private readonly id: number;
private worker: Promise<Worker> | null;
private readonly _onMessage = this._register(new Emitter<Message>());
public readonly onMessage = this._onMessage.event;
private readonly _onError = this._register(new Emitter<MessageEvent | ErrorEvent>());
public readonly onError = this._onError.event;
constructor(worker: Promise<Worker>, id: number) {
super();
this.id = id;
this.worker = worker;
this.postMessage('-please-ignore-', []); // TODO: Eliminate this extra message
const errorHandler = (ev: ErrorEvent) => {
this._onError.fire(ev);
};
this.worker.then((w) => {
w.onmessage = (ev) => {
this._onMessage.fire(ev.data);
};
w.onmessageerror = (ev) => {
this._onError.fire(ev);
};
if (typeof w.addEventListener === 'function') {
w.addEventListener('error', errorHandler);
}
});
this._register(toDisposable(() => {
this.worker?.then(w => {
w.onmessage = null;
w.onmessageerror = null;
w.removeEventListener('error', errorHandler);
w.terminate();
});
this.worker = null;
}));
}
public getId(): number {
return this.id;
}
public postMessage(message: unknown, transfer: Transferable[]): void {
this.worker?.then(w => {
try {
w.postMessage(message, transfer);
} catch (err) {
onUnexpectedError(err);
onUnexpectedError(new Error(`FAILED to post message to worker`, { cause: err }));
}
});
}
}

View File

@@ -6,7 +6,8 @@
import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../base/common/uri.js';
import { IWebWorkerClient, Proxied } from '../../../../../base/common/worker/webWorker.js';
import { createWebWorker, WebWorkerDescriptor } from '../../../../../base/browser/webWorkerFactory.js';
import { WebWorkerDescriptor } from '../../../../../platform/webWorker/browser/webWorkerDescriptor.js';
import { IWebWorkerService } from '../../../../../platform/webWorker/browser/webWorkerService.js';
import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';
import { CellUri, IMainCellDto, INotebookDiffResult, NotebookCellsChangeType, NotebookRawContentEventDto } from '../../common/notebookCommon.js';
import { INotebookService } from '../../common/notebookService.js';
@@ -26,10 +27,11 @@ export class NotebookEditorWorkerServiceImpl extends Disposable implements INote
constructor(
@INotebookService notebookService: INotebookService,
@IModelService modelService: IModelService,
@IWebWorkerService webWorkerService: IWebWorkerService,
) {
super();
this._workerManager = this._register(new WorkerManager(notebookService, modelService));
this._workerManager = this._register(new WorkerManager(notebookService, modelService, webWorkerService));
}
canComputeDiff(original: URI, modified: URI): boolean {
throw new Error('Method not implemented.');
@@ -55,6 +57,7 @@ class WorkerManager extends Disposable {
constructor(
private readonly _notebookService: INotebookService,
private readonly _modelService: IModelService,
private readonly _webWorkerService: IWebWorkerService,
) {
super();
this._editorWorkerClient = null;
@@ -64,7 +67,7 @@ class WorkerManager extends Disposable {
withWorker(): Promise<NotebookWorkerClient> {
// this._lastWorkerUsedTime = (new Date()).getTime();
if (!this._editorWorkerClient) {
this._editorWorkerClient = new NotebookWorkerClient(this._notebookService, this._modelService);
this._editorWorkerClient = new NotebookWorkerClient(this._notebookService, this._modelService, this._webWorkerService);
this._register(this._editorWorkerClient);
}
return Promise.resolve(this._editorWorkerClient);
@@ -240,7 +243,11 @@ class NotebookWorkerClient extends Disposable {
private _modelManager: NotebookEditorModelManager | null;
constructor(private readonly _notebookService: INotebookService, private readonly _modelService: IModelService) {
constructor(
private readonly _notebookService: INotebookService,
private readonly _modelService: IModelService,
private readonly _webWorkerService: IWebWorkerService,
) {
super();
this._worker = null;
this._modelManager = null;
@@ -273,7 +280,7 @@ class NotebookWorkerClient extends Disposable {
private _getOrCreateWorker(): IWebWorkerClient<NotebookWorker> {
if (!this._worker) {
try {
this._worker = this._register(createWebWorker<NotebookWorker>(
this._worker = this._register(this._webWorkerService.createWorkerClient<NotebookWorker>(
new WebWorkerDescriptor({
esmModuleLocation: FileAccess.asBrowserUri('vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain.js'),
label: 'NotebookEditorWorker'

View File

@@ -12,7 +12,8 @@ import { OUTPUT_MODE_ID, LOG_MODE_ID } from '../../../services/output/common/out
import { OutputLinkComputer } from '../common/outputLinkComputer.js';
import { IDisposable, dispose, Disposable } from '../../../../base/common/lifecycle.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { createWebWorker, WebWorkerDescriptor } from '../../../../base/browser/webWorkerFactory.js';
import { WebWorkerDescriptor } from '../../../../platform/webWorker/browser/webWorkerDescriptor.js';
import { IWebWorkerService } from '../../../../platform/webWorker/browser/webWorkerService.js';
import { IWebWorkerClient } from '../../../../base/common/worker/webWorker.js';
import { WorkerTextModelSyncClient } from '../../../../editor/common/services/textModelSync/textModelSync.impl.js';
import { FileAccess } from '../../../../base/common/network.js';
@@ -29,6 +30,7 @@ export class OutputLinkProvider extends Disposable {
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IModelService private readonly modelService: IModelService,
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IWebWorkerService private readonly webWorkerService: IWebWorkerService,
) {
super();
@@ -70,7 +72,7 @@ export class OutputLinkProvider extends Disposable {
this.disposeWorkerScheduler.schedule();
if (!this.worker) {
this.worker = new OutputLinkWorkerClient(this.contextService, this.modelService);
this.worker = new OutputLinkWorkerClient(this.contextService, this.modelService, this.webWorkerService);
}
return this.worker;
@@ -96,9 +98,10 @@ class OutputLinkWorkerClient extends Disposable {
constructor(
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IModelService modelService: IModelService,
@IWebWorkerService webWorkerService: IWebWorkerService,
) {
super();
this._workerClient = this._register(createWebWorker<OutputLinkComputer>(
this._workerClient = this._register(webWorkerService.createWorkerClient<OutputLinkComputer>(
new WebWorkerDescriptor({
esmModuleLocation: FileAccess.asBrowserUri('vs/workbench/contrib/output/common/outputLinkComputerMain.js'),
label: 'OutputLinkDetectionWorker'

View File

@@ -22,7 +22,8 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { LRUCache } from '../../../../base/common/map.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { canASAR } from '../../../../amdX.js';
import { createWebWorker, WebWorkerDescriptor } from '../../../../base/browser/webWorkerFactory.js';
import { WebWorkerDescriptor } from '../../../../platform/webWorker/browser/webWorkerDescriptor.js';
import { IWebWorkerService } from '../../../../platform/webWorker/browser/webWorkerService.js';
import { WorkerTextModelSyncClient } from '../../../../editor/common/services/textModelSync/textModelSync.impl.js';
import { ILanguageDetectionWorker, LanguageDetectionWorkerHost } from './languageDetectionWorker.protocol.js';
@@ -62,7 +63,8 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet
@IEditorService private readonly _editorService: IEditorService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@ILogService private readonly _logService: ILogService
@ILogService private readonly _logService: ILogService,
@IWebWorkerService webWorkerService: IWebWorkerService,
) {
super();
@@ -71,6 +73,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet
modelService,
languageService,
telemetryService,
webWorkerService,
// TODO See if it's possible to bundle vscode-languagedetection
useAsar
? FileAccess.asBrowserUri(`${moduleLocationAsar}/dist/lib/index.js`).toString(true)
@@ -187,6 +190,7 @@ export class LanguageDetectionWorkerClient extends Disposable {
private readonly _modelService: IModelService,
private readonly _languageService: ILanguageService,
private readonly _telemetryService: ITelemetryService,
private readonly _webWorkerService: IWebWorkerService,
private readonly _indexJsUri: string,
private readonly _modelJsonUri: string,
private readonly _weightsUri: string,
@@ -200,7 +204,7 @@ export class LanguageDetectionWorkerClient extends Disposable {
workerTextModelSyncClient: WorkerTextModelSyncClient;
} {
if (!this.worker) {
const workerClient = this._register(createWebWorker<ILanguageDetectionWorker>(
const workerClient = this._register(this._webWorkerService.createWorkerClient<ILanguageDetectionWorker>(
new WebWorkerDescriptor({
esmModuleLocation: FileAccess.asBrowserUri('vs/workbench/services/languageDetection/browser/languageDetectionWebWorkerMain.js'),
label: 'LanguageDetectionWorker'

View File

@@ -16,7 +16,8 @@ import { SearchService } from '../common/searchService.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { IWebWorkerClient, logOnceWebWorkerWarning } from '../../../../base/common/worker/webWorker.js';
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
import { createWebWorker, WebWorkerDescriptor } from '../../../../base/browser/webWorkerFactory.js';
import { WebWorkerDescriptor } from '../../../../platform/webWorker/browser/webWorkerDescriptor.js';
import { IWebWorkerService } from '../../../../platform/webWorker/browser/webWorkerService.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { ILocalFileSearchWorker, LocalFileSearchWorkerHost } from '../common/localFileSearchWorkerTypes.js';
import { memoize } from '../../../../base/common/decorators.js';
@@ -60,6 +61,7 @@ export class LocalFileSearchWorkerClient extends Disposable implements ISearchRe
constructor(
@IFileService private fileService: IFileService,
@IUriIdentityService private uriIdentityService: IUriIdentityService,
@IWebWorkerService private webWorkerService: IWebWorkerService,
) {
super();
this._worker = null;
@@ -187,7 +189,7 @@ export class LocalFileSearchWorkerClient extends Disposable implements ISearchRe
private _getOrCreateWorker(): IWebWorkerClient<ILocalFileSearchWorker> {
if (!this._worker) {
try {
this._worker = this._register(createWebWorker<ILocalFileSearchWorker>(
this._worker = this._register(this.webWorkerService.createWorkerClient<ILocalFileSearchWorker>(
new WebWorkerDescriptor({
esmModuleLocation: FileAccess.asBrowserUri('vs/workbench/services/search/worker/localFileSearchMain.js'),
label: 'LocalFileSearchWorker'

View File

@@ -22,7 +22,8 @@ import { TextMateWorkerHost } from './worker/textMateWorkerHost.js';
import { TextMateWorkerTokenizerController } from './textMateWorkerTokenizerController.js';
import { IValidGrammarDefinition } from '../../common/TMScopeRegistry.js';
import type { IRawTheme } from 'vscode-textmate';
import { createWebWorker, WebWorkerDescriptor } from '../../../../../base/browser/webWorkerFactory.js';
import { WebWorkerDescriptor } from '../../../../../platform/webWorker/browser/webWorkerDescriptor.js';
import { IWebWorkerService } from '../../../../../platform/webWorker/browser/webWorkerService.js';
import { IWebWorkerClient, Proxied } from '../../../../../base/common/worker/webWorker.js';
export class ThreadedBackgroundTokenizerFactory implements IDisposable {
@@ -46,6 +47,7 @@ export class ThreadedBackgroundTokenizerFactory implements IDisposable {
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
@INotificationService private readonly _notificationService: INotificationService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IWebWorkerService private readonly _webWorkerService: IWebWorkerService,
) {
}
@@ -137,7 +139,7 @@ export class ThreadedBackgroundTokenizerFactory implements IDisposable {
grammarDefinitions: this._grammarDefinitions,
onigurumaWASMUri: FileAccess.asBrowserUri(onigurumaWASM).toString(true),
};
const worker = this._worker = createWebWorker<TextMateTokenizationWorker>(
const worker = this._worker = this._webWorkerService.createWorkerClient<TextMateTokenizationWorker>(
new WebWorkerDescriptor({
esmModuleLocation: FileAccess.asBrowserUri('vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.workerMain.js'),
label: 'TextMateWorker'

View File

@@ -18,7 +18,7 @@ import { IPaneCompositePartService } from '../../panecomposite/browser/panecompo
import { ViewContainerLocation } from '../../../common/views.js';
import { TelemetryTrustedValue } from '../../../../platform/telemetry/common/telemetryUtils.js';
import { isWeb } from '../../../../base/common/platform.js';
import { createBlobWorker } from '../../../../base/browser/webWorkerFactory.js';
import { createBlobWorker } from '../../../../platform/webWorker/browser/webWorkerServiceImpl.js';
import { Registry } from '../../../../platform/registry/common/platform.js';
import { ITerminalBackendRegistry, TerminalExtensions } from '../../../../platform/terminal/common/terminal.js';

View File

@@ -159,6 +159,8 @@ import { AllowedExtensionsService } from '../platform/extensionManagement/common
import { IAllowedMcpServersService, IMcpGalleryService } from '../platform/mcp/common/mcpManagement.js';
import { McpGalleryService } from '../platform/mcp/common/mcpGalleryService.js';
import { AllowedMcpServersService } from '../platform/mcp/common/allowedMcpServersService.js';
import { IWebWorkerService } from '../platform/webWorker/browser/webWorkerService.js';
import { WebWorkerService } from '../platform/webWorker/browser/webWorkerServiceImpl.js';
registerSingleton(IUserDataSyncLogService, UserDataSyncLogService, InstantiationType.Delayed);
registerSingleton(IAllowedExtensionsService, AllowedExtensionsService, InstantiationType.Delayed);
@@ -173,6 +175,7 @@ registerSingleton(IContextKeyService, ContextKeyService, InstantiationType.Delay
registerSingleton(ITextResourceConfigurationService, TextResourceConfigurationService, InstantiationType.Delayed);
registerSingleton(IDownloadService, DownloadService, InstantiationType.Delayed);
registerSingleton(IOpenerService, OpenerService, InstantiationType.Delayed);
registerSingleton(IWebWorkerService, WebWorkerService, InstantiationType.Delayed);
registerSingleton(IMcpGalleryService, McpGalleryService, InstantiationType.Delayed);
registerSingleton(IAllowedMcpServersService, AllowedMcpServersService, InstantiationType.Delayed);