151 lines
4.1 KiB
JavaScript
151 lines
4.1 KiB
JavaScript
/**
|
|
* 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);
|
|
};
|
|
}
|