@n8n/expression-runtime 0.30.0 → 0.31.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 +4 -0
- package/dist/bundle/runtime.esm.js +17 -17
- package/dist/bundle/runtime.esm.js.map +4 -4
- package/dist/bundle/runtime.iife.js +17 -17
- package/dist/bundle/runtime.iife.js.map +4 -4
- package/dist/cjs/bridge/host-functions.d.ts +74 -0
- package/dist/cjs/bridge/host-functions.d.ts.map +1 -0
- package/dist/cjs/bridge/host-functions.js +336 -0
- package/dist/cjs/bridge/host-functions.js.map +1 -0
- package/dist/cjs/bridge/isolated-vm-bridge.d.ts +24 -150
- package/dist/cjs/bridge/isolated-vm-bridge.d.ts.map +1 -1
- package/dist/cjs/bridge/isolated-vm-bridge.js +130 -429
- package/dist/cjs/bridge/isolated-vm-bridge.js.map +1 -1
- package/dist/cjs/bridge/quickjs-bridge.d.ts +21 -6
- package/dist/cjs/bridge/quickjs-bridge.d.ts.map +1 -1
- package/dist/cjs/bridge/quickjs-bridge.js +377 -402
- package/dist/cjs/bridge/quickjs-bridge.js.map +1 -1
- package/dist/cjs/build.tsbuildinfo +1 -1
- package/dist/cjs/evaluator/expression-evaluator.d.ts +17 -0
- package/dist/cjs/evaluator/expression-evaluator.d.ts.map +1 -1
- package/dist/cjs/evaluator/expression-evaluator.js +61 -1
- 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 +7 -15
- package/dist/cjs/extensions/array-extensions.js.map +1 -1
- package/dist/cjs/extensions/function-extensions.d.ts +2 -0
- package/dist/cjs/extensions/function-extensions.d.ts.map +1 -1
- package/dist/cjs/extensions/function-extensions.js +13 -10
- package/dist/cjs/extensions/function-extensions.js.map +1 -1
- package/dist/cjs/extensions/object-extensions.d.ts.map +1 -1
- package/dist/cjs/extensions/object-extensions.js +3 -11
- package/dist/cjs/extensions/object-extensions.js.map +1 -1
- package/dist/cjs/extensions/utils.d.ts +1 -0
- package/dist/cjs/extensions/utils.d.ts.map +1 -1
- package/dist/cjs/extensions/utils.js +10 -0
- package/dist/cjs/extensions/utils.js.map +1 -1
- package/dist/cjs/observability/metrics.d.ts +12 -0
- package/dist/cjs/observability/metrics.d.ts.map +1 -1
- package/dist/cjs/observability/metrics.js +12 -0
- package/dist/cjs/observability/metrics.js.map +1 -1
- package/dist/cjs/runtime/lazy-proxy.d.ts +2 -0
- package/dist/cjs/runtime/lazy-proxy.d.ts.map +1 -1
- package/dist/cjs/runtime/lazy-proxy.js +29 -15
- package/dist/cjs/runtime/lazy-proxy.js.map +1 -1
- package/dist/cjs/runtime/luxon-transfer.d.ts +62 -0
- package/dist/cjs/runtime/luxon-transfer.d.ts.map +1 -0
- package/dist/cjs/runtime/luxon-transfer.js +200 -0
- package/dist/cjs/runtime/luxon-transfer.js.map +1 -0
- package/dist/cjs/runtime/serialize.d.ts +7 -7
- package/dist/cjs/runtime/serialize.d.ts.map +1 -1
- package/dist/cjs/runtime/serialize.js +42 -30
- package/dist/cjs/runtime/serialize.js.map +1 -1
- package/dist/cjs/runtime/transfer.d.ts +77 -0
- package/dist/cjs/runtime/transfer.d.ts.map +1 -0
- package/dist/cjs/runtime/transfer.js +183 -0
- package/dist/cjs/runtime/transfer.js.map +1 -0
- package/dist/cjs/types/bridge.d.ts +16 -0
- package/dist/cjs/types/bridge.d.ts.map +1 -1
- package/dist/cjs/types/bridge.js +1 -0
- package/dist/cjs/types/bridge.js.map +1 -1
- package/dist/cjs/types/evaluator.d.ts +9 -0
- package/dist/cjs/types/evaluator.d.ts.map +1 -1
- package/dist/cjs/types/evaluator.js.map +1 -1
- package/dist/esm/bridge/host-functions.d.ts +74 -0
- package/dist/esm/bridge/host-functions.d.ts.map +1 -0
- package/dist/esm/bridge/host-functions.js +328 -0
- package/dist/esm/bridge/host-functions.js.map +1 -0
- package/dist/esm/bridge/isolated-vm-bridge.d.ts +24 -150
- package/dist/esm/bridge/isolated-vm-bridge.d.ts.map +1 -1
- package/dist/esm/bridge/isolated-vm-bridge.js +126 -425
- package/dist/esm/bridge/isolated-vm-bridge.js.map +1 -1
- package/dist/esm/bridge/quickjs-bridge.d.ts +21 -6
- package/dist/esm/bridge/quickjs-bridge.d.ts.map +1 -1
- package/dist/esm/bridge/quickjs-bridge.js +368 -393
- package/dist/esm/bridge/quickjs-bridge.js.map +1 -1
- package/dist/esm/build.tsbuildinfo +1 -1
- package/dist/esm/evaluator/expression-evaluator.d.ts +17 -0
- package/dist/esm/evaluator/expression-evaluator.d.ts.map +1 -1
- package/dist/esm/evaluator/expression-evaluator.js +61 -1
- 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 +1 -9
- package/dist/esm/extensions/array-extensions.js.map +1 -1
- package/dist/esm/extensions/function-extensions.d.ts +2 -0
- package/dist/esm/extensions/function-extensions.d.ts.map +1 -1
- package/dist/esm/extensions/function-extensions.js +12 -9
- package/dist/esm/extensions/function-extensions.js.map +1 -1
- package/dist/esm/extensions/object-extensions.d.ts.map +1 -1
- package/dist/esm/extensions/object-extensions.js +1 -9
- package/dist/esm/extensions/object-extensions.js.map +1 -1
- package/dist/esm/extensions/utils.d.ts +1 -0
- package/dist/esm/extensions/utils.d.ts.map +1 -1
- package/dist/esm/extensions/utils.js +9 -0
- package/dist/esm/extensions/utils.js.map +1 -1
- package/dist/esm/observability/metrics.d.ts +12 -0
- package/dist/esm/observability/metrics.d.ts.map +1 -1
- package/dist/esm/observability/metrics.js +12 -0
- package/dist/esm/observability/metrics.js.map +1 -1
- package/dist/esm/runtime/lazy-proxy.d.ts +2 -0
- package/dist/esm/runtime/lazy-proxy.d.ts.map +1 -1
- package/dist/esm/runtime/lazy-proxy.js +29 -15
- package/dist/esm/runtime/lazy-proxy.js.map +1 -1
- package/dist/esm/runtime/luxon-transfer.d.ts +62 -0
- package/dist/esm/runtime/luxon-transfer.d.ts.map +1 -0
- package/dist/esm/runtime/luxon-transfer.js +183 -0
- package/dist/esm/runtime/luxon-transfer.js.map +1 -0
- package/dist/esm/runtime/serialize.d.ts +7 -7
- package/dist/esm/runtime/serialize.d.ts.map +1 -1
- package/dist/esm/runtime/serialize.js +42 -30
- package/dist/esm/runtime/serialize.js.map +1 -1
- package/dist/esm/runtime/transfer.d.ts +77 -0
- package/dist/esm/runtime/transfer.d.ts.map +1 -0
- package/dist/esm/runtime/transfer.js +173 -0
- package/dist/esm/runtime/transfer.js.map +1 -0
- package/dist/esm/types/bridge.d.ts +16 -0
- package/dist/esm/types/bridge.d.ts.map +1 -1
- package/dist/esm/types/bridge.js +1 -0
- package/dist/esm/types/bridge.js.map +1 -1
- package/dist/esm/types/evaluator.d.ts +9 -0
- package/dist/esm/types/evaluator.d.ts.map +1 -1
- package/dist/esm/types/evaluator.js.map +1 -1
- package/package.json +6 -6
|
@@ -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
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
275
|
-
*
|
|
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)((
|
|
297
|
+
return new (getIvm().Callback)((pathArr) => {
|
|
288
298
|
try {
|
|
289
|
-
|
|
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
|
-
*
|
|
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)((
|
|
317
|
+
return new (getIvm().Callback)((pathArr, index) => {
|
|
361
318
|
try {
|
|
362
|
-
|
|
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
|
|
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
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
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:
|
|
448
|
-
* data. Results cross into the isolate through an ivm.Callback, which
|
|
449
|
-
* them via the structured-clone algorithm —
|
|
450
|
-
* isolated-vm objects (`Reference`/`ExternalCopy`) or other
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
*
|