diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-10-27 01:23:43 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-10-31 19:26:12 +0100 |
commit | 6c651cf4c81a5c49381c15d89e9e9a43d38f632a (patch) | |
tree | 08b4911fab43ec26a68284ea4b2e68d26018cf91 | |
parent | chore(web): refactor websocket state machine (diff) | |
download | refinery-6c651cf4c81a5c49381c15d89e9e9a43d38f632a.tar.gz refinery-6c651cf4c81a5c49381c15d89e9e9a43d38f632a.tar.zst refinery-6c651cf4c81a5c49381c15d89e9e9a43d38f632a.zip |
chore(web): simplify websocket state machine
-rw-r--r-- | language-web/src/main/js/editor/XtextClient.ts | 2 | ||||
-rw-r--r-- | language-web/src/main/js/editor/XtextWebSocketClient.ts | 54 |
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 | ||
16 | const WEBSOCKET_CLOSE_OK = 1000; | 16 | const WEBSOCKET_CLOSE_OK = 1000; |
17 | 17 | ||
18 | const RECONNECT_DELAY_MS = [1000, 5000, 30_000]; | 18 | const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000]; |
19 | 19 | ||
20 | const MAX_RECONNECT_DELAY_MS = RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1]; | 20 | const MAX_RECONNECT_DELAY_MS = RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1]; |
21 | 21 | ||
22 | const MAX_APP_ERROR_COUNT = 1; | ||
23 | |||
24 | const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000; | 22 | const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000; |
25 | 23 | ||
26 | const PING_TIMEOUT_MS = 10 * 1000; | 24 | const 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 | ||
39 | enum State { | 37 | enum 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')); |