First commit

This commit is contained in:
2026-03-18 22:42:33 +09:00
commit 50657066a6
64 changed files with 5290 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
import SetupCreateAdmin from "./setup/create-admin";
import SetupInitilization from "./setup/initialization";
type ApiMap =
SetupInitilization &
SetupCreateAdmin;
export default ApiMap;
@@ -0,0 +1,23 @@
import { InputError, InputNoneError } from "../../modules/error/input";
import ErrorBase from "../../modules/error";
import DatabaseError from "../../modules/error/database";
export default interface SetupCreateAdmin {
"setup/create-admin": {
body: {
userid: string;
username: string;
email: string;
password: string;
};
response: Success | DatabaseError | ErrorBase<{
bad: "client",
code: "yet_initialization",
message: "初期設定が行われていません。",
}> | ErrorBase<{
bad: "client",
code: "first_admin_already_exists",
message: "最初の管理者ユーザーは既に存在します。",
}> | InputError | InputNoneError;
};
}
@@ -0,0 +1,19 @@
import { InputError, InputNoneError } from "../../modules/error/input";
import ErrorBase from "../../modules/error";
import DatabaseError from "../../modules/error/database";
export default interface SetupInitilization {
"setup/initilization": {
body: {
name: string;
description: string;
requiredInvitationCode: boolean;
force?: "use_force_initialization";
};
response: Success | DatabaseError | ErrorBase<{
bad: "client",
code: "already_initialization",
message: "既に初期設定が行われています。",
}> | InputError | InputNoneError;
};
}
@@ -0,0 +1,9 @@
import ErrorBase from ".";
type DatabaseError = ErrorBase<{
bad: "server",
code: "database_error",
message: "サーバーでデータベースの問題が発生しました。",
}>;
export default DatabaseError;
@@ -0,0 +1,13 @@
type ErrorType<R = unknown> = {
bad: "client" | "server";
code: string;
message: string;
reason?: R,
}
type ErrorBase<E extends ErrorType<any>> = {
success: false;
error: E;
}
export default ErrorBase;
@@ -0,0 +1,16 @@
export type InputError = ErrorBase<{
bad: "client";
code: "input_wrong";
message: "入力に問題があります。";
reason: {
code: string,
path: string,
message: string,
}[];
}>;
export type InputNoneError = ErrorBase<{
bad: "client",
code: "input_none",
message: "入力がありません。",
}>;
@@ -0,0 +1,3 @@
export default interface Success {
success: true;
}
+71
View File
@@ -0,0 +1,71 @@
import lynqError from "./lib/error";
import lynqFetch from "./lib/fetch";
interface Options {
origin: string;
retry?: number;
waiting?: number;
}
type BodyArgs<M, E extends keyof M> =
M[E] extends { body: never } ? [] :
M[E] extends { body: infer B } ? [body: B] :
M[E] extends { body?: infer B } ? [body?: B] :
[];
export default class LynqChat<
M extends { [K in keyof M]: { body?: any; response: any } }
> {
readonly origin: string;
readonly retry: number;
readonly waiting: number;
private _token: string | null = null;
constructor(options: Options) {
this.origin = options.origin;
this.retry = options.retry ?? 5;
this.waiting = options.waiting ?? 500;
if (this.retry < 1) throw new lynqError("Invalid retry count.");
if (this.waiting < 1) throw new lynqError("Invalid base waiting time.");
if (options.origin !== new URL(options.origin).origin)
throw new lynqError("Invalid origin.");
}
get token(): string | null {
return this._token;
}
set token(token: string) {
if (token.length !== 64) throw new lynqError("Invalid token.");
this._token = token;
}
public async request<E extends keyof M>(
endpoint: E,
...args: BodyArgs<M, E>
): Promise<M[E]["response"]>;
public async request(
endpoint: string,
body?: any
): Promise<any>;
public async request(
endpoint: string,
...args: any[]
): Promise<any> {
const req = await lynqFetch(
this.origin,
this.retry,
this.waiting,
endpoint,
{
method: "POST",
cache: "no-store",
body: args[0],
}
);
return await req.json();
}
}
+6
View File
@@ -0,0 +1,6 @@
export default class lynqError extends Error {
constructor(message: string) {
super(message);
this.name = "lynqError";
}
}
+56
View File
@@ -0,0 +1,56 @@
import lynqError from "./error";
export default async function lynqFetch(
origin: string,
retryCount: number,
waitingTime: number,
endpoint: string,
init?: RequestInit,
) {
const waiting = (ms: number) =>
new Promise<void>(resolve => setTimeout(resolve, ms));
let lastError;
for (let i = 0; i < retryCount; i++) {
try {
if (init?.signal?.aborted) {
throw new DOMException("Aborted", "AbortError");
}
const req = await fetch(new URL(`/api/${endpoint}`, origin), init);
if (
Math.floor(req.status / 200) === 1 ||
Math.floor(req.status / 300) === 1
) {
return req;
}
if (Math.floor(req.status / 400) === 1)
throw new lynqError(`Client error: HTTP${req.status} - ${req.statusText}`);
lastError = new Error(`Request failed: HTTP${req.status} - ${req.statusText}`);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
throw err;
}
if (
err instanceof lynqError &&
err.name === "Client error" &&
err.message.startsWith("HTTP4")
)
throw err;
lastError = err;
}
if (i < retryCount - 1) {
const waitTime = waitingTime * 2 ** i;
await waiting(waitTime);
}
}
throw lastError ?? new Error("Unknown Error");
}