tywrap 0.9.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.
- package/README.md +20 -15
- package/SECURITY.md +5 -6
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +11 -0
- package/dist/core/generator.js.map +1 -1
- package/dist/runtime/bridge-codec.d.ts +2 -2
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +51 -7
- package/dist/runtime/bridge-codec.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
- package/dist/runtime/validators.d.ts +3 -2
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +9 -7
- package/dist/runtime/validators.js.map +1 -1
- package/dist/utils/codec.d.ts +16 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +493 -78
- package/dist/utils/codec.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/runtime/tywrap_bridge_core.py +394 -49
- package/src/core/generator.ts +15 -0
- package/src/runtime/bridge-codec.ts +64 -9
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/validators.ts +17 -9
- package/src/utils/codec.ts +743 -114
- package/src/version.ts +1 -1
package/dist/utils/codec.js
CHANGED
|
@@ -9,6 +9,34 @@
|
|
|
9
9
|
* Sklearn estimators: { "__tywrap__": "sklearn.estimator", "encoding": "json", ... }
|
|
10
10
|
*/
|
|
11
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
|
+
}
|
|
12
40
|
let arrowTableFrom;
|
|
13
41
|
// Why: lazy auto-registration (on first Arrow decode) imports apache-arrow at most
|
|
14
42
|
// once per process. We cache the in-flight/settled attempt so concurrent decodes
|
|
@@ -110,7 +138,12 @@ function isObject(value) {
|
|
|
110
138
|
function isNumberArray(value) {
|
|
111
139
|
return Array.isArray(value) && value.every(item => typeof item === 'number');
|
|
112
140
|
}
|
|
113
|
-
|
|
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
|
+
}
|
|
114
147
|
if (typeof Buffer !== 'undefined') {
|
|
115
148
|
const buf = Buffer.from(b64, 'base64');
|
|
116
149
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.length);
|
|
@@ -128,9 +161,9 @@ const ARROW_MISSING_MESSAGE = 'Received an Arrow-encoded payload but no Arrow de
|
|
|
128
161
|
'Install the optional dependency with `npm install apache-arrow`, or set ' +
|
|
129
162
|
'TYWRAP_CODEC_FALLBACK=json on the Python side to receive JSON instead ' +
|
|
130
163
|
'(lossy for dtype/NA fidelity). tywrap never silently downgrades Arrow payloads.';
|
|
131
|
-
function requireArrowDecoder() {
|
|
164
|
+
function requireArrowDecoder(marker) {
|
|
132
165
|
if (!arrowTableFrom) {
|
|
133
|
-
throw new Error(ARROW_MISSING_MESSAGE);
|
|
166
|
+
throw new ScientificDecodeError('arrow', marker, new Error(ARROW_MISSING_MESSAGE));
|
|
134
167
|
}
|
|
135
168
|
return arrowTableFrom;
|
|
136
169
|
}
|
|
@@ -142,7 +175,7 @@ function requireArrowDecoder() {
|
|
|
142
175
|
* for the rest of the process. If apache-arrow is absent we throw a clear, actionable
|
|
143
176
|
* error rather than silently producing wrong data.
|
|
144
177
|
*/
|
|
145
|
-
async function ensureArrowDecoder() {
|
|
178
|
+
async function ensureArrowDecoder(marker) {
|
|
146
179
|
if (arrowTableFrom) {
|
|
147
180
|
return arrowTableFrom;
|
|
148
181
|
}
|
|
@@ -150,17 +183,17 @@ async function ensureArrowDecoder() {
|
|
|
150
183
|
lazyRegistration ??= autoRegisterArrowDecoder(lazyArrowLoaderOverride ? { loader: lazyArrowLoaderOverride } : {});
|
|
151
184
|
await lazyRegistration;
|
|
152
185
|
if (!arrowTableFrom) {
|
|
153
|
-
throw new Error(ARROW_MISSING_MESSAGE);
|
|
186
|
+
throw new ScientificDecodeError('arrow', marker, new Error(ARROW_MISSING_MESSAGE));
|
|
154
187
|
}
|
|
155
188
|
return arrowTableFrom;
|
|
156
189
|
}
|
|
157
|
-
async function tryDecodeArrowTable(bytes) {
|
|
158
|
-
const decoder = await ensureArrowDecoder();
|
|
190
|
+
async function tryDecodeArrowTable(bytes, marker) {
|
|
191
|
+
const decoder = await ensureArrowDecoder(marker);
|
|
159
192
|
try {
|
|
160
193
|
return decoder(bytes);
|
|
161
194
|
}
|
|
162
195
|
catch (err) {
|
|
163
|
-
throw new
|
|
196
|
+
throw new ScientificDecodeError('arrow', marker, err, `Arrow decode failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
164
197
|
}
|
|
165
198
|
}
|
|
166
199
|
function isPromiseLike(value) {
|
|
@@ -199,8 +232,7 @@ function typedArrayToPlain(arr) {
|
|
|
199
232
|
if (arr !== null && arr !== undefined && typeof arr === 'object' && Symbol.iterator in arr) {
|
|
200
233
|
return Array.from(arr);
|
|
201
234
|
}
|
|
202
|
-
|
|
203
|
-
return [];
|
|
235
|
+
return null;
|
|
204
236
|
}
|
|
205
237
|
/**
|
|
206
238
|
* Extract values from an Arrow table as a plain JavaScript array.
|
|
@@ -222,6 +254,22 @@ function extractArrowValues(data) {
|
|
|
222
254
|
}
|
|
223
255
|
return null;
|
|
224
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
|
+
}
|
|
225
273
|
/**
|
|
226
274
|
* Reshape a flat array into a multi-dimensional nested array.
|
|
227
275
|
*
|
|
@@ -256,6 +304,111 @@ function reshapeArray(flat, shape) {
|
|
|
256
304
|
}
|
|
257
305
|
// Why: decoding needs to reject incompatible envelopes before we attempt to interpret payloads.
|
|
258
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
|
+
}
|
|
259
412
|
function assertCodecVersion(envelope, typeTag) {
|
|
260
413
|
if (!('codecVersion' in envelope)) {
|
|
261
414
|
return;
|
|
@@ -284,8 +437,8 @@ function decodeArrowOrJsonEnvelope(value, decodeArrow, typeTag) {
|
|
|
284
437
|
if (typeof b64 !== 'string') {
|
|
285
438
|
throw new Error(`Invalid ${typeTag} envelope: missing b64`);
|
|
286
439
|
}
|
|
287
|
-
const bytes = fromBase64(b64);
|
|
288
|
-
const decoded = decodeArrow(bytes);
|
|
440
|
+
const bytes = fromBase64(b64, isStrictV1Envelope(value) ? typeTag : undefined);
|
|
441
|
+
const decoded = decodeArrow(bytes, typeTag);
|
|
289
442
|
return isPromiseLike(decoded)
|
|
290
443
|
? decoded.then(item => tagDecodedShape(item, { marker: typeTag }))
|
|
291
444
|
: tagDecodedShape(decoded, { marker: typeTag });
|
|
@@ -300,43 +453,59 @@ function decodeArrowOrJsonEnvelope(value, decodeArrow, typeTag) {
|
|
|
300
453
|
}
|
|
301
454
|
const decodeDataframeEnvelope = (value, decodeArrow) => decodeArrowOrJsonEnvelope(value, decodeArrow, 'dataframe');
|
|
302
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
|
+
}
|
|
303
468
|
const decodeNdarrayEnvelope = (value, decodeArrow) => {
|
|
304
469
|
const encoding = value.encoding;
|
|
305
|
-
const
|
|
306
|
-
const shape =
|
|
307
|
-
const dtype =
|
|
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);
|
|
308
473
|
const metadata = { marker: 'ndarray', dims: shape?.length, dtype };
|
|
309
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
|
+
}
|
|
310
479
|
const b64 = value.b64;
|
|
311
480
|
if (typeof b64 !== 'string') {
|
|
312
|
-
|
|
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)}`);
|
|
313
487
|
}
|
|
314
|
-
const bytes = fromBase64(b64);
|
|
315
|
-
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');
|
|
316
490
|
// Extract values from Arrow table and reshape if needed
|
|
317
491
|
// Arrow only handles 1D arrays, so we flatten on encode and reshape here
|
|
318
492
|
// Reshape for: scalars (shape.length === 0) and multi-dim (shape.length > 1)
|
|
319
493
|
// Skip reshape for: 1D arrays (shape.length === 1) - return as-is
|
|
320
494
|
if (isPromiseLike(decoded)) {
|
|
321
|
-
return decoded.then(data =>
|
|
322
|
-
const values = extractArrowValues(data);
|
|
323
|
-
if (!values) {
|
|
324
|
-
return tagDecodedShape(data, metadata); // Fallback: keep provenance on raw data.
|
|
325
|
-
}
|
|
326
|
-
// Reshape scalars and multi-dimensional arrays, but not 1D
|
|
327
|
-
return tagDecodedShape(shape && shape.length !== 1 ? reshapeArray(values, shape) : values, metadata);
|
|
328
|
-
});
|
|
495
|
+
return decoded.then(data => finishNdarrayDecode(data, strictV1, shape, dtype, metadata));
|
|
329
496
|
}
|
|
330
|
-
|
|
331
|
-
if (!values) {
|
|
332
|
-
return tagDecodedShape(decoded, metadata); // Fallback: keep provenance on raw data.
|
|
333
|
-
}
|
|
334
|
-
// Reshape scalars and multi-dimensional arrays, but not 1D
|
|
335
|
-
return tagDecodedShape(shape && shape.length !== 1 ? reshapeArray(values, shape) : values, metadata);
|
|
497
|
+
return finishNdarrayDecode(decoded, strictV1, shape, dtype, metadata);
|
|
336
498
|
}
|
|
337
499
|
if (encoding === 'json') {
|
|
338
500
|
if (!('data' in value)) {
|
|
339
|
-
|
|
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
510
|
return tagDecodedShape(value.data, metadata);
|
|
342
511
|
}
|
|
@@ -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
|
|
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
|
|
392
|
-
|
|
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
|
|
483
|
-
const shape =
|
|
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 =
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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(
|
|
753
|
+
return decoded.then(finishTensorDecode);
|
|
514
754
|
}
|
|
515
|
-
return
|
|
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
|
-
|
|
566
|
-
|
|
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 (
|
|
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
|
-
|
|
616
|
-
|
|
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
|
-
|
|
628
|
-
|
|
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
|
-
|
|
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
|
|
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);
|