glimr/session/store
Session Store
The session middleware needs to load, save, and destroy session data without knowing which backend is in use. This module defines a function-based interface that backends implement, so swapping from file-based to cookie-based sessions requires changing one line at boot — not modifying middleware or handler code. The active store is cached in persistent_term so every request can access it without threading it through function arguments.
Types
Gleam doesn’t have traits or interfaces, so the store is modeled as a record of closures — each backend provides its own implementations at construction time. Making the type opaque prevents callers from reaching into the closures directly, ensuring all access goes through the public functions that handle the “no store configured” fallback.
pub opaque type SessionStore
Values
pub fn cache_store(store: SessionStore) -> Nil
The driver calls this once at boot so every subsequent request reads the store from persistent_term instead of passing it through function arguments. This keeps the session API ergonomic — callers never need a store reference.
pub fn cookie_value(
session_id: String,
data: dict.Dict(String, String),
flash: dict.Dict(String, String),
) -> String
Server-side stores put only the session ID in the cookie while cookie stores encode the full payload. Abstracting this behind a callback lets the middleware set the cookie without knowing which strategy is active — the store decides what goes over the wire.
pub fn destroy(session_id: String) -> Nil
Invalidation must remove the old session immediately so a stolen session ID can never be reused. The middleware calls this before saving the new session, ensuring the old and new entries never coexist in the store.
pub fn gc() -> Nil
Expired sessions accumulate in the store over time. Running GC probabilistically (2% of requests) spreads cleanup cost so no single request pays the full scan price, while still purging stale entries within a reasonable window.
pub fn load(
session_id: String,
) -> #(dict.Dict(String, String), dict.Dict(String, String))
Middleware calls this at request start to hydrate the session actor. Returning empty dicts when no store is configured lets the app boot and serve requests even if sessions aren’t set up — reads just return nothing rather than crashing.
pub fn new(
load load: fn(String) -> #(
dict.Dict(String, String),
dict.Dict(String, String),
),
save save: fn(
String,
dict.Dict(String, String),
dict.Dict(String, String),
) -> Nil,
destroy destroy: fn(String) -> Nil,
gc gc: fn() -> Nil,
cookie_value cookie_value: fn(
String,
dict.Dict(String, String),
dict.Dict(String, String),
) -> String,
) -> SessionStore
Labeled arguments make construction self-documenting at the call site — each backend explicitly names every callback it provides. This is the only way to build a SessionStore, so the opaque type guarantee holds: every store has all five callbacks populated.
pub fn save(
session_id: String,
data: dict.Dict(String, String),
flash: dict.Dict(String, String),
) -> Nil
Middleware calls this after the handler returns to persist mutations. The flash dict here contains only values set during this request — the previous request’s flash was already consumed and cleared, enforcing one-shot semantics at the store level.