refactor(progress): ProgressService required at construction; delete the 9 unreachable erase-fallbacks
Batch H1 step 3a. The ceiling census (exhaustive UpdateUniversalProgress
grep) found 9 nil-guarded `if progressSvc != nil { funnel } else { raw
erase-write }` fallbacks — the survey's "four" under-counted kobo.go,
which has FOUR sites (Markup ReadingSync, Markup last-read-place,
AnalyticsGettests, SyncFromServer), plus one each in media.go, queue.go
and koreader.go. All unreachable: the service is created unconditionally
at main.go:127 and wired into every handler and the queue processor; the
test harness wires them too.
The erasure pattern is now unrepresentable:
- NewMediaHandler / NewKOReaderHandler / NewKoboHandler / both
NewSyncQueueProcessor flavors take the ProgressService as a REQUIRED
constructor arg; the SetProgressService setters are gone.
- Every fallback body (raw UpdateUniversalProgress with explicit-NULL
pgtypes for viewport/scroll/panel/reading_mode/zoom and no
audio_offset_seconds/context_text) is deleted; each write funnels
through SaveProgress with the LWW seed, so fields the writer doesn't
know survive.
- Removed the now-dead koreader pg-type helpers (textPtrToPgText,
intPtrToPgInt4, int64PtrToPgInt8) and their self-tests.
Behavior deltas (stored-row content, no wire shape changes —
KOReader/Kobo response pins stayed green):
- KOReader fallback used to write explicit NULL viewports and drop the
context_text the koplugin sends; the funneled mainline already maps
book.ContextText.
- Queue flushes no longer overwrite chapter_progress with the book
percentage (bogus data; preserved via the seed).
- A web PUT without percentage no longer writes 0% via the deleted
fallback; percentage seeds from the stored row.
Contract suite: KOReader×4 + Kobo×3 pins green.
This commit is contained in:
1 parent
28bd323505
commit
e83c00f869
9 files changed
+163
-419
No files matched your search
+30
-79
@@ -73,26 +73,26 @@ type SyncQueueItem struct {
|
||||
ProcessedAt *time.Time
|
||||
}
|
||||
|
||||
func NewSyncQueueProcessor(db *database.Queries) *SyncQueueProcessor {
|
||||
return NewSyncQueueProcessorWithConfig(db, 5*time.Second, 50)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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, interval time.Duration, batchSize int) *SyncQueueProcessor {
|
||||
func NewSyncQueueProcessorWithConfig(db *database.Queries, progressSvc *ProgressService, interval time.Duration, batchSize int) *SyncQueueProcessor {
|
||||
return &SyncQueueProcessor{
|
||||
db: db,
|
||||
progressSvc: progressSvc,
|
||||
progressChan: make(chan *ProgressUpdate, 100),
|
||||
interval: interval,
|
||||
batchSize: batchSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SyncQueueProcessor) SetProgressService(svc *ProgressService) {
|
||||
p.progressSvc = svc
|
||||
}
|
||||
|
||||
func (p *SyncQueueProcessor) SetAnnotationService(svc *AnnotationService) {
|
||||
p.annotationSvc = svc
|
||||
}
|
||||
@@ -432,89 +432,40 @@ func (p *SyncQueueProcessor) syncProgress(ctx context.Context, userID pgtype.UUI
|
||||
source = v
|
||||
}
|
||||
|
||||
if p.progressSvc != nil {
|
||||
req := SaveProgressRequest{
|
||||
MediaItemID: mediaItemID,
|
||||
UserID: userID,
|
||||
Source: source,
|
||||
Percentage: &percentage,
|
||||
Broadcast: false,
|
||||
}
|
||||
if v, ok := syncData["epubcfi"].(string); ok {
|
||||
req.Epubcfi = &v
|
||||
}
|
||||
// Entries without a CFI are percentage-only by intent: declare it
|
||||
// so the service clears any inherited stale anchor instead of
|
||||
// re-pairing it with this percentage.
|
||||
req.ClearLocator = req.Epubcfi == nil
|
||||
if v, ok := syncData["context_text"].(string); ok {
|
||||
req.ContextText = &v
|
||||
}
|
||||
if v, ok := syncData["chapter"].(float64); ok {
|
||||
ch := int(v)
|
||||
req.Chapter = &ch
|
||||
}
|
||||
if v, ok := syncData["character"].(float64); ok {
|
||||
co := int64(v)
|
||||
req.CharacterOffset = &co
|
||||
}
|
||||
if v, ok := syncData["page"].(float64); ok {
|
||||
pg := int(v)
|
||||
req.CurrentPage = &pg
|
||||
}
|
||||
if v, ok := syncData["total_pages"].(float64); ok {
|
||||
tp := int(v)
|
||||
req.TotalPages = &tp
|
||||
}
|
||||
_, err := p.progressSvc.SaveProgress(ctx, req)
|
||||
return err
|
||||
req := SaveProgressRequest{
|
||||
MediaItemID: mediaItemID,
|
||||
UserID: userID,
|
||||
Source: source,
|
||||
Percentage: &percentage,
|
||||
Broadcast: false,
|
||||
}
|
||||
|
||||
var epubcfi pgtype.Text
|
||||
if v, ok := syncData["epubcfi"].(string); ok {
|
||||
epubcfi = pgtype.Text{String: v, Valid: true}
|
||||
req.Epubcfi = &v
|
||||
}
|
||||
// Entries without a CFI are percentage-only by intent: declare it
|
||||
// so the service clears any inherited stale anchor instead of
|
||||
// re-pairing it with this percentage.
|
||||
req.ClearLocator = req.Epubcfi == nil
|
||||
if v, ok := syncData["context_text"].(string); ok {
|
||||
req.ContextText = &v
|
||||
}
|
||||
|
||||
var chapter pgtype.Int4
|
||||
if v, ok := syncData["chapter"].(float64); ok {
|
||||
chapter = pgtype.Int4{Int32: int32(v), Valid: true}
|
||||
ch := int(v)
|
||||
req.Chapter = &ch
|
||||
}
|
||||
|
||||
var characterOffset pgtype.Int8
|
||||
if v, ok := syncData["character"].(float64); ok {
|
||||
characterOffset = pgtype.Int8{Int64: int64(v), Valid: true}
|
||||
co := int64(v)
|
||||
req.CharacterOffset = &co
|
||||
}
|
||||
|
||||
var currentPage pgtype.Int4
|
||||
if v, ok := syncData["page"].(float64); ok {
|
||||
currentPage = pgtype.Int4{Int32: int32(v), Valid: true}
|
||||
pg := int(v)
|
||||
req.CurrentPage = &pg
|
||||
}
|
||||
|
||||
var totalPages pgtype.Int4
|
||||
if v, ok := syncData["total_pages"].(float64); ok {
|
||||
totalPages = pgtype.Int4{Int32: int32(v), Valid: true}
|
||||
tp := int(v)
|
||||
req.TotalPages = &tp
|
||||
}
|
||||
|
||||
_, err := p.db.UpdateUniversalProgress(ctx, database.UpdateUniversalProgressParams{
|
||||
MediaItemID: mediaItemID,
|
||||
UserID: userID,
|
||||
Percentage: pgtype.Float8{Float64: percentage, Valid: true},
|
||||
Epubcfi: epubcfi,
|
||||
Chapter: chapter,
|
||||
ChapterProgress: pgtype.Float8{Float64: percentage, Valid: true},
|
||||
CharacterOffset: characterOffset,
|
||||
CurrentPage: currentPage,
|
||||
TotalPages: totalPages,
|
||||
LastSyncDevice: pgtype.Text{String: source, Valid: true},
|
||||
LastSyncSource: pgtype.Text{String: source, Valid: true},
|
||||
ViewportX: pgtype.Float8{},
|
||||
ScrollPositionX: pgtype.Float8{},
|
||||
ScrollPositionY: pgtype.Float8{},
|
||||
PanelNumber: pgtype.Int4{},
|
||||
ReadingMode: pgtype.Text{},
|
||||
ZoomLevel: pgtype.Float8{},
|
||||
})
|
||||
|
||||
_, err := p.progressSvc.SaveProgress(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user