diff options
Diffstat (limited to 'subprojects/frontend/src')
5 files changed, 274 insertions, 90 deletions
diff --git a/subprojects/frontend/src/editor/ConnectionStatusNotification.tsx b/subprojects/frontend/src/editor/ConnectionStatusNotification.tsx index e402e296..54c4e834 100644 --- a/subprojects/frontend/src/editor/ConnectionStatusNotification.tsx +++ b/subprojects/frontend/src/editor/ConnectionStatusNotification.tsx | |||
@@ -1,50 +1,83 @@ | |||
1 | import Button from '@mui/material/Button'; | 1 | import Button from '@mui/material/Button'; |
2 | import { observer } from 'mobx-react-lite'; | 2 | import { observer } from 'mobx-react-lite'; |
3 | import { type SnackbarKey, useSnackbar } from 'notistack'; | 3 | import { |
4 | useSnackbar, | ||
5 | type SnackbarKey, | ||
6 | type SnackbarMessage, | ||
7 | type OptionsObject, | ||
8 | } from 'notistack'; | ||
4 | import React, { useEffect } from 'react'; | 9 | import React, { useEffect } from 'react'; |
5 | 10 | ||
6 | import { ContrastThemeProvider } from '../theme/ThemeProvider'; | 11 | import { ContrastThemeProvider } from '../theme/ThemeProvider'; |
7 | 12 | ||
8 | import type EditorStore from './EditorStore'; | 13 | import type EditorStore from './EditorStore'; |
9 | 14 | ||
10 | const CONNECTING_DEBOUNCE_TIMEOUT = 250; | 15 | const DEBOUNCE_TIMEOUT = 350; |
16 | |||
17 | function enqueueLater( | ||
18 | enqueueSnackbar: ( | ||
19 | message: SnackbarMessage, | ||
20 | options: OptionsObject | undefined, | ||
21 | ) => SnackbarKey, | ||
22 | closeSnackbar: (key: SnackbarKey) => void, | ||
23 | message: SnackbarMessage, | ||
24 | options?: OptionsObject | undefined, | ||
25 | debounceTimeout = DEBOUNCE_TIMEOUT, | ||
26 | ): () => void { | ||
27 | let key: SnackbarKey | undefined; | ||
28 | let timeout: number | undefined = setTimeout(() => { | ||
29 | timeout = undefined; | ||
30 | key = enqueueSnackbar(message, options); | ||
31 | }, debounceTimeout); | ||
32 | return () => { | ||
33 | if (timeout !== undefined) { | ||
34 | clearTimeout(timeout); | ||
35 | } | ||
36 | if (key !== undefined) { | ||
37 | closeSnackbar(key); | ||
38 | } | ||
39 | }; | ||
40 | } | ||
11 | 41 | ||
12 | export default observer(function ConnectionStatusNotification({ | 42 | export default observer(function ConnectionStatusNotification({ |
13 | editorStore, | 43 | editorStore, |
14 | }: { | 44 | }: { |
15 | editorStore: EditorStore; | 45 | editorStore: EditorStore; |
16 | }): null { | 46 | }): null { |
17 | const { opened, opening, connectionErrors } = editorStore; | 47 | const { |
48 | opened, | ||
49 | opening, | ||
50 | connectionErrors, | ||
51 | disconnectedByUser, | ||
52 | networkMissing, | ||
53 | } = editorStore; | ||
18 | const { enqueueSnackbar, closeSnackbar } = useSnackbar(); | 54 | const { enqueueSnackbar, closeSnackbar } = useSnackbar(); |
19 | 55 | ||
20 | useEffect(() => { | 56 | useEffect(() => { |
21 | if (opening) { | 57 | if (opening) { |
22 | let key: SnackbarKey | undefined; | 58 | return enqueueLater( |
23 | let timeout: number | undefined = setTimeout(() => { | 59 | enqueueSnackbar, |
24 | timeout = undefined; | 60 | closeSnackbar, |
25 | key = enqueueSnackbar('Connecting to Refinery', { | 61 | 'Connecting to Refinery', |
62 | { | ||
26 | persist: true, | 63 | persist: true, |
27 | action: ( | 64 | action: ( |
28 | <Button onClick={() => editorStore.disconnect()} color="inherit"> | 65 | <Button onClick={() => editorStore.disconnect()} color="inherit"> |
29 | Cancel | 66 | Cancel |
30 | </Button> | 67 | </Button> |
31 | ), | 68 | ), |
32 | }); | 69 | }, |
33 | }, CONNECTING_DEBOUNCE_TIMEOUT); | 70 | 500, |
34 | return () => { | 71 | ); |
35 | if (timeout !== undefined) { | ||
36 | clearTimeout(timeout); | ||
37 | } | ||
38 | if (key !== undefined) { | ||
39 | closeSnackbar(key); | ||
40 | } | ||
41 | }; | ||
42 | } | 72 | } |
43 | 73 | ||
44 | if (connectionErrors.length >= 1) { | 74 | if (connectionErrors.length >= 1 && !opening) { |
45 | const key = enqueueSnackbar( | 75 | return enqueueLater( |
76 | enqueueSnackbar, | ||
77 | closeSnackbar, | ||
46 | <div> | 78 | <div> |
47 | Connection error: <b>{connectionErrors[0]}</b> | 79 | Connection error:{' '} |
80 | <b>{connectionErrors[connectionErrors.length - 1]}</b> | ||
48 | {connectionErrors.length >= 2 && ( | 81 | {connectionErrors.length >= 2 && ( |
49 | <> | 82 | <> |
50 | {' '} | 83 | {' '} |
@@ -57,28 +90,72 @@ export default observer(function ConnectionStatusNotification({ | |||
57 | persist: !opened, | 90 | persist: !opened, |
58 | variant: 'error', | 91 | variant: 'error', |
59 | action: opened ? ( | 92 | action: opened ? ( |
60 | <ContrastThemeProvider> | 93 | <Button onClick={() => editorStore.disconnect()} color="inherit"> |
61 | <Button onClick={() => editorStore.disconnect()} color="inherit"> | 94 | Disconnect |
62 | Disconnect | 95 | </Button> |
63 | </Button> | ||
64 | </ContrastThemeProvider> | ||
65 | ) : ( | 96 | ) : ( |
66 | <ContrastThemeProvider> | 97 | <> |
67 | <Button onClick={() => editorStore.connect()} color="inherit"> | 98 | <Button onClick={() => editorStore.connect()} color="inherit"> |
68 | Reconnect | 99 | Reconnect |
69 | </Button> | 100 | </Button> |
70 | <Button onClick={() => editorStore.disconnect()} color="inherit"> | 101 | <Button onClick={() => editorStore.disconnect()} color="inherit"> |
71 | Cancel | 102 | Cancel |
72 | </Button> | 103 | </Button> |
104 | </> | ||
105 | ), | ||
106 | }, | ||
107 | ); | ||
108 | } | ||
109 | |||
110 | if (networkMissing) { | ||
111 | if (disconnectedByUser) { | ||
112 | return enqueueLater( | ||
113 | enqueueSnackbar, | ||
114 | closeSnackbar, | ||
115 | <div> | ||
116 | <b>No network connection:</b> Some editing features might be | ||
117 | degraded | ||
118 | </div>, | ||
119 | { | ||
120 | action: ( | ||
121 | <ContrastThemeProvider> | ||
122 | <Button onClick={() => editorStore.connect()} color="primary"> | ||
123 | Try reconnecting | ||
124 | </Button> | ||
125 | </ContrastThemeProvider> | ||
126 | ), | ||
127 | }, | ||
128 | 0, | ||
129 | ); | ||
130 | } | ||
131 | |||
132 | return enqueueLater( | ||
133 | enqueueSnackbar, | ||
134 | closeSnackbar, | ||
135 | <div> | ||
136 | <b>No network connection:</b> Refinery will try to reconnect when the | ||
137 | connection is restored | ||
138 | </div>, | ||
139 | { | ||
140 | persist: true, | ||
141 | action: ( | ||
142 | <ContrastThemeProvider> | ||
143 | <Button onClick={() => editorStore.connect()} color="primary"> | ||
144 | Try now | ||
145 | </Button> | ||
146 | <Button onClick={() => editorStore.disconnect()} color="inherit"> | ||
147 | Cancel | ||
148 | </Button> | ||
73 | </ContrastThemeProvider> | 149 | </ContrastThemeProvider> |
74 | ), | 150 | ), |
75 | }, | 151 | }, |
76 | ); | 152 | ); |
77 | return () => closeSnackbar(key); | ||
78 | } | 153 | } |
79 | 154 | ||
80 | if (!opened) { | 155 | if (disconnectedByUser) { |
81 | const key = enqueueSnackbar( | 156 | return enqueueLater( |
157 | enqueueSnackbar, | ||
158 | closeSnackbar, | ||
82 | <div> | 159 | <div> |
83 | <b>Not connected to Refinery:</b> Some editing features might be | 160 | <b>Not connected to Refinery:</b> Some editing features might be |
84 | degraded | 161 | degraded |
@@ -86,12 +163,14 @@ export default observer(function ConnectionStatusNotification({ | |||
86 | { | 163 | { |
87 | action: ( | 164 | action: ( |
88 | <ContrastThemeProvider> | 165 | <ContrastThemeProvider> |
89 | <Button onClick={() => editorStore.connect()}>Reconnect</Button> | 166 | <Button onClick={() => editorStore.connect()} color="primary"> |
167 | Reconnect | ||
168 | </Button> | ||
90 | </ContrastThemeProvider> | 169 | </ContrastThemeProvider> |
91 | ), | 170 | ), |
92 | }, | 171 | }, |
172 | 0, | ||
93 | ); | 173 | ); |
94 | return () => closeSnackbar(key); | ||
95 | } | 174 | } |
96 | 175 | ||
97 | return () => {}; | 176 | return () => {}; |
@@ -100,6 +179,8 @@ export default observer(function ConnectionStatusNotification({ | |||
100 | opened, | 179 | opened, |
101 | opening, | 180 | opening, |
102 | connectionErrors, | 181 | connectionErrors, |
182 | disconnectedByUser, | ||
183 | networkMissing, | ||
103 | closeSnackbar, | 184 | closeSnackbar, |
104 | enqueueSnackbar, | 185 | enqueueSnackbar, |
105 | ]); | 186 | ]); |
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index 3ec33b2c..ecbe6ef8 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts | |||
@@ -67,6 +67,7 @@ export default class EditorStore { | |||
67 | contentAssist: false, | 67 | contentAssist: false, |
68 | formatText: false, | 68 | formatText: false, |
69 | }); | 69 | }); |
70 | this.client.start(); | ||
70 | } | 71 | } |
71 | 72 | ||
72 | get opened(): boolean { | 73 | get opened(): boolean { |
@@ -77,6 +78,14 @@ export default class EditorStore { | |||
77 | return this.client.webSocketClient.opening; | 78 | return this.client.webSocketClient.opening; |
78 | } | 79 | } |
79 | 80 | ||
81 | get disconnectedByUser(): boolean { | ||
82 | return this.client.webSocketClient.disconnectedByUser; | ||
83 | } | ||
84 | |||
85 | get networkMissing(): boolean { | ||
86 | return this.client.webSocketClient.networkMissing; | ||
87 | } | ||
88 | |||
80 | get connectionErrors(): string[] { | 89 | get connectionErrors(): string[] { |
81 | return this.client.webSocketClient.errors; | 90 | return this.client.webSocketClient.errors; |
82 | } | 91 | } |
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts index c02afb3b..1f7e446f 100644 --- a/subprojects/frontend/src/xtext/XtextClient.ts +++ b/subprojects/frontend/src/xtext/XtextClient.ts | |||
@@ -47,6 +47,10 @@ export default class XtextClient { | |||
47 | this.occurrencesService = new OccurrencesService(store, this.updateService); | 47 | this.occurrencesService = new OccurrencesService(store, this.updateService); |
48 | } | 48 | } |
49 | 49 | ||
50 | start(): void { | ||
51 | this.webSocketClient.start(); | ||
52 | } | ||
53 | |||
50 | private onReconnect(): void { | 54 | private onReconnect(): void { |
51 | this.updateService.onReconnect(); | 55 | this.updateService.onReconnect(); |
52 | this.occurrencesService.onReconnect(); | 56 | this.occurrencesService.onReconnect(); |
diff --git a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts index b69e1d6c..cba6f064 100644 --- a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts +++ b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts | |||
@@ -6,7 +6,7 @@ import CancelledError from '../utils/CancelledError'; | |||
6 | import PendingTask from '../utils/PendingTask'; | 6 | import PendingTask from '../utils/PendingTask'; |
7 | import getLogger from '../utils/getLogger'; | 7 | import getLogger from '../utils/getLogger'; |
8 | 8 | ||
9 | import webSocketMachine from './webSocketMachine'; | 9 | import webSocketMachine, { isWebSocketURLLocal } from './webSocketMachine'; |
10 | import { | 10 | import { |
11 | type XtextWebPushService, | 11 | type XtextWebPushService, |
12 | XtextResponse, | 12 | XtextResponse, |
@@ -16,7 +16,9 @@ import { PongResult } from './xtextServiceResults'; | |||
16 | 16 | ||
17 | const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; | 17 | const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; |
18 | 18 | ||
19 | const REQUEST_TIMEOUT = 1000; | 19 | // Use a large enough timeout so that a request can complete successfully |
20 | // even if the browser has throttled the background tab. | ||
21 | const REQUEST_TIMEOUT = 5000; | ||
20 | 22 | ||
21 | const log = getLogger('xtext.XtextWebSocketClient'); | 23 | const log = getLogger('xtext.XtextWebSocketClient'); |
22 | 24 | ||
@@ -52,6 +54,7 @@ export default class XtextWebSocketClient { | |||
52 | openWebSocket: ({ webSocketURL }) => this.openWebSocket(webSocketURL), | 54 | openWebSocket: ({ webSocketURL }) => this.openWebSocket(webSocketURL), |
53 | closeWebSocket: () => this.closeWebSocket(), | 55 | closeWebSocket: () => this.closeWebSocket(), |
54 | notifyReconnect: () => this.onReconnect(), | 56 | notifyReconnect: () => this.onReconnect(), |
57 | notifyDisconnect: () => this.onDisconnect(), | ||
55 | cancelPendingRequests: () => this.cancelPendingRequests(), | 58 | cancelPendingRequests: () => this.cancelPendingRequests(), |
56 | }, | 59 | }, |
57 | services: { | 60 | services: { |
@@ -141,20 +144,6 @@ export default class XtextWebSocketClient { | |||
141 | private readonly onDisconnect: DisconnectHandler, | 144 | private readonly onDisconnect: DisconnectHandler, |
142 | private readonly onPush: PushHandler, | 145 | private readonly onPush: PushHandler, |
143 | ) { | 146 | ) { |
144 | this.interpreter | ||
145 | .onTransition((state, event) => { | ||
146 | log.trace('WebSocke state transition', state.value, 'on event', event); | ||
147 | this.stateAtom.reportChanged(); | ||
148 | }) | ||
149 | .start(); | ||
150 | |||
151 | this.updateVisibility(); | ||
152 | document.addEventListener('visibilitychange', () => | ||
153 | this.updateVisibility(), | ||
154 | ); | ||
155 | |||
156 | this.interpreter.send('CONNECT'); | ||
157 | |||
158 | makeAutoObservable< | 147 | makeAutoObservable< |
159 | XtextWebSocketClient, | 148 | XtextWebSocketClient, |
160 | | 'stateAtom' | 149 | | 'stateAtom' |
@@ -177,6 +166,40 @@ export default class XtextWebSocketClient { | |||
177 | }); | 166 | }); |
178 | } | 167 | } |
179 | 168 | ||
169 | start(): void { | ||
170 | this.interpreter | ||
171 | .onTransition((state, event) => { | ||
172 | log.trace('WebSocke state transition', state.value, 'on event', event); | ||
173 | this.stateAtom.reportChanged(); | ||
174 | }) | ||
175 | .start(); | ||
176 | |||
177 | this.interpreter.send(window.navigator.onLine ? 'ONLINE' : 'OFFLINE'); | ||
178 | window.addEventListener('offline', () => this.interpreter.send('OFFLINE')); | ||
179 | window.addEventListener('online', () => this.interpreter.send('ONLINE')); | ||
180 | this.updateVisibility(); | ||
181 | document.addEventListener('visibilitychange', () => | ||
182 | this.updateVisibility(), | ||
183 | ); | ||
184 | window.addEventListener('pagehide', () => | ||
185 | this.interpreter.send('PAGE_HIDE'), | ||
186 | ); | ||
187 | window.addEventListener('pageshow', () => { | ||
188 | this.updateVisibility(); | ||
189 | this.interpreter.send('PAGE_SHOW'); | ||
190 | }); | ||
191 | // https://developer.chrome.com/blog/page-lifecycle-api/#new-features-added-in-chrome-68 | ||
192 | if ('wasDiscarded' in document) { | ||
193 | document.addEventListener('freeze', () => | ||
194 | this.interpreter.send('PAGE_FREEZE'), | ||
195 | ); | ||
196 | document.addEventListener('resume', () => | ||
197 | this.interpreter.send('PAGE_RESUME'), | ||
198 | ); | ||
199 | } | ||
200 | this.interpreter.send('CONNECT'); | ||
201 | } | ||
202 | |||
180 | get state() { | 203 | get state() { |
181 | this.stateAtom.reportObserved(); | 204 | this.stateAtom.reportObserved(); |
182 | return this.interpreter.state; | 205 | return this.interpreter.state; |
@@ -190,6 +213,19 @@ export default class XtextWebSocketClient { | |||
190 | return this.state.matches('connection.socketCreated.open.opened'); | 213 | return this.state.matches('connection.socketCreated.open.opened'); |
191 | } | 214 | } |
192 | 215 | ||
216 | get disconnectedByUser(): boolean { | ||
217 | return this.state.matches('connection.disconnected'); | ||
218 | } | ||
219 | |||
220 | get networkMissing(): boolean { | ||
221 | return ( | ||
222 | this.state.matches('connection.temporarilyOffline') || | ||
223 | (this.disconnectedByUser && | ||
224 | this.state.matches('network.offline') && | ||
225 | !isWebSocketURLLocal(this.state.context.webSocketURL)) | ||
226 | ); | ||
227 | } | ||
228 | |||
193 | get errors(): string[] { | 229 | get errors(): string[] { |
194 | return this.state.context.errors; | 230 | return this.state.context.errors; |
195 | } | 231 | } |
@@ -217,9 +253,13 @@ export default class XtextWebSocketClient { | |||
217 | const id = nanoid(); | 253 | const id = nanoid(); |
218 | 254 | ||
219 | const promise = new Promise((resolve, reject) => { | 255 | const promise = new Promise((resolve, reject) => { |
220 | const task = new PendingTask(resolve, reject, REQUEST_TIMEOUT, () => | 256 | const task = new PendingTask(resolve, reject, REQUEST_TIMEOUT, () => { |
221 | this.removeTask(id), | 257 | this.interpreter.send({ |
222 | ); | 258 | type: 'ERROR', |
259 | message: 'Connection timed out', | ||
260 | }); | ||
261 | this.removeTask(id); | ||
262 | }); | ||
223 | this.pendingRequests.set(id, task); | 263 | this.pendingRequests.set(id, task); |
224 | }); | 264 | }); |
225 | 265 | ||
@@ -272,7 +312,6 @@ export default class XtextWebSocketClient { | |||
272 | } | 312 | } |
273 | 313 | ||
274 | private cancelPendingRequests(): void { | 314 | private cancelPendingRequests(): void { |
275 | this.onDisconnect(); | ||
276 | this.pendingRequests.forEach((task) => | 315 | this.pendingRequests.forEach((task) => |
277 | task.reject(new CancelledError('Closing connection')), | 316 | task.reject(new CancelledError('Closing connection')), |
278 | ); | 317 | ); |
diff --git a/subprojects/frontend/src/xtext/webSocketMachine.ts b/subprojects/frontend/src/xtext/webSocketMachine.ts index 50eb36a0..25689cec 100644 --- a/subprojects/frontend/src/xtext/webSocketMachine.ts +++ b/subprojects/frontend/src/xtext/webSocketMachine.ts | |||
@@ -7,7 +7,6 @@ const ERROR_WAIT_TIMES = [200, 1000, 5000, 30_000]; | |||
7 | export interface WebSocketContext { | 7 | export interface WebSocketContext { |
8 | webSocketURL: string | undefined; | 8 | webSocketURL: string | undefined; |
9 | errors: string[]; | 9 | errors: string[]; |
10 | retryCount: number; | ||
11 | } | 10 | } |
12 | 11 | ||
13 | export type WebSocketEvent = | 12 | export type WebSocketEvent = |
@@ -17,8 +16,33 @@ export type WebSocketEvent = | |||
17 | | { type: 'OPENED' } | 16 | | { type: 'OPENED' } |
18 | | { type: 'TAB_VISIBLE' } | 17 | | { type: 'TAB_VISIBLE' } |
19 | | { type: 'TAB_HIDDEN' } | 18 | | { type: 'TAB_HIDDEN' } |
19 | | { type: 'PAGE_HIDE' } | ||
20 | | { type: 'PAGE_SHOW' } | ||
21 | | { type: 'PAGE_FREEZE' } | ||
22 | | { type: 'PAGE_RESUME' } | ||
23 | | { type: 'ONLINE' } | ||
24 | | { type: 'OFFLINE' } | ||
20 | | { type: 'ERROR'; message: string }; | 25 | | { type: 'ERROR'; message: string }; |
21 | 26 | ||
27 | export function isWebSocketURLLocal(webSocketURL: string | undefined): boolean { | ||
28 | if (webSocketURL === undefined) { | ||
29 | return false; | ||
30 | } | ||
31 | let hostname: string; | ||
32 | try { | ||
33 | ({ hostname } = new URL(webSocketURL)); | ||
34 | } catch { | ||
35 | return false; | ||
36 | } | ||
37 | // https://stackoverflow.com/a/57949518 | ||
38 | return ( | ||
39 | hostname === 'localhost' || | ||
40 | hostname === '[::1]' || | ||
41 | hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) !== | ||
42 | null | ||
43 | ); | ||
44 | } | ||
45 | |||
22 | export default createMachine( | 46 | export default createMachine( |
23 | { | 47 | { |
24 | id: 'webSocket', | 48 | id: 'webSocket', |
@@ -31,32 +55,65 @@ export default createMachine( | |||
31 | context: { | 55 | context: { |
32 | webSocketURL: undefined, | 56 | webSocketURL: undefined, |
33 | errors: [], | 57 | errors: [], |
34 | retryCount: 0, | ||
35 | }, | 58 | }, |
36 | type: 'parallel', | 59 | type: 'parallel', |
37 | states: { | 60 | states: { |
38 | connection: { | 61 | connection: { |
39 | initial: 'disconnected', | 62 | initial: 'disconnected', |
63 | entry: 'clearErrors', | ||
40 | states: { | 64 | states: { |
41 | disconnected: { | 65 | disconnected: { |
42 | id: 'disconnected', | 66 | id: 'disconnected', |
67 | entry: ['clearErrors', 'notifyDisconnect'], | ||
43 | on: { | 68 | on: { |
44 | CONFIGURE: { actions: 'configure' }, | 69 | CONFIGURE: { actions: 'configure' }, |
45 | }, | 70 | }, |
46 | }, | 71 | }, |
47 | timedOut: { | 72 | timedOut: { |
48 | id: 'timedOut', | 73 | id: 'timedOut', |
74 | always: [ | ||
75 | { | ||
76 | target: 'temporarilyOffline', | ||
77 | cond: 'needsNetwork', | ||
78 | in: '#offline', | ||
79 | }, | ||
80 | { target: 'socketCreated', in: '#tabVisible' }, | ||
81 | ], | ||
49 | on: { | 82 | on: { |
50 | TAB_VISIBLE: 'socketCreated', | 83 | PAGE_HIDE: 'pageHidden', |
84 | PAGE_FREEZE: 'pageHidden', | ||
51 | }, | 85 | }, |
52 | }, | 86 | }, |
53 | errorWait: { | 87 | errorWait: { |
54 | id: 'errorWait', | 88 | id: 'errorWait', |
89 | always: [ | ||
90 | { | ||
91 | target: 'temporarilyOffline', | ||
92 | cond: 'needsNetwork', | ||
93 | in: '#offline', | ||
94 | }, | ||
95 | ], | ||
55 | after: { | 96 | after: { |
56 | ERROR_WAIT_TIME: [ | 97 | ERROR_WAIT_TIME: 'timedOut', |
57 | { target: 'timedOut', in: '#tabHidden' }, | 98 | }, |
58 | { target: 'socketCreated' }, | 99 | on: { |
59 | ], | 100 | PAGE_HIDE: 'pageHidden', |
101 | PAGE_FREEZE: 'pageHidden', | ||
102 | }, | ||
103 | }, | ||
104 | temporarilyOffline: { | ||
105 | entry: ['clearErrors', 'notifyDisconnect'], | ||
106 | always: [{ target: 'timedOut', in: '#online' }], | ||
107 | on: { | ||
108 | PAGE_HIDE: 'pageHidden', | ||
109 | PAGE_FREEZE: 'pageHidden', | ||
110 | }, | ||
111 | }, | ||
112 | pageHidden: { | ||
113 | entry: 'clearErrors', | ||
114 | on: { | ||
115 | PAGE_SHOW: 'timedOut', | ||
116 | PAGE_RESUME: 'timedOut', | ||
60 | }, | 117 | }, |
61 | }, | 118 | }, |
62 | socketCreated: { | 119 | socketCreated: { |
@@ -68,6 +125,7 @@ export default createMachine( | |||
68 | initial: 'opening', | 125 | initial: 'opening', |
69 | states: { | 126 | states: { |
70 | opening: { | 127 | opening: { |
128 | always: [{ target: '#timedOut', in: '#tabHidden' }], | ||
71 | after: { | 129 | after: { |
72 | OPEN_TIMEOUT: { | 130 | OPEN_TIMEOUT: { |
73 | actions: 'raiseTimeoutError', | 131 | actions: 'raiseTimeoutError', |
@@ -76,7 +134,7 @@ export default createMachine( | |||
76 | on: { | 134 | on: { |
77 | OPENED: { | 135 | OPENED: { |
78 | target: 'opened', | 136 | target: 'opened', |
79 | actions: ['clearError', 'notifyReconnect'], | 137 | actions: ['clearErrors', 'notifyReconnect'], |
80 | }, | 138 | }, |
81 | }, | 139 | }, |
82 | }, | 140 | }, |
@@ -102,48 +160,38 @@ export default createMachine( | |||
102 | }, | 160 | }, |
103 | }, | 161 | }, |
104 | idle: { | 162 | idle: { |
105 | initial: 'getTabState', | 163 | initial: 'active', |
106 | states: { | 164 | states: { |
107 | getTabState: { | ||
108 | always: [ | ||
109 | { target: 'inactive', in: '#tabHidden' }, | ||
110 | 'active', | ||
111 | ], | ||
112 | }, | ||
113 | active: { | 165 | active: { |
114 | on: { | 166 | always: [{ target: 'inactive', in: '#tabHidden' }], |
115 | TAB_HIDDEN: 'inactive', | ||
116 | }, | ||
117 | }, | 167 | }, |
118 | inactive: { | 168 | inactive: { |
169 | always: [{ target: 'active', in: '#tabVisible' }], | ||
119 | after: { | 170 | after: { |
120 | IDLE_TIMEOUT: '#timedOut', | 171 | IDLE_TIMEOUT: '#timedOut', |
121 | }, | 172 | }, |
122 | on: { | ||
123 | TAB_VISIBLE: 'active', | ||
124 | }, | ||
125 | }, | 173 | }, |
126 | }, | 174 | }, |
127 | }, | 175 | }, |
128 | }, | 176 | }, |
129 | on: { | 177 | on: { |
130 | CONNECT: undefined, | 178 | CONNECT: undefined, |
131 | ERROR: { | 179 | ERROR: { target: '#errorWait', actions: 'pushError' }, |
132 | target: '#errorWait', | 180 | PAGE_HIDE: 'pageHidden', |
133 | actions: 'increaseRetryCount', | 181 | PAGE_FREEZE: 'pageHidden', |
134 | }, | ||
135 | }, | 182 | }, |
136 | }, | 183 | }, |
137 | }, | 184 | }, |
138 | on: { | 185 | on: { |
139 | CONNECT: { target: '.socketCreated', cond: 'hasWebSocketURL' }, | 186 | CONNECT: { target: '.timedOut', cond: 'hasWebSocketURL' }, |
140 | DISCONNECT: { target: '.disconnected', actions: 'clearError' }, | 187 | DISCONNECT: '.disconnected', |
141 | }, | 188 | }, |
142 | }, | 189 | }, |
143 | tab: { | 190 | tab: { |
144 | initial: 'visibleOrUnknown', | 191 | initial: 'visibleOrUnknown', |
145 | states: { | 192 | states: { |
146 | visibleOrUnknown: { | 193 | visibleOrUnknown: { |
194 | id: 'tabVisible', | ||
147 | on: { | 195 | on: { |
148 | TAB_HIDDEN: 'hidden', | 196 | TAB_HIDDEN: 'hidden', |
149 | }, | 197 | }, |
@@ -156,12 +204,19 @@ export default createMachine( | |||
156 | }, | 204 | }, |
157 | }, | 205 | }, |
158 | }, | 206 | }, |
159 | error: { | 207 | network: { |
160 | initial: 'init', | 208 | initial: 'onlineOrUnknown', |
161 | states: { | 209 | states: { |
162 | init: { | 210 | onlineOrUnknown: { |
211 | id: 'online', | ||
163 | on: { | 212 | on: { |
164 | ERROR: { actions: 'pushError' }, | 213 | OFFLINE: 'offline', |
214 | }, | ||
215 | }, | ||
216 | offline: { | ||
217 | id: 'offline', | ||
218 | on: { | ||
219 | ONLINE: 'onlineOrUnknown', | ||
165 | }, | 220 | }, |
166 | }, | 221 | }, |
167 | }, | 222 | }, |
@@ -171,12 +226,13 @@ export default createMachine( | |||
171 | { | 226 | { |
172 | guards: { | 227 | guards: { |
173 | hasWebSocketURL: ({ webSocketURL }) => webSocketURL !== undefined, | 228 | hasWebSocketURL: ({ webSocketURL }) => webSocketURL !== undefined, |
229 | needsNetwork: ({ webSocketURL }) => !isWebSocketURLLocal(webSocketURL), | ||
174 | }, | 230 | }, |
175 | delays: { | 231 | delays: { |
176 | IDLE_TIMEOUT: 300_000, | 232 | IDLE_TIMEOUT: 300_000, |
177 | OPEN_TIMEOUT: 5000, | 233 | OPEN_TIMEOUT: 10_000, |
178 | PING_PERIOD: 10_000, | 234 | PING_PERIOD: 10_000, |
179 | ERROR_WAIT_TIME: ({ retryCount }) => { | 235 | ERROR_WAIT_TIME: ({ errors: { length: retryCount } }) => { |
180 | const { length } = ERROR_WAIT_TIMES; | 236 | const { length } = ERROR_WAIT_TIMES; |
181 | const index = retryCount < length ? retryCount : length - 1; | 237 | const index = retryCount < length ? retryCount : length - 1; |
182 | return ERROR_WAIT_TIMES[index]; | 238 | return ERROR_WAIT_TIMES[index]; |
@@ -191,14 +247,9 @@ export default createMachine( | |||
191 | ...context, | 247 | ...context, |
192 | errors: [...context.errors, message], | 248 | errors: [...context.errors, message], |
193 | })), | 249 | })), |
194 | increaseRetryCount: assign((context) => ({ | 250 | clearErrors: assign((context) => ({ |
195 | ...context, | ||
196 | retryCount: context.retryCount + 1, | ||
197 | })), | ||
198 | clearError: assign((context) => ({ | ||
199 | ...context, | 251 | ...context, |
200 | errors: [], | 252 | errors: [], |
201 | retryCount: 0, | ||
202 | })), | 253 | })), |
203 | // Workaround from https://github.com/statelyai/xstate/issues/1414#issuecomment-699972485 | 254 | // Workaround from https://github.com/statelyai/xstate/issues/1414#issuecomment-699972485 |
204 | raiseTimeoutError: raise({ | 255 | raiseTimeoutError: raise({ |