Resilience & Sync
Resilience & performance
Debounce and throttle
// Only the value 500ms after typing stops gets written.
const { value: draft } = useStorage('draft', { defaultValue: '', debounce: 500 })
// Written at most once every 200ms while a slider is dragged, instead of
// waiting for it to stop.
const { value: volume } = useStorage('volume', { defaultValue: 50, throttle: 200 })They're mutually exclusive — if both are set, throttle wins. Whichever is used, a write still pending when the component unmounts is flushed immediately rather than dropped.
Quota-exceeded recovery
On QuotaExceededError, useStorage first sweeps this adapter's own expired-TTL entries (via the same logic as TTLManager.cleanExpired()) and retries once. If that's not enough, opt into evictOnQuota to additionally evict the least-recently-written other keys under the same adapter:
const { value } = useStorage('cache-entry', {
defaultValue: null,
evictOnQuota: { max: 3 }, // evict up to 3 other keys before giving up
})Eviction can only judge the age of plain (unencrypted, uncompressed) envelopes — it leaves encrypted/compressed/signed keys belonging to other useStorage() calls alone, since their age can't be safely inspected without their own keys.
Non-quota write failures (a full disk, a broken adapter) are reported as { type: 'write-failed', key, error } via onError — they're never thrown from inside the internal reactive write, which would otherwise be an unhandled rejection your app never sees.
Undo / redo
const {
value: text,
undo,
redo,
canUndo,
canRedo,
} = useStorage('editor-content', {
defaultValue: '',
history: 20, // keep the last 20 values
})
text.value = 'draft one'
text.value = 'draft two'
undo() // text.value === 'draft one'
redo() // text.value === 'draft two'History lives in memory only — it does not persist across reloads, and isn't itself written to storage. canUndo/canRedo are reactive, so you can disable the corresponding buttons in your UI.
Tab sync
When sync: true, writes to value are broadcast to all other open tabs via BroadcastChannel. Remote updates are applied silently (without writing back to storage). Falls back to window.addEventListener('storage', ...) if BroadcastChannel is unavailable.
const { value: cart } = useStorage('cart', {
defaultValue: [] as CartItem[],
sync: true,
})
// cart.value stays in sync across all tabs automaticallySyncOptions
| Option | Type | Default | Description |
|---|---|---|---|
channel | string | 'vue-storage-kit' | BroadcastChannel name |
leader | boolean | false | Enable leader election — only one tab writes to storage on conflict |
debounce | number | 50 | Milliseconds to debounce outgoing broadcasts |
Leader election uses navigator.locks. The leader tab holds a named lock for its lifetime. When the leader closes, another tab automatically acquires the lock and becomes the new leader. When leader: true, conflicts are resolved as last-write-wins by timestamp — on a tie the leader's version is kept.
const { value: sharedState } = useStorage('shared', {
defaultValue: { count: 0 },
sync: { channel: 'app-sync', leader: true, debounce: 100 },
})Use TabSync directly
import { TabSync } from 'vue-storage-kit/sync'
const sync = new TabSync({ channel: 'custom-channel', leader: true })
await sync.start()
sync.subscribe('my-key', (rawValue) => {
console.log('Received from another tab:', rawValue)
})
sync.broadcast('my-key', JSON.stringify({ count: 1 }), Date.now())
sync.stop()