supastash
Version:
Offline-first sync engine for Supabase in React Native using SQLite
128 lines (127 loc) • 5.09 kB
JavaScript
import { getSupastashConfig } from "../../../core/config";
import { getSupastashDb } from "../../../db/dbInitializer";
import { isOnline } from "../../connection";
import { getTableSchema } from "../../getTableSchema";
import log, { logError, logWarn } from "../../logs";
import { refreshScreen } from "../../refreshScreenCalls";
import { updateLocalSyncedAt } from "../../syncUpdate";
import { pullData } from "./pullData";
import { pullDeletedData } from "./pullDeletedData";
import { stringifyValue } from "./stringifyFields";
let isInSync = new Map();
const DEFAULT_DATE = "1970-01-01T00:00:00Z";
/**
* Updates the local database with the remote changes
* @param table - The table to update
*/
export async function updateLocalDb(table, filters, onReceiveData) {
if (isInSync.get(table))
return;
isInSync.set(table, true);
try {
if (!(await isOnline()))
return;
const db = await getSupastashDb();
const deletedData = await pullDeletedData(table, filters);
const data = await pullData(table, filters);
const refreshNeeded = !!deletedData?.records.length || !!data?.length;
// Delete records that are no longer in the remote data
if (deletedData && deletedData.records.length > 0) {
for (const record of deletedData.records) {
await db.runAsync(`DELETE FROM ${table} WHERE id = ?`, [record.id]);
}
}
// Update local database with remote changes
if (data && data.length > 0) {
for (const record of data) {
if (deletedData?.deletedDataMap.has(record.id))
continue;
const { doesExist, newer } = await checkIfRecordExistsAndIsNewer(table, record);
if (newer) {
if (onReceiveData) {
await onReceiveData(record);
}
else {
await upsertData(table, record, doesExist);
}
}
}
}
if (refreshNeeded)
refreshScreen(table);
}
catch (error) {
if (__DEV__) {
logError(`[Supastash] Error updating local db for ${table}`, error);
}
}
finally {
isInSync.delete(table);
}
}
const warned = new Map();
/**
* Upserts a record into the local database
* @param table - The table to upsert the record into
* @param record - The record to upsert
* @param exists - Whether the record already exists in the database
*/
export async function upsertData(table, record, doesExist) {
if (!record?.id)
return;
let itemExists = !!doesExist;
if (doesExist === undefined) {
const { doesExist: exists } = await checkIfRecordExistsAndIsNewer(table, record);
itemExists = exists;
}
try {
const db = await getSupastashDb();
const columns = await getTableSchema(table);
const recordToSave = {
...record,
synced_at: new Date().toISOString(),
};
if (__DEV__ && getSupastashConfig().debugMode) {
const unknownKeys = Object.keys(record).filter((key) => !columns.includes(key));
if (unknownKeys.length > 0 && !warned.get(table)) {
warned.set(table, true);
logWarn(`⚠️ [Supastash] ${table} record contains keys not in local schema: ${unknownKeys.join(", ")}. Data will still be stored`);
}
}
// Prep for upsert
const keys = columns;
const placeholders = keys.map(() => "?").join(", ");
const updateColumns = keys.filter((key) => key !== "id");
const updateParts = updateColumns.map((key) => `${key} = ?`);
const updatePlaceholders = updateParts.join(", ");
const values = keys.map((key) => stringifyValue(recordToSave[key]));
const updateValues = updateColumns.map((key) => stringifyValue(recordToSave[key]));
if (itemExists) {
// Update existing record
await db.runAsync(`UPDATE ${table} SET ${updatePlaceholders} WHERE id = ?`, [...updateValues, record.id]);
}
else {
// Insert new record
await db.runAsync(`INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`, values);
}
await updateLocalSyncedAt(table, record.id);
}
catch (error) {
logError(`[Supastash] Error upserting data for ${table}`, error);
}
}
async function checkIfRecordExistsAndIsNewer(table, item) {
if (!item?.id)
return { doesExist: false, newer: false };
const db = await getSupastashDb();
const record = await db.getFirstAsync(`SELECT * FROM ${table} WHERE id = ?`, [
item.id,
]);
if (record &&
new Date(record.updated_at || DEFAULT_DATE) >=
new Date(item.updated_at || DEFAULT_DATE)) {
log(`Skipping ${table}:${record.id} - local is newer`);
return { doesExist: true, newer: false };
}
return { doesExist: !!record, newer: true };
}