The callsign->grid map died with the process. Every restart began with an empty
Locator column that took an hour of listening to refill, and everything learnt
the day before was thrown away.
internal/gridcache is its own SQLite file in the data directory, not a table in
the settings database: that one sits wherever the operator chose to put it,
often a synchronised folder, and a store that rewrites itself every minute has
no business there. Deleting the file costs a few days of listening and nothing
else.
Callsign is the primary key and the newest report wins — operators move, go
portable, go on expedition, and a stale locator is worse than none for grid
chasing because it reads as a square already worked. Entries not seen in two
years are pruned at open: that is the one way this cache can be actively wrong
rather than merely empty, and it is what bounds a store that would otherwise
only grow.
Only CHANGES are queued. The feeds repeat themselves, so writing every report
would put the whole stream in the batch instead of the news in it. Batches
flush on a timer and on shutdown — a restart is exactly when the cache is worth
the most.
Rotation is switched off while the store is attached: the cap would discard
callsigns the database still holds and the lookup would then miss what we know.
Age in the store is the bound instead.
The cache dropped EVERY entry once it passed 20 000. That was survivable while
the only feed was this station's own WSJT-X decodes, which never reached the
ceiling — it is a cliff for anything larger, and every locator in the cluster
list would disappear at once, periodically, for no reason the operator could
see.
Two generations: when the current map fills it becomes the previous one and a
fresh map takes over; lookups consult both. A rotation therefore costs the
older half and nothing more. It needs no insertion order, no per-entry
timestamp and no bookkeeping on the write path — all of which "evict the
oldest thousand" would require, on a path that runs once per decode.
The cap is 100 000, measured at 82 bytes an entry: 8 MB a generation, 16 MB
for both. 20 000 was chosen when the ceiling was unreachable anyway.
Both maps now go through rememberDecodeGrid / lookupDecodeGrid. Rotation swaps
the map headers, so the read had to be guarded; routing every access through
one accessor is what makes that checkable rather than remembered.