@@ -30,7 +30,17 @@ const defaultValues = {
3030
3131type ExtendedCloseEvent = CloseEvent & { wasClean : boolean } ;
3232
33+ /**
34+ * When a massive simultaneous disconnection occurs (e.g. infra restart), all
35+ * clients would reconnect and invalidate their queries at exactly the same
36+ * time, causing a possible DB spike. Adding random jitter spreads these events over a
37+ * time window so the load is absorbed gradually.
38+ */
39+ const RECONNECT_BASE_DELAY_MS = 1000 ;
40+ const RECONNECT_JITTER_MAX_MS = 3000 ;
41+
3342let reconnectTimeout : ReturnType < typeof setTimeout > | undefined ;
43+ let lostConnectionTimeout : ReturnType < typeof setTimeout > | undefined ;
3444
3545export const useProviderStore = create < UseCollaborationStore > ( ( set , get ) => ( {
3646 ...defaultValues ,
@@ -63,7 +73,14 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
6373 }
6474
6575 clearTimeout ( reconnectTimeout ) ;
66- reconnectTimeout = setTimeout ( ( ) => void provider . connect ( ) , 1000 ) ;
76+
77+ // Jitter spreading for reconnection attempts
78+ // Math.random() generates a random delay to avoid all clients
79+ // reconnecting at the same time
80+ reconnectTimeout = setTimeout (
81+ ( ) => void provider . connect ( ) ,
82+ RECONNECT_BASE_DELAY_MS + Math . random ( ) * RECONNECT_JITTER_MAX_MS ,
83+ ) ;
6784 }
6885 } ,
6986 onAuthenticationFailed ( ) {
@@ -73,13 +90,30 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
7390 set ( { isReady : true , isConnected : true } ) ;
7491 } ,
7592 onStatus : ( { status } ) => {
76- set ( ( state ) => {
77- const nextConnected = status === WebSocketStatus . Connected ;
93+ const isConnected = status === WebSocketStatus . Connected ;
94+ const wasConnected = get ( ) . isConnected ;
7895
96+ if ( isConnected ) {
97+ clearTimeout ( lostConnectionTimeout ) ;
98+ }
99+ // If we were previously connected and now we're not,
100+ // we might have lost the connection
101+ else if ( wasConnected ) {
102+ clearTimeout ( lostConnectionTimeout ) ;
103+ // Jitter spreading for reconnection attempts
104+ // Math.random() generates a random delay to avoid all clients
105+ // reconnecting at the same time
106+ lostConnectionTimeout = setTimeout (
107+ ( ) => set ( { hasLostConnection : true } ) ,
108+ Math . random ( ) * RECONNECT_JITTER_MAX_MS ,
109+ ) ;
110+ }
111+
112+ set ( ( state ) => {
79113 /**
80114 * status === WebSocketStatus.Connected does not mean we are totally connected
81115 * because authentication can still be in progress and failed
82- * So we only update isConnected when we loose the connection
116+ * So we only update isConnected when we lose the connection
83117 */
84118 const connected =
85119 status !== WebSocketStatus . Connected
@@ -91,10 +125,6 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
91125 return {
92126 ...connected ,
93127 isReady : state . isReady || status === WebSocketStatus . Disconnected ,
94- hasLostConnection :
95- state . isConnected && ! nextConnected
96- ? true
97- : state . hasLostConnection ,
98128 } ;
99129 } ) ;
100130 } ,
@@ -123,6 +153,7 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
123153 } ,
124154 destroyProvider : ( ) => {
125155 clearTimeout ( reconnectTimeout ) ;
156+ clearTimeout ( lostConnectionTimeout ) ;
126157 const provider = get ( ) . provider ;
127158 if ( provider ) {
128159 provider . destroy ( ) ;
0 commit comments