From 29919c02d86da10acf2b902fb9cab9998bb731a6 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 4 Sep 2022 20:44:39 +0200 Subject: 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 --- subprojects/frontend/src/xtext/webSocketMachine.ts | 215 +++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 subprojects/frontend/src/xtext/webSocketMachine.ts (limited to 'subprojects/frontend/src/xtext/webSocketMachine.ts') 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 @@ +import { actions, assign, createMachine, RaiseAction } from 'xstate'; + +const { raise } = actions; + +const ERROR_WAIT_TIMES = [200, 1000, 5000, 30_000]; + +export interface WebSocketContext { + webSocketURL: string | undefined; + errors: string[]; + retryCount: number; +} + +export type WebSocketEvent = + | { type: 'CONFIGURE'; webSocketURL: string } + | { type: 'CONNECT' } + | { type: 'DISCONNECT' } + | { type: 'OPENED' } + | { type: 'TAB_VISIBLE' } + | { type: 'TAB_HIDDEN' } + | { type: 'ERROR'; message: string }; + +export default createMachine( + { + id: 'webSocket', + predictableActionArguments: true, + schema: { + context: {} as WebSocketContext, + events: {} as WebSocketEvent, + }, + tsTypes: {} as import('./webSocketMachine.typegen').Typegen0, + context: { + webSocketURL: undefined, + errors: [], + retryCount: 0, + }, + type: 'parallel', + states: { + connection: { + initial: 'disconnected', + states: { + disconnected: { + id: 'disconnected', + on: { + CONFIGURE: { actions: 'configure' }, + }, + }, + timedOut: { + id: 'timedOut', + on: { + TAB_VISIBLE: 'socketCreated', + }, + }, + errorWait: { + id: 'errorWait', + after: { + ERROR_WAIT_TIME: [ + { target: 'timedOut', in: '#tabHidden' }, + { target: 'socketCreated' }, + ], + }, + }, + socketCreated: { + type: 'parallel', + entry: 'openWebSocket', + exit: ['cancelPendingRequests', 'closeWebSocket'], + states: { + open: { + initial: 'opening', + states: { + opening: { + after: { + OPEN_TIMEOUT: { + actions: 'raiseTimeoutError', + }, + }, + on: { + OPENED: { + target: 'opened', + actions: ['clearError', 'notifyReconnect'], + }, + }, + }, + opened: { + initial: 'pongReceived', + states: { + pongReceived: { + after: { + PING_PERIOD: 'pingSent', + }, + }, + pingSent: { + invoke: { + src: 'pingService', + onDone: 'pongReceived', + onError: { + actions: 'raisePromiseRejectionError', + }, + }, + }, + }, + }, + }, + }, + idle: { + initial: 'getTabState', + states: { + getTabState: { + always: [ + { target: 'inactive', in: '#tabHidden' }, + 'active', + ], + }, + active: { + on: { + TAB_HIDDEN: 'inactive', + }, + }, + inactive: { + after: { + IDLE_TIMEOUT: '#timedOut', + }, + on: { + TAB_VISIBLE: 'active', + }, + }, + }, + }, + }, + on: { + CONNECT: undefined, + ERROR: { + target: '#errorWait', + actions: 'increaseRetryCount', + }, + }, + }, + }, + on: { + CONNECT: { target: '.socketCreated', cond: 'hasWebSocketURL' }, + DISCONNECT: { target: '.disconnected', actions: 'clearError' }, + }, + }, + tab: { + initial: 'visibleOrUnknown', + states: { + visibleOrUnknown: { + on: { + TAB_HIDDEN: 'hidden', + }, + }, + hidden: { + id: 'tabHidden', + on: { + TAB_VISIBLE: 'visibleOrUnknown', + }, + }, + }, + }, + error: { + initial: 'init', + states: { + init: { + on: { + ERROR: { actions: 'pushError' }, + }, + }, + }, + }, + }, + }, + { + guards: { + hasWebSocketURL: ({ webSocketURL }) => webSocketURL !== undefined, + }, + delays: { + IDLE_TIMEOUT: 300_000, + OPEN_TIMEOUT: 5000, + PING_PERIOD: 10_000, + ERROR_WAIT_TIME: ({ retryCount }) => { + const { length } = ERROR_WAIT_TIMES; + const index = retryCount < length ? retryCount : length - 1; + return ERROR_WAIT_TIMES[index]; + }, + }, + actions: { + configure: assign((context, { webSocketURL }) => ({ + ...context, + webSocketURL, + })), + pushError: assign((context, { message }) => ({ + ...context, + errors: [...context.errors, message], + })), + increaseRetryCount: assign((context) => ({ + ...context, + retryCount: context.retryCount + 1, + })), + clearError: assign((context) => ({ + ...context, + errors: [], + retryCount: 0, + })), + // Workaround from https://github.com/statelyai/xstate/issues/1414#issuecomment-699972485 + raiseTimeoutError: raise({ + type: 'ERROR', + message: 'Open timeout', + }) as RaiseAction, + raisePromiseRejectionError: (_context, { data }) => + raise({ + type: 'ERROR', + message: data, + }) as RaiseAction, + }, + }, +); -- cgit v1.2.3-54-g00ecf