Skip to content

feat: Add Redis-backed conversation store and distributed rate limiter - #8

Merged
ThomasHartDev merged 1 commit into
mainfrom
thomas/feat/store-redis
Jul 21, 2026
Merged

feat: Add Redis-backed conversation store and distributed rate limiter#8
ThomasHartDev merged 1 commit into
mainfrom
thomas/feat/store-redis

Conversation

@ThomasHartDev

Copy link
Copy Markdown
Owner

This adds packages/store-redis, the persistence layer for conversations plus a rate limiter that actually works across more than one process. The whole thing sits behind a small RedisPort interface (just the handful of commands we use) so the store and limiter never touch a concrete client directly. That seam is what lets InMemoryRedis stand in as both the no-config fallback and the test double, so nothing here needs a running Redis to pass CI.

Conversation history goes into a Redis list rather than a serialized blob, so appending is a single atomic RPUSH and two nodes writing at once can't clobber each other. Reads run every message back through Zod since data coming out of an external store isn't something I want to trust blindly, and there's an optional TTL that slides forward on each append so idle sessions age out on their own. The rate limiter is a fixed-window counter where the increment and the expiry happen in one Lua script, which closes the gap where a crash right after INCR could leave a counter that never resets. It returns the same result shape as the in-process limiters in rate-limiter so callers can swap between them.

Fixed-window has the usual boundary overshoot, so I left a note pointing at SlidingWindowLog for when exactness matters more than memory. Full unit coverage on both, including ttl expiry, window rollover, and key isolation.

@ThomasHartDev

Copy link
Copy Markdown
Owner Author

The InMemoryRedis.eval only knows how to run the one script we ship. That's deliberate: without a real Lua interpreter I can't run arbitrary scripts, but faithfully reproducing INCR + PEXPIRE-on-first-hit + PTTL means the limiter's actual math (remaining, retryAfterMs, window rollover) gets exercised for real in CI, and the Lua path is what runs in prod. If someone passes a different script it throws loudly rather than silently doing the wrong thing.

@ThomasHartDev

Copy link
Copy Markdown
Owner Author

Went with meta-in-a-string + messages-in-a-list instead of one JSON blob specifically to make appends atomic. A serialized blob forces read-modify-write, which loses updates under concurrent writers. The one wart is the existence check in append is a separate GET before the RPUSH, so there's a tiny TOCTOU if a conversation expires in between. Acceptable here since the ttl is measured in minutes/hours, but worth knowing.

@ThomasHartDev
ThomasHartDev merged commit 0ad10e6 into main Jul 21, 2026
1 check passed
@ThomasHartDev
ThomasHartDev deleted the thomas/feat/store-redis branch July 21, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant