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

+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++
}
}