fix indexdb
This commit is contained in:
parent
9d7058d533
commit
a1451fa534
5 changed files with 411 additions and 271 deletions
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.background": "#221489",
|
||||
"titleBar.activeBackground": "#301DC0",
|
||||
"titleBar.activeForeground": "#F9F9FE"
|
||||
}
|
||||
}
|
|
@ -1,8 +1,12 @@
|
|||
import { Authentication } from '../pocketbase/Authentication';
|
||||
import { DataSyncService } from './DataSyncService';
|
||||
import { DexieService } from './DexieService';
|
||||
import { Collections } from '../../schemas/pocketbase/schema';
|
||||
import { SendLog } from '../pocketbase/SendLog';
|
||||
import { Authentication } from "../pocketbase/Authentication";
|
||||
import { DataSyncService } from "./DataSyncService";
|
||||
import { DexieService } from "./DexieService";
|
||||
import { Collections } from "../../schemas/pocketbase/schema";
|
||||
import { SendLog } from "../pocketbase/SendLog";
|
||||
|
||||
// Check if we're in a browser environment
|
||||
const isBrowser =
|
||||
typeof window !== "undefined" && typeof window.indexedDB !== "undefined";
|
||||
|
||||
/**
|
||||
* Service to handle data synchronization during authentication flows
|
||||
|
@ -27,7 +31,7 @@ export class AuthSyncService {
|
|||
Collections.OFFICERS,
|
||||
Collections.REIMBURSEMENTS,
|
||||
Collections.RECEIPTS,
|
||||
Collections.SPONSORS
|
||||
Collections.SPONSORS,
|
||||
];
|
||||
|
||||
private constructor() {
|
||||
|
@ -36,8 +40,10 @@ export class AuthSyncService {
|
|||
this.dexieService = DexieService.getInstance();
|
||||
this.logger = SendLog.getInstance();
|
||||
|
||||
// Listen for auth state changes
|
||||
this.auth.onAuthStateChange(this.handleAuthStateChange.bind(this));
|
||||
// Listen for auth state changes only in browser
|
||||
if (isBrowser) {
|
||||
this.auth.onAuthStateChange(this.handleAuthStateChange.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,6 +60,8 @@ export class AuthSyncService {
|
|||
* Handle authentication state changes
|
||||
*/
|
||||
private async handleAuthStateChange(isAuthenticated: boolean): Promise<void> {
|
||||
if (!isBrowser) return;
|
||||
|
||||
if (isAuthenticated) {
|
||||
// User just logged in
|
||||
await this.handleLogin();
|
||||
|
@ -67,8 +75,10 @@ export class AuthSyncService {
|
|||
* Handle login by syncing user data
|
||||
*/
|
||||
public async handleLogin(): Promise<boolean> {
|
||||
if (!isBrowser) return true;
|
||||
|
||||
if (this.isSyncing) {
|
||||
console.log('Sync already in progress, queueing login sync');
|
||||
console.log("Sync already in progress, queueing login sync");
|
||||
if (this.syncPromise) {
|
||||
this.syncPromise = this.syncPromise.then(() => this.performLoginSync());
|
||||
} else {
|
||||
|
@ -78,15 +88,19 @@ export class AuthSyncService {
|
|||
}
|
||||
|
||||
this.syncPromise = this.performLoginSync();
|
||||
return this.syncPromise.then(() => Object.keys(this.syncErrors).length === 0);
|
||||
return this.syncPromise.then(
|
||||
() => Object.keys(this.syncErrors).length === 0,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual login sync
|
||||
*/
|
||||
private async performLoginSync(): Promise<void> {
|
||||
if (!isBrowser) return;
|
||||
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
console.log('Not authenticated, skipping login sync');
|
||||
console.log("Not authenticated, skipping login sync");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -94,19 +108,22 @@ export class AuthSyncService {
|
|||
this.syncErrors = {};
|
||||
|
||||
try {
|
||||
console.log('Starting login sync process...');
|
||||
console.log("Starting login sync process...");
|
||||
|
||||
// Display sync notification if in browser environment
|
||||
this.showSyncNotification('Syncing your data...');
|
||||
this.showSyncNotification("Syncing your data...");
|
||||
|
||||
// Sync user-specific data first
|
||||
const userId = this.auth.getUserId();
|
||||
if (userId) {
|
||||
// First sync the current user's data
|
||||
await this.dataSync.syncCollection(Collections.USERS, `id = "${userId}"`);
|
||||
await this.dataSync.syncCollection(
|
||||
Collections.USERS,
|
||||
`id = "${userId}"`,
|
||||
);
|
||||
|
||||
// Log the sync operation
|
||||
console.log('User data synchronized on login');
|
||||
console.log("User data synchronized on login");
|
||||
}
|
||||
|
||||
// Sync all collections in parallel with conflict resolution
|
||||
|
@ -119,32 +136,34 @@ export class AuthSyncService {
|
|||
console.error(`Error syncing ${collection}:`, error);
|
||||
this.syncErrors[collection] = error as Error;
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify sync was successful
|
||||
const syncVerification = await this.verifySyncSuccess();
|
||||
|
||||
if (syncVerification.success) {
|
||||
console.log('Login sync completed successfully');
|
||||
this.showSyncNotification('Data sync complete!', 'success');
|
||||
console.log("Login sync completed successfully");
|
||||
this.showSyncNotification("Data sync complete!", "success");
|
||||
} else {
|
||||
console.warn('Login sync completed with issues:', syncVerification.errors);
|
||||
this.showSyncNotification('Some data could not be synced', 'warning');
|
||||
console.warn(
|
||||
"Login sync completed with issues:",
|
||||
syncVerification.errors,
|
||||
);
|
||||
this.showSyncNotification("Some data could not be synced", "warning");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during login sync:', error);
|
||||
this.showSyncNotification('Failed to sync data', 'error');
|
||||
console.error("Error during login sync:", error);
|
||||
this.showSyncNotification("Failed to sync data", "error");
|
||||
} finally {
|
||||
this.isSyncing = false;
|
||||
|
||||
// Process any queued sync operations
|
||||
if (this.syncQueue.length > 0) {
|
||||
const nextSync = this.syncQueue.shift();
|
||||
if (nextSync === 'login') {
|
||||
if (nextSync === "login") {
|
||||
this.handleLogin();
|
||||
} else if (nextSync === 'logout') {
|
||||
} else if (nextSync === "logout") {
|
||||
this.handleLogout();
|
||||
}
|
||||
}
|
||||
|
@ -155,16 +174,18 @@ export class AuthSyncService {
|
|||
* Handle logout by clearing user data
|
||||
*/
|
||||
public async handleLogout(): Promise<boolean> {
|
||||
if (!isBrowser) return true;
|
||||
|
||||
if (this.isSyncing) {
|
||||
console.log('Sync already in progress, queueing logout cleanup');
|
||||
this.syncQueue.push('logout');
|
||||
console.log("Sync already in progress, queueing logout cleanup");
|
||||
this.syncQueue.push("logout");
|
||||
return true;
|
||||
}
|
||||
|
||||
this.isSyncing = true;
|
||||
|
||||
try {
|
||||
console.log('Starting logout cleanup process...');
|
||||
console.log("Starting logout cleanup process...");
|
||||
|
||||
// Ensure any pending changes are synced before logout
|
||||
await this.syncPendingChanges();
|
||||
|
@ -172,10 +193,10 @@ export class AuthSyncService {
|
|||
// Clear all data from IndexedDB
|
||||
await this.dexieService.clearAllData();
|
||||
|
||||
console.log('Logout cleanup completed successfully');
|
||||
console.log("Logout cleanup completed successfully");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error during logout cleanup:', error);
|
||||
console.error("Error during logout cleanup:", error);
|
||||
return false;
|
||||
} finally {
|
||||
this.isSyncing = false;
|
||||
|
@ -183,9 +204,9 @@ export class AuthSyncService {
|
|||
// Process any queued sync operations
|
||||
if (this.syncQueue.length > 0) {
|
||||
const nextSync = this.syncQueue.shift();
|
||||
if (nextSync === 'login') {
|
||||
if (nextSync === "login") {
|
||||
this.handleLogin();
|
||||
} else if (nextSync === 'logout') {
|
||||
} else if (nextSync === "logout") {
|
||||
this.handleLogout();
|
||||
}
|
||||
}
|
||||
|
@ -196,16 +217,23 @@ export class AuthSyncService {
|
|||
* Sync any pending changes before logout
|
||||
*/
|
||||
private async syncPendingChanges(): Promise<void> {
|
||||
if (!isBrowser) return;
|
||||
|
||||
// This would be implemented if we had offline capabilities
|
||||
// For now, we just log that we would sync pending changes
|
||||
console.log('Checking for pending changes to sync before logout...');
|
||||
console.log("Checking for pending changes to sync before logout...");
|
||||
// In a real implementation, this would sync any offline changes
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that sync was successful by checking data in IndexedDB
|
||||
*/
|
||||
private async verifySyncSuccess(): Promise<{ success: boolean; errors: Record<string, string> }> {
|
||||
private async verifySyncSuccess(): Promise<{
|
||||
success: boolean;
|
||||
errors: Record<string, string>;
|
||||
}> {
|
||||
if (!isBrowser) return { success: true, errors: {} };
|
||||
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
// Check each collection that had errors
|
||||
|
@ -217,30 +245,39 @@ export class AuthSyncService {
|
|||
const userId = this.auth.getUserId();
|
||||
if (userId) {
|
||||
try {
|
||||
const user = await this.dataSync.getItem(Collections.USERS, userId, false);
|
||||
const user = await this.dataSync.getItem(
|
||||
Collections.USERS,
|
||||
userId,
|
||||
false,
|
||||
);
|
||||
if (!user) {
|
||||
errors['user_verification'] = 'User data not found in IndexedDB after sync';
|
||||
errors["user_verification"] =
|
||||
"User data not found in IndexedDB after sync";
|
||||
}
|
||||
} catch (error) {
|
||||
errors['user_verification'] = `Error verifying user data: ${(error as Error).message}`;
|
||||
errors["user_verification"] =
|
||||
`Error verifying user data: ${(error as Error).message}`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: Object.keys(errors).length === 0,
|
||||
errors
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a notification to the user about sync status
|
||||
*/
|
||||
private showSyncNotification(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info'): void {
|
||||
private showSyncNotification(
|
||||
message: string,
|
||||
type: "info" | "success" | "warning" | "error" = "info",
|
||||
): void {
|
||||
// Only run in browser environment
|
||||
if (typeof window === 'undefined') return;
|
||||
if (!isBrowser) return;
|
||||
|
||||
// Check if toast function exists (from react-hot-toast or similar)
|
||||
if (typeof window.toast === 'function') {
|
||||
if (typeof window.toast === "function") {
|
||||
window.toast(message, { type });
|
||||
} else {
|
||||
// Fallback to console
|
||||
|
@ -253,8 +290,8 @@ export class AuthSyncService {
|
|||
*/
|
||||
public async forceSyncAll(): Promise<boolean> {
|
||||
if (this.isSyncing) {
|
||||
console.log('Sync already in progress, queueing full sync');
|
||||
this.syncQueue.push('login'); // Reuse login sync logic
|
||||
console.log("Sync already in progress, queueing full sync");
|
||||
this.syncQueue.push("login"); // Reuse login sync logic
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -279,6 +316,9 @@ export class AuthSyncService {
|
|||
// Add toast type to window for TypeScript
|
||||
declare global {
|
||||
interface Window {
|
||||
toast?: (message: string, options?: { type: 'info' | 'success' | 'warning' | 'error' }) => void;
|
||||
toast?: (
|
||||
message: string,
|
||||
options?: { type: "info" | "success" | "warning" | "error" },
|
||||
) => void;
|
||||
}
|
||||
}
|
|
@ -1,16 +1,20 @@
|
|||
import { DexieService } from './DexieService';
|
||||
import { Get } from '../pocketbase/Get';
|
||||
import { Update } from '../pocketbase/Update';
|
||||
import { Authentication } from '../pocketbase/Authentication';
|
||||
import { Collections, type BaseRecord } from '../../schemas/pocketbase/schema';
|
||||
import type Dexie from 'dexie';
|
||||
import { DexieService } from "./DexieService";
|
||||
import { Get } from "../pocketbase/Get";
|
||||
import { Update } from "../pocketbase/Update";
|
||||
import { Authentication } from "../pocketbase/Authentication";
|
||||
import { Collections, type BaseRecord } from "../../schemas/pocketbase/schema";
|
||||
import type Dexie from "dexie";
|
||||
|
||||
// Check if we're in a browser environment
|
||||
const isBrowser =
|
||||
typeof window !== "undefined" && typeof window.indexedDB !== "undefined";
|
||||
|
||||
// Interface for tracking offline changes
|
||||
interface OfflineChange {
|
||||
id: string;
|
||||
collection: string;
|
||||
recordId: string;
|
||||
operation: 'create' | 'update' | 'delete';
|
||||
operation: "create" | "update" | "delete";
|
||||
data?: any;
|
||||
timestamp: number;
|
||||
synced: boolean;
|
||||
|
@ -33,13 +37,13 @@ export class DataSyncService {
|
|||
this.update = Update.getInstance();
|
||||
this.auth = Authentication.getInstance();
|
||||
|
||||
// Initialize offline changes table
|
||||
this.initOfflineChangesTable();
|
||||
// Initialize offline changes table only in browser
|
||||
if (isBrowser) {
|
||||
this.initOfflineChangesTable();
|
||||
|
||||
// Check for network status
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('online', this.handleOnline.bind(this));
|
||||
window.addEventListener('offline', this.handleOffline.bind(this));
|
||||
// Check for network status
|
||||
window.addEventListener("online", this.handleOnline.bind(this));
|
||||
window.addEventListener("offline", this.handleOffline.bind(this));
|
||||
this.offlineMode = !navigator.onLine;
|
||||
}
|
||||
}
|
||||
|
@ -55,16 +59,21 @@ export class DataSyncService {
|
|||
* Initialize the offline changes table
|
||||
*/
|
||||
private initOfflineChangesTable(): void {
|
||||
if (!isBrowser) return;
|
||||
|
||||
try {
|
||||
const db = this.dexieService.getDB();
|
||||
// Check if the table exists in the schema
|
||||
if ('offlineChanges' in db) {
|
||||
this.offlineChangesTable = db.offlineChanges as Dexie.Table<OfflineChange, string>;
|
||||
if ("offlineChanges" in db) {
|
||||
this.offlineChangesTable = db.offlineChanges as Dexie.Table<
|
||||
OfflineChange,
|
||||
string
|
||||
>;
|
||||
} else {
|
||||
console.warn('Offline changes table not found in schema');
|
||||
console.warn("Offline changes table not found in schema");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing offline changes table:', error);
|
||||
console.error("Error initializing offline changes table:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,7 +81,9 @@ export class DataSyncService {
|
|||
* Handle device coming online
|
||||
*/
|
||||
private async handleOnline(): Promise<void> {
|
||||
console.log('Device is online, syncing pending changes...');
|
||||
if (!isBrowser) return;
|
||||
|
||||
console.log("Device is online, syncing pending changes...");
|
||||
this.offlineMode = false;
|
||||
await this.syncOfflineChanges();
|
||||
}
|
||||
|
@ -81,7 +92,9 @@ export class DataSyncService {
|
|||
* Handle device going offline
|
||||
*/
|
||||
private handleOffline(): void {
|
||||
console.log('Device is offline, enabling offline mode...');
|
||||
if (!isBrowser) return;
|
||||
|
||||
console.log("Device is offline, enabling offline mode...");
|
||||
this.offlineMode = true;
|
||||
}
|
||||
|
||||
|
@ -90,10 +103,16 @@ export class DataSyncService {
|
|||
*/
|
||||
public async syncCollection<T extends BaseRecord>(
|
||||
collection: string,
|
||||
filter: string = '',
|
||||
sort: string = '-created',
|
||||
expand: Record<string, any> | string[] | string = {}
|
||||
filter: string = "",
|
||||
sort: string = "-created",
|
||||
expand: Record<string, any> | string[] | string = {},
|
||||
): Promise<T[]> {
|
||||
// Skip in non-browser environments
|
||||
if (!isBrowser) {
|
||||
console.log(`Skipping sync for ${collection} in non-browser environment`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Prevent multiple syncs of the same collection at the same time
|
||||
if (this.syncInProgress[collection]) {
|
||||
console.log(`Sync already in progress for ${collection}`);
|
||||
|
@ -114,7 +133,7 @@ export class DataSyncService {
|
|||
console.log(`Device is offline, using cached data for ${collection}`);
|
||||
const db = this.dexieService.getDB();
|
||||
const table = this.getTableForCollection(collection);
|
||||
return table ? (await table.toArray() as T[]) : [];
|
||||
return table ? ((await table.toArray()) as T[]) : [];
|
||||
}
|
||||
|
||||
console.log(`Syncing ${collection}...`);
|
||||
|
@ -123,13 +142,13 @@ export class DataSyncService {
|
|||
let normalizedExpand: string[] | undefined;
|
||||
|
||||
if (expand) {
|
||||
if (typeof expand === 'string') {
|
||||
if (typeof expand === "string") {
|
||||
// If expand is a string, convert it to an array
|
||||
normalizedExpand = [expand];
|
||||
} else if (Array.isArray(expand)) {
|
||||
// If expand is already an array, use it as is
|
||||
normalizedExpand = expand;
|
||||
} else if (typeof expand === 'object') {
|
||||
} else if (typeof expand === "object") {
|
||||
// If expand is an object, extract the keys
|
||||
normalizedExpand = Object.keys(expand);
|
||||
}
|
||||
|
@ -137,7 +156,7 @@ export class DataSyncService {
|
|||
|
||||
// Get data from PocketBase
|
||||
const items = await this.get.getAll<T>(collection, filter, sort, {
|
||||
expand: normalizedExpand
|
||||
expand: normalizedExpand,
|
||||
});
|
||||
console.log(`Fetched ${items.length} items from ${collection}`);
|
||||
|
||||
|
@ -152,20 +171,28 @@ export class DataSyncService {
|
|||
|
||||
// Get existing items to handle conflicts
|
||||
const existingItems = await table.toArray();
|
||||
const existingItemsMap = new Map(existingItems.map(item => [item.id, item]));
|
||||
const existingItemsMap = new Map(
|
||||
existingItems.map((item) => [item.id, item]),
|
||||
);
|
||||
|
||||
// Handle conflicts and merge changes
|
||||
const itemsToStore = await Promise.all(items.map(async (item) => {
|
||||
const existingItem = existingItemsMap.get(item.id);
|
||||
const itemsToStore = await Promise.all(
|
||||
items.map(async (item) => {
|
||||
const existingItem = existingItemsMap.get(item.id);
|
||||
|
||||
if (existingItem) {
|
||||
// Check for conflicts (local changes vs server changes)
|
||||
const resolvedItem = await this.resolveConflict(collection, existingItem, item);
|
||||
return resolvedItem;
|
||||
}
|
||||
if (existingItem) {
|
||||
// Check for conflicts (local changes vs server changes)
|
||||
const resolvedItem = await this.resolveConflict(
|
||||
collection,
|
||||
existingItem,
|
||||
item,
|
||||
);
|
||||
return resolvedItem;
|
||||
}
|
||||
|
||||
return item;
|
||||
}));
|
||||
return item;
|
||||
}),
|
||||
);
|
||||
|
||||
// Store in IndexedDB
|
||||
await table.bulkPut(itemsToStore);
|
||||
|
@ -188,20 +215,25 @@ export class DataSyncService {
|
|||
private async resolveConflict<T extends BaseRecord>(
|
||||
collection: string,
|
||||
localItem: T,
|
||||
serverItem: T
|
||||
serverItem: T,
|
||||
): Promise<T> {
|
||||
// Check if there are pending offline changes for this item
|
||||
const pendingChanges = await this.getPendingChangesForRecord(collection, localItem.id);
|
||||
const pendingChanges = await this.getPendingChangesForRecord(
|
||||
collection,
|
||||
localItem.id,
|
||||
);
|
||||
|
||||
if (pendingChanges.length > 0) {
|
||||
console.log(`Found ${pendingChanges.length} pending changes for ${collection}:${localItem.id}`);
|
||||
console.log(
|
||||
`Found ${pendingChanges.length} pending changes for ${collection}:${localItem.id}`,
|
||||
);
|
||||
|
||||
// Server-wins strategy by default, but preserve local changes that haven't been synced
|
||||
const mergedItem = { ...serverItem };
|
||||
|
||||
// Apply pending changes on top of server data
|
||||
for (const change of pendingChanges) {
|
||||
if (change.operation === 'update' && change.data) {
|
||||
if (change.operation === "update" && change.data) {
|
||||
// Apply each field change individually
|
||||
Object.entries(change.data).forEach(([key, value]) => {
|
||||
(mergedItem as any)[key] = value;
|
||||
|
@ -219,17 +251,23 @@ export class DataSyncService {
|
|||
/**
|
||||
* Get pending changes for a specific record
|
||||
*/
|
||||
private async getPendingChangesForRecord(collection: string, recordId: string): Promise<OfflineChange[]> {
|
||||
private async getPendingChangesForRecord(
|
||||
collection: string,
|
||||
recordId: string,
|
||||
): Promise<OfflineChange[]> {
|
||||
if (!this.offlineChangesTable) return [];
|
||||
|
||||
try {
|
||||
return await this.offlineChangesTable
|
||||
.where('collection')
|
||||
.where("collection")
|
||||
.equals(collection)
|
||||
.and(item => item.recordId === recordId && !item.synced)
|
||||
.and((item) => item.recordId === recordId && !item.synced)
|
||||
.toArray();
|
||||
} catch (error) {
|
||||
console.error(`Error getting pending changes for ${collection}:${recordId}:`, error);
|
||||
console.error(
|
||||
`Error getting pending changes for ${collection}:${recordId}:`,
|
||||
error,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -243,26 +281,29 @@ export class DataSyncService {
|
|||
try {
|
||||
// Get all unsynced changes
|
||||
const pendingChanges = await this.offlineChangesTable
|
||||
.where('synced')
|
||||
.where("synced")
|
||||
.equals(0) // Use 0 instead of false for indexable type
|
||||
.toArray();
|
||||
|
||||
if (pendingChanges.length === 0) {
|
||||
console.log('No pending offline changes to sync');
|
||||
console.log("No pending offline changes to sync");
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`Syncing ${pendingChanges.length} offline changes...`);
|
||||
|
||||
// Group changes by collection for more efficient processing
|
||||
const changesByCollection = pendingChanges.reduce((groups, change) => {
|
||||
const key = change.collection;
|
||||
if (!groups[key]) {
|
||||
groups[key] = [];
|
||||
}
|
||||
groups[key].push(change);
|
||||
return groups;
|
||||
}, {} as Record<string, OfflineChange[]>);
|
||||
const changesByCollection = pendingChanges.reduce(
|
||||
(groups, change) => {
|
||||
const key = change.collection;
|
||||
if (!groups[key]) {
|
||||
groups[key] = [];
|
||||
}
|
||||
groups[key].push(change);
|
||||
return groups;
|
||||
},
|
||||
{} as Record<string, OfflineChange[]>,
|
||||
);
|
||||
|
||||
// Process each collection's changes
|
||||
for (const [collection, changes] of Object.entries(changesByCollection)) {
|
||||
|
@ -272,13 +313,17 @@ export class DataSyncService {
|
|||
// Then apply each change
|
||||
for (const change of changes) {
|
||||
try {
|
||||
if (change.operation === 'update' && change.data) {
|
||||
await this.update.updateFields(collection, change.recordId, change.data);
|
||||
if (change.operation === "update" && change.data) {
|
||||
await this.update.updateFields(
|
||||
collection,
|
||||
change.recordId,
|
||||
change.data,
|
||||
);
|
||||
|
||||
// Mark as synced
|
||||
await this.offlineChangesTable.update(change.id, {
|
||||
synced: true,
|
||||
syncAttempts: change.syncAttempts + 1
|
||||
syncAttempts: change.syncAttempts + 1,
|
||||
});
|
||||
}
|
||||
// Add support for create and delete operations as needed
|
||||
|
@ -287,7 +332,7 @@ export class DataSyncService {
|
|||
|
||||
// Increment sync attempts
|
||||
await this.offlineChangesTable.update(change.id, {
|
||||
syncAttempts: change.syncAttempts + 1
|
||||
syncAttempts: change.syncAttempts + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -298,7 +343,7 @@ export class DataSyncService {
|
|||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error syncing offline changes:', error);
|
||||
console.error("Error syncing offline changes:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -309,35 +354,40 @@ export class DataSyncService {
|
|||
public async recordOfflineChange(
|
||||
collection: string,
|
||||
recordId: string,
|
||||
operation: 'create' | 'update' | 'delete',
|
||||
data?: any
|
||||
operation: "create" | "update" | "delete",
|
||||
data?: any,
|
||||
): Promise<string | null> {
|
||||
if (!this.offlineChangesTable) return null;
|
||||
|
||||
try {
|
||||
const change: Omit<OfflineChange, 'id'> = {
|
||||
const change: Omit<OfflineChange, "id"> = {
|
||||
collection,
|
||||
recordId,
|
||||
operation,
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
synced: false,
|
||||
syncAttempts: 0
|
||||
syncAttempts: 0,
|
||||
};
|
||||
|
||||
const id = await this.offlineChangesTable.add(change as OfflineChange);
|
||||
console.log(`Recorded offline change: ${operation} on ${collection}:${recordId}`);
|
||||
console.log(
|
||||
`Recorded offline change: ${operation} on ${collection}:${recordId}`,
|
||||
);
|
||||
|
||||
// Try to sync immediately if we're online
|
||||
if (!this.offlineMode) {
|
||||
this.syncOfflineChanges().catch(err => {
|
||||
console.error('Error syncing after recording change:', err);
|
||||
this.syncOfflineChanges().catch((err) => {
|
||||
console.error("Error syncing after recording change:", err);
|
||||
});
|
||||
}
|
||||
|
||||
return id;
|
||||
} catch (error) {
|
||||
console.error(`Error recording offline change for ${collection}:${recordId}:`, error);
|
||||
console.error(
|
||||
`Error recording offline change for ${collection}:${recordId}:`,
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -348,9 +398,9 @@ export class DataSyncService {
|
|||
public async getData<T extends BaseRecord>(
|
||||
collection: string,
|
||||
forceSync: boolean = false,
|
||||
filter: string = '',
|
||||
sort: string = '-created',
|
||||
expand: Record<string, any> | string[] | string = {}
|
||||
filter: string = "",
|
||||
sort: string = "-created",
|
||||
expand: Record<string, any> | string[] | string = {},
|
||||
): Promise<T[]> {
|
||||
const db = this.dexieService.getDB();
|
||||
const table = this.getTableForCollection(collection);
|
||||
|
@ -365,7 +415,7 @@ export class DataSyncService {
|
|||
const now = Date.now();
|
||||
const syncThreshold = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
if (!this.offlineMode && (forceSync || (now - lastSync > syncThreshold))) {
|
||||
if (!this.offlineMode && (forceSync || now - lastSync > syncThreshold)) {
|
||||
try {
|
||||
await this.syncCollection<T>(collection, filter, sort, expand);
|
||||
} catch (error) {
|
||||
|
@ -382,12 +432,12 @@ export class DataSyncService {
|
|||
// and apply it properly. This is just a basic example.
|
||||
data = data.filter((item: any) => {
|
||||
// Split filter by logical operators
|
||||
const conditions = filter.split(' && ');
|
||||
return conditions.every(condition => {
|
||||
const conditions = filter.split(" && ");
|
||||
return conditions.every((condition) => {
|
||||
// Parse condition (very basic implementation)
|
||||
if (condition.includes('=')) {
|
||||
const [field, value] = condition.split('=');
|
||||
const cleanValue = value.replace(/"/g, '');
|
||||
if (condition.includes("=")) {
|
||||
const [field, value] = condition.split("=");
|
||||
const cleanValue = value.replace(/"/g, "");
|
||||
return item[field] === cleanValue;
|
||||
}
|
||||
return true;
|
||||
|
@ -397,7 +447,7 @@ export class DataSyncService {
|
|||
|
||||
// Apply sort if provided
|
||||
if (sort) {
|
||||
const isDesc = sort.startsWith('-');
|
||||
const isDesc = sort.startsWith("-");
|
||||
const field = isDesc ? sort.substring(1) : sort;
|
||||
|
||||
data.sort((a: any, b: any) => {
|
||||
|
@ -413,7 +463,11 @@ export class DataSyncService {
|
|||
/**
|
||||
* Get a single item by ID
|
||||
*/
|
||||
public async getItem<T extends BaseRecord>(collection: string, id: string, forceSync: boolean = false): Promise<T | undefined> {
|
||||
public async getItem<T extends BaseRecord>(
|
||||
collection: string,
|
||||
id: string,
|
||||
forceSync: boolean = false,
|
||||
): Promise<T | undefined> {
|
||||
const db = this.dexieService.getDB();
|
||||
const table = this.getTableForCollection(collection);
|
||||
|
||||
|
@ -423,7 +477,7 @@ export class DataSyncService {
|
|||
}
|
||||
|
||||
// Try to get from IndexedDB first
|
||||
let item = await table.get(id) as T | undefined;
|
||||
let item = (await table.get(id)) as T | undefined;
|
||||
|
||||
// If not found or force sync, try to get from PocketBase
|
||||
if ((!item || forceSync) && !this.offlineMode) {
|
||||
|
@ -447,7 +501,7 @@ export class DataSyncService {
|
|||
public async updateItem<T extends BaseRecord>(
|
||||
collection: string,
|
||||
id: string,
|
||||
data: Partial<T>
|
||||
data: Partial<T>,
|
||||
): Promise<T | undefined> {
|
||||
const table = this.getTableForCollection(collection);
|
||||
|
||||
|
@ -457,19 +511,23 @@ export class DataSyncService {
|
|||
}
|
||||
|
||||
// Get the current item
|
||||
const currentItem = await table.get(id) as T | undefined;
|
||||
const currentItem = (await table.get(id)) as T | undefined;
|
||||
if (!currentItem) {
|
||||
console.error(`Item ${id} not found in ${collection}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Update the item in IndexedDB
|
||||
const updatedItem = { ...currentItem, ...data, updated: new Date().toISOString() };
|
||||
const updatedItem = {
|
||||
...currentItem,
|
||||
...data,
|
||||
updated: new Date().toISOString(),
|
||||
};
|
||||
await table.put(updatedItem);
|
||||
|
||||
// If offline, record the change for later sync
|
||||
if (this.offlineMode) {
|
||||
await this.recordOfflineChange(collection, id, 'update', data);
|
||||
await this.recordOfflineChange(collection, id, "update", data);
|
||||
return updatedItem;
|
||||
}
|
||||
|
||||
|
@ -481,7 +539,7 @@ export class DataSyncService {
|
|||
console.error(`Error updating ${collection} item ${id}:`, error);
|
||||
|
||||
// Record as offline change to retry later
|
||||
await this.recordOfflineChange(collection, id, 'update', data);
|
||||
await this.recordOfflineChange(collection, id, "update", data);
|
||||
return updatedItem;
|
||||
}
|
||||
}
|
||||
|
@ -503,7 +561,9 @@ export class DataSyncService {
|
|||
/**
|
||||
* Get the appropriate Dexie table for a collection
|
||||
*/
|
||||
private getTableForCollection(collection: string): Dexie.Table<any, string> | null {
|
||||
private getTableForCollection(
|
||||
collection: string,
|
||||
): Dexie.Table<any, string> | null {
|
||||
const db = this.dexieService.getDB();
|
||||
|
||||
switch (collection) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Dexie from 'dexie';
|
||||
import Dexie from "dexie";
|
||||
import type {
|
||||
User,
|
||||
Event,
|
||||
|
@ -7,15 +7,19 @@ import type {
|
|||
Officer,
|
||||
Reimbursement,
|
||||
Receipt,
|
||||
Sponsor
|
||||
} from '../../schemas/pocketbase/schema';
|
||||
Sponsor,
|
||||
} from "../../schemas/pocketbase/schema";
|
||||
|
||||
// Check if we're in a browser environment
|
||||
const isBrowser =
|
||||
typeof window !== "undefined" && typeof window.indexedDB !== "undefined";
|
||||
|
||||
// Interface for tracking offline changes
|
||||
interface OfflineChange {
|
||||
id: string;
|
||||
collection: string;
|
||||
recordId: string;
|
||||
operation: 'create' | 'update' | 'delete';
|
||||
operation: "create" | "update" | "delete";
|
||||
data?: any;
|
||||
timestamp: number;
|
||||
synced: boolean;
|
||||
|
@ -34,34 +38,44 @@ export class DashboardDatabase extends Dexie {
|
|||
offlineChanges!: Dexie.Table<OfflineChange, string>;
|
||||
|
||||
// Store last sync timestamps
|
||||
syncInfo!: Dexie.Table<{id: string, collection: string, lastSync: number}, string>;
|
||||
syncInfo!: Dexie.Table<
|
||||
{ id: string; collection: string; lastSync: number },
|
||||
string
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
super('IEEEDashboardDB');
|
||||
super("IEEEDashboardDB");
|
||||
|
||||
this.version(1).stores({
|
||||
users: 'id, email, name',
|
||||
events: 'id, event_name, event_code, start_date, end_date, published',
|
||||
eventRequests: 'id, name, status, requested_user, created, updated',
|
||||
logs: 'id, user, type, created',
|
||||
officers: 'id, user, role, type',
|
||||
reimbursements: 'id, title, status, submitted_by, created',
|
||||
receipts: 'id, created_by, date',
|
||||
sponsors: 'id, user, company',
|
||||
syncInfo: 'id, collection, lastSync'
|
||||
users: "id, email, name",
|
||||
events: "id, event_name, event_code, start_date, end_date, published",
|
||||
eventRequests: "id, name, status, requested_user, created, updated",
|
||||
logs: "id, user, type, created",
|
||||
officers: "id, user, role, type",
|
||||
reimbursements: "id, title, status, submitted_by, created",
|
||||
receipts: "id, created_by, date",
|
||||
sponsors: "id, user, company",
|
||||
syncInfo: "id, collection, lastSync",
|
||||
});
|
||||
|
||||
// Add version 2 with offlineChanges table
|
||||
this.version(2).stores({
|
||||
offlineChanges: 'id, collection, recordId, operation, timestamp, synced, syncAttempts'
|
||||
offlineChanges:
|
||||
"id, collection, recordId, operation, timestamp, synced, syncAttempts",
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize the database with default values
|
||||
async initialize() {
|
||||
const collections = [
|
||||
'users', 'events', 'event_request', 'logs',
|
||||
'officers', 'reimbursement', 'receipts', 'sponsors'
|
||||
"users",
|
||||
"events",
|
||||
"event_request",
|
||||
"logs",
|
||||
"officers",
|
||||
"reimbursement",
|
||||
"receipts",
|
||||
"sponsors",
|
||||
];
|
||||
|
||||
for (const collection of collections) {
|
||||
|
@ -70,21 +84,36 @@ export class DashboardDatabase extends Dexie {
|
|||
await this.syncInfo.put({
|
||||
id: collection,
|
||||
collection,
|
||||
lastSync: 0
|
||||
lastSync: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock database for server-side rendering
|
||||
class MockDashboardDatabase {
|
||||
// Implement empty methods that won't fail during SSR
|
||||
async initialize() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton pattern
|
||||
export class DexieService {
|
||||
private static instance: DexieService;
|
||||
private db: DashboardDatabase;
|
||||
private db: DashboardDatabase | MockDashboardDatabase;
|
||||
|
||||
private constructor() {
|
||||
this.db = new DashboardDatabase();
|
||||
this.db.initialize();
|
||||
if (isBrowser) {
|
||||
// Only initialize Dexie in browser environments
|
||||
this.db = new DashboardDatabase();
|
||||
this.db.initialize();
|
||||
} else {
|
||||
// Use a mock database in non-browser environments
|
||||
console.log("Running in Node.js environment, using mock database");
|
||||
this.db = new MockDashboardDatabase() as any;
|
||||
}
|
||||
}
|
||||
|
||||
public static getInstance(): DexieService {
|
||||
|
@ -96,40 +125,58 @@ export class DexieService {
|
|||
|
||||
// Get the database instance
|
||||
public getDB(): DashboardDatabase {
|
||||
return this.db;
|
||||
if (!isBrowser) {
|
||||
console.warn(
|
||||
"Attempting to access IndexedDB in a non-browser environment",
|
||||
);
|
||||
}
|
||||
return this.db as DashboardDatabase;
|
||||
}
|
||||
|
||||
// Update the last sync timestamp for a collection
|
||||
public async updateLastSync(collection: string): Promise<void> {
|
||||
await this.db.syncInfo.update(collection, { lastSync: Date.now() });
|
||||
if (!isBrowser) return;
|
||||
await (this.db as DashboardDatabase).syncInfo.update(collection, {
|
||||
lastSync: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Get the last sync timestamp for a collection
|
||||
public async getLastSync(collection: string): Promise<number> {
|
||||
const info = await this.db.syncInfo.get(collection);
|
||||
if (!isBrowser) return 0;
|
||||
const info = await (this.db as DashboardDatabase).syncInfo.get(collection);
|
||||
return info?.lastSync || 0;
|
||||
}
|
||||
|
||||
// Clear all data (useful for logout)
|
||||
public async clearAllData(): Promise<void> {
|
||||
await this.db.users.clear();
|
||||
await this.db.events.clear();
|
||||
await this.db.eventRequests.clear();
|
||||
await this.db.logs.clear();
|
||||
await this.db.officers.clear();
|
||||
await this.db.reimbursements.clear();
|
||||
await this.db.receipts.clear();
|
||||
await this.db.sponsors.clear();
|
||||
await this.db.offlineChanges.clear();
|
||||
if (!isBrowser) return;
|
||||
|
||||
const db = this.db as DashboardDatabase;
|
||||
await db.users.clear();
|
||||
await db.events.clear();
|
||||
await db.eventRequests.clear();
|
||||
await db.logs.clear();
|
||||
await db.officers.clear();
|
||||
await db.reimbursements.clear();
|
||||
await db.receipts.clear();
|
||||
await db.sponsors.clear();
|
||||
await db.offlineChanges.clear();
|
||||
|
||||
// Reset sync timestamps
|
||||
const collections = [
|
||||
'users', 'events', 'event_request', 'logs',
|
||||
'officers', 'reimbursement', 'receipts', 'sponsors'
|
||||
"users",
|
||||
"events",
|
||||
"event_request",
|
||||
"logs",
|
||||
"officers",
|
||||
"reimbursement",
|
||||
"receipts",
|
||||
"sponsors",
|
||||
];
|
||||
|
||||
for (const collection of collections) {
|
||||
await this.db.syncInfo.update(collection, { lastSync: 0 });
|
||||
await db.syncInfo.update(collection, { lastSync: 0 });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -143,7 +143,7 @@ export class Get {
|
|||
if (options?.expand) {
|
||||
if (Array.isArray(options.expand)) {
|
||||
expandString = options.expand.join(",");
|
||||
} else if (typeof options.expand === 'string') {
|
||||
} else if (typeof options.expand === "string") {
|
||||
expandString = options.expand;
|
||||
}
|
||||
}
|
||||
|
@ -196,7 +196,7 @@ export class Get {
|
|||
if (options?.expand) {
|
||||
if (Array.isArray(options.expand)) {
|
||||
expandString = options.expand.join(",");
|
||||
} else if (typeof options.expand === 'string') {
|
||||
} else if (typeof options.expand === "string") {
|
||||
expandString = options.expand;
|
||||
}
|
||||
}
|
||||
|
@ -306,7 +306,7 @@ export class Get {
|
|||
if (options?.expand) {
|
||||
if (Array.isArray(options.expand)) {
|
||||
expandString = options.expand.join(",");
|
||||
} else if (typeof options.expand === 'string') {
|
||||
} else if (typeof options.expand === "string") {
|
||||
expandString = options.expand;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue