diff options
Diffstat (limited to 'subprojects/frontend/src/xtext/webSocketMachine.ts')
-rw-r--r-- | subprojects/frontend/src/xtext/webSocketMachine.ts | 129 |
1 files changed, 90 insertions, 39 deletions
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({ |