Documentation
¶
Overview ¶
Package store defines the unified byte-level key-value interface shared by celeris middleware (session, csrf, cache, idempotency, SSE replay).
Core interface ¶
KV is the minimal required surface: Get, Set with TTL, Delete. All implementations must be safe for concurrent use and honor the contract that Get returns (nil, ErrNotFound) on a missing key; returning (nil, nil) is forbidden.
Optional extension interfaces surface backend capabilities; middleware feature-detects them via type assertion and falls back to emulation or no-op semantics when a backend does not implement them:
- GetAndDeleter — atomic GET+DEL (e.g. Redis GETDEL); used by csrf.
- Scanner — key enumeration by prefix; used by session Reset.
- PrefixDeleter — bulk delete by prefix; used by cache InvalidatePrefix.
- SetNXer — atomic set-if-not-exists; used by idempotency lock acquisition.
- Counter — atomic monotonic counter (INCR); used by SSE replay for cross-process IDs.
- Scripter — server-side atomic scripts (EVALSHA); used by ratelimit Redis adapter.
Reference implementations ¶
- MemoryKV / NewMemoryKV — in-memory sharded store. Implements KV, GetAndDeleter, Scanner, PrefixDeleter, SetNXer, and Counter. Default store for single-process deployments and tests.
- middleware/session/redisstore — Redis-backed; KV + Scanner + GetAndDeleter.
- middleware/session/postgresstore — PostgreSQL-backed; KV only.
- middleware/csrf/redisstore — Redis-backed CSRF; KV + GetAndDeleter.
- middleware/ratelimit/redisstore — Redis-backed ratelimit via EVALSHA; implements Scripter.
Utilities ¶
Prefixed wraps any KV so all keys are transparently namespaced with a prefix — useful when sharing one backend among multiple middleware. EncodeJSON / DecodeJSON are convenience helpers for adapters that persist structured payloads through the byte-level KV surface. EncodedResponse + ResponseWireVersion define the versioned wire format used by cache and idempotency middleware to persist captured HTTP responses.
Documentation ¶
Full guides and examples: https://goceleris.dev/docs/data-stores
Package store defines the unified key-value interface that middleware stores (session, csrf, cache, idempotency, jwks cache) build on.
The core interface is KV — a small, byte-level, context-aware Get/Set/Delete surface. Backends implement KV plus any optional extensions they can support atomically (GETDEL, SCAN, SETNX, EVALSHA). Middleware adapters feature-detect extensions via type assertion.
All implementations must follow the KV.Get contract: return nil, ErrNotFound on a missing key. Returning nil, nil is forbidden.
Index ¶
- Constants
- Variables
- func DecodeJSON(data []byte, v any) error
- func EncodeJSON(v any) ([]byte, error)
- type Counter
- type EncodedResponse
- type GetAndDeleter
- type KV
- type MemoryKV
- func (m *MemoryKV) Close()
- func (m *MemoryKV) Delete(_ context.Context, key string) error
- func (m *MemoryKV) DeletePrefix(_ context.Context, prefix string) error
- func (m *MemoryKV) Get(_ context.Context, key string) ([]byte, error)
- func (m *MemoryKV) GetAndDelete(_ context.Context, key string) ([]byte, error)
- func (m *MemoryKV) Increment(_ context.Context, key string, ttl time.Duration) (int64, error)
- func (m *MemoryKV) Scan(_ context.Context, prefix string) ([]string, error)
- func (m *MemoryKV) Set(_ context.Context, key string, value []byte, ttl time.Duration) error
- func (m *MemoryKV) SetNX(_ context.Context, key string, value []byte, ttl time.Duration) (bool, error)
- type MemoryKVConfig
- type PrefixDeleter
- type Scanner
- type Scripter
- type SetNXer
Examples ¶
Constants ¶
const ResponseWireVersion byte = 1
ResponseWireVersion is the current wire format version for encoded HTTP responses stored in KV backends by cache and idempotency middleware.
Variables ¶
var ErrInvalidWireFormat = errors.New("store: invalid response wire format")
ErrInvalidWireFormat is returned by DecodeResponse when the buffer is truncated or carries an unknown version byte.
var ErrNotFound = errors.New("store: key not found")
ErrNotFound is returned by KV.Get and extension methods when the key does not exist or has expired. Adapters map their native "missing key" signal (redis.ErrNil, sql.ErrNoRows, 0 rows) onto this sentinel.
Functions ¶
func DecodeJSON ¶
DecodeJSON unmarshals data into v. Convenience wrapper around json.Unmarshal for symmetry with EncodeJSON.
Types ¶
type Counter ¶ added in v1.4.2
type Counter interface {
Increment(ctx context.Context, key string, ttl time.Duration) (int64, error)
}
Counter is implemented by backends that expose an atomic monotonically-increasing counter (e.g. Redis INCR, Postgres `UPDATE ... RETURNING id`). Used by middleware/sse.NewKVReplayStore to share a single ID space across processes that hit the same KV backend — without a shared counter, multi-instance replay cannot guarantee unique IDs across reconnects. When a KV does not implement Counter the SSE replay store falls back silently to a per-process counter; callers that need cross-instance monotonicity should type-assert their KV against this interface at startup.
Increment returns the value AFTER the increment, so a fresh counter hands out 1, 2, 3, ... ttl bounds the counter's lifetime in the backend; ttl == 0 means "no expiry", same convention as KV.Set. Backends that don't support TTL on counters may ignore it.
type EncodedResponse ¶
EncodedResponse is a byte-efficient snapshot of an HTTP response used by cache and idempotency middleware to persist captured responses.
Wire format (version 1):
[1 byte version=1] [2 bytes status (big-endian)] [2 bytes header_count (big-endian)] for each header: [2 bytes key_len (big-endian)] [key_len bytes key] [2 bytes val_len (big-endian)] [val_len bytes value] [remaining bytes: body]
func DecodeResponse ¶
func DecodeResponse(buf []byte) (EncodedResponse, error)
DecodeResponse parses buf into an EncodedResponse following the versioned wire format. Returns ErrInvalidWireFormat if the version is unknown or the buffer is truncated.
func (EncodedResponse) AppendTo ¶
func (r EncodedResponse) AppendTo(dst []byte) []byte
AppendTo serializes r into dst and returns the extended slice. Lets callers reuse a pooled buffer across requests (cache / idempotency MISS paths transfer the encoded bytes into a Store.Set that copies internally, so the buffer is free to recycle immediately after).
func (EncodedResponse) Encode ¶
func (r EncodedResponse) Encode() []byte
Encode serializes r into a new byte slice using the current wire format version.
type GetAndDeleter ¶
GetAndDeleter is implemented by backends that support atomic GET+DEL (e.g., Redis GETDEL). Used by csrf for TOCTOU-safe single-use token validation. When a KV does not implement GetAndDeleter, callers fall back to a non-atomic Get+Delete pair.
type KV ¶
type KV interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
Delete(ctx context.Context, key string) error
}
KV is the unified byte-level key-value interface for middleware stores.
Contract:
- Get returns (nil, ErrNotFound) on a missing key. A (nil, nil) return is forbidden; callers may rely on err != nil when the value is absent.
- Set with ttl == 0 stores a value with no expiry. Positive ttl is honored by the backend; sub-second precision is best-effort.
- Delete returning nil means "the key is not present" regardless of whether it was present before the call.
- All methods are safe for concurrent use from multiple goroutines.
- Values returned by Get are owned by the caller; backends must copy any internal buffer before returning.
func Prefixed ¶
Prefixed returns a KV that transparently prepends prefix to every key passed through Get/Set/Delete. Useful when sharing one backend among multiple middleware (e.g., session on "sess:" and cache on "cache:").
Feature extensions (GetAndDeleter, Scanner, PrefixDeleter, SetNXer) on the inner KV are transparently surfaced: the returned value implements whichever of those the inner value implements, with keys/prefixes rewritten accordingly.
Example ¶
ExamplePrefixed wraps any store.KV with a key namespace. Useful when several middlewares share one Redis instance — give each one its own prefix so keys never collide.
package main
import (
"context"
"fmt"
"time"
"github.com/goceleris/celeris/middleware/store"
)
func main() {
kv := store.NewMemoryKV()
defer kv.Close()
sessions := store.Prefixed(kv, "sess:")
_ = sessions.Set(context.Background(), "abc", []byte("hi"), time.Minute)
// Underlying key is "sess:abc".
raw, _ := kv.Get(context.Background(), "sess:abc")
fmt.Println(string(raw))
}
Output: hi
type MemoryKV ¶
type MemoryKV struct {
// contains filtered or unexported fields
}
MemoryKV is an in-memory, sharded implementation of KV. It also implements GetAndDeleter, Scanner, PrefixDeleter, and SetNXer.
MemoryKV is intended as the default/test backend and for single-process deployments. For multi-instance deployments, use a driver-backed adapter (middleware/session/redisstore, etc.).
func NewMemoryKV ¶
func NewMemoryKV(config ...MemoryKVConfig) *MemoryKV
NewMemoryKV returns a new MemoryKV with a running cleanup goroutine. Call MemoryKV.Close when the store is no longer needed, or pass a cancellable MemoryKVConfig.CleanupContext.
Example ¶
ExampleNewMemoryKV — the in-memory sharded LRU is the default backing store for middleware/session, middleware/csrf, middleware/idempotency and middleware/cache. It implements every optional extension (Scanner, PrefixDeleter, GetAndDeleter, SetNXer) so any middleware that requires one will accept it.
package main
import (
"context"
"fmt"
"time"
"github.com/goceleris/celeris/middleware/store"
)
func main() {
kv := store.NewMemoryKV()
defer kv.Close()
ctx := context.Background()
_ = kv.Set(ctx, "session:abc", []byte("payload"), 30*time.Minute)
v, _ := kv.Get(ctx, "session:abc")
fmt.Println(string(v))
}
Output: payload
func (*MemoryKV) Close ¶
func (m *MemoryKV) Close()
Close stops the cleanup goroutine. Safe to call multiple times. After Close, Get/Set/Delete still work but expired entries are not reaped.
func (*MemoryKV) DeletePrefix ¶
DeletePrefix implements PrefixDeleter.
func (*MemoryKV) GetAndDelete ¶
GetAndDelete implements GetAndDeleter atomically under the shard lock. Returns nil, ErrNotFound on a missing or expired key.
func (*MemoryKV) Increment ¶ added in v1.4.2
Increment atomically increments the integer counter at key by 1, returning the post-increment value. Implements the Counter extension. The stored value is the decimal-string encoding of the current count; this matches the wire format Redis INCR returns and keeps the on-disk shape compatible across backends.
Namespace: Increment shares the key namespace with MemoryKV.Set — they are not separate maps. A key that holds a non-integer value previously written via Set returns an error on the next Increment (matching Redis "ERR value is not an integer or out of range"). A Set on a key currently used as a counter overwrites the counter state atomically, so the next Increment starts from the new value (or errors if the new value is non-integer).
TTL: ttl == 0 preserves any existing expiry on the key (matches Redis INCR semantics, which never clears an existing TTL). ttl > 0 resets the expiry to now+ttl on every call — useful when the counter's lifetime is bounded by recent activity (sliding window). If the key did not previously exist, ttl == 0 stores the counter with no expiry.
func (*MemoryKV) Scan ¶
Scan implements Scanner. Returns all non-expired keys with the given prefix. Scan is a point-in-time snapshot; concurrent Set/Delete may or may not be reflected.
type MemoryKVConfig ¶
type MemoryKVConfig struct {
// Shards is the number of lock shards. Default: runtime.NumCPU(),
// rounded up to the next power of two.
Shards int
// CleanupInterval is how often expired entries are evicted.
// Default: 1 minute. Zero is replaced with the default.
CleanupInterval time.Duration
// CleanupContext, if set, controls the cleanup goroutine lifetime.
// When the context is cancelled, the goroutine stops. If nil,
// [MemoryKV.Close] is the only way to stop the goroutine.
CleanupContext context.Context
}
MemoryKVConfig configures the in-memory MemoryKV store.
type PrefixDeleter ¶
PrefixDeleter is implemented by backends that can atomically (or efficiently) delete all keys matching a prefix. Used by Cache for InvalidatePrefix operations. Backends that do not implement this may emulate it via Scan + Delete, but with weaker atomicity guarantees.
type Scanner ¶
Scanner is implemented by backends that support key enumeration by prefix (e.g., Redis SCAN). Used by session Reset to delete all keys matching a prefix. When a KV does not implement Scanner, Reset is a documented no-op.
type Scripter ¶
type Scripter interface {
EvalSHA(ctx context.Context, sha string, keys []string, args ...any) (any, error)
ScriptLoad(ctx context.Context, script string) (string, error)
}
Scripter is implemented by backends that support server-side atomic scripts (e.g., Redis EVALSHA + ScriptLoad). Used by the ratelimit Redis adapter for atomic token-bucket updates via a Lua script. Most KV backends do not implement Scripter; it is strictly optional.
type SetNXer ¶
type SetNXer interface {
SetNX(ctx context.Context, key string, value []byte, ttl time.Duration) (acquired bool, err error)
}
SetNXer is implemented by backends that support atomic "set if not exists" semantics (e.g., Redis SET NX, Postgres INSERT ON CONFLICT DO NOTHING). Used by idempotency for lock acquisition. A backend without SetNX cannot serve as an idempotency store.