fix(offline): harden sync catch + add queue/sync test coverage + atomic transactions

This commit is contained in:
Tyler
2026-06-17 14:16:20 -06:00
parent d50ec4deda
commit cc5c0e49be
4 changed files with 234 additions and 46 deletions
+35 -12
View File
@@ -13,8 +13,15 @@ export async function enqueueAction(
): Promise<QueuedAction> {
const db = await getOfflineDB();
const id = options.id ?? uuidv4();
const existing = await db.get("queue", id);
if (existing) return existing;
// Atomic read-then-write inside a single readwrite transaction so two
// concurrent calls with the same explicit `id` can't both pass the
// existence check and write twice.
const tx = db.transaction("queue", "readwrite");
const existing = await tx.store.get(id);
if (existing) {
await tx.done;
return existing;
}
const action: QueuedAction = {
id,
actionName,
@@ -24,7 +31,8 @@ export async function enqueueAction(
lastAttemptAt: null,
status: "pending",
};
await db.put("queue", action);
await tx.store.put(action);
await tx.done;
return action;
}
@@ -49,12 +57,17 @@ export async function dequeueAction(id: string): Promise<QueuedAction | null> {
export async function markInFlight(id: string): Promise<void> {
const db = await getOfflineDB();
const action = await db.get("queue", id);
if (!action) return;
const tx = db.transaction("queue", "readwrite");
const action = await tx.store.get(id);
if (!action) {
await tx.done;
return;
}
action.status = "in-flight";
action.lastAttemptAt = Date.now();
action.attempts += 1;
await db.put("queue", action);
await tx.store.put(action);
await tx.done;
}
export async function markSynced(id: string): Promise<void> {
@@ -64,20 +77,30 @@ export async function markSynced(id: string): Promise<void> {
export async function markConflict(id: string, error: string): Promise<void> {
const db = await getOfflineDB();
const action = await db.get("queue", id);
if (!action) return;
const tx = db.transaction("queue", "readwrite");
const action = await tx.store.get(id);
if (!action) {
await tx.done;
return;
}
action.status = "conflict";
action.error = error;
await db.put("queue", action);
await tx.store.put(action);
await tx.done;
}
export async function markFailed(id: string, error: string): Promise<void> {
const db = await getOfflineDB();
const action = await db.get("queue", id);
if (!action) return;
const tx = db.transaction("queue", "readwrite");
const action = await tx.store.get(id);
if (!action) {
await tx.done;
return;
}
action.status = "failed";
action.error = error;
action.attempts += 1;
action.lastAttemptAt = Date.now();
await db.put("queue", action);
await tx.store.put(action);
await tx.done;
}