From c281c55ef13bb751592844327e14de852ca0bf2c Mon Sep 17 00:00:00 2001 From: stefan-gorules <127550877+stefan-gorules@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:48:08 +0100 Subject: [PATCH] feat: replace v8 with quickjs (#119) * feat: replace v8 with quickjs * fix format; fix pipelines * fix: workflow * fix: remove incorrect target in node windows pipeline --- .github/workflows/node.yaml | 67 +- .github/workflows/python.yaml | 10 +- bindings/nodejs/index.js | 37 +- core/engine/Cargo.toml | 4 +- core/engine/js/big.js | 1024 +++++++++++++++++ core/engine/js/dayjs.js | 1 + .../function/scripts => js}/internals.js | 9 +- core/engine/src/handler/decision.rs | 8 +- core/engine/src/handler/function/js_value.rs | 89 ++ core/engine/src/handler/function/mod.rs | 38 +- core/engine/src/handler/function/runtime.rs | 16 + core/engine/src/handler/function/script.rs | 96 +- .../src/handler/function/scripts/big.js | 7 - .../src/handler/function/scripts/dayjs.js | 1 - core/engine/src/handler/function/vm.rs | 61 - core/engine/src/handler/graph.rs | 46 +- core/engine/tests/engine.rs | 2 +- 17 files changed, 1315 insertions(+), 201 deletions(-) create mode 100644 core/engine/js/big.js create mode 100644 core/engine/js/dayjs.js rename core/engine/{src/handler/function/scripts => js}/internals.js (69%) create mode 100644 core/engine/src/handler/function/js_value.rs create mode 100644 core/engine/src/handler/function/runtime.rs delete mode 100644 core/engine/src/handler/function/scripts/big.js delete mode 100644 core/engine/src/handler/function/scripts/dayjs.js delete mode 100644 core/engine/src/handler/function/vm.rs diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index d69b0602..725be2ac 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -33,30 +33,18 @@ jobs: fail-fast: false matrix: settings: + - host: windows-latest + target: 'x86_64-pc-windows-msvc' + build: | + yarn lerna exec "yarn build --target x86_64-pc-windows-msvc" --concurrency 1 --stream --no-prefix + - host: macos-latest target: 'x86_64-apple-darwin' build: | - yarn build && + rustup target add x86_64-apple-darwin + yarn lerna exec "yarn build --target x86_64-apple-darwin" --concurrency 1 --stream --no-prefix strip -x ./*.node - - host: windows-latest - build: | - yarn build - target: 'x86_64-pc-windows-msvc' - - host: ubuntu-latest - target: 'x86_64-unknown-linux-gnu' - docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian - build: >- - set -e && - rustup target add x86_64-unknown-linux-gnu && - cd bindings/nodejs && - yarn lerna exec "yarn build --target x86_64-unknown-linux-gnu" --concurrency 1 --stream --no-prefix && - strip ./*.node -# - host: ubuntu-latest -# target: 'x86_64-unknown-linux-musl' -# docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine -# build: | -# cd bindings/nodejs && -# yarn build + - host: macos-latest target: 'aarch64-apple-darwin' build: | @@ -67,22 +55,41 @@ jobs: export CFLAGS="-isysroot $SYSROOT -isystem $SYSROOT"; yarn lerna exec "yarn build --target aarch64-apple-darwin" --concurrency 1 --stream --no-prefix strip -x ./*.node + - host: ubuntu-latest - target: aarch64-unknown-linux-gnu + target: 'x86_64-unknown-linux-gnu' + docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian + build: | + set -e + sudo apt-get update + sudo apt-get install -y patch + cd bindings/nodejs + rustup target add x86_64-unknown-linux-gnu + yarn lerna exec "yarn build --target x86_64-unknown-linux-gnu" --concurrency 1 --stream --no-prefix + strip ./*.node + + - host: ubuntu-latest + target: 'aarch64-unknown-linux-gnu' docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - build: >- - rustup target add aarch64-unknown-linux-gnu && - cd bindings/nodejs && - yarn lerna exec "yarn build --target aarch64-unknown-linux-gnu" --concurrency 1 --stream --no-prefix && + build: | + set -e + sudo apt-get update + sudo apt-get install -y patch + cd bindings/nodejs + rustup target add aarch64-unknown-linux-gnu + yarn lerna exec "yarn build --target aarch64-unknown-linux-gnu" --concurrency 1 --stream --no-prefix llvm-strip ./*.node + +# - host: ubuntu-latest +# target: 'x86_64-unknown-linux-musl' +# build: | +# cd bindings/nodejs && +# yarn build # - host: ubuntu-latest # target: 'aarch64-unknown-linux-musl' -# downloadTarget: 'aarch64-unknown-linux-musl' -# docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine # build: >- # set -e && # rustup target add aarch64-unknown-linux-musl && -# cd bindings/nodejs && # yarn lerna exec "yarn build --target aarch64-unknown-linux-musl" --concurrency 1 --stream --no-prefix name: stable - ${{ matrix.settings.target }} - node@18 @@ -163,7 +170,7 @@ jobs: target: 'x86_64-apple-darwin' - host: windows-latest target: 'x86_64-pc-windows-msvc' - node: ['14', '16', '18'] + node: ['16', '18', '20'] runs-on: ${{ matrix.settings.host }} steps: @@ -207,7 +214,7 @@ jobs: strategy: fail-fast: false matrix: - node: ['14', '16', '18'] + node: ['16', '18', '20'] runs-on: ubuntu-latest steps: diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index 1f23b644..c69492c6 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -38,13 +38,17 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.10' + - name: Build wheels - uses: PyO3/maturin-action@v1 + uses: stefan-gorules/maturin-action@main with: target: ${{ matrix.target }} args: --release --out dist --find-interpreter -m bindings/python/Cargo.toml - sccache: 'true' - manylinux: auto + manylinux: 2_28 + before-script-linux: | + apt update + apt install -y patch + - name: Upload wheels uses: actions/upload-artifact@v3 with: diff --git a/bindings/nodejs/index.js b/bindings/nodejs/index.js index 7200507d..08c34739 100644 --- a/bindings/nodejs/index.js +++ b/bindings/nodejs/index.js @@ -1,3 +1,9 @@ +/* tslint:disable */ +/* eslint-disable */ +/* prettier-ignore */ + +/* auto-generated by NAPI-RS */ + const { existsSync, readFileSync } = require('fs') const { join } = require('path') @@ -11,7 +17,7 @@ function isMusl() { // For Node 10 if (!process.report || typeof process.report.getReport !== 'function') { try { - const lddPath = require('child_process').execSync('which ldd').toString().trim(); + const lddPath = require('child_process').execSync('which ldd').toString().trim() return readFileSync(lddPath, 'utf8').includes('musl') } catch (e) { return true @@ -231,6 +237,35 @@ switch (platform) { loadError = e } break + case 'riscv64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'zen-engine.linux-riscv64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./zen-engine.linux-riscv64-musl.node') + } else { + nativeBinding = require('@gorules/zen-engine-linux-riscv64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'zen-engine.linux-riscv64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./zen-engine.linux-riscv64-gnu.node') + } else { + nativeBinding = require('@gorules/zen-engine-linux-riscv64-gnu') + } + } catch (e) { + loadError = e + } + } + break default: throw new Error(`Unsupported architecture on Linux: ${arch}`) } diff --git a/core/engine/Cargo.toml b/core/engine/Cargo.toml index cf1d7e91..60927c2e 100644 --- a/core/engine/Cargo.toml +++ b/core/engine/Cargo.toml @@ -19,11 +19,11 @@ bincode = { workspace = true, optional = true } petgraph = { workspace = true } serde_json = { workspace = true, features = ["arbitrary_precision"] } serde = { workspace = true, features = ["derive"] } -serde_v8 = { version = "0.162" } once_cell = { workspace = true } fixedbitset = "0.4.2" futures = { workspace = true } -v8 = { version = "0.83.1" } +rquickjs = { version = "0.4.3", features = ["macro", "loader", "rust-alloc"] } +itertools = "0.12.1" zen-expression = { path = "../expression", version = "0.17.0" } [dev-dependencies] diff --git a/core/engine/js/big.js b/core/engine/js/big.js new file mode 100644 index 00000000..e1d1150b --- /dev/null +++ b/core/engine/js/big.js @@ -0,0 +1,1024 @@ +/* + * big.js v6.2.1 + * A small, fast, easy-to-use library for arbitrary-precision decimal arithmetic. + * Copyright (c) 2022 Michael Mclaughlin + * https://github.com/MikeMcl/big.js/LICENCE.md + */ + + +/************************************** EDITABLE DEFAULTS *****************************************/ + + +// The default values below must be integers within the stated ranges. + +/* + * The maximum number of decimal places (DP) of the results of operations involving division: + * div and sqrt, and pow with negative exponents. + */ +var DP = 20, // 0 to MAX_DP + + /* + * The rounding mode (RM) used when rounding to the above decimal places. + * + * 0 Towards zero (i.e. truncate, no rounding). (ROUND_DOWN) + * 1 To nearest neighbour. If equidistant, round up. (ROUND_HALF_UP) + * 2 To nearest neighbour. If equidistant, to even. (ROUND_HALF_EVEN) + * 3 Away from zero. (ROUND_UP) + */ + RM = 1, // 0, 1, 2 or 3 + + // The maximum value of DP and Big.DP. + MAX_DP = 1E6, // 0 to 1000000 + + // The maximum magnitude of the exponent argument to the pow method. + MAX_POWER = 1E6, // 1 to 1000000 + + /* + * The negative exponent (NE) at and beneath which toString returns exponential notation. + * (JavaScript numbers: -7) + * -1000000 is the minimum recommended exponent value of a Big. + */ + NE = -7, // 0 to -1000000 + + /* + * The positive exponent (PE) at and above which toString returns exponential notation. + * (JavaScript numbers: 21) + * 1000000 is the maximum recommended exponent value of a Big, but this limit is not enforced. + */ + PE = 21, // 0 to 1000000 + + /* + * When true, an error will be thrown if a primitive number is passed to the Big constructor, + * or if valueOf is called, or if toNumber is called on a Big which cannot be converted to a + * primitive number without a loss of precision. + */ + STRICT = false, // true or false + + + /**************************************************************************************************/ + + + // Error messages. + NAME = '[big.js] ', + INVALID = NAME + 'Invalid ', + INVALID_DP = INVALID + 'decimal places', + INVALID_RM = INVALID + 'rounding mode', + DIV_BY_ZERO = NAME + 'Division by zero', + + // The shared prototype object. + P = {}, + UNDEFINED = void 0, + NUMERIC = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i; + + +/* + * Create and return a Big constructor. + */ +function _Big_() { + + /* + * The Big constructor and exported function. + * Create and return a new instance of a Big number object. + * + * n {number|string|Big} A numeric value. + */ + function Big(n) { + var x = this; + + // Enable constructor usage without new. + if (!(x instanceof Big)) return n === UNDEFINED ? _Big_() : new Big(n); + + // Duplicate. + if (n instanceof Big) { + x.s = n.s; + x.e = n.e; + x.c = n.c.slice(); + } else { + if (typeof n !== 'string') { + if (Big.strict === true && typeof n !== 'bigint') { + throw TypeError(INVALID + 'value'); + } + + // Minus zero? + n = n === 0 && 1 / n < 0 ? '-0' : String(n); + } + + parse(x, n); + } + + // Retain a reference to this Big constructor. + // Shadow Big.prototype.constructor which points to Object. + x.constructor = Big; + } + + Big.prototype = P; + Big.DP = DP; + Big.RM = RM; + Big.NE = NE; + Big.PE = PE; + Big.strict = STRICT; + Big.roundDown = 0; + Big.roundHalfUp = 1; + Big.roundHalfEven = 2; + Big.roundUp = 3; + + return Big; +} + + +/* + * Parse the number or string value passed to a Big constructor. + * + * x {Big} A Big number instance. + * n {number|string} A numeric value. + */ +function parse(x, n) { + var e, i, nl; + + if (!NUMERIC.test(n)) { + throw Error(INVALID + 'number'); + } + + // Determine sign. + x.s = n.charAt(0) == '-' ? (n = n.slice(1), -1) : 1; + + // Decimal point? + if ((e = n.indexOf('.')) > -1) n = n.replace('.', ''); + + // Exponential form? + if ((i = n.search(/e/i)) > 0) { + + // Determine exponent. + if (e < 0) e = i; + e += +n.slice(i + 1); + n = n.substring(0, i); + } else if (e < 0) { + + // Integer. + e = n.length; + } + + nl = n.length; + + // Determine leading zeros. + for (i = 0; i < nl && n.charAt(i) == '0';) ++i; + + if (i == nl) { + + // Zero. + x.c = [x.e = 0]; + } else { + + // Determine trailing zeros. + for (; nl > 0 && n.charAt(--nl) == '0';); + x.e = e - i - 1; + x.c = []; + + // Convert string to array of digits without leading/trailing zeros. + for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++); + } + + return x; +} + + +/* + * Round Big x to a maximum of sd significant digits using rounding mode rm. + * + * x {Big} The Big to round. + * sd {number} Significant digits: integer, 0 to MAX_DP inclusive. + * rm {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + * [more] {boolean} Whether the result of division was truncated. + */ +function round(x, sd, rm, more) { + var xc = x.c; + + if (rm === UNDEFINED) rm = x.constructor.RM; + if (rm !== 0 && rm !== 1 && rm !== 2 && rm !== 3) { + throw Error(INVALID_RM); + } + + if (sd < 1) { + more = + rm === 3 && (more || !!xc[0]) || sd === 0 && ( + rm === 1 && xc[0] >= 5 || + rm === 2 && (xc[0] > 5 || xc[0] === 5 && (more || xc[1] !== UNDEFINED)) + ); + + xc.length = 1; + + if (more) { + + // 1, 0.1, 0.01, 0.001, 0.0001 etc. + x.e = x.e - sd + 1; + xc[0] = 1; + } else { + + // Zero. + xc[0] = x.e = 0; + } + } else if (sd < xc.length) { + + // xc[sd] is the digit after the digit that may be rounded up. + more = + rm === 1 && xc[sd] >= 5 || + rm === 2 && (xc[sd] > 5 || xc[sd] === 5 && + (more || xc[sd + 1] !== UNDEFINED || xc[sd - 1] & 1)) || + rm === 3 && (more || !!xc[0]); + + // Remove any digits after the required precision. + xc.length = sd; + + // Round up? + if (more) { + + // Rounding up may mean the previous digit has to be rounded up. + for (; ++xc[--sd] > 9;) { + xc[sd] = 0; + if (sd === 0) { + ++x.e; + xc.unshift(1); + break; + } + } + } + + // Remove trailing zeros. + for (sd = xc.length; !xc[--sd];) xc.pop(); + } + + return x; +} + + +/* + * Return a string representing the value of Big x in normal or exponential notation. + * Handles P.toExponential, P.toFixed, P.toJSON, P.toPrecision, P.toString and P.valueOf. + */ +function stringify(x, doExponential, isNonzero) { + var e = x.e, + s = x.c.join(''), + n = s.length; + + // Exponential notation? + if (doExponential) { + s = s.charAt(0) + (n > 1 ? '.' + s.slice(1) : '') + (e < 0 ? 'e' : 'e+') + e; + + // Normal notation. + } else if (e < 0) { + for (; ++e;) s = '0' + s; + s = '0.' + s; + } else if (e > 0) { + if (++e > n) { + for (e -= n; e--;) s += '0'; + } else if (e < n) { + s = s.slice(0, e) + '.' + s.slice(e); + } + } else if (n > 1) { + s = s.charAt(0) + '.' + s.slice(1); + } + + return x.s < 0 && isNonzero ? '-' + s : s; +} + + +// Prototype/instance methods + + +/* + * Return a new Big whose value is the absolute value of this Big. + */ +P.abs = function () { + var x = new this.constructor(this); + x.s = 1; + return x; +}; + + +/* + * Return 1 if the value of this Big is greater than the value of Big y, + * -1 if the value of this Big is less than the value of Big y, or + * 0 if they have the same value. + */ +P.cmp = function (y) { + var isneg, + x = this, + xc = x.c, + yc = (y = new x.constructor(y)).c, + i = x.s, + j = y.s, + k = x.e, + l = y.e; + + // Either zero? + if (!xc[0] || !yc[0]) return !xc[0] ? !yc[0] ? 0 : -j : i; + + // Signs differ? + if (i != j) return i; + + isneg = i < 0; + + // Compare exponents. + if (k != l) return k > l ^ isneg ? 1 : -1; + + j = (k = xc.length) < (l = yc.length) ? k : l; + + // Compare digit by digit. + for (i = -1; ++i < j;) { + if (xc[i] != yc[i]) return xc[i] > yc[i] ^ isneg ? 1 : -1; + } + + // Compare lengths. + return k == l ? 0 : k > l ^ isneg ? 1 : -1; +}; + + +/* + * Return a new Big whose value is the value of this Big divided by the value of Big y, rounded, + * if necessary, to a maximum of Big.DP decimal places using rounding mode Big.RM. + */ +P.div = function (y) { + var x = this, + Big = x.constructor, + a = x.c, // dividend + b = (y = new Big(y)).c, // divisor + k = x.s == y.s ? 1 : -1, + dp = Big.DP; + + if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { + throw Error(INVALID_DP); + } + + // Divisor is zero? + if (!b[0]) { + throw Error(DIV_BY_ZERO); + } + + // Dividend is 0? Return +-0. + if (!a[0]) { + y.s = k; + y.c = [y.e = 0]; + return y; + } + + var bl, bt, n, cmp, ri, + bz = b.slice(), + ai = bl = b.length, + al = a.length, + r = a.slice(0, bl), // remainder + rl = r.length, + q = y, // quotient + qc = q.c = [], + qi = 0, + p = dp + (q.e = x.e - y.e) + 1; // precision of the result + + q.s = k; + k = p < 0 ? 0 : p; + + // Create version of divisor with leading zero. + bz.unshift(0); + + // Add zeros to make remainder as long as divisor. + for (; rl++ < bl;) r.push(0); + + do { + + // n is how many times the divisor goes into current remainder. + for (n = 0; n < 10; n++) { + + // Compare divisor and remainder. + if (bl != (rl = r.length)) { + cmp = bl > rl ? 1 : -1; + } else { + for (ri = -1, cmp = 0; ++ri < bl;) { + if (b[ri] != r[ri]) { + cmp = b[ri] > r[ri] ? 1 : -1; + break; + } + } + } + + // If divisor < remainder, subtract divisor from remainder. + if (cmp < 0) { + + // Remainder can't be more than 1 digit longer than divisor. + // Equalise lengths using divisor with extra leading zero? + for (bt = rl == bl ? b : bz; rl;) { + if (r[--rl] < bt[rl]) { + ri = rl; + for (; ri && !r[--ri];) r[ri] = 9; + --r[ri]; + r[rl] += 10; + } + r[rl] -= bt[rl]; + } + + for (; !r[0];) r.shift(); + } else { + break; + } + } + + // Add the digit n to the result array. + qc[qi++] = cmp ? n : ++n; + + // Update the remainder. + if (r[0] && cmp) r[rl] = a[ai] || 0; + else r = [a[ai]]; + + } while ((ai++ < al || r[0] !== UNDEFINED) && k--); + + // Leading zero? Do not remove if result is simply zero (qi == 1). + if (!qc[0] && qi != 1) { + + // There can't be more than one zero. + qc.shift(); + q.e--; + p--; + } + + // Round? + if (qi > p) round(q, p, Big.RM, r[0] !== UNDEFINED); + + return q; +}; + + +/* + * Return true if the value of this Big is equal to the value of Big y, otherwise return false. + */ +P.eq = function (y) { + return this.cmp(y) === 0; +}; + + +/* + * Return true if the value of this Big is greater than the value of Big y, otherwise return + * false. + */ +P.gt = function (y) { + return this.cmp(y) > 0; +}; + + +/* + * Return true if the value of this Big is greater than or equal to the value of Big y, otherwise + * return false. + */ +P.gte = function (y) { + return this.cmp(y) > -1; +}; + + +/* + * Return true if the value of this Big is less than the value of Big y, otherwise return false. + */ +P.lt = function (y) { + return this.cmp(y) < 0; +}; + + +/* + * Return true if the value of this Big is less than or equal to the value of Big y, otherwise + * return false. + */ +P.lte = function (y) { + return this.cmp(y) < 1; +}; + + +/* + * Return a new Big whose value is the value of this Big minus the value of Big y. + */ +P.minus = P.sub = function (y) { + var i, j, t, xlty, + x = this, + Big = x.constructor, + a = x.s, + b = (y = new Big(y)).s; + + // Signs differ? + if (a != b) { + y.s = -b; + return x.plus(y); + } + + var xc = x.c.slice(), + xe = x.e, + yc = y.c, + ye = y.e; + + // Either zero? + if (!xc[0] || !yc[0]) { + if (yc[0]) { + y.s = -b; + } else if (xc[0]) { + y = new Big(x); + } else { + y.s = 1; + } + return y; + } + + // Determine which is the bigger number. Prepend zeros to equalise exponents. + if (a = xe - ye) { + + if (xlty = a < 0) { + a = -a; + t = xc; + } else { + ye = xe; + t = yc; + } + + t.reverse(); + for (b = a; b--;) t.push(0); + t.reverse(); + } else { + + // Exponents equal. Check digit by digit. + j = ((xlty = xc.length < yc.length) ? xc : yc).length; + + for (a = b = 0; b < j; b++) { + if (xc[b] != yc[b]) { + xlty = xc[b] < yc[b]; + break; + } + } + } + + // x < y? Point xc to the array of the bigger number. + if (xlty) { + t = xc; + xc = yc; + yc = t; + y.s = -y.s; + } + + /* + * Append zeros to xc if shorter. No need to add zeros to yc if shorter as subtraction only + * needs to start at yc.length. + */ + if ((b = (j = yc.length) - (i = xc.length)) > 0) for (; b--;) xc[i++] = 0; + + // Subtract yc from xc. + for (b = i; j > a;) { + if (xc[--j] < yc[j]) { + for (i = j; i && !xc[--i];) xc[i] = 9; + --xc[i]; + xc[j] += 10; + } + + xc[j] -= yc[j]; + } + + // Remove trailing zeros. + for (; xc[--b] === 0;) xc.pop(); + + // Remove leading zeros and adjust exponent accordingly. + for (; xc[0] === 0;) { + xc.shift(); + --ye; + } + + if (!xc[0]) { + + // n - n = +0 + y.s = 1; + + // Result must be zero. + xc = [ye = 0]; + } + + y.c = xc; + y.e = ye; + + return y; +}; + + +/* + * Return a new Big whose value is the value of this Big modulo the value of Big y. + */ +P.mod = function (y) { + var ygtx, + x = this, + Big = x.constructor, + a = x.s, + b = (y = new Big(y)).s; + + if (!y.c[0]) { + throw Error(DIV_BY_ZERO); + } + + x.s = y.s = 1; + ygtx = y.cmp(x) == 1; + x.s = a; + y.s = b; + + if (ygtx) return new Big(x); + + a = Big.DP; + b = Big.RM; + Big.DP = Big.RM = 0; + x = x.div(y); + Big.DP = a; + Big.RM = b; + + return this.minus(x.times(y)); +}; + + +/* + * Return a new Big whose value is the value of this Big negated. + */ +P.neg = function () { + var x = new this.constructor(this); + x.s = -x.s; + return x; +}; + + +/* + * Return a new Big whose value is the value of this Big plus the value of Big y. + */ +P.plus = P.add = function (y) { + var e, k, t, + x = this, + Big = x.constructor; + + y = new Big(y); + + // Signs differ? + if (x.s != y.s) { + y.s = -y.s; + return x.minus(y); + } + + var xe = x.e, + xc = x.c, + ye = y.e, + yc = y.c; + + // Either zero? + if (!xc[0] || !yc[0]) { + if (!yc[0]) { + if (xc[0]) { + y = new Big(x); + } else { + y.s = x.s; + } + } + return y; + } + + xc = xc.slice(); + + // Prepend zeros to equalise exponents. + // Note: reverse faster than unshifts. + if (e = xe - ye) { + if (e > 0) { + ye = xe; + t = yc; + } else { + e = -e; + t = xc; + } + + t.reverse(); + for (; e--;) t.push(0); + t.reverse(); + } + + // Point xc to the longer array. + if (xc.length - yc.length < 0) { + t = yc; + yc = xc; + xc = t; + } + + e = yc.length; + + // Only start adding at yc.length - 1 as the further digits of xc can be left as they are. + for (k = 0; e; xc[e] %= 10) k = (xc[--e] = xc[e] + yc[e] + k) / 10 | 0; + + // No need to check for zero, as +x + +y != 0 && -x + -y != 0 + + if (k) { + xc.unshift(k); + ++ye; + } + + // Remove trailing zeros. + for (e = xc.length; xc[--e] === 0;) xc.pop(); + + y.c = xc; + y.e = ye; + + return y; +}; + + +/* + * Return a Big whose value is the value of this Big raised to the power n. + * If n is negative, round to a maximum of Big.DP decimal places using rounding + * mode Big.RM. + * + * n {number} Integer, -MAX_POWER to MAX_POWER inclusive. + */ +P.pow = function (n) { + var x = this, + one = new x.constructor('1'), + y = one, + isneg = n < 0; + + if (n !== ~~n || n < -MAX_POWER || n > MAX_POWER) { + throw Error(INVALID + 'exponent'); + } + + if (isneg) n = -n; + + for (;;) { + if (n & 1) y = y.times(x); + n >>= 1; + if (!n) break; + x = x.times(x); + } + + return isneg ? one.div(y) : y; +}; + + +/* + * Return a new Big whose value is the value of this Big rounded to a maximum precision of sd + * significant digits using rounding mode rm, or Big.RM if rm is not specified. + * + * sd {number} Significant digits: integer, 1 to MAX_DP inclusive. + * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + */ +P.prec = function (sd, rm) { + if (sd !== ~~sd || sd < 1 || sd > MAX_DP) { + throw Error(INVALID + 'precision'); + } + return round(new this.constructor(this), sd, rm); +}; + + +/* + * Return a new Big whose value is the value of this Big rounded to a maximum of dp decimal places + * using rounding mode rm, or Big.RM if rm is not specified. + * If dp is negative, round to an integer which is a multiple of 10**-dp. + * If dp is not specified, round to 0 decimal places. + * + * dp? {number} Integer, -MAX_DP to MAX_DP inclusive. + * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + */ +P.round = function (dp, rm) { + if (dp === UNDEFINED) dp = 0; + else if (dp !== ~~dp || dp < -MAX_DP || dp > MAX_DP) { + throw Error(INVALID_DP); + } + return round(new this.constructor(this), dp + this.e + 1, rm); +}; + + +/* + * Return a new Big whose value is the square root of the value of this Big, rounded, if + * necessary, to a maximum of Big.DP decimal places using rounding mode Big.RM. + */ +P.sqrt = function () { + var r, c, t, + x = this, + Big = x.constructor, + s = x.s, + e = x.e, + half = new Big('0.5'); + + // Zero? + if (!x.c[0]) return new Big(x); + + // Negative? + if (s < 0) { + throw Error(NAME + 'No square root'); + } + + // Estimate. + s = Math.sqrt(x + ''); + + // Math.sqrt underflow/overflow? + // Re-estimate: pass x coefficient to Math.sqrt as integer, then adjust the result exponent. + if (s === 0 || s === 1 / 0) { + c = x.c.join(''); + if (!(c.length + e & 1)) c += '0'; + s = Math.sqrt(c); + e = ((e + 1) / 2 | 0) - (e < 0 || e & 1); + r = new Big((s == 1 / 0 ? '5e' : (s = s.toExponential()).slice(0, s.indexOf('e') + 1)) + e); + } else { + r = new Big(s + ''); + } + + e = r.e + (Big.DP += 4); + + // Newton-Raphson iteration. + do { + t = r; + r = half.times(t.plus(x.div(t))); + } while (t.c.slice(0, e).join('') !== r.c.slice(0, e).join('')); + + return round(r, (Big.DP -= 4) + r.e + 1, Big.RM); +}; + + +/* + * Return a new Big whose value is the value of this Big times the value of Big y. + */ +P.times = P.mul = function (y) { + var c, + x = this, + Big = x.constructor, + xc = x.c, + yc = (y = new Big(y)).c, + a = xc.length, + b = yc.length, + i = x.e, + j = y.e; + + // Determine sign of result. + y.s = x.s == y.s ? 1 : -1; + + // Return signed 0 if either 0. + if (!xc[0] || !yc[0]) { + y.c = [y.e = 0]; + return y; + } + + // Initialise exponent of result as x.e + y.e. + y.e = i + j; + + // If array xc has fewer digits than yc, swap xc and yc, and lengths. + if (a < b) { + c = xc; + xc = yc; + yc = c; + j = a; + a = b; + b = j; + } + + // Initialise coefficient array of result with zeros. + for (c = new Array(j = a + b); j--;) c[j] = 0; + + // Multiply. + + // i is initially xc.length. + for (i = b; i--;) { + b = 0; + + // a is yc.length. + for (j = a + i; j > i;) { + + // Current sum of products at this digit position, plus carry. + b = c[j] + yc[i] * xc[j - i - 1] + b; + c[j--] = b % 10; + + // carry + b = b / 10 | 0; + } + + c[j] = b; + } + + // Increment result exponent if there is a final carry, otherwise remove leading zero. + if (b) ++y.e; + else c.shift(); + + // Remove trailing zeros. + for (i = c.length; !c[--i];) c.pop(); + y.c = c; + + return y; +}; + + +/* + * Return a string representing the value of this Big in exponential notation rounded to dp fixed + * decimal places using rounding mode rm, or Big.RM if rm is not specified. + * + * dp? {number} Decimal places: integer, 0 to MAX_DP inclusive. + * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + */ +P.toExponential = function (dp, rm) { + var x = this, + n = x.c[0]; + + if (dp !== UNDEFINED) { + if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { + throw Error(INVALID_DP); + } + x = round(new x.constructor(x), ++dp, rm); + for (; x.c.length < dp;) x.c.push(0); + } + + return stringify(x, true, !!n); +}; + + +/* + * Return a string representing the value of this Big in normal notation rounded to dp fixed + * decimal places using rounding mode rm, or Big.RM if rm is not specified. + * + * dp? {number} Decimal places: integer, 0 to MAX_DP inclusive. + * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + * + * (-0).toFixed(0) is '0', but (-0.1).toFixed(0) is '-0'. + * (-0).toFixed(1) is '0.0', but (-0.01).toFixed(1) is '-0.0'. + */ +P.toFixed = function (dp, rm) { + var x = this, + n = x.c[0]; + + if (dp !== UNDEFINED) { + if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { + throw Error(INVALID_DP); + } + x = round(new x.constructor(x), dp + x.e + 1, rm); + + // x.e may have changed if the value is rounded up. + for (dp = dp + x.e + 1; x.c.length < dp;) x.c.push(0); + } + + return stringify(x, false, !!n); +}; + + +/* + * Return a string representing the value of this Big. + * Return exponential notation if this Big has a positive exponent equal to or greater than + * Big.PE, or a negative exponent equal to or less than Big.NE. + * Omit the sign for negative zero. + */ +P[Symbol.for('nodejs.util.inspect.custom')] = P.toJSON = P.toString = function () { + var x = this, + Big = x.constructor; + return stringify(x, x.e <= Big.NE || x.e >= Big.PE, !!x.c[0]); +}; + + +/* + * Return the value of this Big as a primitve number. + */ +P.toNumber = function () { + var n = Number(stringify(this, true, true)); + if (this.constructor.strict === true && !this.eq(n.toString())) { + throw Error(NAME + 'Imprecise conversion'); + } + return n; +}; + + +/* + * Return a string representing the value of this Big rounded to sd significant digits using + * rounding mode rm, or Big.RM if rm is not specified. + * Use exponential notation if sd is less than the number of digits necessary to represent + * the integer part of the value in normal notation. + * + * sd {number} Significant digits: integer, 1 to MAX_DP inclusive. + * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + */ +P.toPrecision = function (sd, rm) { + var x = this, + Big = x.constructor, + n = x.c[0]; + + if (sd !== UNDEFINED) { + if (sd !== ~~sd || sd < 1 || sd > MAX_DP) { + throw Error(INVALID + 'precision'); + } + x = round(new Big(x), sd, rm); + for (; x.c.length < sd;) x.c.push(0); + } + + return stringify(x, sd <= x.e || x.e <= Big.NE || x.e >= Big.PE, !!n); +}; + + +/* + * Return a string representing the value of this Big. + * Return exponential notation if this Big has a positive exponent equal to or greater than + * Big.PE, or a negative exponent equal to or less than Big.NE. + * Include the sign for negative zero. + */ +P.valueOf = function () { + var x = this, + Big = x.constructor; + if (Big.strict === true) { + throw Error(NAME + 'valueOf disallowed'); + } + return stringify(x, x.e <= Big.NE || x.e >= Big.PE, true); +}; + + +// Export + + +globalThis.Big = _Big_(); \ No newline at end of file diff --git a/core/engine/js/dayjs.js b/core/engine/js/dayjs.js new file mode 100644 index 00000000..721de44f --- /dev/null +++ b/core/engine/js/dayjs.js @@ -0,0 +1 @@ +globalThis.dayjs=function(){"use strict";var t="millisecond",n="second",e="minute",r="hour",i="week",s="month",u="quarter",o="year",a="date",f="Invalid Date",h=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,c=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,d={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var n=["th","st","nd","rd"],e=t%100;return"["+t+(n[(e-20)%10]||n[e]||n[0])+"]"}},l=function(t,n,e){var r=String(t);return!r||r.length>=n?t:""+Array(n+1-r.length).join(e)+t},_="en",$={};$[_]=d;var y=function(t){return t instanceof g},v=function t(n,e,r){var i;if(!n)return _;if("string"==typeof n){var s=n.toLowerCase();$[s]&&(i=s),e&&($[s]=e,i=s);var u=n.split("-");if(!i&&u.length>1)return t(u[0])}else{var o=n.name;$[o]=n,i=o}return!r&&i&&(_=i),i||!r&&_},m=function(t,n){if(y(t))return t.clone();var e="object"==typeof n?n:{};return e.date=t,e.args=arguments,new g(e)},M={s:l,z:function(t){var n=-t.utcOffset(),e=Math.abs(n);return(n<=0?"+":"-")+l(Math.floor(e/60),2,"0")+":"+l(e%60,2,"0")},m:function t(n,e){if(n.date() { try { log.push({ @@ -16,7 +19,7 @@ const console = { } }; -const main = (input) => JSON.stringify({ - output: handler(input, {moment: __GLOBAL__DAYJS, dayjs: __GLOBAL__DAYJS, Big: Big, env: __GLOBAL__ENV}), +globalThis.main = (input) => JSON.stringify({ + output: handler(input, {moment: dayjs, dayjs: dayjs, Big: Big, env: {}}), log, }); diff --git a/core/engine/src/handler/decision.rs b/core/engine/src/handler/decision.rs index cc945d74..6037c263 100644 --- a/core/engine/src/handler/decision.rs +++ b/core/engine/src/handler/decision.rs @@ -4,6 +4,7 @@ use crate::loader::DecisionLoader; use crate::model::DecisionNodeKind; use anyhow::{anyhow, Context}; use async_recursion::async_recursion; +use rquickjs::Runtime; use std::ops::Deref; use std::sync::Arc; @@ -11,14 +12,16 @@ pub struct DecisionHandler { trace: bool, loader: Arc, max_depth: u8, + js_runtime: Option, } impl DecisionHandler { - pub fn new(trace: bool, max_depth: u8, loader: Arc) -> Self { + pub fn new(trace: bool, max_depth: u8, loader: Arc, js_runtime: Option) -> Self { Self { trace, loader, max_depth, + js_runtime, } } @@ -36,7 +39,8 @@ impl DecisionHandler { loader: self.loader.clone(), iteration: request.iteration + 1, trace: self.trace, - })?; + })? + .with_runtime(self.js_runtime.clone()); let result = sub_tree .evaluate(&request.input) diff --git a/core/engine/src/handler/function/js_value.rs b/core/engine/src/handler/function/js_value.rs new file mode 100644 index 00000000..8d7f60c1 --- /dev/null +++ b/core/engine/src/handler/function/js_value.rs @@ -0,0 +1,89 @@ +use itertools::Itertools; +use rquickjs::{Ctx, Error as QError, FromJs, Type, Value as QValue}; +use serde_json::{json, Map, Number, Value}; + +#[derive(Debug)] +pub(crate) struct JsValue(pub(crate) Value); + +impl<'js> FromJs<'js> for JsValue { + fn from_js(ctx: &Ctx<'js>, v: QValue<'js>) -> rquickjs::Result { + let computed_value = match v.type_of() { + Type::Uninitialized | Type::Undefined | Type::Null => Value::Null, + Type::Bool => Value::Bool( + v.as_bool() + .ok_or_else(|| QError::new_from_js("bool", "bool"))?, + ), + Type::Int => Value::Number(Number::from( + v.as_int() + .ok_or_else(|| QError::new_from_js("int", "number"))?, + )), + Type::BigInt => Value::Number(Number::from( + v.as_big_int() + .map(|b| b.clone().to_i64().ok()) + .flatten() + .ok_or_else(|| QError::new_from_js("bigint", "number"))?, + )), + Type::Float => Value::Number( + v.as_float() + .map(|n| Number::from_f64(n)) + .flatten() + .ok_or_else(|| QError::new_from_js("float", "number"))?, + ), + Type::String => Value::String( + v.as_string() + .map(|s| s.to_string().ok()) + .flatten() + .ok_or_else(|| QError::new_from_js("string", "string"))?, + ), + Type::Array => { + let arr = v + .as_array() + .ok_or_else(|| QError::new_from_js("array", "array"))?; + + let js_arr: Vec = arr + .iter::() + .map_ok(|n| JsValue::from_js(ctx, n.clone()).map(|js_val| js_val.0)) + .flatten() + .try_collect() + .map_err(|_| QError::new_from_js("array", "array"))?; + + Value::Array(js_arr) + } + Type::Object => { + let object = v + .as_object() + .ok_or_else(|| QError::new_from_js("object", "object"))?; + + let js_object: Map = object + .props::() + .map_ok(|(key, value)| { + JsValue::from_js(ctx, value.clone()).map(|js_val| (key, js_val.0)) + }) + .flatten() + .try_collect() + .map_err(|_| QError::new_from_js("object", "object"))?; + + Value::Object(js_object) + } + Type::Exception => { + let exception = v + .as_exception() + .ok_or_else(|| QError::new_from_js("exception", "object"))?; + + let message = exception + .message() + .ok_or_else(|| QError::new_from_js("exception", "object"))?; + let description = exception.to_string(); + + json!({ "message": message, "description": description }) + } + Type::Function => json!("[Function]"), + Type::Module => json!("[Module]"), + Type::Constructor => json!("[Constructor]"), + Type::Symbol => json!("[Symbol]"), + Type::Unknown => json!("[Unknown]"), + }; + + Ok(JsValue(computed_value)) + } +} diff --git a/core/engine/src/handler/function/mod.rs b/core/engine/src/handler/function/mod.rs index 4bc270ed..646789a4 100644 --- a/core/engine/src/handler/function/mod.rs +++ b/core/engine/src/handler/function/mod.rs @@ -1,27 +1,27 @@ +use std::time::{Duration, Instant}; + use anyhow::anyhow; -use std::time::Duration; +use rquickjs::Runtime; +use serde_json::json; -use serde_json::{json, Value}; - -use crate::handler::function::script::{EvaluateResponse, Script}; +use crate::handler::function::script::Script; use crate::handler::node::{NodeRequest, NodeResponse, NodeResult}; use crate::model::DecisionNodeKind; +mod js_value; +pub(crate) mod runtime; mod script; -mod vm; - -async fn evaluate(source: &str, args: &Value) -> anyhow::Result { - let mut script = Script::new().with_timeout(Duration::from_millis(50)); - script.call(source, args).await -} pub struct FunctionHandler { trace: bool, + runtime: Runtime, } +static MAX_DURATION: Duration = Duration::from_millis(500); + impl FunctionHandler { - pub fn new(trace: bool) -> Self { - Self { trace } + pub fn new(trace: bool, runtime: Runtime) -> Self { + Self { trace, runtime } } pub async fn handle(&self, request: &NodeRequest<'_>) -> NodeResult { @@ -30,11 +30,19 @@ impl FunctionHandler { _ => Err(anyhow!("Unexpected node type")), }?; - let result = evaluate(content.as_str(), &request.input).await?; + let start = Instant::now(); + let interrupt_handler = Box::new(move || start.elapsed() > MAX_DURATION); + self.runtime.set_interrupt_handler(Some(interrupt_handler)); + let mut script = Script::new(self.runtime.clone()); + let result_response = script.call(content, &request.input).await; + + self.runtime.set_interrupt_handler(None); + + let response = result_response?; Ok(NodeResponse { - output: result.output, - trace_data: self.trace.then(|| json!({ "log": result.log })), + output: response.output, + trace_data: self.trace.then(|| json!({ "log": response.log })), }) } } diff --git a/core/engine/src/handler/function/runtime.rs b/core/engine/src/handler/function/runtime.rs new file mode 100644 index 00000000..63ad68a3 --- /dev/null +++ b/core/engine/src/handler/function/runtime.rs @@ -0,0 +1,16 @@ +use anyhow::Context; +use rquickjs::loader::Bundle; +use rquickjs::{embed, Runtime}; + +static JS_BUNDLE: Bundle = embed! { + "dayjs": "js/dayjs.js", + "big": "js/big.js", + "internals": "js/internals.js" +}; + +pub(crate) fn create_runtime() -> anyhow::Result { + let runtime = Runtime::new().context("Failed to create runtime")?; + runtime.set_loader(JS_BUNDLE, JS_BUNDLE); + + Ok(runtime) +} diff --git a/core/engine/src/handler/function/script.rs b/core/engine/src/handler/function/script.rs index 25712a66..f6f76d25 100644 --- a/core/engine/src/handler/function/script.rs +++ b/core/engine/src/handler/function/script.rs @@ -1,12 +1,11 @@ use std::fmt::Debug; -use std::thread; -use std::time::Duration; -use anyhow::{anyhow, Context}; +use anyhow::Context as _; +use rquickjs::{Context, Ctx, Error as QError, FromJs, Runtime}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::handler::function::vm::BASE_VM; +use crate::handler::function::js_value::JsValue; #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -16,84 +15,47 @@ pub struct EvaluateResponse { } pub struct Script { - isolate: v8::OwnedIsolate, - timeout: Option, + runtime: Runtime, } impl Script { - pub fn new() -> Self { - Self { - isolate: v8::Isolate::new( - v8::CreateParams::default().snapshot_blob(BASE_VM.as_slice()), - ), - timeout: None, - } - } - - pub fn with_timeout(mut self, timeout: Duration) -> Self { - assert!(timeout > Duration::ZERO); - - self.timeout = Some(timeout); - self + pub fn new(runtime: Runtime) -> Self { + Self { runtime } } pub async fn call

(&mut self, source: &str, args: &P) -> anyhow::Result where P: Serialize, { - let handle = self.isolate.thread_safe_handle(); + let runtime = &self.runtime; + let context = Context::full(&runtime).context("Failed to create context")?; let args_str = serde_json::to_string(args).context("Failed to serialize function arguments")?; - let js_code_source = format!("const now = Date.now(); main({});", args_str); + let json_response = context.with(|ctx| -> anyhow::Result { + let _ = ctx + .clone() + .compile("main", "import 'internals'; globalThis.now = Date.now();") + .map_err(|e| map_js_error(&ctx, e))?; - let handle_scope = &mut v8::HandleScope::new(&mut self.isolate); - let context = v8::Context::new(handle_scope); - let scope = &mut v8::ContextScope::new(handle_scope, context); - let tc_scope = &mut v8::TryCatch::new(scope); + let _ = ctx + .globals() + .set("log", Vec::::new()) + .map_err(|e| map_js_error(&ctx, e))?; - let src = v8::String::new(tc_scope, source).context("Failed to compile source code")?; + ctx.eval::(format!("{source};main({args_str})")) + .map_err(|e| map_js_error(&ctx, e)) + })?; - let js_src = v8::String::new(tc_scope, js_code_source.as_str()) - .context("Failed to compile source code")?; - - if let Some(timeout) = self.timeout { - thread::spawn(move || { - thread::sleep(timeout); - handle.terminate_execution(); - }); - } - - let Some(src_script) = v8::Script::compile(tc_scope, src, None) else { - let exception = tc_scope.exception().context("Failed to load script")?; - return Err(anyhow!(exception.to_rust_string_lossy(tc_scope))); - }; - - if let None = src_script.run(tc_scope) { - let exception = tc_scope.exception().unwrap(); - return Err(anyhow!(exception.to_rust_string_lossy(tc_scope))); - } - - let Some(js_script) = v8::Script::compile(tc_scope, js_src, None) else { - let exception = tc_scope.exception().context("Failed to load script")?; - return Err(anyhow!(exception.to_rust_string_lossy(tc_scope))); - }; - - let Some(result) = js_script.run(tc_scope) else { - if tc_scope.has_terminated() { - return Err(anyhow!("Timeout exceeded")); - } - - let exception = tc_scope - .exception() - .context("Failed to run loaded script")?; - return Err(anyhow!(exception.to_rust_string_lossy(tc_scope))); - }; - - let result_string: String = - serde_v8::from_v8(tc_scope, result).context("Failed to parse function result")?; - - serde_json::from_str(result_string.as_str()).context("Failed to parse function result") + serde_json::from_str(json_response.as_str()).context("Failed to parse function result") } } + +fn map_js_error(ctx: &Ctx, e: QError) -> anyhow::Error { + let error = JsValue::from_js(&ctx, ctx.catch()) + .map(|v| v.0) + .unwrap_or(Value::String(e.to_string())); + + anyhow::Error::msg(error.to_string()) +} diff --git a/core/engine/src/handler/function/scripts/big.js b/core/engine/src/handler/function/scripts/big.js deleted file mode 100644 index 301bf0a0..00000000 --- a/core/engine/src/handler/function/scripts/big.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Minified by jsDelivr using Terser v5.14.1. - * Original file: /npm/big.js@6.2.1/big.js - * - * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files - */ -!function(r){"use strict";var e,t=1e6,n=1e6,i="[big.js] ",o=i+"Invalid ",s=o+"decimal places",c=i+"Division by zero",f={},u=void 0,h=/^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;function l(r,e,t,n){var i=r.c;if(t===u&&(t=r.constructor.RM),0!==t&&1!==t&&2!==t&&3!==t)throw Error("[big.js] Invalid rounding mode");if(e<1)n=3===t&&(n||!!i[0])||0===e&&(1===t&&i[0]>=5||2===t&&(i[0]>5||5===i[0]&&(n||i[1]!==u))),i.length=1,n?(r.e=r.e-e+1,i[0]=1):i[0]=r.e=0;else if(e=5||2===t&&(i[e]>5||5===i[e]&&(n||i[e+1]!==u||1&i[e-1]))||3===t&&(n||!!i[0]),i.length=e,n)for(;++i[--e]>9;)if(i[e]=0,0===e){++r.e,i.unshift(1);break}for(e=i.length;!i[--e];)i.pop()}return r}function a(r,e,t){var n=r.e,i=r.c.join(""),o=i.length;if(e)i=i.charAt(0)+(o>1?"."+i.slice(1):"")+(n<0?"e":"e+")+n;else if(n<0){for(;++n;)i="0"+i;i="0."+i}else if(n>0)if(++n>o)for(n-=o;n--;)i+="0";else n1&&(i=i.charAt(0)+"."+i.slice(1));return r.s<0&&t?"-"+i:i}f.abs=function(){var r=new this.constructor(this);return r.s=1,r},f.cmp=function(r){var e,t=this,n=t.c,i=(r=new t.constructor(r)).c,o=t.s,s=r.s,c=t.e,f=r.e;if(!n[0]||!i[0])return n[0]?o:i[0]?-s:0;if(o!=s)return o;if(e=o<0,c!=f)return c>f^e?1:-1;for(s=(c=n.length)<(f=i.length)?c:f,o=-1;++oi[o]^e?1:-1;return c==f?0:c>f^e?1:-1},f.div=function(r){var e=this,n=e.constructor,i=e.c,o=(r=new n(r)).c,f=e.s==r.s?1:-1,h=n.DP;if(h!==~~h||h<0||h>t)throw Error(s);if(!o[0])throw Error(c);if(!i[0])return r.s=f,r.c=[r.e=0],r;var a,p,g,w,v,d=o.slice(),m=a=o.length,E=i.length,b=i.slice(0,a),P=b.length,D=r,M=D.c=[],x=0,y=h+(D.e=e.e-r.e)+1;for(D.s=f,f=y<0?0:y,d.unshift(0);P++P?1:-1;else for(v=-1,w=0;++vb[v]?1:-1;break}if(!(w<0))break;for(p=P==a?o:d;P;){if(b[--P]y&&l(D,y,n.RM,b[0]!==u),D},f.eq=function(r){return 0===this.cmp(r)},f.gt=function(r){return this.cmp(r)>0},f.gte=function(r){return this.cmp(r)>-1},f.lt=function(r){return this.cmp(r)<0},f.lte=function(r){return this.cmp(r)<1},f.minus=f.sub=function(r){var e,t,n,i,o=this,s=o.constructor,c=o.s,f=(r=new s(r)).s;if(c!=f)return r.s=-f,o.plus(r);var u=o.c.slice(),h=o.e,l=r.c,a=r.e;if(!u[0]||!l[0])return l[0]?r.s=-f:u[0]?r=new s(o):r.s=1,r;if(c=h-a){for((i=c<0)?(c=-c,n=u):(a=h,n=l),n.reverse(),f=c;f--;)n.push(0);n.reverse()}else for(t=((i=u.length0)for(;f--;)u[e++]=0;for(f=e;t>c;){if(u[--t]0?(f=s,n=u):(e=-e,n=c),n.reverse();e--;)n.push(0);n.reverse()}for(c.length-u.length<0&&(n=u,u=c,c=n),e=u.length,t=0;e;c[e]%=10)t=(c[--e]=c[e]+u[e]+t)/10|0;for(t&&(c.unshift(t),++f),e=c.length;0===c[--e];)c.pop();return r.c=c,r.e=f,r},f.pow=function(r){var e=this,t=new e.constructor("1"),i=t,s=r<0;if(r!==~~r||r<-1e6||r>n)throw Error(o+"exponent");for(s&&(r=-r);1&r&&(i=i.times(e)),r>>=1;)e=e.times(e);return s?t.div(i):i},f.prec=function(r,e){if(r!==~~r||r<1||r>t)throw Error(o+"precision");return l(new this.constructor(this),r,e)},f.round=function(r,e){if(r===u)r=0;else if(r!==~~r||r<-t||r>t)throw Error(s);return l(new this.constructor(this),r+this.e+1,e)},f.sqrt=function(){var r,e,t,n=this,o=n.constructor,s=n.s,c=n.e,f=new o("0.5");if(!n.c[0])return new o(n);if(s<0)throw Error(i+"No square root");0===(s=Math.sqrt(n+""))||s===1/0?((e=n.c.join("")).length+c&1||(e+="0"),c=((c+1)/2|0)-(c<0||1&c),r=new o(((s=Math.sqrt(e))==1/0?"5e":(s=s.toExponential()).slice(0,s.indexOf("e")+1))+c)):r=new o(s+""),c=r.e+(o.DP+=4);do{t=r,r=f.times(t.plus(n.div(t)))}while(t.c.slice(0,c).join("")!==r.c.slice(0,c).join(""));return l(r,(o.DP-=4)+r.e+1,o.RM)},f.times=f.mul=function(r){var e,t=this,n=t.constructor,i=t.c,o=(r=new n(r)).c,s=i.length,c=o.length,f=t.e,u=r.e;if(r.s=t.s==r.s?1:-1,!i[0]||!o[0])return r.c=[r.e=0],r;for(r.e=f+u,sf;)c=e[u]+o[f]*i[u-f-1]+c,e[u--]=c%10,c=c/10|0;e[u]=c}for(c?++r.e:e.shift(),f=e.length;!e[--f];)e.pop();return r.c=e,r},f.toExponential=function(r,e){var n=this,i=n.c[0];if(r!==u){if(r!==~~r||r<0||r>t)throw Error(s);for(n=l(new n.constructor(n),++r,e);n.c.lengtht)throw Error(s);for(r=r+(n=l(new n.constructor(n),r+n.e+1,e)).e+1;n.c.length=e.PE,!!r.c[0])},f.toNumber=function(){var r=Number(a(this,!0,!0));if(!0===this.constructor.strict&&!this.eq(r.toString()))throw Error(i+"Imprecise conversion");return r},f.toPrecision=function(r,e){var n=this,i=n.constructor,s=n.c[0];if(r!==u){if(r!==~~r||r<1||r>t)throw Error(o+"precision");for(n=l(new i(n),r,e);n.c.length=i.PE,!!s)},f.valueOf=function(){var r=this,e=r.constructor;if(!0===e.strict)throw Error(i+"valueOf disallowed");return a(r,r.e<=e.NE||r.e>=e.PE,!0)},e=function r(){function e(t){var n=this;if(!(n instanceof e))return t===u?r():new e(t);if(t instanceof e)n.s=t.s,n.e=t.e,n.c=t.c.slice();else{if("string"!=typeof t){if(!0===e.strict&&"bigint"!=typeof t)throw TypeError(o+"value");t=0===t&&1/t<0?"-0":String(t)}!function(r,e){var t,n,i;if(!h.test(e))throw Error(o+"number");r.s="-"==e.charAt(0)?(e=e.slice(1),-1):1,(t=e.indexOf("."))>-1&&(e=e.replace(".",""));(n=e.search(/e/i))>0?(t<0&&(t=n),t+=+e.slice(n+1),e=e.substring(0,n)):t<0&&(t=e.length);for(i=e.length,n=0;n0&&"0"==e.charAt(--i););for(r.e=t-n-1,r.c=[],t=0;n<=i;)r.c[t++]=+e.charAt(n++)}}(n,t)}n.constructor=e}return e.prototype=f,e.DP=20,e.RM=1,e.NE=-7,e.PE=21,e.strict=false,e.roundDown=0,e.roundHalfUp=1,e.roundHalfEven=2,e.roundUp=3,e}(),e.default=e.Big=e,"function"==typeof define&&define.amd?define((function(){return e})):"undefined"!=typeof module&&module.exports?module.exports=e:r.Big=e}(this); \ No newline at end of file diff --git a/core/engine/src/handler/function/scripts/dayjs.js b/core/engine/src/handler/function/scripts/dayjs.js deleted file mode 100644 index 501d2a21..00000000 --- a/core/engine/src/handler/function/scripts/dayjs.js +++ /dev/null @@ -1 +0,0 @@ -const __GLOBAL__DAYJS=function(){"use strict";var t="millisecond",n="second",e="minute",r="hour",i="week",s="month",u="quarter",o="year",a="date",f="Invalid Date",h=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,c=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,d={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var n=["th","st","nd","rd"],e=t%100;return"["+t+(n[(e-20)%10]||n[e]||n[0])+"]"}},l=function(t,n,e){var r=String(t);return!r||r.length>=n?t:""+Array(n+1-r.length).join(e)+t},_="en",$={};$[_]=d;var y=function(t){return t instanceof g},v=function t(n,e,r){var i;if(!n)return _;if("string"==typeof n){var s=n.toLowerCase();$[s]&&(i=s),e&&($[s]=e,i=s);var u=n.split("-");if(!i&&u.length>1)return t(u[0])}else{var o=n.name;$[o]=n,i=o}return!r&&i&&(_=i),i||!r&&_},m=function(t,n){if(y(t))return t.clone();var e="object"==typeof n?n:{};return e.date=t,e.args=arguments,new g(e)},M={s:l,z:function(t){var n=-t.utcOffset(),e=Math.abs(n);return(n<=0?"+":"-")+l(Math.floor(e/60),2,"0")+":"+l(e%60,2,"0")},m:function t(n,e){if(n.date()> = Lazy::new(|| { - let platform = v8::new_default_platform(0, false).make_shared(); - v8::V8::initialize_platform(platform); - v8::V8::initialize(); - - let env_src_string = { - let mut env_vars = HashMap::new(); - env::vars().for_each(|(key, value)| { - if let Some(mod_key) = key.strip_prefix(EXPOSED_PREFIX) { - env_vars.insert(mod_key.to_string(), value); - } - }); - - format!( - "const __GLOBAL__ENV = {}", - serde_json::to_string(&env_vars).unwrap() - ) - }; - - let mut isolate = v8::Isolate::snapshot_creator(Default::default()); - - { - let handle_scope = &mut v8::HandleScope::new(&mut isolate); - let context = v8::Context::new(handle_scope); - - let scope = &mut v8::ContextScope::new(handle_scope, context); - - let dayjs_src = v8::String::new(scope, include_str!("scripts/dayjs.js")).unwrap(); - let bigjs_src = v8::String::new(scope, include_str!("scripts/big.js")).unwrap(); - let internal_src = v8::String::new(scope, include_str!("scripts/internals.js")).unwrap(); - let env_src = v8::String::new(scope, env_src_string.as_str()).unwrap(); - - v8::Script::compile(scope, dayjs_src, None) - .unwrap() - .run(scope); - v8::Script::compile(scope, bigjs_src, None) - .unwrap() - .run(scope); - v8::Script::compile(scope, internal_src, None) - .unwrap() - .run(scope); - v8::Script::compile(scope, env_src, None) - .unwrap() - .run(scope); - - scope.set_default_context(context); - } - - isolate - .create_blob(FunctionCodeHandling::Keep) - .unwrap() - .to_vec() -}); diff --git a/core/engine/src/handler/graph.rs b/core/engine/src/handler/graph.rs index c1afec0a..90b13a91 100644 --- a/core/engine/src/handler/graph.rs +++ b/core/engine/src/handler/graph.rs @@ -4,6 +4,7 @@ use std::time::Instant; use anyhow::anyhow; use petgraph::algo::is_cyclic_directed; +use rquickjs::Runtime; use serde::ser::SerializeMap; use serde::{Deserialize, Serialize, Serializer}; use serde_json::Value; @@ -11,6 +12,7 @@ use thiserror::Error; use crate::handler::decision::DecisionHandler; use crate::handler::expression::ExpressionHandler; +use crate::handler::function::runtime::create_runtime; use crate::handler::function::FunctionHandler; use crate::handler::node::NodeRequest; use crate::handler::table::zen::DecisionTableHandler; @@ -25,6 +27,7 @@ pub struct DecisionGraph<'a, L: DecisionLoader> { trace: bool, max_depth: u8, iteration: u8, + runtime: Option, } pub struct DecisionGraphConfig<'a, T: DecisionLoader> { @@ -68,9 +71,26 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { trace: config.trace, loader: config.loader.clone(), max_depth: config.max_depth, + runtime: None, }) } + pub(crate) fn with_runtime(mut self, runtime: Option) -> Self { + self.runtime = runtime; + self + } + + fn get_or_insert_runtime(&mut self) -> anyhow::Result { + if let Some(runtime) = &self.runtime { + return Ok(runtime.clone()); + } + + let runtime = create_runtime()?; + self.runtime.replace(runtime.clone()); + + Ok(runtime) + } + pub fn validate(&self) -> Result<(), DecisionGraphValidationError> { let input_count = self.node_kind_count(DecisionNodeKind::InputNode); if input_count != 1 { @@ -180,7 +200,12 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { }); } DecisionNodeKind::FunctionNode { .. } => { - let res = FunctionHandler::new(self.trace) + let runtime = self.get_or_insert_runtime().map_err(|e| NodeError { + source: e.into(), + node_id: node.id.clone(), + })?; + + let res = FunctionHandler::new(self.trace, runtime) .handle(&node_request) .await .map_err(|e| NodeError { @@ -199,13 +224,18 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { }); } DecisionNodeKind::DecisionNode { .. } => { - let res = DecisionHandler::new(self.trace, self.max_depth, self.loader.clone()) - .handle(&node_request) - .await - .map_err(|e| NodeError { - source: e.into(), - node_id: node.id.to_string(), - })?; + let res = DecisionHandler::new( + self.trace, + self.max_depth, + self.loader.clone(), + self.runtime.clone(), + ) + .handle(&node_request) + .await + .map_err(|e| NodeError { + source: e.into(), + node_id: node.id.to_string(), + })?; walker.set_node_data(nid, res.output.clone()); trace!({ diff --git a/core/engine/tests/engine.rs b/core/engine/tests/engine.rs index ccce6a35..ac84a1f8 100644 --- a/core/engine/tests/engine.rs +++ b/core/engine/tests/engine.rs @@ -101,7 +101,7 @@ async fn engine_errors() { match infinite_fn.unwrap_err().deref() { EvaluationError::NodeError(e) => { assert_eq!(e.node_id, "e0fd96d0-44dc-4f0e-b825-06e56b442d78"); - assert_eq!(e.source.to_string(), "Timeout exceeded"); + assert!(e.source.to_string().contains("interrupted")); } _ => assert!(false, "Wrong error type"), }