2026-06-04 10:39:45 +03:30
package db
import (
"context"
"encoding/json"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
// ScanJob is an async "scan this project's AE template" job.
type ScanJob struct {
ID uuid . UUID ` json:"id" `
ProjectID uuid . UUID ` json:"project_id" `
Status string ` json:"status" ` // queued | running | done | error
Engine string ` json:"engine" `
Result json . RawMessage ` json:"result,omitempty" ` // ScanResult JSON, present when done
Error * string ` json:"error,omitempty" `
}
// ScanClaim is the minimal info a node needs to run a claimed scan.
type ScanClaim struct {
ID uuid . UUID
ProjectID uuid . UUID
2026-06-04 19:06:08 +03:30
Mode string // fix | flexible | mockup | musicvisualizer → drives scan.jsx parsing
2026-06-04 10:39:45 +03:30
}
2026-06-04 19:06:08 +03:30
func ( s * Store ) CreateScanJob ( ctx context . Context , projectID uuid . UUID , engine , mode string ) ( uuid . UUID , error ) {
if mode == "" {
mode = "flexible"
}
2026-06-04 10:39:45 +03:30
var id uuid . UUID
err := s . pool . QueryRow ( ctx ,
2026-06-04 19:06:08 +03:30
` INSERT INTO render.scan_jobs (project_id, engine, status, mode) VALUES ($1, $2, 'queued', $3) RETURNING id ` ,
projectID , engine , mode ) . Scan ( & id )
2026-06-04 10:39:45 +03:30
return id , err
}
// ClaimScanJob atomically grabs the oldest queued ae-jsx scan for a node.
// Returns nil when the queue is empty.
func ( s * Store ) ClaimScanJob ( ctx context . Context , nodeID uuid . UUID ) ( * ScanClaim , error ) {
var c ScanClaim
err := s . pool . QueryRow ( ctx , `
UPDATE render.scan_jobs SET status = 'running', node_id = $1, updated_at = NOW()
WHERE id = (
SELECT id FROM render.scan_jobs
WHERE status = 'queued' AND engine = 'ae-jsx'
ORDER BY created_at
LIMIT 1 FOR UPDATE SKIP LOCKED
)
2026-06-04 19:06:08 +03:30
RETURNING id, project_id, mode ` , nodeID ) . Scan ( & c . ID , & c . ProjectID , & c . Mode )
2026-06-04 10:39:45 +03:30
if err != nil {
if err == pgx . ErrNoRows {
return nil , nil
}
return nil , err
}
return & c , nil
}
2026-06-04 19:06:08 +03:30
// SetScanResult / SetScanError only act on a 'running' job, so a result that
// arrives after the user cancelled doesn't un-cancel it.
2026-06-04 10:39:45 +03:30
func ( s * Store ) SetScanResult ( ctx context . Context , id uuid . UUID , resultJSON string ) error {
_ , err := s . pool . Exec ( ctx ,
2026-06-04 19:06:08 +03:30
` UPDATE render.scan_jobs SET status = 'done', result = $2::jsonb, error = NULL, updated_at = NOW() WHERE id = $1 AND status = 'running' ` ,
2026-06-04 10:39:45 +03:30
id , resultJSON )
return err
}
func ( s * Store ) SetScanError ( ctx context . Context , id uuid . UUID , msg string ) error {
_ , err := s . pool . Exec ( ctx ,
2026-06-04 19:06:08 +03:30
` UPDATE render.scan_jobs SET status = 'error', error = $2, updated_at = NOW() WHERE id = $1 AND status = 'running' ` , id , msg )
return err
}
// CancelScanJob marks a queued/running scan as cancelled (user-requested). The
// node's watchdog sees this and kills the AE process.
func ( s * Store ) CancelScanJob ( ctx context . Context , id uuid . UUID ) error {
_ , err := s . pool . Exec ( ctx ,
` UPDATE render.scan_jobs SET status = 'cancelled', error = 'cancelled by user', updated_at = NOW() WHERE id = $1 AND status IN ('queued','running') ` , id )
2026-06-04 10:39:45 +03:30
return err
}
2026-06-04 19:06:08 +03:30
// GetScanStatus returns just the status string (lightweight, for the node watchdog).
func ( s * Store ) GetScanStatus ( ctx context . Context , id uuid . UUID ) ( string , error ) {
var st string
err := s . pool . QueryRow ( ctx , ` SELECT status FROM render.scan_jobs WHERE id = $1 ` , id ) . Scan ( & st )
if err != nil {
if err == pgx . ErrNoRows {
return "" , nil
}
return "" , err
}
return st , nil
}
2026-06-04 10:39:45 +03:30
func ( s * Store ) GetScanJob ( ctx context . Context , id uuid . UUID ) ( * ScanJob , error ) {
var j ScanJob
err := s . pool . QueryRow ( ctx ,
` SELECT id, project_id, status, engine, result, error FROM render.scan_jobs WHERE id = $1 ` ,
id ) . Scan ( & j . ID , & j . ProjectID , & j . Status , & j . Engine , & j . Result , & j . Error )
if err != nil {
if err == pgx . ErrNoRows {
return nil , nil
}
return nil , err
}
return & j , nil
}