@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.
Files changed (122) 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 +2 -0
  27. package/dist/cjs/extensions/function-extensions.d.ts.map +1 -1
  28. package/dist/cjs/extensions/function-extensions.js +13 -10
  29. package/dist/cjs/extensions/function-extensions.js.map +1 -1
  30. package/dist/cjs/extensions/object-extensions.d.ts.map +1 -1
  31. package/dist/cjs/extensions/object-extensions.js +3 -11
  32. package/dist/cjs/extensions/object-extensions.js.map +1 -1
  33. package/dist/cjs/extensions/utils.d.ts +1 -0
  34. package/dist/cjs/extensions/utils.d.ts.map +1 -1
  35. package/dist/cjs/extensions/utils.js +10 -0
  36. package/dist/cjs/extensions/utils.js.map +1 -1
  37. package/dist/cjs/observability/metrics.d.ts +12 -0
  38. package/dist/cjs/observability/metrics.d.ts.map +1 -1
  39. package/dist/cjs/observability/metrics.js +12 -0
  40. package/dist/cjs/observability/metrics.js.map +1 -1
  41. package/dist/cjs/runtime/lazy-proxy.d.ts +2 -0
  42. package/dist/cjs/runtime/lazy-proxy.d.ts.map +1 -1
  43. package/dist/cjs/runtime/lazy-proxy.js +29 -15
  44. package/dist/cjs/runtime/lazy-proxy.js.map +1 -1
  45. package/dist/cjs/runtime/luxon-transfer.d.ts +62 -0
  46. package/dist/cjs/runtime/luxon-transfer.d.ts.map +1 -0
  47. package/dist/cjs/runtime/luxon-transfer.js +200 -0
  48. package/dist/cjs/runtime/luxon-transfer.js.map +1 -0
  49. package/dist/cjs/runtime/serialize.d.ts +7 -7
  50. package/dist/cjs/runtime/serialize.d.ts.map +1 -1
  51. package/dist/cjs/runtime/serialize.js +42 -30
  52. package/dist/cjs/runtime/serialize.js.map +1 -1
  53. package/dist/cjs/runtime/transfer.d.ts +77 -0
  54. package/dist/cjs/runtime/transfer.d.ts.map +1 -0
  55. package/dist/cjs/runtime/transfer.js +183 -0
  56. package/dist/cjs/runtime/transfer.js.map +1 -0
  57. package/dist/cjs/types/bridge.d.ts +16 -0
  58. package/dist/cjs/types/bridge.d.ts.map +1 -1
  59. package/dist/cjs/types/bridge.js +1 -0
  60. package/dist/cjs/types/bridge.js.map +1 -1
  61. package/dist/cjs/types/evaluator.d.ts +9 -0
  62. package/dist/cjs/types/evaluator.d.ts.map +1 -1
  63. package/dist/cjs/types/evaluator.js.map +1 -1
  64. package/dist/esm/bridge/host-functions.d.ts +74 -0
  65. package/dist/esm/bridge/host-functions.d.ts.map +1 -0
  66. package/dist/esm/bridge/host-functions.js +328 -0
  67. package/dist/esm/bridge/host-functions.js.map +1 -0
  68. package/dist/esm/bridge/isolated-vm-bridge.d.ts +24 -150
  69. package/dist/esm/bridge/isolated-vm-bridge.d.ts.map +1 -1
  70. package/dist/esm/bridge/isolated-vm-bridge.js +126 -425
  71. package/dist/esm/bridge/isolated-vm-bridge.js.map +1 -1
  72. package/dist/esm/bridge/quickjs-bridge.d.ts +21 -6
  73. package/dist/esm/bridge/quickjs-bridge.d.ts.map +1 -1
  74. package/dist/esm/bridge/quickjs-bridge.js +368 -393
  75. package/dist/esm/bridge/quickjs-bridge.js.map +1 -1
  76. package/dist/esm/build.tsbuildinfo +1 -1
  77. package/dist/esm/evaluator/expression-evaluator.d.ts +17 -0
  78. package/dist/esm/evaluator/expression-evaluator.d.ts.map +1 -1
  79. package/dist/esm/evaluator/expression-evaluator.js +61 -1
  80. package/dist/esm/evaluator/expression-evaluator.js.map +1 -1
  81. package/dist/esm/extensions/array-extensions.d.ts.map +1 -1
  82. package/dist/esm/extensions/array-extensions.js +1 -9
  83. package/dist/esm/extensions/array-extensions.js.map +1 -1
  84. package/dist/esm/extensions/function-extensions.d.ts +2 -0
  85. package/dist/esm/extensions/function-extensions.d.ts.map +1 -1
  86. package/dist/esm/extensions/function-extensions.js +12 -9
  87. package/dist/esm/extensions/function-extensions.js.map +1 -1
  88. package/dist/esm/extensions/object-extensions.d.ts.map +1 -1
  89. package/dist/esm/extensions/object-extensions.js +1 -9
  90. package/dist/esm/extensions/object-extensions.js.map +1 -1
  91. package/dist/esm/extensions/utils.d.ts +1 -0
  92. package/dist/esm/extensions/utils.d.ts.map +1 -1
  93. package/dist/esm/extensions/utils.js +9 -0
  94. package/dist/esm/extensions/utils.js.map +1 -1
  95. package/dist/esm/observability/metrics.d.ts +12 -0
  96. package/dist/esm/observability/metrics.d.ts.map +1 -1
  97. package/dist/esm/observability/metrics.js +12 -0
  98. package/dist/esm/observability/metrics.js.map +1 -1
  99. package/dist/esm/runtime/lazy-proxy.d.ts +2 -0
  100. package/dist/esm/runtime/lazy-proxy.d.ts.map +1 -1
  101. package/dist/esm/runtime/lazy-proxy.js +29 -15
  102. package/dist/esm/runtime/lazy-proxy.js.map +1 -1
  103. package/dist/esm/runtime/luxon-transfer.d.ts +62 -0
  104. package/dist/esm/runtime/luxon-transfer.d.ts.map +1 -0
  105. package/dist/esm/runtime/luxon-transfer.js +183 -0
  106. package/dist/esm/runtime/luxon-transfer.js.map +1 -0
  107. package/dist/esm/runtime/serialize.d.ts +7 -7
  108. package/dist/esm/runtime/serialize.d.ts.map +1 -1
  109. package/dist/esm/runtime/serialize.js +42 -30
  110. package/dist/esm/runtime/serialize.js.map +1 -1
  111. package/dist/esm/runtime/transfer.d.ts +77 -0
  112. package/dist/esm/runtime/transfer.d.ts.map +1 -0
  113. package/dist/esm/runtime/transfer.js +173 -0
  114. package/dist/esm/runtime/transfer.js.map +1 -0
  115. package/dist/esm/types/bridge.d.ts +16 -0
  116. package/dist/esm/types/bridge.d.ts.map +1 -1
  117. package/dist/esm/types/bridge.js +1 -0
  118. package/dist/esm/types/bridge.js.map +1 -1
  119. package/dist/esm/types/evaluator.d.ts +9 -0
  120. package/dist/esm/types/evaluator.d.ts.map +1 -1
  121. package/dist/esm/types/evaluator.js.map +1 -1
  122. package/package.json +6 -6
