@n8n/expression-runtime 0.29.1 → 0.31.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 (120) hide show
  1. package/README.md +4 -0
  2. package/dist/bundle/runtime.esm.js +17 -17
  3. package/dist/bundle/runtime.esm.js.map +4 -4
  4. package/dist/bundle/runtime.iife.js +17 -17
  5. package/dist/bundle/runtime.iife.js.map +4 -4
  6. package/dist/cjs/bridge/host-functions.d.ts +74 -0
  7. package/dist/cjs/bridge/host-functions.d.ts.map +1 -0
  8. package/dist/cjs/bridge/host-functions.js +336 -0
  9. package/dist/cjs/bridge/host-functions.js.map +1 -0
  10. package/dist/cjs/bridge/isolated-vm-bridge.d.ts +24 -150
  11. package/dist/cjs/bridge/isolated-vm-bridge.d.ts.map +1 -1
  12. package/dist/cjs/bridge/isolated-vm-bridge.js +130 -429
  13. package/dist/cjs/bridge/isolated-vm-bridge.js.map +1 -1
  14. package/dist/cjs/bridge/quickjs-bridge.d.ts +21 -6
  15. package/dist/cjs/bridge/quickjs-bridge.d.ts.map +1 -1
  16. package/dist/cjs/bridge/quickjs-bridge.js +377 -402
  17. package/dist/cjs/bridge/quickjs-bridge.js.map +1 -1
  18. package/dist/cjs/build.tsbuildinfo +1 -1
  19. package/dist/cjs/evaluator/expression-evaluator.d.ts +17 -0
  20. package/dist/cjs/evaluator/expression-evaluator.d.ts.map +1 -1
  21. package/dist/cjs/evaluator/expression-evaluator.js +61 -1
  22. package/dist/cjs/evaluator/expression-evaluator.js.map +1 -1
  23. package/dist/cjs/extensions/array-extensions.d.ts.map +1 -1
  24. package/dist/cjs/extensions/array-extensions.js +7 -15
  25. package/dist/cjs/extensions/array-extensions.js.map +1 -1
  26. package/dist/cjs/extensions/function-extensions.d.ts.map +1 -1
  27. package/dist/cjs/extensions/function-extensions.js +2 -10
  28. package/dist/cjs/extensions/function-extensions.js.map +1 -1
  29. package/dist/cjs/extensions/object-extensions.d.ts.map +1 -1
  30. package/dist/cjs/extensions/object-extensions.js +3 -11
  31. package/dist/cjs/extensions/object-extensions.js.map +1 -1
  32. package/dist/cjs/extensions/utils.d.ts +1 -0
  33. package/dist/cjs/extensions/utils.d.ts.map +1 -1
  34. package/dist/cjs/extensions/utils.js +10 -0
  35. package/dist/cjs/extensions/utils.js.map +1 -1
  36. package/dist/cjs/observability/metrics.d.ts +12 -0
  37. package/dist/cjs/observability/metrics.d.ts.map +1 -1
  38. package/dist/cjs/observability/metrics.js +12 -0
  39. package/dist/cjs/observability/metrics.js.map +1 -1
  40. package/dist/cjs/runtime/lazy-proxy.d.ts +2 -0
  41. package/dist/cjs/runtime/lazy-proxy.d.ts.map +1 -1
  42. package/dist/cjs/runtime/lazy-proxy.js +29 -15
  43. package/dist/cjs/runtime/lazy-proxy.js.map +1 -1
  44. package/dist/cjs/runtime/luxon-transfer.d.ts +62 -0
  45. package/dist/cjs/runtime/luxon-transfer.d.ts.map +1 -0
  46. package/dist/cjs/runtime/luxon-transfer.js +200 -0
  47. package/dist/cjs/runtime/luxon-transfer.js.map +1 -0
  48. package/dist/cjs/runtime/serialize.d.ts +7 -7
  49. package/dist/cjs/runtime/serialize.d.ts.map +1 -1
  50. package/dist/cjs/runtime/serialize.js +42 -30
  51. package/dist/cjs/runtime/serialize.js.map +1 -1
  52. package/dist/cjs/runtime/transfer.d.ts +77 -0
  53. package/dist/cjs/runtime/transfer.d.ts.map +1 -0
  54. package/dist/cjs/runtime/transfer.js +183 -0
  55. package/dist/cjs/runtime/transfer.js.map +1 -0
  56. package/dist/cjs/types/bridge.d.ts +16 -0
  57. package/dist/cjs/types/bridge.d.ts.map +1 -1
  58. package/dist/cjs/types/bridge.js +1 -0
  59. package/dist/cjs/types/bridge.js.map +1 -1
  60. package/dist/cjs/types/evaluator.d.ts +9 -0
  61. package/dist/cjs/types/evaluator.d.ts.map +1 -1
  62. package/dist/cjs/types/evaluator.js.map +1 -1
  63. package/dist/esm/bridge/host-functions.d.ts +74 -0
  64. package/dist/esm/bridge/host-functions.d.ts.map +1 -0
  65. package/dist/esm/bridge/host-functions.js +328 -0
  66. package/dist/esm/bridge/host-functions.js.map +1 -0
  67. package/dist/esm/bridge/isolated-vm-bridge.d.ts +24 -150
  68. package/dist/esm/bridge/isolated-vm-bridge.d.ts.map +1 -1
  69. package/dist/esm/bridge/isolated-vm-bridge.js +126 -425
  70. package/dist/esm/bridge/isolated-vm-bridge.js.map +1 -1
  71. package/dist/esm/bridge/quickjs-bridge.d.ts +21 -6
  72. package/dist/esm/bridge/quickjs-bridge.d.ts.map +1 -1
  73. package/dist/esm/bridge/quickjs-bridge.js +368 -393
  74. package/dist/esm/bridge/quickjs-bridge.js.map +1 -1
  75. package/dist/esm/build.tsbuildinfo +1 -1
  76. package/dist/esm/evaluator/expression-evaluator.d.ts +17 -0
  77. package/dist/esm/evaluator/expression-evaluator.d.ts.map +1 -1
  78. package/dist/esm/evaluator/expression-evaluator.js +61 -1
  79. package/dist/esm/evaluator/expression-evaluator.js.map +1 -1
  80. package/dist/esm/extensions/array-extensions.d.ts.map +1 -1
  81. package/dist/esm/extensions/array-extensions.js +1 -9
  82. package/dist/esm/extensions/array-extensions.js.map +1 -1
  83. package/dist/esm/extensions/function-extensions.d.ts.map +1 -1
  84. package/dist/esm/extensions/function-extensions.js +1 -9
  85. package/dist/esm/extensions/function-extensions.js.map +1 -1
  86. package/dist/esm/extensions/object-extensions.d.ts.map +1 -1
  87. package/dist/esm/extensions/object-extensions.js +1 -9
  88. package/dist/esm/extensions/object-extensions.js.map +1 -1
  89. package/dist/esm/extensions/utils.d.ts +1 -0
  90. package/dist/esm/extensions/utils.d.ts.map +1 -1
  91. package/dist/esm/extensions/utils.js +9 -0
  92. package/dist/esm/extensions/utils.js.map +1 -1
  93. package/dist/esm/observability/metrics.d.ts +12 -0
  94. package/dist/esm/observability/metrics.d.ts.map +1 -1
  95. package/dist/esm/observability/metrics.js +12 -0
  96. package/dist/esm/observability/metrics.js.map +1 -1
  97. package/dist/esm/runtime/lazy-proxy.d.ts +2 -0
  98. package/dist/esm/runtime/lazy-proxy.d.ts.map +1 -1
  99. package/dist/esm/runtime/lazy-proxy.js +29 -15
  100. package/dist/esm/runtime/lazy-proxy.js.map +1 -1
  101. package/dist/esm/runtime/luxon-transfer.d.ts +62 -0
  102. package/dist/esm/runtime/luxon-transfer.d.ts.map +1 -0
  103. package/dist/esm/runtime/luxon-transfer.js +183 -0
  104. package/dist/esm/runtime/luxon-transfer.js.map +1 -0
  105. package/dist/esm/runtime/serialize.d.ts +7 -7
  106. package/dist/esm/runtime/serialize.d.ts.map +1 -1
  107. package/dist/esm/runtime/serialize.js +42 -30
  108. package/dist/esm/runtime/serialize.js.map +1 -1
  109. package/dist/esm/runtime/transfer.d.ts +77 -0
  110. package/dist/esm/runtime/transfer.d.ts.map +1 -0
  111. package/dist/esm/runtime/transfer.js +173 -0
  112. package/dist/esm/runtime/transfer.js.map +1 -0
  113. package/dist/esm/types/bridge.d.ts +16 -0
  114. package/dist/esm/types/bridge.d.ts.map +1 -1
  115. package/dist/esm/types/bridge.js +1 -0
  116. package/dist/esm/types/bridge.js.map +1 -1
  117. package/dist/esm/types/evaluator.d.ts +9 -0
  118. package/dist/esm/types/evaluator.d.ts.map +1 -1
  119. package/dist/esm/types/evaluator.js.map +1 -1
  120. package/package.json +8 -8
