tywrap 0.8.0 → 0.10.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 (159) hide show
  1. package/README.md +42 -22
  2. package/SECURITY.md +38 -0
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/index.js +8 -0
  5. package/dist/config/index.js.map +1 -1
  6. package/dist/core/annotation-parser.d.ts +2 -1
  7. package/dist/core/annotation-parser.d.ts.map +1 -1
  8. package/dist/core/annotation-parser.js +6 -3
  9. package/dist/core/annotation-parser.js.map +1 -1
  10. package/dist/core/generator.d.ts +23 -4
  11. package/dist/core/generator.d.ts.map +1 -1
  12. package/dist/core/generator.js +254 -170
  13. package/dist/core/generator.js.map +1 -1
  14. package/dist/core/mapper.d.ts +3 -2
  15. package/dist/core/mapper.d.ts.map +1 -1
  16. package/dist/core/mapper.js +5 -5
  17. package/dist/core/mapper.js.map +1 -1
  18. package/dist/index.d.ts +7 -5
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +6 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/runtime/base-bridge.d.ts +3 -7
  23. package/dist/runtime/base-bridge.d.ts.map +1 -1
  24. package/dist/runtime/base-bridge.js +4 -17
  25. package/dist/runtime/base-bridge.js.map +1 -1
  26. package/dist/runtime/bounded-context.d.ts +3 -22
  27. package/dist/runtime/bounded-context.d.ts.map +1 -1
  28. package/dist/runtime/bounded-context.js +13 -53
  29. package/dist/runtime/bounded-context.js.map +1 -1
  30. package/dist/runtime/bridge-codec.d.ts +3 -3
  31. package/dist/runtime/bridge-codec.d.ts.map +1 -1
  32. package/dist/runtime/bridge-codec.js +129 -62
  33. package/dist/runtime/bridge-codec.js.map +1 -1
  34. package/dist/runtime/errors.d.ts +16 -0
  35. package/dist/runtime/errors.d.ts.map +1 -1
  36. package/dist/runtime/errors.js +17 -0
  37. package/dist/runtime/errors.js.map +1 -1
  38. package/dist/runtime/http-transport.d.ts +1 -1
  39. package/dist/runtime/http-transport.d.ts.map +1 -1
  40. package/dist/runtime/http-transport.js +1 -1
  41. package/dist/runtime/http-transport.js.map +1 -1
  42. package/dist/runtime/index.d.ts +1 -0
  43. package/dist/runtime/index.d.ts.map +1 -1
  44. package/dist/runtime/index.js +1 -0
  45. package/dist/runtime/index.js.map +1 -1
  46. package/dist/runtime/node.d.ts +7 -36
  47. package/dist/runtime/node.d.ts.map +1 -1
  48. package/dist/runtime/node.js +2 -80
  49. package/dist/runtime/node.js.map +1 -1
  50. package/dist/runtime/pooled-transport.d.ts +120 -59
  51. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  52. package/dist/runtime/pooled-transport.js +345 -78
  53. package/dist/runtime/pooled-transport.js.map +1 -1
  54. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  55. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  56. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  57. package/dist/runtime/pyodide-transport.d.ts +1 -1
  58. package/dist/runtime/pyodide-transport.d.ts.map +1 -1
  59. package/dist/runtime/pyodide-transport.js +2 -3
  60. package/dist/runtime/pyodide-transport.js.map +1 -1
  61. package/dist/runtime/rpc-client.d.ts +7 -36
  62. package/dist/runtime/rpc-client.d.ts.map +1 -1
  63. package/dist/runtime/rpc-client.js +20 -102
  64. package/dist/runtime/rpc-client.js.map +1 -1
  65. package/dist/runtime/subprocess-transport.d.ts +30 -69
  66. package/dist/runtime/subprocess-transport.d.ts.map +1 -1
  67. package/dist/runtime/subprocess-transport.js +179 -236
  68. package/dist/runtime/subprocess-transport.js.map +1 -1
  69. package/dist/runtime/timed-out-request-tracker.d.ts +2 -1
  70. package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -1
  71. package/dist/runtime/transport.d.ts +9 -19
  72. package/dist/runtime/transport.d.ts.map +1 -1
  73. package/dist/runtime/transport.js +1 -1
  74. package/dist/runtime/transport.js.map +1 -1
  75. package/dist/runtime/validators.d.ts +50 -0
  76. package/dist/runtime/validators.d.ts.map +1 -1
  77. package/dist/runtime/validators.js +154 -0
  78. package/dist/runtime/validators.js.map +1 -1
  79. package/dist/types/index.d.ts +14 -39
  80. package/dist/types/index.d.ts.map +1 -1
  81. package/dist/tywrap.d.ts +3 -2
  82. package/dist/tywrap.d.ts.map +1 -1
  83. package/dist/tywrap.js +140 -13
  84. package/dist/tywrap.js.map +1 -1
  85. package/dist/utils/cache.d.ts +3 -16
  86. package/dist/utils/cache.d.ts.map +1 -1
  87. package/dist/utils/codec.d.ts +17 -0
  88. package/dist/utils/codec.d.ts.map +1 -1
  89. package/dist/utils/codec.js +500 -85
  90. package/dist/utils/codec.js.map +1 -1
  91. package/dist/utils/ir-cache.d.ts +2 -1
  92. package/dist/utils/ir-cache.d.ts.map +1 -1
  93. package/dist/utils/runtime.d.ts +0 -29
  94. package/dist/utils/runtime.d.ts.map +1 -1
  95. package/dist/utils/runtime.js +16 -107
  96. package/dist/utils/runtime.js.map +1 -1
  97. package/dist/version.d.ts.map +1 -1
  98. package/dist/version.js +1 -1
  99. package/dist/version.js.map +1 -1
  100. package/package.json +7 -6
  101. package/runtime/frame_codec.py +7 -1
  102. package/runtime/python_bridge.py +85 -125
  103. package/runtime/tywrap_bridge_core.py +515 -187
  104. package/src/config/index.ts +11 -0
  105. package/src/core/annotation-parser.ts +7 -4
  106. package/src/core/generator.ts +330 -205
  107. package/src/core/mapper.ts +8 -8
  108. package/src/index.ts +7 -4
  109. package/src/runtime/base-bridge.ts +5 -30
  110. package/src/runtime/bounded-context.ts +12 -67
  111. package/src/runtime/bridge-codec.ts +158 -74
  112. package/src/runtime/errors.ts +21 -0
  113. package/src/runtime/http-transport.ts +6 -1
  114. package/src/runtime/index.ts +6 -0
  115. package/src/runtime/node.ts +9 -120
  116. package/src/runtime/pooled-transport.ts +424 -90
  117. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  118. package/src/runtime/pyodide-transport.ts +7 -3
  119. package/src/runtime/rpc-client.ts +37 -139
  120. package/src/runtime/subprocess-transport.ts +210 -285
  121. package/src/runtime/timed-out-request-tracker.ts +1 -1
  122. package/src/runtime/transport.ts +15 -23
  123. package/src/runtime/validators.ts +212 -0
  124. package/src/types/index.ts +21 -60
  125. package/src/tywrap.ts +157 -22
  126. package/src/utils/cache.ts +3 -3
  127. package/src/utils/codec.ts +749 -112
  128. package/src/utils/ir-cache.ts +1 -1
  129. package/src/utils/runtime.ts +17 -128
  130. package/src/version.ts +1 -1
  131. package/dist/core/discovery.d.ts +0 -103
  132. package/dist/core/discovery.d.ts.map +0 -1
  133. package/dist/core/discovery.js +0 -380
  134. package/dist/core/discovery.js.map +0 -1
  135. package/dist/core/validation.d.ts +0 -102
  136. package/dist/core/validation.d.ts.map +0 -1
  137. package/dist/core/validation.js +0 -490
  138. package/dist/core/validation.js.map +0 -1
  139. package/dist/runtime/base.d.ts +0 -22
  140. package/dist/runtime/base.d.ts.map +0 -1
  141. package/dist/runtime/base.js +0 -23
  142. package/dist/runtime/base.js.map +0 -1
  143. package/dist/runtime/transport-pool.d.ts +0 -196
  144. package/dist/runtime/transport-pool.d.ts.map +0 -1
  145. package/dist/runtime/transport-pool.js +0 -418
  146. package/dist/runtime/transport-pool.js.map +0 -1
  147. package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
  148. package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
  149. package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
  150. package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
  151. package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
  152. package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
  153. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  154. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  155. package/runtime/safe_codec.py +0 -352
  156. package/src/core/discovery.ts +0 -477
  157. package/src/core/validation.ts +0 -729
  158. package/src/runtime/base.ts +0 -24
  159. package/src/runtime/transport-pool.ts +0 -538
