Skip to content

Commit 1553944

Browse files
TS: defer FFI callback unregister to avoid DEP0168 at teardown
The in-process cell is green, but teardown emitted repeated DEP0168 "Uncaught Node-API callback exception" warnings. They come from inside koffi, not the SDK's callbacks (the guards added alongside never logged): koffi delivers the native outbound callback from a secondary thread by queuing it onto the event loop, and at teardown one such delivery can still be queued when dispose() calls koffi.unregister synchronously. Unregistering while a queued call is pending makes koffi invoke a freed callback and raise inside its own native code, which no JS try/catch can intercept. Defer the unregister to a setImmediate: the native connection and host are already closed by then, so no new deliveries originate, and a pending delivery fires in libuv's poll phase with the callback still valid (it no-ops since we're disposed) before the check-phase setImmediate frees the slot. The immediate is unref'd so it never keeps the process alive. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 02e407b commit 1553944

1 file changed

Lines changed: 24 additions & 3 deletions

File tree

nodejs/src/ffiRuntimeHost.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,10 +368,31 @@ export class FfiRuntimeHost {
368368
}
369369

370370
private unregisterCallback(): void {
371-
if (this.outboundCallback !== undefined) {
372-
koffi.unregister(this.outboundCallback);
373-
this.outboundCallback = undefined;
371+
if (this.outboundCallback === undefined) {
372+
return;
374373
}
374+
const callback = this.outboundCallback;
375+
this.outboundCallback = undefined;
376+
// Defer the unregister to a later tick instead of unregistering synchronously.
377+
// koffi delivers outbound callbacks from a secondary thread by queuing them onto
378+
// the JS event loop; at teardown one such delivery can still be queued after we
379+
// stop the native side. Unregistering while koffi still has a queued call makes
380+
// koffi invoke a torn-down callback and raise inside its own native code — an
381+
// uncaught Node-API callback exception (DEP0168) that no JS try/catch can catch.
382+
// The native connection/host are already closed by the time this runs (see
383+
// dispose), so no new deliveries originate; a queued delivery fires in libuv's
384+
// poll phase and setImmediate (check phase) runs right after it in the same loop
385+
// iteration, so the pending delivery (a no-op, since we are disposed) drains
386+
// before we free the slot.
387+
const immediate = setImmediate(() => {
388+
try {
389+
koffi.unregister(callback);
390+
} catch {
391+
// Ignore teardown failures.
392+
}
393+
});
394+
// Don't let this housekeeping timer keep the process alive.
395+
immediate.unref?.();
375396
}
376397

377398
/** Closes the FFI connection, shuts down the native host, and releases resources. */

0 commit comments

Comments
 (0)