107 lines
2.8 KiB
TypeScript
107 lines
2.8 KiB
TypeScript
// src/lib/offline/queue.ts
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import { getOfflineDB, type QueuedAction } from "./db";
|
|
|
|
export interface EnqueueOptions {
|
|
id?: string; // supply for idempotency
|
|
}
|
|
|
|
export async function enqueueAction(
|
|
actionName: string,
|
|
payload: unknown,
|
|
options: EnqueueOptions = {}
|
|
): Promise<QueuedAction> {
|
|
const db = await getOfflineDB();
|
|
const id = options.id ?? uuidv4();
|
|
// 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,
|
|
payload,
|
|
createdAt: Date.now(),
|
|
attempts: 0,
|
|
lastAttemptAt: null,
|
|
status: "pending",
|
|
};
|
|
await tx.store.put(action);
|
|
await tx.done;
|
|
return action;
|
|
}
|
|
|
|
export async function getQueuedActions(): Promise<QueuedAction[]> {
|
|
const db = await getOfflineDB();
|
|
const all = await db.getAllFromIndex("queue", "by-created");
|
|
return all.filter((a) => a.status !== "synced");
|
|
}
|
|
|
|
export async function dequeueAction(id: string): Promise<QueuedAction | null> {
|
|
const db = await getOfflineDB();
|
|
const tx = db.transaction("queue", "readwrite");
|
|
const action = await tx.store.get(id);
|
|
if (!action) {
|
|
await tx.done;
|
|
return null;
|
|
}
|
|
await tx.store.delete(id);
|
|
await tx.done;
|
|
return action;
|
|
}
|
|
|
|
export async function markInFlight(id: string): Promise<void> {
|
|
const db = await getOfflineDB();
|
|
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 tx.store.put(action);
|
|
await tx.done;
|
|
}
|
|
|
|
export async function markSynced(id: string): Promise<void> {
|
|
const db = await getOfflineDB();
|
|
await db.delete("queue", id);
|
|
}
|
|
|
|
export async function markConflict(id: string, error: string): Promise<void> {
|
|
const db = await getOfflineDB();
|
|
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 tx.store.put(action);
|
|
await tx.done;
|
|
}
|
|
|
|
export async function markFailed(id: string, error: string): Promise<void> {
|
|
const db = await getOfflineDB();
|
|
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 tx.store.put(action);
|
|
await tx.done;
|
|
}
|