adding variables to router

This commit is contained in:
2025-03-19 20:15:48 +03:00
parent b8d705a805
commit ea308b2f1a
10 changed files with 142 additions and 33 deletions

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Keyborg
A Dockerized USB-over-IP server + Tauri-powered client for seamless USB device export and control.

View File

@ -22,6 +22,7 @@ import {
RequestValidationError,
} from "@src/lib/errors.ts";
import devices from "@src/lib/devices.ts";
import { WebSocketClientsGroup } from "@src/lib/websocket.ts";
const AUTH_COOKIE_NAME = "token";
const VERSION = "0.1.0-a.1";
@ -75,8 +76,6 @@ router
)
.toBoolean();
console.log(alreadyLoggedIn);
return c.html(eta.render("./login.html", { alreadyLoggedIn }));
})
.get("/setup", (c) => {
@ -93,6 +92,8 @@ router
);
});
const group = new WebSocketClientsGroup();
router.get("/api/admin/ws", (c) => {
if (c.req.headers.get("upgrade") != "websocket") {
return new Response(null, { status: 501 });
@ -100,6 +101,8 @@ router.get("/api/admin/ws", (c) => {
const { socket, response } = Deno.upgradeWebSocket(c.req);
group.addClient(socket);
socket.addEventListener("open", () => {
console.log("a client connected!");
});

File diff suppressed because one or more lines are too long

View File

@ -4,13 +4,8 @@ import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
import { deleteCookie, getCookies, setCookie } from "@std/http/cookie";
import { type Cookie } from "@std/http/cookie";
import { getMessageFromError, ok } from "@shared/utils/result.ts";
import {
InferSchemaType,
Schema,
SchemaValidationError,
} from "@shared/utils/validator.ts";
import { okAsync, ResultAsync } from "@shared/utils/resultasync.ts";
import log from "@shared/utils/logger.ts";
import { InferSchemaType, Schema } from "@shared/utils/validator.ts";
import { ResultAsync } from "@shared/utils/resultasync.ts";
import {
FailedToParseRequestAsJSONError,
failedToParseRequestAsJSONError,
@ -55,6 +50,7 @@ export class Context<
S extends string = string,
ReqSchema extends Schema<any> = Schema<unknown>,
ResSchema extends Schema<any> = Schema<unknown>,
Vars extends Record<string | number, any> = Record<string | number, any>,
> {
private _url?: URL;
private _hostname?: string;
@ -263,6 +259,19 @@ export class Context<
delete: (name: string) => deleteCookie(this.res.headers, name),
};
}
private _var: Vars = {} as Vars;
public get var() {
return {
set: (key: keyof Vars, value: Vars[number]) => {
this._var[key] = value;
},
get: <K extends keyof Vars>(key: K): Vars[K] => {
return this._var[key];
},
};
}
}
type ExtractPath<S extends string> = S extends

View File

@ -174,3 +174,11 @@ export const tooManyConnectionError = createErrorFactory(
export type TooManyConnectionError = InferSchemaType<
typeof tooManyConnectionErrorSchema
>;
export const webSocketMsgSendErrorSchema = defineError("WebSocketMsgSendError");
export const webSocketMsgSendError = createErrorFactory(
webSocketMsgSendErrorSchema,
);
export type WebSocketMsgSendError = InferSchemaType<
typeof webSocketMsgSendErrorSchema
>;

0
server/src/lib/events.ts Normal file
View File

View File

@ -34,7 +34,12 @@ const DEFAULT_NOT_ALLOWED_HANDLER =
status: 405,
})) as RequestHandler;
class HttpRouter {
class HttpRouter<
Variables extends Record<string | number, any> = Record<
string | number,
any
>,
> {
public readonly routerTree = new RouterTree<MethodHandlers>();
public pathTransformer?: (path: string) => string;
private middlewares: Middleware[] = [];
@ -152,8 +157,8 @@ class HttpRouter {
? this.pathTransformer(ctx.path)
: ctx.path;
const { handler, params } = this.resolveRoute(ctx, path);
ctx = ctx.setParams(params);
const { handler, params, ctx: routeCtx } = this.resolveRoute(ctx, path);
ctx = routeCtx.setParams(params);
const res =
(await this.runMiddlewares(this.middlewares, handler, ctx)).res;
@ -163,7 +168,11 @@ class HttpRouter {
private resolveRoute(
ctx: Context,
path: string,
): { handler: RequestHandler; params: Record<string, string> } {
): {
handler: RequestHandler;
params: Record<string, string>;
ctx: Context<any, any, any, Variables>;
} {
const routeOption = this.routerTree.find(path);
if (routeOption.isSome()) {
@ -174,17 +183,21 @@ class HttpRouter {
route = methodHandlers["GET"];
} else if (!route && ctx.req.method !== "GET") {
if (ctx.preferredType.map((v) => v === "json").toBoolean()) {
return { handler: this.methodNotAllowedHandler, params };
return {
handler: this.methodNotAllowedHandler,
params,
ctx,
};
}
}
if (route) {
if (route.schema) {
ctx = ctx.setSchema(route.schema);
}
return { handler: route.handler, params };
return { handler: route.handler, params, ctx };
}
}
return { handler: this.notFoundHandler, params: {} };
return { handler: this.notFoundHandler, params: {}, ctx };
}
private removeBodyFromResponse(res: Response): Response {

View File

@ -1,18 +1,43 @@
import {
TooManyConnectionError,
tooManyConnectionError,
WebSocketMsgSendError,
webSocketMsgSendError,
} from "@src/lib/errors.ts";
import { err, ok, Result } from "@shared/utils/result.ts";
import { err, getMessageFromError, ok, Result } from "@shared/utils/result.ts";
import {
InferSchemaType,
Schema,
SchemaValidationError,
z,
} from "@shared/utils/validator.ts";
import log from "@shared/utils/logger.ts";
const MAX_CONNECTIONS_PER_TOKEN = 2;
const MAX_CONNECTIONS = 500;
class WebSocketManager {
private adminSockets: Map<string, WebSocket[]> = new Map();
private userSockets: Map<string, WebSocket[]> = new Map();
export class WebSocketClientsGroup<
ReceiveSchema extends Schema<unknown> = Schema<unknown>,
SendSchema extends Schema<unknown> = Schema<unknown>,
> {
private clients: Map<string, Map<string, WebSocket>> = new Map();
private connectionsCounter: number = 0;
public addAdminClient(
constructor(
public schemas: {
onReceive: ReceiveSchema;
onSend: SendSchema;
} = {
onReceive: z.unknown() as Schema<unknown> as ReceiveSchema,
onSend: z.unknown() as Schema<unknown> as SendSchema,
},
public onopen?: EventListenerOrEventListenerObject,
public onclose?: EventListenerOrEventListenerObject,
public onerror?: EventListenerOrEventListenerObject,
public onmessage?: EventListenerOrEventListenerObject,
) {}
public addClient(
token: string,
socket: WebSocket,
): Result<void, TooManyConnectionError> {
@ -20,19 +45,56 @@ class WebSocketManager {
return err(tooManyConnectionError("Too many connections"));
}
const sockets = this.adminSockets.get(token);
let clientConnections = this.clients.get(token);
if (!sockets) {
const sockets = [socket];
this.adminSockets.set(token, sockets);
this.connectionsCounter++;
return ok();
} else if (sockets.length < MAX_CONNECTIONS_PER_TOKEN) {
sockets.push(socket);
this.connectionsCounter++;
return ok();
} else {
if (!clientConnections) {
this.clients.set(token, new Map());
clientConnections = this.clients.get(token) as Map<
string,
WebSocket
>;
} else if (clientConnections.size >= MAX_CONNECTIONS_PER_TOKEN) {
return err(tooManyConnectionError("Too many connections"));
}
const uuid = crypto.randomUUID();
clientConnections.set(uuid, socket);
socket.addEventListener("close", () => {
clientConnections.delete(uuid);
});
socket.addEventListener("error", () => {
clientConnections.delete(uuid);
});
this.connectionsCounter++;
socket.addEventListener("open", this.onopen!);
socket.addEventListener("open", this.onclose!);
socket.addEventListener("open", this.onerror!);
socket.addEventListener("open", this.onmessage!);
return ok();
}
sendToAll(
msg: InferSchemaType<SendSchema>,
): Result<void, SchemaValidationError | WebSocketMsgSendError[]> {
return this.schemas.onSend.parse(msg)
.andThen((msg) => {
const errors = [];
for (const client of this.clients.values()) {
for (const connection of client) {
try {
connection.send(JSON.stringify(msg));
} catch (e) {
log.error("Failed to send messages to all clients");
errors.push(
webSocketMsgSendError(getMessageFromError(e)),
);
}
}
}
return errors.length === 0 ? ok() : err(errors);
});
}
}

Binary file not shown.

11
shared/utils/test.ts Normal file
View File

@ -0,0 +1,11 @@
import { z } from "@shared/utils/validator.ts";
const schema = z.obj({
password: z.string(),
});
console.log(
schema.parse({
passwor: "string",
}),
);