@boundaryml/baml-bridge 0.0.0 → 0.13.1-nightly.20260707.e

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 (46) hide show
  1. package/dist/ctx_manager.d.ts +22 -0
  2. package/dist/ctx_manager.d.ts.map +1 -0
  3. package/dist/ctx_manager.js +91 -0
  4. package/dist/ctx_manager.js.map +1 -0
  5. package/dist/define_function.d.ts +38 -0
  6. package/dist/define_function.d.ts.map +1 -0
  7. package/dist/define_function.js +281 -0
  8. package/dist/define_function.js.map +1 -0
  9. package/dist/errors.d.ts +51 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +67 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/exit_hook.d.ts +9 -0
  14. package/dist/exit_hook.d.ts.map +1 -0
  15. package/dist/exit_hook.js +27 -0
  16. package/dist/exit_hook.js.map +1 -0
  17. package/dist/host_value_registry.d.ts +57 -0
  18. package/dist/host_value_registry.d.ts.map +1 -0
  19. package/dist/host_value_registry.js +141 -0
  20. package/dist/host_value_registry.js.map +1 -0
  21. package/dist/index.d.ts +67 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +162 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/native.d.ts +312 -0
  26. package/dist/native.js +625 -0
  27. package/dist/proto/baml_cffi.d.ts +6385 -0
  28. package/dist/proto/baml_cffi.js +17738 -0
  29. package/dist/proto.d.ts +62 -0
  30. package/dist/proto.d.ts.map +1 -0
  31. package/dist/proto.js +963 -0
  32. package/dist/proto.js.map +1 -0
  33. package/dist/stream.d.ts +23 -0
  34. package/dist/stream.d.ts.map +1 -0
  35. package/dist/stream.js +68 -0
  36. package/dist/stream.js.map +1 -0
  37. package/dist/typemap.d.ts +37 -0
  38. package/dist/typemap.d.ts.map +1 -0
  39. package/dist/typemap.js +110 -0
  40. package/dist/typemap.js.map +1 -0
  41. package/dist/wire_ty.d.ts +58 -0
  42. package/dist/wire_ty.d.ts.map +1 -0
  43. package/dist/wire_ty.js +147 -0
  44. package/dist/wire_ty.js.map +1 -0
  45. package/package.json +64 -5
  46. package/README.md +0 -3
