Skip to content

fix: only promisify raw natives so re-evaluation does not stack wrappers#25

Open
erunion wants to merge 2 commits into
masterfrom
resolve-plan-identified-issue
Open

fix: only promisify raw natives so re-evaluation does not stack wrappers#25
erunion wants to merge 2 commits into
masterfrom
resolve-plan-identified-issue

Conversation

@erunion

@erunion erunion commented Jul 15, 2026

Copy link
Copy Markdown
Member

Problem

The native binding is a process-wide singleton, but the generated lib/nodegit.js assigns promisified wrappers onto the shared native classes (_Tree.lookup = promisify(_Tree.lookup), etc.). Anything that evaluates the package's JS more than once in the same process — notably Jest, which gives every test file a fresh module registry inside a long-lived shard worker — stacks another wrapper layer per evaluation. On Node 22, every call through a util.promisify layer whose inner function already returns a Promise emits:

(node:4686) [DEP0174] DeprecationWarning: Calling promisify on a function that returns a Promise is likely a mistake.

In a downstream CI run this produced thousands of warnings per shard (in escalating bursts as workers accumulated layers), drowning out real test failures. Each extra layer also leaks a pending promise per call.

Why a simple "already wrapped" flag isn't enough

Tagging promisify's output and skipping flagged functions does not fix this (verified against a local reproduction — all 156 warnings remained). After the template promisifies a method, the manual extensions replace it with their own plain promise-returning function (e.g. Commit.lookup = lookupWrapper(Commit) in lib/commit.js). The next evaluation promisifies that unflagged extension wrapper — which is why the --trace-deprecation stacks point at lib/utils/lookup_wrapper.js:27.

Fix

Guard the generated promisify helper so it only ever wraps the raw callback-style natives, which stringify as [native code]. Anything else (a promisify wrapper or an extension wrapper installed by a prior evaluation) is already promise-based and is returned untouched:

var promisify = fn => {
  if (typeof fn !== "function" ||
      !Function.prototype.toString.call(fn).includes("[native code]")) {
    return fn;
  }
  return util.promisify(fn);
};

Regression test

test/tests/reload.js runs test/utils/reload_fixture.js in a child process (so the suite's own module cache is untouched). The fixture evaluates lib/nodegit.js twice in one process — fresh JS module registry, shared native binding, exactly the Jest scenario — exercises Repository.open/Commit.lookup including the lookupWrapper path, and fails on any DEP0174 warning. It exits 1 against the old promisify and 0 with this fix.

Verification

  • Double-load reproduction: 156 DEP0174 warnings before → 0 after, with lookups across three evaluations still resolving correctly.
  • npm run lint passes; regenerated lib/nodegit.js matches the template output.
  • Full mocha suite locally: 418 passing, 9 failing — all 9 are pre-existing environmental failures (network/credential-dependent clone/fetch tests, missing test/id_rsa keys, and init.defaultBranch=main vs tests expecting master); none related to this change.

🤖 Generated with Claude Code

erunion and others added 2 commits July 15, 2026 09:03
The native binding is a process-wide singleton, but lib/nodegit.js assigns
promisified wrappers onto its shared classes. Anything that evaluates the JS
more than once per process (e.g. Jest, which gives every test file a fresh
module registry inside a long-lived worker) re-promisified the methods each
time: after the first evaluation the methods are promise-returning wrappers
(util.promisify output, or extension wrappers such as lookupWrapper), and
wrapping those again makes Node 22 emit a DEP0174 deprecation warning on
every call and leaks a pending promise per extra layer.

Guard the generated promisify helper so it only ever wraps the raw
callback-style natives (which stringify as "[native code]") and returns
anything already promise-based untouched. Add a regression test that
evaluates lib/nodegit.js twice in one process and asserts no DEP0174
warnings are emitted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@erunion erunion added the bug Something isn't working label Jul 15, 2026
@erunion
erunion marked this pull request as ready for review July 15, 2026 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants