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.
- package/README.md +42 -22
- package/SECURITY.md +38 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +2 -1
- package/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +6 -3
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/generator.d.ts +23 -4
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +254 -170
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +3 -2
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +5 -5
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/base-bridge.d.ts +3 -7
- package/dist/runtime/base-bridge.d.ts.map +1 -1
- package/dist/runtime/base-bridge.js +4 -17
- package/dist/runtime/base-bridge.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +3 -22
- package/dist/runtime/bounded-context.d.ts.map +1 -1
- package/dist/runtime/bounded-context.js +13 -53
- package/dist/runtime/bounded-context.js.map +1 -1
- package/dist/runtime/bridge-codec.d.ts +3 -3
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +129 -62
- package/dist/runtime/bridge-codec.js.map +1 -1
- package/dist/runtime/errors.d.ts +16 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +17 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-transport.d.ts +1 -1
- package/dist/runtime/http-transport.d.ts.map +1 -1
- package/dist/runtime/http-transport.js +1 -1
- package/dist/runtime/http-transport.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +7 -36
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +2 -80
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +120 -59
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +345 -78
- package/dist/runtime/pooled-transport.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/pyodide-transport.d.ts +1 -1
- package/dist/runtime/pyodide-transport.d.ts.map +1 -1
- package/dist/runtime/pyodide-transport.js +2 -3
- package/dist/runtime/pyodide-transport.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +7 -36
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +20 -102
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +30 -69
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +179 -236
- package/dist/runtime/subprocess-transport.js.map +1 -1
- package/dist/runtime/timed-out-request-tracker.d.ts +2 -1
- package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -1
- package/dist/runtime/transport.d.ts +9 -19
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +1 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +50 -0
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +154 -0
- package/dist/runtime/validators.js.map +1 -1
- package/dist/types/index.d.ts +14 -39
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +3 -2
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +140 -13
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts +3 -16
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/codec.d.ts +17 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +500 -85
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +2 -1
- package/dist/utils/ir-cache.d.ts.map +1 -1
- package/dist/utils/runtime.d.ts +0 -29
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +16 -107
- package/dist/utils/runtime.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 +7 -6
- package/runtime/frame_codec.py +7 -1
- package/runtime/python_bridge.py +85 -125
- package/runtime/tywrap_bridge_core.py +515 -187
- package/src/config/index.ts +11 -0
- package/src/core/annotation-parser.ts +7 -4
- package/src/core/generator.ts +330 -205
- package/src/core/mapper.ts +8 -8
- package/src/index.ts +7 -4
- package/src/runtime/base-bridge.ts +5 -30
- package/src/runtime/bounded-context.ts +12 -67
- package/src/runtime/bridge-codec.ts +158 -74
- package/src/runtime/errors.ts +21 -0
- package/src/runtime/http-transport.ts +6 -1
- package/src/runtime/index.ts +6 -0
- package/src/runtime/node.ts +9 -120
- package/src/runtime/pooled-transport.ts +424 -90
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/pyodide-transport.ts +7 -3
- package/src/runtime/rpc-client.ts +37 -139
- package/src/runtime/subprocess-transport.ts +210 -285
- package/src/runtime/timed-out-request-tracker.ts +1 -1
- package/src/runtime/transport.ts +15 -23
- package/src/runtime/validators.ts +212 -0
- package/src/types/index.ts +21 -60
- package/src/tywrap.ts +157 -22
- package/src/utils/cache.ts +3 -3
- package/src/utils/codec.ts +749 -112
- package/src/utils/ir-cache.ts +1 -1
- package/src/utils/runtime.ts +17 -128
- package/src/version.ts +1 -1
- package/dist/core/discovery.d.ts +0 -103
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -380
- package/dist/core/discovery.js.map +0 -1
- package/dist/core/validation.d.ts +0 -102
- package/dist/core/validation.d.ts.map +0 -1
- package/dist/core/validation.js +0 -490
- package/dist/core/validation.js.map +0 -1
- package/dist/runtime/base.d.ts +0 -22
- package/dist/runtime/base.d.ts.map +0 -1
- package/dist/runtime/base.js +0 -23
- package/dist/runtime/base.js.map +0 -1
- package/dist/runtime/transport-pool.d.ts +0 -196
- package/dist/runtime/transport-pool.d.ts.map +0 -1
- package/dist/runtime/transport-pool.js +0 -418
- package/dist/runtime/transport-pool.js.map +0 -1
- package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/safe_codec.py +0 -352
- package/src/core/discovery.ts +0 -477
- package/src/core/validation.ts +0 -729
- package/src/runtime/base.ts +0 -24
- package/src/runtime/transport-pool.ts +0 -538
package/src/utils/codec.ts
CHANGED
|
@@ -9,6 +9,47 @@
|
|
|
9
9
|
* Sklearn estimators: { "__tywrap__": "sklearn.estimator", "encoding": "json", ... }
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { tagDecodedShape } from '../runtime/validators.js';
|
|
13
|
+
|
|
14
|
+
const SCIENTIFIC_MARKERS = [
|
|
15
|
+
'dataframe',
|
|
16
|
+
'series',
|
|
17
|
+
'ndarray',
|
|
18
|
+
'scipy.sparse',
|
|
19
|
+
'torch.tensor',
|
|
20
|
+
'sklearn.estimator',
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type ScientificMarker = (typeof SCIENTIFIC_MARKERS)[number];
|
|
24
|
+
|
|
25
|
+
export function isScientificMarker(value: unknown): value is ScientificMarker {
|
|
26
|
+
return SCIENTIFIC_MARKERS.includes(value as ScientificMarker);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ScientificDecodeErrorKind = 'arrow' | 'envelope';
|
|
30
|
+
type ScientificDecodeErrorMarker = ScientificMarker | 'unknown';
|
|
31
|
+
|
|
32
|
+
/** @internal Structured failure consumed by BridgeCodec without parsing messages. */
|
|
33
|
+
export class ScientificDecodeError extends Error {
|
|
34
|
+
constructor(
|
|
35
|
+
readonly kind: ScientificDecodeErrorKind,
|
|
36
|
+
readonly marker: ScientificDecodeErrorMarker,
|
|
37
|
+
override readonly cause: unknown,
|
|
38
|
+
message = cause instanceof Error ? cause.message : String(cause)
|
|
39
|
+
) {
|
|
40
|
+
super(message, { cause });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function asScientificDecodeError(
|
|
45
|
+
error: unknown,
|
|
46
|
+
marker: ScientificDecodeErrorMarker
|
|
47
|
+
): ScientificDecodeError {
|
|
48
|
+
return error instanceof ScientificDecodeError
|
|
49
|
+
? error
|
|
50
|
+
: new ScientificDecodeError('envelope', marker, error);
|
|
51
|
+
}
|
|
52
|
+
|
|
12
53
|
// Avoid hard dependency on apache-arrow types at compile time to keep install optional.
|
|
13
54
|
export type ArrowTable = { readonly numCols?: number; readonly numRows?: number } & Record<
|
|
14
55
|
string,
|
|
@@ -31,6 +72,8 @@ export interface TorchTensor {
|
|
|
31
72
|
shape?: readonly number[];
|
|
32
73
|
dtype?: string;
|
|
33
74
|
device?: string;
|
|
75
|
+
sourceDtype?: string;
|
|
76
|
+
sourceDevice?: string;
|
|
34
77
|
}
|
|
35
78
|
|
|
36
79
|
export interface SklearnEstimator {
|
|
@@ -68,6 +111,7 @@ export type ValueEnvelope =
|
|
|
68
111
|
readonly b64?: string; // when encoding=arrow
|
|
69
112
|
readonly data?: unknown; // when encoding=json
|
|
70
113
|
readonly shape?: readonly number[];
|
|
114
|
+
readonly dtype?: string | null;
|
|
71
115
|
}
|
|
72
116
|
| {
|
|
73
117
|
readonly __tywrap__: 'scipy.sparse';
|
|
@@ -90,6 +134,8 @@ export type ValueEnvelope =
|
|
|
90
134
|
readonly shape?: readonly number[];
|
|
91
135
|
readonly dtype?: string;
|
|
92
136
|
readonly device?: string;
|
|
137
|
+
readonly sourceDtype?: string;
|
|
138
|
+
readonly sourceDevice?: string;
|
|
93
139
|
}
|
|
94
140
|
| {
|
|
95
141
|
readonly __tywrap__: 'sklearn.estimator';
|
|
@@ -229,7 +275,15 @@ function isNumberArray(value: unknown): value is number[] {
|
|
|
229
275
|
return Array.isArray(value) && value.every(item => typeof item === 'number');
|
|
230
276
|
}
|
|
231
277
|
|
|
232
|
-
|
|
278
|
+
const STRICT_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
279
|
+
|
|
280
|
+
function fromBase64(b64: string, marker?: string, context?: string): Uint8Array {
|
|
281
|
+
if (marker && !STRICT_BASE64_PATTERN.test(b64)) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Invalid ${marker} envelope: b64 at path b64 must be well-formed base64; ` +
|
|
284
|
+
`${context ? `${context}; ` : ''}actual count unknown, actual type string with length ${b64.length}`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
233
287
|
if (typeof Buffer !== 'undefined') {
|
|
234
288
|
const buf = Buffer.from(b64, 'base64');
|
|
235
289
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.length);
|
|
@@ -250,9 +304,11 @@ const ARROW_MISSING_MESSAGE =
|
|
|
250
304
|
'TYWRAP_CODEC_FALLBACK=json on the Python side to receive JSON instead ' +
|
|
251
305
|
'(lossy for dtype/NA fidelity). tywrap never silently downgrades Arrow payloads.';
|
|
252
306
|
|
|
253
|
-
function requireArrowDecoder(
|
|
307
|
+
function requireArrowDecoder(
|
|
308
|
+
marker: ScientificMarker
|
|
309
|
+
): (bytes: Uint8Array) => ArrowTable | Uint8Array {
|
|
254
310
|
if (!arrowTableFrom) {
|
|
255
|
-
throw new Error(ARROW_MISSING_MESSAGE);
|
|
311
|
+
throw new ScientificDecodeError('arrow', marker, new Error(ARROW_MISSING_MESSAGE));
|
|
256
312
|
}
|
|
257
313
|
return arrowTableFrom;
|
|
258
314
|
}
|
|
@@ -265,7 +321,9 @@ function requireArrowDecoder(): (bytes: Uint8Array) => ArrowTable | Uint8Array {
|
|
|
265
321
|
* for the rest of the process. If apache-arrow is absent we throw a clear, actionable
|
|
266
322
|
* error rather than silently producing wrong data.
|
|
267
323
|
*/
|
|
268
|
-
async function ensureArrowDecoder(
|
|
324
|
+
async function ensureArrowDecoder(
|
|
325
|
+
marker: ScientificMarker
|
|
326
|
+
): Promise<(bytes: Uint8Array) => ArrowTable | Uint8Array> {
|
|
269
327
|
if (arrowTableFrom) {
|
|
270
328
|
return arrowTableFrom;
|
|
271
329
|
}
|
|
@@ -275,21 +333,30 @@ async function ensureArrowDecoder(): Promise<(bytes: Uint8Array) => ArrowTable |
|
|
|
275
333
|
);
|
|
276
334
|
await lazyRegistration;
|
|
277
335
|
if (!arrowTableFrom) {
|
|
278
|
-
throw new Error(ARROW_MISSING_MESSAGE);
|
|
336
|
+
throw new ScientificDecodeError('arrow', marker, new Error(ARROW_MISSING_MESSAGE));
|
|
279
337
|
}
|
|
280
338
|
return arrowTableFrom;
|
|
281
339
|
}
|
|
282
340
|
|
|
283
|
-
async function tryDecodeArrowTable(
|
|
284
|
-
|
|
341
|
+
async function tryDecodeArrowTable(
|
|
342
|
+
bytes: Uint8Array,
|
|
343
|
+
marker: ScientificMarker
|
|
344
|
+
): Promise<ArrowTable | Uint8Array> {
|
|
345
|
+
const decoder = await ensureArrowDecoder(marker);
|
|
285
346
|
try {
|
|
286
347
|
return decoder(bytes);
|
|
287
348
|
} catch (err) {
|
|
288
|
-
throw new
|
|
349
|
+
throw new ScientificDecodeError(
|
|
350
|
+
'arrow',
|
|
351
|
+
marker,
|
|
352
|
+
err,
|
|
353
|
+
`Arrow decode failed: ${err instanceof Error ? err.message : String(err)}`
|
|
354
|
+
);
|
|
289
355
|
}
|
|
290
356
|
}
|
|
291
357
|
|
|
292
358
|
type MaybePromise<T> = T | Promise<T>;
|
|
359
|
+
type DecodeArrow<T> = (bytes: Uint8Array, marker: ScientificMarker) => MaybePromise<T>;
|
|
293
360
|
|
|
294
361
|
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
295
362
|
return (
|
|
@@ -309,7 +376,7 @@ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
|
309
376
|
* @param arr - Typed array or plain array
|
|
310
377
|
* @returns Plain JavaScript array with values converted (BigInt → Number where safe)
|
|
311
378
|
*/
|
|
312
|
-
function typedArrayToPlain(arr: unknown): unknown[] {
|
|
379
|
+
function typedArrayToPlain(arr: unknown): unknown[] | null {
|
|
313
380
|
if (Array.isArray(arr)) {
|
|
314
381
|
return arr;
|
|
315
382
|
}
|
|
@@ -330,8 +397,7 @@ function typedArrayToPlain(arr: unknown): unknown[] {
|
|
|
330
397
|
if (arr !== null && arr !== undefined && typeof arr === 'object' && Symbol.iterator in arr) {
|
|
331
398
|
return Array.from(arr as Iterable<unknown>);
|
|
332
399
|
}
|
|
333
|
-
|
|
334
|
-
return [];
|
|
400
|
+
return null;
|
|
335
401
|
}
|
|
336
402
|
|
|
337
403
|
/**
|
|
@@ -355,6 +421,30 @@ function extractArrowValues(data: unknown): unknown[] | null {
|
|
|
355
421
|
return null;
|
|
356
422
|
}
|
|
357
423
|
|
|
424
|
+
function extractNdarrayArrowValues(
|
|
425
|
+
data: unknown,
|
|
426
|
+
shape: readonly number[] | undefined,
|
|
427
|
+
dtype: string | undefined
|
|
428
|
+
): unknown[] {
|
|
429
|
+
try {
|
|
430
|
+
const values = extractArrowValues(data);
|
|
431
|
+
if (values) {
|
|
432
|
+
return values;
|
|
433
|
+
}
|
|
434
|
+
} catch (error) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
`Invalid ndarray envelope: b64 decoded data extraction failed at path b64 for declared ` +
|
|
437
|
+
`shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; actual count unknown, ` +
|
|
438
|
+
`actual type ${actualType(data)} (${error instanceof Error ? error.message : String(error)})`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
throw new Error(
|
|
442
|
+
`Invalid ndarray envelope: b64 decoded data at path b64 could not extract Arrow values ` +
|
|
443
|
+
`for declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
|
|
444
|
+
`actual count unknown, actual type ${actualType(data)}`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
358
448
|
/**
|
|
359
449
|
* Reshape a flat array into a multi-dimensional nested array.
|
|
360
450
|
*
|
|
@@ -394,6 +484,168 @@ function reshapeArray(flat: unknown[], shape: readonly number[]): unknown {
|
|
|
394
484
|
// Why: decoding needs to reject incompatible envelopes before we attempt to interpret payloads.
|
|
395
485
|
const CODEC_VERSION = 1;
|
|
396
486
|
|
|
487
|
+
function isStrictV1Envelope(envelope: { codecVersion?: unknown }): boolean {
|
|
488
|
+
return envelope.codecVersion === CODEC_VERSION;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function actualType(value: unknown): string {
|
|
492
|
+
if (Array.isArray(value)) {
|
|
493
|
+
return `array(length=${value.length})`;
|
|
494
|
+
}
|
|
495
|
+
if (value === null) {
|
|
496
|
+
return 'null';
|
|
497
|
+
}
|
|
498
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
499
|
+
return `number(${String(value)})`;
|
|
500
|
+
}
|
|
501
|
+
return typeof value;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function assertShape(
|
|
505
|
+
shapeValue: unknown,
|
|
506
|
+
marker: 'ndarray' | 'torch.tensor' | 'scipy.sparse',
|
|
507
|
+
field = 'shape',
|
|
508
|
+
dtype?: unknown
|
|
509
|
+
): number[] {
|
|
510
|
+
if (!Array.isArray(shapeValue)) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Invalid ${marker} envelope: ${field} at path ${field} must be a list of ` +
|
|
513
|
+
`non-negative safe integers; declared shape ${String(shapeValue)} and dtype ` +
|
|
514
|
+
`${JSON.stringify(dtype)}, actual count unknown, actual type ${actualType(shapeValue)}`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
for (let i = 0; i < shapeValue.length; i += 1) {
|
|
518
|
+
const dim = shapeValue[i];
|
|
519
|
+
if (typeof dim !== 'number' || !Number.isSafeInteger(dim) || dim < 0) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Invalid ${marker} envelope: ${field}[${i}]=${String(dim)} must be a non-negative integer ` +
|
|
522
|
+
`within the safe range for declared shape ${JSON.stringify(shapeValue)}; ` +
|
|
523
|
+
`declared dtype ${JSON.stringify(dtype)}, actual count unknown, ` +
|
|
524
|
+
`actual type ${actualType(dim)} with value ${String(dim)}`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return shapeValue as number[];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function assertOptionalNonEmptyDtype(
|
|
532
|
+
value: unknown,
|
|
533
|
+
marker: 'ndarray' | 'torch.tensor' | 'scipy.sparse',
|
|
534
|
+
field = 'dtype',
|
|
535
|
+
shape?: unknown
|
|
536
|
+
): string | undefined {
|
|
537
|
+
if (value === undefined) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
541
|
+
throw new Error(
|
|
542
|
+
`Invalid ${marker} envelope: ${field} at path ${field} must be a non-empty string; ` +
|
|
543
|
+
`declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(value)}, ` +
|
|
544
|
+
`actual count unknown, actual type ${actualType(value)}`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return value;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
type ShapedScientificMarker = 'ndarray' | 'torch.tensor' | 'scipy.sparse';
|
|
551
|
+
|
|
552
|
+
function readEnvelopeShape(
|
|
553
|
+
envelope: { codecVersion?: unknown },
|
|
554
|
+
shapeValue: unknown,
|
|
555
|
+
marker: ShapedScientificMarker,
|
|
556
|
+
field = 'shape',
|
|
557
|
+
dtype?: unknown
|
|
558
|
+
): number[] | undefined {
|
|
559
|
+
return isStrictV1Envelope(envelope)
|
|
560
|
+
? assertShape(shapeValue, marker, field, dtype)
|
|
561
|
+
: isNumberArray(shapeValue)
|
|
562
|
+
? shapeValue
|
|
563
|
+
: undefined;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function readEnvelopeDtype(
|
|
567
|
+
envelope: { codecVersion?: unknown },
|
|
568
|
+
dtypeValue: unknown,
|
|
569
|
+
marker: ShapedScientificMarker,
|
|
570
|
+
field = 'dtype',
|
|
571
|
+
shape?: unknown
|
|
572
|
+
): string | undefined {
|
|
573
|
+
return isStrictV1Envelope(envelope)
|
|
574
|
+
? assertOptionalNonEmptyDtype(dtypeValue, marker, field, shape)
|
|
575
|
+
: typeof dtypeValue === 'string'
|
|
576
|
+
? dtypeValue
|
|
577
|
+
: undefined;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function countJsonLeaves(value: unknown): number {
|
|
581
|
+
if (!Array.isArray(value)) {
|
|
582
|
+
return 1;
|
|
583
|
+
}
|
|
584
|
+
return value.reduce((count, item) => count + countJsonLeaves(item), 0);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function assertJsonMatchesShape(
|
|
588
|
+
data: unknown,
|
|
589
|
+
shape: readonly number[],
|
|
590
|
+
dtype: string | undefined
|
|
591
|
+
): void {
|
|
592
|
+
const declaredCount = shapeProduct(shape);
|
|
593
|
+
const strictLeaves = dtype !== undefined;
|
|
594
|
+
const actualCount = strictLeaves ? countJsonLeaves(data) : undefined;
|
|
595
|
+
const visit = (value: unknown, depth: number, path: string): void => {
|
|
596
|
+
if (depth === shape.length) {
|
|
597
|
+
if (strictLeaves && Array.isArray(value)) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`Invalid ndarray envelope: data at path ${path} exceeds nesting depth ` +
|
|
600
|
+
`${shape.length} for declared shape ${JSON.stringify(shape)} and dtype ` +
|
|
601
|
+
`${JSON.stringify(dtype)}; actual count ${actualCount}, actual type ${actualType(value)}`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (!Array.isArray(value)) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`Invalid ndarray envelope: data at path ${path} must be an array at depth ${depth} ` +
|
|
609
|
+
`for declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
|
|
610
|
+
`actual count ${actualCount ?? 'unknown'}, actual type ${actualType(value)}`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const expectedLength = shape[depth] as number;
|
|
614
|
+
if (value.length !== expectedLength) {
|
|
615
|
+
throw new Error(
|
|
616
|
+
`Invalid ndarray envelope: data at path ${path} has length ${value.length}, expected ` +
|
|
617
|
+
`${expectedLength} for declared shape ${JSON.stringify(shape)} and dtype ` +
|
|
618
|
+
`${JSON.stringify(dtype)}; declared count ${declaredCount}, actual count ${actualCount ?? 'unknown'}, ` +
|
|
619
|
+
`actual type ${actualType(value)}`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
value.forEach((item, index) => visit(item, depth + 1, `${path}[${index}]`));
|
|
623
|
+
};
|
|
624
|
+
visit(data, 0, 'data');
|
|
625
|
+
if (strictLeaves && actualCount !== declaredCount) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
`Invalid ndarray envelope: data at path data has leaf count ${actualCount}, expected ` +
|
|
628
|
+
`${declaredCount} for declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
|
|
629
|
+
`actual type ${actualType(data)}`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function assertArrowCount(
|
|
635
|
+
values: readonly unknown[],
|
|
636
|
+
shape: readonly number[],
|
|
637
|
+
dtype: string
|
|
638
|
+
): void {
|
|
639
|
+
const expected = shapeProduct(shape);
|
|
640
|
+
if (values.length !== expected) {
|
|
641
|
+
throw new Error(
|
|
642
|
+
`Invalid ndarray envelope: b64 decoded data at path b64 has element count ${values.length}, ` +
|
|
643
|
+
`expected ${expected} for declared shape ${JSON.stringify(shape)} and dtype ` +
|
|
644
|
+
`${JSON.stringify(dtype)}; actual type array(length=${values.length})`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
397
649
|
function assertCodecVersion(envelope: { codecVersion?: unknown }, typeTag: string): void {
|
|
398
650
|
if (!('codecVersion' in envelope)) {
|
|
399
651
|
return;
|
|
@@ -420,7 +672,7 @@ function assertCodecVersion(envelope: { codecVersion?: unknown }, typeTag: strin
|
|
|
420
672
|
*/
|
|
421
673
|
type EnvelopeHandler = <T>(
|
|
422
674
|
value: { [k: string]: unknown },
|
|
423
|
-
decodeArrow:
|
|
675
|
+
decodeArrow: DecodeArrow<T>,
|
|
424
676
|
recurse: (value: unknown) => MaybePromise<T | unknown>
|
|
425
677
|
) => MaybePromise<T | unknown>;
|
|
426
678
|
|
|
@@ -432,8 +684,8 @@ type EnvelopeHandler = <T>(
|
|
|
432
684
|
*/
|
|
433
685
|
function decodeArrowOrJsonEnvelope<T>(
|
|
434
686
|
value: { [k: string]: unknown },
|
|
435
|
-
decodeArrow:
|
|
436
|
-
typeTag:
|
|
687
|
+
decodeArrow: DecodeArrow<T>,
|
|
688
|
+
typeTag: 'dataframe' | 'series'
|
|
437
689
|
): MaybePromise<T | unknown> {
|
|
438
690
|
const encoding = value.encoding;
|
|
439
691
|
if (encoding === 'arrow') {
|
|
@@ -441,14 +693,17 @@ function decodeArrowOrJsonEnvelope<T>(
|
|
|
441
693
|
if (typeof b64 !== 'string') {
|
|
442
694
|
throw new Error(`Invalid ${typeTag} envelope: missing b64`);
|
|
443
695
|
}
|
|
444
|
-
const bytes = fromBase64(b64);
|
|
445
|
-
|
|
696
|
+
const bytes = fromBase64(b64, isStrictV1Envelope(value) ? typeTag : undefined);
|
|
697
|
+
const decoded = decodeArrow(bytes, typeTag);
|
|
698
|
+
return isPromiseLike(decoded)
|
|
699
|
+
? decoded.then(item => tagDecodedShape(item, { marker: typeTag }))
|
|
700
|
+
: tagDecodedShape(decoded, { marker: typeTag });
|
|
446
701
|
}
|
|
447
702
|
if (encoding === 'json') {
|
|
448
703
|
if (!('data' in value)) {
|
|
449
704
|
throw new Error(`Invalid ${typeTag} envelope: missing data`);
|
|
450
705
|
}
|
|
451
|
-
return value.data;
|
|
706
|
+
return tagDecodedShape(value.data, { marker: typeTag });
|
|
452
707
|
}
|
|
453
708
|
throw new Error(`Invalid ${typeTag} envelope: unsupported encoding ${String(encoding)}`);
|
|
454
709
|
}
|
|
@@ -459,51 +714,83 @@ const decodeDataframeEnvelope: EnvelopeHandler = (value, decodeArrow) =>
|
|
|
459
714
|
const decodeSeriesEnvelope: EnvelopeHandler = (value, decodeArrow) =>
|
|
460
715
|
decodeArrowOrJsonEnvelope(value, decodeArrow, 'series');
|
|
461
716
|
|
|
717
|
+
function finishNdarrayDecode(
|
|
718
|
+
data: unknown,
|
|
719
|
+
strictV1: boolean,
|
|
720
|
+
shape: readonly number[] | undefined,
|
|
721
|
+
dtype: string | undefined,
|
|
722
|
+
metadata: { marker: 'ndarray'; dims: number | undefined; dtype: string | undefined }
|
|
723
|
+
): unknown {
|
|
724
|
+
const values = strictV1
|
|
725
|
+
? extractNdarrayArrowValues(data, shape, dtype)
|
|
726
|
+
: extractArrowValues(data);
|
|
727
|
+
if (!values) {
|
|
728
|
+
return tagDecodedShape(data, metadata);
|
|
729
|
+
}
|
|
730
|
+
if (strictV1) {
|
|
731
|
+
assertArrowCount(values, shape as number[], dtype as string);
|
|
732
|
+
}
|
|
733
|
+
return tagDecodedShape(
|
|
734
|
+
shape && shape.length !== 1 ? reshapeArray(values, shape) : values,
|
|
735
|
+
metadata
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
462
739
|
const decodeNdarrayEnvelope: EnvelopeHandler = (value, decodeArrow) => {
|
|
463
740
|
const encoding = value.encoding;
|
|
464
|
-
const
|
|
465
|
-
const shape =
|
|
741
|
+
const strictV1 = isStrictV1Envelope(value);
|
|
742
|
+
const shape = readEnvelopeShape(value, value.shape, 'ndarray', 'shape', value.dtype);
|
|
743
|
+
const dtype = readEnvelopeDtype(value, value.dtype, 'ndarray', 'dtype', shape);
|
|
744
|
+
const metadata = { marker: 'ndarray' as const, dims: shape?.length, dtype };
|
|
466
745
|
|
|
467
746
|
if (encoding === 'arrow') {
|
|
747
|
+
if (strictV1 && dtype === undefined) {
|
|
748
|
+
throw new Error(
|
|
749
|
+
`Invalid ndarray envelope: dtype at path dtype is required for Arrow encoding; ` +
|
|
750
|
+
`declared shape ${JSON.stringify(shape)} and dtype undefined, actual count unknown, actual type undefined`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
468
753
|
const b64 = value.b64;
|
|
469
754
|
if (typeof b64 !== 'string') {
|
|
470
|
-
|
|
755
|
+
if (!strictV1) {
|
|
756
|
+
throw new Error('Invalid ndarray envelope: missing b64');
|
|
757
|
+
}
|
|
758
|
+
throw new Error(
|
|
759
|
+
`Invalid ndarray envelope: b64 at path b64 must be a base64 string for declared ` +
|
|
760
|
+
`shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; actual count unknown, ` +
|
|
761
|
+
`actual type ${actualType(b64)}`
|
|
762
|
+
);
|
|
471
763
|
}
|
|
472
|
-
const bytes = fromBase64(
|
|
473
|
-
|
|
764
|
+
const bytes = fromBase64(
|
|
765
|
+
b64,
|
|
766
|
+
strictV1 ? 'ndarray' : undefined,
|
|
767
|
+
`declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}`
|
|
768
|
+
);
|
|
769
|
+
const decoded = decodeArrow(bytes, 'ndarray');
|
|
474
770
|
|
|
475
771
|
// Extract values from Arrow table and reshape if needed
|
|
476
772
|
// Arrow only handles 1D arrays, so we flatten on encode and reshape here
|
|
477
773
|
// Reshape for: scalars (shape.length === 0) and multi-dim (shape.length > 1)
|
|
478
774
|
// Skip reshape for: 1D arrays (shape.length === 1) - return as-is
|
|
479
775
|
if (isPromiseLike(decoded)) {
|
|
480
|
-
return decoded.then(data =>
|
|
481
|
-
const values = extractArrowValues(data);
|
|
482
|
-
if (!values) {
|
|
483
|
-
return data; // Fallback: return raw data if extraction fails
|
|
484
|
-
}
|
|
485
|
-
// Reshape scalars and multi-dimensional arrays, but not 1D
|
|
486
|
-
if (shape && shape.length !== 1) {
|
|
487
|
-
return reshapeArray(values, shape);
|
|
488
|
-
}
|
|
489
|
-
return values;
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
const values = extractArrowValues(decoded);
|
|
493
|
-
if (!values) {
|
|
494
|
-
return decoded; // Fallback: return raw data if extraction fails
|
|
776
|
+
return decoded.then(data => finishNdarrayDecode(data, strictV1, shape, dtype, metadata));
|
|
495
777
|
}
|
|
496
|
-
|
|
497
|
-
if (shape && shape.length !== 1) {
|
|
498
|
-
return reshapeArray(values, shape);
|
|
499
|
-
}
|
|
500
|
-
return values;
|
|
778
|
+
return finishNdarrayDecode(decoded, strictV1, shape, dtype, metadata);
|
|
501
779
|
}
|
|
502
780
|
if (encoding === 'json') {
|
|
503
781
|
if (!('data' in value)) {
|
|
504
|
-
|
|
782
|
+
if (!strictV1) {
|
|
783
|
+
throw new Error('Invalid ndarray envelope: missing data');
|
|
784
|
+
}
|
|
785
|
+
throw new Error(
|
|
786
|
+
`Invalid ndarray envelope: data at path data is required for declared shape ` +
|
|
787
|
+
`${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; actual count 0, actual type undefined`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (strictV1) {
|
|
791
|
+
assertJsonMatchesShape(value.data, shape as number[], dtype);
|
|
505
792
|
}
|
|
506
|
-
return value.data;
|
|
793
|
+
return tagDecodedShape(value.data, metadata);
|
|
507
794
|
}
|
|
508
795
|
throw new Error(`Invalid ndarray envelope: unsupported encoding ${String(encoding)}`);
|
|
509
796
|
};
|
|
@@ -533,6 +820,53 @@ function assertIndexArrayInRange(arr: readonly unknown[], bound: number, label:
|
|
|
533
820
|
}
|
|
534
821
|
}
|
|
535
822
|
|
|
823
|
+
function assertSparseDataDomain(
|
|
824
|
+
data: readonly unknown[],
|
|
825
|
+
dtype: string,
|
|
826
|
+
shape: readonly number[]
|
|
827
|
+
): void {
|
|
828
|
+
const expectsBoolean = dtype === 'bool' || dtype === 'bool_';
|
|
829
|
+
const integerMatch = /^(u?int)(\d*)$/.exec(dtype);
|
|
830
|
+
const expectsInteger = integerMatch !== null;
|
|
831
|
+
const expectsFloat = /^float\d*$/.test(dtype);
|
|
832
|
+
const width = integerMatch?.[2] ? Number(integerMatch[2]) : undefined;
|
|
833
|
+
const unsignedInteger = integerMatch?.[1] === 'uint';
|
|
834
|
+
let integerMin: number | undefined;
|
|
835
|
+
let integerMax: number | undefined;
|
|
836
|
+
if (width === 8 || width === 16 || width === 32) {
|
|
837
|
+
integerMin = unsignedInteger ? 0 : -(2 ** (width - 1));
|
|
838
|
+
integerMax = unsignedInteger ? 2 ** width - 1 : 2 ** (width - 1) - 1;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
842
|
+
const item = data[i];
|
|
843
|
+
if (typeof item === 'number' && !Number.isFinite(item)) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
`Invalid scipy.sparse envelope: data[${i}] at path data[${i}] must be finite for ` +
|
|
846
|
+
`declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
|
|
847
|
+
`actual count ${data.length}, actual type number with value ${String(item)}`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
const valid = expectsBoolean
|
|
851
|
+
? typeof item === 'boolean'
|
|
852
|
+
: expectsInteger
|
|
853
|
+
? typeof item === 'number' &&
|
|
854
|
+
Number.isInteger(item) &&
|
|
855
|
+
(integerMin === undefined || item >= integerMin) &&
|
|
856
|
+
(integerMax === undefined || item <= integerMax)
|
|
857
|
+
: expectsFloat
|
|
858
|
+
? typeof item === 'number'
|
|
859
|
+
: true;
|
|
860
|
+
if (!valid) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
`Invalid scipy.sparse envelope: data[${i}] at path data[${i}] is incompatible with ` +
|
|
863
|
+
`declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
|
|
864
|
+
`actual count ${data.length}, actual type ${actualType(item)} with value ${String(item)}`
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
536
870
|
const decodeScipySparseEnvelope: EnvelopeHandler = value => {
|
|
537
871
|
const encoding = value.encoding;
|
|
538
872
|
if (encoding !== 'json') {
|
|
@@ -542,7 +876,8 @@ const decodeScipySparseEnvelope: EnvelopeHandler = value => {
|
|
|
542
876
|
if (format !== 'csr' && format !== 'csc' && format !== 'coo') {
|
|
543
877
|
throw new Error(`Invalid scipy.sparse envelope: unsupported format ${String(format)}`);
|
|
544
878
|
}
|
|
545
|
-
const
|
|
879
|
+
const strictV1 = isStrictV1Envelope(value);
|
|
880
|
+
const shape = readEnvelopeShape(value, value.shape, 'scipy.sparse', 'shape', value.dtype);
|
|
546
881
|
if (
|
|
547
882
|
!Array.isArray(shape) ||
|
|
548
883
|
shape.length !== 2 ||
|
|
@@ -561,8 +896,16 @@ const decodeScipySparseEnvelope: EnvelopeHandler = value => {
|
|
|
561
896
|
if (!Array.isArray(data)) {
|
|
562
897
|
throw new Error('Invalid scipy.sparse envelope: data must be an array');
|
|
563
898
|
}
|
|
564
|
-
const
|
|
565
|
-
|
|
899
|
+
const dtype = readEnvelopeDtype(value, value.dtype, 'scipy.sparse', 'dtype', shape);
|
|
900
|
+
if (strictV1 && dtype === undefined) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
`Invalid scipy.sparse envelope: dtype at path dtype is required for declared shape ` +
|
|
903
|
+
`${JSON.stringify(shape)}; actual count ${data.length}, actual type undefined`
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
if (strictV1) {
|
|
907
|
+
assertSparseDataDomain(data, dtype as string, shape);
|
|
908
|
+
}
|
|
566
909
|
|
|
567
910
|
if (format === 'coo') {
|
|
568
911
|
const row = value.row;
|
|
@@ -580,14 +923,17 @@ const decodeScipySparseEnvelope: EnvelopeHandler = value => {
|
|
|
580
923
|
}
|
|
581
924
|
assertIndexArrayInRange(row, rows, 'row');
|
|
582
925
|
assertIndexArrayInRange(col, cols, 'col');
|
|
583
|
-
return
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
926
|
+
return tagDecodedShape(
|
|
927
|
+
{
|
|
928
|
+
format,
|
|
929
|
+
shape,
|
|
930
|
+
data,
|
|
931
|
+
row,
|
|
932
|
+
col,
|
|
933
|
+
dtype,
|
|
934
|
+
} satisfies SparseMatrix,
|
|
935
|
+
{ marker: 'scipy.sparse', dims: shape.length, dtype }
|
|
936
|
+
);
|
|
591
937
|
}
|
|
592
938
|
|
|
593
939
|
const indices = value.indices;
|
|
@@ -641,14 +987,17 @@ const decodeScipySparseEnvelope: EnvelopeHandler = value => {
|
|
|
641
987
|
);
|
|
642
988
|
}
|
|
643
989
|
assertIndexArrayInRange(indices, minorAxis, 'indices');
|
|
644
|
-
return
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
990
|
+
return tagDecodedShape(
|
|
991
|
+
{
|
|
992
|
+
format,
|
|
993
|
+
shape,
|
|
994
|
+
data,
|
|
995
|
+
indices,
|
|
996
|
+
indptr,
|
|
997
|
+
dtype,
|
|
998
|
+
} satisfies SparseMatrix,
|
|
999
|
+
{ marker: 'scipy.sparse', dims: shape.length, dtype }
|
|
1000
|
+
);
|
|
652
1001
|
};
|
|
653
1002
|
|
|
654
1003
|
/** Product of a shape's dimensions (the element count). [] (scalar) -> 1. */
|
|
@@ -658,7 +1007,7 @@ function shapeProduct(shape: readonly number[]): number {
|
|
|
658
1007
|
|
|
659
1008
|
const decodeTorchTensorEnvelope: EnvelopeHandler = <T>(
|
|
660
1009
|
value: { [k: string]: unknown },
|
|
661
|
-
_decodeArrow:
|
|
1010
|
+
_decodeArrow: DecodeArrow<T>,
|
|
662
1011
|
recurse: (value: unknown) => MaybePromise<T | unknown>
|
|
663
1012
|
): MaybePromise<T | unknown> => {
|
|
664
1013
|
const encoding = value.encoding;
|
|
@@ -672,34 +1021,49 @@ const decodeTorchTensorEnvelope: EnvelopeHandler = <T>(
|
|
|
672
1021
|
if (!isObject(nested) || (nested as { __tywrap__?: unknown }).__tywrap__ !== 'ndarray') {
|
|
673
1022
|
throw new Error('Invalid torch.tensor envelope: value must be an ndarray envelope');
|
|
674
1023
|
}
|
|
675
|
-
const
|
|
676
|
-
const shape =
|
|
677
|
-
// The tensor shape must be a non-negative-integer dimension list. A negative or
|
|
678
|
-
// non-integer dim is a corrupt envelope, not a valid tensor.
|
|
679
|
-
if (shape) {
|
|
680
|
-
for (let i = 0; i < shape.length; i += 1) {
|
|
681
|
-
const dim = shape[i] as number;
|
|
682
|
-
if (!Number.isInteger(dim) || dim < 0) {
|
|
683
|
-
throw new Error(
|
|
684
|
-
`Invalid torch.tensor envelope: shape[${i}]=${dim} must be a non-negative integer`
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
// Cross-check the tensor shape's element count against the nested ndarray's
|
|
690
|
-
// declared shape (metadata only — no decode needed). A mismatch means the two
|
|
691
|
-
// shapes disagree about how many elements the payload holds.
|
|
1024
|
+
const strictV1 = isStrictV1Envelope(value);
|
|
1025
|
+
const shape = readEnvelopeShape(value, value.shape, 'torch.tensor', 'shape', value.dtype);
|
|
692
1026
|
const nestedShapeValue = (nested as { shape?: unknown }).shape;
|
|
693
|
-
const nestedShape =
|
|
694
|
-
|
|
1027
|
+
const nestedShape = readEnvelopeShape(
|
|
1028
|
+
value,
|
|
1029
|
+
nestedShapeValue,
|
|
1030
|
+
'torch.tensor',
|
|
1031
|
+
'value.shape',
|
|
1032
|
+
nested.dtype
|
|
1033
|
+
);
|
|
1034
|
+
const scalarNormalization =
|
|
1035
|
+
shape !== undefined &&
|
|
1036
|
+
nestedShape !== undefined &&
|
|
1037
|
+
((shape.length === 0 && nestedShape.length === 1 && nestedShape[0] === 1) ||
|
|
1038
|
+
(nestedShape.length === 0 && shape.length === 1 && shape[0] === 1));
|
|
1039
|
+
const shapesEqual =
|
|
1040
|
+
shape !== undefined &&
|
|
1041
|
+
nestedShape !== undefined &&
|
|
1042
|
+
shape.length === nestedShape.length &&
|
|
1043
|
+
shape.every((dim, index) => dim === nestedShape[index]);
|
|
1044
|
+
const shapesDisagree = strictV1
|
|
1045
|
+
? !shapesEqual && !scalarNormalization
|
|
1046
|
+
: shape !== undefined &&
|
|
1047
|
+
nestedShape !== undefined &&
|
|
1048
|
+
shapeProduct(shape) !== shapeProduct(nestedShape);
|
|
1049
|
+
if (shape && nestedShape && shapesDisagree) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
`Invalid torch.tensor envelope: shape at path shape ${JSON.stringify(shape)} disagrees ` +
|
|
1052
|
+
`with nested ndarray shape at path value.shape ${JSON.stringify(nestedShape)}; exact shapes are required ` +
|
|
1053
|
+
`(except scalar []/[1]), declared dtypes ${JSON.stringify(value.dtype)}/${JSON.stringify(nested.dtype)}, ` +
|
|
1054
|
+
`actual counts ${shapeProduct(shape)} and ${shapeProduct(nestedShape)}`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
const dtype = readEnvelopeDtype(value, value.dtype, 'torch.tensor', 'dtype', shape);
|
|
1058
|
+
if (strictV1 && dtype === undefined) {
|
|
695
1059
|
throw new Error(
|
|
696
|
-
`Invalid torch.tensor envelope:
|
|
697
|
-
|
|
698
|
-
`${JSON.stringify(nestedShape)} (product ${shapeProduct(nestedShape)})`
|
|
1060
|
+
`Invalid torch.tensor envelope: dtype at path dtype is required for declared shape ` +
|
|
1061
|
+
`${JSON.stringify(shape)}; actual count ${shapeProduct(shape as number[])}, actual type undefined`
|
|
699
1062
|
);
|
|
700
1063
|
}
|
|
701
|
-
|
|
702
|
-
|
|
1064
|
+
if (strictV1 && 'dtype' in nested) {
|
|
1065
|
+
assertOptionalNonEmptyDtype(nested.dtype, 'torch.tensor', 'value.dtype', nestedShape);
|
|
1066
|
+
}
|
|
703
1067
|
const deviceValue = value.device;
|
|
704
1068
|
if (deviceValue !== undefined && (typeof deviceValue !== 'string' || deviceValue.length === 0)) {
|
|
705
1069
|
throw new Error(
|
|
@@ -707,12 +1071,45 @@ const decodeTorchTensorEnvelope: EnvelopeHandler = <T>(
|
|
|
707
1071
|
);
|
|
708
1072
|
}
|
|
709
1073
|
const device = typeof deviceValue === 'string' ? deviceValue : undefined;
|
|
1074
|
+
const sourceDtypeValue = value.sourceDtype;
|
|
1075
|
+
if (
|
|
1076
|
+
sourceDtypeValue !== undefined &&
|
|
1077
|
+
(typeof sourceDtypeValue !== 'string' || sourceDtypeValue.length === 0)
|
|
1078
|
+
) {
|
|
1079
|
+
throw new Error(
|
|
1080
|
+
'Invalid torch.tensor envelope: sourceDtype must be a non-empty string when provided'
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
const sourceDtype = typeof sourceDtypeValue === 'string' ? sourceDtypeValue : undefined;
|
|
1084
|
+
const sourceDeviceValue = value.sourceDevice;
|
|
1085
|
+
if (
|
|
1086
|
+
sourceDeviceValue !== undefined &&
|
|
1087
|
+
(typeof sourceDeviceValue !== 'string' || sourceDeviceValue.length === 0)
|
|
1088
|
+
) {
|
|
1089
|
+
throw new Error(
|
|
1090
|
+
'Invalid torch.tensor envelope: sourceDevice must be a non-empty string when provided'
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
const sourceDevice = typeof sourceDeviceValue === 'string' ? sourceDeviceValue : undefined;
|
|
1094
|
+
|
|
1095
|
+
const finishTensorDecode = (data: unknown): TorchTensor =>
|
|
1096
|
+
tagDecodedShape(
|
|
1097
|
+
{
|
|
1098
|
+
data,
|
|
1099
|
+
shape,
|
|
1100
|
+
dtype,
|
|
1101
|
+
device,
|
|
1102
|
+
...(sourceDtype === undefined ? {} : { sourceDtype }),
|
|
1103
|
+
...(sourceDevice === undefined ? {} : { sourceDevice }),
|
|
1104
|
+
},
|
|
1105
|
+
{ marker: 'torch.tensor', dims: shape?.length, dtype }
|
|
1106
|
+
);
|
|
710
1107
|
|
|
711
1108
|
const decoded = recurse(nested);
|
|
712
1109
|
if (isPromiseLike(decoded)) {
|
|
713
|
-
return decoded.then(
|
|
1110
|
+
return decoded.then(finishTensorDecode) as Promise<T | unknown>;
|
|
714
1111
|
}
|
|
715
|
-
return
|
|
1112
|
+
return finishTensorDecode(decoded);
|
|
716
1113
|
};
|
|
717
1114
|
|
|
718
1115
|
/**
|
|
@@ -770,9 +1167,17 @@ const decodeSklearnEstimatorEnvelope: EnvelopeHandler = value => {
|
|
|
770
1167
|
const className = value.className;
|
|
771
1168
|
const module = value.module;
|
|
772
1169
|
const params = value.params;
|
|
773
|
-
|
|
1170
|
+
const strictV1 = isStrictV1Envelope(value);
|
|
1171
|
+
if (
|
|
1172
|
+
typeof className !== 'string' ||
|
|
1173
|
+
typeof module !== 'string' ||
|
|
1174
|
+
(strictV1 && (className.length === 0 || module.length === 0)) ||
|
|
1175
|
+
!isObject(params)
|
|
1176
|
+
) {
|
|
774
1177
|
throw new Error(
|
|
775
|
-
|
|
1178
|
+
`Invalid sklearn.estimator envelope: expected className/module strings + params object; ` +
|
|
1179
|
+
`className at path className and module at path module must be non-empty; declared class ${JSON.stringify(className)}, ` +
|
|
1180
|
+
`declared module ${JSON.stringify(module)}, actual types ${actualType(className)}/${actualType(module)}`
|
|
776
1181
|
);
|
|
777
1182
|
}
|
|
778
1183
|
// params must be a PLAIN JSON object end to end — metadata-only estimators never
|
|
@@ -793,19 +1198,22 @@ const decodeSklearnEstimatorEnvelope: EnvelopeHandler = value => {
|
|
|
793
1198
|
throw new Error('Invalid sklearn.estimator envelope: version must be a string when provided');
|
|
794
1199
|
}
|
|
795
1200
|
const version = typeof versionValue === 'string' ? versionValue : undefined;
|
|
796
|
-
return
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1201
|
+
return tagDecodedShape(
|
|
1202
|
+
{
|
|
1203
|
+
className,
|
|
1204
|
+
module,
|
|
1205
|
+
version,
|
|
1206
|
+
params,
|
|
1207
|
+
} satisfies SklearnEstimator,
|
|
1208
|
+
{ marker: 'sklearn.estimator' }
|
|
1209
|
+
);
|
|
802
1210
|
};
|
|
803
1211
|
|
|
804
1212
|
// Why: dispatch over the __tywrap__ typeTag instead of a long if-chain so each type's
|
|
805
1213
|
// decode logic lives in one focused handler. The typeTag strings are the on-the-wire keys
|
|
806
1214
|
// emitted by the Python bridge and MUST stay byte-identical. A Map keyed by the typeTag
|
|
807
1215
|
// avoids prototype-chain lookups when dispatching on attacker-controlled typeTag strings.
|
|
808
|
-
const ENVELOPE_HANDLERS: ReadonlyMap<
|
|
1216
|
+
const ENVELOPE_HANDLERS: ReadonlyMap<ScientificMarker, EnvelopeHandler> = new Map([
|
|
809
1217
|
['dataframe', decodeDataframeEnvelope],
|
|
810
1218
|
['series', decodeSeriesEnvelope],
|
|
811
1219
|
['ndarray', decodeNdarrayEnvelope],
|
|
@@ -814,16 +1222,38 @@ const ENVELOPE_HANDLERS: ReadonlyMap<string, EnvelopeHandler> = new Map([
|
|
|
814
1222
|
['sklearn.estimator', decodeSklearnEstimatorEnvelope],
|
|
815
1223
|
]);
|
|
816
1224
|
|
|
1225
|
+
const MAX_DECODE_DEPTH = 2048;
|
|
1226
|
+
const MAX_DECODE_NODES = 1_000_000;
|
|
1227
|
+
|
|
1228
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
1229
|
+
if (!isObject(value)) {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
const proto = Object.getPrototypeOf(value);
|
|
1233
|
+
return proto === Object.prototype || proto === null;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function isPlainArray(value: unknown): value is unknown[] {
|
|
1237
|
+
return Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function decodePath(base: string, key: string | number): string {
|
|
1241
|
+
if (typeof key === 'number') {
|
|
1242
|
+
return `${base}[${key}]`;
|
|
1243
|
+
}
|
|
1244
|
+
return /^[A-Za-z_$][\w$]*$/.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
817
1247
|
function decodeEnvelopeCore<T>(
|
|
818
1248
|
value: unknown,
|
|
819
|
-
decodeArrow:
|
|
1249
|
+
decodeArrow: DecodeArrow<T>,
|
|
820
1250
|
recurse: (value: unknown) => MaybePromise<T | unknown>
|
|
821
1251
|
): MaybePromise<T | unknown> {
|
|
822
1252
|
if (!isObject(value)) {
|
|
823
1253
|
return value;
|
|
824
1254
|
}
|
|
825
1255
|
const typeTag = (value as { __tywrap__?: unknown }).__tywrap__;
|
|
826
|
-
if (
|
|
1256
|
+
if (!isScientificMarker(typeTag)) {
|
|
827
1257
|
return value as unknown;
|
|
828
1258
|
}
|
|
829
1259
|
|
|
@@ -832,11 +1262,23 @@ function decodeEnvelopeCore<T>(
|
|
|
832
1262
|
return value as unknown;
|
|
833
1263
|
}
|
|
834
1264
|
|
|
835
|
-
|
|
836
|
-
|
|
1265
|
+
try {
|
|
1266
|
+
assertCodecVersion(value as { codecVersion?: unknown }, typeTag);
|
|
1267
|
+
const decoded = handler(value, decodeArrow, recurse);
|
|
1268
|
+
return isPromiseLike(decoded)
|
|
1269
|
+
? Promise.resolve(decoded).catch(error => {
|
|
1270
|
+
throw asScientificDecodeError(error, typeTag);
|
|
1271
|
+
})
|
|
1272
|
+
: decoded;
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
throw asScientificDecodeError(error, typeTag);
|
|
1275
|
+
}
|
|
837
1276
|
}
|
|
838
1277
|
|
|
839
|
-
function decodeEnvelope<T>(
|
|
1278
|
+
function decodeEnvelope<T>(
|
|
1279
|
+
value: unknown,
|
|
1280
|
+
decodeArrow: (bytes: Uint8Array, marker: ScientificMarker) => T
|
|
1281
|
+
): T | unknown {
|
|
840
1282
|
const recurse: (value: unknown) => MaybePromise<T | unknown> = v =>
|
|
841
1283
|
decodeEnvelopeCore(v, decodeArrow, recurse);
|
|
842
1284
|
const decoded = decodeEnvelopeCore(value, decodeArrow, recurse);
|
|
@@ -848,30 +1290,225 @@ function decodeEnvelope<T>(value: unknown, decodeArrow: (bytes: Uint8Array) => T
|
|
|
848
1290
|
|
|
849
1291
|
async function decodeEnvelopeAsync<T>(
|
|
850
1292
|
value: unknown,
|
|
851
|
-
decodeArrow: (bytes: Uint8Array) => Promise<T>
|
|
1293
|
+
decodeArrow: (bytes: Uint8Array, marker: ScientificMarker) => Promise<T>
|
|
852
1294
|
): Promise<T | unknown> {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1295
|
+
let visitedNodes = 0;
|
|
1296
|
+
|
|
1297
|
+
type DecodeTarget =
|
|
1298
|
+
| { container: unknown[]; key: number }
|
|
1299
|
+
| { container: Record<string, unknown>; key: string };
|
|
1300
|
+
|
|
1301
|
+
interface ArrayFrame {
|
|
1302
|
+
kind: 'array';
|
|
1303
|
+
container: unknown[];
|
|
1304
|
+
depth: number;
|
|
1305
|
+
path: string;
|
|
1306
|
+
nextIndex: number;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
interface ObjectFrame {
|
|
1310
|
+
kind: 'object';
|
|
1311
|
+
container: Record<string, unknown>;
|
|
1312
|
+
depth: number;
|
|
1313
|
+
path: string;
|
|
1314
|
+
entries: [string, unknown][];
|
|
1315
|
+
nextIndex: number;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
type DecodeFrame = ArrayFrame | ObjectFrame;
|
|
1319
|
+
|
|
1320
|
+
const recordVisit = (depth: number, path: string): void => {
|
|
1321
|
+
visitedNodes += 1;
|
|
1322
|
+
if (visitedNodes > MAX_DECODE_NODES) {
|
|
1323
|
+
throw new Error(
|
|
1324
|
+
`Scientific envelope decode maximum visited nodes ${MAX_DECODE_NODES} exceeded at ${path}`
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
if (depth > MAX_DECODE_DEPTH) {
|
|
1328
|
+
throw new Error(
|
|
1329
|
+
`Scientific envelope decode maximum depth ${MAX_DECODE_DEPTH} exceeded at ${path}`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
const prefixHandlerError = (error: unknown, path: string): never => {
|
|
1335
|
+
if (path === 'result') {
|
|
1336
|
+
throw error;
|
|
1337
|
+
}
|
|
1338
|
+
throw new ScientificDecodeError(
|
|
1339
|
+
'envelope',
|
|
1340
|
+
'unknown',
|
|
1341
|
+
error,
|
|
1342
|
+
`Scientific envelope decode failed at ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
1343
|
+
);
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
const assignDecoded = (target: DecodeTarget, original: unknown, decoded: unknown): void => {
|
|
1347
|
+
if (decoded !== original) {
|
|
1348
|
+
if (Array.isArray(target.container)) {
|
|
1349
|
+
target.container[target.key as number] = decoded;
|
|
1350
|
+
} else {
|
|
1351
|
+
target.container[target.key as string] = decoded;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
const settleDecoded = async (
|
|
1357
|
+
decoded: PromiseLike<T | unknown>,
|
|
1358
|
+
original: unknown,
|
|
1359
|
+
target: DecodeTarget,
|
|
1360
|
+
path: string
|
|
1361
|
+
): Promise<void> => {
|
|
1362
|
+
let resolved: T | unknown;
|
|
1363
|
+
try {
|
|
1364
|
+
resolved = await decoded;
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
return prefixHandlerError(error, path);
|
|
1367
|
+
}
|
|
1368
|
+
assignDecoded(target, original, resolved);
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
const decodeSingle = (item: unknown, depth: number, path: string): MaybePromise<T | unknown> => {
|
|
1372
|
+
// Handlers sometimes decode a required child envelope themselves (currently
|
|
1373
|
+
// torch.tensor.value). Keep that single-envelope recursion unchanged.
|
|
1374
|
+
const recurseSingle: (nested: unknown) => MaybePromise<T | unknown> = nested => {
|
|
1375
|
+
const nestedPath = decodePath(path, 'value');
|
|
1376
|
+
recordVisit(depth + 1, nestedPath);
|
|
1377
|
+
return decodeEnvelopeCore(nested, decodeArrow, recurseSingle);
|
|
1378
|
+
};
|
|
1379
|
+
return decodeEnvelopeCore(item, decodeArrow, recurseSingle);
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
const frames: DecodeFrame[] = [];
|
|
1383
|
+
const pending: Promise<void>[] = [];
|
|
1384
|
+
|
|
1385
|
+
const visit = (current: unknown, depth: number, path: string, target?: DecodeTarget): void => {
|
|
1386
|
+
const plainArray = isPlainArray(current);
|
|
1387
|
+
const plainObject = isPlainObject(current);
|
|
1388
|
+
|
|
1389
|
+
// Only containers consume the traversal budget. Primitive leaves are already
|
|
1390
|
+
// bounded by the payload byte cap, and counting them would reject large plain
|
|
1391
|
+
// arrays that decoded fine before recursion existed.
|
|
1392
|
+
if (plainArray || plainObject) {
|
|
1393
|
+
recordVisit(depth, path);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (plainObject && typeof current.__tywrap__ === 'string') {
|
|
1397
|
+
if (!isScientificMarker(current.__tywrap__)) {
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
let decoded: MaybePromise<T | unknown>;
|
|
1402
|
+
try {
|
|
1403
|
+
decoded = decodeSingle(current, depth, path);
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
return prefixHandlerError(error, path);
|
|
1406
|
+
}
|
|
1407
|
+
if (isPromiseLike(decoded)) {
|
|
1408
|
+
if (target) {
|
|
1409
|
+
pending.push(settleDecoded(decoded, current, target, path));
|
|
1410
|
+
} else {
|
|
1411
|
+
pending.push(settleDecoded(decoded, current, { container: root, key: 'value' }, path));
|
|
1412
|
+
}
|
|
1413
|
+
} else if (target) {
|
|
1414
|
+
assignDecoded(target, current, decoded);
|
|
1415
|
+
} else if (decoded !== current) {
|
|
1416
|
+
root.value = decoded;
|
|
1417
|
+
}
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (plainArray) {
|
|
1422
|
+
frames.push({ kind: 'array', container: current, depth, path, nextIndex: 0 });
|
|
1423
|
+
} else if (plainObject) {
|
|
1424
|
+
frames.push({
|
|
1425
|
+
kind: 'object',
|
|
1426
|
+
container: current,
|
|
1427
|
+
depth,
|
|
1428
|
+
path,
|
|
1429
|
+
entries: Object.entries(current),
|
|
1430
|
+
nextIndex: 0,
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Values have copy semantics across the bridge: repeated Python references are
|
|
1437
|
+
* serialized independently, and this decoder does not restore alias identity.
|
|
1438
|
+
*/
|
|
1439
|
+
const root: { value: unknown } = { value };
|
|
1440
|
+
visit(value, 0, 'result');
|
|
1441
|
+
|
|
1442
|
+
const visitChild = (
|
|
1443
|
+
item: unknown,
|
|
1444
|
+
key: string | number,
|
|
1445
|
+
target: DecodeTarget,
|
|
1446
|
+
frame: DecodeFrame
|
|
1447
|
+
): void => {
|
|
1448
|
+
try {
|
|
1449
|
+
visit(item, frame.depth + 1, decodePath(frame.path, key), target);
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
pending.push(Promise.reject(error));
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
while (frames.length > 0) {
|
|
1456
|
+
const frame = frames[frames.length - 1];
|
|
1457
|
+
if (!frame) {
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
if (
|
|
1461
|
+
frame.nextIndex >= (frame.kind === 'array' ? frame.container.length : frame.entries.length)
|
|
1462
|
+
) {
|
|
1463
|
+
frames.pop();
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const index = frame.nextIndex;
|
|
1468
|
+
frame.nextIndex += 1;
|
|
1469
|
+
if (frame.kind === 'array') {
|
|
1470
|
+
visitChild(frame.container[index], index, { container: frame.container, key: index }, frame);
|
|
1471
|
+
} else {
|
|
1472
|
+
const entry = frame.entries[index];
|
|
1473
|
+
if (entry) {
|
|
1474
|
+
const [key, item] = entry;
|
|
1475
|
+
visitChild(item, key, { container: frame.container, key }, frame);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (pending.length > 0) {
|
|
1481
|
+
await Promise.all(pending);
|
|
1482
|
+
}
|
|
1483
|
+
return root.value;
|
|
856
1484
|
}
|
|
857
1485
|
|
|
858
1486
|
/**
|
|
859
1487
|
* Decode values produced by the Python bridge.
|
|
860
1488
|
*/
|
|
861
1489
|
export async function decodeValueAsync(value: unknown): Promise<DecodedValue> {
|
|
862
|
-
|
|
1490
|
+
try {
|
|
1491
|
+
return await decodeEnvelopeAsync(value, tryDecodeArrowTable);
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
throw asScientificDecodeError(error, 'unknown');
|
|
1494
|
+
}
|
|
863
1495
|
}
|
|
864
1496
|
|
|
865
1497
|
/**
|
|
866
1498
|
* Synchronous decode. Arrow decoding requires a registered decoder.
|
|
867
1499
|
*/
|
|
868
1500
|
export function decodeValue(value: unknown): DecodedValue {
|
|
869
|
-
const decodeArrow = (bytes: Uint8Array): DecodedValue => {
|
|
870
|
-
const decoder = requireArrowDecoder();
|
|
1501
|
+
const decodeArrow = (bytes: Uint8Array, marker: ScientificMarker): DecodedValue => {
|
|
1502
|
+
const decoder = requireArrowDecoder(marker);
|
|
871
1503
|
try {
|
|
872
1504
|
return decoder(bytes);
|
|
873
1505
|
} catch (err) {
|
|
874
|
-
throw new
|
|
1506
|
+
throw new ScientificDecodeError(
|
|
1507
|
+
'arrow',
|
|
1508
|
+
marker,
|
|
1509
|
+
err,
|
|
1510
|
+
`Arrow decode failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1511
|
+
);
|
|
875
1512
|
}
|
|
876
1513
|
};
|
|
877
1514
|
return decodeEnvelope(value, decodeArrow);
|