Skip to content

Commit 47fddfc

Browse files
committed
refactor(sync): improve provider sync status messaging and logic
- Updated sync status messages for clarity, including changes to tooltips and text for different states (saving, synced, saved, error). - Removed debounced state setting in favor of immediate updates to provide real-time feedback during document updates. - Enhanced the handling of sync states to ensure accurate representation of document status during user interactions.
1 parent f51f76f commit 47fddfc

File tree

2 files changed

+41
-27
lines changed

2 files changed

+41
-27
lines changed

packages/webapp/src/components/TipTap/pad-title-section/ProviderSyncStatus.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ const ProviderSyncStatus = () => {
88
const statusConfig = {
99
saving: {
1010
icon: <MdSync className="animate-spin" size={18} />,
11-
text: 'Saving...',
12-
tooltip: 'Saving changes...',
11+
text: 'Saving',
12+
tooltip: 'Syncing changes to server...',
1313
className: 'text-gray-500'
1414
},
1515
synced: {
1616
icon: <MdCloudQueue size={18} />,
1717
text: '',
18-
tooltip: 'All changes synced to server and can be seen by other users',
18+
tooltip: 'Changes synced to server (visible to collaborators). Saving to database...',
1919
className: 'text-gray-500'
2020
},
2121
saved: {
@@ -39,7 +39,7 @@ const ProviderSyncStatus = () => {
3939
error: {
4040
icon: <MdCloudOff size={18} />,
4141
text: 'Error',
42-
tooltip: 'Error syncing changes. Check your connection.',
42+
tooltip: 'Connection error. Your changes are saved locally.',
4343
className: 'text-red-500'
4444
}
4545
}

packages/webapp/src/hooks/useYdocAndProvider.ts

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
import { useState, useEffect, useRef, useMemo } from 'react'
1+
import { useState, useEffect, useRef } from 'react'
22
import * as Y from 'yjs'
33
import { HocuspocusProvider } from '@hocuspocus/provider'
4-
import { debounce } from 'lodash'
54
import { useStore } from '@stores'
65

6+
/**
7+
* Provider Status Flow (Single Source of Truth):
8+
*
9+
* 1. User types → "saving" (changes being sent to server)
10+
* 2. Server receives → "synced" (in server memory, visible to other users)
11+
* 3. Server persists to DB → "saved" (durably stored, survives restart)
12+
*
13+
* The server debounces DB writes (10s), but syncs to memory immediately.
14+
* We only show "saved" when we get actual confirmation from the server.
15+
*/
16+
717
const useYdocAndProvider = ({ accessToken }: { accessToken: string }) => {
818
const {
919
metadata: { documentId, slug }
@@ -13,20 +23,10 @@ const useYdocAndProvider = ({ accessToken }: { accessToken: string }) => {
1323
const ydocRef = useRef(new Y.Doc())
1424
const providerRef = useRef<any>(null)
1525
const isSyncedRef = useRef(false)
26+
const syncedTimeoutRef = useRef<NodeJS.Timeout | null>(null)
1627
const setWorkspaceEditorSetting = useStore((state) => state.setWorkspaceEditorSetting)
1728
const setWorkspaceSetting = useStore((state) => state.setWorkspaceSetting)
18-
const { hocuspocusProvider } = useStore((state) => state.settings)
19-
20-
// Debounced function to set synced state
21-
const setSyncedState = useMemo(
22-
() =>
23-
debounce(() => {
24-
if (isSyncedRef.current) {
25-
setWorkspaceSetting('providerStatus', 'synced')
26-
}
27-
}, 500),
28-
[setWorkspaceSetting]
29-
)
29+
const { hocuspocusProvider, providerStatus } = useStore((state) => state.settings)
3030

3131
useEffect(() => {
3232
if (!documentId) return
@@ -49,6 +49,8 @@ const useYdocAndProvider = ({ accessToken }: { accessToken: string }) => {
4949
onSynced: (data) => {
5050
console.info('++onSynced', data)
5151
isSyncedRef.current = true
52+
53+
// Initial sync complete - document loaded from server (already saved)
5254
setWorkspaceSetting('providerStatus', 'saved')
5355

5456
if (data?.state) setWorkspaceEditorSetting('providerSyncing', false)
@@ -84,12 +86,10 @@ const useYdocAndProvider = ({ accessToken }: { accessToken: string }) => {
8486
const data = JSON.parse(payload)
8587

8688
// Listen for save confirmations from server (real DB persistence)
89+
// This is the ONLY place where we set "saved" - Single Source of Truth
8790
if (data.msg === 'document:saved' && data.documentId === documentId) {
8891
console.info('📝 Document saved to DB:', data)
8992
setWorkspaceSetting('providerStatus', 'saved')
90-
91-
// Cancel any pending debounced call since we have real server confirmation
92-
setSyncedState.cancel()
9393
}
9494
} catch {
9595
// Ignore malformed payloads
@@ -134,7 +134,7 @@ const useYdocAndProvider = ({ accessToken }: { accessToken: string }) => {
134134
const ydoc = ydocRef.current
135135
if (!ydoc) return
136136

137-
const handleUpdate = (update: Uint8Array, origin: any) => {
137+
const handleUpdate = (_update: Uint8Array, origin: any) => {
138138
// Only track local updates, ignore remote updates from provider
139139
if (origin === providerRef.current) return
140140

@@ -143,20 +143,34 @@ const useYdocAndProvider = ({ accessToken }: { accessToken: string }) => {
143143
return
144144
}
145145

146+
// Clear any pending timeout
147+
if (syncedTimeoutRef.current) {
148+
clearTimeout(syncedTimeoutRef.current)
149+
}
150+
151+
// Show "saving" immediately - changes are being sent to server
146152
setWorkspaceSetting('providerStatus', 'saving')
147153

148-
// After 500ms of no typing → "synced" (synced to server memory)
149-
// Server will send "document:saved" message when actually persisted to DB
150-
setSyncedState()
154+
// After 300ms of no updates, show "synced" (WebSocket is real-time, ~50-100ms latency)
155+
// This gives user feedback that changes are on the server (in memory)
156+
// "saved" will come from server when DB write completes (after 10s debounce)
157+
syncedTimeoutRef.current = setTimeout(() => {
158+
// Only transition if we're still in "saving" state
159+
if (providerStatus === 'saving') {
160+
setWorkspaceSetting('providerStatus', 'synced')
161+
}
162+
}, 300)
151163
}
152164

153165
ydoc.on('update', handleUpdate)
154166

155167
return () => {
156168
ydoc.off('update', handleUpdate)
157-
setSyncedState.cancel() // Cancel pending debounced calls
169+
if (syncedTimeoutRef.current) {
170+
clearTimeout(syncedTimeoutRef.current)
171+
}
158172
}
159-
}, [setSyncedState, setWorkspaceSetting])
173+
}, [setWorkspaceSetting, providerStatus])
160174

161175
// Track browser online/offline state
162176
useEffect(() => {

0 commit comments

Comments
 (0)