ModReleaser/src/services/publish.ts

130 lines
3.7 KiB
TypeScript
Raw Normal View History

import { initTRPC } from "@trpc/server";
import EventEmitter, { on } from "node:events";
import { getConfig } from "./config";
import { errorMessage } from "@/lib/utils/error";
import {
PublishInputSchema,
type ProgressEvent,
type PlatformResult,
type PublishResult,
} from "./publish.schemas";
import { publishModrinthVersion } from "./platforms/modrinth";
import { publishCurseForgeVersion } from "./platforms/curseforge";
import type { PlatformContext } from "./platforms/types";
// ── tRPC init ──────────────────────────────────────────
const t = initTRPC.create();
// ── Shared EventEmitter for progress ───────────────────
const ee = new EventEmitter();
// ── Platform orchestrator ──────────────────────────────
async function runPlatform(
platform: "modrinth" | "curseforge",
mcVersions: { mc_version: string }[],
ctx: PlatformContext,
): Promise<PlatformResult> {
const result: PlatformResult = { success: 0, fail: 0, errors: [] };
const total = mcVersions.length;
if (total === 0) {
ee.emit("progress", {
platform,
current: 0,
total: 0,
status: "completed",
errors: [],
} satisfies ProgressEvent);
return result;
}
// Emit initial running state
ee.emit("progress", {
platform,
current: 0,
total,
status: "running",
errors: [],
} satisfies ProgressEvent);
const publishFn =
platform === "modrinth"
? publishModrinthVersion
: publishCurseForgeVersion;
for (let i = 0; i < mcVersions.length; i++) {
const { mc_version } = mcVersions[i];
try {
await publishFn(ctx, mc_version);
result.success++;
const isLast = i + 1 === total;
ee.emit("progress", {
platform,
current: i + 1,
total,
status: isLast ? "completed" : "running",
errors: result.errors,
} satisfies ProgressEvent);
} catch (err) {
result.fail++;
const msg = `${mc_version}: ${errorMessage(err)}`;
result.errors.push(msg);
ee.emit("progress", {
platform,
current: i,
total,
status: "failed",
errors: result.errors,
} satisfies ProgressEvent);
// Cancel remaining for this platform only
break;
}
}
return result;
}
// ── Router ─────────────────────────────────────────────
export const appRouter = t.router({
publish: t.procedure
.input(PublishInputSchema)
.mutation(async ({ input }): Promise<PublishResult> => {
const config = await getConfig(input.configName);
const ctx: PlatformContext = { input, config };
const [modrinthSettled, curseforgeSettled] = await Promise.allSettled([
runPlatform("modrinth", input.mcVersions.modrinth, ctx),
runPlatform("curseforge", input.mcVersions.curseforge, ctx),
]);
const modrinth: PlatformResult = modrinthSettled.status === "fulfilled"
? modrinthSettled.value
: { success: 0, fail: 0, errors: [errorMessage(modrinthSettled.reason)] };
const curseforge: PlatformResult = curseforgeSettled.status === "fulfilled"
? curseforgeSettled.value
: { success: 0, fail: 0, errors: [errorMessage(curseforgeSettled.reason)] };
return { modrinth, curseforge };
}),
progress: t.procedure.subscription(async function* (opts) {
for await (const [data] of on(ee, "progress", {
signal: opts.signal,
})) {
yield data as ProgressEvent;
}
}),
});
export type AppRouter = typeof appRouter;