@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.
- package/.prettierrc +9 -0
- package/CHANGELOG.md +8 -0
- package/eslint.config.mjs +28 -0
- package/package.json +53 -0
- package/src/engine/agent.ts +478 -0
- package/src/engine/llm-provider.test.ts +275 -0
- package/src/engine/llm-provider.ts +330 -0
- package/src/engine/stream-parser.ts +170 -0
- package/src/index.ts +142 -0
- package/src/mounts/mount-manager.test.ts +516 -0
- package/src/mounts/mount-manager.ts +327 -0
- package/src/mounts/mount-registry.ts +196 -0
- package/src/mounts/zod-to-string.test.ts +154 -0
- package/src/mounts/zod-to-string.ts +213 -0
- package/src/presets/agent-tools.ts +57 -0
- package/src/presets/index.ts +5 -0
- package/src/sandbox/README.md +1321 -0
- package/src/sandbox/bridges/README.md +571 -0
- package/src/sandbox/bridges/actor.test.ts +229 -0
- package/src/sandbox/bridges/actor.ts +195 -0
- package/src/sandbox/bridges/bridge-fixes.test.ts +614 -0
- package/src/sandbox/bridges/bucket.test.ts +300 -0
- package/src/sandbox/bridges/cleanup-reproduction.test.ts +225 -0
- package/src/sandbox/bridges/console-multiple.test.ts +187 -0
- package/src/sandbox/bridges/console.test.ts +157 -0
- package/src/sandbox/bridges/console.ts +122 -0
- package/src/sandbox/bridges/fetch.ts +93 -0
- package/src/sandbox/bridges/index.ts +78 -0
- package/src/sandbox/bridges/readable-stream.ts +323 -0
- package/src/sandbox/bridges/response.test.ts +154 -0
- package/src/sandbox/bridges/response.ts +123 -0
- package/src/sandbox/bridges/review-fixes.test.ts +331 -0
- package/src/sandbox/bridges/search.test.ts +475 -0
- package/src/sandbox/bridges/search.ts +264 -0
- package/src/sandbox/bridges/shared/body-methods.ts +93 -0
- package/src/sandbox/bridges/shared/cleanup.ts +112 -0
- package/src/sandbox/bridges/shared/convert.ts +76 -0
- package/src/sandbox/bridges/shared/headers.ts +181 -0
- package/src/sandbox/bridges/shared/index.ts +36 -0
- package/src/sandbox/bridges/shared/json-helpers.ts +77 -0
- package/src/sandbox/bridges/shared/path-parser.ts +109 -0
- package/src/sandbox/bridges/shared/promise-helper.ts +108 -0
- package/src/sandbox/bridges/shared/registry-setup.ts +84 -0
- package/src/sandbox/bridges/shared/response-object.ts +280 -0
- package/src/sandbox/bridges/shared/result-builder.ts +130 -0
- package/src/sandbox/bridges/shared/scope-helpers.ts +44 -0
- package/src/sandbox/bridges/shared/stream-reader.ts +90 -0
- package/src/sandbox/bridges/storage-bridge.test.ts +893 -0
- package/src/sandbox/bridges/storage.ts +421 -0
- package/src/sandbox/bridges/text-decoder.ts +190 -0
- package/src/sandbox/bridges/text-encoder.ts +102 -0
- package/src/sandbox/bridges/types.ts +39 -0
- package/src/sandbox/bridges/utils.ts +123 -0
- package/src/sandbox/index.ts +6 -0
- package/src/sandbox/quickjs-wasm.d.ts +9 -0
- package/src/sandbox/sandbox.test.ts +191 -0
- package/src/sandbox/sandbox.ts +831 -0
- package/src/sandbox/test-helper.ts +43 -0
- package/src/sandbox/test-mocks.ts +154 -0
- package/src/sandbox/user-stream.test.ts +77 -0
- package/src/skills/frontmatter.test.ts +305 -0
- package/src/skills/frontmatter.ts +200 -0
- package/src/skills/index.ts +9 -0
- package/src/skills/skills-loader.test.ts +237 -0
- package/src/skills/skills-loader.ts +200 -0
- package/src/tools/actor-storage-tools.ts +250 -0
- package/src/tools/code-tools.test.ts +199 -0
- package/src/tools/code-tools.ts +444 -0
- package/src/tools/file-tools.ts +206 -0
- package/src/tools/registry.ts +125 -0
- package/src/tools/script-tools.ts +145 -0
- package/src/tools/smartbucket-tools.ts +203 -0
- package/src/tools/sql-tools.ts +213 -0
- package/src/tools/tool-factory.ts +119 -0
- package/src/types.ts +512 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +15 -0
- 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
|
+
}
|