---
id: runtime/updates-invalidation
title: Updates & Invalidation
---

Update checks compare per-namespace content hashes against a freshly fetched `catalog.json` — **one ping detects every change**. The hashes cover the compiled `.dat` bytes, so the check works offline and works in dev (the plugin serves the identical compiled `catalog.json` + `.dat` through the dev-server middleware).

## The refresh cycle

```ts
const report = await Stift.pack.checkForUpdates(locale) // StaleNamespaceReport { stale, current }
await Stift.pack.refreshStale(locale) // re-fetch only stale namespaces
```

Production single-ping pattern — one catalog fetch diffed against every persisted locale (per-locale `checkForUpdates` costs one fetch each):

```ts
const reports = await Stift.pack.checkAllForUpdates()
```

Pattern: on load, boot from cache instantly (offline-first), then run `checkAllForUpdates` in the background; if the catalog is newer, `refreshStale` swaps the cached packs and re-renders. Guard with `navigator.onLine` + a cooldown so it only runs while online, not on every mount. Boot sequence: [Offline & Persistence](./offline-persistence.md).

## Honest statuses

Statuses that lie are bugs: `install`/`refreshStale` write `__meta` per namespace (a failed write never corrupts the previous good pack), wipes report `error` instead of fake `idle`, and byte-true `bytesReceived`/`bytesTotal` come from the catalog.

## Freshness budget (optional)

`createStift({ persisterTtlMs })` treats packs older than the budget as stale (`reason: 'ttl-expired'`, still fully usable offline) and refreshes them in the background on `activate`. Content-hash staleness stays the real invalidation signal — TTL is cache hygiene.

## Next

- Bounded memory and preload pins → [Loading Strategies](../configuration/loading-strategies.md)
- Surfacing "update available" via `usePackStatus` → [Setup & Boot Modes](../react/setup.md)
