fix: add close() so a file-backed db can be released deterministically - #1
Merged
Merged
Conversation
minigraf 1.2.2 refuses a second handle on a file this process already has open (project-minigraf/minigraf#304): two handles each cache their own header.page_count and corrupt each other. Every other binding already has a way to release a handle on demand -- UniFFI exposes destroy() (Python, Java, Swift), the C API exposes minigraf_close -- but this one exposed only the constructor, inMemory, execute and checkpoint. That left Node with no way to reopen a file at all. JavaScript has no deterministic destructor: letting the handle go out of scope makes it unreachable but frees nothing until V8 next collects, which is arbitrarily later or never. `file-backed roundtrip` scopes its writer in a block and then reopens, which is correct in a refcounted host and impossible here -- it broke on all four platforms as soon as 1.2.2 landed. close() drops the inner Minigraf, running its Drop impl (best-effort checkpoint, then release of the sidecar lock). It is idempotent; execute() and checkpoint() throw "database is closed" afterwards. inner becomes an Option to make that representable. Tests: the roundtrip now closes before reopening -- removing that one call reproduces the CI failure exactly, so it is the fix and not incidental. Adds coverage for reopen-without-close throwing, close being idempotent, and use-after-close throwing. index.js is deliberately left alone: regenerating it locally produces ~200 lines of churn unrelated to this change (node:fs -> fs, optional chaining removed, a stale version string) because the local @napi-rs/cli minor differs from CI's. CI regenerates it during its own build. Refs project-minigraf/minigraf#304 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRGNkzJc5FdfpbzAh4ZZzK
adityamukho
added a commit
to project-minigraf/minigraf
that referenced
this pull request
Aug 10, 2026
No code changes in the core crate since v1.2.2. This release exists so the language bindings have a version to republish on. v1.2.2 made a second handle on an already-open file an error (#304). The UniFFI bindings and the C API could already release a handle on demand -- destroy() and minigraf_close -- but the Node binding could not, and JavaScript has no deterministic destructor, so reopening a .graph file in one process became impossible there. minigraf-node added close() (project-minigraf/minigraf-node#1) and needs a cascade to ship it. Binding versions track the core version, so that has to be driven from here. Doc sync precedes the bump as usual. Test counts are unchanged at 998; only the version stamps moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRGNkzJc5FdfpbzAh4ZZzK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the
Node.js CIfailure onmainafter the v1.2.2 cascade —file-backed roundtripbroke on all four platforms.Why it broke
minigraf 1.2.2 (project-minigraf/minigraf#304) refuses a second handle on a file this process already has open. Two handles each cache their own
header.page_count, allocate pages from it, and bounds-check reads against it, so they diverge and corrupt the file — that was the source of the intermittentPage N out of bounds. Refusing is correct.The problem is that this binding had no way to release a handle. The class exposed only
constructor,inMemory,executeandcheckpoint. Every other binding already has an escape hatch:destroy()minigraf_closeSo the existing test was not doing anything wrong:
Leaving the block makes
dbunreachable but frees nothing — V8 collects whenever it likes, which here is well after the reopen. In Python or Rust scope exit releases the handle; in JavaScript it cannot. The consequence was that Node users could not reopen a.graphfile in one process at all, with no workaround available.For contrast,
minigraf-java's equivalent test passes precisely because it callsdb.destroy()between the two opens.The fix
close()drops the innerMinigraf, running itsDropimpl (best-effort checkpoint, then release of the sidecar.graph.lock).innerbecomes anOptionso that is representable. It is idempotent, andexecute()/checkpoint()throwdatabase is closedafterwards.Tests
file-backed roundtripnow closes before reopening. Removing that single call reproduces the CI failure exactly — verified locally — soclose()is what fixes it rather than something incidental:Three new cases: reopening without close throws, close is idempotent, and use-after-close throws. Full suite 6/6 locally against minigraf 1.2.2 from crates.io;
cargo clippy -D warningsandcargo fmt --checkclean.Note on
index.jsLeft untouched on purpose. Regenerating it locally produces ~200 lines of churn unrelated to this change (
node:fs→fs, optional chaining stripped, a stale1.1.1version string baked into the mismatch check) because my@napi-rs/climinor differs from CI's.index.d.tsis included since its diff is purely the newclose()and doc comments. CI regenerates both during its own build.Follow-up, not in this PR
minigraf@1.2.2is already published to npm — the Release workflow succeeded before CI failed — so the version on the registry has this gap. It needs a1.2.3once this lands.🤖 Generated with Claude Code
https://claude.ai/code/session_01QRGNkzJc5FdfpbzAh4ZZzK