@@ -8,6 +8,35 @@
8
8
  * Torch tensors: { "__tywrap__": "torch.tensor", "encoding": "ndarray", "value": { ... } }
9
9
  * Sklearn estimators: { "__tywrap__": "sklearn.estimator", "encoding": "json", ... }
10
10
  */
11
+ import { tagDecodedShape } from '../runtime/validators.js';
12
+ const SCIENTIFIC_MARKERS = [
13
+ 'dataframe',
14
+ 'series',
15
+ 'ndarray',
16
+ 'scipy.sparse',
17
+ 'torch.tensor',
18
+ 'sklearn.estimator',
19
+ ];
20
+ export function isScientificMarker(value) {
21
+ return SCIENTIFIC_MARKERS.includes(value);
22
+ }
23
+ /** @internal Structured failure consumed by BridgeCodec without parsing messages. */
24
+ export class ScientificDecodeError extends Error {
25
+ kind;
26
+ marker;
27
+ cause;
28
+ constructor(kind, marker, cause, message = cause instanceof Error ? cause.message : String(cause)) {
29
+ super(message, { cause });
30
+ this.kind = kind;
31
+ this.marker = marker;
32
+ this.cause = cause;
33
+ }
34
+ }
35
+ function asScientificDecodeError(error, marker) {
36
+ return error instanceof ScientificDecodeError
37
+ ? error
38
+ : new ScientificDecodeError('envelope', marker, error);
39
+ }
11
40
  let arrowTableFrom;
12
41
  // Why: lazy auto-registration (on first Arrow decode) imports apache-arrow at most
13
42
  // once per process. We cache the in-flight/settled attempt so concurrent decodes
