bazarr/frontend/src/modules/socketio/index.ts

168 lines
4.3 KiB
TypeScript

import { debounce, forIn, remove, uniq } from "lodash";
import { onlineManager } from "react-query";
import { io, Socket } from "socket.io-client";
import { Environment, isDevEnv } from "../../utilities";
import { ENSURE, GROUP, LOG } from "../../utilities/console";
import { createDefaultReducer } from "./reducer";
class SocketIOClient {
private socket: Socket;
private events: SocketIO.Event[];
private debounceReduce: () => void;
private reducers: SocketIO.Reducer[];
constructor() {
this.socket = io({
path: `${Environment.baseUrl}/api/socket.io`,
transports: ["polling", "websocket"],
upgrade: true,
rememberUpgrade: true,
autoConnect: false,
});
this.socket.on("connect", this.onConnect.bind(this));
this.socket.on("disconnect", this.onDisconnect.bind(this));
this.socket.on("connect_error", this.onConnectError.bind(this));
this.socket.on("data", this.onEvent.bind(this));
this.events = [];
this.debounceReduce = debounce(this.reduce, 20);
this.reducers = [];
onlineManager.setOnline(false);
if (isDevEnv) {
window.socketIO = {
dump: () => {
GROUP("Socket.IO Reducers", (logger) => {
this.reducers.forEach((reducer) => {
logger(reducer.key);
});
});
},
emit: (e) => {
if (e) {
this.onEvent(e);
}
},
};
}
}
initialize() {
LOG("info", "Initializing Socket.IO client...");
this.reducers.push(...createDefaultReducer());
window.addEventListener("app-auth-changed", (ev) => {
const authenticated = ev.detail.authenticated;
LOG("info", "Authentication status change to", authenticated);
if (authenticated) {
this.connect();
} else {
this.disconnect();
}
});
}
connect() {
LOG("info", "Connecting Socket.IO client...");
this.socket.connect();
}
disconnect() {
LOG("info", "Disconnecting Socket.IO client...");
this.socket.disconnect();
}
addReducer(reducer: SocketIO.Reducer) {
this.reducers.push(reducer);
}
removeReducer(reducer: SocketIO.Reducer) {
const removed = remove(this.reducers, (r) => r === reducer);
ENSURE(removed.length === 0, "Fail to remove reducer", reducer);
}
private reduce() {
const events = [...this.events];
this.events = [];
const records: SocketIO.ActionRecord = {};
events.forEach((e) => {
if (!(e.type in records)) {
records[e.type] = {};
}
const record = records[e.type];
if (record === undefined) {
return;
}
if (!(e.action in record)) {
record[e.action] = [];
}
if (e.payload) {
record[e.action]?.push(e.payload);
}
});
forIn(records, (element, type) => {
if (element) {
const handlers = this.reducers.filter((v) => v.key === type);
if (handlers.length === 0) {
LOG("warning", "Unhandled SocketIO event", type);
return;
}
handlers.forEach((handler) => {
const anyAction = handler.any;
if (anyAction) {
anyAction();
}
forIn(element, (ids, key) => {
ids = uniq(ids);
const action = handler[key as SocketIO.ActionType];
if (action) {
// FIXME: type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
action(ids as any[]);
} else if (anyAction === undefined) {
LOG("warning", "Unhandled SocketIO event", key, type);
}
});
});
}
});
}
private onConnect() {
LOG("info", "Socket.IO has connected");
onlineManager.setOnline(true);
this.onEvent({ type: "connect", action: "update" });
}
private onConnectError() {
LOG("warning", "Socket.IO has error connecting backend");
onlineManager.setOnline(false);
this.onEvent({ type: "connect_error", action: "update" });
}
private onDisconnect() {
LOG("warning", "Socket.IO has disconnected");
onlineManager.setOnline(false);
this.onEvent({ type: "disconnect", action: "update" });
}
private onEvent(event: SocketIO.Event) {
LOG("info", "Socket.IO receives", event);
this.events.push(event);
this.debounceReduce();
}
}
export default new SocketIOClient();