session

package
v0.2.1 Latest Latest
Warning

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

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

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

View Source
const CurrentSerializationVersion = 2

CurrentSerializationVersion is the current version of the serialization format. Increment when making breaking changes to the format.

Variables

View Source
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.

View Source
var ErrInvalidBlob = errors.New("vango: invalid session blob")

ErrInvalidBlob indicates an invalid blob encoding.

View Source
var ErrInvalidSignature = errors.New("vango: invalid session blob signature")

ErrInvalidSignature indicates an HMAC verification failure.

View Source
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

func (m *Manager) CheckIPLimit(ip string) error

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

func (m *Manager) OnDisconnect(sessionID string, serializedData []byte)

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

func (m *Manager) Remove(sessionID string)

Remove removes a session from the manager. Called on explicit logout or session termination.

func (*Manager) Shutdown

func (m *Manager) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down the manager, persisting all sessions.

func (*Manager) Stats

func (m *Manager) Stats() ManagerStats

Stats returns manager statistics.

func (*Manager) Touch

func (m *Manager) Touch(sessionID string)

Touch updates the last active time for a session.

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) Load

func (m *MemoryStore) Load(ctx context.Context, sessionID string) ([]byte, error)

Load retrieves session data if it exists and hasn't expired.

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).

func (*MemoryStore) Touch

func (m *MemoryStore) Touch(ctx context.Context, sessionID string, expiresAt time.Time) error

Touch updates the expiration time for a session.

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) Load

func (r *RedisStore) Load(ctx context.Context, sessionID string) ([]byte, error)

Load retrieves session data if it exists.

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.

func (*RedisStore) Touch

func (r *RedisStore) Touch(ctx context.Context, sessionID string, expiresAt time.Time) error

Touch updates the expiration time for a session.

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

type RedisStringCmd interface {
	Bytes() ([]byte, error)
	Err() error
}

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

func (s *SQLStore) Close() error

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

func (s *SQLStore) CreateTable(ctx context.Context) error

CreateTable creates the session table if it doesn't exist. This is a convenience method for development/testing.

func (*SQLStore) Delete

func (s *SQLStore) Delete(ctx context.Context, sessionID string) error

Delete removes a session from the database.

func (*SQLStore) Load

func (s *SQLStore) Load(ctx context.Context, sessionID string) ([]byte, error)

Load retrieves session data if it exists and hasn't expired.

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) SaveAll

func (s *SQLStore) SaveAll(ctx context.Context, sessions map[string]SessionData) error

SaveAll saves multiple sessions using a transaction.

func (*SQLStore) SaveAllAtomic

func (s *SQLStore) SaveAllAtomic(ctx context.Context, sessions map[string]SessionData) error

SaveAllAtomic persists a batch of keys atomically. For SQLStore this is atomic by construction (single DB transaction).

func (*SQLStore) Touch

func (s *SQLStore) Touch(ctx context.Context, sessionID string, expiresAt time.Time) error

Touch updates the expiration time for a session.

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.

Jump to

Keyboard shortcuts

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