Browse Source

Define socket message middleware.

main
Titouan Rigoudy 4 years ago
parent
commit
fd6cb72625
8 changed files with 124 additions and 99 deletions
  1. +2
    -2
      src/app/store.ts
  2. +16
    -2
      src/components/LoginStatusPane.tsx
  3. +2
    -14
      src/containers/ConnectPage.tsx
  4. +50
    -24
      src/modules/login/message.ts
  5. +5
    -4
      src/modules/login/slice.ts
  6. +17
    -12
      src/modules/websocket/message.ts
  7. +31
    -41
      src/modules/websocket/middleware.ts
  8. +1
    -0
      src/modules/websocket/slice.ts

+ 2
- 2
src/app/store.ts View File

@ -12,7 +12,7 @@ import counterReducer from "../features/counter/counterSlice";
// import rooms from "../reducers/rooms";
// import users from "../reducers/users";
//
import { loginSocketMessageHandlers } from "../modules/login/message";
import { loginSocketMessageMiddleware } from "../modules/login/message";
import loginReducer from "../modules/login/slice";
import roomReducer from "../modules/room/slice";
import makeSocketMiddleware from "../modules/websocket/middleware";
@ -36,7 +36,7 @@ export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
makeSocketMiddleware(loginSocketMessageHandlers)
makeSocketMiddleware([loginSocketMessageMiddleware])
),
});


+ 16
- 2
src/components/LoginStatusPane.tsx View File

