|
|
|
@ -17,24 +17,24 @@ import { |
|
|
|
socketClosed, |
|
|
|
socketSendMessage, |
|
|
|
} from "./slice"; |
|
|
|
import { AppDispatch, RootState } from "../app/store"; |
|
|
|
import { AppDispatch, RootState } from "../../app/store"; |
|
|
|
|
|
|
|
// The WebSocket singleton.
|
|
|
|
// TODO: use undefined instead, it is more idiomatic.
|
|
|
|
let socket: WebSocket | null = null; |
|
|
|
let socket: WebSocket | undefined; |
|
|
|
|
|
|
|
const onOpen = (dispatch: AppDispatch) => (event) => { |
|
|
|
console.log("Websocket open", event.target.url); |
|
|
|
dispatch(socketOpened(event.target.url)); |
|
|
|
const makeOnOpen = (dispatch: AppDispatch) => (event: Event) => { |
|
|
|
const target = event.target as WebSocket; |
|
|
|
console.log("WebSocket open", target.url); |
|
|
|
dispatch(socketOpened(target.url)); |
|
|
|
}; |
|
|
|
|
|
|
|
const onClose = (dispatch: AppDispatch) => () => { |
|
|
|
const makeOnClose = (dispatch: AppDispatch) => () => { |
|
|
|
dispatch(socketClosed()); |
|
|
|
}; |
|
|
|
|
|
|
|
function prepareHandlers( |
|
|
|
handlers: SocketMessageHandler[] |
|
|
|
): Map<string, SocketMessageDataHandler[]> { |
|
|
|
type HandlerMap = Map<string, SocketMessageDataHandler[]>; |
|
|
|
|
|
|
|
function prepareHandlers(handlers: SocketMessageHandler[]): HandlerMap { |
|
|
|
const map = new Map(); |
|
|
|
|
|
|
|
for (const handler of handlers) { |
|
|
|
@ -52,16 +52,15 @@ function prepareHandlers( |
|
|
|
return map; |
|
|
|
} |
|
|
|
|
|
|
|
const onMessage = |
|
|
|
(dispatch: AppDispatch, handlers: Map<string, SocketMessageDataHandler[]>) => |
|
|
|
(event: MessageEvent) => { |
|
|
|
console.log("Websocket received message", event.data); |
|
|
|
const makeOnMessage = |
|
|
|
(dispatch: AppDispatch, handlers: HandlerMap) => (event: MessageEvent) => { |
|
|
|
console.log("WebSocket received message", event.data); |
|
|
|
|
|
|
|
let message; |
|
|
|
try { |
|
|
|
message = parseMessage(event.data); |
|
|
|
} catch (err) { |
|
|
|
console.log("Websocket received invalid message:", err); |
|
|
|
console.log("WebSocket received invalid message:", err); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
@ -70,7 +69,7 @@ const onMessage = |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
for (handler of list) { |
|
|
|
for (const handler of list) { |
|
|
|
const action = handler(message); |
|
|
|
if (action !== undefined) { |
|
|
|
dispatch(action); |
|
|
|
@ -78,38 +77,65 @@ const onMessage = |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
type SocketFactory = (url: string) => WebSocket; |
|
|
|
|
|
|
|
function makeSocketFactory( |
|
|
|
dispatch: AppDispatch, |
|
|
|
handlers: HandlerMap |
|
|
|
): SocketFactory { |
|
|
|
const onOpen = makeOnOpen(dispatch); |
|
|
|
const onClose = makeOnClose(dispatch); |
|
|
|
const onMessage = makeOnMessage(dispatch, handlers); |
|
|
|
|
|
|
|
return (url) => { |
|
|
|
socket = new WebSocket(url); |
|
|
|
|
|
|
|
// Bind websocket handlers.
|
|
|
|
socket.onopen = onOpen; |
|
|
|
socket.onclose = onClose; |
|
|
|
socket.onmessage = onMessage; |
|
|
|
|
|
|
|
return socket; |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
// See: https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middleware
|
|
|
|
function makeMiddleware( |
|
|
|
handlers: SocketMessageHandler[] |
|
|
|
): Middleware<{}, RootState> { |
|
|
|
const handlerMap = prepareHandlers(handlers); |
|
|
|
|
|
|
|
return (storeApi) => (next) => (action) => { |
|
|
|
if (socketOpen.match(action)) { |
|
|
|
if (socket !== null) { |
|
|
|
socket.close(); |
|
|
|
return (storeApi) => { |
|
|
|
const socketFactory = makeSocketFactory(storeApi.dispatch, handlerMap); |
|
|
|
|
|
|
|
return (next) => (action) => { |
|
|
|
if (socketOpen.match(action)) { |
|
|
|
if (socket === undefined) { |
|
|
|
const url = action.payload; |
|
|
|
console.log(`Opening WebSocket to ${url}`); |
|
|
|
socket = socketFactory(url); |
|
|
|
} else { |
|
|
|
console.log("Ignoring socketOpen action, socket is already open."); |
|
|
|
} |
|
|
|
} else if (socketClose.match(action)) { |
|
|
|
if (socket !== undefined) { |
|
|
|
socket.close(); |
|
|
|
socket = undefined; |
|
|
|
console.log("WebSocket closed."); |
|
|
|
} else { |
|
|
|
console.log("Ignoring socketClose action, socket is already closed."); |
|
|
|
} |
|
|
|
} else if (socketSendMessage.match(action)) { |
|
|
|
if (socket !== undefined) { |
|
|
|
console.log("WebSocket sending message", action.payload); |
|
|
|
socket.send(JSON.stringify(action.payload)); |
|
|
|
} else { |
|
|
|
console.log("Ignoring socketSendMessage action, socket is closed."); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Connect to the remote host.
|
|
|
|
socket = new WebSocket(action.payload); |
|
|
|
|
|
|
|
// Bind websocket handlers.
|
|
|
|
socket.onmessage = onMessage(storeApi.dispatch, handlerMap); |
|
|
|
socket.onclose = onClose(storeApi.dispatch); |
|
|
|
socket.onopen = onOpen(storeApi.dispatch); |
|
|
|
} else if (socketClose.match(action)) { |
|
|
|
if (socket !== null) { |
|
|
|
socket.close(); |
|
|
|
} |
|
|
|
|
|
|
|
socket = null; |
|
|
|
console.log("Websocket closed."); |
|
|
|
} else if (socketSendMessage.match(action)) { |
|
|
|
console.log("Websocket sending message", action.payload); |
|
|
|
socket.send(JSON.stringify(action.payload)); |
|
|
|
} |
|
|
|
|
|
|
|
return next(action); |
|
|
|
return next(action); |
|
|
|
}; |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
|