UNPKG

supastash

Version:

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

126 lines (125 loc) 4.95 kB
import { getSupastashDb } from "../../db/dbInitializer"; import { localCache } from "../../store/localCache"; import log, { logError, logWarn } from "../logs"; import { buildFilters, sanitizeOrderBy, sanitizeTableName, } from "./liteHelpers"; import { notifySubscribers } from "./snapShot"; const fetchingPromises = new Map(); const versionMap = new Map(); const debounceMap = new Map(); function parseJSONColumns(row) { const parsedRow = { ...row }; for (const key in parsedRow) { const value = parsedRow[key]; if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) { try { parsedRow[key] = JSON.parse(value); } catch { } } } return parsedRow; } function getNewVersion(table) { const queue = versionMap.get(table) ?? []; queue.push(Date.now()); versionMap.set(table, queue); const timeoutMs = queue.length > 30 ? 800 : 500; clearTimeout(debounceMap.get(table)); const timeout = setTimeout(() => { if (queue.length > 10) { logWarn(`[Supastash] Table "${table}" is noisy: ${queue.length} events in ${timeoutMs}ms`); } versionMap.delete(table); notifySubscribers(table); debounceMap.delete(table); }, timeoutMs); debounceMap.set(table, timeout); } const timesFetched = new Map(); /** * Fetches the local data from the database * @param table - The table name to fetch from * @param shouldFetch - Whether to fetch the data * @param limit - Optional limit for rows * @param extraMapKeys - Optional fields to group data by */ export async function fetchLocalData(table, shouldFetch = true, limit = 200, extraMapKeys, daylength, filter, orderBy, orderDesc = true) { if (!shouldFetch || fetchingPromises.has(table)) return null; timesFetched.set(table, (timesFetched.get(table) || 0) + 1); if ((timesFetched.get(table) || 0) > 150) { log(`[Supastash] Fetching data for ${table} (times fetched: ${timesFetched.get(table)})`); timesFetched.delete(table); } const fetchPromise = (async () => { if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) { throw new Error(`Invalid table name: ${table}`); } const day = Number(daylength); const daylengthClause = !isNaN(day) && day > 0 ? `AND datetime(created_at) >= datetime('now', '-${day} days')` : ""; try { const filterClause = await buildFilters(filter ?? [], sanitizeTableName(table)); const db = await getSupastashDb(); const safeOrderBy = sanitizeOrderBy((orderBy ?? "created_at")); const query = ` SELECT * FROM ${sanitizeTableName(table)} WHERE deleted_at IS NULL ${filterClause} ${daylengthClause} ORDER BY ${safeOrderBy} ${orderDesc ? "DESC" : "ASC"} LIMIT ? `; const localData = await db.getAllAsync(query, [limit]); const dataMap = new Map(); const data = []; const groupedBy = {}; for (const item of localData) { if (!item.id) continue; const parsedItem = parseJSONColumns(item); dataMap.set(item.id, parsedItem); data.push(parsedItem); if (extraMapKeys?.length) { for (const key of extraMapKeys) { if (key === "id") { logWarn(`[Supastash] Key 'id' can be accessed directly from the data map (dataMap.get(row.id))`); continue; } if (item[key] == null) { logWarn(`[Supastash] Item ${item.id} has no ${String(key)} field on table:${table}. Check extraMapKeys on useSupastashData`); continue; } const groupVal = item[key]; if (!groupedBy[key]) groupedBy[key] = new Map(); if (!groupedBy[key].has(groupVal)) groupedBy[key].set(groupVal, []); groupedBy[key].get(groupVal)?.push(parsedItem); } } } localCache.set(table, { dataMap, data, groupedBy: groupedBy ?? undefined, }); getNewVersion(table); return { data, dataMap, groupedBy }; } catch (error) { logError(`[Supastash] Error fetching local data for ${table}:`, error); return null; } })(); fetchingPromises.set(table, fetchPromise); try { const result = await fetchPromise; return result; } finally { fetchingPromises.delete(table); } }