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,220 @@
1
+ /**
2
+ * nodepyx — Error Translator
3
+ * Converts Python exceptions into proper JavaScript Error subclasses.
4
+ * Preserves full traceback, exception type, and chained causes.
5
+ */
6
+
7
+ import type { PyErrorInfo } from '../types/python';
8
+
9
+ /**
10
+ * Base class for all Python-originating errors.
11
+ * Provides access to Python-specific error metadata.
12
+ */
13
+ export class PythonError extends Error {
14
+ /** Python exception class name (e.g. 'ValueError', 'FileNotFoundError') */
15
+ readonly pythonType: string;
16
+ /** Python module where the exception originated */
17
+ readonly pythonModule: string;
18
+ /** Full Python traceback string */
19
+ readonly traceback: string;
20
+ /** Original PyErrorInfo object */
21
+ readonly pyErrorInfo: PyErrorInfo;
22
+
23
+ constructor(info: PyErrorInfo) {
24
+ super(`[Python ${info.type}] ${info.message}`);
25
+ this.name = 'PythonError';
26
+ this.pythonType = info.type;
27
+ this.pythonModule = info.module ?? 'builtins';
28
+ this.traceback = info.traceback;
29
+ this.pyErrorInfo = info;
30
+
31
+ // Maintain proper prototype chain
32
+ Object.setPrototypeOf(this, new.target.prototype);
33
+
34
+ // Capture stack trace
35
+ if (Error.captureStackTrace) {
36
+ Error.captureStackTrace(this, PythonError);
37
+ }
38
+ }
39
+
40
+ /** Format error with Python traceback for display */
41
+ format(): string {
42
+ return [
43
+ `PythonError: ${this.pythonType}: ${this.pyErrorInfo.message}`,
44
+ '',
45
+ '--- Python Traceback ---',
46
+ this.traceback,
47
+ '--- Node.js Stack ---',
48
+ this.stack ?? '',
49
+ ].join('\n');
50
+ }
51
+ }
52
+
53
+ // ─── Specific Python Exception Subclasses ──────────────────────────────────
54
+
55
+ export class PythonValueError extends PythonError {
56
+ constructor(info: PyErrorInfo) {
57
+ super(info);
58
+ this.name = 'PythonValueError';
59
+ Object.setPrototypeOf(this, new.target.prototype);
60
+ }
61
+ }
62
+
63
+ export class PythonTypeError extends PythonError {
64
+ constructor(info: PyErrorInfo) {
65
+ super(info);
66
+ this.name = 'PythonTypeError';
67
+ Object.setPrototypeOf(this, new.target.prototype);
68
+ }
69
+ }
70
+
71
+ export class PythonKeyError extends PythonError {
72
+ constructor(info: PyErrorInfo) {
73
+ super(info);
74
+ this.name = 'PythonKeyError';
75
+ Object.setPrototypeOf(this, new.target.prototype);
76
+ }
77
+ }
78
+
79
+ export class PythonIndexError extends PythonError {
80
+ constructor(info: PyErrorInfo) {
81
+ super(info);
82
+ this.name = 'PythonIndexError';
83
+ Object.setPrototypeOf(this, new.target.prototype);
84
+ }
85
+ }
86
+
87
+ export class PythonAttributeError extends PythonError {
88
+ constructor(info: PyErrorInfo) {
89
+ super(info);
90
+ this.name = 'PythonAttributeError';
91
+ Object.setPrototypeOf(this, new.target.prototype);
92
+ }
93
+ }
94
+
95
+ export class PythonImportError extends PythonError {
96
+ constructor(info: PyErrorInfo) {
97
+ super(info);
98
+ this.name = 'PythonImportError';
99
+ Object.setPrototypeOf(this, new.target.prototype);
100
+ }
101
+ }
102
+
103
+ export class PythonModuleNotFoundError extends PythonImportError {
104
+ constructor(info: PyErrorInfo) {
105
+ super(info);
106
+ this.name = 'PythonModuleNotFoundError';
107
+ Object.setPrototypeOf(this, new.target.prototype);
108
+ }
109
+ }
110
+
111
+ export class PythonFileNotFoundError extends PythonError {
112
+ constructor(info: PyErrorInfo) {
113
+ super(info);
114
+ this.name = 'PythonFileNotFoundError';
115
+ Object.setPrototypeOf(this, new.target.prototype);
116
+ }
117
+ }
118
+
119
+ export class PythonPermissionError extends PythonError {
120
+ constructor(info: PyErrorInfo) {
121
+ super(info);
122
+ this.name = 'PythonPermissionError';
123
+ Object.setPrototypeOf(this, new.target.prototype);
124
+ }
125
+ }
126
+
127
+ export class PythonRuntimeError extends PythonError {
128
+ constructor(info: PyErrorInfo) {
129
+ super(info);
130
+ this.name = 'PythonRuntimeError';
131
+ Object.setPrototypeOf(this, new.target.prototype);
132
+ }
133
+ }
134
+
135
+ export class PythonMemoryError extends PythonError {
136
+ constructor(info: PyErrorInfo) {
137
+ super(info);
138
+ this.name = 'PythonMemoryError';
139
+ Object.setPrototypeOf(this, new.target.prototype);
140
+ }
141
+ }
142
+
143
+ export class PythonStopIteration extends PythonError {
144
+ constructor(info: PyErrorInfo) {
145
+ super(info);
146
+ this.name = 'PythonStopIteration';
147
+ Object.setPrototypeOf(this, new.target.prototype);
148
+ }
149
+ }
150
+
151
+ export class PythonTimeoutError extends Error {
152
+ readonly timeoutMs: number;
153
+
154
+ constructor(timeoutMs: number, operation: string) {
155
+ super(`Python operation timed out after ${timeoutMs}ms: ${operation}`);
156
+ this.name = 'PythonTimeoutError';
157
+ this.timeoutMs = timeoutMs;
158
+ Object.setPrototypeOf(this, new.target.prototype);
159
+ }
160
+ }
161
+
162
+ export class nodepyxNotInitializedError extends Error {
163
+ constructor(message = 'nodepyx is not initialized. Call nodepyx.init() first.') {
164
+ super(message);
165
+ this.name = 'nodepyxNotInitializedError';
166
+ Object.setPrototypeOf(this, new.target.prototype);
167
+ }
168
+ }
169
+
170
+ export class nodepyxShutdownError extends Error {
171
+ constructor(message = 'nodepyx runtime has been shut down.') {
172
+ super(message);
173
+ this.name = 'nodepyxShutdownError';
174
+ Object.setPrototypeOf(this, new.target.prototype);
175
+ }
176
+ }
177
+
178
+ // ─── Error Translation Map ──────────────────────────────────────────────────
179
+
180
+ type PythonErrorConstructor = new (info: PyErrorInfo) => PythonError;
181
+
182
+ const EXCEPTION_MAP: Record<string, PythonErrorConstructor> = {
183
+ ValueError: PythonValueError,
184
+ TypeError: PythonTypeError,
185
+ KeyError: PythonKeyError,
186
+ IndexError: PythonIndexError,
187
+ AttributeError: PythonAttributeError,
188
+ ImportError: PythonImportError,
189
+ ModuleNotFoundError: PythonModuleNotFoundError,
190
+ FileNotFoundError: PythonFileNotFoundError,
191
+ PermissionError: PythonPermissionError,
192
+ RuntimeError: PythonRuntimeError,
193
+ MemoryError: PythonMemoryError,
194
+ StopIteration: PythonStopIteration,
195
+ StopAsyncIteration: PythonStopIteration,
196
+ };
197
+
198
+ /**
199
+ * Translates a PyErrorInfo (from the C++ addon) into the appropriate
200
+ * JavaScript Error subclass. Falls back to generic PythonError.
201
+ */
202
+ export function translatePythonError(info: PyErrorInfo): PythonError {
203
+ const Ctor = EXCEPTION_MAP[info.type] ?? PythonError;
204
+ return new Ctor(info);
205
+ }
206
+
207
+ /**
208
+ * Type guard: checks whether a value is a PythonError.
209
+ */
210
+ export function isPythonError(value: unknown): value is PythonError {
211
+ return value instanceof PythonError;
212
+ }
213
+
214
+ /**
215
+ * Type guard: checks whether a value is a specific Python exception type.
216
+ */
217
+ export function isPythonErrorOfType(value: unknown, pythonType: string): value is PythonError {
218
+ return value instanceof PythonError && value.pythonType === pythonType;
219
+ }
220
+
@@ -0,0 +1,119 @@
1
+ /**
2
+ * nodepyx — Logger
3
+ * Structured logger with level filtering and colorized output.
4
+ */
5
+
6
+ import type { LogLevel } from '../types/config';
7
+
8
+ const LEVELS: Record<LogLevel, number> = {
9
+ silent: 0,
10
+ error: 1,
11
+ warn: 2,
12
+ info: 3,
13
+ debug: 4,
14
+ };
15
+
16
+ const COLORS = {
17
+ reset: '\x1b[0m',
18
+ red: '\x1b[31m',
19
+ yellow: '\x1b[33m',
20
+ cyan: '\x1b[36m',
21
+ gray: '\x1b[90m',
22
+ green: '\x1b[32m',
23
+ blue: '\x1b[34m',
24
+ };
25
+
26
+ function colorize(color: keyof typeof COLORS, text: string): string {
27
+ if (!process.stdout.isTTY) {return text;}
28
+ return `${COLORS[color]}${text}${COLORS.reset}`;
29
+ }
30
+
31
+ function formatTimestamp(): string {
32
+ return new Date().toISOString().replace('T', ' ').replace('Z', '');
33
+ }
34
+
35
+ export class Logger {
36
+ private _level: LogLevel;
37
+ private _prefix: string;
38
+ private static _globalLevel: LogLevel = 'warn';
39
+
40
+ constructor(prefix: string, level?: LogLevel) {
41
+ this._prefix = prefix;
42
+ this._level = level ?? Logger._globalLevel;
43
+ }
44
+
45
+ static setGlobalLevel(level: LogLevel): void {
46
+ Logger._globalLevel = level;
47
+ }
48
+
49
+ setLevel(level: LogLevel): void {
50
+ this._level = level;
51
+ }
52
+
53
+ private _shouldLog(level: LogLevel): boolean {
54
+ return LEVELS[level] <= LEVELS[this._level];
55
+ }
56
+
57
+ private _format(level: string, message: string, data?: unknown): string {
58
+ const ts = colorize('gray', formatTimestamp());
59
+ const lvl = level;
60
+ const prefix = colorize('blue', `[${this._prefix}]`);
61
+ let line = `${ts} ${lvl} ${prefix} ${message}`;
62
+ if (data !== undefined) {
63
+ const dataStr = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
64
+ line += `\n${colorize('gray', dataStr)}`;
65
+ }
66
+ return line;
67
+ }
68
+
69
+ error(message: string, data?: unknown): void {
70
+ if (!this._shouldLog('error')) {return;}
71
+ const lvl = colorize('red', 'ERROR');
72
+ console.error(this._format(lvl, message, data));
73
+ }
74
+
75
+ warn(message: string, data?: unknown): void {
76
+ if (!this._shouldLog('warn')) {return;}
77
+ const lvl = colorize('yellow', 'WARN ');
78
+ console.warn(this._format(lvl, message, data));
79
+ }
80
+
81
+ info(message: string, data?: unknown): void {
82
+ if (!this._shouldLog('info')) {return;}
83
+ const lvl = colorize('cyan', 'INFO ');
84
+ console.info(this._format(lvl, message, data));
85
+ }
86
+
87
+ debug(message: string, data?: unknown): void {
88
+ if (!this._shouldLog('debug')) {return;}
89
+ const lvl = colorize('gray', 'DEBUG');
90
+ console.debug(this._format(lvl, message, data));
91
+ }
92
+
93
+ success(message: string, data?: unknown): void {
94
+ if (!this._shouldLog('info')) {return;}
95
+ const lvl = colorize('green', 'OK ');
96
+ console.info(this._format(lvl, message, data));
97
+ }
98
+
99
+ time(label: string): () => void {
100
+ const start = performance.now();
101
+ return () => {
102
+ const elapsed = (performance.now() - start).toFixed(2);
103
+ this.debug(`${label} completed in ${elapsed}ms`);
104
+ };
105
+ }
106
+
107
+ child(subPrefix: string): Logger {
108
+ return new Logger(`${this._prefix}:${subPrefix}`, this._level);
109
+ }
110
+ }
111
+
112
+ /** Global logger factory */
113
+ export function createLogger(prefix: string, level?: LogLevel): Logger {
114
+ return new Logger(prefix, level);
115
+ }
116
+
117
+ /** Default nodepyx logger */
118
+ export const logger = createLogger('nodepyx');
119
+
@@ -0,0 +1,175 @@
1
+ /**
2
+ * nodepyx — Memory Monitor
3
+ * Tracks Python object references, heap usage, and triggers GC when needed.
4
+ */
5
+
6
+ import { Logger } from './Logger';
7
+ import type { NativeAddon } from '../types/addon';
8
+
9
+ export interface MemorySnapshot {
10
+ timestamp: number;
11
+ pythonHeapBytes: number;
12
+ pythonHeapPeakBytes: number;
13
+ trackedObjects: number;
14
+ pendingCallbacks: number;
15
+ nodeHeapUsed: number;
16
+ nodeHeapTotal: number;
17
+ nodeExternal: number;
18
+ }
19
+
20
+ export interface MemoryThresholds {
21
+ /** Trigger GC when Python heap exceeds this (bytes). Default: 1GB */
22
+ gcThreshold: number;
23
+ /** Emit warning when tracked objects exceed this. Default: 10000 */
24
+ objectWarningCount: number;
25
+ /** Emit warning when pending callbacks exceed this. Default: 500 */
26
+ callbackWarningCount: number;
27
+ }
28
+
29
+ export class MemoryMonitor {
30
+ private readonly _logger: Logger;
31
+ private _addon: NativeAddon | null = null;
32
+ private _snapshots: MemorySnapshot[] = [];
33
+ private _maxSnapshots = 100;
34
+ private _monitorInterval: NodeJS.Timeout | null = null;
35
+ private _thresholds: MemoryThresholds;
36
+ private _gcRunning = false;
37
+
38
+ constructor(thresholds?: Partial<MemoryThresholds>) {
39
+ this._logger = new Logger('MemoryMonitor');
40
+ this._thresholds = {
41
+ gcThreshold: this._parseBytes('1GB'),
42
+ objectWarningCount: 10000,
43
+ callbackWarningCount: 500,
44
+ ...thresholds,
45
+ };
46
+ }
47
+
48
+ attach(addon: NativeAddon): void {
49
+ this._addon = addon;
50
+ }
51
+
52
+ startMonitoring(intervalMs = 5000): void {
53
+ if (this._monitorInterval) {return;}
54
+ this._monitorInterval = setInterval(() => {
55
+ this._checkMemory().catch(err => {
56
+ this._logger.error('Memory check failed', err);
57
+ });
58
+ }, intervalMs);
59
+
60
+ // Don't keep event loop alive
61
+ if (this._monitorInterval.unref) {
62
+ this._monitorInterval.unref();
63
+ }
64
+
65
+ this._logger.debug(`Memory monitoring started (interval: ${intervalMs}ms)`);
66
+ }
67
+
68
+ stopMonitoring(): void {
69
+ if (this._monitorInterval) {
70
+ clearInterval(this._monitorInterval);
71
+ this._monitorInterval = null;
72
+ this._logger.debug('Memory monitoring stopped');
73
+ }
74
+ }
75
+
76
+ async takeSnapshot(): Promise<MemorySnapshot> {
77
+ const nodeHeap = process.memoryUsage();
78
+ let pythonStats = { pythonHeapBytes: 0, pythonHeapPeakBytes: 0, trackedObjects: 0, pendingCallbacks: 0 };
79
+
80
+ if (this._addon) {
81
+ const stats = this._addon.getMemoryStats();
82
+ pythonStats = {
83
+ pythonHeapBytes: stats.pythonHeapBytes,
84
+ pythonHeapPeakBytes: stats.pythonHeapPeakBytes,
85
+ trackedObjects: stats.trackedObjects,
86
+ pendingCallbacks: stats.pendingCallbacks,
87
+ };
88
+ }
89
+
90
+ const snapshot: MemorySnapshot = {
91
+ timestamp: Date.now(),
92
+ ...pythonStats,
93
+ nodeHeapUsed: nodeHeap.heapUsed,
94
+ nodeHeapTotal: nodeHeap.heapTotal,
95
+ nodeExternal: nodeHeap.external,
96
+ };
97
+
98
+ this._snapshots.push(snapshot);
99
+ if (this._snapshots.length > this._maxSnapshots) {
100
+ this._snapshots.shift();
101
+ }
102
+
103
+ return snapshot;
104
+ }
105
+
106
+ getSnapshots(): MemorySnapshot[] {
107
+ return [...this._snapshots];
108
+ }
109
+
110
+ getLatestSnapshot(): MemorySnapshot | null {
111
+ return this._snapshots[this._snapshots.length - 1] ?? null;
112
+ }
113
+
114
+ async forceGC(): Promise<void> {
115
+ if (this._gcRunning || !this._addon) {return;}
116
+ this._gcRunning = true;
117
+ try {
118
+ this._logger.debug('Running Python garbage collection...');
119
+ await this._addon.collectGarbage();
120
+ this._logger.debug('Python GC completed');
121
+ } finally {
122
+ this._gcRunning = false;
123
+ }
124
+ }
125
+
126
+ private async _checkMemory(): Promise<void> {
127
+ const snapshot = await this.takeSnapshot();
128
+
129
+ // Check Python heap threshold
130
+ if (snapshot.pythonHeapBytes > this._thresholds.gcThreshold) {
131
+ this._logger.warn(
132
+ `Python heap (${this._formatBytes(snapshot.pythonHeapBytes)}) exceeded GC threshold ` +
133
+ `(${this._formatBytes(this._thresholds.gcThreshold)}). Running GC...`
134
+ );
135
+ await this.forceGC();
136
+ }
137
+
138
+ // Check tracked objects
139
+ if (snapshot.trackedObjects > this._thresholds.objectWarningCount) {
140
+ this._logger.warn(
141
+ `High Python object count: ${snapshot.trackedObjects} tracked objects`
142
+ );
143
+ }
144
+
145
+ // Check pending callbacks
146
+ if (snapshot.pendingCallbacks > this._thresholds.callbackWarningCount) {
147
+ this._logger.warn(
148
+ `High pending callback count: ${snapshot.pendingCallbacks}`
149
+ );
150
+ }
151
+ }
152
+
153
+ private _parseBytes(str: string): number {
154
+ const match = str.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)?$/i);
155
+ if (!match) {return 1_073_741_824;} // 1GB default
156
+ const value = parseFloat(match[1]);
157
+ const unit = (match[2] ?? 'B').toUpperCase();
158
+ const units: Record<string, number> = {
159
+ B: 1,
160
+ KB: 1024,
161
+ MB: 1024 ** 2,
162
+ GB: 1024 ** 3,
163
+ TB: 1024 ** 4,
164
+ };
165
+ return Math.floor(value * (units[unit] ?? 1));
166
+ }
167
+
168
+ private _formatBytes(bytes: number): string {
169
+ if (bytes < 1024) {return `${bytes}B`;}
170
+ if (bytes < 1024 ** 2) {return `${(bytes / 1024).toFixed(1)}KB`;}
171
+ if (bytes < 1024 ** 3) {return `${(bytes / 1024 ** 2).toFixed(1)}MB`;}
172
+ return `${(bytes / 1024 ** 3).toFixed(2)}GB`;
173
+ }
174
+ }
175
+