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 { Authentication } from "../pocketbase/Authentication";
|
||||||
import { DataSyncService } from './DataSyncService';
|
import { DataSyncService } from "./DataSyncService";
|
||||||
import { DexieService } from './DexieService';
|
import { DexieService } from "./DexieService";
|
||||||
import { Collections } from '../../schemas/pocketbase/schema';
|
import { Collections } from "../../schemas/pocketbase/schema";
|
||||||
import { SendLog } from '../pocketbase/SendLog';
|
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
|
* Service to handle data synchronization during authentication flows
|
||||||
|
@ -27,7 +31,7 @@ export class AuthSyncService {
|
||||||
Collections.OFFICERS,
|
Collections.OFFICERS,
|
||||||
Collections.REIMBURSEMENTS,
|
Collections.REIMBURSEMENTS,
|
||||||
Collections.RECEIPTS,
|
Collections.RECEIPTS,
|
||||||
Collections.SPONSORS
|
Collections.SPONSORS,
|
||||||
];
|
];
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
|
@ -36,8 +40,10 @@ export class AuthSyncService {
|
||||||
this.dexieService = DexieService.getInstance();
|
this.dexieService = DexieService.getInstance();
|
||||||
this.logger = SendLog.getInstance();
|
this.logger = SendLog.getInstance();
|
||||||
|
|
||||||
// Listen for auth state changes
|
// Listen for auth state changes only in browser
|
||||||
this.auth.onAuthStateChange(this.handleAuthStateChange.bind(this));
|
if (isBrowser) {
|
||||||
|
this.auth.onAuthStateChange(this.handleAuthStateChange.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,6 +60,8 @@ export class AuthSyncService {
|
||||||
* Handle authentication state changes
|
* Handle authentication state changes
|
||||||
*/
|
*/
|
||||||
private async handleAuthStateChange(isAuthenticated: boolean): Promise<void> {
|
private async handleAuthStateChange(isAuthenticated: boolean): Promise<void> {
|
||||||
|
if (!isBrowser) return;
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
// User just logged in
|
// User just logged in
|
||||||
await this.handleLogin();
|
await this.handleLogin();
|
||||||
|
@ -67,8 +75,10 @@ export class AuthSyncService {
|
||||||
* Handle login by syncing user data
|
* Handle login by syncing user data
|
||||||
*/
|
*/
|
||||||
public async handleLogin(): Promise<boolean> {
|
public async handleLogin(): Promise<boolean> {
|
||||||
|
if (!isBrowser) return true;
|
||||||
|
|
||||||
if (this.isSyncing) {
|
if (this.isSyncing) {
|
||||||
console.log('Sync already in progress, queueing login sync');
|
console.log("Sync already in progress, queueing login sync");
|
||||||
if (this.syncPromise) {
|
if (this.syncPromise) {
|
||||||
this.syncPromise = this.syncPromise.then(() => this.performLoginSync());
|
this.syncPromise = this.syncPromise.then(() => this.performLoginSync());
|
||||||
} else {
|
} else {
|
||||||
|
@ -78,37 +88,44 @@ export class AuthSyncService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.syncPromise = this.performLoginSync();
|
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
|
* Perform the actual login sync
|
||||||
*/
|
*/
|
||||||
private async performLoginSync(): Promise<void> {
|
private async performLoginSync(): Promise<void> {
|
||||||
|
if (!isBrowser) return;
|
||||||
|
|
||||||
if (!this.auth.isAuthenticated()) {
|
if (!this.auth.isAuthenticated()) {
|
||||||
console.log('Not authenticated, skipping login sync');
|
console.log("Not authenticated, skipping login sync");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isSyncing = true;
|
this.isSyncing = true;
|
||||||
this.syncErrors = {};
|
this.syncErrors = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Starting login sync process...');
|
console.log("Starting login sync process...");
|
||||||
|
|
||||||
// Display sync notification if in browser environment
|
// Display sync notification if in browser environment
|
||||||
this.showSyncNotification('Syncing your data...');
|
this.showSyncNotification("Syncing your data...");
|
||||||
|
|
||||||
// Sync user-specific data first
|
// Sync user-specific data first
|
||||||
const userId = this.auth.getUserId();
|
const userId = this.auth.getUserId();
|
||||||
if (userId) {
|
if (userId) {
|
||||||
// First sync the current user's data
|
// 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
|
// 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
|
// Sync all collections in parallel with conflict resolution
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.collectionsToSync.map(async (collection) => {
|
this.collectionsToSync.map(async (collection) => {
|
||||||
|
@ -119,32 +136,34 @@ export class AuthSyncService {
|
||||||
console.error(`Error syncing ${collection}:`, error);
|
console.error(`Error syncing ${collection}:`, error);
|
||||||
this.syncErrors[collection] = error as Error;
|
this.syncErrors[collection] = error as Error;
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify sync was successful
|
// Verify sync was successful
|
||||||
const syncVerification = await this.verifySyncSuccess();
|
const syncVerification = await this.verifySyncSuccess();
|
||||||
|
|
||||||
if (syncVerification.success) {
|
if (syncVerification.success) {
|
||||||
console.log('Login sync completed successfully');
|
console.log("Login sync completed successfully");
|
||||||
this.showSyncNotification('Data sync complete!', 'success');
|
this.showSyncNotification("Data sync complete!", "success");
|
||||||
} else {
|
} else {
|
||||||
console.warn('Login sync completed with issues:', syncVerification.errors);
|
console.warn(
|
||||||
this.showSyncNotification('Some data could not be synced', 'warning');
|
"Login sync completed with issues:",
|
||||||
|
syncVerification.errors,
|
||||||
|
);
|
||||||
|
this.showSyncNotification("Some data could not be synced", "warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during login sync:', error);
|
console.error("Error during login sync:", error);
|
||||||
this.showSyncNotification('Failed to sync data', 'error');
|
this.showSyncNotification("Failed to sync data", "error");
|
||||||
} finally {
|
} finally {
|
||||||
this.isSyncing = false;
|
this.isSyncing = false;
|
||||||
|
|
||||||
// Process any queued sync operations
|
// Process any queued sync operations
|
||||||
if (this.syncQueue.length > 0) {
|
if (this.syncQueue.length > 0) {
|
||||||
const nextSync = this.syncQueue.shift();
|
const nextSync = this.syncQueue.shift();
|
||||||
if (nextSync === 'login') {
|
if (nextSync === "login") {
|
||||||
this.handleLogin();
|
this.handleLogin();
|
||||||
} else if (nextSync === 'logout') {
|
} else if (nextSync === "logout") {
|
||||||
this.handleLogout();
|
this.handleLogout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,37 +174,39 @@ export class AuthSyncService {
|
||||||
* Handle logout by clearing user data
|
* Handle logout by clearing user data
|
||||||
*/
|
*/
|
||||||
public async handleLogout(): Promise<boolean> {
|
public async handleLogout(): Promise<boolean> {
|
||||||
|
if (!isBrowser) return true;
|
||||||
|
|
||||||
if (this.isSyncing) {
|
if (this.isSyncing) {
|
||||||
console.log('Sync already in progress, queueing logout cleanup');
|
console.log("Sync already in progress, queueing logout cleanup");
|
||||||
this.syncQueue.push('logout');
|
this.syncQueue.push("logout");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isSyncing = true;
|
this.isSyncing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Starting logout cleanup process...');
|
console.log("Starting logout cleanup process...");
|
||||||
|
|
||||||
// Ensure any pending changes are synced before logout
|
// Ensure any pending changes are synced before logout
|
||||||
await this.syncPendingChanges();
|
await this.syncPendingChanges();
|
||||||
|
|
||||||
// Clear all data from IndexedDB
|
// Clear all data from IndexedDB
|
||||||
await this.dexieService.clearAllData();
|
await this.dexieService.clearAllData();
|
||||||
|
|
||||||
console.log('Logout cleanup completed successfully');
|
console.log("Logout cleanup completed successfully");
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during logout cleanup:', error);
|
console.error("Error during logout cleanup:", error);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
this.isSyncing = false;
|
this.isSyncing = false;
|
||||||
|
|
||||||
// Process any queued sync operations
|
// Process any queued sync operations
|
||||||
if (this.syncQueue.length > 0) {
|
if (this.syncQueue.length > 0) {
|
||||||
const nextSync = this.syncQueue.shift();
|
const nextSync = this.syncQueue.shift();
|
||||||
if (nextSync === 'login') {
|
if (nextSync === "login") {
|
||||||
this.handleLogin();
|
this.handleLogin();
|
||||||
} else if (nextSync === 'logout') {
|
} else if (nextSync === "logout") {
|
||||||
this.handleLogout();
|
this.handleLogout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,51 +217,67 @@ export class AuthSyncService {
|
||||||
* Sync any pending changes before logout
|
* Sync any pending changes before logout
|
||||||
*/
|
*/
|
||||||
private async syncPendingChanges(): Promise<void> {
|
private async syncPendingChanges(): Promise<void> {
|
||||||
|
if (!isBrowser) return;
|
||||||
|
|
||||||
// This would be implemented if we had offline capabilities
|
// This would be implemented if we had offline capabilities
|
||||||
// For now, we just log that we would sync pending changes
|
// 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
|
// In a real implementation, this would sync any offline changes
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that sync was successful by checking data in IndexedDB
|
* 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> = {};
|
const errors: Record<string, string> = {};
|
||||||
|
|
||||||
// Check each collection that had errors
|
// Check each collection that had errors
|
||||||
for (const [collection, error] of Object.entries(this.syncErrors)) {
|
for (const [collection, error] of Object.entries(this.syncErrors)) {
|
||||||
errors[collection] = error.message;
|
errors[collection] = error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user data was synced properly
|
// Check if user data was synced properly
|
||||||
const userId = this.auth.getUserId();
|
const userId = this.auth.getUserId();
|
||||||
if (userId) {
|
if (userId) {
|
||||||
try {
|
try {
|
||||||
const user = await this.dataSync.getItem(Collections.USERS, userId, false);
|
const user = await this.dataSync.getItem(
|
||||||
|
Collections.USERS,
|
||||||
|
userId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
if (!user) {
|
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) {
|
} 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 {
|
return {
|
||||||
success: Object.keys(errors).length === 0,
|
success: Object.keys(errors).length === 0,
|
||||||
errors
|
errors,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a notification to the user about sync status
|
* 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
|
// Only run in browser environment
|
||||||
if (typeof window === 'undefined') return;
|
if (!isBrowser) return;
|
||||||
|
|
||||||
// Check if toast function exists (from react-hot-toast or similar)
|
// 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 });
|
window.toast(message, { type });
|
||||||
} else {
|
} else {
|
||||||
// Fallback to console
|
// Fallback to console
|
||||||
|
@ -253,8 +290,8 @@ export class AuthSyncService {
|
||||||
*/
|
*/
|
||||||
public async forceSyncAll(): Promise<boolean> {
|
public async forceSyncAll(): Promise<boolean> {
|
||||||
if (this.isSyncing) {
|
if (this.isSyncing) {
|
||||||
console.log('Sync already in progress, queueing full sync');
|
console.log("Sync already in progress, queueing full sync");
|
||||||
this.syncQueue.push('login'); // Reuse login sync logic
|
this.syncQueue.push("login"); // Reuse login sync logic
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,6 +316,9 @@ export class AuthSyncService {
|
||||||
// Add toast type to window for TypeScript
|
// Add toast type to window for TypeScript
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
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 { DexieService } from "./DexieService";
|
||||||
import { Get } from '../pocketbase/Get';
|
import { Get } from "../pocketbase/Get";
|
||||||
import { Update } from '../pocketbase/Update';
|
import { Update } from "../pocketbase/Update";
|
||||||
import { Authentication } from '../pocketbase/Authentication';
|
import { Authentication } from "../pocketbase/Authentication";
|
||||||
import { Collections, type BaseRecord } from '../../schemas/pocketbase/schema';
|
import { Collections, type BaseRecord } from "../../schemas/pocketbase/schema";
|
||||||
import type Dexie from 'dexie';
|
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 for tracking offline changes
|
||||||
interface OfflineChange {
|
interface OfflineChange {
|
||||||
id: string;
|
id: string;
|
||||||
collection: string;
|
collection: string;
|
||||||
recordId: string;
|
recordId: string;
|
||||||
operation: 'create' | 'update' | 'delete';
|
operation: "create" | "update" | "delete";
|
||||||
data?: any;
|
data?: any;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
synced: boolean;
|
synced: boolean;
|
||||||
|
@ -32,14 +36,14 @@ export class DataSyncService {
|
||||||
this.get = Get.getInstance();
|
this.get = Get.getInstance();
|
||||||
this.update = Update.getInstance();
|
this.update = Update.getInstance();
|
||||||
this.auth = Authentication.getInstance();
|
this.auth = Authentication.getInstance();
|
||||||
|
|
||||||
// Initialize offline changes table
|
// Initialize offline changes table only in browser
|
||||||
this.initOfflineChangesTable();
|
if (isBrowser) {
|
||||||
|
this.initOfflineChangesTable();
|
||||||
// Check for network status
|
|
||||||
if (typeof window !== 'undefined') {
|
// Check for network status
|
||||||
window.addEventListener('online', this.handleOnline.bind(this));
|
window.addEventListener("online", this.handleOnline.bind(this));
|
||||||
window.addEventListener('offline', this.handleOffline.bind(this));
|
window.addEventListener("offline", this.handleOffline.bind(this));
|
||||||
this.offlineMode = !navigator.onLine;
|
this.offlineMode = !navigator.onLine;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,16 +59,21 @@ export class DataSyncService {
|
||||||
* Initialize the offline changes table
|
* Initialize the offline changes table
|
||||||
*/
|
*/
|
||||||
private initOfflineChangesTable(): void {
|
private initOfflineChangesTable(): void {
|
||||||
|
if (!isBrowser) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const db = this.dexieService.getDB();
|
const db = this.dexieService.getDB();
|
||||||
// Check if the table exists in the schema
|
// Check if the table exists in the schema
|
||||||
if ('offlineChanges' in db) {
|
if ("offlineChanges" in db) {
|
||||||
this.offlineChangesTable = db.offlineChanges as Dexie.Table<OfflineChange, string>;
|
this.offlineChangesTable = db.offlineChanges as Dexie.Table<
|
||||||
|
OfflineChange,
|
||||||
|
string
|
||||||
|
>;
|
||||||
} else {
|
} else {
|
||||||
console.warn('Offline changes table not found in schema');
|
console.warn("Offline changes table not found in schema");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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
|
* Handle device coming online
|
||||||
*/
|
*/
|
||||||
private async handleOnline(): Promise<void> {
|
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;
|
this.offlineMode = false;
|
||||||
await this.syncOfflineChanges();
|
await this.syncOfflineChanges();
|
||||||
}
|
}
|
||||||
|
@ -81,7 +92,9 @@ export class DataSyncService {
|
||||||
* Handle device going offline
|
* Handle device going offline
|
||||||
*/
|
*/
|
||||||
private handleOffline(): void {
|
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;
|
this.offlineMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,11 +102,17 @@ export class DataSyncService {
|
||||||
* Sync a specific collection from PocketBase to IndexedDB
|
* Sync a specific collection from PocketBase to IndexedDB
|
||||||
*/
|
*/
|
||||||
public async syncCollection<T extends BaseRecord>(
|
public async syncCollection<T extends BaseRecord>(
|
||||||
collection: string,
|
collection: string,
|
||||||
filter: string = '',
|
filter: string = "",
|
||||||
sort: string = '-created',
|
sort: string = "-created",
|
||||||
expand: Record<string, any> | string[] | string = {}
|
expand: Record<string, any> | string[] | string = {},
|
||||||
): Promise<T[]> {
|
): 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
|
// Prevent multiple syncs of the same collection at the same time
|
||||||
if (this.syncInProgress[collection]) {
|
if (this.syncInProgress[collection]) {
|
||||||
console.log(`Sync already in progress for ${collection}`);
|
console.log(`Sync already in progress for ${collection}`);
|
||||||
|
@ -101,7 +120,7 @@ export class DataSyncService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.syncInProgress[collection] = true;
|
this.syncInProgress[collection] = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if we're authenticated
|
// Check if we're authenticated
|
||||||
if (!this.auth.isAuthenticated()) {
|
if (!this.auth.isAuthenticated()) {
|
||||||
|
@ -114,65 +133,73 @@ export class DataSyncService {
|
||||||
console.log(`Device is offline, using cached data for ${collection}`);
|
console.log(`Device is offline, using cached data for ${collection}`);
|
||||||
const db = this.dexieService.getDB();
|
const db = this.dexieService.getDB();
|
||||||
const table = this.getTableForCollection(collection);
|
const table = this.getTableForCollection(collection);
|
||||||
return table ? (await table.toArray() as T[]) : [];
|
return table ? ((await table.toArray()) as T[]) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Syncing ${collection}...`);
|
console.log(`Syncing ${collection}...`);
|
||||||
|
|
||||||
// Normalize expand parameter to be an array of strings
|
// Normalize expand parameter to be an array of strings
|
||||||
let normalizedExpand: string[] | undefined;
|
let normalizedExpand: string[] | undefined;
|
||||||
|
|
||||||
if (expand) {
|
if (expand) {
|
||||||
if (typeof expand === 'string') {
|
if (typeof expand === "string") {
|
||||||
// If expand is a string, convert it to an array
|
// If expand is a string, convert it to an array
|
||||||
normalizedExpand = [expand];
|
normalizedExpand = [expand];
|
||||||
} else if (Array.isArray(expand)) {
|
} else if (Array.isArray(expand)) {
|
||||||
// If expand is already an array, use it as is
|
// If expand is already an array, use it as is
|
||||||
normalizedExpand = expand;
|
normalizedExpand = expand;
|
||||||
} else if (typeof expand === 'object') {
|
} else if (typeof expand === "object") {
|
||||||
// If expand is an object, extract the keys
|
// If expand is an object, extract the keys
|
||||||
normalizedExpand = Object.keys(expand);
|
normalizedExpand = Object.keys(expand);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get data from PocketBase
|
// Get data from PocketBase
|
||||||
const items = await this.get.getAll<T>(collection, filter, sort, {
|
const items = await this.get.getAll<T>(collection, filter, sort, {
|
||||||
expand: normalizedExpand
|
expand: normalizedExpand,
|
||||||
});
|
});
|
||||||
console.log(`Fetched ${items.length} items from ${collection}`);
|
console.log(`Fetched ${items.length} items from ${collection}`);
|
||||||
|
|
||||||
// Get the database table
|
// Get the database table
|
||||||
const db = this.dexieService.getDB();
|
const db = this.dexieService.getDB();
|
||||||
const table = this.getTableForCollection(collection);
|
const table = this.getTableForCollection(collection);
|
||||||
|
|
||||||
if (!table) {
|
if (!table) {
|
||||||
console.error(`No table found for collection ${collection}`);
|
console.error(`No table found for collection ${collection}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get existing items to handle conflicts
|
// Get existing items to handle conflicts
|
||||||
const existingItems = await table.toArray();
|
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
|
// Handle conflicts and merge changes
|
||||||
const itemsToStore = await Promise.all(items.map(async (item) => {
|
const itemsToStore = await Promise.all(
|
||||||
const existingItem = existingItemsMap.get(item.id);
|
items.map(async (item) => {
|
||||||
|
const existingItem = existingItemsMap.get(item.id);
|
||||||
if (existingItem) {
|
|
||||||
// Check for conflicts (local changes vs server changes)
|
if (existingItem) {
|
||||||
const resolvedItem = await this.resolveConflict(collection, existingItem, item);
|
// Check for conflicts (local changes vs server changes)
|
||||||
return resolvedItem;
|
const resolvedItem = await this.resolveConflict(
|
||||||
}
|
collection,
|
||||||
|
existingItem,
|
||||||
return item;
|
item,
|
||||||
}));
|
);
|
||||||
|
return resolvedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Store in IndexedDB
|
// Store in IndexedDB
|
||||||
await table.bulkPut(itemsToStore);
|
await table.bulkPut(itemsToStore);
|
||||||
|
|
||||||
// Update last sync timestamp
|
// Update last sync timestamp
|
||||||
await this.dexieService.updateLastSync(collection);
|
await this.dexieService.updateLastSync(collection);
|
||||||
|
|
||||||
return itemsToStore as T[];
|
return itemsToStore as T[];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error syncing ${collection}:`, error);
|
console.error(`Error syncing ${collection}:`, error);
|
||||||
|
@ -188,30 +215,35 @@ export class DataSyncService {
|
||||||
private async resolveConflict<T extends BaseRecord>(
|
private async resolveConflict<T extends BaseRecord>(
|
||||||
collection: string,
|
collection: string,
|
||||||
localItem: T,
|
localItem: T,
|
||||||
serverItem: T
|
serverItem: T,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
// Check if there are pending offline changes for this item
|
// 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) {
|
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
|
// Server-wins strategy by default, but preserve local changes that haven't been synced
|
||||||
const mergedItem = { ...serverItem };
|
const mergedItem = { ...serverItem };
|
||||||
|
|
||||||
// Apply pending changes on top of server data
|
// Apply pending changes on top of server data
|
||||||
for (const change of pendingChanges) {
|
for (const change of pendingChanges) {
|
||||||
if (change.operation === 'update' && change.data) {
|
if (change.operation === "update" && change.data) {
|
||||||
// Apply each field change individually
|
// Apply each field change individually
|
||||||
Object.entries(change.data).forEach(([key, value]) => {
|
Object.entries(change.data).forEach(([key, value]) => {
|
||||||
(mergedItem as any)[key] = value;
|
(mergedItem as any)[key] = value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergedItem;
|
return mergedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No pending changes, use server data
|
// No pending changes, use server data
|
||||||
return serverItem;
|
return serverItem;
|
||||||
}
|
}
|
||||||
|
@ -219,17 +251,23 @@ export class DataSyncService {
|
||||||
/**
|
/**
|
||||||
* Get pending changes for a specific record
|
* 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 [];
|
if (!this.offlineChangesTable) return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.offlineChangesTable
|
return await this.offlineChangesTable
|
||||||
.where('collection')
|
.where("collection")
|
||||||
.equals(collection)
|
.equals(collection)
|
||||||
.and(item => item.recordId === recordId && !item.synced)
|
.and((item) => item.recordId === recordId && !item.synced)
|
||||||
.toArray();
|
.toArray();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error getting pending changes for ${collection}:${recordId}:`, error);
|
console.error(
|
||||||
|
`Error getting pending changes for ${collection}:${recordId}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,66 +277,73 @@ export class DataSyncService {
|
||||||
*/
|
*/
|
||||||
public async syncOfflineChanges(): Promise<boolean> {
|
public async syncOfflineChanges(): Promise<boolean> {
|
||||||
if (!this.offlineChangesTable || this.offlineMode) return false;
|
if (!this.offlineChangesTable || this.offlineMode) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all unsynced changes
|
// Get all unsynced changes
|
||||||
const pendingChanges = await this.offlineChangesTable
|
const pendingChanges = await this.offlineChangesTable
|
||||||
.where('synced')
|
.where("synced")
|
||||||
.equals(0) // Use 0 instead of false for indexable type
|
.equals(0) // Use 0 instead of false for indexable type
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
if (pendingChanges.length === 0) {
|
if (pendingChanges.length === 0) {
|
||||||
console.log('No pending offline changes to sync');
|
console.log("No pending offline changes to sync");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Syncing ${pendingChanges.length} offline changes...`);
|
console.log(`Syncing ${pendingChanges.length} offline changes...`);
|
||||||
|
|
||||||
// Group changes by collection for more efficient processing
|
// Group changes by collection for more efficient processing
|
||||||
const changesByCollection = pendingChanges.reduce((groups, change) => {
|
const changesByCollection = pendingChanges.reduce(
|
||||||
const key = change.collection;
|
(groups, change) => {
|
||||||
if (!groups[key]) {
|
const key = change.collection;
|
||||||
groups[key] = [];
|
if (!groups[key]) {
|
||||||
}
|
groups[key] = [];
|
||||||
groups[key].push(change);
|
}
|
||||||
return groups;
|
groups[key].push(change);
|
||||||
}, {} as Record<string, OfflineChange[]>);
|
return groups;
|
||||||
|
},
|
||||||
|
{} as Record<string, OfflineChange[]>,
|
||||||
|
);
|
||||||
|
|
||||||
// Process each collection's changes
|
// Process each collection's changes
|
||||||
for (const [collection, changes] of Object.entries(changesByCollection)) {
|
for (const [collection, changes] of Object.entries(changesByCollection)) {
|
||||||
// First sync the collection to get latest data
|
// First sync the collection to get latest data
|
||||||
await this.syncCollection(collection);
|
await this.syncCollection(collection);
|
||||||
|
|
||||||
// Then apply each change
|
// Then apply each change
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
try {
|
try {
|
||||||
if (change.operation === 'update' && change.data) {
|
if (change.operation === "update" && change.data) {
|
||||||
await this.update.updateFields(collection, change.recordId, change.data);
|
await this.update.updateFields(
|
||||||
|
collection,
|
||||||
|
change.recordId,
|
||||||
|
change.data,
|
||||||
|
);
|
||||||
|
|
||||||
// Mark as synced
|
// Mark as synced
|
||||||
await this.offlineChangesTable.update(change.id, {
|
await this.offlineChangesTable.update(change.id, {
|
||||||
synced: true,
|
synced: true,
|
||||||
syncAttempts: change.syncAttempts + 1
|
syncAttempts: change.syncAttempts + 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Add support for create and delete operations as needed
|
// Add support for create and delete operations as needed
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error syncing change ${change.id}:`, error);
|
console.error(`Error syncing change ${change.id}:`, error);
|
||||||
|
|
||||||
// Increment sync attempts
|
// Increment sync attempts
|
||||||
await this.offlineChangesTable.update(change.id, {
|
await this.offlineChangesTable.update(change.id, {
|
||||||
syncAttempts: change.syncAttempts + 1
|
syncAttempts: change.syncAttempts + 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync again to ensure we have the latest data
|
// Sync again to ensure we have the latest data
|
||||||
await this.syncCollection(collection);
|
await this.syncCollection(collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error syncing offline changes:', error);
|
console.error("Error syncing offline changes:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -309,35 +354,40 @@ export class DataSyncService {
|
||||||
public async recordOfflineChange(
|
public async recordOfflineChange(
|
||||||
collection: string,
|
collection: string,
|
||||||
recordId: string,
|
recordId: string,
|
||||||
operation: 'create' | 'update' | 'delete',
|
operation: "create" | "update" | "delete",
|
||||||
data?: any
|
data?: any,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
if (!this.offlineChangesTable) return null;
|
if (!this.offlineChangesTable) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const change: Omit<OfflineChange, 'id'> = {
|
const change: Omit<OfflineChange, "id"> = {
|
||||||
collection,
|
collection,
|
||||||
recordId,
|
recordId,
|
||||||
operation,
|
operation,
|
||||||
data,
|
data,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
synced: false,
|
synced: false,
|
||||||
syncAttempts: 0
|
syncAttempts: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const id = await this.offlineChangesTable.add(change as OfflineChange);
|
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
|
// Try to sync immediately if we're online
|
||||||
if (!this.offlineMode) {
|
if (!this.offlineMode) {
|
||||||
this.syncOfflineChanges().catch(err => {
|
this.syncOfflineChanges().catch((err) => {
|
||||||
console.error('Error syncing after recording change:', err);
|
console.error("Error syncing after recording change:", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error recording offline change for ${collection}:${recordId}:`, error);
|
console.error(
|
||||||
|
`Error recording offline change for ${collection}:${recordId}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -346,85 +396,89 @@ export class DataSyncService {
|
||||||
* Get data from IndexedDB, syncing from PocketBase if needed
|
* Get data from IndexedDB, syncing from PocketBase if needed
|
||||||
*/
|
*/
|
||||||
public async getData<T extends BaseRecord>(
|
public async getData<T extends BaseRecord>(
|
||||||
collection: string,
|
collection: string,
|
||||||
forceSync: boolean = false,
|
forceSync: boolean = false,
|
||||||
filter: string = '',
|
filter: string = "",
|
||||||
sort: string = '-created',
|
sort: string = "-created",
|
||||||
expand: Record<string, any> | string[] | string = {}
|
expand: Record<string, any> | string[] | string = {},
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const db = this.dexieService.getDB();
|
const db = this.dexieService.getDB();
|
||||||
const table = this.getTableForCollection(collection);
|
const table = this.getTableForCollection(collection);
|
||||||
|
|
||||||
if (!table) {
|
if (!table) {
|
||||||
console.error(`No table found for collection ${collection}`);
|
console.error(`No table found for collection ${collection}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we need to sync
|
// Check if we need to sync
|
||||||
const lastSync = await this.dexieService.getLastSync(collection);
|
const lastSync = await this.dexieService.getLastSync(collection);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const syncThreshold = 5 * 60 * 1000; // 5 minutes
|
const syncThreshold = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
if (!this.offlineMode && (forceSync || (now - lastSync > syncThreshold))) {
|
if (!this.offlineMode && (forceSync || now - lastSync > syncThreshold)) {
|
||||||
try {
|
try {
|
||||||
await this.syncCollection<T>(collection, filter, sort, expand);
|
await this.syncCollection<T>(collection, filter, sort, expand);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error syncing ${collection}, using cached data:`, error);
|
console.error(`Error syncing ${collection}, using cached data:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get data from IndexedDB
|
// Get data from IndexedDB
|
||||||
let data = await table.toArray();
|
let data = await table.toArray();
|
||||||
|
|
||||||
// Apply filter if provided
|
// Apply filter if provided
|
||||||
if (filter) {
|
if (filter) {
|
||||||
// This is a simple implementation - in a real app, you'd want to parse the filter string
|
// This is a simple implementation - in a real app, you'd want to parse the filter string
|
||||||
// and apply it properly. This is just a basic example.
|
// and apply it properly. This is just a basic example.
|
||||||
data = data.filter((item: any) => {
|
data = data.filter((item: any) => {
|
||||||
// Split filter by logical operators
|
// Split filter by logical operators
|
||||||
const conditions = filter.split(' && ');
|
const conditions = filter.split(" && ");
|
||||||
return conditions.every(condition => {
|
return conditions.every((condition) => {
|
||||||
// Parse condition (very basic implementation)
|
// Parse condition (very basic implementation)
|
||||||
if (condition.includes('=')) {
|
if (condition.includes("=")) {
|
||||||
const [field, value] = condition.split('=');
|
const [field, value] = condition.split("=");
|
||||||
const cleanValue = value.replace(/"/g, '');
|
const cleanValue = value.replace(/"/g, "");
|
||||||
return item[field] === cleanValue;
|
return item[field] === cleanValue;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply sort if provided
|
// Apply sort if provided
|
||||||
if (sort) {
|
if (sort) {
|
||||||
const isDesc = sort.startsWith('-');
|
const isDesc = sort.startsWith("-");
|
||||||
const field = isDesc ? sort.substring(1) : sort;
|
const field = isDesc ? sort.substring(1) : sort;
|
||||||
|
|
||||||
data.sort((a: any, b: any) => {
|
data.sort((a: any, b: any) => {
|
||||||
if (a[field] < b[field]) return isDesc ? 1 : -1;
|
if (a[field] < b[field]) return isDesc ? 1 : -1;
|
||||||
if (a[field] > b[field]) return isDesc ? -1 : 1;
|
if (a[field] > b[field]) return isDesc ? -1 : 1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return data as T[];
|
return data as T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single item by ID
|
* 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 db = this.dexieService.getDB();
|
||||||
const table = this.getTableForCollection(collection);
|
const table = this.getTableForCollection(collection);
|
||||||
|
|
||||||
if (!table) {
|
if (!table) {
|
||||||
console.error(`No table found for collection ${collection}`);
|
console.error(`No table found for collection ${collection}`);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get from IndexedDB first
|
// 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 not found or force sync, try to get from PocketBase
|
||||||
if ((!item || forceSync) && !this.offlineMode) {
|
if ((!item || forceSync) && !this.offlineMode) {
|
||||||
try {
|
try {
|
||||||
|
@ -437,7 +491,7 @@ export class DataSyncService {
|
||||||
console.error(`Error fetching ${collection} item ${id}:`, error);
|
console.error(`Error fetching ${collection} item ${id}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,41 +501,45 @@ export class DataSyncService {
|
||||||
public async updateItem<T extends BaseRecord>(
|
public async updateItem<T extends BaseRecord>(
|
||||||
collection: string,
|
collection: string,
|
||||||
id: string,
|
id: string,
|
||||||
data: Partial<T>
|
data: Partial<T>,
|
||||||
): Promise<T | undefined> {
|
): Promise<T | undefined> {
|
||||||
const table = this.getTableForCollection(collection);
|
const table = this.getTableForCollection(collection);
|
||||||
|
|
||||||
if (!table) {
|
if (!table) {
|
||||||
console.error(`No table found for collection ${collection}`);
|
console.error(`No table found for collection ${collection}`);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current item
|
// Get the current item
|
||||||
const currentItem = await table.get(id) as T | undefined;
|
const currentItem = (await table.get(id)) as T | undefined;
|
||||||
if (!currentItem) {
|
if (!currentItem) {
|
||||||
console.error(`Item ${id} not found in ${collection}`);
|
console.error(`Item ${id} not found in ${collection}`);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the item in IndexedDB
|
// 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);
|
await table.put(updatedItem);
|
||||||
|
|
||||||
// If offline, record the change for later sync
|
// If offline, record the change for later sync
|
||||||
if (this.offlineMode) {
|
if (this.offlineMode) {
|
||||||
await this.recordOfflineChange(collection, id, 'update', data);
|
await this.recordOfflineChange(collection, id, "update", data);
|
||||||
return updatedItem;
|
return updatedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If online, update in PocketBase
|
// If online, update in PocketBase
|
||||||
try {
|
try {
|
||||||
const result = await this.update.updateFields(collection, id, data);
|
const result = await this.update.updateFields(collection, id, data);
|
||||||
return result as T;
|
return result as T;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error updating ${collection} item ${id}:`, error);
|
console.error(`Error updating ${collection} item ${id}:`, error);
|
||||||
|
|
||||||
// Record as offline change to retry later
|
// Record as offline change to retry later
|
||||||
await this.recordOfflineChange(collection, id, 'update', data);
|
await this.recordOfflineChange(collection, id, "update", data);
|
||||||
return updatedItem;
|
return updatedItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -503,9 +561,11 @@ export class DataSyncService {
|
||||||
/**
|
/**
|
||||||
* Get the appropriate Dexie table for a collection
|
* 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();
|
const db = this.dexieService.getDB();
|
||||||
|
|
||||||
switch (collection) {
|
switch (collection) {
|
||||||
case Collections.USERS:
|
case Collections.USERS:
|
||||||
return db.users;
|
return db.users;
|
||||||
|
@ -528,4 +588,4 @@ export class DataSyncService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
import Dexie from 'dexie';
|
import Dexie from "dexie";
|
||||||
import type {
|
import type {
|
||||||
User,
|
User,
|
||||||
Event,
|
Event,
|
||||||
EventRequest,
|
EventRequest,
|
||||||
Log,
|
Log,
|
||||||
Officer,
|
Officer,
|
||||||
Reimbursement,
|
Reimbursement,
|
||||||
Receipt,
|
Receipt,
|
||||||
Sponsor
|
Sponsor,
|
||||||
} from '../../schemas/pocketbase/schema';
|
} 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 for tracking offline changes
|
||||||
interface OfflineChange {
|
interface OfflineChange {
|
||||||
id: string;
|
id: string;
|
||||||
collection: string;
|
collection: string;
|
||||||
recordId: string;
|
recordId: string;
|
||||||
operation: 'create' | 'update' | 'delete';
|
operation: "create" | "update" | "delete";
|
||||||
data?: any;
|
data?: any;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
synced: boolean;
|
synced: boolean;
|
||||||
|
@ -32,59 +36,84 @@ export class DashboardDatabase extends Dexie {
|
||||||
receipts!: Dexie.Table<Receipt, string>;
|
receipts!: Dexie.Table<Receipt, string>;
|
||||||
sponsors!: Dexie.Table<Sponsor, string>;
|
sponsors!: Dexie.Table<Sponsor, string>;
|
||||||
offlineChanges!: Dexie.Table<OfflineChange, string>;
|
offlineChanges!: Dexie.Table<OfflineChange, string>;
|
||||||
|
|
||||||
// Store last sync timestamps
|
// 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() {
|
constructor() {
|
||||||
super('IEEEDashboardDB');
|
super("IEEEDashboardDB");
|
||||||
|
|
||||||
this.version(1).stores({
|
this.version(1).stores({
|
||||||
users: 'id, email, name',
|
users: "id, email, name",
|
||||||
events: 'id, event_name, event_code, start_date, end_date, published',
|
events: "id, event_name, event_code, start_date, end_date, published",
|
||||||
eventRequests: 'id, name, status, requested_user, created, updated',
|
eventRequests: "id, name, status, requested_user, created, updated",
|
||||||
logs: 'id, user, type, created',
|
logs: "id, user, type, created",
|
||||||
officers: 'id, user, role, type',
|
officers: "id, user, role, type",
|
||||||
reimbursements: 'id, title, status, submitted_by, created',
|
reimbursements: "id, title, status, submitted_by, created",
|
||||||
receipts: 'id, created_by, date',
|
receipts: "id, created_by, date",
|
||||||
sponsors: 'id, user, company',
|
sponsors: "id, user, company",
|
||||||
syncInfo: 'id, collection, lastSync'
|
syncInfo: "id, collection, lastSync",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add version 2 with offlineChanges table
|
// Add version 2 with offlineChanges table
|
||||||
this.version(2).stores({
|
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
|
// Initialize the database with default values
|
||||||
async initialize() {
|
async initialize() {
|
||||||
const collections = [
|
const collections = [
|
||||||
'users', 'events', 'event_request', 'logs',
|
"users",
|
||||||
'officers', 'reimbursement', 'receipts', 'sponsors'
|
"events",
|
||||||
|
"event_request",
|
||||||
|
"logs",
|
||||||
|
"officers",
|
||||||
|
"reimbursement",
|
||||||
|
"receipts",
|
||||||
|
"sponsors",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
const exists = await this.syncInfo.get(collection);
|
const exists = await this.syncInfo.get(collection);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
await this.syncInfo.put({
|
await this.syncInfo.put({
|
||||||
id: collection,
|
id: collection,
|
||||||
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
|
// Singleton pattern
|
||||||
export class DexieService {
|
export class DexieService {
|
||||||
private static instance: DexieService;
|
private static instance: DexieService;
|
||||||
private db: DashboardDatabase;
|
private db: DashboardDatabase | MockDashboardDatabase;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.db = new DashboardDatabase();
|
if (isBrowser) {
|
||||||
this.db.initialize();
|
// 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 {
|
public static getInstance(): DexieService {
|
||||||
|
@ -96,40 +125,58 @@ export class DexieService {
|
||||||
|
|
||||||
// Get the database instance
|
// Get the database instance
|
||||||
public getDB(): DashboardDatabase {
|
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
|
// Update the last sync timestamp for a collection
|
||||||
public async updateLastSync(collection: string): Promise<void> {
|
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
|
// Get the last sync timestamp for a collection
|
||||||
public async getLastSync(collection: string): Promise<number> {
|
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;
|
return info?.lastSync || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear all data (useful for logout)
|
// Clear all data (useful for logout)
|
||||||
public async clearAllData(): Promise<void> {
|
public async clearAllData(): Promise<void> {
|
||||||
await this.db.users.clear();
|
if (!isBrowser) return;
|
||||||
await this.db.events.clear();
|
|
||||||
await this.db.eventRequests.clear();
|
const db = this.db as DashboardDatabase;
|
||||||
await this.db.logs.clear();
|
await db.users.clear();
|
||||||
await this.db.officers.clear();
|
await db.events.clear();
|
||||||
await this.db.reimbursements.clear();
|
await db.eventRequests.clear();
|
||||||
await this.db.receipts.clear();
|
await db.logs.clear();
|
||||||
await this.db.sponsors.clear();
|
await db.officers.clear();
|
||||||
await this.db.offlineChanges.clear();
|
await db.reimbursements.clear();
|
||||||
|
await db.receipts.clear();
|
||||||
|
await db.sponsors.clear();
|
||||||
|
await db.offlineChanges.clear();
|
||||||
|
|
||||||
// Reset sync timestamps
|
// Reset sync timestamps
|
||||||
const collections = [
|
const collections = [
|
||||||
'users', 'events', 'event_request', 'logs',
|
"users",
|
||||||
'officers', 'reimbursement', 'receipts', 'sponsors'
|
"events",
|
||||||
|
"event_request",
|
||||||
|
"logs",
|
||||||
|
"officers",
|
||||||
|
"reimbursement",
|
||||||
|
"receipts",
|
||||||
|
"sponsors",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
await this.db.syncInfo.update(collection, { lastSync: 0 });
|
await db.syncInfo.update(collection, { lastSync: 0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,17 +137,17 @@ export class Get {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pb = this.auth.getPocketBase();
|
const pb = this.auth.getPocketBase();
|
||||||
|
|
||||||
// Handle expand parameter
|
// Handle expand parameter
|
||||||
let expandString: string | undefined;
|
let expandString: string | undefined;
|
||||||
if (options?.expand) {
|
if (options?.expand) {
|
||||||
if (Array.isArray(options.expand)) {
|
if (Array.isArray(options.expand)) {
|
||||||
expandString = options.expand.join(",");
|
expandString = options.expand.join(",");
|
||||||
} else if (typeof options.expand === 'string') {
|
} else if (typeof options.expand === "string") {
|
||||||
expandString = options.expand;
|
expandString = options.expand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
...(options?.fields && { fields: options.fields.join(",") }),
|
...(options?.fields && { fields: options.fields.join(",") }),
|
||||||
...(expandString && { expand: expandString }),
|
...(expandString && { expand: expandString }),
|
||||||
|
@ -190,17 +190,17 @@ export class Get {
|
||||||
const filter = recordIds.map((id) => `id="${id}"`).join(" || ");
|
const filter = recordIds.map((id) => `id="${id}"`).join(" || ");
|
||||||
|
|
||||||
const pb = this.auth.getPocketBase();
|
const pb = this.auth.getPocketBase();
|
||||||
|
|
||||||
// Handle expand parameter
|
// Handle expand parameter
|
||||||
let expandString: string | undefined;
|
let expandString: string | undefined;
|
||||||
if (options?.expand) {
|
if (options?.expand) {
|
||||||
if (Array.isArray(options.expand)) {
|
if (Array.isArray(options.expand)) {
|
||||||
expandString = options.expand.join(",");
|
expandString = options.expand.join(",");
|
||||||
} else if (typeof options.expand === 'string') {
|
} else if (typeof options.expand === "string") {
|
||||||
expandString = options.expand;
|
expandString = options.expand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
filter,
|
filter,
|
||||||
...(options?.fields && { fields: options.fields.join(",") }),
|
...(options?.fields && { fields: options.fields.join(",") }),
|
||||||
|
@ -300,17 +300,17 @@ export class Get {
|
||||||
// but the token is still valid for API requests
|
// but the token is still valid for API requests
|
||||||
try {
|
try {
|
||||||
const pb = this.auth.getPocketBase();
|
const pb = this.auth.getPocketBase();
|
||||||
|
|
||||||
// Handle expand parameter
|
// Handle expand parameter
|
||||||
let expandString: string | undefined;
|
let expandString: string | undefined;
|
||||||
if (options?.expand) {
|
if (options?.expand) {
|
||||||
if (Array.isArray(options.expand)) {
|
if (Array.isArray(options.expand)) {
|
||||||
expandString = options.expand.join(",");
|
expandString = options.expand.join(",");
|
||||||
} else if (typeof options.expand === 'string') {
|
} else if (typeof options.expand === "string") {
|
||||||
expandString = options.expand;
|
expandString = options.expand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
...(filter && { filter }),
|
...(filter && { filter }),
|
||||||
...(sort && { sort }),
|
...(sort && { sort }),
|
||||||
|
|
Loading…
Reference in a new issue