@liquidmetal-ai/precip 1.0.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.
Files changed (78) hide show
  1. package/.prettierrc +9 -0
  2. package/CHANGELOG.md +8 -0
  3. package/eslint.config.mjs +28 -0
  4. package/package.json +53 -0
  5. package/src/engine/agent.ts +478 -0
  6. package/src/engine/llm-provider.test.ts +275 -0
  7. package/src/engine/llm-provider.ts +330 -0
  8. package/src/engine/stream-parser.ts +170 -0
  9. package/src/index.ts +142 -0
  10. package/src/mounts/mount-manager.test.ts +516 -0
  11. package/src/mounts/mount-manager.ts +327 -0
  12. package/src/mounts/mount-registry.ts +196 -0
  13. package/src/mounts/zod-to-string.test.ts +154 -0
  14. package/src/mounts/zod-to-string.ts +213 -0
  15. package/src/presets/agent-tools.ts +57 -0
  16. package/src/presets/index.ts +5 -0
  17. package/src/sandbox/README.md +1321 -0
  18. package/src/sandbox/bridges/README.md +571 -0
  19. package/src/sandbox/bridges/actor.test.ts +229 -0
  20. package/src/sandbox/bridges/actor.ts +195 -0
  21. package/src/sandbox/bridges/bridge-fixes.test.ts +614 -0
  22. package/src/sandbox/bridges/bucket.test.ts +300 -0
  23. package/src/sandbox/bridges/cleanup-reproduction.test.ts +225 -0
  24. package/src/sandbox/bridges/console-multiple.test.ts +187 -0
  25. package/src/sandbox/bridges/console.test.ts +157 -0
  26. package/src/sandbox/bridges/console.ts +122 -0
  27. package/src/sandbox/bridges/fetch.ts +93 -0
  28. package/src/sandbox/bridges/index.ts +78 -0
  29. package/src/sandbox/bridges/readable-stream.ts +323 -0
  30. package/src/sandbox/bridges/response.test.ts +154 -0
  31. package/src/sandbox/bridges/response.ts +123 -0
  32. package/src/sandbox/bridges/review-fixes.test.ts +331 -0
  33. package/src/sandbox/bridges/search.test.ts +475 -0
  34. package/src/sandbox/bridges/search.ts +264 -0
  35. package/src/sandbox/bridges/shared/body-methods.ts +93 -0
  36. package/src/sandbox/bridges/shared/cleanup.ts +112 -0
  37. package/src/sandbox/bridges/shared/convert.ts +76 -0
  38. package/src/sandbox/bridges/shared/headers.ts +181 -0
  39. package/src/sandbox/bridges/shared/index.ts +36 -0
  40. package/src/sandbox/bridges/shared/json-helpers.ts +77 -0
  41. package/src/sandbox/bridges/shared/path-parser.ts +109 -0
  42. package/src/sandbox/bridges/shared/promise-helper.ts +108 -0
  43. package/src/sandbox/bridges/shared/registry-setup.ts +84 -0
  44. package/src/sandbox/bridges/shared/response-object.ts +280 -0
  45. package/src/sandbox/bridges/shared/result-builder.ts +130 -0
  46. package/src/sandbox/bridges/shared/scope-helpers.ts +44 -0
  47. package/src/sandbox/bridges/shared/stream-reader.ts +90 -0
  48. package/src/sandbox/bridges/storage-bridge.test.ts +893 -0
  49. package/src/sandbox/bridges/storage.ts +421 -0
  50. package/src/sandbox/bridges/text-decoder.ts +190 -0
  51. package/src/sandbox/bridges/text-encoder.ts +102 -0
  52. package/src/sandbox/bridges/types.ts +39 -0
  53. package/src/sandbox/bridges/utils.ts +123 -0
  54. package/src/sandbox/index.ts +6 -0
  55. package/src/sandbox/quickjs-wasm.d.ts +9 -0
  56. package/src/sandbox/sandbox.test.ts +191 -0
  57. package/src/sandbox/sandbox.ts +831 -0
  58. package/src/sandbox/test-helper.ts +43 -0
  59. package/src/sandbox/test-mocks.ts +154 -0
  60. package/src/sandbox/user-stream.test.ts +77 -0
  61. package/src/skills/frontmatter.test.ts +305 -0
  62. package/src/skills/frontmatter.ts +200 -0
  63. package/src/skills/index.ts +9 -0
  64. package/src/skills/skills-loader.test.ts +237 -0
  65. package/src/skills/skills-loader.ts +200 -0
  66. package/src/tools/actor-storage-tools.ts +250 -0
  67. package/src/tools/code-tools.test.ts +199 -0
  68. package/src/tools/code-tools.ts +444 -0
  69. package/src/tools/file-tools.ts +206 -0
  70. package/src/tools/registry.ts +125 -0
  71. package/src/tools/script-tools.ts +145 -0
  72. package/src/tools/smartbucket-tools.ts +203 -0
  73. package/src/tools/sql-tools.ts +213 -0
  74. package/src/tools/tool-factory.ts +119 -0
  75. package/src/types.ts +512 -0
  76. package/tsconfig.eslint.json +5 -0
  77. package/tsconfig.json +15 -0
  78. package/vitest.config.ts +33 -0
