@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
|
@@ -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
|
|
37
|
+
const node_fs_1 = require("node:fs");
|
|
38
38
|
const path = __importStar(require("node:path"));
|
|
39
39
|
const types_1 = require("../types");
|
|
40
|
-
const
|
|
40
|
+
const luxon_transfer_1 = require("../runtime/luxon-transfer");
|
|
41
|
+
const 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]) => [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
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
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
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
|
|
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
|
-
|
|
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, '
|
|
631
|
-
this.intlHandles.push(
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
};
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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');
|