You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 2398c76
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cache): enforce the size cap from the DB and keep healthy DBs on non-corruption errors (#107)
Size accounting lived in a process-local counter that drifted in both
directions and went negative under concurrent processes, so the cap
stopped being enforced; eviction under-freed on skewed sizes; an entry
larger than maxSize wiped the cache; the expired sweep never ran in
sub-60s processes; and getInstance/close cycles accumulated signal
listeners. The total is now read from SUM(size), eviction loops until
space is actually freed, oversized entries are skipped, the sweep runs
on first use, and handlers register once per process.
The constructor's corruption recovery is now allowlisted to
SQLITE_CORRUPT/SQLITE_NOTADB: SQLITE_BUSY waits via busy_timeout and
then propagates (previously it renamed a healthy cache aside, emptied
it, and killed the other process's transaction), a newer-schema DB is
refused instead of destroyed, backup WAL sidecars are preserved before
close under names SQLite can resolve, and rename-aside backups are
pruned to the most recent three.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -34,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
34
34
35
35
### Fixed
36
36
37
+
- **cache**: The size cap is now actually enforced. Size accounting previously lived in a process-local counter that drifted in both directions — downward when an overwrite's eviction deleted the very key being replaced (after which the cap stopped firing and the DB grew without bound), upward when `get()` deleted expired rows without decrementing (evicting entries that didn't need evicting), and negative when a concurrent process's rows were swept (disabling eviction for the process lifetime). The total is now always read from `SELECT SUM(size)`. Eviction now deletes oldest rows in batches until enough space is actually freed, instead of a one-shot estimate from the average row size that under-evicted whenever sizes were skewed. An entry larger than `maxSize` itself is skipped instead of stored — previously it wiped every other entry first (eviction deleted all rows and inserted the oversized one anyway), leaving a cache that re-wiped on every subsequent write. The expired-entry sweep's throttle timer is seeded so the sweep runs on a process's first operation; it was seeded to construction time, so it never ran in any process shorter than 60 seconds — essentially every CLI invocation. Repeated `getInstance()`/`close()` cycles no longer accumulate process signal listeners until Node prints `MaxListenersExceededWarning` to stderr. Docs now describe eviction as oldest-first rather than LRU, which is what the code has always done (`timestamp` is only written on `set()`).
38
+
- **cache**: Only genuine corruption (`SQLITE_CORRUPT` / `SQLITE_NOTADB`) now triggers the rename-aside-and-recreate recovery. The constructor previously treated *every* unexpected error as corruption, so transient lock contention (`SQLITE_BUSY` — e.g. two concurrent invocations against a cache on a filesystem where WAL cannot be enabled) renamed a healthy cache aside, recreated it empty, and broke the other process's open transaction; and a DB written by a newer CLI version was destroyed instead of refused, defeating the schema check's stated purpose. Lock waits now go through `PRAGMA busy_timeout` (5s), and everything that isn't corruption propagates so the CLI degrades to an uncached run with a warning. When recovery does run, the backup's WAL/SHM sidecars are copied before the failed handle is closed (SQLite deletes them on close) and named `<backup>-wal` / `<backup>-shm` so SQLite can actually recover the preserved data — the old naming (`<original>-wal<suffix>`) orphaned the WAL, silently losing any rows not yet checkpointed. Rename-aside backups are pruned to the most recent three so repeated corruption cannot fill the disk.
37
39
-**sync**: `deepl sync pull` no longer discards existing translations for keys named after `Object.prototype` members. `sanitizePullKeysResponse` built its result on a plain object, and callers test membership with `pulledKeys[key] !== undefined` / `??` — so for a source key called `toString`, `constructor`, `valueOf`, `hasOwnProperty`, or `__proto__`, the lookup returned an inherited *function*, which is neither `undefined` nor nullish. The entry was therefore treated as a freshly approved TMS translation and won over the real one, then vanished from the file when `JSON.stringify` dropped the function value — while the lockfile recorded it as `translated` / `human_reviewed`, so no later sync repaired it. The accumulator now has a null prototype.
38
40
- **sync**: ICU plural/select messages with text around the block are now preserved. The detection pattern was anchored to the start of the string, so `You have {count, plural, one {# item} other {# items}} in your cart.` — arguably the most common real shape — was not recognised as ICU at all and the raw syntax went to the engine as prose. Confirmed against the live API: it returned `{count, Plural, ein {…} weiteres {…}}`, translating the format keyword *and* both selectors, leaving a message with no valid format type and no `other` fallback. Detection now matches an ICU block anywhere in the string, and the prose on either side becomes a translatable segment instead of being silently dropped from the reassembled output (trailing text was previously lost outright). Re-verified end to end after the fix: `plural`, `one`, `other`, and the `count` argument all survive while the surrounding prose is translated. Note this affects `deepl sync`, the only path with ICU preservation; `deepl translate` on a raw ICU string is unchanged.
39
41
-**sync**: ICU structural damage is now detected instead of reported as passing. `checkIcuBrackets` compared only brace counts and nesting depth, which are identical when the engine translates the keyword, a selector, or the argument name — so `fail_on_error` never tripped on a message that no longer renders. Validation now compares the parsed argument/format-type headers and the selector keyword sets, while tolerating reordered selectors (order is not meaningful in ICU).
0 commit comments