@framers/agentos 0.12.21 → 0.12.23
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 +2 -2
- package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts +4 -2
- package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts.map +1 -1
- package/dist/cognition/emergent/EmergentCapabilityEngine.js +20 -1
- package/dist/cognition/emergent/EmergentCapabilityEngine.js.map +1 -1
- package/dist/cognition/emergent/executor/QuickJSExecutor.d.ts +64 -0
- package/dist/cognition/emergent/executor/QuickJSExecutor.d.ts.map +1 -0
- package/dist/cognition/emergent/executor/QuickJSExecutor.js +655 -0
- package/dist/cognition/emergent/executor/QuickJSExecutor.js.map +1 -0
- package/dist/cognition/emergent/executor/guest-prelude.d.ts +22 -0
- package/dist/cognition/emergent/executor/guest-prelude.d.ts.map +1 -0
- package/dist/cognition/emergent/executor/guest-prelude.js +559 -0
- package/dist/cognition/emergent/executor/guest-prelude.js.map +1 -0
- package/dist/cognition/emergent/executor/guest-surface.d.ts +34 -0
- package/dist/cognition/emergent/executor/guest-surface.d.ts.map +1 -0
- package/dist/cognition/emergent/executor/guest-surface.js +218 -0
- package/dist/cognition/emergent/executor/guest-surface.js.map +1 -0
- package/dist/cognition/emergent/executor/types.d.ts +7 -0
- package/dist/cognition/emergent/executor/types.d.ts.map +1 -1
- package/dist/cognition/emergent/index.d.ts +1 -0
- package/dist/cognition/emergent/index.d.ts.map +1 -1
- package/dist/cognition/emergent/index.js +1 -0
- package/dist/cognition/emergent/index.js.map +1 -1
- package/dist/cognition/emergent/types.d.ts +11 -3
- package/dist/cognition/emergent/types.d.ts.map +1 -1
- package/dist/cognition/emergent/types.js.map +1 -1
- package/dist/io/channels/adapters/PlivoSmsChannelAdapter.d.ts +88 -23
- package/dist/io/channels/adapters/PlivoSmsChannelAdapter.d.ts.map +1 -1
- package/dist/io/channels/adapters/PlivoSmsChannelAdapter.js +245 -75
- package/dist/io/channels/adapters/PlivoSmsChannelAdapter.js.map +1 -1
- package/package.json +11 -1
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview An executor that runs forged code in QuickJS compiled to
|
|
3
|
+
* WebAssembly, one instance per call.
|
|
4
|
+
*
|
|
5
|
+
* QuickJS comes from two optional peer dependencies, `quickjs-emscripten-core`
|
|
6
|
+
* and `@jitl/quickjs-wasmfile-release-sync`, both at {@link QUICKJS_VERSION},
|
|
7
|
+
* imported when the executor loads. The variant's WebAssembly is compiled
|
|
8
|
+
* once. Each call instantiates it on a fresh `WebAssembly.Memory` whose
|
|
9
|
+
* maximum is the call's `memoryMB` (never below the build's initial 16 MiB,
|
|
10
|
+
* never above its 2,048 MiB maximum), runs one QuickJS runtime and context in
|
|
11
|
+
* it, and drops all of it when the call ends. Under Emscripten QuickJS's own
|
|
12
|
+
* memory limit counts only each allocation's overhead, so the memory's
|
|
13
|
+
* maximum is the bound. The granted functions reach the guest as host
|
|
14
|
+
* functions that take and return data (guest-surface.ts), under a prelude
|
|
15
|
+
* evaluated before the forged code (guest-prelude.ts).
|
|
16
|
+
*
|
|
17
|
+
* The binding layer copies a host value into the guest's memory without
|
|
18
|
+
* checking its allocation, so a value of more than 4 KiB is copied only after
|
|
19
|
+
* QuickJS's own allocator has found room for it: a host value that does not
|
|
20
|
+
* fit ends the call `memory_exceeded`. Smaller values and the boxes of new
|
|
21
|
+
* handles take the unchecked path; they fail only in a guest that has left
|
|
22
|
+
* itself less free memory than their size, and then only that call's own
|
|
23
|
+
* instance is affected.
|
|
24
|
+
*
|
|
25
|
+
* The guest runs on the host's thread: a guest that runs synchronously holds
|
|
26
|
+
* the event loop until the interrupt handler stops it at the call's deadline.
|
|
27
|
+
*
|
|
28
|
+
* @module @framers/agentos/emergent/executor/QuickJSExecutor
|
|
29
|
+
*/
|
|
30
|
+
import { readFile } from 'node:fs/promises';
|
|
31
|
+
import { createRequire } from 'node:module';
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
import { GUEST_PRELUDE } from './guest-prelude.js';
|
|
34
|
+
import { guestBindings } from './guest-surface.js';
|
|
35
|
+
/** The version of both QuickJS packages this release runs on. */
|
|
36
|
+
export const QUICKJS_VERSION = '0.32.0';
|
|
37
|
+
/** Host calls (`fetch`, `fs.readFile`) one call may have in flight at once. */
|
|
38
|
+
export const MAX_PENDING_HOST_CALLS = 16;
|
|
39
|
+
const CORE_PACKAGE = 'quickjs-emscripten-core';
|
|
40
|
+
const VARIANT_PACKAGE = '@jitl/quickjs-wasmfile-release-sync';
|
|
41
|
+
/** The bindings that start host work and answer with a promise. */
|
|
42
|
+
const ASYNC_BINDINGS = new Set(['fetch', 'fs_readFile']);
|
|
43
|
+
const PAGE_BYTES = 65536;
|
|
44
|
+
const MIB = 1048576;
|
|
45
|
+
/** The variant's initial memory: Emscripten's default 16 MiB, 5 MiB of it the C stack. */
|
|
46
|
+
const INITIAL_PAGES = 256;
|
|
47
|
+
/** The variant's maximum memory: Emscripten's default 2,048 MiB with memory growth. */
|
|
48
|
+
const MAXIMUM_PAGES = 32768;
|
|
49
|
+
/** QuickJS's stack limit. At 1 MiB, deep recursion exhausts the host's own stack first. */
|
|
50
|
+
const STACK_BYTES = 256 * 1024;
|
|
51
|
+
/** The result's limit after JSON, in UTF-8 bytes, as the in-process executor's output limit. */
|
|
52
|
+
const RESULT_LIMIT_BYTES = 1048576;
|
|
53
|
+
/** Host values larger than this are copied in only after QuickJS's allocator has found room for them. */
|
|
54
|
+
const CHECKED_COPY_BYTES = 4 * 1024;
|
|
55
|
+
/**
|
|
56
|
+
* The QuickJS packages are missing, at another version, or failed to load.
|
|
57
|
+
* {@link QuickJSExecutor.create} and `ready()` reject with it; a call on an
|
|
58
|
+
* executor that could not load fails with its message.
|
|
59
|
+
*/
|
|
60
|
+
export class QuickJSUnavailableError extends Error {
|
|
61
|
+
constructor(detail) {
|
|
62
|
+
super(`quickjs_unavailable: QuickJSExecutor needs ${CORE_PACKAGE} and ${VARIANT_PACKAGE}, both at ${QUICKJS_VERSION}, installed beside @framers/agentos (${detail})`);
|
|
63
|
+
this.code = 'quickjs_unavailable';
|
|
64
|
+
this.name = 'QuickJSUnavailableError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function message(error) {
|
|
68
|
+
return error instanceof Error ? error.message : String(error);
|
|
69
|
+
}
|
|
70
|
+
function errorShape(error) {
|
|
71
|
+
return error instanceof Error ? { name: error.name, message: error.message } : { name: 'Error', message: String(error) };
|
|
72
|
+
}
|
|
73
|
+
/** The version in the package.json of `name`, found above its resolved entry point. */
|
|
74
|
+
async function installedVersion(require, name) {
|
|
75
|
+
// The core's exports map does not export its package.json, so both are read
|
|
76
|
+
// from the file system above the entry the resolver returns.
|
|
77
|
+
let dir = path.dirname(require.resolve(name));
|
|
78
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
79
|
+
try {
|
|
80
|
+
const manifest = JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf8'));
|
|
81
|
+
if (manifest.name === name) {
|
|
82
|
+
return String(manifest.version);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Not this directory; look one up.
|
|
87
|
+
}
|
|
88
|
+
dir = path.dirname(dir);
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`the version of ${name} could not be read`);
|
|
91
|
+
}
|
|
92
|
+
async function loadEngine() {
|
|
93
|
+
const require = createRequire(import.meta.url);
|
|
94
|
+
let core;
|
|
95
|
+
let variant;
|
|
96
|
+
try {
|
|
97
|
+
core = await import(/* webpackIgnore: true */ 'quickjs-emscripten-core');
|
|
98
|
+
variant = (await import(/* webpackIgnore: true */ '@jitl/quickjs-wasmfile-release-sync')).default;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new QuickJSUnavailableError(`not installed: ${message(error)}`);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
for (const name of [CORE_PACKAGE, VARIANT_PACKAGE]) {
|
|
105
|
+
const version = await installedVersion(require, name);
|
|
106
|
+
if (version !== QUICKJS_VERSION) {
|
|
107
|
+
throw new Error(`${name} is ${version}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const wasm = await readFile(require.resolve(`${VARIANT_PACKAGE}/wasm`));
|
|
111
|
+
return { core, variant, compiled: await WebAssembly.compile(new Uint8Array(wasm)) };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
throw new QuickJSUnavailableError(message(error));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** The pages of the call's memory: its `memoryMB`, held between the build's initial and maximum memory. */
|
|
118
|
+
function maximumPagesFor(memoryMB) {
|
|
119
|
+
const requested = Number.isFinite(memoryMB) && memoryMB > 0 ? Math.ceil((memoryMB * MIB) / PAGE_BYTES) : 0;
|
|
120
|
+
return Math.min(MAXIMUM_PAGES, Math.max(INITIAL_PAGES, requested));
|
|
121
|
+
}
|
|
122
|
+
/** An allocation failure the WebAssembly module reported to the host rather than to QuickJS. */
|
|
123
|
+
function isHostOutOfMemory(error) {
|
|
124
|
+
return /out of memory|Aborted\(OOM\)|Cannot enlarge memory/i.test(message(error));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* About how many bytes of guest memory a host value takes once copied in,
|
|
128
|
+
* with room for the boxes and temporary copies the binding layer makes;
|
|
129
|
+
* anything that is not data throws.
|
|
130
|
+
*/
|
|
131
|
+
function guestBytes(value) {
|
|
132
|
+
if (value === undefined || value === null || typeof value === 'boolean' || typeof value === 'number') {
|
|
133
|
+
return 32;
|
|
134
|
+
}
|
|
135
|
+
if (typeof value === 'string') {
|
|
136
|
+
return 4 * value.length + 64;
|
|
137
|
+
}
|
|
138
|
+
if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
139
|
+
return value.byteLength + 64;
|
|
140
|
+
}
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
return value.reduce((total, item) => total + guestBytes(item), 128);
|
|
143
|
+
}
|
|
144
|
+
if (typeof value === 'object') {
|
|
145
|
+
const proto = Object.getPrototypeOf(value);
|
|
146
|
+
if (proto === Object.prototype || proto === null) {
|
|
147
|
+
return Object.entries(value).reduce((total, [key, item]) => total + 4 * key.length + 128 + guestBytes(item), 128);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
throw new TypeError(`${Object.prototype.toString.call(value)} cannot cross into the guest`);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Runs forged code in QuickJS compiled to WebAssembly, an instance per call.
|
|
154
|
+
* Declares `isolates: true`: the guest's heap is the call's own WebAssembly
|
|
155
|
+
* memory, and only data crosses between the host and the guest.
|
|
156
|
+
*/
|
|
157
|
+
export class QuickJSExecutor {
|
|
158
|
+
constructor() {
|
|
159
|
+
this.name = 'quickjs-wasm';
|
|
160
|
+
this.isolates = true;
|
|
161
|
+
}
|
|
162
|
+
/** An executor whose engine has loaded; rejects with {@link QuickJSUnavailableError} when it cannot load. */
|
|
163
|
+
static async create() {
|
|
164
|
+
const executor = new QuickJSExecutor();
|
|
165
|
+
await executor.ready();
|
|
166
|
+
return executor;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Imports the two packages, checks their versions and compiles the
|
|
170
|
+
* variant's WebAssembly, once; rejects with {@link QuickJSUnavailableError}.
|
|
171
|
+
* A failure is kept: later calls fail with it. The engine awaits this
|
|
172
|
+
* before a forge's test cases.
|
|
173
|
+
*/
|
|
174
|
+
async ready() {
|
|
175
|
+
await (this.engine ?? (this.engine = loadEngine()));
|
|
176
|
+
}
|
|
177
|
+
async run(request) {
|
|
178
|
+
let engine;
|
|
179
|
+
try {
|
|
180
|
+
engine = await (this.engine ?? (this.engine = loadEngine()));
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return { status: 'error', error: `Execution error: ${message(error)}`, memoryUsedBytes: 0 };
|
|
184
|
+
}
|
|
185
|
+
const maximumPages = maximumPagesFor(request.memoryMB);
|
|
186
|
+
const memory = new WebAssembly.Memory({ initial: INITIAL_PAGES, maximum: maximumPages });
|
|
187
|
+
// Ended with the call: host work the call started (a request on the path
|
|
188
|
+
// without a ceiling) stops with it.
|
|
189
|
+
const ended = new AbortController();
|
|
190
|
+
let runtime;
|
|
191
|
+
let guest;
|
|
192
|
+
let result;
|
|
193
|
+
try {
|
|
194
|
+
const module = await engine.core.newQuickJSWASMModuleFromVariant(engine.core.newVariant(engine.variant, { wasmModule: engine.compiled, wasmMemory: memory }));
|
|
195
|
+
runtime = module.newRuntime();
|
|
196
|
+
runtime.setMemoryLimit(maximumPages * PAGE_BYTES);
|
|
197
|
+
runtime.setMaxStackSize(STACK_BYTES);
|
|
198
|
+
guest = new GuestRun(runtime, memory, performance.now() + request.timeoutMs, request.signal);
|
|
199
|
+
result = await guest.execute(request, maximumPages * PAGE_BYTES, ended.signal);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
const memoryUsedBytes = memory.buffer.byteLength;
|
|
203
|
+
result = isHostOutOfMemory(error)
|
|
204
|
+
? { status: 'memory_exceeded', memoryUsedBytes }
|
|
205
|
+
: { status: 'error', error: `Execution error: the executor failed: ${message(error)}`, memoryUsedBytes };
|
|
206
|
+
}
|
|
207
|
+
ended.abort();
|
|
208
|
+
try {
|
|
209
|
+
guest?.dispose();
|
|
210
|
+
runtime?.dispose();
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// The instance is this call's alone and is dropped either way.
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* One call's guest: its context, the host work it started and its open
|
|
220
|
+
* promises. Dispose drops every reference to the instance,
|
|
221
|
+
* so host work still settling after the call holds none of the call's memory.
|
|
222
|
+
*/
|
|
223
|
+
class GuestRun {
|
|
224
|
+
constructor(runtime, memory, deadline, signal) {
|
|
225
|
+
this.memory = memory;
|
|
226
|
+
this.deadline = deadline;
|
|
227
|
+
this.signal = signal;
|
|
228
|
+
this.exhausted = false;
|
|
229
|
+
this.interrupted = false;
|
|
230
|
+
this.nextDeferred = 1;
|
|
231
|
+
this.deferreds = new Map();
|
|
232
|
+
this.pending = new Set();
|
|
233
|
+
this.runtime = runtime;
|
|
234
|
+
runtime.setInterruptHandler(() => {
|
|
235
|
+
if (performance.now() > this.deadline || this.signal?.aborted === true) {
|
|
236
|
+
this.interrupted = true;
|
|
237
|
+
}
|
|
238
|
+
return this.interrupted || this.exhausted;
|
|
239
|
+
});
|
|
240
|
+
this.context = runtime.newContext();
|
|
241
|
+
}
|
|
242
|
+
get live() {
|
|
243
|
+
if (!this.context || !this.runtime) {
|
|
244
|
+
throw new Error('the call has ended');
|
|
245
|
+
}
|
|
246
|
+
return { context: this.context, runtime: this.runtime };
|
|
247
|
+
}
|
|
248
|
+
async execute(request, memoryBytes, ended) {
|
|
249
|
+
const { context, runtime } = this.live;
|
|
250
|
+
// The intrinsic ArrayBuffer, taken before any guest code runs: it
|
|
251
|
+
// allocates through QuickJS's checked allocator.
|
|
252
|
+
const allocator = context.evalCode('((AB) => (n) => new AB(n))(ArrayBuffer)', 'guest-allocator.js', {
|
|
253
|
+
type: 'global',
|
|
254
|
+
});
|
|
255
|
+
if (allocator.error) {
|
|
256
|
+
return this.failure(allocator.error, 'the guest allocator failed: ');
|
|
257
|
+
}
|
|
258
|
+
this.allocate = context.unwrapResult(allocator);
|
|
259
|
+
const bindings = guestBindings(request.globals, {
|
|
260
|
+
bodyBytes: memoryBytes,
|
|
261
|
+
// Under a ceiling the broker ends the call's requests; without one, this does.
|
|
262
|
+
...(request.signal ? {} : { signal: ended }),
|
|
263
|
+
});
|
|
264
|
+
for (const [name, binding] of Object.entries(bindings)) {
|
|
265
|
+
this.install(name, binding);
|
|
266
|
+
}
|
|
267
|
+
const prelude = context.evalCode(GUEST_PRELUDE, 'guest-prelude.js', { type: 'global' });
|
|
268
|
+
if (prelude.error) {
|
|
269
|
+
return this.failure(prelude.error, 'the guest prelude failed: ');
|
|
270
|
+
}
|
|
271
|
+
context.unwrapResult(prelude).dispose();
|
|
272
|
+
// The in-process wrapper's contract: the code runs inside an async
|
|
273
|
+
// function, so top-level await and return behave as they do there.
|
|
274
|
+
const wrapped = [
|
|
275
|
+
'(async () => {',
|
|
276
|
+
`${request.code};`,
|
|
277
|
+
"const __entry = typeof execute === 'function' ? execute : (typeof run === 'function' ? run : null);",
|
|
278
|
+
'if (!__entry) {',
|
|
279
|
+
" throw new Error('Sandboxed tool must define execute(input) or run(input).');",
|
|
280
|
+
'}',
|
|
281
|
+
`const __out = await __entry(${JSON.stringify(request.input)});`,
|
|
282
|
+
'return __out === undefined ? undefined : JSON.stringify(__out);',
|
|
283
|
+
'})()',
|
|
284
|
+
].join('\n');
|
|
285
|
+
// The source and its input are copied in unchecked, like any host value.
|
|
286
|
+
if (4 * wrapped.length > CHECKED_COPY_BYTES) {
|
|
287
|
+
const room = this.allocateGuest(4 * wrapped.length + 64);
|
|
288
|
+
if (!room) {
|
|
289
|
+
return { status: 'memory_exceeded', memoryUsedBytes: this.memoryUsed() };
|
|
290
|
+
}
|
|
291
|
+
room.dispose();
|
|
292
|
+
}
|
|
293
|
+
const evaluated = context.evalCode(wrapped, 'forged.js', { type: 'global' });
|
|
294
|
+
if (evaluated.error) {
|
|
295
|
+
return this.failure(evaluated.error);
|
|
296
|
+
}
|
|
297
|
+
const promise = context.unwrapResult(evaluated);
|
|
298
|
+
try {
|
|
299
|
+
for (;;) {
|
|
300
|
+
const jobs = runtime.executePendingJobs();
|
|
301
|
+
if (jobs.error) {
|
|
302
|
+
return this.failure(jobs.error);
|
|
303
|
+
}
|
|
304
|
+
if (this.exhausted) {
|
|
305
|
+
return { status: 'memory_exceeded', memoryUsedBytes: this.memoryUsed() };
|
|
306
|
+
}
|
|
307
|
+
if (this.interrupted) {
|
|
308
|
+
return { status: 'timeout', memoryUsedBytes: this.memoryUsed() };
|
|
309
|
+
}
|
|
310
|
+
const state = context.getPromiseState(promise);
|
|
311
|
+
if (state.type === 'fulfilled') {
|
|
312
|
+
try {
|
|
313
|
+
return this.readResult(state.value);
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
if (!state.notAPromise && state.value.alive) {
|
|
317
|
+
state.value.dispose();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (state.type === 'rejected') {
|
|
322
|
+
return this.failure(state.error);
|
|
323
|
+
}
|
|
324
|
+
if (performance.now() > this.deadline || this.signal?.aborted === true) {
|
|
325
|
+
return { status: 'timeout', memoryUsedBytes: this.memoryUsed() };
|
|
326
|
+
}
|
|
327
|
+
await this.progress();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
if (promise.alive) {
|
|
332
|
+
promise.dispose();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** Frees what the call holds in the guest and drops every reference to the instance. */
|
|
337
|
+
dispose() {
|
|
338
|
+
for (const deferred of this.deferreds.values()) {
|
|
339
|
+
if (deferred.alive) {
|
|
340
|
+
deferred.dispose();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
this.deferreds.clear();
|
|
344
|
+
if (this.allocate?.alive) {
|
|
345
|
+
this.allocate.dispose();
|
|
346
|
+
}
|
|
347
|
+
this.allocate = undefined;
|
|
348
|
+
const context = this.context;
|
|
349
|
+
this.context = undefined;
|
|
350
|
+
this.runtime = undefined;
|
|
351
|
+
context?.dispose();
|
|
352
|
+
}
|
|
353
|
+
memoryUsed() {
|
|
354
|
+
return this.memory.buffer.byteLength;
|
|
355
|
+
}
|
|
356
|
+
/** An ArrayBuffer of `bytes` made by QuickJS's checked allocator, or undefined when the guest's memory cannot hold it. */
|
|
357
|
+
allocateGuest(bytes) {
|
|
358
|
+
const { context } = this.live;
|
|
359
|
+
if (!this.allocate) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
const size = context.newNumber(bytes);
|
|
363
|
+
try {
|
|
364
|
+
const made = context.callFunction(this.allocate, context.undefined, size);
|
|
365
|
+
if (made.error) {
|
|
366
|
+
made.error.dispose();
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
return context.unwrapResult(made);
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
size.dispose();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Host data into a new guest value, when the guest's memory can hold it.
|
|
377
|
+
* A value of more than {@link CHECKED_COPY_BYTES} is checked first by
|
|
378
|
+
* QuickJS's allocator: a buffer of the value's size is made and freed, and
|
|
379
|
+
* the value is built in the room that leaves. (The binding layer offers no
|
|
380
|
+
* write into a guest buffer: `getArrayBuffer` hands back a copy.) A value
|
|
381
|
+
* that does not fit marks the call out of memory; the guest gets undefined
|
|
382
|
+
* and is stopped at its next interrupt check.
|
|
383
|
+
*/
|
|
384
|
+
deliver(value) {
|
|
385
|
+
const { context } = this.live;
|
|
386
|
+
const bytes = guestBytes(value);
|
|
387
|
+
if (bytes > CHECKED_COPY_BYTES) {
|
|
388
|
+
const room = this.allocateGuest(bytes);
|
|
389
|
+
if (!room) {
|
|
390
|
+
this.exhausted = true;
|
|
391
|
+
return context.undefined;
|
|
392
|
+
}
|
|
393
|
+
room.dispose();
|
|
394
|
+
}
|
|
395
|
+
return this.toHandle(value);
|
|
396
|
+
}
|
|
397
|
+
install(name, binding) {
|
|
398
|
+
const { context } = this.live;
|
|
399
|
+
const fn = context.newFunction(`__host_${name}`, (...args) => {
|
|
400
|
+
const live = this.live;
|
|
401
|
+
if (ASYNC_BINDINGS.has(name) && this.pending.size >= MAX_PENDING_HOST_CALLS) {
|
|
402
|
+
return {
|
|
403
|
+
error: live.context.newError({
|
|
404
|
+
name: 'RangeError',
|
|
405
|
+
message: `${name}: ${MAX_PENDING_HOST_CALLS} host calls are in flight in this call; await one before starting another`,
|
|
406
|
+
}),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
let values;
|
|
410
|
+
try {
|
|
411
|
+
values = args.map((arg) => live.context.dump(arg));
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
return { error: live.context.newError({ name: 'TypeError', message: `${name} takes data only: ${message(error)}` }) };
|
|
415
|
+
}
|
|
416
|
+
let result;
|
|
417
|
+
try {
|
|
418
|
+
result = binding(...values);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
return { error: live.context.newError(errorShape(error)) };
|
|
422
|
+
}
|
|
423
|
+
if (result instanceof Promise) {
|
|
424
|
+
return this.bridge(result);
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
return this.deliver(result);
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
return {
|
|
431
|
+
error: live.context.newError({ name: 'TypeError', message: `${name} returned a value that is not data: ${message(error)}` }),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
context.setProp(context.global, `__host_${name}`, fn);
|
|
436
|
+
fn.dispose();
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* A guest promise settled by a host promise; the guest's continuation runs
|
|
440
|
+
* at the next pump. The host promise's reactions hold this run and the
|
|
441
|
+
* promise's id only, so after the call they reach nothing of the instance.
|
|
442
|
+
*/
|
|
443
|
+
bridge(result) {
|
|
444
|
+
const { context } = this.live;
|
|
445
|
+
const deferred = context.newPromise();
|
|
446
|
+
const id = this.nextDeferred;
|
|
447
|
+
this.nextDeferred += 1;
|
|
448
|
+
this.deferreds.set(id, deferred);
|
|
449
|
+
const settled = result
|
|
450
|
+
.then((value) => this.settle(id, { value }), (error) => this.settle(id, { error }))
|
|
451
|
+
.catch(() => undefined)
|
|
452
|
+
.finally(() => {
|
|
453
|
+
this.pending.delete(settled);
|
|
454
|
+
});
|
|
455
|
+
this.pending.add(settled);
|
|
456
|
+
return deferred.handle;
|
|
457
|
+
}
|
|
458
|
+
settle(id, outcome) {
|
|
459
|
+
const deferred = this.deferreds.get(id);
|
|
460
|
+
this.deferreds.delete(id);
|
|
461
|
+
if (!deferred || !deferred.alive || !this.context) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const context = this.context;
|
|
465
|
+
try {
|
|
466
|
+
if ('value' in outcome) {
|
|
467
|
+
let handle;
|
|
468
|
+
try {
|
|
469
|
+
handle = this.deliver(outcome.value);
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
const rejection = context.newError({
|
|
473
|
+
name: 'TypeError',
|
|
474
|
+
message: `the host returned a value that is not data: ${message(error)}`,
|
|
475
|
+
});
|
|
476
|
+
deferred.reject(rejection);
|
|
477
|
+
rejection.dispose();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
deferred.resolve(handle);
|
|
481
|
+
if (handle.alive) {
|
|
482
|
+
handle.dispose();
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const rejection = context.newError(errorShape(outcome.error));
|
|
487
|
+
deferred.reject(rejection);
|
|
488
|
+
rejection.dispose();
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
if (deferred.alive) {
|
|
492
|
+
deferred.dispose();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/** Host data into a new guest value, built through the binding layer; anything that is not data throws. */
|
|
497
|
+
toHandle(value) {
|
|
498
|
+
const { context } = this.live;
|
|
499
|
+
if (value === undefined)
|
|
500
|
+
return context.undefined;
|
|
501
|
+
if (value === null)
|
|
502
|
+
return context.null;
|
|
503
|
+
if (typeof value === 'boolean')
|
|
504
|
+
return value ? context.true : context.false;
|
|
505
|
+
if (typeof value === 'number')
|
|
506
|
+
return context.newNumber(value);
|
|
507
|
+
if (typeof value === 'string')
|
|
508
|
+
return context.newString(value);
|
|
509
|
+
if (value instanceof Uint8Array) {
|
|
510
|
+
return context.newArrayBuffer(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
|
|
511
|
+
}
|
|
512
|
+
if (value instanceof ArrayBuffer)
|
|
513
|
+
return context.newArrayBuffer(value.slice(0));
|
|
514
|
+
if (Array.isArray(value)) {
|
|
515
|
+
const array = context.newArray();
|
|
516
|
+
try {
|
|
517
|
+
value.forEach((item, index) => {
|
|
518
|
+
const handle = this.toHandle(item);
|
|
519
|
+
context.setProp(array, index, handle);
|
|
520
|
+
handle.dispose();
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
array.dispose();
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
return array;
|
|
528
|
+
}
|
|
529
|
+
if (typeof value === 'object') {
|
|
530
|
+
const proto = Object.getPrototypeOf(value);
|
|
531
|
+
if (proto === Object.prototype || proto === null) {
|
|
532
|
+
const object = context.newObject();
|
|
533
|
+
try {
|
|
534
|
+
for (const [key, item] of Object.entries(value)) {
|
|
535
|
+
const handle = this.toHandle(item);
|
|
536
|
+
context.setProp(object, key, handle);
|
|
537
|
+
handle.dispose();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
object.dispose();
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
return object;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
throw new TypeError(`${Object.prototype.toString.call(value)} cannot cross into the guest`);
|
|
548
|
+
}
|
|
549
|
+
/** Waits for a host operation to settle, the deadline, or the call's signal. */
|
|
550
|
+
async progress() {
|
|
551
|
+
let timer;
|
|
552
|
+
let onAbort;
|
|
553
|
+
const waits = [...this.pending];
|
|
554
|
+
waits.push(new Promise((resolve) => {
|
|
555
|
+
timer = setTimeout(resolve, Math.max(0, this.deadline - performance.now()) + 1);
|
|
556
|
+
}));
|
|
557
|
+
const signal = this.signal;
|
|
558
|
+
if (signal) {
|
|
559
|
+
waits.push(new Promise((resolve) => {
|
|
560
|
+
onAbort = () => resolve();
|
|
561
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
562
|
+
}));
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
await Promise.race(waits);
|
|
566
|
+
}
|
|
567
|
+
finally {
|
|
568
|
+
if (timer !== undefined) {
|
|
569
|
+
clearTimeout(timer);
|
|
570
|
+
}
|
|
571
|
+
if (signal && onAbort) {
|
|
572
|
+
signal.removeEventListener('abort', onAbort);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* The call's result: undefined, or the JSON text the wrapper made. The
|
|
578
|
+
* text is copied out through an allocation the binding layer does not
|
|
579
|
+
* check, so its length is read first: past the output limit it is not
|
|
580
|
+
* copied, and a long text is copied only once QuickJS's allocator has found
|
|
581
|
+
* room for the copy.
|
|
582
|
+
*/
|
|
583
|
+
readResult(handle) {
|
|
584
|
+
const { context } = this.live;
|
|
585
|
+
if (context.typeof(handle) !== 'string') {
|
|
586
|
+
return this.success(context.dump(handle));
|
|
587
|
+
}
|
|
588
|
+
const lengthHandle = context.getProp(handle, 'length');
|
|
589
|
+
const length = context.getNumber(lengthHandle);
|
|
590
|
+
lengthHandle.dispose();
|
|
591
|
+
// More characters than the limit's bytes is past the limit in UTF-8 too.
|
|
592
|
+
if (length > RESULT_LIMIT_BYTES) {
|
|
593
|
+
return this.overLimit();
|
|
594
|
+
}
|
|
595
|
+
if (3 * length > CHECKED_COPY_BYTES) {
|
|
596
|
+
const room = this.allocateGuest(3 * length + 64);
|
|
597
|
+
if (!room) {
|
|
598
|
+
return { status: 'memory_exceeded', memoryUsedBytes: this.memoryUsed() };
|
|
599
|
+
}
|
|
600
|
+
room.dispose();
|
|
601
|
+
}
|
|
602
|
+
return this.success(context.getString(handle));
|
|
603
|
+
}
|
|
604
|
+
success(value) {
|
|
605
|
+
const memoryUsedBytes = this.memoryUsed();
|
|
606
|
+
if (typeof value !== 'string') {
|
|
607
|
+
return { status: 'ok', output: value, memoryUsedBytes };
|
|
608
|
+
}
|
|
609
|
+
if (Buffer.byteLength(value, 'utf8') > RESULT_LIMIT_BYTES) {
|
|
610
|
+
return this.overLimit();
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
return { status: 'ok', output: JSON.parse(value), memoryUsedBytes };
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
return { status: 'ok', output: value, memoryUsedBytes };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
overLimit() {
|
|
620
|
+
return {
|
|
621
|
+
status: 'error',
|
|
622
|
+
error: "Execution error: the result passed the QuickJS executor's output limit of 1 MB",
|
|
623
|
+
memoryUsedBytes: this.memoryUsed(),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
/** How a call that threw ended: out of memory or past its deadline before anything the error says. */
|
|
627
|
+
failure(errorHandle, prefix = '') {
|
|
628
|
+
const memoryUsedBytes = this.memoryUsed();
|
|
629
|
+
if (this.exhausted || this.interrupted) {
|
|
630
|
+
if (errorHandle.alive) {
|
|
631
|
+
errorHandle.dispose();
|
|
632
|
+
}
|
|
633
|
+
return this.exhausted ? { status: 'memory_exceeded', memoryUsedBytes } : { status: 'timeout', memoryUsedBytes };
|
|
634
|
+
}
|
|
635
|
+
let error;
|
|
636
|
+
try {
|
|
637
|
+
error = this.live.context.dump(errorHandle);
|
|
638
|
+
}
|
|
639
|
+
catch (dumpError) {
|
|
640
|
+
error = { message: `(the error could not be read: ${message(dumpError)})` };
|
|
641
|
+
}
|
|
642
|
+
finally {
|
|
643
|
+
if (errorHandle.alive) {
|
|
644
|
+
errorHandle.dispose();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const shape = error !== null && typeof error === 'object' ? error : undefined;
|
|
648
|
+
if (shape?.message === 'out of memory') {
|
|
649
|
+
return { status: 'memory_exceeded', memoryUsedBytes };
|
|
650
|
+
}
|
|
651
|
+
const text = typeof shape?.message === 'string' ? shape.message : String(error);
|
|
652
|
+
return { status: 'error', error: `Execution error: ${prefix}${text}`, memoryUsedBytes };
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
//# sourceMappingURL=QuickJSExecutor.js.map
|