refactor(annotations): one LWW decision core + one tombstone gate + typed queue decoders

The consolidation half of H3 — shared LOGIC with thin per-type remains:

- lwwDecision: the three compareIncoming* functions were the same
  skeleton (client timestamp -> strictly-newer wins; no timestamp ->
  any content difference wins) drifting apart only in per-type
  equality and the schema-driven fallback timestamp (bookmarks carry
  no updated_at). The decision now lives in one function; each compare
  keeps only its equality expression.
- idTargetOwnership + tombstoneWins: the id-targeted resolution blocks
  (HighlightID/BookmarkID/NoteID) shared the ownership check and the
  deleted-row gate — now one gate each instead of three copies of the
  resurrect semantics.
- queue.go: syncNote/syncHighlight/syncBookmark decoded device payloads
  through three 10-key if-type-assertion chains; typed map accessors
  (mapStr/mapF64/mapI32/mapDeviceSyncData) replace them — same
  missing-key-is-zero semantics, declarative field lists. The progress
  decoder is untouched (H1 territory, separately tested).

Per-type remains are the things that genuinely cannot merge: request
structs, dedup triples, the per-table sqlc param builders, and the
KOReader/Kobo payload adapters. Pins, H1/H3 tests, both unit suites
green.
This commit is contained in:
John O'Keefe committed 2026-10-17 15:56:24 -04:00
1 parent 7d698ea0d8
commit 11609d13d1
2 files changed
+120 -161

No files matched your search

