plan/react-rewrite #1

Merged
gnezim merged 138 commits from plan/react-rewrite into main 2026-04-15 12:21:16 +03:00
2 changed files with 203 additions and 0 deletions
Showing only changes of commit f0e540aa3f - Show all commits
+143
View File
@@ -0,0 +1,143 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { healthMiddleware } from "./health.js";
import type { ApiClient } from "@/shared/api/client.js";
function createMockApiClient(
behavior: "ok" | "error" | "timeout" = "ok",
): ApiClient {
const client = {
get: vi.fn(),
post: vi.fn(),
} as unknown as ApiClient;
if (behavior === "ok") {
vi.mocked(client.get).mockResolvedValue({ status: "ok" });
} else if (behavior === "error") {
vi.mocked(client.get).mockRejectedValue(new Error("upstream down"));
} else {
vi.mocked(client.get).mockImplementation(
() => new Promise(() => {}), // never resolves
);
}
return client;
}
function createMockRes() {
const res = {
statusCode: 200,
status: vi.fn().mockReturnThis(),
json: vi.fn().mockReturnThis(),
setHeader: vi.fn(),
};
return res;
}
describe("healthMiddleware", () => {
beforeEach(() => {
vi.useFakeTimers();
});
it("returns 200 when upstream is reachable", async () => {
const apiClient = createMockApiClient("ok");
const handler = healthMiddleware({ apiClient });
const req = {} as any;
const res = createMockRes();
const next = vi.fn();
await handler(req, res as any, next);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ status: "ok" });
});
it("returns 503 when upstream ping fails", async () => {
const apiClient = createMockApiClient("error");
const handler = healthMiddleware({ apiClient });
const req = {} as any;
const res = createMockRes();
const next = vi.fn();
await handler(req, res as any, next);
expect(res.status).toHaveBeenCalledWith(503);
expect(res.json).toHaveBeenCalledWith({
status: "degraded",
reason: "upstream_unreachable",
});
});
it("returns 200 if last success is within 60s even if current ping fails", async () => {
const apiClient = createMockApiClient("ok");
const handler = healthMiddleware({ apiClient });
const req = {} as any;
const res1 = createMockRes();
await handler(req, res1 as any, vi.fn());
expect(res1.status).toHaveBeenCalledWith(200);
// Now make it fail
vi.mocked(apiClient.get).mockRejectedValue(new Error("fail"));
// Advance 30s — still within 60s window
vi.advanceTimersByTime(30_000);
const res2 = createMockRes();
await handler(req, res2 as any, vi.fn());
expect(res2.status).toHaveBeenCalledWith(200);
});
it("returns 503 if last success is older than 60s", async () => {
const apiClient = createMockApiClient("ok");
const handler = healthMiddleware({ apiClient });
const req = {} as any;
const res1 = createMockRes();
await handler(req, res1 as any, vi.fn());
expect(res1.status).toHaveBeenCalledWith(200);
// Now make it fail
vi.mocked(apiClient.get).mockRejectedValue(new Error("fail"));
// Advance 61s — beyond 60s window
vi.advanceTimersByTime(61_000);
const res2 = createMockRes();
await handler(req, res2 as any, vi.fn());
expect(res2.status).toHaveBeenCalledWith(503);
});
it("respects custom upstreamTimeoutMs", async () => {
const apiClient = createMockApiClient("timeout");
const handler = healthMiddleware({
apiClient,
upstreamTimeoutMs: 100,
});
const req = {} as any;
const res = createMockRes();
const next = vi.fn();
// The handler should abort the ping after 100ms
const promise = handler(req, res as any, next);
vi.advanceTimersByTime(200);
await promise;
expect(res.status).toHaveBeenCalledWith(503);
});
it("does not call next — health is a terminal response", async () => {
const apiClient = createMockApiClient("ok");
const handler = healthMiddleware({ apiClient });
const req = {} as any;
const res = createMockRes();
const next = vi.fn();
await handler(req, res as any, next);
expect(next).not.toHaveBeenCalled();
});
});
+60
View File
@@ -0,0 +1,60 @@
import type { ApiClient } from "@/shared/api/client.js";
import type { IncomingMessage, ServerResponse } from "node:http";
export interface HealthMiddlewareOptions {
apiClient: ApiClient;
upstreamTimeoutMs?: number;
}
const STALE_THRESHOLD_MS = 60_000;
const DEFAULT_UPSTREAM_TIMEOUT_MS = 5_000;
/**
* Factory that creates a health-check handler. Returns 200 if the last
* successful upstream ping is within 60 s, 503 otherwise.
*
* This is an Express-style `(req, res, next)` handler — it does NOT
* auto-register itself as Modern.js middleware. Registration happens
* in a future integration step.
*/
export function healthMiddleware(options: HealthMiddlewareOptions) {
const { apiClient, upstreamTimeoutMs = DEFAULT_UPSTREAM_TIMEOUT_MS } =
options;
let lastSuccessTs = 0;
return async (
_req: IncomingMessage,
res: ServerResponse & {
status: (code: number) => ServerResponse;
json: (body: unknown) => void;
},
_next: () => void,
): Promise<void> => {
// Attempt to ping the upstream API
try {
await Promise.race([
apiClient.get("/health"),
new Promise<never>((_resolve, reject) =>
setTimeout(
() => reject(new Error("upstream_timeout")),
upstreamTimeoutMs,
),
),
]);
lastSuccessTs = Date.now();
} catch {
// ping failed — rely on cached lastSuccessTs
}
const age = Date.now() - lastSuccessTs;
if (lastSuccessTs > 0 && age < STALE_THRESHOLD_MS) {
res.status(200).json({ status: "ok" });
} else {
res
.status(503)
.json({ status: "degraded", reason: "upstream_unreachable" });
}
};
}