Implements automatic background synchronization every 5 minutes: - Worker pushes pending changes to server (PushPendingChanges) - Worker pulls new pricelists (SyncPricelistsIfNeeded) - Graceful shutdown with context cancellation - Automatic online/offline detection via DB ping New files: - internal/services/sync/worker.go - Background sync worker - internal/services/local_configuration.go - Local-first CRUD - internal/localdb/converters.go - MariaDB ↔ SQLite converters Extended sync infrastructure: - Pending changes queue (pending_changes table) - Push/pull sync endpoints (/api/sync/push, /pending) - ConfigurationGetter interface for handler compatibility - LocalConfigurationService replaces ConfigurationService All configuration operations now run through SQLite with automatic background sync to MariaDB when online. Phase 2.5 nearly complete. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
96 lines
2.0 KiB
Go
96 lines
2.0 KiB
Go
package sync
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Worker performs background synchronization at regular intervals
|
|
type Worker struct {
|
|
service *Service
|
|
db *gorm.DB
|
|
interval time.Duration
|
|
logger *slog.Logger
|
|
stopCh chan struct{}
|
|
}
|
|
|
|
// NewWorker creates a new background sync worker
|
|
func NewWorker(service *Service, db *gorm.DB, interval time.Duration) *Worker {
|
|
return &Worker{
|
|
service: service,
|
|
db: db,
|
|
interval: interval,
|
|
logger: slog.Default(),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// isOnline checks if the database connection is available
|
|
func (w *Worker) isOnline() bool {
|
|
sqlDB, err := w.db.DB()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return sqlDB.Ping() == nil
|
|
}
|
|
|
|
// Start begins the background sync loop in a goroutine
|
|
func (w *Worker) Start(ctx context.Context) {
|
|
w.logger.Info("starting background sync worker", "interval", w.interval)
|
|
|
|
ticker := time.NewTicker(w.interval)
|
|
defer ticker.Stop()
|
|
|
|
// Run once immediately
|
|
w.runSync()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
w.logger.Info("background sync worker stopped by context")
|
|
return
|
|
case <-w.stopCh:
|
|
w.logger.Info("background sync worker stopped")
|
|
return
|
|
case <-ticker.C:
|
|
w.runSync()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop gracefully stops the worker
|
|
func (w *Worker) Stop() {
|
|
w.logger.Info("stopping background sync worker")
|
|
close(w.stopCh)
|
|
}
|
|
|
|
// runSync performs a single sync iteration
|
|
func (w *Worker) runSync() {
|
|
// Check if online
|
|
if !w.isOnline() {
|
|
w.logger.Debug("offline, skipping background sync")
|
|
return
|
|
}
|
|
|
|
w.logger.Debug("running background sync")
|
|
|
|
// Push pending changes first
|
|
pushed, err := w.service.PushPendingChanges()
|
|
if err != nil {
|
|
w.logger.Warn("failed to push pending changes", "error", err)
|
|
} else if pushed > 0 {
|
|
w.logger.Info("pushed pending changes", "count", pushed)
|
|
}
|
|
|
|
// Then check for new pricelists
|
|
err = w.service.SyncPricelistsIfNeeded()
|
|
if err != nil {
|
|
w.logger.Warn("failed to sync pricelists", "error", err)
|
|
}
|
|
|
|
w.logger.Debug("background sync completed")
|
|
}
|