@@ -109,7 +138,12 @@ function isObject(value) {
109
138
  function isNumberArray(value) {
110
139
  return Array.isArray(value) && value.every(item => typeof item === 'number');
111
140
  }
112
- function fromBase64(b64) {
141
+ const STRICT_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
142
+ function fromBase64(b64, marker, context) {
143
+ if (marker && !STRICT_BASE64_PATTERN.test(b64)) {
144
+ throw new Error(`Invalid ${marker} envelope: b64 at path b64 must be well-formed base64; ` +
145
+ `${context ? `${context}; ` : ''}actual count unknown, actual type string with length ${b64.length}`);
146
+ }
113
147
  if (typeof Buffer !== 'undefined') {
114
148
  const buf = Buffer.from(b64, 'base64');
115
149
  return new Uint8Array(buf.buffer, buf.byteOffset, buf.length);
@@ -127,9 +161,9 @@ const ARROW_MISSING_MESSAGE = 'Received an Arrow-encoded payload but no Arrow de
127
161
  'Install the optional dependency with `npm install apache-arrow`, or set ' +
128
162
  'TYWRAP_CODEC_FALLBACK=json on the Python side to receive JSON instead ' +
129
163
  '(lossy for dtype/NA fidelity). tywrap never silently downgrades Arrow payloads.';
130
- function requireArrowDecoder() {
164
+ function requireArrowDecoder(marker) {
131
165
  if (!arrowTableFrom) {
132
- throw new Error(ARROW_MISSING_MESSAGE);
166
+ throw new ScientificDecodeError('arrow', marker, new Error(ARROW_MISSING_MESSAGE));
133
167
  }
134
168
  return arrowTableFrom;
135
169
  }
@@ -141,7 +175,7 @@ function requireArrowDecoder() {
141
175
  * for the rest of the process. If apache-arrow is absent we throw a clear, actionable
142
176
  * error rather than silently producing wrong data.
143
177
  */
144
- async function ensureArrowDecoder() {
178
+ async function ensureArrowDecoder(marker) {
145
179
  if (arrowTableFrom) {
146
180
  return arrowTableFrom;
147
181
  }
@@ -149,17 +183,17 @@ async function ensureArrowDecoder() {
149
183
  lazyRegistration ??= autoRegisterArrowDecoder(lazyArrowLoaderOverride ? { loader: lazyArrowLoaderOverride } : {});
150
184
  await lazyRegistration;
151
185
  if (!arrowTableFrom) {
152
- throw new Error(ARROW_MISSING_MESSAGE);
186
+ throw new ScientificDecodeError('arrow', marker, new Error(ARROW_MISSING_MESSAGE));
153
187
  }
154
188
  return arrowTableFrom;
155
189
  }
156
- async function tryDecodeArrowTable(bytes) {
157
- const decoder = await ensureArrowDecoder();
190
+ async function tryDecodeArrowTable(bytes, marker) {
191
+ const decoder = await ensureArrowDecoder(marker);
158
192
  try {
159
193
  return decoder(bytes);
160
194
  }
161
195
  catch (err) {
162
- throw new Error(`Arrow decode failed: ${err instanceof Error ? err.message : String(err)}`);
196
+ throw new ScientificDecodeError('arrow', marker, err, `Arrow decode failed: ${err instanceof Error ? err.message : String(err)}`);
163
197
  }
164
198
  }
165
199
  function isPromiseLike(value) {
@@ -198,8 +232,7 @@ function typedArrayToPlain(arr) {
198
232
  if (arr !== null && arr !== undefined && typeof arr === 'object' && Symbol.iterator in arr) {
199
233
  return Array.from(arr);
200
234
  }
201
- // Non-iterable: return empty array (shouldn't happen with valid Arrow data)
202
- return [];
235
+ return null;
203
236
  }
204
237
  /**
205
238
  * Extract values from an Arrow table as a plain JavaScript array.
@@ -221,6 +254,22 @@ function extractArrowValues(data) {
221
254
  }
222
255
  return null;
223
256
  }
257
+ function extractNdarrayArrowValues(data, shape, dtype) {
258
+ try {
259
+ const values = extractArrowValues(data);
260
+ if (values) {
261
+ return values;
262
+ }
263
+ }
264
+ catch (error) {
265
+ throw new Error(`Invalid ndarray envelope: b64 decoded data extraction failed at path b64 for declared ` +
266
+ `shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; actual count unknown, ` +
267
+ `actual type ${actualType(data)} (${error instanceof Error ? error.message : String(error)})`);
268
+ }
269
+ throw new Error(`Invalid ndarray envelope: b64 decoded data at path b64 could not extract Arrow values ` +
270
+ `for declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
271
+ `actual count unknown, actual type ${actualType(data)}`);
272
+ }
224
273
  /**
225
274
  * Reshape a flat array into a multi-dimensional nested array.
226
275
  *
@@ -255,6 +304,111 @@ function reshapeArray(flat, shape) {
255
304
  }
256
305
  // Why: decoding needs to reject incompatible envelopes before we attempt to interpret payloads.
257
306
  const CODEC_VERSION = 1;
307
+ function isStrictV1Envelope(envelope) {
308
+ return envelope.codecVersion === CODEC_VERSION;
309
+ }
310
+ function actualType(value) {
311
+ if (Array.isArray(value)) {
312
+ return `array(length=${value.length})`;
313
+ }
314
+ if (value === null) {
315
+ return 'null';
316
+ }
317
+ if (typeof value === 'number' && !Number.isFinite(value)) {
318
+ return `number(${String(value)})`;
319
+ }
320
+ return typeof value;
321
+ }
322
+ function assertShape(shapeValue, marker, field = 'shape', dtype) {
323
+ if (!Array.isArray(shapeValue)) {
324
+ throw new Error(`Invalid ${marker} envelope: ${field} at path ${field} must be a list of ` +
325
+ `non-negative safe integers; declared shape ${String(shapeValue)} and dtype ` +
326
+ `${JSON.stringify(dtype)}, actual count unknown, actual type ${actualType(shapeValue)}`);
327
+ }
328
+ for (let i = 0; i < shapeValue.length; i += 1) {
329
+ const dim = shapeValue[i];
330
+ if (typeof dim !== 'number' || !Number.isSafeInteger(dim) || dim < 0) {
331
+ throw new Error(`Invalid ${marker} envelope: ${field}[${i}]=${String(dim)} must be a non-negative integer ` +
332
+ `within the safe range for declared shape ${JSON.stringify(shapeValue)}; ` +
333
+ `declared dtype ${JSON.stringify(dtype)}, actual count unknown, ` +
334
+ `actual type ${actualType(dim)} with value ${String(dim)}`);
335
+ }
336
+ }
337
+ return shapeValue;
338
+ }
339
+ function assertOptionalNonEmptyDtype(value, marker, field = 'dtype', shape) {
340
+ if (value === undefined) {
341
+ return undefined;
342
+ }
343
+ if (typeof value !== 'string' || value.length === 0) {
344
+ throw new Error(`Invalid ${marker} envelope: ${field} at path ${field} must be a non-empty string; ` +
345
+ `declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(value)}, ` +
346
+ `actual count unknown, actual type ${actualType(value)}`);
347
+ }
348
+ return value;
349
+ }
350
+ function readEnvelopeShape(envelope, shapeValue, marker, field = 'shape', dtype) {
351
+ return isStrictV1Envelope(envelope)
352
+ ? assertShape(shapeValue, marker, field, dtype)
353
+ : isNumberArray(shapeValue)
354
+ ? shapeValue
355
+ : undefined;
356
+ }
357
+ function readEnvelopeDtype(envelope, dtypeValue, marker, field = 'dtype', shape) {
358
+ return isStrictV1Envelope(envelope)
359
+ ? assertOptionalNonEmptyDtype(dtypeValue, marker, field, shape)
360
+ : typeof dtypeValue === 'string'
361
+ ? dtypeValue
362
+ : undefined;
363
+ }
364
+ function countJsonLeaves(value) {
365
+ if (!Array.isArray(value)) {
366
+ return 1;
367
+ }
368
+ return value.reduce((count, item) => count + countJsonLeaves(item), 0);
369
+ }
370
+ function assertJsonMatchesShape(data, shape, dtype) {
371
+ const declaredCount = shapeProduct(shape);
372
+ const strictLeaves = dtype !== undefined;
373
+ const actualCount = strictLeaves ? countJsonLeaves(data) : undefined;
374
+ const visit = (value, depth, path) => {
375
+ if (depth === shape.length) {
376
+ if (strictLeaves && Array.isArray(value)) {
377
+ throw new Error(`Invalid ndarray envelope: data at path ${path} exceeds nesting depth ` +
378
+ `${shape.length} for declared shape ${JSON.stringify(shape)} and dtype ` +
379
+ `${JSON.stringify(dtype)}; actual count ${actualCount}, actual type ${actualType(value)}`);
380
+ }
381
+ return;
382
+ }
383
+ if (!Array.isArray(value)) {
384
+ throw new Error(`Invalid ndarray envelope: data at path ${path} must be an array at depth ${depth} ` +
385
+ `for declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
386
+ `actual count ${actualCount ?? 'unknown'}, actual type ${actualType(value)}`);
387
+ }
388
+ const expectedLength = shape[depth];
389
+ if (value.length !== expectedLength) {
390
+ throw new Error(`Invalid ndarray envelope: data at path ${path} has length ${value.length}, expected ` +
391
+ `${expectedLength} for declared shape ${JSON.stringify(shape)} and dtype ` +
392
+ `${JSON.stringify(dtype)}; declared count ${declaredCount}, actual count ${actualCount ?? 'unknown'}, ` +
393
+ `actual type ${actualType(value)}`);
394
+ }
395
+ value.forEach((item, index) => visit(item, depth + 1, `${path}[${index}]`));
396
+ };
397
+ visit(data, 0, 'data');
398
+ if (strictLeaves && actualCount !== declaredCount) {
399
+ throw new Error(`Invalid ndarray envelope: data at path data has leaf count ${actualCount}, expected ` +
400
+ `${declaredCount} for declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
401
+ `actual type ${actualType(data)}`);
402
+ }
403
+ }
404
+ function assertArrowCount(values, shape, dtype) {
405
+ const expected = shapeProduct(shape);
406
+ if (values.length !== expected) {
407
+ throw new Error(`Invalid ndarray envelope: b64 decoded data at path b64 has element count ${values.length}, ` +
408
+ `expected ${expected} for declared shape ${JSON.stringify(shape)} and dtype ` +
409
+ `${JSON.stringify(dtype)}; actual type array(length=${values.length})`);
410
+ }
411
+ }
258
412
  function assertCodecVersion(envelope, typeTag) {
259
413
  if (!('codecVersion' in envelope)) {
260
414
  return;
@@ -283,62 +437,77 @@ function decodeArrowOrJsonEnvelope(value, decodeArrow, typeTag) {
283
437
  if (typeof b64 !== 'string') {
284
438
  throw new Error(`Invalid ${typeTag} envelope: missing b64`);
285
439
  }
286
- const bytes = fromBase64(b64);
287
- return decodeArrow(bytes);
440
+ const bytes = fromBase64(b64, isStrictV1Envelope(value) ? typeTag : undefined);
441
+ const decoded = decodeArrow(bytes, typeTag);
442
+ return isPromiseLike(decoded)
443
+ ? decoded.then(item => tagDecodedShape(item, { marker: typeTag }))
444
+ : tagDecodedShape(decoded, { marker: typeTag });
288
445
  }
289
446
  if (encoding === 'json') {
290
447
  if (!('data' in value)) {
291
448
  throw new Error(`Invalid ${typeTag} envelope: missing data`);
292
449
  }
293
- return value.data;
450
+ return tagDecodedShape(value.data, { marker: typeTag });
294
451
  }
295
452
  throw new Error(`Invalid ${typeTag} envelope: unsupported encoding ${String(encoding)}`);
296
453
  }
297
454
  const decodeDataframeEnvelope = (value, decodeArrow) => decodeArrowOrJsonEnvelope(value, decodeArrow, 'dataframe');
298
455
  const decodeSeriesEnvelope = (value, decodeArrow) => decodeArrowOrJsonEnvelope(value, decodeArrow, 'series');
456
+ function finishNdarrayDecode(data, strictV1, shape, dtype, metadata) {
457
+ const values = strictV1
458
+ ? extractNdarrayArrowValues(data, shape, dtype)
459
+ : extractArrowValues(data);
460
+ if (!values) {
461
+ return tagDecodedShape(data, metadata);
462
+ }
463
+ if (strictV1) {
464
+ assertArrowCount(values, shape, dtype);
465
+ }
466
+ return tagDecodedShape(shape && shape.length !== 1 ? reshapeArray(values, shape) : values, metadata);
467
+ }
299
468
  const decodeNdarrayEnvelope = (value, decodeArrow) => {
300
469
  const encoding = value.encoding;
301
- const shapeValue = value.shape;
302
- const shape = isNumberArray(shapeValue) ? shapeValue : undefined;
470
+ const strictV1 = isStrictV1Envelope(value);
471
+ const shape = readEnvelopeShape(value, value.shape, 'ndarray', 'shape', value.dtype);
472
+ const dtype = readEnvelopeDtype(value, value.dtype, 'ndarray', 'dtype', shape);
473
+ const metadata = { marker: 'ndarray', dims: shape?.length, dtype };
303
474
  if (encoding === 'arrow') {
475
+ if (strictV1 && dtype === undefined) {
476
+ throw new Error(`Invalid ndarray envelope: dtype at path dtype is required for Arrow encoding; ` +
477
+ `declared shape ${JSON.stringify(shape)} and dtype undefined, actual count unknown, actual type undefined`);
478
+ }
304
479
  const b64 = value.b64;
305
480
  if (typeof b64 !== 'string') {
306
- throw new Error('Invalid ndarray envelope: missing b64');
481
+ if (!strictV1) {
482
+ throw new Error('Invalid ndarray envelope: missing b64');
483
+ }
484
+ throw new Error(`Invalid ndarray envelope: b64 at path b64 must be a base64 string for declared ` +
485
+ `shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; actual count unknown, ` +
486
+ `actual type ${actualType(b64)}`);
307
487
  }
308
- const bytes = fromBase64(b64);
309
- const decoded = decodeArrow(bytes);
488
+ const bytes = fromBase64(b64, strictV1 ? 'ndarray' : undefined, `declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}`);
489
+ const decoded = decodeArrow(bytes, 'ndarray');
310
490
  // Extract values from Arrow table and reshape if needed
311
491
  // Arrow only handles 1D arrays, so we flatten on encode and reshape here
312
492
  // Reshape for: scalars (shape.length === 0) and multi-dim (shape.length > 1)
313
493
  // Skip reshape for: 1D arrays (shape.length === 1) - return as-is
314
494
  if (isPromiseLike(decoded)) {
315
- return decoded.then(data => {
316
- const values = extractArrowValues(data);
317
- if (!values) {
318
- return data; // Fallback: return raw data if extraction fails
319
- }
320
- // Reshape scalars and multi-dimensional arrays, but not 1D
321
- if (shape && shape.length !== 1) {
322
- return reshapeArray(values, shape);
323
- }
324
- return values;
325
- });
326
- }
327
- const values = extractArrowValues(decoded);
328
- if (!values) {
329
- return decoded; // Fallback: return raw data if extraction fails
330
- }
331
- // Reshape scalars and multi-dimensional arrays, but not 1D
332
- if (shape && shape.length !== 1) {
333
- return reshapeArray(values, shape);
495
+ return decoded.then(data => finishNdarrayDecode(data, strictV1, shape, dtype, metadata));
334
496
  }
335
- return values;
497
+ return finishNdarrayDecode(decoded, strictV1, shape, dtype, metadata);
336
498
  }
337
499
  if (encoding === 'json') {
338
500
  if (!('data' in value)) {
339
- throw new Error('Invalid ndarray envelope: missing data');
501
+ if (!strictV1) {
502
+ throw new Error('Invalid ndarray envelope: missing data');
503
+ }
504
+ throw new Error(`Invalid ndarray envelope: data at path data is required for declared shape ` +
505
+ `${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; actual count 0, actual type undefined`);
506
+ }
507
+ if (strictV1) {
508
+ assertJsonMatchesShape(value.data, shape, dtype);
340
509
  }
341
- return value.data;
510
+ return tagDecodedShape(value.data, metadata);
342
511
  }
343
512
  throw new Error(`Invalid ndarray envelope: unsupported encoding ${String(encoding)}`);
344
513
  };
@@ -362,6 +531,43 @@ function assertIndexArrayInRange(arr, bound, label) {
362
531
  }
363
532
  }
364
533
  }
534
+ function assertSparseDataDomain(data, dtype, shape) {
535
+ const expectsBoolean = dtype === 'bool' || dtype === 'bool_';
536
+ const integerMatch = /^(u?int)(\d*)$/.exec(dtype);
537
+ const expectsInteger = integerMatch !== null;
538
+ const expectsFloat = /^float\d*$/.test(dtype);
539
+ const width = integerMatch?.[2] ? Number(integerMatch[2]) : undefined;
540
+ const unsignedInteger = integerMatch?.[1] === 'uint';
541
+ let integerMin;
542
+ let integerMax;
543
+ if (width === 8 || width === 16 || width === 32) {
544
+ integerMin = unsignedInteger ? 0 : -(2 ** (width - 1));
545
+ integerMax = unsignedInteger ? 2 ** width - 1 : 2 ** (width - 1) - 1;
546
+ }
547
+ for (let i = 0; i < data.length; i += 1) {
548
+ const item = data[i];
549
+ if (typeof item === 'number' && !Number.isFinite(item)) {
550
+ throw new Error(`Invalid scipy.sparse envelope: data[${i}] at path data[${i}] must be finite for ` +
551
+ `declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
552
+ `actual count ${data.length}, actual type number with value ${String(item)}`);
553
+ }
554
+ const valid = expectsBoolean
555
+ ? typeof item === 'boolean'
556
+ : expectsInteger
557
+ ? typeof item === 'number' &&
558
+ Number.isInteger(item) &&
559
+ (integerMin === undefined || item >= integerMin) &&
560
+ (integerMax === undefined || item <= integerMax)
561
+ : expectsFloat
562
+ ? typeof item === 'number'
563
+ : true;
564
+ if (!valid) {
565
+ throw new Error(`Invalid scipy.sparse envelope: data[${i}] at path data[${i}] is incompatible with ` +
566
+ `declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
567
+ `actual count ${data.length}, actual type ${actualType(item)} with value ${String(item)}`);
568
+ }
569
+ }
570
+ }
365
571
  const decodeScipySparseEnvelope = value => {
366
572
  const encoding = value.encoding;
367
573
  if (encoding !== 'json') {
@@ -371,7 +577,8 @@ const decodeScipySparseEnvelope = value => {
371
577
  if (format !== 'csr' && format !== 'csc' && format !== 'coo') {
372
578
  throw new Error(`Invalid scipy.sparse envelope: unsupported format ${String(format)}`);
373
579
  }
374
- const shape = value.shape;
580
+ const strictV1 = isStrictV1Envelope(value);
581
+ const shape = readEnvelopeShape(value, value.shape, 'scipy.sparse', 'shape', value.dtype);
375
582
  if (!Array.isArray(shape) ||
376
583
  shape.length !== 2 ||
377
584
  typeof shape[0] !== 'number' ||
@@ -388,8 +595,14 @@ const decodeScipySparseEnvelope = value => {
388
595
  if (!Array.isArray(data)) {
389
596
  throw new Error('Invalid scipy.sparse envelope: data must be an array');
390
597
  }
391
- const dtypeValue = value.dtype;
392
- const dtype = typeof dtypeValue === 'string' ? dtypeValue : undefined;
598
+ const dtype = readEnvelopeDtype(value, value.dtype, 'scipy.sparse', 'dtype', shape);
599
+ if (strictV1 && dtype === undefined) {
600
+ throw new Error(`Invalid scipy.sparse envelope: dtype at path dtype is required for declared shape ` +
601
+ `${JSON.stringify(shape)}; actual count ${data.length}, actual type undefined`);
602
+ }
603
+ if (strictV1) {
604
+ assertSparseDataDomain(data, dtype, shape);
605
+ }
393
606
  if (format === 'coo') {
394
607
  const row = value.row;
395
608
  const col = value.col;
@@ -404,14 +617,14 @@ const decodeScipySparseEnvelope = value => {
404
617
  }
405
618
  assertIndexArrayInRange(row, rows, 'row');
406
619
  assertIndexArrayInRange(col, cols, 'col');
407
- return {
620
+ return tagDecodedShape({
408
621
  format,
409
622
  shape,
410
623
  data,
411
624
  row,
412
625
  col,
413
626
  dtype,
414
- };
627
+ }, { marker: 'scipy.sparse', dims: shape.length, dtype });
415
628
  }
416
629
  const indices = value.indices;
417
630
  const indptr = value.indptr;
@@ -454,14 +667,14 @@ const decodeScipySparseEnvelope = value => {
454
667
  throw new Error(`Invalid scipy.sparse envelope: indptr must end at data.length (${data.length})`);
455
668
  }
456
669
  assertIndexArrayInRange(indices, minorAxis, 'indices');
457
- return {
670
+ return tagDecodedShape({
458
671
  format,
459
672
  shape,
460
673
  data,
461
674
  indices,
462
675
  indptr,
463
676
  dtype,
464
- };
677
+ }, { marker: 'scipy.sparse', dims: shape.length, dtype });
465
678
  };
466
679
  /** Product of a shape's dimensions (the element count). [] (scalar) -> 1. */
467
680
  function shapeProduct(shape) {
@@ -479,40 +692,67 @@ const decodeTorchTensorEnvelope = (value, _decodeArrow, recurse) => {
479
692
  if (!isObject(nested) || nested.__tywrap__ !== 'ndarray') {
480
693
  throw new Error('Invalid torch.tensor envelope: value must be an ndarray envelope');
481
694
  }
482
- const shapeValue = value.shape;
483
- const shape = isNumberArray(shapeValue) ? shapeValue : undefined;
484
- // The tensor shape must be a non-negative-integer dimension list. A negative or
485
- // non-integer dim is a corrupt envelope, not a valid tensor.
486
- if (shape) {
487
- for (let i = 0; i < shape.length; i += 1) {
488
- const dim = shape[i];
489
- if (!Number.isInteger(dim) || dim < 0) {
490
- throw new Error(`Invalid torch.tensor envelope: shape[${i}]=${dim} must be a non-negative integer`);
491
- }
492
- }
493
- }
494
- // Cross-check the tensor shape's element count against the nested ndarray's
495
- // declared shape (metadata only — no decode needed). A mismatch means the two
496
- // shapes disagree about how many elements the payload holds.
695
+ const strictV1 = isStrictV1Envelope(value);
696
+ const shape = readEnvelopeShape(value, value.shape, 'torch.tensor', 'shape', value.dtype);
497
697
  const nestedShapeValue = nested.shape;
498
- const nestedShape = isNumberArray(nestedShapeValue) ? nestedShapeValue : undefined;
499
- if (shape && nestedShape && shapeProduct(shape) !== shapeProduct(nestedShape)) {
500
- throw new Error(`Invalid torch.tensor envelope: shape ${JSON.stringify(shape)} ` +
501
- `(product ${shapeProduct(shape)}) disagrees with nested ndarray shape ` +
502
- `${JSON.stringify(nestedShape)} (product ${shapeProduct(nestedShape)})`);
503
- }
504
- const dtypeValue = value.dtype;
505
- const dtype = typeof dtypeValue === 'string' ? dtypeValue : undefined;
698
+ const nestedShape = readEnvelopeShape(value, nestedShapeValue, 'torch.tensor', 'value.shape', nested.dtype);
699
+ const scalarNormalization = shape !== undefined &&
700
+ nestedShape !== undefined &&
701
+ ((shape.length === 0 && nestedShape.length === 1 && nestedShape[0] === 1) ||
702
+ (nestedShape.length === 0 && shape.length === 1 && shape[0] === 1));
703
+ const shapesEqual = shape !== undefined &&
704
+ nestedShape !== undefined &&
705
+ shape.length === nestedShape.length &&
706
+ shape.every((dim, index) => dim === nestedShape[index]);
707
+ const shapesDisagree = strictV1
708
+ ? !shapesEqual && !scalarNormalization
709
+ : shape !== undefined &&
710
+ nestedShape !== undefined &&
711
+ shapeProduct(shape) !== shapeProduct(nestedShape);
712
+ if (shape && nestedShape && shapesDisagree) {
713
+ throw new Error(`Invalid torch.tensor envelope: shape at path shape ${JSON.stringify(shape)} disagrees ` +
714
+ `with nested ndarray shape at path value.shape ${JSON.stringify(nestedShape)}; exact shapes are required ` +
715
+ `(except scalar []/[1]), declared dtypes ${JSON.stringify(value.dtype)}/${JSON.stringify(nested.dtype)}, ` +
716
+ `actual counts ${shapeProduct(shape)} and ${shapeProduct(nestedShape)}`);
717
+ }
718
+ const dtype = readEnvelopeDtype(value, value.dtype, 'torch.tensor', 'dtype', shape);
719
+ if (strictV1 && dtype === undefined) {
720
+ throw new Error(`Invalid torch.tensor envelope: dtype at path dtype is required for declared shape ` +
721
+ `${JSON.stringify(shape)}; actual count ${shapeProduct(shape)}, actual type undefined`);
722
+ }
723
+ if (strictV1 && 'dtype' in nested) {
724
+ assertOptionalNonEmptyDtype(nested.dtype, 'torch.tensor', 'value.dtype', nestedShape);
725
+ }
506
726
  const deviceValue = value.device;
507
727
  if (deviceValue !== undefined && (typeof deviceValue !== 'string' || deviceValue.length === 0)) {
508
728
  throw new Error('Invalid torch.tensor envelope: device must be a non-empty string when provided');
509
729
  }
510
730
  const device = typeof deviceValue === 'string' ? deviceValue : undefined;
731
+ const sourceDtypeValue = value.sourceDtype;
732
+ if (sourceDtypeValue !== undefined &&
733
+ (typeof sourceDtypeValue !== 'string' || sourceDtypeValue.length === 0)) {
734
+ throw new Error('Invalid torch.tensor envelope: sourceDtype must be a non-empty string when provided');
735
+ }
736
+ const sourceDtype = typeof sourceDtypeValue === 'string' ? sourceDtypeValue : undefined;
737
+ const sourceDeviceValue = value.sourceDevice;
738
+ if (sourceDeviceValue !== undefined &&
739
+ (typeof sourceDeviceValue !== 'string' || sourceDeviceValue.length === 0)) {
740
+ throw new Error('Invalid torch.tensor envelope: sourceDevice must be a non-empty string when provided');
741
+ }
742
+ const sourceDevice = typeof sourceDeviceValue === 'string' ? sourceDeviceValue : undefined;
743
+ const finishTensorDecode = (data) => tagDecodedShape({
744
+ data,
745
+ shape,
746
+ dtype,
747
+ device,
748
+ ...(sourceDtype === undefined ? {} : { sourceDtype }),
749
+ ...(sourceDevice === undefined ? {} : { sourceDevice }),
750
+ }, { marker: 'torch.tensor', dims: shape?.length, dtype });
511
751
  const decoded = recurse(nested);
512
752
  if (isPromiseLike(decoded)) {
513
- return decoded.then(data => ({ data, shape, dtype, device }));
753
+ return decoded.then(finishTensorDecode);
514
754
  }
515
- return { data: decoded, shape, dtype, device };
755
+ return finishTensorDecode(decoded);
516
756
  };
517
757
  /**
518
758
  * Recursively assert a value is plain JSON (null | boolean | number | string |
@@ -562,8 +802,14 @@ const decodeSklearnEstimatorEnvelope = value => {
562
802
  const className = value.className;
563
803
  const module = value.module;
564
804
  const params = value.params;
565
- if (typeof className !== 'string' || typeof module !== 'string' || !isObject(params)) {
566
- throw new Error('Invalid sklearn.estimator envelope: expected className/module strings + params object');
805
+ const strictV1 = isStrictV1Envelope(value);
806
+ if (typeof className !== 'string' ||
807
+ typeof module !== 'string' ||
808
+ (strictV1 && (className.length === 0 || module.length === 0)) ||
809
+ !isObject(params)) {
810
+ throw new Error(`Invalid sklearn.estimator envelope: expected className/module strings + params object; ` +
811
+ `className at path className and module at path module must be non-empty; declared class ${JSON.stringify(className)}, ` +
812
+ `declared module ${JSON.stringify(module)}, actual types ${actualType(className)}/${actualType(module)}`);
567
813
  }
568
814
  // params must be a PLAIN JSON object end to end — metadata-only estimators never
569
815
  // carry callables, class instances, or nested non-JSON values. Validate (do not
@@ -581,12 +827,12 @@ const decodeSklearnEstimatorEnvelope = value => {
581
827
  throw new Error('Invalid sklearn.estimator envelope: version must be a string when provided');
582
828
  }
583
829
  const version = typeof versionValue === 'string' ? versionValue : undefined;
584
- return {
830
+ return tagDecodedShape({
585
831
  className,
586
832
  module,
587
833
  version,
588
834
  params,
589
- };
835
+ }, { marker: 'sklearn.estimator' });
590
836
  };
591
837
  // Why: dispatch over the __tywrap__ typeTag instead of a long if-chain so each type's
592
838
  // decode logic lives in one focused handler. The typeTag strings are the on-the-wire keys
@@ -600,20 +846,48 @@ const ENVELOPE_HANDLERS = new Map([
600
846
  ['torch.tensor', decodeTorchTensorEnvelope],
601
847
  ['sklearn.estimator', decodeSklearnEstimatorEnvelope],
602
848
  ]);
849
+ const MAX_DECODE_DEPTH = 2048;
850
+ const MAX_DECODE_NODES = 1_000_000;
851
+ function isPlainObject(value) {
852
+ if (!isObject(value)) {
853
+ return false;
854
+ }
855
+ const proto = Object.getPrototypeOf(value);
856
+ return proto === Object.prototype || proto === null;
857
+ }
858
+ function isPlainArray(value) {
859
+ return Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype;
860
+ }
861
+ function decodePath(base, key) {
862
+ if (typeof key === 'number') {
863
+ return `${base}[${key}]`;
864
+ }
865
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
866
+ }
603
867
  function decodeEnvelopeCore(value, decodeArrow, recurse) {
604
868
  if (!isObject(value)) {
605
869
  return value;
606
870
  }
607
871
  const typeTag = value.__tywrap__;
608
- if (typeof typeTag !== 'string') {
872
+ if (!isScientificMarker(typeTag)) {
609
873
  return value;
610
874
  }
611
875
  const handler = ENVELOPE_HANDLERS.get(typeTag);
612
876
  if (!handler) {
613
877
  return value;
614
878
  }
615
- assertCodecVersion(value, typeTag);
616
- return handler(value, decodeArrow, recurse);
879
+ try {
880
+ assertCodecVersion(value, typeTag);
881
+ const decoded = handler(value, decodeArrow, recurse);
882
+ return isPromiseLike(decoded)
883
+ ? Promise.resolve(decoded).catch(error => {
884
+ throw asScientificDecodeError(error, typeTag);
885
+ })
886
+ : decoded;
887
+ }
888
+ catch (error) {
889
+ throw asScientificDecodeError(error, typeTag);
890
+ }
617
891
  }
618
892
  function decodeEnvelope(value, decodeArrow) {
619
893
  const recurse = v => decodeEnvelopeCore(v, decodeArrow, recurse);
@@ -624,26 +898,167 @@ function decodeEnvelope(value, decodeArrow) {
624
898
  return decoded;
625
899
  }
626
900
  async function decodeEnvelopeAsync(value, decodeArrow) {
627
- const recurse = v => decodeEnvelopeCore(v, decodeArrow, recurse);
628
- return await decodeEnvelopeCore(value, decodeArrow, recurse);
901
+ let visitedNodes = 0;
902
+ const recordVisit = (depth, path) => {
903
+ visitedNodes += 1;
904
+ if (visitedNodes > MAX_DECODE_NODES) {
905
+ throw new Error(`Scientific envelope decode maximum visited nodes ${MAX_DECODE_NODES} exceeded at ${path}`);
906
+ }
907
+ if (depth > MAX_DECODE_DEPTH) {
908
+ throw new Error(`Scientific envelope decode maximum depth ${MAX_DECODE_DEPTH} exceeded at ${path}`);
909
+ }
910
+ };
911
+ const prefixHandlerError = (error, path) => {
912
+ if (path === 'result') {
913
+ throw error;
914
+ }
915
+ throw new ScientificDecodeError('envelope', 'unknown', error, `Scientific envelope decode failed at ${path}: ${error instanceof Error ? error.message : String(error)}`);
916
+ };
917
+ const assignDecoded = (target, original, decoded) => {
918
+ if (decoded !== original) {
919
+ if (Array.isArray(target.container)) {
920
+ target.container[target.key] = decoded;
921
+ }
922
+ else {
923
+ target.container[target.key] = decoded;
924
+ }
925
+ }
926
+ };
927
+ const settleDecoded = async (decoded, original, target, path) => {
928
+ let resolved;
929
+ try {
930
+ resolved = await decoded;
931
+ }
932
+ catch (error) {
933
+ return prefixHandlerError(error, path);
934
+ }
935
+ assignDecoded(target, original, resolved);
936
+ };
937
+ const decodeSingle = (item, depth, path) => {
938
+ // Handlers sometimes decode a required child envelope themselves (currently
939
+ // torch.tensor.value). Keep that single-envelope recursion unchanged.
940
+ const recurseSingle = nested => {
941
+ const nestedPath = decodePath(path, 'value');
942
+ recordVisit(depth + 1, nestedPath);
943
+ return decodeEnvelopeCore(nested, decodeArrow, recurseSingle);
944
+ };
945
+ return decodeEnvelopeCore(item, decodeArrow, recurseSingle);
946
+ };
947
+ const frames = [];
948
+ const pending = [];
949
+ const visit = (current, depth, path, target) => {
950
+ const plainArray = isPlainArray(current);
951
+ const plainObject = isPlainObject(current);
952
+ // Only containers consume the traversal budget. Primitive leaves are already
953
+ // bounded by the payload byte cap, and counting them would reject large plain
954
+ // arrays that decoded fine before recursion existed.
955
+ if (plainArray || plainObject) {
956
+ recordVisit(depth, path);
957
+ }
958
+ if (plainObject && typeof current.__tywrap__ === 'string') {
959
+ if (!isScientificMarker(current.__tywrap__)) {
960
+ return;
961
+ }
962
+ let decoded;
963
+ try {
964
+ decoded = decodeSingle(current, depth, path);
965
+ }
966
+ catch (error) {
967
+ return prefixHandlerError(error, path);
968
+ }
969
+ if (isPromiseLike(decoded)) {
970
+ if (target) {
971
+ pending.push(settleDecoded(decoded, current, target, path));
972
+ }
973
+ else {
974
+ pending.push(settleDecoded(decoded, current, { container: root, key: 'value' }, path));
975
+ }
976
+ }
977
+ else if (target) {
978
+ assignDecoded(target, current, decoded);
979
+ }
980
+ else if (decoded !== current) {
981
+ root.value = decoded;
982
+ }
983
+ return;
984
+ }
985
+ if (plainArray) {
986
+ frames.push({ kind: 'array', container: current, depth, path, nextIndex: 0 });
987
+ }
988
+ else if (plainObject) {
989
+ frames.push({
990
+ kind: 'object',
991
+ container: current,
992
+ depth,
993
+ path,
994
+ entries: Object.entries(current),
995
+ nextIndex: 0,
996
+ });
997
+ }
998
+ };
999
+ /**
1000
+ * Values have copy semantics across the bridge: repeated Python references are
1001
+ * serialized independently, and this decoder does not restore alias identity.
1002
+ */
1003
+ const root = { value };
1004
+ visit(value, 0, 'result');
1005
+ const visitChild = (item, key, target, frame) => {
1006
+ try {
1007
+ visit(item, frame.depth + 1, decodePath(frame.path, key), target);
1008
+ }
1009
+ catch (error) {
1010
+ pending.push(Promise.reject(error));
1011
+ }
1012
+ };
1013
+ while (frames.length > 0) {
1014
+ const frame = frames[frames.length - 1];
1015
+ if (!frame) {
1016
+ break;
1017
+ }
1018
+ if (frame.nextIndex >= (frame.kind === 'array' ? frame.container.length : frame.entries.length)) {
1019
+ frames.pop();
1020
+ continue;
1021
+ }
1022
+ const index = frame.nextIndex;
1023
+ frame.nextIndex += 1;
1024
+ if (frame.kind === 'array') {
1025
+ visitChild(frame.container[index], index, { container: frame.container, key: index }, frame);
1026
+ }
1027
+ else {
1028
+ const entry = frame.entries[index];
1029
+ if (entry) {
1030
+ const [key, item] = entry;
1031
+ visitChild(item, key, { container: frame.container, key }, frame);
1032
+ }
1033
+ }
1034
+ }
1035
+ if (pending.length > 0) {
1036
+ await Promise.all(pending);
1037
+ }
1038
+ return root.value;
629
1039
  }
630
1040
  /**
631
1041
  * Decode values produced by the Python bridge.
632
1042
  */
633
1043
  export async function decodeValueAsync(value) {
634
- return await decodeEnvelopeAsync(value, tryDecodeArrowTable);
1044
+ try {
1045
+ return await decodeEnvelopeAsync(value, tryDecodeArrowTable);
1046
+ }
1047
+ catch (error) {
1048
+ throw asScientificDecodeError(error, 'unknown');
1049
+ }
635
1050
  }
636
1051
  /**
637
1052
  * Synchronous decode. Arrow decoding requires a registered decoder.
638
1053
  */
639
1054
  export function decodeValue(value) {
640
- const decodeArrow = (bytes) => {
641
- const decoder = requireArrowDecoder();
1055
+ const decodeArrow = (bytes, marker) => {
1056
+ const decoder = requireArrowDecoder(marker);
642
1057
  try {
643
1058
  return decoder(bytes);
644
1059
  }
645
1060
  catch (err) {
646
- throw new Error(`Arrow decode failed: ${err instanceof Error ? err.message : String(err)}`);
1061
+ throw new ScientificDecodeError('arrow', marker, err, `Arrow decode failed: ${err instanceof Error ? err.message : String(err)}`);
647
1062
  }
648
1063
  };
649
1064
  return decodeEnvelope(value, decodeArrow);