aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-27 01:23:43 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-31 19:26:12 +0100
commit6c651cf4c81a5c49381c15d89e9e9a43d38f632a (patch)
tree08b4911fab43ec26a68284ea4b2e68d26018cf91 /language-web/src
parentchore(web): refactor websocket state machine (diff)
downloadrefinery-6c651cf4c81a5c49381c15d89e9e9a43d38f632a.tar.gz
refinery-6c651cf4c81a5c49381c15d89e9e9a43d38f632a.tar.zst
refinery-6c651cf4c81a5c49381c15d89e9e9a43d38f632a.zip
chore(web): simplify websocket state machine
Diffstat (limited to 'language-web/src')
-rw-r--r--language-web/src/main/js/editor/XtextClient.ts2
-rw-r--r--language-web/src/main/js/editor/XtextWebSocketClient.ts54
2 files changed, 17 insertions, 39 deletions
diff --git a/language-web/src/main/js/editor/XtextClient.ts b/language-web/src/main/js/editor/XtextClient.ts
index 5216154e..39458e93 100644
--- a/language-web/src/main/js/editor/XtextClient.ts
+++ b/language-web/src/main/js/editor/XtextClient.ts
@@ -223,7 +223,7 @@ export class XtextClient {
223 this.pendingUpdate = null; 223 this.pendingUpdate = null;
224 switch (newStateId) { 224 switch (newStateId) {
225 case UpdateAction.ForceReconnect: 225 case UpdateAction.ForceReconnect:
226 this.webSocketClient.handleApplicationError(); 226 this.webSocketClient.forceReconnectOnError();
227 break; 227 break;
228 case UpdateAction.FullTextUpdate: 228 case UpdateAction.FullTextUpdate:
229 await this.updateFullText(); 229 await this.updateFullText();
diff --git a/language-web/src/main/js/editor/XtextWebSocketClient.ts b/language-web/src/main/js/editor/XtextWebSocketClient.ts
index 6766029b..489ac03d 100644
--- a/language-web/src/main/js/editor/XtextWebSocketClient.ts
+++ b/language-web/src/main/js/editor/XtextWebSocketClient.ts
@@ -15,12 +15,10 @@ const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1';
15 15
16const WEBSOCKET_CLOSE_OK = 1000; 16const WEBSOCKET_CLOSE_OK = 1000;
17 17
18const RECONNECT_DELAY_MS = [1000, 5000, 30_000]; 18const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000];
19 19
20const MAX_RECONNECT_DELAY_MS = RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1]; 20const MAX_RECONNECT_DELAY_MS = RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1];
21 21
22const MAX_APP_ERROR_COUNT = 1;
23
24const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000; 22const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
25 23
26const PING_TIMEOUT_MS = 10 * 1000; 24const PING_TIMEOUT_MS = 10 * 1000;
@@ -33,7 +31,7 @@ type PushHandler = (
33 resourceId: string, 31 resourceId: string,
34 stateId: string, 32 stateId: string,
35 service: string, 33 service: string,
36 data: unknown 34 data: unknown,
37) => Promise<void>; 35) => Promise<void>;
38 36
39enum State { 37enum State {
@@ -59,8 +57,6 @@ export class XtextWebSocketClient {
59 57
60 state = State.Initial; 58 state = State.Initial;
61 59
62 appErrorCount = 0;
63
64 reconnectTryCount = 0; 60 reconnectTryCount = 0;
65 61
66 idleTimer = new Timer(() => { 62 idleTimer = new Timer(() => {
@@ -106,7 +102,7 @@ export class XtextWebSocketClient {
106 this.connection.addEventListener('open', () => { 102 this.connection.addEventListener('open', () => {
107 if (this.connection.protocol !== XTEXT_SUBPROTOCOL_V1) { 103 if (this.connection.protocol !== XTEXT_SUBPROTOCOL_V1) {
108 log.error('Unknown subprotocol', this.connection.protocol, 'selected by server'); 104 log.error('Unknown subprotocol', this.connection.protocol, 'selected by server');
109 this.handleProtocolError(); 105 this.forceReconnectOnError();
110 } 106 }
111 if (document.visibilityState === 'hidden') { 107 if (document.visibilityState === 'hidden') {
112 this.handleTabHidden(); 108 this.handleTabHidden();
@@ -115,7 +111,6 @@ export class XtextWebSocketClient {
115 } 111 }
116 log.info('Connected to websocket'); 112 log.info('Connected to websocket');
117 this.nextMessageId = 0; 113 this.nextMessageId = 0;
118 this.appErrorCount = 0;
119 this.reconnectTryCount = 0; 114 this.reconnectTryCount = 0;
120 this.pingTimer.schedule(); 115 this.pingTimer.schedule();
121 this.onReconnect().catch((error) => { 116 this.onReconnect().catch((error) => {
@@ -124,7 +119,7 @@ export class XtextWebSocketClient {
124 }); 119 });
125 this.connection.addEventListener('error', (event) => { 120 this.connection.addEventListener('error', (event) => {
126 log.error('Unexpected websocket error', event); 121 log.error('Unexpected websocket error', event);
127 this.handleProtocolError(); 122 this.forceReconnectOnError();
128 }); 123 });
129 this.connection.addEventListener('message', (event) => { 124 this.connection.addEventListener('message', (event) => {
130 this.handleMessage(event.data); 125 this.handleMessage(event.data);
@@ -136,7 +131,7 @@ export class XtextWebSocketClient {
136 return; 131 return;
137 } 132 }
138 log.error('Websocket closed unexpectedly', event.code, event.reason); 133 log.error('Websocket closed unexpectedly', event.code, event.reason);
139 this.handleProtocolError(); 134 this.forceReconnectOnError();
140 }); 135 });
141 } 136 }
142 137
@@ -158,13 +153,13 @@ export class XtextWebSocketClient {
158 } 153 }
159 154
160 private handleTabHidden() { 155 private handleTabHidden() {
161 log.trace('Tab became hidden while websocket is connected'); 156 log.debug('Tab hidden while websocket is connected');
162 this.state = State.TabHiddenIdle; 157 this.state = State.TabHiddenIdle;
163 this.idleTimer.schedule(); 158 this.idleTimer.schedule();
164 } 159 }
165 160
166 private handleTabVisibleConnected() { 161 private handleTabVisibleConnected() {
167 log.trace('Tab became visible while websocket is connected'); 162 log.debug('Tab visible while websocket is connected');
168 this.state = State.TabVisible; 163 this.state = State.TabVisible;
169 } 164 }
170 165
@@ -202,11 +197,11 @@ export class XtextWebSocketClient {
202 this.pingTimer.schedule(); 197 this.pingTimer.schedule();
203 } else { 198 } else {
204 log.error('Invalid pong'); 199 log.error('Invalid pong');
205 this.handleProtocolError(); 200 this.forceReconnectOnError();
206 } 201 }
207 }).catch((error) => { 202 }).catch((error) => {
208 log.error('Error while waiting for ping', error); 203 log.error('Error while waiting for ping', error);
209 this.handleProtocolError(); 204 this.forceReconnectOnError();
210 }); 205 });
211 } 206 }
212 207
@@ -241,7 +236,7 @@ export class XtextWebSocketClient {
241 private handleMessage(messageStr: unknown) { 236 private handleMessage(messageStr: unknown) {
242 if (typeof messageStr !== 'string') { 237 if (typeof messageStr !== 'string') {
243 log.error('Unexpected binary message', messageStr); 238 log.error('Unexpected binary message', messageStr);
244 this.handleProtocolError(); 239 this.forceReconnectOnError();
245 return; 240 return;
246 } 241 }
247 log.trace('Incoming websocket message', messageStr); 242 log.trace('Incoming websocket message', messageStr);
@@ -250,7 +245,7 @@ export class XtextWebSocketClient {
250 message = JSON.parse(messageStr); 245 message = JSON.parse(messageStr);
251 } catch (error) { 246 } catch (error) {
252 log.error('Json parse error', error); 247 log.error('Json parse error', error);
253 this.handleProtocolError(); 248 this.forceReconnectOnError();
254 return; 249 return;
255 } 250 }
256 if (isOkResponse(message)) { 251 if (isOkResponse(message)) {
@@ -259,7 +254,7 @@ export class XtextWebSocketClient {
259 this.rejectRequest(message.id, new Error(`${message.error} error: ${message.message}`)); 254 this.rejectRequest(message.id, new Error(`${message.error} error: ${message.message}`));
260 if (message.error === 'server') { 255 if (message.error === 'server') {
261 log.error('Reconnecting due to server error: ', message.message); 256 log.error('Reconnecting due to server error: ', message.message);
262 this.handleApplicationError(); 257 this.forceReconnectOnError();
263 } 258 }
264 } else if (isPushMessage(message)) { 259 } else if (isPushMessage(message)) {
265 this.onPush( 260 this.onPush(
@@ -272,7 +267,7 @@ export class XtextWebSocketClient {
272 }); 267 });
273 } else { 268 } else {
274 log.error('Unexpected websocket message', message); 269 log.error('Unexpected websocket message', message);
275 this.handleProtocolError(); 270 this.forceReconnectOnError();
276 } 271 }
277 } 272 }
278 273
@@ -301,33 +296,16 @@ export class XtextWebSocketClient {
301 this.handleWaitingForDisconnect(); 296 this.handleWaitingForDisconnect();
302 } 297 }
303 298
304 private handleProtocolError() { 299 forceReconnectOnError(): void {
305 if (this.isLogicallyClosed) { 300 if (this.isLogicallyClosed) {
306 return; 301 return;
307 } 302 }
308 this.abortPendingRequests(); 303 this.abortPendingRequests();
309 this.closeConnection(1000, 'reconnecting due to protocol error'); 304 this.closeConnection(1000, 'reconnecting due to error');
310 log.error('Reconnecting after delay due to protocol error'); 305 log.error('Reconnecting after delay due to error');
311 this.handleErrorState(); 306 this.handleErrorState();
312 } 307 }
313 308
314 handleApplicationError(): void {
315 if (this.isLogicallyClosed) {
316 return;
317 }
318 this.abortPendingRequests();
319 this.closeConnection(1000, 'reconnecting due to application error');
320 this.appErrorCount += 1;
321 if (this.appErrorCount <= MAX_APP_ERROR_COUNT) {
322 log.error('Immediately reconnecting due to application error');
323 this.state = State.Initial;
324 this.reconnect();
325 } else {
326 log.error('Reconnecting after delay due to application error');
327 this.handleErrorState();
328 }
329 }
330
331 private abortPendingRequests() { 309 private abortPendingRequests() {
332 this.pendingRequests.forEach((request) => { 310 this.pendingRequests.forEach((request) => {
333 request.reject(new Error('Websocket disconnect')); 311 request.reject(new Error('Websocket disconnect'));