File size: 4,336 Bytes
2b43822
 
 
e3cb794
2b43822
 
 
58ad246
2b43822
58ad246
2b43822
58ad246
 
35fc662
58ad246
 
2b43822
 
 
 
d32b1fb
2b43822
 
e3cb794
2b43822
 
e3cb794
34a23a6
2b43822
9369ecd
2b43822
 
 
58ad246
2b43822
e3cb794
2b43822
 
 
 
 
 
58ad246
35fc662
58ad246
 
35fc662
58ad246
 
e3cb794
2b43822
 
 
 
 
 
 
58ad246
 
 
 
 
e3cb794
2b43822
 
307d8b2
 
 
2b43822
 
 
58ad246
 
 
 
 
 
 
d32b1fb
9dffdbf
35fc662
2b43822
 
9dffdbf
 
 
35fc662
 
2b43822
 
 
 
58ad246
 
 
 
 
 
 
 
e3cb794
2b43822
 
 
 
 
d32b1fb
2b43822
 
58ad246
d32b1fb
2b43822
 
 
58ad246
2b43822
 
 
 
 
58ad246
2b43822
 
 
 
58ad246
2b43822
 
 
 
 
 
 
 
58ad246
2b43822
 
 
 
35fc662
 
2b43822
e1a2bcd
 
 
 
58ad246
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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 of route
    name: string;
    
    // ZodSchema used to validate request body
    schema: ZodSchema<T>;

    // Handler Function. To be called after request body is validated
    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";
    }
}

// Context holds all available information for any given request
export interface Context<T> {
    ws: WebSocketInterface;
    user: User;
    payload: T;
    server: HapticLinkServer;
}

export class HapticLinkServer {
    // Router map
    routes: { [key: string]: Route<any> };

    // All rooms are stored in this map
    rooms: { [key: string]: Room };

    // All users are stored in this map and should be dropped when their connection does.
    users: Map<WebSocketInterface, User>;

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

    /**
     * Removes user from registered users
     * @param {WebSocketInterface} ws 
     * @returns {boolean} true/false whether the user was successfully removed
     */
    removeUser(ws: WebSocketInterface): boolean {
        const user = this.users.get(ws);
        if (!user) return false;
        if (user.currentRoom) {
            user.currentRoom.removeUserById(user.id);
        }
        return true;
    }

    /**
     * Registers a route with router
     * @param {string} name The name of the route which be used by the client to identify the route.
     * @param {ZodSchema<T>} schema A ZodSchema to match the body of the WS message
     * @param {RouteHandler<T>} handler A handler function that should have a matching schema
     * @returns 
     */
    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;
    }

    /**
     * Compares message param with names of registered routes
     * If route exists, validates the schema and then calls
     * the handler function with the formatted payload
     * @param {WebSocketInterface} ws
     * @param {string} message WebSocket message
     * @returns 
     */
    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" }));
        }

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

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

        const route = this.routes[payload.route];
        // Removes route from body
        delete payload.route;

        let user: User;

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

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

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

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