Skip to content

feat(#2): autoAdapter — localStorage primary, IndexedDB fallback on size/quota#9

Merged
mayrang merged 1 commit into
mainfrom
feat/issue-2-auto-adapter
May 27, 2026
Merged

feat(#2): autoAdapter — localStorage primary, IndexedDB fallback on size/quota#9
mayrang merged 1 commit into
mainfrom
feat/issue-2-auto-adapter

Conversation

@mayrang
Copy link
Copy Markdown
Owner

@mayrang mayrang commented May 27, 2026

Closes #2.

Summary

  • New autoAdapter({ thresholdBytes?, primary?, fallback?, onMigration? }) storage adapter exported from main entry.
  • Writes < thresholdBytes (default 1 MB / UTF-16 code units) go to localStorage; larger writes route the key to IndexedDB and clean up the stale localStorage entry.
  • Recovers from QuotaExceededError by falling back to IndexedDB transparently and reporting reason: 'quota' via onMigration.
  • Per-key serialization queue + clearing barrier ensures concurrent writes / removes / clears never lose data.
  • Reads are primary-first so v0.1 → v0.2 upgrades keep working without explicit migration.

Vetting

7 rounds of code review + adversarial audit dispatched in parallel each round. Real bugs caught before merge:

  • Concurrent same-key writes losing data (data race in unprotected mock pattern)
  • clear() racing with writes started during its own await
  • Two concurrent clear() calls — fast one installs barrier, slow one's late adapter.clear wipes data that chained on the new barrier
  • read() mid-clear returning inconsistent partial-wipe data
  • onMigration firing even when primary.remove failed (telemetry lied)
  • Cryptic TypeError on write(undefined)

Plus tightened quota detection to name+code only (removed loose /quota/i regex), guarded primary === fallback at construction, documented concurrent-read+clear ordering as undefined.

Test plan

  • npm test — 131 tests pass (26 autoAdapter + 105 existing)
  • npx size-limit — 4.7 KB brotli (8 KB gate)
  • npm run typecheck — clean
  • Manual: integrate into example app with a rich-text editor that exceeds 1 MB, observe onMigration callback firing with reason: 'size'

…ize/quota

Closes #2. Wraps localStorage and IndexedDB so users don't have to pre-pick
storage by upfront-unknown size. Small payloads stay in fast synchronous
localStorage; once a key's serialized size crosses the threshold (default
1 MB) the adapter routes that key's writes to IndexedDB and cleans up the
stale localStorage entry. Reads are primary-first so v0.1 → v0.2 upgrades
keep working without explicit migration.

  useFormDraft({
    storage: autoAdapter({
      thresholdBytes: 500_000,
      onMigration: (key, reason) => console.log(`migrated`, key, reason),
    }),
    // ...
  });

Also recovers from QuotaExceededError (other libs filling localStorage) by
falling back to IndexedDB and reporting `reason: 'quota'`.

7 rounds of code review + adversarial audit in parallel each round. Bugs
caught and fixed before merge:

  R1: Two concurrent same-key writes interleaved into "both succeeded /
      read returns null" data loss → per-key serialization queue.
  R2: clear() raced with writes started DURING its own pending-await
      → installed a clearing barrier that runSerialized chains on.
  R3: Two concurrent clear() calls — fast clear2 installs barrier,
      writes chain on it, slow clear1's still-pending adapter.clear()
      then wipes them → clear chains on prev clearing too. Also moved
      pending snapshot to the sync prefix to avoid deadlock (clear
      awaits write awaits clear).
  R4: read() interleaved with clear could see a mid-wipe state →
      read awaits clearing barrier.
  R5: onMigration fired even when primary.remove failed (telemetry
      lied) → only fires on full success.
  R6: write(undefined) threw a cryptic TypeError → guard with a
      formdraft-tagged TypeError.

Plus tightened quota detection to name+code only (no /quota/i regex
false positives), guarded primary===fallback at construction, and
documented concurrent-read+clear ordering as undefined.

26 autoAdapter unit tests, 131 total in the package. Bundle 4.7 KB brotli
(8 KB CI gate).
@mayrang mayrang force-pushed the feat/issue-2-auto-adapter branch from 01a63e5 to 60c3999 Compare May 27, 2026 18:06
@mayrang mayrang merged commit 42c180a into main May 27, 2026
2 checks passed
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.

v0.2: Auto-fallback from localStorage to IndexedDB when size exceeds threshold

1 participant