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/dist/utils/codec.js
CHANGED
|
@@ -8,6 +8,35 @@
|
|
|
8
8
|
* Torch tensors: { "__tywrap__": "torch.tensor", "encoding": "ndarray", "value": { ... } }
|
|
9
9
|
* Sklearn estimators: { "__tywrap__": "sklearn.estimator", "encoding": "json", ... }
|
|
10
10
|
*/
|
|
11
|
+
import { tagDecodedShape } from '../runtime/validators.js';
|
|
12
|
+
const SCIENTIFIC_MARKERS = [
|
|
13
|
+
'dataframe',
|
|
14
|
+
'series',
|
|
15
|
+
'ndarray',
|
|
16
|
+
'scipy.sparse',
|
|
17
|
+
'torch.tensor',
|
|
18
|
+
'sklearn.estimator',
|
|
19
|
+
];
|
|
20
|
+
export function isScientificMarker(value) {
|
|
21
|
+
return SCIENTIFIC_MARKERS.includes(value);
|
|
22
|
+
}
|
|
23
|
+
/** @internal Structured failure consumed by BridgeCodec without parsing messages. */
|
|
24
|
+
export class ScientificDecodeError extends Error {
|
|
25
|
+
kind;
|
|
26
|
+
marker;
|
|
27
|
+
cause;
|
|
28
|
+
constructor(kind, marker, cause, message = cause instanceof Error ? cause.message : String(cause)) {
|
|
29
|
+
super(message, { cause });
|
|
30
|
+
this.kind = kind;
|
|
31
|
+
this.marker = marker;
|
|
32
|
+
this.cause = cause;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function asScientificDecodeError(error, marker) {
|
|
36
|
+
return error instanceof ScientificDecodeError
|
|
37
|
+
? error
|
|
38
|
+
: new ScientificDecodeError('envelope', marker, error);
|
|
39
|
+
}
|
|
11
40
|
let arrowTableFrom;
|
|
12
41
|
// Why: lazy auto-registration (on first Arrow decode) imports apache-arrow at most
|
|
13
42
|
// once per process. We cache the in-flight/settled attempt so concurrent decodes
|
|
@@ -109,7 +138,12 @@ function isObject(value) {
|
|
|
109
138
|
function isNumberArray(value) {
|
|
110
139
|
return Array.isArray(value) && value.every(item => typeof item === 'number');
|
|
111
140
|
}
|
|
112
|
-
|
|
141
|
+
const STRICT_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
142
|
+
function fromBase64(b64, marker, context) {
|
|
143
|
+
if (marker && !STRICT_BASE64_PATTERN.test(b64)) {
|
|
144
|
+
throw new Error(`Invalid ${marker} envelope: b64 at path b64 must be well-formed base64; ` +
|
|
145
|
+
`${context ? `${context}; ` : ''}actual count unknown, actual type string with length ${b64.length}`);
|
|
146
|
+
}
|
|
113
147
|
if (typeof Buffer !== 'undefined') {
|
|
114
148
|
const buf = Buffer.from(b64, 'base64');
|
|
115
149
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.length);
|
|
@@ -127,9 +161,9 @@ const ARROW_MISSING_MESSAGE = 'Received an Arrow-encoded payload but no Arrow de
|
|
|
127
161
|
'Install the optional dependency with `npm install apache-arrow`, or set ' +
|
|
128
162
|
'TYWRAP_CODEC_FALLBACK=json on the Python side to receive JSON instead ' +
|
|
129
163
|
'(lossy for dtype/NA fidelity). tywrap never silently downgrades Arrow payloads.';
|
|
130
|
-
function requireArrowDecoder() {
|
|
164
|
+
function requireArrowDecoder(marker) {
|
|
131
165
|
if (!arrowTableFrom) {
|
|
132
|
-
throw new Error(ARROW_MISSING_MESSAGE);
|
|
166
|
+
throw new ScientificDecodeError('arrow', marker, new Error(ARROW_MISSING_MESSAGE));
|
|
133
167
|
}
|
|
134
168
|
return arrowTableFrom;
|
|
135
169
|
}
|
|
@@ -141,7 +175,7 @@ function requireArrowDecoder() {
|
|
|
141
175
|
* for the rest of the process. If apache-arrow is absent we throw a clear, actionable
|
|
142
176
|
* error rather than silently producing wrong data.
|
|
143
177
|
*/
|
|
144
|
-
async function ensureArrowDecoder() {
|
|
178
|
+
async function ensureArrowDecoder(marker) {
|
|
145
179
|
if (arrowTableFrom) {
|
|
146
180
|
return arrowTableFrom;
|
|
147
181
|
}
|
|
@@ -149,17 +183,17 @@ async function ensureArrowDecoder() {
|
|
|
149
183
|
lazyRegistration ??= autoRegisterArrowDecoder(lazyArrowLoaderOverride ? { loader: lazyArrowLoaderOverride } : {});
|
|
150
184
|
await lazyRegistration;
|
|
151
185
|
if (!arrowTableFrom) {
|
|
152
|
-
throw new Error(ARROW_MISSING_MESSAGE);
|
|
186
|
+
throw new ScientificDecodeError('arrow', marker, new Error(ARROW_MISSING_MESSAGE));
|
|
153
187
|
}
|
|
154
188
|
return arrowTableFrom;
|
|
155
189
|
}
|
|
156
|
-
async function tryDecodeArrowTable(bytes) {
|
|
157
|
-
const decoder = await ensureArrowDecoder();
|
|
190
|
+
async function tryDecodeArrowTable(bytes, marker) {
|
|
191
|
+
const decoder = await ensureArrowDecoder(marker);
|
|
158
192
|
try {
|
|
159
193
|
return decoder(bytes);
|
|
160
194
|
}
|
|
161
195
|
catch (err) {
|
|
162
|
-
throw new
|
|
196
|
+
throw new ScientificDecodeError('arrow', marker, err, `Arrow decode failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
163
197
|
}
|
|
164
198
|
}
|
|
165
199
|
function isPromiseLike(value) {
|
|
@@ -198,8 +232,7 @@ function typedArrayToPlain(arr) {
|
|
|
198
232
|
if (arr !== null && arr !== undefined && typeof arr === 'object' && Symbol.iterator in arr) {
|
|
199
233
|
return Array.from(arr);
|
|
200
234
|
}
|
|
201
|
-
|
|
202
|
-
return [];
|
|
235
|
+
return null;
|
|
203
236
|
}
|
|
204
237
|
/**
|
|
205
238
|
* Extract values from an Arrow table as a plain JavaScript array.
|
|
@@ -221,6 +254,22 @@ function extractArrowValues(data) {
|
|
|
221
254
|
}
|
|
222
255
|
return null;
|
|
223
256
|
}
|
|
257
|
+
function extractNdarrayArrowValues(data, shape, dtype) {
|
|
258
|
+
try {
|
|
259
|
+
const values = extractArrowValues(data);
|
|
260
|
+
if (values) {
|
|
261
|
+
return values;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
throw new Error(`Invalid ndarray envelope: b64 decoded data extraction failed at path b64 for declared ` +
|
|
266
|
+
`shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; actual count unknown, ` +
|
|
267
|
+
`actual type ${actualType(data)} (${error instanceof Error ? error.message : String(error)})`);
|
|
268
|
+
}
|
|
269
|
+
throw new Error(`Invalid ndarray envelope: b64 decoded data at path b64 could not extract Arrow values ` +
|
|
270
|
+
`for declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
|
|
271
|
+
`actual count unknown, actual type ${actualType(data)}`);
|
|
272
|
+
}
|
|
224
273
|
/**
|
|
225
274
|
* Reshape a flat array into a multi-dimensional nested array.
|
|
226
275
|
*
|
|
@@ -255,6 +304,111 @@ function reshapeArray(flat, shape) {
|
|
|
255
304
|
}
|
|
256
305
|
// Why: decoding needs to reject incompatible envelopes before we attempt to interpret payloads.
|
|
257
306
|
const CODEC_VERSION = 1;
|
|
307
|
+
function isStrictV1Envelope(envelope) {
|
|
308
|
+
return envelope.codecVersion === CODEC_VERSION;
|
|
309
|
+
}
|
|
310
|
+
function actualType(value) {
|
|
311
|
+
if (Array.isArray(value)) {
|
|
312
|
+
return `array(length=${value.length})`;
|
|
313
|
+
}
|
|
314
|
+
if (value === null) {
|
|
315
|
+
return 'null';
|
|
316
|
+
}
|
|
317
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
318
|
+
return `number(${String(value)})`;
|
|
319
|
+
}
|
|
320
|
+
return typeof value;
|
|
321
|
+
}
|
|
322
|
+
function assertShape(shapeValue, marker, field = 'shape', dtype) {
|
|
323
|
+
if (!Array.isArray(shapeValue)) {
|
|
324
|
+
throw new Error(`Invalid ${marker} envelope: ${field} at path ${field} must be a list of ` +
|
|
325
|
+
`non-negative safe integers; declared shape ${String(shapeValue)} and dtype ` +
|
|
326
|
+
`${JSON.stringify(dtype)}, actual count unknown, actual type ${actualType(shapeValue)}`);
|
|
327
|
+
}
|
|
328
|
+
for (let i = 0; i < shapeValue.length; i += 1) {
|
|
329
|
+
const dim = shapeValue[i];
|
|
330
|
+
if (typeof dim !== 'number' || !Number.isSafeInteger(dim) || dim < 0) {
|
|
331
|
+
throw new Error(`Invalid ${marker} envelope: ${field}[${i}]=${String(dim)} must be a non-negative integer ` +
|
|
332
|
+
`within the safe range for declared shape ${JSON.stringify(shapeValue)}; ` +
|
|
333
|
+
`declared dtype ${JSON.stringify(dtype)}, actual count unknown, ` +
|
|
334
|
+
`actual type ${actualType(dim)} with value ${String(dim)}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return shapeValue;
|
|
338
|
+
}
|
|
339
|
+
function assertOptionalNonEmptyDtype(value, marker, field = 'dtype', shape) {
|
|
340
|
+
if (value === undefined) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
344
|
+
throw new Error(`Invalid ${marker} envelope: ${field} at path ${field} must be a non-empty string; ` +
|
|
345
|
+
`declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(value)}, ` +
|
|
346
|
+
`actual count unknown, actual type ${actualType(value)}`);
|
|
347
|
+
}
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
350
|
+
function readEnvelopeShape(envelope, shapeValue, marker, field = 'shape', dtype) {
|
|
351
|
+
return isStrictV1Envelope(envelope)
|
|
352
|
+
? assertShape(shapeValue, marker, field, dtype)
|
|
353
|
+
: isNumberArray(shapeValue)
|
|
354
|
+
? shapeValue
|
|
355
|
+
: undefined;
|
|
356
|
+
}
|
|
357
|
+
function readEnvelopeDtype(envelope, dtypeValue, marker, field = 'dtype', shape) {
|
|
358
|
+
return isStrictV1Envelope(envelope)
|
|
359
|
+
? assertOptionalNonEmptyDtype(dtypeValue, marker, field, shape)
|
|
360
|
+
: typeof dtypeValue === 'string'
|
|
361
|
+
? dtypeValue
|
|
362
|
+
: undefined;
|
|
363
|
+
}
|
|
364
|
+
function countJsonLeaves(value) {
|
|
365
|
+
if (!Array.isArray(value)) {
|
|
366
|
+
return 1;
|
|
367
|
+
}
|
|
368
|
+
return value.reduce((count, item) => count + countJsonLeaves(item), 0);
|
|
369
|
+
}
|
|
370
|
+
function assertJsonMatchesShape(data, shape, dtype) {
|
|
371
|
+
const declaredCount = shapeProduct(shape);
|
|
372
|
+
const strictLeaves = dtype !== undefined;
|
|
373
|
+
const actualCount = strictLeaves ? countJsonLeaves(data) : undefined;
|
|
374
|
+
const visit = (value, depth, path) => {
|
|
375
|
+
if (depth === shape.length) {
|
|
376
|
+
if (strictLeaves && Array.isArray(value)) {
|
|
377
|
+
throw new Error(`Invalid ndarray envelope: data at path ${path} exceeds nesting depth ` +
|
|
378
|
+
`${shape.length} for declared shape ${JSON.stringify(shape)} and dtype ` +
|
|
379
|
+
`${JSON.stringify(dtype)}; actual count ${actualCount}, actual type ${actualType(value)}`);
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (!Array.isArray(value)) {
|
|
384
|
+
throw new Error(`Invalid ndarray envelope: data at path ${path} must be an array at depth ${depth} ` +
|
|
385
|
+
`for declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
|
|
386
|
+
`actual count ${actualCount ?? 'unknown'}, actual type ${actualType(value)}`);
|
|
387
|
+
}
|
|
388
|
+
const expectedLength = shape[depth];
|
|
389
|
+
if (value.length !== expectedLength) {
|
|
390
|
+
throw new Error(`Invalid ndarray envelope: data at path ${path} has length ${value.length}, expected ` +
|
|
391
|
+
`${expectedLength} for declared shape ${JSON.stringify(shape)} and dtype ` +
|
|
392
|
+
`${JSON.stringify(dtype)}; declared count ${declaredCount}, actual count ${actualCount ?? 'unknown'}, ` +
|
|
393
|
+
`actual type ${actualType(value)}`);
|
|
394
|
+
}
|
|
395
|
+
value.forEach((item, index) => visit(item, depth + 1, `${path}[${index}]`));
|
|
396
|
+
};
|
|
397
|
+
visit(data, 0, 'data');
|
|
398
|
+
if (strictLeaves && actualCount !== declaredCount) {
|
|
399
|
+
throw new Error(`Invalid ndarray envelope: data at path data has leaf count ${actualCount}, expected ` +
|
|
400
|
+
`${declaredCount} for declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
|
|
401
|
+
`actual type ${actualType(data)}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function assertArrowCount(values, shape, dtype) {
|
|
405
|
+
const expected = shapeProduct(shape);
|
|
406
|
+
if (values.length !== expected) {
|
|
407
|
+
throw new Error(`Invalid ndarray envelope: b64 decoded data at path b64 has element count ${values.length}, ` +
|
|
408
|
+
`expected ${expected} for declared shape ${JSON.stringify(shape)} and dtype ` +
|
|
409
|
+
`${JSON.stringify(dtype)}; actual type array(length=${values.length})`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
258
412
|
function assertCodecVersion(envelope, typeTag) {
|
|
259
413
|
if (!('codecVersion' in envelope)) {
|
|
260
414
|
return;
|
|
@@ -283,62 +437,77 @@ function decodeArrowOrJsonEnvelope(value, decodeArrow, typeTag) {
|
|
|
283
437
|
if (typeof b64 !== 'string') {
|
|
284
438
|
throw new Error(`Invalid ${typeTag} envelope: missing b64`);
|
|
285
439
|
}
|
|
286
|
-
const bytes = fromBase64(b64);
|
|
287
|
-
|
|
440
|
+
const bytes = fromBase64(b64, isStrictV1Envelope(value) ? typeTag : undefined);
|
|
441
|
+
const decoded = decodeArrow(bytes, typeTag);
|
|
442
|
+
return isPromiseLike(decoded)
|
|
443
|
+
? decoded.then(item => tagDecodedShape(item, { marker: typeTag }))
|
|
444
|
+
: tagDecodedShape(decoded, { marker: typeTag });
|
|
288
445
|
}
|
|
289
446
|
if (encoding === 'json') {
|
|
290
447
|
if (!('data' in value)) {
|
|
291
448
|
throw new Error(`Invalid ${typeTag} envelope: missing data`);
|
|
292
449
|
}
|
|
293
|
-
return value.data;
|
|
450
|
+
return tagDecodedShape(value.data, { marker: typeTag });
|
|
294
451
|
}
|
|
295
452
|
throw new Error(`Invalid ${typeTag} envelope: unsupported encoding ${String(encoding)}`);
|
|
296
453
|
}
|
|
297
454
|
const decodeDataframeEnvelope = (value, decodeArrow) => decodeArrowOrJsonEnvelope(value, decodeArrow, 'dataframe');
|
|
298
455
|
const decodeSeriesEnvelope = (value, decodeArrow) => decodeArrowOrJsonEnvelope(value, decodeArrow, 'series');
|
|
456
|
+
function finishNdarrayDecode(data, strictV1, shape, dtype, metadata) {
|
|
457
|
+
const values = strictV1
|
|
458
|
+
? extractNdarrayArrowValues(data, shape, dtype)
|
|
459
|
+
: extractArrowValues(data);
|
|
460
|
+
if (!values) {
|
|
461
|
+
return tagDecodedShape(data, metadata);
|
|
462
|
+
}
|
|
463
|
+
if (strictV1) {
|
|
464
|
+
assertArrowCount(values, shape, dtype);
|
|
465
|
+
}
|
|
466
|
+
return tagDecodedShape(shape && shape.length !== 1 ? reshapeArray(values, shape) : values, metadata);
|
|
467
|
+
}
|
|
299
468
|
const decodeNdarrayEnvelope = (value, decodeArrow) => {
|
|
300
469
|
const encoding = value.encoding;
|
|
301
|
-
const
|
|
302
|
-
const shape =
|
|
470
|
+
const strictV1 = isStrictV1Envelope(value);
|
|
471
|
+
const shape = readEnvelopeShape(value, value.shape, 'ndarray', 'shape', value.dtype);
|
|
472
|
+
const dtype = readEnvelopeDtype(value, value.dtype, 'ndarray', 'dtype', shape);
|
|
473
|
+
const metadata = { marker: 'ndarray', dims: shape?.length, dtype };
|
|
303
474
|
if (encoding === 'arrow') {
|
|
475
|
+
if (strictV1 && dtype === undefined) {
|
|
476
|
+
throw new Error(`Invalid ndarray envelope: dtype at path dtype is required for Arrow encoding; ` +
|
|
477
|
+
`declared shape ${JSON.stringify(shape)} and dtype undefined, actual count unknown, actual type undefined`);
|
|
478
|
+
}
|
|
304
479
|
const b64 = value.b64;
|
|
305
480
|
if (typeof b64 !== 'string') {
|
|
306
|
-
|
|
481
|
+
if (!strictV1) {
|
|
482
|
+
throw new Error('Invalid ndarray envelope: missing b64');
|
|
483
|
+
}
|
|
484
|
+
throw new Error(`Invalid ndarray envelope: b64 at path b64 must be a base64 string for declared ` +
|
|
485
|
+
`shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; actual count unknown, ` +
|
|
486
|
+
`actual type ${actualType(b64)}`);
|
|
307
487
|
}
|
|
308
|
-
const bytes = fromBase64(b64);
|
|
309
|
-
const decoded = decodeArrow(bytes);
|
|
488
|
+
const bytes = fromBase64(b64, strictV1 ? 'ndarray' : undefined, `declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}`);
|
|
489
|
+
const decoded = decodeArrow(bytes, 'ndarray');
|
|
310
490
|
// Extract values from Arrow table and reshape if needed
|
|
311
491
|
// Arrow only handles 1D arrays, so we flatten on encode and reshape here
|
|
312
492
|
// Reshape for: scalars (shape.length === 0) and multi-dim (shape.length > 1)
|
|
313
493
|
// Skip reshape for: 1D arrays (shape.length === 1) - return as-is
|
|
314
494
|
if (isPromiseLike(decoded)) {
|
|
315
|
-
return decoded.then(data =>
|
|
316
|
-
const values = extractArrowValues(data);
|
|
317
|
-
if (!values) {
|
|
318
|
-
return data; // Fallback: return raw data if extraction fails
|
|
319
|
-
}
|
|
320
|
-
// Reshape scalars and multi-dimensional arrays, but not 1D
|
|
321
|
-
if (shape && shape.length !== 1) {
|
|
322
|
-
return reshapeArray(values, shape);
|
|
323
|
-
}
|
|
324
|
-
return values;
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
const values = extractArrowValues(decoded);
|
|
328
|
-
if (!values) {
|
|
329
|
-
return decoded; // Fallback: return raw data if extraction fails
|
|
330
|
-
}
|
|
331
|
-
// Reshape scalars and multi-dimensional arrays, but not 1D
|
|
332
|
-
if (shape && shape.length !== 1) {
|
|
333
|
-
return reshapeArray(values, shape);
|
|
495
|
+
return decoded.then(data => finishNdarrayDecode(data, strictV1, shape, dtype, metadata));
|
|
334
496
|
}
|
|
335
|
-
return
|
|
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
|
-
return value.data;
|
|
510
|
+
return tagDecodedShape(value.data, metadata);
|
|
342
511
|
}
|
|
343
512
|
throw new Error(`Invalid ndarray envelope: unsupported encoding ${String(encoding)}`);
|
|
344
513
|
};
|
|
@@ -362,6 +531,43 @@ function assertIndexArrayInRange(arr, bound, label) {
|
|
|
362
531
|
}
|
|
363
532
|
}
|
|
364
533
|
}
|
|
534
|
+
function assertSparseDataDomain(data, dtype, shape) {
|
|
535
|
+
const expectsBoolean = dtype === 'bool' || dtype === 'bool_';
|
|
536
|
+
const integerMatch = /^(u?int)(\d*)$/.exec(dtype);
|
|
537
|
+
const expectsInteger = integerMatch !== null;
|
|
538
|
+
const expectsFloat = /^float\d*$/.test(dtype);
|
|
539
|
+
const width = integerMatch?.[2] ? Number(integerMatch[2]) : undefined;
|
|
540
|
+
const unsignedInteger = integerMatch?.[1] === 'uint';
|
|
541
|
+
let integerMin;
|
|
542
|
+
let integerMax;
|
|
543
|
+
if (width === 8 || width === 16 || width === 32) {
|
|
544
|
+
integerMin = unsignedInteger ? 0 : -(2 ** (width - 1));
|
|
545
|
+
integerMax = unsignedInteger ? 2 ** width - 1 : 2 ** (width - 1) - 1;
|
|
546
|
+
}
|
|
547
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
548
|
+
const item = data[i];
|
|
549
|
+
if (typeof item === 'number' && !Number.isFinite(item)) {
|
|
550
|
+
throw new Error(`Invalid scipy.sparse envelope: data[${i}] at path data[${i}] must be finite for ` +
|
|
551
|
+
`declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
|
|
552
|
+
`actual count ${data.length}, actual type number with value ${String(item)}`);
|
|
553
|
+
}
|
|
554
|
+
const valid = expectsBoolean
|
|
555
|
+
? typeof item === 'boolean'
|
|
556
|
+
: expectsInteger
|
|
557
|
+
? typeof item === 'number' &&
|
|
558
|
+
Number.isInteger(item) &&
|
|
559
|
+
(integerMin === undefined || item >= integerMin) &&
|
|
560
|
+
(integerMax === undefined || item <= integerMax)
|
|
561
|
+
: expectsFloat
|
|
562
|
+
? typeof item === 'number'
|
|
563
|
+
: true;
|
|
564
|
+
if (!valid) {
|
|
565
|
+
throw new Error(`Invalid scipy.sparse envelope: data[${i}] at path data[${i}] is incompatible with ` +
|
|
566
|
+
`declared shape ${JSON.stringify(shape)} and dtype ${JSON.stringify(dtype)}; ` +
|
|
567
|
+
`actual count ${data.length}, actual type ${actualType(item)} with value ${String(item)}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
365
571
|
const decodeScipySparseEnvelope = value => {
|
|
366
572
|
const encoding = value.encoding;
|
|
367
573
|
if (encoding !== 'json') {
|
|
@@ -371,7 +577,8 @@ const decodeScipySparseEnvelope = value => {
|
|
|
371
577
|
if (format !== 'csr' && format !== 'csc' && format !== 'coo') {
|
|
372
578
|
throw new Error(`Invalid scipy.sparse envelope: unsupported format ${String(format)}`);
|
|
373
579
|
}
|
|
374
|
-
const
|
|
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);
|