Documentation
¶
Overview ¶
Package session provides session persistence and management for Vango.
This package implements pluggable session storage backends, serialization, and memory protection mechanisms for server-driven web applications.
Session Storage ¶
The SessionStore interface defines the contract for session persistence:
store := session.NewRedisStore(redisClient) // or store := session.NewSQLStore(db) // or (default) store := session.NewMemoryStore()
Session Serialization ¶
Sessions can be serialized to bytes for persistence:
data, err := sess.Serialize() // Later... err := sess.Deserialize(data)
Memory Protection ¶
The Manager provides LRU eviction and per-IP limits:
manager := session.NewManager(session.ManagerConfig{
MaxDetached: 10000,
MaxPerIP: 100,
Store: store,
EvictionPolicy: session.EvictionLRU,
})
Persisted State Primitives ¶
Persisted state is allocated during Setup using scope-singleton signals or typed SessionKeys:
cursor := setup.Signal(&s, Point{0, 0}) // Local (not persisted)
cart := setup.SharedSignal(&s, []CartItemRef{}) // Session persisted
theme := vango.NewSessionKey[Theme]("theme", vango.Default(DefaultTheme))
Index ¶
- Constants
- Variables
- func EncodeBlob(payload []byte, schemaHash string, schemaVersion uint16, secret []byte) ([]byte, error)
- func Serialize(ss *SerializableSession) ([]byte, error)
- func SerializeV2(ss *SerializableSessionV2) ([]byte, error)
- type AtomicKVStore
- type AuthHintsV1
- type BlobHeader
- type ErrStoreClosed
- type EvictionPolicy
- type ManagedSession
- type Manager
- func (m *Manager) CheckIPLimit(ip string) error
- func (m *Manager) Get(sessionID string) *ManagedSession
- func (m *Manager) OnDisconnect(sessionID string, serializedData []byte)
- func (m *Manager) OnReconnect(sessionID string) (*ManagedSession, []byte, error)
- func (m *Manager) Register(sess *ManagedSession) error
- func (m *Manager) Remove(sessionID string)
- func (m *Manager) Shutdown(ctx context.Context) error
- func (m *Manager) Stats() ManagerStats
- func (m *Manager) Touch(sessionID string)
- type ManagerConfig
- type ManagerStats
- type MemoryStore
- func (m *MemoryStore) Close() error
- func (m *MemoryStore) Count() int
- func (m *MemoryStore) Delete(ctx context.Context, sessionID string) error
- func (m *MemoryStore) Load(ctx context.Context, sessionID string) ([]byte, error)
- func (m *MemoryStore) Save(ctx context.Context, sessionID string, data []byte, expiresAt time.Time) error
- func (m *MemoryStore) SaveAll(ctx context.Context, sessions map[string]SessionData) error
- func (m *MemoryStore) SaveAllAtomic(ctx context.Context, sessions map[string]SessionData) error
- func (m *MemoryStore) Touch(ctx context.Context, sessionID string, expiresAt time.Time) error
- type MemoryStoreOption
- type RedisBoolCmd
- type RedisClient
- type RedisIntCmd
- type RedisPipeliner
- type RedisStatusCmd
- type RedisStore
- func (r *RedisStore) Close() error
- func (r *RedisStore) Delete(ctx context.Context, sessionID string) error
- func (r *RedisStore) Load(ctx context.Context, sessionID string) ([]byte, error)
- func (r *RedisStore) Prefix() string
- func (r *RedisStore) Save(ctx context.Context, sessionID string, data []byte, expiresAt time.Time) error
- func (r *RedisStore) SaveAll(ctx context.Context, sessions map[string]SessionData) error
- func (r *RedisStore) SaveAllAtomic(ctx context.Context, sessions map[string]SessionData) error
- func (r *RedisStore) Touch(ctx context.Context, sessionID string, expiresAt time.Time) error
- type RedisStoreOption
- type RedisStringCmd
- type SQLDialect
- type SQLStore
- func (s *SQLStore) Close() error
- func (s *SQLStore) CreateTable(ctx context.Context) error
- func (s *SQLStore) Delete(ctx context.Context, sessionID string) error
- func (s *SQLStore) Load(ctx context.Context, sessionID string) ([]byte, error)
- func (s *SQLStore) Save(ctx context.Context, sessionID string, data []byte, expiresAt time.Time) error
- func (s *SQLStore) SaveAll(ctx context.Context, sessions map[string]SessionData) error
- func (s *SQLStore) SaveAllAtomic(ctx context.Context, sessions map[string]SessionData) error
- func (s *SQLStore) Touch(ctx context.Context, sessionID string, expiresAt time.Time) error
- type SQLStoreOption
- type SerializableSession
- type SerializableSessionV2
- type SessionData
- type SessionNotFoundError
- type SessionStore
- type SignalConfig
- type StoreOption
Constants ¶
const CurrentSerializationVersion = 2
CurrentSerializationVersion is the current version of the serialization format. Increment when making breaking changes to the format.
Variables ¶
var ( // ErrTooManySessionsFromIP is returned when the per-IP session limit is exceeded. ErrTooManySessionsFromIP = errors.New("too many sessions from this IP address") // ErrMaxSessionsReached is returned when the maximum session limit is reached. ErrMaxSessionsReached = errors.New("maximum session limit reached") // ErrSessionExpired is returned when trying to resume an expired session. ErrSessionExpired = errors.New("session has expired") // ErrSessionNotFound is returned when a session doesn't exist. ErrSessionNotFound = errors.New("session not found") // ErrManagerStopped is returned when operations are attempted on a stopped manager. ErrManagerStopped = errors.New("session manager is stopped") )
Error types for session management.
var ErrInvalidBlob = errors.New("vango: invalid session blob")
ErrInvalidBlob indicates an invalid blob encoding.
var ErrInvalidSignature = errors.New("vango: invalid session blob signature")
ErrInvalidSignature indicates an HMAC verification failure.
var ErrRedisNil = errors.New("redis: nil")
ErrRedisNil is returned when a key doesn't exist in Redis. This should match redis.Nil from go-redis.
Functions ¶
func EncodeBlob ¶
func EncodeBlob(payload []byte, schemaHash string, schemaVersion uint16, secret []byte) ([]byte, error)
EncodeBlob wraps the payload in a signed blob.
func Serialize ¶
func Serialize(ss *SerializableSession) ([]byte, error)
Serialize converts a SerializableSession to bytes (legacy JSON v1).
func SerializeV2 ¶
func SerializeV2(ss *SerializableSessionV2) ([]byte, error)
SerializeV2 encodes a session using the new format.
Types ¶
type AtomicKVStore ¶
type AtomicKVStore interface {
SaveAllAtomic(ctx context.Context, sessions map[string]SessionData) error
}
AtomicKVStore is an optional capability interface for session stores that can persist a batch of keys atomically.
This is used by Vango's global signal persistence layer to provide strict Tx-level all-or-nothing durability for global persisted writes, without requiring SessionStore.SaveAll to be cluster-compatible or globally atomic.
Implementations MUST commit the provided batch atomically. Callers are responsible for ensuring any backend-specific constraints (e.g. Redis Cluster hash slot co-location) are satisfied by the keys provided.
type AuthHintsV1 ¶
type AuthHintsV1 struct {
HadAuth bool `cbor:"1,keyasint,omitempty" json:"had_auth,omitempty"`
Presence bool `cbor:"2,keyasint,omitempty" json:"presence,omitempty"`
}
AuthHintsV1 stores non-authoritative auth resume hints. These hints are bounded booleans and never contain principal data.
type BlobHeader ¶
type BlobHeader struct {
FormatVersion uint8
SchemaVersion uint16
SchemaHash string
TimestampMs int64
}
BlobHeader describes metadata stored alongside a session payload.
func DecodeBlob ¶
func DecodeBlob(data []byte, secret, previousSecret []byte) ([]byte, BlobHeader, error)
DecodeBlob verifies and unwraps a signed blob.
type ErrStoreClosed ¶
type ErrStoreClosed struct{}
ErrStoreClosed is returned when operations are attempted on a closed store.
func (ErrStoreClosed) Error ¶
func (e ErrStoreClosed) Error() string
type EvictionPolicy ¶
type EvictionPolicy int
EvictionPolicy determines which sessions are evicted first.
const ( // EvictionLRU evicts the least recently accessed sessions first. EvictionLRU EvictionPolicy = iota // EvictionOldest evicts the oldest sessions first (by creation time). EvictionOldest // EvictionRandom evicts sessions randomly (faster but less fair). EvictionRandom )
type ManagedSession ¶
type ManagedSession struct {
// ID is the unique session identifier.
ID string
// IP is the client IP address for per-IP limiting.
IP string
// CreatedAt is when the session was created.
CreatedAt time.Time
// LastActive is when the session was last accessed.
LastActive time.Time
// DisconnectedAt is when the client disconnected (zero if connected).
DisconnectedAt time.Time
// Data is the serialized session state (set when disconnected).
Data []byte
// Connected indicates whether the client has an active WebSocket.
Connected bool
// UserID is the authenticated user ID, if any.
UserID string
}
ManagedSession wraps session data with management metadata.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager manages session lifecycle, persistence, and memory protection. It provides LRU eviction for detached sessions and per-IP session limits.
func NewManager ¶
func NewManager(store SessionStore, config ManagerConfig, logger *slog.Logger) *Manager
NewManager creates a new session manager.
func (*Manager) CheckIPLimit ¶
CheckIPLimit verifies that the IP hasn't exceeded its session limit. This should be called before creating a new session.
func (*Manager) Get ¶
func (m *Manager) Get(sessionID string) *ManagedSession
Get retrieves a session by ID.
func (*Manager) OnDisconnect ¶
OnDisconnect handles a client disconnect. The session becomes detached and can be resumed within ResumeWindow.
func (*Manager) OnReconnect ¶
func (m *Manager) OnReconnect(sessionID string) (*ManagedSession, []byte, error)
OnReconnect attempts to restore a session after reconnect. Returns the restored session data if found and not expired.
func (*Manager) Register ¶
func (m *Manager) Register(sess *ManagedSession) error
Register adds a new session to the manager. The session is marked as connected.
func (*Manager) Remove ¶
Remove removes a session from the manager. Called on explicit logout or session termination.
type ManagerConfig ¶
type ManagerConfig struct {
// MaxDetachedSessions is the maximum number of detached sessions before LRU eviction.
// Default: 10000.
MaxDetachedSessions int
// MaxSessionsPerIP is the maximum number of active sessions per IP address.
// Default: 100.
MaxSessionsPerIP int
// ResumeWindow is how long a detached session remains resumable.
// Default: 5 minutes.
ResumeWindow time.Duration
// PersistInterval is how often to persist dirty sessions.
// Default: 30 seconds.
PersistInterval time.Duration
// CleanupInterval is how often to clean up expired sessions.
// Default: 1 minute.
CleanupInterval time.Duration
// EvictionPolicy determines how sessions are evicted when limits are exceeded.
// Default: EvictionLRU.
EvictionPolicy EvictionPolicy
}
ManagerConfig configures the session manager.
func DefaultManagerConfig ¶
func DefaultManagerConfig() ManagerConfig
DefaultManagerConfig returns a ManagerConfig with sensible defaults.
type ManagerStats ¶
type ManagerStats struct {
// Total is the total number of sessions (connected + detached).
Total int
// Connected is the number of sessions with active WebSocket connections.
Connected int
// Detached is the number of sessions waiting for reconnection.
Detached int
// UniqueIPs is the number of unique client IP addresses.
UniqueIPs int
}
ManagerStats contains session manager statistics.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-memory session store implementation. It's the default store and suitable for single-server deployments. For multi-server deployments, use RedisStore or SQLStore.
func NewMemoryStore ¶
func NewMemoryStore(opts ...MemoryStoreOption) *MemoryStore
NewMemoryStore creates a new in-memory session store.
func (*MemoryStore) Close ¶
func (m *MemoryStore) Close() error
Close shuts down the store and releases resources.
func (*MemoryStore) Count ¶
func (m *MemoryStore) Count() int
Count returns the number of sessions in the store. This is for monitoring/testing purposes.
func (*MemoryStore) Delete ¶
func (m *MemoryStore) Delete(ctx context.Context, sessionID string) error
Delete removes a session from the store.
func (*MemoryStore) Save ¶
func (m *MemoryStore) Save(ctx context.Context, sessionID string, data []byte, expiresAt time.Time) error
Save stores session data with an expiration time.
func (*MemoryStore) SaveAll ¶
func (m *MemoryStore) SaveAll(ctx context.Context, sessions map[string]SessionData) error
SaveAll saves multiple sessions atomically.
func (*MemoryStore) SaveAllAtomic ¶
func (m *MemoryStore) SaveAllAtomic(ctx context.Context, sessions map[string]SessionData) error
SaveAllAtomic persists a batch of keys atomically. For MemoryStore this is atomic by construction (single critical section).
type MemoryStoreOption ¶
type MemoryStoreOption func(*memoryStoreConfig)
MemoryStoreOption configures MemoryStore behavior.
func WithCleanupInterval ¶
func WithCleanupInterval(d time.Duration) MemoryStoreOption
WithCleanupInterval sets how often expired sessions are cleaned up. Default: 1 minute.
type RedisBoolCmd ¶
type RedisBoolCmd interface {
Err() error
}
RedisBoolCmd represents a Redis bool command result.
type RedisClient ¶
type RedisClient interface {
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) RedisStatusCmd
Get(ctx context.Context, key string) RedisStringCmd
Del(ctx context.Context, keys ...string) RedisIntCmd
Expire(ctx context.Context, key string, expiration time.Duration) RedisBoolCmd
Pipeline() RedisPipeliner
TxPipeline() RedisPipeliner
Close() error
}
RedisClient defines the interface for Redis operations. This interface is compatible with github.com/redis/go-redis/v9.
type RedisIntCmd ¶
type RedisIntCmd interface {
Err() error
}
RedisIntCmd represents a Redis int command result.
type RedisPipeliner ¶
type RedisPipeliner interface {
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) RedisStatusCmd
Exec(ctx context.Context) ([]interface{}, error)
}
RedisPipeliner represents a Redis pipeline.
type RedisStatusCmd ¶
type RedisStatusCmd interface {
Err() error
}
RedisStatusCmd represents a Redis status command result.
type RedisStore ¶
type RedisStore struct {
// contains filtered or unexported fields
}
RedisStore is a Redis-backed session store. It's suitable for multi-server deployments with shared session state.
func NewRedisStore ¶
func NewRedisStore(client RedisClient, opts ...RedisStoreOption) *RedisStore
NewRedisStore creates a new Redis-backed session store.
func (*RedisStore) Close ¶
func (r *RedisStore) Close() error
Close marks the store as closed. Note: This does not close the underlying Redis client, as it may be shared with other components.
func (*RedisStore) Delete ¶
func (r *RedisStore) Delete(ctx context.Context, sessionID string) error
Delete removes a session from Redis.
func (*RedisStore) Prefix ¶
func (r *RedisStore) Prefix() string
Prefix returns the current key prefix. This is for testing/debugging purposes.
func (*RedisStore) Save ¶
func (r *RedisStore) Save(ctx context.Context, sessionID string, data []byte, expiresAt time.Time) error
Save stores session data with an expiration time.
func (*RedisStore) SaveAll ¶
func (r *RedisStore) SaveAll(ctx context.Context, sessions map[string]SessionData) error
SaveAll saves multiple sessions using a Redis pipeline.
func (*RedisStore) SaveAllAtomic ¶
func (r *RedisStore) SaveAllAtomic(ctx context.Context, sessions map[string]SessionData) error
SaveAllAtomic persists a batch of keys atomically.
This uses Redis MULTI/EXEC via TxPipeline. Callers MUST ensure that all keys in the batch are co-located in a single Redis Cluster hash slot (e.g. by using a constant hash tag in the key name). If the keys are not co-located, Redis will return CROSSSLOT and this method will return that error.
type RedisStoreOption ¶
type RedisStoreOption func(*redisStoreConfig)
RedisStoreOption configures RedisStore behavior.
func WithRedisPrefix ¶
func WithRedisPrefix(prefix string) RedisStoreOption
WithRedisPrefix sets the key prefix for session keys. Default: "vango:session:".
type RedisStringCmd ¶
RedisStringCmd represents a Redis string command result.
type SQLDialect ¶
type SQLDialect int
SQLDialect represents the SQL dialect for query generation.
const ( // DialectPostgreSQL uses PostgreSQL syntax ($1, $2 placeholders). DialectPostgreSQL SQLDialect = iota // DialectMySQL uses MySQL syntax (? placeholders). DialectMySQL // DialectSQLite uses SQLite syntax (? placeholders). DialectSQLite )
type SQLStore ¶
type SQLStore struct {
// contains filtered or unexported fields
}
SQLStore is a SQL-backed session store. It works with any database/sql compatible driver (PostgreSQL, MySQL, SQLite). Requires a table with schema:
CREATE TABLE vango_sessions (
id VARCHAR(64) PRIMARY KEY,
data BYTEA NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_vango_sessions_expires ON vango_sessions(expires_at);
func NewSQLStore ¶
func NewSQLStore(db *sql.DB, opts ...SQLStoreOption) *SQLStore
NewSQLStore creates a new SQL-backed session store.
func (*SQLStore) Close ¶
Close shuts down the store and releases resources. Note: This does not close the underlying database connection, as it may be shared with other components.
func (*SQLStore) CreateTable ¶
CreateTable creates the session table if it doesn't exist. This is a convenience method for development/testing.
func (*SQLStore) Save ¶
func (s *SQLStore) Save(ctx context.Context, sessionID string, data []byte, expiresAt time.Time) error
Save stores session data with an expiration time.
func (*SQLStore) SaveAllAtomic ¶
SaveAllAtomic persists a batch of keys atomically. For SQLStore this is atomic by construction (single DB transaction).
type SQLStoreOption ¶
type SQLStoreOption func(*sqlStoreConfig)
SQLStoreOption configures SQLStore behavior.
func WithSQLCleanupInterval ¶
func WithSQLCleanupInterval(d time.Duration) SQLStoreOption
WithSQLCleanupInterval sets how often expired sessions are cleaned up. Default: 5 minutes.
func WithSQLDialect ¶
func WithSQLDialect(dialect SQLDialect) SQLStoreOption
WithSQLDialect sets the SQL dialect for query generation. Default: DialectPostgreSQL.
func WithSQLTableName ¶
func WithSQLTableName(name string) SQLStoreOption
WithSQLTableName sets the table name for session storage. Default: "vango_sessions".
type SerializableSession ¶
type SerializableSession struct {
// ID is the unique session identifier.
ID string `json:"id"`
// UserID is the authenticated user ID, if any.
UserID string `json:"user_id,omitempty"`
// CreatedAt is when the session was created.
CreatedAt time.Time `json:"created_at"`
// LastActive is when the session was last active.
LastActive time.Time `json:"last_active"`
// Values contains Session.Get/Set values.
Values map[string]json.RawMessage `json:"values,omitempty"`
// Signals contains persisted signal values by key.
// Transient signals are excluded.
Signals map[string]json.RawMessage `json:"signals,omitempty"`
// Route is the current page route.
Route string `json:"route,omitempty"`
// RouteParams contains the current route parameters.
RouteParams map[string]string `json:"route_params,omitempty"`
// Version is the serialization format version.
Version int `json:"version"`
}
SerializableSession is the JSON-serializable representation of a session. This structure is used for persistence across server restarts.
func Deserialize ¶
func Deserialize(data []byte) (*SerializableSession, error)
Deserialize converts bytes back to a SerializableSession (legacy JSON v1).
type SerializableSessionV2 ¶
type SerializableSessionV2 struct {
// Identity
ID string `cbor:"1,keyasint" json:"id"`
UserID string `cbor:"2,keyasint,omitempty" json:"user_id,omitempty"`
CreatedAt time.Time `cbor:"3,keyasint" json:"created_at"`
LastActive time.Time `cbor:"4,keyasint" json:"last_active"`
// Navigation
Route string `cbor:"5,keyasint,omitempty" json:"route,omitempty"`
RouteParams map[string]string `cbor:"6,keyasint,omitempty" json:"route_params,omitempty"`
// Persisted signals and typed session keys
Signals map[string][]byte `cbor:"7,keyasint,omitempty" json:"signals,omitempty"`
SessionKeys map[string][]byte `cbor:"8,keyasint,omitempty" json:"session_keys,omitempty"`
// Legacy read-compatibility map.
// Deprecated: new writes should use AuthHints instead of LegacyValues.
LegacyValues map[string]json.RawMessage `cbor:"9,keyasint,omitempty" json:"legacy_values,omitempty"`
// Version (format migration)
Version int `cbor:"10,keyasint" json:"version"`
// Schema describes the persisted schema at the time of serialization.
Schema *state.Schema `cbor:"11,keyasint,omitempty" json:"schema,omitempty"`
// Explicit auth resume hints.
AuthHints *AuthHintsV1 `cbor:"12,keyasint,omitempty" json:"auth_hints,omitempty"`
}
SerializableSessionV2 is the CBOR-based serialization format.
func DeserializeV2 ¶
func DeserializeV2(data []byte) (*SerializableSessionV2, error)
DeserializeV2 decodes a session from the new format.
func MigrateV1ToV2 ¶
func MigrateV1ToV2(v1 *SerializableSession) *SerializableSessionV2
MigrateV1ToV2 converts old format to new.
type SessionData ¶
type SessionData struct {
// Data is the serialized session state.
Data []byte
// ExpiresAt is when the session should expire.
ExpiresAt time.Time
}
SessionData contains serialized session state with metadata.
type SessionNotFoundError ¶
type SessionNotFoundError struct {
SessionID string
}
SessionNotFoundError is returned when a session doesn't exist. Note: Load returns (nil, nil) for missing sessions, not this error. This is used by implementations that need an explicit error type.
func (SessionNotFoundError) Error ¶
func (e SessionNotFoundError) Error() string
type SessionStore ¶
type SessionStore interface {
// Save persists session state. Called periodically and on graceful shutdown.
// The expiresAt parameter indicates when the session should expire.
// If sessionID already exists, it should be overwritten.
Save(ctx context.Context, sessionID string, data []byte, expiresAt time.Time) error
// Load retrieves session state by ID.
// Returns (nil, nil) if the session doesn't exist or has expired.
// Returns (data, nil) if found and not expired.
// Returns (nil, err) on backend errors.
Load(ctx context.Context, sessionID string) ([]byte, error)
// Delete removes a session. Called on explicit logout or expiration.
// Should not return an error if the session doesn't exist.
Delete(ctx context.Context, sessionID string) error
// Touch updates the expiration time without loading full state.
// This is more efficient than Load+Save for keep-alive operations.
// Should not return an error if the session doesn't exist.
Touch(ctx context.Context, sessionID string, expiresAt time.Time) error
// SaveAll persists multiple sessions atomically (if possible).
// Used during graceful shutdown to save all active sessions.
// Implementations that don't support atomicity should save sequentially.
SaveAll(ctx context.Context, sessions map[string]SessionData) error
// Close releases any resources held by the store.
// Called when the server shuts down.
Close() error
}
SessionStore defines the interface for session persistence backends. Implementations must be safe for concurrent use.
func ParseStoreURL ¶
func ParseStoreURL(raw string) (SessionStore, error)
ParseStoreURL creates a SessionStore from a URL.
type SignalConfig ¶
type SignalConfig struct {
// Transient signals are not persisted to the store.
Transient bool
// PersistKey is the explicit key for serialization.
// If empty, an auto-generated key is used based on component/position.
PersistKey string
}
SignalConfig holds configuration for signal persistence. This is used by the vango.Signal to track persistence options.
func NewSignalConfig ¶
func NewSignalConfig() *SignalConfig
NewSignalConfig creates a default SignalConfig.
type StoreOption ¶
type StoreOption func(interface{})
StoreOption is a functional option for configuring stores.