@@ -34,11 +34,22 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.QuickJsBridge = 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 transfer_1 = require("../runtime/transfer");
42
+ const lru_cache_1 = require("../evaluator/lru-cache");
43
+ const host_functions_1 = require("./host-functions");
41
44
  let _quickjs = null;
45
+ /**
46
+ * The instantiated WASM module, cached after the first async initialize().
47
+ * Creating runtimes/contexts from it is synchronous, so once it is cached
48
+ * (pool warmup does this) initializeSync() can build a bridge on demand.
49
+ */
50
+ let _quickjsWasm = null;
51
+ /** Runtime bundle source, read once per process by loadRuntimeBundle(). */
52
+ let _runtimeBundle = null;
42
53
  async function getQuickJSModule() {
43
54
  if (!_quickjs) {
44
55
  _quickjs = await import('quickjs-emscripten');
@@ -85,21 +96,43 @@ function isSetSentinel(value) {
85
96
  value !== null &&
86
97
  value.__isSet === true);
87
98
  }
88
- function isErrorSentinel(value) {
89
- return (typeof value === 'object' &&
90
- value !== null &&
91
- value.__isError === true);
92
- }
93
99
  function isEscapedObject(value) {
94
100
  return (typeof value === 'object' &&
95
101
  value !== null &&
96
102
  value.__isEscaped === true);
97
103
  }
104
+ /**
105
+ * Give back the payload of an escape wrapper.
106
+ *
107
+ * A walked payload only had its own keys collide, so the walk goes on and
108
+ * rebuilds the markers deeper in it. An opaque payload is a value the guest
109
+ * could not walk, so `opaque` keeps every marker in it as data.
110
+ */
111
+ function unescapeTransferValue(value) {
112
+ const inner = value.__value;
113
+ const opaque = (0, transfer_1.isOpaqueTransferValue)(value);
114
+ if (typeof inner !== 'object' || inner === null)
115
+ return inner;
116
+ if (isEscapedObject(inner))
117
+ return unwrapSentinels(inner, opaque);
118
+ if (Array.isArray(inner))
119
+ return inner.map((entry) => unwrapSentinels(entry, opaque));
120
+ const unescaped = {};
121
+ for (const [key, entry] of Object.entries(inner)) {
122
+ unescaped[key] = unwrapSentinels(entry, opaque);
123
+ }
124
+ return unescaped;
125
+ }
98
126
  /**
99
127
  * Recursively reconstruct Date objects, NaN values, Map, and Set from
100
128
  * sentinels produced by the QuickJS-side __prepareForTransfer wrapper.
129
+ *
130
+ * With `markersAsData` set, a transfer marker is left as the plain object it
131
+ * is, for the contents of a payload the guest marked opaque. The guest escapes
132
+ * most such objects itself, but one that also carries `__isError` leaves the
133
+ * guest walk before the escape, so the host must not read it as a marker.
101
134
  */
102
- function unwrapSentinels(value) {
135
+ function unwrapSentinels(value, markersAsData = false) {
103
136
  if (value === null || value === undefined)
104
137
  return value;
105
138
  if (typeof value !== 'object')
@@ -111,10 +144,16 @@ function unwrapSentinels(value) {
111
144
  const inner = value.__value;
112
145
  const result = {};
113
146
  for (const key of Object.keys(inner)) {
114
- result[key] = unwrapSentinels(inner[key]);
147
+ result[key] = unwrapSentinels(inner[key], markersAsData);
115
148
  }
116
149
  return result;
117
150
  }
151
+ if (!markersAsData) {
152
+ if ((0, luxon_transfer_1.isLuxonSentinel)(value))
153
+ return (0, luxon_transfer_1.rebuildLuxonValue)(value);
154
+ if ((0, transfer_1.isEscapedTransferValue)(value))
155
+ return unescapeTransferValue(value);
156
+ }
118
157
  if (isDateSentinel(value))
119
158
  return new Date(value.__isoString);
120
159
  if (isNaNSentinel(value))
@@ -131,26 +170,29 @@ function unwrapSentinels(value) {
131
170
  const err = new ErrorCtor(value.__message);
132
171
  if (value.__extra) {
133
172
  for (const [k, v] of Object.entries(value.__extra)) {
134
- err[k] = unwrapSentinels(v);
173
+ err[k] = unwrapSentinels(v, markersAsData);
135
174
  }
136
175
  }
137
176
  return err;
138
177
  }
139
178
  if (isMapSentinel(value)) {
140
- return new Map(value.__entries.map(([k, v]) => [unwrapSentinels(k), unwrapSentinels(v)]));
179
+ return new Map(value.__entries.map(([k, v]) => [
180
+ unwrapSentinels(k, markersAsData),
181
+ unwrapSentinels(v, markersAsData),
182
+ ]));
141
183
  }
142
184
  if (isSetSentinel(value)) {
143
- return new Set(value.__values.map(unwrapSentinels));
185
+ return new Set(value.__values.map((entry) => unwrapSentinels(entry, markersAsData)));
144
186
  }
145
187
  if (Array.isArray(value))
146
- return value.map(unwrapSentinels);
188
+ return value.map((entry) => unwrapSentinels(entry, markersAsData));
147
189
  // Pass error sentinels through untouched — execute() detects them after
148
190
  // unwrapping and reconstructs the Error on the host.
149
- if (isErrorSentinel(value))
191
+ if ((0, host_functions_1.isErrorSentinel)(value))
150
192
  return value;
151
193
  const result = {};
152
194
  for (const key of Object.keys(value)) {
153
- result[key] = unwrapSentinels(value[key]);
195
+ result[key] = unwrapSentinels(value[key], markersAsData);
154
196
  }
155
197
  return result;
156
198
  }
@@ -196,7 +238,7 @@ function wrapSpecialValuesForGuest(value) {
196
238
  if (Array.isArray(value))
197
239
  return value.map(wrapSpecialValuesForGuest);
198
240
  // Error sentinels are already in transfer shape — leave them intact.
199
- if (isErrorSentinel(value))
241
+ if ((0, host_functions_1.isErrorSentinel)(value))
200
242
  return value;
201
243
  const record = value;
202
244
  const result = {};
@@ -209,38 +251,21 @@ function wrapSpecialValuesForGuest(value) {
209
251
  return collides ? { __isEscaped: true, __value: result } : result;
210
252
  }
211
253
  /**
212
- * Serialize an error into a transferable metadata object.
213
- *
214
- * Host-side callbacks (getValueAtPath, etc.) catch errors and return this
215
- * sentinel instead of letting the error cross the boundary (which strips
216
- * custom class identity and properties). The in-context proxy detects
217
- * __isError and throws the sentinel; the host reconstructs a real Error
218
- * after it round-trips back (see execute() / reconstructError).
219
- */
220
- function serializeError(err) {
221
- if (err instanceof Error) {
222
- const extra = Object.fromEntries(Object.entries(err).filter(([key]) => key !== 'name' && key !== 'message' && key !== 'stack'));
223
- return {
224
- __isError: true,
225
- name: err.name,
226
- message: err.message,
227
- stack: err.stack,
228
- extra,
229
- };
230
- }
231
- return { __isError: true, name: 'Error', message: String(err), extra: {} };
232
- }
254
+
233
255
  /**
234
256
  * Read the runtime IIFE bundle by walking up from `__dirname` until
235
257
  * `dist/bundle/runtime.iife.js` is found. Walking up (rather than a fixed
236
258
  * relative path) works from either compiled output dir — `dist/cjs/bridge/`
237
259
  * and `dist/esm/bridge/` sit at different depths from the bundle.
238
260
  */
239
- async function readRuntimeBundle() {
261
+ function loadRuntimeBundle() {
262
+ if (_runtimeBundle !== null)
263
+ return _runtimeBundle;
240
264
  let dir = __dirname;
241
265
  while (dir !== path.dirname(dir)) {
242
266
  try {
243
- return await (0, promises_1.readFile)(path.join(dir, BUNDLE_RELATIVE_PATH), 'utf-8');
267
+ _runtimeBundle = (0, node_fs_1.readFileSync)(path.join(dir, BUNDLE_RELATIVE_PATH), 'utf-8');
268
+ return _runtimeBundle;
244
269
  }
245
270
  catch { }
246
271
  dir = path.dirname(dir);
@@ -248,6 +273,7 @@ async function readRuntimeBundle() {
248
273
  throw new Error(`Could not find runtime bundle (${BUNDLE_RELATIVE_PATH}) in any parent of ${__dirname}`);
249
274
  }
250
275
  /**
276
+
251
277
  * Convert a host JavaScript value to a JSON string suitable for round-tripping
252
278
  * into QuickJS via evalCode. Handles undefined (not valid JSON) by returning
253
279
  * the string "undefined".
@@ -271,165 +297,100 @@ function hostValueToJson(value) {
271
297
  return 'undefined';
272
298
  }
273
299
  }
274
- /**
275
- * Navigate data object by path and return metadata or primitive value.
276
- * Mirrors the IsolatedVmBridge getValueAtPath callback so QuickJS
277
- * matches isolated-vm semantics, including special-cased $/$item navigation.
278
- */
279
- function getValueAtPath(data, pathArr) {
280
- let value = data;
281
- let startIndex = 0;
282
- const itemFn = data.$item;
283
- if (pathArr.length >= 2 && pathArr[0] === '$item' && typeof itemFn === 'function') {
284
- const itemIndex = parseInt(pathArr[1], 10);
285
- if (!isNaN(itemIndex)) {
286
- value = itemFn(itemIndex);
287
- startIndex = 2;
288
- }
289
- }
290
- else {
291
- const dollarFn = data.$;
292
- if (pathArr.length >= 2 && pathArr[0] === '$' && typeof dollarFn === 'function') {
293
- value = dollarFn(pathArr[1]);
294
- startIndex = 2;
295
- }
296
- }
297
- for (let i = startIndex; i < pathArr.length; i++) {
298
- value = value?.[pathArr[i]];
299
- if (value === undefined || value === null) {
300
- return value;
301
- }
302
- }
303
- // Functions must never cross the boundary — resolve them as undefined,
304
- // matching IsolatedVmBridge (invariant: __tests__/host-fn-shadowing.test.ts).
305
- if (typeof value === 'function') {
306
- return undefined;
307
- }
308
- if (Array.isArray(value)) {
309
- return {
310
- __isArray: true,
311
- __length: value.length,
312
- __data: null,
313
- };
314
- }
315
- // Dates have no enumerable own keys; pass through instead of
316
- // marshaling as an empty object.
317
- if (value instanceof Date) {
318
- return value;
319
- }
320
- if (value !== null && typeof value === 'object') {
321
- return {
322
- __isObject: true,
323
- __keys: Object.keys(value),
324
- };
325
- }
326
- return value;
327
- }
328
- function getArrayElement(data, pathArr, index) {
329
- let arr = data;
330
- let startIndex = 0;
331
- const itemFn = data.$item;
332
- if (pathArr.length >= 2 && pathArr[0] === '$item' && typeof itemFn === 'function') {
333
- const itemIndex = parseInt(pathArr[1], 10);
334
- if (!isNaN(itemIndex)) {
335
- arr = itemFn(itemIndex);
336
- startIndex = 2;
337
- }
338
- }
339
- else {
340
- const dollarFn = data.$;
341
- if (pathArr.length >= 2 && pathArr[0] === '$' && typeof dollarFn === 'function') {
342
- arr = dollarFn(pathArr[1]);
343
- startIndex = 2;
344
- }
345
- }
346
- for (let i = startIndex; i < pathArr.length; i++) {
347
- arr = arr?.[pathArr[i]];
348
- if (arr === undefined || arr === null) {
349
- return undefined;
350
- }
351
- }
352
- if (!Array.isArray(arr)) {
353
- return undefined;
354
- }
355
- // Reject non-integer / negative indices so a crafted "index" can't read off
356
- // the prototype chain. Mirrors IsolatedVmBridge.getArrayElement.
357
- if (!Number.isInteger(index) || index < 0) {
358
- return undefined;
359
- }
360
- const element = arr[index];
361
- // Functions must never cross the boundary — resolve as undefined, matching
362
- // getValueAtPath and IsolatedVmBridge (invariant: host-fn-shadowing.test.ts).
363
- if (typeof element === 'function') {
364
- return undefined;
365
- }
366
- // Dates have no enumerable own keys; pass through instead of
367
- // marshaling as an empty object.
368
- if (element instanceof Date) {
369
- return element;
370
- }
371
- if (element !== null && typeof element === 'object') {
372
- if (Array.isArray(element)) {
373
- return {
374
- __isArray: true,
375
- __length: element.length,
376
- __data: null,
377
- };
378
- }
379
- return {
380
- __isObject: true,
381
- __keys: Object.keys(element),
382
- };
383
- }
384
- return element;
300
+ /** Formatter args arrive as dumped guest values; DateTimeFormat dates travel as timestamps. */
301
+ function toDateArg(ts) {
302
+ return typeof ts === 'number' ? new Date(ts) : new Date();
385
303
  }
386
304
  /**
387
- * Host-side dispatcher for the typed-RPC `callHost` channel.
388
- *
389
- * Mirrors IsolatedVmBridge's dispatcher — see isolated-vm-bridge.ts for the
390
- * per-message rationale. The two copies are kept in sync at compile time:
391
- * the `never` check in the default case fails to compile when a new schema
392
- * lands in bridge-messages.ts without a matching case here.
305
+ * Snapshot the properties Luxon and user code read from Intl.Locale into a
306
+ * plain object the guest copies onto its wrapper instance. weekInfo moved
307
+ * from a getter to getWeekInfo() across V8 versions — read both shapes.
393
308
  */
394
- function dispatchHostCall(rawMsg, data) {
395
- const msg = bridge_messages_1.bridgeMessageSchema.parse(rawMsg);
396
- switch (msg.type) {
397
- case 'getNodeFirst':
398
- return data.$?.(msg.nodeName)?.first?.(msg.branchIndex, msg.runIndex);
399
- case 'getNodeLast':
400
- return data.$?.(msg.nodeName)?.last?.(msg.branchIndex, msg.runIndex);
401
- case 'getNodeAll':
402
- return data.$?.(msg.nodeName)?.all?.(msg.branchIndex, msg.runIndex);
403
- case 'getInputFirst':
404
- return data.$input?.first?.();
405
- case 'getInputLast':
406
- return data.$input?.last?.();
407
- case 'getInputAll':
408
- return data.$input?.all?.();
409
- case 'getItems':
410
- return data.$items?.(msg.nodeName, msg.outputIndex, msg.runIndex);
411
- case 'fromAi':
412
- return data.$fromAI?.(msg.name, msg.description, msg.valueType, msg.defaultValue);
413
- case 'getNodePairedItem':
414
- return data.$?.(msg.nodeName)?.pairedItem?.(msg.itemIndex);
415
- case 'getNodeItemMatching':
416
- return data.$?.(msg.nodeName)?.itemMatching?.(msg.itemIndex);
417
- case 'getNodeItem':
418
- // `.item` is a host getter — accessing it invokes the resolver.
419
- return data.$?.(msg.nodeName)?.item;
420
- case 'evaluateExpression':
421
- return data.$evaluateExpression?.(msg.expression, msg.itemIndex);
422
- case 'getPairedItem':
423
- return data.$getPairedItem?.(msg.destinationNodeName, msg.incomingSourceData, msg.initialPairedItem);
424
- default: {
425
- // Unreachable at runtime — zod rejects unknown `type` values before
426
- // the switch. The `never` assignment is the compile-time guard.
427
- const exhaustive = msg;
428
- void exhaustive;
429
- throw new Error('Unhandled bridge message');
430
- }
431
- }
309
+ function dumpLocale(loc) {
310
+ const withWeekInfo = loc;
311
+ return {
312
+ tag: loc.toString(),
313
+ baseName: loc.baseName,
314
+ language: loc.language,
315
+ script: loc.script,
316
+ region: loc.region,
317
+ calendar: loc.calendar,
318
+ caseFirst: loc.caseFirst,
319
+ collation: loc.collation,
320
+ hourCycle: loc.hourCycle,
321
+ numberingSystem: loc.numberingSystem,
322
+ numeric: loc.numeric,
323
+ weekInfo: withWeekInfo.getWeekInfo?.() ?? withWeekInfo.weekInfo,
324
+ };
432
325
  }
326
+ const INTL_DISPATCH = {
327
+ DateTimeFormat: {
328
+ create: (locales, options) => new Intl.DateTimeFormat(locales, options),
329
+ supportedLocalesOf: (locales) => Intl.DateTimeFormat.supportedLocalesOf(locales ?? []),
330
+ ops: {
331
+ format: (fmt, args) => fmt.format(toDateArg(args[0])),
332
+ formatToParts: (fmt, args) => fmt.formatToParts(toDateArg(args[0])),
333
+ resolvedOptions: (fmt) => fmt.resolvedOptions(),
334
+ },
335
+ },
336
+ NumberFormat: {
337
+ create: (locales, options) => new Intl.NumberFormat(locales, options),
338
+ supportedLocalesOf: (locales) => Intl.NumberFormat.supportedLocalesOf(locales ?? []),
339
+ ops: {
340
+ format: (fmt, args) => fmt.format(args[0]),
341
+ formatToParts: (fmt, args) => fmt.formatToParts(args[0]),
342
+ resolvedOptions: (fmt) => fmt.resolvedOptions(),
343
+ },
344
+ },
345
+ RelativeTimeFormat: {
346
+ create: (locales, options) => new Intl.RelativeTimeFormat(locales, options),
347
+ supportedLocalesOf: (locales) => Intl.RelativeTimeFormat.supportedLocalesOf(locales ?? []),
348
+ ops: {
349
+ format: (fmt, args) => fmt.format(args[0], args[1]),
350
+ formatToParts: (fmt, args) => fmt.formatToParts(args[0], args[1]),
351
+ resolvedOptions: (fmt) => fmt.resolvedOptions(),
352
+ },
353
+ },
354
+ ListFormat: {
355
+ create: (locales, options) => new Intl.ListFormat(locales, options),
356
+ supportedLocalesOf: (locales) => Intl.ListFormat.supportedLocalesOf(locales ?? []),
357
+ ops: {
358
+ format: (fmt, args) => fmt.format(args[0]),
359
+ formatToParts: (fmt, args) => fmt.formatToParts(args[0]),
360
+ resolvedOptions: (fmt) => fmt.resolvedOptions(),
361
+ },
362
+ },
363
+ Collator: {
364
+ create: (locales, options) => new Intl.Collator(locales, options),
365
+ supportedLocalesOf: (locales) => Intl.Collator.supportedLocalesOf(locales ?? []),
366
+ ops: {
367
+ compare: (col, args) => col.compare(args[0], args[1]),
368
+ resolvedOptions: (col) => col.resolvedOptions(),
369
+ },
370
+ },
371
+ PluralRules: {
372
+ create: (locales, options) => new Intl.PluralRules(locales, options),
373
+ supportedLocalesOf: (locales) => Intl.PluralRules.supportedLocalesOf(locales ?? []),
374
+ ops: {
375
+ select: (pr, args) => pr.select(args[0]),
376
+ resolvedOptions: (pr) => pr.resolvedOptions(),
377
+ },
378
+ },
379
+ DisplayNames: {
380
+ create: (locales, options) => new Intl.DisplayNames(locales, options),
381
+ supportedLocalesOf: (locales) => Intl.DisplayNames.supportedLocalesOf(locales ?? []),
382
+ ops: {
383
+ of: (dn, args) => dn.of(args[0]),
384
+ resolvedOptions: (dn) => dn.resolvedOptions(),
385
+ },
386
+ },
387
+ Locale: {
388
+ create: (locales, options) => new Intl.Locale(locales, options),
389
+ ops: {
390
+ dump: (loc) => dumpLocale(loc),
391
+ },
392
+ },
393
+ };
433
394
  /**
434
395
  * QuickJsBridge - Runtime bridge using quickjs-emscripten for expression evaluation.
435
396
  *
@@ -453,6 +414,11 @@ class QuickJsBridge {
453
414
  logger;
454
415
  // Long-lived host-callback handles (Intl polyfills) — disposed on dispose()
455
416
  intlHandles = [];
417
+ // Memoized host Intl formatters keyed by ctor + locales + options.
418
+ // Construction dominates per-call cost (~39µs for a DateTimeFormat vs ~1µs
419
+ // for format on an existing one). Capped because locales/options are
420
+ // user-influenced — unbounded, this would be a memory leak by user input.
421
+ intlFormatterCache = new lru_cache_1.LruCache(200);
456
422
  // Wall-clock deadlines of the execute() calls currently in flight, outer to
457
423
  // inner. execute() can re-enter (e.g. $evaluateExpression), and the runtime
458
424
  // has a single interrupt handler; the handler interrupts once the earliest
@@ -473,6 +439,31 @@ class QuickJsBridge {
473
439
  return;
474
440
  const { getQuickJS } = await getQuickJSModule();
475
441
  const QuickJS = await getQuickJS();
442
+ _quickjsWasm = QuickJS;
443
+ this.setupContext(QuickJS, loadRuntimeBundle());
444
+ }
445
+ /**
446
+ * Synchronous variant of initialize(), for on-demand creation inside the
447
+ * synchronous evaluate() path (lazy acquisition with an exhausted pool).
448
+ * Requires the WASM module to have been instantiated by an earlier async
449
+ * initialize() in this process — pool warmup provides that.
450
+ */
451
+ initializeSync() {
452
+ if (this.disposed)
453
+ throw new Error('Bridge has been disposed and cannot be reinitialized.');
454
+ if (this.initialized)
455
+ return;
456
+ // Both caches are populated by the same async initialize() (pool
457
+ // warmup), so the sync path never touches the filesystem or the
458
+ // event loop beyond the context setup itself.
459
+ if (_quickjsWasm === null || _runtimeBundle === null) {
460
+ throw new Error('QuickJS WASM module and runtime bundle are not loaded yet: an async initialize() ' +
461
+ 'must run once (pool warmup) before bridges can be created synchronously');
462
+ }
463
+ this.setupContext(_quickjsWasm, _runtimeBundle);
464
+ }
465
+ /** Everything after module/bundle acquisition is synchronous and shared. */
466
+ setupContext(QuickJS, runtimeBundle) {
476
467
  // Create runtime with memory limit (MB → bytes)
477
468
  this.runtime = QuickJS.newRuntime();
478
469
  this.runtime.setMemoryLimit(this.config.memoryLimit * 1024 * 1024);
@@ -488,7 +479,7 @@ class QuickJsBridge {
488
479
  // Inject Intl polyfill — QuickJS doesn't include Intl, but Luxon needs it
489
480
  this.injectIntlPolyfill();
490
481
  // Load runtime bundle (DateTime, extend, SafeObject, proxy system, buildContext)
491
- await this.loadRuntimeBundle();
482
+ this.loadRuntimeBundle(runtimeBundle);
492
483
  // Wrap __prepareForTransfer to mark Date/NaN/Map/Set/Error so they survive vm.dump()
493
484
  this.injectTransferWrapper();
494
485
  // Inject E() error handler matching IsolatedVmBridge's E() semantics
@@ -500,10 +491,9 @@ class QuickJsBridge {
500
491
  /**
501
492
  * Load the runtime IIFE bundle and verify required globals are present.
502
493
  */
503
- async loadRuntimeBundle() {
494
+ loadRuntimeBundle(runtimeBundle) {
504
495
  if (!this.vm)
505
496
  throw new Error('Context not initialized');
506
- const runtimeBundle = await readRuntimeBundle();
507
497
  const result = this.vm.evalCode(runtimeBundle);
508
498
  if (result.error) {
509
499
  const errorStr = this.vm.dump(result.error);
@@ -519,105 +509,20 @@ class QuickJsBridge {
519
509
  /**
520
510
  * Inject a polyfill for the Intl API.
521
511
  *
522
- * QuickJS doesn't include Intl, but Luxon (bundled in the runtime) uses
523
- * Intl.DateTimeFormat, Intl.NumberFormat, Intl.RelativeTimeFormat, and
524
- * Intl.ListFormat extensively. Rather than shimming all of this in pure JS,
525
- * we register host callback functions that delegate to Node.js's real Intl
526
- * implementation, then build lightweight JS wrapper classes that call them.
512
+ * QuickJS doesn't include Intl, but Luxon (bundled in the runtime) and user
513
+ * expressions use it extensively. Rather than shimming ECMA-402 in pure JS,
514
+ * we register a single `__intl` host callback that dispatches to Node.js's
515
+ * real Intl implementation (see INTL_DISPATCH), then build lightweight JS
516
+ * wrapper classes that call it.
527
517
  */
528
518
  injectIntlPolyfill() {
529
519
  if (!this.vm)
530
520
  throw new Error('Context not initialized');
531
521
  const vm = this.vm;
532
- const dtfFn = vm.newFunction('__intl_dtf', (...handles) => {
533
- const args = handles.map((h) => vm.dump(h));
534
- const op = args[0];
535
- const locales = args[1];
536
- const options = args[2];
537
- try {
538
- if (op === 'resolvedOptions') {
539
- const fmt = new Intl.DateTimeFormat(locales, options);
540
- return this.hostValueToQuickJSHandle(fmt.resolvedOptions());
541
- }
542
- if (op === 'format') {
543
- const fmt = new Intl.DateTimeFormat(locales, options);
544
- const ts = args[3];
545
- const date = ts !== undefined ? new Date(ts) : new Date();
546
- return this.hostValueToQuickJSHandle(fmt.format(date));
547
- }
548
- if (op === 'formatToParts') {
549
- const fmt = new Intl.DateTimeFormat(locales, options);
550
- const ts = args[3];
551
- const date = ts !== undefined ? new Date(ts) : new Date();
552
- return this.hostValueToQuickJSHandle(fmt.formatToParts(date));
553
- }
554
- if (op === 'supportedLocalesOf') {
555
- return this.hostValueToQuickJSHandle(Intl.DateTimeFormat.supportedLocalesOf(locales ?? []));
556
- }
557
- }
558
- catch (err) {
559
- return this.hostValueToQuickJSHandle({
560
- __intlError: true,
561
- message: err instanceof Error ? err.message : String(err),
562
- });
563
- }
564
- return vm.undefined;
565
- });
566
- vm.setProp(vm.global, '__intl_dtf', dtfFn);
567
- this.intlHandles.push(dtfFn);
568
- const nfFn = vm.newFunction('__intl_nf', (...handles) => {
569
- const args = handles.map((h) => vm.dump(h));
570
- const op = args[0];
571
- const locales = args[1];
572
- const options = args[2];
573
- try {
574
- if (op === 'resolvedOptions') {
575
- const fmt = new Intl.NumberFormat(locales, options);
576
- return this.hostValueToQuickJSHandle(fmt.resolvedOptions());
577
- }
578
- if (op === 'format') {
579
- const fmt = new Intl.NumberFormat(locales, options);
580
- const num = args[3];
581
- return this.hostValueToQuickJSHandle(fmt.format(num));
582
- }
583
- if (op === 'formatToParts') {
584
- const fmt = new Intl.NumberFormat(locales, options);
585
- const num = args[3];
586
- return this.hostValueToQuickJSHandle(fmt.formatToParts(num));
587
- }
588
- }
589
- catch (err) {
590
- return this.hostValueToQuickJSHandle({
591
- __intlError: true,
592
- message: err instanceof Error ? err.message : String(err),
593
- });
594
- }
595
- return vm.undefined;
596
- });
597
- vm.setProp(vm.global, '__intl_nf', nfFn);
598
- this.intlHandles.push(nfFn);
599
- const rtfFn = vm.newFunction('__intl_rtf', (...handles) => {
522
+ const intlFn = vm.newFunction('__intl', (...handles) => {
600
523
  const args = handles.map((h) => vm.dump(h));
601
- const op = args[0];
602
- const locales = args[1];
603
- const options = args[2];
604
524
  try {
605
- if (op === 'resolvedOptions') {
606
- const fmt = new Intl.RelativeTimeFormat(locales, options);
607
- return this.hostValueToQuickJSHandle(fmt.resolvedOptions());
608
- }
609
- if (op === 'format') {
610
- const fmt = new Intl.RelativeTimeFormat(locales, options);
611
- const value = args[3];
612
- const unit = args[4];
613
- return this.hostValueToQuickJSHandle(fmt.format(value, unit));
614
- }
615
- if (op === 'formatToParts') {
616
- const fmt = new Intl.RelativeTimeFormat(locales, options);
617
- const value = args[3];
618
- const unit = args[4];
619
- return this.hostValueToQuickJSHandle(fmt.formatToParts(value, unit));
620
- }
525
+ return this.hostValueToQuickJSHandle(this.dispatchIntl(args[0], args[1], args[2], args[3], args.slice(4)));
621
526
  }
622
527
  catch (err) {
623
528
  return this.hostValueToQuickJSHandle({
@@ -625,41 +530,9 @@ class QuickJsBridge {
625
530
  message: err instanceof Error ? err.message : String(err),
626
531
  });
627
532
  }
628
- return vm.undefined;
629
533
  });
630
- vm.setProp(vm.global, '__intl_rtf', rtfFn);
631
- this.intlHandles.push(rtfFn);
632
- const lfFn = vm.newFunction('__intl_lf', (...handles) => {
633
- const args = handles.map((h) => vm.dump(h));
634
- const op = args[0];
635
- const locales = args[1];
636
- const options = args[2];
637
- try {
638
- if (op === 'format') {
639
- const fmt = new Intl.ListFormat(locales, options);
640
- const list = args[3];
641
- return this.hostValueToQuickJSHandle(fmt.format(list));
642
- }
643
- if (op === 'formatToParts') {
644
- const fmt = new Intl.ListFormat(locales, options);
645
- const list = args[3];
646
- return this.hostValueToQuickJSHandle(fmt.formatToParts(list));
647
- }
648
- if (op === 'resolvedOptions') {
649
- const fmt = new Intl.ListFormat(locales, options);
650
- return this.hostValueToQuickJSHandle(fmt.resolvedOptions());
651
- }
652
- }
653
- catch (err) {
654
- return this.hostValueToQuickJSHandle({
655
- __intlError: true,
656
- message: err instanceof Error ? err.message : String(err),
657
- });
658
- }
659
- return vm.undefined;
660
- });
661
- vm.setProp(vm.global, '__intl_lf', lfFn);
662
- this.intlHandles.push(lfFn);
534
+ vm.setProp(vm.global, '__intl', intlFn);
535
+ this.intlHandles.push(intlFn);
663
536
  const shimCode = `
664
537
  (function() {
665
538
  function checkError(result) {
@@ -669,73 +542,107 @@ class QuickJsBridge {
669
542
  return result;
670
543
  }
671
544
 
672
- function DateTimeFormat(locales, options) {
673
- this._locales = locales;
674
- this._options = options || {};
545
+ // Shallow-copy locales/options at construction — V8 snapshots them when a
546
+ // formatter is created, so mutating them afterwards must not change output.
547
+ // The stable snapshot also keys the host-side formatter memo.
548
+ function snapshotOptions(options) {
549
+ if (options === null || options === undefined) return undefined;
550
+ var copy = {};
551
+ for (var k in options) copy[k] = options[k];
552
+ return copy;
553
+ }
554
+ function snapshotLocales(locales) {
555
+ return Array.isArray(locales) ? locales.slice() : locales;
675
556
  }
676
- DateTimeFormat.prototype.resolvedOptions = function() {
677
- return checkError(__intl_dtf('resolvedOptions', this._locales, this._options));
678
- };
679
- DateTimeFormat.prototype.format = function(date) {
680
- var ts = date instanceof Date ? date.getTime() : (typeof date === 'number' ? date : Date.now());
681
- return checkError(__intl_dtf('format', this._locales, this._options, ts));
682
- };
683
- DateTimeFormat.prototype.formatToParts = function(date) {
684
- var ts = date instanceof Date ? date.getTime() : (typeof date === 'number' ? date : Date.now());
685
- return checkError(__intl_dtf('formatToParts', this._locales, this._options, ts));
686
- };
687
- DateTimeFormat.supportedLocalesOf = function(locales) {
688
- return checkError(__intl_dtf('supportedLocalesOf', locales));
689
- };
690
557
 
691
- function NumberFormat(locales, options) {
692
- this._locales = locales;
693
- this._options = options || {};
558
+ function callIntl(inst, ctorName, op, args, transform) {
559
+ var callArgs = [ctorName, op, inst._locales, inst._options];
560
+ var i = 0;
561
+ if (transform) {
562
+ callArgs.push(transform(args[0]));
563
+ i = 1;
564
+ }
565
+ for (; i < args.length; i++) callArgs.push(args[i]);
566
+ return checkError(__intl.apply(null, callArgs));
694
567
  }
695
- NumberFormat.prototype.resolvedOptions = function() {
696
- return checkError(__intl_nf('resolvedOptions', this._locales, this._options));
697
- };
698
- NumberFormat.prototype.format = function(num) {
699
- return checkError(__intl_nf('format', this._locales, this._options, num));
700
- };
701
- NumberFormat.prototype.formatToParts = function(num) {
702
- return checkError(__intl_nf('formatToParts', this._locales, this._options, num));
703
- };
704
568
 
705
- function RelativeTimeFormat(locales, options) {
706
- this._locales = locales;
707
- this._options = options || {};
569
+ // Build a wrapper class that delegates each op to the host dispatcher.
570
+ // \`ops\` maps op name → optional first-arg transform. \`boundOp\` is also
571
+ // installed as an instance function: V8 models format/compare as
572
+ // bound-function getters, so e.g. dates.map(dtf.format) must work unbound.
573
+ function makeWrapper(ctorName, ops, boundOp) {
574
+ function Wrapper(locales, options) {
575
+ this._locales = snapshotLocales(locales);
576
+ this._options = snapshotOptions(options);
577
+ if (boundOp) {
578
+ var self = this;
579
+ this[boundOp] = function () {
580
+ return callIntl(self, ctorName, boundOp, arguments, ops[boundOp]);
581
+ };
582
+ }
583
+ }
584
+ var opNames = Object.keys(ops);
585
+ for (var i = 0; i < opNames.length; i++) {
586
+ (function (op) {
587
+ Wrapper.prototype[op] = function () {
588
+ return callIntl(this, ctorName, op, arguments, ops[op]);
589
+ };
590
+ })(opNames[i]);
591
+ }
592
+ Wrapper.supportedLocalesOf = function (locales) {
593
+ return checkError(__intl(ctorName, 'supportedLocalesOf', locales));
594
+ };
595
+ return Wrapper;
708
596
  }
709
- RelativeTimeFormat.prototype.resolvedOptions = function() {
710
- return checkError(__intl_rtf('resolvedOptions', this._locales, this._options));
711
- };
712
- RelativeTimeFormat.prototype.format = function(value, unit) {
713
- return checkError(__intl_rtf('format', this._locales, this._options, value, unit));
714
- };
715
- RelativeTimeFormat.prototype.formatToParts = function(value, unit) {
716
- return checkError(__intl_rtf('formatToParts', this._locales, this._options, value, unit));
717
- };
718
597
 
719
- function ListFormat(locales, options) {
720
- this._locales = locales;
721
- this._options = options || {};
598
+ // DateTimeFormat dates travel as timestamps — guest Dates don't survive vm.dump().
599
+ function toTs(date) {
600
+ return date instanceof Date ? date.getTime() : (typeof date === 'number' ? date : Date.now());
722
601
  }
723
- ListFormat.prototype.format = function(list) {
724
- return checkError(__intl_lf('format', this._locales, this._options, list));
725
- };
726
- ListFormat.prototype.formatToParts = function(list) {
727
- return checkError(__intl_lf('formatToParts', this._locales, this._options, list));
728
- };
729
- ListFormat.prototype.resolvedOptions = function() {
730
- return checkError(__intl_lf('resolvedOptions', this._locales, this._options));
731
- };
602
+
603
+ var DateTimeFormat = makeWrapper('DateTimeFormat', { format: toTs, formatToParts: toTs, resolvedOptions: null }, 'format');
604
+ var NumberFormat = makeWrapper('NumberFormat', { format: null, formatToParts: null, resolvedOptions: null }, 'format');
605
+ var RelativeTimeFormat = makeWrapper('RelativeTimeFormat', { format: null, formatToParts: null, resolvedOptions: null }, null);
606
+ var ListFormat = makeWrapper('ListFormat', { format: null, formatToParts: null, resolvedOptions: null }, null);
607
+ var Collator = makeWrapper('Collator', { compare: null, resolvedOptions: null }, 'compare');
608
+ var PluralRules = makeWrapper('PluralRules', { select: null, resolvedOptions: null }, null);
609
+ var DisplayNames = makeWrapper('DisplayNames', { of: null, resolvedOptions: null }, null);
610
+
611
+ // Host-backed Intl.Locale: the host dumps the locale's properties once at
612
+ // construction. Luxon feature-detects week support via ('weekInfo' in
613
+ // Intl.Locale.prototype || 'getWeekInfo' in Intl.Locale.prototype) and
614
+ // prefers getWeekInfo() — expose both, like current V8.
615
+ function Locale(tag, options) {
616
+ var dumped = checkError(__intl('Locale', 'dump', tag, snapshotOptions(options)));
617
+ var keys = Object.keys(dumped);
618
+ for (var i = 0; i < keys.length; i++) {
619
+ var k = keys[i];
620
+ if (k === 'weekInfo') this._weekInfo = dumped[k];
621
+ else if (k === 'tag') this._tag = dumped[k];
622
+ else this[k] = dumped[k];
623
+ }
624
+ }
625
+ Object.defineProperty(Locale.prototype, 'weekInfo', {
626
+ get: function () { return this._weekInfo; },
627
+ });
628
+ Locale.prototype.getWeekInfo = function () { return this._weekInfo; };
629
+ Locale.prototype.toString = function () { return this._tag; };
732
630
 
733
631
  globalThis.Intl = {
734
632
  DateTimeFormat: DateTimeFormat,
735
633
  NumberFormat: NumberFormat,
736
634
  RelativeTimeFormat: RelativeTimeFormat,
737
635
  ListFormat: ListFormat,
738
- Locale: function Locale(tag) { this.baseName = tag; this.language = tag.split('-')[0]; }
636
+ Collator: Collator,
637
+ PluralRules: PluralRules,
638
+ DisplayNames: DisplayNames,
639
+ Locale: Locale,
640
+ getCanonicalLocales: function (locales) {
641
+ return checkError(__intl('Intl', 'getCanonicalLocales', locales));
642
+ },
643
+ supportedValuesOf: function (key) {
644
+ return checkError(__intl('Intl', 'supportedValuesOf', undefined, undefined, key));
645
+ },
739
646
  };
740
647
 
741
648
  // QuickJS's built-in toLocale* methods are locale-unaware (no ECMA-402) and
@@ -791,6 +698,43 @@ class QuickJsBridge {
791
698
  result.value.dispose();
792
699
  this.logger.debug('[QuickJsBridge] Intl polyfill injected');
793
700
  }
701
+ /**
702
+ * Host side of the guest's `__intl(ctorName, op, locales, options, ...args)`
703
+ * callback. Resolves the constructor and op against INTL_DISPATCH (rejecting
704
+ * anything not in the table) and memoizes constructed formatters.
705
+ */
706
+ dispatchIntl(ctorName, op, locales, options, args) {
707
+ // Pure static Intl functions — no constructed instance involved.
708
+ if (ctorName === 'Intl') {
709
+ if (op === 'getCanonicalLocales')
710
+ return Intl.getCanonicalLocales(locales);
711
+ if (op === 'supportedValuesOf') {
712
+ return Intl.supportedValuesOf(args[0]);
713
+ }
714
+ throw new TypeError(`Unsupported Intl operation: ${op}`);
715
+ }
716
+ const entry = INTL_DISPATCH[ctorName];
717
+ if (!entry)
718
+ throw new TypeError(`Unsupported Intl constructor: ${ctorName}`);
719
+ if (op === 'supportedLocalesOf') {
720
+ if (!entry.supportedLocalesOf) {
721
+ throw new TypeError(`Intl.${ctorName} has no supportedLocalesOf`);
722
+ }
723
+ return entry.supportedLocalesOf(locales);
724
+ }
725
+ const opFn = entry.ops[op];
726
+ if (!opFn)
727
+ throw new TypeError(`Unsupported Intl.${ctorName} operation: ${op}`);
728
+ // Guest wrappers snapshot options at construction, so the JSON key is
729
+ // stable across calls on the same wrapper instance.
730
+ const key = `${ctorName}${JSON.stringify(locales) ?? ''}${JSON.stringify(options) ?? ''}`;
731
+ let instance = this.intlFormatterCache.get(key);
732
+ if (instance === undefined) {
733
+ instance = entry.create(locales, options);
734
+ this.intlFormatterCache.set(key, instance);
735
+ }
736
+ return opFn(instance, args);
737
+ }
794
738
  /**
795
739
  * Wrap __prepareForTransfer so values that don't survive vm.dump()
796
740
  * (Date, NaN, Map, Set, Error) are converted to sentinel objects.
@@ -809,7 +753,10 @@ class QuickJsBridge {
809
753
  var prepared = original(value);
810
754
  return wrapSpecialValues(prepared);
811
755
  };
812
- function wrapSpecialValues(v) {
756
+ // __prepareForTransfer does not walk into a Map, a Set or the extra keys of an
757
+ // Error, and it does not walk an opaque payload. The inCollection flag marks
758
+ // those places, where a transfer marker can only come from user data.
759
+ function wrapSpecialValues(v, inCollection) {
813
760
  if (v === null || v === undefined) return v;
814
761
  // Functions and Promises must not leave the sandbox as results.
815
762
  // isolated-vm's structured clone rejects them; match its error.
@@ -832,7 +779,7 @@ class QuickJsBridge {
832
779
  var errKeys = Object.keys(v);
833
780
  for (var ei = 0; ei < errKeys.length; ei++) {
834
781
  if (errKeys[ei] !== 'name' && errKeys[ei] !== 'message' && errKeys[ei] !== 'stack') {
835
- errExtra[errKeys[ei]] = wrapSpecialValues(v[errKeys[ei]]);
782
+ errExtra[errKeys[ei]] = wrapSpecialValues(v[errKeys[ei]], true);
836
783
  }
837
784
  }
838
785
  return { __isErrorValue: true, __name: v.name || 'Error', __message: v.message || '', __extra: errExtra };
@@ -840,20 +787,48 @@ class QuickJsBridge {
840
787
  if (v instanceof Map) {
841
788
  var entries = [];
842
789
  v.forEach(function(val, key) {
843
- entries.push([wrapSpecialValues(key), wrapSpecialValues(val)]);
790
+ entries.push([wrapSpecialValues(key, true), wrapSpecialValues(val, true)]);
844
791
  });
845
792
  return { __isMap: true, __entries: entries };
846
793
  }
847
794
  if (v instanceof Set) {
848
795
  var values = [];
849
796
  v.forEach(function(val) {
850
- values.push(wrapSpecialValues(val));
797
+ values.push(wrapSpecialValues(val, true));
851
798
  });
852
799
  return { __isSet: true, __values: values };
853
800
  }
854
- if (Array.isArray(v)) return v.map(wrapSpecialValues);
801
+ if (Array.isArray(v)) return v.map(function(item) { return wrapSpecialValues(item, inCollection); });
855
802
  // Error sentinels are already in transfer shape — leave them intact.
856
803
  if (v.__isError) return v;
804
+ // Outside a collection, these markers come from __prepareForTransfer above,
805
+ // so pass them to the host as they are.
806
+ if (!inCollection) {
807
+ if (typeof v['${transfer_1.TRANSFER_TYPE_KEY}'] === 'string') return v;
808
+ if (v['${transfer_1.TRANSFER_ESCAPED_KEY}'] === true) {
809
+ var payload = v.__value;
810
+ if (v['${transfer_1.TRANSFER_OPAQUE_KEY}'] === true) {
811
+ // The host gives an opaque payload back as data, so wrap its contents
812
+ // as a collection and keep any marker in them as data too.
813
+ var opaqueWrapper = { __value: wrapOwnKeys(payload, true) };
814
+ opaqueWrapper['${transfer_1.TRANSFER_ESCAPED_KEY}'] = true;
815
+ opaqueWrapper['${transfer_1.TRANSFER_OPAQUE_KEY}'] = true;
816
+ return opaqueWrapper;
817
+ }
818
+ if (payload !== null && typeof payload === 'object' && !Array.isArray(payload)) {
819
+ var walkedWrapper = { __value: wrapOwnKeys(payload, inCollection) };
820
+ walkedWrapper['${transfer_1.TRANSFER_ESCAPED_KEY}'] = true;
821
+ return walkedWrapper;
822
+ }
823
+ var plainWrapper = { __value: wrapSpecialValues(payload, inCollection) };
824
+ plainWrapper['${transfer_1.TRANSFER_ESCAPED_KEY}'] = true;
825
+ return plainWrapper;
826
+ }
827
+ }
828
+ return wrapOwnKeys(v, inCollection);
829
+ }
830
+
831
+ function wrapOwnKeys(v, inCollection) {
857
832
  var result = {};
858
833
  var keys = Object.keys(v);
859
834
  var collides = false;
@@ -865,7 +840,18 @@ class QuickJsBridge {
865
840
  ) {
866
841
  collides = true;
867
842
  }
868
- result[key] = wrapSpecialValues(v[key]);
843
+ // Inside a collection a transfer marker is user data, so escape the object
844
+ // and the host reads the keys as the plain data they are.
845
+ if (
846
+ inCollection && (
847
+ key === '${transfer_1.TRANSFER_TYPE_KEY}' ||
848
+ key === '${transfer_1.TRANSFER_ESCAPED_KEY}' ||
849
+ key === '${transfer_1.TRANSFER_OPAQUE_KEY}'
850
+ )
851
+ ) {
852
+ collides = true;
853
+ }
854
+ result[key] = wrapSpecialValues(v[key], inCollection);
869
855
  }
870
856
  // User objects whose keys collide with transfer markers are escaped so
871
857
  // the host returns them as plain data (as isolated-vm does) instead of
@@ -989,32 +975,32 @@ class QuickJsBridge {
989
975
  const getValueFn = vm.newFunction('__getValueAtPathImpl', (pathHandle) => {
990
976
  const pathArr = vm.dump(pathHandle);
991
977
  try {
992
- const result = getValueAtPath(data, pathArr);
978
+ const result = (0, host_functions_1.getValueAtPath)(data, pathArr);
993
979
  return this.hostValueToQuickJSHandle(result);
994
980
  }
995
981
  catch (err) {
996
- return this.hostValueToQuickJSHandle(serializeError(err));
982
+ return this.hostValueToQuickJSHandle((0, host_functions_1.serializeError)(err));
997
983
  }
998
984
  });
999
985
  const getArrayFn = vm.newFunction('__getArrayElementImpl', (pathHandle, indexHandle) => {
1000
986
  const pathArr = vm.dump(pathHandle);
1001
987
  const index = vm.dump(indexHandle);
1002
988
  try {
1003
- const result = getArrayElement(data, pathArr, index);
989
+ const result = (0, host_functions_1.getArrayElement)(data, pathArr, index);
1004
990
  return this.hostValueToQuickJSHandle(result);
1005
991
  }
1006
992
  catch (err) {
1007
- return this.hostValueToQuickJSHandle(serializeError(err));
993
+ return this.hostValueToQuickJSHandle((0, host_functions_1.serializeError)(err));
1008
994
  }
1009
995
  });
1010
996
  const callHostFn = vm.newFunction('__callHostImpl', (msgHandle) => {
1011
997
  const rawMsg = vm.dump(msgHandle);
1012
998
  try {
1013
- const result = dispatchHostCall(rawMsg, data);
999
+ const result = (0, host_functions_1.dispatchHostCall)(rawMsg, data);
1014
1000
  return this.hostValueToQuickJSHandle(result);
1015
1001
  }
1016
1002
  catch (err) {
1017
- return this.hostValueToQuickJSHandle(serializeError(err));
1003
+ return this.hostValueToQuickJSHandle((0, host_functions_1.serializeError)(err));
1018
1004
  }
1019
1005
  });
1020
1006
  return [getValueFn, getArrayFn, callHostFn];
@@ -1152,8 +1138,8 @@ class QuickJsBridge {
1152
1138
  const rawResult = this.vm.dump(execResult.value);
1153
1139
  execResult.value.dispose();
1154
1140
  const result = unwrapSentinels(rawResult);
1155
- if (isErrorSentinel(result)) {
1156
- throw this.reconstructError(result);
1141
+ if ((0, host_functions_1.isErrorSentinel)(result)) {
1142
+ throw (0, host_functions_1.reconstructError)(result);
1157
1143
  }
1158
1144
  this.logger.debug('[QuickJsBridge] Expression executed successfully');
1159
1145
  return result;
@@ -1192,17 +1178,6 @@ class QuickJsBridge {
1192
1178
  ? `Nested expressions timed out after sharing the ${this.config.timeout}ms limit`
1193
1179
  : `Expression timed out after ${this.config.timeout}ms`, {});
1194
1180
  }
1195
- reconstructError(data) {
1196
- const error = new Error(data.message);
1197
- error.name = data.name || 'Error';
1198
- if (data.stack) {
1199
- error.stack = data.stack;
1200
- }
1201
- if (data.extra) {
1202
- Object.assign(error, data.extra);
1203
- }
1204
- return error;
1205
- }
1206
1181
  evalCodeOrThrow(code, label) {
1207
1182
  if (!this.vm)
1208
1183
  throw new Error('Context not initialized');