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
|
import { debounce, forIn, remove, uniq } from "lodash";
import { io, Socket } from "socket.io-client";
import { getBaseUrl } from "../utilites";
import { conditionalLog, log } from "../utilites/logger";
import { createDefaultReducer } from "./reducer";
class SocketIOClient {
private socket: Socket;
private events: SocketIO.Event[];
private debounceReduce: () => void;
private reducers: SocketIO.Reducer[];
constructor() {
const baseUrl = getBaseUrl();
this.socket = io({
path: `${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 = [];
}
initialize() {
this.reducers.push(...createDefaultReducer());
this.socket.connect();
// Debug Command
window._socketio = {
dump: this.dump.bind(this),
emit: this.onEvent.bind(this),
};
}
private dump() {
console.log("SocketIO reducers", this.reducers);
}
addReducer(reducer: SocketIO.Reducer) {
this.reducers.push(reducer);
}
removeReducer(reducer: SocketIO.Reducer) {
const removed = remove(this.reducers, (r) => r === reducer);
conditionalLog(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 (!(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", "Unhandle SocketIO event", type);
return;
}
// eslint-disable-next-line no-loop-func
handlers.forEach((handler) => {
const anyAction = handler.any;
if (anyAction) {
anyAction();
}
forIn(element, (ids, key) => {
ids = uniq(ids);
const action = handler[key as SocketIO.Action];
if (action) {
action(ids);
} else if (anyAction === undefined) {
log("warning", "Unhandle action of SocketIO event", key, type);
}
});
});
}
});
}
private onConnect() {
log("info", "Socket.IO has connected");
this.onEvent({ type: "connect", action: "update", payload: null });
}
private onConnectError() {
log("warning", "Socket.IO has error connecting backend");
this.onEvent({ type: "connect_error", action: "update", payload: null });
}
private onDisconnect() {
log("warning", "Socket.IO has disconnected");
this.onEvent({ type: "disconnect", action: "update", payload: null });
}
private onEvent(event: SocketIO.Event) {
log("info", "Socket.IO receives", event);
this.events.push(event);
this.debounceReduce();
}
}
export default new SocketIOClient();
|