pynect 0.2.6 → 0.2.9

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 (70) hide show
  1. package/README.md +50 -4
  2. package/binding.gyp +2 -1
  3. package/dist/cli/codegen.d.ts +3 -0
  4. package/dist/cli/codegen.d.ts.map +1 -1
  5. package/dist/cli/codegen.js +12 -0
  6. package/dist/cli/codegen.js.map +1 -1
  7. package/dist/config/project.d.ts +7 -0
  8. package/dist/config/project.d.ts.map +1 -1
  9. package/dist/config/project.js +23 -2
  10. package/dist/config/project.js.map +1 -1
  11. package/dist/core/py-proxy.d.ts +1 -1
  12. package/dist/core/py-proxy.d.ts.map +1 -1
  13. package/dist/core/py-proxy.js +40 -7
  14. package/dist/core/py-proxy.js.map +1 -1
  15. package/dist/core/py-runtime.d.ts +7 -0
  16. package/dist/core/py-runtime.d.ts.map +1 -1
  17. package/dist/core/py-runtime.js +50 -3
  18. package/dist/core/py-runtime.js.map +1 -1
  19. package/dist/core/pynect-callback.d.ts +25 -0
  20. package/dist/core/pynect-callback.d.ts.map +1 -0
  21. package/dist/core/pynect-callback.js +99 -0
  22. package/dist/core/pynect-callback.js.map +1 -0
  23. package/dist/env/project-environment.d.ts.map +1 -1
  24. package/dist/env/project-environment.js +1 -0
  25. package/dist/env/project-environment.js.map +1 -1
  26. package/dist/index.d.ts +7 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +8 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/native/native-addon-loader.d.ts.map +1 -1
  31. package/dist/native/native-addon-loader.js +34 -10
  32. package/dist/native/native-addon-loader.js.map +1 -1
  33. package/dist/types/addon.d.ts +10 -0
  34. package/dist/types/addon.d.ts.map +1 -1
  35. package/dist/types/addon.js.map +1 -1
  36. package/dist/types/config.d.ts +10 -0
  37. package/dist/types/config.d.ts.map +1 -1
  38. package/dist/types/config.js +6 -0
  39. package/dist/types/config.js.map +1 -1
  40. package/dist/types/index.d.ts +1 -1
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/types/index.js.map +1 -1
  43. package/dist/types/python.d.ts +2 -0
  44. package/dist/types/python.d.ts.map +1 -1
  45. package/package.json +2 -2
  46. package/prebuilds/darwin-arm64/pynect-prebuild.json +5 -4
  47. package/prebuilds/darwin-arm64/pynect_addon.node +0 -0
  48. package/prebuilds/darwin-x64/pynect-prebuild.json +5 -4
  49. package/prebuilds/darwin-x64/pynect_addon.node +0 -0
  50. package/prebuilds/linux-arm64/pynect-prebuild.json +5 -4
  51. package/prebuilds/linux-arm64/pynect_addon.node +0 -0
  52. package/prebuilds/linux-x64/pynect-prebuild.json +5 -4
  53. package/prebuilds/linux-x64/pynect_addon.node +0 -0
  54. package/prebuilds/win32-arm64/pynect-prebuild.json +5 -4
  55. package/prebuilds/win32-arm64/pynect_addon.node +0 -0
  56. package/prebuilds/win32-x64/pynect-prebuild.json +5 -4
  57. package/prebuilds/win32-x64/pynect_addon.node +0 -0
  58. package/python/type_inspector.py +12 -0
  59. package/scripts/build-native.js +9 -3
  60. package/scripts/ensure-native.js +8 -2
  61. package/scripts/smoke-python-boundary.js +71 -1
  62. package/src/native/callback_bridge.h +11 -0
  63. package/src/native/native-addon-loader.ts +39 -11
  64. package/src/native/pynect_addon.cpp +354 -4
  65. package/src/native/python_bridge.cpp +117 -40
  66. package/src/native/python_bridge.h +13 -0
  67. package/src/native/thread_pool.cpp +62 -55
  68. package/src/native/thread_pool.h +13 -0
  69. package/src/native/type_converter.cpp +32 -22
  70. package/src/native/type_converter.h +2 -0
