@@ -10,6 +10,7 @@ import {
10
10
import type { MessageEvent } from 'ws' ;
11
11
import * as Y from 'yjs' ;
12
12
13
+ import { isAPIError } from '@/api' ;
13
14
import { isFirefox } from '@/utils' ;
14
15
15
16
import { pollOutgoingMessageRequest , postPollSyncRequest } from '../api' ;
@@ -32,14 +33,16 @@ type CollaborationProviderConfiguration = HocuspocusProviderConfiguration & {
32
33
} ;
33
34
34
35
export class CollaborationProvider extends HocuspocusProvider {
35
- private websocketFailureCount = 0 ;
36
- private websocketMaxFailureCount = 2 ;
37
- private isWebsocketFailed = false ;
38
- private isLongPollingStarted = false ;
39
- private url = '' ;
40
- public static TIMEOUT = 30000 ;
36
+ public canEdit = false ;
37
+ public isLongPollingStarted = false ;
38
+ public isWebsocketFailed = false ;
39
+ public seemsUnsyncCount = 0 ;
40
+ public seemsUnsyncMaxCount = 5 ;
41
41
// Server-Sent Events
42
- private sse : EventSource | null = null ;
42
+ protected sse : EventSource | null = null ;
43
+ protected url = '' ;
44
+ public websocketFailureCount = 0 ;
45
+ public websocketMaxFailureCount = 2 ;
43
46
44
47
public constructor ( configuration : CollaborationProviderConfiguration ) {
45
48
const withWS = isFirefox ( ) ;
@@ -53,6 +56,7 @@ export class CollaborationProvider extends HocuspocusProvider {
53
56
super ( configuration ) ;
54
57
55
58
this . url = url ;
59
+ this . canEdit = configuration . canEdit ;
56
60
57
61
if ( configuration . canEdit ) {
58
62
this . on ( 'outgoingMessage' , this . onPollOutgoingMessage . bind ( this ) ) ;
@@ -65,11 +69,12 @@ export class CollaborationProvider extends HocuspocusProvider {
65
69
}
66
70
67
71
public setPollDefaultValues ( ) : void {
68
- this . websocketFailureCount = 0 ;
69
- this . isWebsocketFailed = false ;
70
72
this . isLongPollingStarted = false ;
73
+ this . isWebsocketFailed = false ;
74
+ this . seemsUnsyncCount = 0 ;
71
75
this . sse ?. close ( ) ;
72
76
this . sse = null ;
77
+ this . websocketFailureCount = 0 ;
73
78
}
74
79
75
80
public destroy ( ) : void {
@@ -97,7 +102,7 @@ export class CollaborationProvider extends HocuspocusProvider {
97
102
98
103
if ( ! this . isLongPollingStarted ) {
99
104
this . isLongPollingStarted = true ;
100
- void this . pollSync ( ) ;
105
+ void this . pollSync ( true ) ;
101
106
this . initCollaborationSSE ( ) ;
102
107
}
103
108
}
@@ -116,7 +121,7 @@ export class CollaborationProvider extends HocuspocusProvider {
116
121
}
117
122
118
123
public async onPollOutgoingMessage ( { message } : onOutgoingMessageParameters ) {
119
- if ( ! this . isWebsocketFailed ) {
124
+ if ( ! this . isWebsocketFailed || ! this . canEdit ) {
120
125
return ;
121
126
}
122
127
@@ -133,6 +138,14 @@ export class CollaborationProvider extends HocuspocusProvider {
133
138
await this . pollSync ( ) ;
134
139
}
135
140
} catch ( error : unknown ) {
141
+ if ( isAPIError ( error ) ) {
142
+ // The user is not allowed to send messages
143
+ if ( error . status === 403 ) {
144
+ this . off ( 'outgoingMessage' , this . onPollOutgoingMessage . bind ( this ) ) ;
145
+ this . canEdit = false ;
146
+ }
147
+ }
148
+
136
149
console . error ( 'Polling message failed:' , error ) ;
137
150
}
138
151
}
@@ -148,78 +161,67 @@ export class CollaborationProvider extends HocuspocusProvider {
148
161
withCredentials : true ,
149
162
} ) ;
150
163
151
- //eventSource.close();
152
-
153
- // 1. onmessage handles messages sent with `data:` lines
154
164
this . sse . onmessage = ( event ) => {
155
165
const { updatedDoc64, stateFingerprint, awareness64 } = JSON . parse (
156
166
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
157
167
event . data ,
158
168
) as {
159
- updatedDoc64 : string ;
160
- stateFingerprint : string ;
161
- awareness64 : string ;
169
+ updatedDoc64 ? : string ;
170
+ stateFingerprint ? : string ;
171
+ awareness64 ? : string ;
162
172
} ;
163
173
console . log ( 'Received SSE event:' , event . data ) ;
164
- console . log ( 'CLineId:' , this . document . clientID ) ;
165
-
166
- const localStateFingerprint = this . getStateFingerprint ( this . document ) ;
167
- console . log ( 'EQUAL BEF' , localStateFingerprint === stateFingerprint ) ;
168
174
169
175
if ( awareness64 ) {
170
176
const awareness = Buffer . from ( awareness64 , 'base64' ) ;
171
177
172
178
this . onMessage ( {
173
179
data : awareness ,
174
180
} as MessageEvent ) ;
175
-
176
- console . log ( 'EQUAL AWA' , localStateFingerprint === stateFingerprint ) ;
177
- // if (localStateFingerprint !== stateFingerprint) {
178
- // await this.pollSync();
179
- // }
180
181
}
181
182
182
183
if ( updatedDoc64 ) {
183
184
const uint8Array = Buffer . from ( updatedDoc64 , 'base64' ) ;
184
185
Y . applyUpdate ( this . document , uint8Array ) ;
186
+ }
185
187
186
- console . log ( 'EQUAL' , localStateFingerprint === stateFingerprint ) ;
187
-
188
- // if (localStateFingerprint !== stateFingerprint) {
189
- // await this.pollSync();
190
- // }
188
+ const localStateFingerprint = this . getStateFingerprint ( this . document ) ;
189
+ console . log ( 'EQUAL AWA' , localStateFingerprint === stateFingerprint ) ;
190
+ if ( localStateFingerprint !== stateFingerprint ) {
191
+ void this . pollSync ( ) ;
192
+ } else {
193
+ this . seemsUnsyncCount = 0 ;
191
194
}
192
195
} ;
193
196
194
- // 2. onopen is triggered when the connection is first established
195
197
this . sse . onopen = ( ) => {
196
198
console . log ( 'SSE connection opened.' ) ;
197
199
} ;
198
200
199
201
// 3. onerror is triggered if there's a connection issue
200
202
this . sse . onerror = ( err ) => {
201
203
console . error ( 'SSE error:' , err ) ;
202
- // Depending on the error, the browser may or may not automatically reconnect
203
204
} ;
204
-
205
- //console.log('initCollaborationSSE:data', data);
206
205
}
207
206
208
- public onMessage ( event : MessageEvent ) {
209
- super . onMessage ( event ) ;
210
-
211
- // console.log('onMessage', event);
212
- // console.log('isSynced', this.isSynced);
213
- // console.log('unsyncedChanges', this.unsyncedChanges);
207
+ /**
208
+ * Sync the document with the server.
209
+ *
210
+ * In some rare cases, the document may be out of sync.
211
+ * We use a fingerprint to compare documents,
212
+ * it happens that the local fingerprint is different from the server one
213
+ * when awareness plus the document are updated quickly.
214
+ * The system is resilient to this kind of problems, so `seemsUnsyncCount` should
215
+ * go back to 0 after a few seconds. If not, we will force a sync.
216
+ */
217
+ public async pollSync ( forseSync = false ) {
218
+ if ( ! this . isWebsocketFailed ) {
219
+ return ;
220
+ }
214
221
215
- // if (this.hasUnsyncedChanges) {
216
- // this.unsyncedChanges = 0;
217
- // void this.pollSync();
218
- // }
219
- }
222
+ this . seemsUnsyncCount ++ ;
220
223
221
- public async pollSync ( ) {
222
- if ( ! this . isWebsocketFailed ) {
224
+ if ( this . seemsUnsyncCount < this . seemsUnsyncMaxCount && ! forseSync ) {
223
225
return ;
224
226
}
225
227
0 commit comments