@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,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
+ }