From 7b8089e183d5e8376407d126cd042e2be24ef11f Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Sat, 14 Jun 2025 22:27:31 +1000 Subject: [PATCH] Try to make concurrent requests work --- package.json | 1 + server.js | 309 +++++++++++++++++++++------------------------------ 2 files changed, 128 insertions(+), 182 deletions(-) diff --git a/package.json b/package.json index bad3b1b..f028f14 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "keywords": [], "author": "", "license": "ISC", + "type": "module", "dependencies": { "cheerio": "^1.0.0-rc.12", "express": "^4.18.2", diff --git a/server.js b/server.js index 7ebe206..768252f 100644 --- a/server.js +++ b/server.js @@ -1,201 +1,146 @@ -const express = require("express"); -const bodyParser = require("body-parser"); -const { VM } = require("vm2"); -const { JSDOM } = require("jsdom"); -const cheerio = require("cheerio"); -var FileReader = require("filereader"); +// speedy-functions.js +import express from "express"; +import { VM, VMScript } from "vm2"; +import { JSDOM } from "jsdom"; +import cheerio from "cheerio"; +import FileReader from "filereader"; -const app = express(); -const port = 5000; - -app.use(bodyParser.json({ limit: "50mb" })); - -app.use((req, res, next) => { - res.header("Access-Control-Allow-Origin", "*"); - res.header( - "Access-Control-Allow-Headers", - "Origin, X-Requested-With, Content-Type, Accept" - ); - next(); +// ────── 1. one-time constants ────────────────────────────────────────── +const PORT = +(process.env.PORT ?? 5000); +const TIMEOUT_MS = 5_000; +const HTTP_STATUS_CODES = Object.freeze({ + OK: 200, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + INTERNAL_SERVER_ERROR: 500, + BAD_GATEWAY: 502, + SERVICE_UNAVAILABLE: 503, + GATEWAY_TIMEOUT: 504, }); -const TIMEOUT_MS = 5000; // Set timeout to 5000 milliseconds (5 seconds) +const States = Object.freeze({ + SUCCESS: "SUCCESS", + NOT_A_FUNCTION: "NOT_A_FUNCTION", + SCRIPT_ERROR: "ERROR", + TIMEOUT: "TIMEOUT", +}); +// convenience helpers +const Response = (body = "", headers = {}, status = HTTP_STATUS_CODES.OK) => ({ + body, + status, + headers, +}); +const JsonResponse = (body = {}, headers = {}, status = HTTP_STATUS_CODES.OK) => + Response( + JSON.stringify(body), + { "Content-Type": "application/json", ...headers }, + status + ); +const HtmlResponse = (body = "", headers = {}, status = HTTP_STATUS_CODES.OK) => + Response(body, { "Content-Type": "text/html", ...headers }, status); +const TextResponse = (body = "", headers = {}, status = HTTP_STATUS_CODES.OK) => + Response(body, { "Content-Type": "text/plain", ...headers }, status); + +// ────── 2. one-time imports ──────────────────────────────────────────── +const fetch = (await import("node-fetch")).default; + +// ────── 3. tiny script cache (precompiled) ───────────────────────────── +const scriptCache = new Map(); // code-string -> VMScript +const cachedScript = (code) => { + if (!scriptCache.has(code)) { + scriptCache.set(code, new VMScript(code)); + } + return scriptCache.get(code); +}; + +// ────── 4. shared VM options (lightweight to clone) ──────────────────── +const baseSandbox = { + fetch, + parseHTML: (html) => new JSDOM(html).window.document, + JSDOM, + cheerio, + HTTP_STATUS_CODES, + Response, + JsonResponse, + HtmlResponse, + TextResponse, + FileReader, +}; + +function createVm(logs, requestObject, env, funcName) { + return new VM({ + timeout: TIMEOUT_MS, + sandbox: { + ...baseSandbox, + console: { + log: (...args) => ( + logs.push(args), /* bubble to host */ console.log(...args) + ), + error: (...args) => (logs.push(args), console.error(...args)), + }, + requestObject, + environment: env, + FUNCTION_NAME: funcName, + }, + require: { external: true }, + }); +} + +// ────── 5. evaluator ─────────────────────────────────────────────────── async function executeUserCode( code, requestObject, - environment = {}, - timeout = TIMEOUT_MS, - name + env = {}, + funcName = "userFunc" ) { const logs = []; - - const States = { - SUCCESS: "SUCCESS", - NOT_A_FUNCTION: "NOT_A_FUNCTION", - SCRIPT_ERROR: "ERROR", - TIMEOUT: "TIMEOUT", - }; - - const HTTP_STATUS_CODES = { - OK: 200, - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - INTERNAL_SERVER_ERROR: 500, - BAD_GATEWAY: 502, - SERVICE_UNAVAILABLE: 503, - GATEWAY_TIMEOUT: 504, - }; - - const Response = ( - body = "", - headers = {}, - status = HTTP_STATUS_CODES.OK - ) => ({ - body, - status, - headers, - }); - - const JsonResponse = ( - body = {}, - headers = {}, - status = HTTP_STATUS_CODES.OK - ) => ({ - status, - body: JSON.stringify(body), - headers: { - "Content-Type": "application/json", - ...headers, - }, - }); - - const HtmlResponse = ( - body = "", - headers = {}, - status = HTTP_STATUS_CODES.OK - ) => ({ - status, - body, - headers: { - "Content-Type": "text/html", - ...headers, - }, - }); - - const TextResponse = ( - body = "", - headers = {}, - status = HTTP_STATUS_CODES.OK - ) => ({ - status, - body, - headers: { - "Content-Type": "text/plain", - ...headers, - }, - }); - - const Result = (status, result, environment) => { - console.log(`Status: ${status}`); - //console.log(`Result: ${JSON.stringify(result, null, 2)}`); - //console.log(`Logs: ${JSON.stringify(logs, null, 2)}`); - //console.log(`Environment (post): ${JSON.stringify(environment, null, 2)}`); - //console.log(`\n`); - - return { - status, - result, - logs, - environment, - }; - }; - - //Set Function name as environment variable - const FUNCTION_NAME = name; - - // Dynamically import node-fetch - const fetch = await import("node-fetch").then((module) => module.default); - - const vm = new VM({ - timeout, - sandbox: { - fetch, - parseHTML: async (html) => { - const dom = new JSDOM(html); - return dom.window.document; - }, - console: { - log: (...args) => { - logs.push(args); - console.log(...args); - }, - error: (...args) => { - logs.push(args); - console.error(...args); - }, - }, - requestObject, - environment, - JSDOM, - cheerio, - HTTP_STATUS_CODES, - Response, - JsonResponse, - HtmlResponse, - TextResponse, - FUNCTION_NAME, - FileReader, - }, - require: { - external: true, - }, - }); + const vm = createVm( + logs, + requestObject, + JSON.parse(JSON.stringify(env)), + funcName + ); try { - // If the user code is a function that needs to be invoked - const userFunction = vm.run(code); - if (typeof userFunction === "function") { - console.log(`Function: ${code}`); - let requestObjectString = JSON.stringify(requestObject, null, 2); - console.log(`Request: ${requestObjectString}`); - console.log( - `Environment (pre): ${JSON.stringify(environment, null, 2)})` - ); - - // Call the user function with request object - let result = await userFunction(requestObject); - - return Result(States.SUCCESS, result, environment); - } else { - return Result(States.NOT_A_FUNCTION, null, environment); + const userFn = vm.run(cachedScript(code)); + if (typeof userFn !== "function") { + return { + status: States.NOT_A_FUNCTION, + result: null, + logs, + environment: env, + }; } + + const result = await Promise.resolve(userFn(requestObject)); + return { status: States.SUCCESS, result, logs, environment: env }; } catch (err) { - if (err.message === "Script execution timed out.") { - return Result(States.TIMEOUT, null, environment); - } else { - return Result(States.SCRIPT_ERROR, err.message || err, environment); - } + const status = /timed out/i.test(err.message) + ? States.TIMEOUT + : States.SCRIPT_ERROR; + return { status, result: err.message ?? err, logs, environment: env }; } } -app.post("/execute", async (req, res) => { - const { code, request, environment, name } = req.body; - - const timeout = req.query.timeout || TIMEOUT_MS; - - const result = await executeUserCode( - code, - request, - environment, - timeout, - name - ); - res.send(result); +// ────── 6. API surface ──────────────────────────────────────────────── +const app = express(); +app.use(express.json({ limit: "50mb" })); +app.use((_, res, next) => { + res.set({ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": + "Origin, X-Requested-With, Content-Type, Accept", + }); + next(); }); -app.listen(port, () => { - console.log(`Server listening on port: ${port}`); +app.post("/execute", async ({ body }, res) => { + const { code = "", request = {}, environment = {}, name } = body; + const payload = await executeUserCode(code, request, environment, name); + res.json(payload); }); + +app.listen(PORT, () => console.log(`⚡ server ready on :${PORT}`));