storage

package
v0.1.22 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 22, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("state not found")

Functions

This section is empty.

Types

type Action

type Action string
const (
	ActionUpdated Action = "updated"
	ActionDeleted Action = "deleted"
)

type Backend

type Backend interface {
	RateLimiter
	StateStore

	Close() error

	Ping(ctx context.Context) error
}

type EntityType

type EntityType string
const (
	EntityTypeRecovery EntityType = "recovery"
	EntityTypeWorkout  EntityType = "workout"
	EntityTypeSleep    EntityType = "sleep"
)

type GlobalRateLimitStats

type GlobalRateLimitStats struct {
	MinuteRemaining int
	DayRemaining    int
}

type HybridNotificationStore

type HybridNotificationStore struct {
	// contains filtered or unexported fields
}

func NewHybridNotificationStore

func NewHybridNotificationStore(pool *pgxpool.Pool, redis *redis.Client) *HybridNotificationStore

func (*HybridNotificationStore) Acknowledge

func (s *HybridNotificationStore) Acknowledge(ctx context.Context, userID int64, traceIDs []string) error

func (*HybridNotificationStore) Add

func (*HybridNotificationStore) GetUnacked

func (s *HybridNotificationStore) GetUnacked(ctx context.Context, userID int64, cursor int64, limit int32) ([]Notification, error)

func (*HybridNotificationStore) Subscribe

func (s *HybridNotificationStore) Subscribe(ctx context.Context, userID int64) (<-chan Notification, func(), error)

type Notification

type Notification struct {
	ID         int64      `json:"id"`       // monotonic, for cursor
	TraceID    string     `json:"trace_id"` // WHOOP's trace_id, for acking
	EntityType EntityType `json:"entity_type"`
	EntityID   string     `json:"entity_id"`
	Action     Action     `json:"action"`
	Timestamp  time.Time  `json:"timestamp"`
}

type NotificationStore

type NotificationStore interface {
	// Add persists a notification and publishes to real-time subscribers.
	// Idempotent: duplicate trace_ids are ignored.
	Add(ctx context.Context, userID int64, n Notification) error

	// GetUnacked returns unacknowledged notifications using cursor-based pagination.
	// Pass 0 for the first page. Returns notifications with id > cursor.
	GetUnacked(ctx context.Context, userID int64, cursor int64, limit int32) ([]Notification, error)

	// Acknowledge marks the specified notifications as acknowledged by trace_id.
	Acknowledge(ctx context.Context, userID int64, traceIDs []string) error

	// Subscribe returns a channel that receives notifications for a user.
	// The returned function should be called to unsubscribe.
	Subscribe(ctx context.Context, userID int64) (<-chan Notification, func(), error)
}

type RateLimitResult

type RateLimitResult struct {
	Allowed    bool
	RetryAfter time.Duration
}

type RateLimiter

type RateLimiter interface {
	Allow(ctx context.Context, key string) (RateLimitResult, error)
}

type RedisBackend

type RedisBackend struct {
	// contains filtered or unexported fields
}

func NewRedisBackend

func NewRedisBackend(cfg RedisConfig, rateLimit int) (*RedisBackend, error)

func (*RedisBackend) Allow

func (r *RedisBackend) Allow(ctx context.Context, key string) (RateLimitResult, error)

func (*RedisBackend) Close

func (r *RedisBackend) Close() error

func (*RedisBackend) GetAndDelete

func (r *RedisBackend) GetAndDelete(ctx context.Context, state string) (StateEntry, error)

func (*RedisBackend) Ping

func (r *RedisBackend) Ping(ctx context.Context) error

func (*RedisBackend) Set

func (r *RedisBackend) Set(ctx context.Context, state string, entry StateEntry, ttl time.Duration) error

type RedisConfig

type RedisConfig struct {
	Client *redis.Client
}

type RedisTokenCache

type RedisTokenCache struct {
	// contains filtered or unexported fields
}

func NewRedisTokenCache

func NewRedisTokenCache(cfg RedisConfig) *RedisTokenCache

func (*RedisTokenCache) GetUserID

func (c *RedisTokenCache) GetUserID(ctx context.Context, tokenHash string) (int64, error)

func (*RedisTokenCache) SetUserID

