tywrap 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +15 -5
  2. package/dist/core/annotation-parser.d.ts.map +1 -1
  3. package/dist/core/annotation-parser.js.map +1 -1
  4. package/dist/core/emit-call.d.ts.map +1 -1
  5. package/dist/core/emit-call.js +1 -1
  6. package/dist/core/emit-call.js.map +1 -1
  7. package/dist/core/generator.d.ts.map +1 -1
  8. package/dist/core/generator.js +40 -9
  9. package/dist/core/generator.js.map +1 -1
  10. package/dist/dev.d.ts.map +1 -1
  11. package/dist/dev.js +1 -3
  12. package/dist/dev.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/runtime/base-bridge.d.ts +57 -0
  17. package/dist/runtime/base-bridge.d.ts.map +1 -0
  18. package/dist/runtime/base-bridge.js +72 -0
  19. package/dist/runtime/base-bridge.js.map +1 -0
  20. package/dist/runtime/frame-codec.d.ts +111 -0
  21. package/dist/runtime/frame-codec.d.ts.map +1 -0
  22. package/dist/runtime/frame-codec.js +352 -0
  23. package/dist/runtime/frame-codec.js.map +1 -0
  24. package/dist/runtime/http-transport.d.ts +11 -1
  25. package/dist/runtime/http-transport.d.ts.map +1 -1
  26. package/dist/runtime/http-transport.js +19 -0
  27. package/dist/runtime/http-transport.js.map +1 -1
  28. package/dist/runtime/http.d.ts +5 -12
  29. package/dist/runtime/http.d.ts.map +1 -1
  30. package/dist/runtime/http.js +6 -29
  31. package/dist/runtime/http.js.map +1 -1
  32. package/dist/runtime/index.d.ts +2 -2
  33. package/dist/runtime/index.d.ts.map +1 -1
  34. package/dist/runtime/index.js +1 -1
  35. package/dist/runtime/index.js.map +1 -1
  36. package/dist/runtime/node.d.ts +25 -19
  37. package/dist/runtime/node.d.ts.map +1 -1
  38. package/dist/runtime/node.js +19 -34
  39. package/dist/runtime/node.js.map +1 -1
  40. package/dist/runtime/pooled-transport.d.ts +21 -2
  41. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  42. package/dist/runtime/pooled-transport.js +16 -0
  43. package/dist/runtime/pooled-transport.js.map +1 -1
  44. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  45. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  46. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  47. package/dist/runtime/pyodide-transport.d.ts +12 -1
  48. package/dist/runtime/pyodide-transport.d.ts.map +1 -1
  49. package/dist/runtime/pyodide-transport.js +20 -0
  50. package/dist/runtime/pyodide-transport.js.map +1 -1
  51. package/dist/runtime/pyodide.d.ts +5 -12
  52. package/dist/runtime/pyodide.d.ts.map +1 -1
  53. package/dist/runtime/pyodide.js +6 -29
  54. package/dist/runtime/pyodide.js.map +1 -1
  55. package/dist/runtime/rpc-client.d.ts +14 -1
  56. package/dist/runtime/rpc-client.d.ts.map +1 -1
  57. package/dist/runtime/rpc-client.js +68 -6
  58. package/dist/runtime/rpc-client.js.map +1 -1
  59. package/dist/runtime/subprocess-transport.d.ts +177 -3
  60. package/dist/runtime/subprocess-transport.d.ts.map +1 -1
  61. package/dist/runtime/subprocess-transport.js +526 -26
  62. package/dist/runtime/subprocess-transport.js.map +1 -1
  63. package/dist/runtime/transport.d.ts +141 -0
  64. package/dist/runtime/transport.d.ts.map +1 -1
  65. package/dist/runtime/transport.js +21 -0
  66. package/dist/runtime/transport.js.map +1 -1
  67. package/dist/types/index.d.ts +59 -0
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/dist/tywrap.d.ts.map +1 -1
  70. package/dist/tywrap.js +204 -149
  71. package/dist/tywrap.js.map +1 -1
  72. package/dist/utils/codec.d.ts +2 -0
  73. package/dist/utils/codec.d.ts.map +1 -1
  74. package/dist/utils/codec.js +205 -6
  75. package/dist/utils/codec.js.map +1 -1
  76. package/dist/version.js +1 -1
  77. package/package.json +7 -1
  78. package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
  79. package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
  80. package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
  81. package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
  82. package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
  83. package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
  84. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  85. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  86. package/runtime/frame_codec.py +424 -0
  87. package/runtime/python_bridge.py +241 -42
  88. package/runtime/tywrap_bridge_core.py +152 -13
  89. package/src/core/annotation-parser.ts +2 -1
  90. package/src/core/emit-call.ts +1 -7
  91. package/src/core/generator.ts +50 -11
  92. package/src/dev.ts +1 -3
  93. package/src/index.ts +1 -0
  94. package/src/runtime/base-bridge.ts +106 -0
  95. package/src/runtime/frame-codec.ts +469 -0
  96. package/src/runtime/http-transport.ts +21 -1
  97. package/src/runtime/http.ts +7 -51
  98. package/src/runtime/index.ts +2 -6
  99. package/src/runtime/node.ts +42 -53
  100. package/src/runtime/pooled-transport.ts +25 -2
  101. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  102. package/src/runtime/pyodide-transport.ts +22 -0
  103. package/src/runtime/pyodide.ts +7 -52
  104. package/src/runtime/rpc-client.ts +91 -7
  105. package/src/runtime/subprocess-transport.ts +629 -30
  106. package/src/runtime/transport.ts +169 -0
  107. package/src/types/index.ts +62 -0
  108. package/src/tywrap.ts +265 -162
  109. package/src/utils/codec.ts +245 -7
  110. package/src/version.ts +1 -1
