Skip to content

Cache correctness fixes + CacheStore extraction; drop dead code#322

Merged
3lvis merged 7 commits into
masterfrom
drop-dead-code-and-cache-fixes
Jun 18, 2026
Merged

Cache correctness fixes + CacheStore extraction; drop dead code#322
3lvis merged 7 commits into
masterfrom
drop-dead-code-and-cache-fixes

Conversation

@3lvis

@3lvis 3lvis commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Found during a file-by-file review. Each bug fix is red-first tested; the refactor is behavior-preserving (full suite green, warning-free).

Dead code

  • Drop AnyCodable.encode(to:)AnyCodable is Decodable-only, so it satisfied no protocol and had no callers (and was incomplete: threw on the nested collections init(from:) produces).

Bug fixes (red-first, see CacheWriteAndDataBodyRegressionTests)

  • Download orphan cache write. requestData cached the payload under the path-derived key, then the download handlers cached it again under the real cacheName — leaving a stray file no read path uses (and a redundant double-write when no cacheName). Caching is now the handlers' job only.
  • Empty Data on verbs. A verb with T == Data returned an empty Data on success instead of the response body. Returns the body now (the Void overloads discard it, so they're unaffected).
  • .memory read destroyed the disk tier. objectFromCache's .memory branch deleted the on-disk file on every read (since the 2018 caching feature). With warm/cold tiering, that file is the durable copy of an earlier .memoryAndFile write — so once NSCache evicted the warm copy under memory pressure (which iOS does routinely), the entry was lost entirely.
  • .none read purged both tiers. Same class of loss on the download path. objectFromCache is now a pure read at every level — purging is the write path's and clearCache's job.

Refactor

  • Extract the disk-cache subsystem into CacheStore. The two-tier cache, sharded on-disk layout, TTL/expiry, sweep, and clear were scattered across static + instance members on the Networking actor. Now a dedicated non-actor @unchecked Sendable keyed by a resolved resource string. Networking keeps the public surface unchanged (destinationURL/objectFromCache/cacheOrPurge* are thin shims; the injected NSCache is shared by reference so it stays inspectable).

Tests

180 tests green against go-httpbin; build warning-free. New regression tests cover the orphan write, the Data body, and the .memory/.none pure-read contracts.

3lvis added 7 commits June 18, 2026 11:52
AnyCodable conforms only to Decodable, so encode(to:) satisfied no
protocol and had no callers. It was also incomplete (threw on the
nested AnyCodable collections that init(from:) produces).
- requestData wrote the payload under the path-derived key, then the
  download handlers wrote it again under the real cacheName. With a
  cacheName that left a stray file no read path uses; with none it was a
  redundant double write. Caching is now the handlers' job only; drop
  requestData's now-dead cachingLevel parameter.
- A verb with T == Data returned an empty Data on success instead of the
  response body. Return the actual body (the Void overloads discard it,
  so they're unaffected).
objectFromCache's .memory branch deleted the on-disk file on every read
(present since the 2018 caching feature, #227). With the warm/cold
tiering, that file is the durable cold copy of an earlier .memoryAndFile
write — so a .memory read evicted it, and once the warm NSCache tier was
dropped under pressure the entry was lost entirely, forcing a re-fetch.

Make .memory a pure read of the warm tier; purging stays the write
path's and clearCache's job.
objectFromCache's .none branch purged both tiers on read. The verb path
already guards against calling it for .none, but the download path reads
through it unconditionally, so a .none download wiped a disk entry an
earlier .memoryAndFile write had created. Report a miss without touching
either tier; clearCache is the way to evict. objectFromCache is now a
pure read at every level.
The two-tier cache (warm NSCache over the sharded on-disk layout), its
TTL/expiry, sweep, and clear were scattered across static and instance
members on the Networking actor. Move them into a dedicated CacheStore —
a non-actor @unchecked Sendable keyed by a resolved resource string, so
the layout/sharding/sweep logic is cohesive and independently testable.

Networking keeps the public surface unchanged: destinationURL,
objectFromCache, cacheOrPurgeData/Image are thin nonisolated shims that
resolve the cache key (baseURL + path stays the networking layer's job,
via cacheResource) and delegate. The injected NSCache is shared by
reference so it stays inspectable. Behavior-preserving — full suite green.
Cut DI-narration on folderName, a duplicated baseURL-agnostic note on
cacheResource, the regression-test file header (duplicated the per-test
comments), and two test comments that restated the test name/assertion.
Comment-only; full suite green.
The cache-layout, sliding-TTL expiry, pure-read, and clear behaviors are
the store's, but were tested through Networking's internal shims
(objectFromCache/cacheOrPurgeData) — coupling the tests to actor
internals. Move them to CacheStoreTests against CacheStore directly.

Bonus: the .memory/.none pure-read contracts now run as deterministic
unit tests with real NSCache eviction (cache.removeAllObjects), which the
download-based versions couldn't do — and they need no go-httpbin.

destinationURL/CacheExpiry tests stay on Networking (public API / own
type). The internal shims keep their production callers (5/3/2 sites each
+ the path→resource resolution), so they remain — but no test depends on
them now.
@3lvis 3lvis marked this pull request as ready for review June 18, 2026 12:30
@3lvis 3lvis merged commit 7866c21 into master Jun 18, 2026
1 check passed
@3lvis 3lvis deleted the drop-dead-code-and-cache-fixes branch June 18, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant