Documentation
¶
Overview ¶
Package store defines the unified byte-level key-value interface shared by celeris middleware (session, csrf, ratelimit, jwt, cache, idempotency).
Interfaces ¶
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.
Optional extension interfaces (GetAndDeleter, Scanner, PrefixDeleter, SetNXer, Scripter) surface backend capabilities. Middleware feature- detect these via type assertion and fall back to emulation or no-op semantics when a backend does not implement them.
Reference backends ¶
- MemoryKV — in-memory, sharded implementation. Implements every optional extension. Used as the default store and in tests.
- middleware/session/redisstore — Redis-backed via driver/redis. Implements KV + Scanner; GetAndDeleter when Redis >= 6.2.
- middleware/session/postgresstore — PostgreSQL-backed via driver/postgres. Implements KV only; Reset via TRUNCATE (exposed as io.Closer).
- middleware/csrf/redisstore — Redis-backed CSRF storage. Implements KV + GetAndDeleter (GETDEL).
- middleware/ratelimit/redisstore — Redis-backed ratelimit store. Implements ratelimit.Store via EVALSHA; not a KV adapter.
Response wire format ¶
EncodedResponse + ResponseWireVersion define a byte-efficient snapshot format used by cache and idempotency middleware to persist captured HTTP responses. The format is versioned to allow forward compatibility; decoders reject unknown versions.
Contract summary ¶
- Get returns (nil, ErrNotFound) on miss. (nil, nil) is forbidden.
- Set with ttl == 0 stores with no expiry.
- Delete is idempotent: deleting a missing key returns nil.
- Values returned by Get are caller-owned; backends must copy.
- All methods are safe for concurrent use.
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 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) 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 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) 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.