fix(photos): faster large-album loading and album cover lifecycle
Large albums opened slowly and flickered; the default album cover was also lost in the media-objects refactor. Large-album loading: - quantize signed object URLs within a window and send Cache-Control so previews are reused instead of re-fetched - publish photo.updated (not photo.created) when variants are ready, so clients update in place without reordering the grid - worker concurrency + newest-first variant priority; raise default batch Album cover: - first uploaded photo becomes the cover when none is set - clear the cover when the cover photo is trashed or moved out - "set as album cover" in the photo context menu and actions panel OpenSpec changes: fix-large-album-load, fix-album-cover
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"galo/internal/auth"
|
||||
@@ -16,15 +17,16 @@ const maxVariantAttempts = 5
|
||||
// and the object garbage collector entirely from the state recorded in the
|
||||
// metadata layer, so it resumes correctly after a restart.
|
||||
type Worker struct {
|
||||
svc *Service
|
||||
interval time.Duration
|
||||
batch int
|
||||
pendingTTL time.Duration
|
||||
lease time.Duration
|
||||
svc *Service
|
||||
interval time.Duration
|
||||
batch int
|
||||
pendingTTL time.Duration
|
||||
lease time.Duration
|
||||
concurrency int
|
||||
}
|
||||
|
||||
// NewWorker builds a worker. Parameters are clamped to sane defaults.
|
||||
func NewWorker(svc *Service, interval time.Duration, batch int, pendingTTL time.Duration) *Worker {
|
||||
func NewWorker(svc *Service, interval time.Duration, batch int, pendingTTL time.Duration, concurrency int) *Worker {
|
||||
if interval <= 0 {
|
||||
interval = 5 * time.Second
|
||||
}
|
||||
@@ -34,12 +36,16 @@ func NewWorker(svc *Service, interval time.Duration, batch int, pendingTTL time.
|
||||
if pendingTTL <= 0 {
|
||||
pendingTTL = 10 * time.Minute
|
||||
}
|
||||
if concurrency <= 0 {
|
||||
concurrency = 4
|
||||
}
|
||||
return &Worker{
|
||||
svc: svc,
|
||||
interval: interval,
|
||||
batch: batch,
|
||||
pendingTTL: pendingTTL,
|
||||
lease: interval * 4,
|
||||
svc: svc,
|
||||
interval: interval,
|
||||
batch: batch,
|
||||
pendingTTL: pendingTTL,
|
||||
lease: interval * 4,
|
||||
concurrency: concurrency,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,9 +77,36 @@ func (w *Worker) processVariantJobs(ctx context.Context) {
|
||||
slog.Error("claim variant jobs", "error", err)
|
||||
return
|
||||
}
|
||||
for _, id := range ids {
|
||||
w.forEachConcurrent(ids, func(id string) {
|
||||
w.generateVariants(ctx, id)
|
||||
})
|
||||
}
|
||||
|
||||
// forEachConcurrent runs fn over ids with at most w.concurrency functions in
|
||||
// flight. Generation is CPU-bound (decode + resize + encode), so parallelism
|
||||
// scales with cores; a concurrency of 1 stays strictly sequential.
|
||||
func (w *Worker) forEachConcurrent(ids []string, fn func(string)) {
|
||||
if len(ids) == 0 {
|
||||
return
|
||||
}
|
||||
if w.concurrency <= 1 {
|
||||
for _, id := range ids {
|
||||
fn(id)
|
||||
}
|
||||
return
|
||||
}
|
||||
sem := make(chan struct{}, w.concurrency)
|
||||
var wg sync.WaitGroup
|
||||
for _, id := range ids {
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func(id string) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
fn(id)
|
||||
}(id)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (w *Worker) generateVariants(ctx context.Context, photoID string) {
|
||||
@@ -162,7 +195,9 @@ func (w *Worker) notify(ctx context.Context, photoID string) {
|
||||
if err := w.svc.attachURLs(ctx, p); err != nil {
|
||||
return
|
||||
}
|
||||
w.svc.hub.Publish(p.AlbumID, "photo.created", p)
|
||||
// Variant readiness updates an existing photo; it is not a new photo in the
|
||||
// album, so clients update it in place instead of inserting at the top.
|
||||
w.svc.hub.Publish(p.AlbumID, "photo.updated", p)
|
||||
}
|
||||
|
||||
func (w *Worker) fail(ctx context.Context, photoID, msg string) {
|
||||
|
||||
Reference in New Issue
Block a user