Shipmind Labs

If four workers can touch your queue at once, the write that lands last is not necessarily the newest one.

That is how an offline write queue quietly undoes work it had already recorded.

The setup comes from our open-source offlinequeue library. Pending operations get grouped into lanes keyed by entity, and up to four lanes run concurrently by default. Each lane persists twice per attempt: once when it marks a row in-flight with a start timestamp before the request leaves, and once when it records the outcome.

So at any moment several lanes may be holding a snapshot of the whole operation list and asking storage to save it.

Two overlapping saves is all it takes. Lane A reads the list, lane B reads it, B writes, then A writes, and A's older snapshot lands last, erasing an outcome that was already committed. Nothing errors. The queue just forgets.

The fix is not a lock in the adapter. It is a single serialized chain in the queue itself: a private writing promise, initialized to a resolved one. Persisting no longer awaits the save directly. It chains the save onto that promise, assigns the result back, and attaches a catch that swallows the rejection on the chain only, while the caller still gets the unswallowed promise.

Two consequences fall out of that, and both are deliberate.

Each save serializes the queue as it stands when its turn comes, not as it looked when the lane started. The snapshot is taken late, so there is no stale write left to land.

And a failed write still rejects for whoever asked for it, because enqueue and flush should not lose a change silently, but the next save is not held hostage behind a rejection it cannot act on. One failure is one caller's problem, not the end of the write path.

The part worth stealing is what this does to the extension point. Because the chain exists, the storage contract only ever sees one save in flight. An adapter author writing against AsyncStorage, IndexedDB, SQLite or a plain file does not have to reason about reentrancy or add a mutex, and does not have to wonder whether their driver is safe under concurrent calls.

We pushed the hard guarantee up into the library and handed the adapter a boring job. Boring is the feature: every adapter that follows is correct by default instead of correct if its author happened to think about it.

With concurrency, the real design question is usually about placement: where to draw the line so that everything downstream of it can stop caring, rather than how to make the parallel part safe.

If you have had a concurrency guarantee sitting in the wrong layer, you probably found out the way we did, through corrupted state or a contributor's bug report.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com