Conversation
kddejong
left a comment
There was a problem hiding this comment.
Review Feedback
1. TOCTOU race in EncryptedFile.put() — first write is unlocked
if (!this.exists()) {
await this.save(); // no lock
return true;
}
const release = await lock(this.file, LOCK_OPTIONS);Two language server processes writing the same region key for the first time will both see exists() === false and both skip the lock. The atomic rename prevents corruption, but proper-lockfile also has a problem on the remove() side — if one process deletes the data file, another process calling lock() on it gets ENOENT. Since the language server can run as multiple concurrent sessions (one per IDE window), this is a realistic scenario during simultaneous startup.
In practice the data is idempotent (same public schemas), so there is no data loss, but the unhandled ENOENT from proper-lockfile could surface as an uncaught exception.
Suggestion: Always create the file before locking, or use a lock file path independent of the data file (e.g., {file}.lk).
2. loadAllKeys() re-scans and re-decrypts everything on every call
loadAllKeys() is called from the constructor, keys(), stats(), and clear(). Since keys() and stats() are called every 60 seconds from the metrics interval, this results in a full readdirSync + N × (lockSync + readFileSync + decrypt) every minute per language server process. The old code just returned Object.keys(this.content) and did a single statSync.
Additionally, recoverKey() unconditionally creates a new EncryptedFile and sets it in the map, replacing any existing instance — discarding the in-memory reference and doing redundant I/O for keys already loaded.
Suggestion: Only scan for new files — skip filenames already mapped in keysToFiles. Or limit the full directory scan to construction time and let keys()/stats() return from the in-memory map only.
Summary
Restructures the
FileStorepersistence layer from a single-file-per-store model to a one-file-per-key model. Each key gets its own encrypted file on disk instead of all keys being serialized into one monolithic JSON blob.Key Semantics
In the context of this language server, a "key" is an AWS region (e.g.,
us-east-1,eu-west-1) most of the time. Each store (such aspublic_schemas) caches resource schemas per region. The number of keys is bounded by the number of AWS regions a user works with — typically single digits, at most a few dozen. This means the per-key file count stays small and hash collisions are effectively impossible.Architecture Change
Old (
EncryptedFileStore){storeName}.enc{ "key1": value1, "key2": value2, ... }{}to disk if the file doesn't existNew (
KeyedFileStore+EncryptedFile){storeName}.{stableHashCode(key)}.encEncryptedEntryenvelope:{ "key": "original-key", "value": <data> }KeyedFileStoremaintains aMap<string, EncryptedFile>mapping logical keys to file handlesEncryptedFileis a new low-level class handling single-file encryption, locking, read, write, and deleteput()— lazy initializationWhat This Enables
Behavioral Differences
loadAllKeys(){}put()clear(){}to single filekeys()