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

View File

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

View File

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

View File

@ -3,43 +3,41 @@ import formidable from "formidable";
import { BadRequestError, HttpError, MethodNotAllowedError, NotFoundError } from "./errors.js"; import { BadRequestError, HttpError, MethodNotAllowedError, NotFoundError } from "./errors.js";
/** /**
* @param {URL} url * @returns {import("./types/http-server.js").HttpRouter}
* @param {import('node:async_hooks').AsyncLocalStorage<import('./http-server.js').HttpContext>} ctxStorage
* @param {import("./http-server.js").HttpRouteMap} routeMap
*/ */
export async function routeHandler(url, ctxStorage, routeMap) { export function createRouter() {
const ctx = ctxStorage.getStore(); return {
routes: [],
if (!ctx) throw new Error("HTTP context storage is undefined!"); async handle(ctx) {
const { req, res } = ctx; const { req, res } = ctx;
try { try {
// TODO: create router based on URLPattern for (const route of this.routes) {
if (url.pathname in routeMap) { const patternResult = route.pattern.exec(req.url.pathname);
const route = routeMap[url.pathname];
if (!patternResult) continue;
if (route.method !== req.req.method) { if (route.method !== req.req.method) {
throw new MethodNotAllowedError(); throw new MethodNotAllowedError();
} }
ctx.params = patternResult.pathname.groups;
if (req.req.headers["content-type"]?.includes("application/json")) { if (req.req.headers["content-type"]?.includes("application/json")) {
ctx.data = await ctx.req.parseJson(); ctx.data = await ctx.req.parseJson();
} }
if (req.req.headers["content-type"]?.includes("multipart/form-data")) { 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."); 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; return;
} }
throw new NotFoundError("Route not found"); throw new NotFoundError("Route not found");
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -70,4 +68,10 @@ export async function routeHandler(url, ctxStorage, routeMap) {
500 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"; import formidable from "formidable";
export interface HttpServer { export interface HttpServer {
listen: (port: number, cb: () => void) => void; listen: (port: number, cb: () => void) => { router: HttpRouter };
} }
export interface HttpServerOptions { export interface HttpServerOptions {
routeMap: HttpRouteMap; cors?: {
enableCors?: boolean; allow?: string;
methods?: string;
headers?: string;
};
}
export type HttpMethod = "POST" | "GET" | "PUT" | "DELETE" | "PATCH";
export interface HttpRouteOptions {
incomingForm?: formidable.Options;
} }
export interface HttpRoute { export interface HttpRoute {
method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH"; method: HttpMethod;
handle: HttpRouteHandler; pattern: URLPattern;
incomingForm?: formidable.Options; 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 { export interface HttpRequest {
parseJson: () => Promise<any>; parseJson: () => Promise<any>;
req: http.IncomingMessage; req: http.IncomingMessage;
url: URL;
} }
export interface HttpContext { export interface HttpContext {
data: any; data: any;
res: HttpResponse; res: HttpResponse;
req: HttpRequest; req: HttpRequest;
params: URLSearchParams; searchParams: URLSearchParams;
params: Record<string, unknown>;
local: Record<string, unknown>; local: Record<string, unknown>;
} }
@ -38,5 +60,4 @@ export interface HttpResponse {
} }
export type HttpRouteHandler = (ctx: HttpContext) => any; export type HttpRouteHandler = (ctx: HttpContext) => any;
export type HttpRouteMap = Record<string, HttpRoute>; export type HttpCreateServer = (options: HttpServerOptions) => HttpServer;
export type HttpCreateServer = (options: { routeMap: HttpRouteMap }) => 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>} */ /** @type {Map<string, import("./http-server/types/http-server.js").HttpContext>} */
export const clients = new Map(); export const clients = new Map();
/** /** @type {import("./http-server/http-server.js").HttpServerOptions['cors']} */
* @type {import("#root/http-server/http-server.js").HttpRouteMap} const cors = {
*/ allow: "*",
export const routeMap = { methods: "GET,POST,DELETE,UPDATE,PUT,PATCH",
"/": { headers: "Content-Type,Accept"
method: "GET", };
handle: (ctx) => ctx.res.sendText("Сервис инструментов таблицы АСУ.")
}, const server = createServer({ cors }).listen(SERVER_PORT, async () => {
"/api/reports_status_parse": { console.log(`Service ${packageJson.name} is running at port ${SERVER_PORT}.`);
method: "GET", await fsp.mkdir(path.join(process.cwd(), "/uploads"), { recursive: true });
handle: reportsHandlers.statusParseHandler await db.connect().then(() => console.log("Database connected!"));
}, });
"/api/reports_excel_file_parse": {
method: "POST", server.router.define("GET", "/", ({ res }) => res.sendText("Сервис инструментов таблицы АСУ."));
handle: reportsHandlers.parseExcelHandler, server.router.define("GET", "/api/reports_status_parse", reportsHandlers.statusParseHandler);
server.router.define("POST", "/api/reports_excel_file_parse", reportsHandlers.parseExcelHandler, {
incomingForm: { incomingForm: {
maxFields: 1, maxFields: 1,
maxFiles: 1, maxFiles: 1,
@ -33,15 +34,5 @@ export const routeMap = {
uploadDir: path.join(process.cwd(), "./uploads"), uploadDir: path.join(process.cwd(), "./uploads"),
filter: ({ mimetype }) => !!mimetype && mimetype.includes("application/vnd.ms-excel") 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);