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.
- package/LICENSE +22 -0
- package/README.md +399 -0
- package/binding.gyp +73 -0
- package/dist/core/PyCallable.d.ts +65 -0
- package/dist/core/PyCallable.d.ts.map +1 -0
- package/dist/core/PyCallable.js +109 -0
- package/dist/core/PyCallable.js.map +1 -0
- package/dist/core/PyContext.d.ts +76 -0
- package/dist/core/PyContext.d.ts.map +1 -0
- package/dist/core/PyContext.js +228 -0
- package/dist/core/PyContext.js.map +1 -0
- package/dist/core/PyIterator.d.ts +84 -0
- package/dist/core/PyIterator.d.ts.map +1 -0
- package/dist/core/PyIterator.js +243 -0
- package/dist/core/PyIterator.js.map +1 -0
- package/dist/core/PyModule.d.ts +55 -0
- package/dist/core/PyModule.d.ts.map +1 -0
- package/dist/core/PyModule.js +172 -0
- package/dist/core/PyModule.js.map +1 -0
- package/dist/core/PyProxy.d.ts +65 -0
- package/dist/core/PyProxy.d.ts.map +1 -0
- package/dist/core/PyProxy.js +483 -0
- package/dist/core/PyProxy.js.map +1 -0
- package/dist/core/PyRuntime.d.ts +105 -0
- package/dist/core/PyRuntime.d.ts.map +1 -0
- package/dist/core/PyRuntime.js +438 -0
- package/dist/core/PyRuntime.js.map +1 -0
- package/dist/env/CondaManager.d.ts +118 -0
- package/dist/env/CondaManager.d.ts.map +1 -0
- package/dist/env/CondaManager.js +401 -0
- package/dist/env/CondaManager.js.map +1 -0
- package/dist/env/PackageInstaller.d.ts +233 -0
- package/dist/env/PackageInstaller.d.ts.map +1 -0
- package/dist/env/PackageInstaller.js +609 -0
- package/dist/env/PackageInstaller.js.map +1 -0
- package/dist/env/PythonDetector.d.ts +103 -0
- package/dist/env/PythonDetector.d.ts.map +1 -0
- package/dist/env/PythonDetector.js +381 -0
- package/dist/env/PythonDetector.js.map +1 -0
- package/dist/env/VenvManager.d.ts +117 -0
- package/dist/env/VenvManager.d.ts.map +1 -0
- package/dist/env/VenvManager.js +331 -0
- package/dist/env/VenvManager.js.map +1 -0
- package/dist/index.d.ts +169 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +393 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/Plugin.interface.d.ts +41 -0
- package/dist/plugins/Plugin.interface.d.ts.map +1 -0
- package/dist/plugins/Plugin.interface.js +12 -0
- package/dist/plugins/Plugin.interface.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +26 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +174 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.js +41 -0
- package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.js +57 -0
- package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.js +50 -0
- package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +12 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/serialization/DataFrameBridge.d.ts +141 -0
- package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
- package/dist/serialization/DataFrameBridge.js +355 -0
- package/dist/serialization/DataFrameBridge.js.map +1 -0
- package/dist/serialization/MsgPackSerializer.d.ts +45 -0
- package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
- package/dist/serialization/MsgPackSerializer.js +242 -0
- package/dist/serialization/MsgPackSerializer.js.map +1 -0
- package/dist/serialization/NumpyBridge.d.ts +96 -0
- package/dist/serialization/NumpyBridge.d.ts.map +1 -0
- package/dist/serialization/NumpyBridge.js +323 -0
- package/dist/serialization/NumpyBridge.js.map +1 -0
- package/dist/serialization/Serializer.d.ts +78 -0
- package/dist/serialization/Serializer.d.ts.map +1 -0
- package/dist/serialization/Serializer.js +281 -0
- package/dist/serialization/Serializer.js.map +1 -0
- package/dist/types/PythonTypeMapper.d.ts +87 -0
- package/dist/types/PythonTypeMapper.d.ts.map +1 -0
- package/dist/types/PythonTypeMapper.js +449 -0
- package/dist/types/PythonTypeMapper.js.map +1 -0
- package/dist/types/StubCache.d.ts +109 -0
- package/dist/types/StubCache.d.ts.map +1 -0
- package/dist/types/StubCache.js +333 -0
- package/dist/types/StubCache.js.map +1 -0
- package/dist/types/TypeGenerator.d.ts +139 -0
- package/dist/types/TypeGenerator.d.ts.map +1 -0
- package/dist/types/TypeGenerator.js +372 -0
- package/dist/types/TypeGenerator.js.map +1 -0
- package/dist/types/addon.d.ts +114 -0
- package/dist/types/addon.d.ts.map +1 -0
- package/dist/types/addon.js +32 -0
- package/dist/types/addon.js.map +1 -0
- package/dist/types/config.d.ts +175 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +35 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/python.d.ts +235 -0
- package/dist/types/python.d.ts.map +1 -0
- package/dist/types/python.js +7 -0
- package/dist/types/python.js.map +1 -0
- package/dist/utils/ErrorTranslator.d.ts +83 -0
- package/dist/utils/ErrorTranslator.d.ts.map +1 -0
- package/dist/utils/ErrorTranslator.js +210 -0
- package/dist/utils/ErrorTranslator.js.map +1 -0
- package/dist/utils/Logger.d.ts +27 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +115 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/MemoryMonitor.d.ts +44 -0
- package/dist/utils/MemoryMonitor.d.ts.map +1 -0
- package/dist/utils/MemoryMonitor.js +143 -0
- package/dist/utils/MemoryMonitor.js.map +1 -0
- package/package.json +177 -0
- package/python/error_handler.py +433 -0
- package/python/nodepyx_runtime.py +575 -0
- package/python/serializer.py +379 -0
- package/python/type_inspector.py +288 -0
- package/scripts/build-native.js +68 -0
- package/scripts/download-prebuilds.js +99 -0
- package/scripts/generate-stubs.js +405 -0
- package/scripts/install.js +260 -0
- package/src/core/PyCallable.ts +137 -0
- package/src/core/PyContext.ts +296 -0
- package/src/core/PyIterator.ts +294 -0
- package/src/core/PyModule.ts +194 -0
- package/src/core/PyProxy.ts +605 -0
- package/src/core/PyRuntime.ts +504 -0
- package/src/env/CondaManager.ts +451 -0
- package/src/env/PackageInstaller.ts +738 -0
- package/src/env/PythonDetector.ts +414 -0
- package/src/env/VenvManager.ts +396 -0
- package/src/index.ts +425 -0
- package/src/native/gil_guard.cpp +26 -0
- package/src/native/gil_guard.h +175 -0
- package/src/native/nodepyx_addon.cpp +886 -0
- package/src/native/python_bridge.cpp +790 -0
- package/src/native/python_bridge.h +257 -0
- package/src/native/thread_pool.cpp +336 -0
- package/src/native/thread_pool.h +175 -0
- package/src/native/type_converter.cpp +901 -0
- package/src/native/type_converter.h +272 -0
- package/src/nextjs/PyProvider.tsx +123 -0
- package/src/nextjs/index.ts +21 -0
- package/src/nextjs/usePython.ts +106 -0
- package/src/nextjs/withnodepyx.ts +88 -0
- package/src/plugins/Plugin.interface.ts +51 -0
- package/src/plugins/PluginManager.ts +155 -0
- package/src/plugins/builtin/NumpyPlugin.ts +36 -0
- package/src/plugins/builtin/PandasPlugin.ts +49 -0
- package/src/plugins/builtin/TorchPlugin.ts +56 -0
- package/src/plugins/index.ts +7 -0
- package/src/serialization/DataFrameBridge.ts +398 -0
- package/src/serialization/MsgPackSerializer.ts +220 -0
- package/src/serialization/NumpyBridge.ts +332 -0
- package/src/serialization/Serializer.ts +320 -0
- package/src/types/PythonTypeMapper.ts +495 -0
- package/src/types/StubCache.ts +340 -0
- package/src/types/TypeGenerator.ts +491 -0
- package/src/types/addon.ts +170 -0
- package/src/types/config.ts +226 -0
- package/src/types/index.ts +55 -0
- package/src/types/python.ts +309 -0
- package/src/types/stubs/numpy.d.ts +441 -0
- package/src/types/stubs/pandas.d.ts +575 -0
- package/src/types/stubs/sklearn.d.ts +728 -0
- package/src/types/stubs/torch.d.ts +694 -0
- package/src/utils/ErrorTranslator.ts +220 -0
- package/src/utils/Logger.ts +119 -0
- 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
|
+
|