@smartspectra/node-sdk 3.3.0-rc.7 → 3.3.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 (2) hide show
  1. package/js/ffi.js +37 -16
  2. package/package.json +5 -5
package/js/ffi.js CHANGED
@@ -262,6 +262,17 @@ class Session {
262
262
  }
263
263
 
264
264
  this._registeredThunks = [];
265
+ // Held from a successful start*() until native teardown completes
266
+ // (_teardownNative(), the destroy() path) — NOT released by
267
+ // stop()/stopAsync()/reset(). Those calls can return while a worker
268
+ // thread is still mid-relay of a trailing callback (e.g. the idle
269
+ // status transition after stop()); only session_destroy's joins
270
+ // guarantee no SDK thread can still call our trampolines. koffi's
271
+ // registered thunks do NOT ref the libuv loop, so without this a host
272
+ // whose only pending work is awaiting an SDK-callback-resolved
273
+ // promise drains its loop, Node disposes the isolate, and the next
274
+ // relayed callback segfaults the process (exit 139).
275
+ this._keepAlive = null;
265
276
  this._destroyed = false;
266
277
  // In-flight stopAsync() promise, if any. session_stop runs on a koffi
267
278
  // worker thread; destroy() must wait for it to settle before deleting
@@ -329,6 +340,7 @@ class Session {
329
340
  } : null;
330
341
  const rc = sessionStartFile(this._sess, videoPath, timestampsPath, opts, err);
331
342
  if (rc !== 0) throw makeFfiError(err);
343
+ this._takeKeepAlive();
332
344
  }
333
345
 
334
346
  // options (optional): { deviceIndex, width, height, fps, frameTransform }
@@ -344,6 +356,7 @@ class Session {
344
356
  } : null;
345
357
  const rc = sessionStartCamera(this._sess, opts, err);
346
358
  if (rc !== 0) throw makeFfiError(err);
359
+ this._takeKeepAlive();
347
360
  }
348
361
 
349
362
  startCustom(frameTransform = 0) {
@@ -351,6 +364,7 @@ class Session {
351
364
  const err = {};
352
365
  const rc = sessionStartCustom(this._sess, frameTransform | 0, err);
353
366
  if (rc !== 0) throw makeFfiError(err);
367
+ this._takeKeepAlive();
354
368
  }
355
369
 
356
370
  sendFrame(buffer, width, height, strideBytes, pixelFormat, timestampUs) {
@@ -455,27 +469,21 @@ class Session {
455
469
  // Returns a promise that resolves once native teardown (including any
456
470
  // worker-thread joins inside session_destroy) has completed.
457
471
  _teardownNative() {
458
- if (this._sess === null) return Promise.resolve();
459
- // stop → wait → destroy → unregister. session_destroy joins every
460
- // worker thread SpawnWorker ever started for this session — including
461
- // one that's mid-flight relaying an onError/onStatus callback to JS
462
- // (e.g. an async graph failure). That relay can only complete once
463
- // the JS event loop is free to service it, so running session_destroy
464
- // synchronously here would deadlock: this call IS the JS thread, and
465
- // it would be blocked joining a thread that's blocked waiting on this
466
- // same thread's event loop. Run it on a koffi worker thread instead —
467
- // the same reason stopAsync() runs session_stop off-thread above —
468
- // so the event loop stays free no matter what a worker is mid-relay
469
- // of. _releaseThunks() only runs after the callback fires, matching
470
- // the original synchronous ordering (unregister only after the joins
471
- // that guarantee no SDK thread can still call our trampolines).
472
- sessionStop(this._sess);
473
- sessionWait(this._sess, 0);
472
+ if (this._sess === null) {
473
+ this._releaseKeepAlive();
474
+ return Promise.resolve();
475
+ }
476
+ // session_destroy performs the required best-effort stop and worker
477
+ // joins. Run that entire drain off the JS thread: an SDK worker may be
478
+ // blocked relaying a callback onto this event loop, so synchronously
479
+ // stopping or waiting here would deadlock both threads. Unregister the
480
+ // thunks only after destroy has joined every possible caller.
474
481
  const sess = this._sess;
475
482
  this._sess = null;
476
483
  return new Promise((resolve) => {
477
484
  sessionDestroy.async(sess, () => {
478
485
  this._releaseThunks();
486
+ this._releaseKeepAlive();
479
487
  resolve();
480
488
  });
481
489
  });
@@ -498,6 +506,19 @@ class Session {
498
506
  for (const t of this._registeredThunks) koffi.unregister(t);
499
507
  this._registeredThunks = [];
500
508
  }
509
+
510
+ _takeKeepAlive() {
511
+ if (this._keepAlive !== null) return;
512
+ // Max-int32 ms (~24.8 days): the timer exists only to hold a loop
513
+ // reference; it never meaningfully fires.
514
+ this._keepAlive = setInterval(() => {}, 2 ** 31 - 1);
515
+ }
516
+
517
+ _releaseKeepAlive() {
518
+ if (this._keepAlive === null) return;
519
+ clearInterval(this._keepAlive);
520
+ this._keepAlive = null;
521
+ }
501
522
  }
502
523
 
503
524
  // Wrap a populated smartspectra_error_t into a JS Error with numeric `code`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartspectra/node-sdk",
3
- "version": "3.3.0-rc.7",
3
+ "version": "3.3.0",
4
4
  "description": "Node.js (Electron) FFI binding for SmartSpectra vitals measurement",
5
5
  "author": "Presage Technologies, Inc.",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -70,10 +70,10 @@
70
70
  "dependencies": {
71
71
  "koffi": "^2.10.0",
72
72
  "protobufjs": "^7.6.5",
73
- "@smartspectra/node-sdk-linux-x64": "3.3.0-rc.7",
74
- "@smartspectra/node-sdk-linux-arm64": "3.3.0-rc.7",
75
- "@smartspectra/node-sdk-darwin-arm64": "3.3.0-rc.7",
76
- "@smartspectra/node-sdk-win32-x64": "3.3.0-rc.7"
73
+ "@smartspectra/node-sdk-linux-x64": "3.3.0",
74
+ "@smartspectra/node-sdk-linux-arm64": "3.3.0",
75
+ "@smartspectra/node-sdk-darwin-arm64": "3.3.0",
76
+ "@smartspectra/node-sdk-win32-x64": "3.3.0"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@types/node": "^24.0.0",