Skip to content

fix(cache): correct cache refresh, invalidation and persistence - #108

Merged
diegomvh merged 1 commit into
diegomvh:mainfrom
MrRoundRobin:fix/cache
Oct 9, 2026
Merged

diegomvh merged 1 commit into
diegomvh:mainfrom
MrRoundRobin:fix/cache

Conversation

@MrRoundRobin

Copy link
Copy Markdown
Contributor

Summary

Fixes several problems in the response cache, including stale or wrong data being served, crashes with observe: 'events', and IndexedDB startup races. Adds two cache options, configurable per API and per request. The cache API stays synchronous; custom caches keep working without changes.

New options

Both can be set in the API options and overridden on a single request:

Option Default Effect
ignoreCacheControl false Ignores the server's Cache-Control (no-store, no-cache, max-age). Fetch policies and Vary still apply.
cacheInvalidation 'entity-set' After a successful write, clears cached responses for the affected entity set or singleton. 'api' clears all of that API's responses.
provideODataClient({
  config: { ...TripPinConfig, cache, options: { cacheInvalidation: 'entity-set' } },
});

people.entity('russell').patch(changes, { cacheInvalidation: 'api' });
airports.fetch({ fetchPolicy: 'cache-first', ignoreCacheControl: true, maxAge: 300 });

Persistent caches also gain:

  • ODataIndexedDBCache.ready() – waits until stored entries are loaded and pending writes are done. Optional on the ODataCache interface.
  • An onError constructor option – if storage fails, the cache keeps working in memory only and reports the error here. Without it, the error is raised.

Fixes

Fetch policies

  • network-only and cache-and-network now replace an existing entry with the fresh response; before, the old entry stayed.
  • With observe: 'events', progress events are passed through and only the final successful response is cached.
  • The cache is read when the observable is subscribed, not when it is created. no-cache never touches the cache.
  • Network responses are not held back by cache writes.

Cache keys

  • Keys now include the service root and API name, the response type, withCredentials, ignoreCacheControl, and the Accept, Accept-Language and Prefer headers.
  • Keys are readable strings separated by |.
  • Requests that move query options into a $query body share the plain GET's key.

Server headers

  • Cache-Control parsing handles whitespace, case and quotes. The most restrictive directive wins (no-store, then no-cache, then private, then public); with repeated max-age, the shortest is used.
  • Responses marked no-store or no-cache, or with a Vary on headers not in the key, are not reused. A refresh that returns one of these removes the old entry.

Invalidation

  • Happens only after a successful write, not when the observable is created or when the write fails.
  • A GET that started before a successful write can't put its older data back into the cache. This also holds when several API instances share one cache.
  • In a batch:
    • Successful write responses invalidate the cache even if nobody subscribed to them.
    • JSON batch responses are matched to requests by id.
    • One request's cache error doesn't skip invalidation for later requests.
    • A cache-only miss fails only its own request; the batch is still sent.

Expiry and matching

  • An entry expires exactly at its deadline, so maxAge: 0 is stale right away.
  • Expired entries are removed during normal use, and size() counts only live entries.
  • Invalid maxAge values throw a RangeError.
  • forget() matches scopes and names on separator boundaries (People no longer matches PeopleArchive) and doesn't modify the arrays passed in.

Storage backends

  • Cached responses are copies, and are rebuilt with the current request's resource and parser.
  • Web Storage:
    • Blob and ArrayBuffer responses stay in memory instead of being stored as {}.
    • flush() only clears this cache's own prefix (odata no longer clears odata2).
  • IndexedDB:
    • Writes, forget() and flush() called before loading finishes are applied in order, without bringing back older entries.
    • Failures while opening, blocking, loading, writing or upgrading always end in a settled ready().
    • Binary responses are stored.
  • Corrupt stored entries are deleted and treated as cache misses.
  • After falling back to memory, flush() and forget() still remove stored data.

Behavior changes

  • Cache keys changed format, so existing cached responses won't be found once after upgrading. Entries stored with put() under your own names are unaffected.
  • Invalid maxAge values now throw instead of creating entries with odd lifetimes.
  • Cache errors are no longer swallowed: without onError they reach the request's error path and errorHandler.
  • By default, cached expanded or navigation responses that start from another entity set can stay stale after a write. Use cacheInvalidation: 'api', or clear them yourself.

Tests and docs

  • New cache/integration.spec.ts (API, fetch policies, invalidation, batch) and cache/indexeddb.spec.ts. The existing cache and option specs are extended.
  • Adds fake-indexeddb as a dev dependency.
  • npm test: 856 tests pass. The production build (npm run build) passes.
  • Updated handbook/caching.md and handbook/configuration.md.

- Enhance caching logic in cache.ts and related specs.
- Update caching documentation in caching.md.
- Add new tests for IndexedDB and integration scenarios.
@diegomvh
diegomvh merged commit 750ae80 into diegomvh:main Oct 9, 2026
2 checks passed
@MrRoundRobin
MrRoundRobin deleted the fix/cache branch October 11, 2026 22:14
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.

2 participants