package/dist/proto.js ADDED
@@ -0,0 +1,963 @@
1
+ /**
2
+ * THIS FILE IS AUTO-GENERATED — DO NOT EDIT BY HAND.
3
+ *
4
+ * Source: baml_language/crates/bridge_nodejs/typescript_src/
5
+ * Proto: baml_language/crates/bridge_ctypes/types/baml_bridge/cffi/v1/*.proto
6
+ * Build: cd baml_language/crates/bridge_nodejs && pnpm build:debug
7
+ */
8
+ // proto.ts — mirrors bridge_python/python_src/baml_py/proto.py
9
+ //
10
+ // Encodes TS objects → CallFunctionArgs protobuf bytes (for sending to Rust)
11
+ // Decodes the BamlOutboundResult envelope → TS objects (call results), and
12
+ // bare BamlOutboundValue bytes → TS objects (host-callable args).
13
+ import { baml_bridge } from './proto/baml_cffi.js';
14
+ import { BamlHandle, BamlImage, BamlAudio, BamlVideo, BamlPdf, registerHostCallable, releaseHostCallable, completeHostCall, } from './native.js';
15
+ import { BamlStream } from './stream.js';
16
+ import { BamlAbortError, BamlCancelledError, BamlError, BamlPanic } from './errors.js';
17
+ import { registerHostOpaque, tryRehydrateHostValueByKey, } from './host_value_registry.js';
18
+ import { getTypeMap } from './typemap.js';
19
+ import { lowerTypeToWireTy, outboundTyToBamlType } from './wire_ty.js';
20
+ const CallFunctionArgs = baml_bridge.cffi.v1.CallFunctionArgs;
21
+ const BamlOutboundValue = baml_bridge.cffi.v1.BamlOutboundValue;
22
+ const BamlOutboundResult = baml_bridge.cffi.v1.BamlOutboundResult;
23
+ const BamlToHostCall = baml_bridge.cffi.v1.BamlToHostCall;
24
+ const InboundValue = baml_bridge.cffi.v1.InboundValue;
25
+ const InboundClassValue = baml_bridge.cffi.v1.InboundClassValue;
26
+ const InboundMapEntry = baml_bridge.cffi.v1.InboundMapEntry;
27
+ const BamlHandleType = baml_bridge.cffi.v1.BamlHandleType;
28
+ const CANCELLED_PANIC_CLASS = 'baml.panics.Cancelled';
29
+ // ─── Inbound (TS → Rust) ───
30
+ /**
31
+ * Error thrown when a host callable (a JS `function`) is passed to the
32
+ * *synchronous* call path. See {@link encodeCallArgs} for why this can't work.
33
+ */
34
+ export class HostCallableSyncError extends Error {
35
+ constructor(message) {
36
+ super(message);
37
+ this.name = 'HostCallableSyncError';
38
+ }
39
+ }
40
+ /**
41
+ * Generic-class instances declare their TypeVar names (declaration order) in a
42
+ * static `$generic` field that codegen emits; this reads it back. Returns the
43
+ * param-name list for a generic class instance, or `null` for a non-generic
44
+ * one. The value-level type args ride a sibling `$types` instance field.
45
+ */
46
+ function genericParamNames(value) {
47
+ const ctor = value.constructor;
48
+ const g = ctor?.$generic;
49
+ if (Array.isArray(g) && g.length > 0 && g.every((x) => typeof x === 'string')) {
50
+ return g;
51
+ }
52
+ return null;
53
+ }
54
+ function setInboundValue(iv, value, ctx) {
55
+ if (value === null || value === undefined) {
56
+ return; // Leave oneof unset → null
57
+ }
58
+ if (typeof value === 'boolean') {
59
+ iv.boolValue = value;
60
+ }
61
+ else if (typeof value === 'number') {
62
+ if (Number.isInteger(value)) {
63
+ iv.intValue = value;
64
+ }
65
+ else {
66
+ iv.floatValue = value;
67
+ }
68
+ }
69
+ else if (typeof value === 'bigint') {
70
+ // Hex / base sixteen on the wire. BigInt.prototype.toString(16)
71
+ // yields e.g. "-2a"; signed values round-trip via num-bigint's
72
+ // LowerHex impl on the Rust side.
73
+ iv.bigintValue = value.toString(16);
74
+ }
75
+ else if (typeof value === 'string') {
76
+ iv.stringValue = value;
77
+ }
78
+ else if (value instanceof Uint8Array) {
79
+ iv.uint8arrayValue = value;
80
+ }
81
+ else if (value instanceof BamlHandle) {
82
+ // A round-tripped host callable arrives as a handle, not a raw
83
+ // function — apply the same sync-path fast-fail so `callFunctionSync`
84
+ // can't hang waiting on a callback that the blocked main thread can
85
+ // never run. HOST_VALUE_CALLABLE is currently the only handle type
86
+ // that dispatches back into the host; the rest are engine-side ADT/
87
+ // heap handles that need no host callback and so are safe on the sync
88
+ // path. Any future dispatch-backed handle type must be guarded here.
89
+ if (ctx.syncMode && value.handleType === BamlHandleType.HOST_VALUE_CALLABLE) {
90
+ throw new HostCallableSyncError('host callables are only supported on the async call path; use the async API ' +
91
+ '(callFunction) instead of callFunctionSync. The sync path blocks the Node main ' +
92
+ 'thread, so the host callback can never run and the call would hang.');
93
+ }
94
+ // The Rust inbound decoder drains handle-table entries. Send a fresh
95
+ // cloned key so the JS-owned handle remains valid for later calls.
96
+ iv.handle = { key: value._cloneKeyForWire(), handleType: value.handleType };
97
+ }
98
+ else if (value instanceof BamlStream) {
99
+ // Stream wrapper → its inner TaggedHeapHandle. Mirrors the BamlHandle
100
+ // branch above: the Rust inbound decoder *drains* the handle-table
101
+ // entry, so send a fresh cloned key — otherwise the engine consumes the
102
+ // stream's only key and the next `next()`/`final()` call fails with
103
+ // "Invalid handle key". (`BamlStream._toHandle()` returns the inner
104
+ // handle without cloning, unlike the media wrappers' `_toHandle`.)
105
+ const h = value._toHandle();
106
+ iv.handle = { key: h._cloneKeyForWire(), handleType: h.handleType };
107
+ }
108
+ else if (value instanceof BamlImage
109
+ || value instanceof BamlAudio
110
+ || value instanceof BamlVideo
111
+ || value instanceof BamlPdf) {
112
+ // Stdlib media wrappers → their backing ADT_MEDIA_* handle. `_toHandle`
113
+ // clones the table row so the wrapper stays usable after encode.
114
+ const h = value._toHandle();
115
+ iv.handle = { key: h.key, handleType: h.handleType };
116
+ }
117
+ else if (typeof value === 'function') {
118
+ // Host callables cannot work on the synchronous call path —
119
+ // fast-fail before any blocking happens (and before we register a
120
+ // tsfn, which would otherwise be orphaned).
121
+ if (ctx.syncMode) {
122
+ throw new HostCallableSyncError('host callables are only supported on the async call path; use the async API ' +
123
+ '(callFunction) instead of callFunctionSync. The sync path blocks the Node main ' +
124
+ 'thread, so the host callback can never run and the call would hang.');
125
+ }
126
+ // JS callable → register a dispatch wrapper in the host-value
127
+ // registry and emit `Handle{key, HOST_VALUE_CALLABLE}`. The Rust
128
+ // side decodes this into `BexExternalValue::HostValue` and binds it
129
+ // to an `Object::HostClosure`; BAML invocations land back in
130
+ // `hostCallableDispatch` below via the ThreadsafeFunction.
131
+ const key = registerHostCallable(makeHostCallableDispatch(value));
132
+ // Remember the key so a later encode failure can release it.
133
+ ctx.registered.push(key);
134
+ iv.handle = { key, handleType: BamlHandleType.HOST_VALUE_CALLABLE };
135
+ }
136
+ else if (Array.isArray(value)) {
137
+ const listVal = [];
138
+ for (const item of value) {
139
+ const child = {};
140
+ setInboundValue(child, item, ctx);
141
+ listVal.push(child);
142
+ }
143
+ iv.listValue = { values: listVal };
144
+ }
145
+ else if (value !== null && typeof value === 'object') {
146
+ // Any remaining object — a plain object OR a codegen-emitted class
147
+ // instance (e.g. `new Resume({...})`) — encodes as `map_value` with
148
+ // no FQN tag. The Rust side's `coerce_arg_to_declared_type` reshapes
149
+ // it against the function's declared parameter type (the 10a
150
+ // typemap-free encode simplification). `Object.entries` yields the
151
+ // class's own enumerable fields, set by the constructor's
152
+ // `Object.assign(this, init)`. The specific built-in wrappers
153
+ // (BamlHandle/BamlStream/media) are handled by the instanceof
154
+ // branches above, so they never reach here.
155
+ //
156
+ // Class instances additionally carry their instance-method bindings
157
+ // (`m = defineInstanceFunction(...).bind(this)`) as own enumerable
158
+ // fields. Those are behavior, not state — skip function-valued fields
159
+ // on a class instance so re-encoding a handle-backed value (e.g. a
160
+ // `baml.fs.File` with `read`/`text` bindings) sends only its data
161
+ // (the `_handle`). Plain objects keep every field, so a host callable
162
+ // nested in a plain object still encodes as a callable.
163
+ const proto = Object.getPrototypeOf(value);
164
+ const isClassInstance = proto !== Object.prototype && proto !== null;
165
+ // Handle-backed stdlib types (e.g. `baml.fs.File`, `baml.http.Response`)
166
+ // decode to a class instance that carries the engine's handle in a
167
+ // field (`_handle` / `_body`). The engine resolves these from a
168
+ // FQN-tagged `class_value` (not a bare `map` — which has no FQN — nor a
169
+ // bare `handle`), so re-sending the same handle inside the named class
170
+ // value lets it resolve the same object and preserve cursor/connection
171
+ // state across FFI calls. The FQN comes from the typemap reverse map.
172
+ if (isClassInstance && Object.values(value).some(v => v instanceof BamlHandle)) {
173
+ const fqn = getTypeMap().jsTypeToBamlType(value.constructor);
174
+ if (fqn) {
175
+ const classFields = [];
176
+ for (const [k, v] of Object.entries(value)) {
177
+ if (typeof v === 'function')
178
+ continue;
179
+ const childVal = {};
180
+ setInboundValue(childVal, v, ctx);
181
+ classFields.push({ stringKey: k, value: childVal });
182
+ }
183
+ iv.classValue = { classTy: { name: fqn }, fields: classFields };
184
+ return;
185
+ }
186
+ }
187
+ // Generic class instance → a FQN-tagged `class_value` carrying the
188
+ // value-level class type-args channel (`class_ty`). Unlike a non-generic
189
+ // class instance (which encodes as a bare `map_value` for the engine to
190
+ // reshape against the declared param type), a generic instance MUST send
191
+ // its concrete type args: the engine strictly rejects coercing a bare map
192
+ // into a generic class slot ("a bare map carries no class type
193
+ // arguments"). The args come from the optional `$types` instance field;
194
+ // an absent binding lowers to the unknown/top type. Mirrors
195
+ // bridge_python's pydantic-generic-metadata path in proto.py, which sets
196
+ // `class_value.class_ty` for a generic instance.
197
+ if (isClassInstance) {
198
+ const params = genericParamNames(value);
199
+ if (params) {
200
+ const fqn = getTypeMap().jsTypeToBamlType(value.constructor);
201
+ const userTypes = value.$types;
202
+ const typeArgs = params.map((p) => lowerTypeToWireTy(userTypes?.[p]));
203
+ const classFields = [];
204
+ for (const [k, v] of Object.entries(value)) {
205
+ // Skip method bindings (behavior, not state) and the synthetic
206
+ // `$types` carrier (it rides `class_ty`, not the field list).
207
+ if (typeof v === 'function')
208
+ continue;
209
+ if (k === '$types')
210
+ continue;
211
+ const childVal = {};
212
+ setInboundValue(childVal, v, ctx);
213
+ classFields.push({ stringKey: k, value: childVal });
214
+ }
215
+ iv.classValue = { classTy: { name: fqn, typeArgs }, fields: classFields };
216
+ return;
217
+ }
218
+ }
219
+ const entries = [];
220
+ for (const [k, v] of Object.entries(value)) {
221
+ if (isClassInstance && typeof v === 'function')
222
+ continue;
223
+ const entry = { stringKey: k };
224
+ const childVal = {};
225
+ setInboundValue(childVal, v, ctx);
226
+ entry.value = childVal;
227
+ entries.push(entry);
228
+ }
229
+ iv.mapValue = { entries };
230
+ }
231
+ else {
232
+ throw new TypeError(`Cannot encode value of type ${Object.prototype.toString.call(value)} to protobuf`);
233
+ }
234
+ }
235
+ /**
236
+ * Encode kwargs into `CallFunctionArgs` bytes.
237
+ *
238
+ * `syncMode` (default false) selects the sync guard: a host callable in the
239
+ * kwargs of a *synchronous* call rejects with {@link HostCallableSyncError}
240
+ * before any work, rather than registering a tsfn and then hanging.
241
+ *
242
+ * Release tradeoff: a callable that encodes successfully is registered in the
243
+ * host-value table and is normally released only when the engine GCs the
244
+ * `HostClosure` it allocated and fires the C release callback (a GC-timed
245
+ * release, drained by the engine after collection).
246
+ * Because the Node tsfn is built with `weak::<false>` it keeps a strong libuv
247
+ * ref, so a *leaked* registry entry can also keep the Node process from
248
+ * exiting — which is exactly why the encode-error rollback below matters: if a
249
+ * later kwarg fails, the engine never sees (and so never releases) the keys we
250
+ * already registered, so we release them here.
251
+ */
252
+ export function encodeCallArgs(kwargs, options) {
253
+ const callId = options.callId;
254
+ if (callId === 0n) {
255
+ throw new TypeError('callId must be a nonzero uint64');
256
+ }
257
+ const ctx = { syncMode: options.syncMode ?? false, registered: [] };
258
+ try {
259
+ const entries = [];
260
+ for (const [key, value] of Object.entries(kwargs)) {
261
+ const entry = { stringKey: key };
262
+ const iv = {};
263
+ setInboundValue(iv, value, ctx);
264
+ entry.value = iv;
265
+ entries.push(entry);
266
+ }
267
+ const typeArgs = (options.typeArgs ?? []).map(([typeVar, typeValue]) => ({
268
+ typeVar,
269
+ typeValue,
270
+ }));
271
+ const msg = CallFunctionArgs.fromObject({
272
+ kwargs: entries,
273
+ callId: callId.toString(),
274
+ typeArgs,
275
+ });
276
+ return Buffer.from(CallFunctionArgs.encode(msg).finish());
277
+ }
278
+ catch (err) {
279
+ // Roll back any host callables registered before the failure so
280
+ // they don't leak in the registry (and pin the libuv loop) for the
281
+ // life of the process — the call never reaches the engine, so the
282
+ // engine would never release them.
283
+ for (const k of ctx.registered) {
284
+ try {
285
+ releaseHostCallable(k);
286
+ }
287
+ catch {
288
+ // Best-effort cleanup; never mask the original error.
289
+ }
290
+ }
291
+ throw err;
292
+ }
293
+ }
294
+ // ─── Outbound (Rust → TS) ───
295
+ // Hex / base sixteen on the wire. Shared by `bigint_value` (runtime
296
+ // values) and `bigint_literal` (type literals) since both fields use
297
+ // the same wire format. BigInt() accepts a "0x"-prefixed hex literal;
298
+ // strip a leading minus so we can parse the magnitude. Guard against
299
+ // empty or sign-only inputs — `BigInt("0x")` throws `SyntaxError`, so
300
+ // we surface a clearer error instead.
301
+ // Workspace bigint cap = 2^28 bits ⇒ at most (2^28)/4 hex digits, plus a
302
+ // small slack to match the Rust-side `MAX_BIGINT_HEX_LEN` constant in
303
+ // `bridge_ctypes/src/value_decode.rs`. Reject longer inputs before calling
304
+ // `BigInt()` so a megabyte-scale payload can't drive an unbounded allocation.
305
+ const MAX_BIGINT_HEX_LEN = (1 << 28) / 4 + 2;
306
+ function parseHexBigint(s) {
307
+ const magnitude = s.startsWith('-') ? s.slice(1) : s;
308
+ if (magnitude.length === 0 || !/^[0-9a-fA-F]+$/.test(magnitude)) {
309
+ throw new Error(`Invalid bigint hex on the wire: ${JSON.stringify(s)}`);
310
+ }
311
+ if (magnitude.length > MAX_BIGINT_HEX_LEN) {
312
+ throw new Error(`bigint hex exceeds the workspace cap (${magnitude.length} chars, limit ${MAX_BIGINT_HEX_LEN})`);
313
+ }
314
+ return s.startsWith('-')
315
+ ? -BigInt(`0x${magnitude}`)
316
+ : BigInt(`0x${magnitude}`);
317
+ }
318
+ function decodeValueHolder(holder, typeMap) {
319
+ if (holder.nullValue != null)
320
+ return null;
321
+ if (holder.stringValue != null)
322
+ return holder.stringValue;
323
+ if (holder.intValue != null)
324
+ return Number(holder.intValue);
325
+ if (holder.bigintValue != null) {
326
+ return parseHexBigint(holder.bigintValue);
327
+ }
328
+ if (holder.floatValue != null)
329
+ return holder.floatValue;
330
+ if (holder.boolValue != null)
331
+ return holder.boolValue;
332
+ if (holder.uint8arrayValue != null)
333
+ return holder.uint8arrayValue;
334
+ if (holder.classValue) {
335
+ return decodeClass(holder.classValue, typeMap);
336
+ }
337
+ if (holder.enumValue) {
338
+ return decodeEnum(holder.enumValue, typeMap);
339
+ }
340
+ if (holder.literalValue) {
341
+ // Inline scalars in a oneof (mirrors `BamlTyLiteral`); dispatch on the
342
+ // protobufjs oneof virtual getter (only on the decoded class instance,
343
+ // not the `I`-interface) since scalar fields default to a non-null zero
344
+ // value rather than `null`.
345
+ const lit = holder.literalValue;
346
+ switch (lit.literal) {
347
+ case 'stringValue':
348
+ return lit.stringValue;
349
+ case 'intValue':
350
+ return Number(lit.intValue);
351
+ case 'boolValue':
352
+ return lit.boolValue;
353
+ // Hex / base sixteen on the wire, matching `bigint_value`.
354
+ case 'bigintValue':
355
+ return parseHexBigint(lit.bigintValue ?? '');
356
+ // Source text on the wire (mirrors `BamlTyLiteral.float_value`).
357
+ case 'floatValue':
358
+ return Number(lit.floatValue);
359
+ }
360
+ }
361
+ if (holder.listValue) {
362
+ return (holder.listValue.items || []).map(item => decodeValueHolder(item, typeMap));
363
+ }
364
+ if (holder.mapValue) {
365
+ const obj = Object.create(null);
366
+ for (const entry of holder.mapValue.entries || []) {
367
+ if (entry.key != null && entry.value) {
368
+ obj[entry.key] = decodeValueHolder(entry.value, typeMap);
369
+ }
370
+ }
371
+ return obj;
372
+ }
373
+ if (holder.unionVariantValue && holder.unionVariantValue.value) {
374
+ return decodeValueHolder(holder.unionVariantValue.value, typeMap);
375
+ }
376
+ // handle_value: pass the protobufjs Long directly as the key — BamlHandle's
377
+ // constructor accepts { low, high } which is layout-compatible with Long.
378
+ // Dispatch on handle_type so media handles decode to their typed wrapper.
379
+ if (holder.handleValue) {
380
+ const ht = holder.handleValue.handleType ?? 0;
381
+ if (ht === BamlHandleType.HANDLE_UNSPECIFIED) {
382
+ // Never a valid decoded handle (mirrors Python's _decode_handle).
383
+ throw new BamlError('decoded handle has HANDLE_UNSPECIFIED handle_type');
384
+ }
385
+ const handle = new BamlHandle(holder.handleValue.key, ht);
386
+ if (ht === BamlHandleType.ADT_MEDIA_IMAGE)
387
+ return BamlImage._fromHandle(handle);
388
+ if (ht === BamlHandleType.ADT_MEDIA_AUDIO)
389
+ return BamlAudio._fromHandle(handle);
390
+ if (ht === BamlHandleType.ADT_MEDIA_VIDEO)
391
+ return BamlVideo._fromHandle(handle);
392
+ if (ht === BamlHandleType.ADT_MEDIA_PDF)
393
+ return BamlPdf._fromHandle(handle);
394
+ if (ht === BamlHandleType.ADT_TAGGED_HEAP_HANDLE) {
395
+ // Dispatch via the typemap: every tagged-heap class self-registers
396
+ // under its engine FQN (codegen emits the entry, e.g.
397
+ // `baml.llm.Stream → BamlStream`), so any class is reachable without
398
+ // special-casing here. Mirrors bridge_python's `_decode_handle`
399
+ // ADT_TAGGED_HEAP_HANDLE arm (sdks/python/.../proto.py).
400
+ // The handle's `ty` is a full `BamlTy`; the typed-wrapper FQN lives
401
+ // on its class variant (a non-class `ty` reads back as `''`).
402
+ const fqn = holder.handleValue.ty?.classTy?.name ?? '';
403
+ const Cls = typeMap.getClass(fqn);
404
+ return Cls._fromHandle(handle);
405
+ }
406
+ // ADT_MEDIA_GENERIC has no typed wrapper — stays a bare BamlHandle.
407
+ return handle;
408
+ }
409
+ // Inline media / prompt AST are not expected on the Node FFI path — they
410
+ // travel via `handle_value`. Reject loudly rather than silently collapsing
411
+ // to null (mirrors bridge_python's proto.py, which raises here).
412
+ if (holder.mediaValue || holder.promptAstValue) {
413
+ const which = holder.mediaValue ? 'media_value' : 'prompt_ast_value';
414
+ throw new BamlError(`BEX emitted ${which} on the FFI path — media/prompt AST are expected ` +
415
+ `via handle_value, not inline`);
416
+ }
417
+ // Any remaining unset oneof is a legitimate null: an all-default holder is a
418
+ // null BAML result.
419
+ return null;
420
+ }
421
+ /**
422
+ * Decode a `class_value` to a typed instance via the typemap. When the FQN is
423
+ * in the typemap (the generated-SDK path), construct `new Cls(fieldDict)`
424
+ * (codegen emits `constructor(init) { Object.assign(this, init); }`). The five
425
+ * stdlib media wrappers unwrap their `_data` envelope to the wrapper itself.
426
+ * When the FQN is absent (the bare bridge has no typemap, or an unmapped
427
+ * class), fall back to a plain object — preserving the pre-typemap behavior.
428
+ */
429
+ function decodeClass(classValue, typeMap) {
430
+ const fieldDict = {};
431
+ for (const entry of classValue.fields || []) {
432
+ if (entry.key != null && entry.value) {
433
+ fieldDict[entry.key] = decodeValueHolder(entry.value, typeMap);
434
+ }
435
+ }
436
+ const fqn = classValue.name ?? '';
437
+ if (fqn) {
438
+ let Cls;
439
+ try {
440
+ Cls = typeMap.getClass(fqn);
441
+ }
442
+ catch {
443
+ Cls = undefined; // unmapped FQN — fall back below
444
+ }
445
+ if (Cls !== undefined) {
446
+ // Stdlib media wrappers: the decoded `_data` is already the typed
447
+ // wrapper (its inner handle_value decoded via the media branch);
448
+ // unwrap the envelope per the spec's Instance row.
449
+ if ((Cls === BamlImage || Cls === BamlAudio || Cls === BamlVideo || Cls === BamlPdf)
450
+ && '_data' in fieldDict) {
451
+ return fieldDict._data;
452
+ }
453
+ const Ctor = Cls;
454
+ const instance = new Ctor(fieldDict);
455
+ // Repopulate a generic instance's `$types` from the wire's positional
456
+ // `type_args` (the value-level type channel), keyed by the class's
457
+ // static `$generic` param names — the decode-side mirror of the
458
+ // encoder's `class_ty`. Defined non-enumerable so it neither perturbs
459
+ // structural equality (`toEqual`) nor re-encodes as a data field,
460
+ // while still being readable by a later generic instance-method call.
461
+ const params = Array.isArray(Ctor.$generic) ? Ctor.$generic : null;
462
+ const typeArgs = classValue.typeArgs;
463
+ if (params && typeArgs && typeArgs.length) {
464
+ const types = {};
465
+ params.forEach((p, i) => {
466
+ types[p] = outboundTyToBamlType(typeArgs[i]);
467
+ });
468
+ Object.defineProperty(instance, '$types', {
469
+ value: types,
470
+ enumerable: false,
471
+ writable: true,
472
+ configurable: true,
473
+ });
474
+ }
475
+ return instance;
476
+ }
477
+ }
478
+ // Fallback: plain object (null-prototype, matching the prior behavior).
479
+ const obj = Object.create(null);
480
+ for (const [k, v] of Object.entries(fieldDict))
481
+ obj[k] = v;
482
+ return obj;
483
+ }
484
+ /**
485
+ * Decode an `enum_value` to a typed enum member via the typemap. Falls back to
486
+ * the raw variant string when the FQN is unmapped (bare bridge / unmapped enum).
487
+ */
488
+ function decodeEnum(enumValue, typeMap) {
489
+ const fqn = enumValue.name ?? '';
490
+ const variant = enumValue.value;
491
+ if (fqn && variant != null) {
492
+ let En;
493
+ try {
494
+ En = typeMap.getEnum(fqn);
495
+ }
496
+ catch {
497
+ En = undefined;
498
+ }
499
+ if (En !== undefined && variant in En) {
500
+ return En[variant];
501
+ }
502
+ }
503
+ return variant;
504
+ }
505
+ /**
506
+ * Decode a bare `BamlOutboundValue` to a JS value. Used for the host-callable
507
+ * args path, where the engine sends a list-shaped `BamlOutboundValue` rather
508
+ * than the call-result `BamlOutboundResult` envelope.
509
+ */
510
+ export function decodeOutboundValue(data) {
511
+ const msg = BamlOutboundValue.decode(data instanceof Buffer ? data : Buffer.from(data));
512
+ return decodeValueHolder(msg, getTypeMap());
513
+ }
514
+ /**
515
+ * Decode the engine→host `BamlToHostCall`. The engine has already resolved the
516
+ * call against the callee's declared params and dropped omitted optionals, so
517
+ * `args` is a flat, declared-order list of the supplied args. Partition it back
518
+ * into the required positional run and the supplied optionals (keyed by
519
+ * `argName`) using each arg's `isOptionalArg` flag.
520
+ */
521
+ function decodeHostCall(data) {
522
+ const msg = BamlToHostCall.decode(data instanceof Buffer ? data : Buffer.from(data));
523
+ const typeMap = getTypeMap();
524
+ const positional = [];
525
+ const optional = {};
526
+ for (const arg of msg.args ?? []) {
527
+ const value = arg.value ? decodeValueHolder(arg.value, typeMap) : undefined;
528
+ if (arg.isOptionalArg) {
529
+ optional[arg.argName ?? ''] = value;
530
+ }
531
+ else {
532
+ positional.push(value);
533
+ }
534
+ }
535
+ return { positional, optional };
536
+ }
537
+ /**
538
+ * Reshape the split args into the TypeScript calling convention the callback's
539
+ * generated type advertises: the required args stay positional and the supplied
540
+ * optionals are passed as a single trailing `$opts` object — exactly mirroring
541
+ * how a BAML function is *called* from TypeScript.
542
+ *
543
+ * The `$opts` object is appended only when at least one optional was supplied.
544
+ * With none supplied (or a callback with no optional params at all) the args
545
+ * stay purely positional — the correct shape for a callback declared without an
546
+ * `$opts` object, and the shape that lets the callback's own defaults fill any
547
+ * omitted optionals.
548
+ */
549
+ function reshapeHostArgs(positional, optional) {
550
+ if (Object.keys(optional).length === 0) {
551
+ return positional;
552
+ }
553
+ return [...positional, optional];
554
+ }
555
+ /**
556
+ * Decode the thrown value off the wire holder. Returns the fully decoded BAML
557
+ * `value` (a generated class instance when the FQN is mapped, else a plain
558
+ * object / primitive), the class FQN (`className`), and a readable `message`
559
+ * lifted from the value's `message` field when present. Mirrors
560
+ * bridge_python's `decode_value` + `_outbound_class_fqn` so the surfaced
561
+ * `BamlError`/`BamlPanic` carries the decoded value, not just a string.
562
+ *
563
+ * Decoding is defensive: a malformed/unsupported thrown payload must not mask
564
+ * the original error/panic, so a decode failure degrades to an undefined value
565
+ * (the formatted message and className are still surfaced).
566
+ */
567
+ /**
568
+ * Peel any `union_variant_value` wrapper(s) so a metadata read sees the inner
569
+ * value. The engine wraps a thrown value in `union_variant_value` when the
570
+ * function declares a multi-member `throws` union; `decodeValueHolder` already
571
+ * unwraps this for the value itself (see the `unionVariantValue` arm), so the
572
+ * FQN read below must match or `className` is lost for union throws.
573
+ */
574
+ function unwrapUnionVariant(holder) {
575
+ let h = holder;
576
+ while (h?.unionVariantValue?.value) {
577
+ h = h.unionVariantValue.value;
578
+ }
579
+ return h;
580
+ }
581
+ function decodeThrown(holder) {
582
+ const className = unwrapUnionVariant(holder)?.classValue?.name ?? undefined;
583
+ let value;
584
+ try {
585
+ value = holder ? decodeValueHolder(holder, getTypeMap()) : undefined;
586
+ }
587
+ catch {
588
+ value = undefined;
589
+ }
590
+ let message = '';
591
+ if (value != null && typeof value === 'object' && 'message' in value) {
592
+ const m = value.message;
593
+ if (typeof m === 'string')
594
+ message = m;
595
+ }
596
+ return { value, className, message };
597
+ }
598
+ function formatThrownMessage(kind, className, message, trace) {
599
+ const label = className || `baml.${kind}`;
600
+ let text = `baml ${kind}: ${label}`;
601
+ if (message)
602
+ text += `: ${message}`;
603
+ if (trace.length)
604
+ text += '\n' + trace.map(l => ' ' + l).join('\n');
605
+ return text;
606
+ }
607
+ function cancellationAbortError(message) {
608
+ return new BamlAbortError(message, {
609
+ reason: new BamlCancelledError(message),
610
+ });
611
+ }
612
+ /**
613
+ * Decode a `BamlOutboundResult` envelope (the engine's call-result wire shape
614
+ * after 31c/31e). The `ok` arm returns the decoded value; the `error`/`panic`
615
+ * arms **throw** a `BamlError`/`BamlPanic` carrying the fully decoded thrown
616
+ * value (`.value`), the BAML trace (`.bamlTrace`), and the class FQN
617
+ * (`.className`), with a readable formatted `.message`. An `is_exit_panic`
618
+ * (clean `baml.sys.exit`) terminates the process via `process.exit(code)`
619
+ * rather than throwing.
620
+ */
621
+ export function decodeCallResult(data) {
622
+ const buf = data instanceof Buffer ? data : Buffer.from(data);
623
+ const result = BamlOutboundResult.decode(buf);
624
+ switch (result.result) {
625
+ case 'error': {
626
+ const { value, className, message } = decodeThrown(result.error?.value);
627
+ const trace = result.error?.trace ?? [];
628
+ // Same-host rehydration: a `baml.errors.HostCallable` carrying a
629
+ // `_handle` that still resolves in this process's host-value
630
+ // registry re-throws the *original* JS error object the bridge
631
+ // registered on the inbound throw — preserving `raised === caught`
632
+ // identity. Foreign runtimes (a different Node process, the
633
+ // Python bridge) and released keys fall through to the
634
+ // metadata-bearing `BamlError(HostCallable)` wrapper below.
635
+ if (className === 'baml.errors.HostCallable' && value !== null && typeof value === 'object') {
636
+ const handle = value._handle;
637
+ const original = tryRehydrateHostValueByKey(handle);
638
+ if (original !== undefined) {
639
+ throw original;
640
+ }
641
+ }
642
+ const formatted = formatThrownMessage('error', className ?? '', message, trace);
643
+ if (className === CANCELLED_PANIC_CLASS) {
644
+ throw cancellationAbortError(formatted);
645
+ }
646
+ throw new BamlError(formatted, { value, bamlTrace: trace, className });
647
+ }
648
+ case 'panic': {
649
+ const panic = result.panic;
650
+ if (panic?.isExitPanic) {
651
+ // Clean process-exit panic: exit after flushing telemetry (the
652
+ // registered `process.once('exit', flushEvents)` hook fires
653
+ // synchronously inside process.exit), rather than throwing.
654
+ const code = Number(panic.exitCode ?? 0);
655
+ process.exit(code);
656
+ }
657
+ const { value, className, message } = decodeThrown(panic?.value);
658
+ const trace = panic?.trace ?? [];
659
+ const formatted = formatThrownMessage('panic', className ?? '', message, trace);
660
+ if (className === CANCELLED_PANIC_CLASS) {
661
+ throw cancellationAbortError(formatted);
662
+ }
663
+ throw new BamlPanic(formatted, { value, bamlTrace: trace, className });
664
+ }
665
+ case 'ok':
666
+ default:
667
+ // `ok` (or an absent oneof — an all-default envelope is a null `ok`).
668
+ return result.ok ? decodeValueHolder(result.ok, getTypeMap()) : null;
669
+ }
670
+ }
671
+ // ─── Host-callable dispatch (BAML → JS) ───
672
+ //
673
+ // When BAML invokes a `HostValue` registered via `registerHostCallable`, the
674
+ // engine fires the C `HostDispatchFn`, which schedules the per-callable
675
+ // `ThreadsafeFunction` (built from the wrapper below) onto the libuv event
676
+ // loop with `(callId, argsBytes)`. The wrapper decodes args, invokes the
677
+ // user function, encodes the result (or error), and forwards the result
678
+ // back to the engine via `completeHostCall`.
679
+ //
680
+ // Mirrors the Python bridge's `dispatch_in_python` flow
681
+ // (sdks/python/rust/bridge_python/src/host_value.rs:152), but the Python
682
+ // side calls into `_decode_value_holder` / `_set_inbound_value` directly
683
+ // inside the Rust dispatch callback (under the GIL) rather than going
684
+ // through a JS-side wrapper. Node's tsfn model makes the wrapper natural.
685
+ function makeHostCallableDispatch(userFn) {
686
+ return (callId, argsBytes) => {
687
+ // Every reachable exit from this wrapper must complete `callId`
688
+ // exactly once — if it doesn't, the engine awaits the in-flight call
689
+ // forever (there is no timeout). The outer try/catch is a last-resort
690
+ // net: if anything below throws *after* deciding not to complete (or
691
+ // the normal error path itself throws), we still fire one generic
692
+ // completion. The branches below never both complete and fall
693
+ // through, so we never double-complete.
694
+ try {
695
+ let args;
696
+ try {
697
+ // Host-callable args arrive as a `BamlToHostCall` with a flat
698
+ // `args` list. Partition it into the positional run and the
699
+ // supplied optionals, then reshape into the `$opts` calling
700
+ // convention the callback's type advertises.
701
+ const { positional, optional } = decodeHostCall(argsBytes);
702
+ args = reshapeHostArgs(positional, optional);
703
+ }
704
+ catch (err) {
705
+ sendHostCallableError(callId, err);
706
+ return;
707
+ }
708
+ let result;
709
+ try {
710
+ result = userFn(...args);
711
+ }
712
+ catch (err) {
713
+ sendHostCallableError(callId, err);
714
+ return;
715
+ }
716
+ // Async callables: the wrapper resolves the promise on the libuv
717
+ // loop and then forwards the result. The engine has released its
718
+ // heap permit while awaiting `complete_host_call`, so JS-side
719
+ // delay is safe (mirrors the Python `run_until_complete` model).
720
+ if (isPromiseLike(result)) {
721
+ // Adopt the thenable via `Promise.resolve` rather than calling
722
+ // its `.then` directly: a non-compliant thenable could invoke
723
+ // its callbacks more than once, but `Promise.resolve(...)`
724
+ // collapses to a single settlement, so the call completes
725
+ // exactly once. The handlers only call the defended send*
726
+ // helpers (they can't throw synchronously).
727
+ Promise.resolve(result).then((resolved) => sendHostCallableResult(callId, resolved), (err) => sendHostCallableError(callId, err));
728
+ }
729
+ else {
730
+ sendHostCallableResult(callId, result);
731
+ }
732
+ }
733
+ catch (err) {
734
+ // Reached only if a send* helper or the promise plumbing threw
735
+ // *and* did not complete the call. Last-resort completion.
736
+ completeHostCallLastResort(callId, err);
737
+ }
738
+ };
739
+ }
740
+ function isPromiseLike(value) {
741
+ return (value != null &&
742
+ (typeof value === 'object' || typeof value === 'function') &&
743
+ typeof value.then === 'function');
744
+ }
745
+ function sendHostCallableResult(callId, value) {
746
+ let bytes;
747
+ // Result-encode path (host → engine): no sync guard (we're already on
748
+ // libuv). We do track registrations, though — a callable nested in the
749
+ // result is registered before encoding finishes, and if encoding then
750
+ // throws, the bytes never reach the engine, so it never decodes (and
751
+ // never releases) the callable. Roll those back on failure, mirroring the
752
+ // argument-path rollback in `encodeCallArgs`.
753
+ const ctx = { syncMode: false, registered: [] };
754
+ try {
755
+ const iv = {};
756
+ setInboundValue(iv, value, ctx);
757
+ const msg = InboundValue.create(iv);
758
+ bytes = Buffer.from(InboundValue.encode(msg).finish());
759
+ }
760
+ catch (err) {
761
+ for (const k of ctx.registered) {
762
+ try {
763
+ releaseHostCallable(k);
764
+ }
765
+ catch {
766
+ // Best-effort cleanup; never mask the original error.
767
+ }
768
+ }
769
+ sendHostCallableError(callId, err);
770
+ return;
771
+ }
772
+ completeHostCall(callId, 0, bytes);
773
+ }
774
+ /**
775
+ * Build an `InboundValue` carrying a `baml.errors.HostCallable` Instance with
776
+ * the four metadata fields. The engine's `materialize_host_throw` runs the
777
+ * declared-throws contract check against this value and either re-injects it
778
+ * as a catchable BAML throw or escalates to a `HostContractViolation` panic.
779
+ */
780
+ function buildHostCallableInbound(className, message, traceback, handleKey) {
781
+ const stringField = (key, value) => InboundMapEntry.create({
782
+ stringKey: key,
783
+ value: InboundValue.create({ stringValue: value }),
784
+ });
785
+ const handleField = InboundMapEntry.create({
786
+ stringKey: '_handle',
787
+ value: InboundValue.create({
788
+ handle: {
789
+ key: handleKey,
790
+ handleType: BamlHandleType.HOST_VALUE_OPAQUE,
791
+ },
792
+ }),
793
+ });
794
+ const fields = [
795
+ stringField('message', message),
796
+ stringField('class_name', className),
797
+ stringField('language', 'nodejs'),
798
+ ];
799
+ if (traceback != null) {
800
+ fields.push(stringField('traceback', traceback));
801
+ }
802
+ fields.push(handleField);
803
+ return InboundValue.create({
804
+ classValue: InboundClassValue.create({
805
+ classTy: { name: 'baml.errors.HostCallable' },
806
+ fields,
807
+ }),
808
+ });
809
+ }
810
+ // Sentinel `_handle` key used by paths that have *no* JS error object to
811
+ // register (the `completeHostCallLastResort` fallback below). Real host
812
+ // throws register the JS error via `registerHostOpaque` and emit its
813
+ // minted key; engine-internal synthetic faults use this sentinel. The
814
+ // engine's structural check accepts either; same-host decoders that look
815
+ // up `{low:0,high:0}` find nothing and fall through to the
816
+ // metadata-bearing `BamlError(HostCallable)` wrapper. Mirrors the
817
+ // reserved sentinel in `bridge_python::host_value::next_key` (which
818
+ // skips `0`), so a real registered key can never collide.
819
+ //
820
+ // Cast through `as unknown as HandleKey` because `HandleKey` is a native
821
+ // napi class with private fields; protobufjs only reads the public
822
+ // `{low, high}` shape and the wire serializes them identically.
823
+ const UNRESOLVED_HOST_ERROR_KEY = { low: 0, high: 0 };
824
+ function sendHostCallableError(callId, err) {
825
+ // Normal error path: never leaves the call uncompleted. If building or
826
+ // encoding the Instance throws (e.g. `describeError`, proto `create` /
827
+ // `encode`, or the native `completeHostCall` itself), fall back to the
828
+ // last-resort completion below.
829
+ try {
830
+ // BAML-error-unwrap path: `BamlError(value=<codegenned BAML class>)`.
831
+ // The user wrapped a real BAML class (or primitive) in a BamlError;
832
+ // emit it as that real BAML class on the wire so the BAML caller's
833
+ // typed `catch (e: MyError)` matches structurally and reads typed
834
+ // fields. Mirrors `bridge_python`'s `try_encode_baml_error_throw`.
835
+ //
836
+ // `value == null` (a bare `new BamlError("msg")` with no wrapped
837
+ // value, or `new BamlError("", { value: null })`) falls through to
838
+ // the opaque-handle fallback below: an unset / null inner oneof
839
+ // encodes as BAML `null`, which fails the engine's contract check
840
+ // for any concrete `E` (and produces a bizarre `null` throw for
841
+ // `E=unknown`). The opaque path always produces a well-formed
842
+ // `HostCallable` instance the engine can route.
843
+ //
844
+ // `BamlPanic extends BamlError`, so a `BamlPanic(value=...)` also
845
+ // hits this branch. The engine routes by BAML class namespace
846
+ // (`baml.panics.*` → panic, `baml.errors.*` / user classes →
847
+ // error), so a properly-constructed `BamlPanic(value=<panic class>)`
848
+ // surfaces as a panic; a `BamlPanic(value=<error class>)` surfaces
849
+ // as an error. Treating both via the same wire path is consistent
850
+ // with how engine-internal throws are routed.
851
+ if (err instanceof BamlError && err.value != null) {
852
+ const ctx = { syncMode: false, registered: [] };
853
+ try {
854
+ const iv = InboundValue.create({});
855
+ setInboundValue(iv, err.value, ctx);
856
+ const bytes = Buffer.from(InboundValue.encode(iv).finish());
857
+ completeHostCall(callId, 1, bytes);
858
+ return;
859
+ }
860
+ catch {
861
+ // Encoding the inner value failed — roll back any callable
862
+ // registrations its nested fields triggered and fall through
863
+ // to the opaque-handle path below. The fall-through is
864
+ // intentional: the throw must still reach the engine even if
865
+ // the typed-value encode broke, so we never leave the call
866
+ // hanging. Each release is wrapped in its own try/catch so
867
+ // a single bad entry doesn't abort the rest of the rollback
868
+ // and leak the remaining registrations (mirrors the other
869
+ // rollback sites in `setInboundValue` and
870
+ // `sendHostCallableResult`).
871
+ for (const key of ctx.registered) {
872
+ try {
873
+ releaseHostCallable(key);
874
+ }
875
+ catch {
876
+ // Best-effort cleanup; never mask the original error.
877
+ }
878
+ }
879
+ }
880
+ }
881
+ // Opaque-handle path: a `baml.errors.HostCallable` Instance carrying
882
+ // a handle to the originating JS error so the BAML→host decoder on
883
+ // the same Node process can rehydrate the *same* exception object
884
+ // on round-trip (`raised === caught` identity). Foreign runtimes /
885
+ // released keys fall back to metadata.
886
+ //
887
+ // Identity-loss edge case (acceptable): `describeError` can itself
888
+ // throw when given a hostile input (e.g. a Proxy whose `constructor`,
889
+ // `name`, or `message` getters throw — see the
890
+ // `host-callable always completes on abnormal paths` jest suite).
891
+ // In that case `registerHostOpaque` is never reached, control jumps
892
+ // to the outer `catch (innerErr)` → `completeHostCallLastResort`,
893
+ // and the user sees a metadata-only `HostCallable` instead of the
894
+ // original Proxy. Identity loss here is the right trade — the
895
+ // alternative is hanging the call.
896
+ //
897
+ // Registration-leak edge case (rare, bounded): if encoding succeeds
898
+ // through `registerHostOpaque` but `buildHostCallableInbound` or
899
+ // `InboundValue.encode` fails after, the TS map entry stays alive
900
+ // with no corresponding engine-side `HostValueArc` to release it,
901
+ // so it lives until process exit. Both downstream calls are deeply
902
+ // mechanical (build a proto message, serialize fixed-shape fields)
903
+ // and don't depend on `err`'s shape, so this is effectively
904
+ // unreachable outside protobufjs / native-binding corruption.
905
+ const { className, message, stack } = describeError(err);
906
+ const handleKey = registerHostOpaque(err);
907
+ const inbound = buildHostCallableInbound(className, message, stack, handleKey);
908
+ const bytes = Buffer.from(InboundValue.encode(inbound).finish());
909
+ completeHostCall(callId, 1, bytes);
910
+ }
911
+ catch (innerErr) {
912
+ completeHostCallLastResort(callId, innerErr);
913
+ }
914
+ }
915
+ /**
916
+ * Absolute last-resort completion. Encodes a fixed, minimal
917
+ * `baml.errors.HostCallable` Instance with no dependence on the original
918
+ * error object, so the only ways it can fail are a broken proto runtime or a
919
+ * broken native binding — at which point nothing can complete the call. We
920
+ * swallow any throw here to avoid surfacing an unhandled rejection on the
921
+ * libuv loop; the engine's lack of completion would then be the
922
+ * (unavoidable) failure mode.
923
+ */
924
+ function completeHostCallLastResort(callId, err) {
925
+ try {
926
+ const inbound = buildHostCallableInbound('InternalError', `host callable dispatch failed and the error could not be reported: ${safeStringify(err)}`, undefined, UNRESOLVED_HOST_ERROR_KEY);
927
+ const bytes = Buffer.from(InboundValue.encode(inbound).finish());
928
+ completeHostCall(callId, 1, bytes);
929
+ }
930
+ catch (innerErr) {
931
+ // Nothing more we can safely do — a throw here would surface as an
932
+ // unhandled rejection on the libuv loop. The engine's lack of
933
+ // completion will then be the (unavoidable) failure mode; log so the
934
+ // failure is at least attributable.
935
+ console.error('BAML internal: last-resort host-call completion failed; call will hang.', { callId, originalError: safeStringify(err), lastResortError: innerErr });
936
+ }
937
+ }
938
+ /** `String(err)` that cannot itself throw (e.g. a Proxy with a throwing
939
+ * `toString`). */
940
+ function safeStringify(err) {
941
+ try {
942
+ return String(err);
943
+ }
944
+ catch {
945
+ return '<unstringifiable error>';
946
+ }
947
+ }
948
+ function describeError(err) {
949
+ if (err instanceof Error) {
950
+ return {
951
+ className: err.name || err.constructor.name || 'Error',
952
+ message: err.message || String(err),
953
+ stack: err.stack ?? undefined,
954
+ };
955
+ }
956
+ if (err != null && typeof err === 'object') {
957
+ const ctor = err.constructor;
958
+ const className = ctor?.name && ctor.name !== 'Object' ? ctor.name : 'Error';
959
+ return { className, message: String(err), stack: undefined };
960
+ }
961
+ return { className: 'Error', message: String(err), stack: undefined };
962
+ }
963
+ //# sourceMappingURL=proto.js.map