feat(sandbox) implement sandbox functionality for lazy loaded plugins
This commit is contained in:
150
lib/core/Sandbox.js
Normal file
150
lib/core/Sandbox.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Represents a sandbox for executing code in a controlled environment.
|
||||
*/
|
||||
export default class Sandbox {
|
||||
#callbacks = new Map();
|
||||
#worker = null;
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Executes the provided code and invokes the callback function with the result.
|
||||
* @param {string} code - The code to be executed.
|
||||
* @param {function} cb - The callback function to be invoked with the result.
|
||||
* @returns {string} - The unique identifier for the execution.
|
||||
* @throws {Error} - If the callback is not a function or if the code is not a string.
|
||||
*/
|
||||
execute(code, cb) {
|
||||
if (typeof cb !== 'function')
|
||||
throw new Error('Callback must be a function');
|
||||
if (typeof code !== 'string') throw new Error('Code must be a string');
|
||||
if (code == '') throw new Error('Code cannot be empty');
|
||||
|
||||
const uniqueId = Math.random().toString(36).substring(2, 9);
|
||||
if (!this.#worker) this.#startup(code, cb);
|
||||
this.#callbacks.set(uniqueId, cb);
|
||||
this.#worker.postMessage({ code, id: uniqueId });
|
||||
|
||||
return uniqueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming messages and invokes the corresponding callback.
|
||||
* @param {Event} event - The message event.
|
||||
*/
|
||||
#onMessage(event) {
|
||||
for (let [id, cb] of this.#callbacks) {
|
||||
if (event.data.id === id) {
|
||||
cb(event.data);
|
||||
this.#callbacks.delete(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the execution of a callback with the given ID.
|
||||
* @param {string} id - The ID of the callback to stop.
|
||||
*/
|
||||
stop(id) {
|
||||
this.#callbacks.delete(id);
|
||||
if (this.#callbacks.size === 0) {
|
||||
this.#terminate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the sandbox by initializing the worker and setting up the event listener for messages.
|
||||
*
|
||||
* @param {string} code - The code to be executed in the sandbox.
|
||||
* @param {function} cb - The callback function to be called when a message is received from the worker.
|
||||
*/
|
||||
#startup(code, cb) {
|
||||
if (this.#worker) return; // Already started
|
||||
const workerCode = workerFunction
|
||||
.toString()
|
||||
.replace(/^[^{]*{\s*/, '')
|
||||
.replace(/\s*}[^}]*$/, '');
|
||||
const workerBlob = new Blob(workerCode, {
|
||||
type: 'text/javascript',
|
||||
});
|
||||
const workerUrl = URL.createObjectURL(workerBlob);
|
||||
this.#worker = new Worker(workerUrl);
|
||||
this.#worker.addEventListener('message', this.#onMessage.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates the worker and clears the callbacks.
|
||||
*/
|
||||
#terminate() {
|
||||
this.#worker.terminate();
|
||||
this.#callbacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets inserted into the web worker for executing code.
|
||||
* Executes the code received from the event data in a sandboxed environment.
|
||||
* @function workerFunction
|
||||
*/
|
||||
function workerFunction() {
|
||||
self.onmessage = (event) => {
|
||||
// Get the code from the event data
|
||||
const { id, code } = event.data;
|
||||
|
||||
// Define the sandbox API
|
||||
const sandbox = {
|
||||
console: {
|
||||
log: (...args) => {
|
||||
self.postMessage({ type: 'log', data: args, id });
|
||||
},
|
||||
error: (...args) => {
|
||||
self.postMessage({ type: 'error', data: args, id });
|
||||
},
|
||||
warn: (...args) => {
|
||||
self.postMessage({ type: 'warn', data: args, id });
|
||||
},
|
||||
},
|
||||
setTimeout: (callback, delay) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
callback();
|
||||
sandbox.timeouts.delete(timeoutId);
|
||||
}, delay);
|
||||
sandbox.timeouts.set(timeoutId, true);
|
||||
return timeoutId;
|
||||
},
|
||||
clearTimeout: (timeoutId) => {
|
||||
clearTimeout(timeoutId);
|
||||
sandbox.timeouts.delete(timeoutId);
|
||||
},
|
||||
setInterval: (callback, interval) => {
|
||||
const intervalId = setInterval(callback, interval);
|
||||
sandbox.intervals.set(intervalId, Date.now());
|
||||
return intervalId;
|
||||
},
|
||||
clearInterval: (intervalId) => {
|
||||
clearInterval(intervalId);
|
||||
sandbox.intervals.delete(intervalId);
|
||||
},
|
||||
timeouts: new Map(),
|
||||
intervals: new Map(),
|
||||
// overwrite self.postMessage to always send the public id with the message
|
||||
postMessage: (data) => {
|
||||
self.postMessage({ ...data, id });
|
||||
},
|
||||
};
|
||||
|
||||
Object.freeze(sandbox);
|
||||
|
||||
const sbCode = [
|
||||
'try {',
|
||||
' ' + code,
|
||||
'} catch (error) {',
|
||||
' self.postMessage({ type: "fatal", data: [error] });',
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
// Wrap the code in a function and execute it in the context of the sandbox
|
||||
const func = new Function('sandbox', `with (sandbox) { ${sbCode} }`);
|
||||
func(sandbox);
|
||||
};
|
||||
}
|
||||
71
lib/core/SandboxMessenger.js
Normal file
71
lib/core/SandboxMessenger.js
Normal file
@@ -0,0 +1,71 @@
|
||||
export default class SandboxMessenger {
|
||||
constructor() {
|
||||
this.messageQueue = [];
|
||||
this.responseQueue = [];
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
async sendMessage(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.messageQueue.push({ message, resolve, reject });
|
||||
this.processMessages();
|
||||
});
|
||||
}
|
||||
|
||||
async receiveMessage() {
|
||||
return new Promise((resolve) => {
|
||||
this.responseQueue.push(resolve);
|
||||
this.processMessages();
|
||||
});
|
||||
}
|
||||
|
||||
async processMessages() {
|
||||
if (this.isProcessing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessing = true;
|
||||
|
||||
while (this.messageQueue.length > 0 && this.responseQueue.length > 0) {
|
||||
const { message, resolve, reject } = this.messageQueue.shift();
|
||||
const responseCallback = this.responseQueue.shift();
|
||||
|
||||
try {
|
||||
// Send the message to the Main Thread and wait for the response
|
||||
const response = await this.sendMessageToMainThread(message);
|
||||
|
||||
// Resolve the promise with the response
|
||||
resolve(response);
|
||||
|
||||
// Call the response callback with the response
|
||||
responseCallback(response);
|
||||
} catch (error) {
|
||||
// Reject the promise with the error
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
async sendMessageToMainThread(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Send the message to the Main Thread
|
||||
postMessage(message);
|
||||
|
||||
// Listen for the response from the Main Thread
|
||||
const messageHandler = (event) => {
|
||||
const response = event.data;
|
||||
|
||||
// Remove the event listener
|
||||
removeEventListener('message', messageHandler);
|
||||
|
||||
// Resolve the promise with the response
|
||||
resolve(response);
|
||||
};
|
||||
|
||||
// Add the event listener for the response
|
||||
addEventListener('message', messageHandler, { once: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user