Skip to content

Commit e33b29e

Browse files
committed
more resilient
1 parent 582cf74 commit e33b29e

File tree

3 files changed

+50
-56
lines changed

3 files changed

+50
-56
lines changed

src/frontend/apps/impress/src/features/docs/doc-management/libs/CollaborationProvider.ts

Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import type { MessageEvent } from 'ws';
1111
import * as Y from 'yjs';
1212

13+
import { isAPIError } from '@/api';
1314
import { isFirefox } from '@/utils';
1415

1516
import { pollOutgoingMessageRequest, postPollSyncRequest } from '../api';
@@ -32,14 +33,16 @@ type CollaborationProviderConfiguration = HocuspocusProviderConfiguration & {
3233
};
3334

3435
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;
4141
// 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;
4346

4447
public constructor(configuration: CollaborationProviderConfiguration) {
4548
const withWS = isFirefox();
@@ -53,6 +56,7 @@ export class CollaborationProvider extends HocuspocusProvider {
5356
super(configuration);
5457

5558
this.url = url;
59+
this.canEdit = configuration.canEdit;
5660

5761
if (configuration.canEdit) {
5862
this.on('outgoingMessage', this.onPollOutgoingMessage.bind(this));
@@ -65,11 +69,12 @@ export class CollaborationProvider extends HocuspocusProvider {
6569
}
6670

6771
public setPollDefaultValues(): void {
68-
this.websocketFailureCount = 0;
69-
this.isWebsocketFailed = false;
7072
this.isLongPollingStarted = false;
73+
this.isWebsocketFailed = false;
74+
this.seemsUnsyncCount = 0;
7175
this.sse?.close();
7276
this.sse = null;
77+
this.websocketFailureCount = 0;
7378
}
7479

7580
public destroy(): void {
@@ -97,7 +102,7 @@ export class CollaborationProvider extends HocuspocusProvider {
97102

98103
if (!this.isLongPollingStarted) {
99104
this.isLongPollingStarted = true;
100-
void this.pollSync();
105+
void this.pollSync(true);
101106
this.initCollaborationSSE();
102107
}
103108
}
@@ -116,7 +121,7 @@ export class CollaborationProvider extends HocuspocusProvider {
116121
}
117122

118123
public async onPollOutgoingMessage({ message }: onOutgoingMessageParameters) {
119-
if (!this.isWebsocketFailed) {
124+
if (!this.isWebsocketFailed || !this.canEdit) {
120125
return;
121126
}
122127

@@ -133,6 +138,14 @@ export class CollaborationProvider extends HocuspocusProvider {
133138
await this.pollSync();
134139
}
135140
} 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+
136149
console.error('Polling message failed:', error);
137150
}
138151
}
@@ -148,78 +161,67 @@ export class CollaborationProvider extends HocuspocusProvider {
148161
withCredentials: true,
149162
});
150163

151-
//eventSource.close();
152-
153-
// 1. onmessage handles messages sent with `data:` lines
154164
this.sse.onmessage = (event) => {
155165
const { updatedDoc64, stateFingerprint, awareness64 } = JSON.parse(
156166
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
157167
event.data,
158168
) as {
159-
updatedDoc64: string;
160-
stateFingerprint: string;
161-
awareness64: string;
169+
updatedDoc64?: string;
170+
stateFingerprint?: string;
171+
awareness64?: string;
162172
};
163173
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);
168174

169175
if (awareness64) {
170176
const awareness = Buffer.from(awareness64, 'base64');
171177

172178
this.onMessage({
173179
data: awareness,
174180
} as MessageEvent);
175-
176-
console.log('EQUAL AWA', localStateFingerprint === stateFingerprint);
177-
// if (localStateFingerprint !== stateFingerprint) {
178-
// await this.pollSync();
179-
// }
180181
}
181182

182183
if (updatedDoc64) {
183184
const uint8Array = Buffer.from(updatedDoc64, 'base64');
184185
Y.applyUpdate(this.document, uint8Array);
186+
}
185187

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;
191194
}
192195
};
193196

194-
// 2. onopen is triggered when the connection is first established
195197
this.sse.onopen = () => {
196198
console.log('SSE connection opened.');
197199
};
198200

199201
// 3. onerror is triggered if there's a connection issue
200202
this.sse.onerror = (err) => {
201203
console.error('SSE error:', err);
202-
// Depending on the error, the browser may or may not automatically reconnect
203204
};
204-
205-
//console.log('initCollaborationSSE:data', data);
206205
}
207206

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+
}
214221

215-
// if (this.hasUnsyncedChanges) {
216-
// this.unsyncedChanges = 0;
217-
// void this.pollSync();
218-
// }
219-
}
222+
this.seemsUnsyncCount++;
220223

221-
public async pollSync() {
222-
if (!this.isWebsocketFailed) {
224+
if (this.seemsUnsyncCount < this.seemsUnsyncMaxCount && !forseSync) {
223225
return;
224226
}
225227

src/frontend/servers/y-provider/src/handlers/collaborationPollHandler.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { Response } from 'express';
33
import { PollSync, PollSyncRequest } from '@/libs/PollSync';
44
import { hocusPocusServer } from '@/servers/hocusPocusServer';
55

6-
const TIMEOUT = 30000;
7-
86
interface CollaborationPollPostMessagePayload {
97
message64: string;
108
}

src/frontend/servers/y-provider/src/libs/PollSync.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ import crypto from 'crypto';
33

44
import {
55
AwarenessUpdate,
6-
Debugger,
76
Document,
87
Hocuspocus,
98
IncomingMessage,
10-
MessageReceiver,
119
MessageType,
1210
OutgoingMessage,
1311
} from '@hocuspocus/server';
@@ -137,10 +135,6 @@ export class PollSync<T> {
137135
});
138136
}
139137

140-
// hpDoc.getConnections().forEach((connection) => {
141-
// connection.handleMessage(messageBuffer);
142-
// });
143-
144138
logger('Polling Updated YDoc', hpDoc.name);
145139
}
146140

0 commit comments

Comments
 (0)