Skip to content

Add Per-Request Response Caching and a Typed Response

Latest

Choose a tag to compare

@krishnavarma1 krishnavarma1 released this 07 Oct 20:48
5617945

Atom already caches responses today, but that behavior is inherited from the selected SessionConfiguration rather than something Atom or the consumer explicitly chooses. .ephemeral gets a private memory cache, .default gets URLCache.shared, and .background gets none. This PR makes that behavior intentional instead of depending on those defaults.

There is also a smaller gap around response metadata. Today resume(expecting:) gives the decoded model while resume() gives the response. Consumers managing their own persistence or freshness may need both from the same request, so this also adds resume(decoding:) to return the model together with its AtomResponse.

Proposed Solution:

Caching now has two explicit parts: the cache itself and whether an individual request is allowed to use it.

ServiceConfiguration accepts a URLCache?, defaulting to nil:

cache: URLCache?

The consumer creates and owns that cache, which also means it can clear it when needed, such as during sign out. Having a cache configured does not automatically enable caching for every endpoint.

Each Requestable separately decides how it can use that cache through ResponseCaching:

public enum ResponseCaching: Equatable, Sendable {
    case disabled
    case revalidatingWithService
    case whileCacheFresh
}

The default is .disabled. Atom translates each case into the corresponding URLRequest.CachePolicy:

Case Policy Fresh entry Heuristic entry Offline with stored entry
.disabled .reloadIgnoringLocalCacheData 2 hits 2 hits fails
.revalidatingWithService .reloadRevalidatingCacheData 2 hits, [200, 304] 2 hits, sends If-Modified-Since fails
.whileCacheFresh .useProtocolCachePolicy 1 hit 1 hit serves stored response

Defaulting to .disabled is intentional. Request deduplication can safely default on because it only combines identical requests that are already in flight. Caching is different because it can return older data, so it needs to be explicitly enabled.

Atom also stamps the cache policy on every request, including .disabled, rather than falling back to the session configuration. Setting a policy directly on the request changes how the session-level policy is handled, so doing this consistently keeps the behavior the same regardless of which SessionConfiguration the consumer uses.

AuthorizedRequestable forwards caching explicitly as well. It forwards protocol members manually, so without this the code would still compile but silently fall back to .disabled.

This also adds resume(decoding:), which returns both the decoded model and the AtomResponse it came from:

DecodedResponse<T>

The response contains both values:

model
response

Both async and completion-handler versions are available. resume(expecting:) now uses resume(decoding:) internally so decoder selection, decoding, AtomError mapping, and the existing Data shortcut continue to live in one place.

Additional Info:

  • .disabled prevents reading from cache, but does not prevent the response from being stored. Because of that, authenticated consumers should use a memory-only cache and clear the cache during sign out. This is documented in the README.
  • .reloadRevalidatingCacheData does work. This was verified even though Apple's documentation suggests otherwise, which is what allows .revalidatingWithService to work.
  • .revalidatingWithService requires the network. It does not fall back to the cached response when offline. That is the same failure behavior .disabled already has.