feat(sync): add ProgressService with merge, enrichment, and conflict detection
Introduces a centralized ProgressService that handles all reading progress writes across web, KOReader, and Kobo clients. The service implements: - Merge strategy: reads existing progress first, then only overwrites non-nil fields from the incoming request. This fixes the data loss bug where partial updates (e.g., Kobo last-read-place sending only epubcfi and chapter) would NULL out percentage, character_offset, etc. - Enrichment: computes missing fields from available data: - character_offset from percentage + total_characters - current_page from percentage + total_pages - percentage from current_page + total_pages (reverse) - percentage from character_offset + total_characters (reverse) - Conflict detection: when a different source writes progress within 5 minutes with >1% difference, records a sync_conflicts row and broadcasts a WebSocket notification for real-time UI alerts. - Broadcast control: SaveProgressRequest.Broadcast flag lets Kobo last-read-place and SyncFromServer skip WebSocket broadcasts. - Pointer fields on SaveProgressRequest: nil means preserve existing, non-nil means overwrite. Eliminates ambiguity between zero values and not-provided fields. Also adds unit tests for buildProgressSnapshot helper function.
This commit is contained in:
@@ -456,3 +456,43 @@ func TestEdgeCases(t *testing.T) {
|
||||
assert.InDelta(t, 100, page, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildProgressSnapshot(t *testing.T) {
|
||||
t.Run("nil fields are omitted", func(t *testing.T) {
|
||||
pct := 0.5
|
||||
ch := 3
|
||||
req := SaveProgressRequest{
|
||||
Percentage: &pct,
|
||||
Chapter: &ch,
|
||||
}
|
||||
snapshot := buildProgressSnapshot(req)
|
||||
assert.InDelta(t, 0.5, snapshot["percentage"], 0.001)
|
||||
assert.Equal(t, 3, snapshot["chapter"])
|
||||
_, hasEpubcfi := snapshot["epubcfi"]
|
||||
assert.False(t, hasEpubcfi)
|
||||
})
|
||||
|
||||
t.Run("all fields present", func(t *testing.T) {
|
||||
pct := 0.75
|
||||
cfi := "epubcfi(/6/4/2:10)"
|
||||
ch := 5
|
||||
charOff := int64(10000)
|
||||
page := 150
|
||||
tp := 200
|
||||
req := SaveProgressRequest{
|
||||
Percentage: &pct,
|
||||
Epubcfi: &cfi,
|
||||
Chapter: &ch,
|
||||
CharacterOffset: &charOff,
|
||||
CurrentPage: &page,
|
||||
TotalPages: &tp,
|
||||
}
|
||||
snapshot := buildProgressSnapshot(req)
|
||||
assert.InDelta(t, 0.75, snapshot["percentage"], 0.001)
|
||||
assert.Equal(t, "epubcfi(/6/4/2:10)", snapshot["epubcfi"])
|
||||
assert.Equal(t, 5, snapshot["chapter"])
|
||||
assert.Equal(t, int64(10000), snapshot["character"])
|
||||
assert.Equal(t, 150, snapshot["page"])
|
||||
assert.Equal(t, 200, snapshot["total_pages"])
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user