-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.ts
More file actions
142 lines (125 loc) · 5.32 KB
/
index.ts
File metadata and controls
142 lines (125 loc) · 5.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import {useRef} from 'react'
import {createMachine} from "xstate";
import {useMachine} from "@xstate/react";
export enum LState {
SILENT_LOADING = 'SILENT_LOADING', // for: do loading but not to show loading animation
LOADING = 'LOADING', // for: show loading animation
DELAY_DONE = 'DELAY_DONE', // for: loading finished but keep showing loading animation
DONE = 'DONE', // for: all ready
}
enum FSMEvent {
START_LOADING = 'START_LOADING',
LOADING_FINISHED = 'LOADING_FINISHED',
SET_DONE = 'SET_DONE',
RESET = 'RESET',
}
const createFSM = (initialState: LState, resetState: LState) => createMachine({
id: 'loading_fsm',
initial: initialState,
states: {
[LState.SILENT_LOADING]: {
on: {
[FSMEvent.START_LOADING]: LState.LOADING,
[FSMEvent.LOADING_FINISHED]: LState.DELAY_DONE,
[FSMEvent.SET_DONE]: LState.DONE,
[FSMEvent.RESET]: resetState,
},
},
[LState.LOADING]: {
on: {
[FSMEvent.LOADING_FINISHED]: LState.DELAY_DONE,
[FSMEvent.SET_DONE]: LState.DONE,
[FSMEvent.RESET]: resetState,
},
},
[LState.DELAY_DONE]: {
on: {
[FSMEvent.SET_DONE]: LState.DONE,
[FSMEvent.RESET]: resetState,
},
},
[LState.DONE]: {
on: {
[FSMEvent.RESET]: resetState,
},
},
}
});
// Utility functions
export const isLoading = (state: LState) => LState.LOADING === state;
export const isReloading = (state: LState) => [LState.SILENT_LOADING, LState.LOADING].includes(state);
export const isDone = (state: LState) => LState.DONE === state;
interface Options {
renderDelay?: number; // timeout of SILENT_LOADING state after started
resetDelay?: number; // timeout of SILENT_LOADING state after reset
doneDelay?: number; // timeout of DELAY_DONE state
name?: string; // for debug log (useful if there are multiple pages / components use this hook)
}
export const useLoadingSteps = (totalSteps: number, initialLoaded: boolean = false, options: Options = {}): [
loadingState: LState,
setStepDone: (stepName: string) => void,
resetLoading: () => void,
skipStep: (stepName: string) => void
] => {
const {renderDelay = 0, resetDelay = 0, doneDelay = 0, name = 'Loading'} = options;
let initialState: LState = LState.DONE;
if (!initialLoaded) {
initialState = renderDelay ? LState.SILENT_LOADING : LState.LOADING;
}
const resetState: LState = resetDelay ? LState.SILENT_LOADING : LState.LOADING;
const fsm = useRef(createFSM(initialState, resetState));
const [loadingState, transition, fsmService] = useMachine(fsm.current);
const silentLoadingTimeout = useRef(renderDelay);
const lastState = useRef("<init>");
const doneSteps = useRef(new Map());
const timers = useRef([] as number[]);
const startTime = useRef(new Date());
const elapsed = (): number => (new Date().getTime()) - startTime.current.getTime();
const debug = (msg: string) => !isProdEnv() && console.debug(`[${name}]${msg} @${elapsed()}ms`);
const isFinished = () => doneSteps.current.size >= totalSteps;
fsmService.onTransition(state => {
if (lastState.current === state.value) {
return;
}
debug(`[transition] ${lastState.current} -> ${state.value}`);
lastState.current = state.value.toString();
if (state.value === LState.SILENT_LOADING) {
timers.current.push(delayExec(() => transition(FSMEvent.START_LOADING), silentLoadingTimeout.current)!);
}
if (state.value === LState.DELAY_DONE) {
timers.current.push(delayExec(() => transition(FSMEvent.SET_DONE), doneDelay)!);
}
});
const addStep = (stepName: string, msg: string): void => {
if (isFinished()) {
return;
}
doneSteps.current.set(stepName, true);
debug(`[${doneSteps.current.size}/${totalSteps}] ${msg}: ${stepName}`);
// When the SILENT_LOADING onTransition callback not called yet (due to delay caused by js single thread)
if (elapsed() > silentLoadingTimeout.current) {
transition(FSMEvent.START_LOADING);
}
if (isFinished()) {
transition(doneDelay ? FSMEvent.LOADING_FINISHED : FSMEvent.SET_DONE);
}
};
const setStepDone = (stepName: string) => addStep(stepName, 'done');
const skipStep = (stepName: string) => addStep(stepName, 'skipped');
const resetLoading = () => {
doneSteps.current.clear();
startTime.current = new Date();
silentLoadingTimeout.current = Math.max(resetDelay, renderDelay);
debug(`[reset] delay ${silentLoadingTimeout.current}ms resetting to ${resetState}`);
timers.current.forEach(timer => Number.isInteger(timer) && clearTimeout(timer));
timers.current.length = 0;
transition(FSMEvent.RESET);
};
return [<LState>loadingState.value.toString(), setStepDone, resetLoading, skipStep];
};
const isProdEnv = () => process.env.NODE_ENV === "production";
const delayExec = (fn: Function, delay: number): number | null => {
if (delay > 0) return setTimeout(fn, delay);
fn();
return null;
};