package asynqqueue import ( "context" "encoding/json" "fmt" "github.com/hibiken/asynq" "github.com/libnovel/backend/internal/taskqueue" ) // Producer dual-writes every task: first to PocketBase (via pb, for audit / // UI status), then to Redis via Asynq so the runner picks it up immediately. type Producer struct { pb taskqueue.Producer // underlying PocketBase producer client *asynq.Client } // NewProducer wraps an existing PocketBase Producer with Asynq dispatch. func NewProducer(pb taskqueue.Producer, redisOpt asynq.RedisConnOpt) *Producer { return &Producer{ pb: pb, client: asynq.NewClient(redisOpt), } } // Close shuts down the underlying Asynq client connection. func (p *Producer) Close() error { return p.client.Close() } // CreateScrapeTask creates a PocketBase record then enqueues an Asynq job. func (p *Producer) CreateScrapeTask(ctx context.Context, kind, targetURL string, fromChapter, toChapter int) (string, error) { id, err := p.pb.CreateScrapeTask(ctx, kind, targetURL, fromChapter, toChapter) if err != nil { return "", err } payload := ScrapePayload{ PBTaskID: id, Kind: kind, TargetURL: targetURL, FromChapter: fromChapter, ToChapter: toChapter, } taskType := TypeScrapeBook if kind == "catalogue" { taskType = TypeScrapeCatalogue } if err := p.enqueue(ctx, taskType, payload); err != nil { // Non-fatal: PB record exists; runner will pick it up on next poll. return id, fmt.Errorf("asynq enqueue scrape (task still in PB): %w", err) } return id, nil } // CreateAudioTask creates a PocketBase record then enqueues an Asynq job. func (p *Producer) CreateAudioTask(ctx context.Context, slug string, chapter int, voice string) (string, error) { id, err := p.pb.CreateAudioTask(ctx, slug, chapter, voice) if err != nil { return "", err } payload := AudioPayload{ PBTaskID: id, Slug: slug, Chapter: chapter, Voice: voice, } if err := p.enqueue(ctx, TypeAudioGenerate, payload); err != nil { return id, fmt.Errorf("asynq enqueue audio (task still in PB): %w", err) } return id, nil } // CancelTask delegates to PocketBase; Asynq jobs may already be running and // cannot be reliably cancelled, so we only update the audit record. func (p *Producer) CancelTask(ctx context.Context, id string) error { return p.pb.CancelTask(ctx, id) } // enqueue serialises payload and dispatches it to Asynq. func (p *Producer) enqueue(_ context.Context, taskType string, payload any) error { b, err := json.Marshal(payload) if err != nil { return fmt.Errorf("marshal payload: %w", err) } _, err = p.client.Enqueue(asynq.NewTask(taskType, b)) return err }