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, platforms, type PlatformContext, type PlatformAdaptor, } from "./publish.schemas"; import { publishModrinthVersion } from "./platforms/modrinth"; import { publishCurseForgeVersion } from "./platforms/curseforge"; import { computeVersionRanges } from "@/lib/utils/mcVersion"; import { getCurseForgeMcVersions, getModrinthMcVersions } from "./meta"; import { readAsFile } from "@/lib/utils/file"; import { Template } from "@/lib/utils/template"; import { buildJarPath } from "@/lib/utils/format"; export const platformAdaptors: { [key in typeof platforms[number]]: PlatformAdaptor } = { modrinth: { publish: publishModrinthVersion, getGameVersions: getModrinthMcVersions, }, curseforge: { publish: publishCurseForgeVersion, getGameVersions: getCurseForgeMcVersions, }, }; // ── tRPC init ────────────────────────────────────────── const t = initTRPC.create(); // ── Shared EventEmitter for progress ─────────────────── const ee = new EventEmitter(); // ── Platform orchestrator ────────────────────────────── async function runPlatform( platform: typeof platforms[number], mcVersions: { mc_version: string }[], ctx: PlatformContext, ): Promise { 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 = platformAdaptors[platform].publish; const getGameVersionsFn = platformAdaptors[platform].getGameVersions; const mcVersionEntries = computeVersionRanges( mcVersions.map(v => v.mc_version), await getGameVersionsFn(), ctx.input.cutoffMcVersion ); const primaryFileTemplate = new Template(ctx.config.filename_format); const sourceFileTemplate = new Template(ctx.config.source_filename_format); for (let i = 0; i < mcVersionEntries.length; i++) { const mcVersionEntry = mcVersionEntries[i]; try { const primaryFile = await readAsFile(buildJarPath(ctx.config.project_dir, primaryFileTemplate, ctx.input.version, mcVersionEntry.version)); const sourceFile = await readAsFile(buildJarPath(ctx.config.project_dir, sourceFileTemplate, ctx.input.version, mcVersionEntry.version)); await publishFn(ctx, mcVersionEntry, { primary: primaryFile, source: sourceFile, }); 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 = `${mcVersionEntry.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 => { const config = await getConfig(input.configName); const ctx: PlatformContext = { input, config }; const results = await Promise.all(platforms.map(async (platform) => { try { const result = await runPlatform(platform, input.mcVersions[platform], ctx); return {platform, result}; } catch (err) { return { platform, result: { success: 0, fail: 0, errors: [errorMessage(err)] } } } })); return Object.fromEntries(results.map(result => [result.platform, result.result])); }), 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;