Introduces code.nochebuena.dev/einherjar/cache-valkey — the Valkey cache starter for the Einherjar framework. Absorbs the valkey package from micro-lib and adds three duck-typed adapters that wire directly into auth, web, and auth-jwt. Core: - Provider interface — Get, Set, Del, Exists, Expire, IncrWithTTL, Native() - Component interface — lifecycle.Component + observability.Checkable + Provider - Config struct (EINHERJAR_VALKEY_* env vars) - New(logger, cfg) Component — creates valkey-go client in OnInit; PING in OnStart; logs "valkey: connected" - IncrWithTTL implemented with Lua script (atomic INCR + conditional EXPIRE); race-free fixed-window semantics with no MULTI/EXEC overhead - Priority: LevelDegraded — Valkey outage degrades, does not halt the service Adapters (duck-typed — no import of auth, web, or auth-jwt): - PermissionCache — Get/Set int64 bitmasks as string; satisfies auth/rbac.Cache - RateLimiterStore — fixed-window via IncrWithTTL; satisfies web/mw.RateLimiterStore - Blacklist — Exists/Set "1" with TTL; satisfies auth-jwt.Blacklist Compliance test verifies CT-6, duck-type shape assignments, and full adapter behavioural coverage with a mockProvider (no live server required). - Component interface embeds observability.Identifiable; identifiable.go implements ModulePath and ModuleVersion via runtime/debug.ReadBuildInfo() — prints in launcher banner
105 lines
4.4 KiB
Markdown
105 lines
4.4 KiB
Markdown
# einherjar/cache-valkey
|
|
|
|
[](https://code.nochebuena.dev/einherjar/cache-valkey)
|
|
[](LICENSE)
|
|
[](https://go.dev)
|
|
[]()
|
|
|
|
> Speed is not a virtue. It is the difference between arriving and never arriving.
|
|
|
|
`code.nochebuena.dev/einherjar/cache-valkey` is the Valkey cache component of the Einherjar framework. It wraps `valkey-go` behind a lifecycle-aware `Component` and ships three adapters — permission cache, rate limiter, and JWT blacklist — that satisfy interfaces in `auth`, `web`, and `auth-jwt` via Go's structural typing, with no cross-module import required.
|
|
|
|
## Quick start
|
|
|
|
```go
|
|
import cachevalkey "code.nochebuena.dev/einherjar/cache-valkey"
|
|
|
|
vk := cachevalkey.New(logger, cachevalkey.Config{
|
|
Addrs: []string{"localhost:6379"},
|
|
})
|
|
launcher.Register(vk) // OnInit → OnStart → OnStop
|
|
health.Register(vk) // PING-based health check, LevelDegraded
|
|
```
|
|
|
|
## Connecting to other modules
|
|
|
|
Go's structural typing handles interface assignment across module boundaries —
|
|
no cast, no glue interface required. Create adapters from the component and pass
|
|
them directly to the consuming modules:
|
|
|
|
```go
|
|
// --- 1. Create the component (lifecycle + health + Provider) ---
|
|
vk := cachevalkey.New(logger, cfg)
|
|
launcher.Register(vk)
|
|
health.Register(vk)
|
|
|
|
// --- 2. Create adapters (one per consumer interface) ---
|
|
// Each adapter wraps vk (a Provider) and satisfies one interface in another module.
|
|
// No import of auth, web, or auth-jwt is needed here.
|
|
permCache := cachevalkey.NewPermissionCache(vk)
|
|
rateLimiter := cachevalkey.NewRateLimiterStore(vk, time.Second, 100)
|
|
blacklist := cachevalkey.NewBlacklist(vk)
|
|
|
|
// --- 3. Wire adapters into consuming modules ---
|
|
// Go's structural typing handles the interface assignment — no cast required.
|
|
|
|
// auth: permission cache (satisfies rbac.Cache)
|
|
permProvider := rbac.NewCachedPermissionProvider(permCache, baseProvider, rbac.CacheConfig{TTL: 5 * time.Minute})
|
|
|
|
// web: rate limiter (satisfies mw.RateLimiterStore)
|
|
router.Use(mw.IPRateLimit(rateLimiter))
|
|
|
|
// auth-jwt: refresh token blacklist (satisfies authjwt.Blacklist)
|
|
pair, err := authjwt.RefreshTokenPair(ctx, signer, oldRefreshToken, blacklist, tokenCfg, newClaims)
|
|
```
|
|
|
|
The compile-time duck-type checks in `compliance_test.go` verify that each adapter
|
|
satisfies its target interface. If those checks compile, the wiring above is guaranteed
|
|
to work.
|
|
|
|
## Configuration
|
|
|
|
| Environment variable | Required | Default | Description |
|
|
|---|---|---|---|
|
|
| `EINHERJAR_VALKEY_ADDRS` | Yes | — | Comma-separated `host:port` addresses |
|
|
| `EINHERJAR_VALKEY_PASSWORD` | No | `""` | Auth password |
|
|
| `EINHERJAR_VALKEY_DB` | No | `0` | Database index |
|
|
| `EINHERJAR_VALKEY_CLIENT_CACHE_MB` | No | `0` | Client-side cache per connection in MB (0 = disabled) |
|
|
|
|
## API
|
|
|
|
### Provider interface
|
|
|
|
`Component` satisfies `Provider`. Pass the result of `New()` wherever a `Provider`
|
|
is expected.
|
|
|
|
| Method | Description |
|
|
|---|---|
|
|
| `Get(ctx, key) (string, bool, error)` | GET key; returns `("", false, nil)` on miss |
|
|
| `Set(ctx, key, value, ttl) error` | SET key value [EX ttl]; `ttl=0` = no expiry |
|
|
| `Del(ctx, keys...) error` | DEL one or more keys |
|
|
| `Exists(ctx, key) (bool, error)` | EXISTS key |
|
|
| `Expire(ctx, key, ttl) error` | EXPIRE key ttl |
|
|
| `IncrWithTTL(ctx, key, ttl) (int64, error)` | Atomic INCR + EXPIRE on first increment (Lua) |
|
|
| `Native() vk.Client` | Raw valkey-go client for operations not in Provider |
|
|
|
|
### Adapters
|
|
|
|
| Constructor | Satisfies | Description |
|
|
|---|---|---|
|
|
| `NewPermissionCache(Provider)` | `auth/rbac.Cache` | Stores int64 permission bitmasks as decimal strings |
|
|
| `NewRateLimiterStore(Provider, window, limit)` | `web/mw.RateLimiterStore` | Fixed-window counter; Lua atomic increment |
|
|
| `NewBlacklist(Provider)` | `auth-jwt.Blacklist` | JWT JTI revocation via SET/EXISTS |
|
|
|
|
## Dependency graph
|
|
|
|
```
|
|
cache-valkey
|
|
├── contracts v1.0.0 (lifecycle.Component, observability.Checkable, logging.Logger)
|
|
├── core v1.0.0 (xerrors)
|
|
└── valkey-go v1.0.54 (native client)
|
|
```
|
|
|
|
No dependency on `web`, `auth`, or `auth-jwt`.
|
|
The three adapters satisfy interfaces in those modules via duck typing.
|