@ -1,12 +1,26 @@
import { FC } from "react";
import { FC, useEffect } from "react";
import { useDispatch } from "react-redux";
import { LoginStatus, LoginSliceState } from "../modules/login/slice";
import {
loginFetchStatus,
LoginStatus,
LoginSliceState,
} from "../modules/login/slice";
interface Props {
login: LoginSliceState;
}
const LoginStatusPane: FC<Props> = ({ login }) => {
const dispatch = useDispatch();
// Asynchronously fetch the login status if it is still unknown.
useEffect(() => {
if (login.status === LoginStatus.Unknown) {
dispatch(loginFetchStatus());
}
});
let statusText;
let motd;
let reason;


+ 2
- 14
src/containers/ConnectPage.tsx View File

@ -1,28 +1,16 @@
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import { useLocation } from "react-router";
import { Redirect } from "react-router-dom";
import ConnectForm from "../components/ConnectForm";
import { LoginStatus, selectLogin } from "../modules/login/slice";
import { loginStatusRequest } from "../modules/login/message";
import {
selectSocket,
socketSendMessage,
SocketState,
} from "../modules/websocket/slice";
import { selectSocket, SocketState } from "../modules/websocket/slice";
const ConnectPage: React.FC = () => {
const socket = useSelector(selectSocket);
const login = useSelector(selectLogin);
const location = useLocation();
const dispatch = useDispatch();
// If the socket is open and we are logged in, we can proceed to the app.
if (socket.state === SocketState.Open) {
if (login.status === LoginStatus.Unknown) {
dispatch(socketSendMessage(loginStatusRequest()));
}
// TODO: Only redirect once login status is known. The solstice client does
// not seem to respond to our request for the login status, however. Until
// that is the case, we cannot block on receiving a response...


+ 50
- 24
src/modules/login/message.ts View File

@ -1,44 +1,70 @@
import { loginSetState, LoginStatus } from "../login/slice";
import { SocketMessage, SocketMessageHandler } from "../websocket/message";
import { AppDispatch } from "../../app/store";
import { loginFetchStatus, loginSetState, LoginStatus } from "../login/slice";
import { SocketMessage, SocketMessageMiddleware } from "../websocket/message";
export function loginStatusRequest(): SocketMessage {
function loginStatusRequest(): SocketMessage {
return {
variant: "LoginStatusRequest",
fields: [],
};
}
const loginStatusResponseHandler: SocketMessageHandler = {
variant: "LoginStatusResponse",
function handleLoginStatusResponse(dispatch: AppDispatch, outerFields: any[]) {
if (outerFields.length !== 1) {
console.log("LoginStatusResponse has wrong number of fields:", outerFields);
return;
}
handleData: ({ variant, fields }) => {
switch (variant) {
case "Pending": {
const [username] = fields;
return loginSetState({
const { variant, fields } = outerFields[0];
switch (variant) {
case "Pending": {
const [username] = fields;
dispatch(
loginSetState({
status: LoginStatus.Pending,
username,
});
}
case "Success": {
const [username, motd] = fields;
return loginSetState({
})
);
break;
}
case "Success": {
const [username, motd] = fields;
dispatch(
loginSetState({
status: LoginStatus.Success,
username,
motd,
});
}
})
);
break;
}
case "Failure": {
const [username, reason] = fields;
return loginSetState({
case "Failure": {
const [username, reason] = fields;
dispatch(
loginSetState({
status: LoginStatus.Failure,
username,
reason,
});
}
})
);
break;
}
}
}
export const loginSocketMessageMiddleware: SocketMessageMiddleware = {
handleMessage: (dispatch, { variant, fields }) => {
switch (variant) {
case "LoginStatusResponse":
handleLoginStatusResponse(dispatch, fields);
break;
}
},
};
export const loginSocketMessageHandlers = [loginStatusResponseHandler];
handleAction: (send, action) => {
if (loginFetchStatus.match(action)) {
send(loginStatusRequest());
}
},
};

+ 5
- 4
src/modules/login/slice.ts View File

@ -1,4 +1,4 @@
import { createAction, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
@ -25,6 +25,9 @@ export const loginSlice = createSlice({
name: "login",
initialState,
reducers: {
loginFetchStatus: (state: LoginSliceState) => {
state.status = LoginStatus.Fetching;
},
loginSetState: (
state: LoginSliceState,
action: PayloadAction<LoginSliceState>
@ -34,9 +37,7 @@ export const loginSlice = createSlice({
},
});
export const { loginSetState } = loginSlice.actions;
export const loginFetchStatus = createAction<undefined>("loginFetchStatus");
export const { loginFetchStatus, loginSetState } = loginSlice.actions;
export const selectLogin = (state: RootState) => state.login;


+ 17
- 12
src/modules/websocket/message.ts View File

@ -2,6 +2,8 @@
import { PayloadAction } from "@reduxjs/toolkit";
import { AppDispatch } from "../../app/store";
// The type of a message exchanged over a websocket.
export interface SocketMessage {
variant: string;
@ -24,18 +26,21 @@ export function parseMessage(serialized: string): SocketMessage {
return { variant, fields };
}
// Handles the `data` field of a `SocketMessage` and returns an optional action.
export type SocketMessageDataHandler = (
data: any
) => PayloadAction<any> | undefined;
// An interface passed to socket message middleware, used to send messages.
export type SocketMessageSender = (message: SocketMessage) => void;
// A message handler creates an action out of a socket message.
// A message middleware translates messages into actions and vice-versa.
//
// Handlers are used by the websocket middleware to translate incoming messages
// into actions dispatched to the store. This way reducers can stay focused on
// their actions only. Otherwise, most reducers would have to handle all
// incoming messages.
export interface SocketMessageHandler {
variant: string;
handleData: SocketMessageDataHandler;
// It is passed to the main middleware of this module, and observes both
// messages received from the socket, and actions flowing to the Redux store.
//
// Messages can be translated into actions and dispatched to the store.
// Actions can be translated into messages and sent on the socket.
export interface SocketMessageMiddleware {
// Handles the given `message`, and possible `dispatch` some actions.
handleMessage: (dispatch: AppDispatch, message: SocketMessage) => void;
// Handles the given action, and possible `send` some messages.
// Note that this is only called when the socket is open.
handleAction: (send: SocketMessageSender, action: PayloadAction<any>) => void;
}

+ 31
- 41
src/modules/websocket/middleware.ts View File

@ -6,9 +6,11 @@
import { Middleware } from "redux";
import {
SocketMessageDataHandler,
SocketMessageHandler,
parseMessage,
serializeMessage,
SocketMessage,
SocketMessageMiddleware,
SocketMessageSender,
} from "./message";
import {
socketOpen,
@ -22,6 +24,13 @@ import { AppDispatch, RootState } from "../../app/store";
// The WebSocket singleton.
let socket: WebSocket | undefined;
function socketMessageSender(socket: WebSocket): SocketMessageSender {
return (message: SocketMessage) => {
console.log("WebSocket: sending message", message);
socket.send(serializeMessage(message));
};
}
const makeOnOpen = (dispatch: AppDispatch) => (event: Event) => {
const target = event.target as WebSocket;
console.log("WebSocket open", target.url);
@ -32,28 +41,11 @@ const makeOnClose = (dispatch: AppDispatch) => () => {
dispatch(socketClosed());
};
type HandlerMap = Map<string, SocketMessageDataHandler[]>;
function prepareHandlers(handlers: SocketMessageHandler[]): HandlerMap {
const map = new Map();
for (const handler of handlers) {
console.log(`Registering handler for message variant ${handler.variant}`);
let list = map.get(handler.variant);
if (list === undefined) {
list = [];
map.set(handler.variant, list);
}
list.push(handler.handleData);
}
return map;
}
const makeOnMessage =
(dispatch: AppDispatch, handlers: HandlerMap) => (event: MessageEvent) => {
const makeOnMessage = (
dispatch: AppDispatch,
middlewares: SocketMessageMiddleware[]
) => {
return (event: MessageEvent) => {
console.log("WebSocket received message", event.data);
let message;
@ -64,28 +56,21 @@ const makeOnMessage =
return;
}
const list = handlers.get(message.variant);
if (list === undefined) {
return;
}
for (const handler of list) {
const action = handler(message);
if (action !== undefined) {
dispatch(action);
}
for (const middleware of middlewares) {
middleware.handleMessage(dispatch, message);
}
};
};
type SocketFactory = (url: string) => WebSocket;
function makeSocketFactory(
dispatch: AppDispatch,
handlers: HandlerMap
middlewares: SocketMessageMiddleware[]
): SocketFactory {
const onOpen = makeOnOpen(dispatch);
const onClose = makeOnClose(dispatch);
const onMessage = makeOnMessage(dispatch, handlers);
const onMessage = makeOnMessage(dispatch, middlewares);
return (url) => {
socket = new WebSocket(url);
@ -101,12 +86,13 @@ function makeSocketFactory(
// See: https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middleware
function makeMiddleware(
handlers: SocketMessageHandler[]
messageMiddlewares: SocketMessageMiddleware[]
): Middleware<{}, RootState> {
const handlerMap = prepareHandlers(handlers);
return (storeApi) => {
const socketFactory = makeSocketFactory(storeApi.dispatch, handlerMap);
const socketFactory = makeSocketFactory(
storeApi.dispatch,
messageMiddlewares
);
return (next) => (action) => {
if (socketOpen.match(action)) {
@ -128,10 +114,14 @@ function makeMiddleware(
} else if (socketSendMessage.match(action)) {
if (socket !== undefined) {
console.log("WebSocket sending message", action.payload);
socket.send(JSON.stringify(action.payload));
socket.send(serializeMessage(action.payload));
} else {
console.log("Ignoring socketSendMessage action, socket is closed.");
}
} else if (socket !== undefined) {
for (const middleware of messageMiddlewares) {
middleware.handleAction(socketMessageSender(socket), action);
}
}
return next(action);


+ 1
- 0
src/modules/websocket/slice.ts View File

@ -45,6 +45,7 @@ export const { socketOpen, socketOpened, socketClose, socketClosed } =
export const selectSocket = (state: RootState) => state.socket;
// TODO: Remove this in favor of message middlewares.
export const socketSendMessage =
createAction<SocketMessage>("socketSendMessage");


Loading…
Cancel
Save