You import a decade of mail, search works, and then you try to scroll back to the beginning. Somewhere around the oldest years the list stops responding, the spinner appears, and the app sits there while the database counts its way through everything you already scrolled past. mailin 2.0 replaces the two pieces of the storage layer that caused this. Here is what was wrong and what changed.
Why scrolling a large email archive got slow
The old way to fetch a page deep in a list is to ask the database for the rows after a numeric offset. The database has no shortcut for "row number half a million": it walks the index from the start, discards everything before that point, and then returns the page you asked for. The cost grows in proportion to how deep you are, which is what O(n) means in practice. Page one is instant; a page thousands of screens down costs as much as reading the whole archive up to that point.
That is why the freeze appeared only at depth. Recent mail was always fine. The problem showed up precisely when you went looking for the old thread you actually needed.
Keyset pagination: a cursor instead of a count
mailin 2.0 uses keyset pagination. Instead of "skip N rows", each page request carries a cursor made of the last row you saw: its date and its message id. The next page is "rows after this date and id", which the database answers with a single index seek. That seek is O(log n): it costs about the same at the front of the archive, in the middle or at the very end. mailin describes jumping to email number 1,000,000 as responding in a heartbeat, and the old offset-scan freeze at depth is gone.
The id in the cursor is not decoration. Many emails share a timestamp, especially when a mailbox was exported in bulk or a mailing list delivered in batches. A cursor based on date alone would either skip messages with the same timestamp or show them twice as you scrolled. Ordering by date and then id makes the pagination stable across timestamp ties: every message appears exactly once, in a consistent order, no matter how you got there.
Year-sharded search
Scrolling was one half of the problem. Search over a large index was the other. In v2, the full-text index, built on SQLite FTS5 with BM25 ranking, is split into one shard per year.
The payoff is that most real queries are time-bounded. If you are looking for a term in a particular year, the query opens exactly one shard and ignores the rest, so it does the same amount of work whether the archive holds three years or thirty. A query across the whole archive fans out to every shard and merges the ranked results. mailin reports measured millisecond-level page latency at million-message scale under this design.
The search operators, filters and ranking are covered in the search post; the sharding sits underneath all of them.
Bounded database handles
One shard per year means one SQLite database per year, and an archive spanning many years could leave many files open at once. mailin caps open SQLite handles at 20. When the cap is reached, the least recently used handle is closed. Eviction is also driven by the memory-pressure signals macOS and iOS send to apps, so handles are released when the system reports that memory is tight rather than only when the count runs out.
This is the same discipline applied to import in v2, where messages are drained in batches of 200 so that peak memory stays flat regardless of file size. The post on very large archives covers that side, and resumable imports covers what happens when an import is interrupted.
Same UI, in-place upgrade
mailin 2.0 is a rewrite of storage, ingest and search underneath the same interface. If you are coming from v1, you do not relearn anything; the message list, search box and tools look the same and simply stop stalling at depth.
The upgrade is free and happens in place. Your existing data stays where it was, in ~/Library/Application Support/com.ecosanskriti.mailin/, and the v1 full-text index is migrated to the sharded layout automatically with no rebuild. Nothing needs to be re-imported. The features overview has the rest of what changed in 2.0.
FAQ
Do I have to re-import my archive after upgrading to 2.0?
No. The upgrade is in place, data stays intact in the same location, and the search index is migrated automatically without a rebuild.
Does keyset pagination change how the list is sorted?
The list is ordered by date with the message id as a tiebreaker. That is what makes scrolling stable when many emails share the same timestamp.
Does sharding by year affect searches across the whole archive?
A query without a date bound fans out to every year shard and merges the results into one ranked list. Time-bounded queries are the ones that benefit most, because they touch a single shard.