@@ -0,0 +1,1321 @@
1
+ # Precip Sandbox
2
+
3
+ A secure JavaScript sandbox system using QuickJS WASM that enables async/await execution with bridged runtime APIs.
4
+
5
+ ## Overview
6
+
7
+ The Precip sandbox provides isolated JavaScript execution environments using QuickJS WebAssembly. It enables running untrusted code safely while providing controlled access to runtime APIs through a bridge system. The key innovation is enabling `async/await` in QuickJS (which is synchronous) using a deferred promise pattern with Promise.race optimization.
8
+
9
+ **Key Features:**
10
+
11
+ - Isolated execution in QuickJS WASM
12
+ - Full async/await support via host promise bridging
13
+ - Bridge system for runtime APIs (fetch, console, storage, search, etc.)
14
+ - Proper resource cleanup and handle management
15
+ - Timeout and memory limit enforcement
16
+ - Console output capture
17
+ - 5.81x faster than polling for simple async operations
18
+
19
+ ## Architecture
20
+
21
+ ```
22
+ sandbox/
23
+ ├── sandbox.ts # Singleton sandbox with queued execution
24
+ ├── index.ts # Sandbox exports
25
+ ├── test-helper.ts # Test compatibility helper
26
+ ├── quickjs-wasm.d.ts # TypeScript definitions
27
+ └── bridges/ # API bridge implementations
28
+ ├── types.ts # Shared bridge types
29
+ ├── utils.ts # Bridge utility functions
30
+ ├── index.ts # Bridge installation and cleanup
31
+ ├── console.ts # Console API bridge
32
+ ├── fetch.ts # Fetch API bridge
33
+ ├── response.ts # Response constructor bridge
34
+ ├── readable-stream.ts # ReadableStream bridge
35
+ ├── text-encoder.ts # TextEncoder bridge
36
+ ├── text-decoder.ts # TextDecoder bridge
37
+ ├── storage.ts # KV storage bridge
38
+ ├── search.ts # Vector search bridge
39
+ ├── bucket.ts # Bucket object bridge
40
+ ├── actor.ts # Actor/Durable Object bridge
41
+ └── shared/ # Shared bridge utilities
42
+ ├── index.ts
43
+ ├── headers.ts # Headers object factory
44
+ ├── response-object.ts # Unified Response factory
45
+ ├── body-methods.ts # Body consumption methods
46
+ ├── cleanup.ts # Stream cleanup utilities
47
+ └── json-helpers.ts # JSON parsing utilities
48
+ ```
49
+
50
+ ## Core Concepts
51
+
52
+ ### QuickJS Context
53
+
54
+ QuickJS is a lightweight JavaScript engine compiled to WebAssembly. The `QuickJSContext` represents an isolated JavaScript execution environment with its own global object, heap, and runtime state.
55
+
56
+ **Handles**: All QuickJS values (objects, strings, functions) are represented as `QuickJSHandle` objects. Handles must be manually disposed to prevent memory leaks.
57
+
58
+ ### Async/Await in QuickJS
59
+
60
+ QuickJS is synchronous by design, so it doesn't support native async/await. The sandbox enables async functionality through a **deferred promise pattern**:
61
+
62
+ 1. When sandbox code calls an async host function, the function returns a QuickJS Promise immediately (created via `context.newPromise()`)
63
+ 2. The host function kicks off the actual async work and stores the promise in a tracker
64
+ 3. When the async work completes, the deferred promise is resolved/rejected using the result
65
+ 4. The main execution waits for the top-level promise using a race loop that:
66
+ - Checks if the main promise is settled
67
+ - Races all pending host promises
68
+ - Wakes immediately when any promise settles (no polling)
69
+ - Re-races until the main promise settles
70
+
71
+ ```typescript
72
+ // Host function returns deferred promise immediately
73
+ const deferred = context.newPromise();
74
+
75
+ // Kick off async work
76
+ const hostPromise = fetch(url)
77
+ .then(result => {
78
+ const handle = convertToHandle(context, result);
79
+ deferred.resolve(handle);
80
+ handle.dispose();
81
+ })
82
+ .catch(err => {
83
+ const errHandle = context.newString(err.message);
84
+ deferred.reject(errHandle);
85
+ errHandle.dispose();
86
+ });
87
+
88
+ // Track for cleanup and race optimization
89
+ tracker.pendingPromises.add(hostPromise);
90
+ tracker.notifyNewPromise();
91
+
92
+ return deferred.handle;
93
+ ```
94
+
95
+ ### Bridge System
96
+
97
+ Bridges expose runtime APIs to the sandbox code by creating QuickJS objects and methods that call into host functions. Each bridge is responsible for:
98
+
99
+ 1. Creating QuickJS objects/functions
100
+ 2. Converting between host and sandbox values
101
+ 3. Tracking host resources (streams, responses, readers)
102
+ 4. Managing handle lifetimes
103
+ 5. Registering cleanup handlers
104
+
105
+ ### Handle Management
106
+
107
+ Handles represent QuickJS values and must be disposed when no longer needed. The pattern is:
108
+
109
+ ```typescript
110
+ // Create handle
111
+ const handle = context.newString('hello');
112
+
113
+ // Use handle
114
+ context.setProp(objHandle, 'message', handle);
115
+
116
+ // Dispose handle
117
+ handle.dispose();
118
+ ```
119
+
120
+ **Critical Rules:**
121
+
122
+ - Every `new*` call must be matched with a `dispose()` call
123
+ - `callFunction` returns a handle that must be disposed
124
+ - `evalCode` returns a value/error handle that must be disposed
125
+ - Use `context.dump()` to convert handles to JS values (doesn't dispose the handle)
126
+
127
+ ### Promise Tracking
128
+
129
+ The `PromiseTracker` manages all async operations in the sandbox:
130
+
131
+ ```typescript
132
+ interface PromiseTracker {
133
+ pendingPromises: Set<Promise<void>>; // Host promises being tracked
134
+ notifyNewPromise: () => void; // Signal to wake race loop
135
+ newPromiseSignal: Promise<void>; // Signal promise for racing
136
+ deferredPromises: Set<any>; // QuickJS deferred promises
137
+ }
138
+ ```
139
+
140
+ The tracker is used for:
141
+
142
+ - Race loop optimization (wake when any promise settles)
143
+ - Cleanup (wait for all promises to settle before disposal)
144
+ - Proper handle disposal timing
145
+
146
+ ## Main Components
147
+
148
+ ### sandbox.ts
149
+
150
+ Singleton QuickJS sandbox with queued execution. Ensures only one sandbox (64MB memory limit) exists per worker by using a singleton pattern and execution queue.
151
+
152
+ #### `Sandbox` Class
153
+
154
+ ```typescript
155
+ class Sandbox {
156
+ static async getInstance(options?: SandboxOptions): Promise<Sandbox>;
157
+ async execute(
158
+ code: string,
159
+ globals: SandboxGlobals,
160
+ options?: SandboxOptions
161
+ ): Promise<SandboxResult>;
162
+ async dispose(): Promise<void>;
163
+ static reset(): void;
164
+ }
165
+ ```
166
+
167
+ **Usage:**
168
+
169
+ ```typescript
170
+ // Get or create singleton (same for services and actors)
171
+ const sandbox = await Sandbox.getInstance();
172
+
173
+ // Execute code (queues if another execution is running)
174
+ const result = await sandbox.execute(
175
+ 'const x = await fetch(url); return x;',
176
+ {},
177
+ {
178
+ timeoutMs: 30000,
179
+ memoryLimitBytes: 64 * 1024 * 1024,
180
+ logger: console
181
+ }
182
+ );
183
+
184
+ // Dispose when shutting down (e.g., actor eviction)
185
+ await sandbox.dispose();
186
+ ```
187
+
188
+ **Key Features:**
189
+
190
+ - **Singleton pattern**: One sandbox per worker, created on first `getInstance()`
191
+ - **Execution queue**: Concurrent `execute()` calls wait their turn
192
+ - **Memory limit**: Enforces 64MB limit (or custom via options)
193
+ - **Reusable context**: Created once, reused across executions (recreated after cleanup)
194
+ - **Proper cleanup**: Full cleanup after each execution including context recreation
195
+ - **Queue management**: Pending executions are rejected on `dispose()`
196
+
197
+ **Why Singleton?**
198
+
199
+ In Raindrop Actors, multiple requests can arrive concurrently. Without a singleton, each request would create its own sandbox (64MB each), exceeding worker memory limits (128MB) and causing crashes. The singleton ensures maximum one sandbox per worker.
200
+
201
+ **Execution Flow:**
202
+
203
+ 1. **First `getInstance()`**: Creates QuickJS runtime and context, sets memory limit
204
+ 2. **`execute(code, globals, options)`**:
205
+ - Adds execution to queue
206
+ - If queue is idle, starts processing immediately
207
+ - If another execution is running, waits in queue
208
+ - When turn comes:
209
+ - Set interrupt handler for timeout
210
+ - Create promise tracker and cleanup handlers
211
+ - Install bridges (standard + custom)
212
+ - Inject globals with promise tracking
213
+ - Wrap code in async IIFE
214
+ - Run race loop to wait for promise
215
+ - Return result
216
+ - Full cleanup
217
+ - Recreate context for next execution
218
+ 3. **`dispose()`**:
219
+ - Rejects all queued executions
220
+ - Disposes context and runtime
221
+ - Clears singleton (next `getInstance()` creates new instance)
222
+
223
+ ### Bridge System
224
+
225
+ The bridge system enables sandbox code to interact with host APIs. Bridges are registered functions or objects that expose host capabilities to the sandbox.
226
+
227
+ **Key Types:**
228
+
229
+ ```typescript
230
+ interface BridgeContext {
231
+ context: QuickJSContext;
232
+ runtime: any;
233
+ tracker: PromiseTracker;
234
+ logger?: Logger;
235
+ cleanupHandlers: Array<() => void | Promise<void>>;
236
+ }
237
+
238
+ type BridgeInstaller = (ctx: BridgeContext) => void;
239
+ ```
240
+
241
+ **Bridge Installation:**
242
+
243
+ Bridges are installed in two phases:
244
+
245
+ 1. **Standard bridges**: Always installed (fetch, console, TextEncoder, TextDecoder, ReadableStream, Response)
246
+ 2. **Custom bridges**: Optional, provided via options
247
+
248
+ **Execution Flow:**
249
+
250
+ 1. **Initialization**
251
+ - Load QuickJS WASM module (singleton, cached across executions)
252
+ - Create runtime and context
253
+ - Set memory limit and interrupt handler for timeout
254
+ - Create promise tracker and cleanup handlers array
255
+
256
+ 2. **Bridge Installation**
257
+ - Install standard bridges (fetch, console, TextEncoder, TextDecoder)
258
+ - Install custom bridges (if provided)
259
+
260
+ 3. **Global Injection**
261
+ - For each global value:
262
+ - Primitive → convert to handle, set on global
263
+ - Function → create QuickJS function that tracks async returns
264
+ - Object → create object, inject properties and methods
265
+
266
+ 4. **Code Execution**
267
+ - Wrap code in async IIFE: `(async () => { ${code} })()`
268
+ - Evaluate code (returns promise handle immediately)
269
+ - Run race loop to wait for promise settlement
270
+
271
+ 5. **Race Loop**
272
+
273
+ ```typescript
274
+ while (true) {
275
+ // Check timeout
276
+ // Execute pending QuickJS jobs
277
+ // Check main promise state
278
+ // If settled → resolve/reject with result
279
+ // Race: pending promises | new promise signal | timeout
280
+ // Wait for first to complete
281
+ // Loop
282
+ }
283
+ ```
284
+
285
+ 6. **Cleanup** (in finally block)
286
+ - Run bridge cleanup handlers (abort streams, release readers)
287
+ - Flush microtasks
288
+ - Wait for all pending promises to settle (3s timeout)
289
+ - Settle remaining deferred promises
290
+ - Drain job queue (max 1000 iterations, 1s timeout)
291
+ - Dispose deferred promise handles
292
+ - Final job drain (100 iterations, 500ms timeout)
293
+ - Check for lingering GC objects
294
+ - Dispose context
295
+ - Dispose runtime (only if safe)
296
+
297
+ **Cleanup Complexity:**
298
+
299
+ The cleanup sequence is critical to prevent memory leaks and crashes:
300
+
301
+ 1. **Bridge cleanup first**: Aborts streams, releases readers. This prevents new promises from being created.
302
+ 2. **Flush microtasks**: Lets abort handlers propagate and settle deferred promises.
303
+ 3. **Wait for promises**: All pending host promises must settle before disposal.
304
+ 4. **Settle deferred promises**: Reject pending deferred promises with cleanup error.
305
+ 5. **Drain job queue**: Execute QuickJS jobs repeatedly until queue is empty.
306
+ 6. **Dispose handles**: All QuickJS handles must be disposed.
307
+ 7. **Check GC objects**: Skip runtime disposal if lingering promise objects detected (QuickJS limitation).
308
+ 8. **Dispose context and runtime**: Final cleanup.
309
+
310
+ ### Bridge System
311
+
312
+ #### types.ts
313
+
314
+ Defines shared types for bridges:
315
+
316
+ ```typescript
317
+ interface BridgeContext {
318
+ context: QuickJSContext;
319
+ runtime: any;
320
+ tracker: PromiseTracker;
321
+ logger?: Logger;
322
+ cleanupHandlers: Array<() => void | Promise<void>>;
323
+ }
324
+
325
+ interface PromiseTracker {
326
+ pendingPromises: Set<Promise<void>>;
327
+ notifyNewPromise: () => void;
328
+ newPromiseSignal: Promise<void>;
329
+ deferredPromises: Set<any>;
330
+ }
331
+ ```
332
+
333
+ #### utils.ts
334
+
335
+ Core bridge utility functions:
336
+
337
+ - `convertToHandle(context, value)`: Convert JS values to QuickJS handles
338
+ - `createAsyncBridge(ctx, name, hostFn)`: Create an async bridge function
339
+ - `createSyncBridge(ctx, name, hostFn)`: Create a sync bridge function
340
+ - `defineClass(ctx, classCode, className)`: Define a QuickJS class from code
341
+ - `setProperties(context, handle, props)`: Set multiple properties on an object
342
+ - `addMethod(context, objHandle, methodName, methodHandle)`: Add a method to an object
343
+ - `executeTrackedAsync(ctx, deferred, hostPromise)`: Execute async work with tracking
344
+
345
+ #### index.ts
346
+
347
+ Bridge installation and cleanup orchestration:
348
+
349
+ - `installAllBridges(ctx)`: Install all standard bridges
350
+ - `createPromiseTracker()`: Create a new promise tracker
351
+ - `runCleanupHandlers(handlers, logger)`: Execute all cleanup handlers
352
+
353
+ ### Bridge Implementations
354
+
355
+ #### console.ts
356
+
357
+ Bridges the `console` API with output capture.
358
+
359
+ **Features:**
360
+
361
+ - `console.log`, `console.error`, `console.warn` methods
362
+ - Argument formatting (objects, strings, primitives)
363
+ - Output capture (accessible via `getConsoleOutput(context)`)
364
+ - Cleanup via `clearConsoleOutput(context)`
365
+
366
+ **Pattern:**
367
+
368
+ ```typescript
369
+ export function installConsole(ctx: BridgeContext): void {
370
+ const consoleHandle = context.newObject();
371
+ const output: string[] = [];
372
+
373
+ const logMethod = context.newFunction('log', (...args: QuickJSHandle[]) => {
374
+ const jsArgs = args.map(arg => context.dump(arg));
375
+ const outputLine = formatArgs(jsArgs);
376
+ output.push(`[LOG] ${outputLine}`);
377
+ return context.undefined;
378
+ });
379
+
380
+ context.setProp(consoleHandle, 'log', logMethod);
381
+ logHandle.dispose();
382
+ context.setProp(context.global, 'console', consoleHandle);
383
+ consoleHandle.dispose();
384
+ }
385
+ ```
386
+
387
+ #### fetch.ts
388
+
389
+ Bridges the `fetch` API.
390
+
391
+ **Features:**
392
+
393
+ - Full fetch API support (GET, POST, etc.)
394
+ - Request/Response streaming
395
+ - AbortController support
396
+ - Stream cleanup on abort/timeout
397
+ - Headers support
398
+
399
+ **Resource Tracking:**
400
+
401
+ - Tracks fetch abort controllers for cleanup
402
+ - Registers cleanup handler to abort in-flight requests
403
+
404
+ **Key Implementation:**
405
+
406
+ ```typescript
407
+ export function installFetch(ctx: BridgeContext): void {
408
+ const { context, tracker, cleanupHandlers } = ctx;
409
+ const pendingAbortControllers = new Set<AbortController>();
410
+
411
+ const fetchHandle = context.newFunction(
412
+ 'fetch',
413
+ (urlHandle: QuickJSHandle, initHandle?: QuickJSHandle) => {
414
+ const url = context.dump(urlHandle);
415
+ const init = initHandle ? context.dump(initHandle) : undefined;
416
+
417
+ const deferred = context.newPromise();
418
+ tracker.deferredPromises.add(deferred);
419
+
420
+ // Create AbortController for this request
421
+ const abortController = new AbortController();
422
+ pendingAbortControllers.add(abortController);
423
+
424
+ const hostPromise = fetch(url, { ...init, signal: abortController.signal })
425
+ .then(response => {
426
+ const responseHandle = createResponseObject(ctx, response, objCtx);
427
+ deferred.resolve(responseHandle);
428
+ responseHandle.dispose();
429
+ })
430
+ .catch(err => {
431
+ const errHandle = context.newString(err.message);
432
+ deferred.reject(errHandle);
433
+ errHandle.dispose();
434
+ })
435
+ .finally(() => {
436
+ pendingAbortControllers.delete(abortController);
437
+ tracker.pendingPromises.delete(hostPromise);
438
+ });
439
+
440
+ tracker.pendingPromises.add(hostPromise);
441
+ tracker.notifyNewPromise();
442
+
443
+ return deferred.handle;
444
+ }
445
+ );
446
+
447
+ // Register cleanup handler
448
+ cleanupHandlers.push(() => {
449
+ for (const controller of pendingAbortControllers) {
450
+ controller.abort('Sandbox cleanup');
451
+ }
452
+ pendingAbortControllers.clear();
453
+ });
454
+ }
455
+ ```
456
+
457
+ #### response.ts
458
+
459
+ Bridges the `Response` constructor.
460
+
461
+ **Features:**
462
+
463
+ - `new Response(body, init)` support
464
+ - Stream body bridging
465
+ - Headers support
466
+ - Body consumption methods (text, json, arrayBuffer)
467
+
468
+ **Implementation:**
469
+ Uses `createResponseObject` from shared utilities for unified Response creation.
470
+
471
+ #### readable-stream.ts
472
+
473
+ Bridges the `ReadableStream` API.
474
+
475
+ **Features:**
476
+
477
+ - `getReader()` method
478
+ - `read()` method (async)
479
+ - `releaseLock()` method
480
+ - `locked` property (getter)
481
+ - Coalescing reader for performance (buffers small chunks)
482
+
483
+ **Coalescing Reader:**
484
+ Reduces boundary crossing overhead by buffering small chunks before passing to QuickJS. Minimum chunk size: 16KB.
485
+
486
+ **Key Implementation:**
487
+
488
+ ```typescript
489
+ export function createStreamObject(
490
+ ctx: BridgeContext,
491
+ hostStream: ReadableStream,
492
+ streamId: number,
493
+ hostReaders: Map<number, ReadableStreamDefaultReader>,
494
+ readerIdCounter: { value: number }
495
+ ): QuickJSHandle {
496
+ const { context } = ctx;
497
+ const streamHandle = context.newObject();
498
+
499
+ const getReaderMethod = context.newFunction('getReader', () => {
500
+ const hostReader = hostStream.getReader();
501
+ const readerId = ++readerIdCounter.value;
502
+ hostReaders.set(readerId, hostReader);
503
+ return createReaderObject(ctx, hostReader, readerId);
504
+ });
505
+
506
+ context.setProp(streamHandle, 'getReader', getReaderMethod);
507
+ getReaderMethod.dispose();
508
+
509
+ return streamHandle;
510
+ }
511
+ ```
512
+
513
+ #### text-encoder.ts & text-decoder.ts
514
+
515
+ Bridges `TextEncoder` and `TextDecoder` classes.
516
+
517
+ **Features:**
518
+
519
+ - Standard Web API compatibility
520
+ - UTF-8 encoding/decoding
521
+ - TextDecoder label support
522
+ - Stream mode for TextDecoder
523
+
524
+ #### storage.ts
525
+
526
+ Bridges KV storage API.
527
+
528
+ **Features:**
529
+
530
+ - `await storage.get(path)` - Read values
531
+ - `await storage.write(path, content)` - Write values
532
+ - `await storage.list(prefix, limit)` - List keys
533
+ - `await storage.remove(path)` - Delete keys
534
+ - Response body with metadata (key, size, uploaded)
535
+ - Proper error handling
536
+
537
+ **Mount Info:**
538
+
539
+ ```typescript
540
+ interface StorageMountInfo {
541
+ kv: KVNamespace;
542
+ bucket: R2Bucket;
543
+ }
544
+ ```
545
+
546
+ **Usage:**
547
+
548
+ ```typescript
549
+ const bridgeCtx: BridgeContext = { ... };
550
+ bridgeInstallers.push((ctx) => installStorage(ctx, { kv, bucket }));
551
+ ```
552
+
553
+ #### search.ts
554
+
555
+ Bridges vector search API.
556
+
557
+ **Features:**
558
+
559
+ - `await search(path, query)` - Search documents
560
+ - `await search.chunk(path, query)` - Chunked search
561
+ - Async iteration for results (for await...of)
562
+ - Pagination support
563
+ - Response metadata (text, source, score, chunk)
564
+
565
+ **Mount Info:**
566
+
567
+ ```typescript
568
+ interface SearchMountInfo {
569
+ search: VectorSearch;
570
+ }
571
+ ```
572
+
573
+ #### bucket.ts
574
+
575
+ Bridges R2 bucket API.
576
+
577
+ **Features:**
578
+
579
+ - `await bucket.get(path)` - Get object
580
+ - `await bucket.put(path, content)` - Put object
581
+ - `await bucket.list(prefix, limit)` - List objects
582
+ - `await bucket.delete(path)` - Delete object
583
+ - Response streaming
584
+ - Object metadata
585
+
586
+ #### actor.ts
587
+
588
+ Bridges Durable Object actor API.
589
+
590
+ **Features:**
591
+
592
+ - `await actor(path, instanceName)` - Get actor stub
593
+ - Host stub caching
594
+ - Error handling
595
+
596
+ ### Shared Utilities
597
+
598
+ The `shared/` directory contains reusable components used across multiple bridges.
599
+
600
+ #### headers.ts
601
+
602
+ Creates Headers objects with full Web API support.
603
+
604
+ **Features:**
605
+
606
+ - `get()`, `has()`, `set()` methods
607
+ - `entries()`, `keys()`, `values()` iterators
608
+ - `forEach(callback)` method
609
+ - Direct property access (e.g., `headers['content-type']`)
610
+
611
+ **Usage:**
612
+
613
+ ```typescript
614
+ const headersHandle = createHeadersObject(ctx, {
615
+ 'content-type': 'application/json',
616
+ 'x-custom': 'value'
617
+ });
618
+ ```
619
+
620
+ **Handle Management:**
621
+
622
+ - Disposes all temporary handles created during iteration
623
+ - Returns Headers handle that caller must dispose
624
+
625
+ #### response-object.ts
626
+
627
+ Unified Response object factory.
628
+
629
+ **Features:**
630
+
631
+ - Works with real Response objects (fetch, Response constructor)
632
+ - Works with bare ReadableStream (bucket.get)
633
+ - Sets all standard Response properties (status, ok, headers, etc.)
634
+ - Bridges body stream
635
+ - Attaches body consumption methods (text, json, arrayBuffer)
636
+ - Exposes `bodyUsed` as getter
637
+ - Supports extra metadata (bucket: key, size, uploaded)
638
+
639
+ **Usage:**
640
+
641
+ ```typescript
642
+ // From real Response
643
+ const responseHandle = createResponseObject(ctx, { response: hostResponse }, objCtx);
644
+
645
+ // From bare stream (bucket)
646
+ const responseHandle = createResponseObject(ctx, { body: stream, metadata }, objCtx);
647
+ ```
648
+
649
+ #### body-methods.ts
650
+
651
+ Body consumption methods for Response objects.
652
+
653
+ **Features:**
654
+
655
+ - `createBodyMethod(ctx, methodName, responseId, registry)` - Creates text/json/arrayBuffer method
656
+ - `attachBodyMethods(ctx, responseHandle, responseId, registry)` - Attaches all three
657
+ - Checks `bodyUsed` before consumption
658
+ - Uses cached JSON.parse for efficiency
659
+ - Proper promise tracking
660
+
661
+ **Pattern:**
662
+
663
+ ```typescript
664
+ export function createBodyMethod(
665
+ ctx: BridgeContext,
666
+ methodName: 'json' | 'text' | 'arrayBuffer',
667
+ responseId: number,
668
+ responseRegistry: Map<number, Response>
669
+ ): QuickJSHandle {
670
+ return context.newFunction(methodName, () => {
671
+ const response = responseRegistry.get(responseId);
672
+ if (!response || response.bodyUsed) {
673
+ // Return rejected promise
674
+ }
675
+
676
+ const deferred = context.newPromise();
677
+ const hostPromise = response[methodName]().then(result => {
678
+ // Convert and resolve
679
+ });
680
+ tracker.pendingPromises.add(hostPromise);
681
+ return deferred.handle;
682
+ });
683
+ }
684
+ ```
685
+
686
+ #### cleanup.ts
687
+
688
+ Stream cleanup utilities.
689
+
690
+ **Features:**
691
+
692
+ - `createStreamCleanupHandler(registry, bridgeName, logger)` - Creates cleanup function
693
+ - Proper cleanup order: readers → streams → wait → clear maps
694
+ - Timeout for cleanup operations (2s)
695
+ - Error handling and logging
696
+
697
+ **Critical Pattern:**
698
+
699
+ ```typescript
700
+ export function createStreamCleanupHandler(
701
+ registry: StreamRegistry,
702
+ bridgeName: string,
703
+ logger?: Logger
704
+ ): () => Promise<void> {
705
+ return async () => {
706
+ // STEP 1: Release readers FIRST
707
+ for (const [id, reader] of registry.hostReaders) {
708
+ reader.releaseLock();
709
+ }
710
+ registry.hostReaders.clear();
711
+
712
+ // STEP 2: Cancel streams
713
+ const pendingOperations: Promise<unknown>[] = [];
714
+ for (const [id, stream] of registry.hostStreams) {
715
+ pendingOperations.push(stream.cancel('Sandbox cleanup'));
716
+ }
717
+ registry.hostStreams.clear();
718
+
719
+ // STEP 3: Wait for cancellation (with timeout)
720
+ await Promise.race([
721
+ Promise.allSettled(pendingOperations),
722
+ new Promise(resolve => setTimeout(resolve, 2000))
723
+ ]);
724
+
725
+ // STEP 4: Clear response references
726
+ registry.hostResponses?.clear();
727
+ };
728
+ }
729
+ ```
730
+
731
+ **Why this order?**
732
+
733
+ - Readers must be released before streams can be cancelled
734
+ - Cancellation settles pending read promises
735
+ - Timeout prevents cleanup from blocking disposal indefinitely
736
+
737
+ #### json-helpers.ts
738
+
739
+ JSON parsing utilities with caching.
740
+
741
+ **Features:**
742
+
743
+ - `getJsonParse(context)` - Get or create cached JSON.parse handle
744
+ - `parseJsonInContext(context, jsonString)` - Parse JSON string
745
+ - Caches JSON.parse on context global to avoid evalCode overhead
746
+ - Caller must dispose returned handle
747
+
748
+ **Performance:**
749
+ Caching JSON.parse avoids expensive `evalCode()` calls on every parse operation.
750
+
751
+ **Usage:**
752
+
753
+ ```typescript
754
+ const jsonHandle = parseJsonInContext(context, jsonString);
755
+ // Use handle
756
+ jsonHandle.dispose();
757
+ ```
758
+
759
+ ## Creating a New Bridge
760
+
761
+ ### Step 1: Define the Bridge Function
762
+
763
+ ```typescript
764
+ // packages/precip/src/sandbox/bridges/my-api.ts
765
+ import type { QuickJSHandle } from 'quickjs-emscripten-core';
766
+ import type { BridgeContext } from './types.js';
767
+ import { createAsyncBridge, convertToHandle } from './utils.js';
768
+
769
+ export function installMyApi(ctx: BridgeContext): void {
770
+ const { context, tracker, cleanupHandlers, logger } = ctx;
771
+
772
+ // Create the main API object
773
+ const apiHandle = context.newObject();
774
+
775
+ // Add a method
776
+ const myMethod = context.newFunction('myMethod', (...args: QuickJSHandle[]) => {
777
+ const jsArgs = args.map(arg => context.dump(arg));
778
+
779
+ // Create deferred promise
780
+ const deferred = context.newPromise();
781
+ tracker.deferredPromises.add(deferred);
782
+
783
+ // Kick off async work
784
+ const hostPromise = doAsyncWork(jsArgs)
785
+ .then(result => {
786
+ const handle = convertToHandle(context, result);
787
+ deferred.resolve(handle);
788
+ handle.dispose();
789
+ runtime.executePendingJobs();
790
+ })
791
+ .catch(err => {
792
+ const errHandle = context.newString(err.message);
793
+ deferred.reject(errHandle);
794
+ errHandle.dispose();
795
+ runtime.executePendingJobs();
796
+ })
797
+ .finally(() => {
798
+ tracker.pendingPromises.delete(hostPromise);
799
+ });
800
+
801
+ tracker.pendingPromises.add(hostPromise);
802
+ tracker.notifyNewPromise();
803
+
804
+ return deferred.handle;
805
+ });
806
+
807
+ context.setProp(apiHandle, 'myMethod', myMethod);
808
+ myMethod.dispose();
809
+
810
+ // Register on global
811
+ context.setProp(context.global, 'myApi', apiHandle);
812
+ apiHandle.dispose();
813
+
814
+ logger?.info?.('[MyApi] Bridge installed');
815
+ }
816
+ ```
817
+
818
+ ### Step 2: Register the Bridge
819
+
820
+ ```typescript
821
+ // In packages/precip/src/sandbox/bridges/index.ts
822
+ export { installMyApi } from './my-api.js';
823
+ ```
824
+
825
+ ### Step 3: Use the Bridge
826
+
827
+ ```typescript
828
+ // When executing sandbox code
829
+ const sandbox = await Sandbox.getInstance();
830
+ await sandbox.execute(code, globals, {
831
+ bridgeInstallers: [installMyApi]
832
+ });
833
+ ```
834
+
835
+ ## Common Patterns
836
+
837
+ ### Sync Bridge Function
838
+
839
+ ```typescript
840
+ const syncMethod = context.newFunction('syncMethod', (argHandle: QuickJSHandle) => {
841
+ const arg = context.dump(argHandle);
842
+ const result = doSyncWork(arg);
843
+ return convertToHandle(context, result);
844
+ });
845
+
846
+ context.setProp(objHandle, 'syncMethod', syncMethod);
847
+ syncMethod.dispose();
848
+ ```
849
+
850
+ ### Async Bridge Function
851
+
852
+ ```typescript
853
+ const asyncMethod = context.newFunction('asyncMethod', (argHandle: QuickJSHandle) => {
854
+ const arg = context.dump(argHandle);
855
+ const deferred = context.newPromise();
856
+ tracker.deferredPromises.add(deferred);
857
+
858
+ const hostPromise = doAsyncWork(arg)
859
+ .then(result => {
860
+ const handle = convertToHandle(context, result);
861
+ deferred.resolve(handle);
862
+ handle.dispose();
863
+ runtime.executePendingJobs();
864
+ })
865
+ .catch(err => {
866
+ const errHandle = context.newString(err.message);
867
+ deferred.reject(errHandle);
868
+ errHandle.dispose();
869
+ runtime.executePendingJobs();
870
+ })
871
+ .finally(() => {
872
+ tracker.pendingPromises.delete(hostPromise);
873
+ });
874
+
875
+ tracker.pendingPromises.add(hostPromise);
876
+ tracker.notifyNewPromise();
877
+
878
+ return deferred.handle;
879
+ });
880
+
881
+ context.setProp(objHandle, 'asyncMethod', asyncMethod);
882
+ asyncMethod.dispose();
883
+ ```
884
+
885
+ ### Class Definition
886
+
887
+ ```typescript
888
+ const classCode = `
889
+ class MyClass {
890
+ constructor(value) {
891
+ this.value = value;
892
+ }
893
+
894
+ method() {
895
+ return this.value * 2;
896
+ }
897
+ }
898
+ `;
899
+
900
+ const { value: classHandle, error } = context.evalCode(classCode);
901
+ if (error) {
902
+ error.dispose();
903
+ throw new Error('Failed to define class');
904
+ }
905
+
906
+ context.setProp(context.global, 'MyClass', classHandle);
907
+ classHandle.dispose();
908
+ ```
909
+
910
+ ### Property Getter
911
+
912
+ ```typescript
913
+ // Create getter function
914
+ const getProp = context.newFunction('__getProp', () => {
915
+ return hostProp ? context.true : context.false;
916
+ });
917
+
918
+ context.setProp(objHandle, '__getProp', getProp);
919
+ getProp.dispose();
920
+
921
+ // Define as getter
922
+ const defineCode = `
923
+ (function(obj) {
924
+ var fn = obj.__getProp;
925
+ Object.defineProperty(obj, 'myProp', {
926
+ get: function() { return fn(); },
927
+ enumerable: true,
928
+ configurable: true
929
+ });
930
+ delete obj.__getProp;
931
+ return obj;
932
+ })
933
+ `;
934
+
935
+ const { value: defineFn, error } = context.evalCode(defineCode);
936
+ if (!error) {
937
+ const result = context.callFunction(defineFn, context.undefined, objHandle);
938
+ defineFn.dispose();
939
+ if (!('error' in result)) {
940
+ result.value.dispose();
941
+ }
942
+ }
943
+ ```
944
+
945
+ ## Resource Management
946
+
947
+ ### Handle Disposal
948
+
949
+ **Golden Rule:** Every handle created must be disposed.
950
+
951
+ ```typescript
952
+ // Create handle
953
+ const handle = context.newString('value');
954
+
955
+ // Use it
956
+ context.setProp(obj, 'key', handle);
957
+
958
+ // Dispose it
959
+ handle.dispose();
960
+ ```
961
+
962
+ **Handle Lifecycle:**
963
+
964
+ 1. **Create**: `context.newString()`, `context.newObject()`, etc.
965
+ 2. **Use**: Pass to other functions or set as properties
966
+ 3. **Dispose**: `handle.dispose()`
967
+
968
+ **Common Pitfalls:**
969
+
970
+ ```typescript
971
+ // ❌ Forgot to dispose
972
+ const handle = context.newString('value');
973
+ context.setProp(obj, 'key', handle);
974
+ // handle.dispose() missing!
975
+
976
+ // ❌ Disposed too early
977
+ const handle = context.newString('value');
978
+ handle.dispose();
979
+ context.setProp(obj, 'key', handle); // Error: handle already disposed
980
+
981
+ // ✅ Correct
982
+ const handle = context.newString('value');
983
+ context.setProp(obj, 'key', handle);
984
+ handle.dispose();
985
+ ```
986
+
987
+ ### Stream Cleanup
988
+
989
+ Streams must be cleaned up in a specific order:
990
+
991
+ 1. Release readers (streams can't be cancelled while locked)
992
+ 2. Cancel streams (settles pending promises)
993
+ 3. Wait for cancellation (with timeout)
994
+ 4. Clear all maps
995
+
996
+ ```typescript
997
+ export function cleanupStreams(registry: StreamRegistry): () => Promise<void> {
998
+ return async () => {
999
+ // Release readers
1000
+ for (const reader of registry.hostReaders.values()) {
1001
+ reader.releaseLock();
1002
+ }
1003
+ registry.hostReaders.clear();
1004
+
1005
+ // Cancel streams
1006
+ const pending = [];
1007
+ for (const stream of registry.hostStreams.values()) {
1008
+ pending.push(stream.cancel('Sandbox cleanup'));
1009
+ }
1010
+ registry.hostStreams.clear();
1011
+
1012
+ // Wait (with timeout)
1013
+ await Promise.race([
1014
+ Promise.allSettled(pending),
1015
+ new Promise(resolve => setTimeout(resolve, 2000))
1016
+ ]);
1017
+ };
1018
+ }
1019
+ ```
1020
+
1021
+ ### Registry Pattern
1022
+
1023
+ Host objects are tracked in registries for cleanup:
1024
+
1025
+ ```typescript
1026
+ interface ObjectContext {
1027
+ registry: {
1028
+ hostResponses: Map<number, Response>;
1029
+ hostStreams: Map<number, ReadableStream>;
1030
+ hostReaders: Map<number, ReadableStreamDefaultReader>;
1031
+ };
1032
+ responseIdCounter: { value: number };
1033
+ streamIdCounter: { value: number };
1034
+ readerIdCounter: { value: number };
1035
+ }
1036
+
1037
+ // When creating a response
1038
+ const responseId = ++responseIdCounter.value;
1039
+ registry.hostResponses.set(responseId, response);
1040
+
1041
+ // During cleanup
1042
+ registry.hostResponses.clear();
1043
+ registry.hostStreams.clear();
1044
+ registry.hostReaders.clear();
1045
+ ```
1046
+
1047
+ ## Performance Optimizations
1048
+
1049
+ ### 1. Cached Function Handles
1050
+
1051
+ Expensive operations like `evalCode()` are cached:
1052
+
1053
+ ```typescript
1054
+ // JSON parsing
1055
+ const JSON_PARSE_CACHE_KEY = '__cachedJsonParse';
1056
+
1057
+ function getJsonParse(context: QuickJSContext): QuickJSHandle {
1058
+ const cachedHandle = context.getProp(context.global, JSON_PARSE_CACHE_KEY);
1059
+ if (context.typeof(cachedHandle) === 'function') {
1060
+ return cachedHandle;
1061
+ }
1062
+ cachedHandle.dispose();
1063
+
1064
+ const jsonParse = context.getProp(context.global, 'JSON').parse;
1065
+ context.setProp(context.global, JSON_PARSE_CACHE_KEY, jsonParse);
1066
+ return jsonParse;
1067
+ }
1068
+ ```
1069
+
1070
+ ### 2. Coalescing Reader
1071
+
1072
+ Small stream chunks are buffered before crossing the boundary:
1073
+
1074
+ ```typescript
1075
+ class CoalescingReader {
1076
+ private buffer: Uint8Array[] = [];
1077
+ private bufferSize = 0;
1078
+ private minChunkSize = 16 * 1024;
1079
+
1080
+ async read(): Promise<ReadableStreamReadResult<Uint8Array>> {
1081
+ // Buffer until minChunkSize or stream ends
1082
+ while (!this.done && this.bufferSize < this.minChunkSize) {
1083
+ const result = await this.hostReader.read();
1084
+ if (result.done) {
1085
+ this.done = true;
1086
+ break;
1087
+ }
1088
+ this.buffer.push(result.value);
1089
+ this.bufferSize += result.value.length;
1090
+ }
1091
+
1092
+ // Coalesce and return
1093
+ // ...
1094
+ }
1095
+ }
1096
+ ```
1097
+
1098
+ ### 3. Promise.race Optimization
1099
+
1100
+ Instead of polling, race all pending promises:
1101
+
1102
+ ```typescript
1103
+ const raceLoop = async () => {
1104
+ while (true) {
1105
+ // Check promise state
1106
+ const state = context.getPromiseState(promiseHandle);
1107
+ if (state.type !== 'pending') {
1108
+ // Return result
1109
+ }
1110
+
1111
+ // Create fresh signal BEFORE racing
1112
+ tracker.newPromiseSignal = new Promise<void>(resolve => {
1113
+ tracker.notifyNewPromise = resolve;
1114
+ });
1115
+
1116
+ // Race: pending promises | signal | timeout
1117
+ await Promise.race([
1118
+ ...Array.from(tracker.pendingPromises),
1119
+ tracker.newPromiseSignal,
1120
+ new Promise(r => setTimeout(r, 100))
1121
+ ]);
1122
+
1123
+ // Loop
1124
+ }
1125
+ };
1126
+ ```
1127
+
1128
+ This wakes immediately when any promise settles, achieving 5.81x speedup over polling.
1129
+
1130
+ ## Error Handling
1131
+
1132
+ ### Context Errors
1133
+
1134
+ QuickJS can throw errors during operations. Always handle them:
1135
+
1136
+ ```typescript
1137
+ try {
1138
+ const result = context.callFunction(fn, context.undefined, argHandle);
1139
+ if (result.error) {
1140
+ const error = context.dump(result.error);
1141
+ result.error.dispose();
1142
+ throw new Error(`Call failed: ${error}`);
1143
+ }
1144
+ // Use result.value
1145
+ result.value.dispose();
1146
+ } catch (e) {
1147
+ // Handle error
1148
+ }
1149
+ ```
1150
+
1151
+ ### Disposal Errors
1152
+
1153
+ Disposal can fail if the handle is already disposed or invalid:
1154
+
1155
+ ```typescript
1156
+ try {
1157
+ handle.dispose();
1158
+ } catch (e) {
1159
+ logger?.warn?.(`Failed to dispose handle: ${e}`);
1160
+ }
1161
+ ```
1162
+
1163
+ ### Promise Rejection
1164
+
1165
+ Host promises can reject. Always handle rejections:
1166
+
1167
+ ```typescript
1168
+ const hostPromise = fetch(url)
1169
+ .then(result => {
1170
+ // Handle success
1171
+ })
1172
+ .catch(err => {
1173
+ try {
1174
+ const errHandle = context.newString(err.message);
1175
+ deferred.reject(errHandle);
1176
+ errHandle.dispose();
1177
+ runtime.executePendingJobs();
1178
+ } catch (e) {
1179
+ // Context may be disposed
1180
+ }
1181
+ })
1182
+ .finally(() => {
1183
+ tracker.pendingPromises.delete(hostPromise);
1184
+ });
1185
+ ```
1186
+
1187
+ ## Testing
1188
+
1189
+ Bridges should be tested for:
1190
+
1191
+ 1. **Functionality**: Correct behavior of bridged APIs
1192
+ 2. **Error handling**: Proper error propagation
1193
+ 3. **Resource cleanup**: No handle leaks or memory leaks
1194
+ 4. **Async patterns**: Correct promise tracking and settlement
1195
+ 5. **Edge cases**: Empty values, large payloads, timeouts
1196
+
1197
+ ### Example Test
1198
+
1199
+ ```typescript
1200
+ import { describe, it, expect, beforeEach } from 'vitest';
1201
+ import { Sandbox } from './index.js';
1202
+
1203
+ describe('Console Bridge', () => {
1204
+ let sandbox: Sandbox;
1205
+
1206
+ beforeEach(async () => {
1207
+ sandbox = await Sandbox.getInstance();
1208
+ });
1209
+
1210
+ it('should capture console output', async () => {
1211
+ const result = await sandbox.execute(
1212
+ `
1213
+ console.log("hello");
1214
+ console.error("oops");
1215
+ console.warn("careful");
1216
+ return "done"
1217
+ `,
1218
+ {}
1219
+ );
1220
+
1221
+ expect(result.success).toBe(true);
1222
+ expect(result.result).toBe('done');
1223
+ expect(result.consoleOutput).toContain('hello');
1224
+ expect(result.consoleOutput).toContain('[ERROR] oops');
1225
+ expect(result.consoleOutput).toContain('[WARN] careful');
1226
+ });
1227
+ });
1228
+ ```
1229
+
1230
+ ## Debugging
1231
+
1232
+ ### Logging
1233
+
1234
+ Use the logger parameter to trace execution:
1235
+
1236
+ ```typescript
1237
+ const sandbox = await Sandbox.getInstance();
1238
+ await sandbox.execute(code, globals, {
1239
+ logger: console
1240
+ });
1241
+ ```
1242
+
1243
+ ### Handle Leak Detection
1244
+
1245
+ QuickJS doesn't track handles for you. Watch for:
1246
+
1247
+ - Increasing memory usage
1248
+ - QuickJS assertion errors on disposal
1249
+ - Lingering GC objects in memory usage dump
1250
+
1251
+ ### Promise Leak Detection
1252
+
1253
+ Check promise tracker size:
1254
+
1255
+ ```typescript
1256
+ logger?.info(`Pending promises: ${tracker.pendingPromises.size}`);
1257
+ logger?.info(`Deferred promises: ${tracker.deferredPromises.size}`);
1258
+ ```
1259
+
1260
+ ### Cleanup Verification
1261
+
1262
+ Verify cleanup handlers run:
1263
+
1264
+ ```typescript
1265
+ logger?.info(`Running ${cleanupHandlers.length} cleanup handlers`);
1266
+ await runCleanupHandlers(cleanupHandlers, logger);
1267
+ ```
1268
+
1269
+ ## Known Limitations
1270
+
1271
+ ### QuickJS GC Objects
1272
+
1273
+ When sandbox code holds references to unresolved promises (e.g., timeout during fetch), QuickJS may have lingering GC objects. Disposing the runtime in this state causes assertion errors.
1274
+
1275
+ **Workaround:** Skip runtime disposal if lingering objects detected:
1276
+
1277
+ ```typescript
1278
+ const memUsage = runtime.dumpMemoryUsage();
1279
+ if (memUsage.includes(' resolve') || memUsage.includes(' promise')) {
1280
+ logger?.warn?.('Detected lingering promise objects - skipping runtime disposal');
1281
+ // Memory will leak, but process won't crash
1282
+ } else {
1283
+ runtime.dispose();
1284
+ }
1285
+ ```
1286
+
1287
+ ### Context Disposal
1288
+
1289
+ Context must be disposed before runtime. If context disposal fails, runtime should not be disposed.
1290
+
1291
+ ### Promise Settlement
1292
+
1293
+ Not all promises can be settled during cleanup (e.g., network timeout). After a timeout, remaining promises are abandoned.
1294
+
1295
+ ### Web API Gaps (Accepted)
1296
+
1297
+ The following Web API features are intentionally not implemented. They are known gaps accepted as current limitations:
1298
+
1299
+ - **`Response.clone()`** — Not supported. Body can only be consumed once; sandbox code cannot both stream and read text from the same response.
1300
+ - **`Request` constructor** — Not available. The `fetch` bridge accepts `(url, init)` directly. Sandbox code cannot create `Request` objects for more complex fetch patterns.
1301
+ - **`AbortController` / `AbortSignal`** — Not exposed to sandbox code. The fetch bridge creates its own `AbortController` internally for cleanup, but sandbox code cannot create or use `AbortController` to cancel its own requests. This may be implemented in the future.
1302
+ - **`ReadableStream.tee()`, `pipeTo()`, `pipeThrough()`** — Only `getReader()` is supported on ReadableStream. Advanced stream combinators are not bridged.
1303
+
1304
+ ## Best Practices
1305
+
1306
+ 1. **Always dispose handles**: Use try/finally to ensure cleanup
1307
+ 2. **Track host objects**: Use registries for streams, responses, readers
1308
+ 3. **Register cleanup handlers**: Abort streams, release readers
1309
+ 4. **Check handle validity**: Use `handle.alive` before disposal
1310
+ 5. **Log operations**: Use logger for debugging
1311
+ 6. **Handle errors gracefully**: Context may be disposed during async operations
1312
+ 7. **Use shared utilities**: Don't reinvent headers, response objects, etc.
1313
+ 8. **Test cleanup**: Verify no handle leaks
1314
+ 9. **Time-limit cleanup**: Prevent indefinite blocking
1315
+ 10. **Follow patterns**: Use existing bridges as templates
1316
+
1317
+ ## See Also
1318
+
1319
+ - [QuickJS Documentation](https://bellard.org/quickjs/)
1320
+ - [quickjs-emscripten](https://github.com/justjake/quickjs-emscripten)
1321
+ - [Bridges README](./bridges/README.md) - Detailed bridge development guide