summaryrefslogtreecommitdiffhomepage
path: root/frontend/src/@socketio/index.ts
blob: 44419ef7da607cce17e5362f69b93d2250cc3987 (plain)
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();