@n8n/expression-runtime 0.2.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.
@@ -0,0 +1,427 @@
1
+ # Expression Runtime Architecture
2
+
3
+ This package provides a secure, isolated expression evaluation runtime that works across multiple execution environments (isolated-vm, Web Workers, and task runners).
4
+
5
+ ## Design Goals
6
+
7
+ 1. **Environment Agnostic**: Single codebase that works in Node.js (isolated-vm), browsers (Web Workers), and task runner processes
8
+ 2. **Security**: Expressions run in isolated contexts with memory limits and timeouts
9
+ 3. **Performance**: Lazy data loading, code caching, and efficient data transfer
10
+ 4. **Observability**: Built-in metrics, traces, and logs
11
+ 5. **Maintainability**: Clear separation of concerns with well-defined interfaces
12
+
13
+ ## Three-Layer Architecture
14
+
15
+ The architecture is split into three distinct layers:
16
+
17
+ ```
18
+ ┌─────────────────────────────────────────────────────────┐
19
+ │ Host Process │
20
+ │ │
21
+ │ ┌────────────────────────────────────────────────┐ │
22
+ │ │ ExpressionEvaluator (Layer 3) │ │
23
+ │ │ - Public API │ │
24
+ │ │ - Tournament integration │ │
25
+ │ │ - Code caching │ │
26
+ │ │ - Observability │ │
27
+ │ └────────────────┬───────────────────────────────┘ │
28
+ │ │ │
29
+ │ ┌────────────────▼───────────────────────────────┐ │
30
+ │ │ Bridge (Layer 2) │ │
31
+ │ │ - IsolatedVmBridge (Phase 1.1) │ │
32
+ │ │ - WebWorkerBridge (Phase 2+) │ │
33
+ │ │ - Task Runner Integration (TBD) │ │
34
+ │ └────────────────┬───────────────────────────────┘ │
35
+ │ │ IPC/Message Passing │
36
+ └───────────────────┼─────────────────────────────────────┘
37
+
38
+ ┌───────────────────▼─────────────────────────────────────┐
39
+ │ Isolated Context │
40
+ │ │
41
+ │ ┌────────────────────────────────────────────────┐ │
42
+ │ │ Runtime (Layer 1) │ │
43
+ │ │ - Runs inside isolation │ │
44
+ │ │ - No Node.js dependencies │ │
45
+ │ │ - Lazy loading proxies │ │
46
+ │ │ - Helper functions ($json, $item, etc.) │ │
47
+ │ │ - lodash, Luxon │ │
48
+ │ └────────────────────────────────────────────────┘ │
49
+ │ │
50
+ └─────────────────────────────────────────────────────────┘
51
+ ```
52
+
53
+ ### Layer 1: Runtime (Isolated Context)
54
+
55
+ **Location**: Runs inside the isolated context (isolate, worker, subprocess)
56
+
57
+ **Purpose**: Provides the JavaScript execution environment for expressions
58
+
59
+ **Key Components**:
60
+ - **Lazy Loading Proxies**: Fetch data fields on-demand from host to avoid memory limits
61
+ - **Helper Functions**: `$json`, `$item`, `$input`, `$`, etc.
62
+ - **Libraries**: lodash, Luxon (bundled)
63
+ - **No Node.js APIs**: Pure JavaScript only
64
+
65
+ **Bundle**: IIFE format for isolated-vm, ESM for Web Workers
66
+
67
+ ### Layer 2: Bridge (Host Process)
68
+
69
+ **Location**: Runs in the host process
70
+
71
+ **Purpose**: Manages communication between host and isolated context
72
+
73
+ **Key Components**:
74
+ - **RuntimeBridge Interface**: Abstract interface for all bridge implementations
75
+ - **IsolatedVmBridge**: Uses isolated-vm API for Node.js backend (Phase 1.1)
76
+ - **WebWorkerBridge**: Uses postMessage API for browser (Phase 2+)
77
+ - **Task Runner Integration**: TBD - May use IsolatedVmBridge locally or direct evaluation (Phase 2+)
78
+
79
+ **Responsibilities**:
80
+ - Initialize isolated context
81
+ - Transfer code to context
82
+ - Handle data requests from runtime (lazy loading)
83
+ - Enforce memory limits and timeouts
84
+ - Dispose of context when needed
85
+
86
+ ### Layer 3: Evaluator (Host Process)
87
+
88
+ **Location**: Runs in the host process
89
+
90
+ **Purpose**: Public API for expression evaluation
91
+
92
+ **Key Components**:
93
+ - **ExpressionEvaluator**: Main class used by workflow package
94
+ - **Tournament Integration**: AST transformation and security validation
95
+ - **Code Cache**: Cache transformed code (not evaluation results)
96
+ - **Observability**: Emit metrics, traces, and logs
97
+
98
+ **Responsibilities**:
99
+ - Accept expression strings and workflow data
100
+ - Transform expressions with Tournament
101
+ - Cache transformed code to avoid re-transformation
102
+ - Convert WorkflowData to WorkflowDataProxy for lazy loading
103
+ - Use bridge to evaluate in isolated context
104
+ - Handle errors gracefully
105
+ - Emit observability data
106
+
107
+ ## Data Flow
108
+
109
+ ### Expression Evaluation Flow
110
+
111
+ ```mermaid
112
+ sequenceDiagram
113
+ participant WF as Workflow
114
+ participant Eval as ExpressionEvaluator
115
+ participant Bridge as IsolatedVmBridge
116
+ participant Runtime as Runtime (Isolated)
117
+
118
+ WF->>Eval: evaluate(expr, data)
119
+ Eval->>Eval: Transform with Tournament (cached)
120
+ Eval->>Bridge: execute(transformedCode, data)
121
+ Bridge->>Bridge: registerCallbacks(data) — creates ivm.Reference callbacks
122
+ Bridge->>Runtime: resetDataProxies() — initialise $json, $input, etc. as lazy proxies
123
+ Bridge->>Runtime: run wrapped code (this === __data)
124
+ Runtime->>Runtime: Access $json.field
125
+ Runtime->>Bridge: __getValueAtPath(['$json','field']) via ivm.Reference
126
+ Bridge->>Bridge: Navigate data object
127
+ Bridge-->>Runtime: Metadata or primitive
128
+ Runtime-->>Bridge: Expression result
129
+ Bridge-->>Eval: Result (copied from isolate)
130
+ Eval-->>WF: Result
131
+ ```
132
+
133
+ ### Lazy Data Loading
134
+
135
+ Data access from inside the isolate goes through `ivm.Reference` callbacks
136
+ registered by the bridge — not through a method on `RuntimeBridge` itself.
137
+
138
+ ```mermaid
139
+ sequenceDiagram
140
+ participant Runtime as Runtime (Isolated)
141
+ participant Proxy as Lazy Proxy
142
+ participant Bridge as IsolatedVmBridge (host)
143
+
144
+ Runtime->>Proxy: $json.user.email
145
+ Proxy->>Bridge: __getValueAtPath(['$json','user','email']) via ivm.Reference
146
+ Bridge->>Bridge: Navigate data object registered via registerCallbacks()
147
+ Bridge-->>Proxy: "test@example.com" (primitive copied into isolate)
148
+ Proxy-->>Runtime: "test@example.com"
149
+ ```
150
+
151
+ ## Environment-Specific Implementations
152
+
153
+ ### IsolatedVmBridge (Node.js Backend)
154
+
155
+ Uses [isolated-vm](https://github.com/laverdet/isolated-vm) for V8 isolate-based isolation:
156
+
157
+ ```typescript
158
+ class IsolatedVmBridge implements RuntimeBridge {
159
+ private isolate: ivm.Isolate;
160
+ private context: ivm.Context;
161
+
162
+ async initialize(): Promise<void> {
163
+ this.isolate = new ivm.Isolate({
164
+ memoryLimit: 128
165
+ });
166
+ this.context = await this.isolate.createContext();
167
+
168
+ // Load runtime code
169
+ await this.context.eval(runtimeCode);
170
+ }
171
+
172
+ async execute(code: string, dataId: string): Promise<unknown> {
173
+ // Implementation...
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### WebWorkerBridge (Browser Frontend)
179
+
180
+ Uses Web Workers for browser-based isolation:
181
+
182
+ ```typescript
183
+ class WebWorkerBridge implements RuntimeBridge {
184
+ private worker: Worker;
185
+
186
+ async initialize(): Promise<void> {
187
+ this.worker = new Worker('/runtime.worker.js');
188
+ // Setup message handlers
189
+ }
190
+
191
+ async execute(code: string, dataId: string): Promise<unknown> {
192
+ // Implementation...
193
+ }
194
+ }
195
+ ```
196
+
197
+ ### Task Runner Integration (TBD - Phase 2+)
198
+
199
+ Task runners already provide process-level isolation. When code nodes call `evaluateExpression()`, evaluation happens **inside the task runner** (not via IPC to worker).
200
+
201
+ **Architecture decision pending - two options**:
202
+
203
+ **Option A**: Task runner uses `IsolatedVmBridge` locally
204
+ ```typescript
205
+ // Inside task runner process
206
+ const evaluator = new ExpressionEvaluator({
207
+ bridge: new IsolatedVmBridge(config), // Evaluates locally
208
+ });
209
+
210
+ // Code node calls evaluateExpression()
211
+ const result = await evaluator.evaluate(expression, workflowData);
212
+ // ^ All happens inside task runner, no IPC, no lazy loading needed
213
+ ```
214
+
215
+ **Option B**: Task runner evaluates directly (no extra sandbox)
216
+ ```typescript
217
+ // Task runner already isolated at process level
218
+ // No need for isolated-vm sandbox on top
219
+ const result = evaluateExpressionDirectly(expression, workflowData);
220
+ ```
221
+
222
+ **Key point**: Task runner already has all workflow data, so no lazy loading or IPC communication is needed for data access.
223
+
224
+ ## Package Structure
225
+
226
+ ```
227
+ packages/@n8n/expression-runtime/
228
+ ├── ARCHITECTURE.md # This file
229
+ ├── README.md
230
+ ├── package.json
231
+ ├── tsconfig.json
232
+ ├── tsconfig.build.json
233
+ ├── vitest.config.ts
234
+ ├── esbuild.config.js # Bundles src/runtime/index.ts → dist/bundle/runtime.iife.js
235
+
236
+ ├── src/
237
+ │ ├── index.ts # Public API exports
238
+ │ │
239
+ │ ├── types/ # TypeScript interfaces (no implementations)
240
+ │ │ ├── index.ts
241
+ │ │ ├── bridge.ts # RuntimeBridge, BridgeConfig
242
+ │ │ ├── evaluator.ts # IExpressionEvaluator, EvaluatorConfig, error classes
243
+ │ │ └── runtime.ts # RuntimeHostInterface, RuntimeGlobals, RuntimeError
244
+ │ │
245
+ │ ├── runtime/ # Layer 1: runs inside the V8 isolate
246
+ │ │ └── index.ts # Proxy system, resetDataProxies, __sanitize,
247
+ │ │ # SafeObject, SafeError, Lodash/Luxon wiring,
248
+ │ │ # all extension functions
249
+ │ │
250
+ │ ├── bridge/ # Layer 2: host-process isolate management
251
+ │ │ └── isolated-vm-bridge.ts # IsolatedVmBridge (ivm.Isolate, callbacks, script cache)
252
+ │ │
253
+ │ ├── evaluator/ # Layer 3: public-facing API
254
+ │ │ └── expression-evaluator.ts # Tournament integration, expression code cache
255
+ │ │
256
+ │ ├── extensions/ # Expression extension functions (bundled into runtime)
257
+ │ │ ├── array-extensions.ts
258
+ │ │ ├── boolean-extensions.ts
259
+ │ │ ├── date-extensions.ts
260
+ │ │ ├── number-extensions.ts
261
+ │ │ ├── object-extensions.ts
262
+ │ │ ├── string-extensions.ts
263
+ │ │ ├── extend.ts
264
+ │ │ ├── extensions.ts
265
+ │ │ ├── expression-extension-error.ts
266
+ │ │ └── utils.ts
267
+ │ │
268
+ │ └── __tests__/
269
+ │ └── integration.test.ts
270
+
271
+ └── dist/
272
+ ├── *.js / *.d.ts # Compiled TypeScript (tsc output)
273
+ └── bundle/
274
+ └── runtime.iife.js # Self-contained IIFE loaded into isolated-vm
275
+ ```
276
+
277
+ ## Key Design Decisions
278
+
279
+ ### 1. Why Three Layers?
280
+
281
+ **Separation of Concerns**: Each layer has a single responsibility:
282
+ - Runtime: Execute expressions in isolation
283
+ - Bridge: Handle environment-specific communication
284
+ - Evaluator: Provide clean API with observability
285
+
286
+ **Environment Agnostic**: The Runtime and Evaluator layers are identical across all environments. Only the Bridge changes.
287
+
288
+ ### 2. Why Lazy Loading?
289
+
290
+ **Memory Efficiency**: Large workflow data (100MB+) cannot fit in isolate memory limits (128MB). Lazy loading fetches only the fields that expressions actually access.
291
+
292
+ **Performance**: Transferring only accessed fields is faster than transferring entire objects.
293
+
294
+ **Limitation**: Lazy loading requires **synchronous** callbacks from runtime to host. This works for:
295
+ - ✅ **isolated-vm**: Uses `ivm.Reference` for true synchronous callbacks
296
+ - ✅ **Node.js vm**: Direct synchronous function calls
297
+ - ❌ **Web Workers**: postMessage is always async (see Known Limitations below)
298
+
299
+ ### 3. Why Bundle the Runtime?
300
+
301
+ **No Node.js Dependencies**: Runtime must work in environments without Node.js (browser, isolated-vm). Bundling produces a self-contained IIFE/ESM module.
302
+
303
+ **Immutability**: Bundled runtime is immutable and can be cached.
304
+
305
+ ### 4. Why Abstract Bridge?
306
+
307
+ **Future-Proofing**: Frontend will use Web Workers. Backend uses isolated-vm. Abstract bridge allows adding new environments without changing other layers.
308
+
309
+ **Testing**: NodeVmBridge allows fast testing without native isolated-vm dependency.
310
+
311
+ ## Known Limitations
312
+
313
+ ### Lazy Loading with Async Boundaries
314
+
315
+ JavaScript Proxy trap handlers are **synchronous**, which creates a fundamental limitation:
316
+
317
+ ```javascript
318
+ const proxy = new Proxy({}, {
319
+ get(target, prop) {
320
+ // This handler MUST be synchronous
321
+ // Cannot use await or return Promise
322
+ return someValue;
323
+ }
324
+ });
325
+ ```
326
+
327
+ **Impact by Environment**:
328
+
329
+ 1. **isolated-vm** ✅
330
+ - Uses `ivm.Reference` for true synchronous callbacks from isolate to host
331
+ - Full lazy loading support
332
+
333
+ 2. **Node.js vm** ✅
334
+ - Direct synchronous function calls
335
+ - Full lazy loading support (used for testing)
336
+
337
+ 3. **Web Workers** ❌
338
+ - `postMessage` is always async
339
+ - **Phase 1 Limitation**: No lazy loading, must pre-fetch all data before evaluation
340
+ - **Future Enhancement (Phase 2+)**: Explore `SharedArrayBuffer` + `Atomics` for synchronous data access
341
+
342
+ ### Web Worker Support Roadmap
343
+
344
+ **Phase 1** (Initial implementation):
345
+ - WebWorkerBridge will pre-fetch all workflow data
346
+ - Transfer complete data object to worker before evaluation
347
+ - Works for small/medium datasets (< 50MB)
348
+ - No lazy loading benefit
349
+
350
+ **Phase 2+** (Future enhancement):
351
+ - Investigate `SharedArrayBuffer` + `Atomics` for sync access
352
+ - Or accept pre-fetching as the Web Worker approach
353
+ - Decision based on real-world usage patterns
354
+
355
+ ### Security Boundaries
356
+
357
+ The runtime has **no access** to:
358
+ - ❌ Node.js APIs (fs, net, child_process, etc.)
359
+ - ❌ Host process memory
360
+ - ❌ Other isolates/workers
361
+ - ❌ Cookies
362
+
363
+ The runtime **can only**:
364
+ - ✅ Call `getDataSync()` to fetch workflow data
365
+ - ✅ Access lodash and Luxon libraries
366
+ - ✅ Execute pure JavaScript code
367
+
368
+ ## Testing Strategy
369
+
370
+ **Runtime Tests** (vitest):
371
+ - Use NodeVmBridge for fast, isolated tests
372
+ - Test lazy loading, helpers, error handling
373
+ - No native dependencies required
374
+
375
+ **Bridge Tests** (vitest):
376
+ - Test each bridge implementation
377
+ - Mock environment-specific APIs
378
+ - Test memory limits, timeouts, disposal
379
+
380
+ **Evaluator Tests** (vitest):
381
+ - Test Tournament integration (transformation and validation)
382
+ - Test code caching (transformed code, not results)
383
+ - Test WorkflowData to WorkflowDataProxy conversion
384
+ - Test observability emission
385
+ - Test error handling
386
+
387
+ **Integration Tests** (jest in workflow package):
388
+ - Test full stack with real isolated-vm
389
+ - Test concurrent evaluations
390
+ - Test with real workflow data
391
+
392
+ ## Observability
393
+
394
+ All layers emit metrics, traces, and logs:
395
+
396
+ **Metrics**:
397
+ - `expression.evaluation.count`
398
+ - `expression.evaluation.duration_ms`
399
+ - `expression.code_cache.hit` (transformed code cache)
400
+ - `expression.code_cache.miss`
401
+ - `expression.isolate.memory_mb`
402
+
403
+ **Traces**:
404
+ - `expression.evaluate` span wraps entire evaluation
405
+ - `expression.tournament` span for AST transformation
406
+ - `expression.isolate.execute` span for isolated execution
407
+
408
+ **Logs**:
409
+ - Errors at all levels
410
+ - Warnings for memory pressure
411
+ - Debug logs for development
412
+
413
+ See observability package documentation for details.
414
+
415
+ ## Next Steps
416
+
417
+ 1. Implement TypeScript interfaces (Phase 0.1)
418
+ 2. Implement observability infrastructure (Phase 0.2)
419
+ 3. Create comprehensive benchmarks (Phase 0.3)
420
+ 4. Implement runtime package (Phase 1.1)
421
+ 5. Implement isolate pooling (Phase 1.2)
422
+
423
+ ## References
424
+
425
+ - [isolated-vm GitHub](https://github.com/laverdet/isolated-vm)
426
+ - [Web Workers MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
427
+ - [n8n workflow package](../workflow/)
package/LICENSE.md ADDED
@@ -0,0 +1,88 @@
1
+ # License
2
+
3
+ Portions of this software are licensed as follows:
4
+
5
+ - Content of branches other than the main branch (i.e. "master") are not licensed.
6
+ - Source code files that contain ".ee." in their filename or ".ee" in their dirname are NOT licensed under
7
+ the Sustainable Use License.
8
+ To use source code files that contain ".ee." in their filename or ".ee" in their dirname you must hold a
9
+ valid n8n Enterprise License specifically allowing you access to such source code files and as defined
10
+ in "LICENSE_EE.md".
11
+ - All third party components incorporated into the n8n Software are licensed under the original license
12
+ provided by the owner of the applicable component.
13
+ - Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
14
+ License" as defined below.
15
+
16
+ ## Sustainable Use License
17
+
18
+ Version 1.0
19
+
20
+ ### Acceptance
21
+
22
+ By using the software, you agree to all of the terms and conditions below.
23
+
24
+ ### Copyright License
25
+
26
+ The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
27
+ to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
28
+ to the limitations below.
29
+
30
+ ### Limitations
31
+
32
+ You may use or modify the software only for your own internal business purposes or for non-commercial or
33
+ personal use. You may distribute the software or provide it to others only if you do so free of charge for
34
+ non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
35
+ the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
36
+
37
+ ### Patents
38
+
39
+ The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
40
+ license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
41
+ subject to the limitations and conditions in this license. This license does not cover any patent claims that
42
+ you cause to be infringed by modifications or additions to the software. If you or your company make any
43
+ written claim that the software infringes or contributes to infringement of any patent, your patent license
44
+ for the software granted under these terms ends immediately. If your company makes such a claim, your patent
45
+ license ends immediately for work on behalf of your company.
46
+
47
+ ### Notices
48
+
49
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
50
+ terms. If you modify the software, you must include in any modified copies of the software a prominent notice
51
+ stating that you have modified the software.
52
+
53
+ ### No Other Rights
54
+
55
+ These terms do not imply any licenses other than those expressly granted in these terms.
56
+
57
+ ### Termination
58
+
59
+ If you use the software in violation of these terms, such use is not licensed, and your license will
60
+ automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
61
+ violation of this license no later than 30 days after you receive that notice, your license will be reinstated
62
+ retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
63
+ terms will cause your license to terminate automatically and permanently.
64
+
65
+ ### No Liability
66
+
67
+ As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
68
+ not be liable to you for any damages arising out of these terms or the use or nature of the software, under
69
+ any kind of legal claim.
70
+
71
+ ### Definitions
72
+
73
+ The “licensor” is the entity offering these terms.
74
+
75
+ The “software” is the software the licensor makes available under these terms, including any portion of it.
76
+
77
+ “You” refers to the individual or entity agreeing to these terms.
78
+
79
+ “Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
80
+ all organizations that have control over, are under the control of, or are under common control with that
81
+ organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
82
+ management and policies by vote, contract, or otherwise. Control can be direct or indirect.
83
+
84
+ “Your license” is the license granted to you for the software under these terms.
85
+
86
+ “Use” means anything you do with the software requiring your license.
87
+
88
+ “Trademark” means trademarks, service marks, and similar rights.