@sebastianwessel/quickjs
Version:
A typescript package to execute JavaScript and TypeScript code in a WebAssembly QuickJS sandbox
77 lines (76 loc) • 3.5 kB
JavaScript
import { createTimeInterval } from '../../createTimeInterval.js';
import { rejectAndFlushPendingHostPromises } from '../expose/pendingHostPromises.js';
import { getMaxIntervalAmount } from '../getMaxIntervalAmount.js';
import { getMaxTimeoutAmount } from '../getMaxTimeoutAmount.js';
import { handleEvalError, normalizeInterruptedExecution } from '../handleEvalError.js';
import { handleToNative } from '../handleToNative/handleToNative.js';
import { provideTimingFunctions } from '../provide/provideTimingFunctions.js';
export const createAsyncEvalCodeFunction = (input, scope) => {
const { ctx, sandboxOptions, transpileFile } = input;
return async (code, filename = '/src/index.js', evalOptions) => {
const eventLoopinterval = createTimeInterval(() => ctx.runtime.executePendingJobs(), 0);
const timeoutMs = sandboxOptions.executionTimeout ?? 0;
let timeoutId;
let nativePromise;
let timedOut = false;
const { dispose: disposeTimer } = provideTimingFunctions(ctx, {
maxTimeoutCount: getMaxTimeoutAmount(sandboxOptions),
maxIntervalCount: getMaxIntervalAmount(sandboxOptions),
});
const disposeStep = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
eventLoopinterval?.clear();
disposeTimer();
};
try {
const jsCode = transpileFile(code);
const evalResult = await ctx.evalCodeAsync(jsCode, filename, {
strict: true,
strip: false,
backtraceBarrier: true,
...evalOptions,
type: 'module',
});
const handle = scope.manage(ctx.unwrapResult(evalResult));
const native = handleToNative(ctx, handle, scope);
nativePromise = (async () => {
const res = await native;
return res.default;
})();
const result = await Promise.race([
nativePromise,
new Promise((_resolve, reject) => {
if (timeoutMs > 0) {
timeoutId = setTimeout(() => {
timedOut = true;
const err = new Error('The script execution has exceeded the maximum allowed time limit.');
err.name = 'ExecutionTimeout';
reject(err);
}, timeoutMs);
}
}),
]);
return { ok: true, data: result };
}
catch (err) {
return handleEvalError(normalizeInterruptedExecution(err, timeoutMs));
}
finally {
if (timedOut && nativePromise) {
// Reject unresolved host-injected promises to avoid leaving QuickJS promises
// pending while disposing the runtime after timeout.
await rejectAndFlushPendingHostPromises(ctx);
// Force-fast interrupt handling and give pending guest jobs a short window to settle.
ctx.runtime.setInterruptHandler(() => true);
const cleanupWait = Math.min(Math.max(timeoutMs, 50), 1000);
await Promise.race([
nativePromise.catch(() => undefined),
new Promise(resolve => setTimeout(resolve, cleanupWait)),
]);
}
disposeStep();
}
};
};