+61 -99
View File
@@ -457,32 +457,42 @@ func (p *SyncQueueProcessor) syncProgress(ctx context.Context, userID pgtype.UUI
return err
}
// queue-map accessors: syncData entries arrive as JSON-decoded
// interface{} values; a missing key or wrong type is simply the zero
// value (the decoders only carry fields the device actually pushed).
func mapStr(m map[string]interface{}, key string) string {
v, _ := m[key].(string)
return v
}
func mapF64(m map[string]interface{}, key string) float64 {
v, _ := m[key].(float64)
return v
}
func mapI32(m map[string]interface{}, key string) int32 {
return int32(mapF64(m, key))
}
func mapDeviceSyncData(m map[string]interface{}) json.RawMessage {
if v, ok := m["device_sync_data"].(map[string]interface{}); ok {
raw, _ := json.Marshal(v)
return raw
}
return nil
}
func (p *SyncQueueProcessor) syncNote(ctx context.Context, userID pgtype.UUID, mediaItemID pgtype.UUID, syncData map[string]interface{}) error {
req := SaveNoteRequest{
MediaItemID: mediaItemID,
UserID: userID,
}
if v, ok := syncData["content"].(string); ok {
req.Content = v
}
if v, ok := syncData["position"].(string); ok {
req.Position = v
}
if v, ok := syncData["source"].(string); ok {
req.Source = v
}
if v, ok := syncData["epubcfi_location"].(string); ok {
req.EpubcfiLocation = v
}
if v, ok := syncData["percentage_location"].(float64); ok {
req.PercentageLocation = v
}
if v, ok := syncData["chapter_reference"].(float64); ok {
req.ChapterReference = int32(v)
}
if v, ok := syncData["device_sync_data"].(map[string]interface{}); ok {
req.DeviceSyncData, _ = json.Marshal(v)
MediaItemID: mediaItemID,
UserID: userID,
Content: mapStr(syncData, "content"),
Position: mapStr(syncData, "position"),
Source: mapStr(syncData, "source"),
EpubcfiLocation: mapStr(syncData, "epubcfi_location"),
PercentageLocation: mapF64(syncData, "percentage_location"),
ChapterReference: mapI32(syncData, "chapter_reference"),
DeviceSyncData: mapDeviceSyncData(syncData),
}
_, err := p.annotationSvc.SaveNote(ctx, req)
@@ -491,45 +501,20 @@ 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 {
req := SaveHighlightRequest{
MediaItemID: mediaItemID,
UserID: userID,
}
if v, ok := syncData["selection_text"].(string); ok {
req.SelectionText = v
}
if v, ok := syncData["start_position"].(string); ok {
req.StartPosition = v
}
if v, ok := syncData["end_position"].(string); ok {
req.EndPosition = v
}
if v, ok := syncData["color"].(string); ok {
req.Color = v
}
if v, ok := syncData["note_text"].(string); ok {
req.NoteText = v
}
if v, ok := syncData["source"].(string); ok {
req.Source = v
}
if v, ok := syncData["epubcfi_start"].(string); ok {
req.EpubcfiStart = v
}
if v, ok := syncData["epubcfi_end"].(string); ok {
req.EpubcfiEnd = v
}
if v, ok := syncData["percentage_start"].(float64); ok {
req.PercentageStart = v
}
if v, ok := syncData["percentage_end"].(float64); ok {
req.PercentageEnd = v
}
if v, ok := syncData["chapter_reference"].(float64); ok {
req.ChapterReference = int32(v)
}
if v, ok := syncData["device_sync_data"].(map[string]interface{}); ok {
req.DeviceSyncData, _ = json.Marshal(v)
MediaItemID: mediaItemID,
UserID: userID,
SelectionText: mapStr(syncData, "selection_text"),
StartPosition: mapStr(syncData, "start_position"),
EndPosition: mapStr(syncData, "end_position"),
Color: mapStr(syncData, "color"),
NoteText: mapStr(syncData, "note_text"),
PercentageStart: mapF64(syncData, "percentage_start"),
PercentageEnd: mapF64(syncData, "percentage_end"),
EpubcfiStart: mapStr(syncData, "epubcfi_start"),
EpubcfiEnd: mapStr(syncData, "epubcfi_end"),
ChapterReference: mapI32(syncData, "chapter_reference"),
Source: mapStr(syncData, "source"),
DeviceSyncData: mapDeviceSyncData(syncData),
}
_, err := p.annotationSvc.SaveHighlight(ctx, req)
@@ -538,42 +523,19 @@ 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 {
req := SaveBookmarkRequest{
MediaItemID: mediaItemID,
UserID: userID,
}
if v, ok := syncData["title"].(string); ok {
req.Title = v
}
if v, ok := syncData["position"].(string); ok {
req.Position = v
}
if v, ok := syncData["notes"].(string); ok {
req.Notes = v
}
if v, ok := syncData["source"].(string); ok {
req.Source = v
}
if v, ok := syncData["cfi_position"].(string); ok {
req.CFIPosition = v
}
if v, ok := syncData["epubcfi_location"].(string); ok {
req.EpubcfiLocation = v
}
if v, ok := syncData["percentage_loc"].(float64); ok {
req.PercentageLoc = v
}
if v, ok := syncData["page_number"].(float64); ok {
req.PageNumber = int32(v)
}
if v, ok := syncData["chapter_number"].(float64); ok {
req.ChapterNumber = int32(v)
}
if v, ok := syncData["chapter_reference"].(float64); ok {
req.ChapterReference = int32(v)
}
if v, ok := syncData["device_sync_data"].(map[string]interface{}); ok {
req.DeviceSyncData, _ = json.Marshal(v)
MediaItemID: mediaItemID,
UserID: userID,
Title: mapStr(syncData, "title"),
Position: mapStr(syncData, "position"),
Notes: mapStr(syncData, "notes"),
Source: mapStr(syncData, "source"),
CFIPosition: mapStr(syncData, "cfi_position"),
EpubcfiLocation: mapStr(syncData, "epubcfi_location"),
PercentageLoc: mapF64(syncData, "percentage_loc"),
PageNumber: mapI32(syncData, "page_number"),
ChapterNumber: mapI32(syncData, "chapter_number"),
ChapterReference: mapI32(syncData, "chapter_reference"),
DeviceSyncData: mapDeviceSyncData(syncData),
}
_, err := p.annotationSvc.SaveBookmark(ctx, req)