fix(cache): stop quarantining a healthy database when the native module fails to load - #95
Merged
Merged
Conversation
…le fails to load A native-module load failure (ERR_DLOPEN_FAILED after a Node major upgrade, ERR_UNKNOWN_BUILTIN_MODULE on runtimes without node:sqlite, NODE_MODULE_VERSION mismatch) was caught by the CacheService constructor's corruption handler, which renamed the user's healthy cache.db to cache.db.corrupt-<timestamp> and recreated an empty database. Reproduced with real data loss: a 1MB, 2646-entry cache that passed integrity_check was quarantined when node_modules built for Node 24 ran under Node 22. - classify load failures in CacheService and rethrow without touching the database or its -wal/-shm sidecars; genuine corruption still gets the rename-aside recovery - load the cache behind a warn-once latch (cli/cache-loader.ts): on failure, warn once with an actionable hint and return undefined so translate/write run cacheless with exit code 0 - make the optional cache in TranslationService/WriteService real: no cache means cacheless, instead of falling back to CacheService.getInstance(), which re-triggered the same crash - deepl cache subcommands, which cannot run cacheless, now fail with a ConfigError suggesting a reinstall or matching Node version 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
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.
Summary
src/storage/cache.tsmisclassified a native-module load failure as database corruption and responded by renaming the user's healthycache.dbaside and recreating an empty one. Reproduced with real data loss on 2026-07-27: a 1 MB, 2,646-entry cache that passedintegrity_checkwas quarantined ascache.db.corrupt-<timestamp>whennode_modulesbuilt for Node 24 (NODE_MODULE_VERSION 137) ran under Node 22 (127). This matters for the v2.0.0 release: Homebrew users get Node as a formula dependency, sobrew upgrade nodewould have quarantined a healthy cache on every subsequent invocation. Closes the first gating issue of the v2.0.0 epic (beadssync-8mqh.1).Changes Made
src/utils/native-module-error.ts— classifies load failures by error code (ERR_DLOPEN_FAILED,ERR_UNKNOWN_BUILTIN_MODULE,MODULE_NOT_FOUND,ERR_MODULE_NOT_FOUND) or aNODE_MODULE_VERSIONmessage.CacheServiceconstructor — load failures rethrow without touching the database or its-wal/-shmsidecars; genuine corruption still gets the existing rename-aside recovery.src/cli/cache-loader.ts—createCacheServiceGetter()wraps the dynamic import + construction in a warn-once latch: on failure it emits one actionable warning ("Caching is disabled for this run…", reinstall/Node-version hint) and returnsundefined; the failed import is not retried within the process.TranslationService/WriteServiceno longer fall back toCacheService.getInstance()(which re-triggered the same crash); no cache now means running cacheless.service-factory.tsthreadsCacheService | undefinedthrough.register-cache.ts—deepl cache stats/clear/enable/disablecannot run cacheless, so they fail with aConfigError(exit 7) and a reinstall suggestion instead of a stack trace.docs/TROUBLESHOOTING.md— the NODE_MODULE_VERSION section now describes the degraded-but-working behavior instead of the old corruption symptom.Test Coverage
tests/unit/cache-native-failure.test.ts— per error variant: constructor rethrows, DB bytes and sidecars untouched, no.corrupt-*created, no corruption warning; plus a genuine open failure still renames aside.tests/unit/cache-loader.test.ts— import rejection →undefinedwith exactly one warning; latch does not re-import; construction failure after successful import degrades the same way.tests/unit/translation-service.test.ts/write-service.test.ts— translate/batch/improve succeed with no cache whilecache.enabledis true.tests/unit/register-cache-registration.test.ts— all four cache subcommands report a clear error when the backend resolvesundefined.tests/e2e/cli-cache-degraded.e2e.test.ts— hijacks module resolution forbetter-sqlite3in the CLI subprocess (node:moduleregisterHooks) to simulate the ABI failure end-to-end:translate/writeexit 0 with exactly one warning, the pre-seededcache.dbis byte-identical afterwards,cache statsfails actionably, and a healthy backend still caches silently.Full suite: 235 suites / 5,527 tests green on Node 24.18.0; lint and type-check clean.
Backward Compatibility
✅ Maintained: genuine corruption recovery (rename-aside + recreate) is unchanged, including the double-failure path. Cache hits/misses, TTL, eviction, and all cache subcommand behavior with a working backend are untouched.
✅ Same behavior:
deepl --versionand metadata commands never load the cache (existing lazy-import design, unchanged).❌ Breaking changes: None. One internal semantic change: constructing
TranslationService/WriteServicewithout a cache now runs cacheless instead of implicitly grabbing the singleton — all production call sites always passed a cache explicitly.Benefits
~1 MB.corrupt-*files on every invocation.translateandwrite— the two most important commands, neither of which needs a cache — survive an unavailable cache backend.sync-8mqh.2), and remains the safety net for installs on runtimes withoutnode:sqlite.Size: Medium ✓
~600 lines across 16 files, but the production diff is small and mechanical; most of the change is test coverage for the two failure classifications and the degraded paths.
🤖 Generated with Claude Code