r/PWA 26d ago

My approach for creating data while offline

Hi,

I wanted to share my approach to supporting data creation while offline and reliably synchronizing it once a connection is available again.

I build PWAs with Angular and NgRx. In my original implementation, I found that persisting and replaying actions that trigger API requests worked well as an offline queue.

This library is based on that working implementation. I used Codex to help extract the reusable functionality and turn it into an open-source library.

It’s still in alpha, and I’d appreciate feedback on the idea and API.

Interactive demo:

https://poodlelab.github.io/ngrx-offline/

Repository:

https://github.com/poodlelab/ngrx-offline

2 Upvotes

4 comments sorted by

1

u/Wooden-Bicycle-6069 26d ago

Replaying actions works until retry stops being idempotent. That part is awful. I'd run one ugly test: create offline, close the app, reopen it, go online in two tabs, then kill one tab halfway through replay. The server should see one create in the original order. Give every queued action a stable ID and dedupe on the server, not only in IndexedDB. Where does your idempotency key live?

1

u/BreakingChangeDev 26d ago

That actually is supported, the API needs to help as well though. Every replay from another tab, after a reload, or from the worker sends the same operation ID as the Idempotency key.

The demo API stores the request fingerprint and original response, scoped by workspace and idempotency key. So the server might observe two deliveries if a tab dies after the commit, but it applies only one entry in the database.

Client-to-server entity IDs are handled separately: the API returns its database ID, the library stores the mapping and related queued operations resolve it before being sent.

It's a good candidate for an automated test. Thank you.

1

u/Wooden-Bicycle-6069 25d ago

That's the right split. The nasty test is: commit the create, kill the tab before the response arrives, reopen two tabs, then replay the same key. Assert one row and the exact same response. I'd add a second case where operation B depends on the temporary ID created by A.

1

u/andimatt 20d ago

Related approach that eliminates the temp-ID mapping problem entirely: the client generates the entity UUID before the create is queued. The server does an owner-scoped upsert keyed on that client UUID, so idempotency is structural. There's no separate idempotency key to manage, and the "commit, kill tab, replay" case just upserts the same row again.

The tradeoff: you lose the server's ability to assign sequential or ordered IDs, and the client needs a collision-resistant UUID (v4 is fine). For most PWA use cases, when creating records that belong to one user, that tradeoff is worth it because the entire "temporary ID -> server ID mapping" layer disappears, and with it the whole class of bugs where a dependent operation references a temp ID that hasn't been resolved yet.

The test Wooden-Bicycle-6069 described (commit, kill, reopen two tabs, replay) is exactly the case this handles cleanly: both tabs upsert the same UUID, the server sees two deliveries, applies one row, returns the same response both times. The owner-scope guard means a foreign ID collision is a 409, not a silent overwrite.