@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,181 @@
1
+ /**
2
+ * Shared Headers object creation for fetch and Response bridges
3
+ *
4
+ * Backed by a host Headers object. Mutation methods (set, append, delete)
5
+ * update both the host Headers and the direct-access properties on the
6
+ * QuickJS handle so the two views stay in sync.
7
+ */
8
+
9
+ import type { QuickJSHandle } from 'quickjs-emscripten-core';
10
+ import { Scope, isFail } from 'quickjs-emscripten-core';
11
+ import type { BridgeContext } from '../types.js';
12
+
13
+ /**
14
+ * Create a Headers object with full Web API support:
15
+ * get(), has(), set(), append(), delete(), entries(), forEach(), keys(), values()
16
+ *
17
+ * Mutations go through the host Headers object and are synced back to
18
+ * direct-access properties on the QuickJS handle.
19
+ */
20
+ export function createHeadersObject(
21
+ ctx: BridgeContext,
22
+ headers: Headers | Record<string, string> | undefined
23
+ ): QuickJSHandle {
24
+ const { context } = ctx;
25
+ const headersHandle = context.newObject();
26
+
27
+ if (!headers) {
28
+ return headersHandle;
29
+ }
30
+
31
+ // Convert to Headers object if it's a plain record
32
+ const headersObj = headers instanceof Headers ? headers : new Headers(headers);
33
+
34
+ // Set header properties for direct access
35
+ headersObj.forEach((value: string, name: string) => {
36
+ using valueHandle = context.newString(value);
37
+ context.setProp(headersHandle, name, valueHandle);
38
+ });
39
+
40
+ // Add get() method
41
+ {
42
+ using getMethod = context.newFunction('get', (nameHandle: QuickJSHandle) => {
43
+ const name = context.dump(nameHandle);
44
+ const value = headersObj.get(name);
45
+ if (value === null) {
46
+ return context.null;
47
+ }
48
+ return context.newString(value);
49
+ });
50
+ context.setProp(headersHandle, 'get', getMethod);
51
+ }
52
+
53
+ // Add has() method
54
+ {
55
+ using hasMethod = context.newFunction('has', (nameHandle: QuickJSHandle) => {
56
+ const name = context.dump(nameHandle);
57
+ return headersObj.has(name) ? context.true : context.false;
58
+ });
59
+ context.setProp(headersHandle, 'has', hasMethod);
60
+ }
61
+
62
+ // Add set() method - replaces the value for a header
63
+ {
64
+ using setMethod = context.newFunction(
65
+ 'set',
66
+ (nameHandle: QuickJSHandle, valueHandle: QuickJSHandle) => {
67
+ const name = context.dump(nameHandle) as string;
68
+ const value = context.dump(valueHandle) as string;
69
+ headersObj.set(name, value);
70
+ // Sync the direct-access property
71
+ using newValueHandle = context.newString(headersObj.get(name)!);
72
+ context.setProp(headersHandle, name.toLowerCase(), newValueHandle);
73
+ return context.undefined;
74
+ }
75
+ );
76
+ context.setProp(headersHandle, 'set', setMethod);
77
+ }
78
+
79
+ // Add append() method - appends a value (multi-value headers like Set-Cookie)
80
+ {
81
+ using appendMethod = context.newFunction(
82
+ 'append',
83
+ (nameHandle: QuickJSHandle, valueHandle: QuickJSHandle) => {
84
+ const name = context.dump(nameHandle) as string;
85
+ const value = context.dump(valueHandle) as string;
86
+ headersObj.append(name, value);
87
+ // Sync the direct-access property (combined value with ", " per spec)
88
+ const combined = headersObj.get(name.toLowerCase());
89
+ if (combined !== null) {
90
+ using combinedHandle = context.newString(combined);
91
+ context.setProp(headersHandle, name.toLowerCase(), combinedHandle);
92
+ }
93
+ return context.undefined;
94
+ }
95
+ );
96
+ context.setProp(headersHandle, 'append', appendMethod);
97
+ }
98
+
99
+ // Add delete() method - removes a header entirely
100
+ {
101
+ using deleteMethod = context.newFunction('delete', (nameHandle: QuickJSHandle) => {
102
+ const name = context.dump(nameHandle) as string;
103
+ headersObj.delete(name);
104
+ context.setProp(headersHandle, name.toLowerCase(), context.undefined);
105
+ return context.undefined;
106
+ });
107
+ context.setProp(headersHandle, 'delete', deleteMethod);
108
+ }
109
+
110
+ // Add entries() method that returns an array
111
+ {
112
+ using entriesMethod = context.newFunction('entries', () => {
113
+ const entriesArray = context.newArray();
114
+ let index = 0;
115
+ headersObj.forEach((value: string, name: string) => {
116
+ // Use Scope for per-iteration cleanup of intermediate handles
117
+ Scope.withScope((scope) => {
118
+ const tuple = scope.manage(context.newArray());
119
+ const nameH = scope.manage(context.newString(name));
120
+ const valueH = scope.manage(context.newString(value));
121
+ context.setProp(tuple, 0, nameH);
122
+ context.setProp(tuple, 1, valueH);
123
+ context.setProp(entriesArray, index, tuple);
124
+ });
125
+ index++;
126
+ });
127
+ return entriesArray;
128
+ });
129
+ context.setProp(headersHandle, 'entries', entriesMethod);
130
+ }
131
+
132
+ // Add forEach() method - commonly used
133
+ {
134
+ using forEachMethod = context.newFunction('forEach', (callbackHandle: QuickJSHandle) => {
135
+ headersObj.forEach((value: string, name: string) => {
136
+ using valueH = context.newString(value);
137
+ using nameH = context.newString(name);
138
+ // Call the callback with (value, name) - matches Headers.forEach signature
139
+ const callResult = context.callFunction(callbackHandle, context.undefined, valueH, nameH);
140
+ // Dispose the return value of the callback
141
+ if (isFail(callResult)) {
142
+ callResult.error.dispose();
143
+ } else {
144
+ callResult.value.dispose();
145
+ }
146
+ });
147
+ return context.undefined;
148
+ });
149
+ context.setProp(headersHandle, 'forEach', forEachMethod);
150
+ }
151
+
152
+ // Add keys() method - returns array of header names
153
+ {
154
+ using keysMethod = context.newFunction('keys', () => {
155
+ const keysArray = context.newArray();
156
+ let index = 0;
157
+ headersObj.forEach((_: string, name: string) => {
158
+ using nameHandle = context.newString(name);
159
+ context.setProp(keysArray, index++, nameHandle);
160
+ });
161
+ return keysArray;
162
+ });
163
+ context.setProp(headersHandle, 'keys', keysMethod);
164
+ }
165
+
166
+ // Add values() method - returns array of header values
167
+ {
168
+ using valuesMethod = context.newFunction('values', () => {
169
+ const valuesArray = context.newArray();
170
+ let index = 0;
171
+ headersObj.forEach((value: string, _: string) => {
172
+ using valueHandle = context.newString(value);
173
+ context.setProp(valuesArray, index++, valueHandle);
174
+ });
175
+ return valuesArray;
176
+ });
177
+ context.setProp(headersHandle, 'values', valuesMethod);
178
+ }
179
+
180
+ return headersHandle;
181
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shared bridge utilities
3
+ */
4
+
5
+ export { convertToHandle } from './convert.js';
6
+ export { unwrapResult } from './scope-helpers.js';
7
+ export { createHeadersObject } from './headers.js';
8
+ export { attachBodyMethods } from './body-methods.js';
9
+ export {
10
+ createStreamCleanupHandler,
11
+ createIdCounter,
12
+ nextId,
13
+ type StreamRegistry
14
+ } from './cleanup.js';
15
+ export { getJsonParse, parseJsonInContext } from './json-helpers.js';
16
+ export {
17
+ createResponseObject,
18
+ attachStreamBodyMethods,
19
+ type ResponseObjectOptions,
20
+ type ResponseObjectContext
21
+ } from './response-object.js';
22
+ export {
23
+ withTrackedPromise,
24
+ withTrackedPromiseResult,
25
+ withTrackedPromiseValue
26
+ } from './promise-helper.js';
27
+ export { setupStreamRegistry, setupSimpleStreamRegistry } from './registry-setup.js';
28
+ export { parseMountPath, type ParsedPath } from './path-parser.js';
29
+ export {
30
+ createListResult,
31
+ createSuccessResult,
32
+ createSuccessResultWithKey,
33
+ createSuccessResultWithMetadata,
34
+ type FileEntry
35
+ } from './result-builder.js';
36
+ export { createReaderHandle } from './stream-reader.js';
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Shared JSON parsing utilities for QuickJS bridges
3
+ * Provides cached JSON.parse access to avoid repeated evalCode calls
4
+ */
5
+
6
+ import type { QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core';
7
+
8
+ // Internal symbol name for cached JSON.parse on context global
9
+ const JSON_PARSE_CACHE_KEY = '__cachedJsonParse';
10
+
11
+ /**
12
+ * Get or create a cached JSON.parse reference for the context.
13
+ * Returns a handle - caller must dispose.
14
+ *
15
+ * This avoids evalCode on every JSON parse operation.
16
+ * The cached handle is stored on the context's global object, so it's
17
+ * automatically cleaned up when the context is disposed.
18
+ */
19
+ export function getJsonParse(context: QuickJSContext): QuickJSHandle {
20
+ // Check if we already cached JSON.parse on the global
21
+ const cachedHandle = context.getProp(context.global, JSON_PARSE_CACHE_KEY);
22
+ const cachedType = context.typeof(cachedHandle);
23
+
24
+ if (cachedType === 'function') {
25
+ // Already cached - return (caller must dispose)
26
+ return cachedHandle;
27
+ }
28
+
29
+ // Not cached yet - dispose the undefined handle we got
30
+ cachedHandle.dispose();
31
+
32
+ // Get JSON.parse from the context
33
+ {
34
+ using jsonHandle = context.getProp(context.global, 'JSON');
35
+ const jsonParse = context.getProp(jsonHandle, 'parse');
36
+
37
+ // Store on global for future calls — use Object.defineProperty to make it
38
+ // non-enumerable (hidden from Object.keys/for-in) and non-writable
39
+ // (sandbox code can't tamper with it)
40
+ context.setProp(context.global, JSON_PARSE_CACHE_KEY, jsonParse);
41
+ const hideResult = context.evalCode(`
42
+ try {
43
+ Object.defineProperty(globalThis, '${JSON_PARSE_CACHE_KEY}', {
44
+ enumerable: false, writable: false, configurable: false
45
+ });
46
+ } catch(e) {}
47
+ undefined;
48
+ `);
49
+ if (hideResult.error) {
50
+ hideResult.error.dispose();
51
+ } else {
52
+ (hideResult as { value: QuickJSHandle }).value.dispose();
53
+ }
54
+
55
+ // Return the handle - caller must dispose their reference
56
+ return jsonParse;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Parse a JSON string in QuickJS context.
62
+ * Returns the parsed handle or throws on error.
63
+ * Caller must dispose the returned handle.
64
+ */
65
+ export function parseJsonInContext(context: QuickJSContext, jsonString: string): QuickJSHandle {
66
+ using jsonParse = getJsonParse(context);
67
+ using jsonStrHandle = context.newString(jsonString);
68
+ const result = context.callFunction(jsonParse, context.undefined, jsonStrHandle);
69
+
70
+ if (result.error) {
71
+ const error = context.dump(result.error);
72
+ result.error.dispose();
73
+ throw new Error(`JSON parse failed: ${JSON.stringify(error)}`);
74
+ }
75
+
76
+ return (result as { value: QuickJSHandle }).value;
77
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Unified path parser for mount-based paths.
3
+ *
4
+ * Handles /mount-name/path/to/resource format consistently across all bridges.
5
+ * Reduces ~80 lines of duplicated path parsing code.
6
+ */
7
+
8
+ /**
9
+ * Parsed path result containing the mount and resource path
10
+ */
11
+ export interface ParsedPath<T> {
12
+ mount: T;
13
+ resourcePath: string;
14
+ }
15
+
16
+ /**
17
+ * Normalize a resource path by resolving `.` and `..` segments.
18
+ * Rejects any attempt to traverse above the mount root.
19
+ *
20
+ * @param resourcePath - The raw resource path (after mount name extraction)
21
+ * @param fullPath - The original full path (for error messages)
22
+ * @param mountTypeName - Name for error messages (e.g., "Bucket", "Storage")
23
+ * @returns Normalized path with no `.` or `..` segments
24
+ * @throws Error if path attempts to traverse above the mount root
25
+ */
26
+ function normalizeResourcePath(
27
+ resourcePath: string,
28
+ fullPath: string,
29
+ mountTypeName: string
30
+ ): string {
31
+ const segments = resourcePath.split('/');
32
+ const normalized: string[] = [];
33
+
34
+ for (const segment of segments) {
35
+ if (segment === '..') {
36
+ if (normalized.length === 0) {
37
+ throw new Error(
38
+ `Invalid ${mountTypeName} path: ${fullPath}. Path traversal beyond mount root is not allowed`
39
+ );
40
+ }
41
+ normalized.pop();
42
+ } else if (segment !== '.' && segment !== '') {
43
+ normalized.push(segment);
44
+ }
45
+ }
46
+
47
+ return normalized.join('/');
48
+ }
49
+
50
+ /**
51
+ * Parse a mount-based path like "/mount-name/path/to/resource"
52
+ *
53
+ * @param path - The full path to parse
54
+ * @param mounts - Map of mount name to mount info
55
+ * @param mountTypeName - Name for error messages (e.g., "Bucket", "Storage")
56
+ * @param requireResourcePath - Whether the path must have a resource path (default: false)
57
+ * @returns Parsed path with mount and resource path
58
+ * @throws Error if path is invalid, mount not found, or path traversal attempted
59
+ */
60
+ export function parseMountPath<T>(
61
+ path: string,
62
+ mounts: Map<string, T>,
63
+ mountTypeName: string,
64
+ requireResourcePath: boolean = false
65
+ ): ParsedPath<T> {
66
+ if (!path.startsWith('/')) {
67
+ throw new Error(`Invalid ${mountTypeName} path: ${path}. Paths must start with /`);
68
+ }
69
+
70
+ const withoutSlash = path.slice(1);
71
+
72
+ if (!withoutSlash) {
73
+ throw new Error(`Invalid ${mountTypeName} path: ${path}. Path must include a mount name`);
74
+ }
75
+
76
+ const slashIndex = withoutSlash.indexOf('/');
77
+
78
+ let mountName: string;
79
+ let resourcePath: string;
80
+
81
+ if (slashIndex === -1) {
82
+ // Just /mount-name with no resource path
83
+ mountName = withoutSlash;
84
+ resourcePath = '';
85
+
86
+ if (requireResourcePath) {
87
+ throw new Error(
88
+ `Invalid ${mountTypeName} path: ${path}. Expected /mount-name/path but got only mount name`
89
+ );
90
+ }
91
+ } else {
92
+ mountName = withoutSlash.slice(0, slashIndex);
93
+ resourcePath = normalizeResourcePath(
94
+ withoutSlash.slice(slashIndex + 1),
95
+ path,
96
+ mountTypeName
97
+ );
98
+ }
99
+
100
+ const mount = mounts.get(mountName);
101
+ if (!mount) {
102
+ const available = Array.from(mounts.keys()).join(', ');
103
+ throw new Error(
104
+ `${mountTypeName} mount not found: ${mountName}. Available: ${available || 'none'}`
105
+ );
106
+ }
107
+
108
+ return { mount, resourcePath };
109
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Promise helper utilities for QuickJS bridges
3
+ *
4
+ * Eliminates the boilerplate of deferred promises, tracking, and error handling
5
+ * that's repeated in every async bridge method.
6
+ */
7
+
8
+ import type { QuickJSHandle } from 'quickjs-emscripten-core';
9
+ import type { BridgeContext } from '../types.js';
10
+ import { convertToHandle } from './convert.js';
11
+
12
+ /**
13
+ * Wraps an async operation with automatic promise tracking and error handling.
14
+ *
15
+ * This eliminates 20+ lines of boilerplate from every async bridge method:
16
+ * - Creates deferred promise
17
+ * - Adds to tracker
18
+ * - Runs async function with try/catch
19
+ * - Handles resolve/reject with proper handle disposal
20
+ * - Cleans up promise tracking
21
+ * - Notifies promise tracker
22
+ *
23
+ * @param ctx - Bridge context
24
+ * @param fn - Async function that receives the deferred promise handle
25
+ * @returns QuickJS handle to the promise
26
+ */
27
+ export function withTrackedPromise(
28
+ ctx: BridgeContext,
29
+ fn: (deferred: any) => Promise<void>
30
+ ): QuickJSHandle {
31
+ const { context, runtime, tracker } = ctx;
32
+ const deferred = context.newPromise();
33
+ tracker.deferredPromises.add(deferred);
34
+
35
+ const hostPromise = (async () => {
36
+ try {
37
+ await fn(deferred);
38
+ } catch (e: unknown) {
39
+ // Handle errors by rejecting the deferred promise
40
+ try {
41
+ const errHandle = context.newString(e instanceof Error ? e.message : String(e));
42
+ deferred.reject(errHandle);
43
+ errHandle.dispose();
44
+ runtime.executePendingJobs();
45
+ } catch (rejectError) {
46
+ // Context is likely disposed — deferred will be cleaned up by sandbox cleanup
47
+ ctx.logger?.error?.('[Bridge] Failed to reject promise in withTrackedPromise', {
48
+ originalError: e,
49
+ rejectError
50
+ });
51
+ }
52
+ // Don't rethrow - we've already handled it by rejecting the deferred promise
53
+ }
54
+ })().finally(() => {
55
+ tracker.pendingPromises.delete(hostPromise);
56
+ tracker.deferredPromises.delete(deferred);
57
+ });
58
+
59
+ tracker.pendingPromises.add(hostPromise);
60
+ tracker.notifyNewPromise();
61
+
62
+ return deferred.handle;
63
+ }
64
+
65
+ /**
66
+ * Simplified version for async operations that return a QuickJS handle.
67
+ * Automatically resolves the deferred promise with the result handle and disposes it.
68
+ *
69
+ * @param ctx - Bridge context
70
+ * @param fn - Async function that returns a QuickJSHandle to resolve with
71
+ * @returns QuickJS handle to the promise
72
+ */
73
+ export function withTrackedPromiseResult(
74
+ ctx: BridgeContext,
75
+ fn: () => Promise<QuickJSHandle>
76
+ ): QuickJSHandle {
77
+ return withTrackedPromise(ctx, async (deferred) => {
78
+ const resultHandle = await fn();
79
+ deferred.resolve(resultHandle);
80
+ resultHandle.dispose();
81
+ ctx.runtime.executePendingJobs();
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Like withTrackedPromiseResult but allows custom handling of the result before resolving.
87
+ *
88
+ * @param ctx - Bridge context
89
+ * @param fn - Async function that returns any value (will be converted to handle)
90
+ * @returns QuickJS handle to the promise
91
+ */
92
+ export function withTrackedPromiseValue<T>(
93
+ ctx: BridgeContext,
94
+ fn: () => Promise<T>
95
+ ): QuickJSHandle {
96
+ const { context } = ctx;
97
+
98
+ return withTrackedPromise(ctx, async (deferred) => {
99
+ const value = await fn();
100
+
101
+ // Convert value to handle using the proper utility
102
+ const resultHandle = convertToHandle(context, value);
103
+
104
+ deferred.resolve(resultHandle);
105
+ resultHandle.dispose();
106
+ ctx.runtime.executePendingJobs();
107
+ });
108
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Registry setup utilities for QuickJS bridges
3
+ *
4
+ * Standardizes the initialization of stream registries and response object contexts
5
+ * across all stream-based bridges.
6
+ */
7
+
8
+ import type { BridgeContext } from '../types.js';
9
+ import {
10
+ createStreamCleanupHandler,
11
+ createIdCounter,
12
+ type StreamRegistry
13
+ } from './cleanup.js';
14
+ import type { ResponseObjectContext } from './response-object.js';
15
+
16
+ /**
17
+ * Sets up a stream registry with counters, cleanup handler, and response object context.
18
+ *
19
+ * This eliminates ~17 lines of repetitive setup code from fetch, response, storage, and bucket bridges.
20
+ *
21
+ * @param ctx - Bridge context (cleanupHandlers will be populated)
22
+ * @param bridgeName - Name for logging and cleanup
23
+ * @returns ResponseObjectContext containing registry and counters
24
+ */
25
+ export function setupStreamRegistry(
26
+ ctx: BridgeContext,
27
+ bridgeName: string
28
+ ): ResponseObjectContext {
29
+ const registry: StreamRegistry = {
30
+ hostResponses: new Map<number, Response>(),
31
+ hostStreams: new Map<number, ReadableStream>(),
32
+ hostReaders: new Map<number, ReadableStreamDefaultReader>()
33
+ };
34
+
35
+ const responseIdCounter = createIdCounter();
36
+ const streamIdCounter = createIdCounter();
37
+ const readerIdCounter = createIdCounter();
38
+
39
+ ctx.cleanupHandlers.push(createStreamCleanupHandler(registry, bridgeName, ctx.logger));
40
+
41
+ return {
42
+ registry: {
43
+ hostResponses: registry.hostResponses!,
44
+ hostStreams: registry.hostStreams,
45
+ hostReaders: registry.hostReaders
46
+ },
47
+ responseIdCounter,
48
+ streamIdCounter,
49
+ readerIdCounter
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Sets up a stream registry without Response tracking.
55
+ * Used by bridges that only handle streams/readers (like actor, readable-stream).
56
+ *
57
+ * @param ctx - Bridge context (cleanupHandlers will be populated)
58
+ * @param bridgeName - Name for logging and cleanup
59
+ * @returns Registry and counters for streams/readers
60
+ */
61
+ export function setupSimpleStreamRegistry(
62
+ ctx: BridgeContext,
63
+ bridgeName: string
64
+ ): {
65
+ registry: StreamRegistry;
66
+ streamIdCounter: { value: number };
67
+ readerIdCounter: { value: number };
68
+ } {
69
+ const registry: StreamRegistry = {
70
+ hostStreams: new Map<number, ReadableStream>(),
71
+ hostReaders: new Map<number, ReadableStreamDefaultReader>()
72
+ };
73
+
74
+ const streamIdCounter = createIdCounter();
75
+ const readerIdCounter = createIdCounter();
76
+
77
+ ctx.cleanupHandlers.push(createStreamCleanupHandler(registry, bridgeName, ctx.logger));
78
+
79
+ return {
80
+ registry,
81
+ streamIdCounter,
82
+ readerIdCounter
83
+ };
84
+ }