All notable changes to idemkit are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. Under 0.x the API is considered unstable; minor releases may include breaking changes that are called out below.
- Redis backend (
github.com/polanski13/idemkit/store/redis) — implementsidemkit.Storeviagithub.com/redis/go-redis/v9. Lua-scriptedBegin/Save/Releasefor single-RTT atomic operations;crypto/rand64-bit tokens (no Redis counter, no sequence to maintain); Redis-native TTL handles lock-timeout reclaim (noSELECT FOR UPDATEanalogue needed); pollingWait. Every Lua script touches exactly one key, so the store is Redis Cluster-compatible without hash tags. Constructor acceptsredis.UniversalClient— works transparently with*Client,*ClusterClient,*Ring, and*FailoverClient. - Opt-in Redis pub/sub overlay (
redis.Config.PubSub) — reactiveWaitlayered on top of polling.SaveandReleasePUBLISHinside the same Lua script that mutates state, on a notify channel derived fromConfig.KeyPrefix(default"idemkit:notify"); a subscriber goroutine dispatches each notification's payload (the bare key) to per-key registered waiters. TheWaitselectraces notify-chan, polling-tick, andctx.Done()via the nil-channel idiom — one select block covers both modes. Polling stays as the correctness backstop because Redis pub/sub has no persistence; the overlay is a latency hint, never the sole signal. - Opt-in LISTEN/NOTIFY for Postgres (
pg.Config.ListenConn) — analogous to the Redis pub/sub overlay. Caller supplies a dedicated*pgx.Connoutsidepgxpool(LISTEN is connection-state, pooled conns lose the subscription on return). Store issuesLISTEN idemkit_notifyand emitspg_notifyfromSave/Releasevia the same conditional pattern used in Redis (CASE WHEN $N != '' THEN pg_notify(...)), riding the same statement as the UPDATE / DELETE through a CTE. Polling stays as the correctness backstop. Store does not close the caller's conn onStore.Close()— caller owns conn lifecycle. Store.Close()on bothpg.Storeandredis.Store— stops the listener / subscriber goroutine cleanly viasync.Once. No-op when the overlay is unset. Recommended pattern:defer store.Close()at process shutdown.- CI Redis service —
.github/workflows/ci.ymlnow stands upredis:7alongsidepostgres:16and exposesIDEMKIT_REDIS_TEST_URLat job level. Tests skip cleanly when the env is unset (same convention asIDEMKIT_PG_TEST_URL).
- DESIGN.md gains a "Redis store (v0.3)" section paralleling the Postgres one — storage layout, Lua-script rationale,
crypto/randtoken approach, why-no-WATCH/MULTI/EXEC, Cluster compatibility, and a "Pub/sub overlay" subsection covering the lifecycle contract. - DESIGN.md "Postgres store (v0.2)" section gains a "LISTEN/NOTIFY overlay" subsection covering the dedicated-conn requirement, the CTE rewrite of
saveSQL/releaseSQLwith conditionalpg_notify, why-polling-stays, and the conn-ownership contract. - BENCHMARKS.md gains a "Redis store" section with measured 3-run median numbers; the "Postgres store" section is refreshed on the same methodology. Headline: Redis Begin is ~3.5× faster than pg Begin (Lua single-RTT vs pg multi-statement transaction), Save roundtrip ~2× faster.
- README quickstarts for Redis (basic + pub/sub overlay) and an inline LISTEN/NOTIFY example in the Postgres quickstart.
- #9 Implement Redis store (
store/redis) - #10 Opt-in LISTEN/NOTIFY for Postgres store
- #11 Opt-in Redis pub/sub overlay for
store/redis
- Added
github.com/redis/go-redis/v9as a direct dependency. Only users importingstore/redislink it;idemkitcore,store/mem, andstore/pgare unaffected.
The idemkit.Store interface, the idemkit.Config shape, and the existing pg.Config / mem.Config fields are unchanged. v0.2 callers upgrade to v0.3 with no source edits unless they want to opt into the new overlays via the additive pg.Config.ListenConn and redis.Config.PubSub fields.
- Postgres backend (
github.com/polanski13/idemkit/store/pg) — implementsidemkit.Storeviapgx/v5. Atomic claim throughINSERT ... ON CONFLICT DO NOTHING RETURNING token; row-based reclaim on lock-timeout or TTL expiry viaSELECT ... FOR UPDATE; polling-basedWait(LISTEN/NOTIFYdeferred to v0.3 as opt-in). Schema instore/pg/schema.sql, embedded via//go:embed.ApplySchemais idempotent (IF NOT EXISTS). - Generation tokens (
idemkit.Token,idemkit.ErrTokenMismatch) —Beginnow returns a non-zeroTokenonStateFresh;SaveandReleaserequire the token.Savewith a stale token returnsErrTokenMismatch;Releasewith a stale token is a noop. Closes the lock-timeout-reclaim race documented in DESIGN.md. Result.Clone()— deep copy of aResult(StatusCode + cloned Header + copied Body).mem.Storenow clones on both input (Save) and output (Begin/Waitof cached results); caller mutation cannot corrupt the cache.ConflictMode: ConflictIETF— returns 409 Conflict on body-hash mismatch perdraft-ietf-httpapi-idempotency-key-header-07 §2.6.ConflictStriperemains the default (422 Unprocessable Entity).internal/conformance/— separate test files per spec (stripe_test.go,ietf_draft07_test.go) documenting each mode's contract as a standalone spec. Shared fixtures inhelpers_test.go.examples/chi/— runnable chi router example withKeyScope-based tenant isolation viaX-Tenant-IDheader. Separate Go module keeps chi out of the core dep graph.mem.Config.JanitorInterval+mem.Store.Close()— optional background goroutine for proactive expiry. Closes waiter channels on expired entries regardless of access patterns.Close()stops the goroutine cleanly (idempotent viasync.Once). DefaultJanitorInterval: 0preserves v0.1's zero-goroutine semantics.- Postgres benchmarks (
BenchmarkPG_*) and a new "Postgres store" section in BENCHMARKS.md with measured round-trip costs.
idemkit.Storeinterface signature:Beginnow returns(State, *Result, Token, error)(addedToken).SaveandReleasetake aTokenparameter. Anyone implementing a customStorewill need to update method signatures. Within 0.x semver, breaking changes are explicit.- Min Go version bumped to 1.25 (was 1.22 in v0.1.0). Go 1.22 is N-4 and EOL; the project uses no 1.22-specific features.
- DESIGN.md "Known limitations" #1, #2, #3 marked closed in v0.2:
- #1 Lock-timeout + reclaim race — resolved by tokens
- #2
Resultnot defensively cloned — resolved byResult.Clone() - #3 Waiter without
ctx.Deadlineblocks forever — resolved by optional janitor
- New "Postgres store" architecture section explaining transactional Begin flow, sequence-based tokens, polling-Wait rationale, and why no advisory locks.
- New "Conflict semantics" section enumerating mode-specific status codes and what's not yet covered from the IETF draft (RFC 7807 Problem Details, per-method validation).
- README quickstarts for Postgres and chi.
- #1 Implement Postgres store
- #2 Implement
ConflictMode: ConflictIETF - #3 Generation tokens for safe
Saveunder lock-timeout race - #4 Add
Result.Clone()for defensive copying - #5 Add chi router example (via PR #8 from
@nightcityblade) - #6 Add
internal/conformance/test suite (Stripe + IETF) - #7 Optional
JanitorIntervalfor proactive expiry inmem.Store
- Added
github.com/jackc/pgx/v5as a direct dependency. Only users importingstore/pglink it;idemkitcore andstore/memremain stdlib-only.
Initial release.
net/httpmiddleware viaidemkit.Middleware(store, cfg).- In-memory
Store(store/mem) — single-instance, race-safe, zero non-stdlib deps. - Length-prefixed body-hash fingerprinting (method + path + query + body).
- Streaming safe-skip on
http.Flusher.Flush(). - Stripe-style 422 on body-hash mismatch (configurable via
OnConflict). KeyScopefor tenant isolation via storage-key prefix.MaxRequestBytes/MaxResponseBytescaps (1 MiB defaults).- 84+ tests under
-race; 2M+ fuzz executions clean. - GitHub Actions CI (gofmt + vet + build + race tests on Go 1.25 and stable; 15s fuzz smoke).
- README with quickstart, threat model, FAQ, roadmap.
- DESIGN.md with architecture decisions, plan deviations, named limitations.
- COMPARISON.md vs eight prior-art Go libraries.
- BENCHMARKS.md with measured per-request overhead and methodology.
- Apples-to-apples benchmark vs
velmie/idempoinbenchmarks/.