aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/xtext/webSocketMachine.ts
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-09-04 20:44:39 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-09-06 01:05:23 +0200
commit29919c02d86da10acf2b902fb9cab9998bb731a6 (patch)
treee4ac7dc9bc035327c720514363edee938248c14a /subprojects/frontend/src/xtext/webSocketMachine.ts
parentrefactor(frontend): add eslint-plugin-mobx (diff)
downloadrefinery-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.ts215
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 @@
1import { actions, assign, createMachine, RaiseAction } from 'xstate';
2
3const { raise } = actions;
4
5const ERROR_WAIT_TIMES = [200, 1000, 5000, 30_000];
6
7export interface WebSocketContext {
8 webSocketURL: string | undefined;
9 errors: string[];
10 retryCount: number;
11}
12
13export 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
22export 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);