chore: remove swagger deps, parallelize CI jobs

- Remove @fastify/swagger and @fastify/swagger-ui (API docs live on GitHub Pages)
- Run typecheck, build, and docker CI jobs in parallel instead of sequentially
This commit is contained in:
Siddharth Kumar Sah
2026-03-24 00:41:54 +08:00
parent fbdbe0949a
commit 0aa2a5e5de
18 changed files with 360 additions and 337 deletions
-21
View File
@@ -1,10 +1,7 @@
import Fastify from "fastify";
import cors from "@fastify/cors";
import rateLimit from "@fastify/rate-limit";
import swagger from "@fastify/swagger";
import swaggerUi from "@fastify/swagger-ui";
import { env } from "./config.js";
import { APP_VERSION } from "@stirling-image/shared";
import { runMigrations } from "./db/migrate.js";
import { ensureDefaultAdmin, authRoutes, authMiddleware } from "./plugins/auth.js";
import { registerUpload } from "./plugins/upload.js";
@@ -51,24 +48,6 @@ await app.register(rateLimit, {
timeWindow: "1 minute",
});
// Swagger / OpenAPI documentation (dev only)
if (process.env.NODE_ENV !== "production") {
await app.register(swagger, {
openapi: {
info: {
title: "Stirling Image API",
description: "API for Stirling Image — self-hosted image processing suite",
version: APP_VERSION,
},
servers: [{ url: `http://localhost:1349` }],
},
});
await app.register(swaggerUi, {
routePrefix: "/api/docs",
});
}
// Multipart upload support
await registerUpload(app);
+62 -41
View File
@@ -117,17 +117,34 @@ export async function registerBatchRoutes(
};
updateJobProgress({ ...progress });
// Set up response headers for ZIP streaming
// Tell Fastify we're taking over the response — without this,
// Fastify's lifecycle hooks conflict with reply.raw.writeHead()
// and can throw unhandled errors that crash the process.
reply.hijack();
// Set up response headers for ZIP streaming.
// X-File-Order must be URI-encoded because filenames can contain
// spaces/special chars that are invalid in HTTP header values.
reply.raw.writeHead(200, {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="batch-${toolId}-${jobId.slice(0, 8)}.zip"`,
"Transfer-Encoding": "chunked",
"X-Job-Id": jobId,
"X-File-Order": files.map(f => f.filename).join(","),
"X-File-Order": files.map(f => encodeURIComponent(f.filename)).join(","),
});
// Create ZIP archive that pipes directly to the response
const archive = archiver("zip", { zlib: { level: 5 } });
// Handle archive-level errors to prevent unhandled exceptions
// that would crash the server process.
archive.on("error", (err) => {
request.log.error({ err }, "Archiver error during batch processing");
if (!reply.raw.writableEnded) {
reply.raw.end();
}
});
archive.pipe(reply.raw);
// Use p-queue for concurrency control
@@ -154,50 +171,54 @@ export async function registerBatchRoutes(
}
// Process all files through the queue
const tasks = files.map((file) =>
queue.add(async () => {
progress.currentFile = file.filename;
updateJobProgress({ ...progress });
// Validate the image
const validation = await validateImageBuffer(file.buffer);
if (!validation.valid) {
progress.failedFiles++;
progress.errors.push({
filename: file.filename,
error: `Invalid image: ${validation.reason}`,
});
progress.completedFiles++;
try {
const tasks = files.map((file) =>
queue.add(async () => {
progress.currentFile = file.filename;
updateJobProgress({ ...progress });
return;
}
try {
const result = await toolConfig.process(
file.buffer,
settings,
file.filename,
);
// Validate the image
const validation = await validateImageBuffer(file.buffer);
if (!validation.valid) {
progress.failedFiles++;
progress.errors.push({
filename: file.filename,
error: `Invalid image: ${validation.reason}`,
});
progress.completedFiles++;
updateJobProgress({ ...progress });
return;
}
const zipFilename = getUniqueName(result.filename);
archive.append(result.buffer, { name: zipFilename });
try {
const result = await toolConfig.process(
file.buffer,
settings,
file.filename,
);
progress.completedFiles++;
updateJobProgress({ ...progress });
} catch (err) {
progress.failedFiles++;
progress.errors.push({
filename: file.filename,
error: err instanceof Error ? err.message : "Processing failed",
});
progress.completedFiles++;
updateJobProgress({ ...progress });
}
}),
);
const zipFilename = getUniqueName(result.filename);
archive.append(result.buffer, { name: zipFilename });
// Wait for all tasks to complete
await Promise.all(tasks);
progress.completedFiles++;
updateJobProgress({ ...progress });
} catch (err) {
progress.failedFiles++;
progress.errors.push({
filename: file.filename,
error: err instanceof Error ? err.message : "Processing failed",
});
progress.completedFiles++;
updateJobProgress({ ...progress });
}
}),
);
// Wait for all tasks to complete
await Promise.all(tasks);
} catch (err) {
request.log.error({ err }, "Unexpected error in batch queue");
}
// Finalize progress
progress.status =
+3
View File
@@ -86,6 +86,9 @@ export async function registerProgressRoutes(
) => {
const { jobId } = request.params;
// Take over the response from Fastify for SSE streaming
reply.hijack();
// Send SSE headers via the raw Node response
reply.raw.writeHead(200, {
"Content-Type": "text/event-stream",
+1
View File
@@ -65,6 +65,7 @@ export function registerBulkRename(app: FastifyInstance) {
try {
const jobId = randomUUID();
reply.hijack();
reply.raw.writeHead(200, {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="renamed-${jobId.slice(0, 8)}.zip"`,
+1
View File
@@ -43,6 +43,7 @@ export function registerFavicon(app: FastifyInstance) {
try {
const jobId = randomUUID();
reply.hijack();
reply.raw.writeHead(200, {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="favicons-${jobId.slice(0, 8)}.zip"`,
+1
View File
@@ -70,6 +70,7 @@ export function registerSplit(app: FastifyInstance) {
const jobId = randomUUID();
// Set up response headers for ZIP
reply.hijack();
reply.raw.writeHead(200, {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="split-${jobId.slice(0, 8)}.zip"`,