@@ -19,6 +19,7 @@
19
19
  #include "thread_pool.h"
20
20
  #include "type_converter.h"
21
21
  #include "gil_guard.h"
22
+ #include "callback_bridge.h"
22
23
 
23
24
  #include <memory>
24
25
  #include <string>
@@ -29,6 +30,7 @@
29
30
  #include <iostream>
30
31
  #include <chrono>
31
32
  #include <ctime>
33
+ #include <unordered_map>
32
34
 
33
35
  namespace pynect {
34
36
 
@@ -37,6 +39,38 @@ namespace pynect {
37
39
  static std::unique_ptr<PythonThreadPool> g_threadPool;
38
40
  static bool g_initialized = false;
39
41
  static bool g_includeTracebacks = false;
42
+ static uint32_t g_callbackMaxDepth = 0;
43
+ static std::atomic<uint32_t> g_nextCallbackId{1};
44
+ static std::mutex g_callbacksMutex;
45
+ static thread_local uint32_t g_callbackDepth = 0;
46
+
47
+ struct CallbackState {
48
+ explicit CallbackState(Napi::ThreadSafeFunction callbackDispatcher)
49
+ : dispatcher(std::move(callbackDispatcher)) {}
50
+
51
+ Napi::ThreadSafeFunction dispatcher;
52
+ std::atomic<bool> active{true};
53
+ };
54
+
55
+ struct CallbackCompletion {
56
+ std::atomic<bool> completed{false};
57
+ std::mutex mutex;
58
+ SerializedValue value;
59
+ bool failed = false;
60
+ std::string errorName;
61
+ std::string errorMessage;
62
+ std::string errorStack;
63
+ };
64
+
65
+ struct CallbackInvocationPayload {
66
+ std::shared_ptr<CallbackState> state;
67
+ std::shared_ptr<CallbackCompletion> completion;
68
+ uint32_t depth;
69
+ std::vector<TaskResult> args;
70
+ std::map<std::string, TaskResult> kwargs;
71
+ };
72
+
73
+ static std::unordered_map<uint32_t, std::shared_ptr<CallbackState>> g_callbacks;
40
74
 
41
75
  // Helper: Extract SerializedValue from JS object
42
76
 
@@ -58,6 +92,15 @@ static SerializedValue jsToSerializedValue(const Napi::Object& obj) {
58
92
  } else if (data.IsBuffer()) {
59
93
  Napi::Buffer<uint8_t> buf = data.As<Napi::Buffer<uint8_t>>();
60
94
  sv.binaryData.assign(buf.Data(), buf.Data() + buf.Length());
95
+ } else if (data.IsTypedArray()) {
96
+ Napi::TypedArray array = data.As<Napi::TypedArray>();
97
+ Napi::ArrayBuffer buffer = array.ArrayBuffer();
98
+ auto* begin = static_cast<uint8_t*>(buffer.Data()) + array.ByteOffset();
99
+ sv.binaryData.assign(begin, begin + array.ByteLength());
100
+ } else if (data.IsArrayBuffer()) {
101
+ Napi::ArrayBuffer buffer = data.As<Napi::ArrayBuffer>();
102
+ auto* begin = static_cast<uint8_t*>(buffer.Data());
103
+ sv.binaryData.assign(begin, begin + buffer.ByteLength());
61
104
  }
62
105
  }
63
106
 
@@ -66,6 +109,9 @@ static SerializedValue jsToSerializedValue(const Napi::Object& obj) {
66
109
  if (meta.Has("objectId")) {
67
110
  sv.metadata.objectId = meta.Get("objectId").As<Napi::Number>().Uint32Value();
68
111
  }
112
+ if (meta.Has("callbackId")) {
113
+ sv.metadata.callbackId = meta.Get("callbackId").As<Napi::Number>().Uint32Value();
114
+ }
69
115
  if (meta.Has("dtype") && meta.Get("dtype").IsString()) {
70
116
  sv.metadata.dtype = meta.Get("dtype").As<Napi::String>().Utf8Value();
71
117
  }
@@ -115,6 +161,230 @@ static Napi::Object taskResultToJS(Napi::Env env, const TaskResult& result) {
115
161
  return obj;
116
162
  }
117
163
 
164
+ static void completeCallback(
165
+ const std::shared_ptr<CallbackCompletion>& completion,
166
+ SerializedValue value
167
+ ) {
168
+ {
169
+ std::lock_guard<std::mutex> lock(completion->mutex);
170
+ if (completion->completed.load(std::memory_order_relaxed)) return;
171
+ completion->value = std::move(value);
172
+ completion->completed.store(true, std::memory_order_release);
173
+ }
174
+ if (g_threadPool) g_threadPool->notifyWaiters();
175
+ }
176
+
177
+ static void failCallback(
178
+ const std::shared_ptr<CallbackCompletion>& completion,
179
+ std::string name,
180
+ std::string message,
181
+ std::string stack = {}
182
+ ) {
183
+ {
184
+ std::lock_guard<std::mutex> lock(completion->mutex);
185
+ if (completion->completed.load(std::memory_order_relaxed)) return;
186
+ completion->failed = true;
187
+ completion->errorName = std::move(name);
188
+ completion->errorMessage = std::move(message);
189
+ completion->errorStack = std::move(stack);
190
+ completion->completed.store(true, std::memory_order_release);
191
+ }
192
+ if (g_threadPool) g_threadPool->notifyWaiters();
193
+ }
194
+
195
+ static std::shared_ptr<CallbackState> callbackState(uint32_t callbackId) {
196
+ std::lock_guard<std::mutex> lock(g_callbacksMutex);
197
+ auto found = g_callbacks.find(callbackId);
198
+ if (found == g_callbacks.end() || !found->second->active.load()) return nullptr;
199
+ return found->second;
200
+ }
201
+
202
+ struct CallbackDepthScope {
203
+ explicit CallbackDepthScope(uint32_t depth)
204
+ : previous(g_callbackDepth) {
205
+ g_callbackDepth = depth;
206
+ }
207
+ ~CallbackDepthScope() { g_callbackDepth = previous; }
208
+ uint32_t previous;
209
+ };
210
+
211
+ static constexpr const char* CALLBACK_CAPSULE = "pynect.callback";
212
+
213
+ static void destroyCallbackCapsule(PyObject* capsule) {
214
+ auto* callbackId = static_cast<uint32_t*>(PyCapsule_GetPointer(capsule, CALLBACK_CAPSULE));
215
+ if (!callbackId) PyErr_Clear();
216
+ delete callbackId;
217
+ }
218
+
219
+ static PyObject* invokeJavaScriptCallback(
220
+ PyObject* capsule,
221
+ PyObject* args,
222
+ PyObject* kwargs
223
+ ) {
224
+ auto* callbackId = static_cast<uint32_t*>(PyCapsule_GetPointer(capsule, CALLBACK_CAPSULE));
225
+ if (!callbackId) return nullptr;
226
+ auto state = callbackState(*callbackId);
227
+ if (!state) {
228
+ PyErr_SetString(PyExc_RuntimeError, "Pynect callback has been disposed");
229
+ return nullptr;
230
+ }
231
+
232
+ const uint32_t depth = g_callbackDepth + 1;
233
+ if (g_callbackMaxDepth > 0 && depth > g_callbackMaxDepth) {
234
+ PyErr_Format(
235
+ PyExc_RecursionError,
236
+ "Pynect callback recursion depth %u exceeds the configured maximum of %u",
237
+ depth,
238
+ g_callbackMaxDepth
239
+ );
240
+ return nullptr;
241
+ }
242
+ CallbackDepthScope depthScope(depth);
243
+
244
+ auto completion = std::make_shared<CallbackCompletion>();
245
+ auto* payload = new CallbackInvocationPayload{state, completion, depth, {}, {}};
246
+ const Py_ssize_t argCount = PyTuple_Size(args);
247
+ payload->args.reserve(static_cast<size_t>(argCount));
248
+ for (Py_ssize_t index = 0; index < argCount; ++index) {
249
+ PyObject* value = PyTuple_GetItem(args, index);
250
+ payload->args.push_back(PythonBridge::getInstance().serializeValue(value));
251
+ }
252
+ if (kwargs) {
253
+ PyObject* key = nullptr;
254
+ PyObject* value = nullptr;
255
+ Py_ssize_t position = 0;
256
+ while (PyDict_Next(kwargs, &position, &key, &value)) {
257
+ Py_ssize_t length = 0;
258
+ const char* text = PyUnicode_AsUTF8AndSize(key, &length);
259
+ if (!text) {
260
+ delete payload;
261
+ return nullptr;
262
+ }
263
+ payload->kwargs.emplace(
264
+ std::string(text, static_cast<size_t>(length)),
265
+ PythonBridge::getInstance().serializeValue(value)
266
+ );
267
+ }
268
+ }
269
+
270
+ const napi_status status = state->dispatcher.NonBlockingCall(
271
+ payload,
272
+ [](Napi::Env env, Napi::Function dispatcher, CallbackInvocationPayload* invocation) {
273
+ const auto completion = invocation->completion;
274
+ try {
275
+ Napi::Object request = Napi::Object::New(env);
276
+ request.Set("depth", Napi::Number::New(env, invocation->depth));
277
+ Napi::Array argsArray = Napi::Array::New(env, invocation->args.size());
278
+ for (size_t index = 0; index < invocation->args.size(); ++index) {
279
+ argsArray.Set(index, taskResultToJS(env, invocation->args[index]));
280
+ }
281
+ request.Set("args", argsArray);
282
+ Napi::Object kwargsObject = Napi::Object::New(env);
283
+ for (const auto& [key, value] : invocation->kwargs) {
284
+ kwargsObject.Set(key, taskResultToJS(env, value));
285
+ }
286
+ request.Set("kwargs", kwargsObject);
287
+ request.Set("resolve", Napi::Function::New(
288
+ env,
289
+ [completion](const Napi::CallbackInfo& info) {
290
+ try {
291
+ if (info.Length() < 1 || !info[0].IsObject()) {
292
+ failCallback(completion, "TypeError", "Pynect callback returned an invalid value");
293
+ return;
294
+ }
295
+ completeCallback(completion, jsToSerializedValue(info[0].As<Napi::Object>()));
296
+ } catch (const std::exception& error) {
297
+ failCallback(completion, "TypeError", error.what());
298
+ }
299
+ }
300
+ ));
301
+ request.Set("reject", Napi::Function::New(
302
+ env,
303
+ [completion](const Napi::CallbackInfo& info) {
304
+ std::string name = "Error";
305
+ std::string message = "JavaScript callback failed";
306
+ std::string stack;
307
+ if (info.Length() > 0 && info[0].IsObject()) {
308
+ Napi::Object error = info[0].As<Napi::Object>();
309
+ if (error.Has("name")) name = error.Get("name").ToString().Utf8Value();
310
+ if (error.Has("message")) message = error.Get("message").ToString().Utf8Value();
311
+ if (error.Has("stack")) stack = error.Get("stack").ToString().Utf8Value();
312
+ }
313
+ failCallback(completion, std::move(name), std::move(message), std::move(stack));
314
+ }
315
+ ));
316
+ dispatcher.Call({request});
317
+ } catch (const std::exception& error) {
318
+ failCallback(completion, "Error", error.what());
319
+ }
320
+ delete invocation;
321
+ }
322
+ );
323
+ if (status != napi_ok) {
324
+ delete payload;
325
+ PyErr_SetString(PyExc_RuntimeError, "Unable to schedule the JavaScript callback");
326
+ return nullptr;
327
+ }
328
+
329
+ if (!g_threadPool) {
330
+ failCallback(completion, "RuntimeError", "Pynect thread pool is unavailable");
331
+ } else {
332
+ g_threadPool->pumpUntil(completion->completed);
333
+ }
334
+ if (!completion->completed.load(std::memory_order_acquire)) {
335
+ PyErr_SetString(PyExc_RuntimeError, "Pynect stopped while a callback was running");
336
+ return nullptr;
337
+ }
338
+
339
+ std::lock_guard<std::mutex> lock(completion->mutex);
340
+ if (completion->failed) {
341
+ std::string message = completion->errorName + ": " + completion->errorMessage;
342
+ if (g_includeTracebacks && !completion->errorStack.empty()) {
343
+ message += "\n" + completion->errorStack;
344
+ }
345
+ PyErr_SetString(PyExc_RuntimeError, message.c_str());
346
+ return nullptr;
347
+ }
348
+ return PythonBridge::getInstance().deserializeValue(completion->value);
349
+ }
350
+
351
+ PyObject* createPythonCallback(uint32_t callbackId) {
352
+ if (!callbackState(callbackId)) {
353
+ PyErr_SetString(PyExc_RuntimeError, "Pynect callback is not registered");
354
+ return nullptr;
355
+ }
356
+ auto* storedId = new uint32_t(callbackId);
357
+ PyObject* capsule = PyCapsule_New(storedId, CALLBACK_CAPSULE, destroyCallbackCapsule);
358
+ if (!capsule) {
359
+ delete storedId;
360
+ return nullptr;
361
+ }
362
+ PyRef capsuleRef = PyRef::steal(capsule);
363
+ static PyMethodDef method = {
364
+ "pynect_callback",
365
+ reinterpret_cast<PyCFunction>(invokeJavaScriptCallback),
366
+ METH_VARARGS | METH_KEYWORDS,
367
+ "Invoke a JavaScript callback registered with Pynect."
368
+ };
369
+ return PyCFunction_NewEx(&method, capsule, nullptr);
370
+ }
371
+
372
+ static uint32_t pythonRecursionLimit() {
373
+ GILAcquire gil;
374
+ PyObject* sys = PyImport_ImportModule("sys");
375
+ if (!sys) throw std::runtime_error("Unable to import sys for the recursion limit");
376
+ PyRef sysRef = PyRef::steal(sys);
377
+ PyObject* getter = PyObject_GetAttrString(sys, "getrecursionlimit");
378
+ if (!getter) throw std::runtime_error("sys.getrecursionlimit is unavailable");
379
+ PyRef getterRef = PyRef::steal(getter);
380
+ PyObject* value = PyObject_CallNoArgs(getter);
381
+ if (!value) throw std::runtime_error("Unable to read Python's recursion limit");
382
+ PyRef valueRef = PyRef::steal(value);
383
+ const long limit = PyLong_AsLong(value);
384
+ if (limit <= 0) throw std::runtime_error("Python returned an invalid recursion limit");
385
+ return static_cast<uint32_t>(limit);
386
+ }
387
+
118
388
  // Exported Functions
119
389
 
120
390
  /**
@@ -141,6 +411,12 @@ static Napi::Value InitializePython(const Napi::CallbackInfo& info) {
141
411
  std::string pythonExecutable = opts.Has("pythonExecutable") ?
142
412
  opts.Get("pythonExecutable").As<Napi::String>().Utf8Value() : "python3";
143
413
 
414
+ std::string pythonBaseExecutable = opts.Has("pythonBaseExecutable") ?
415
+ opts.Get("pythonBaseExecutable").As<Napi::String>().Utf8Value() : pythonExecutable;
416
+
417
+ std::string pythonPrefix = opts.Has("pythonPrefix") ?
418
+ opts.Get("pythonPrefix").As<Napi::String>().Utf8Value() : "";
419
+
144
420
  std::string pythonLibrary = opts.Has("pythonLibrary") ?
145
421
  opts.Get("pythonLibrary").As<Napi::String>().Utf8Value() : "";
146
422
 
@@ -150,6 +426,9 @@ static Napi::Value InitializePython(const Napi::CallbackInfo& info) {
150
426
  uint32_t maxQueueSize = opts.Has("maxQueueSize") ?
151
427
  opts.Get("maxQueueSize").As<Napi::Number>().Uint32Value() : 1000;
152
428
 
429
+ uint32_t callbackMaxDepth = opts.Has("callbackMaxDepth") ?
430
+ opts.Get("callbackMaxDepth").As<Napi::Number>().Uint32Value() : 0;
431
+
153
432
  g_includeTracebacks = opts.Has("includeTracebacks") &&
154
433
  opts.Get("includeTracebacks").ToBoolean().Value();
155
434
 
@@ -180,10 +459,13 @@ static Napi::Value InitializePython(const Napi::CallbackInfo& info) {
180
459
  PythonBridge::getInstance().initializePython(
181
460
  pythonHome,
182
461
  pythonExecutable,
462
+ pythonBaseExecutable,
463
+ pythonPrefix,
183
464
  pythonLibrary,
184
465
  pythonPathExtra,
185
466
  envVars
186
467
  );
468
+ g_callbackMaxDepth = callbackMaxDepth > 0 ? callbackMaxDepth : pythonRecursionLimit();
187
469
 
188
470
  // Initialize thread pool
189
471
  g_threadPool = std::make_unique<PythonThreadPool>(threadPoolSize, maxQueueSize);
@@ -217,6 +499,16 @@ static Napi::Value FinalizePython(const Napi::CallbackInfo& info) {
217
499
  "Cannot finalize Python while calls are active or queued"
218
500
  );
219
501
  }
502
+ std::vector<std::shared_ptr<CallbackState>> callbacks;
503
+ {
504
+ std::lock_guard<std::mutex> lock(g_callbacksMutex);
505
+ for (auto& [id, callback] : g_callbacks) {
506
+ callback->active.store(false);
507
+ callbacks.push_back(callback);
508
+ }
509
+ g_callbacks.clear();
510
+ }
511
+ for (auto& callback : callbacks) callback->dispatcher.Release();
220
512
  if (g_threadPool) {
221
513
  g_threadPool->shutdown();
222
514
  g_threadPool.reset();
@@ -239,6 +531,51 @@ static Napi::Value IsInitialized(const Napi::CallbackInfo& info) {
239
531
  return Napi::Boolean::New(info.Env(), g_initialized);
240
532
  }
241
533
 
534
+ static Napi::Value RegisterCallback(const Napi::CallbackInfo& info) {
535
+ Napi::Env env = info.Env();
536
+ if (!g_initialized || info.Length() < 1 || !info[0].IsFunction()) {
537
+ Napi::TypeError::New(env, "registerCallback expects a function on an initialized runtime")
538
+ .ThrowAsJavaScriptException();
539
+ return env.Undefined();
540
+ }
541
+ const uint32_t callbackId = g_nextCallbackId.fetch_add(1, std::memory_order_relaxed);
542
+ Napi::ThreadSafeFunction dispatcher = Napi::ThreadSafeFunction::New(
543
+ env,
544
+ info[0].As<Napi::Function>(),
545
+ "PynectJavaScriptCallback",
546
+ 0,
547
+ 1
548
+ );
549
+ dispatcher.Unref(env);
550
+ auto state = std::make_shared<CallbackState>(std::move(dispatcher));
551
+ {
552
+ std::lock_guard<std::mutex> lock(g_callbacksMutex);
553
+ g_callbacks.emplace(callbackId, state);
554
+ }
555
+ return Napi::Number::New(env, callbackId);
556
+ }
557
+
558
+ static Napi::Value DisposeCallback(const Napi::CallbackInfo& info) {
559
+ Napi::Env env = info.Env();
560
+ if (info.Length() < 1 || !info[0].IsNumber()) {
561
+ Napi::TypeError::New(env, "disposeCallback expects a callback ID")
562
+ .ThrowAsJavaScriptException();
563
+ return env.Undefined();
564
+ }
565
+ const uint32_t callbackId = info[0].As<Napi::Number>().Uint32Value();
566
+ std::shared_ptr<CallbackState> state;
567
+ {
568
+ std::lock_guard<std::mutex> lock(g_callbacksMutex);
569
+ auto found = g_callbacks.find(callbackId);
570
+ if (found != g_callbacks.end()) {
571
+ state = found->second;
572
+ g_callbacks.erase(found);
573
+ }
574
+ }
575
+ if (state && state->active.exchange(false)) state->dispatcher.Release();
576
+ return env.Undefined();
577
+ }
578
+
242
579
  // Generic async wrapper
243
580
 
244
581
  /**
@@ -249,7 +586,8 @@ static Napi::Value SubmitTask(
249
586
  Napi::Env env,
250
587
  std::function<TaskResult()> work,
251
588
  uint32_t timeoutMs = 0,
252
- std::string cancelKey = {}
589
+ std::string cancelKey = {},
590
+ uint32_t callbackDepth = 0
253
591
  ) {
254
592
  auto deferred = std::make_shared<Napi::Promise::Deferred>(
255
593
  Napi::Promise::Deferred::New(env)
@@ -261,8 +599,12 @@ static Napi::Value SubmitTask(
261
599
  }
262
600
 
263
601
  // Capture deferred by shared_ptr to safely use in callback
602
+ auto scopedWork = [work = std::move(work), callbackDepth]() mutable -> TaskResult {
603
+ CallbackDepthScope scope(callbackDepth);
604
+ return work();
605
+ };
264
606
  g_threadPool->submit(
265
- std::move(work),
607
+ std::move(scopedWork),
266
608
  [deferred](TaskResult result) {
267
609
  Napi::Env callEnv = deferred->Env();
268
610
  Napi::HandleScope scope(callEnv);
@@ -473,11 +815,14 @@ static Napi::Value CallFunction(const Napi::CallbackInfo& info) {
473
815
  if (optsObj.Has("requestId") && optsObj.Get("requestId").IsString()) {
474
816
  options.requestId = optsObj.Get("requestId").As<Napi::String>().Utf8Value();
475
817
  }
818
+ if (optsObj.Has("callbackDepth") && optsObj.Get("callbackDepth").IsNumber()) {
819
+ options.callbackDepth = optsObj.Get("callbackDepth").As<Napi::Number>().Uint32Value();
820
+ }
476
821
  }
477
822
 
478
823
  return SubmitTask(env, [objectId, path, options]() -> TaskResult {
479
824
  return PythonBridge::getInstance().callFunctionSync(objectId, path, options);
480
- }, options.timeoutMs, options.requestId);
825
+ }, options.timeoutMs, options.requestId, options.callbackDepth);
481
826
  }
482
827
 
483
828
  /**
@@ -508,11 +853,14 @@ static Napi::Value CallMethod(const Napi::CallbackInfo& info) {
508
853
  if (optsObj.Has("requestId") && optsObj.Get("requestId").IsString()) {
509
854
  options.requestId = optsObj.Get("requestId").As<Napi::String>().Utf8Value();
510
855
  }
856
+ if (optsObj.Has("callbackDepth") && optsObj.Get("callbackDepth").IsNumber()) {
857
+ options.callbackDepth = optsObj.Get("callbackDepth").As<Napi::Number>().Uint32Value();
858
+ }
511
859
  }
512
860
 
513
861
  return SubmitTask(env, [objectId, methodName, options]() -> TaskResult {
514
862
  return PythonBridge::getInstance().callMethodSync(objectId, methodName, options);
515
- }, options.timeoutMs, options.requestId);
863
+ }, options.timeoutMs, options.requestId, options.callbackDepth);
516
864
  }
517
865
 
518
866
  /**
@@ -882,6 +1230,8 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
882
1230
  exports.Set("initializePython", Napi::Function::New(env, InitializePython));
883
1231
  exports.Set("finalizePython", Napi::Function::New(env, FinalizePython));
884
1232
  exports.Set("isInitialized", Napi::Function::New(env, IsInitialized));
1233
+ exports.Set("registerCallback", Napi::Function::New(env, RegisterCallback));
1234
+ exports.Set("disposeCallback", Napi::Function::New(env, DisposeCallback));
885
1235
 
886
1236
  // Module import
887
1237
  exports.Set("importModule", Napi::Function::New(env, ImportModule));