refactor(annotations): AnnotationService is a required constructor dependency

H1's doctrine applied to the annotation service, which had been left on
setters: NewMediaHandler/NewKoboHandler/NewKOReaderHandler/
NewSyncQueueProcessor(WithConfig) now take it as a parameter; the four
SetAnnotationService setters are deleted; wiring updated (main.go,
router/sync.go, test harness).

With the service unskippable, the 18 nil-guards and their plain-query
fallbacks die:
- media.go: the create fallbacks for notes/highlights/bookmarks (the
  bookmark one was the H3 census's Bug #1 — it dropped
  percentage/chapter_reference the sync path carries), the highlight
  PUT fallback, and the three delete-path plain deletes (tombstone is
  now the only delete semantics).
- kobo.go: both markup-ingest annotation loops (the bookmark fallback
  wrote the text into media_notes — a category error the fallback
  never should have had).
- koreader.go: all three push-stack guards.
- queue.go: the three "annotation service not available" errors in
  syncNote/syncHighlight/syncBookmark.

Behavior note: the deleted highlight create/PUT fallbacks were the only
paths passing note_id to the plain query — the sync service has no
NoteID field, so the web's highlight→note link was already being
dropped on the live path; this commit makes that pre-existing
limitation explicit rather than papering it over (surfacing note_id on
SaveHighlightRequest is left for a behavior commit that needs it).

Pins (now 20 incl. the 3 annotation CRUD suites) + H1 tests + both
unit suites green.
This commit is contained in:
John O'Keefe committed 2026-10-17 15:47:57 -04:00
1 parent ff029f1ad5
commit c579179fee
9 files changed
+290 -564

No files matched your search

