nodepyx 1.0.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 (184) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +399 -0
  3. package/binding.gyp +73 -0
  4. package/dist/core/PyCallable.d.ts +65 -0
  5. package/dist/core/PyCallable.d.ts.map +1 -0
  6. package/dist/core/PyCallable.js +109 -0
  7. package/dist/core/PyCallable.js.map +1 -0
  8. package/dist/core/PyContext.d.ts +76 -0
  9. package/dist/core/PyContext.d.ts.map +1 -0
  10. package/dist/core/PyContext.js +228 -0
  11. package/dist/core/PyContext.js.map +1 -0
  12. package/dist/core/PyIterator.d.ts +84 -0
  13. package/dist/core/PyIterator.d.ts.map +1 -0
  14. package/dist/core/PyIterator.js +243 -0
  15. package/dist/core/PyIterator.js.map +1 -0
  16. package/dist/core/PyModule.d.ts +55 -0
  17. package/dist/core/PyModule.d.ts.map +1 -0
  18. package/dist/core/PyModule.js +172 -0
  19. package/dist/core/PyModule.js.map +1 -0
  20. package/dist/core/PyProxy.d.ts +65 -0
  21. package/dist/core/PyProxy.d.ts.map +1 -0
  22. package/dist/core/PyProxy.js +483 -0
  23. package/dist/core/PyProxy.js.map +1 -0
  24. package/dist/core/PyRuntime.d.ts +105 -0
  25. package/dist/core/PyRuntime.d.ts.map +1 -0
  26. package/dist/core/PyRuntime.js +438 -0
  27. package/dist/core/PyRuntime.js.map +1 -0
  28. package/dist/env/CondaManager.d.ts +118 -0
  29. package/dist/env/CondaManager.d.ts.map +1 -0
  30. package/dist/env/CondaManager.js +401 -0
  31. package/dist/env/CondaManager.js.map +1 -0
  32. package/dist/env/PackageInstaller.d.ts +233 -0
  33. package/dist/env/PackageInstaller.d.ts.map +1 -0
  34. package/dist/env/PackageInstaller.js +609 -0
  35. package/dist/env/PackageInstaller.js.map +1 -0
  36. package/dist/env/PythonDetector.d.ts +103 -0
  37. package/dist/env/PythonDetector.d.ts.map +1 -0
  38. package/dist/env/PythonDetector.js +381 -0
  39. package/dist/env/PythonDetector.js.map +1 -0
  40. package/dist/env/VenvManager.d.ts +117 -0
  41. package/dist/env/VenvManager.d.ts.map +1 -0
  42. package/dist/env/VenvManager.js +331 -0
  43. package/dist/env/VenvManager.js.map +1 -0
  44. package/dist/index.d.ts +169 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +393 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/plugins/Plugin.interface.d.ts +41 -0
  49. package/dist/plugins/Plugin.interface.d.ts.map +1 -0
  50. package/dist/plugins/Plugin.interface.js +12 -0
  51. package/dist/plugins/Plugin.interface.js.map +1 -0
  52. package/dist/plugins/PluginManager.d.ts +26 -0
  53. package/dist/plugins/PluginManager.d.ts.map +1 -0
  54. package/dist/plugins/PluginManager.js +174 -0
  55. package/dist/plugins/PluginManager.js.map +1 -0
  56. package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
  57. package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
  58. package/dist/plugins/builtin/NumpyPlugin.js +41 -0
  59. package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
  60. package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
  61. package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
  62. package/dist/plugins/builtin/PandasPlugin.js +57 -0
  63. package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
  64. package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
  65. package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
  66. package/dist/plugins/builtin/TorchPlugin.js +50 -0
  67. package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
  68. package/dist/plugins/index.d.ts +7 -0
  69. package/dist/plugins/index.d.ts.map +1 -0
  70. package/dist/plugins/index.js +12 -0
  71. package/dist/plugins/index.js.map +1 -0
  72. package/dist/serialization/DataFrameBridge.d.ts +141 -0
  73. package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
  74. package/dist/serialization/DataFrameBridge.js +355 -0
  75. package/dist/serialization/DataFrameBridge.js.map +1 -0
  76. package/dist/serialization/MsgPackSerializer.d.ts +45 -0
  77. package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
  78. package/dist/serialization/MsgPackSerializer.js +242 -0
  79. package/dist/serialization/MsgPackSerializer.js.map +1 -0
  80. package/dist/serialization/NumpyBridge.d.ts +96 -0
  81. package/dist/serialization/NumpyBridge.d.ts.map +1 -0
  82. package/dist/serialization/NumpyBridge.js +323 -0
  83. package/dist/serialization/NumpyBridge.js.map +1 -0
  84. package/dist/serialization/Serializer.d.ts +78 -0
  85. package/dist/serialization/Serializer.d.ts.map +1 -0
  86. package/dist/serialization/Serializer.js +281 -0
  87. package/dist/serialization/Serializer.js.map +1 -0
  88. package/dist/types/PythonTypeMapper.d.ts +87 -0
  89. package/dist/types/PythonTypeMapper.d.ts.map +1 -0
  90. package/dist/types/PythonTypeMapper.js +449 -0
  91. package/dist/types/PythonTypeMapper.js.map +1 -0
  92. package/dist/types/StubCache.d.ts +109 -0
  93. package/dist/types/StubCache.d.ts.map +1 -0
  94. package/dist/types/StubCache.js +333 -0
  95. package/dist/types/StubCache.js.map +1 -0
  96. package/dist/types/TypeGenerator.d.ts +139 -0
  97. package/dist/types/TypeGenerator.d.ts.map +1 -0
  98. package/dist/types/TypeGenerator.js +372 -0
  99. package/dist/types/TypeGenerator.js.map +1 -0
  100. package/dist/types/addon.d.ts +114 -0
  101. package/dist/types/addon.d.ts.map +1 -0
  102. package/dist/types/addon.js +32 -0
  103. package/dist/types/addon.js.map +1 -0
  104. package/dist/types/config.d.ts +175 -0
  105. package/dist/types/config.d.ts.map +1 -0
  106. package/dist/types/config.js +35 -0
  107. package/dist/types/config.js.map +1 -0
  108. package/dist/types/index.d.ts +10 -0
  109. package/dist/types/index.d.ts.map +1 -0
  110. package/dist/types/index.js +12 -0
  111. package/dist/types/index.js.map +1 -0
  112. package/dist/types/python.d.ts +235 -0
  113. package/dist/types/python.d.ts.map +1 -0
  114. package/dist/types/python.js +7 -0
  115. package/dist/types/python.js.map +1 -0
  116. package/dist/utils/ErrorTranslator.d.ts +83 -0
  117. package/dist/utils/ErrorTranslator.d.ts.map +1 -0
  118. package/dist/utils/ErrorTranslator.js +210 -0
  119. package/dist/utils/ErrorTranslator.js.map +1 -0
  120. package/dist/utils/Logger.d.ts +27 -0
  121. package/dist/utils/Logger.d.ts.map +1 -0
  122. package/dist/utils/Logger.js +115 -0
  123. package/dist/utils/Logger.js.map +1 -0
  124. package/dist/utils/MemoryMonitor.d.ts +44 -0
  125. package/dist/utils/MemoryMonitor.d.ts.map +1 -0
  126. package/dist/utils/MemoryMonitor.js +143 -0
  127. package/dist/utils/MemoryMonitor.js.map +1 -0
  128. package/package.json +177 -0
  129. package/python/error_handler.py +433 -0
  130. package/python/nodepyx_runtime.py +575 -0
  131. package/python/serializer.py +379 -0
  132. package/python/type_inspector.py +288 -0
  133. package/scripts/build-native.js +68 -0
  134. package/scripts/download-prebuilds.js +99 -0
  135. package/scripts/generate-stubs.js +405 -0
  136. package/scripts/install.js +260 -0
  137. package/src/core/PyCallable.ts +137 -0
  138. package/src/core/PyContext.ts +296 -0
  139. package/src/core/PyIterator.ts +294 -0
  140. package/src/core/PyModule.ts +194 -0
  141. package/src/core/PyProxy.ts +605 -0
  142. package/src/core/PyRuntime.ts +504 -0
  143. package/src/env/CondaManager.ts +451 -0
  144. package/src/env/PackageInstaller.ts +738 -0
  145. package/src/env/PythonDetector.ts +414 -0
  146. package/src/env/VenvManager.ts +396 -0
  147. package/src/index.ts +425 -0
  148. package/src/native/gil_guard.cpp +26 -0
  149. package/src/native/gil_guard.h +175 -0
  150. package/src/native/nodepyx_addon.cpp +886 -0
  151. package/src/native/python_bridge.cpp +790 -0
  152. package/src/native/python_bridge.h +257 -0
  153. package/src/native/thread_pool.cpp +336 -0
  154. package/src/native/thread_pool.h +175 -0
  155. package/src/native/type_converter.cpp +901 -0
  156. package/src/native/type_converter.h +272 -0
  157. package/src/nextjs/PyProvider.tsx +123 -0
  158. package/src/nextjs/index.ts +21 -0
  159. package/src/nextjs/usePython.ts +106 -0
  160. package/src/nextjs/withnodepyx.ts +88 -0
  161. package/src/plugins/Plugin.interface.ts +51 -0
  162. package/src/plugins/PluginManager.ts +155 -0
  163. package/src/plugins/builtin/NumpyPlugin.ts +36 -0
  164. package/src/plugins/builtin/PandasPlugin.ts +49 -0
  165. package/src/plugins/builtin/TorchPlugin.ts +56 -0
  166. package/src/plugins/index.ts +7 -0
  167. package/src/serialization/DataFrameBridge.ts +398 -0
  168. package/src/serialization/MsgPackSerializer.ts +220 -0
  169. package/src/serialization/NumpyBridge.ts +332 -0
  170. package/src/serialization/Serializer.ts +320 -0
  171. package/src/types/PythonTypeMapper.ts +495 -0
  172. package/src/types/StubCache.ts +340 -0
  173. package/src/types/TypeGenerator.ts +491 -0
  174. package/src/types/addon.ts +170 -0
  175. package/src/types/config.ts +226 -0
  176. package/src/types/index.ts +55 -0
  177. package/src/types/python.ts +309 -0
  178. package/src/types/stubs/numpy.d.ts +441 -0
  179. package/src/types/stubs/pandas.d.ts +575 -0
  180. package/src/types/stubs/sklearn.d.ts +728 -0
  181. package/src/types/stubs/torch.d.ts +694 -0
  182. package/src/utils/ErrorTranslator.ts +220 -0
  183. package/src/utils/Logger.ts +119 -0
  184. package/src/utils/MemoryMonitor.ts +175 -0
