@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,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Response object factory for QuickJS bridges
|
|
3
|
+
* Used by fetch(), Response constructor, and bucket.get()
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { QuickJSHandle } from 'quickjs-emscripten-core';
|
|
7
|
+
import { isFail } from 'quickjs-emscripten-core';
|
|
8
|
+
import type { BridgeContext } from '../types.js';
|
|
9
|
+
import { createStreamObject } from '../readable-stream.js';
|
|
10
|
+
import { createHeadersObject } from './headers.js';
|
|
11
|
+
import { attachBodyMethods } from './body-methods.js';
|
|
12
|
+
import { nextId } from './cleanup.js';
|
|
13
|
+
import { parseJsonInContext } from './json-helpers.js';
|
|
14
|
+
import {
|
|
15
|
+
withTrackedPromiseResult,
|
|
16
|
+
withTrackedPromiseValue
|
|
17
|
+
} from './promise-helper.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for creating a Response object in the sandbox
|
|
21
|
+
*/
|
|
22
|
+
export interface ResponseObjectOptions {
|
|
23
|
+
/** The actual host Response to bridge (for fetch, Response constructor) */
|
|
24
|
+
response?: Response;
|
|
25
|
+
/** For objects without a real Response (e.g., bucket) - provide init values */
|
|
26
|
+
responseInit?: {
|
|
27
|
+
status: number;
|
|
28
|
+
statusText?: string;
|
|
29
|
+
ok: boolean;
|
|
30
|
+
url?: string;
|
|
31
|
+
type?: ResponseType;
|
|
32
|
+
redirected?: boolean;
|
|
33
|
+
headers?: Headers;
|
|
34
|
+
};
|
|
35
|
+
/** ReadableStream body (for bucket objects or custom Response) */
|
|
36
|
+
body?: ReadableStream<Uint8Array>;
|
|
37
|
+
/** Extra metadata to set on the response (e.g., bucket-specific: key, size, uploaded) */
|
|
38
|
+
metadata?: Record<string, string | number>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Registry context containing ID counters and host object maps
|
|
43
|
+
*/
|
|
44
|
+
export interface ResponseObjectContext {
|
|
45
|
+
registry: {
|
|
46
|
+
hostResponses: Map<number, Response>;
|
|
47
|
+
hostStreams: Map<number, ReadableStream>;
|
|
48
|
+
hostReaders: Map<number, ReadableStreamDefaultReader>;
|
|
49
|
+
};
|
|
50
|
+
responseIdCounter: { value: number };
|
|
51
|
+
streamIdCounter: { value: number };
|
|
52
|
+
readerIdCounter: { value: number };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Install a dynamic `bodyUsed` getter on a response handle.
|
|
57
|
+
* Falls back to a static property if defineProperty fails.
|
|
58
|
+
*/
|
|
59
|
+
function installBodyUsedGetter(
|
|
60
|
+
context: BridgeContext['context'],
|
|
61
|
+
responseHandle: QuickJSHandle,
|
|
62
|
+
getterFn: QuickJSHandle
|
|
63
|
+
): void {
|
|
64
|
+
{
|
|
65
|
+
using fn = getterFn;
|
|
66
|
+
context.setProp(responseHandle, '__getBodyUsed', fn);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const defineBodyUsed = context.evalCode(`(function(obj) {
|
|
70
|
+
var fn = obj.__getBodyUsed;
|
|
71
|
+
Object.defineProperty(obj, 'bodyUsed', {
|
|
72
|
+
get: function() { return fn(); },
|
|
73
|
+
enumerable: true,
|
|
74
|
+
configurable: true
|
|
75
|
+
});
|
|
76
|
+
delete obj.__getBodyUsed;
|
|
77
|
+
return obj;
|
|
78
|
+
})`);
|
|
79
|
+
if (isFail(defineBodyUsed)) {
|
|
80
|
+
// Fallback: set static property
|
|
81
|
+
defineBodyUsed.error.dispose();
|
|
82
|
+
context.setProp(responseHandle, 'bodyUsed', context.false);
|
|
83
|
+
} else {
|
|
84
|
+
using defineFunc = defineBodyUsed.value;
|
|
85
|
+
const applyResult = context.callFunction(defineFunc, context.undefined, responseHandle);
|
|
86
|
+
if (isFail(applyResult)) {
|
|
87
|
+
applyResult.error.dispose();
|
|
88
|
+
context.setProp(responseHandle, 'bodyUsed', context.false);
|
|
89
|
+
} else {
|
|
90
|
+
applyResult.value.dispose();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Attach body consumption methods that create a Response from the stream.
|
|
97
|
+
* Used by bucket.get() which provides a bare ReadableStream.
|
|
98
|
+
*
|
|
99
|
+
* This is different from attachBodyMethods() which uses an existing Response object.
|
|
100
|
+
* Refactored to use withTrackedPromise* helpers to eliminate manual deferred boilerplate.
|
|
101
|
+
*/
|
|
102
|
+
export function attachStreamBodyMethods(
|
|
103
|
+
ctx: BridgeContext,
|
|
104
|
+
responseHandle: QuickJSHandle,
|
|
105
|
+
bodyStream: ReadableStream<Uint8Array>
|
|
106
|
+
): void {
|
|
107
|
+
const { context, tracker } = ctx;
|
|
108
|
+
|
|
109
|
+
// Track body consumption state - streams can only be consumed once
|
|
110
|
+
let bodyUsed = false;
|
|
111
|
+
|
|
112
|
+
// Helper to check and mark body as used, returning a host Response or null
|
|
113
|
+
const consumeBody = (): Response | null => {
|
|
114
|
+
if (bodyUsed) return null;
|
|
115
|
+
bodyUsed = true;
|
|
116
|
+
return new Response(bodyStream);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Helper to reject with "already consumed" error
|
|
120
|
+
const rejectConsumed = (): QuickJSHandle => {
|
|
121
|
+
const deferred = context.newPromise();
|
|
122
|
+
tracker.deferredPromises.add(deferred);
|
|
123
|
+
using errHandle = context.newString('Body has already been consumed');
|
|
124
|
+
deferred.reject(errHandle);
|
|
125
|
+
tracker.deferredPromises.delete(deferred);
|
|
126
|
+
return deferred.handle;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Expose bodyUsed as a dynamic getter
|
|
130
|
+
installBodyUsedGetter(
|
|
131
|
+
context,
|
|
132
|
+
responseHandle,
|
|
133
|
+
context.newFunction('__getBodyUsed', () => bodyUsed ? context.true : context.false)
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// text() - consumes the body stream
|
|
137
|
+
{
|
|
138
|
+
using textMethod = context.newFunction('text', () => {
|
|
139
|
+
const response = consumeBody();
|
|
140
|
+
if (!response) return rejectConsumed();
|
|
141
|
+
return withTrackedPromiseValue(ctx, () => response.text());
|
|
142
|
+
});
|
|
143
|
+
context.setProp(responseHandle, 'text', textMethod);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// json() - consumes the body stream
|
|
147
|
+
{
|
|
148
|
+
using jsonMethod = context.newFunction('json', () => {
|
|
149
|
+
const response = consumeBody();
|
|
150
|
+
if (!response) return rejectConsumed();
|
|
151
|
+
return withTrackedPromiseResult(ctx, async () => {
|
|
152
|
+
const data = await response.json();
|
|
153
|
+
return parseJsonInContext(context, JSON.stringify(data));
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
context.setProp(responseHandle, 'json', jsonMethod);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// arrayBuffer() - consumes the body stream
|
|
160
|
+
{
|
|
161
|
+
using arrayBufferMethod = context.newFunction('arrayBuffer', () => {
|
|
162
|
+
const response = consumeBody();
|
|
163
|
+
if (!response) return rejectConsumed();
|
|
164
|
+
return withTrackedPromiseResult(ctx, async () => {
|
|
165
|
+
const buffer = await response.arrayBuffer();
|
|
166
|
+
return context.newArrayBuffer(buffer);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
context.setProp(responseHandle, 'arrayBuffer', arrayBufferMethod);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Creates a bridged Response object in QuickJS.
|
|
175
|
+
* Unified factory used by fetch, Response constructor, and bucket.get().
|
|
176
|
+
*/
|
|
177
|
+
export function createResponseObject(
|
|
178
|
+
ctx: BridgeContext,
|
|
179
|
+
opts: ResponseObjectOptions,
|
|
180
|
+
objCtx: ResponseObjectContext
|
|
181
|
+
): QuickJSHandle {
|
|
182
|
+
const { context } = ctx;
|
|
183
|
+
const { registry, responseIdCounter, streamIdCounter, readerIdCounter } = objCtx;
|
|
184
|
+
|
|
185
|
+
const responseHandle = context.newObject();
|
|
186
|
+
|
|
187
|
+
// Determine source - real Response or init object
|
|
188
|
+
const response = opts.response;
|
|
189
|
+
const init = opts.responseInit ?? {
|
|
190
|
+
status: response?.status ?? 200,
|
|
191
|
+
statusText: response?.statusText ?? '',
|
|
192
|
+
ok: response?.ok ?? true,
|
|
193
|
+
url: response?.url ?? '',
|
|
194
|
+
type: response?.type ?? 'default',
|
|
195
|
+
redirected: response?.redirected ?? false,
|
|
196
|
+
headers: response?.headers
|
|
197
|
+
};
|
|
198
|
+
const bodyStream = opts.body ?? response?.body ?? null;
|
|
199
|
+
|
|
200
|
+
// Set standard Response properties
|
|
201
|
+
{
|
|
202
|
+
using statusHandle = context.newNumber(init.status);
|
|
203
|
+
context.setProp(responseHandle, 'status', statusHandle);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
{
|
|
207
|
+
using statusTextHandle = context.newString(init.statusText || '');
|
|
208
|
+
context.setProp(responseHandle, 'statusText', statusTextHandle);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
context.setProp(responseHandle, 'ok', init.ok ? context.true : context.false);
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
using urlHandle = context.newString(init.url || '');
|
|
215
|
+
context.setProp(responseHandle, 'url', urlHandle);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
{
|
|
219
|
+
using typeHandle = context.newString(init.type || 'default');
|
|
220
|
+
context.setProp(responseHandle, 'type', typeHandle);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
context.setProp(responseHandle, 'redirected', init.redirected ? context.true : context.false);
|
|
224
|
+
|
|
225
|
+
// Extra metadata (for bucket responses: key, size, uploaded)
|
|
226
|
+
if (opts.metadata) {
|
|
227
|
+
for (const [key, value] of Object.entries(opts.metadata)) {
|
|
228
|
+
using handle = typeof value === 'number'
|
|
229
|
+
? context.newNumber(value)
|
|
230
|
+
: context.newString(String(value));
|
|
231
|
+
context.setProp(responseHandle, key, handle);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Bridge body stream
|
|
236
|
+
if (bodyStream) {
|
|
237
|
+
const streamId = nextId(streamIdCounter);
|
|
238
|
+
registry.hostStreams.set(streamId, bodyStream);
|
|
239
|
+
using streamHandle = createStreamObject(
|
|
240
|
+
ctx,
|
|
241
|
+
bodyStream,
|
|
242
|
+
streamId,
|
|
243
|
+
registry.hostReaders,
|
|
244
|
+
readerIdCounter
|
|
245
|
+
);
|
|
246
|
+
context.setProp(responseHandle, 'body', streamHandle);
|
|
247
|
+
} else {
|
|
248
|
+
context.setProp(responseHandle, 'body', context.null);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Headers
|
|
252
|
+
{
|
|
253
|
+
using headersHandle = createHeadersObject(ctx, init.headers);
|
|
254
|
+
context.setProp(responseHandle, 'headers', headersHandle);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Body methods - attach based on what we have
|
|
258
|
+
if (response) {
|
|
259
|
+
// We have a real Response - use the original body methods
|
|
260
|
+
const responseId = nextId(responseIdCounter);
|
|
261
|
+
registry.hostResponses.set(responseId, response);
|
|
262
|
+
attachBodyMethods(ctx, responseHandle, responseId, registry.hostResponses);
|
|
263
|
+
|
|
264
|
+
// Expose bodyUsed as a dynamic getter backed by the host Response
|
|
265
|
+
installBodyUsedGetter(
|
|
266
|
+
context,
|
|
267
|
+
responseHandle,
|
|
268
|
+
context.newFunction('__getBodyUsed', () => response.bodyUsed ? context.true : context.false)
|
|
269
|
+
);
|
|
270
|
+
} else if (bodyStream) {
|
|
271
|
+
// We have a bare stream (bucket case) - create Response wrappers
|
|
272
|
+
// (attachStreamBodyMethods sets up bodyUsed getter internally)
|
|
273
|
+
attachStreamBodyMethods(ctx, responseHandle, bodyStream);
|
|
274
|
+
} else {
|
|
275
|
+
// No body - bodyUsed is always true (no body to consume)
|
|
276
|
+
context.setProp(responseHandle, 'bodyUsed', context.true);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return responseHandle;
|
|
280
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result object builder utilities for QuickJS bridges
|
|
3
|
+
*
|
|
4
|
+
* Provides helpers for building common result objects like:
|
|
5
|
+
* - List results with files arrays and counts
|
|
6
|
+
* - Success/result objects
|
|
7
|
+
* - Response metadata objects
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { QuickJSContext, QuickJSHandle } from 'quickjs-emscripten-core';
|
|
11
|
+
import { Scope } from 'quickjs-emscripten-core';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* File entry for list results
|
|
15
|
+
*/
|
|
16
|
+
export interface FileEntry {
|
|
17
|
+
key: string;
|
|
18
|
+
size?: number;
|
|
19
|
+
uploaded?: Date;
|
|
20
|
+
expiration?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build a list result object with files array and count
|
|
25
|
+
*
|
|
26
|
+
* @param context - QuickJS context
|
|
27
|
+
* @param mountName - Name of the mount for prefixing keys
|
|
28
|
+
* @param files - Array of file entries
|
|
29
|
+
* @param additionalProps - Additional properties to add to result (e.g., truncated)
|
|
30
|
+
* @returns QuickJS handle to the result object
|
|
31
|
+
*/
|
|
32
|
+
export function createListResult(
|
|
33
|
+
context: QuickJSContext,
|
|
34
|
+
mountName: string,
|
|
35
|
+
files: FileEntry[],
|
|
36
|
+
additionalProps?: Record<string, QuickJSHandle>
|
|
37
|
+
): QuickJSHandle {
|
|
38
|
+
const resultHandle = context.newObject();
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
using filesArray = context.newArray();
|
|
42
|
+
|
|
43
|
+
files.forEach((file, index) => {
|
|
44
|
+
// Scope per iteration — auto-disposes fileHandle and all property handles
|
|
45
|
+
Scope.withScope((scope) => {
|
|
46
|
+
const fileHandle = scope.manage(context.newObject());
|
|
47
|
+
|
|
48
|
+
const keyHandle = scope.manage(context.newString(`/${mountName}/${file.key}`));
|
|
49
|
+
context.setProp(fileHandle, 'key', keyHandle);
|
|
50
|
+
|
|
51
|
+
if (file.size !== undefined) {
|
|
52
|
+
const sizeHandle = scope.manage(context.newNumber(file.size));
|
|
53
|
+
context.setProp(fileHandle, 'size', sizeHandle);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (file.uploaded) {
|
|
57
|
+
const uploadedHandle = scope.manage(context.newString(file.uploaded.toISOString()));
|
|
58
|
+
context.setProp(fileHandle, 'uploaded', uploadedHandle);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (file.expiration !== undefined) {
|
|
62
|
+
const expHandle = scope.manage(context.newNumber(file.expiration));
|
|
63
|
+
context.setProp(fileHandle, 'expiration', expHandle);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
context.setProp(filesArray, index, fileHandle);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
context.setProp(resultHandle, 'files', filesArray);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
using countHandle = context.newNumber(files.length);
|
|
75
|
+
context.setProp(resultHandle, 'count', countHandle);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add any additional properties
|
|
79
|
+
if (additionalProps) {
|
|
80
|
+
for (const [key, valueHandle] of Object.entries(additionalProps)) {
|
|
81
|
+
context.setProp(resultHandle, key, valueHandle);
|
|
82
|
+
// Don't dispose - caller retains ownership
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return resultHandle;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build a success result object
|
|
91
|
+
*/
|
|
92
|
+
export function createSuccessResult(context: QuickJSContext): QuickJSHandle {
|
|
93
|
+
const resultHandle = context.newObject();
|
|
94
|
+
context.setProp(resultHandle, 'success', context.true);
|
|
95
|
+
return resultHandle;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a success result with a key property
|
|
100
|
+
*/
|
|
101
|
+
export function createSuccessResultWithKey(
|
|
102
|
+
context: QuickJSContext,
|
|
103
|
+
key: string
|
|
104
|
+
): QuickJSHandle {
|
|
105
|
+
const resultHandle = context.newObject();
|
|
106
|
+
{
|
|
107
|
+
using keyHandle = context.newString(key);
|
|
108
|
+
context.setProp(resultHandle, 'key', keyHandle);
|
|
109
|
+
}
|
|
110
|
+
context.setProp(resultHandle, 'success', context.true);
|
|
111
|
+
return resultHandle;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build a success result with custom metadata
|
|
116
|
+
*/
|
|
117
|
+
export function createSuccessResultWithMetadata(
|
|
118
|
+
context: QuickJSContext,
|
|
119
|
+
metadata: Record<string, string | number>
|
|
120
|
+
): QuickJSHandle {
|
|
121
|
+
const resultHandle = context.newObject();
|
|
122
|
+
|
|
123
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
124
|
+
using handle = typeof value === 'number' ? context.newNumber(value) : context.newString(value);
|
|
125
|
+
context.setProp(resultHandle, key, handle);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
context.setProp(resultHandle, 'success', context.true);
|
|
129
|
+
return resultHandle;
|
|
130
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope helpers and result-unwrapping utilities for QuickJS bridges.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - unwrapResult: Disposes error and throws (or returns value) from VmCallResult/evalCode
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { QuickJSContext, QuickJSHandle, VmCallResult } from 'quickjs-emscripten-core';
|
|
9
|
+
import { isFail } from 'quickjs-emscripten-core';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Safely stringify a value, handling circular references and other edge cases.
|
|
13
|
+
*/
|
|
14
|
+
function safeStringify(value: unknown): string {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
} catch {
|
|
18
|
+
return String(value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Unwrap a VmCallResult (from evalCode or callFunction).
|
|
24
|
+
* On success: returns the value handle (caller must dispose).
|
|
25
|
+
* On failure: disposes the error handle and throws an Error.
|
|
26
|
+
*/
|
|
27
|
+
export function unwrapResult(result: VmCallResult<QuickJSHandle>, context?: QuickJSContext): QuickJSHandle {
|
|
28
|
+
if (isFail(result)) {
|
|
29
|
+
let message: string;
|
|
30
|
+
if (context) {
|
|
31
|
+
try {
|
|
32
|
+
const error = context.dump(result.error);
|
|
33
|
+
message = typeof error === 'string' ? error : safeStringify(error);
|
|
34
|
+
} catch {
|
|
35
|
+
message = 'Unknown error';
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
message = 'QuickJS evaluation failed';
|
|
39
|
+
}
|
|
40
|
+
result.error.dispose();
|
|
41
|
+
throw new Error(message);
|
|
42
|
+
}
|
|
43
|
+
return result.value;
|
|
44
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream reader factory for QuickJS bridges
|
|
3
|
+
*
|
|
4
|
+
* Creates reader objects with read(), cancel(), and releaseLock() methods.
|
|
5
|
+
* Eliminates ~100 lines of duplicated reader creation code across actor and readable-stream bridges.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { QuickJSHandle } from 'quickjs-emscripten-core';
|
|
9
|
+
import type { BridgeContext } from '../types.js';
|
|
10
|
+
import {
|
|
11
|
+
withTrackedPromiseResult,
|
|
12
|
+
withTrackedPromiseValue
|
|
13
|
+
} from './promise-helper.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a reader handle with standard reader methods
|
|
17
|
+
*
|
|
18
|
+
* @param ctx - Bridge context
|
|
19
|
+
* @param reader - The host reader
|
|
20
|
+
* @param onRelease - Optional callback when releaseLock is called
|
|
21
|
+
* @returns QuickJS handle to the reader object
|
|
22
|
+
*/
|
|
23
|
+
export function createReaderHandle(
|
|
24
|
+
ctx: BridgeContext,
|
|
25
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
26
|
+
onRelease?: () => void
|
|
27
|
+
): QuickJSHandle {
|
|
28
|
+
const { context, logger } = ctx;
|
|
29
|
+
const readerHandle = context.newObject();
|
|
30
|
+
|
|
31
|
+
// Create read() method
|
|
32
|
+
{
|
|
33
|
+
using readMethod = context.newFunction('read', () => {
|
|
34
|
+
return withTrackedPromiseResult(ctx, async () => {
|
|
35
|
+
const { done, value } = await reader.read();
|
|
36
|
+
|
|
37
|
+
const resultHandle = context.newObject();
|
|
38
|
+
|
|
39
|
+
context.setProp(resultHandle, 'done', done ? context.true : context.false);
|
|
40
|
+
|
|
41
|
+
if (value) {
|
|
42
|
+
// Use slice to handle Uint8Array views into larger ArrayBuffers (e.g. Node.js Buffer)
|
|
43
|
+
const buf =
|
|
44
|
+
value.byteOffset === 0 && value.byteLength === value.buffer.byteLength
|
|
45
|
+
? value.buffer
|
|
46
|
+
: value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
|
47
|
+
using valueHandle = context.newArrayBuffer(buf);
|
|
48
|
+
context.setProp(resultHandle, 'value', valueHandle);
|
|
49
|
+
} else {
|
|
50
|
+
context.setProp(resultHandle, 'value', context.undefined);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return resultHandle;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
context.setProp(readerHandle, 'read', readMethod);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Create cancel() method
|
|
60
|
+
{
|
|
61
|
+
using cancelMethod = context.newFunction('cancel', () => {
|
|
62
|
+
return withTrackedPromiseValue(ctx, async () => {
|
|
63
|
+
await reader.cancel();
|
|
64
|
+
if (onRelease) {
|
|
65
|
+
onRelease();
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
context.setProp(readerHandle, 'cancel', cancelMethod);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create releaseLock() method
|
|
74
|
+
{
|
|
75
|
+
using releaseLockMethod = context.newFunction('releaseLock', () => {
|
|
76
|
+
try {
|
|
77
|
+
reader.releaseLock();
|
|
78
|
+
if (onRelease) {
|
|
79
|
+
onRelease();
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
logger?.warn?.(`releaseLock failed: ${e}`);
|
|
83
|
+
}
|
|
84
|
+
return context.undefined;
|
|
85
|
+
});
|
|
86
|
+
context.setProp(readerHandle, 'releaseLock', releaseLockMethod);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return readerHandle;
|
|
90
|
+
}
|