server refactor

This commit is contained in:
Sergey Yarkov 2026-03-09 15:18:46 +03:00
parent c9914c8f19
commit 19ff6f8827
6 changed files with 138 additions and 122 deletions

View File

@ -26,10 +26,10 @@ const xlsxParseOptions = {
* @type {import("#root/http-server/types/http-server.js").HttpRouteHandler}
*/
export default async (ctx) => {
const { res, params } = ctx;
const { res, searchParams } = ctx;
const [, files] = ctx.data;
const limit = pLimit(10);
const clientId = params.get("clientId");
const clientId = searchParams.get("clientId");
const excelFilePath = files?.report?.at(0)?.filepath;
const reports = [];
let isSseClientClosed = false;
@ -70,7 +70,7 @@ export default async (ctx) => {
if (!sheet) throw new Error("Invalid reports workbook.", { cause: { statusCode: 400 } });
const data = xlsx.utils.sheet_to_json(sheet, { raw: false, blankrows: true });
const data = xlsx.utils.sheet_to_json(sheet, { raw: false, blankrows: true, header: "A" });
res.sendJson({ message: "OK" });
sseClientCtx.local.isParsing = true;
@ -90,6 +90,7 @@ export default async (ctx) => {
root_cause: data[i]["__EMPTY_2"] || "",
applicantName: data[i][cols[3]].split("\r\r\n")[0] || "",
executorNames: data[i][cols[4]].split("\r\r\n")[0] || "",
rowNum: data[i].__rowNum__ + 1,
isMarked: true
});
@ -104,7 +105,8 @@ export default async (ctx) => {
job_description: parseReasonCallAndJobDesc(data[i][cols[4]]).trim(),
root_cause: data[i]["__EMPTY_2"] || "",
applicantName: parseNames(data[i][cols[3]]).trim(),
executorNames: parseNames(data[i][cols[4]]).trim()
executorNames: parseNames(data[i][cols[4]]).trim(),
rowNum: data[i].__rowNum__ + 1
});
}
} catch (error) {

View File

@ -1,14 +1,12 @@
import http from "node:http";
import { AsyncLocalStorage } from "node:async_hooks";
import { createRequest } from "./request.js";
import { createResponse } from "./response.js";
import { routeHandler } from "./route.js";
import { createRouter } from "./route.js";
/**
* @typedef {import('./types/http-server.js').HttpServer} HttpServer
* @typedef {import('./types/http-server.js').HttpServerOptions} HttpServerOptions
* @typedef {import('./types/http-server.js').HttpContext} HttpContext
* @typedef {import('./types/http-server.js').HttpRouteMap} HttpRouteMap
*/
/**
@ -16,38 +14,35 @@ import { routeHandler } from "./route.js";
* @returns {HttpServer}
*/
export function createServer(options) {
const { routeMap, enableCors } = options;
const ctxStorage = new AsyncLocalStorage();
let serverPort = 3000;
const router = createRouter();
const server = http.createServer((req, res) => {
try {
const url = new URL(`http://${req.headers.host || `localhost:${serverPort}`}${req.url}`);
const request = createRequest(req);
const response = createResponse(res);
/** @type {HttpContext} */
const ctx = {
data: null,
req: createRequest(req),
res: createResponse(res),
params: url.searchParams,
req: request,
res: response,
searchParams: request.url.searchParams,
params: {},
local: {}
};
if (enableCors) {
if (options.cors) {
res.setHeaders(
new Headers({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST,DELETE,UPDATE,PUT,PATCH",
"Access-Control-Allow-Headers": "Content-Type,Accept"
"Access-Control-Allow-Origin": options.cors.allow,
"Access-Control-Allow-Methods": options.cors.methods,
"Access-Control-Allow-Headers": options.cors.headers
})
);
}
ctxStorage.run(ctx, () =>
routeHandler(url, ctxStorage, routeMap).then(() => {
console.log(`${new Date().toISOString()} Request: ${url.pathname}`);
})
);
console.log(`${new Date().toISOString()} Request: ${ctx.req.url}`);
router.handle(ctx);
} catch (error) {
console.error(error);
}
@ -55,9 +50,8 @@ export function createServer(options) {
return {
listen(port = 3000, cb) {
serverPort = port;
server.listen(port, cb);
return server;
return { router };
}
};
}

View File

@ -8,6 +8,10 @@ export function createRequest(req) {
return req;
},
get url() {
return new URL(`http://${req.headers.host || `localhost`}${req.url}`);
},
async parseJson() {
return new Promise((resolve, reject) => {
/** @type {Buffer[]} */

View File

@ -3,43 +3,41 @@ import formidable from "formidable";
import { BadRequestError, HttpError, MethodNotAllowedError, NotFoundError } from "./errors.js";
/**
* @param {URL} url
* @param {import('node:async_hooks').AsyncLocalStorage<import('./http-server.js').HttpContext>} ctxStorage
* @param {import("./http-server.js").HttpRouteMap} routeMap
* @returns {import("./types/http-server.js").HttpRouter}
*/
export async function routeHandler(url, ctxStorage, routeMap) {
const ctx = ctxStorage.getStore();
if (!ctx) throw new Error("HTTP context storage is undefined!");
export function createRouter() {
return {
routes: [],
async handle(ctx) {
const { req, res } = ctx;
try {
// TODO: create router based on URLPattern
if (url.pathname in routeMap) {
const route = routeMap[url.pathname];
for (const route of this.routes) {
const patternResult = route.pattern.exec(req.url.pathname);
if (!patternResult) continue;
if (route.method !== req.req.method) {
throw new MethodNotAllowedError();
}
ctx.params = patternResult.pathname.groups;
if (req.req.headers["content-type"]?.includes("application/json")) {
ctx.data = await ctx.req.parseJson();
}
if (req.req.headers["content-type"]?.includes("multipart/form-data")) {
if (!route.incomingForm) {
if (!route.options.incomingForm) {
throw new BadRequestError("Request cannot be processed without defined incoming form.");
}
ctx.data = await formidable(route.incomingForm).parse(req.req);
ctx.data = await formidable(route.options.incomingForm).parse(req.req);
}
await route.handle(ctx);
await route.handler(ctx);
return;
}
throw new NotFoundError("Route not found");
} catch (error) {
console.error(error);
@ -70,4 +68,10 @@ export async function routeHandler(url, ctxStorage, routeMap) {
500
);
}
},
define(method, pathname, handler, options = {}) {
/** @type {import("./types/http-server.js").HttpRoute} */
this.routes.push({ method, pattern: new URLPattern({ pathname }), handler, options });
}
};
}

View File

@ -3,30 +3,52 @@ import type IncomingForm from "formidable/Formidable.js";
import formidable from "formidable";
export interface HttpServer {
listen: (port: number, cb: () => void) => void;
listen: (port: number, cb: () => void) => { router: HttpRouter };
}
export interface HttpServerOptions {
routeMap: HttpRouteMap;
enableCors?: boolean;
cors?: {
allow?: string;
methods?: string;
headers?: string;
};
}
export type HttpMethod = "POST" | "GET" | "PUT" | "DELETE" | "PATCH";
export interface HttpRouteOptions {
incomingForm?: formidable.Options;
}
export interface HttpRoute {
method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH";
handle: HttpRouteHandler;
incomingForm?: formidable.Options;
method: HttpMethod;
pattern: URLPattern;
handler: HttpRouteHandler;
options: HttpRouteOptions;
}
export interface HttpRouter {
routes: Array<HttpRoute>;
handle: (ctx: HttpContext) => Promise<void>;
define: (
method: HttpMethod,
pathname: string,
handler: (ctx: HttpContext) => void,
options?: HttpRouteOptions
) => void;
}
export interface HttpRequest {
parseJson: () => Promise<any>;
req: http.IncomingMessage;
url: URL;
}
export interface HttpContext {
data: any;
res: HttpResponse;
req: HttpRequest;
params: URLSearchParams;
searchParams: URLSearchParams;
params: Record<string, unknown>;
local: Record<string, unknown>;
}
@ -38,5 +60,4 @@ export interface HttpResponse {
}
export type HttpRouteHandler = (ctx: HttpContext) => any;
export type HttpRouteMap = Record<string, HttpRoute>;
export type HttpCreateServer = (options: { routeMap: HttpRouteMap }) => HttpServer;
export type HttpCreateServer = (options: HttpServerOptions) => HttpServer;

View File

@ -10,21 +10,22 @@ const SERVER_PORT = Number.parseInt(`${process.env.PORT || "3000"}`, 10);
/** @type {Map<string, import("./http-server/types/http-server.js").HttpContext>} */
export const clients = new Map();
/**
* @type {import("#root/http-server/http-server.js").HttpRouteMap}
*/
export const routeMap = {
"/": {
method: "GET",
handle: (ctx) => ctx.res.sendText("Сервис инструментов таблицы АСУ.")
},
"/api/reports_status_parse": {
method: "GET",
handle: reportsHandlers.statusParseHandler
},
"/api/reports_excel_file_parse": {
method: "POST",
handle: reportsHandlers.parseExcelHandler,
/** @type {import("./http-server/http-server.js").HttpServerOptions['cors']} */
const cors = {
allow: "*",
methods: "GET,POST,DELETE,UPDATE,PUT,PATCH",
headers: "Content-Type,Accept"
};
const server = createServer({ cors }).listen(SERVER_PORT, async () => {
console.log(`Service ${packageJson.name} is running at port ${SERVER_PORT}.`);
await fsp.mkdir(path.join(process.cwd(), "/uploads"), { recursive: true });
await db.connect().then(() => console.log("Database connected!"));
});
server.router.define("GET", "/", ({ res }) => res.sendText("Сервис инструментов таблицы АСУ."));
server.router.define("GET", "/api/reports_status_parse", reportsHandlers.statusParseHandler);
server.router.define("POST", "/api/reports_excel_file_parse", reportsHandlers.parseExcelHandler, {
incomingForm: {
maxFields: 1,
maxFiles: 1,
@ -33,15 +34,5 @@ export const routeMap = {
uploadDir: path.join(process.cwd(), "./uploads"),
filter: ({ mimetype }) => !!mimetype && mimetype.includes("application/vnd.ms-excel")
}
},
"/api/reports_sync": {
method: "POST",
handle: reportsHandlers.syncParsedHandler
}
};
createServer({ routeMap, enableCors: true }).listen(SERVER_PORT, async () => {
console.log(`Service ${packageJson.name} is running at port ${SERVER_PORT}.`);
await fsp.mkdir(path.join(process.cwd(), "/uploads"), { recursive: true });
await db.connect().then(() => console.log("Database connected!"));
});
server.router.define("POST", "/api/reports_sync", reportsHandlers.syncParsedHandler);