UNPKG

supastash

Version:

Offline-first sync engine for Supabase in React Native using SQLite

240 lines (239 loc) 7.13 kB
import { getSupastashDb } from "../../db/dbInitializer"; import { DEFAULT_SYNC_LOG_ENTRY, syncInfo, syncStatusMap, } from "../../store/syncStatus"; import { supastashEventBus } from "../events/eventBus"; /** * Sets the sync status of a query (row) in a specific table. * Automatically removes the entry if the status is 'success'. * * @param rowId - The unique ID of the row * @param table - The name of the table * @param status - One of: "pending", "success", or "error" */ export function setQueryStatus(rowId, table, status) { if (status === "success") { syncStatusMap.get(table)?.delete(rowId); return; } if (syncStatusMap.has(table)) { syncStatusMap.get(table)?.set(rowId, status); } else { syncStatusMap.set(table, new Map()); syncStatusMap.get(table)?.set(rowId, status); } } /** * Gets the sync status of a table from the database. * * @param table - Table name */ export async function getQueryStatusFromDb(table) { const db = await getSupastashDb(); const tableData = await db.getAllAsync(`SELECT id FROM ${table} WHERE synced_at IS NULL`); let tableMap = syncStatusMap.get(table); if (!tableMap) { tableMap = new Map(); syncStatusMap.set(table, tableMap); } for (const row of tableData) { tableMap.set(row.id, "pending"); } } /** * Gets the aggregate sync status of a specific table. * * @param table - Table name * @returns An object with: * - size: total tracked rows * - errors: number of rows in "error" status * - pending: number of rows in "pending" status * - status: overall table status: "success" | "pending" | "error" */ export function getTableStatus(table) { const tableStatus = syncStatusMap.get(table); const size = tableStatus?.size; const statusArray = Array.from(tableStatus?.values() || []); let errorCount = 0; let pendingCount = 0; statusArray.forEach((status) => { if (status === "error") { errorCount++; } else if (status === "pending") { pendingCount++; } }); let status = "success"; if (pendingCount > 0) { status = "pending"; } else if (errorCount > 0) { status = "error"; } return { size, errors: errorCount, pending: pendingCount, status }; } /** * Gets the global sync status across all tracked tables. * * @returns "error" if any table has errors, * "pending" if any table is syncing, * "synced" if all tables are fully synced */ export function getSupastashStatus() { const tables = Array.from(syncStatusMap.keys()); for (const table of tables) { const tableStatus = getTableStatus(table); if (tableStatus.status === "error") { return "error"; } if (tableStatus.status === "pending") { return "pending"; } } return "synced"; } let storeSyncInfo = structuredClone(syncInfo); function snapshot() { return structuredClone(storeSyncInfo); } function emit() { supastashEventBus.emit("updateSyncInfo", snapshot()); } export const SyncInfoUpdater = { setInProgress: ({ action, type, }) => { const next = structuredClone(storeSyncInfo); next[type].inProgress = action === "start"; storeSyncInfo = next; emit(); }, setTablesCompleted: ({ amount, type, }) => { const next = structuredClone(storeSyncInfo); next[type].tablesCompleted = amount; storeSyncInfo = next; emit(); }, setNumberOfTables: ({ amount, type, }) => { const next = structuredClone(storeSyncInfo); next[type].numberOfTables = amount; storeSyncInfo = next; emit(); }, setCurrentTable: ({ table, type, }) => { const next = structuredClone(storeSyncInfo); next[type].currentTable = { name: table, unsyncedDataCount: 0, unsyncedDeletedCount: 0, }; storeSyncInfo = next; emit(); }, setLastSyncLog: ({ key, value, type, table, }) => { const next = structuredClone(storeSyncInfo); const arr = next[type].lastSyncLog; const row = arr.find((l) => l.table === table); if (!row) { arr.push({ ...DEFAULT_SYNC_LOG_ENTRY, table, [key]: value, }); } else { row[key] = value; } storeSyncInfo = next; emit(); }, setUnsyncedDataCount: ({ amount, type, table, }) => { const next = structuredClone(storeSyncInfo); next[type].currentTable.unsyncedDataCount = amount; SyncInfoUpdater.setLastSyncLog({ type, table, key: "unsyncedDataCount", value: amount, }); storeSyncInfo = next; emit(); }, setUnsyncedDeletedCount: ({ amount, type, table, }) => { const next = structuredClone(storeSyncInfo); next[type].currentTable.unsyncedDeletedCount = amount; SyncInfoUpdater.setLastSyncLog({ type, table, key: "unsyncedDeletedCount", value: amount, }); storeSyncInfo = next; emit(); }, // convenience helpers (optional) markLogStart: ({ type, table }) => SyncInfoUpdater.setLastSyncLog({ type, table, key: "startTime", value: Date.now(), }), markLogSuccess: ({ type, table, }) => { SyncInfoUpdater.setLastSyncLog({ type, table, key: "success", value: true, }); SyncInfoUpdater.setLastSyncLog({ type, table, key: "endTime", value: Date.now(), }); }, markLogError: ({ type, table, lastError, errorCount, rowsFailed = 0, }) => { SyncInfoUpdater.setLastSyncLog({ type, table, key: "success", value: false, }); SyncInfoUpdater.setLastSyncLog({ type, table, key: "lastError", value: lastError, }); SyncInfoUpdater.setLastSyncLog({ type, table, key: "errorCount", value: errorCount, }); SyncInfoUpdater.setLastSyncLog({ type, table, key: "endTime", value: Date.now(), }); SyncInfoUpdater.setLastSyncLog({ type, table, key: "rowsFailed", value: rowsFailed ?? 0, }); }, reset: ({ type }) => { const next = structuredClone(storeSyncInfo); next[type] = { ...next[type], inProgress: false, numberOfTables: 0, tablesCompleted: 0, currentTable: { name: "", unsyncedDataCount: 0, unsyncedDeletedCount: 0 }, lastSyncedAt: Date.now(), }; storeSyncInfo = next; emit(); }, getSnapshot: snapshot, };