File size: 4,381 Bytes
a281968 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | // Phase 7: Sync Manager
// Central orchestrator for all document synchronization.
// Combines debounce, queueing, offline detection, and resolving.
const SYNC_DEBOUNCE_MS = 2500; // Wait 2.5s after typing stops before syncing
let _syncDebounceTimer = null;
let _isSyncing = false;
const SyncManager = {
init() {
window.addEventListener('online', () => {
this._updateUIState('online');
this.flushChanges();
});
window.addEventListener('offline', () => {
this._updateUIState('offline');
});
// Attempt an initial flush in case of lingering queued items
if (navigator.onLine) {
setTimeout(() => this.flushChanges(), 1000);
}
},
/**
* Queue a change to be synced to the cloud. Debounces repeated calls.
* @param {string} docId
* @param {string} content
*/
queueChange(docId, content) {
if (!docId) return;
// Immediately enqueue locally (durably)
if (typeof SyncQueue !== 'undefined') {
SyncQueue.enqueue(docId, content);
}
if (!navigator.onLine) {
this._updateUIState('saved_locally');
return;
}
this._updateUIState('saving');
// Debounce the actual cloud sync
if (_syncDebounceTimer) clearTimeout(_syncDebounceTimer);
_syncDebounceTimer = setTimeout(() => {
this.flushChanges();
}, SYNC_DEBOUNCE_MS);
},
/**
* Force an immediate sync of the queue without waiting for debounce.
*/
async syncNow() {
if (_syncDebounceTimer) clearTimeout(_syncDebounceTimer);
await this.flushChanges();
},
/**
* Process all items in the queue and send them to Supabase.
*/
async flushChanges() {
if (!navigator.onLine || typeof SyncQueue === 'undefined' || typeof saveDocument === 'undefined') return;
if (_isSyncing) return; // Prevent concurrent flushes
const queue = SyncQueue.getAll();
if (queue.length === 0) {
this._updateUIState('saved');
return;
}
_isSyncing = true;
this._updateUIState('saving');
let allSuccess = true;
for (const item of queue) {
try {
// If we want conflict resolution, we could fetch first.
// But for typing updates, usually we just overwrite.
// We will do a direct save here for efficiency.
// SyncResolver is primarily used when opening/loading a document.
const success = await saveDocument(item.docId, item.content);
if (success) {
SyncQueue.remove(item.id);
} else {
allSuccess = false;
SyncQueue.incrementRetry(item.id);
}
} catch (e) {
console.error('Sync error:', e);
allSuccess = false;
SyncQueue.incrementRetry(item.id);
}
}
_isSyncing = false;
if (allSuccess) {
this._updateUIState('saved');
if (typeof markClean === 'function') markClean();
} else {
this._updateUIState('error');
}
},
/**
* Fetch a document and resolve conflicts if a local draft exists.
* @param {string} docId
* @returns {Promise<string|null>} The resolved content
*/
async loadAndResolveDocument(docId) {
if (typeof loadDocument === 'undefined' || typeof SyncResolver === 'undefined') {
return null;
}
const serverDoc = await loadDocument(docId);
if (!serverDoc) return null;
// Check if we have a pending change in the queue for this doc
const queue = SyncQueue.getAll();
const localDraft = queue.find(q => q.docId === docId);
if (localDraft) {
const winner = SyncResolver.resolveConflict(localDraft, serverDoc);
if (winner === 'local') {
// Local is newer. Return local content and queue an immediate sync to update the server.
setTimeout(() => this.flushChanges(), 500);
return localDraft.content;
} else {
// Server is newer. Discard local draft.
SyncQueue.remove(localDraft.id);
return serverDoc.content;
}
}
return serverDoc.content;
},
/**
* Internal helper to dispatch UI state changes
* @param {'saving'|'saved'|'saved_locally'|'offline'|'online'|'error'} state
*/
_updateUIState(state) {
window.dispatchEvent(new CustomEvent('bayan:syncstate', { detail: { state } }));
}
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = { SyncManager, SYNC_DEBOUNCE_MS };
}
|