perf(studio): stream nitro repair progress over SSE and allow cancelling
This commit is contained in:
1 parent
ba81d16f00
commit
9d571e0c29
4 files changed
+249
-26
No files matched your search
@@ -3,9 +3,25 @@ import { withAdmin } from "@/lib/api-handler";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
import { clearFurniImportCache } from "@/lib/services/furni-import-cache";
|
||||
import { repairBrokenNitros } from "@/lib/services/nitro-cleanup";
|
||||
import {
|
||||
type NitroRepairProgress,
|
||||
repairBrokenNitros,
|
||||
} from "@/lib/services/nitro-cleanup";
|
||||
|
||||
/** Re-download the given broken .nitro bundles from the configured sources. */
|
||||
type RepairSummary = {
|
||||
repaired: number;
|
||||
failed: number;
|
||||
errors?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-download the given broken .nitro bundles from the configured nitro
|
||||
* sources. Bundles are pulled in parallel server-side; when the client asks
|
||||
* for `text/event-stream` a `progress` event is streamed after every bundle
|
||||
* settles plus a final `result` event, mirroring the scan endpoint so a large
|
||||
* repair batch cannot be cut off by a reverse-proxy timeout and can be
|
||||
* cancelled client-side.
|
||||
*/
|
||||
export const POST = withAdmin(
|
||||
{ permission: PERMS.ASSETS_IMPORT },
|
||||
async (request, ctx) => {
|
||||
@@ -23,8 +39,22 @@ export const POST = withAdmin(
|
||||
if (fileNames.length === 0) {
|
||||
return apiError("fileNames[] is required", 400);
|
||||
}
|
||||
try {
|
||||
const result = await repairBrokenNitros(fileNames);
|
||||
|
||||
const summarize = (result: {
|
||||
repaired: number;
|
||||
failed: number;
|
||||
errors: string[];
|
||||
}): RepairSummary => ({
|
||||
repaired: result.repaired,
|
||||
failed: result.failed,
|
||||
errors: result.errors.length > 0 ? result.errors : undefined,
|
||||
});
|
||||
|
||||
const finish = async (result: {
|
||||
repaired: number;
|
||||
failed: number;
|
||||
errors: string[];
|
||||
}) => {
|
||||
clearFurniImportCache();
|
||||
if (result.repaired > 0) {
|
||||
logAudit({
|
||||
@@ -39,10 +69,54 @@ export const POST = withAdmin(
|
||||
},
|
||||
});
|
||||
}
|
||||
return apiOk({
|
||||
repaired: result.repaired,
|
||||
failed: result.failed,
|
||||
errors: result.errors.length > 0 ? result.errors : undefined,
|
||||
return summarize(result);
|
||||
};
|
||||
|
||||
const wantsSse = request.headers
|
||||
.get("accept")
|
||||
?.includes("text/event-stream");
|
||||
try {
|
||||
if (!wantsSse) {
|
||||
return apiOk(await finish(await repairBrokenNitros(fileNames)));
|
||||
}
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const send = (data: unknown) => {
|
||||
try {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify(data)}\n\n`),
|
||||
);
|
||||
} catch {
|
||||
/* stream closed by client */
|
||||
}
|
||||
};
|
||||
try {
|
||||
const result = await repairBrokenNitros(fileNames, {
|
||||
onProgress: (progress: NitroRepairProgress) =>
|
||||
send({ type: "progress", ...progress }),
|
||||
});
|
||||
send({ type: "result", ...(await finish(result)) });
|
||||
} catch (err) {
|
||||
send({
|
||||
type: "error",
|
||||
message: `Nitro repair failed: ${(err as Error).message}`,
|
||||
});
|
||||
}
|
||||
try {
|
||||
controller.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return apiError(`Nitro repair failed: ${(err as Error).message}`, 500);
|
||||
|
||||
Reference in new issue
Block a user