1- import { useState , useEffect , useRef , useMemo } from 'react'
1+ import { useState , useEffect , useRef } from 'react'
22import * as Y from 'yjs'
33import { HocuspocusProvider } from '@hocuspocus/provider'
4- import { debounce } from 'lodash'
54import { 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+
717const 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