UNPKG

monaco-editor

Version:
462 lines (461 loc) • 18.1 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { onUnexpectedError, transformErrorForSerialization } from '../errors.js'; import { Emitter } from '../event.js'; import { Disposable } from '../lifecycle.js'; import { FileAccess } from '../network.js'; import { isWeb } from '../platform.js'; import * as strings from '../strings.js'; // ESM-comment-begin // const isESM = false; // ESM-comment-end // ESM-uncomment-begin const isESM = true; // ESM-uncomment-end const DEFAULT_CHANNEL = 'default'; const INITIALIZE = '$initialize'; let webWorkerWarningLogged = false; export function logOnceWebWorkerWarning(err) { if (!isWeb) { // running tests return; } if (!webWorkerWarningLogged) { webWorkerWarningLogged = true; console.warn('Could not create web worker(s). Falling back to loading web worker code in main thread, which might cause UI freezes. Please see https://github.com/microsoft/monaco-editor#faq'); } console.warn(err.message); } class RequestMessage { constructor(vsWorker, req, channel, method, args) { this.vsWorker = vsWorker; this.req = req; this.channel = channel; this.method = method; this.args = args; this.type = 0 /* MessageType.Request */; } } class ReplyMessage { constructor(vsWorker, seq, res, err) { this.vsWorker = vsWorker; this.seq = seq; this.res = res; this.err = err; this.type = 1 /* MessageType.Reply */; } } class SubscribeEventMessage { constructor(vsWorker, req, channel, eventName, arg) { this.vsWorker = vsWorker; this.req = req; this.channel = channel; this.eventName = eventName; this.arg = arg; this.type = 2 /* MessageType.SubscribeEvent */; } } class EventMessage { constructor(vsWorker, req, event) { this.vsWorker = vsWorker; this.req = req; this.event = event; this.type = 3 /* MessageType.Event */; } } class UnsubscribeEventMessage { constructor(vsWorker, req) { this.vsWorker = vsWorker; this.req = req; this.type = 4 /* MessageType.UnsubscribeEvent */; } } class SimpleWorkerProtocol { constructor(handler) { this._workerId = -1; this._handler = handler; this._lastSentReq = 0; this._pendingReplies = Object.create(null); this._pendingEmitters = new Map(); this._pendingEvents = new Map(); } setWorkerId(workerId) { this._workerId = workerId; } sendMessage(channel, method, args) { const req = String(++this._lastSentReq); return new Promise((resolve, reject) => { this._pendingReplies[req] = { resolve: resolve, reject: reject }; this._send(new RequestMessage(this._workerId, req, channel, method, args)); }); } listen(channel, eventName, arg) { let req = null; const emitter = new Emitter({ onWillAddFirstListener: () => { req = String(++this._lastSentReq); this._pendingEmitters.set(req, emitter); this._send(new SubscribeEventMessage(this._workerId, req, channel, eventName, arg)); }, onDidRemoveLastListener: () => { this._pendingEmitters.delete(req); this._send(new UnsubscribeEventMessage(this._workerId, req)); req = null; } }); return emitter.event; } handleMessage(message) { if (!message || !message.vsWorker) { return; } if (this._workerId !== -1 && message.vsWorker !== this._workerId) { return; } this._handleMessage(message); } createProxyToRemoteChannel(channel, sendMessageBarrier) { const handler = { get: (target, name) => { if (typeof name === 'string' && !target[name]) { if (propertyIsDynamicEvent(name)) { // onDynamic... target[name] = (arg) => { return this.listen(channel, name, arg); }; } else if (propertyIsEvent(name)) { // on... target[name] = this.listen(channel, name, undefined); } else if (name.charCodeAt(0) === 36 /* CharCode.DollarSign */) { // $... target[name] = async (...myArgs) => { await sendMessageBarrier?.(); return this.sendMessage(channel, name, myArgs); }; } } return target[name]; } }; return new Proxy(Object.create(null), handler); } _handleMessage(msg) { switch (msg.type) { case 1 /* MessageType.Reply */: return this._handleReplyMessage(msg); case 0 /* MessageType.Request */: return this._handleRequestMessage(msg); case 2 /* MessageType.SubscribeEvent */: return this._handleSubscribeEventMessage(msg); case 3 /* MessageType.Event */: return this._handleEventMessage(msg); case 4 /* MessageType.UnsubscribeEvent */: return this._handleUnsubscribeEventMessage(msg); } } _handleReplyMessage(replyMessage) { if (!this._pendingReplies[replyMessage.seq]) { console.warn('Got reply to unknown seq'); return; } const reply = this._pendingReplies[replyMessage.seq]; delete this._pendingReplies[replyMessage.seq]; if (replyMessage.err) { let err = replyMessage.err; if (replyMessage.err.$isError) { err = new Error(); err.name = replyMessage.err.name; err.message = replyMessage.err.message; err.stack = replyMessage.err.stack; } reply.reject(err); return; } reply.resolve(replyMessage.res); } _handleRequestMessage(requestMessage) { const req = requestMessage.req; const result = this._handler.handleMessage(requestMessage.channel, requestMessage.method, requestMessage.args); result.then((r) => { this._send(new ReplyMessage(this._workerId, req, r, undefined)); }, (e) => { if (e.detail instanceof Error) { // Loading errors have a detail property that points to the actual error e.detail = transformErrorForSerialization(e.detail); } this._send(new ReplyMessage(this._workerId, req, undefined, transformErrorForSerialization(e))); }); } _handleSubscribeEventMessage(msg) { const req = msg.req; const disposable = this._handler.handleEvent(msg.channel, msg.eventName, msg.arg)((event) => { this._send(new EventMessage(this._workerId, req, event)); }); this._pendingEvents.set(req, disposable); } _handleEventMessage(msg) { if (!this._pendingEmitters.has(msg.req)) { console.warn('Got event for unknown req'); return; } this._pendingEmitters.get(msg.req).fire(msg.event); } _handleUnsubscribeEventMessage(msg) { if (!this._pendingEvents.has(msg.req)) { console.warn('Got unsubscribe for unknown req'); return; } this._pendingEvents.get(msg.req).dispose(); this._pendingEvents.delete(msg.req); } _send(msg) { const transfer = []; if (msg.type === 0 /* MessageType.Request */) { for (let i = 0; i < msg.args.length; i++) { if (msg.args[i] instanceof ArrayBuffer) { transfer.push(msg.args[i]); } } } else if (msg.type === 1 /* MessageType.Reply */) { if (msg.res instanceof ArrayBuffer) { transfer.push(msg.res); } } this._handler.sendMessage(msg, transfer); } } /** * Main thread side */ export class SimpleWorkerClient extends Disposable { constructor(workerFactory, workerDescriptor) { super(); this._localChannels = new Map(); this._worker = this._register(workerFactory.create({ amdModuleId: 'vs/base/common/worker/simpleWorker', esmModuleLocation: workerDescriptor.esmModuleLocation, label: workerDescriptor.label }, (msg) => { this._protocol.handleMessage(msg); }, (err) => { // in Firefox, web workers fail lazily :( // we will reject the proxy onUnexpectedError(err); })); this._protocol = new SimpleWorkerProtocol({ sendMessage: (msg, transfer) => { this._worker.postMessage(msg, transfer); }, handleMessage: (channel, method, args) => { return this._handleMessage(channel, method, args); }, handleEvent: (channel, eventName, arg) => { return this._handleEvent(channel, eventName, arg); } }); this._protocol.setWorkerId(this._worker.getId()); // Gather loader configuration let loaderConfiguration = null; const globalRequire = globalThis.require; if (typeof globalRequire !== 'undefined' && typeof globalRequire.getConfig === 'function') { // Get the configuration from the Monaco AMD Loader loaderConfiguration = globalRequire.getConfig(); } else if (typeof globalThis.requirejs !== 'undefined') { // Get the configuration from requirejs loaderConfiguration = globalThis.requirejs.s.contexts._.config; } // Send initialize message this._onModuleLoaded = this._protocol.sendMessage(DEFAULT_CHANNEL, INITIALIZE, [ this._worker.getId(), JSON.parse(JSON.stringify(loaderConfiguration)), workerDescriptor.amdModuleId, ]); this.proxy = this._protocol.createProxyToRemoteChannel(DEFAULT_CHANNEL, async () => { await this._onModuleLoaded; }); this._onModuleLoaded.catch((e) => { this._onError('Worker failed to load ' + workerDescriptor.amdModuleId, e); }); } _handleMessage(channelName, method, args) { const channel = this._localChannels.get(channelName); if (!channel) { return Promise.reject(new Error(`Missing channel ${channelName} on main thread`)); } if (typeof channel[method] !== 'function') { return Promise.reject(new Error(`Missing method ${method} on main thread channel ${channelName}`)); } try { return Promise.resolve(channel[method].apply(channel, args)); } catch (e) { return Promise.reject(e); } } _handleEvent(channelName, eventName, arg) { const channel = this._localChannels.get(channelName); if (!channel) { throw new Error(`Missing channel ${channelName} on main thread`); } if (propertyIsDynamicEvent(eventName)) { const event = channel[eventName].call(channel, arg); if (typeof event !== 'function') { throw new Error(`Missing dynamic event ${eventName} on main thread channel ${channelName}.`); } return event; } if (propertyIsEvent(eventName)) { const event = channel[eventName]; if (typeof event !== 'function') { throw new Error(`Missing event ${eventName} on main thread channel ${channelName}.`); } return event; } throw new Error(`Malformed event name ${eventName}`); } setChannel(channel, handler) { this._localChannels.set(channel, handler); } _onError(message, error) { console.error(message); console.info(error); } } function propertyIsEvent(name) { // Assume a property is an event if it has a form of "onSomething" return name[0] === 'o' && name[1] === 'n' && strings.isUpperAsciiLetter(name.charCodeAt(2)); } function propertyIsDynamicEvent(name) { // Assume a property is a dynamic event (a method that returns an event) if it has a form of "onDynamicSomething" return /^onDynamic/.test(name) && strings.isUpperAsciiLetter(name.charCodeAt(9)); } /** * Worker side */ export class SimpleWorkerServer { constructor(postMessage, requestHandlerFactory) { this._localChannels = new Map(); this._remoteChannels = new Map(); this._requestHandlerFactory = requestHandlerFactory; this._requestHandler = null; this._protocol = new SimpleWorkerProtocol({ sendMessage: (msg, transfer) => { postMessage(msg, transfer); }, handleMessage: (channel, method, args) => this._handleMessage(channel, method, args), handleEvent: (channel, eventName, arg) => this._handleEvent(channel, eventName, arg) }); } onmessage(msg) { this._protocol.handleMessage(msg); } _handleMessage(channel, method, args) { if (channel === DEFAULT_CHANNEL && method === INITIALIZE) { return this.initialize(args[0], args[1], args[2]); } const requestHandler = (channel === DEFAULT_CHANNEL ? this._requestHandler : this._localChannels.get(channel)); if (!requestHandler) { return Promise.reject(new Error(`Missing channel ${channel} on worker thread`)); } if (typeof requestHandler[method] !== 'function') { return Promise.reject(new Error(`Missing method ${method} on worker thread channel ${channel}`)); } try { return Promise.resolve(requestHandler[method].apply(requestHandler, args)); } catch (e) { return Promise.reject(e); } } _handleEvent(channel, eventName, arg) { const requestHandler = (channel === DEFAULT_CHANNEL ? this._requestHandler : this._localChannels.get(channel)); if (!requestHandler) { throw new Error(`Missing channel ${channel} on worker thread`); } if (propertyIsDynamicEvent(eventName)) { const event = requestHandler[eventName].call(requestHandler, arg); if (typeof event !== 'function') { throw new Error(`Missing dynamic event ${eventName} on request handler.`); } return event; } if (propertyIsEvent(eventName)) { const event = requestHandler[eventName]; if (typeof event !== 'function') { throw new Error(`Missing event ${eventName} on request handler.`); } return event; } throw new Error(`Malformed event name ${eventName}`); } getChannel(channel) { if (!this._remoteChannels.has(channel)) { const inst = this._protocol.createProxyToRemoteChannel(channel); this._remoteChannels.set(channel, inst); } return this._remoteChannels.get(channel); } async initialize(workerId, loaderConfig, moduleId) { this._protocol.setWorkerId(workerId); if (this._requestHandlerFactory) { // static request handler this._requestHandler = this._requestHandlerFactory(this); return; } if (loaderConfig) { // Remove 'baseUrl', handling it is beyond scope for now if (typeof loaderConfig.baseUrl !== 'undefined') { delete loaderConfig['baseUrl']; } if (typeof loaderConfig.paths !== 'undefined') { if (typeof loaderConfig.paths.vs !== 'undefined') { delete loaderConfig.paths['vs']; } } if (typeof loaderConfig.trustedTypesPolicy !== 'undefined') { // don't use, it has been destroyed during serialize delete loaderConfig['trustedTypesPolicy']; } // Since this is in a web worker, enable catching errors loaderConfig.catchError = true; globalThis.require.config(loaderConfig); } if (isESM) { const url = FileAccess.asBrowserUri(`${moduleId}.js`).toString(true); return import(`${url}`).then((module) => { this._requestHandler = module.create(this); if (!this._requestHandler) { throw new Error(`No RequestHandler!`); } }); } return new Promise((resolve, reject) => { // Use the global require to be sure to get the global config // ESM-comment-begin // const req = (globalThis.require || require); // ESM-comment-end // ESM-uncomment-begin const req = globalThis.require; // ESM-uncomment-end req([moduleId], (module) => { this._requestHandler = module.create(this); if (!this._requestHandler) { reject(new Error(`No RequestHandler!`)); return; } resolve(); }, reject); }); } } /** * Defines the worker entry point. Must be exported and named `create`. * @skipMangle */ export function create(postMessage) { return new SimpleWorkerServer(postMessage, null); }