func (c *RedisTokenCache) SetUserID(ctx context.Context, tokenHash string, userID int64, ttl time.Duration) error

type StateEntry

type StateEntry struct {
	LocalPort     string    `json:"local_port"`
	ClientVersion string    `json:"client_version"`
	CreatedAt     time.Time `json:"created_at"`
}

type StateStore

type StateStore interface {
	Set(ctx context.Context, state string, entry StateEntry, ttl time.Duration) error

	// GetAndDelete atomically retrieves and removes a state entry.
	// Returns ErrNotFound if the state does not exist or has expired.
	GetAndDelete(ctx context.Context, state string) (StateEntry, error)
}

type TokenCache

type TokenCache interface {
	// GetUserID returns the cached user ID for a token hash.
	// Returns ErrNotFound if not cached or expired.
	GetUserID(ctx context.Context, tokenHash string) (int64, error)

	// SetUserID caches a user ID for a token hash with TTL.
	SetUserID(ctx context.Context, tokenHash string, userID int64, ttl time.Duration) error
}

TokenCache caches validated bearer tokens to avoid repeated API calls. Maps token hash -> WHOOP user ID.

type UserRateLimitStats

type UserRateLimitStats struct {
	MinuteCount int
	DayCount    int
}

type WhoopRateLimitReason

type WhoopRateLimitReason string
const (
	WhoopRateLimitReasonPerUserMinute WhoopRateLimitReason = "per-user-minute"
	WhoopRateLimitReasonPerUserDay    WhoopRateLimitReason = "per-user-day"
	WhoopRateLimitReasonGlobalMinute  WhoopRateLimitReason = "global-minute"
	WhoopRateLimitReasonGlobalDay     WhoopRateLimitReason = "global-day"
)

type WhoopRateLimitState

type WhoopRateLimitState struct {
	Allowed            bool
	MinuteRemaining    int
	MinuteReset        time.Time
	DayRemaining       int
	DayReset           time.Time
	Reason             *WhoopRateLimitReason
	DynamicMinuteLimit int // calculated limit for this user based on active users
	ActiveUserCount    int // current number of active users
}

type WhoopRateLimiter

type WhoopRateLimiter interface {
	// CheckAndIncrement checks both per-user and global limits, incrementing counters if allowed.
	// userKey: hash of access token or user ID extracted from token
	// Returns combined state - allowed only if BOTH per-user AND global limits pass
	CheckAndIncrement(ctx context.Context, userKey string) (*WhoopRateLimitState, error)

	// UpdateFromHeaders updates global rate limit state from WHOOP API response headers.
	// Syncs global counters with WHOOP's actual values.
	UpdateFromHeaders(ctx context.Context, headers http.Header) error

	GetUserStats(ctx context.Context, userKey string) (*UserRateLimitStats, error)

	GetGlobalStats(ctx context.Context) (*GlobalRateLimitStats, error)
}

type WhoopRateLimiterConfig

type WhoopRateLimiterConfig struct {
	PerUserDayLimit       int // default: 1000
	GlobalMinuteLimit     int // default: 95
	GlobalDayLimit        int // default: 9950
	ReserveBuffer         int // default: 5 (reserve for new users)
	MinPerUserMinuteLimit int // default: 5 (floor)
	ActiveWindowSeconds   int // default: 60
}

type WhoopRedisLimiter

type WhoopRedisLimiter struct {
	// contains filtered or unexported fields
}

func NewWhoopRedisLimiter

func NewWhoopRedisLimiter(cfg RedisConfig, config WhoopRateLimiterConfig) *WhoopRedisLimiter

func (*WhoopRedisLimiter) CheckAndIncrement

func (w *WhoopRedisLimiter) CheckAndIncrement(ctx context.Context, userKey string) (*WhoopRateLimitState, error)

func (*WhoopRedisLimiter) GetGlobalStats

func (w *WhoopRedisLimiter) GetGlobalStats(ctx context.Context) (*GlobalRateLimitStats, error)

func (*WhoopRedisLimiter) GetUserStats

func (w *WhoopRedisLimiter) GetUserStats(ctx context.Context, userKey string) (*UserRateLimitStats, error)

func (*WhoopRedisLimiter) UpdateFromHeaders

func (w *WhoopRedisLimiter) UpdateFromHeaders(ctx context.Context, headers http.Header) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL