diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-22 16:03:13 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-22 16:03:13 +0300 |
| commit | 18a475657cbc7b2ff8ee537b082eeef25e9bf619 (patch) | |
| tree | 9295ccbd3e6e0f45f2ab361acc5800197de6265f /internal/gui/queue.go | |
| parent | df496b9888ec29bc86d2b5a8ebee1782e94e49f9 (diff) | |
Fix race conditions in background processing and prevent deletion of active cards
- Fix race condition where images, audio, and phonetic info could be saved to wrong flashcard when navigating quickly between cards
- Add pre-determined card directory that's passed to all background operations
- Track active operations per word to prevent deletion during generation
- Block deletion of cards that are queued or being processed
- Show appropriate error messages when deletion is blocked
This ensures files are always saved to the correct card directory and prevents data loss from deleting cards with active operations.
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
Diffstat (limited to 'internal/gui/queue.go')
| -rw-r--r-- | internal/gui/queue.go | 67 |
1 files changed, 41 insertions, 26 deletions
diff --git a/internal/gui/queue.go b/internal/gui/queue.go index f72b059..1c52c9f 100644 --- a/internal/gui/queue.go +++ b/internal/gui/queue.go @@ -53,14 +53,14 @@ type WordQueue struct { results map[int]*WordJob processing map[int]*WordJob completed []*WordJob - - nextID int - mu sync.RWMutex - + + nextID int + mu sync.RWMutex + // Callbacks for UI updates onStatusUpdate func(job *WordJob) onJobComplete func(job *WordJob) - + ctx context.Context cancel context.CancelFunc wg sync.WaitGroup @@ -69,7 +69,7 @@ type WordQueue struct { // NewWordQueue creates a new word processing queue func NewWordQueue(ctx context.Context) *WordQueue { queueCtx, cancel := context.WithCancel(ctx) - + q := &WordQueue{ jobs: make(chan *WordJob, 100), results: make(map[int]*WordJob), @@ -79,9 +79,9 @@ func NewWordQueue(ctx context.Context) *WordQueue { ctx: queueCtx, cancel: cancel, } - + // Don't start a worker - the GUI will pull jobs - + return q } @@ -110,7 +110,7 @@ func (q *WordQueue) AddWordWithPrompt(word, customPrompt string) *WordJob { q.nextID++ q.results[job.ID] = job q.mu.Unlock() - + // Try to add to queue select { case q.jobs <- job: @@ -134,7 +134,7 @@ func (q *WordQueue) GetJob(id int) *WordJob { func (q *WordQueue) GetQueueStatus() (queued, processing, completed, failed int) { q.mu.RLock() defer q.mu.RUnlock() - + // Count based on job statuses for accuracy for _, job := range q.results { switch job.Status { @@ -148,7 +148,7 @@ func (q *WordQueue) GetQueueStatus() (queued, processing, completed, failed int) failed++ } } - + return } @@ -156,14 +156,14 @@ func (q *WordQueue) GetQueueStatus() (queued, processing, completed, failed int) func (q *WordQueue) GetActiveJobs() []*WordJob { q.mu.RLock() defer q.mu.RUnlock() - + var jobs []*WordJob - + // Add processing jobs for _, job := range q.processing { jobs = append(jobs, job) } - + // Add queued jobs from channel (non-blocking) queuedJobs := make([]*WordJob, 0) for { @@ -198,17 +198,17 @@ func (q *WordQueue) Stop() { func (q *WordQueue) CompleteJob(jobID int, translation, audioFile, imageFile string) { q.mu.Lock() defer q.mu.Unlock() - + if job, exists := q.results[jobID]; exists { job.Status = StatusCompleted job.Translation = translation job.AudioFile = audioFile job.ImageFile = imageFile job.CompletedAt = time.Now() - + delete(q.processing, jobID) q.completed = append(q.completed, job) - + if q.onJobComplete != nil { q.onJobComplete(job) } @@ -219,14 +219,14 @@ func (q *WordQueue) CompleteJob(jobID int, translation, audioFile, imageFile str func (q *WordQueue) FailJob(jobID int, err error) { q.mu.Lock() defer q.mu.Unlock() - + if job, exists := q.results[jobID]; exists { job.Status = StatusFailed job.Error = err job.CompletedAt = time.Now() - + delete(q.processing, jobID) - + if q.onJobComplete != nil { q.onJobComplete(job) } @@ -250,14 +250,14 @@ func (q *WordQueue) ProcessNextJob() *WordJob { job.Status = StatusProcessing job.StartedAt = time.Now() q.mu.Unlock() - + // Call the status update callback if q.onStatusUpdate != nil { q.onStatusUpdate(job) } - + return job - + default: return nil } @@ -267,7 +267,7 @@ func (q *WordQueue) ProcessNextJob() *WordJob { func (q *WordQueue) RemoveCompletedJobByWord(word string) { q.mu.Lock() defer q.mu.Unlock() - + // Remove from completed jobs list newCompleted := make([]*WordJob, 0, len(q.completed)) for _, job := range q.completed { @@ -276,11 +276,26 @@ func (q *WordQueue) RemoveCompletedJobByWord(word string) { } } q.completed = newCompleted - + // Also remove from results map for id, job := range q.results { if job.Word == word && job.Status == StatusCompleted { delete(q.results, id) } } -}
\ No newline at end of file +} + +// IsWordProcessing checks if a word is currently being processed or queued +func (q *WordQueue) IsWordProcessing(word string) bool { + q.mu.Lock() + defer q.mu.Unlock() + + // Check all jobs in results + for _, job := range q.results { + if job.Word == word && (job.Status == StatusQueued || job.Status == StatusProcessing) { + return true + } + } + + return false +} |
