@n8n/expression-runtime 0.27.1 → 0.29.0

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