File size: 2,920 Bytes
2b43822
 
 
e3cb794
2b43822
 
 
 
 
35fc662
2b43822
 
 
 
d32b1fb
2b43822
 
e3cb794
2b43822
 
e3cb794
34a23a6
2b43822
9369ecd
2b43822
 
 
 
e3cb794
2b43822
 
 
 
 
 
35fc662
 
e3cb794
2b43822
 
 
 
 
 
 
e3cb794
2b43822
 
307d8b2
 
 
2b43822
 
 
d32b1fb
9dffdbf
35fc662
2b43822
 
9dffdbf
 
 
35fc662
 
2b43822
 
 
 
e3cb794
2b43822
 
 
 
 
d32b1fb
2b43822
 
d32b1fb
2b43822
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35fc662
 
2b43822
e1a2bcd
 
 
 
 
2b43822
 
 
 
 
 
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
import { ZodSchema } from "zod";
import { generateSessionToken } from "../helpers";
import { Room } from "./room";
import { WebSocketInterface } from "./WebSocketAdapter";

type RouteHandler<T> = (context: Context<T>) => void;

export interface Route<T> {
    name: string;
    schema: ZodSchema<T>;
    handler: RouteHandler<T>;
}

export class User {
    static userIdLength: number = 32;
    id: string;
    username?: string;
    socket: WebSocketInterface;
    currentRoom?: Room;

    constructor(socket: WebSocketInterface) {
        this.id = generateSessionToken(32);
        this.socket = socket;
        this.username = "Unknown";
    }
}

export interface Context<T> {
    ws: WebSocketInterface;
    user: User;
    payload: T;
    server: HapticLinkServer;
}

export class HapticLinkServer {
    routes: { [key: string]: Route<any> };
    rooms: { [key: string]: Room };
    users: Map<WebSocketInterface, User>;

    constructor() {
        this.routes = {};
        this.users = new Map();
        this.rooms = {};
    }

    removeUser(ws: WebSocketInterface): boolean {
        const user = this.users.get(ws);
        if (!user) return false;
        if (user.currentRoom) {
            user.currentRoom.removeUserById(user.id);
        }
        return true;
    }

    addRoute<T>(name: string, schema: ZodSchema<T>, handler: RouteHandler<T>): boolean {
        if (name in this.routes) {
            return false;
        }

        this.routes[name] = {
            name,
            schema,
            handler,
        };

        return true;
    }

    handleRoute(ws: WebSocketInterface, message: string) {
        // Parse JSON
        let payload: any;
        try {
            payload = JSON.parse(message);
        } catch (e) {
            return ws.send(JSON.stringify({ error: "message not in JSON format" }));
        }

        if (typeof payload != "object" || !Object.keys(payload).includes("route")) {
            return ws.send(JSON.stringify({ error: "missing route" }));
        }

        if (!(payload.route in this.routes)) {
            return ws.send(JSON.stringify({ error: "route not found" }));
        }

        const route = this.routes[payload.route];
        delete payload.route;

        let user: User;

        if (this.users.has(ws)) {
            user = this.users.get(ws)!;
        } else {
            const newUser = new User(ws);
            this.users.set(ws, newUser);
            user = newUser;
        }

        let context: Context<any> = {
            ws,
            payload,
            server: this,
            user,
        };

        if (!route) {
            return ws.send(JSON.stringify({ error: "invalid route" }));
        }

        if (route.schema.safeParse(payload).success) {
            route.handler(context);
        } else {
            return ws.send(JSON.stringify({ error: "invalid payload format" }));
        }
    }
}