+3 -6
View File
@@ -130,8 +130,7 @@ func main() {
maintenanceCancel := annotationService.StartDailyMaintenance()
defer maintenanceCancel()
queueProcessor := sync.NewSyncQueueProcessorWithConfig(queries, progressService, registry.SyncQueueConfig().Interval, registry.SyncQueueConfig().BatchSize)
queueProcessor.SetAnnotationService(annotationService)
queueProcessor := sync.NewSyncQueueProcessorWithConfig(queries, progressService, annotationService, registry.SyncQueueConfig().Interval, registry.SyncQueueConfig().BatchSize)
// Create library service
libraryService := services.NewLibraryService(queries)
@@ -147,8 +146,7 @@ func main() {
worker := services.NewWorkerWithConfig(workerCfg.Size, workerCfg.QueueCap, connManager)
services.WorkerInstance = worker
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor, progressService)
koreaderHandler.SetAnnotationService(annotationService)
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor, progressService, annotationService)
koreaderHandler.SetLibraryService(libraryService)
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
conflictHandler := handlers.NewConflictHandler(queries, connManager, progressService)
@@ -167,8 +165,7 @@ func main() {
dashboardHandler := handlers.NewDashboardHandler(queries)
seriesHandler := handlers.NewSeriesHandler(queries)
filtersHandler := handlers.NewFiltersHandler(queries)
mediaHandler := handlers.NewMediaHandler(queries, libraryService, progressService)
mediaHandler.SetAnnotationService(annotationService)
mediaHandler := handlers.NewMediaHandler(queries, libraryService, progressService, annotationService)
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
jobsHandler := handlers.NewJobsHandler(queries, worker)
+3 -5
View File
@@ -483,12 +483,11 @@ func setupTestServer(t *testing.T) *TestServerSetup {
progressService := wsync.NewProgressService(queries, connManager)
annotationService := wsync.NewAnnotationService(queries, connManager)
queueProcessor := wsync.NewSyncQueueProcessor(queries, progressService)
queueProcessor := wsync.NewSyncQueueProcessor(queries, progressService, annotationService)
queueCtx, queueCancel := context.WithCancel(context.Background())
go queueProcessor.Start(queueCtx)
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor, progressService)
koreaderHandler.SetAnnotationService(annotationService)
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor, progressService, annotationService)
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
conflictHandler := handlers.NewConflictHandler(queries, connManager, progressService)
analyticsHandler := handlers.NewAnalyticsHandler(queries)
@@ -508,8 +507,7 @@ func setupTestServer(t *testing.T) *TestServerSetup {
statisticsService := services.NewStatisticsService(queries)
statsHandler := handlers.NewStatsHandler(statisticsService)
seriesHandler := handlers.NewSeriesHandler(queries)
mediaHandler := handlers.NewMediaHandler(queries, libraryService, progressService, worker)
mediaHandler.SetAnnotationService(annotationService)
mediaHandler := handlers.NewMediaHandler(queries, libraryService, progressService, annotationService, worker)
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
// Create conversion service for OPDS
+88 -170
View File
@@ -26,12 +26,8 @@ type KoboHandler struct {
bookResolver *services.BookResolver
}
func NewKoboHandler(db *database.Queries, connManager *wsync.ConnectionManager, progressSvc *wsync.ProgressService) *KoboHandler {
return &KoboHandler{db: db, connManager: connManager, progressSvc: progressSvc, bookResolver: services.NewBookResolver(db)}
}
func (h *KoboHandler) SetAnnotationService(svc *wsync.AnnotationService) {
h.annotationSvc = svc
func NewKoboHandler(db *database.Queries, connManager *wsync.ConnectionManager, progressSvc *wsync.ProgressService, annotationSvc *wsync.AnnotationService) *KoboHandler {
return &KoboHandler{db: db, connManager: connManager, progressSvc: progressSvc, annotationSvc: annotationSvc, bookResolver: services.NewBookResolver(db)}
}
func (h *KoboHandler) SetLibraryService(svc LibraryPathResolver) {
@@ -475,65 +471,43 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
switch bookmarkSync.BookmarkType {
case "annotation":
if bookmarkSync.BookmarkText != "" {
if h.annotationSvc != nil {
deviceData, _ := json.Marshal(map[string]interface{}{
"bookmark_id": bookmarkSync.BookmarkId,
"date_created": bookmarkSync.DateCreated,
})
deviceData, _ := json.Marshal(map[string]interface{}{
"bookmark_id": bookmarkSync.BookmarkId,
"date_created": bookmarkSync.DateCreated,
})
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: bookmarkSync.BookmarkText,
StartPosition: bookmarkSync.BookmarkId,
EndPosition: bookmarkSync.BookmarkId,
Color: "#ffff00",
NoteText: bookmarkSync.BookmarkTitle,
Source: "kobo",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSynced++
}
} else {
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: bookmarkSync.BookmarkText,
StartPosition: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
EndPosition: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
Color: pgtype.Text{String: "#ffff00", Valid: true},
})
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: bookmarkSync.BookmarkText,
StartPosition: bookmarkSync.BookmarkId,
EndPosition: bookmarkSync.BookmarkId,
Color: "#ffff00",
NoteText: bookmarkSync.BookmarkTitle,
Source: "kobo",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSynced++
}
}
case "bookmark":
if bookmarkSync.BookmarkText != "" {
if h.annotationSvc != nil {
deviceData, _ := json.Marshal(map[string]interface{}{
"bookmark_id": bookmarkSync.BookmarkId,
"date_created": bookmarkSync.DateCreated,
})
deviceData, _ := json.Marshal(map[string]interface{}{
"bookmark_id": bookmarkSync.BookmarkId,
"date_created": bookmarkSync.DateCreated,
})
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
Title: bookmarkSync.BookmarkText,
Position: bookmarkSync.BookmarkId,
ChapterNumber: int32(bookmarkSync.Chapter),
Source: "kobo",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSynced++
}
} else {
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
Content: bookmarkSync.BookmarkText,
Position: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
})
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
Title: bookmarkSync.BookmarkText,
Position: bookmarkSync.BookmarkId,
ChapterNumber: int32(bookmarkSync.Chapter),
Source: "kobo",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSynced++
}
}
@@ -600,7 +574,7 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
BookmarksSynced: bookmarksSynced,
}
if h.annotationSvc != nil && len(processedBooks) > 0 {
if len(processedBooks) > 0 {
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{
@@ -668,65 +642,43 @@ func (h *KoboHandler) Bookmark(c *echo.Context) error {
switch bookmarkSync.BookmarkType {
case "annotation":
if bookmarkSync.BookmarkText != "" {
if h.annotationSvc != nil {
deviceData, _ := json.Marshal(map[string]interface{}{
"bookmark_id": bookmarkSync.BookmarkId,
"date_created": bookmarkSync.DateCreated,
})
deviceData, _ := json.Marshal(map[string]interface{}{
"bookmark_id": bookmarkSync.BookmarkId,
"date_created": bookmarkSync.DateCreated,
})
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: bookmarkSync.BookmarkText,
StartPosition: bookmarkSync.BookmarkId,
EndPosition: bookmarkSync.BookmarkId,
Color: "#ffff00",
NoteText: bookmarkSync.BookmarkTitle,
Source: "kobo",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSynced++
}
} else {
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: bookmarkSync.BookmarkText,
StartPosition: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
EndPosition: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
Color: pgtype.Text{String: "#ffff00", Valid: true},
})
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: bookmarkSync.BookmarkText,
StartPosition: bookmarkSync.BookmarkId,
EndPosition: bookmarkSync.BookmarkId,
Color: "#ffff00",
NoteText: bookmarkSync.BookmarkTitle,
Source: "kobo",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSynced++
}
}
case "bookmark":
if bookmarkSync.BookmarkText != "" {
if h.annotationSvc != nil {
deviceData, _ := json.Marshal(map[string]interface{}{
"bookmark_id": bookmarkSync.BookmarkId,
"date_created": bookmarkSync.DateCreated,
})
deviceData, _ := json.Marshal(map[string]interface{}{
"bookmark_id": bookmarkSync.BookmarkId,
"date_created": bookmarkSync.DateCreated,
})
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
Title: bookmarkSync.BookmarkText,
Position: bookmarkSync.BookmarkId,
ChapterNumber: int32(bookmarkSync.Chapter),
Source: "kobo",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSynced++
}
} else {
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
Content: bookmarkSync.BookmarkText,
Position: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
})
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
Title: bookmarkSync.BookmarkText,
Position: bookmarkSync.BookmarkId,
ChapterNumber: int32(bookmarkSync.Chapter),
Source: "kobo",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSynced++
}
}
@@ -842,77 +794,43 @@ func (h *KoboHandler) SyncFromServer(c *echo.Context) error {
for _, bookmark := range syncData.Bookmarks {
if bookmark.BookmarkType == "bookmark" {
if h.annotationSvc != nil {
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
Title: bookmark.BookmarkText,
Position: bookmark.BookmarkId,
Source: "kobo",
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSent++
}
} else {
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
Content: bookmark.BookmarkText,
Position: pgtype.Text{String: bookmark.BookmarkId, Valid: true},
})
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
Title: bookmark.BookmarkText,
Position: bookmark.BookmarkId,
Source: "kobo",
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSent++
}
} else if bookmark.BookmarkType == "annotation" {
if h.annotationSvc != nil {
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: bookmark.BookmarkText,
StartPosition: bookmark.BookmarkId,
EndPosition: bookmark.BookmarkId,
Color: "#ffff00",
Source: "kobo",
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
highlightsSent++
}
} else {
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: bookmark.BookmarkText,
StartPosition: pgtype.Text{String: bookmark.BookmarkId, Valid: true},
EndPosition: pgtype.Text{String: bookmark.BookmarkId, Valid: true},
Color: pgtype.Text{String: "#ffff00", Valid: true},
})
highlightsSent++
}
}
}
for _, highlight := range syncData.Highlights {
if h.annotationSvc != nil {
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: highlight.BookmarkText,
StartPosition: highlight.BookmarkId,
EndPosition: highlight.BookmarkId,
SelectionText: bookmark.BookmarkText,
StartPosition: bookmark.BookmarkId,
EndPosition: bookmark.BookmarkId,
Color: "#ffff00",
Source: "kobo",
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
highlightsSent++
}
} else {
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: highlight.BookmarkText,
StartPosition: pgtype.Text{String: highlight.BookmarkId, Valid: true},
EndPosition: pgtype.Text{String: highlight.BookmarkId, Valid: true},
Color: pgtype.Text{String: "#ffff00", Valid: true},
})
}
}
for _, highlight := range syncData.Highlights {
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: highlight.BookmarkText,
StartPosition: highlight.BookmarkId,
EndPosition: highlight.BookmarkId,
Color: "#ffff00",
Source: "kobo",
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
highlightsSent++
}
}
+78 -122
View File
@@ -36,20 +36,17 @@ type LibraryPathResolver interface {
ResolveMediaPath(ctx context.Context, libraryID pgtype.UUID, relativePath string) (string, error)
}
func NewKOReaderHandler(db *database.Queries, connManager *wsync.ConnectionManager, queue *wsync.SyncQueueProcessor, progressSvc *wsync.ProgressService) *KOReaderHandler {
func NewKOReaderHandler(db *database.Queries, connManager *wsync.ConnectionManager, queue *wsync.SyncQueueProcessor, progressSvc *wsync.ProgressService, annotationSvc *wsync.AnnotationService) *KOReaderHandler {
return &KOReaderHandler{
db: db,
connManager: connManager,
queue: queue,
progressSvc: progressSvc,
bookResolver: services.NewBookResolver(db),
db: db,
connManager: connManager,
queue: queue,
progressSvc: progressSvc,
annotationSvc: annotationSvc,
bookResolver: services.NewBookResolver(db),
}
}
func (h *KOReaderHandler) SetAnnotationService(svc *wsync.AnnotationService) {
h.annotationSvc = svc
}
// annotationEpub carries the per-book context every locator conversion
// needs: the media item (format gating) and the resolved EPUB path. It is
// resolved once per request so all annotations in a push share one
@@ -1489,50 +1486,37 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
position = fmt.Sprintf("page:%d", bookmark.Page)
}
if h.annotationSvc != nil {
ec := h.loadAnnotationEpub(ctx, mediaItemID)
ec := h.loadAnnotationEpub(ctx, mediaItemID)
pctLoc := 0.0
if bookmark.Percentage != nil {
pctLoc = *bookmark.Percentage
}
if pctLoc == 0 {
pctLoc = h.deriveAnnotationPercentage(ec, position, int(bookmark.Page))
}
cfiPosition := h.convertBookmarkPosition(ec, position, pctLoc)
pctLoc := 0.0
if bookmark.Percentage != nil {
pctLoc = *bookmark.Percentage
}
if pctLoc == 0 {
pctLoc = h.deriveAnnotationPercentage(ec, position, int(bookmark.Page))
}
cfiPosition := h.convertBookmarkPosition(ec, position, pctLoc)
deviceData, _ := json.Marshal(map[string]interface{}{
"datetime": bookmark.Datetime,
"pos0": bookmark.Pos0,
"page": bookmark.Page,
})
deviceData, _ := json.Marshal(map[string]interface{}{
"datetime": bookmark.Datetime,
"pos0": bookmark.Pos0,
"page": bookmark.Page,
})
result, err := h.annotationSvc.SaveBookmark(ctx, wsync.SaveBookmarkRequest{
MediaItemID: mediaItemID,
UserID: pgUserID,
Title: bookmark.Text,
Position: position,
CFIPosition: cfiPosition,
PercentageLoc: pctLoc,
ChapterNumber: int32(bookmark.Chapter),
Source: "koreader",
OriginSource: "koreader",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSynced++
}
} else {
_, err := h.db.CreateMediaNote(ctx, database.CreateMediaNoteParams{
MediaItemID: mediaItemID,
UserID: pgUserID,
Content: bookmark.Text,
Position: pgtype.Text{String: position, Valid: position != ""},
})
if err == nil {
bookmarksSynced++
}
result, err := h.annotationSvc.SaveBookmark(ctx, wsync.SaveBookmarkRequest{
MediaItemID: mediaItemID,
UserID: pgUserID,
Title: bookmark.Text,
Position: position,
CFIPosition: cfiPosition,
PercentageLoc: pctLoc,
ChapterNumber: int32(bookmark.Chapter),
Source: "koreader",
OriginSource: "koreader",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
bookmarksSynced++
}
}
@@ -1554,35 +1538,22 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
position = fmt.Sprintf("page:%d", note.Page)
}
if h.annotationSvc != nil {
deviceData, _ := json.Marshal(map[string]interface{}{
"datetime": note.Datetime,
"pos0": note.Pos0,
"page": note.Page,
})
deviceData, _ := json.Marshal(map[string]interface{}{
"datetime": note.Datetime,
"pos0": note.Pos0,
"page": note.Page,
})
result, err := h.annotationSvc.SaveNote(ctx, wsync.SaveNoteRequest{
MediaItemID: mediaItemID,
UserID: pgUserID,
Content: note.Notes,
Position: position,
Source: "koreader",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
notesSynced++
}
} else {
_, err := h.db.CreateMediaNote(ctx, database.CreateMediaNoteParams{
MediaItemID: mediaItemID,
UserID: pgUserID,
Content: note.Notes,
Position: pgtype.Text{String: position, Valid: position != ""},
})
if err == nil {
notesSynced++
}
result, err := h.annotationSvc.SaveNote(ctx, wsync.SaveNoteRequest{
MediaItemID: mediaItemID,
UserID: pgUserID,
Content: note.Notes,
Position: position,
Source: "koreader",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
notesSynced++
}
}
@@ -1609,53 +1580,38 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
color = mapColorFromKOReader(highlight.Color)
}
if h.annotationSvc != nil {
ec := h.loadAnnotationEpub(ctx, mediaItemID)
ec := h.loadAnnotationEpub(ctx, mediaItemID)
pctStart := 0.0
if highlight.Percentage != nil {
pctStart = *highlight.Percentage
}
pctStart := 0.0
if highlight.Percentage != nil {
pctStart = *highlight.Percentage
}
epubcfiStart, epubcfiEnd := h.convertHighlightPositions(ec, highlight.Pos0, highlight.Pos1, highlight.Text, pctStart)
epubcfiStart, epubcfiEnd := h.convertHighlightPositions(ec, highlight.Pos0, highlight.Pos1, highlight.Text, pctStart)
deviceData, _ := json.Marshal(map[string]interface{}{
"datetime": highlight.Datetime,
"pos0": highlight.Pos0,
"pos1": highlight.Pos1,
"page": highlight.Page,
})
deviceData, _ := json.Marshal(map[string]interface{}{
"datetime": highlight.Datetime,
"pos0": highlight.Pos0,
"pos1": highlight.Pos1,
"page": highlight.Page,
})
result, err := h.annotationSvc.SaveHighlight(ctx, wsync.SaveHighlightRequest{
MediaItemID: mediaItemID,
UserID: pgUserID,
SelectionText: highlight.Text,
StartPosition: startPos,
EndPosition: endPos,
Color: color,
NoteText: highlight.Notes,
PercentageStart: pctStart,
EpubcfiStart: epubcfiStart,
EpubcfiEnd: epubcfiEnd,
Source: "koreader",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
highlightsSynced++
}
} else {
_, err := h.db.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{
MediaItemID: mediaItemID,
UserID: pgUserID,
SelectionText: highlight.Text,
StartPosition: pgtype.Text{String: startPos, Valid: startPos != ""},
EndPosition: pgtype.Text{String: endPos, Valid: endPos != ""},
Color: pgtype.Text{String: color, Valid: true},
})
if err == nil {
highlightsSynced++
}
result, err := h.annotationSvc.SaveHighlight(ctx, wsync.SaveHighlightRequest{
MediaItemID: mediaItemID,
UserID: pgUserID,
SelectionText: highlight.Text,
StartPosition: startPos,
EndPosition: endPos,
Color: color,
NoteText: highlight.Notes,
PercentageStart: pctStart,
EpubcfiStart: epubcfiStart,
EpubcfiEnd: epubcfiEnd,
Source: "koreader",
DeviceSyncData: deviceData,
})
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
highlightsSynced++
}
}
+98 -225
View File
@@ -183,12 +183,13 @@ type MediaHandler struct {
// NewMediaHandler requires the shared ProgressService — progress writes
// must funnel through SaveProgress (the LWW-seeded canonical writer);
// a nil service has nowhere to fall back to and must not be wired.
func NewMediaHandler(db *database.Queries, libraryService *services.LibraryService, progressSvc *wsync.ProgressService, worker ...*services.Worker) *MediaHandler {
func NewMediaHandler(db *database.Queries, libraryService *services.LibraryService, progressSvc *wsync.ProgressService, annotationSvc *wsync.AnnotationService, worker ...*services.Worker) *MediaHandler {
mh := &MediaHandler{
db: db,
libraryService: libraryService,
searchService: services.NewSearchService(db),
progressSvc: progressSvc,
annotationSvc: annotationSvc,
}
if len(worker) > 0 && worker[0] != nil {
mh.worker = worker[0]
@@ -196,10 +197,6 @@ func NewMediaHandler(db *database.Queries, libraryService *services.LibraryServi
return mh
}
func (mh *MediaHandler) SetAnnotationService(svc *wsync.AnnotationService) {
mh.annotationSvc = svc
}
// ExecuteSearch performs search and returns results with count
// Public wrapper for shared search logic used by both JSON and HTML endpoints
func (h *MediaHandler) ExecuteSearch(ctx context.Context, params services.SearchParams) ([]database.SearchMediaItemsUnifiedRow, int, error) {
@@ -1197,22 +1194,22 @@ func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error {
// additive keys per docs/contract_pins.md (H1).
func progressResponse(progress database.GetUniversalProgressRow) map[string]interface{} {
return map[string]interface{}{
"id": progress.ID,
"media_item_id": progress.MediaItemID,
"user_id": progress.UserID,
"current_page": progress.CurrentPage,
"total_pages": progress.TotalPages,
"last_read_at": progress.LastReadAt,
"percentage": progress.Percentage,
"character_offset": progress.CharacterOffset,
"epubcfi": progress.Epubcfi,
"chapter": progress.Chapter,
"chapter_progress": progress.ChapterProgress,
"format_group": progress.FormatGroup,
"total_characters": progress.TotalCharacters,
"chapter_count": progress.ChapterCount,
"last_sync_device": progress.LastSyncDevice,
"last_sync_source": progress.LastSyncSource,
"id": progress.ID,
"media_item_id": progress.MediaItemID,
"user_id": progress.UserID,
"current_page": progress.CurrentPage,
"total_pages": progress.TotalPages,
"last_read_at": progress.LastReadAt,
"percentage": progress.Percentage,
"character_offset": progress.CharacterOffset,
"epubcfi": progress.Epubcfi,
"chapter": progress.Chapter,
"chapter_progress": progress.ChapterProgress,
"format_group": progress.FormatGroup,
"total_characters": progress.TotalCharacters,
"chapter_count": progress.ChapterCount,
"last_sync_device": progress.LastSyncDevice,
"last_sync_source": progress.LastSyncSource,
"last_sync_timestamp": progress.LastSyncTimestamp,
// Row-carried fields surfaced additively (H1) — the web audiobook
// reader already reads audio_offset_seconds (audio.ts) and the
@@ -1830,34 +1827,19 @@ func (mh *MediaHandler) CreateMediaNote(c *echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
var note database.MediaNotes
if mh.annotationSvc != nil {
result, err := mh.annotationSvc.SaveNote(c.Request().Context(), wsync.SaveNoteRequest{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
Content: req.Content,
Position: req.Position,
Source: "web",
ModifiedAt: time.Now(),
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
note = result.Note
} else {
var err error
note, err = mh.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
Content: req.Content,
Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
result, err := mh.annotationSvc.SaveNote(c.Request().Context(), wsync.SaveNoteRequest{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
Content: req.Content,
Position: req.Position,
Source: "web",
ModifiedAt: time.Now(),
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, note)
return c.JSON(http.StatusCreated, result.Note)
}
// GetMediaNote handles GET /api/media-items/:id/notes/:noteId
@@ -1928,15 +1910,7 @@ func (mh *MediaHandler) DeleteMediaNote(c *echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
if mh.annotationSvc != nil {
err = mh.annotationSvc.TombstoneNoteByID(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true}, pgtype.UUID{Bytes: userUUID, Valid: true})
} else {
err = mh.db.DeleteMediaNote(c.Request().Context(), database.DeleteMediaNoteParams{
ID: pgtype.UUID{Bytes: noteUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
}
if err != nil {
if err := mh.annotationSvc.TombstoneNoteByID(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true}, pgtype.UUID{Bytes: userUUID, Valid: true}); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
@@ -1990,15 +1964,6 @@ func (mh *MediaHandler) CreateMediaHighlight(c *echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
var noteUUID pgtype.UUID
if req.NoteID != "" {
if noteID, err := uuid.Parse(req.NoteID); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
} else {
noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
}
}
color := "#ffff00"
if req.Color != "" {
color = req.Color
@@ -2007,43 +1972,26 @@ func (mh *MediaHandler) CreateMediaHighlight(c *echo.Context) error {
pgMediaID := pgtype.UUID{Bytes: mediaUUID, Valid: true}
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
if mh.annotationSvc != nil {
result, err := mh.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
MediaItemID: pgMediaID,
UserID: pgUserID,
SelectionText: req.SelectionText,
StartPosition: req.StartPosition,
EndPosition: req.EndPosition,
EpubcfiStart: req.EpubcfiStart,
EpubcfiEnd: req.EpubcfiEnd,
Color: color,
NoteText: req.NoteText,
PercentageStart: req.PercentageStart,
PercentageEnd: req.PercentageEnd,
ChapterReference: req.ChapterReference,
Source: "web",
ModifiedAt: time.Now(),
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, result.Highlight)
}
highlight, err := mh.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
MediaItemID: pgMediaID,
UserID: pgUserID,
SelectionText: req.SelectionText,
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
Color: pgtype.Text{String: color, Valid: true},
NoteID: noteUUID,
result, err := mh.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
MediaItemID: pgMediaID,
UserID: pgUserID,
SelectionText: req.SelectionText,
StartPosition: req.StartPosition,
EndPosition: req.EndPosition,
EpubcfiStart: req.EpubcfiStart,
EpubcfiEnd: req.EpubcfiEnd,
Color: color,
NoteText: req.NoteText,
PercentageStart: req.PercentageStart,
PercentageEnd: req.PercentageEnd,
ChapterReference: req.ChapterReference,
Source: "web",
ModifiedAt: time.Now(),
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, highlight)
return c.JSON(http.StatusCreated, result.Highlight)
}
// GetMediaHighlight handles GET /api/media-items/:id/highlights/:highlightId
@@ -2081,81 +2029,46 @@ func (mh *MediaHandler) UpdateMediaHighlight(c *echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
var noteUUID pgtype.UUID
if req.NoteID != "" {
if noteID, err := uuid.Parse(req.NoteID); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
} else {
noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
}
}
color := "#ffff00"
if req.Color != "" {
color = req.Color
}
// Prefer the sync-aware path: the same selection text + CFI resolves to
// the same dedup key, so this performs an LWW update of the existing row
// (including note_text and CFI columns the plain query cannot touch).
if mh.annotationSvc != nil {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
result, err := mh.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
SelectionText: req.SelectionText,
StartPosition: req.StartPosition,
EndPosition: req.EndPosition,
EpubcfiStart: req.EpubcfiStart,
EpubcfiEnd: req.EpubcfiEnd,
Color: color,
NoteText: req.NoteText,
PercentageStart: req.PercentageStart,
PercentageEnd: req.PercentageEnd,
ChapterReference: req.ChapterReference,
Source: "web",
ModifiedAt: time.Now(),
// The PUT targets this exact row (from the URL): identity must
// not be re-derived from content — a device echo has usually
// rewritten the stored CFI to point shape, so the computed key
// would miss and mint a duplicate beside the edited row.
HighlightID: pgtype.UUID{Bytes: highlightUUID, Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, result.Highlight)
}
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
highlight, err := mh.db.UpdateMediaHighlight(c.Request().Context(), database.UpdateMediaHighlightParams{
ID: pgtype.UUID{Bytes: highlightUUID, Valid: true},
SelectionText: req.SelectionText,
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
Color: pgtype.Text{String: color, Valid: true},
NoteID: noteUUID,
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
result, err := mh.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
SelectionText: req.SelectionText,
StartPosition: req.StartPosition,
EndPosition: req.EndPosition,
EpubcfiStart: req.EpubcfiStart,
EpubcfiEnd: req.EpubcfiEnd,
Color: color,
NoteText: req.NoteText,
PercentageStart: req.PercentageStart,
PercentageEnd: req.PercentageEnd,
ChapterReference: req.ChapterReference,
Source: "web",
ModifiedAt: time.Now(),
// The PUT targets this exact row (from the URL): identity must
// not be re-derived from content — a device echo has usually
// rewritten the stored CFI to point shape, so the computed key
// would miss and mint a duplicate beside the edited row.
HighlightID: pgtype.UUID{Bytes: highlightUUID, Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, highlight)
return c.JSON(http.StatusOK, result.Highlight)
}
// DeleteMediaHighlight handles DELETE /api/media-items/:id/highlights/:highlightId
@@ -2175,24 +2088,12 @@ func (mh *MediaHandler) DeleteMediaHighlight(c *echo.Context) error {
}
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
if mh.annotationSvc != nil {
if err := mh.annotationSvc.TombstoneHighlightByID(c.Request().Context(), pgUserID, pgHighlightID, "web"); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
if err := mh.annotationSvc.TombstoneHighlightByID(c.Request().Context(), pgUserID, pgHighlightID, "web"); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"})
}
return c.NoContent(http.StatusNoContent)
}
err = mh.db.DeleteMediaHighlight(c.Request().Context(), database.DeleteMediaHighlightParams{
ID: pgHighlightID,
UserID: pgUserID,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.NoContent(http.StatusNoContent)
}
@@ -2243,48 +2144,31 @@ func (mh *MediaHandler) CreateMediaBookmark(c *echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
// The sync-aware path (dedup + LWW + tombstones) is preferred; fall back
// to the plain query when the service isn't wired (e.g. some tests).
if mh.annotationSvc != nil {
origin := req.Origin
if origin == "" {
origin = "web"
}
result, err := mh.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
Title: req.Title,
Position: req.Position,
Notes: req.Notes,
PageNumber: req.PageNumber,
ChapterNumber: req.ChapterNumber,
CFIPosition: req.CfiPosition,
PercentageLoc: req.Percentage,
ChapterReference: req.ChapterReference,
Source: "web",
OriginSource: origin,
ModifiedAt: time.Now(),
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, result.Bookmark)
// The sync-aware path (dedup + LWW + tombstones) is the only path: the
// annotation service is a required constructor dependency.
origin := req.Origin
if origin == "" {
origin = "web"
}
bookmark, err := mh.db.CreateMediaBookmark(c.Request().Context(), database.CreateMediaBookmarkParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
PageNumber: pgtype.Int4{Int32: req.PageNumber, Valid: req.PageNumber > 0},
ChapterNumber: pgtype.Int4{Int32: req.ChapterNumber, Valid: req.ChapterNumber > 0},
CfiPosition: pgtype.Text{String: req.CfiPosition, Valid: req.CfiPosition != ""},
Title: req.Title,
Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
Notes: pgtype.Text{String: req.Notes, Valid: req.Notes != ""},
result, err := mh.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
Title: req.Title,
Position: req.Position,
Notes: req.Notes,
PageNumber: req.PageNumber,
ChapterNumber: req.ChapterNumber,
CFIPosition: req.CfiPosition,
PercentageLoc: req.Percentage,
ChapterReference: req.ChapterReference,
Source: "web",
OriginSource: origin,
ModifiedAt: time.Now(),
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, bookmark)
return c.JSON(http.StatusCreated, result.Bookmark)
}
// UpdateMediaBookmark handles PUT /api/media-items/:id/bookmarks/:bookmarkId
@@ -2340,23 +2224,12 @@ func (mh *MediaHandler) DeleteMediaBookmark(c *echo.Context) error {
}
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
if mh.annotationSvc != nil {
if err := mh.annotationSvc.TombstoneBookmarkByID(c.Request().Context(), pgUserID, pgBookmarkID, "web"); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "bookmark not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
if err := mh.annotationSvc.TombstoneBookmarkByID(c.Request().Context(), pgUserID, pgBookmarkID, "web"); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return c.JSON(http.StatusNotFound, map[string]string{"error": "bookmark not found"})
}
return c.NoContent(http.StatusNoContent)
}
if err := mh.db.DeleteMediaBookmark(c.Request().Context(), database.DeleteMediaBookmarkParams{
ID: pgBookmarkID,
UserID: pgUserID,
}); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.NoContent(http.StatusNoContent)
}
+1 -2
View File
@@ -37,8 +37,7 @@ func registerSyncRoutes(cfg *Config) {
// Kobo sync routes (device authentication required)
// Kobo devices use URL path: /api/sync/kobo/{token}/markup
// API clients can use Authorization header: Authorization: Bearer {token}
koboHandler := handlers.NewKoboHandler(cfg.Queries, cfg.ConnManager, cfg.ProgressService)
koboHandler.SetAnnotationService(cfg.AnnotationService)
koboHandler := handlers.NewKoboHandler(cfg.Queries, cfg.ConnManager, cfg.ProgressService, cfg.AnnotationService)
koboHandler.SetLibraryService(cfg.LibraryService)
koboSync := e.Group("/api/sync/kobo/:token")
koboSync.POST("/markup", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Markup))
+9 -9
View File
@@ -73,15 +73,15 @@ type SaveHighlightRequest struct {
// stored CFI drifts range→point shape after device echoes, and the
// user can edit the selection text — so a key-based upsert would mint
// a duplicate beside the very row being edited.
HighlightID pgtype.UUID
PercentageStart float64
PercentageEnd float64
EpubcfiStart string
EpubcfiEnd string
ChapterReference int32
Source string
ModifiedAt time.Time
DeviceSyncData json.RawMessage
HighlightID pgtype.UUID
PercentageStart float64
PercentageEnd float64
EpubcfiStart string
EpubcfiEnd string
ChapterReference int32
Source string
ModifiedAt time.Time
DeviceSyncData json.RawMessage
// DedupKey overrides the computed key when the client echoes back an
// annotation it received from us (device echoes carry device-native
// locators, so the computed key would never match the original row and
+9 -24
View File
@@ -76,27 +76,24 @@ type SyncQueueItem struct {
// NewSyncQueueProcessor requires the shared ProgressService — queue
// progress entries flush through SaveProgress (the LWW-seeded canonical
// writer); a nil service has nowhere to fall back to and must not be wired.
func NewSyncQueueProcessor(db *database.Queries, progressSvc *ProgressService) *SyncQueueProcessor {
return NewSyncQueueProcessorWithConfig(db, progressSvc, 5*time.Second, 50)
func NewSyncQueueProcessor(db *database.Queries, progressSvc *ProgressService, annotationSvc *AnnotationService) *SyncQueueProcessor {
return NewSyncQueueProcessorWithConfig(db, progressSvc, annotationSvc, 5*time.Second, 50)
}
// NewSyncQueueProcessorWithConfig constructs a processor with the given flush
// interval and batch size. Used at startup to source values from the settings
// registry.
func NewSyncQueueProcessorWithConfig(db *database.Queries, progressSvc *ProgressService, interval time.Duration, batchSize int) *SyncQueueProcessor {
func NewSyncQueueProcessorWithConfig(db *database.Queries, progressSvc *ProgressService, annotationSvc *AnnotationService, interval time.Duration, batchSize int) *SyncQueueProcessor {
return &SyncQueueProcessor{
db: db,
progressSvc: progressSvc,
progressChan: make(chan *ProgressUpdate, 100),
interval: interval,
batchSize: batchSize,
db: db,
progressSvc: progressSvc,
annotationSvc: annotationSvc,
progressChan: make(chan *ProgressUpdate, 100),
interval: interval,
batchSize: batchSize,
}
}
func (p *SyncQueueProcessor) SetAnnotationService(svc *AnnotationService) {
p.annotationSvc = svc
}
func (p *SyncQueueProcessor) Start(ctx context.Context) {
log.Printf("Starting sync queue processor (interval: %v, batch: %d)", p.interval, p.batchSize)
@@ -461,10 +458,6 @@ func (p *SyncQueueProcessor) syncProgress(ctx context.Context, userID pgtype.UUI
}
func (p *SyncQueueProcessor) syncNote(ctx context.Context, userID pgtype.UUID, mediaItemID pgtype.UUID, syncData map[string]interface{}) error {
if p.annotationSvc == nil {
return fmt.Errorf("annotation service not available")
}
req := SaveNoteRequest{
MediaItemID: mediaItemID,
UserID: userID,
@@ -497,10 +490,6 @@ func (p *SyncQueueProcessor) syncNote(ctx context.Context, userID pgtype.UUID, m
}
func (p *SyncQueueProcessor) syncHighlight(ctx context.Context, userID pgtype.UUID, mediaItemID pgtype.UUID, syncData map[string]interface{}) error {
if p.annotationSvc == nil {
return fmt.Errorf("annotation service not available")
}
req := SaveHighlightRequest{
MediaItemID: mediaItemID,
UserID: userID,
@@ -548,10 +537,6 @@ func (p *SyncQueueProcessor) syncHighlight(ctx context.Context, userID pgtype.UU
}
func (p *SyncQueueProcessor) syncBookmark(ctx context.Context, userID pgtype.UUID, mediaItemID pgtype.UUID, syncData map[string]interface{}) error {
if p.annotationSvc == nil {
return fmt.Errorf("annotation service not available")
}
req := SaveBookmarkRequest{
MediaItemID: mediaItemID,
UserID: userID,
+1 -1
View File
@@ -8,7 +8,7 @@ import (
)
func TestCalculateNextRetry(t *testing.T) {
processor := NewSyncQueueProcessor(nil, nil)
processor := NewSyncQueueProcessor(nil, nil, nil)
tests := []struct {
name string