import { existsSync } from "node:fs"; import { createClient } from "@supabase/supabase-js"; import { prepareAssets } from "./assets.mjs"; if (existsSync(".env.local")) process.loadEnvFile(".env.local"); const id = process.argv[2]; if ( !id || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) ) throw new Error( "Usage: npm run reprocess -- . Stop the MineGeo worker first.", ); if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.SUPABASE_SECRET_KEY) throw new Error("Configure Supabase in .env.local before reprocessing."); const client = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.SUPABASE_SECRET_KEY, { auth: { persistSession: false, autoRefreshToken: false }, }, ); function check(result) { if (result.error) throw new Error(result.error.message); return result.data; } const dataset = check( await client.from("datasets").select("*").eq("id", id).single(), ); if ( dataset.revision_id || dataset.parent_dataset_id || !["draft", "review", "rejected", "failed"].includes(dataset.state) ) throw new Error( "Only an unpublished, inactive original capture can be reprocessed. Published revisions cannot be changed. Contributions cannot be overwritten; upload a new capture instead.", ); const job = check( await client.from("upload_jobs").select("*").eq("dataset_id", id).single(), ); if (!["complete", "failed", "cancelled"].includes(job.state)) throw new Error( "This job is already queued or running. Stop the worker and wait for it to finish before retrying.", ); const input = check( await client .from("dataset_inputs") .select("validated,manifest") .eq("dataset_id", id) .single(), ); if ( (input.manifest.availableViewpoints ?? input.manifest.viewpoints).some( (p) => p.assetRoot, ) ) throw new Error( "This draft contains accepted contributions. Upload a new capture instead of reprocessing mixed media.", ); if (dataset.source === "plugin") await prepareAssets(); // This maintenance command runs with the worker stopped. Compare-and-set // guards reject intervening publication, ownership of a job, or another repair. const changed = check( await client .from("datasets") .update({ state: "processing", rejection_reason: null, map_path: null, cover_path: null, panorama_count: 0, updated_at: new Date().toISOString(), }) .eq("id", id) .eq("state", dataset.state) .is("revision_id", null) .select("id"), ); if (changed.length !== 1) throw new Error( "Dataset state changed; nothing was queued. Run the command again.", ); try { check( await client .from("dataset_inputs") .update({ validated: false }) .eq("dataset_id", id), ); const queued = check( await client .from("upload_jobs") .update({ state: "queued", attempts: 0, progress: 0, error: null, lease_owner: null, lease_until: null, updated_at: new Date().toISOString(), }) .eq("id", job.id) .eq("state", job.state) .eq("updated_at", job.updated_at) .select("id"), ); if (queued.length !== 1) throw new Error( "Job state changed while queuing; stop the worker before retrying.", ); } catch (error) { // A transport error can occur after an update committed. If the job is // queued/running, leave its dataset processing; otherwise restore its draft. const current = await client .from("upload_jobs") .select("state") .eq("id", job.id) .single(); if (!current.error && !["queued", "running"].includes(current.data.state)) { check( await client .from("datasets") .update({ state: dataset.state, rejection_reason: dataset.rejection_reason, map_path: dataset.map_path, cover_path: dataset.cover_path, panorama_count: dataset.panorama_count, updated_at: new Date().toISOString(), }) .eq("id", id) .eq("state", "processing") .is("revision_id", null), ); check( await client .from("dataset_inputs") .update({ validated: input.validated }) .eq("dataset_id", id), ); } throw error; } console.log( `Queued dataset ${id} for a fresh render. Start MineGeo; no new export or upload is needed.`, );