@tanstack/ai-isolate-quickjs 0.1.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 +57 -0
- package/dist/esm/error-normalizer.d.ts +10 -0
- package/dist/esm/error-normalizer.js +60 -0
- package/dist/esm/error-normalizer.js.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/isolate-context.d.ts +15 -0
- package/dist/esm/isolate-context.js +122 -0
- package/dist/esm/isolate-context.js.map +1 -0
- package/dist/esm/isolate-driver.d.ts +55 -0
- package/dist/esm/isolate-driver.js +88 -0
- package/dist/esm/isolate-driver.js.map +1 -0
- package/package.json +57 -0
- package/src/error-normalizer.ts +79 -0
- package/src/index.ts +13 -0
- package/src/isolate-context.ts +164 -0
- package/src/isolate-driver.ts +185 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @tanstack/ai-isolate-quickjs
|
|
2
|
+
|
|
3
|
+
QuickJS WASM driver for TanStack AI Code Mode. Runs everywhere — Node.js, browsers, and edge runtimes — with zero native dependencies.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @tanstack/ai-isolate-quickjs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createQuickJSIsolateDriver } from '@tanstack/ai-isolate-quickjs'
|
|
15
|
+
import { createCodeModeTool } from '@tanstack/ai-code-mode'
|
|
16
|
+
|
|
17
|
+
const driver = createQuickJSIsolateDriver({
|
|
18
|
+
timeout: 30000, // execution timeout in ms (default: 30000)
|
|
19
|
+
memoryLimit: 128, // memory limit in MB (default: 128)
|
|
20
|
+
maxStackSize: 512 * 1024, // max stack size in bytes (default: 512 KiB)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const executeTypescript = createCodeModeTool({
|
|
24
|
+
driver,
|
|
25
|
+
tools: [myTool],
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Config Options
|
|
30
|
+
|
|
31
|
+
- `timeout` — Default execution timeout in milliseconds (default: 30000)
|
|
32
|
+
- `memoryLimit` — Default QuickJS runtime memory limit in MB (default: 128)
|
|
33
|
+
- `maxStackSize` — Default QuickJS runtime max stack size in bytes (default: 524288)
|
|
34
|
+
|
|
35
|
+
## Tradeoffs vs Node Driver
|
|
36
|
+
|
|
37
|
+
| | QuickJS (WASM) | Node (`isolated-vm`) |
|
|
38
|
+
| --------------- | -------------------------- | ----------------------- |
|
|
39
|
+
| Native deps | None | Yes (C++ addon) |
|
|
40
|
+
| Browser support | Yes | No |
|
|
41
|
+
| Performance | Slower (interpreted) | Faster (V8 JIT) |
|
|
42
|
+
| Memory limit | Configurable | Configurable |
|
|
43
|
+
| Best for | Browser, edge, portability | Server-side performance |
|
|
44
|
+
|
|
45
|
+
## How It Works
|
|
46
|
+
|
|
47
|
+
Uses [QuickJS](https://bellard.org/quickjs/) compiled to WebAssembly via [`quickjs-emscripten`](https://github.com/nicolo-ribaudo/quickjs-emscripten). Each execution creates a fresh async QuickJS context with tool bindings injected as global async functions.
|
|
48
|
+
|
|
49
|
+
## Runtime Limits and Errors
|
|
50
|
+
|
|
51
|
+
- QuickJS enforces runtime memory and stack limits for each context.
|
|
52
|
+
- Exceeding limits can produce normalized errors such as `MemoryLimitError` or `StackOverflowError`.
|
|
53
|
+
- Certain fatal limit conditions may dispose the underlying VM; create a fresh context before running more code after disposal.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NormalizedError } from '@tanstack/ai-code-mode';
|
|
2
|
+
/**
|
|
3
|
+
* Whether this normalized error indicates the QuickJS VM should not be reused
|
|
4
|
+
* (memory or stack limit exceeded).
|
|
5
|
+
*/
|
|
6
|
+
export declare function isFatalQuickJSLimitError(error: NormalizedError): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Normalize various error types into a consistent format
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeError(error: unknown): NormalizedError;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const MEMORY_LIMIT_ERROR = "MemoryLimitError";
|
|
2
|
+
const STACK_OVERFLOW_ERROR = "StackOverflowError";
|
|
3
|
+
function isFatalQuickJSLimitError(error) {
|
|
4
|
+
return error.name === MEMORY_LIMIT_ERROR || error.name === STACK_OVERFLOW_ERROR;
|
|
5
|
+
}
|
|
6
|
+
function normalizeError(error) {
|
|
7
|
+
if (error instanceof Error) {
|
|
8
|
+
const msg = error.message;
|
|
9
|
+
const lower = msg.toLowerCase();
|
|
10
|
+
if (lower.includes("out of memory") || lower.includes("memory alloc") || error.name === "InternalError" && lower.includes("memory")) {
|
|
11
|
+
return {
|
|
12
|
+
name: MEMORY_LIMIT_ERROR,
|
|
13
|
+
message: "Code execution exceeded memory limit",
|
|
14
|
+
stack: error.stack
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (lower.includes("stack overflow")) {
|
|
18
|
+
return {
|
|
19
|
+
name: STACK_OVERFLOW_ERROR,
|
|
20
|
+
message: "Code execution exceeded stack size limit",
|
|
21
|
+
stack: error.stack
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (error.name === "RuntimeError" && lower.includes("unreachable")) {
|
|
25
|
+
return {
|
|
26
|
+
name: "WasmRuntimeError",
|
|
27
|
+
message: msg || "WebAssembly runtime error",
|
|
28
|
+
stack: error.stack
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
name: error.name,
|
|
33
|
+
message: error.message,
|
|
34
|
+
stack: error.stack
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (typeof error === "string") {
|
|
38
|
+
return {
|
|
39
|
+
name: "Error",
|
|
40
|
+
message: error
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (typeof error === "object" && error !== null) {
|
|
44
|
+
const errObj = error;
|
|
45
|
+
return {
|
|
46
|
+
name: String(errObj.name || "Error"),
|
|
47
|
+
message: String(errObj.message || "Unknown error"),
|
|
48
|
+
stack: errObj.stack ? String(errObj.stack) : void 0
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
name: "UnknownError",
|
|
53
|
+
message: String(error)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
isFatalQuickJSLimitError,
|
|
58
|
+
normalizeError
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=error-normalizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-normalizer.js","sources":["../../src/error-normalizer.ts"],"sourcesContent":["import type { NormalizedError } from '@tanstack/ai-code-mode'\n\nconst MEMORY_LIMIT_ERROR = 'MemoryLimitError'\nconst STACK_OVERFLOW_ERROR = 'StackOverflowError'\n\n/**\n * Whether this normalized error indicates the QuickJS VM should not be reused\n * (memory or stack limit exceeded).\n */\nexport function isFatalQuickJSLimitError(error: NormalizedError): boolean {\n return (\n error.name === MEMORY_LIMIT_ERROR || error.name === STACK_OVERFLOW_ERROR\n )\n}\n\n/**\n * Normalize various error types into a consistent format\n */\nexport function normalizeError(error: unknown): NormalizedError {\n if (error instanceof Error) {\n const msg = error.message\n const lower = msg.toLowerCase()\n\n if (\n lower.includes('out of memory') ||\n lower.includes('memory alloc') ||\n (error.name === 'InternalError' && lower.includes('memory'))\n ) {\n return {\n name: MEMORY_LIMIT_ERROR,\n message: 'Code execution exceeded memory limit',\n stack: error.stack,\n }\n }\n\n if (lower.includes('stack overflow')) {\n return {\n name: STACK_OVERFLOW_ERROR,\n message: 'Code execution exceeded stack size limit',\n stack: error.stack,\n }\n }\n\n if (error.name === 'RuntimeError' && lower.includes('unreachable')) {\n return {\n name: 'WasmRuntimeError',\n message: msg || 'WebAssembly runtime error',\n stack: error.stack,\n }\n }\n\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n if (typeof error === 'string') {\n return {\n name: 'Error',\n message: error,\n }\n }\n\n if (typeof error === 'object' && error !== null) {\n const errObj = error as Record<string, unknown>\n return {\n name: String(errObj.name || 'Error'),\n message: String(errObj.message || 'Unknown error'),\n stack: errObj.stack ? String(errObj.stack) : undefined,\n }\n }\n\n return {\n name: 'UnknownError',\n message: String(error),\n }\n}\n"],"names":[],"mappings":"AAEA,MAAM,qBAAqB;AAC3B,MAAM,uBAAuB;AAMtB,SAAS,yBAAyB,OAAiC;AACxE,SACE,MAAM,SAAS,sBAAsB,MAAM,SAAS;AAExD;AAKO,SAAS,eAAe,OAAiC;AAC9D,MAAI,iBAAiB,OAAO;AAC1B,UAAM,MAAM,MAAM;AAClB,UAAM,QAAQ,IAAI,YAAA;AAElB,QACE,MAAM,SAAS,eAAe,KAC9B,MAAM,SAAS,cAAc,KAC5B,MAAM,SAAS,mBAAmB,MAAM,SAAS,QAAQ,GAC1D;AACA,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS;AAAA,QACT,OAAO,MAAM;AAAA,MAAA;AAAA,IAEjB;AAEA,QAAI,MAAM,SAAS,gBAAgB,GAAG;AACpC,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS;AAAA,QACT,OAAO,MAAM;AAAA,MAAA;AAAA,IAEjB;AAEA,QAAI,MAAM,SAAS,kBAAkB,MAAM,SAAS,aAAa,GAAG;AAClE,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,OAAO;AAAA,QAChB,OAAO,MAAM;AAAA,MAAA;AAAA,IAEjB;AAEA,WAAO;AAAA,MACL,MAAM,MAAM;AAAA,MACZ,SAAS,MAAM;AAAA,MACf,OAAO,MAAM;AAAA,IAAA;AAAA,EAEjB;AAEA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,IAAA;AAAA,EAEb;AAEA,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,UAAM,SAAS;AACf,WAAO;AAAA,MACL,MAAM,OAAO,OAAO,QAAQ,OAAO;AAAA,MACnC,SAAS,OAAO,OAAO,WAAW,eAAe;AAAA,MACjD,OAAO,OAAO,QAAQ,OAAO,OAAO,KAAK,IAAI;AAAA,IAAA;AAAA,EAEjD;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS,OAAO,KAAK;AAAA,EAAA;AAEzB;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { QuickJSAsyncContext } from 'quickjs-emscripten';
|
|
2
|
+
import { ExecutionResult, IsolateContext } from '@tanstack/ai-code-mode';
|
|
3
|
+
/**
|
|
4
|
+
* IsolateContext implementation using QuickJS WASM
|
|
5
|
+
*/
|
|
6
|
+
export declare class QuickJSIsolateContext implements IsolateContext {
|
|
7
|
+
private vm;
|
|
8
|
+
private logs;
|
|
9
|
+
private timeout;
|
|
10
|
+
private disposed;
|
|
11
|
+
private executing;
|
|
12
|
+
constructor(vm: QuickJSAsyncContext, logs: Array<string>, timeout: number);
|
|
13
|
+
execute<T = unknown>(code: string): Promise<ExecutionResult<T>>;
|
|
14
|
+
dispose(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { wrapCode } from "@tanstack/ai-code-mode";
|
|
2
|
+
import { normalizeError, isFatalQuickJSLimitError } from "./error-normalizer.js";
|
|
3
|
+
let globalExecQueue = Promise.resolve();
|
|
4
|
+
class QuickJSIsolateContext {
|
|
5
|
+
constructor(vm, logs, timeout) {
|
|
6
|
+
this.disposed = false;
|
|
7
|
+
this.executing = false;
|
|
8
|
+
this.vm = vm;
|
|
9
|
+
this.logs = logs;
|
|
10
|
+
this.timeout = timeout;
|
|
11
|
+
}
|
|
12
|
+
async execute(code) {
|
|
13
|
+
if (this.disposed) {
|
|
14
|
+
return {
|
|
15
|
+
success: false,
|
|
16
|
+
error: {
|
|
17
|
+
name: "DisposedError",
|
|
18
|
+
message: "Context has been disposed"
|
|
19
|
+
},
|
|
20
|
+
logs: []
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
let resolve;
|
|
24
|
+
const myTurn = new Promise((r) => {
|
|
25
|
+
resolve = r;
|
|
26
|
+
});
|
|
27
|
+
const waitForPrev = globalExecQueue;
|
|
28
|
+
globalExecQueue = myTurn;
|
|
29
|
+
await waitForPrev;
|
|
30
|
+
if (this.disposed) {
|
|
31
|
+
resolve();
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
error: {
|
|
35
|
+
name: "DisposedError",
|
|
36
|
+
message: "Context has been disposed"
|
|
37
|
+
},
|
|
38
|
+
logs: []
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
this.executing = true;
|
|
42
|
+
this.logs.length = 0;
|
|
43
|
+
const releaseVmAfterFatalLimit = () => {
|
|
44
|
+
if (this.disposed) return;
|
|
45
|
+
try {
|
|
46
|
+
this.vm.runtime.setInterruptHandler(() => false);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
this.disposed = true;
|
|
50
|
+
this.vm.dispose();
|
|
51
|
+
};
|
|
52
|
+
const fail = (error) => {
|
|
53
|
+
const normalized = normalizeError(error);
|
|
54
|
+
if (isFatalQuickJSLimitError(normalized)) {
|
|
55
|
+
releaseVmAfterFatalLimit();
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: normalized,
|
|
60
|
+
logs: [...this.logs]
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
const wrappedCode = wrapCode(code);
|
|
65
|
+
const deadline = Date.now() + this.timeout;
|
|
66
|
+
this.vm.runtime.setInterruptHandler(() => {
|
|
67
|
+
return Date.now() > deadline;
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
const result = await this.vm.evalCodeAsync(wrappedCode);
|
|
71
|
+
let parsedResult;
|
|
72
|
+
try {
|
|
73
|
+
const promiseHandle = this.vm.unwrapResult(result);
|
|
74
|
+
const nativePromise = this.vm.resolvePromise(promiseHandle);
|
|
75
|
+
promiseHandle.dispose();
|
|
76
|
+
this.vm.runtime.executePendingJobs();
|
|
77
|
+
const resolvedResult = await nativePromise;
|
|
78
|
+
const valueHandle = this.vm.unwrapResult(resolvedResult);
|
|
79
|
+
const dumpedResult = this.vm.dump(valueHandle);
|
|
80
|
+
valueHandle.dispose();
|
|
81
|
+
if (typeof dumpedResult === "string") {
|
|
82
|
+
try {
|
|
83
|
+
parsedResult = JSON.parse(dumpedResult);
|
|
84
|
+
} catch {
|
|
85
|
+
parsedResult = dumpedResult;
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
parsedResult = dumpedResult;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
value: parsedResult,
|
|
93
|
+
logs: [...this.logs]
|
|
94
|
+
};
|
|
95
|
+
} catch (unwrapError) {
|
|
96
|
+
return fail(unwrapError);
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
if (!this.disposed) {
|
|
100
|
+
this.vm.runtime.setInterruptHandler(() => false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return fail(error);
|
|
105
|
+
} finally {
|
|
106
|
+
this.executing = false;
|
|
107
|
+
resolve();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async dispose() {
|
|
111
|
+
if (this.disposed) return;
|
|
112
|
+
if (this.executing) {
|
|
113
|
+
await globalExecQueue;
|
|
114
|
+
}
|
|
115
|
+
this.disposed = true;
|
|
116
|
+
this.vm.dispose();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
QuickJSIsolateContext
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=isolate-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isolate-context.js","sources":["../../src/isolate-context.ts"],"sourcesContent":["import { wrapCode } from '@tanstack/ai-code-mode'\nimport { isFatalQuickJSLimitError, normalizeError } from './error-normalizer'\nimport type { QuickJSAsyncContext } from 'quickjs-emscripten'\nimport type { ExecutionResult, IsolateContext } from '@tanstack/ai-code-mode'\n\n/**\n * Serializes all QuickJS evalCodeAsync calls across contexts.\n * Required because newAsyncContext() reuses a singleton WASM module\n * whose asyncify stack can only handle one suspension at a time.\n */\nlet globalExecQueue: Promise<void> = Promise.resolve()\n\n/**\n * IsolateContext implementation using QuickJS WASM\n */\nexport class QuickJSIsolateContext implements IsolateContext {\n private vm: QuickJSAsyncContext\n private logs: Array<string>\n private timeout: number\n private disposed = false\n private executing = false\n\n constructor(vm: QuickJSAsyncContext, logs: Array<string>, timeout: number) {\n this.vm = vm\n this.logs = logs\n this.timeout = timeout\n }\n\n async execute<T = unknown>(code: string): Promise<ExecutionResult<T>> {\n if (this.disposed) {\n return {\n success: false,\n error: {\n name: 'DisposedError',\n message: 'Context has been disposed',\n },\n logs: [],\n }\n }\n\n // Serialize through the global queue to prevent concurrent\n // WASM asyncify suspensions across contexts.\n let resolve!: () => void\n const myTurn = new Promise<void>((r) => {\n resolve = r\n })\n const waitForPrev = globalExecQueue\n globalExecQueue = myTurn\n\n await waitForPrev\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- dispose() may be called concurrently while awaiting the queue\n if (this.disposed) {\n resolve()\n return {\n success: false,\n error: {\n name: 'DisposedError',\n message: 'Context has been disposed',\n },\n logs: [],\n }\n }\n\n this.executing = true\n this.logs.length = 0\n\n const releaseVmAfterFatalLimit = () => {\n if (this.disposed) return\n try {\n this.vm.runtime.setInterruptHandler(() => false)\n } catch {\n // ignore if runtime is already torn down\n }\n this.disposed = true\n this.vm.dispose()\n }\n\n const fail = (error: unknown) => {\n const normalized = normalizeError(error)\n if (isFatalQuickJSLimitError(normalized)) {\n releaseVmAfterFatalLimit()\n }\n return {\n success: false as const,\n error: normalized,\n logs: [...this.logs],\n }\n }\n\n try {\n const wrappedCode = wrapCode(code)\n\n const deadline = Date.now() + this.timeout\n this.vm.runtime.setInterruptHandler(() => {\n return Date.now() > deadline\n })\n\n try {\n const result = await this.vm.evalCodeAsync(wrappedCode)\n\n let parsedResult: T\n try {\n const promiseHandle = this.vm.unwrapResult(result)\n\n // evalCodeAsync returns a Promise handle (our wrapper is an async IIFE).\n // Use resolvePromise + executePendingJobs to properly await the\n // QuickJS promise without re-entering the WASM asyncify state.\n const nativePromise = this.vm.resolvePromise(promiseHandle)\n promiseHandle.dispose()\n this.vm.runtime.executePendingJobs()\n const resolvedResult = await nativePromise\n\n const valueHandle = this.vm.unwrapResult(resolvedResult)\n const dumpedResult = this.vm.dump(valueHandle)\n valueHandle.dispose()\n\n if (typeof dumpedResult === 'string') {\n try {\n parsedResult = JSON.parse(dumpedResult) as T\n } catch {\n parsedResult = dumpedResult as T\n }\n } else {\n parsedResult = dumpedResult as T\n }\n\n return {\n success: true,\n value: parsedResult,\n logs: [...this.logs],\n }\n } catch (unwrapError) {\n return fail(unwrapError)\n }\n } finally {\n // fail() may set disposed when releasing the VM after memory/stack limit errors\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- disposed set in fail()\n if (!this.disposed) {\n this.vm.runtime.setInterruptHandler(() => false)\n }\n }\n } catch (error) {\n return fail(error)\n } finally {\n this.executing = false\n resolve()\n }\n }\n\n async dispose(): Promise<void> {\n if (this.disposed) return\n\n // If an execution is in flight, wait for the global queue to drain\n // before disposing the VM. Otherwise the asyncified callback would\n // try to access a freed context.\n if (this.executing) {\n await globalExecQueue\n }\n\n this.disposed = true\n this.vm.dispose()\n }\n}\n"],"names":[],"mappings":";;AAUA,IAAI,kBAAiC,QAAQ,QAAA;AAKtC,MAAM,sBAAgD;AAAA,EAO3D,YAAY,IAAyB,MAAqB,SAAiB;AAH3E,SAAQ,WAAW;AACnB,SAAQ,YAAY;AAGlB,SAAK,KAAK;AACV,SAAK,OAAO;AACZ,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAM,QAAqB,MAA2C;AACpE,QAAI,KAAK,UAAU;AACjB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO;AAAA,UACL,MAAM;AAAA,UACN,SAAS;AAAA,QAAA;AAAA,QAEX,MAAM,CAAA;AAAA,MAAC;AAAA,IAEX;AAIA,QAAI;AACJ,UAAM,SAAS,IAAI,QAAc,CAAC,MAAM;AACtC,gBAAU;AAAA,IACZ,CAAC;AACD,UAAM,cAAc;AACpB,sBAAkB;AAElB,UAAM;AAGN,QAAI,KAAK,UAAU;AACjB,cAAA;AACA,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO;AAAA,UACL,MAAM;AAAA,UACN,SAAS;AAAA,QAAA;AAAA,QAEX,MAAM,CAAA;AAAA,MAAC;AAAA,IAEX;AAEA,SAAK,YAAY;AACjB,SAAK,KAAK,SAAS;AAEnB,UAAM,2BAA2B,MAAM;AACrC,UAAI,KAAK,SAAU;AACnB,UAAI;AACF,aAAK,GAAG,QAAQ,oBAAoB,MAAM,KAAK;AAAA,MACjD,QAAQ;AAAA,MAER;AACA,WAAK,WAAW;AAChB,WAAK,GAAG,QAAA;AAAA,IACV;AAEA,UAAM,OAAO,CAAC,UAAmB;AAC/B,YAAM,aAAa,eAAe,KAAK;AACvC,UAAI,yBAAyB,UAAU,GAAG;AACxC,iCAAA;AAAA,MACF;AACA,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO;AAAA,QACP,MAAM,CAAC,GAAG,KAAK,IAAI;AAAA,MAAA;AAAA,IAEvB;AAEA,QAAI;AACF,YAAM,cAAc,SAAS,IAAI;AAEjC,YAAM,WAAW,KAAK,IAAA,IAAQ,KAAK;AACnC,WAAK,GAAG,QAAQ,oBAAoB,MAAM;AACxC,eAAO,KAAK,QAAQ;AAAA,MACtB,CAAC;AAED,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,GAAG,cAAc,WAAW;AAEtD,YAAI;AACJ,YAAI;AACF,gBAAM,gBAAgB,KAAK,GAAG,aAAa,MAAM;AAKjD,gBAAM,gBAAgB,KAAK,GAAG,eAAe,aAAa;AAC1D,wBAAc,QAAA;AACd,eAAK,GAAG,QAAQ,mBAAA;AAChB,gBAAM,iBAAiB,MAAM;AAE7B,gBAAM,cAAc,KAAK,GAAG,aAAa,cAAc;AACvD,gBAAM,eAAe,KAAK,GAAG,KAAK,WAAW;AAC7C,sBAAY,QAAA;AAEZ,cAAI,OAAO,iBAAiB,UAAU;AACpC,gBAAI;AACF,6BAAe,KAAK,MAAM,YAAY;AAAA,YACxC,QAAQ;AACN,6BAAe;AAAA,YACjB;AAAA,UACF,OAAO;AACL,2BAAe;AAAA,UACjB;AAEA,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,OAAO;AAAA,YACP,MAAM,CAAC,GAAG,KAAK,IAAI;AAAA,UAAA;AAAA,QAEvB,SAAS,aAAa;AACpB,iBAAO,KAAK,WAAW;AAAA,QACzB;AAAA,MACF,UAAA;AAGE,YAAI,CAAC,KAAK,UAAU;AAClB,eAAK,GAAG,QAAQ,oBAAoB,MAAM,KAAK;AAAA,QACjD;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,aAAO,KAAK,KAAK;AAAA,IACnB,UAAA;AACE,WAAK,YAAY;AACjB,cAAA;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,UAAyB;AAC7B,QAAI,KAAK,SAAU;AAKnB,QAAI,KAAK,WAAW;AAClB,YAAM;AAAA,IACR;AAEA,SAAK,WAAW;AAChB,SAAK,GAAG,QAAA;AAAA,EACV;AACF;"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { IsolateDriver } from '@tanstack/ai-code-mode';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for the QuickJS WASM isolate driver
|
|
4
|
+
*/
|
|
5
|
+
export interface QuickJSIsolateDriverConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Default execution timeout in ms (default: 30000)
|
|
8
|
+
*/
|
|
9
|
+
timeout?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Default memory limit in MB (default: 128).
|
|
12
|
+
* Applied via QuickJS `runtime.setMemoryLimit`.
|
|
13
|
+
*/
|
|
14
|
+
memoryLimit?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Default max stack size in bytes (default: 512 KiB).
|
|
17
|
+
* Applied via QuickJS `runtime.setMaxStackSize`.
|
|
18
|
+
*/
|
|
19
|
+
maxStackSize?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create a QuickJS WASM isolate driver
|
|
23
|
+
*
|
|
24
|
+
* This driver uses QuickJS compiled to WebAssembly via Emscripten.
|
|
25
|
+
* It provides a sandboxed JavaScript environment that runs anywhere
|
|
26
|
+
* (Node.js, browser, edge) without native dependencies.
|
|
27
|
+
*
|
|
28
|
+
* Tools are injected as async functions that bridge back to the host.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { createQuickJSIsolateDriver } from '@tanstack/ai-isolate-quickjs'
|
|
33
|
+
*
|
|
34
|
+
* const driver = createQuickJSIsolateDriver({
|
|
35
|
+
* timeout: 30000,
|
|
36
|
+
* })
|
|
37
|
+
*
|
|
38
|
+
* const context = await driver.createContext({
|
|
39
|
+
* bindings: {
|
|
40
|
+
* readFile: {
|
|
41
|
+
* name: 'readFile',
|
|
42
|
+
* description: 'Read a file',
|
|
43
|
+
* inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
|
|
44
|
+
* execute: async ({ path }) => fs.readFile(path, 'utf-8'),
|
|
45
|
+
* },
|
|
46
|
+
* },
|
|
47
|
+
* })
|
|
48
|
+
*
|
|
49
|
+
* const result = await context.execute(`
|
|
50
|
+
* const content = await readFile({ path: './data.json' })
|
|
51
|
+
* return JSON.parse(content)
|
|
52
|
+
* `)
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function createQuickJSIsolateDriver(config?: QuickJSIsolateDriverConfig): IsolateDriver;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { newAsyncContext } from "quickjs-emscripten";
|
|
2
|
+
import { QuickJSIsolateContext } from "./isolate-context.js";
|
|
3
|
+
const DEFAULT_MEMORY_LIMIT_MB = 128;
|
|
4
|
+
const DEFAULT_MAX_STACK_SIZE_BYTES = 512 * 1024;
|
|
5
|
+
function createQuickJSIsolateDriver(config = {}) {
|
|
6
|
+
const defaultTimeout = config.timeout ?? 3e4;
|
|
7
|
+
const defaultMemoryLimit = config.memoryLimit ?? DEFAULT_MEMORY_LIMIT_MB;
|
|
8
|
+
const defaultMaxStackSize = config.maxStackSize ?? DEFAULT_MAX_STACK_SIZE_BYTES;
|
|
9
|
+
return {
|
|
10
|
+
async createContext(isolateConfig) {
|
|
11
|
+
const timeout = isolateConfig.timeout ?? defaultTimeout;
|
|
12
|
+
const memoryLimitMb = isolateConfig.memoryLimit ?? defaultMemoryLimit;
|
|
13
|
+
const maxStackSizeBytes = defaultMaxStackSize;
|
|
14
|
+
const vm = await newAsyncContext();
|
|
15
|
+
vm.runtime.setMemoryLimit(memoryLimitMb * 1024 * 1024);
|
|
16
|
+
vm.runtime.setMaxStackSize(maxStackSizeBytes);
|
|
17
|
+
const logs = [];
|
|
18
|
+
const consoleObj = vm.newObject();
|
|
19
|
+
const createConsoleMethod = (prefix) => {
|
|
20
|
+
return vm.newFunction(`console.${prefix}`, (...args) => {
|
|
21
|
+
const parts = args.map((arg) => {
|
|
22
|
+
const str = vm.getString(arg);
|
|
23
|
+
return str;
|
|
24
|
+
});
|
|
25
|
+
const msg = prefix ? `${prefix}: ${parts.join(" ")}` : parts.join(" ");
|
|
26
|
+
logs.push(msg);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
const logFn = createConsoleMethod("");
|
|
30
|
+
const errorFn = createConsoleMethod("ERROR");
|
|
31
|
+
const warnFn = createConsoleMethod("WARN");
|
|
32
|
+
const infoFn = createConsoleMethod("INFO");
|
|
33
|
+
vm.setProp(consoleObj, "log", logFn);
|
|
34
|
+
vm.setProp(consoleObj, "error", errorFn);
|
|
35
|
+
vm.setProp(consoleObj, "warn", warnFn);
|
|
36
|
+
vm.setProp(consoleObj, "info", infoFn);
|
|
37
|
+
vm.setProp(vm.global, "console", consoleObj);
|
|
38
|
+
logFn.dispose();
|
|
39
|
+
errorFn.dispose();
|
|
40
|
+
warnFn.dispose();
|
|
41
|
+
infoFn.dispose();
|
|
42
|
+
consoleObj.dispose();
|
|
43
|
+
for (const [name, binding] of Object.entries(isolateConfig.bindings)) {
|
|
44
|
+
const toolFn = vm.newAsyncifiedFunction(name, async (argsHandle) => {
|
|
45
|
+
try {
|
|
46
|
+
const argsJson = vm.getString(argsHandle);
|
|
47
|
+
const args = JSON.parse(argsJson);
|
|
48
|
+
const result = await binding.execute(args);
|
|
49
|
+
const returnHandle = vm.newString(
|
|
50
|
+
JSON.stringify({ success: true, value: result })
|
|
51
|
+
);
|
|
52
|
+
return returnHandle;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
55
|
+
const returnHandle = vm.newString(
|
|
56
|
+
JSON.stringify({ success: false, error: errorMessage })
|
|
57
|
+
);
|
|
58
|
+
return returnHandle;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
vm.setProp(vm.global, `__${name}_impl`, toolFn);
|
|
62
|
+
toolFn.dispose();
|
|
63
|
+
const wrapperCode = `
|
|
64
|
+
async function ${name}(input) {
|
|
65
|
+
const resultJson = await __${name}_impl(JSON.stringify(input));
|
|
66
|
+
const result = JSON.parse(resultJson);
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
throw new Error(result.error);
|
|
69
|
+
}
|
|
70
|
+
return result.value;
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
const wrapperResult = vm.evalCode(wrapperCode);
|
|
74
|
+
if (wrapperResult.error) {
|
|
75
|
+
const errorStr = vm.dump(wrapperResult.error);
|
|
76
|
+
wrapperResult.error.dispose();
|
|
77
|
+
throw new Error(`Failed to create wrapper for ${name}: ${errorStr}`);
|
|
78
|
+
}
|
|
79
|
+
wrapperResult.value.dispose();
|
|
80
|
+
}
|
|
81
|
+
return new QuickJSIsolateContext(vm, logs, timeout);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
createQuickJSIsolateDriver
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=isolate-driver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isolate-driver.js","sources":["../../src/isolate-driver.ts"],"sourcesContent":["import { newAsyncContext } from 'quickjs-emscripten'\nimport { QuickJSIsolateContext } from './isolate-context'\nimport type {\n IsolateConfig,\n IsolateContext,\n IsolateDriver,\n} from '@tanstack/ai-code-mode'\n\n/** Default memory limit in MB (matches Node isolate driver default). */\nconst DEFAULT_MEMORY_LIMIT_MB = 128\n\n/** Default max stack size in bytes for QuickJS runtime. */\nconst DEFAULT_MAX_STACK_SIZE_BYTES = 512 * 1024\n\n/**\n * Configuration for the QuickJS WASM isolate driver\n */\nexport interface QuickJSIsolateDriverConfig {\n /**\n * Default execution timeout in ms (default: 30000)\n */\n timeout?: number\n\n /**\n * Default memory limit in MB (default: 128).\n * Applied via QuickJS `runtime.setMemoryLimit`.\n */\n memoryLimit?: number\n\n /**\n * Default max stack size in bytes (default: 512 KiB).\n * Applied via QuickJS `runtime.setMaxStackSize`.\n */\n maxStackSize?: number\n}\n\n/**\n * Create a QuickJS WASM isolate driver\n *\n * This driver uses QuickJS compiled to WebAssembly via Emscripten.\n * It provides a sandboxed JavaScript environment that runs anywhere\n * (Node.js, browser, edge) without native dependencies.\n *\n * Tools are injected as async functions that bridge back to the host.\n *\n * @example\n * ```typescript\n * import { createQuickJSIsolateDriver } from '@tanstack/ai-isolate-quickjs'\n *\n * const driver = createQuickJSIsolateDriver({\n * timeout: 30000,\n * })\n *\n * const context = await driver.createContext({\n * bindings: {\n * readFile: {\n * name: 'readFile',\n * description: 'Read a file',\n * inputSchema: { type: 'object', properties: { path: { type: 'string' } } },\n * execute: async ({ path }) => fs.readFile(path, 'utf-8'),\n * },\n * },\n * })\n *\n * const result = await context.execute(`\n * const content = await readFile({ path: './data.json' })\n * return JSON.parse(content)\n * `)\n * ```\n */\nexport function createQuickJSIsolateDriver(\n config: QuickJSIsolateDriverConfig = {},\n): IsolateDriver {\n const defaultTimeout = config.timeout ?? 30000\n const defaultMemoryLimit = config.memoryLimit ?? DEFAULT_MEMORY_LIMIT_MB\n const defaultMaxStackSize =\n config.maxStackSize ?? DEFAULT_MAX_STACK_SIZE_BYTES\n\n return {\n async createContext(isolateConfig: IsolateConfig): Promise<IsolateContext> {\n const timeout = isolateConfig.timeout ?? defaultTimeout\n const memoryLimitMb = isolateConfig.memoryLimit ?? defaultMemoryLimit\n const maxStackSizeBytes = defaultMaxStackSize\n\n // Create async QuickJS context (supports async host functions)\n const vm = await newAsyncContext()\n\n // Enforce heap and stack limits so OOM/stack overflow surface as JS errors\n // instead of growing WASM memory until the host process OOMs.\n vm.runtime.setMemoryLimit(memoryLimitMb * 1024 * 1024)\n vm.runtime.setMaxStackSize(maxStackSizeBytes)\n\n // Set up console.log capture\n const logs: Array<string> = []\n\n // Create console object\n const consoleObj = vm.newObject()\n\n // Helper to create console methods\n const createConsoleMethod = (prefix: string) => {\n return vm.newFunction(`console.${prefix}`, (...args) => {\n const parts = args.map((arg) => {\n const str = vm.getString(arg)\n return str\n })\n const msg = prefix ? `${prefix}: ${parts.join(' ')}` : parts.join(' ')\n logs.push(msg)\n })\n }\n\n const logFn = createConsoleMethod('')\n const errorFn = createConsoleMethod('ERROR')\n const warnFn = createConsoleMethod('WARN')\n const infoFn = createConsoleMethod('INFO')\n\n vm.setProp(consoleObj, 'log', logFn)\n vm.setProp(consoleObj, 'error', errorFn)\n vm.setProp(consoleObj, 'warn', warnFn)\n vm.setProp(consoleObj, 'info', infoFn)\n vm.setProp(vm.global, 'console', consoleObj)\n\n // Dispose console handles\n logFn.dispose()\n errorFn.dispose()\n warnFn.dispose()\n infoFn.dispose()\n consoleObj.dispose()\n\n // Inject each tool binding as an async function\n for (const [name, binding] of Object.entries(isolateConfig.bindings)) {\n // Create async function that calls back to host\n // newAsyncifiedFunction receives QuickJS handles as arguments\n const toolFn = vm.newAsyncifiedFunction(name, async (argsHandle) => {\n try {\n // Get the input argument - argsHandle is a QuickJS handle\n const argsJson = vm.getString(argsHandle)\n const args = JSON.parse(argsJson)\n\n // Execute the tool on the host\n const result = await binding.execute(args)\n\n // Return result as JSON string handle\n const returnHandle = vm.newString(\n JSON.stringify({ success: true, value: result }),\n )\n return returnHandle\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error)\n const returnHandle = vm.newString(\n JSON.stringify({ success: false, error: errorMessage }),\n )\n return returnHandle\n }\n })\n\n // Set on global - the VM keeps its own reference\n vm.setProp(vm.global, `__${name}_impl`, toolFn)\n toolFn.dispose()\n\n // Create wrapper that parses input and output\n // Function names match the binding keys (e.g., external_fetchWeather)\n const wrapperCode = `\n async function ${name}(input) {\n const resultJson = await __${name}_impl(JSON.stringify(input));\n const result = JSON.parse(resultJson);\n if (!result.success) {\n throw new Error(result.error);\n }\n return result.value;\n }\n `\n const wrapperResult = vm.evalCode(wrapperCode)\n if (wrapperResult.error) {\n const errorStr = vm.dump(wrapperResult.error)\n wrapperResult.error.dispose()\n throw new Error(`Failed to create wrapper for ${name}: ${errorStr}`)\n }\n wrapperResult.value.dispose()\n }\n\n return new QuickJSIsolateContext(vm, logs, timeout)\n },\n }\n}\n"],"names":[],"mappings":";;AASA,MAAM,0BAA0B;AAGhC,MAAM,+BAA+B,MAAM;AA0DpC,SAAS,2BACd,SAAqC,IACtB;AACf,QAAM,iBAAiB,OAAO,WAAW;AACzC,QAAM,qBAAqB,OAAO,eAAe;AACjD,QAAM,sBACJ,OAAO,gBAAgB;AAEzB,SAAO;AAAA,IACL,MAAM,cAAc,eAAuD;AACzE,YAAM,UAAU,cAAc,WAAW;AACzC,YAAM,gBAAgB,cAAc,eAAe;AACnD,YAAM,oBAAoB;AAG1B,YAAM,KAAK,MAAM,gBAAA;AAIjB,SAAG,QAAQ,eAAe,gBAAgB,OAAO,IAAI;AACrD,SAAG,QAAQ,gBAAgB,iBAAiB;AAG5C,YAAM,OAAsB,CAAA;AAG5B,YAAM,aAAa,GAAG,UAAA;AAGtB,YAAM,sBAAsB,CAAC,WAAmB;AAC9C,eAAO,GAAG,YAAY,WAAW,MAAM,IAAI,IAAI,SAAS;AACtD,gBAAM,QAAQ,KAAK,IAAI,CAAC,QAAQ;AAC9B,kBAAM,MAAM,GAAG,UAAU,GAAG;AAC5B,mBAAO;AAAA,UACT,CAAC;AACD,gBAAM,MAAM,SAAS,GAAG,MAAM,KAAK,MAAM,KAAK,GAAG,CAAC,KAAK,MAAM,KAAK,GAAG;AACrE,eAAK,KAAK,GAAG;AAAA,QACf,CAAC;AAAA,MACH;AAEA,YAAM,QAAQ,oBAAoB,EAAE;AACpC,YAAM,UAAU,oBAAoB,OAAO;AAC3C,YAAM,SAAS,oBAAoB,MAAM;AACzC,YAAM,SAAS,oBAAoB,MAAM;AAEzC,SAAG,QAAQ,YAAY,OAAO,KAAK;AACnC,SAAG,QAAQ,YAAY,SAAS,OAAO;AACvC,SAAG,QAAQ,YAAY,QAAQ,MAAM;AACrC,SAAG,QAAQ,YAAY,QAAQ,MAAM;AACrC,SAAG,QAAQ,GAAG,QAAQ,WAAW,UAAU;AAG3C,YAAM,QAAA;AACN,cAAQ,QAAA;AACR,aAAO,QAAA;AACP,aAAO,QAAA;AACP,iBAAW,QAAA;AAGX,iBAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,cAAc,QAAQ,GAAG;AAGpE,cAAM,SAAS,GAAG,sBAAsB,MAAM,OAAO,eAAe;AAClE,cAAI;AAEF,kBAAM,WAAW,GAAG,UAAU,UAAU;AACxC,kBAAM,OAAO,KAAK,MAAM,QAAQ;AAGhC,kBAAM,SAAS,MAAM,QAAQ,QAAQ,IAAI;AAGzC,kBAAM,eAAe,GAAG;AAAA,cACtB,KAAK,UAAU,EAAE,SAAS,MAAM,OAAO,QAAQ;AAAA,YAAA;AAEjD,mBAAO;AAAA,UACT,SAAS,OAAO;AACd,kBAAM,eACJ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACvD,kBAAM,eAAe,GAAG;AAAA,cACtB,KAAK,UAAU,EAAE,SAAS,OAAO,OAAO,cAAc;AAAA,YAAA;AAExD,mBAAO;AAAA,UACT;AAAA,QACF,CAAC;AAGD,WAAG,QAAQ,GAAG,QAAQ,KAAK,IAAI,SAAS,MAAM;AAC9C,eAAO,QAAA;AAIP,cAAM,cAAc;AAAA,2BACD,IAAI;AAAA,yCACU,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQrC,cAAM,gBAAgB,GAAG,SAAS,WAAW;AAC7C,YAAI,cAAc,OAAO;AACvB,gBAAM,WAAW,GAAG,KAAK,cAAc,KAAK;AAC5C,wBAAc,MAAM,QAAA;AACpB,gBAAM,IAAI,MAAM,gCAAgC,IAAI,KAAK,QAAQ,EAAE;AAAA,QACrE;AACA,sBAAc,MAAM,QAAA;AAAA,MACtB;AAEA,aAAO,IAAI,sBAAsB,IAAI,MAAM,OAAO;AAAA,IACpD;AAAA,EAAA;AAEJ;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/ai-isolate-quickjs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "QuickJS WASM driver for TanStack AI Code Mode - runs everywhere",
|
|
5
|
+
"author": "",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/TanStack/ai.git",
|
|
13
|
+
"directory": "packages/typescript/ai-isolate-quickjs"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"module": "./dist/esm/index.js",
|
|
17
|
+
"types": "./dist/esm/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/esm/index.d.ts",
|
|
21
|
+
"import": "./dist/esm/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "vite build",
|
|
31
|
+
"clean": "premove ./build ./dist",
|
|
32
|
+
"lint:fix": "eslint ./src --fix",
|
|
33
|
+
"test:build": "publint --strict",
|
|
34
|
+
"test:eslint": "eslint ./src",
|
|
35
|
+
"test:lib": "vitest --passWithNoTests",
|
|
36
|
+
"test:lib:dev": "pnpm test:lib --watch",
|
|
37
|
+
"test:types": "tsc"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"ai",
|
|
41
|
+
"tanstack",
|
|
42
|
+
"code-mode",
|
|
43
|
+
"isolate",
|
|
44
|
+
"sandbox",
|
|
45
|
+
"quickjs",
|
|
46
|
+
"wasm"
|
|
47
|
+
],
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"quickjs-emscripten": "^0.31.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@tanstack/ai-code-mode": "workspace:*"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@vitest/coverage-v8": "4.0.14"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { NormalizedError } from '@tanstack/ai-code-mode'
|
|
2
|
+
|
|
3
|
+
const MEMORY_LIMIT_ERROR = 'MemoryLimitError'
|
|
4
|
+
const STACK_OVERFLOW_ERROR = 'StackOverflowError'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether this normalized error indicates the QuickJS VM should not be reused
|
|
8
|
+
* (memory or stack limit exceeded).
|
|
9
|
+
*/
|
|
10
|
+
export function isFatalQuickJSLimitError(error: NormalizedError): boolean {
|
|
11
|
+
return (
|
|
12
|
+
error.name === MEMORY_LIMIT_ERROR || error.name === STACK_OVERFLOW_ERROR
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalize various error types into a consistent format
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeError(error: unknown): NormalizedError {
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
const msg = error.message
|
|
22
|
+
const lower = msg.toLowerCase()
|
|
23
|
+
|
|
24
|
+
if (
|
|
25
|
+
lower.includes('out of memory') ||
|
|
26
|
+
lower.includes('memory alloc') ||
|
|
27
|
+
(error.name === 'InternalError' && lower.includes('memory'))
|
|
28
|
+
) {
|
|
29
|
+
return {
|
|
30
|
+
name: MEMORY_LIMIT_ERROR,
|
|
31
|
+
message: 'Code execution exceeded memory limit',
|
|
32
|
+
stack: error.stack,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (lower.includes('stack overflow')) {
|
|
37
|
+
return {
|
|
38
|
+
name: STACK_OVERFLOW_ERROR,
|
|
39
|
+
message: 'Code execution exceeded stack size limit',
|
|
40
|
+
stack: error.stack,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (error.name === 'RuntimeError' && lower.includes('unreachable')) {
|
|
45
|
+
return {
|
|
46
|
+
name: 'WasmRuntimeError',
|
|
47
|
+
message: msg || 'WebAssembly runtime error',
|
|
48
|
+
stack: error.stack,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
name: error.name,
|
|
54
|
+
message: error.message,
|
|
55
|
+
stack: error.stack,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof error === 'string') {
|
|
60
|
+
return {
|
|
61
|
+
name: 'Error',
|
|
62
|
+
message: error,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof error === 'object' && error !== null) {
|
|
67
|
+
const errObj = error as Record<string, unknown>
|
|
68
|
+
return {
|
|
69
|
+
name: String(errObj.name || 'Error'),
|
|
70
|
+
message: String(errObj.message || 'Unknown error'),
|
|
71
|
+
stack: errObj.stack ? String(errObj.stack) : undefined,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
name: 'UnknownError',
|
|
77
|
+
message: String(error),
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createQuickJSIsolateDriver,
|
|
3
|
+
type QuickJSIsolateDriverConfig,
|
|
4
|
+
} from './isolate-driver'
|
|
5
|
+
|
|
6
|
+
// Re-export types from ai-code-mode for convenience
|
|
7
|
+
export type {
|
|
8
|
+
IsolateDriver,
|
|
9
|
+
IsolateConfig,
|
|
10
|
+
IsolateContext,
|
|
11
|
+
ExecutionResult,
|
|
12
|
+
NormalizedError,
|
|
13
|
+
} from '@tanstack/ai-code-mode'
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { wrapCode } from '@tanstack/ai-code-mode'
|
|
2
|
+
import { isFatalQuickJSLimitError, normalizeError } from './error-normalizer'
|
|
3
|
+
import type { QuickJSAsyncContext } from 'quickjs-emscripten'
|
|
4
|
+
import type { ExecutionResult, IsolateContext } from '@tanstack/ai-code-mode'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Serializes all QuickJS evalCodeAsync calls across contexts.
|
|
8
|
+
* Required because newAsyncContext() reuses a singleton WASM module
|
|
9
|
+
* whose asyncify stack can only handle one suspension at a time.
|
|
10
|
+
*/
|
|
11
|
+
let globalExecQueue: Promise<void> = Promise.resolve()
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* IsolateContext implementation using QuickJS WASM
|
|
15
|
+
*/
|
|
16
|
+
export class QuickJSIsolateContext implements IsolateContext {
|
|
17
|
+
private vm: QuickJSAsyncContext
|
|
18
|
+
private logs: Array<string>
|
|
19
|
+
private timeout: number
|
|
20
|
+
private disposed = false
|
|
21
|
+
private executing = false
|
|
22
|
+
|
|
23
|
+
constructor(vm: QuickJSAsyncContext, logs: Array<string>, timeout: number) {
|
|
24
|
+
this.vm = vm
|
|
25
|
+
this.logs = logs
|
|
26
|
+
this.timeout = timeout
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async execute<T = unknown>(code: string): Promise<ExecutionResult<T>> {
|
|
30
|
+
if (this.disposed) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: {
|
|
34
|
+
name: 'DisposedError',
|
|
35
|
+
message: 'Context has been disposed',
|
|
36
|
+
},
|
|
37
|
+
logs: [],
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Serialize through the global queue to prevent concurrent
|
|
42
|
+
// WASM asyncify suspensions across contexts.
|
|
43
|
+
let resolve!: () => void
|
|
44
|
+
const myTurn = new Promise<void>((r) => {
|
|
45
|
+
resolve = r
|
|
46
|
+
})
|
|
47
|
+
const waitForPrev = globalExecQueue
|
|
48
|
+
globalExecQueue = myTurn
|
|
49
|
+
|
|
50
|
+
await waitForPrev
|
|
51
|
+
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- dispose() may be called concurrently while awaiting the queue
|
|
53
|
+
if (this.disposed) {
|
|
54
|
+
resolve()
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: {
|
|
58
|
+
name: 'DisposedError',
|
|
59
|
+
message: 'Context has been disposed',
|
|
60
|
+
},
|
|
61
|
+
logs: [],
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.executing = true
|
|
66
|
+
this.logs.length = 0
|
|
67
|
+
|
|
68
|
+
const releaseVmAfterFatalLimit = () => {
|
|
69
|
+
if (this.disposed) return
|
|
70
|
+
try {
|
|
71
|
+
this.vm.runtime.setInterruptHandler(() => false)
|
|
72
|
+
} catch {
|
|
73
|
+
// ignore if runtime is already torn down
|
|
74
|
+
}
|
|
75
|
+
this.disposed = true
|
|
76
|
+
this.vm.dispose()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fail = (error: unknown) => {
|
|
80
|
+
const normalized = normalizeError(error)
|
|
81
|
+
if (isFatalQuickJSLimitError(normalized)) {
|
|
82
|
+
releaseVmAfterFatalLimit()
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
success: false as const,
|
|
86
|
+
error: normalized,
|
|
87
|
+
logs: [...this.logs],
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const wrappedCode = wrapCode(code)
|
|
93
|
+
|
|
94
|
+
const deadline = Date.now() + this.timeout
|
|
95
|
+
this.vm.runtime.setInterruptHandler(() => {
|
|
96
|
+
return Date.now() > deadline
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result = await this.vm.evalCodeAsync(wrappedCode)
|
|
101
|
+
|
|
102
|
+
let parsedResult: T
|
|
103
|
+
try {
|
|
104
|
+
const promiseHandle = this.vm.unwrapResult(result)
|
|
105
|
+
|
|
106
|
+
// evalCodeAsync returns a Promise handle (our wrapper is an async IIFE).
|
|
107
|
+
// Use resolvePromise + executePendingJobs to properly await the
|
|
108
|
+
// QuickJS promise without re-entering the WASM asyncify state.
|
|
109
|
+
const nativePromise = this.vm.resolvePromise(promiseHandle)
|
|
110
|
+
promiseHandle.dispose()
|
|
111
|
+
this.vm.runtime.executePendingJobs()
|
|
112
|
+
const resolvedResult = await nativePromise
|
|
113
|
+
|
|
114
|
+
const valueHandle = this.vm.unwrapResult(resolvedResult)
|
|
115
|
+
const dumpedResult = this.vm.dump(valueHandle)
|
|
116
|
+
valueHandle.dispose()
|
|
117
|
+
|
|
118
|
+
if (typeof dumpedResult === 'string') {
|
|
119
|
+
try {
|
|
120
|
+
parsedResult = JSON.parse(dumpedResult) as T
|
|
121
|
+
} catch {
|
|
122
|
+
parsedResult = dumpedResult as T
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
parsedResult = dumpedResult as T
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
value: parsedResult,
|
|
131
|
+
logs: [...this.logs],
|
|
132
|
+
}
|
|
133
|
+
} catch (unwrapError) {
|
|
134
|
+
return fail(unwrapError)
|
|
135
|
+
}
|
|
136
|
+
} finally {
|
|
137
|
+
// fail() may set disposed when releasing the VM after memory/stack limit errors
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- disposed set in fail()
|
|
139
|
+
if (!this.disposed) {
|
|
140
|
+
this.vm.runtime.setInterruptHandler(() => false)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return fail(error)
|
|
145
|
+
} finally {
|
|
146
|
+
this.executing = false
|
|
147
|
+
resolve()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async dispose(): Promise<void> {
|
|
152
|
+
if (this.disposed) return
|
|
153
|
+
|
|
154
|
+
// If an execution is in flight, wait for the global queue to drain
|
|
155
|
+
// before disposing the VM. Otherwise the asyncified callback would
|
|
156
|
+
// try to access a freed context.
|
|
157
|
+
if (this.executing) {
|
|
158
|
+
await globalExecQueue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.disposed = true
|
|
162
|
+
this.vm.dispose()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { newAsyncContext } from 'quickjs-emscripten'
|
|
2
|
+
import { QuickJSIsolateContext } from './isolate-context'
|
|
3
|
+
import type {
|
|
4
|
+
IsolateConfig,
|
|
5
|
+
IsolateContext,
|
|
6
|
+
IsolateDriver,
|
|
7
|
+
} from '@tanstack/ai-code-mode'
|
|
8
|
+
|
|
9
|
+
/** Default memory limit in MB (matches Node isolate driver default). */
|
|
10
|
+
const DEFAULT_MEMORY_LIMIT_MB = 128
|
|
11
|
+
|
|
12
|
+
/** Default max stack size in bytes for QuickJS runtime. */
|
|
13
|
+
const DEFAULT_MAX_STACK_SIZE_BYTES = 512 * 1024
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for the QuickJS WASM isolate driver
|
|
17
|
+
*/
|
|
18
|
+
export interface QuickJSIsolateDriverConfig {
|
|
19
|
+
/**
|
|
20
|
+
* Default execution timeout in ms (default: 30000)
|
|
21
|
+
*/
|
|
22
|
+
timeout?: number
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default memory limit in MB (default: 128).
|
|
26
|
+
* Applied via QuickJS `runtime.setMemoryLimit`.
|
|
27
|
+
*/
|
|
28
|
+
memoryLimit?: number
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default max stack size in bytes (default: 512 KiB).
|
|
32
|
+
* Applied via QuickJS `runtime.setMaxStackSize`.
|
|
33
|
+
*/
|
|
34
|
+
maxStackSize?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a QuickJS WASM isolate driver
|
|
39
|
+
*
|
|
40
|
+
* This driver uses QuickJS compiled to WebAssembly via Emscripten.
|
|
41
|
+
* It provides a sandboxed JavaScript environment that runs anywhere
|
|
42
|
+
* (Node.js, browser, edge) without native dependencies.
|
|
43
|
+
*
|
|
44
|
+
* Tools are injected as async functions that bridge back to the host.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* import { createQuickJSIsolateDriver } from '@tanstack/ai-isolate-quickjs'
|
|
49
|
+
*
|
|
50
|
+
* const driver = createQuickJSIsolateDriver({
|
|
51
|
+
* timeout: 30000,
|
|
52
|
+
* })
|
|
53
|
+
*
|
|
54
|
+
* const context = await driver.createContext({
|
|
55
|
+
* bindings: {
|
|
56
|
+
* readFile: {
|
|
57
|
+
* name: 'readFile',
|
|
58
|
+
* description: 'Read a file',
|
|
59
|
+
* inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
|
|
60
|
+
* execute: async ({ path }) => fs.readFile(path, 'utf-8'),
|
|
61
|
+
* },
|
|
62
|
+
* },
|
|
63
|
+
* })
|
|
64
|
+
*
|
|
65
|
+
* const result = await context.execute(`
|
|
66
|
+
* const content = await readFile({ path: './data.json' })
|
|
67
|
+
* return JSON.parse(content)
|
|
68
|
+
* `)
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function createQuickJSIsolateDriver(
|
|
72
|
+
config: QuickJSIsolateDriverConfig = {},
|
|
73
|
+
): IsolateDriver {
|
|
74
|
+
const defaultTimeout = config.timeout ?? 30000
|
|
75
|
+
const defaultMemoryLimit = config.memoryLimit ?? DEFAULT_MEMORY_LIMIT_MB
|
|
76
|
+
const defaultMaxStackSize =
|
|
77
|
+
config.maxStackSize ?? DEFAULT_MAX_STACK_SIZE_BYTES
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
async createContext(isolateConfig: IsolateConfig): Promise<IsolateContext> {
|
|
81
|
+
const timeout = isolateConfig.timeout ?? defaultTimeout
|
|
82
|
+
const memoryLimitMb = isolateConfig.memoryLimit ?? defaultMemoryLimit
|
|
83
|
+
const maxStackSizeBytes = defaultMaxStackSize
|
|
84
|
+
|
|
85
|
+
// Create async QuickJS context (supports async host functions)
|
|
86
|
+
const vm = await newAsyncContext()
|
|
87
|
+
|
|
88
|
+
// Enforce heap and stack limits so OOM/stack overflow surface as JS errors
|
|
89
|
+
// instead of growing WASM memory until the host process OOMs.
|
|
90
|
+
vm.runtime.setMemoryLimit(memoryLimitMb * 1024 * 1024)
|
|
91
|
+
vm.runtime.setMaxStackSize(maxStackSizeBytes)
|
|
92
|
+
|
|
93
|
+
// Set up console.log capture
|
|
94
|
+
const logs: Array<string> = []
|
|
95
|
+
|
|
96
|
+
// Create console object
|
|
97
|
+
const consoleObj = vm.newObject()
|
|
98
|
+
|
|
99
|
+
// Helper to create console methods
|
|
100
|
+
const createConsoleMethod = (prefix: string) => {
|
|
101
|
+
return vm.newFunction(`console.${prefix}`, (...args) => {
|
|
102
|
+
const parts = args.map((arg) => {
|
|
103
|
+
const str = vm.getString(arg)
|
|
104
|
+
return str
|
|
105
|
+
})
|
|
106
|
+
const msg = prefix ? `${prefix}: ${parts.join(' ')}` : parts.join(' ')
|
|
107
|
+
logs.push(msg)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const logFn = createConsoleMethod('')
|
|
112
|
+
const errorFn = createConsoleMethod('ERROR')
|
|
113
|
+
const warnFn = createConsoleMethod('WARN')
|
|
114
|
+
const infoFn = createConsoleMethod('INFO')
|
|
115
|
+
|
|
116
|
+
vm.setProp(consoleObj, 'log', logFn)
|
|
117
|
+
vm.setProp(consoleObj, 'error', errorFn)
|
|
118
|
+
vm.setProp(consoleObj, 'warn', warnFn)
|
|
119
|
+
vm.setProp(consoleObj, 'info', infoFn)
|
|
120
|
+
vm.setProp(vm.global, 'console', consoleObj)
|
|
121
|
+
|
|
122
|
+
// Dispose console handles
|
|
123
|
+
logFn.dispose()
|
|
124
|
+
errorFn.dispose()
|
|
125
|
+
warnFn.dispose()
|
|
126
|
+
infoFn.dispose()
|
|
127
|
+
consoleObj.dispose()
|
|
128
|
+
|
|
129
|
+
// Inject each tool binding as an async function
|
|
130
|
+
for (const [name, binding] of Object.entries(isolateConfig.bindings)) {
|
|
131
|
+
// Create async function that calls back to host
|
|
132
|
+
// newAsyncifiedFunction receives QuickJS handles as arguments
|
|
133
|
+
const toolFn = vm.newAsyncifiedFunction(name, async (argsHandle) => {
|
|
134
|
+
try {
|
|
135
|
+
// Get the input argument - argsHandle is a QuickJS handle
|
|
136
|
+
const argsJson = vm.getString(argsHandle)
|
|
137
|
+
const args = JSON.parse(argsJson)
|
|
138
|
+
|
|
139
|
+
// Execute the tool on the host
|
|
140
|
+
const result = await binding.execute(args)
|
|
141
|
+
|
|
142
|
+
// Return result as JSON string handle
|
|
143
|
+
const returnHandle = vm.newString(
|
|
144
|
+
JSON.stringify({ success: true, value: result }),
|
|
145
|
+
)
|
|
146
|
+
return returnHandle
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const errorMessage =
|
|
149
|
+
error instanceof Error ? error.message : String(error)
|
|
150
|
+
const returnHandle = vm.newString(
|
|
151
|
+
JSON.stringify({ success: false, error: errorMessage }),
|
|
152
|
+
)
|
|
153
|
+
return returnHandle
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Set on global - the VM keeps its own reference
|
|
158
|
+
vm.setProp(vm.global, `__${name}_impl`, toolFn)
|
|
159
|
+
toolFn.dispose()
|
|
160
|
+
|
|
161
|
+
// Create wrapper that parses input and output
|
|
162
|
+
// Function names match the binding keys (e.g., external_fetchWeather)
|
|
163
|
+
const wrapperCode = `
|
|
164
|
+
async function ${name}(input) {
|
|
165
|
+
const resultJson = await __${name}_impl(JSON.stringify(input));
|
|
166
|
+
const result = JSON.parse(resultJson);
|
|
167
|
+
if (!result.success) {
|
|
168
|
+
throw new Error(result.error);
|
|
169
|
+
}
|
|
170
|
+
return result.value;
|
|
171
|
+
}
|
|
172
|
+
`
|
|
173
|
+
const wrapperResult = vm.evalCode(wrapperCode)
|
|
174
|
+
if (wrapperResult.error) {
|
|
175
|
+
const errorStr = vm.dump(wrapperResult.error)
|
|
176
|
+
wrapperResult.error.dispose()
|
|
177
|
+
throw new Error(`Failed to create wrapper for ${name}: ${errorStr}`)
|
|
178
|
+
}
|
|
179
|
+
wrapperResult.value.dispose()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return new QuickJSIsolateContext(vm, logs, timeout)
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
}
|