monaco-editor
Version:
A browser based code editor
442 lines (441 loc) • 16.9 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { transformErrorForSerialization } from '../errors.js';
import { Emitter } from '../event.js';
import { Disposable } from '../lifecycle.js';
import { getAllMethodNames } from '../objects.js';
import { isWeb } from '../platform.js';
import * as strings from '../strings.js';
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, method, args) {
this.vsWorker = vsWorker;
this.req = req;
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, eventName, arg) {
this.vsWorker = vsWorker;
this.req = req;
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(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, method, args));
});
}
listen(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, 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);
}
_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.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.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, moduleId, host) {
super();
let lazyProxyReject = null;
this._worker = this._register(workerFactory.create('vs/base/common/worker/simpleWorker', (msg) => {
this._protocol.handleMessage(msg);
}, (err) => {
// in Firefox, web workers fail lazily :(
// we will reject the proxy
lazyProxyReject === null || lazyProxyReject === void 0 ? void 0 : lazyProxyReject(err);
}));
this._protocol = new SimpleWorkerProtocol({
sendMessage: (msg, transfer) => {
this._worker.postMessage(msg, transfer);
},
handleMessage: (method, args) => {
if (typeof host[method] !== 'function') {
return Promise.reject(new Error('Missing method ' + method + ' on main thread host.'));
}
try {
return Promise.resolve(host[method].apply(host, args));
}
catch (e) {
return Promise.reject(e);
}
},
handleEvent: (eventName, arg) => {
if (propertyIsDynamicEvent(eventName)) {
const event = host[eventName].call(host, arg);
if (typeof event !== 'function') {
throw new Error(`Missing dynamic event ${eventName} on main thread host.`);
}
return event;
}
if (propertyIsEvent(eventName)) {
const event = host[eventName];
if (typeof event !== 'function') {
throw new Error(`Missing event ${eventName} on main thread host.`);
}
return event;
}
throw new Error(`Malformed event name ${eventName}`);
}
});
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;
}
const hostMethods = getAllMethodNames(host);
// Send initialize message
this._onModuleLoaded = this._protocol.sendMessage(INITIALIZE, [
this._worker.getId(),
JSON.parse(JSON.stringify(loaderConfiguration)),
moduleId,
hostMethods,
]);
// Create proxy to loaded code
const proxyMethodRequest = (method, args) => {
return this._request(method, args);
};
const proxyListen = (eventName, arg) => {
return this._protocol.listen(eventName, arg);
};
this._lazyProxy = new Promise((resolve, reject) => {
lazyProxyReject = reject;
this._onModuleLoaded.then((availableMethods) => {
resolve(createProxyObject(availableMethods, proxyMethodRequest, proxyListen));
}, (e) => {
reject(e);
this._onError('Worker failed to load ' + moduleId, e);
});
});
}
getProxyObject() {
return this._lazyProxy;
}
_request(method, args) {
return new Promise((resolve, reject) => {
this._onModuleLoaded.then(() => {
this._protocol.sendMessage(method, args).then(resolve, reject);
}, reject);
});
}
_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));
}
function createProxyObject(methodNames, invoke, proxyListen) {
const createProxyMethod = (method) => {
return function () {
const args = Array.prototype.slice.call(arguments, 0);
return invoke(method, args);
};
};
const createProxyDynamicEvent = (eventName) => {
return function (arg) {
return proxyListen(eventName, arg);
};
};
const result = {};
for (const methodName of methodNames) {
if (propertyIsDynamicEvent(methodName)) {
result[methodName] = createProxyDynamicEvent(methodName);
continue;
}
if (propertyIsEvent(methodName)) {
result[methodName] = proxyListen(methodName, undefined);
continue;
}
result[methodName] = createProxyMethod(methodName);
}
return result;
}
/**
* Worker side
*/
export class SimpleWorkerServer {
constructor(postMessage, requestHandlerFactory) {
this._requestHandlerFactory = requestHandlerFactory;
this._requestHandler = null;
this._protocol = new SimpleWorkerProtocol({
sendMessage: (msg, transfer) => {
postMessage(msg, transfer);
},
handleMessage: (method, args) => this._handleMessage(method, args),
handleEvent: (eventName, arg) => this._handleEvent(eventName, arg)
});
}
onmessage(msg) {
this._protocol.handleMessage(msg);
}
_handleMessage(method, args) {
if (method === INITIALIZE) {
return this.initialize(args[0], args[1], args[2], args[3]);
}
if (!this._requestHandler || typeof this._requestHandler[method] !== 'function') {
return Promise.reject(new Error('Missing requestHandler or method: ' + method));
}
try {
return Promise.resolve(this._requestHandler[method].apply(this._requestHandler, args));
}
catch (e) {
return Promise.reject(e);
}
}
_handleEvent(eventName, arg) {
if (!this._requestHandler) {
throw new Error(`Missing requestHandler`);
}
if (propertyIsDynamicEvent(eventName)) {
const event = this._requestHandler[eventName].call(this._requestHandler, arg);
if (typeof event !== 'function') {
throw new Error(`Missing dynamic event ${eventName} on request handler.`);
}
return event;
}
if (propertyIsEvent(eventName)) {
const event = this._requestHandler[eventName];
if (typeof event !== 'function') {
throw new Error(`Missing event ${eventName} on request handler.`);
}
return event;
}
throw new Error(`Malformed event name ${eventName}`);
}
initialize(workerId, loaderConfig, moduleId, hostMethods) {
this._protocol.setWorkerId(workerId);
const proxyMethodRequest = (method, args) => {
return this._protocol.sendMessage(method, args);
};
const proxyListen = (eventName, arg) => {
return this._protocol.listen(eventName, arg);
};
const hostProxy = createProxyObject(hostMethods, proxyMethodRequest, proxyListen);
if (this._requestHandlerFactory) {
// static request handler
this._requestHandler = this._requestHandlerFactory(hostProxy);
return Promise.resolve(getAllMethodNames(this._requestHandler));
}
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);
}
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(hostProxy);
if (!this._requestHandler) {
reject(new Error(`No RequestHandler!`));
return;
}
resolve(getAllMethodNames(this._requestHandler));
}, reject);
});
}
}
/**
* Called on the worker side
* @skipMangle
*/
export function create(postMessage) {
return new SimpleWorkerServer(postMessage, null);
}