refactoring everything before moving on

This commit is contained in:
2025-02-11 16:47:32 +03:00
parent 64519e11ff
commit f0ec7a1f00
25 changed files with 272 additions and 114 deletions

5
deno.lock generated
View File

@ -673,6 +673,9 @@
] ]
} }
}, },
"redirects": {
"https://deno.land/x/sleep/mod.ts": "https://deno.land/x/sleep@v1.3.0/mod.ts"
},
"remote": { "remote": {
"https://deno.land/std@0.203.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", "https://deno.land/std@0.203.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee",
"https://deno.land/std@0.203.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", "https://deno.land/std@0.203.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56",
@ -686,6 +689,8 @@
"https://deno.land/std@0.203.0/async/pool.ts": "47c1841cfa9c036144943d11747ddd44064f5baf8cb7ece25473ba873c6aceb0", "https://deno.land/std@0.203.0/async/pool.ts": "47c1841cfa9c036144943d11747ddd44064f5baf8cb7ece25473ba873c6aceb0",
"https://deno.land/std@0.203.0/async/retry.ts": "296fb9c323e1325a69bee14ba947e7da7409a8dd9dd646d70cb51ea0d301f24e", "https://deno.land/std@0.203.0/async/retry.ts": "296fb9c323e1325a69bee14ba947e7da7409a8dd9dd646d70cb51ea0d301f24e",
"https://deno.land/std@0.203.0/async/tee.ts": "47e42d35f622650b02234d43803d0383a89eb4387e1b83b5a40106d18ae36757", "https://deno.land/std@0.203.0/async/tee.ts": "47e42d35f622650b02234d43803d0383a89eb4387e1b83b5a40106d18ae36757",
"https://deno.land/x/sleep@v1.3.0/mod.ts": "e9955ecd3228a000e29d46726cd6ab14b65cf83904e9b365f3a8d64ec61c1af3",
"https://deno.land/x/sleep@v1.3.0/sleep.ts": "b6abaca093b094b0c2bba94f287b19a60946a8d15764d168f83fcf555f5bb59e",
"https://wilsonl.in/minify-html/deno/0.15.0/index.js": "8e7ee5067ca84fb5d5a1f33118cac4998de0b7d80b3f56cc5c6728b84e6bfb70" "https://wilsonl.in/minify-html/deno/0.15.0/index.js": "8e7ee5067ca84fb5d5a1f33118cac4998de0b7d80b3f56cc5c6728b84e6bfb70"
}, },
"workspace": { "workspace": {

View File

@ -5,7 +5,12 @@ const schema = {
req: z.obj({ req: z.obj({
password: z.string(), password: z.string(),
}), }),
res: z.result(z.string(), z.any()), res: z.result(
z.obj({
isMatch: z.boolean(),
}),
z.any(),
),
}; };
export const loginApi = new Api("/login", "POST", schema); export const loginApi = new Api("/login", "POST", schema);

View File

@ -5,9 +5,8 @@ import rateLimitMiddleware from "@src/middleware/rateLimiter.ts";
import authMiddleware from "@src/middleware/auth.ts"; import authMiddleware from "@src/middleware/auth.ts";
import loggerMiddleware from "@src/middleware/logger.ts"; import loggerMiddleware from "@src/middleware/logger.ts";
import { SchemaValidationError, z } from "@shared/utils/validator.ts"; import { SchemaValidationError, z } from "@shared/utils/validator.ts";
import { Api } from "@src/lib/apiValidator.ts";
import { loginApi } from "./api.ts"; import { loginApi } from "./api.ts";
import { err, getMessageFromError, ok } from "@shared/utils/result.ts"; import { err, ok } from "@shared/utils/result.ts";
import admin from "@src/lib/admin.ts"; import admin from "@src/lib/admin.ts";
import { import {
FailedToParseRequestAsJSON, FailedToParseRequestAsJSON,
@ -49,15 +48,17 @@ router
return c.html(eta.render("./index.html", {})); return c.html(eta.render("./index.html", {}));
}) })
.get(["/login", "/login.html"], (c) => { .get(["/login", "/login.html"], (c) => {
return c.html(eta.render("./login.html", {})); const alreadyLoggedIn = c.cookies.get("token").map((token) =>
admin.sessions.verifyToken(token)
)
.toBoolean();
console.log(alreadyLoggedIn);
return c.html(eta.render("./login.html", { alreadyLoggedIn }));
}); });
const schema = { admin.setPassword("Vermont5481");
req: z.obj({
password: z.string().max(1024),
}),
res: z.result(z.void(), z.string()),
};
router.api(loginApi, async (c) => { router.api(loginApi, async (c) => {
const r = await c const r = await c
@ -68,7 +69,7 @@ router.api(loginApi, async (c) => {
if (r.isErr()) { if (r.isErr()) {
if (r.error.type === "AdminPasswordNotSetError") { if (r.error.type === "AdminPasswordNotSetError") {
return c.json( return c.json400(
err({ err({
type: r.error.type, type: r.error.type,
msg: r.error.message, msg: r.error.message,
@ -78,6 +79,9 @@ router.api(loginApi, async (c) => {
return handleCommonErrors(c, r.error); return handleCommonErrors(c, r.error);
} }
const isMatch = r.value;
if (isMatch) {
return admin.sessions.create() return admin.sessions.create()
.map(({ value, expires }) => { .map(({ value, expires }) => {
c.cookies.set({ c.cookies.set({
@ -85,11 +89,16 @@ router.api(loginApi, async (c) => {
value, value,
expires, expires,
}); });
return ok(); return ok({ isMatch: true });
}).match( }).match(
() => c.json(ok()), (v) => c.json(v),
(e) => handleCommonErrors(c, e), (e) => handleCommonErrors(c, e),
); );
} else {
return c.json(ok({
isMatch: false,
}));
}
}); });
function handleCommonErrors( function handleCommonErrors(
@ -106,11 +115,18 @@ function handleCommonErrors(
{ status: 500 }, { status: 500 },
); );
case "FailedToParseRequestAsJSON": case "FailedToParseRequestAsJSON":
case "SchemaValiationError":
return c.json( return c.json(
err(error), err(error),
{ status: 400 }, { status: 400 },
); );
case "SchemaValiationError":
return c.json(
err({
type: "ValidationError",
msg: error.msg,
}),
{ status: 400 },
);
} }
} }

View File

View File

@ -1 +1 @@
import{loginApi as o}from"./shared.bundle.js";const s=document.getElementById("loginForm"),m=document.getElementById("passwordInput");s.addEventListener("submit",async e=>{e.preventDefault();const t=m.value,n=await o.makeRequest({password:t},{});console.log(n)}); import{loginApi as s}from"./shared.bundle.js";const o=document.getElementById("loginForm"),i=document.getElementById("passwordInput"),t=document.getElementById("errDiv");o.addEventListener("submit",async n=>{n.preventDefault();const r=i.value,e=(await s.makeRequest({password:r},{})).flatten();e.isErr()?e.error.type==="RequestValidationError"&&(t.innerText=e.error.msg):e.value.isMatch?window.location.href="/":t.innerText="invalid password"});

File diff suppressed because one or more lines are too long

1
server/src/js/index.ts Normal file
View File

@ -0,0 +1 @@

View File

@ -6,13 +6,24 @@ const form = document.getElementById("loginForm") as HTMLFormElement;
const passwordInput = document.getElementById( const passwordInput = document.getElementById(
"passwordInput", "passwordInput",
) as HTMLInputElement; ) as HTMLInputElement;
const errDiv = document.getElementById("errDiv") as HTMLDivElement;
form.addEventListener("submit", async (e) => { form.addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
const password = passwordInput.value; const password = passwordInput.value;
const res = await loginApi.makeRequest({ password }, {}); const res = (await loginApi.makeRequest({ password }, {})).flatten();
console.log(res); if (res.isErr()) {
if (res.error.type === "RequestValidationError") {
errDiv.innerText = res.error.msg;
}
} else {
if (!res.value.isMatch) {
errDiv.innerText = "invalid password";
} else {
window.location.href = "/";
}
}
}); });

View File

@ -1,15 +1,9 @@
import { Result } from "@shared/utils/result.ts"; import { InferSchemaType, Schema } from "@shared/utils/validator.ts";
import {
InferSchemaType,
ResultSchema,
Schema,
z,
} from "@shared/utils/validator.ts";
import { import {
RequestValidationError, RequestValidationError,
ResponseValidationError, ResponseValidationError,
} from "@src/lib/errors.ts"; } from "@src/lib/errors.ts";
import { errAsync, okAsync, ResultAsync } from "@shared/utils/resultasync.ts"; import { ResultAsync } from "@shared/utils/resultasync.ts";
export type ExtractRouteParams<T extends string> = T extends string export type ExtractRouteParams<T extends string> = T extends string
? T extends `${infer _Start}:${infer Param}/${infer Rest}` ? T extends `${infer _Start}:${infer Param}/${infer Rest}`
@ -85,4 +79,16 @@ export class Api<
); );
}); });
} }
public makeSafeRequest(
reqBody: InferSchemaType<ReqSchema>,
params: { [K in ExtractRouteParams<Path>]: string },
): ResultAsync<InferSchemaType<ResSchema>, ResponseValidationError> {
return this.makeRequest(reqBody, params).mapErr((e) => {
if (e.type === "RequestValidationError") {
throw "Failed to validate request";
}
return e;
});
}
} }

