diff --git a/internal/handlers/kobo.go b/internal/handlers/kobo.go index d36efd4..73ab1ee 100644 --- a/internal/handlers/kobo.go +++ b/internal/handlers/kobo.go @@ -623,7 +623,7 @@ func (h *KoboHandler) Markup(c *echo.Context) error { } if h.annotationSvc != nil && len(processedBooks) > 0 { - cutoff := pgtype.Timestamptz{Time: time.Now().Add(-wsync.TombstoneTTL), Valid: true} + cutoff := pgtype.Timestamptz{Time: time.Now().Add(-h.annotationSvc.ActiveTombstoneTTL()), Valid: true} for mediaItemID, contentId := range processedBooks { tombstones, _ := h.db.GetTombstonedAnnotationsForBook(c.Request().Context(), database.GetTombstonedAnnotationsForBookParams{ MediaItemID: mediaItemID, diff --git a/internal/handlers/koreader.go b/internal/handlers/koreader.go index a28b1d0..a19775e 100644 --- a/internal/handlers/koreader.go +++ b/internal/handlers/koreader.go @@ -854,7 +854,7 @@ func (h *KOReaderHandler) GetMetadata(c *echo.Context) error { annotationsResponse.Bookmarks = append(annotationsResponse.Bookmarks, koreaderBookmark) } - cutoff := pgtype.Timestamptz{Time: time.Now().Add(-wsync.TombstoneTTL), Valid: true} + cutoff := pgtype.Timestamptz{Time: time.Now().Add(-h.annotationSvc.ActiveTombstoneTTL()), Valid: true} tombstones, _ := h.db.GetTombstonedAnnotationsForBook(c.Request().Context(), database.GetTombstonedAnnotationsForBookParams{ MediaItemID: pgBookUUID, UserID: pgUserID, diff --git a/internal/sync/annotations.go b/internal/sync/annotations.go index d047479..4689436 100644 --- a/internal/sync/annotations.go +++ b/internal/sync/annotations.go @@ -18,8 +18,38 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +// TombstoneTTL is the fallback retention for soft-deleted annotations when no +// settings registry is wired (e.g. in tests). It matches the historical value. const TombstoneTTL = 30 * 24 * time.Hour +type AnnotationService struct { + db *database.Queries + connMgr *ConnectionManager + settings *database.SettingsRegistry +} + +func NewAnnotationService(db *database.Queries, connMgr *ConnectionManager) *AnnotationService { + return &AnnotationService{db: db, connMgr: connMgr} +} + +// SetSettings wires the tunable settings registry. When wired, the tombstone +// TTL is read live from the DB; otherwise the package const TombstoneTTL is +// used. +func (s *AnnotationService) SetSettings(reg *database.SettingsRegistry) { s.settings = reg } + +// tombstoneTTL returns the active tombstone retention window. +func (s *AnnotationService) tombstoneTTL() time.Duration { + if s.settings != nil { + return s.settings.TombstoneTTL() + } + return TombstoneTTL +} + +// ActiveTombstoneTTL exposes the configured tombstone retention window for +// callers outside the sync package (e.g. kobo/koreader handlers) that need to +// compute cutoffs consistently with the service. +func (s *AnnotationService) ActiveTombstoneTTL() time.Duration { return s.tombstoneTTL() } + type SaveOutcome string const ( @@ -29,15 +59,6 @@ const ( SaveOutcomeDeleted SaveOutcome = "deleted" ) -type AnnotationService struct { - db *database.Queries - connMgr *ConnectionManager -} - -func NewAnnotationService(db *database.Queries, connMgr *ConnectionManager) *AnnotationService { - return &AnnotationService{db: db, connMgr: connMgr} -} - type SaveHighlightRequest struct { MediaItemID pgtype.UUID UserID pgtype.UUID @@ -79,7 +100,7 @@ func (s *AnnotationService) SaveHighlight(ctx context.Context, req SaveHighlight } if existing.Deleted.Bool { - if existing.DeletedAt.Valid && time.Since(existing.DeletedAt.Time) < TombstoneTTL { + if existing.DeletedAt.Valid && time.Since(existing.DeletedAt.Time) < s.tombstoneTTL() { return &SaveHighlightResult{Highlight: existing, Outcome: SaveOutcomeDeleted}, nil } return s.createHighlight(ctx, req, dedupKey) @@ -238,7 +259,7 @@ func (s *AnnotationService) TombstoneHighlightByID( } func (s *AnnotationService) PurgeExpiredTombstones(ctx context.Context) error { - cutoff := pgtype.Timestamptz{Time: time.Now().Add(-TombstoneTTL), Valid: true} + cutoff := pgtype.Timestamptz{Time: time.Now().Add(-s.tombstoneTTL()), Valid: true} if err := s.db.PurgeExpiredHighlightTombstones(ctx, cutoff); err != nil { return fmt.Errorf("purge highlight tombstones: %w", err) } @@ -457,7 +478,7 @@ func (s *AnnotationService) SaveBookmark(ctx context.Context, req SaveBookmarkRe } if existing.Deleted.Bool { - if existing.DeletedAt.Valid && time.Since(existing.DeletedAt.Time) < TombstoneTTL { + if existing.DeletedAt.Valid && time.Since(existing.DeletedAt.Time) < s.tombstoneTTL() { return &SaveBookmarkResult{Bookmark: existing, Outcome: SaveOutcomeDeleted}, nil } return s.createBookmark(ctx, req, dedupKey)