@@ -111,6 +111,24 @@ export type DecodedValue =
111
111
 
112
112
  let arrowTableFrom: ((bytes: Uint8Array) => ArrowTable | Uint8Array) | undefined;
113
113
 
114
+ // Why: lazy auto-registration (on first Arrow decode) imports apache-arrow at most
115
+ // once per process. We cache the in-flight/settled attempt so concurrent decodes
116
+ // share a single dynamic import, and a missing module is not re-probed on every call.
117
+ let lazyRegistration: Promise<boolean> | undefined;
118
+
119
+ // Why: the lazy decode path imports apache-arrow through the default Node loader, which
120
+ // is hard to simulate-as-absent in a test env where the dependency IS installed. This
121
+ // internal seam lets the unit suite exercise the "apache-arrow missing" clear-failure
122
+ // branch deterministically. It is intentionally NOT re-exported from the package root
123
+ // (see test/api_surface.test.ts) and is reset by clearArrowDecoder().
124
+ let lazyArrowLoaderOverride: ArrowModuleLoader | undefined;
125
+
126
+ /** @internal Test-only: override the loader used by lazy auto-registration. */
127
+ export const _setLazyArrowLoaderForTesting = (loader: ArrowModuleLoader | undefined): void => {
128
+ lazyArrowLoaderOverride = loader;
129
+ lazyRegistration = undefined;
130
+ };
131
+
114
132
  export function registerArrowDecoder(
115
133
  decoder: (bytes: Uint8Array) => ArrowTable | Uint8Array
116
134
  ): void {
@@ -119,6 +137,10 @@ export function registerArrowDecoder(
119
137
 
120
138
  export function clearArrowDecoder(): void {
121
139
  arrowTableFrom = undefined;
140
+ // Why: reset the cached import attempt so tests (and reload helpers) can exercise
141
+ // the auto-registration path again from a clean slate.
142
+ lazyRegistration = undefined;
143
+ lazyArrowLoaderOverride = undefined;
122
144
  }
123
145
 
124
146
  /**
@@ -186,6 +208,12 @@ export async function autoRegisterArrowDecoder(
186
208
  }
187
209
  try {
188
210
  const arrowModule = await loader();
211
+ // Another path may have registered a decoder while the import was in flight
212
+ // (e.g. an explicit registerArrowDecoder() during concurrent startup/reload).
213
+ // Don't clobber it — the explicit registration wins.
214
+ if (hasArrowDecoder()) {
215
+ return true;
216
+ }
189
217
  registerArrowDecoderFromModule(arrowModule as { tableFromIPC?: unknown });
190
218
  return true;
191
219
  } catch {
@@ -214,17 +242,46 @@ function fromBase64(b64: string): Uint8Array {
214
242
  throw new Error('Base64 decoding is not available in this runtime');
215
243
  }
216
244
 
245
+ // Why: a single, actionable message for both decode paths so users always know the two
246
+ // supported remedies — install the optional dependency, or opt into the lossy JSON fallback.
247
+ const ARROW_MISSING_MESSAGE =
248
+ 'Received an Arrow-encoded payload but no Arrow decoder is available. ' +
249
+ 'Install the optional dependency with `npm install apache-arrow`, or set ' +
250
+ 'TYWRAP_CODEC_FALLBACK=json on the Python side to receive JSON instead ' +
251
+ '(lossy for dtype/NA fidelity). tywrap never silently downgrades Arrow payloads.';
252
+
217
253
  function requireArrowDecoder(): (bytes: Uint8Array) => ArrowTable | Uint8Array {
218
254
  if (!arrowTableFrom) {
219
- throw new Error(
220
- 'Arrow decoder not registered. Call registerArrowDecoder(...) or set TYWRAP_CODEC_FALLBACK=json in Python.'
221
- );
255
+ throw new Error(ARROW_MISSING_MESSAGE);
256
+ }
257
+ return arrowTableFrom;
258
+ }
259
+
260
+ /**
261
+ * Ensure an Arrow decoder is registered, lazily importing apache-arrow on first use.
262
+ *
263
+ * Why: keep apache-arrow optional and zero-config. The first Arrow-encoded payload
264
+ * triggers a single best-effort dynamic import; if it succeeds the decoder is cached
265
+ * for the rest of the process. If apache-arrow is absent we throw a clear, actionable
266
+ * error rather than silently producing wrong data.
267
+ */
268
+ async function ensureArrowDecoder(): Promise<(bytes: Uint8Array) => ArrowTable | Uint8Array> {
269
+ if (arrowTableFrom) {
270
+ return arrowTableFrom;
271
+ }
272
+ // Reuse a single import attempt across concurrent decodes.
273
+ lazyRegistration ??= autoRegisterArrowDecoder(
274
+ lazyArrowLoaderOverride ? { loader: lazyArrowLoaderOverride } : {}
275
+ );
276
+ await lazyRegistration;
277
+ if (!arrowTableFrom) {
278
+ throw new Error(ARROW_MISSING_MESSAGE);
222
279
  }
223
280
  return arrowTableFrom;
224
281
  }
225
282
 
226
283
  async function tryDecodeArrowTable(bytes: Uint8Array): Promise<ArrowTable | Uint8Array> {
227
- const decoder = requireArrowDecoder();
284
+ const decoder = await ensureArrowDecoder();
228
285
  try {
229
286
  return decoder(bytes);
230
287
  } catch (err) {
@@ -451,6 +508,31 @@ const decodeNdarrayEnvelope: EnvelopeHandler = (value, decodeArrow) => {
451
508
  throw new Error(`Invalid ndarray envelope: unsupported encoding ${String(encoding)}`);
452
509
  };
453
510
 
511
+ /**
512
+ * Assert an array holds only integer indices within [0, bound). Used to re-validate
513
+ * scipy.sparse index arrays on the JS side so a corrupt/oversized index can never
514
+ * silently address out of the declared shape.
515
+ *
516
+ * Why: the JS decoder never reconstructs a Python object, but it IS the boundary a
517
+ * downstream consumer trusts — validating index ranges here turns a corrupt payload
518
+ * into a clear, early failure instead of a confusing downstream error.
519
+ */
520
+ function assertIndexArrayInRange(arr: readonly unknown[], bound: number, label: string): void {
521
+ for (let i = 0; i < arr.length; i += 1) {
522
+ const idx = arr[i];
523
+ if (typeof idx !== 'number' || !Number.isInteger(idx)) {
524
+ throw new Error(
525
+ `Invalid scipy.sparse envelope: ${label}[${i}] must be an integer, got ${String(idx)}`
526
+ );
527
+ }
528
+ if (idx < 0 || idx >= bound) {
529
+ throw new Error(
530
+ `Invalid scipy.sparse envelope: ${label}[${i}]=${idx} is out of range [0, ${bound})`
531
+ );
532
+ }
533
+ }
534
+ }
535
+
454
536
  const decodeScipySparseEnvelope: EnvelopeHandler = value => {
455
537
  const encoding = value.encoding;
456
538
  if (encoding !== 'json') {
@@ -465,10 +547,16 @@ const decodeScipySparseEnvelope: EnvelopeHandler = value => {
465
547
  !Array.isArray(shape) ||
466
548
  shape.length !== 2 ||
467
549
  typeof shape[0] !== 'number' ||
468
- typeof shape[1] !== 'number'
550
+ !Number.isInteger(shape[0]) ||
551
+ shape[0] < 0 ||
552
+ typeof shape[1] !== 'number' ||
553
+ !Number.isInteger(shape[1]) ||
554
+ shape[1] < 0
469
555
  ) {
470
- throw new Error('Invalid scipy.sparse envelope: shape must be a 2-item number[]');
556
+ throw new Error('Invalid scipy.sparse envelope: shape must be a 2-item non-negative integer[]');
471
557
  }
558
+ const rows = shape[0];
559
+ const cols = shape[1];
472
560
  const data = value.data;
473
561
  if (!Array.isArray(data)) {
474
562
  throw new Error('Invalid scipy.sparse envelope: data must be an array');
@@ -482,6 +570,16 @@ const decodeScipySparseEnvelope: EnvelopeHandler = value => {
482
570
  if (!Array.isArray(row) || !Array.isArray(col)) {
483
571
  throw new Error('Invalid scipy.sparse envelope: coo requires row and col arrays');
484
572
  }
573
+ // COO: one (row, col, value) triple per stored entry, so all three arrays
574
+ // share a length; row/col index into [0, rows)/[0, cols) respectively.
575
+ if (row.length !== data.length || col.length !== data.length) {
576
+ throw new Error(
577
+ `Invalid scipy.sparse envelope: coo row/col/data lengths must match ` +
578
+ `(data=${data.length}, row=${row.length}, col=${col.length})`
579
+ );
580
+ }
581
+ assertIndexArrayInRange(row, rows, 'row');
582
+ assertIndexArrayInRange(col, cols, 'col');
485
583
  return {
486
584
  format,
487
585
  shape,
@@ -497,6 +595,52 @@ const decodeScipySparseEnvelope: EnvelopeHandler = value => {
497
595
  if (!Array.isArray(indices) || !Array.isArray(indptr)) {
498
596
  throw new Error('Invalid scipy.sparse envelope: csr/csc requires indices and indptr arrays');
499
597
  }
598
+ // CSR/CSC: one column-index (CSR) or row-index (CSC) per stored value, so
599
+ // indices and data share a length. indptr partitions indices into one segment
600
+ // per major axis (rows for CSR, cols for CSC), so indptr has majorAxis + 1
601
+ // entries; the inner indices address the minor axis.
602
+ if (indices.length !== data.length) {
603
+ throw new Error(
604
+ `Invalid scipy.sparse envelope: ${format} indices/data lengths must match ` +
605
+ `(data=${data.length}, indices=${indices.length})`
606
+ );
607
+ }
608
+ const majorAxis = format === 'csr' ? rows : cols;
609
+ const minorAxis = format === 'csr' ? cols : rows;
610
+ if (indptr.length !== majorAxis + 1) {
611
+ throw new Error(
612
+ `Invalid scipy.sparse envelope: ${format} indptr length must be ${majorAxis + 1} ` +
613
+ `(${format === 'csr' ? 'rows' : 'cols'}+1), got ${indptr.length}`
614
+ );
615
+ }
616
+ // indptr must be a valid CSR/CSC pointer array: integers that start at 0, end
617
+ // at data.length, and never decrease — otherwise a structurally impossible
618
+ // matrix (e.g. [0, 99, 1]) would slip through the length check above.
619
+ for (let i = 0; i < indptr.length; i += 1) {
620
+ const ptr = indptr[i];
621
+ if (typeof ptr !== 'number' || !Number.isInteger(ptr)) {
622
+ throw new Error(
623
+ `Invalid scipy.sparse envelope: indptr[${i}] must be an integer, got ${String(ptr)}`
624
+ );
625
+ }
626
+ if (ptr < 0 || ptr > data.length) {
627
+ throw new Error(
628
+ `Invalid scipy.sparse envelope: indptr[${i}]=${ptr} is out of range [0, ${data.length}]`
629
+ );
630
+ }
631
+ if (i > 0 && ptr < (indptr[i - 1] as number)) {
632
+ throw new Error('Invalid scipy.sparse envelope: indptr must be non-decreasing');
633
+ }
634
+ }
635
+ if (indptr[0] !== 0) {
636
+ throw new Error('Invalid scipy.sparse envelope: indptr must start at 0');
637
+ }
638
+ if (indptr[indptr.length - 1] !== data.length) {
639
+ throw new Error(
640
+ `Invalid scipy.sparse envelope: indptr must end at data.length (${data.length})`
641
+ );
642
+ }
643
+ assertIndexArrayInRange(indices, minorAxis, 'indices');
500
644
  return {
501
645
  format,
502
646
  shape,
@@ -507,6 +651,11 @@ const decodeScipySparseEnvelope: EnvelopeHandler = value => {
507
651
  } satisfies SparseMatrix;
508
652
  };
509
653
 
654
+ /** Product of a shape's dimensions (the element count). [] (scalar) -> 1. */
655
+ function shapeProduct(shape: readonly number[]): number {
656
+ return shape.reduce((acc, dim) => acc * dim, 1);
657
+ }
658
+
510
659
  const decodeTorchTensorEnvelope: EnvelopeHandler = <T>(
511
660
  value: { [k: string]: unknown },
512
661
  _decodeArrow: (bytes: Uint8Array) => MaybePromise<T>,
@@ -523,20 +672,96 @@ const decodeTorchTensorEnvelope: EnvelopeHandler = <T>(
523
672
  if (!isObject(nested) || (nested as { __tywrap__?: unknown }).__tywrap__ !== 'ndarray') {
524
673
  throw new Error('Invalid torch.tensor envelope: value must be an ndarray envelope');
525
674
  }
526
- const decoded = recurse(nested);
527
675
  const shapeValue = value.shape;
528
676
  const shape = isNumberArray(shapeValue) ? shapeValue : undefined;
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.
692
+ const nestedShapeValue = (nested as { shape?: unknown }).shape;
693
+ const nestedShape = isNumberArray(nestedShapeValue) ? nestedShapeValue : undefined;
694
+ if (shape && nestedShape && shapeProduct(shape) !== shapeProduct(nestedShape)) {
695
+ throw new Error(
696
+ `Invalid torch.tensor envelope: shape ${JSON.stringify(shape)} ` +
697
+ `(product ${shapeProduct(shape)}) disagrees with nested ndarray shape ` +
698
+ `${JSON.stringify(nestedShape)} (product ${shapeProduct(nestedShape)})`
699
+ );
700
+ }
529
701
  const dtypeValue = value.dtype;
530
702
  const dtype = typeof dtypeValue === 'string' ? dtypeValue : undefined;
531
703
  const deviceValue = value.device;
704
+ if (deviceValue !== undefined && (typeof deviceValue !== 'string' || deviceValue.length === 0)) {
705
+ throw new Error(
706
+ 'Invalid torch.tensor envelope: device must be a non-empty string when provided'
707
+ );
708
+ }
532
709
  const device = typeof deviceValue === 'string' ? deviceValue : undefined;
533
710
 
711
+ const decoded = recurse(nested);
534
712
  if (isPromiseLike(decoded)) {
535
713
  return decoded.then(data => ({ data, shape, dtype, device })) as Promise<T | unknown>;
536
714
  }
537
715
  return { data: decoded, shape, dtype, device } satisfies TorchTensor;
538
716
  };
539
717
 
718
+ /**
719
+ * Recursively assert a value is plain JSON (null | boolean | number | string |
720
+ * JSON array | plain object of JSON). Rejects functions, symbols, bigints, class
721
+ * instances, and any non-finite number — the things a metadata-only sklearn
722
+ * envelope must never carry. This validates; it never reconstructs.
723
+ */
724
+ function assertPlainJson(value: unknown, path: string): void {
725
+ if (value === null) {
726
+ return;
727
+ }
728
+ const t = typeof value;
729
+ if (t === 'string' || t === 'boolean') {
730
+ return;
731
+ }
732
+ if (t === 'number') {
733
+ if (!Number.isFinite(value as number)) {
734
+ throw new Error(
735
+ `Invalid sklearn.estimator envelope: ${path} must be a finite JSON number, got ${String(value)}`
736
+ );
737
+ }
738
+ return;
739
+ }
740
+ if (Array.isArray(value)) {
741
+ value.forEach((item, i) => assertPlainJson(item, `${path}[${i}]`));
742
+ return;
743
+ }
744
+ if (t === 'object') {
745
+ // Reject exotic objects (class instances, Map/Set, etc.): a JSON object is a
746
+ // plain object whose prototype is Object.prototype or null.
747
+ const proto = Object.getPrototypeOf(value as object);
748
+ if (proto !== Object.prototype && proto !== null) {
749
+ throw new Error(
750
+ `Invalid sklearn.estimator envelope: ${path} must be a plain JSON object, ` +
751
+ `got ${(value as object).constructor?.name ?? 'object'}`
752
+ );
753
+ }
754
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
755
+ assertPlainJson(v, `${path}.${k}`);
756
+ }
757
+ return;
758
+ }
759
+ // function | symbol | bigint | undefined
760
+ throw new Error(
761
+ `Invalid sklearn.estimator envelope: ${path} is not JSON-serializable (type ${t})`
762
+ );
763
+ }
764
+
540
765
  const decodeSklearnEstimatorEnvelope: EnvelopeHandler = value => {
541
766
  const encoding = value.encoding;
542
767
  if (encoding !== 'json') {
@@ -550,6 +775,19 @@ const decodeSklearnEstimatorEnvelope: EnvelopeHandler = value => {
550
775
  'Invalid sklearn.estimator envelope: expected className/module strings + params object'
551
776
  );
552
777
  }
778
+ // params must be a PLAIN JSON object end to end — metadata-only estimators never
779
+ // carry callables, class instances, or nested non-JSON values. Validate (do not
780
+ // reconstruct) so a corrupt envelope fails clearly instead of leaking a function
781
+ // or exotic object to a downstream consumer.
782
+ if (
783
+ Object.getPrototypeOf(params) !== Object.prototype &&
784
+ Object.getPrototypeOf(params) !== null
785
+ ) {
786
+ throw new Error('Invalid sklearn.estimator envelope: params must be a plain JSON object');
787
+ }
788
+ for (const [k, v] of Object.entries(params)) {
789
+ assertPlainJson(v, `params.${k}`);
790
+ }
553
791
  const versionValue = value.version;
554
792
  if (versionValue !== undefined && typeof versionValue !== 'string') {
555
793
  throw new Error('Invalid sklearn.estimator envelope: version must be a string when provided');
package/src/version.ts CHANGED
@@ -9,4 +9,4 @@
9
9
  * Regenerate with: node scripts/generate-version.mjs
10
10
  */
11
11
 
12
- export const VERSION: string = "0.6.1";
12
+ export const VERSION: string = "0.8.0";