Shipmind Labs

A failed read is not an empty queue. If your lazy-load path treats it as one, the next write persists a fresh queue over data the user still expects to be there.

This came out of our open-source project offlinequeue, a write queue for apps that keep accepting input while the network is gone. Two details in it decide whether offline work survives.

First: the read is single-flight.

The queue tracks two things, whether it is loaded and whether a read is in progress. The load call checks them in that order. Loaded means return immediately and touch storage not at all. Not loaded means claim the in-progress slot with one read promise, so however many callers arrive before the first read settles, they all await that same promise.

The reason is not efficiency. A second storage read settling on top of the first returns a snapshot taken before the enqueue that happened in between, and that snapshot overwrites the newer one in memory. The user's tap is gone, with no error anywhere.

Second: failure is not cached.

When the read rejects, the rejection travels out to the caller, and the in-progress slot is cleared only if it is still that same promise and the queue is still unloaded. So the queue stays unloaded, and the next caller reads again instead of concluding there was nothing there.

One place assigns operations from storage, flips the loaded flag, and then reclaims attempts whose lease expired. Every public entry point (enqueue, flush, retry, discard, prune) begins by awaiting load. Nothing reaches the persist path until a read has actually succeeded.

The operational consequence is where this gets useful. A flush triggered by a connectivity event has no caller to throw to, because the OS woke us up, not the app. So that path routes failures to an error callback instead of letting them vanish into an unhandled rejection. Any background trigger needs that: a place for errors to land when nobody is awaiting them.

We keep meeting the same shape in payment and delivery systems. The bug is rarely the retry logic. It is the gate in front of it, treating "the read failed" and "there was nothing to read" as the same answer.

Your offline layer draws that line somewhere, and it is worth going to look at where.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com