aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/xtext/webSocketMachine.ts
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/xtext/webSocketMachine.ts')
-rw-r--r--subprojects/frontend/src/xtext/webSocketMachine.ts129
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];
7export interface WebSocketContext { 7export interface WebSocketContext {
8 webSocketURL: string | undefined; 8 webSocketURL: string | undefined;
9 errors: string[]; 9 errors: string[];
10 retryCount: number;
11} 10}
12 11
13export type WebSocketEvent = 12export 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
27export 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
22export default createMachine( 46export 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({