Introduced ConnectionManager to support offline-first architecture: - New internal/db/connection.go with thread-safe connection management - Lazy connection establishment (5s timeout, 10s cooldown) - Automatic ping caching (30s interval) to avoid excessive checks - Updated middleware/offline.go to use ConnectionManager.IsOnline() - Updated sync/worker.go to use ConnectionManager instead of direct DB This enables the application to start without MariaDB and gracefully handle offline/online transitions. Part of Phase 2.5: Full Offline Mode Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
90 lines
2.0 KiB
Go
90 lines
2.0 KiB
Go
package sync
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/quoteforge/internal/db"
|
|
)
|
|
|
|
// Worker performs background synchronization at regular intervals
|
|
type Worker struct {
|
|
service *Service
|
|
connMgr *db.ConnectionManager
|
|
interval time.Duration
|
|
logger *slog.Logger
|
|
stopCh chan struct{}
|
|
}
|
|
|
|
// NewWorker creates a new background sync worker
|
|
func NewWorker(service *Service, connMgr *db.ConnectionManager, interval time.Duration) *Worker {
|
|
return &Worker{
|
|
service: service,
|
|
connMgr: connMgr,
|
|
interval: interval,
|
|
logger: slog.Default(),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// isOnline checks if the database connection is available
|
|
func (w *Worker) isOnline() bool {
|
|
return w.connMgr.IsOnline()
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Push pending changes first
|
|
pushed, err := w.service.PushPendingChanges()
|
|
if err != nil {
|
|
w.logger.Warn("background sync: failed to push pending changes", "error", err)
|
|
} else if pushed > 0 {
|
|
w.logger.Info("background sync: pushed pending changes", "count", pushed)
|
|
}
|
|
|
|
// Then check for new pricelists
|
|
err = w.service.SyncPricelistsIfNeeded()
|
|
if err != nil {
|
|
w.logger.Warn("background sync: failed to sync pricelists", "error", err)
|
|
}
|
|
|
|
w.logger.Info("background sync cycle completed")
|
|
}
|