Files
ozan a81a450e7e feat: monorepo consolidation — merge CLI, bot, admin, team-tool, website, docs, runner, proxy
Merged into tinqs/studio:
- cmd/tinqs-cli/    — tinqs-cli (Go binary, from bot/cli)
- cmd/tea/          — Gitea CLI tool (from tinqs/cli-tea)
- services/bot/     — Bot service (from tinqs-ltd/bot on git.arikigame.com)
- services/admin/   — Admin panel (from tinqs/admin)
- services/team-tool/ — Team Tool (from tinqs/team-tool)
- services/proxy/   — tinqs-proxy (from bot/proxy)
- web/landing/      — tinqs.com website (from tinqs/website)
- web/docs/         — Platform docs (from tinqs/docs)
- web/blog/         — Blog (placeholder)
- runner/           — Ephemeral CI runner (from tinqs/runner)

All source repos will be deleted after verification.
2026-05-22 04:55:50 +00:00

98 lines
2.7 KiB
TypeScript

import { PgBoss } from "pg-boss";
// ── Singleton (reuse across hot-reloads in dev) ──
const globalForBoss = globalThis as unknown as { pgboss: PgBoss | undefined };
function getDirectUrl(): string {
const url = process.env.DATABASE_URL;
if (!url) throw new Error("DATABASE_URL is not set");
// Strip Supabase/Neon pooler suffix if present
return url.replace("-pooler", "");
}
async function createBoss(): Promise<PgBoss> {
const boss = new PgBoss({
connectionString: getDirectUrl(),
schedule: false,
});
boss.on("error", (err) => console.error("[pg-boss]", err));
await boss.start();
// Generate queue — fal.ai media generation with rate limiting
const genExists = await boss.getQueue(QUEUES.GENERATE);
if (!genExists) {
await boss.createQueue(QUEUES.GENERATE, {
retryLimit: 3,
retryDelay: 10,
retryBackoff: true,
expireInSeconds: 7200,
});
}
return boss;
}
export async function getBoss(): Promise<PgBoss> {
if (globalForBoss.pgboss) return globalForBoss.pgboss;
const boss = await createBoss();
if (process.env.NODE_ENV !== "production") globalForBoss.pgboss = boss;
return boss;
}
// ── Queue names ──
export const QUEUES = {
GENERATE: "platform-generate",
} as const;
// ── Job types ──
export type GenerationJob = {
type: "text2img" | "img2mesh" | "full" | "styledFull" | "itemSet" | "styleExtract" | "readGdd";
userId: string;
projectId?: string;
prompt: string;
styleDNA?: Record<string, unknown> | null;
seed?: number | null;
referenceUrls?: string[];
imageUrl?: string;
gameContext?: string;
quality?: string;
items?: Array<{ name: string; prompt: string; category: string }>;
assetId?: string;
costCredits: number;
costType: string;
};
// ── Enqueue ──
export async function enqueueJob(
job: GenerationJob,
): Promise<{ jobId: string }> {
const boss = await getBoss();
const id = await boss.send(QUEUES.GENERATE, job);
if (!id) throw new Error("pg-boss send returned null");
return { jobId: id };
}
// ── Fetch + complete (manual polling, matches code-caster pattern) ──
export async function fetchJob(): Promise<Record<string, unknown> | null> {
const boss = await getBoss();
const jobs = await boss.fetch<GenerationJob>(QUEUES.GENERATE, { batchSize: 1 });
return (jobs?.[0] ?? null) as any;
}
export async function completeJob(jobId: string): Promise<void> {
const boss = await getBoss();
await boss.complete(QUEUES.GENERATE, jobId);
}
export async function failJob(jobId: string, error?: Error): Promise<void> {
const boss = await getBoss();
await boss.fail(QUEUES.GENERATE, jobId, error ? { message: error.message } : null);
}