@@ -0,0 +1,332 @@
1
+ /**
2
+ * nodepyx — NumpyBridge
3
+ * Converts NumPy arrays to/from JavaScript TypedArrays.
4
+ *
5
+ * Wire protocol (binary layout for NUMPY_ARRAY format):
6
+ * Bytes 0-3 : uint32 LE — ndim (number of dimensions)
7
+ * Bytes 4-7 : uint32 LE — dtype code (see NumpyDTypeCode)
8
+ * Bytes 8-11 : uint32 LE — itemsize in bytes
9
+ * Bytes 12-15 : uint32 LE — total element count
10
+ * Bytes 16.. : ndim × uint32 LE — shape dimensions
11
+ * After shape : raw element bytes (C-contiguous order)
12
+ *
13
+ * When SharedArrayBuffer is enabled the element bytes are placed in a
14
+ * SharedArrayBuffer so the data can be read from Node.js worker threads
15
+ * without copying.
16
+ */
17
+
18
+ import type { SerializedValue, SerializedFormat, NumPyArrayResult, NumpyTypedArray } from '../types/python';
19
+ import { Logger } from '../utils/Logger';
20
+
21
+ const logger = new Logger('NumpyBridge');
22
+
23
+ // ─── NumPy dtype codes (must match Python side nodepyx_runtime.py) ───────────
24
+
25
+ export enum NumpyDTypeCode {
26
+ BOOL = 0,
27
+ INT8 = 1,
28
+ INT16 = 2,
29
+ INT32 = 3,
30
+ INT64 = 4,
31
+ UINT8 = 5,
32
+ UINT16 = 6,
33
+ UINT32 = 7,
34
+ UINT64 = 8,
35
+ FLOAT32 = 9,
36
+ FLOAT64 = 10,
37
+ COMPLEX64 = 11,
38
+ COMPLEX128 = 12,
39
+ }
40
+
41
+ export type NumpyDType =
42
+ | 'bool'
43
+ | 'int8' | 'int16' | 'int32' | 'int64'
44
+ | 'uint8' | 'uint16' | 'uint32' | 'uint64'
45
+ | 'float32' | 'float64'
46
+ | 'complex64' | 'complex128';
47
+
48
+ interface NumpyBridgeOptions {
49
+ /**
50
+ * Use SharedArrayBuffer for large arrays.
51
+ * Requires `Cross-Origin-Opener-Policy: same-origin` and
52
+ * `Cross-Origin-Embedder-Policy: require-corp` response headers.
53
+ */
54
+ useSharedArrayBuffer?: boolean;
55
+ /** Threshold in bytes above which SharedArrayBuffer is used. Default: 1 MB */
56
+ sharedArrayBufferThreshold?: number;
57
+ }
58
+
59
+ /** Header size in bytes (see layout above) */
60
+ const HEADER_BYTES = 16;
61
+
62
+ // ─── dtype string → NumpyDTypeCode ──────────────────────────────────────────
63
+
64
+ const DTYPE_STRING_TO_CODE: Readonly<Record<NumpyDType, NumpyDTypeCode>> = {
65
+ bool: NumpyDTypeCode.BOOL,
66
+ int8: NumpyDTypeCode.INT8,
67
+ int16: NumpyDTypeCode.INT16,
68
+ int32: NumpyDTypeCode.INT32,
69
+ int64: NumpyDTypeCode.INT64,
70
+ uint8: NumpyDTypeCode.UINT8,
71
+ uint16: NumpyDTypeCode.UINT16,
72
+ uint32: NumpyDTypeCode.UINT32,
73
+ uint64: NumpyDTypeCode.UINT64,
74
+ float32: NumpyDTypeCode.FLOAT32,
75
+ float64: NumpyDTypeCode.FLOAT64,
76
+ complex64: NumpyDTypeCode.COMPLEX64,
77
+ complex128: NumpyDTypeCode.COMPLEX128,
78
+ } as const;
79
+
80
+ // ─── NumpyDTypeCode → item size in bytes ────────────────────────────────────
81
+
82
+ const DTYPE_CODE_TO_ITEMSIZE: Readonly<Record<NumpyDTypeCode, number>> = {
83
+ [NumpyDTypeCode.BOOL]: 1,
84
+ [NumpyDTypeCode.INT8]: 1,
85
+ [NumpyDTypeCode.INT16]: 2,
86
+ [NumpyDTypeCode.INT32]: 4,
87
+ [NumpyDTypeCode.INT64]: 8,
88
+ [NumpyDTypeCode.UINT8]: 1,
89
+ [NumpyDTypeCode.UINT16]: 2,
90
+ [NumpyDTypeCode.UINT32]: 4,
91
+ [NumpyDTypeCode.UINT64]: 8,
92
+ [NumpyDTypeCode.FLOAT32]: 4,
93
+ [NumpyDTypeCode.FLOAT64]: 8,
94
+ [NumpyDTypeCode.COMPLEX64]: 8,
95
+ [NumpyDTypeCode.COMPLEX128]: 16,
96
+ } as const;
97
+
98
+ /**
99
+ * NumpyBridge — converts NumPy arrays ↔ TypedArrays.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const bridge = new NumpyBridge();
104
+ *
105
+ * // Serialize a Float32Array → NumPy wire format
106
+ * const sv = bridge.serializeTypedArray(new Float32Array([1, 2, 3]), 'float32');
107
+ *
108
+ * // Deserialize NumPy wire format → TypedArray
109
+ * const arr = bridge.deserialize(sv) as Float32Array;
110
+ * ```
111
+ */
112
+ export class NumpyBridge {
113
+ private readonly _useShared: boolean;
114
+ private readonly _sharedThreshold: number;
115
+
116
+ constructor(options: NumpyBridgeOptions = {}) {
117
+ this._useShared = options.useSharedArrayBuffer ?? false;
118
+ this._sharedThreshold = options.sharedArrayBufferThreshold ?? 1_048_576; // 1 MB
119
+ }
120
+
121
+ // ─── Python wire format → TypedArray ─────────────────────────────────────
122
+
123
+ /**
124
+ * Deserialize a NUMPY_ARRAY SerializedValue to a TypedArray.
125
+ */
126
+ deserialize(sv: SerializedValue): NumPyArrayResult | null {
127
+ if (sv.format !== 'numpy_array') { return null; }
128
+ const rawData = sv.data;
129
+ if (!rawData) {
130
+ logger.warn('NumpyBridge.deserialize: invalid data');
131
+ return null;
132
+ }
133
+
134
+ try {
135
+ let u8: Uint8Array;
136
+ if (typeof rawData === 'string') {
137
+ // base64-encoded binary
138
+ const buf = Buffer.from(rawData, 'base64');
139
+ u8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
140
+ } else if (rawData instanceof Uint8Array) {
141
+ u8 = rawData;
142
+ } else {
143
+ logger.warn('NumpyBridge.deserialize: unsupported data type');
144
+ return null;
145
+ }
146
+ if (u8.length < HEADER_BYTES) {
147
+ logger.warn('NumpyBridge.deserialize: buffer too small');
148
+ return null;
149
+ }
150
+ return this._decodeBuffer(u8);
151
+ } catch (err) {
152
+ logger.error('NumpyBridge.deserialize failed', err);
153
+ return null;
154
+ }
155
+ }
156
+
157
+ // ─── TypedArray → Python wire format ─────────────────────────────────────
158
+
159
+ /**
160
+ * Serialize a TypedArray to a NUMPY_ARRAY SerializedValue.
161
+ *
162
+ * @param arr — the typed array
163
+ * @param dtype — NumPy dtype string
164
+ * @param shape — optional explicit shape; defaults to [arr.length]
165
+ */
166
+ serializeTypedArray(
167
+ arr: ArrayBufferView,
168
+ dtype: NumpyDType | { dtype: string; itemsize: number },
169
+ shape?: number[],
170
+ ): SerializedValue {
171
+ const dtypeStr: NumpyDType = (typeof dtype === 'string' ? dtype : dtype.dtype) as NumpyDType;
172
+ const dtypeCode = DTYPE_STRING_TO_CODE[dtypeStr] ?? NumpyDTypeCode.FLOAT64;
173
+ const itemSize = DTYPE_CODE_TO_ITEMSIZE[dtypeCode];
174
+ const dims = shape ?? [arr.byteLength / itemSize];
175
+ const ndim = dims.length;
176
+ const count = dims.reduce((a, b) => a * b, 1);
177
+ const dataBytes = count * itemSize;
178
+
179
+ const totalBytes = HEADER_BYTES + ndim * 4 + dataBytes;
180
+ const buffer = new ArrayBuffer(totalBytes);
181
+ const dv = new DataView(buffer);
182
+ const u8 = new Uint8Array(buffer);
183
+
184
+ // Write header
185
+ dv.setUint32(0, ndim, true);
186
+ dv.setUint32(4, dtypeCode, true);
187
+ dv.setUint32(8, itemSize, true);
188
+ dv.setUint32(12, count, true);
189
+
190
+ // Write shape
191
+ for (let i = 0; i < ndim; i++) {
192
+ dv.setUint32(HEADER_BYTES + i * 4, dims[i]!, true);
193
+ }
194
+
195
+ // Write data
196
+ const srcU8 = new Uint8Array(
197
+ (arr.buffer as ArrayBuffer),
198
+ arr.byteOffset,
199
+ arr.byteLength,
200
+ );
201
+ u8.set(srcU8, HEADER_BYTES + ndim * 4);
202
+
203
+ return {
204
+ format: 'numpy_array' as SerializedFormat,
205
+ data: u8,
206
+ metadata: {
207
+ dtype: dtypeStr,
208
+ shape: dims,
209
+ itemSize,
210
+ size: dataBytes,
211
+ length: count,
212
+ },
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Create an empty NumPy array SerializedValue (used for zero-size results).
218
+ */
219
+ serializeEmpty(dtype: NumpyDType = 'float64'): SerializedValue {
220
+ return this.serializeTypedArray(new Float64Array(0), dtype, [0]);
221
+ }
222
+
223
+ // ─── Helpers ──────────────────────────────────────────────────────────────
224
+
225
+ private _decodeBuffer(u8: Uint8Array): NumPyArrayResult {
226
+ const dv = new DataView((u8.buffer as ArrayBuffer), u8.byteOffset, u8.byteLength);
227
+ const ndim = dv.getUint32(0, true);
228
+ const dtypeCode = dv.getUint32(4, true) as NumpyDTypeCode;
229
+ const itemSize = dv.getUint32(8, true);
230
+ const count = dv.getUint32(12, true);
231
+
232
+ // Read shape
233
+ const shape: number[] = [];
234
+ for (let i = 0; i < ndim; i++) {
235
+ shape.push(dv.getUint32(HEADER_BYTES + i * 4, true));
236
+ }
237
+
238
+ const dataOffset = HEADER_BYTES + ndim * 4;
239
+ const dataLength = count * itemSize;
240
+
241
+ if (u8.length < dataOffset + dataLength) {
242
+ throw new Error(
243
+ `NumpyBridge: buffer too small. ` +
244
+ `Expected ${dataOffset + dataLength} bytes, got ${u8.length}`,
245
+ );
246
+ }
247
+
248
+ const srcBuffer = (u8.buffer as ArrayBuffer);
249
+ const srcByteOffset = u8.byteOffset + dataOffset;
250
+
251
+ return this._buildTypedArray(dtypeCode, srcBuffer, srcByteOffset, count, shape, ndim);
252
+ }
253
+
254
+ private _buildTypedArray(
255
+ code: NumpyDTypeCode,
256
+ srcBuffer: ArrayBuffer,
257
+ byteOffset: number,
258
+ count: number,
259
+ shape: number[] = [],
260
+ ndim: number = 1,
261
+ ): NumPyArrayResult {
262
+ const useShared =
263
+ this._useShared &&
264
+ typeof SharedArrayBuffer !== 'undefined' &&
265
+ count * DTYPE_CODE_TO_ITEMSIZE[code] >= this._sharedThreshold;
266
+
267
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
268
+ const targetBuffer = (useShared
269
+ ? new SharedArrayBuffer(count * DTYPE_CODE_TO_ITEMSIZE[code])
270
+ : new ArrayBuffer(count * DTYPE_CODE_TO_ITEMSIZE[code])) as ArrayBuffer;
271
+
272
+ // Copy data
273
+ const src = new Uint8Array(srcBuffer, byteOffset, count * DTYPE_CODE_TO_ITEMSIZE[code]);
274
+ const dest = new Uint8Array(targetBuffer);
275
+ dest.set(src);
276
+
277
+ let data: NumpyTypedArray;
278
+ let dtypeStr: string;
279
+ switch (code) {
280
+ case NumpyDTypeCode.BOOL:
281
+ case NumpyDTypeCode.UINT8: data = new Uint8Array(targetBuffer); dtypeStr = code === NumpyDTypeCode.BOOL ? 'bool' : 'uint8'; break;
282
+ case NumpyDTypeCode.INT8: data = new Int8Array(targetBuffer); dtypeStr = 'int8'; break;
283
+ case NumpyDTypeCode.INT16: data = new Int16Array(targetBuffer); dtypeStr = 'int16'; break;
284
+ case NumpyDTypeCode.INT32: data = new Int32Array(targetBuffer); dtypeStr = 'int32'; break;
285
+ case NumpyDTypeCode.INT64: data = new BigInt64Array(targetBuffer); dtypeStr = 'int64'; break;
286
+ case NumpyDTypeCode.UINT16: data = new Uint16Array(targetBuffer); dtypeStr = 'uint16'; break;
287
+ case NumpyDTypeCode.UINT32: data = new Uint32Array(targetBuffer); dtypeStr = 'uint32'; break;
288
+ case NumpyDTypeCode.UINT64: data = new BigUint64Array(targetBuffer); dtypeStr = 'uint64'; break;
289
+ case NumpyDTypeCode.FLOAT32: data = new Float32Array(targetBuffer); dtypeStr = 'float32'; break;
290
+ case NumpyDTypeCode.COMPLEX64:
291
+ data = new Float32Array(targetBuffer); dtypeStr = 'complex64'; break;
292
+ case NumpyDTypeCode.COMPLEX128:
293
+ data = new Float64Array(targetBuffer); dtypeStr = 'complex128'; break;
294
+ case NumpyDTypeCode.FLOAT64:
295
+ default:
296
+ data = new Float64Array(targetBuffer); dtypeStr = 'float64'; break;
297
+ }
298
+
299
+ return { data, dtype: dtypeStr, shape, ndim };
300
+ }
301
+
302
+ // ─── Static helpers ───────────────────────────────────────────────────────
303
+
304
+ /**
305
+ * Detect dtype code from a TypedArray instance.
306
+ */
307
+ static detectDType(arr: ArrayBufferView): { dtype: NumpyDType; itemsize: number } {
308
+ let dtype: NumpyDType;
309
+ if (arr instanceof Float32Array) { dtype = 'float32'; }
310
+ else if (arr instanceof Float64Array) { dtype = 'float64'; }
311
+ else if (arr instanceof Int8Array) { dtype = 'int8'; }
312
+ else if (arr instanceof Int16Array) { dtype = 'int16'; }
313
+ else if (arr instanceof Int32Array) { dtype = 'int32'; }
314
+ else if (arr instanceof BigInt64Array) { dtype = 'int64'; }
315
+ else if (arr instanceof Uint8Array) { dtype = 'uint8'; }
316
+ else if (arr instanceof Uint16Array) { dtype = 'uint16'; }
317
+ else if (arr instanceof Uint32Array) { dtype = 'uint32'; }
318
+ else if (arr instanceof BigUint64Array) { dtype = 'uint64'; }
319
+ else { dtype = 'float64'; }
320
+ const code = DTYPE_STRING_TO_CODE[dtype] ?? NumpyDTypeCode.FLOAT64;
321
+ return { dtype, itemsize: DTYPE_CODE_TO_ITEMSIZE[code] };
322
+ }
323
+
324
+ /**
325
+ * Return the item size in bytes for a dtype string.
326
+ */
327
+ static itemSizeOf(dtype: NumpyDType): number {
328
+ const code = DTYPE_STRING_TO_CODE[dtype] ?? NumpyDTypeCode.FLOAT64;
329
+ return DTYPE_CODE_TO_ITEMSIZE[code];
330
+ }
331
+ }
332
+
@@ -0,0 +1,320 @@
1
+ /**
2
+ * nodepyx — Serializer
3
+ * Central coordinator for Python ↔ JavaScript data conversion.
4
+ * Routes to specialized bridges (MsgPack, NumPy, DataFrame) based on format.
5
+ */
6
+
7
+ import type {
8
+ SerializedValue,
9
+ SerializedFormat,
10
+ } from '../types/python';
11
+ import { NumpyBridge } from './NumpyBridge';
12
+ import { DataFrameBridge } from './DataFrameBridge';
13
+ import { MsgPackSerializer } from './MsgPackSerializer';
14
+ import { Logger } from '../utils/Logger';
15
+
16
+ const logger = new Logger('Serializer');
17
+
18
+ /**
19
+ * SerializerOptions — controls serialization behavior.
20
+ */
21
+ export interface SerializerOptions {
22
+ /** Use SharedArrayBuffer for large NumPy arrays (requires cross-origin isolation) */
23
+ useSharedArrayBuffer?: boolean;
24
+ /** Maximum size for inline JSON (bytes). Larger → binary */
25
+ maxJsonSize?: number;
26
+ /** Whether to wrap Python objects in PyProxy */
27
+ wrapPythonRefs?: boolean;
28
+ }
29
+
30
+ /**
31
+ * Serializer — converts between Python wire format and JavaScript values.
32
+ *
33
+ * This is a singleton accessed via `Serializer.getInstance()`.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const serializer = Serializer.getInstance();
38
+ *
39
+ * // JavaScript → Python wire format
40
+ * const sv = await serializer.serialize([1, 2, 3, 4, 5]);
41
+ *
42
+ * // Python wire format → JavaScript
43
+ * const jsValue = await serializer.deserialize(sv);
44
+ * ```
45
+ */
46
+ export class Serializer {
47
+ private static _instance: Serializer | null = null;
48
+ private readonly _options: Required<SerializerOptions>;
49
+ private readonly _numpyBridge: NumpyBridge;
50
+ private readonly _dataframeBridge: DataFrameBridge;
51
+ private readonly _msgpackSerializer: MsgPackSerializer;
52
+
53
+ private constructor(options: SerializerOptions = {}) {
54
+ this._options = {
55
+ useSharedArrayBuffer: options.useSharedArrayBuffer ?? false,
56
+ maxJsonSize: options.maxJsonSize ?? 65536, // 64KB
57
+ wrapPythonRefs: options.wrapPythonRefs ?? true,
58
+ };
59
+ this._numpyBridge = new NumpyBridge({ useSharedArrayBuffer: this._options.useSharedArrayBuffer });
60
+ this._dataframeBridge = new DataFrameBridge();
61
+ this._msgpackSerializer = new MsgPackSerializer();
62
+ }
63
+
64
+ static getInstance(options?: SerializerOptions): Serializer {
65
+ if (!Serializer._instance) {
66
+ Serializer._instance = new Serializer(options);
67
+ }
68
+ return Serializer._instance;
69
+ }
70
+
71
+ static resetInstance(): void {
72
+ Serializer._instance = null;
73
+ }
74
+
75
+ // ─── Python Wire Format → JavaScript ───────────────────────────────────────
76
+
77
+ /**
78
+ * Deserialize a Python wire format value to JavaScript.
79
+ *
80
+ * Format routing:
81
+ * - JSON (0) → JSON.parse
82
+ * - MsgPack (1) → MessagePack decode
83
+ * - NumPy array (2) → TypedArray
84
+ * - DataFrame (3) → DataFrameResult
85
+ * - Series (4) → SeriesResult
86
+ * - PyRef (6) → PyProxy (via addon)
87
+ * - Bytes (7) → Uint8Array
88
+ * - None (8) → null
89
+ */
90
+ async deserialize(sv: SerializedValue): Promise<unknown> {
91
+ const format = sv.format as number;
92
+
93
+ switch (format) {
94
+ case 0: // JSON
95
+ return this._deserializeJson(sv);
96
+
97
+ case 1: // MSGPACK
98
+ return this._deserializeMsgPack(sv);
99
+
100
+ case 2: // NUMPY_ARRAY
101
+ return this._numpyBridge.deserialize(sv);
102
+
103
+ case 3: // PANDAS_DATAFRAME
104
+ return this._dataframeBridge.deserializeDataFrame(sv);
105
+
106
+ case 4: // PANDAS_SERIES
107
+ return this._dataframeBridge.deserializeSeries(sv);
108
+
109
+ case 6: // PYTHON_REF
110
+ return this._deserializePythonRef(sv);
111
+
112
+ case 7: // BYTES
113
+ return this._deserializeBytes(sv);
114
+
115
+ case 8: // NONE
116
+ return null;
117
+
118
+ default:
119
+ logger.warn(`Unknown serialization format: ${format}`);
120
+ return null;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Deserialize synchronously (for non-async contexts).
126
+ * Limited: only works for simple types.
127
+ */
128
+ deserializeSync(sv: SerializedValue): unknown {
129
+ const format = sv.format as number;
130
+
131
+ switch (format) {
132
+ case 0: // JSON
133
+ return this._deserializeJson(sv);
134
+
135
+ case 7: // BYTES
136
+ return sv.data instanceof Uint8Array ? sv.data : null;
137
+
138
+ case 8: // NONE
139
+ return null;
140
+
141
+ default:
142
+ // For binary formats, return null in sync mode
143
+ return null;
144
+ }
145
+ }
146
+
147
+ // ─── JavaScript → Python Wire Format ───────────────────────────────────────
148
+
149
+ /**
150
+ * Serialize a JavaScript value to Python wire format.
151
+ * Chooses the most efficient encoding automatically.
152
+ */
153
+ async serialize(value: unknown): Promise<SerializedValue> {
154
+ return this._serializeValue(value);
155
+ }
156
+
157
+ /**
158
+ * Serialize synchronously (for non-async contexts).
159
+ */
160
+ serializeSync(value: unknown): SerializedValue {
161
+ return this._serializeValueSync(value);
162
+ }
163
+
164
+ // ─── Private Deserialization ───────────────────────────────────────────────
165
+
166
+ private _deserializeJson(sv: SerializedValue): unknown {
167
+ const data = sv.data;
168
+ if (data === null || data === undefined) {return null;}
169
+
170
+ const jsonStr = typeof data === 'string' ? data : new TextDecoder().decode(data as Uint8Array);
171
+
172
+ if (!jsonStr || jsonStr === 'null') {return null;}
173
+ if (jsonStr === 'true') {return true;}
174
+ if (jsonStr === 'false') {return false;}
175
+
176
+ try {
177
+ return JSON.parse(jsonStr);
178
+ } catch {
179
+ // If JSON parse fails, return the raw string
180
+ return jsonStr;
181
+ }
182
+ }
183
+
184
+ private _deserializeMsgPack(sv: SerializedValue): unknown {
185
+ const data = sv.data;
186
+ if (!data || !(data instanceof Uint8Array)) {return null;}
187
+ try {
188
+ return this._msgpackSerializer.decode(data);
189
+ } catch (err) {
190
+ logger.warn('MsgPack deserialization failed', err);
191
+ return null;
192
+ }
193
+ }
194
+
195
+ private _deserializePythonRef(sv: SerializedValue): unknown {
196
+ // PyRef deserialization is handled by PyProxy._deserializeAddonResult
197
+ // which has access to the addon. Here we return the raw ref.
198
+ const objectId = sv.metadata?.objectId;
199
+ if (objectId) {
200
+ return { __pyref__: objectId };
201
+ }
202
+ return null;
203
+ }
204
+
205
+ private _deserializeBytes(sv: SerializedValue): Uint8Array | null {
206
+ const data = sv.data;
207
+ if (!data) {return null;}
208
+ if (data instanceof Uint8Array) {return data;}
209
+ if (typeof data === 'string') {
210
+ return new TextEncoder().encode(data);
211
+ }
212
+ return null;
213
+ }
214
+
215
+ // ─── Private Serialization ─────────────────────────────────────────────────
216
+
217
+ private async _serializeValue(value: unknown): Promise<SerializedValue> {
218
+ return this._serializeValueSync(value);
219
+ }
220
+
221
+ private _serializeValueSync(value: unknown): SerializedValue {
222
+ // null / undefined
223
+ if (value === null || value === undefined) {
224
+ return { format: 0 as unknown as SerializedFormat, data: 'null', metadata: {} };
225
+ }
226
+
227
+ // Boolean
228
+ if (typeof value === 'boolean') {
229
+ return { format: 0 as unknown as SerializedFormat, data: String(value), metadata: {} };
230
+ }
231
+
232
+ // Number
233
+ if (typeof value === 'number') {
234
+ if (Number.isNaN(value)) {
235
+ return { format: 0 as unknown as SerializedFormat, data: 'null', metadata: {} };
236
+ }
237
+ return { format: 0 as unknown as SerializedFormat, data: String(value), metadata: {} };
238
+ }
239
+
240
+ // BigInt
241
+ if (typeof value === 'bigint') {
242
+ return { format: 0 as unknown as SerializedFormat, data: `"${value.toString()}"`, metadata: {} };
243
+ }
244
+
245
+ // String
246
+ if (typeof value === 'string') {
247
+ return { format: 0 as unknown as SerializedFormat, data: JSON.stringify(value), metadata: {} };
248
+ }
249
+
250
+ // TypedArrays → NumPy format
251
+ if (value instanceof Float32Array) {
252
+ return this._numpyBridge.serializeTypedArray(value, 'float32');
253
+ }
254
+ if (value instanceof Float64Array) {
255
+ return this._numpyBridge.serializeTypedArray(value, 'float64');
256
+ }
257
+ if (value instanceof Int8Array) {
258
+ return this._numpyBridge.serializeTypedArray(value, 'int8');
259
+ }
260
+ if (value instanceof Int16Array) {
261
+ return this._numpyBridge.serializeTypedArray(value, 'int16');
262
+ }
263
+ if (value instanceof Int32Array) {
264
+ return this._numpyBridge.serializeTypedArray(value, 'int32');
265
+ }
266
+ if (value instanceof BigInt64Array) {
267
+ return this._numpyBridge.serializeTypedArray(value, 'int64');
268
+ }
269
+ if (value instanceof Uint8Array) {
270
+ return this._numpyBridge.serializeTypedArray(value, 'uint8');
271
+ }
272
+ if (value instanceof Uint16Array) {
273
+ return this._numpyBridge.serializeTypedArray(value, 'uint16');
274
+ }
275
+ if (value instanceof Uint32Array) {
276
+ return this._numpyBridge.serializeTypedArray(value, 'uint32');
277
+ }
278
+ if (value instanceof BigUint64Array) {
279
+ return this._numpyBridge.serializeTypedArray(value, 'uint64');
280
+ }
281
+
282
+ // Raw ArrayBuffer
283
+ if (value instanceof ArrayBuffer) {
284
+ return { format: 7 as unknown as SerializedFormat, data: new Uint8Array(value), metadata: {} };
285
+ }
286
+
287
+ // Array — use MsgPack for large, JSON for small
288
+ if (Array.isArray(value)) {
289
+ try {
290
+ const json = JSON.stringify(value);
291
+ if (json.length <= this._options.maxJsonSize) {
292
+ return { format: 0 as unknown as SerializedFormat, data: json, metadata: { length: value.length } };
293
+ }
294
+ // Large array → MessagePack
295
+ const packed = this._msgpackSerializer.encode(value);
296
+ return { format: 1 as unknown as SerializedFormat, data: packed, metadata: { length: value.length } };
297
+ } catch {
298
+ return { format: 8 as unknown as SerializedFormat, data: null, metadata: {} };
299
+ }
300
+ }
301
+
302
+ // Plain object — JSON or MsgPack
303
+ if (typeof value === 'object') {
304
+ try {
305
+ const json = JSON.stringify(value);
306
+ if (json.length <= this._options.maxJsonSize) {
307
+ return { format: 0 as unknown as SerializedFormat, data: json, metadata: {} };
308
+ }
309
+ const packed = this._msgpackSerializer.encode(value);
310
+ return { format: 1 as unknown as SerializedFormat, data: packed, metadata: {} };
311
+ } catch {
312
+ return { format: 8 as unknown as SerializedFormat, data: null, metadata: {} };
313
+ }
314
+ }
315
+
316
+ // Fallback
317
+ return { format: 8 as unknown as SerializedFormat, data: null, metadata: {} };
318
+ }
319
+ }
320
+