diff options
author | 2022-09-04 20:44:39 +0200 | |
---|---|---|
committer | 2022-09-06 01:05:23 +0200 | |
commit | 29919c02d86da10acf2b902fb9cab9998bb731a6 (patch) | |
tree | e4ac7dc9bc035327c720514363edee938248c14a /subprojects/frontend/src/xtext/webSocketMachine.ts | |
parent | refactor(frontend): add eslint-plugin-mobx (diff) | |
download | refinery-29919c02d86da10acf2b902fb9cab9998bb731a6.tar.gz refinery-29919c02d86da10acf2b902fb9cab9998bb731a6.tar.zst refinery-29919c02d86da10acf2b902fb9cab9998bb731a6.zip |
feat(frontend): XState statecharts
Expressing logic in statecharts for complex stateful behaviours should
improve maintainability
We use @xstate/cli to statically analyze statcharts before typechecking
Diffstat (limited to 'subprojects/frontend/src/xtext/webSocketMachine.ts')
-rw-r--r-- | subprojects/frontend/src/xtext/webSocketMachine.ts | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/subprojects/frontend/src/xtext/webSocketMachine.ts b/subprojects/frontend/src/xtext/webSocketMachine.ts new file mode 100644 index 00000000..50eb36a0 --- /dev/null +++ b/subprojects/frontend/src/xtext/webSocketMachine.ts | |||
@@ -0,0 +1,215 @@ | |||
1 | import { actions, assign, createMachine, RaiseAction } from 'xstate'; | ||
2 | |||
3 | const { raise } = actions; | ||
4 | |||
5 | const ERROR_WAIT_TIMES = [200, 1000, 5000, 30_000]; | ||
6 | |||
7 | export interface WebSocketContext { | ||
8 | webSocketURL: string | undefined; | ||
9 | errors: string[]; | ||
10 | retryCount: number; | ||
11 | } | ||
12 | |||
13 | export type WebSocketEvent = | ||
14 | | { type: 'CONFIGURE'; webSocketURL: string } | ||
15 | | { type: 'CONNECT' } | ||
16 | | { type: 'DISCONNECT' } | ||
17 | | { type: 'OPENED' } | ||
18 | | { type: 'TAB_VISIBLE' } | ||
19 | | { type: 'TAB_HIDDEN' } | ||
20 | | { type: 'ERROR'; message: string }; | ||
21 | |||
22 | export default createMachine( | ||
23 | { | ||
24 | id: 'webSocket', | ||
25 | predictableActionArguments: true, | ||
26 | schema: { | ||
27 | context: {} as WebSocketContext, | ||
28 | events: {} as WebSocketEvent, | ||
29 | }, | ||
30 | tsTypes: {} as import('./webSocketMachine.typegen').Typegen0, | ||
31 | context: { | ||
32 | webSocketURL: undefined, | ||
33 | errors: [], | ||
34 | retryCount: 0, | ||
35 | }, | ||
36 | type: 'parallel', | ||
37 | states: { | ||
38 | connection: { | ||
39 | initial: 'disconnected', | ||
40 | states: { | ||
41 | disconnected: { | ||
42 | id: 'disconnected', | ||
43 | on: { | ||
44 | CONFIGURE: { actions: 'configure' }, | ||
45 | }, | ||
46 | }, | ||
47 | timedOut: { | ||
48 | id: 'timedOut', | ||
49 | on: { | ||
50 | TAB_VISIBLE: 'socketCreated', | ||
51 | }, | ||
52 | }, | ||
53 | errorWait: { | ||
54 | id: 'errorWait', | ||
55 | after: { | ||
56 | ERROR_WAIT_TIME: [ | ||
57 | { target: 'timedOut', in: '#tabHidden' }, | ||
58 | { target: 'socketCreated' }, | ||
59 | ], | ||
60 | }, | ||
61 | }, | ||
62 | socketCreated: { | ||
63 | type: 'parallel', | ||
64 | entry: 'openWebSocket', | ||
65 | exit: ['cancelPendingRequests', 'closeWebSocket'], | ||
66 | states: { | ||
67 | open: { | ||
68 | initial: 'opening', | ||
69 | states: { | ||
70 | opening: { | ||
71 | after: { | ||
72 | OPEN_TIMEOUT: { | ||
73 | actions: 'raiseTimeoutError', | ||
74 | }, | ||
75 | }, | ||
76 | on: { | ||
77 | OPENED: { | ||
78 | target: 'opened', | ||
79 | actions: ['clearError', 'notifyReconnect'], | ||
80 | }, | ||
81 | }, | ||
82 | }, | ||
83 | opened: { | ||
84 | initial: 'pongReceived', | ||
85 | states: { | ||
86 | pongReceived: { | ||
87 | after: { | ||
88 | PING_PERIOD: 'pingSent', | ||
89 | }, | ||
90 | }, | ||
91 | pingSent: { | ||
92 | invoke: { | ||
93 | src: 'pingService', | ||
94 | onDone: 'pongReceived', | ||
95 | onError: { | ||
96 | actions: 'raisePromiseRejectionError', | ||
97 | }, | ||
98 | }, | ||
99 | }, | ||
100 | }, | ||
101 | }, | ||
102 | }, | ||
103 | }, | ||
104 | idle: { | ||
105 | initial: 'getTabState', | ||
106 | states: { | ||
107 | getTabState: { | ||
108 | always: [ | ||
109 | { target: 'inactive', in: '#tabHidden' }, | ||
110 | 'active', | ||
111 | ], | ||
112 | }, | ||
113 | active: { | ||
114 | on: { | ||
115 | TAB_HIDDEN: 'inactive', | ||
116 | }, | ||
117 | }, | ||
118 | inactive: { | ||
119 | after: { | ||
120 | IDLE_TIMEOUT: '#timedOut', | ||
121 | }, | ||
122 | on: { | ||
123 | TAB_VISIBLE: 'active', | ||
124 | }, | ||
125 | }, | ||
126 | }, | ||
127 | }, | ||
128 | }, | ||
129 | on: { | ||
130 | CONNECT: undefined, | ||
131 | ERROR: { | ||
132 | target: '#errorWait', | ||
133 | actions: 'increaseRetryCount', | ||
134 | }, | ||
135 | }, | ||
136 | }, | ||
137 | }, | ||
138 | on: { | ||
139 | CONNECT: { target: '.socketCreated', cond: 'hasWebSocketURL' }, | ||
140 | DISCONNECT: { target: '.disconnected', actions: 'clearError' }, | ||
141 | }, | ||
142 | }, | ||
143 | tab: { | ||
144 | initial: 'visibleOrUnknown', | ||
145 | states: { | ||
146 | visibleOrUnknown: { | ||
147 | on: { | ||
148 | TAB_HIDDEN: 'hidden', | ||
149 | }, | ||
150 | }, | ||
151 | hidden: { | ||
152 | id: 'tabHidden', | ||
153 | on: { | ||
154 | TAB_VISIBLE: 'visibleOrUnknown', | ||
155 | }, | ||
156 | }, | ||
157 | }, | ||
158 | }, | ||
159 | error: { | ||
160 | initial: 'init', | ||
161 | states: { | ||
162 | init: { | ||
163 | on: { | ||
164 | ERROR: { actions: 'pushError' }, | ||
165 | }, | ||
166 | }, | ||
167 | }, | ||
168 | }, | ||
169 | }, | ||
170 | }, | ||
171 | { | ||
172 | guards: { | ||
173 | hasWebSocketURL: ({ webSocketURL }) => webSocketURL !== undefined, | ||
174 | }, | ||
175 | delays: { | ||
176 | IDLE_TIMEOUT: 300_000, | ||
177 | OPEN_TIMEOUT: 5000, | ||
178 | PING_PERIOD: 10_000, | ||
179 | ERROR_WAIT_TIME: ({ retryCount }) => { | ||
180 | const { length } = ERROR_WAIT_TIMES; | ||
181 | const index = retryCount < length ? retryCount : length - 1; | ||
182 | return ERROR_WAIT_TIMES[index]; | ||
183 | }, | ||
184 | }, | ||
185 | actions: { | ||
186 | configure: assign((context, { webSocketURL }) => ({ | ||
187 | ...context, | ||
188 | webSocketURL, | ||
189 | })), | ||
190 | pushError: assign((context, { message }) => ({ | ||
191 | ...context, | ||
192 | errors: [...context.errors, message], | ||
193 | })), | ||
194 | increaseRetryCount: assign((context) => ({ | ||
195 | ...context, | ||
196 | retryCount: context.retryCount + 1, | ||
197 | })), | ||
198 | clearError: assign((context) => ({ | ||
199 | ...context, | ||
200 | errors: [], | ||
201 | retryCount: 0, | ||
202 | })), | ||
203 | // Workaround from https://github.com/statelyai/xstate/issues/1414#issuecomment-699972485 | ||
204 | raiseTimeoutError: raise({ | ||
205 | type: 'ERROR', | ||
206 | message: 'Open timeout', | ||
207 | }) as RaiseAction<WebSocketEvent>, | ||
208 | raisePromiseRejectionError: (_context, { data }) => | ||
209 | raise({ | ||
210 | type: 'ERROR', | ||
211 | message: data, | ||
212 | }) as RaiseAction<WebSocketEvent>, | ||
213 | }, | ||
214 | }, | ||
215 | ); | ||