@boundaryml/baml-bridge-web 0.0.0 → 0.15.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/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -14
- package/dist/index.js.map +1 -1
- package/dist/native.d.ts +50 -14
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +327 -70
- package/dist/native.js.map +1 -1
- package/dist/platform.d.ts +4 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +5 -0
- package/dist/platform.js.map +1 -0
- package/dist/shared/call_context.d.ts +7 -0
- package/dist/shared/call_context.d.ts.map +1 -0
- package/dist/shared/call_context.js +11 -0
- package/dist/shared/call_context.js.map +1 -0
- package/dist/shared/define_function.d.ts.map +1 -1
- package/dist/shared/define_function.js +13 -24
- package/dist/shared/define_function.js.map +1 -1
- package/dist/shared/errors.d.ts +4 -4
- package/dist/shared/errors.d.ts.map +1 -1
- package/dist/shared/errors.js +16 -10
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/host_value_registry.d.ts +5 -3
- package/dist/shared/host_value_registry.d.ts.map +1 -1
- package/dist/shared/host_value_registry.js +10 -6
- package/dist/shared/host_value_registry.js.map +1 -1
- package/dist/shared/platform.d.ts +4 -0
- package/dist/shared/platform.d.ts.map +1 -0
- package/dist/shared/platform.js +5 -0
- package/dist/shared/platform.js.map +1 -0
- package/dist/shared/proto/baml_cffi.d.ts +35 -8
- package/dist/shared/proto/baml_cffi.js +138 -25
- package/dist/shared/proto.d.ts +9 -5
- package/dist/shared/proto.d.ts.map +1 -1
- package/dist/shared/proto.js +197 -104
- package/dist/shared/proto.js.map +1 -1
- package/dist/shared/stream.d.ts +3 -2
- package/dist/shared/stream.d.ts.map +1 -1
- package/dist/shared/stream.js +22 -15
- package/dist/shared/stream.js.map +1 -1
- package/dist/wasm/bridge_web_core.d.ts +99 -68
- package/dist/wasm/bridge_web_core.js +519 -492
- package/dist/wasm/bridge_web_core_bg.wasm +0 -0
- package/dist/wasm/bridge_web_core_bg.wasm.d.ts +41 -27
- package/dist/wasm/package.json +1 -1
- package/dist/workerd/index.d.ts +3 -3
- package/dist/workerd/index.d.ts.map +1 -1
- package/dist/workerd/index.js +21 -14
- package/dist/workerd/index.js.map +1 -1
- package/dist/workerd/native.d.ts +50 -14
- package/dist/workerd/native.d.ts.map +1 -1
- package/dist/workerd/native.js +327 -70
- package/dist/workerd/shared/call_context.d.ts +7 -0
- package/dist/workerd/shared/call_context.d.ts.map +1 -0
- package/dist/workerd/shared/call_context.js +11 -0
- package/dist/workerd/shared/call_context.js.map +1 -0
- package/dist/workerd/shared/define_function.d.ts.map +1 -1
- package/dist/workerd/shared/define_function.js +13 -24
- package/dist/workerd/shared/define_function.js.map +1 -1
- package/dist/workerd/shared/errors.d.ts +4 -4
- package/dist/workerd/shared/errors.d.ts.map +1 -1
- package/dist/workerd/shared/errors.js +16 -10
- package/dist/workerd/shared/errors.js.map +1 -1
- package/dist/workerd/shared/host_value_registry.d.ts +5 -3
- package/dist/workerd/shared/host_value_registry.d.ts.map +1 -1
- package/dist/workerd/shared/host_value_registry.js +10 -6
- package/dist/workerd/shared/host_value_registry.js.map +1 -1
- package/dist/workerd/shared/platform.d.ts +4 -0
- package/dist/workerd/shared/platform.d.ts.map +1 -0
- package/dist/workerd/shared/platform.js +5 -0
- package/dist/workerd/shared/platform.js.map +1 -0
- package/dist/workerd/shared/proto/baml_cffi.d.ts +35 -8
- package/dist/workerd/shared/proto/baml_cffi.js +140 -25
- package/dist/workerd/shared/proto.d.ts +9 -5
- package/dist/workerd/shared/proto.d.ts.map +1 -1
- package/dist/workerd/shared/proto.js +197 -104
- package/dist/workerd/shared/proto.js.map +1 -1
- package/dist/workerd/shared/stream.d.ts +3 -2
- package/dist/workerd/shared/stream.d.ts.map +1 -1
- package/dist/workerd/shared/stream.js +22 -15
- package/dist/workerd/shared/stream.js.map +1 -1
- package/dist/workerd-wasm/bridge_web_core.d.ts +62 -45
- package/dist/workerd-wasm/bridge_web_core.js +3 -5
- package/dist/workerd-wasm/bridge_web_core_bg.js +519 -493
- package/dist/workerd-wasm/bridge_web_core_bg.wasm +0 -0
- package/dist/workerd-wasm/bridge_web_core_bg.wasm.d.ts +41 -27
- package/dist/workerd-wasm/package.json +1 -1
- package/package.json +22 -5
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// Decodes the BamlOutboundResult envelope → TS objects (call results), and
|
|
5
5
|
// bare BamlOutboundValue bytes → TS objects (host-callable args).
|
|
6
6
|
import { baml_bridge } from './proto/baml_cffi.js';
|
|
7
|
-
import { BamlHandle, BamlImage, BamlAudio, BamlVideo, BamlPdf, registerHostCallable, releaseHostCallable, completeHostCall, } from '../native.js';
|
|
7
|
+
import { BamlHandle, BamlImage, BamlAudio, BamlVideo, BamlPdf, registerHostCallable, releaseHostCallable, completeHostCall, getRuntime, newFunctionCall, } from '../native.js';
|
|
8
8
|
import { BamlStream } from './stream.js';
|
|
9
|
-
import { BamlAbortError, BamlCancelledError, BamlError, BamlPanic } from './errors.js';
|
|
10
|
-
import {
|
|
9
|
+
import { BamlAbortError, BamlCancelledError, BamlClientError, BamlError, BamlInvalidArgumentError, BamlPanic } from './errors.js';
|
|
10
|
+
import { handleExitPanic } from './platform.js';
|
|
11
|
+
import { registerHostOpaque, releaseHostOpaque, tryRehydrateHostValueByKey, } from './host_value_registry.js';
|
|
11
12
|
import { getTypeMap } from './typemap.js';
|
|
12
13
|
import { lowerTypeToWireTy, outboundTyToBamlType } from './wire_ty.js';
|
|
13
14
|
const CallFunctionArgs = baml_bridge.cffi.v1.CallFunctionArgs;
|
|
@@ -30,6 +31,16 @@ export class HostCallableSyncError extends Error {
|
|
|
30
31
|
this.name = 'HostCallableSyncError';
|
|
31
32
|
}
|
|
32
33
|
}
|
|
34
|
+
function rollbackHostCallables(keys) {
|
|
35
|
+
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
36
|
+
try {
|
|
37
|
+
releaseHostCallable(keys[index]);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Best-effort cleanup; never mask the original encode failure.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
33
44
|
/**
|
|
34
45
|
* Generic-class instances declare their TypeVar names (declaration order) in a
|
|
35
46
|
* static `$generic` field that codegen emits; this reads it back. Returns the
|
|
@@ -81,8 +92,8 @@ function setInboundValue(iv, value, ctx) {
|
|
|
81
92
|
// path. Any future dispatch-backed handle type must be guarded here.
|
|
82
93
|
if (ctx.syncMode && value.handleType === BamlHandleType.HOST_VALUE_CALLABLE) {
|
|
83
94
|
throw new HostCallableSyncError('host callables are only supported on the async call path; use the async API ' +
|
|
84
|
-
'(callFunction) instead of callFunctionSync. The sync path
|
|
85
|
-
'
|
|
95
|
+
'(callFunction) instead of callFunctionSync. The sync path occupies the single ' +
|
|
96
|
+
'JavaScript/bridge event loop, so the host callback cannot await a future turn.');
|
|
86
97
|
}
|
|
87
98
|
// The Rust inbound decoder drains handle-table entries. Send a fresh
|
|
88
99
|
// cloned key so the JS-owned handle remains valid for later calls.
|
|
@@ -113,8 +124,8 @@ function setInboundValue(iv, value, ctx) {
|
|
|
113
124
|
// tsfn, which would otherwise be orphaned).
|
|
114
125
|
if (ctx.syncMode) {
|
|
115
126
|
throw new HostCallableSyncError('host callables are only supported on the async call path; use the async API ' +
|
|
116
|
-
'(callFunction) instead of callFunctionSync. The sync path
|
|
117
|
-
'
|
|
127
|
+
'(callFunction) instead of callFunctionSync. The sync path occupies the single ' +
|
|
128
|
+
'JavaScript/bridge event loop, so the host callback cannot await a future turn.');
|
|
118
129
|
}
|
|
119
130
|
// JS callable → register a dispatch wrapper in the host-value
|
|
120
131
|
// registry and emit `Handle{key, HOST_VALUE_CALLABLE}`. The Rust
|
|
@@ -136,13 +147,12 @@ function setInboundValue(iv, value, ctx) {
|
|
|
136
147
|
iv.listValue = { values: listVal };
|
|
137
148
|
}
|
|
138
149
|
else if (value !== null && typeof value === 'object') {
|
|
139
|
-
// Any remaining object
|
|
140
|
-
// instance
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
// `Object.assign(this, init)`. The specific built-in wrappers
|
|
150
|
+
// Any remaining object is either a plain object or a codegen-emitted
|
|
151
|
+
// class instance. Plain objects encode as `map_value` and are reshaped
|
|
152
|
+
// against contextual types; generated instances carry a sparse nominal
|
|
153
|
+
// annotation when the typemap identifies their constructor.
|
|
154
|
+
// `Object.entries` yields the class's own enumerable fields, set by the
|
|
155
|
+
// constructor's `Object.assign(this, init)`. The specific built-in wrappers
|
|
146
156
|
// (BamlHandle/BamlStream/media) are handled by the instanceof
|
|
147
157
|
// branches above, so they never reach here.
|
|
148
158
|
//
|
|
@@ -173,30 +183,27 @@ function setInboundValue(iv, value, ctx) {
|
|
|
173
183
|
setInboundValue(childVal, v, ctx);
|
|
174
184
|
classFields.push({ stringKey: k, value: childVal });
|
|
175
185
|
}
|
|
176
|
-
iv.
|
|
186
|
+
iv.valueType = { classTy: { name: fqn } };
|
|
187
|
+
iv.classValue = { fields: classFields };
|
|
177
188
|
return;
|
|
178
189
|
}
|
|
179
190
|
}
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// an absent binding lowers to the unknown/top type. Mirrors
|
|
188
|
-
// bridge_python's pydantic-generic-metadata path in proto.py, which sets
|
|
189
|
-
// `class_value.class_ty` for a generic instance.
|
|
191
|
+
// A codegen class instance has nominal identity even though its payload
|
|
192
|
+
// is structurally object-shaped. Preserve that identity with a sparse
|
|
193
|
+
// node annotation. For a generic class, include exact args when every
|
|
194
|
+
// `$types` entry is present; otherwise send only the class name. The
|
|
195
|
+
// engine may refine that nominal hint from one contextual class but
|
|
196
|
+
// will not use it to choose between two concrete instantiations of the
|
|
197
|
+
// same generic class in a union.
|
|
190
198
|
if (isClassInstance) {
|
|
199
|
+
const fqn = getTypeMap().jsTypeToBamlType(value.constructor);
|
|
191
200
|
const params = genericParamNames(value);
|
|
192
|
-
if (
|
|
193
|
-
const fqn = getTypeMap().jsTypeToBamlType(value.constructor);
|
|
201
|
+
if (fqn) {
|
|
194
202
|
const userTypes = value.$types;
|
|
195
|
-
const typeArgs = params.map((p) => lowerTypeToWireTy(userTypes?.[p]));
|
|
196
203
|
const classFields = [];
|
|
197
204
|
for (const [k, v] of Object.entries(value)) {
|
|
198
205
|
// Skip method bindings (behavior, not state) and the synthetic
|
|
199
|
-
// `$types` carrier (it rides `
|
|
206
|
+
// `$types` carrier (it rides `value_type`, not the field list).
|
|
200
207
|
if (typeof v === 'function')
|
|
201
208
|
continue;
|
|
202
209
|
if (k === '$types')
|
|
@@ -205,7 +212,14 @@ function setInboundValue(iv, value, ctx) {
|
|
|
205
212
|
setInboundValue(childVal, v, ctx);
|
|
206
213
|
classFields.push({ stringKey: k, value: childVal });
|
|
207
214
|
}
|
|
208
|
-
|
|
215
|
+
if (params && userTypes !== undefined && params.every((p) => userTypes[p] !== undefined)) {
|
|
216
|
+
const typeArgs = params.map((p) => lowerTypeToWireTy(userTypes[p]));
|
|
217
|
+
iv.valueType = { classTy: { name: fqn, typeArgs } };
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
iv.valueType = { classTy: { name: fqn, typeArgs: [] } };
|
|
221
|
+
}
|
|
222
|
+
iv.classValue = { fields: classFields };
|
|
209
223
|
return;
|
|
210
224
|
}
|
|
211
225
|
}
|
|
@@ -236,8 +250,8 @@ function setInboundValue(iv, value, ctx) {
|
|
|
236
250
|
* host-value table and is normally released only when the engine GCs the
|
|
237
251
|
* `HostClosure` it allocated and fires the C release callback (a GC-timed
|
|
238
252
|
* release, drained by the engine after collection).
|
|
239
|
-
* Because the
|
|
240
|
-
* ref, so a *leaked* registry entry can also keep the
|
|
253
|
+
* Because the host bridge is built with `weak::<false>` it keeps a strong host event loop
|
|
254
|
+
* ref, so a *leaked* registry entry can also keep the JavaScript realm from
|
|
241
255
|
* exiting — which is exactly why the encode-error rollback below matters: if a
|
|
242
256
|
* later kwarg fails, the engine never sees (and so never releases) the keys we
|
|
243
257
|
* already registered, so we release them here.
|
|
@@ -247,6 +261,9 @@ export function encodeCallArgs(kwargs, options) {
|
|
|
247
261
|
if (callId === 0n) {
|
|
248
262
|
throw new TypeError('callId must be a nonzero uint64');
|
|
249
263
|
}
|
|
264
|
+
if (options.functionName !== undefined && options.functionHandle !== undefined) {
|
|
265
|
+
throw new TypeError('exactly one BAML call target may be set');
|
|
266
|
+
}
|
|
250
267
|
const ctx = { syncMode: options.syncMode ?? false, registered: [] };
|
|
251
268
|
try {
|
|
252
269
|
const entries = [];
|
|
@@ -265,22 +282,17 @@ export function encodeCallArgs(kwargs, options) {
|
|
|
265
282
|
kwargs: entries,
|
|
266
283
|
callId: callId.toString(),
|
|
267
284
|
typeArgs,
|
|
285
|
+
functionName: options.functionName,
|
|
286
|
+
functionHandle: options.functionHandle,
|
|
268
287
|
});
|
|
269
288
|
return Uint8Array.from(CallFunctionArgs.encode(msg).finish());
|
|
270
289
|
}
|
|
271
290
|
catch (err) {
|
|
272
291
|
// Roll back any host callables registered before the failure so
|
|
273
|
-
// they don't leak in the registry (and pin the
|
|
274
|
-
// life of the
|
|
292
|
+
// they don't leak in the registry (and pin the host event loop loop) for the
|
|
293
|
+
// life of the runtime — the call never reaches the engine, so the
|
|
275
294
|
// engine would never release them.
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
releaseHostCallable(k);
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
// Best-effort cleanup; never mask the original error.
|
|
282
|
-
}
|
|
283
|
-
}
|
|
295
|
+
rollbackHostCallables(ctx.registered);
|
|
284
296
|
throw err;
|
|
285
297
|
}
|
|
286
298
|
}
|
|
@@ -376,6 +388,9 @@ function decodeValueHolder(holder, typeMap) {
|
|
|
376
388
|
throw new BamlError('decoded handle has HANDLE_UNSPECIFIED handle_type');
|
|
377
389
|
}
|
|
378
390
|
const handle = new BamlHandle(holder.handleValue.key, ht);
|
|
391
|
+
if (ht === BamlHandleType.FUNCTION_REF) {
|
|
392
|
+
return decodeBamlClosure(handle, holder.handleValue.ty?.function);
|
|
393
|
+
}
|
|
379
394
|
if (ht === BamlHandleType.ADT_MEDIA_IMAGE)
|
|
380
395
|
return BamlImage._fromHandle(handle);
|
|
381
396
|
if (ht === BamlHandleType.ADT_MEDIA_AUDIO)
|
|
@@ -387,14 +402,14 @@ function decodeValueHolder(holder, typeMap) {
|
|
|
387
402
|
if (ht === BamlHandleType.ADT_TAGGED_HEAP_HANDLE) {
|
|
388
403
|
// Dispatch via the typemap: every tagged-heap class self-registers
|
|
389
404
|
// under its engine FQN (codegen emits the entry, e.g.
|
|
390
|
-
// `
|
|
405
|
+
// `ai.stream.Stream → BamlStream`), so any class is reachable without
|
|
391
406
|
// special-casing here. Mirrors bridge_python's `_decode_handle`
|
|
392
407
|
// ADT_TAGGED_HEAP_HANDLE arm (sdks/python/.../proto.py).
|
|
393
408
|
// The handle's `ty` is a full `BamlTy`; the typed-wrapper FQN lives
|
|
394
409
|
// on its class variant (a non-class `ty` reads back as `''`).
|
|
395
410
|
const fqn = holder.handleValue.ty?.classTy?.name ?? '';
|
|
396
411
|
const Cls = typeMap.getClass(fqn);
|
|
397
|
-
return Cls._fromHandle(handle);
|
|
412
|
+
return Cls._fromHandle(handle, fqn);
|
|
398
413
|
}
|
|
399
414
|
// ADT_MEDIA_GENERIC has no typed wrapper — stays a bare BamlHandle.
|
|
400
415
|
return handle;
|
|
@@ -411,6 +426,45 @@ function decodeValueHolder(holder, typeMap) {
|
|
|
411
426
|
// null BAML result.
|
|
412
427
|
return null;
|
|
413
428
|
}
|
|
429
|
+
function decodeBamlClosure(handle, functionTy) {
|
|
430
|
+
const params = functionTy?.params ?? [];
|
|
431
|
+
const required = params.filter((param) => param.mode !== baml_bridge.cffi.v1.BamlTyFunctionParamMode.BAML_TY_FUNCTION_PARAM_MODE_OPTIONAL);
|
|
432
|
+
const optional = params.filter((param) => param.mode === baml_bridge.cffi.v1.BamlTyFunctionParamMode.BAML_TY_FUNCTION_PARAM_MODE_OPTIONAL);
|
|
433
|
+
const requiredNames = required.map((param, index) => param.name ?? `arg${index}`);
|
|
434
|
+
const optionalNames = new Set(optional.map((param, index) => param.name ?? `arg${required.length + index}`));
|
|
435
|
+
return (...args) => {
|
|
436
|
+
if (args.length > requiredNames.length + 1) {
|
|
437
|
+
throw new TypeError(`got ${args.length} arguments but this BAML closure accepts ` +
|
|
438
|
+
`${requiredNames.length} required arguments and one optional-arguments object`);
|
|
439
|
+
}
|
|
440
|
+
const kwargs = {};
|
|
441
|
+
const positionalCount = Math.min(args.length, requiredNames.length);
|
|
442
|
+
for (let index = 0; index < positionalCount; index += 1) {
|
|
443
|
+
kwargs[requiredNames[index]] = args[index];
|
|
444
|
+
}
|
|
445
|
+
if (args.length > requiredNames.length) {
|
|
446
|
+
const options = args[requiredNames.length];
|
|
447
|
+
if (options === null || Array.isArray(options) || typeof options !== 'object') {
|
|
448
|
+
throw new TypeError('optional BAML closure arguments must be passed as an object');
|
|
449
|
+
}
|
|
450
|
+
for (const [name, value] of Object.entries(options)) {
|
|
451
|
+
if (!optionalNames.has(name)) {
|
|
452
|
+
throw new TypeError(`unknown optional argument ${JSON.stringify(name)}`);
|
|
453
|
+
}
|
|
454
|
+
if (value !== undefined)
|
|
455
|
+
kwargs[name] = value;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const runtime = getRuntime();
|
|
459
|
+
const callId = BigInt(newFunctionCall());
|
|
460
|
+
const encodedArgs = encodeCallArgs(kwargs, {
|
|
461
|
+
syncMode: true,
|
|
462
|
+
callId,
|
|
463
|
+
functionHandle: handle.key,
|
|
464
|
+
});
|
|
465
|
+
return decodeCallResult(runtime.callFunctionSync(encodedArgs));
|
|
466
|
+
};
|
|
467
|
+
}
|
|
414
468
|
/**
|
|
415
469
|
* Decode a `class_value` to a typed instance via the typemap. When the FQN is
|
|
416
470
|
* in the typemap (the generated-SDK path), construct `new Cls(fieldDict)`
|
|
@@ -448,7 +502,7 @@ function decodeClass(classValue, typeMap) {
|
|
|
448
502
|
// Repopulate a generic instance's `$types` from the wire's positional
|
|
449
503
|
// `type_args` (the value-level type channel), keyed by the class's
|
|
450
504
|
// static `$generic` param names — the decode-side mirror of the
|
|
451
|
-
// encoder's `
|
|
505
|
+
// encoder's sparse `value_type`. Defined non-enumerable so it neither perturbs
|
|
452
506
|
// structural equality (`toEqual`) nor re-encodes as a data field,
|
|
453
507
|
// while still being readable by a later generic instance-method call.
|
|
454
508
|
const params = Array.isArray(Ctor.$generic) ? Ctor.$generic : null;
|
|
@@ -602,13 +656,25 @@ function cancellationAbortError(message) {
|
|
|
602
656
|
reason: new BamlCancelledError(message),
|
|
603
657
|
});
|
|
604
658
|
}
|
|
659
|
+
function errorForClassName(formatted, detail) {
|
|
660
|
+
switch (detail.className) {
|
|
661
|
+
case 'baml.errors.InvalidArgument':
|
|
662
|
+
return new BamlInvalidArgumentError(formatted, detail);
|
|
663
|
+
case 'baml.errors.GenericSdkError':
|
|
664
|
+
case 'baml.errors.CompilationError':
|
|
665
|
+
case 'baml.errors.AccessError':
|
|
666
|
+
return new BamlClientError(formatted, detail);
|
|
667
|
+
default:
|
|
668
|
+
return new BamlError(formatted, detail);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
605
671
|
/**
|
|
606
672
|
* Decode a `BamlOutboundResult` envelope (the engine's call-result wire shape
|
|
607
673
|
* after 31c/31e). The `ok` arm returns the decoded value; the `error`/`panic`
|
|
608
674
|
* arms **throw** a `BamlError`/`BamlPanic` carrying the fully decoded thrown
|
|
609
675
|
* value (`.value`), the BAML trace (`.bamlTrace`), and the class FQN
|
|
610
676
|
* (`.className`), with a readable formatted `.message`. An `is_exit_panic`
|
|
611
|
-
* (clean `baml.sys.exit`) terminates the
|
|
677
|
+
* (clean `baml.sys.exit`) terminates the runtime via `runtime.exit(code)`
|
|
612
678
|
* rather than throwing.
|
|
613
679
|
*/
|
|
614
680
|
export function decodeCallResult(data) {
|
|
@@ -619,10 +685,10 @@ export function decodeCallResult(data) {
|
|
|
619
685
|
const { value, className, message } = decodeThrown(result.error?.value);
|
|
620
686
|
const trace = result.error?.trace ?? [];
|
|
621
687
|
// Same-host rehydration: a `baml.errors.HostCallable` carrying a
|
|
622
|
-
// `_handle` that still resolves in this
|
|
688
|
+
// `_handle` that still resolves in this runtime's host-value
|
|
623
689
|
// registry re-throws the *original* JS error object the bridge
|
|
624
690
|
// registered on the inbound throw — preserving `raised === caught`
|
|
625
|
-
// identity. Foreign runtimes (a different
|
|
691
|
+
// identity. Foreign runtimes (a different JavaScript realm, the
|
|
626
692
|
// Python bridge) and released keys fall through to the
|
|
627
693
|
// metadata-bearing `BamlError(HostCallable)` wrapper below.
|
|
628
694
|
if (className === 'baml.errors.HostCallable' && value !== null && typeof value === 'object') {
|
|
@@ -636,24 +702,21 @@ export function decodeCallResult(data) {
|
|
|
636
702
|
if (className === CANCELLED_PANIC_CLASS) {
|
|
637
703
|
throw cancellationAbortError(formatted);
|
|
638
704
|
}
|
|
639
|
-
throw
|
|
705
|
+
throw errorForClassName(formatted, { value, bamlTrace: trace, className });
|
|
640
706
|
}
|
|
641
707
|
case 'panic': {
|
|
642
708
|
const panic = result.panic;
|
|
643
|
-
if (panic?.isExitPanic) {
|
|
644
|
-
// Clean process-exit panic: exit after flushing telemetry (the
|
|
645
|
-
// registered `process.once('exit', flushEvents)` hook fires
|
|
646
|
-
// synchronously inside process.exit), rather than throwing.
|
|
647
|
-
const code = Number(panic.exitCode ?? 0);
|
|
648
|
-
process.exit(code);
|
|
649
|
-
}
|
|
650
709
|
const { value, className, message } = decodeThrown(panic?.value);
|
|
651
710
|
const trace = panic?.trace ?? [];
|
|
652
711
|
const formatted = formatThrownMessage('panic', className ?? '', message, trace);
|
|
653
712
|
if (className === CANCELLED_PANIC_CLASS) {
|
|
654
713
|
throw cancellationAbortError(formatted);
|
|
655
714
|
}
|
|
656
|
-
|
|
715
|
+
const fallback = new BamlPanic(formatted, { value, bamlTrace: trace, className });
|
|
716
|
+
if (panic?.isExitPanic) {
|
|
717
|
+
handleExitPanic(Number(panic.exitCode ?? 0), fallback);
|
|
718
|
+
}
|
|
719
|
+
throw fallback;
|
|
657
720
|
}
|
|
658
721
|
case 'ok':
|
|
659
722
|
default:
|
|
@@ -665,7 +728,7 @@ export function decodeCallResult(data) {
|
|
|
665
728
|
//
|
|
666
729
|
// When BAML invokes a `HostValue` registered via `registerHostCallable`, the
|
|
667
730
|
// engine fires the C `HostDispatchFn`, which schedules the per-callable
|
|
668
|
-
// `ThreadsafeFunction` (built from the wrapper below) onto the
|
|
731
|
+
// `ThreadsafeFunction` (built from the wrapper below) onto the host event loop event
|
|
669
732
|
// loop with `(callId, argsBytes)`. The wrapper decodes args, invokes the
|
|
670
733
|
// user function, encodes the result (or error), and forwards the result
|
|
671
734
|
// back to the engine via `completeHostCall`.
|
|
@@ -675,7 +738,7 @@ export function decodeCallResult(data) {
|
|
|
675
738
|
// side calls into `_decode_value_holder` / `_set_inbound_value` directly
|
|
676
739
|
// inside the Rust dispatch callback (under the GIL) rather than going
|
|
677
740
|
// through a JS-side wrapper. Node's tsfn model makes the wrapper natural.
|
|
678
|
-
function makeHostCallableDispatch(userFn) {
|
|
741
|
+
export function makeHostCallableDispatch(userFn) {
|
|
679
742
|
return (callId, argsBytes) => {
|
|
680
743
|
// Every reachable exit from this wrapper must complete `callId`
|
|
681
744
|
// exactly once — if it doesn't, the engine awaits the in-flight call
|
|
@@ -706,7 +769,7 @@ function makeHostCallableDispatch(userFn) {
|
|
|
706
769
|
sendHostCallableError(callId, err);
|
|
707
770
|
return;
|
|
708
771
|
}
|
|
709
|
-
// Async callables: the wrapper resolves the promise on the
|
|
772
|
+
// Async callables: the wrapper resolves the promise on the host event loop
|
|
710
773
|
// loop and then forwards the result. The engine has released its
|
|
711
774
|
// heap permit while awaiting `complete_host_call`, so JS-side
|
|
712
775
|
// delay is safe (mirrors the Python `run_until_complete` model).
|
|
@@ -738,7 +801,7 @@ function isPromiseLike(value) {
|
|
|
738
801
|
function sendHostCallableResult(callId, value) {
|
|
739
802
|
let bytes;
|
|
740
803
|
// Result-encode path (host → engine): no sync guard (we're already on
|
|
741
|
-
//
|
|
804
|
+
// host event loop). We do track registrations, though — a callable nested in the
|
|
742
805
|
// result is registered before encoding finishes, and if encoding then
|
|
743
806
|
// throws, the bytes never reach the engine, so it never decodes (and
|
|
744
807
|
// never releases) the callable. Roll those back on failure, mirroring the
|
|
@@ -751,14 +814,7 @@ function sendHostCallableResult(callId, value) {
|
|
|
751
814
|
bytes = Uint8Array.from(InboundValue.encode(msg).finish());
|
|
752
815
|
}
|
|
753
816
|
catch (err) {
|
|
754
|
-
|
|
755
|
-
try {
|
|
756
|
-
releaseHostCallable(k);
|
|
757
|
-
}
|
|
758
|
-
catch {
|
|
759
|
-
// Best-effort cleanup; never mask the original error.
|
|
760
|
-
}
|
|
761
|
-
}
|
|
817
|
+
rollbackHostCallables(ctx.registered);
|
|
762
818
|
sendHostCallableError(callId, err);
|
|
763
819
|
return;
|
|
764
820
|
}
|
|
@@ -788,14 +844,12 @@ function buildHostCallableInbound(className, message, traceback, handleKey) {
|
|
|
788
844
|
stringField('message', message),
|
|
789
845
|
stringField('class_name', className),
|
|
790
846
|
stringField('language', 'nodejs'),
|
|
847
|
+
stringField('traceback', traceback ?? ''),
|
|
791
848
|
];
|
|
792
|
-
if (traceback != null) {
|
|
793
|
-
fields.push(stringField('traceback', traceback));
|
|
794
|
-
}
|
|
795
849
|
fields.push(handleField);
|
|
796
850
|
return InboundValue.create({
|
|
851
|
+
valueType: { classTy: { name: 'baml.errors.HostCallable' } },
|
|
797
852
|
classValue: InboundClassValue.create({
|
|
798
|
-
classTy: { name: 'baml.errors.HostCallable' },
|
|
799
853
|
fields,
|
|
800
854
|
}),
|
|
801
855
|
});
|
|
@@ -811,9 +865,40 @@ function buildHostCallableInbound(className, message, traceback, handleKey) {
|
|
|
811
865
|
// skips `0`), so a real registered key can never collide.
|
|
812
866
|
//
|
|
813
867
|
// Cast through `as unknown as HandleKey` because `HandleKey` is a native
|
|
814
|
-
//
|
|
868
|
+
// native class with private fields; protobufjs only reads the public
|
|
815
869
|
// `{low, high}` shape and the wire serializes them identically.
|
|
816
870
|
const UNRESOLVED_HOST_ERROR_KEY = { low: 0, high: 0 };
|
|
871
|
+
/** Encode a BamlError's wrapped value with its concrete generated-class FQN.
|
|
872
|
+
* Ordinary function arguments may use a bare map because the declared
|
|
873
|
+
* parameter supplies the target class. A thrown value has no such coercion
|
|
874
|
+
* context: the engine must see the class identity before it can validate the
|
|
875
|
+
* callable's declared `throws` contract. */
|
|
876
|
+
function setInboundTypedThrowValue(iv, value, ctx) {
|
|
877
|
+
if (value !== null && typeof value === 'object') {
|
|
878
|
+
const proto = Object.getPrototypeOf(value);
|
|
879
|
+
const isClassInstance = proto !== Object.prototype && proto !== null;
|
|
880
|
+
if (isClassInstance) {
|
|
881
|
+
const fqn = getTypeMap().jsTypeToBamlType(value.constructor);
|
|
882
|
+
if (fqn) {
|
|
883
|
+
const params = genericParamNames(value);
|
|
884
|
+
const userTypes = value.$types;
|
|
885
|
+
const typeArgs = params?.map((param) => lowerTypeToWireTy(userTypes?.[param])) ?? [];
|
|
886
|
+
const fields = [];
|
|
887
|
+
for (const [key, fieldValue] of Object.entries(value)) {
|
|
888
|
+
if (typeof fieldValue === 'function' || key === '$types')
|
|
889
|
+
continue;
|
|
890
|
+
const child = {};
|
|
891
|
+
setInboundValue(child, fieldValue, ctx);
|
|
892
|
+
fields.push({ stringKey: key, value: child });
|
|
893
|
+
}
|
|
894
|
+
iv.valueType = { classTy: { name: fqn, typeArgs } };
|
|
895
|
+
iv.classValue = { fields };
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
setInboundValue(iv, value, ctx);
|
|
901
|
+
}
|
|
817
902
|
function sendHostCallableError(callId, err) {
|
|
818
903
|
// Normal error path: never leaves the call uncompleted. If building or
|
|
819
904
|
// encoding the Instance throws (e.g. `describeError`, proto `create` /
|
|
@@ -845,7 +930,7 @@ function sendHostCallableError(callId, err) {
|
|
|
845
930
|
const ctx = { syncMode: false, registered: [] };
|
|
846
931
|
try {
|
|
847
932
|
const iv = InboundValue.create({});
|
|
848
|
-
|
|
933
|
+
setInboundTypedThrowValue(iv, err.value, ctx);
|
|
849
934
|
const bytes = Uint8Array.from(InboundValue.encode(iv).finish());
|
|
850
935
|
completeHostCall(callId, 1, bytes);
|
|
851
936
|
return;
|
|
@@ -855,25 +940,18 @@ function sendHostCallableError(callId, err) {
|
|
|
855
940
|
// registrations its nested fields triggered and fall through
|
|
856
941
|
// to the opaque-handle path below. The fall-through is
|
|
857
942
|
// intentional: the throw must still reach the engine even if
|
|
858
|
-
// the typed-
|
|
943
|
+
// the sparse typed-node encode broke, so we never leave the call
|
|
859
944
|
// hanging. Each release is wrapped in its own try/catch so
|
|
860
945
|
// a single bad entry doesn't abort the rest of the rollback
|
|
861
946
|
// and leak the remaining registrations (mirrors the other
|
|
862
947
|
// rollback sites in `setInboundValue` and
|
|
863
948
|
// `sendHostCallableResult`).
|
|
864
|
-
|
|
865
|
-
try {
|
|
866
|
-
releaseHostCallable(key);
|
|
867
|
-
}
|
|
868
|
-
catch {
|
|
869
|
-
// Best-effort cleanup; never mask the original error.
|
|
870
|
-
}
|
|
871
|
-
}
|
|
949
|
+
rollbackHostCallables(ctx.registered);
|
|
872
950
|
}
|
|
873
951
|
}
|
|
874
952
|
// Opaque-handle path: a `baml.errors.HostCallable` Instance carrying
|
|
875
953
|
// a handle to the originating JS error so the BAML→host decoder on
|
|
876
|
-
// the same
|
|
954
|
+
// the same JavaScript realm can rehydrate the *same* exception object
|
|
877
955
|
// on round-trip (`raised === caught` identity). Foreign runtimes /
|
|
878
956
|
// released keys fall back to metadata.
|
|
879
957
|
//
|
|
@@ -887,19 +965,20 @@ function sendHostCallableError(callId, err) {
|
|
|
887
965
|
// original Proxy. Identity loss here is the right trade — the
|
|
888
966
|
// alternative is hanging the call.
|
|
889
967
|
//
|
|
890
|
-
//
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
// with no corresponding engine-side `HostValueArc` to release it,
|
|
894
|
-
// so it lives until process exit. Both downstream calls are deeply
|
|
895
|
-
// mechanical (build a proto message, serialize fixed-shape fields)
|
|
896
|
-
// and don't depend on `err`'s shape, so this is effectively
|
|
897
|
-
// unreachable outside protobufjs / native-binding corruption.
|
|
968
|
+
// Register transactionally: if fixed-shape payload construction,
|
|
969
|
+
// encoding, or delivery fails before Rust retains the handle, remove
|
|
970
|
+
// the opaque entry before falling back to the last-resort payload.
|
|
898
971
|
const { className, message, stack } = describeError(err);
|
|
899
972
|
const handleKey = registerHostOpaque(err);
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
973
|
+
try {
|
|
974
|
+
const inbound = buildHostCallableInbound(className, message, stack, handleKey);
|
|
975
|
+
const bytes = Uint8Array.from(InboundValue.encode(inbound).finish());
|
|
976
|
+
completeHostCall(callId, 1, bytes);
|
|
977
|
+
}
|
|
978
|
+
catch (innerErr) {
|
|
979
|
+
releaseHostOpaque(handleKey);
|
|
980
|
+
throw innerErr;
|
|
981
|
+
}
|
|
903
982
|
}
|
|
904
983
|
catch (innerErr) {
|
|
905
984
|
completeHostCallLastResort(callId, innerErr);
|
|
@@ -911,7 +990,7 @@ function sendHostCallableError(callId, err) {
|
|
|
911
990
|
* error object, so the only ways it can fail are a broken proto runtime or a
|
|
912
991
|
* broken native binding — at which point nothing can complete the call. We
|
|
913
992
|
* swallow any throw here to avoid surfacing an unhandled rejection on the
|
|
914
|
-
*
|
|
993
|
+
* host event loop loop; the engine's lack of completion would then be the
|
|
915
994
|
* (unavoidable) failure mode.
|
|
916
995
|
*/
|
|
917
996
|
function completeHostCallLastResort(callId, err) {
|
|
@@ -922,7 +1001,7 @@ function completeHostCallLastResort(callId, err) {
|
|
|
922
1001
|
}
|
|
923
1002
|
catch (innerErr) {
|
|
924
1003
|
// Nothing more we can safely do — a throw here would surface as an
|
|
925
|
-
// unhandled rejection on the
|
|
1004
|
+
// unhandled rejection on the host event loop loop. The engine's lack of
|
|
926
1005
|
// completion will then be the (unavoidable) failure mode; log so the
|
|
927
1006
|
// failure is at least attributable.
|
|
928
1007
|
console.error('BAML internal: last-resort host-call completion failed; call will hang.', { callId, originalError: safeStringify(err), lastResortError: innerErr });
|
|
@@ -940,17 +1019,31 @@ function safeStringify(err) {
|
|
|
940
1019
|
}
|
|
941
1020
|
function describeError(err) {
|
|
942
1021
|
if (err instanceof Error) {
|
|
1022
|
+
const rawName = err.name;
|
|
1023
|
+
const rawMessage = err.message;
|
|
1024
|
+
const rawStack = err.stack;
|
|
943
1025
|
return {
|
|
944
|
-
className:
|
|
945
|
-
|
|
946
|
-
|
|
1026
|
+
className: typeof rawName === 'string' && rawName.length > 0
|
|
1027
|
+
? rawName
|
|
1028
|
+
: err.constructor.name || 'Error',
|
|
1029
|
+
message: typeof rawMessage === 'string' && rawMessage.length > 0
|
|
1030
|
+
? rawMessage
|
|
1031
|
+
: safeStringify(err),
|
|
1032
|
+
stack: rawStack == null
|
|
1033
|
+
? undefined
|
|
1034
|
+
: typeof rawStack === 'string'
|
|
1035
|
+
? rawStack
|
|
1036
|
+
: safeStringify(rawStack),
|
|
947
1037
|
};
|
|
948
1038
|
}
|
|
949
1039
|
if (err != null && typeof err === 'object') {
|
|
950
1040
|
const ctor = err.constructor;
|
|
951
|
-
const
|
|
952
|
-
|
|
1041
|
+
const rawName = ctor?.name;
|
|
1042
|
+
const className = typeof rawName === 'string' && rawName.length > 0 && rawName !== 'Object'
|
|
1043
|
+
? rawName
|
|
1044
|
+
: 'Error';
|
|
1045
|
+
return { className, message: safeStringify(err), stack: undefined };
|
|
953
1046
|
}
|
|
954
|
-
return { className: 'Error', message:
|
|
1047
|
+
return { className: 'Error', message: safeStringify(err), stack: undefined };
|
|
955
1048
|
}
|
|
956
1049
|
//# sourceMappingURL=proto.js.map
|