import { BehaviorSubject, Subject } from "rxjs" import { logHoppRequestRunToAnalytics } from "../fb/analytics" import { SIOClientV2, SIOClientV3, SIOClientV4, SIOClient } from "./SIOClients" import { SIOClientVersion } from "~/newstore/SocketIOSession" export const SOCKET_CLIENTS = { v2: SIOClientV2, v3: SIOClientV3, v4: SIOClientV4, } as const type SIOAuth = { type: "None" } | { type: "Bearer"; token: string } export type ConnectionOption = { url: string path: string clientVersion: SIOClientVersion auth: SIOAuth | undefined } export type SIOMessage = { eventName: string value: unknown } type SIOErrorType = "CONNECTION" | "RECONNECT_ERROR" | "UNKNOWN" export type SIOError = { type: SIOErrorType value: unknown } export type SIOEvent = { time: number } & ( | { type: "CONNECTING" } | { type: "CONNECTED" } | { type: "MESSAGE_SENT"; message: SIOMessage } | { type: "MESSAGE_RECEIVED"; message: SIOMessage } | { type: "DISCONNECTED"; manual: boolean } | { type: "ERROR"; error: SIOError } ) export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED" export class SIOConnection { connectionState$: BehaviorSubject event$: Subject = new Subject() socket: SIOClient | undefined constructor() { this.connectionState$ = new BehaviorSubject("DISCONNECTED") } private addEvent(event: SIOEvent) { this.event$.next(event) } connect({ url, path, clientVersion, auth }: ConnectionOption) { this.connectionState$.next("CONNECTING") this.addEvent({ time: Date.now(), type: "CONNECTING", }) try { this.socket = new SOCKET_CLIENTS[clientVersion]() if (auth?.type === "Bearer") { this.socket.connect(url, { path, auth: { token: auth.token, }, }) } else { this.socket.connect(url) } this.socket.on("connect", () => { this.connectionState$.next("CONNECTED") this.addEvent({ type: "CONNECTED", time: Date.now(), }) }) this.socket.on("*", ({ data }: { data: string[] }) => { const [eventName, message] = data this.addEvent({ message: { eventName, value: message }, type: "MESSAGE_RECEIVED", time: Date.now(), }) }) this.socket.on("connect_error", (error: unknown) => { this.handleError(error, "CONNECTION") }) this.socket.on("reconnect_error", (error: unknown) => { this.handleError(error, "RECONNECT_ERROR") }) this.socket.on("error", (error: unknown) => { this.handleError(error, "UNKNOWN") }) this.socket.on("disconnect", () => { this.connectionState$.next("DISCONNECTED") this.addEvent({ type: "DISCONNECTED", time: Date.now(), manual: true, }) }) } catch (error) { this.handleError(error, "CONNECTION") } logHoppRequestRunToAnalytics({ platform: "socketio", }) } private handleError(error: unknown, type: SIOErrorType) { this.disconnect() this.addEvent({ time: Date.now(), type: "ERROR", error: { type, value: error, }, }) } sendMessage(event: { message: string; eventName: string }) { if (this.connectionState$.value === "DISCONNECTED") return const { message, eventName } = event this.socket?.emit(eventName, message, (data) => { // receive response from server this.addEvent({ time: Date.now(), type: "MESSAGE_RECEIVED", message: { eventName, value: data, }, }) }) this.addEvent({ time: Date.now(), type: "MESSAGE_SENT", message: { eventName, value: message, }, }) } disconnect() { this.socket?.close() this.connectionState$.next("DISCONNECTED") } }