diff --git a/packages/server/src/jsRunner/bundles/snippets.ivm.bundle.js b/packages/server/src/jsRunner/bundles/snippets.ivm.bundle.js index fc49a121a4..5adb19eaf7 100644 --- a/packages/server/src/jsRunner/bundles/snippets.ivm.bundle.js +++ b/packages/server/src/jsRunner/bundles/snippets.ivm.bundle.js @@ -1,3 +1,3 @@ -"use strict";var snippets=(()=>{var u=Object.create;var i=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var d=Object.getOwnPropertyNames;var m=Object.getPrototypeOf,x=Object.prototype.hasOwnProperty;var l=(e,n)=>()=>(n||e((n={exports:{}}).exports,n),n.exports),W=(e,n)=>{for(var r in n)i(e,r,{get:n[r],enumerable:!0})},o=(e,n,r,p)=>{if(n&&typeof n=="object"||typeof n=="function")for(let t of d(n))!x.call(e,t)&&t!==r&&i(e,t,{get:()=>n[t],enumerable:!(p=c(n,t))||p.enumerable});return e};var g=(e,n,r)=>(r=e!=null?u(m(e)):{},o(n||!e||!e.__esModule?i(r,"default",{value:e,enumerable:!0}):r,e)),v=e=>o(i({},"__esModule",{value:!0}),e);var a=l((_,f)=>{f.exports.iifeWrapper=e=>`(function(){ +"use strict";var snippets=(()=>{var u=Object.create;var p=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var d=Object.getOwnPropertyNames;var m=Object.getPrototypeOf,x=Object.prototype.hasOwnProperty;var l=(e,n)=>()=>(n||e((n={exports:{}}).exports,n),n.exports),W=(e,n)=>{for(var i in n)p(e,i,{get:n[i],enumerable:!0})},o=(e,n,i,t)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of d(n))!x.call(e,r)&&r!==i&&p(e,r,{get:()=>n[r],enumerable:!(t=c(n,r))||t.enumerable});return e};var g=(e,n,i)=>(i=e!=null?u(m(e)):{},o(n||!e||!e.__esModule?p(i,"default",{value:e,enumerable:!0}):i,e)),v=e=>o(p({},"__esModule",{value:!0}),e);var a=l((P,f)=>{f.exports.iifeWrapper=e=>`(function(){ ${e} -})();`});var y={};W(y,{default:()=>w});var s=g(a()),w=new Proxy({},{get:function(e,n){let r=($("snippets")||[]).find(p=>p.name===n);return[eval][0]((0,s.iifeWrapper)(r.code))}});return v(y);})(); +})();`});var y={};W(y,{default:()=>w});var s=g(a()),w=new Proxy({},{get:function(e,n){let i=(snippetDefinitions||[]).find(t=>t.name===n);return[eval][0]((0,s.iifeWrapper)(i.code))}});return v(y);})(); diff --git a/packages/server/src/jsRunner/bundles/snippets.ts b/packages/server/src/jsRunner/bundles/snippets.ts index 861bacaec5..f473aaf7b4 100644 --- a/packages/server/src/jsRunner/bundles/snippets.ts +++ b/packages/server/src/jsRunner/bundles/snippets.ts @@ -6,14 +6,13 @@ export default new Proxy( {}, { get: function (_, name) { - // Get snippet definitions from global context, get the correct snippet - // then eval the JS. This will error if the snippet doesn't exist, but - // that's intended. + // Snippet definitions are injected to the isolate global scope before + // this bundle is loaded, so we can access it from there. // https://esbuild.github.io/content-types/#direct-eval for info on why // eval is being called this way. // @ts-ignore // eslint-disable-next-line no-undef - const snippet = ($("snippets") || []).find(x => x.name === name) + const snippet = (snippetDefinitions || []).find(x => x.name === name) return [eval][0](iifeWrapper(snippet.code)) }, } diff --git a/packages/server/src/jsRunner/index.ts b/packages/server/src/jsRunner/index.ts index 67aaffae7f..d97fa4cc94 100644 --- a/packages/server/src/jsRunner/index.ts +++ b/packages/server/src/jsRunner/index.ts @@ -8,29 +8,48 @@ import { import { context, logging } from "@budibase/backend-core" import tracer from "dd-trace" import { IsolatedVM } from "./vm" +import { App, DocumentType, Snippet, VM } from "@budibase/types" + +async function getIsolate(ctx: any): Promise { + // Reuse the existing isolate if one exists + if (ctx?.vm) { + return ctx.vm + } + + // Get snippets to build into new isolate, if inside app context + let snippets: Snippet[] | undefined + const db = context.getAppDB() + if (db) { + console.log("READ APP METADATA") + const app = await db.get(DocumentType.APP_METADATA) + snippets = app.snippets + } + + // Build a new isolate + return new IsolatedVM({ + memoryLimit: env.JS_RUNNER_MEMORY_LIMIT, + invocationTimeout: env.JS_PER_INVOCATION_TIMEOUT_MS, + isolateAccumulatedTimeout: env.JS_PER_REQUEST_TIMEOUT_MS, + }) + .withHelpers() + .withSnippets(snippets) +} export function init() { setJSRunner((js: string, ctx: Record) => { - return tracer.trace("runJS", {}, span => { + return tracer.trace("runJS", {}, async span => { try { + // Reuse an existing isolate from context, or make a new one const bbCtx = context.getCurrentContext() - - const vm = bbCtx?.vm - ? bbCtx.vm - : new IsolatedVM({ - memoryLimit: env.JS_RUNNER_MEMORY_LIMIT, - invocationTimeout: env.JS_PER_INVOCATION_TIMEOUT_MS, - isolateAccumulatedTimeout: env.JS_PER_REQUEST_TIMEOUT_MS, - }) - .withHelpers() - .withSnippets() - + const vm = await getIsolate(bbCtx) if (bbCtx) { - // If we have a context, we want to persist it to reuse the isolate bbCtx.vm = vm } + + // Strip helpers (an array) and snippets (a proxy isntance) as these + // will not survive the isolated-vm barrier const { helpers, snippets, ...rest } = ctx - return vm.withContext(rest, () => vm.execute(js)) + return vm.withContext(rest, () => vm!.execute(js)) } catch (error: any) { if (error.message === "Script execution timed out.") { throw new JsErrorTimeout() diff --git a/packages/server/src/jsRunner/vm/isolated-vm.ts b/packages/server/src/jsRunner/vm/isolated-vm.ts index fb45abf5df..f18888895f 100644 --- a/packages/server/src/jsRunner/vm/isolated-vm.ts +++ b/packages/server/src/jsRunner/vm/isolated-vm.ts @@ -6,7 +6,7 @@ import crypto from "crypto" import querystring from "querystring" import { BundleType, loadBundle } from "../bundles" -import { VM } from "@budibase/types" +import { Snippet, VM } from "@budibase/types" import { iifeWrapper } from "@budibase/string-templates" import environment from "../../environment" @@ -98,11 +98,13 @@ export class IsolatedVM implements VM { return this } - withSnippets() { + withSnippets(snippets?: Snippet[]) { const snippetsSource = loadBundle(BundleType.SNIPPETS) - const script = this.isolate.compileScriptSync( - `${snippetsSource};snippets=snippets.default;` - ) + const script = this.isolate.compileScriptSync(` + const snippetDefinitions = ${JSON.stringify(snippets || [])}; + ${snippetsSource}; + snippets = snippets.default; + `) script.runSync(this.vm, { timeout: this.invocationTimeout, release: false }) new Promise(() => { script.release() diff --git a/packages/types/src/documents/app/app.ts b/packages/types/src/documents/app/app.ts index ae4f3fa6da..3b7f481253 100644 --- a/packages/types/src/documents/app/app.ts +++ b/packages/types/src/documents/app/app.ts @@ -1,4 +1,4 @@ -import { User, Document, Plugin } from "../" +import { User, Document, Plugin, Snippet } from "../" import { SocketSession } from "../../sdk" export type AppMetadataErrors = { [key: string]: string[] } @@ -26,6 +26,7 @@ export interface App extends Document { automations?: AutomationSettings usedPlugins?: Plugin[] upgradableVersion?: string + snippets?: Snippet[] } export interface AppInstance { diff --git a/packages/types/src/documents/app/index.ts b/packages/types/src/documents/app/index.ts index b81c9e36ac..a58b708de3 100644 --- a/packages/types/src/documents/app/index.ts +++ b/packages/types/src/documents/app/index.ts @@ -14,3 +14,4 @@ export * from "./backup" export * from "./webhook" export * from "./links" export * from "./component" +export * from "./snippet" diff --git a/packages/types/src/documents/app/snippet.ts b/packages/types/src/documents/app/snippet.ts new file mode 100644 index 0000000000..1b8433b32e --- /dev/null +++ b/packages/types/src/documents/app/snippet.ts @@ -0,0 +1,4 @@ +export interface Snippet { + name: string + code: string +}