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,257 @@
1
+ /**
2
+ * nodepyx — Python Bridge Header
3
+ * High-level operations on Python objects: import, call, attribute access.
4
+ * Uses TypeConverter for serialization and ThreadPool for async execution.
5
+ */
6
+
7
+ #pragma once
8
+
9
+ #include <Python.h>
10
+ #include <napi.h>
11
+ #include <string>
12
+ #include <vector>
13
+ #include <memory>
14
+ #include <functional>
15
+
16
+ #include "type_converter.h"
17
+ #include "thread_pool.h"
18
+
19
+ namespace nodepyx {
20
+
21
+ /**
22
+ * Options for function calls.
23
+ */
24
+ struct CallOptions {
25
+ std::vector<SerializedValue> args;
26
+ std::vector<std::pair<std::string, SerializedValue>> kwargs;
27
+ uint32_t timeoutMs = 0;
28
+ };
29
+
30
+ /**
31
+ * Attribute access result.
32
+ */
33
+ struct AttributeResult {
34
+ bool success = false;
35
+ SerializedValue value;
36
+ bool isCallable = false;
37
+ bool isObject = false;
38
+ uint32_t objectId = 0;
39
+ std::string errorType;
40
+ std::string errorMessage;
41
+ std::string errorTraceback;
42
+ };
43
+
44
+ /**
45
+ * PythonBridge — orchestrates Python operations.
46
+ *
47
+ * This class provides the business logic on top of raw Python C API calls.
48
+ * All operations are designed to be executed in worker threads (GIL held).
49
+ */
50
+ class PythonBridge {
51
+ public:
52
+ static PythonBridge& getInstance();
53
+
54
+ // Non-copyable singleton
55
+ PythonBridge(const PythonBridge&) = delete;
56
+ PythonBridge& operator=(const PythonBridge&) = delete;
57
+
58
+ // ─── Python Lifecycle ────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Initialize the Python interpreter.
62
+ * Must be called once from the main thread before any Python operations.
63
+ */
64
+ void initializePython(
65
+ const std::string& pythonHome,
66
+ const std::string& pythonExecutable,
67
+ const std::vector<std::string>& extraSysPaths,
68
+ const std::vector<std::pair<std::string, std::string>>& envVars
69
+ );
70
+
71
+ /**
72
+ * Finalize the Python interpreter.
73
+ * Must be called once from the main thread at shutdown.
74
+ */
75
+ void finalizePython();
76
+
77
+ bool isInitialized() const noexcept { return _initialized; }
78
+
79
+ // ─── Module Import ───────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * Import a Python module. Returns objectId.
83
+ * Sync version (GIL must be held by caller).
84
+ */
85
+ TaskResult importModuleSync(const std::string& moduleName);
86
+
87
+ /**
88
+ * Reload a module by objectId.
89
+ */
90
+ TaskResult reloadModuleSync(uint32_t objectId);
91
+
92
+ // ─── Attribute Access ─────────────────────────────────────────────────────
93
+
94
+ /**
95
+ * Get attribute from a Python object by name.
96
+ */
97
+ TaskResult getAttributeSync(uint32_t objectId, const std::string& attrName);
98
+
99
+ /**
100
+ * Get attribute path (chain of attribute accesses).
101
+ * e.g. path = ["groupby", "agg"] means obj.groupby.agg
102
+ */
103
+ TaskResult getAttributePathSync(
104
+ uint32_t objectId,
105
+ const std::vector<std::string>& path
106
+ );
107
+
108
+ /**
109
+ * Set attribute on a Python object.
110
+ */
111
+ TaskResult setAttributeSync(
112
+ uint32_t objectId,
113
+ const std::string& attrName,
114
+ const SerializedValue& value
115
+ );
116
+
117
+ /**
118
+ * Check if an attribute exists.
119
+ */
120
+ bool hasAttributeSync(uint32_t objectId, const std::string& attrName);
121
+
122
+ /**
123
+ * Delete an attribute.
124
+ */
125
+ TaskResult deleteAttributeSync(uint32_t objectId, const std::string& attrName);
126
+
127
+ // ─── Function Calls ──────────────────────────────────────────────────────
128
+
129
+ /**
130
+ * Call a Python callable (resolving attribute path first).
131
+ */
132
+ TaskResult callFunctionSync(
133
+ uint32_t objectId,
134
+ const std::vector<std::string>& path,
135
+ const CallOptions& options
136
+ );
137
+
138
+ /**
139
+ * Call a method by name on a Python object.
140
+ */
141
+ TaskResult callMethodSync(
142
+ uint32_t objectId,
143
+ const std::string& methodName,
144
+ const CallOptions& options
145
+ );
146
+
147
+ // ─── Direct Evaluation ───────────────────────────────────────────────────
148
+
149
+ /**
150
+ * Run arbitrary Python code (statements).
151
+ */
152
+ TaskResult runCodeSync(const std::string& code);
153
+
154
+ /**
155
+ * Evaluate a Python expression and return the result.
156
+ */
157
+ TaskResult evalExpressionSync(const std::string& expression);
158
+
159
+ /**
160
+ * Run a Python file.
161
+ */
162
+ TaskResult runFileSync(const std::string& filePath);
163
+
164
+ // ─── Object Operations ───────────────────────────────────────────────────
165
+
166
+ /**
167
+ * Get the serialized value of an object (resolve it).
168
+ */
169
+ TaskResult getObjectValueSync(uint32_t objectId);
170
+
171
+ /**
172
+ * Get repr() of an object.
173
+ */
174
+ std::string getObjectReprSync(uint32_t objectId);
175
+
176
+ /**
177
+ * Get len() of an object.
178
+ */
179
+ TaskResult getObjectLengthSync(uint32_t objectId);
180
+
181
+ /**
182
+ * Get list of attribute names (dir()).
183
+ */
184
+ TaskResult getObjectKeysSync(uint32_t objectId);
185
+
186
+ // ─── Iterator Support ─────────────────────────────────────────────────────
187
+
188
+ /**
189
+ * Create an iterator for an object.
190
+ */
191
+ TaskResult createIteratorSync(uint32_t objectId, const std::vector<std::string>& path);
192
+
193
+ /**
194
+ * Get next value from an iterator.
195
+ */
196
+ TaskResult iteratorNextSync(uint32_t iteratorId);
197
+
198
+ /**
199
+ * Destroy an iterator.
200
+ */
201
+ void destroyIteratorSync(uint32_t iteratorId);
202
+
203
+ // ─── Memory ──────────────────────────────────────────────────────────────
204
+
205
+ void collectGarbage();
206
+ size_t getTrackedObjectCount() const;
207
+
208
+ private:
209
+ PythonBridge() = default;
210
+ ~PythonBridge() = default;
211
+
212
+ /**
213
+ * Resolve attribute path starting from an objectId.
214
+ * Returns new Python reference, or nullptr on error.
215
+ * GIL must be held.
216
+ */
217
+ PyObject* _resolveAttributePath(
218
+ PyObject* obj,
219
+ const std::vector<std::string>& path
220
+ );
221
+
222
+ /**
223
+ * Convert a SerializedValue back to a Python object.
224
+ * Returns new reference.
225
+ */
226
+ PyObject* _deserialize(const SerializedValue& value);
227
+
228
+ /**
229
+ * Build Python args tuple and kwargs dict from CallOptions.
230
+ */
231
+ bool _buildCallArgs(
232
+ const CallOptions& options,
233
+ PyObject*& argsOut,
234
+ PyObject*& kwargsOut
235
+ );
236
+
237
+ /**
238
+ * Build a TaskResult from Python exception state.
239
+ */
240
+ TaskResult _makeErrorResult();
241
+
242
+ /**
243
+ * Build a successful TaskResult from a Python object.
244
+ */
245
+ TaskResult _makeSuccessResult(PyObject* obj);
246
+
247
+ bool _initialized = false;
248
+ PyThreadState* _mainThreadState = nullptr;
249
+
250
+ // Iterator registry: iteratorId → Python iterator object
251
+ std::map<uint32_t, PyObject*> _iterators;
252
+ std::mutex _iteratorsMutex;
253
+ std::atomic<uint32_t> _nextIteratorId{1};
254
+ };
255
+
256
+ } // namespace nodepyx
257
+
@@ -0,0 +1,336 @@
1
+ /**
2
+ * nodepyx — Thread Pool Implementation
3
+ * Worker thread management for executing Python code off the event loop.
4
+ */
5
+
6
+ #include "thread_pool.h"
7
+ #include "gil_guard.h"
8
+
9
+ #include <Python.h>
10
+ #include <algorithm>
11
+ #include <cassert>
12
+ #include <stdexcept>
13
+ #include <iostream>
14
+
15
+ namespace nodepyx {
16
+
17
+ // ─── Constructor / Destructor ───────────────────────────────────────────────
18
+
19
+ PythonThreadPool::PythonThreadPool(uint32_t numThreads, uint32_t maxQueueSize)
20
+ : _numThreads(numThreads > 0 ? numThreads : 1)
21
+ , _maxQueueSize(maxQueueSize > 0 ? maxQueueSize : 1000)
22
+ {
23
+ // Validate parameters
24
+ if (_numThreads > 64) {
25
+ std::cerr << "[nodepyx] Warning: Thread pool size " << _numThreads
26
+ << " is very large. Consider using <= 16 threads.\n";
27
+ }
28
+ }
29
+
30
+ PythonThreadPool::~PythonThreadPool() {
31
+ if (_running.load()) {
32
+ forceShutdown();
33
+ }
34
+ }
35
+
36
+ // ─── Initialization ─────────────────────────────────────────────────────────
37
+
38
+ void PythonThreadPool::initialize(Napi::Env env) {
39
+ if (_running.load()) {
40
+ throw std::runtime_error("PythonThreadPool already initialized");
41
+ }
42
+
43
+ // Create a ThreadSafeFunction for delivering results back to the JS event loop.
44
+ // This is the safe way to call JS functions from non-JS threads.
45
+ _tsfn = Napi::ThreadSafeFunction::New(
46
+ env,
47
+ Napi::Function::New(env, [](const Napi::CallbackInfo&) {}), // dummy JS function
48
+ "PythonThreadPoolCallback",
49
+ 0, // unlimited queue size
50
+ 1 // initial thread count (we manage threads ourselves)
51
+ );
52
+
53
+ _running.store(true);
54
+ _draining.store(false);
55
+
56
+ // Spawn worker threads
57
+ _threads.reserve(_numThreads);
58
+ for (uint32_t i = 0; i < _numThreads; ++i) {
59
+ _threads.emplace_back(&PythonThreadPool::_workerThread, this, i);
60
+ }
61
+ }
62
+
63
+ // ─── Task Submission ────────────────────────────────────────────────────────
64
+
65
+ uint64_t PythonThreadPool::submit(
66
+ std::function<TaskResult()> work,
67
+ std::function<void(TaskResult)> callback,
68
+ uint32_t timeoutMs
69
+ ) {
70
+ if (!_running.load()) {
71
+ TaskResult errorResult;
72
+ errorResult.success = false;
73
+ errorResult.errorType = "RuntimeError";
74
+ errorResult.errorMessage = "Thread pool is shut down";
75
+ errorResult.errorTraceback = "";
76
+ callback(std::move(errorResult));
77
+ return 0;
78
+ }
79
+
80
+ // Check queue capacity
81
+ {
82
+ std::lock_guard<std::mutex> lock(_queueMutex);
83
+ if (_queue.size() >= _maxQueueSize) {
84
+ // Backpressure: queue is full
85
+ TaskResult errorResult;
86
+ errorResult.success = false;
87
+ errorResult.errorType = "RuntimeError";
88
+ errorResult.errorMessage = "Python thread pool queue is full (max: " +
89
+ std::to_string(_maxQueueSize) + "). " +
90
+ "Consider increasing threadPoolSize or maxQueueSize.";
91
+ errorResult.errorTraceback = "";
92
+ callback(std::move(errorResult));
93
+ return 0;
94
+ }
95
+ }
96
+
97
+ uint64_t taskId = _nextTaskId.fetch_add(1, std::memory_order_relaxed);
98
+
99
+ PythonTask task;
100
+ task.id = taskId;
101
+ task.work = std::move(work);
102
+ task.callback = std::move(callback);
103
+ task.submittedAt = std::chrono::steady_clock::now();
104
+ task.timeoutMs = timeoutMs;
105
+
106
+ {
107
+ std::lock_guard<std::mutex> lock(_queueMutex);
108
+ _queue.push_back(std::move(task));
109
+ _stats.totalTasksSubmitted.fetch_add(1, std::memory_order_relaxed);
110
+ _stats.currentQueueSize.fetch_add(1, std::memory_order_relaxed);
111
+ }
112
+
113
+ _queueCv.notify_one();
114
+ return taskId;
115
+ }
116
+
117
+ // ─── Worker Thread ──────────────────────────────────────────────────────────
118
+
119
+ void PythonThreadPool::_workerThread(uint32_t threadId) {
120
+ // Give the thread a name for debugging
121
+ // Note: thread naming is platform-specific
122
+ #ifdef __linux__
123
+ pthread_setname_np(pthread_self(), ("nodepyx-worker-" + std::to_string(threadId)).c_str());
124
+ #endif
125
+
126
+ while (_running.load()) {
127
+ PythonTask task;
128
+
129
+ // Wait for a task
130
+ {
131
+ std::unique_lock<std::mutex> lock(_queueMutex);
132
+ _queueCv.wait(lock, [this] {
133
+ return !_queue.empty() || !_running.load();
134
+ });
135
+
136
+ if (!_running.load() && _queue.empty()) {
137
+ break;
138
+ }
139
+
140
+ if (_queue.empty()) {
141
+ continue;
142
+ }
143
+
144
+ task = std::move(_queue.front());
145
+ _queue.pop_front();
146
+ _stats.currentQueueSize.fetch_sub(1, std::memory_order_relaxed);
147
+ }
148
+
149
+ // Check timeout before executing
150
+ if (_isTimedOut(task)) {
151
+ TaskResult timeoutResult;
152
+ timeoutResult.success = false;
153
+ timeoutResult.errorType = "TimeoutError";
154
+ timeoutResult.errorMessage = "Python task timed out after " +
155
+ std::to_string(task.timeoutMs) + "ms in queue";
156
+ timeoutResult.errorTraceback = "";
157
+ _stats.totalTasksTimedOut.fetch_add(1, std::memory_order_relaxed);
158
+ _postResult(task.id, std::move(timeoutResult), std::move(task.callback));
159
+ continue;
160
+ }
161
+
162
+ // Execute the task with GIL held
163
+ _stats.activeThreads.fetch_add(1, std::memory_order_relaxed);
164
+
165
+ auto startTime = std::chrono::steady_clock::now();
166
+ TaskResult result;
167
+
168
+ try {
169
+ // Acquire GIL — critical: must be released when we're done
170
+ GILAcquire gil;
171
+
172
+ // Check timeout again (task might have waited in queue)
173
+ if (task.timeoutMs > 0 && _isTimedOut(task)) {
174
+ result.success = false;
175
+ result.errorType = "TimeoutError";
176
+ result.errorMessage = "Python task timed out";
177
+ result.errorTraceback = "";
178
+ } else {
179
+ // Execute the Python work
180
+ result = task.work();
181
+ }
182
+ } catch (const std::exception& e) {
183
+ result.success = false;
184
+ result.errorType = "RuntimeError";
185
+ result.errorMessage = std::string("C++ exception in worker thread: ") + e.what();
186
+ result.errorTraceback = "";
187
+ } catch (...) {
188
+ result.success = false;
189
+ result.errorType = "RuntimeError";
190
+ result.errorMessage = "Unknown C++ exception in worker thread";
191
+ result.errorTraceback = "";
192
+ }
193
+
194
+ auto endTime = std::chrono::steady_clock::now();
195
+ auto durationUs = std::chrono::duration_cast<std::chrono::microseconds>(endTime - startTime).count();
196
+ _stats.totalExecutionTimeUs.fetch_add(static_cast<uint64_t>(durationUs), std::memory_order_relaxed);
197
+
198
+ if (result.success) {
199
+ _stats.totalTasksCompleted.fetch_add(1, std::memory_order_relaxed);
200
+ } else {
201
+ _stats.totalTasksFailed.fetch_add(1, std::memory_order_relaxed);
202
+ }
203
+
204
+ _stats.activeThreads.fetch_sub(1, std::memory_order_relaxed);
205
+
206
+ // Post result back to Node.js event loop
207
+ _postResult(task.id, std::move(result), std::move(task.callback));
208
+
209
+ // Signal drain waiters
210
+ if (_draining.load()) {
211
+ std::lock_guard<std::mutex> lock(_queueMutex);
212
+ _drainCv.notify_all();
213
+ }
214
+ }
215
+ }
216
+
217
+ // ─── Result Delivery ────────────────────────────────────────────────────────
218
+
219
+ void PythonThreadPool::_postResult(
220
+ uint64_t taskId,
221
+ TaskResult result,
222
+ std::function<void(TaskResult)> callback
223
+ ) {
224
+ // Package the callback + result into a heap allocation
225
+ // that will be freed in the JS thread
226
+ auto* payload = new std::pair<std::function<void(TaskResult)>, TaskResult>(
227
+ std::move(callback),
228
+ std::move(result)
229
+ );
230
+
231
+ // Use ThreadSafeFunction to invoke the callback in the JS event loop
232
+ napi_status status = _tsfn.NonBlockingCall(
233
+ payload,
234
+ [](Napi::Env env, Napi::Function jsCallback, void* data) {
235
+ auto* p = static_cast<std::pair<std::function<void(TaskResult)>, TaskResult>*>(data);
236
+ try {
237
+ p->first(std::move(p->second));
238
+ } catch (...) {
239
+ // Don't let JS callback exceptions crash the worker
240
+ }
241
+ delete p;
242
+ }
243
+ );
244
+
245
+ if (status != napi_ok) {
246
+ // This can happen during shutdown; just clean up
247
+ auto* p = static_cast<std::pair<std::function<void(TaskResult)>, TaskResult>*>(payload);
248
+ try {
249
+ // Try to call with error result
250
+ TaskResult shutdownResult;
251
+ shutdownResult.success = false;
252
+ shutdownResult.errorType = "RuntimeError";
253
+ shutdownResult.errorMessage = "Thread pool shut down while task was running";
254
+ p->first(std::move(shutdownResult));
255
+ } catch (...) {}
256
+ delete p;
257
+ }
258
+ }
259
+
260
+ // ─── Timeout Check ──────────────────────────────────────────────────────────
261
+
262
+ bool PythonThreadPool::_isTimedOut(const PythonTask& task) const {
263
+ if (task.timeoutMs == 0) return false;
264
+
265
+ auto now = std::chrono::steady_clock::now();
266
+ auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
267
+ now - task.submittedAt
268
+ ).count();
269
+
270
+ return elapsed >= static_cast<long long>(task.timeoutMs);
271
+ }
272
+
273
+ // ─── Resize ─────────────────────────────────────────────────────────────────
274
+
275
+ void PythonThreadPool::resize(uint32_t newNumThreads) {
276
+ if (!_running.load()) return;
277
+ if (newNumThreads == 0) newNumThreads = 1;
278
+
279
+ // For simplicity: add threads if growing, remove by setting flag if shrinking
280
+ // A production implementation would use a more sophisticated approach
281
+ uint32_t current = static_cast<uint32_t>(_threads.size());
282
+
283
+ if (newNumThreads > current) {
284
+ for (uint32_t i = current; i < newNumThreads; ++i) {
285
+ _threads.emplace_back(&PythonThreadPool::_workerThread, this, i);
286
+ }
287
+ }
288
+ // Shrinking is complex and rarely needed; skip for now
289
+ _numThreads = newNumThreads;
290
+ }
291
+
292
+ // ─── Shutdown ───────────────────────────────────────────────────────────────
293
+
294
+ void PythonThreadPool::shutdown() {
295
+ if (!_running.load()) return;
296
+
297
+ // Drain the queue first
298
+ _draining.store(true);
299
+ {
300
+ std::unique_lock<std::mutex> lock(_queueMutex);
301
+ _drainCv.wait_for(lock, std::chrono::seconds(30), [this] {
302
+ return _queue.empty() && _stats.activeThreads.load() == 0;
303
+ });
304
+ }
305
+ _draining.store(false);
306
+
307
+ forceShutdown();
308
+ }
309
+
310
+ void PythonThreadPool::forceShutdown() {
311
+ if (!_running.load()) return;
312
+
313
+ _running.store(false);
314
+ _queueCv.notify_all();
315
+
316
+ // Wait for all threads to finish
317
+ for (auto& thread : _threads) {
318
+ if (thread.joinable()) {
319
+ thread.join();
320
+ }
321
+ }
322
+ _threads.clear();
323
+
324
+ // Release ThreadSafeFunction
325
+ _tsfn.Release();
326
+ }
327
+
328
+ // ─── Queue Size ─────────────────────────────────────────────────────────────
329
+
330
+ size_t PythonThreadPool::getQueueSize() const {
331
+ std::lock_guard<std::mutex> lock(_queueMutex);
332
+ return _queue.size();
333
+ }
334
+
335
+ } // namespace nodepyx
336
+