View File

@ -3,19 +3,13 @@ import { type ExtractRouteParams } from "@lib/router.ts";
import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts"; import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
import { deleteCookie, getCookies, setCookie } from "@std/http/cookie"; import { deleteCookie, getCookies, setCookie } from "@std/http/cookie";
import { type Cookie } from "@std/http/cookie"; import { type Cookie } from "@std/http/cookie";
import { import { getMessageFromError } from "@shared/utils/result.ts";
Err,
getMessageFromError,
Ok,
type Result,
ResultFromJSON,
} from "@shared/utils/result.ts";
import { import {
InferSchemaType, InferSchemaType,
Schema, Schema,
SchemaValidationError, SchemaValidationError,
} from "@shared/utils/validator.ts"; } from "@shared/utils/validator.ts";
import { errAsync, ResultAsync } from "@shared/utils/resultasync.ts"; import { ResultAsync } from "@shared/utils/resultasync.ts";
import log from "@shared/utils/logger.ts"; import log from "@shared/utils/logger.ts";
import { FailedToParseRequestAsJSON } from "@src/lib/errors.ts"; import { FailedToParseRequestAsJSON } from "@src/lib/errors.ts";
@ -141,7 +135,10 @@ export class Context<
return none; return none;
} }
public json(body?: object | string, init: ResponseInit = {}): Response { public json(
body?: ResSchema extends Schema<infer T> ? T : object | string,
init: ResponseInit = {},
): Response {
const headers = mergeHeaders( const headers = mergeHeaders(
SECURITY_HEADERS, SECURITY_HEADERS,
this._responseHeaders, this._responseHeaders,

83
server/src/lib/devices.ts Normal file
View File

@ -0,0 +1,83 @@
import usbip from "@src/lib/usbip.ts";
import {
type CommandExecutionError,
DeviceDetailed,
type UsbipUnknownError,
} from "@shared/utils/usbip.ts";
import { none } from "@shared/utils/option.ts";
import { InferSchemaType, z } from "@shared/utils/validator.ts";
import log from "@shared/utils/logger.ts";
import { errAsync, ResultAsync } from "@shared/utils/resultasync.ts";
class Devices {
private readonly devices: Map<string, Device> = new Map();
updateDevices(): ResultAsync<
void,
CommandExecutionError | UsbipUnknownError
> {
return usbip.getDevicesDetailed().mapErr((e) => {
log.error("Failed to update devices!");
return e;
}).map((d) => d.unwrapOr([])).map(
(devices) => {
const current = new Set(devices.map((d) => d.busid));
const old = new Set(this.devices.keys());
const connected = current.difference(old);
const disconnected = old.difference(current);
for (const device of devices) {
if (connected.has(device.busid)) {
this.devices.set(
device.busid,
this.deviceFromDetailed(device),
);
}
}
for (const device of disconnected) {
this.devices.delete(device);
}
},
);
}
deviceFromDetailed(d: DeviceDetailed): Device {
return {
busid: d.busid,
usbid: d.usbid,
vendor: d.vendor,
name: d.name,
displayName: none,
description: none,
connectedAt: new Date(),
};
}
}
export const deviceSchema = z.obj({
busid: z.string(),
usbid: z.option(z.string()),
vendor: z.option(z.string()),
name: z.option(z.string()),
displayName: z.option(z.string()),
description: z.option(z.string()),
connectedAt: z.date(),
});
const test = new Devices();
await test.updateDevices();
console.log(test);
import { sleep } from "https://deno.land/x/sleep/mod.ts";
await sleep(5);
await test.updateDevices();
console.log(test);
export type Device = InferSchemaType<typeof deviceSchema>;

View File

@ -1,9 +1,16 @@
import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts"; import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
const DEFAULT_WILDCARD_SYMBOL = "*"; const DEFAULT_WILDCARD = "*";
const DEFAULT_PARAM_PREFIX = ":"; const DEFAULT_PARAM_PREFIX = ":";
const DEFAULT_PATH_SEPARATOR = "/"; const DEFAULT_PATH_SEPARATOR = "/";
export type Params = Record<string, string>;
interface RouteMatch<T> {
value: T;
params: Params;
}
interface Node<T> { interface Node<T> {
handler: Option<T>; handler: Option<T>;
paramNames: string[]; paramNames: string[];
@ -29,52 +36,52 @@ class StaticNode<T> implements Node<T> {
this.handler = fromNullableVal(handler); this.handler = fromNullableVal(handler);
} }
addStaticChild(segment: string, handler?: T): StaticNode<T> { private addStaticChild(segment: string, handler?: T): StaticNode<T> {
const child = new StaticNode(handler); const child = new StaticNode(handler);
this.staticChildren.set(segment, child); this.staticChildren.set(segment, child);
return child; return child;
} }
setDynamicChild(handler?: T): DynamicNode<T> { private createDynamicChild(handler?: T): DynamicNode<T> {
const child = new DynamicNode(handler); const child = new DynamicNode(handler);
this.dynamicChild = some(child); this.dynamicChild = some(child);
return child; return child;
} }
setWildcardNode(handler?: T): WildcardNode<T> { private createWildcardNode(handler?: T): WildcardNode<T> {
const child = new WildcardNode(handler); const child = new WildcardNode(handler);
this.wildcardChild = some(child); this.wildcardChild = some(child);
return child; return child;
} }
addChild( public addChild(
segment: string, segment: string,
wildcardSymbol: string, wildcardSymbol: string,
paramPrefixSymbol: string, paramPrefixSymbol: string,
handler?: T, handler?: T,
): Node<T> { ): Node<T> {
if (segment === wildcardSymbol) { if (segment === wildcardSymbol) {
return this.setWildcardNode(handler); return this.createWildcardNode(handler);
} }
if (segment.startsWith(paramPrefixSymbol)) { if (segment.startsWith(paramPrefixSymbol)) {
return this.setDynamicChild(handler); return this.createDynamicChild(handler);
} }
return this.addStaticChild(segment, handler); return this.addStaticChild(segment, handler);
} }
getStaticChild(segment: string): Option<StaticNode<T>> { private getStaticChild(segment: string): Option<StaticNode<T>> {
return fromNullableVal(this.staticChildren.get(segment)); return fromNullableVal(this.staticChildren.get(segment));
} }
getDynamicChild(): Option<DynamicNode<T>> { public getDynamicChild(): Option<DynamicNode<T>> {
return this.dynamicChild; return this.dynamicChild;
} }
getWildcardChild(): Option<WildcardNode<T>> { public getWildcardChild(): Option<WildcardNode<T>> {
return this.wildcardChild; return this.wildcardChild;
} }
getChild(segment: string): Option<Node<T>> { public getChild(segment: string): Option<Node<T>> {
return this.getStaticChild(segment) return this.getStaticChild(segment)
.orElse(() => this.getWildcardChild()) .orElse(() => this.getWildcardChild())
.orElse(() => this.getDynamicChild()); .orElse(() => this.getDynamicChild());
@ -89,7 +96,6 @@ class StaticNode<T> implements Node<T> {
} }
} }
// TODO: get rid of fixed param name
class DynamicNode<T> extends StaticNode<T> implements Node<T> { class DynamicNode<T> extends StaticNode<T> implements Node<T> {
constructor( constructor(
handler?: T, handler?: T,
@ -112,7 +118,7 @@ class WildcardNode<T> implements Node<T> {
// Override to prevent adding children to a wildcard node // Override to prevent adding children to a wildcard node
public addChild(): Node<T> { public addChild(): Node<T> {
throw new Error("Cannot add child to a WildcardNode."); throw new Error("Cannot add child to a wildcard (catch-all) node.");
} }
public getChild(): Option<Node<T>> { public getChild(): Option<Node<T>> {
@ -128,16 +134,13 @@ class WildcardNode<T> implements Node<T> {
} }
} }
// Using Node<T> as the unified type for tree nodes.
type TreeNode<T> = Node<T>;
export class RouterTree<T> { export class RouterTree<T> {
public readonly root: StaticNode<T>; public readonly root: StaticNode<T>;
constructor( constructor(
handler?: T, handler?: T,
private readonly wildcardSymbol: string = DEFAULT_WILDCARD_SYMBOL, private readonly wildcardSymbol: string = DEFAULT_WILDCARD,
private readonly paramPrefixSymbol: string = DEFAULT_PARAM_PREFIX, private readonly paramPrefix: string = DEFAULT_PARAM_PREFIX,
private readonly pathSeparator: string = DEFAULT_PATH_SEPARATOR, private readonly pathSeparator: string = DEFAULT_PATH_SEPARATOR,
) { ) {
this.root = new StaticNode(handler); this.root = new StaticNode(handler);
@ -146,7 +149,7 @@ export class RouterTree<T> {
public add(path: string, handler: T): void { public add(path: string, handler: T): void {
const segments = this.splitPath(path); const segments = this.splitPath(path);
const paramNames: string[] = this.extractParams(segments); const paramNames: string[] = this.extractParams(segments);
let current: TreeNode<T> = this.root; let current: Node<T> = this.root;
for (const segment of segments) { for (const segment of segments) {
current = current current = current
@ -155,7 +158,7 @@ export class RouterTree<T> {
current.addChild( current.addChild(
segment, segment,
this.wildcardSymbol, this.wildcardSymbol,
this.paramPrefixSymbol, this.paramPrefix,
) )
); );
@ -174,7 +177,7 @@ export class RouterTree<T> {
public find(path: string): Option<RouteMatch<T>> { public find(path: string): Option<RouteMatch<T>> {
const segments = this.splitPath(path); const segments = this.splitPath(path);
const paramValues: string[] = []; const paramValues: string[] = [];
let current: TreeNode<T> = this.root; let current: Node<T> = this.root;
let i = 0; let i = 0;
for (; i < segments.length; i++) { for (; i < segments.length; i++) {
@ -209,7 +212,7 @@ export class RouterTree<T> {
public getHandler(path: string): Option<T> { public getHandler(path: string): Option<T> {
const segments = this.splitPath(path); const segments = this.splitPath(path);
let current: TreeNode<T> = this.root; let current: Node<T> = this.root;
for (const segment of segments) { for (const segment of segments) {
if (current.isWildcardNode()) break; if (current.isWildcardNode()) break;
@ -224,6 +227,16 @@ export class RouterTree<T> {
return current.handler; return current.handler;
} }
private traverseOrCreate(segments: string[]): Node<T> {
let node: Node<T> = this.root;
for (const segment of segments) {
if (node.isWildcardNode()) break;
node = node.getChild(segment).unwrapOrElse(() =>
node.addChild(segment, this.wildcardSymbol, this.paramPrefix)
);
}
}
private splitPath(path: string): string[] { private splitPath(path: string): string[] {
const trimmed = path.trim().replace(/^\/+/, "").replace(/\/+$/, ""); const trimmed = path.trim().replace(/^\/+/, "").replace(/\/+$/, "");
return trimmed ? trimmed.split(this.pathSeparator) : []; return trimmed ? trimmed.split(this.pathSeparator) : [];
@ -231,18 +244,11 @@ export class RouterTree<T> {
public extractParams(segments: string[]): string[] { public extractParams(segments: string[]): string[] {
return segments.filter((segment) => return segments.filter((segment) =>
segment.startsWith(this.paramPrefixSymbol) segment.startsWith(this.paramPrefix)
).map((segment) => this.stripParamPrefix(segment)); ).map((segment) => this.stripParamPrefix(segment));
} }
public stripParamPrefix(segment: string): string { public stripParamPrefix(segment: string): string {
return segment.slice(this.paramPrefixSymbol.length); return segment.slice(this.paramPrefix.length);
} }
} }
export type Params = Record<string, string>;
interface RouteMatch<T> {
value: T;
params: Params;
}

View File

@ -17,10 +17,6 @@ const authMiddleware: Middleware = async (c, next) => {
return c.redirect("/login"); return c.redirect("/login");
} }
if (path === LOGIN_PATH && isValid) {
return c.redirect("");
}
await next(); await next();
} }
}; };

View File

@ -4,7 +4,6 @@ const loggerMiddleware: Middleware = async (c, next) => {
console.log("", c.req.method, c.path); console.log("", c.req.method, c.path);
await next(); await next();
console.log("", c.res.status, "\n"); console.log("", c.res.status, "\n");
console.log(await c.res.json());
}; };
export default loggerMiddleware; export default loggerMiddleware;

View File

@ -1,3 +1,3 @@
<% layout("./layouts/layout.html") %> <% layout("./layouts/layout.html") %>
this is an index.html <script src="/public/js/index.js" defer></script>

View File

@ -1,7 +1,17 @@
<% layout("./layouts/basic.html") %> <% layout("./layouts/basic.html") %>
<% if (!it.alreadyLoggedIn) { %>
<main> <main>
<form id=loginForm method=POST> <form id=loginForm method=POST>
<p>password</p><input id=passwordInput name=password type=password><input value="sign in" type=submit> <p>password</p><input id=passwordInput name=password type=password><input value="sign in" type=submit>
<div id="errDiv"></div>
</form> </form>
</main> </main>
<script defer src=/public/js/login.js type=module></script> <script defer src=/public/js/login.js type=module></script>
<% } else { %>
<main>
You are already logged in!
</main>
<script>
setTimeout(() => {window.location.href = "/"}, 1500)
</script>
<% } %>

Binary file not shown.

View File

@ -1 +1,6 @@
<% layout("./layouts/layout.html") %> this is an index.html <% layout("./layouts/layout.html") %>
devices:
<div id="Devices"></div>
<script defer src=/public/js/index.js></script>

View File

@ -1 +1 @@
<% layout("./layouts/basic.html") %> <main><form id=loginForm method=POST><p>password</p><input id=passwordInput name=password type=password><input value="sign in" type=submit></form></main><script defer src=/public/js/login.js type=module></script> <% layout("./layouts/basic.html") %> <% if (!it.alreadyLoggedIn) { %> <main><form id=loginForm method=POST><p>password</p><input id=passwordInput name=password type=password><input value="sign in" type=submit><div id=errDiv></div></form></main><script defer src=/public/js/login.js type=module></script> <% } else { %> <main>You are already logged in!</main><script>setTimeout(() => {window.location.href = "/"}, 1500)</script> <% } %>

View File

@ -371,7 +371,7 @@ export function flattenResult<R extends Result<any, any>>(
currentResult = currentResult.value; currentResult = currentResult.value;
} }
return currentResult as FlattenResult<R>; return ok(currentResult) as FlattenResult<R>;
} }
export type UnwrapOption<T> = T extends Option<infer V> ? V : T; export type UnwrapOption<T> = T extends Option<infer V> ? V : T;

View File

@ -254,13 +254,12 @@ export function errAsync<E, T = never>(err: E): ResultAsync<T, E> {
return new ResultAsync(Promise.resolve(new Err<T, E>(err))); return new ResultAsync(Promise.resolve(new Err<T, E>(err)));
} }
export type FlattenResultAsync<R> = R extends ResultAsync<infer T, infer E> type FlattenResultAsync<R> = R extends
? T extends ResultAsync<any, any> ResultAsync<infer Inner, infer OuterError>
? FlattenResultAsync<T> extends ResultAsync<infer V, infer innerE> ? Inner extends ResultAsync<infer T, infer InnerError>
? ResultAsync<V, E | innerE> ? ResultAsync<T, OuterError | InnerError>
: never
: R : R
: never; : R;
type UnwrapPromise<Pr extends Promise<unknown>> = Pr extends Promise<infer U> type UnwrapPromise<Pr extends Promise<unknown>> = Pr extends Promise<infer U>
? U ? U

View File

@ -9,42 +9,42 @@ import {
some, some,
} from "@shared/utils/option.ts"; } from "@shared/utils/option.ts";
class CommandExecutionError extends Error { export class CommandExecutionError extends Error {
code = "CommandExecutionError"; type = "CommandExecutionError";
constructor(msg: string) { constructor(msg: string) {
super(msg); super(msg);
} }
} }
class DeviceDoesNotExistError extends Error { export class DeviceDoesNotExistError extends Error {
code = "DeviceDoesNotExist"; type = "DeviceDoesNotExist";
constructor(msg: string) { constructor(msg: string) {
super(msg); super(msg);
} }
} }
class DeviceAlreadyBoundError extends Error { export class DeviceAlreadyBoundError extends Error {
code = "DeviceAlreadyBound"; type = "DeviceAlreadyBound";
constructor(msg: string) { constructor(msg: string) {
super(msg); super(msg);
} }
} }
class DeviceNotBound extends Error { export class DeviceNotBound extends Error {
code = "DeviceNotBound"; type = "DeviceNotBound";
constructor(msg: string) { constructor(msg: string) {
super(msg); super(msg);
} }
} }
class UsbipUknownError extends Error { export class UsbipUnknownError extends Error {
code = "UsbipUknownError"; type = "UsbipUknownError";
constructor(msg: string) { constructor(msg: string) {
super(msg); super(msg);
} }
} }
type UsbipCommonError = DeviceDoesNotExistError | UsbipUknownError; type UsbipCommonError = DeviceDoesNotExistError | UsbipUnknownError;
class UsbipManager { class UsbipManager {
private readonly listDeatiledCmd = new Deno.Command("usbip", { private readonly listDeatiledCmd = new Deno.Command("usbip", {
@ -84,7 +84,7 @@ class UsbipManager {
return new DeviceDoesNotExistError(stderr); return new DeviceDoesNotExistError(stderr);
} }
return new UsbipUknownError(stderr); return new UsbipUnknownError(stderr);
} }
private parseDetailedList(stdout: string): Option<DeviceDetailed[]> { private parseDetailedList(stdout: string): Option<DeviceDetailed[]> {
@ -140,7 +140,7 @@ class UsbipManager {
public getDevicesDetailed(): ResultAsync< public getDevicesDetailed(): ResultAsync<
Option<DeviceDetailed[]>, Option<DeviceDetailed[]>,
CommandExecutionError | UsbipUknownError CommandExecutionError | UsbipUnknownError
> { > {
return this.executeCommand(this.listDeatiledCmd).andThen( return this.executeCommand(this.listDeatiledCmd).andThen(
({ stdout, stderr, success }) => { ({ stdout, stderr, success }) => {
@ -153,7 +153,7 @@ class UsbipManager {
return ok(this.parseDetailedList(stdout)); return ok(this.parseDetailedList(stdout));
} }
return err(new UsbipUknownError(stderr)); return err(new UsbipUnknownError(stderr));
}, },
); );
} }
@ -193,7 +193,7 @@ class UsbipManager {
public getDevices(): ResultAsync< public getDevices(): ResultAsync<
Option<Device[]>, Option<Device[]>,
CommandExecutionError | UsbipUknownError CommandExecutionError | UsbipUnknownError
> { > {
return this.executeCommand(this.listParsableCmd).andThenAsync( return this.executeCommand(this.listParsableCmd).andThenAsync(
({ stdout, stderr, success }) => { ({ stdout, stderr, success }) => {
@ -205,7 +205,7 @@ class UsbipManager {
} }
return okAsync(this.parseParsableList(stdout)); return okAsync(this.parseParsableList(stdout));
} }
return errAsync(new UsbipUknownError(stderr)); return errAsync(new UsbipUnknownError(stderr));
}, },
); );
} }
@ -268,7 +268,7 @@ class CommandOutput {
} }
} }
interface DeviceDetailed { export interface DeviceDetailed {
busid: string; busid: string;
usbid: Option<string>; usbid: Option<string>;
vendor: Option<string>; vendor: Option<string>;

View File

@ -0,0 +1,2 @@
export * from "./sleep.ts"

View File

@ -0,0 +1,11 @@
// I buy and sell https://FreedomCash.org
export function sleep(seconds: number) {
return new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
export function sleepRandomAmountOfSeconds(minimumSeconds: number, maximumSeconds: number) {
const secondsOfSleep = getRandomArbitrary(minimumSeconds, maximumSeconds)
return new Promise((resolve) => setTimeout(resolve, secondsOfSleep * 1000))
}
function getRandomArbitrary(min: number, max: number) {
return Math.random() * (max - min) + min
}

View File

@ -1,5 +1,11 @@
{ {
"modules": { "modules": {
"https://deno.land/x/sleep/mod.ts": {
"headers": {
"location": "/x/sleep@v1.3.0/mod.ts",
"x-deno-warning": "Implicitly using latest version (v1.3.0) for https://deno.land/x/sleep/mod.ts"
}
},
"https://jsr.io/@std/crypto/1.0.3/_wasm/lib/deno_std_wasm_crypto.generated.d.mts": {}, "https://jsr.io/@std/crypto/1.0.3/_wasm/lib/deno_std_wasm_crypto.generated.d.mts": {},
"https://jsr.io/@std/crypto/1.0.3/_wasm/lib/deno_std_wasm_crypto.generated.mjs": {}, "https://jsr.io/@std/crypto/1.0.3/_wasm/lib/deno_std_wasm_crypto.generated.mjs": {},
"https://jsr.io/@std/net/1.0.4/unstable_get_network_address.ts": {} "https://jsr.io/@std/net/1.0.4/unstable_get_network_address.ts": {}