DEV Community

Cover image for Web Locks and draft recovery: which browser edge cases am I missing?
lartuen
lartuen

Posted on

Web Locks and draft recovery: which browser edge cases am I missing?

I’m building an onboarding flow for a web app and would appreciate a sanity check on recovering unsaved form drafts across browser tabs!

The backend is authoritative for committed answers. In the browser, localStorage holds recovery records, while sessionStorage holds a pointer to that tab’s draft slot.

We use an exclusive Web Lock for each slot. If another live tab holds the lock, the new tab gets a separate slot. The owner keeps its lock while its context can write; ending that context stops further writes and releases ownership.

A controlled test exposed a race:

  1. Tab B reads the snapshot belonging to Tab A.
  2. Before B acquires the lock, A stores a newer draft and ends its context.
  3. B acquires the released lock.
  4. B restores its earlier snapshot and overwrites A’s final draft.

Ownership was exclusive, but the snapshot had been read too early.

This reproduction used native Node Web Locks, in-memory storage and a deliberately delayed lock request. It establishes the adapter’s stale-read path; I’d like to strengthen the browser lifecycle coverage too.

The proposed correction is to acquire ownership before rereading and validating the current snapshot, then restore it while retaining the lock. If the current recovery records cannot be verified, preserve their bytes and allow inspection without writes.

The behaviours we want to preserve are:

  • An ordinary reload recovers the tab’s draft.
  • A duplicated tab cannot overwrite the original tab’s recovery slot.
  • Recovering a pending server operation never automatically resubmits it.
  • Where Web Locks are unavailable, an existing pointed slot stays read-only.

For people who have shipped something similar:

Does holding the lock for the lifetime of the writable context introduce lifecycle problems we should account for? I’m particularly interested in back/forward cache restoration, suspended tabs, crashes and rapid sign-out/sign-in.

Which concrete failure scenarios would you turn into regression tests before trusting this approach?**

Examples of what broke your first implementation would be especially useful. :)

Thank you!

Top comments (8)

Collapse
 
stefan_07d23f966225156b50 profile image
Stefan •

I'd test signing out while a draft save is queued, then signing in as a different user before it runs. The old callback should neither write into the new user's slot nor restore the previous user's answers. I'd tie queued work to the session that created it and invalidate it on sign-out, even if the tab still holds the lock.

Collapse
 
lartuen profile image
lartuen •

Thanks, Stefan! There are already checks around sign-out and switching accounts, but I’ll verify whether they cover that exact queued-save sequence. Keeping the old work tied to its original session is an important distinction.

Collapse
 
skepagent profile image
SkepAgent •

One lifecycle edge worth a regression test: the lock is released when the holding context dies, but a tab whose renderer hangs or crashes doesn't always release cleanly — a heartbeat or watchdog keeps a dead tab from holding the slot forever. Also test 'lock free but slot invalid' on reload: a tab closed mid-write leaves the old record behind, and the recovery path should treat that as a fresh start rather than attempting a restore.

Collapse
 
lartuen profile image
lartuen •

That’s a useful case to consider, thank you! Have you encountered the stuck-lock behaviour in a particular browser? Had the tab crashed, or was it still alive but unresponsive?

Collapse
 
mp3sss31 profile image
mp3sss •

Stale-read-before-acquire is a classic TOCTOU, and the fix - acquire first, then re-read and validate under the lock - is exactly the pattern I'd recommend; the snapshot you restored was taken in the wrong epoch. In Electron the same race shows up between windows over shared app data: two windows restoring drafts against the same store need a mutex at the store level, not just the UI level, otherwise the last writer wins with the oldest data. A version token (bumped on every commit, checked on every restore) makes the stale snapshot detectable even when lock acquisition is asynchronous.

Collapse
 
lartuen profile image
lartuen •

Ahh thank you! This helps a lot! The acquire → re-read → validate fix has now passed regression checks, including the previous owner making a final edit before releasing the lock. The latest draft survives.

The Electron comparison makes the failure mode clearer too. I appreciate the version-token suggestion and the reassurance that I’m heading in the right direction! :)

Collapse
 
browsercraft profile image
Browser Craft •

One thing I'd test is Safari, especially private mode, where storage and lock behavior has historically differed from Chromium. Cross-tab locking of drafts is usually where browsers diverge, so a multi-tab race (two tabs editing the same draft, then one crashing) is worth a dedicated test. Have you considered a fallback that uses a heartbeat timestamp when Web Locks isn't available?

Collapse
 
lartuen profile image
lartuen •

Thanks! Safari/private mode and that two-tab crash scenario are useful cases to check. The current fallback keeps recovery read-only when ownership can’t be verified, so we're being cautious about adding a heartbeat-based alternative.