a81a450e7e
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.
98 lines
2.7 KiB
TypeScript
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);
|
|
}
|