package store import ( "context" "errors" "fmt" "strings" "time" "yovision/sense/internal/device" ) const ( DriverSQLite = "sqlite" DriverPostgres = "postgres" ) var ( ErrQuotaProjectionUnavailable = errors.New("quota projection unavailable") ErrQuotaProjectionInvalid = errors.New("quota projection invalid") ErrAreaPolicyUnavailable = errors.New("area policy unavailable") ErrAreaPolicyInvalid = errors.New("area policy invalid") ErrAreaPolicyDenied = errors.New("area policy denies imaging device") ErrReconcileLeaseLost = errors.New("reconcile lease lost") ErrOperationalLeaseLost = errors.New("operational lease lost") ) // Repository is the storage boundary used by the Sense process. SQLite stays // available for M1 development; PostgreSQL implements the M2 production path. type Repository interface { Close() error CreateDevice(context.Context, device.Device) error SetDesiredState(context.Context, string, device.DesiredState) error GetDevice(context.Context, string) (device.Device, error) ListDueReconcile(context.Context, time.Time, int) ([]ReconcileCandidate, error) ClaimDueReconcile(context.Context, ReconcileClaim) ([]ReconcileCandidate, error) RenewReconcileLease(context.Context, string, string, string, time.Time, time.Duration) (bool, error) CompleteReconcile(context.Context, string, int64, string, string, time.Time) error FailReconcile(context.Context, string, int, time.Time, string, string, string, time.Time) error ListEnabledVideoDevices(context.Context, int) ([]device.Device, error) MarkReconciled(context.Context, string, int64, time.Time) error MarkReconcileFailure(context.Context, string, int, time.Time, string, time.Time) error UpdateActualState(context.Context, string, device.ActualState, time.Time) error RequestReconcile(context.Context, string, time.Time) error ConvergenceSnapshot(context.Context) (ConvergenceSnapshot, error) } // ReconcileClaim identifies one short-lived batch claim. Token is unique per // run and fences a worker whose lease expired and was acquired by another // process. SQLite accepts the shape but remains explicitly single-process. type ReconcileClaim struct { Owner string Token string Now time.Time LeaseDuration time.Duration Limit int } func OpenRepository(ctx context.Context, driver, dsn string) (Repository, error) { switch strings.ToLower(strings.TrimSpace(driver)) { case "", DriverSQLite: return OpenSQLite(ctx, dsn) case DriverPostgres: return OpenPostgres(ctx, dsn) default: return nil, fmt.Errorf("unsupported database driver %q", driver) } } type quotaProjectionError struct { kind error } func (e *quotaProjectionError) Error() string { return e.kind.Error() } func (e *quotaProjectionError) Unwrap() error { return e.kind } func projectionUnavailable() error { return "aProjectionError{kind: ErrQuotaProjectionUnavailable} } func projectionInvalid() error { return "aProjectionError{kind: ErrQuotaProjectionInvalid} } type areaPolicyError struct { kind error } func (e *areaPolicyError) Error() string { return e.kind.Error() } func (e *areaPolicyError) Unwrap() error { return e.kind } func areaPolicyUnavailable() error { return &areaPolicyError{kind: ErrAreaPolicyUnavailable} } func areaPolicyInvalid() error { return &areaPolicyError{kind: ErrAreaPolicyInvalid} } func areaPolicyDenied() error { return &areaPolicyError{kind: ErrAreaPolicyDenied} }