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