Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228// keep-job.mjs — MongoDB-backed job model for the keeps mint pipeline.//// A "keep job" tracks the async preparation of a KidLisp piece for minting.// Jobs are keyed by (piece, wallet) — only one active job per piece+wallet.//// Stages: validate → analyze → bundle → thumbnail → ipfs → metadata → ready// Terminal states: ready, failed, expired//// Jobs auto-expire via MongoDB TTL index on `expiresAt`.
import { connect } from "./database.mjs";
const COLLECTION = "keep-jobs";const JOB_TTL_MS = 30 * 60 * 1000; // 30 minutes
// ─── Statuses ─────────────────────────────────────────────────────────────────export const STATUS = { PREPARING: "preparing", READY: "ready", FAILED: "failed", EXPIRED: "expired", CONFIRMED: "confirmed",};
// ─── Stage ordering (for progress %) ─────────────────────────────────────────export const STAGES = [ "validate", "analyze", "bundle", "thumbnail", "ipfs", "metadata", "security", "ready",];
function stagePercent(stage) { const idx = STAGES.indexOf(stage); if (idx < 0) return 0; return Math.round(((idx + 1) / STAGES.length) * 100);}
// ─── Collection helper ───────────────────────────────────────────────────────async function collection() { const { db } = await connect(); return db.collection(COLLECTION);}
// ─── Ensure TTL index exists (idempotent, call on cold start) ────────────────let indexEnsured = false;export async function ensureIndexes() { if (indexEnsured) return; const col = await collection(); await col.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(() => {}); // Compound unique index for upsert await col.createIndex({ piece: 1, wallet: 1 }, { unique: true }).catch(() => {}); indexEnsured = true;}
// ─── Create or reset a job ───────────────────────────────────────────────────export async function upsertJob({ piece, wallet, user, handle, isRebake, regenerate }) { await ensureIndexes(); const col = await collection(); const now = new Date();
const doc = { $set: { status: STATUS.PREPARING, stage: "validate", stageMessage: "Starting...", progress: 0, isRebake: !!isRebake, regenerate: !!regenerate, user: user || null, handle: handle || null, // Partial results — survive reconnects artifactUri: null, thumbnailUri: null, metadataUri: null, // Prepared params for wallet signing preparedData: null, error: null, errorStage: null, updatedAt: now, expiresAt: new Date(now.getTime() + JOB_TTL_MS), }, $setOnInsert: { piece, wallet, createdAt: now, }, };
const result = await col.findOneAndUpdate( { piece, wallet }, doc, { upsert: true, returnDocument: "after" } );
return result;}
// ─── Get job by piece + wallet ───────────────────────────────────────────────export async function getJob(piece, wallet) { const col = await collection(); return col.findOne({ piece, wallet });}
// ─── Get job by _id ──────────────────────────────────────────────────────────export async function getJobById(jobId) { const col = await collection(); const { ObjectId } = await import("mongodb"); return col.findOne({ _id: new ObjectId(jobId) });}
// ─── Update job stage + message ──────────────────────────────────────────────export async function updateJobStage(jobId, stage, message) { const col = await collection(); const { ObjectId } = await import("mongodb"); return col.updateOne( { _id: new ObjectId(jobId) }, { $set: { stage, stageMessage: message || stage, progress: stagePercent(stage), updatedAt: new Date(), }, } );}
// ─── Set a partial result (e.g. artifactUri, thumbnailUri) ───────────────────export async function setJobResult(jobId, fields) { const col = await collection(); const { ObjectId } = await import("mongodb"); return col.updateOne( { _id: new ObjectId(jobId) }, { $set: { ...fields, updatedAt: new Date(), }, } );}
// ─── Mark job as ready with prepared data ────────────────────────────────────export async function markJobReady(jobId, preparedData) { const col = await collection(); const { ObjectId } = await import("mongodb"); return col.updateOne( { _id: new ObjectId(jobId) }, { $set: { status: STATUS.READY, stage: "ready", stageMessage: "Ready for wallet signature", progress: 100, preparedData, updatedAt: new Date(), }, } );}
// ─── Mark job as failed ──────────────────────────────────────────────────────export async function markJobFailed(jobId, error, stage) { const col = await collection(); const { ObjectId } = await import("mongodb"); return col.updateOne( { _id: new ObjectId(jobId) }, { $set: { status: STATUS.FAILED, error: typeof error === "string" ? error : error?.message || "Unknown error", errorStage: stage || null, updatedAt: new Date(), }, } );}
// ─── Mark job as confirmed (after on-chain tx) ──────────────────────────────export async function markJobConfirmed(jobId, txHash, tokenId) { const col = await collection(); const { ObjectId } = await import("mongodb"); return col.updateOne( { _id: new ObjectId(jobId) }, { $set: { status: STATUS.CONFIRMED, txHash, tokenId, updatedAt: new Date(), }, } );}
// ─── Format job for client response (strip internal fields) ──────────────────export function formatJobForClient(job) { if (!job) return null; return { jobId: job._id.toString(), piece: job.piece, wallet: job.wallet, status: job.status, stage: job.stage, stageMessage: job.stageMessage, progress: job.progress, isRebake: job.isRebake, // Partial results artifactUri: job.artifactUri, thumbnailUri: job.thumbnailUri, metadataUri: job.metadataUri, thumbnailFallback: job.thumbnailFallback || null, // Ready state preparedData: job.preparedData, // Error state error: job.error, errorStage: job.errorStage, // Timestamps createdAt: job.createdAt, updatedAt: job.updatedAt, };}