Refactor app state to be a state machine with access selectors
This commit is contained in:
@@ -1,31 +1,32 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { useEffect } from 'react';
|
||||
import { atom, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { atom, selector, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useMachine } from '@xstate/react';
|
||||
import { makeEmptyClientConfig, ClientConfig } from '../../interfaces/client-config.model';
|
||||
import ClientConfigService from '../../services/client-config-service';
|
||||
import ChatService from '../../services/chat-service';
|
||||
import WebsocketService from '../../services/websocket-service';
|
||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||
import { ServerStatus, makeEmptyServerStatus } from '../../interfaces/server-status.model';
|
||||
|
||||
import {
|
||||
AppState,
|
||||
ChatState,
|
||||
VideoState,
|
||||
ChatVisibilityState,
|
||||
getChatState,
|
||||
getChatVisibilityState,
|
||||
} from '../../interfaces/application-state';
|
||||
import appStateModel, {
|
||||
AppStateEvent,
|
||||
AppStateOptions,
|
||||
makeEmptyAppState,
|
||||
} from './application-state';
|
||||
import { setLocalStorage, getLocalStorage } from '../../utils/helpers';
|
||||
import {
|
||||
ConnectedClientInfoEvent,
|
||||
MessageType,
|
||||
ChatEvent,
|
||||
SocketEvent,
|
||||
} from '../../interfaces/socket-events';
|
||||
import handleConnectedClientInfoMessage from './eventhandlers/connectedclientinfo';
|
||||
|
||||
import handleChatMessage from './eventhandlers/handleChatMessage';
|
||||
import handleConnectedClientInfoMessage from './eventhandlers/connected-client-info-handler';
|
||||
import ServerStatusService from '../../services/status-service';
|
||||
|
||||
const SERVER_STATUS_POLL_DURATION = 5000;
|
||||
const ACCESS_TOKEN_KEY = 'accessToken';
|
||||
|
||||
// Server status is what gets updated such as viewer count, durations,
|
||||
// stream title, online/offline state, etc.
|
||||
export const serverStatusState = atom<ServerStatus>({
|
||||
@@ -39,26 +40,6 @@ export const clientConfigStateAtom = atom({
|
||||
default: makeEmptyClientConfig(),
|
||||
});
|
||||
|
||||
export const appStateAtom = atom<AppState>({
|
||||
key: 'appStateAtom',
|
||||
default: AppState.Loading,
|
||||
});
|
||||
|
||||
export const chatStateAtom = atom<ChatState>({
|
||||
key: 'chatStateAtom',
|
||||
default: ChatState.Offline,
|
||||
});
|
||||
|
||||
export const videoStateAtom = atom<VideoState>({
|
||||
key: 'videoStateAtom',
|
||||
default: VideoState.Unavailable,
|
||||
});
|
||||
|
||||
export const chatVisibilityAtom = atom<ChatVisibilityState>({
|
||||
key: 'chatVisibility',
|
||||
default: ChatVisibilityState.Visible,
|
||||
});
|
||||
|
||||
export const chatDisplayNameAtom = atom<string>({
|
||||
key: 'chatDisplayName',
|
||||
default: null,
|
||||
@@ -79,23 +60,79 @@ export const websocketServiceAtom = atom<WebsocketService>({
|
||||
default: null,
|
||||
});
|
||||
|
||||
export const appStateAtom = atom<AppStateOptions>({
|
||||
key: 'appState',
|
||||
default: makeEmptyAppState(),
|
||||
});
|
||||
|
||||
export const chatVisibleToggleAtom = atom<boolean>({
|
||||
key: 'chatVisibilityToggleAtom',
|
||||
default: true,
|
||||
});
|
||||
|
||||
export const isVideoPlayingAtom = atom<boolean>({
|
||||
key: 'isVideoPlayingAtom',
|
||||
default: false,
|
||||
});
|
||||
|
||||
// Chat is visible if the user wishes it to be visible AND the required
|
||||
// chat state is set.
|
||||
export const isChatVisibleSelector = selector({
|
||||
key: 'isChatVisibleSelector',
|
||||
get: ({ get }) => {
|
||||
const state: AppStateOptions = get(appStateAtom);
|
||||
const userVisibleToggle: boolean = get(chatVisibleToggleAtom);
|
||||
const accessToken: String = get(accessTokenAtom);
|
||||
return accessToken && state.chatAvailable && userVisibleToggle;
|
||||
},
|
||||
});
|
||||
|
||||
// We display in an "online/live" state as long as video is actively playing.
|
||||
// Even during the time where technically the server has said it's no longer
|
||||
// live, however the last few seconds of video playback is still taking place.
|
||||
export const isOnlineSelector = selector({
|
||||
key: 'isOnlineSelector',
|
||||
get: ({ get }) => {
|
||||
const state: AppStateOptions = get(appStateAtom);
|
||||
const isVideoPlaying: boolean = get(isVideoPlayingAtom);
|
||||
return state.videoAvailable || isVideoPlaying;
|
||||
},
|
||||
});
|
||||
|
||||
// Take a nested object of state metadata and merge it into
|
||||
// a single flattened node.
|
||||
function mergeMeta(meta) {
|
||||
return Object.keys(meta).reduce((acc, key) => {
|
||||
const value = meta[key];
|
||||
Object.assign(acc, value);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function ClientConfigStore() {
|
||||
const [appState, appStateSend, appStateService] = useMachine(appStateModel);
|
||||
|
||||
const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
|
||||
const setClientConfig = useSetRecoilState<ClientConfig>(clientConfigStateAtom);
|
||||
const setServerStatus = useSetRecoilState<ServerStatus>(serverStatusState);
|
||||
const setChatVisibility = useSetRecoilState<ChatVisibilityState>(chatVisibilityAtom);
|
||||
const setChatState = useSetRecoilState<ChatState>(chatStateAtom);
|
||||
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
|
||||
const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
|
||||
const [appState, setAppState] = useRecoilState<AppState>(appStateAtom);
|
||||
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
||||
const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom);
|
||||
const setAppState = useSetRecoilState<AppStateOptions>(appStateAtom);
|
||||
|
||||
const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom);
|
||||
let ws: WebsocketService;
|
||||
|
||||
const sendEvent = (event: string) => {
|
||||
// console.log('---- sending event:', event);
|
||||
appStateSend({ type: event });
|
||||
};
|
||||
|
||||
const updateClientConfig = async () => {
|
||||
try {
|
||||
const config = await ClientConfigService.getConfig();
|
||||
setClientConfig(config);
|
||||
sendEvent('LOADED');
|
||||
} catch (error) {
|
||||
console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`);
|
||||
}
|
||||
@@ -105,32 +142,42 @@ export function ClientConfigStore() {
|
||||
try {
|
||||
const status = await ServerStatusService.getStatus();
|
||||
setServerStatus(status);
|
||||
|
||||
if (status.online) {
|
||||
setAppState(AppState.Online);
|
||||
} else {
|
||||
setAppState(AppState.Offline);
|
||||
sendEvent(AppStateEvent.Online);
|
||||
} else if (!status.online) {
|
||||
sendEvent(AppStateEvent.Offline);
|
||||
}
|
||||
return status;
|
||||
} catch (error) {
|
||||
sendEvent(AppStateEvent.Fail);
|
||||
console.error(`serverStatusState -> getStatus() ERROR: \n${error}`);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleUserRegistration = async (optionalDisplayName?: string) => {
|
||||
const savedAccessToken = getLocalStorage(ACCESS_TOKEN_KEY);
|
||||
if (savedAccessToken) {
|
||||
setAccessToken(savedAccessToken);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setAppState(AppState.Registering);
|
||||
sendEvent(AppStateEvent.NeedsRegister);
|
||||
const response = await ChatService.registerUser(optionalDisplayName);
|
||||
console.log(`ChatService -> registerUser() response: \n${response}`);
|
||||
const { accessToken: newAccessToken, displayName: newDisplayName } = response;
|
||||
if (!newAccessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('setting access token', newAccessToken);
|
||||
setAccessToken(newAccessToken);
|
||||
// setLocalStorage('accessToken', newAccessToken);
|
||||
setLocalStorage(ACCESS_TOKEN_KEY, newAccessToken);
|
||||
setChatDisplayName(newDisplayName);
|
||||
// sendEvent(AppStateEvent.Registered);
|
||||
} catch (e) {
|
||||
sendEvent(AppStateEvent.Fail);
|
||||
console.error(`ChatService -> registerUser() ERROR: \n${e}`);
|
||||
}
|
||||
};
|
||||
@@ -138,7 +185,7 @@ export function ClientConfigStore() {
|
||||
const handleMessage = (message: SocketEvent) => {
|
||||
switch (message.type) {
|
||||
case MessageType.CONNECTED_USER_INFO:
|
||||
handleConnectedClientInfoMessage(message as ConnectedClientInfoEvent);
|
||||
handleConnectedClientInfoMessage(message as ConnectedClientInfoEvent, setChatDisplayName);
|
||||
break;
|
||||
case MessageType.CHAT:
|
||||
handleChatMessage(message as ChatEvent, chatMessages, setChatMessages);
|
||||
@@ -159,11 +206,12 @@ export function ClientConfigStore() {
|
||||
};
|
||||
|
||||
const startChat = async () => {
|
||||
setChatState(ChatState.Loading);
|
||||
sendEvent(AppStateEvent.Loading);
|
||||
try {
|
||||
ws = new WebsocketService(accessToken, '/ws');
|
||||
ws.handleMessage = handleMessage;
|
||||
setWebsocketService(ws);
|
||||
sendEvent(AppStateEvent.Loaded);
|
||||
} catch (error) {
|
||||
console.error(`ChatService -> startChat() ERROR: \n${error}`);
|
||||
}
|
||||
@@ -172,14 +220,11 @@ export function ClientConfigStore() {
|
||||
useEffect(() => {
|
||||
updateClientConfig();
|
||||
handleUserRegistration();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updateServerStatus();
|
||||
setInterval(() => {
|
||||
updateServerStatus();
|
||||
}, 5000);
|
||||
updateServerStatus();
|
||||
}, []);
|
||||
}, SERVER_STATUS_POLL_DURATION);
|
||||
}, [appState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken) {
|
||||
@@ -190,21 +235,18 @@ export function ClientConfigStore() {
|
||||
startChat();
|
||||
}, [accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
const updatedChatState = getChatState(appState);
|
||||
console.log('updatedChatState', updatedChatState);
|
||||
setChatState(updatedChatState);
|
||||
const updatedChatVisibility = getChatVisibilityState(appState);
|
||||
console.log(
|
||||
'app state: ',
|
||||
AppState[appState],
|
||||
'chat state:',
|
||||
ChatState[updatedChatState],
|
||||
'chat visibility:',
|
||||
ChatVisibilityState[updatedChatVisibility],
|
||||
);
|
||||
setChatVisibility(updatedChatVisibility);
|
||||
}, [appState]);
|
||||
appStateService.onTransition(state => {
|
||||
if (!state.changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = mergeMeta(state.meta) as AppStateOptions;
|
||||
|
||||
console.log('--- APP STATE: ', state.value);
|
||||
console.log('--- APP META: ', metadata);
|
||||
|
||||
setAppState(metadata);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user