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