Skip to content

Commit 6bc757d

Browse files
committed
feat(ai-isolate-quickjs): adding support for wasmLocation option for QuickJsWASM driver and documentation update
1 parent 24622c7 commit 6bc757d

6 files changed

Lines changed: 133 additions & 5 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/ai-isolate-quickjs': minor
3+
---
4+
5+
Add a `wasmLocation` driver option for loading the QuickJS WASM binary from a custom URL or path, such as a public directory or CDN.

docs/code-mode/code-mode-isolates.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ const driver = createQuickJSIsolateDriver({
9393
memoryLimit: 128, // MB
9494
timeout: 30_000, // ms
9595
maxStackSize: 524288, // bytes (512 KiB)
96+
wasmLocation: '/assets/quickjs/emscripten-module.wasm',
9697
})
9798
```
9899

@@ -104,11 +105,24 @@ const driver = createQuickJSIsolateDriver({
104105
| `memoryLimit` | `number` | `128` | Maximum heap memory for the QuickJS VM, in megabytes. |
105106
| `timeout` | `number` | `30000` | Maximum wall-clock time per execution, in milliseconds. |
106107
| `maxStackSize` | `number` | `524288` | Maximum call stack size in bytes (default: 512 KiB). Increase for deeply recursive code; decrease to catch runaway recursion sooner. |
108+
| `wasmLocation` | `string` || URL or path from which Emscripten loads the QuickJS WASM binary. When omitted, `quickjs-emscripten` resolves its bundled binary. |
109+
110+
### Serving the WASM binary
111+
112+
Set `wasmLocation` when the QuickJS WASM binary is hosted in a public directory or on a CDN:
113+
114+
```typescript
115+
const driver = createQuickJSIsolateDriver({
116+
wasmLocation: 'https://cdn.example.com/quickjs/emscripten-module.wasm',
117+
})
118+
```
119+
120+
Serve the synchronous release binary exported by `@jitl/quickjs-wasmfile-release-sync/wasm`. For a cross-origin URL, configure the host to allow cross-origin requests.
107121

108122

109123
### How it works
110124

111-
QuickJS WASM uses an asyncified execution model — the WASM module can pause while awaiting host async functions (your tools). Executions are serialized through a global queue to prevent concurrent WASM calls, which the asyncify model does not support. Fatal errors (memory exhaustion, stack overflow) are detected, the VM is disposed, and a structured error is returned. Console output is captured and returned with the result.
125+
QuickJS runs the synchronous WASM build and bridges host async functions (your tools) through QuickJS promises, avoiding suspension of the WASM stack. Fatal errors (memory exhaustion, stack overflow) are detected, the VM is disposed, and a structured error is returned. Console output is captured and returned with the result.
112126

113127
> **Performance note:** QuickJS interprets JavaScript rather than JIT-compiling it, so compute-heavy scripts run slower than with the Node driver. For typical LLM-generated scripts that are mostly waiting on `external_*` tool calls, this difference is not significant.
114128

docs/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@
384384
"label": "Code Mode Isolate Drivers",
385385
"to": "code-mode/code-mode-isolates",
386386
"addedAt": "2026-04-15",
387-
"updatedAt": "2026-06-11"
387+
"updatedAt": "2026-07-20"
388388
},
389389
{
390390
"label": "Lazy Tools",

packages/ai-isolate-quickjs/README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const driver = createQuickJSIsolateDriver({
1818
timeout: 30000, // execution timeout in ms (default: 30000)
1919
memoryLimit: 128, // memory limit in MB (default: 128)
2020
maxStackSize: 512 * 1024, // max stack size in bytes (default: 512 KiB)
21+
wasmLocation: '/assets/quickjs/emscripten-module.wasm', // optional public URL or path
2122
})
2223

2324
const executeTypescript = createCodeModeTool({
@@ -31,6 +32,19 @@ const executeTypescript = createCodeModeTool({
3132
- `timeout` — Default execution timeout in milliseconds (default: 30000)
3233
- `memoryLimit` — Default QuickJS runtime memory limit in MB (default: 128)
3334
- `maxStackSize` — Default QuickJS runtime max stack size in bytes (default: 524288)
35+
- `wasmLocation` — Optional URL or path from which Emscripten loads the QuickJS WASM binary. When omitted, `quickjs-emscripten` resolves its bundled binary.
36+
37+
## Serving the WASM Binary
38+
39+
Use `wasmLocation` when your runtime requires the QuickJS WASM binary to be served from a public directory or CDN:
40+
41+
```typescript
42+
const driver = createQuickJSIsolateDriver({
43+
wasmLocation: 'https://cdn.example.com/quickjs/emscripten-module.wasm',
44+
})
45+
```
46+
47+
The configured file must be the synchronous release binary exported by `@jitl/quickjs-wasmfile-release-sync/wasm`. Ensure cross-origin requests are allowed when serving it from another origin.
3448

3549
## Tradeoffs vs Node Driver
3650

@@ -44,7 +58,7 @@ const executeTypescript = createCodeModeTool({
4458

4559
## How It Works
4660

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.
61+
Uses [QuickJS](https://bellard.org/quickjs/) compiled to WebAssembly via [`quickjs-emscripten`](https://github.com/nicolo-ribaudo/quickjs-emscripten). Each execution creates a fresh QuickJS context with tool bindings injected as global async functions.
4862

4963
## Runtime Limits and Errors
5064

packages/ai-isolate-quickjs/src/isolate-driver.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { getQuickJS } from 'quickjs-emscripten'
1+
import {
2+
RELEASE_SYNC,
3+
getQuickJS,
4+
newQuickJSWASMModule,
5+
newVariant,
6+
} from 'quickjs-emscripten'
27
import { QuickJSIsolateContext } from './isolate-context'
38
import type { ExecState } from './isolate-context'
49
import type { QuickJSContext } from 'quickjs-emscripten'
@@ -35,6 +40,14 @@ export interface QuickJSIsolateDriverConfig {
3540
* Applied via QuickJS `runtime.setMaxStackSize`.
3641
*/
3742
maxStackSize?: number
43+
44+
/**
45+
* URL or path from which Emscripten loads the QuickJS WASM binary.
46+
*
47+
* When omitted, `quickjs-emscripten` resolves its bundled WASM binary.
48+
* Set this when serving the binary from a public directory or CDN.
49+
*/
50+
wasmLocation?: string
3851
}
3952

4053
/**
@@ -185,6 +198,18 @@ export function createQuickJSIsolateDriver(
185198
const defaultMemoryLimit = config.memoryLimit ?? DEFAULT_MEMORY_LIMIT_MB
186199
const defaultMaxStackSize =
187200
config.maxStackSize ?? DEFAULT_MAX_STACK_SIZE_BYTES
201+
let customQuickJSModule: ReturnType<typeof newQuickJSWASMModule> | undefined
202+
203+
const loadQuickJS = () => {
204+
if (config.wasmLocation === undefined) {
205+
return getQuickJS()
206+
}
207+
208+
customQuickJSModule ??= newQuickJSWASMModule(
209+
newVariant(RELEASE_SYNC, { wasmLocation: config.wasmLocation }),
210+
)
211+
return customQuickJSModule
212+
}
188213

189214
return {
190215
async createContext(isolateConfig: IsolateConfig): Promise<IsolateContext> {
@@ -195,7 +220,7 @@ export function createQuickJSIsolateDriver(
195220
// Create a plain (non-asyncify) QuickJS context. Host async functions
196221
// are bridged with QuickJS promises instead of asyncify suspensions,
197222
// so the sync WASM build is sufficient and sidesteps asyncify bugs.
198-
const QuickJS = await getQuickJS()
223+
const QuickJS = await loadQuickJS()
199224
const vm = QuickJS.newContext()
200225

201226
// Enforce heap and stack limits so OOM/stack overflow surface as JS errors
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
const quickJSMocks = vi.hoisted(() => {
4+
const contextError = new Error('context created')
5+
const releaseVariant = { type: 'sync' }
6+
7+
return {
8+
contextError,
9+
releaseVariant,
10+
getQuickJS: vi.fn(),
11+
newVariant: vi.fn(() => releaseVariant),
12+
newQuickJSWASMModule: vi.fn(async () => ({
13+
newContext: () => {
14+
throw contextError
15+
},
16+
})),
17+
}
18+
})
19+
20+
vi.mock('quickjs-emscripten', () => ({
21+
getQuickJS: quickJSMocks.getQuickJS,
22+
newQuickJSWASMModule: quickJSMocks.newQuickJSWASMModule,
23+
newVariant: quickJSMocks.newVariant,
24+
RELEASE_SYNC: quickJSMocks.releaseVariant,
25+
}))
26+
27+
import { createQuickJSIsolateDriver } from '../src/isolate-driver'
28+
29+
describe('QuickJS WASM loading', () => {
30+
beforeEach(() => {
31+
vi.clearAllMocks()
32+
})
33+
34+
it('uses the shared QuickJS module by default', async () => {
35+
quickJSMocks.getQuickJS.mockResolvedValue({
36+
newContext: () => {
37+
throw quickJSMocks.contextError
38+
},
39+
})
40+
const driver = createQuickJSIsolateDriver()
41+
42+
await expect(driver.createContext({ bindings: {} })).rejects.toThrow(
43+
quickJSMocks.contextError,
44+
)
45+
46+
expect(quickJSMocks.getQuickJS).toHaveBeenCalledOnce()
47+
expect(quickJSMocks.newVariant).not.toHaveBeenCalled()
48+
expect(quickJSMocks.newQuickJSWASMModule).not.toHaveBeenCalled()
49+
})
50+
51+
it('loads a custom WASM location once per driver', async () => {
52+
const wasmLocation = 'https://cdn.example.com/quickjs.wasm'
53+
const driver = createQuickJSIsolateDriver({ wasmLocation })
54+
55+
await expect(driver.createContext({ bindings: {} })).rejects.toThrow(
56+
quickJSMocks.contextError,
57+
)
58+
await expect(driver.createContext({ bindings: {} })).rejects.toThrow(
59+
quickJSMocks.contextError,
60+
)
61+
62+
expect(quickJSMocks.getQuickJS).not.toHaveBeenCalled()
63+
expect(quickJSMocks.newVariant).toHaveBeenCalledOnce()
64+
expect(quickJSMocks.newVariant).toHaveBeenCalledWith(
65+
quickJSMocks.releaseVariant,
66+
{ wasmLocation },
67+
)
68+
expect(quickJSMocks.newQuickJSWASMModule).toHaveBeenCalledOnce()
69+
})
70+
})

0 commit comments

Comments
 (0)