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 };
}