@@ -34,11 +34,14 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.IsolatedVmBridge = void 0;
37
- const promises_1 = require("node:fs/promises");
37
+ const node_fs_1 = require("node:fs");
38
38
  const path = __importStar(require("node:path"));
39
39
  const types_1 = require("../types");
40
- const bridge_messages_1 = require("./bridge-messages");
40
+ const luxon_transfer_1 = require("../runtime/luxon-transfer");
41
+ const host_functions_1 = require("./host-functions");
41
42
  let _ivm = null;
43
+ /** Runtime bundle source, read once per process by loadRuntimeBundle(). */
44
+ let _runtimeBundle = null;
42
45
  function getIvm() {
43
46
  if (!_ivm) {
44
47
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -50,33 +53,52 @@ const BUNDLE_RELATIVE_PATH = path.join('dist', 'bundle', 'runtime.iife.js');
50
53
  // Captured at module load so values rendered into generated code stay stable
51
54
  // even if the global is later replaced.
52
55
  const safeStringify = JSON.stringify;
53
- /** Check if a value is an error sentinel returned by serializeError. */
54
- function isErrorSentinel(value) {
55
- return (typeof value === 'object' &&
56
- value !== null &&
57
- value.__isError === true);
56
+ // V8 compile cache for the runtime bundle (BridgeConfig.compileCache): the
57
+ // first bundle compile produces it, later builds consume it and skip
58
+ // re-parsing. V8 validates the data and recompiles when it is stale.
59
+ let _bundleCachedData = null;
60
+ // The runtime sets Script.cachedData when produceCachedData is passed, but
61
+ // ivm's typings omit the property (CachedDataResult exists unattached).
62
+ function producedCachedData(script) {
63
+ const data = Reflect.get(script, 'cachedData');
64
+ return data instanceof getIvm().ExternalCopy ? data : null;
58
65
  }
59
- /**
60
- * Serialize an error into a transferable metadata object.
61
- *
62
- * Host-side callbacks (getValueAtPath, etc.) catch errors and return this
63
- * sentinel instead of letting the error cross the isolate boundary (which
64
- * strips custom class identity and properties). The isolate-side proxy
65
- * detects __isError and reconstructs a proper Error to throw.
66
- */
67
- function serializeError(err) {
68
- if (err instanceof Error) {
69
- const extra = Object.fromEntries(Object.entries(err).filter(([key]) => key !== 'name' && key !== 'message' && key !== 'stack'));
70
- return {
71
- __isError: true,
72
- name: err.name,
73
- message: err.message,
74
- stack: err.stack,
75
- extra,
76
- };
66
+ /** Globals the runtime bundle must define. Verified after every bundle load. */
67
+ const RUNTIME_GLOBALS = [
68
+ 'DateTime',
69
+ 'extend',
70
+ 'createDeepLazyProxy',
71
+ 'SafeObject',
72
+ 'SafeError',
73
+ 'buildContext',
74
+ ];
75
+ /** Evaluates inside the isolate to a JSON array of the RUNTIME_GLOBALS that are missing. */
76
+ const MISSING_RUNTIME_GLOBALS_SOURCE = `JSON.stringify(${JSON.stringify(RUNTIME_GLOBALS)}.filter((name) => typeof globalThis[name] === 'undefined'))`;
77
+ function assertRuntimeGlobals(missingJson) {
78
+ const missing = typeof missingJson === 'string' ? JSON.parse(missingJson) : missingJson;
79
+ if (Array.isArray(missing) && missing.length > 0) {
80
+ throw new Error(`Runtime bundle verification failed: missing ${missing.join(', ')}`);
77
81
  }
78
- return { __isError: true, name: 'Error', message: String(err), extra: {} };
79
82
  }
83
+ /**
84
+ * The E() error handler injected into every isolate; see injectErrorHandler()
85
+ * for the two exception-handling layers it participates in.
86
+ */
87
+ const ERROR_HANDLER_SOURCE = `
88
+ if (typeof E === 'undefined') {
89
+ globalThis.E = function(error, _context) {
90
+ // Re-throw ExpressionError / ExpressionExtensionError to match
91
+ // the legacy handler in expression.ts. Errors from host callbacks
92
+ // arrive as sentinels (not class instances), so check by name.
93
+ const name = error?.name;
94
+ if (name === 'ExpressionError' || name === 'ExpressionExtensionError') {
95
+ throw error;
96
+ }
97
+ // Swallow everything else (TypeErrors, generic Errors, etc.)
98
+ return undefined;
99
+ };
100
+ }
101
+ `;
80
102
  /**
81
103
  * Read the runtime IIFE bundle by walking up from `__dirname` until
82
104
  * `dist/bundle/runtime.iife.js` is found.
@@ -85,11 +107,14 @@ function serializeError(err) {
85
107
  * - `src/bridge/` (vitest running against source)
86
108
  * - `dist/cjs/bridge/` (CJS build)
87
109
  */
88
- async function readRuntimeBundle() {
110
+ function loadRuntimeBundle() {
111
+ if (_runtimeBundle !== null)
112
+ return _runtimeBundle;
89
113
  let dir = __dirname;
90
114
  while (dir !== path.dirname(dir)) {
91
115
  try {
92
- return await (0, promises_1.readFile)(path.join(dir, BUNDLE_RELATIVE_PATH), 'utf-8');
116
+ _runtimeBundle = (0, node_fs_1.readFileSync)(path.join(dir, BUNDLE_RELATIVE_PATH), 'utf-8');
117
+ return _runtimeBundle;
93
118
  }
94
119
  catch { }
95
120
  dir = path.dirname(dir);
@@ -149,8 +174,6 @@ class IsolatedVmBridge {
149
174
  await jail.set('global', jail.derefInto());
150
175
  // Load runtime bundle (DateTime, extend, SafeObject, proxy system)
151
176
  await this.loadVendorLibraries();
152
- // Verify proxy system loaded correctly
153
- await this.verifyProxySystem();
154
177
  // Inject E() error handler needed by tournament-generated try-catch code
155
178
  await this.injectErrorHandler();
156
179
  this.initialized = true;
@@ -174,54 +197,24 @@ class IsolatedVmBridge {
174
197
  }
175
198
  try {
176
199
  // Load runtime bundle (includes vendor libraries + proxy system)
177
- const runtimeBundle = await readRuntimeBundle();
200
+ const runtimeBundle = loadRuntimeBundle();
178
201
  // Evaluate bundle in isolate context
179
202
  // This makes all exported globals available (DateTime, extend, extendOptional, SafeObject, SafeError, createDeepLazyProxy, buildContext)
180
- await this.context.eval(runtimeBundle);
181
- this.logger.debug('[IsolatedVmBridge] Runtime bundle loaded');
182
- // Verify vendor libraries loaded correctly
183
- const hasDateTime = await this.context.eval('typeof DateTime !== "undefined"');
184
- const hasExtend = await this.context.eval('typeof extend !== "undefined"');
185
- if (!hasDateTime || !hasExtend) {
186
- throw new Error(`Library verification failed: DateTime=${hasDateTime}, extend=${hasExtend}`);
203
+ if (this.config.compileCache) {
204
+ const script = await this.isolate.compileScript(runtimeBundle, _bundleCachedData ? { cachedData: _bundleCachedData } : { produceCachedData: true });
205
+ if (!_bundleCachedData)
206
+ _bundleCachedData = producedCachedData(script);
207
+ await script.run(this.context);
187
208
  }
188
- this.logger.debug('[IsolatedVmBridge] Vendor libraries verified successfully');
189
- }
190
- catch (error) {
191
- const errorMessage = error instanceof Error ? error.message : String(error);
192
- throw new Error(`Failed to load runtime bundle: ${errorMessage}`);
193
- }
194
- }
195
- /**
196
- * Verify the proxy system loaded correctly.
197
- *
198
- * The proxy system is loaded as part of the runtime bundle in loadVendorLibraries().
199
- * This method verifies all required components are available.
200
- *
201
- * @private
202
- * @throws {Error} If context not initialized or proxy system verification fails
203
- */
204
- async verifyProxySystem() {
205
- if (!this.context) {
206
- throw new Error('Context not initialized');
207
- }
208
- try {
209
- // Verify proxy system components loaded correctly
210
- const hasProxyCreator = await this.context.eval('typeof createDeepLazyProxy !== "undefined"');
211
- const hasSafeObject = await this.context.eval('typeof SafeObject !== "undefined"');
212
- const hasSafeError = await this.context.eval('typeof SafeError !== "undefined"');
213
- const hasBuildContext = await this.context.eval('typeof buildContext !== "undefined"');
214
- if (!hasProxyCreator || !hasSafeObject || !hasSafeError || !hasBuildContext) {
215
- throw new Error(`Proxy system verification failed: ` +
216
- `createDeepLazyProxy=${hasProxyCreator}, ` +
217
- `SafeObject=${hasSafeObject}, SafeError=${hasSafeError}, ` +
218
- `buildContext=${hasBuildContext}`);
209
+ else {
210
+ await this.context.eval(runtimeBundle);
219
211
  }
220
- this.logger.debug('[IsolatedVmBridge] Proxy system verified successfully');
212
+ assertRuntimeGlobals(await this.context.eval(MISSING_RUNTIME_GLOBALS_SOURCE));
213
+ this.logger.debug('[IsolatedVmBridge] Runtime bundle loaded and verified');
221
214
  }
222
215
  catch (error) {
223
216
  const errorMessage = error instanceof Error ? error.message : String(error);
224
- throw new Error(`Failed to verify proxy system: ${errorMessage}`);
217
+ throw new Error(`Failed to load runtime bundle: ${errorMessage}`);
225
218
  }
226
219
  }
227
220
  /**
@@ -251,204 +244,98 @@ class IsolatedVmBridge {
251
244
  if (!this.context) {
252
245
  throw new Error('Context not initialized');
253
246
  }
254
- await this.context.eval(`
255
- if (typeof E === 'undefined') {
256
- globalThis.E = function(error, _context) {
257
- // Re-throw ExpressionError / ExpressionExtensionError to match
258
- // the legacy handler in expression.ts. Errors from host callbacks
259
- // arrive as sentinels (not class instances), so check by name.
260
- const name = error?.name;
261
- if (name === 'ExpressionError' || name === 'ExpressionExtensionError') {
262
- throw error;
263
- }
264
- // Swallow everything else (TypeErrors, generic Errors, etc.)
265
- return undefined;
266
- };
267
- }
268
- `);
247
+ await this.context.eval(ERROR_HANDLER_SOURCE);
269
248
  this.logger.debug('[IsolatedVmBridge] Error handler injected successfully');
270
249
  }
250
+ /**
251
+ * Synchronous variant of initialize(): the same steps through isolated-vm's
252
+ * sync APIs, for on-demand creation inside the synchronous evaluate() path
253
+ * (lazy acquisition with an exhausted pool). Blocks the event loop for the
254
+ * duration of one isolate setup — pool warmup should stay on the async
255
+ * initialize().
256
+ */
257
+ initializeSync() {
258
+ if (this.initialized)
259
+ return;
260
+ try {
261
+ this.context = this.isolate.createContextSync();
262
+ const jail = this.context.global;
263
+ jail.setSync('global', jail.derefInto());
264
+ if (this.config.compileCache) {
265
+ const script = this.isolate.compileScriptSync(loadRuntimeBundle(), _bundleCachedData ? { cachedData: _bundleCachedData } : { produceCachedData: true });
266
+ if (!_bundleCachedData)
267
+ _bundleCachedData = producedCachedData(script);
268
+ script.runSync(this.context);
269
+ }
270
+ else {
271
+ this.context.evalSync(loadRuntimeBundle());
272
+ }
273
+ assertRuntimeGlobals(this.context.evalSync(MISSING_RUNTIME_GLOBALS_SOURCE));
274
+ this.context.evalSync(ERROR_HANDLER_SOURCE);
275
+ }
276
+ catch (error) {
277
+ // A failed cold start must not retain the native isolate. dispose()
278
+ // has no awaits before the isolate is released, so not awaiting it
279
+ // here is safe.
280
+ void this.dispose();
281
+ throw error;
282
+ }
283
+ this.initialized = true;
284
+ this.logger.debug('[IsolatedVmBridge] Initialized synchronously');
285
+ }
271
286
  /**
272
287
  * Create an ivm.Callback for getting value/metadata at a path.
273
288
  *
274
- * Used by createDeepLazyProxy when accessing properties. Returns metadata
275
- * markers for arrays and objects, or the primitive value directly.
276
- *
277
- * Function-typed values are returned as `undefined` — every callable on
278
- * the host data surface (`$('Foo').first()`, `$items()`, `$fromAI()`,
279
- * `$evaluateExpression()`, `$getPairedItem()`) is wired in-isolate via
280
- * the typed-RPC dispatcher (`callHost`). No expression form should
281
- * reach a function through this path.
289
+ * Thin wrapper around the shared getValueAtPath (see host-functions.ts
290
+ * for navigation and guard semantics); errors are caught and returned
291
+ * as sentinels instead of crossing the isolate boundary.
282
292
  *
283
293
  * @param data - Current workflow data to use for callback responses
284
294
  * @private
285
295
  */
286
296
  createGetValueAtPathRef(data) {
287
- return new (getIvm().Callback)((path) => {
297
+ return new (getIvm().Callback)((pathArr) => {
288
298
  try {
289
- // Navigate to value
290
- // Special-case: paths starting with ['$item', index] call data.$item(index)
291
- // to get the sub-proxy for that item, then continue navigating the rest.
292
- let value = data;
293
- let startIndex = 0;
294
- const itemFn = data.$item;
295
- if (path.length >= 2 && path[0] === '$item' && typeof itemFn === 'function') {
296
- const itemIndex = parseInt(path[1], 10);
297
- if (!isNaN(itemIndex)) {
298
- value = itemFn(itemIndex);
299
- startIndex = 2;
300
- }
301
- }
302
- else {
303
- const dollarFn = data.$;
304
- if (path.length >= 2 && path[0] === '$' && typeof dollarFn === 'function') {
305
- value = dollarFn(path[1]);
306
- startIndex = 2;
307
- }
308
- }
309
- for (let i = startIndex; i < path.length; i++) {
310
- value = value?.[path[i]];
311
- if (value === undefined || value === null) {
312
- return value;
313
- }
314
- }
315
- // Functions are not reachable via the lazy-proxy data path —
316
- // every callable on the host data surface routes through the
317
- // typed-RPC dispatcher. Return undefined so any residual
318
- // access surfaces as missing rather than as a stale metadata
319
- // marker the runtime no longer knows how to interpret.
320
- if (typeof value === 'function') {
321
- return undefined;
322
- }
323
- // Handle arrays - always lazy, only transfer length
324
- if (Array.isArray(value)) {
325
- return {
326
- __isArray: true,
327
- __length: value.length,
328
- __data: null,
329
- };
330
- }
331
- // Dates have no enumerable own keys; pass through instead of
332
- // marshaling as an empty object.
333
- if (value instanceof Date) {
334
- return value;
335
- }
336
- // Handle objects - return metadata with keys
337
- if (value !== null && typeof value === 'object') {
338
- return {
339
- __isObject: true,
340
- __keys: Object.keys(value),
341
- };
342
- }
343
- // Primitive value
344
- return value;
299
+ return (0, host_functions_1.getValueAtPath)(data, pathArr);
345
300
  }
346
301
  catch (err) {
347
- return serializeError(err);
302
+ return (0, host_functions_1.serializeError)(err);
348
303
  }
349
304
  });
350
305
  }
351
306
  /**
352
307
  * Create an ivm.Callback for getting array elements at an index.
353
308
  *
354
- * Used by array proxy when accessing numeric indices.
309
+ * Thin wrapper around the shared getArrayElement (see host-functions.ts
310
+ * for navigation and guard semantics); errors are caught and returned
311
+ * as sentinels instead of crossing the isolate boundary.
355
312
  *
356
313
  * @param data - Current workflow data to use for callback responses
357
314
  * @private
358
315
  */
359
316
  createGetArrayElementRef(data) {
360
- return new (getIvm().Callback)((path, index) => {
317
+ return new (getIvm().Callback)((pathArr, index) => {
361
318
  try {
362
- // Navigate to array
363
- // Special-case: paths starting with ['$item', index] call data.$item(index)
364
- let arr = data;
365
- let startIndex = 0;
366
- const itemFn = data.$item;
367
- if (path.length >= 2 && path[0] === '$item' && typeof itemFn === 'function') {
368
- const itemIndex = parseInt(path[1], 10);
369
- if (!isNaN(itemIndex)) {
370
- arr = itemFn(itemIndex);
371
- startIndex = 2;
372
- }
373
- }
374
- else {
375
- const dollarFn = data.$;
376
- if (path.length >= 2 && path[0] === '$' && typeof dollarFn === 'function') {
377
- arr = dollarFn(path[1]);
378
- startIndex = 2;
379
- }
380
- }
381
- for (let i = startIndex; i < path.length; i++) {
382
- arr = arr?.[path[i]];
383
- if (arr === undefined || arr === null) {
384
- return undefined;
385
- }
386
- }
387
- if (!Array.isArray(arr)) {
388
- return undefined;
389
- }
390
- // Only genuine array indices are reachable; anything else (e.g.
391
- // 'constructor', '__lookupGetter__') would read off the prototype
392
- // chain and could leak a host function reference across the boundary.
393
- if (!Number.isInteger(index) || index < 0) {
394
- return undefined;
395
- }
396
- const element = arr[index];
397
- // Functions are never reachable through the data surface — mirror the
398
- // guard in getValueAtPath so a host callable can't cross the boundary.
399
- if (typeof element === 'function') {
400
- return undefined;
401
- }
402
- // Dates have no enumerable own keys; pass through instead of
403
- // marshaling as an empty object.
404
- if (element instanceof Date) {
405
- return element;
406
- }
407
- // If element is object/array, return metadata
408
- if (element !== null && typeof element === 'object') {
409
- if (Array.isArray(element)) {
410
- return {
411
- __isArray: true,
412
- __length: element.length,
413
- __data: null,
414
- };
415
- }
416
- return {
417
- __isObject: true,
418
- __keys: Object.keys(element),
419
- };
420
- }
421
- // Primitive element
422
- return element;
319
+ return (0, host_functions_1.getArrayElement)(data, pathArr, index);
423
320
  }
424
321
  catch (err) {
425
- return serializeError(err);
322
+ return (0, host_functions_1.serializeError)(err);
426
323
  }
427
324
  });
428
325
  }
429
326
  /**
430
- * Create the single typed-RPC dispatcher.
431
- *
432
- * The isolate sends one envelope per typed RPC invocation:
433
- * `callHost({ type: 'getNodeFirst', nodeName, branchIndex?, runIndex? })`
434
- *
435
- * Inputs cross a trust boundary, so the dispatcher parses every envelope
436
- * with the host-side zod schema (`bridgeMessageSchema`) before any
437
- * dispatch happens. Anything that deviates from the declared shape —
438
- * unknown `type`, missing required fields, extra unexpected fields,
439
- * wrong field types — fails the parse and an error sentinel is returned
440
- * to the caller.
327
+ * Create the ivm.Callback for the typed-RPC `callHost` channel.
441
328
  *
442
- * After parsing, `switch (msg.type)` dispatches to a private handler with
443
- * a fully narrowed message type. The operation set is exactly the cases
444
- * in this switch; the `type` field selects a static branch in source,
445
- * not a property lookup on a runtime object.
329
+ * Thin wrapper around the shared dispatchHostCall (see host-functions.ts
330
+ * for envelope validation and per-message rationale); errors — including
331
+ * zod parse failures — are caught and returned as sentinels instead of
332
+ * crossing the isolate boundary.
446
333
  *
447
- * Return-value note: handlers must return plain, structured-clone-able
448
- * data. Results cross into the isolate through an ivm.Callback, which copies
449
- * them via the structured-clone algorithm — return JSON-shaped values, not
450
- * isolated-vm objects (`Reference`/`ExternalCopy`) or other non-cloneable
451
- * values.
334
+ * Return-value note: the dispatcher returns plain, structured-clone-able
335
+ * data. Results cross into the isolate through an ivm.Callback, which
336
+ * copies them via the structured-clone algorithm — JSON-shaped values,
337
+ * not isolated-vm objects (`Reference`/`ExternalCopy`) or other
338
+ * non-cloneable values.
452
339
  *
453
340
  * @param data - Current workflow data
454
341
  * @private
@@ -456,183 +343,13 @@ class IsolatedVmBridge {
456
343
  createCallHostRef(data) {
457
344
  return new (getIvm().Callback)((rawMsg) => {
458
345
  try {
459
- const msg = bridge_messages_1.bridgeMessageSchema.parse(rawMsg);
460
- switch (msg.type) {
461
- case 'getNodeFirst':
462
- return this.handleGetNodeFirst(msg, data);
463
- case 'getNodeLast':
464
- return this.handleGetNodeLast(msg, data);
465
- case 'getNodeAll':
466
- return this.handleGetNodeAll(msg, data);
467
- case 'getInputFirst':
468
- return this.handleGetInputFirst(data);
469
- case 'getInputLast':
470
- return this.handleGetInputLast(data);
471
- case 'getInputAll':
472
- return this.handleGetInputAll(data);
473
- case 'getItems':
474
- return this.handleGetItems(msg, data);
475
- case 'fromAi':
476
- return this.handleFromAi(msg, data);
477
- case 'getNodePairedItem':
478
- return this.handleGetNodePairedItem(msg, data);
479
- case 'getNodeItemMatching':
480
- return this.handleGetNodeItemMatching(msg, data);
481
- case 'getNodeItem':
482
- return this.handleGetNodeItem(msg, data);
483
- case 'evaluateExpression':
484
- return this.handleEvaluateExpression(msg, data);
485
- case 'getPairedItem':
486
- return this.handleGetPairedItem(msg, data);
487
- default: {
488
- // Unreachable at runtime — zod rejects unknown `type` values
489
- // before the switch. The `never` assignment is the compile-time
490
- // guard: a new schema added to `bridgeMessageSchema` without a
491
- // matching case here becomes a type error.
492
- const exhaustive = msg;
493
- void exhaustive;
494
- throw new Error('Unhandled bridge message');
495
- }
496
- }
346
+ return (0, host_functions_1.dispatchHostCall)(rawMsg, data);
497
347
  }
498
348
  catch (err) {
499
- return serializeError(err);
349
+ return (0, host_functions_1.serializeError)(err);
500
350
  }
501
351
  });
502
352
  }
503
- /**
504
- * Handlers for the `$('Foo').{first,last,all}` typed RPCs.
505
- *
506
- * Each handler reads a fixed literal property name off the host-side node
507
- * proxy — the isolate cannot influence which property is dereferenced.
508
- * Eliminating `data.$` as a host-callable entirely would require reaching
509
- * the `WorkflowDataProxy` internals (e.g. `getNodeExecutionOrPinnedData`)
510
- * rather than the public `$()` API; that's a follow-up.
511
- *
512
- * `data.$` is a host-wired function (`WorkflowDataProxy`'s `$`). If it
513
- * ever isn't, optional chaining short-circuits to `undefined` — the same
514
- * observable result the runtime's `E()` handler produces from any thrown
515
- * error here.
516
- *
517
- * @private
518
- */
519
- handleGetNodeFirst(msg, data) {
520
- return data.$?.(msg.nodeName)?.first?.(msg.branchIndex, msg.runIndex);
521
- }
522
- handleGetNodeLast(msg, data) {
523
- return data.$?.(msg.nodeName)?.last?.(msg.branchIndex, msg.runIndex);
524
- }
525
- handleGetNodeAll(msg, data) {
526
- return data.$?.(msg.nodeName)?.all?.(msg.branchIndex, msg.runIndex);
527
- }
528
- /**
529
- * Handlers for the `$input.{first,last,all}` typed RPCs.
530
- *
531
- * Each reads a fixed literal property name off `data.$input` (the host's
532
- * `WorkflowDataProxy` input proxy). The host enforces zero arguments on
533
- * these methods — the schemas have no fields besides `type`, so the
534
- * isolate cannot pass anything that would trigger the "should have no
535
- * arguments" error path on the host side.
536
- *
537
- * @private
538
- */
539
- handleGetInputFirst(data) {
540
- return data.$input?.first?.();
541
- }
542
- handleGetInputLast(data) {
543
- return data.$input?.last?.();
544
- }
545
- handleGetInputAll(data) {
546
- return data.$input?.all?.();
547
- }
548
- /**
549
- * Handler for `$items(nodeName?, outputIndex?, runIndex?)` — the
550
- * global accessor for a node's execution data. Reads the literal
551
- * `$items` property off `data` (host-wired by `WorkflowDataProxy`)
552
- * and forwards the validated args verbatim. The host applies its own
553
- * defaults when fields are `undefined`.
554
- *
555
- * @private
556
- */
557
- handleGetItems(msg, data) {
558
- return data.$items?.(msg.nodeName, msg.outputIndex, msg.runIndex);
559
- }
560
- /**
561
- * Handler for `$fromAI(name, description?, type?, defaultValue?)` and its
562
- * `$fromAi` / `$fromai` aliases. Reads the literal `$fromAI` property
563
- * off `data` (host-wired) and forwards the args. The host validates
564
- * `name` (required + regex) and applies its own resolution / fallback
565
- * logic, so empty / invalid names surface as the host's structured
566
- * `ExpressionError` rather than a generic zod parse error.
567
- *
568
- * Note: `msg.valueType` maps to the host's third positional parameter
569
- * (`_type` in `WorkflowDataProxy.handleFromAi`). The bridge protocol
570
- * renames it to avoid collision with the `type` discriminator on the
571
- * envelope — the host parameter currently goes unused, but if it ever
572
- * gains a name (`type`), this mapping should stay explicit.
573
- *
574
- * @private
575
- */
576
- handleFromAi(msg, data) {
577
- return data.$fromAI?.(msg.name, msg.description, msg.valueType, msg.defaultValue);
578
- }
579
- /**
580
- * Handlers for the `$('Foo').pairedItem(itemIndex?)` / `.itemMatching(...)` /
581
- * `.item` cluster. Three separate typed RPCs, each reading exactly one
582
- * literal property off the host node proxy.
583
- *
584
- * The split is load-bearing: the host's `pairedItemMethod` closure
585
- * captures which property name the proxy `get` trap saw, and uses
586
- * that to pick the right error message (e.g. "Missing item index for
587
- * .itemMatching()") and to decide between method-call vs getter
588
- * semantics for `.item`. Reading the matching property here lets
589
- * those host-side branches fire exactly as they do in the legacy
590
- * engine; no in-isolate validation needed.
591
- *
592
- * @private
593
- */
594
- handleGetNodePairedItem(msg, data) {
595
- return data.$?.(msg.nodeName)?.pairedItem?.(msg.itemIndex);
596
- }
597
- handleGetNodeItemMatching(msg, data) {
598
- return data.$?.(msg.nodeName)?.itemMatching?.(msg.itemIndex);
599
- }
600
- handleGetNodeItem(msg, data) {
601
- // `.item` is a host getter — accessing it invokes the resolver and
602
- // returns the value immediately. Optional chaining only short-
603
- // circuits on null/undefined; the getter still fires on access.
604
- return data.$?.(msg.nodeName)?.item;
605
- }
606
- /**
607
- * Handler for `$evaluateExpression(expression, itemIndex?)`. Forwards
608
- * the string to the host's nested-evaluation helper, which re-enters
609
- * the expression engine on the inner expression. Under the VM engine
610
- * this round-trips through the bridge again as a new evaluation on the
611
- * enclosing call's time budget, which is the same shape the legacy
612
- * engine supports.
613
- *
614
- * @private
615
- */
616
- handleEvaluateExpression(msg, data) {
617
- return data.$evaluateExpression?.(msg.expression, msg.itemIndex);
618
- }
619
- /**
620
- * Handler for `$getPairedItem(destinationNodeName, incomingSourceData,
621
- * initialPairedItem)`. Forwards directly to the host binding, which
622
- * walks the paired-item ancestry chain back to the named upstream node
623
- * and returns the matching execution item.
624
- *
625
- * The two trailing host parameters — `usedMethodName` and
626
- * `nodeBeforeLast` — are deliberately not part of the wire protocol:
627
- * the host's default for `usedMethodName` is already `$getPairedItem`,
628
- * and `nodeBeforeLast` is an internal recursion argument the host sets
629
- * during traversal.
630
- *
631
- * @private
632
- */
633
- handleGetPairedItem(msg, data) {
634
- return data.$getPairedItem?.(msg.destinationNodeName, msg.incomingSourceData, msg.initialPairedItem);
635
- }
636
353
  /**
637
354
  * Execute JavaScript code in the isolated context.
638
355
  *
@@ -721,11 +438,13 @@ try {
721
438
  };
722
439
  }`;
723
440
  const result = this.context.evalClosureSync(wrappedCode, [getValueAtPath, getArrayElement, callHost], { result: { copy: true }, timeout });
724
- if (isErrorSentinel(result)) {
725
- throw this.reconstructError(result);
441
+ if ((0, host_functions_1.isErrorSentinel)(result)) {
442
+ throw (0, host_functions_1.reconstructError)(result);
726
443
  }
727
444
  this.logger.debug('[IsolatedVmBridge] Expression executed successfully');
728
- return result;
445
+ // The structured clone above drops the prototype of a class instance, so
446
+ // rebuild the luxon instances from the markers the isolate sent.
447
+ return (0, luxon_transfer_1.unwrapLuxonValues)(result);
729
448
  }
730
449
  catch (error) {
731
450
  // Re-throw reconstructed errors as-is.
@@ -756,24 +475,6 @@ try {
756
475
  ? `Nested expressions timed out after sharing the ${this.config.timeout}ms limit`
757
476
  : `Expression timed out after ${this.config.timeout}ms`, {});
758
477
  }
759
- /**
760
- * Reconstruct an error from serialized isolate data.
761
- *
762
- * Maps error names back to their host-side classes and restores
763
- * custom properties that would otherwise be lost crossing the boundary.
764
- */
765
- reconstructError(data) {
766
- const error = new Error(data.message);
767
- error.name = data.name || 'Error';
768
- if (data.stack) {
769
- error.stack = data.stack;
770
- }
771
- // Restore custom properties transferred via copy: true
772
- if (data.extra) {
773
- Object.assign(error, data.extra);
774
- }
775
- return error;
776
- }
777
478
  /**
778
479
  * Dispose of the isolate and free resources.
779
480
  *