@n8n/expression-runtime 0.28.1 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/bundle/runtime.esm.js +6 -6
- package/dist/bundle/runtime.esm.js.map +3 -3
- package/dist/bundle/runtime.iife.js +6 -6
- package/dist/bundle/runtime.iife.js.map +3 -3
- package/dist/cjs/bridge/isolated-vm-bridge.d.ts +10 -3
- package/dist/cjs/bridge/isolated-vm-bridge.d.ts.map +1 -1
- package/dist/cjs/bridge/isolated-vm-bridge.js +33 -5
- package/dist/cjs/bridge/isolated-vm-bridge.js.map +1 -1
- package/dist/cjs/bridge/quickjs-bridge.d.ts +100 -0
- package/dist/cjs/bridge/quickjs-bridge.d.ts.map +1 -0
- package/dist/cjs/bridge/quickjs-bridge.js +1243 -0
- package/dist/cjs/bridge/quickjs-bridge.js.map +1 -0
- package/dist/cjs/build.tsbuildinfo +1 -1
- package/dist/cjs/evaluator/expression-evaluator.d.ts +13 -0
- package/dist/cjs/evaluator/expression-evaluator.d.ts.map +1 -1
- package/dist/cjs/evaluator/expression-evaluator.js +39 -12
- package/dist/cjs/evaluator/expression-evaluator.js.map +1 -1
- package/dist/cjs/extensions/array-extensions.d.ts.map +1 -1
- package/dist/cjs/extensions/array-extensions.js +6 -0
- package/dist/cjs/extensions/array-extensions.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/runtime/lazy-proxy.d.ts.map +1 -1
- package/dist/cjs/runtime/lazy-proxy.js +74 -19
- package/dist/cjs/runtime/lazy-proxy.js.map +1 -1
- package/dist/cjs/types/bridge.d.ts +9 -1
- package/dist/cjs/types/bridge.d.ts.map +1 -1
- package/dist/cjs/types/bridge.js.map +1 -1
- package/dist/esm/bridge/isolated-vm-bridge.d.ts +10 -3
- package/dist/esm/bridge/isolated-vm-bridge.d.ts.map +1 -1
- package/dist/esm/bridge/isolated-vm-bridge.js +33 -5
- package/dist/esm/bridge/isolated-vm-bridge.js.map +1 -1
- package/dist/esm/bridge/quickjs-bridge.d.ts +100 -0
- package/dist/esm/bridge/quickjs-bridge.d.ts.map +1 -0
- package/dist/esm/bridge/quickjs-bridge.js +1206 -0
- package/dist/esm/bridge/quickjs-bridge.js.map +1 -0
- package/dist/esm/build.tsbuildinfo +1 -1
- package/dist/esm/evaluator/expression-evaluator.d.ts +13 -0
- package/dist/esm/evaluator/expression-evaluator.d.ts.map +1 -1
- package/dist/esm/evaluator/expression-evaluator.js +39 -12
- package/dist/esm/evaluator/expression-evaluator.js.map +1 -1
- package/dist/esm/extensions/array-extensions.d.ts.map +1 -1
- package/dist/esm/extensions/array-extensions.js +6 -0
- package/dist/esm/extensions/array-extensions.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/runtime/lazy-proxy.d.ts.map +1 -1
- package/dist/esm/runtime/lazy-proxy.js +74 -19
- package/dist/esm/runtime/lazy-proxy.js.map +1 -1
- package/dist/esm/types/bridge.d.ts +9 -1
- package/dist/esm/types/bridge.d.ts.map +1 -1
- package/dist/esm/types/bridge.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,1243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.QuickJsBridge = void 0;
|
|
37
|
+
const promises_1 = require("node:fs/promises");
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const types_1 = require("../types");
|
|
40
|
+
const bridge_messages_1 = require("./bridge-messages");
|
|
41
|
+
let _quickjs = null;
|
|
42
|
+
async function getQuickJSModule() {
|
|
43
|
+
if (!_quickjs) {
|
|
44
|
+
_quickjs = await import('quickjs-emscripten');
|
|
45
|
+
}
|
|
46
|
+
return _quickjs;
|
|
47
|
+
}
|
|
48
|
+
const BUNDLE_RELATIVE_PATH = path.join('dist', 'bundle', 'runtime.iife.js');
|
|
49
|
+
// Captured at module load so values rendered into generated code stay stable
|
|
50
|
+
// even if the global is later replaced.
|
|
51
|
+
const safeStringify = JSON.stringify;
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Sentinel helpers
|
|
54
|
+
//
|
|
55
|
+
// QuickJS's vm.dump() loses Date / NaN / Map / Set / Error type identity
|
|
56
|
+
// (Date → ISO string, NaN → null, Map/Set/Error → empty plain object). Inside
|
|
57
|
+
// the QuickJS context we wrap those values as sentinel objects
|
|
58
|
+
// ({ __isDate: true, __isoString: ... } etc.) before they cross the boundary,
|
|
59
|
+
// then unwrap them on the host side. The isolated-vm bridge does NOT need this
|
|
60
|
+
// — structured clone preserves type identity natively.
|
|
61
|
+
// ============================================================================
|
|
62
|
+
function isDateSentinel(value) {
|
|
63
|
+
return (typeof value === 'object' &&
|
|
64
|
+
value !== null &&
|
|
65
|
+
value.__isDate === true &&
|
|
66
|
+
typeof value.__isoString === 'string');
|
|
67
|
+
}
|
|
68
|
+
function isNaNSentinel(value) {
|
|
69
|
+
return (typeof value === 'object' &&
|
|
70
|
+
value !== null &&
|
|
71
|
+
value.__isNaN === true);
|
|
72
|
+
}
|
|
73
|
+
function isErrorValueSentinel(value) {
|
|
74
|
+
return (typeof value === 'object' &&
|
|
75
|
+
value !== null &&
|
|
76
|
+
value.__isErrorValue === true);
|
|
77
|
+
}
|
|
78
|
+
function isMapSentinel(value) {
|
|
79
|
+
return (typeof value === 'object' &&
|
|
80
|
+
value !== null &&
|
|
81
|
+
value.__isMap === true);
|
|
82
|
+
}
|
|
83
|
+
function isSetSentinel(value) {
|
|
84
|
+
return (typeof value === 'object' &&
|
|
85
|
+
value !== null &&
|
|
86
|
+
value.__isSet === true);
|
|
87
|
+
}
|
|
88
|
+
function isErrorSentinel(value) {
|
|
89
|
+
return (typeof value === 'object' &&
|
|
90
|
+
value !== null &&
|
|
91
|
+
value.__isError === true);
|
|
92
|
+
}
|
|
93
|
+
function isEscapedObject(value) {
|
|
94
|
+
return (typeof value === 'object' &&
|
|
95
|
+
value !== null &&
|
|
96
|
+
value.__isEscaped === true);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Recursively reconstruct Date objects, NaN values, Map, and Set from
|
|
100
|
+
* sentinels produced by the QuickJS-side __prepareForTransfer wrapper.
|
|
101
|
+
*/
|
|
102
|
+
function unwrapSentinels(value) {
|
|
103
|
+
if (value === null || value === undefined)
|
|
104
|
+
return value;
|
|
105
|
+
if (typeof value !== 'object')
|
|
106
|
+
return value;
|
|
107
|
+
// Escaped user objects: keys collided with the sentinel markers, so the
|
|
108
|
+
// guest wrapped them (see injectTransferWrapper). Unwrap the values but
|
|
109
|
+
// treat the object itself as plain data.
|
|
110
|
+
if (isEscapedObject(value)) {
|
|
111
|
+
const inner = value.__value;
|
|
112
|
+
const result = {};
|
|
113
|
+
for (const key of Object.keys(inner)) {
|
|
114
|
+
result[key] = unwrapSentinels(inner[key]);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
if (isDateSentinel(value))
|
|
119
|
+
return new Date(value.__isoString);
|
|
120
|
+
if (isNaNSentinel(value))
|
|
121
|
+
return NaN;
|
|
122
|
+
if (isErrorValueSentinel(value)) {
|
|
123
|
+
const ErrorCtor = {
|
|
124
|
+
TypeError,
|
|
125
|
+
SyntaxError,
|
|
126
|
+
EvalError,
|
|
127
|
+
RangeError,
|
|
128
|
+
ReferenceError,
|
|
129
|
+
URIError,
|
|
130
|
+
}[value.__name] ?? Error;
|
|
131
|
+
const err = new ErrorCtor(value.__message);
|
|
132
|
+
if (value.__extra) {
|
|
133
|
+
for (const [k, v] of Object.entries(value.__extra)) {
|
|
134
|
+
err[k] = unwrapSentinels(v);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return err;
|
|
138
|
+
}
|
|
139
|
+
if (isMapSentinel(value)) {
|
|
140
|
+
return new Map(value.__entries.map(([k, v]) => [unwrapSentinels(k), unwrapSentinels(v)]));
|
|
141
|
+
}
|
|
142
|
+
if (isSetSentinel(value)) {
|
|
143
|
+
return new Set(value.__values.map(unwrapSentinels));
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(value))
|
|
146
|
+
return value.map(unwrapSentinels);
|
|
147
|
+
// Pass error sentinels through untouched — execute() detects them after
|
|
148
|
+
// unwrapping and reconstructs the Error on the host.
|
|
149
|
+
if (isErrorSentinel(value))
|
|
150
|
+
return value;
|
|
151
|
+
const result = {};
|
|
152
|
+
for (const key of Object.keys(value)) {
|
|
153
|
+
result[key] = unwrapSentinels(value[key]);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
/** Keys the transfer encoding reserves — keep in sync with the guest-side wrappers in injectTransferWrapper(). */
|
|
158
|
+
const TRANSFER_MARKER_KEYS = new Set([
|
|
159
|
+
'__isDate',
|
|
160
|
+
'__isNaN',
|
|
161
|
+
'__isErrorValue',
|
|
162
|
+
'__isMap',
|
|
163
|
+
'__isSet',
|
|
164
|
+
'__isEscaped',
|
|
165
|
+
]);
|
|
166
|
+
/**
|
|
167
|
+
* Recursively wrap host values that JSON.stringify would flatten (Date, NaN,
|
|
168
|
+
* Map, Set) into the same sentinel format the guest transfer wrapper uses,
|
|
169
|
+
* escaping user objects whose keys collide with the markers. The guest
|
|
170
|
+
* rebuilds real instances via __unwrapFromHost (see injectTransferWrapper),
|
|
171
|
+
* so host-callback results match what isolated-vm's structured clone delivers.
|
|
172
|
+
*/
|
|
173
|
+
function wrapSpecialValuesForGuest(value) {
|
|
174
|
+
if (value === null || value === undefined)
|
|
175
|
+
return value;
|
|
176
|
+
if (value instanceof Date) {
|
|
177
|
+
// Invalid Dates have no ISO string; '' rebuilds an Invalid Date in the guest.
|
|
178
|
+
return { __isDate: true, __isoString: isNaN(value.getTime()) ? '' : value.toISOString() };
|
|
179
|
+
}
|
|
180
|
+
if (typeof value === 'number' && Number.isNaN(value))
|
|
181
|
+
return { __isNaN: true };
|
|
182
|
+
if (typeof value !== 'object')
|
|
183
|
+
return value;
|
|
184
|
+
if (value instanceof Map) {
|
|
185
|
+
return {
|
|
186
|
+
__isMap: true,
|
|
187
|
+
__entries: [...value.entries()].map(([k, v]) => [
|
|
188
|
+
wrapSpecialValuesForGuest(k),
|
|
189
|
+
wrapSpecialValuesForGuest(v),
|
|
190
|
+
]),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (value instanceof Set) {
|
|
194
|
+
return { __isSet: true, __values: [...value.values()].map(wrapSpecialValuesForGuest) };
|
|
195
|
+
}
|
|
196
|
+
if (Array.isArray(value))
|
|
197
|
+
return value.map(wrapSpecialValuesForGuest);
|
|
198
|
+
// Error sentinels are already in transfer shape — leave them intact.
|
|
199
|
+
if (isErrorSentinel(value))
|
|
200
|
+
return value;
|
|
201
|
+
const record = value;
|
|
202
|
+
const result = {};
|
|
203
|
+
let collides = false;
|
|
204
|
+
for (const key of Object.keys(record)) {
|
|
205
|
+
if (TRANSFER_MARKER_KEYS.has(key))
|
|
206
|
+
collides = true;
|
|
207
|
+
result[key] = wrapSpecialValuesForGuest(record[key]);
|
|
208
|
+
}
|
|
209
|
+
return collides ? { __isEscaped: true, __value: result } : result;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Serialize an error into a transferable metadata object.
|
|
213
|
+
*
|
|
214
|
+
* Host-side callbacks (getValueAtPath, etc.) catch errors and return this
|
|
215
|
+
* sentinel instead of letting the error cross the boundary (which strips
|
|
216
|
+
* custom class identity and properties). The in-context proxy detects
|
|
217
|
+
* __isError and throws the sentinel; the host reconstructs a real Error
|
|
218
|
+
* after it round-trips back (see execute() / reconstructError).
|
|
219
|
+
*/
|
|
220
|
+
function serializeError(err) {
|
|
221
|
+
if (err instanceof Error) {
|
|
222
|
+
const extra = Object.fromEntries(Object.entries(err).filter(([key]) => key !== 'name' && key !== 'message' && key !== 'stack'));
|
|
223
|
+
return {
|
|
224
|
+
__isError: true,
|
|
225
|
+
name: err.name,
|
|
226
|
+
message: err.message,
|
|
227
|
+
stack: err.stack,
|
|
228
|
+
extra,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return { __isError: true, name: 'Error', message: String(err), extra: {} };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Read the runtime IIFE bundle by walking up from `__dirname` until
|
|
235
|
+
* `dist/bundle/runtime.iife.js` is found. Walking up (rather than a fixed
|
|
236
|
+
* relative path) works from either compiled output dir — `dist/cjs/bridge/`
|
|
237
|
+
* and `dist/esm/bridge/` sit at different depths from the bundle.
|
|
238
|
+
*/
|
|
239
|
+
async function readRuntimeBundle() {
|
|
240
|
+
let dir = __dirname;
|
|
241
|
+
while (dir !== path.dirname(dir)) {
|
|
242
|
+
try {
|
|
243
|
+
return await (0, promises_1.readFile)(path.join(dir, BUNDLE_RELATIVE_PATH), 'utf-8');
|
|
244
|
+
}
|
|
245
|
+
catch { }
|
|
246
|
+
dir = path.dirname(dir);
|
|
247
|
+
}
|
|
248
|
+
throw new Error(`Could not find runtime bundle (${BUNDLE_RELATIVE_PATH}) in any parent of ${__dirname}`);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Convert a host JavaScript value to a JSON string suitable for round-tripping
|
|
252
|
+
* into QuickJS via evalCode. Handles undefined (not valid JSON) by returning
|
|
253
|
+
* the string "undefined".
|
|
254
|
+
*
|
|
255
|
+
* Only objects/arrays reach this — primitives (including top-level NaN via
|
|
256
|
+
* newNumber) and Date are handled directly by the caller. Everything else
|
|
257
|
+
* (lazy-proxy metadata, Intl outputs, typed-RPC results, error-sentinel
|
|
258
|
+
* extras) is sentinel-wrapped first so nested Date/NaN/Map/Set survive the
|
|
259
|
+
* JSON round-trip and rebuild in the guest via __unwrapFromHost — matching
|
|
260
|
+
* what isolated-vm's structured clone delivers.
|
|
261
|
+
*/
|
|
262
|
+
function hostValueToJson(value) {
|
|
263
|
+
if (value === undefined)
|
|
264
|
+
return 'undefined';
|
|
265
|
+
if (value === null)
|
|
266
|
+
return 'null';
|
|
267
|
+
try {
|
|
268
|
+
return safeStringify(wrapSpecialValuesForGuest(value));
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return 'undefined';
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Navigate data object by path and return metadata or primitive value.
|
|
276
|
+
* Mirrors the IsolatedVmBridge getValueAtPath callback so QuickJS
|
|
277
|
+
* matches isolated-vm semantics, including special-cased $/$item navigation.
|
|
278
|
+
*/
|
|
279
|
+
function getValueAtPath(data, pathArr) {
|
|
280
|
+
let value = data;
|
|
281
|
+
let startIndex = 0;
|
|
282
|
+
const itemFn = data.$item;
|
|
283
|
+
if (pathArr.length >= 2 && pathArr[0] === '$item' && typeof itemFn === 'function') {
|
|
284
|
+
const itemIndex = parseInt(pathArr[1], 10);
|
|
285
|
+
if (!isNaN(itemIndex)) {
|
|
286
|
+
value = itemFn(itemIndex);
|
|
287
|
+
startIndex = 2;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const dollarFn = data.$;
|
|
292
|
+
if (pathArr.length >= 2 && pathArr[0] === '$' && typeof dollarFn === 'function') {
|
|
293
|
+
value = dollarFn(pathArr[1]);
|
|
294
|
+
startIndex = 2;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (let i = startIndex; i < pathArr.length; i++) {
|
|
298
|
+
value = value?.[pathArr[i]];
|
|
299
|
+
if (value === undefined || value === null) {
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Functions must never cross the boundary — resolve them as undefined,
|
|
304
|
+
// matching IsolatedVmBridge (invariant: __tests__/host-fn-shadowing.test.ts).
|
|
305
|
+
if (typeof value === 'function') {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
if (Array.isArray(value)) {
|
|
309
|
+
return {
|
|
310
|
+
__isArray: true,
|
|
311
|
+
__length: value.length,
|
|
312
|
+
__data: null,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
// Dates have no enumerable own keys; pass through instead of
|
|
316
|
+
// marshaling as an empty object.
|
|
317
|
+
if (value instanceof Date) {
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
if (value !== null && typeof value === 'object') {
|
|
321
|
+
return {
|
|
322
|
+
__isObject: true,
|
|
323
|
+
__keys: Object.keys(value),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
function getArrayElement(data, pathArr, index) {
|
|
329
|
+
let arr = data;
|
|
330
|
+
let startIndex = 0;
|
|
331
|
+
const itemFn = data.$item;
|
|
332
|
+
if (pathArr.length >= 2 && pathArr[0] === '$item' && typeof itemFn === 'function') {
|
|
333
|
+
const itemIndex = parseInt(pathArr[1], 10);
|
|
334
|
+
if (!isNaN(itemIndex)) {
|
|
335
|
+
arr = itemFn(itemIndex);
|
|
336
|
+
startIndex = 2;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
const dollarFn = data.$;
|
|
341
|
+
if (pathArr.length >= 2 && pathArr[0] === '$' && typeof dollarFn === 'function') {
|
|
342
|
+
arr = dollarFn(pathArr[1]);
|
|
343
|
+
startIndex = 2;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (let i = startIndex; i < pathArr.length; i++) {
|
|
347
|
+
arr = arr?.[pathArr[i]];
|
|
348
|
+
if (arr === undefined || arr === null) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (!Array.isArray(arr)) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
// Reject non-integer / negative indices so a crafted "index" can't read off
|
|
356
|
+
// the prototype chain. Mirrors IsolatedVmBridge.getArrayElement.
|
|
357
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
const element = arr[index];
|
|
361
|
+
// Functions must never cross the boundary — resolve as undefined, matching
|
|
362
|
+
// getValueAtPath and IsolatedVmBridge (invariant: host-fn-shadowing.test.ts).
|
|
363
|
+
if (typeof element === 'function') {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
// Dates have no enumerable own keys; pass through instead of
|
|
367
|
+
// marshaling as an empty object.
|
|
368
|
+
if (element instanceof Date) {
|
|
369
|
+
return element;
|
|
370
|
+
}
|
|
371
|
+
if (element !== null && typeof element === 'object') {
|
|
372
|
+
if (Array.isArray(element)) {
|
|
373
|
+
return {
|
|
374
|
+
__isArray: true,
|
|
375
|
+
__length: element.length,
|
|
376
|
+
__data: null,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
__isObject: true,
|
|
381
|
+
__keys: Object.keys(element),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return element;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Host-side dispatcher for the typed-RPC `callHost` channel.
|
|
388
|
+
*
|
|
389
|
+
* Mirrors IsolatedVmBridge's dispatcher — see isolated-vm-bridge.ts for the
|
|
390
|
+
* per-message rationale. The two copies are kept in sync at compile time:
|
|
391
|
+
* the `never` check in the default case fails to compile when a new schema
|
|
392
|
+
* lands in bridge-messages.ts without a matching case here.
|
|
393
|
+
*/
|
|
394
|
+
function dispatchHostCall(rawMsg, data) {
|
|
395
|
+
const msg = bridge_messages_1.bridgeMessageSchema.parse(rawMsg);
|
|
396
|
+
switch (msg.type) {
|
|
397
|
+
case 'getNodeFirst':
|
|
398
|
+
return data.$?.(msg.nodeName)?.first?.(msg.branchIndex, msg.runIndex);
|
|
399
|
+
case 'getNodeLast':
|
|
400
|
+
return data.$?.(msg.nodeName)?.last?.(msg.branchIndex, msg.runIndex);
|
|
401
|
+
case 'getNodeAll':
|
|
402
|
+
return data.$?.(msg.nodeName)?.all?.(msg.branchIndex, msg.runIndex);
|
|
403
|
+
case 'getInputFirst':
|
|
404
|
+
return data.$input?.first?.();
|
|
405
|
+
case 'getInputLast':
|
|
406
|
+
return data.$input?.last?.();
|
|
407
|
+
case 'getInputAll':
|
|
408
|
+
return data.$input?.all?.();
|
|
409
|
+
case 'getItems':
|
|
410
|
+
return data.$items?.(msg.nodeName, msg.outputIndex, msg.runIndex);
|
|
411
|
+
case 'fromAi':
|
|
412
|
+
return data.$fromAI?.(msg.name, msg.description, msg.valueType, msg.defaultValue);
|
|
413
|
+
case 'getNodePairedItem':
|
|
414
|
+
return data.$?.(msg.nodeName)?.pairedItem?.(msg.itemIndex);
|
|
415
|
+
case 'getNodeItemMatching':
|
|
416
|
+
return data.$?.(msg.nodeName)?.itemMatching?.(msg.itemIndex);
|
|
417
|
+
case 'getNodeItem':
|
|
418
|
+
// `.item` is a host getter — accessing it invokes the resolver.
|
|
419
|
+
return data.$?.(msg.nodeName)?.item;
|
|
420
|
+
case 'evaluateExpression':
|
|
421
|
+
return data.$evaluateExpression?.(msg.expression, msg.itemIndex);
|
|
422
|
+
case 'getPairedItem':
|
|
423
|
+
return data.$getPairedItem?.(msg.destinationNodeName, msg.incomingSourceData, msg.initialPairedItem);
|
|
424
|
+
default: {
|
|
425
|
+
// Unreachable at runtime — zod rejects unknown `type` values before
|
|
426
|
+
// the switch. The `never` assignment is the compile-time guard.
|
|
427
|
+
const exhaustive = msg;
|
|
428
|
+
void exhaustive;
|
|
429
|
+
throw new Error('Unhandled bridge message');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* QuickJsBridge - Runtime bridge using quickjs-emscripten for expression evaluation.
|
|
435
|
+
*
|
|
436
|
+
* Mirrors the IsolatedVmBridge contract:
|
|
437
|
+
* - Memory limit enforcement
|
|
438
|
+
* - No access to Node.js APIs
|
|
439
|
+
* - Timeout enforcement via interrupt handler
|
|
440
|
+
* - Complete isolation from host process
|
|
441
|
+
*
|
|
442
|
+
* Callbacks are scoped per execute() call (see createCallbackHandles), so
|
|
443
|
+
* concurrent / nested evaluations see fresh data. The runtime treats bridge
|
|
444
|
+
* callbacks as plain functions, so `buildContext(callbacks, timezone)` gets
|
|
445
|
+
* the host callback functions directly.
|
|
446
|
+
*/
|
|
447
|
+
class QuickJsBridge {
|
|
448
|
+
runtime;
|
|
449
|
+
vm;
|
|
450
|
+
initialized = false;
|
|
451
|
+
disposed = false;
|
|
452
|
+
config;
|
|
453
|
+
logger;
|
|
454
|
+
// Long-lived host-callback handles (Intl polyfills) — disposed on dispose()
|
|
455
|
+
intlHandles = [];
|
|
456
|
+
// Wall-clock deadlines of the execute() calls currently in flight, outer to
|
|
457
|
+
// inner. execute() can re-enter (e.g. $evaluateExpression), and the runtime
|
|
458
|
+
// has a single interrupt handler; the handler interrupts once the earliest
|
|
459
|
+
// deadline passes, so a nested call cannot extend the outer call's budget.
|
|
460
|
+
deadlines = [];
|
|
461
|
+
constructor(config = {}) {
|
|
462
|
+
this.config = {
|
|
463
|
+
...types_1.DEFAULT_BRIDGE_CONFIG,
|
|
464
|
+
...config,
|
|
465
|
+
};
|
|
466
|
+
this.logger = this.config.logger;
|
|
467
|
+
}
|
|
468
|
+
async initialize() {
|
|
469
|
+
// Disposal is terminal (matches IsolatedVmBridge, whose isolate cannot be revived).
|
|
470
|
+
if (this.disposed)
|
|
471
|
+
throw new Error('Bridge has been disposed and cannot be reinitialized.');
|
|
472
|
+
if (this.initialized)
|
|
473
|
+
return;
|
|
474
|
+
const { getQuickJS } = await getQuickJSModule();
|
|
475
|
+
const QuickJS = await getQuickJS();
|
|
476
|
+
// Create runtime with memory limit (MB → bytes)
|
|
477
|
+
this.runtime = QuickJS.newRuntime();
|
|
478
|
+
this.runtime.setMemoryLimit(this.config.memoryLimit * 1024 * 1024);
|
|
479
|
+
this.vm = this.runtime.newContext();
|
|
480
|
+
// Install the interrupt handler once. It reads the live deadline stack, so
|
|
481
|
+
// nested execute() calls share one budget: the earliest deadline wins.
|
|
482
|
+
// Math.min() of an empty stack is Infinity, so an idle runtime never fires.
|
|
483
|
+
this.runtime.setInterruptHandler(() => Date.now() > Math.min(...this.deadlines));
|
|
484
|
+
// Set up 'global' / 'globalThis' self-reference
|
|
485
|
+
const globalHandle = this.vm.global;
|
|
486
|
+
this.vm.setProp(globalHandle, 'global', globalHandle);
|
|
487
|
+
this.vm.setProp(globalHandle, 'globalThis', globalHandle);
|
|
488
|
+
// Inject Intl polyfill — QuickJS doesn't include Intl, but Luxon needs it
|
|
489
|
+
this.injectIntlPolyfill();
|
|
490
|
+
// Load runtime bundle (DateTime, extend, SafeObject, proxy system, buildContext)
|
|
491
|
+
await this.loadRuntimeBundle();
|
|
492
|
+
// Wrap __prepareForTransfer to mark Date/NaN/Map/Set/Error so they survive vm.dump()
|
|
493
|
+
this.injectTransferWrapper();
|
|
494
|
+
// Inject E() error handler matching IsolatedVmBridge's E() semantics
|
|
495
|
+
this.injectErrorHandler();
|
|
496
|
+
this.verifyProxySystem();
|
|
497
|
+
this.initialized = true;
|
|
498
|
+
this.logger.info('[QuickJsBridge] Initialized successfully');
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Load the runtime IIFE bundle and verify required globals are present.
|
|
502
|
+
*/
|
|
503
|
+
async loadRuntimeBundle() {
|
|
504
|
+
if (!this.vm)
|
|
505
|
+
throw new Error('Context not initialized');
|
|
506
|
+
const runtimeBundle = await readRuntimeBundle();
|
|
507
|
+
const result = this.vm.evalCode(runtimeBundle);
|
|
508
|
+
if (result.error) {
|
|
509
|
+
const errorStr = this.vm.dump(result.error);
|
|
510
|
+
result.error.dispose();
|
|
511
|
+
throw new Error(`Failed to load runtime bundle: ${String(errorStr)}`);
|
|
512
|
+
}
|
|
513
|
+
result.value.dispose();
|
|
514
|
+
this.logger.info('[QuickJsBridge] Runtime bundle loaded');
|
|
515
|
+
this.evalCodeOrThrow('typeof DateTime !== "undefined"', 'DateTime verification');
|
|
516
|
+
this.evalCodeOrThrow('typeof extend !== "undefined"', 'extend verification');
|
|
517
|
+
this.logger.info('[QuickJsBridge] Vendor libraries verified successfully');
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Inject a polyfill for the Intl API.
|
|
521
|
+
*
|
|
522
|
+
* QuickJS doesn't include Intl, but Luxon (bundled in the runtime) uses
|
|
523
|
+
* Intl.DateTimeFormat, Intl.NumberFormat, Intl.RelativeTimeFormat, and
|
|
524
|
+
* Intl.ListFormat extensively. Rather than shimming all of this in pure JS,
|
|
525
|
+
* we register host callback functions that delegate to Node.js's real Intl
|
|
526
|
+
* implementation, then build lightweight JS wrapper classes that call them.
|
|
527
|
+
*/
|
|
528
|
+
injectIntlPolyfill() {
|
|
529
|
+
if (!this.vm)
|
|
530
|
+
throw new Error('Context not initialized');
|
|
531
|
+
const vm = this.vm;
|
|
532
|
+
const dtfFn = vm.newFunction('__intl_dtf', (...handles) => {
|
|
533
|
+
const args = handles.map((h) => vm.dump(h));
|
|
534
|
+
const op = args[0];
|
|
535
|
+
const locales = args[1];
|
|
536
|
+
const options = args[2];
|
|
537
|
+
try {
|
|
538
|
+
if (op === 'resolvedOptions') {
|
|
539
|
+
const fmt = new Intl.DateTimeFormat(locales, options);
|
|
540
|
+
return this.hostValueToQuickJSHandle(fmt.resolvedOptions());
|
|
541
|
+
}
|
|
542
|
+
if (op === 'format') {
|
|
543
|
+
const fmt = new Intl.DateTimeFormat(locales, options);
|
|
544
|
+
const ts = args[3];
|
|
545
|
+
const date = ts !== undefined ? new Date(ts) : new Date();
|
|
546
|
+
return this.hostValueToQuickJSHandle(fmt.format(date));
|
|
547
|
+
}
|
|
548
|
+
if (op === 'formatToParts') {
|
|
549
|
+
const fmt = new Intl.DateTimeFormat(locales, options);
|
|
550
|
+
const ts = args[3];
|
|
551
|
+
const date = ts !== undefined ? new Date(ts) : new Date();
|
|
552
|
+
return this.hostValueToQuickJSHandle(fmt.formatToParts(date));
|
|
553
|
+
}
|
|
554
|
+
if (op === 'supportedLocalesOf') {
|
|
555
|
+
return this.hostValueToQuickJSHandle(Intl.DateTimeFormat.supportedLocalesOf(locales ?? []));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
return this.hostValueToQuickJSHandle({
|
|
560
|
+
__intlError: true,
|
|
561
|
+
message: err instanceof Error ? err.message : String(err),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return vm.undefined;
|
|
565
|
+
});
|
|
566
|
+
vm.setProp(vm.global, '__intl_dtf', dtfFn);
|
|
567
|
+
this.intlHandles.push(dtfFn);
|
|
568
|
+
const nfFn = vm.newFunction('__intl_nf', (...handles) => {
|
|
569
|
+
const args = handles.map((h) => vm.dump(h));
|
|
570
|
+
const op = args[0];
|
|
571
|
+
const locales = args[1];
|
|
572
|
+
const options = args[2];
|
|
573
|
+
try {
|
|
574
|
+
if (op === 'resolvedOptions') {
|
|
575
|
+
const fmt = new Intl.NumberFormat(locales, options);
|
|
576
|
+
return this.hostValueToQuickJSHandle(fmt.resolvedOptions());
|
|
577
|
+
}
|
|
578
|
+
if (op === 'format') {
|
|
579
|
+
const fmt = new Intl.NumberFormat(locales, options);
|
|
580
|
+
const num = args[3];
|
|
581
|
+
return this.hostValueToQuickJSHandle(fmt.format(num));
|
|
582
|
+
}
|
|
583
|
+
if (op === 'formatToParts') {
|
|
584
|
+
const fmt = new Intl.NumberFormat(locales, options);
|
|
585
|
+
const num = args[3];
|
|
586
|
+
return this.hostValueToQuickJSHandle(fmt.formatToParts(num));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch (err) {
|
|
590
|
+
return this.hostValueToQuickJSHandle({
|
|
591
|
+
__intlError: true,
|
|
592
|
+
message: err instanceof Error ? err.message : String(err),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return vm.undefined;
|
|
596
|
+
});
|
|
597
|
+
vm.setProp(vm.global, '__intl_nf', nfFn);
|
|
598
|
+
this.intlHandles.push(nfFn);
|
|
599
|
+
const rtfFn = vm.newFunction('__intl_rtf', (...handles) => {
|
|
600
|
+
const args = handles.map((h) => vm.dump(h));
|
|
601
|
+
const op = args[0];
|
|
602
|
+
const locales = args[1];
|
|
603
|
+
const options = args[2];
|
|
604
|
+
try {
|
|
605
|
+
if (op === 'resolvedOptions') {
|
|
606
|
+
const fmt = new Intl.RelativeTimeFormat(locales, options);
|
|
607
|
+
return this.hostValueToQuickJSHandle(fmt.resolvedOptions());
|
|
608
|
+
}
|
|
609
|
+
if (op === 'format') {
|
|
610
|
+
const fmt = new Intl.RelativeTimeFormat(locales, options);
|
|
611
|
+
const value = args[3];
|
|
612
|
+
const unit = args[4];
|
|
613
|
+
return this.hostValueToQuickJSHandle(fmt.format(value, unit));
|
|
614
|
+
}
|
|
615
|
+
if (op === 'formatToParts') {
|
|
616
|
+
const fmt = new Intl.RelativeTimeFormat(locales, options);
|
|
617
|
+
const value = args[3];
|
|
618
|
+
const unit = args[4];
|
|
619
|
+
return this.hostValueToQuickJSHandle(fmt.formatToParts(value, unit));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
catch (err) {
|
|
623
|
+
return this.hostValueToQuickJSHandle({
|
|
624
|
+
__intlError: true,
|
|
625
|
+
message: err instanceof Error ? err.message : String(err),
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
return vm.undefined;
|
|
629
|
+
});
|
|
630
|
+
vm.setProp(vm.global, '__intl_rtf', rtfFn);
|
|
631
|
+
this.intlHandles.push(rtfFn);
|
|
632
|
+
const lfFn = vm.newFunction('__intl_lf', (...handles) => {
|
|
633
|
+
const args = handles.map((h) => vm.dump(h));
|
|
634
|
+
const op = args[0];
|
|
635
|
+
const locales = args[1];
|
|
636
|
+
const options = args[2];
|
|
637
|
+
try {
|
|
638
|
+
if (op === 'format') {
|
|
639
|
+
const fmt = new Intl.ListFormat(locales, options);
|
|
640
|
+
const list = args[3];
|
|
641
|
+
return this.hostValueToQuickJSHandle(fmt.format(list));
|
|
642
|
+
}
|
|
643
|
+
if (op === 'formatToParts') {
|
|
644
|
+
const fmt = new Intl.ListFormat(locales, options);
|
|
645
|
+
const list = args[3];
|
|
646
|
+
return this.hostValueToQuickJSHandle(fmt.formatToParts(list));
|
|
647
|
+
}
|
|
648
|
+
if (op === 'resolvedOptions') {
|
|
649
|
+
const fmt = new Intl.ListFormat(locales, options);
|
|
650
|
+
return this.hostValueToQuickJSHandle(fmt.resolvedOptions());
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch (err) {
|
|
654
|
+
return this.hostValueToQuickJSHandle({
|
|
655
|
+
__intlError: true,
|
|
656
|
+
message: err instanceof Error ? err.message : String(err),
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
return vm.undefined;
|
|
660
|
+
});
|
|
661
|
+
vm.setProp(vm.global, '__intl_lf', lfFn);
|
|
662
|
+
this.intlHandles.push(lfFn);
|
|
663
|
+
const shimCode = `
|
|
664
|
+
(function() {
|
|
665
|
+
function checkError(result) {
|
|
666
|
+
if (result && typeof result === 'object' && result.__intlError) {
|
|
667
|
+
throw new Error(result.message);
|
|
668
|
+
}
|
|
669
|
+
return result;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function DateTimeFormat(locales, options) {
|
|
673
|
+
this._locales = locales;
|
|
674
|
+
this._options = options || {};
|
|
675
|
+
}
|
|
676
|
+
DateTimeFormat.prototype.resolvedOptions = function() {
|
|
677
|
+
return checkError(__intl_dtf('resolvedOptions', this._locales, this._options));
|
|
678
|
+
};
|
|
679
|
+
DateTimeFormat.prototype.format = function(date) {
|
|
680
|
+
var ts = date instanceof Date ? date.getTime() : (typeof date === 'number' ? date : Date.now());
|
|
681
|
+
return checkError(__intl_dtf('format', this._locales, this._options, ts));
|
|
682
|
+
};
|
|
683
|
+
DateTimeFormat.prototype.formatToParts = function(date) {
|
|
684
|
+
var ts = date instanceof Date ? date.getTime() : (typeof date === 'number' ? date : Date.now());
|
|
685
|
+
return checkError(__intl_dtf('formatToParts', this._locales, this._options, ts));
|
|
686
|
+
};
|
|
687
|
+
DateTimeFormat.supportedLocalesOf = function(locales) {
|
|
688
|
+
return checkError(__intl_dtf('supportedLocalesOf', locales));
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
function NumberFormat(locales, options) {
|
|
692
|
+
this._locales = locales;
|
|
693
|
+
this._options = options || {};
|
|
694
|
+
}
|
|
695
|
+
NumberFormat.prototype.resolvedOptions = function() {
|
|
696
|
+
return checkError(__intl_nf('resolvedOptions', this._locales, this._options));
|
|
697
|
+
};
|
|
698
|
+
NumberFormat.prototype.format = function(num) {
|
|
699
|
+
return checkError(__intl_nf('format', this._locales, this._options, num));
|
|
700
|
+
};
|
|
701
|
+
NumberFormat.prototype.formatToParts = function(num) {
|
|
702
|
+
return checkError(__intl_nf('formatToParts', this._locales, this._options, num));
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
function RelativeTimeFormat(locales, options) {
|
|
706
|
+
this._locales = locales;
|
|
707
|
+
this._options = options || {};
|
|
708
|
+
}
|
|
709
|
+
RelativeTimeFormat.prototype.resolvedOptions = function() {
|
|
710
|
+
return checkError(__intl_rtf('resolvedOptions', this._locales, this._options));
|
|
711
|
+
};
|
|
712
|
+
RelativeTimeFormat.prototype.format = function(value, unit) {
|
|
713
|
+
return checkError(__intl_rtf('format', this._locales, this._options, value, unit));
|
|
714
|
+
};
|
|
715
|
+
RelativeTimeFormat.prototype.formatToParts = function(value, unit) {
|
|
716
|
+
return checkError(__intl_rtf('formatToParts', this._locales, this._options, value, unit));
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
function ListFormat(locales, options) {
|
|
720
|
+
this._locales = locales;
|
|
721
|
+
this._options = options || {};
|
|
722
|
+
}
|
|
723
|
+
ListFormat.prototype.format = function(list) {
|
|
724
|
+
return checkError(__intl_lf('format', this._locales, this._options, list));
|
|
725
|
+
};
|
|
726
|
+
ListFormat.prototype.formatToParts = function(list) {
|
|
727
|
+
return checkError(__intl_lf('formatToParts', this._locales, this._options, list));
|
|
728
|
+
};
|
|
729
|
+
ListFormat.prototype.resolvedOptions = function() {
|
|
730
|
+
return checkError(__intl_lf('resolvedOptions', this._locales, this._options));
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
globalThis.Intl = {
|
|
734
|
+
DateTimeFormat: DateTimeFormat,
|
|
735
|
+
NumberFormat: NumberFormat,
|
|
736
|
+
RelativeTimeFormat: RelativeTimeFormat,
|
|
737
|
+
ListFormat: ListFormat,
|
|
738
|
+
Locale: function Locale(tag) { this.baseName = tag; this.language = tag.split('-')[0]; }
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
// QuickJS's built-in toLocale* methods are locale-unaware (no ECMA-402) and
|
|
742
|
+
// ignore locales/options. Route them through the polyfill so they format on
|
|
743
|
+
// the host, like the spec routes them through NumberFormat/DateTimeFormat.
|
|
744
|
+
Number.prototype.toLocaleString = function(locales, options) {
|
|
745
|
+
return new NumberFormat(locales, options).format(Number(this));
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
function hasAny(options, keys) {
|
|
749
|
+
if (!options) return false;
|
|
750
|
+
for (var i = 0; i < keys.length; i++) {
|
|
751
|
+
if (options[keys[i]] !== undefined) return true;
|
|
752
|
+
}
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
var DATE_COMPONENTS = ['weekday', 'year', 'month', 'day'];
|
|
756
|
+
var TIME_COMPONENTS = ['dayPeriod', 'hour', 'minute', 'second', 'fractionalSecondDigits'];
|
|
757
|
+
// ToDateTimeOptions (ECMA-402 sec. 12): reject the style option the method
|
|
758
|
+
// doesn't cover (as V8 does), and when options carry none of the method's
|
|
759
|
+
// required components (and no dateStyle/timeStyle), fill in the method's
|
|
760
|
+
// numeric defaults so e.g. toLocaleTimeString() shows a time.
|
|
761
|
+
function toDateTimeOptions(options, requiredKeys, addDate, addTime, rejectedStyle) {
|
|
762
|
+
if (rejectedStyle && options && options[rejectedStyle] !== undefined) {
|
|
763
|
+
throw new TypeError('Invalid option : ' + rejectedStyle);
|
|
764
|
+
}
|
|
765
|
+
var merged = {};
|
|
766
|
+
for (var k in options || {}) merged[k] = options[k];
|
|
767
|
+
if (merged.dateStyle !== undefined || merged.timeStyle !== undefined) return merged;
|
|
768
|
+
if (hasAny(options, requiredKeys)) return merged;
|
|
769
|
+
if (addDate) { merged.year = 'numeric'; merged.month = 'numeric'; merged.day = 'numeric'; }
|
|
770
|
+
if (addTime) { merged.hour = 'numeric'; merged.minute = 'numeric'; merged.second = 'numeric'; }
|
|
771
|
+
return merged;
|
|
772
|
+
}
|
|
773
|
+
var ANY_COMPONENTS = DATE_COMPONENTS.concat(TIME_COMPONENTS);
|
|
774
|
+
Date.prototype.toLocaleString = function(locales, options) {
|
|
775
|
+
return new DateTimeFormat(locales, toDateTimeOptions(options, ANY_COMPONENTS, true, true)).format(this);
|
|
776
|
+
};
|
|
777
|
+
Date.prototype.toLocaleDateString = function(locales, options) {
|
|
778
|
+
return new DateTimeFormat(locales, toDateTimeOptions(options, DATE_COMPONENTS, true, false, 'timeStyle')).format(this);
|
|
779
|
+
};
|
|
780
|
+
Date.prototype.toLocaleTimeString = function(locales, options) {
|
|
781
|
+
return new DateTimeFormat(locales, toDateTimeOptions(options, TIME_COMPONENTS, false, true, 'dateStyle')).format(this);
|
|
782
|
+
};
|
|
783
|
+
})();
|
|
784
|
+
`;
|
|
785
|
+
const result = this.vm.evalCode(shimCode);
|
|
786
|
+
if (result.error) {
|
|
787
|
+
const errStr = this.vm.dump(result.error);
|
|
788
|
+
result.error.dispose();
|
|
789
|
+
throw new Error(`Failed to inject Intl polyfill: ${typeof errStr === 'object' ? JSON.stringify(errStr) : String(errStr)}`);
|
|
790
|
+
}
|
|
791
|
+
result.value.dispose();
|
|
792
|
+
this.logger.debug('[QuickJsBridge] Intl polyfill injected');
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Wrap __prepareForTransfer so values that don't survive vm.dump()
|
|
796
|
+
* (Date, NaN, Map, Set, Error) are converted to sentinel objects.
|
|
797
|
+
* The host post-processes vm.dump() output to reconstruct real instances
|
|
798
|
+
* via unwrapSentinels(). Also injects __unwrapFromHost, the guest-side
|
|
799
|
+
* reverse: it rebuilds instances from the sentinels the host's
|
|
800
|
+
* wrapSpecialValuesForGuest() emits for callback results.
|
|
801
|
+
*/
|
|
802
|
+
injectTransferWrapper() {
|
|
803
|
+
if (!this.vm)
|
|
804
|
+
throw new Error('Context not initialized');
|
|
805
|
+
const wrapperCode = `
|
|
806
|
+
(function() {
|
|
807
|
+
var original = __prepareForTransfer;
|
|
808
|
+
globalThis.__prepareForTransfer = function(value) {
|
|
809
|
+
var prepared = original(value);
|
|
810
|
+
return wrapSpecialValues(prepared);
|
|
811
|
+
};
|
|
812
|
+
function wrapSpecialValues(v) {
|
|
813
|
+
if (v === null || v === undefined) return v;
|
|
814
|
+
// Functions and Promises must not leave the sandbox as results.
|
|
815
|
+
// isolated-vm's structured clone rejects them; match its error.
|
|
816
|
+
if (typeof v === 'function') {
|
|
817
|
+
throw new TypeError(String(v) + ' could not be cloned');
|
|
818
|
+
}
|
|
819
|
+
if (v instanceof Date) {
|
|
820
|
+
// Invalid Dates have no ISO string; '' rebuilds an Invalid Date on the host.
|
|
821
|
+
return { __isDate: true, __isoString: isNaN(v.getTime()) ? '' : v.toISOString() };
|
|
822
|
+
}
|
|
823
|
+
if (typeof v === 'number' && isNaN(v)) {
|
|
824
|
+
return { __isNaN: true };
|
|
825
|
+
}
|
|
826
|
+
if (typeof v !== 'object') return v;
|
|
827
|
+
if (v instanceof Promise) {
|
|
828
|
+
throw new TypeError('#<Promise> could not be cloned');
|
|
829
|
+
}
|
|
830
|
+
if (v instanceof Error) {
|
|
831
|
+
var errExtra = {};
|
|
832
|
+
var errKeys = Object.keys(v);
|
|
833
|
+
for (var ei = 0; ei < errKeys.length; ei++) {
|
|
834
|
+
if (errKeys[ei] !== 'name' && errKeys[ei] !== 'message' && errKeys[ei] !== 'stack') {
|
|
835
|
+
errExtra[errKeys[ei]] = wrapSpecialValues(v[errKeys[ei]]);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return { __isErrorValue: true, __name: v.name || 'Error', __message: v.message || '', __extra: errExtra };
|
|
839
|
+
}
|
|
840
|
+
if (v instanceof Map) {
|
|
841
|
+
var entries = [];
|
|
842
|
+
v.forEach(function(val, key) {
|
|
843
|
+
entries.push([wrapSpecialValues(key), wrapSpecialValues(val)]);
|
|
844
|
+
});
|
|
845
|
+
return { __isMap: true, __entries: entries };
|
|
846
|
+
}
|
|
847
|
+
if (v instanceof Set) {
|
|
848
|
+
var values = [];
|
|
849
|
+
v.forEach(function(val) {
|
|
850
|
+
values.push(wrapSpecialValues(val));
|
|
851
|
+
});
|
|
852
|
+
return { __isSet: true, __values: values };
|
|
853
|
+
}
|
|
854
|
+
if (Array.isArray(v)) return v.map(wrapSpecialValues);
|
|
855
|
+
// Error sentinels are already in transfer shape — leave them intact.
|
|
856
|
+
if (v.__isError) return v;
|
|
857
|
+
var result = {};
|
|
858
|
+
var keys = Object.keys(v);
|
|
859
|
+
var collides = false;
|
|
860
|
+
for (var i = 0; i < keys.length; i++) {
|
|
861
|
+
var key = keys[i];
|
|
862
|
+
if (
|
|
863
|
+
key === '__isDate' || key === '__isNaN' || key === '__isErrorValue' ||
|
|
864
|
+
key === '__isMap' || key === '__isSet' || key === '__isEscaped'
|
|
865
|
+
) {
|
|
866
|
+
collides = true;
|
|
867
|
+
}
|
|
868
|
+
result[key] = wrapSpecialValues(v[key]);
|
|
869
|
+
}
|
|
870
|
+
// User objects whose keys collide with transfer markers are escaped so
|
|
871
|
+
// the host returns them as plain data (as isolated-vm does) instead of
|
|
872
|
+
// misreading them as sentinels.
|
|
873
|
+
return collides ? { __isEscaped: true, __value: result } : result;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Reverse direction: rebuild real instances from the sentinels the host's
|
|
877
|
+
// wrapSpecialValuesForGuest() produces for callback results.
|
|
878
|
+
globalThis.__unwrapFromHost = function unwrapFromHost(v) {
|
|
879
|
+
if (v === null || typeof v !== 'object') return v;
|
|
880
|
+
// Error sentinels stay as-is — the in-context proxy detects and throws them.
|
|
881
|
+
if (v.__isError === true) return v;
|
|
882
|
+
if (v.__isDate === true) return new Date(v.__isoString);
|
|
883
|
+
if (v.__isNaN === true) return NaN;
|
|
884
|
+
if (v.__isMap === true) {
|
|
885
|
+
var m = new Map();
|
|
886
|
+
for (var i = 0; i < v.__entries.length; i++) {
|
|
887
|
+
m.set(unwrapFromHost(v.__entries[i][0]), unwrapFromHost(v.__entries[i][1]));
|
|
888
|
+
}
|
|
889
|
+
return m;
|
|
890
|
+
}
|
|
891
|
+
if (v.__isSet === true) {
|
|
892
|
+
var s = new Set();
|
|
893
|
+
for (var j = 0; j < v.__values.length; j++) s.add(unwrapFromHost(v.__values[j]));
|
|
894
|
+
return s;
|
|
895
|
+
}
|
|
896
|
+
if (v.__isEscaped === true) {
|
|
897
|
+
var inner = v.__value;
|
|
898
|
+
var out = {};
|
|
899
|
+
var ekeys = Object.keys(inner);
|
|
900
|
+
for (var e = 0; e < ekeys.length; e++) out[ekeys[e]] = unwrapFromHost(inner[ekeys[e]]);
|
|
901
|
+
return out;
|
|
902
|
+
}
|
|
903
|
+
if (Array.isArray(v)) return v.map(unwrapFromHost);
|
|
904
|
+
var result = {};
|
|
905
|
+
var keys = Object.keys(v);
|
|
906
|
+
for (var n = 0; n < keys.length; n++) result[keys[n]] = unwrapFromHost(v[keys[n]]);
|
|
907
|
+
return result;
|
|
908
|
+
};
|
|
909
|
+
})();
|
|
910
|
+
`;
|
|
911
|
+
const result = this.vm.evalCode(wrapperCode);
|
|
912
|
+
if (result.error) {
|
|
913
|
+
const errStr = this.vm.dump(result.error);
|
|
914
|
+
result.error.dispose();
|
|
915
|
+
throw new Error(`Failed to inject transfer wrapper: ${typeof errStr === 'object' ? JSON.stringify(errStr) : String(errStr)}`);
|
|
916
|
+
}
|
|
917
|
+
result.value.dispose();
|
|
918
|
+
this.logger.debug('[QuickJsBridge] Transfer wrapper injected');
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Inject the E() error handler matching IsolatedVmBridge semantics:
|
|
922
|
+
* - Re-throw ExpressionError / ExpressionExtensionError
|
|
923
|
+
* - Swallow everything else (TypeErrors, generic Errors, etc.)
|
|
924
|
+
*
|
|
925
|
+
* Errors from host callbacks arrive as sentinel objects (not class
|
|
926
|
+
* instances), so we match by `name` instead of instanceof.
|
|
927
|
+
*/
|
|
928
|
+
injectErrorHandler() {
|
|
929
|
+
if (!this.vm)
|
|
930
|
+
throw new Error('Context not initialized');
|
|
931
|
+
const result = this.vm.evalCode(`
|
|
932
|
+
if (typeof E === 'undefined') {
|
|
933
|
+
globalThis.E = function(error, _context) {
|
|
934
|
+
var name = error && error.name;
|
|
935
|
+
if (name === 'ExpressionError' || name === 'ExpressionExtensionError') {
|
|
936
|
+
throw error;
|
|
937
|
+
}
|
|
938
|
+
return undefined;
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
`);
|
|
942
|
+
if (result.error) {
|
|
943
|
+
const errorStr = this.vm.dump(result.error);
|
|
944
|
+
result.error.dispose();
|
|
945
|
+
throw new Error(`Failed to inject error handler: ${String(errorStr)}`);
|
|
946
|
+
}
|
|
947
|
+
result.value.dispose();
|
|
948
|
+
this.logger.debug('[QuickJsBridge] Error handler injected');
|
|
949
|
+
}
|
|
950
|
+
verifyProxySystem() {
|
|
951
|
+
if (!this.vm)
|
|
952
|
+
throw new Error('Context not initialized');
|
|
953
|
+
const checks = [
|
|
954
|
+
['createDeepLazyProxy', 'typeof createDeepLazyProxy !== "undefined"'],
|
|
955
|
+
['buildContext', 'typeof buildContext !== "undefined"'],
|
|
956
|
+
['SafeObject', 'typeof SafeObject !== "undefined"'],
|
|
957
|
+
['SafeError', 'typeof SafeError !== "undefined"'],
|
|
958
|
+
];
|
|
959
|
+
for (const [name, code] of checks) {
|
|
960
|
+
const result = this.vm.evalCode(code);
|
|
961
|
+
if (result.error) {
|
|
962
|
+
result.error.dispose();
|
|
963
|
+
throw new Error(`Proxy system verification failed: ${name} check errored`);
|
|
964
|
+
}
|
|
965
|
+
const val = this.vm.dump(result.value);
|
|
966
|
+
result.value.dispose();
|
|
967
|
+
if (val !== true) {
|
|
968
|
+
throw new Error(`Proxy system verification failed: ${name} not available`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
this.logger.debug('[QuickJsBridge] Proxy system verified');
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Create per-execute callback function handles, closure-scoped to `data`.
|
|
975
|
+
*
|
|
976
|
+
* The handles are passed as arguments into the per-call wrapper function
|
|
977
|
+
* (see execute()) instead of being set as VM globals: `execute()` can
|
|
978
|
+
* re-enter synchronously (e.g. `$evaluateExpression`), and globals would
|
|
979
|
+
* let the nested call's callbacks clobber the in-flight outer call's data
|
|
980
|
+
* bindings. This mirrors IsolatedVmBridge's closure-scoped `$0`/`$1`/`$2`
|
|
981
|
+
* evalClosureSync references.
|
|
982
|
+
*
|
|
983
|
+
* Callers must dispose the returned handles when the call completes.
|
|
984
|
+
*/
|
|
985
|
+
createCallbackHandles(data) {
|
|
986
|
+
if (!this.vm)
|
|
987
|
+
throw new Error('Context not initialized');
|
|
988
|
+
const vm = this.vm;
|
|
989
|
+
const getValueFn = vm.newFunction('__getValueAtPathImpl', (pathHandle) => {
|
|
990
|
+
const pathArr = vm.dump(pathHandle);
|
|
991
|
+
try {
|
|
992
|
+
const result = getValueAtPath(data, pathArr);
|
|
993
|
+
return this.hostValueToQuickJSHandle(result);
|
|
994
|
+
}
|
|
995
|
+
catch (err) {
|
|
996
|
+
return this.hostValueToQuickJSHandle(serializeError(err));
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
const getArrayFn = vm.newFunction('__getArrayElementImpl', (pathHandle, indexHandle) => {
|
|
1000
|
+
const pathArr = vm.dump(pathHandle);
|
|
1001
|
+
const index = vm.dump(indexHandle);
|
|
1002
|
+
try {
|
|
1003
|
+
const result = getArrayElement(data, pathArr, index);
|
|
1004
|
+
return this.hostValueToQuickJSHandle(result);
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
return this.hostValueToQuickJSHandle(serializeError(err));
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
const callHostFn = vm.newFunction('__callHostImpl', (msgHandle) => {
|
|
1011
|
+
const rawMsg = vm.dump(msgHandle);
|
|
1012
|
+
try {
|
|
1013
|
+
const result = dispatchHostCall(rawMsg, data);
|
|
1014
|
+
return this.hostValueToQuickJSHandle(result);
|
|
1015
|
+
}
|
|
1016
|
+
catch (err) {
|
|
1017
|
+
return this.hostValueToQuickJSHandle(serializeError(err));
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
return [getValueFn, getArrayFn, callHostFn];
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Convert a host JavaScript value to a QuickJS handle.
|
|
1024
|
+
*
|
|
1025
|
+
* For primitives, uses the dedicated vm.newXxx() methods.
|
|
1026
|
+
* For complex objects (arrays, objects), uses JSON round-trip via evalCode.
|
|
1027
|
+
*/
|
|
1028
|
+
hostValueToQuickJSHandle(value) {
|
|
1029
|
+
if (!this.vm)
|
|
1030
|
+
throw new Error('Context not initialized');
|
|
1031
|
+
if (value === undefined)
|
|
1032
|
+
return this.vm.undefined;
|
|
1033
|
+
if (value === null)
|
|
1034
|
+
return this.vm.null;
|
|
1035
|
+
if (typeof value === 'number') {
|
|
1036
|
+
return this.vm.newNumber(value);
|
|
1037
|
+
}
|
|
1038
|
+
if (typeof value === 'string')
|
|
1039
|
+
return this.vm.newString(value);
|
|
1040
|
+
if (typeof value === 'boolean') {
|
|
1041
|
+
return value ? this.vm.true : this.vm.false;
|
|
1042
|
+
}
|
|
1043
|
+
// Construct a real Date inside the context — JSON round-tripping would
|
|
1044
|
+
// collapse it to an ISO string (via Date.prototype.toJSON).
|
|
1045
|
+
if (value instanceof Date) {
|
|
1046
|
+
const dateResult = this.vm.evalCode(`(new Date(${value.getTime()}))`);
|
|
1047
|
+
if (dateResult.error) {
|
|
1048
|
+
dateResult.error.dispose();
|
|
1049
|
+
return this.vm.undefined;
|
|
1050
|
+
}
|
|
1051
|
+
return dateResult.value;
|
|
1052
|
+
}
|
|
1053
|
+
const json = hostValueToJson(value);
|
|
1054
|
+
if (json === 'undefined')
|
|
1055
|
+
return this.vm.undefined;
|
|
1056
|
+
const result = this.vm.evalCode(`__unwrapFromHost(${json})`);
|
|
1057
|
+
if (result.error) {
|
|
1058
|
+
result.error.dispose();
|
|
1059
|
+
return this.vm.undefined;
|
|
1060
|
+
}
|
|
1061
|
+
return result.value;
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Execute JavaScript code in the QuickJS context.
|
|
1065
|
+
*
|
|
1066
|
+
* Mirrors IsolatedVmBridge.execute():
|
|
1067
|
+
* 1. Create per-call host callbacks scoped to `data` (createCallbackHandles)
|
|
1068
|
+
* 2. Build a fresh evaluation context via buildContext(callbacks, timezone)
|
|
1069
|
+
* 3. Run wrapped code with `this` set to the context
|
|
1070
|
+
* 4. Reconstruct error sentinels into real Errors
|
|
1071
|
+
*/
|
|
1072
|
+
execute(code, data, options) {
|
|
1073
|
+
if (!this.initialized || !this.vm || !this.runtime) {
|
|
1074
|
+
throw new Error('Bridge not initialized. Call initialize() first.');
|
|
1075
|
+
}
|
|
1076
|
+
// A nested call runs on what is left of the configured timeout, so the
|
|
1077
|
+
// chain shares one budget (mirrors IsolatedVmBridge). A configured
|
|
1078
|
+
// timeout of 0 means "no limit", so there is nothing to share.
|
|
1079
|
+
const elapsedMs = options?.elapsedMs ?? 0;
|
|
1080
|
+
const nested = elapsedMs > 0 && this.config.timeout > 0;
|
|
1081
|
+
let timeout = this.config.timeout;
|
|
1082
|
+
if (nested) {
|
|
1083
|
+
timeout = Math.trunc(this.config.timeout - elapsedMs);
|
|
1084
|
+
if (timeout <= 0)
|
|
1085
|
+
throw this.timeoutError(true);
|
|
1086
|
+
}
|
|
1087
|
+
const callbackHandles = this.createCallbackHandles(data);
|
|
1088
|
+
let wrapperFn;
|
|
1089
|
+
// Push this call's deadline; the interrupt handler (set in initialize)
|
|
1090
|
+
// interrupts on the earliest in-flight deadline. Popped in finally so a
|
|
1091
|
+
// nested call can't leave the outer budget extended.
|
|
1092
|
+
this.deadlines.push(Date.now() + timeout);
|
|
1093
|
+
try {
|
|
1094
|
+
const timezone = options?.timezone ? safeStringify(options.timezone) : 'undefined';
|
|
1095
|
+
// The callback impls arrive as function arguments (scoped to this call),
|
|
1096
|
+
// never as globals — see createCallbackHandles(). The runtime calls them
|
|
1097
|
+
// as plain functions; the isolated-vm bridge wires ivm.Callback there,
|
|
1098
|
+
// while QuickJS host functions are already plain functions in the context.
|
|
1099
|
+
const wrappedCode = `
|
|
1100
|
+
(function(__getValueAtPathImpl, __getArrayElementImpl, __callHostImpl) {
|
|
1101
|
+
var __ctx = buildContext({
|
|
1102
|
+
getValueAtPath: __getValueAtPathImpl,
|
|
1103
|
+
getArrayElement: __getArrayElementImpl,
|
|
1104
|
+
callHost: __callHostImpl,
|
|
1105
|
+
}, ${timezone});
|
|
1106
|
+
try {
|
|
1107
|
+
var __result = (function() {
|
|
1108
|
+
${code}
|
|
1109
|
+
}).call(__ctx);
|
|
1110
|
+
return __prepareForTransfer(__result);
|
|
1111
|
+
} catch(e) {
|
|
1112
|
+
if (e && e.__isError) return e;
|
|
1113
|
+
if (e == null) return { __isError: true, name: "Error", message: String(e), stack: "", extra: {} };
|
|
1114
|
+
var extra = {};
|
|
1115
|
+
for (var k in e) {
|
|
1116
|
+
if (Object.prototype.hasOwnProperty.call(e, k) && k !== "name" && k !== "message" && k !== "stack") extra[k] = e[k];
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
__isError: true,
|
|
1120
|
+
name: e.name || "Error",
|
|
1121
|
+
message: e.message || "",
|
|
1122
|
+
stack: e.stack || "",
|
|
1123
|
+
extra: extra
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
})`;
|
|
1127
|
+
const wrapperResult = this.vm.evalCode(wrappedCode);
|
|
1128
|
+
if (wrapperResult.error) {
|
|
1129
|
+
const errDump = this.vm.dump(wrapperResult.error);
|
|
1130
|
+
wrapperResult.error.dispose();
|
|
1131
|
+
throw new Error(`Expression compilation failed: ${String(errDump)}`);
|
|
1132
|
+
}
|
|
1133
|
+
wrapperFn = wrapperResult.value;
|
|
1134
|
+
const execResult = this.vm.callFunction(wrapperFn, this.vm.undefined, ...callbackHandles);
|
|
1135
|
+
if (execResult.error) {
|
|
1136
|
+
const errDump = this.vm.dump(execResult.error);
|
|
1137
|
+
execResult.error.dispose();
|
|
1138
|
+
// buildContext throws on invalid timezone — propagate the original message
|
|
1139
|
+
const errStr = String(typeof errDump === 'object' && errDump !== null
|
|
1140
|
+
? (errDump.message ?? errDump)
|
|
1141
|
+
: errDump);
|
|
1142
|
+
if (errStr.includes('interrupted')) {
|
|
1143
|
+
throw this.timeoutError(nested);
|
|
1144
|
+
}
|
|
1145
|
+
if (typeof errDump === 'object' &&
|
|
1146
|
+
errDump !== null &&
|
|
1147
|
+
typeof errDump.message === 'string') {
|
|
1148
|
+
throw new Error(errDump.message);
|
|
1149
|
+
}
|
|
1150
|
+
throw new Error(`Expression evaluation failed: ${errStr}`);
|
|
1151
|
+
}
|
|
1152
|
+
const rawResult = this.vm.dump(execResult.value);
|
|
1153
|
+
execResult.value.dispose();
|
|
1154
|
+
const result = unwrapSentinels(rawResult);
|
|
1155
|
+
if (isErrorSentinel(result)) {
|
|
1156
|
+
throw this.reconstructError(result);
|
|
1157
|
+
}
|
|
1158
|
+
this.logger.debug('[QuickJsBridge] Expression executed successfully');
|
|
1159
|
+
return result;
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
if (error instanceof Error &&
|
|
1163
|
+
(error.name === 'ExpressionError' || error.name === 'ExpressionExtensionError')) {
|
|
1164
|
+
throw error;
|
|
1165
|
+
}
|
|
1166
|
+
if (error instanceof types_1.TimeoutError || error instanceof types_1.MemoryLimitError) {
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1169
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1170
|
+
if (errorMessage.includes('interrupted')) {
|
|
1171
|
+
throw this.timeoutError(nested);
|
|
1172
|
+
}
|
|
1173
|
+
if (errorMessage.includes('out of memory') || errorMessage.includes('memory')) {
|
|
1174
|
+
throw new types_1.MemoryLimitError(`Expression exceeded memory limit of ${this.config.memoryLimit}MB`, {});
|
|
1175
|
+
}
|
|
1176
|
+
throw new Error(`Expression evaluation failed: ${errorMessage}`);
|
|
1177
|
+
}
|
|
1178
|
+
finally {
|
|
1179
|
+
this.deadlines.pop();
|
|
1180
|
+
wrapperFn?.dispose();
|
|
1181
|
+
for (const handle of callbackHandles) {
|
|
1182
|
+
handle.dispose();
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Always names the configured limit, never the reduced budget a nested call
|
|
1188
|
+
* ran on — reporting "timed out after 137ms" would misstate the limit.
|
|
1189
|
+
*/
|
|
1190
|
+
timeoutError(nested) {
|
|
1191
|
+
return new types_1.TimeoutError(nested
|
|
1192
|
+
? `Nested expressions timed out after sharing the ${this.config.timeout}ms limit`
|
|
1193
|
+
: `Expression timed out after ${this.config.timeout}ms`, {});
|
|
1194
|
+
}
|
|
1195
|
+
reconstructError(data) {
|
|
1196
|
+
const error = new Error(data.message);
|
|
1197
|
+
error.name = data.name || 'Error';
|
|
1198
|
+
if (data.stack) {
|
|
1199
|
+
error.stack = data.stack;
|
|
1200
|
+
}
|
|
1201
|
+
if (data.extra) {
|
|
1202
|
+
Object.assign(error, data.extra);
|
|
1203
|
+
}
|
|
1204
|
+
return error;
|
|
1205
|
+
}
|
|
1206
|
+
evalCodeOrThrow(code, label) {
|
|
1207
|
+
if (!this.vm)
|
|
1208
|
+
throw new Error('Context not initialized');
|
|
1209
|
+
const result = this.vm.evalCode(code);
|
|
1210
|
+
if (result.error) {
|
|
1211
|
+
const errStr = this.vm.dump(result.error);
|
|
1212
|
+
result.error.dispose();
|
|
1213
|
+
throw new Error(`${label} failed: ${String(errStr)}`);
|
|
1214
|
+
}
|
|
1215
|
+
const value = this.vm.dump(result.value);
|
|
1216
|
+
result.value.dispose();
|
|
1217
|
+
return value;
|
|
1218
|
+
}
|
|
1219
|
+
async dispose() {
|
|
1220
|
+
if (this.disposed)
|
|
1221
|
+
return;
|
|
1222
|
+
for (const handle of this.intlHandles) {
|
|
1223
|
+
handle.dispose();
|
|
1224
|
+
}
|
|
1225
|
+
this.intlHandles = [];
|
|
1226
|
+
if (this.vm) {
|
|
1227
|
+
this.vm.dispose();
|
|
1228
|
+
this.vm = undefined;
|
|
1229
|
+
}
|
|
1230
|
+
if (this.runtime) {
|
|
1231
|
+
this.runtime.dispose();
|
|
1232
|
+
this.runtime = undefined;
|
|
1233
|
+
}
|
|
1234
|
+
this.disposed = true;
|
|
1235
|
+
this.initialized = false;
|
|
1236
|
+
this.logger.info('[QuickJsBridge] Disposed');
|
|
1237
|
+
}
|
|
1238
|
+
isDisposed() {
|
|
1239
|
+
return this.disposed;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
exports.QuickJsBridge = QuickJsBridge;
|
|
1243
|
+
//# sourceMappingURL=quickjs-bridge.js.map
|