Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions src/components/forms/AutoSaveManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

'use client';

import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { AutoSaveManagerImpl } from '@/form-management/auto-save/auto-save-manager';
import { FormState, SaveStatus } from '@/form-management/types/core';
import { useNotification } from '@/hooks/use-notification';
Expand Down Expand Up @@ -38,6 +38,17 @@ export const AutoSaveManager: React.FC<AutoSaveManagerProps> = ({
queuedSaves: 0,
});
const [lastSavedTime, setLastSavedTime] = useState<string>('');
const isSavingRef = useRef(false);
const onSaveSuccessRef = useRef(onSaveSuccess);
const onSaveErrorRef = useRef(onSaveError);

useEffect(() => {
onSaveSuccessRef.current = onSaveSuccess;
}, [onSaveSuccess]);

useEffect(() => {
onSaveErrorRef.current = onSaveError;
}, [onSaveError]);

useEffect(() => {
if (!enabled) return;
Expand All @@ -51,14 +62,10 @@ export const AutoSaveManager: React.FC<AutoSaveManagerProps> = ({

if (status.status === 'saved') {
setLastSavedTime(new Date().toLocaleTimeString());
if (onSaveSuccess) {
onSaveSuccess();
}
onSaveSuccessRef.current?.();
} else if (status.status === 'error' && status.error) {
notifyError(`Auto-save Error: ${status.error.message}`);
if (onSaveError) {
onSaveError(status.error);
}
onSaveErrorRef.current?.(status.error);
}
});

Expand All @@ -72,21 +79,25 @@ export const AutoSaveManager: React.FC<AutoSaveManagerProps> = ({
subscription.unsubscribe();
autoSaveManager.destroy();
};
}, [formId, enabled, interval, autoSaveManager, onSaveSuccess, onSaveError]);
}, [formId, enabled, interval, autoSaveManager, notifyError]);

// Save on form state changes
// Save on form state changes (debounced; guard against concurrent saves)
useEffect(() => {
if (!enabled) return;

const saveData = async () => {
if (isSavingRef.current) return;
isSavingRef.current = true;
try {
await autoSaveManager.saveNow(formId, formState);
} catch (error) {
console.error('Auto-save failed:', error);
} finally {
isSavingRef.current = false;
}
};

const timer = setTimeout(saveData, 500); // Debounce saves
const timer = setTimeout(saveData, 500);
return () => clearTimeout(timer);
}, [formState, formId, enabled, autoSaveManager]);

Expand Down
49 changes: 34 additions & 15 deletions src/form-management/auto-save/auto-save-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export class AutoSaveManagerImpl implements AutoSaveManager {
private storageQuota: number = 5 * 1024 * 1024; // 5MB default
private isOnline: boolean = true;
private maxRetries: number = 3;
private lastKnownUpdatedAt: Map<string, Date> = new Map();
private onlineHandler: (() => void) | null = null;
private offlineHandler: (() => void) | null = null;

constructor(private storage: Storage = localStorage) {
this.setupNetworkListeners();
Expand Down Expand Up @@ -105,12 +108,28 @@ export class AutoSaveManagerImpl implements AutoSaveManager {
compressed: false,
};

// Conflict detection: if stored draft is newer than what we last read, abort
const key = this.getDraftKey(formId);
const existingJson = this.storage.getItem(key);
if (existingJson) {
const existing: DraftData = JSON.parse(existingJson);
const storedUpdatedAt = new Date(existing.updatedAt);
const lastKnown = this.lastKnownUpdatedAt.get(formId);
if (lastKnown && storedUpdatedAt > lastKnown) {
this.updateSaveStatus(formId, {
status: 'error',
error: new Error('Conflict: draft was modified externally'),
queuedSaves: this.saveQueue.length,
});
return;
}
}

// Check storage quota before saving
await this.ensureStorageQuota(formId, draftData);

// Save to storage
const key = this.getDraftKey(formId);
this.storage.setItem(key, JSON.stringify(draftData));
this.lastKnownUpdatedAt.set(formId, draftData.updatedAt);

this.updateSaveStatus(formId, {
status: 'saved',
Expand Down Expand Up @@ -159,6 +178,7 @@ export class AutoSaveManagerImpl implements AutoSaveManager {
return null;
}

this.lastKnownUpdatedAt.set(formId, new Date(draftData.updatedAt));
return draftData.data;
} catch (error) {
console.error('Error loading draft:', error);
Expand Down Expand Up @@ -260,15 +280,15 @@ export class AutoSaveManagerImpl implements AutoSaveManager {
*/
private setupNetworkListeners(): void {
if (typeof window !== 'undefined') {
window.addEventListener('online', () => {
this.onlineHandler = () => {
this.isOnline = true;
this.processQueue();
});

window.addEventListener('offline', () => {
};
this.offlineHandler = () => {
this.isOnline = false;
});

};
window.addEventListener('online', this.onlineHandler);
window.addEventListener('offline', this.offlineHandler);
this.isOnline = navigator.onLine;
}
}
Expand Down Expand Up @@ -409,17 +429,16 @@ export class AutoSaveManagerImpl implements AutoSaveManager {
* Cleanup resources
*/
destroy(): void {
// Clear all intervals
this.saveIntervals.forEach((intervalId) => clearInterval(intervalId));
this.saveIntervals.clear();

// Clear callbacks
this.statusCallbacks.clear();

// Clear status
this.saveStatus.clear();

// Clear queue
this.saveQueue = [];
this.lastKnownUpdatedAt.clear();

if (typeof window !== 'undefined') {
if (this.onlineHandler) window.removeEventListener('online', this.onlineHandler);
if (this.offlineHandler) window.removeEventListener('offline', this.offlineHandler);
}
}
}
Loading