Offline files and durability

Measure quota and request persistence

Estimate origin usage, handle quota failures, and request persistent storage only when the product can justify it.

By default, everything an origin stores is best-effort. When the disk runs low, the browser picks origins it hasn’t seen in a while and wipes their data. No prompt, no event. Your app wakes up empty one day.

The Storage API gives us two tools: a way to measure usage, and a way to ask the browser not to evict us.

Estimate usage and quota

navigator.storage.estimate() returns how many bytes the origin uses across IndexedDB, Cache Storage, and OPFS, plus the quota the browser allows:

const { usage = 0, quota = 0 } = await navigator.storage.estimate()
const alreadyPersistent = await navigator.storage.persisted()
const persistent = alreadyPersistent || await navigator.storage.persist()

console.log({ usage, quota, persistent })

In Chrome on a desktop with a large disk, that logs something like { usage: 1843200, quota: 299977900032, persistent: false }. Roughly 1.8 MB used, about 300 GB available.

The values are estimates on purpose. Browsers round and pad them so a site can’t fingerprint a user by their exact free disk space. Show them as “about 2 MB used”, never as an exact figure you promise to honor.

Ask for persistence, but only when it’s earned

navigator.storage.persist() asks the browser to mark the origin as persistent, which excludes it from automatic eviction. It resolves to true or false.

Each browser decides differently. Chrome grants it silently for sites the user visits often or has installed. Firefox shows a permission prompt. You can’t force it.

Call it when the data deserves it: the user has created notes, they’re not synced anywhere yet, and losing them would hurt. persisted() tells you whether you already have it, so you don’t ask twice.

Whatever the answer, the app must keep working. Persistence is a request, not a feature you can rely on. If it comes back false, the recovery plan from the last lesson protects the user.

When the quota runs out

Any write to IndexedDB, Cache Storage, or OPFS can fail with a QuotaExceededError. Catch it where you write, and have a plan.

The plan follows the ownership map from module 1. Cached help pages are replaceable, so delete them first. Notes and attachments are the user’s work, so never touch them automatically. Retry the write and, if it still fails, tell the user what they can do: export notes, or delete attachments.

try {
  await db.put('notes', note)
} catch (error) {
  if (error.name === 'QuotaExceededError') {
    await caches.delete('field-notes-help-v1')
    await db.put('notes', note)
  } else {
    throw error
  }
}

Don’t turn an estimate into a promise

The quota you displayed a minute ago can shrink. The user downloads a movie, the OS reclaims space, a browser update changes the policy. Never tell users “you have 4 GB left for notes”. Say what’s used and offer cleanup.

Add a storage status panel showing usage, quota, and whether persistence was granted. Then force a QuotaExceededError by filling the origin with large blobs, and confirm your cleanup deletes the cached help and leaves every note and attachment alone.

Lesson completed