Files
Kaysser Kayyali 86d4354b51 feat(session): atomicMutate (per-threadId mutex) + Redis key registry (Story 5.2)
sessionManager.atomicMutate(threadId, mutator): atomic read-modify-write
serialized per threadId via an in-process promise chain. Single-process
Node: the only concurrency is across await boundaries in one event loop, so
an in-process per-key mutex prevents lost updates (no Lua/WATCH — swap path
documented for a future multi-instance). update() retained (constant patches
with no concurrent read) but flagged as non-atomic.

Migrate every read-derived / concurrency-sensitive sessionManager.update()
call site to atomicMutate: pendingSkillCheck set/clear (skillCheckEmit,
rollHandler, messageRouter roll-resolve + auto-cancel), pendingSkillCheckAttempts
increment (messageRouter pending-block — now reads current count inside the
mutator so two concurrent pending messages can't both read a stale count),
heldMessages append/clear, players join (re-checks presence inside the
mutator), addMessage history (trim inside the lock), goal_register spec
update, encounter/turn resolve. Closes the TOCTOU windows the group-check
multi-player fan-out will stress.

Add src/db/keys.ts: the Redis key registry (single source for key shapes,
documented owner/TTL/sweep table) — groupcheck/lobby/encounter:active/
character_status/campaign key builders for the upcoming feature stories.

Tests: 432 unit pass (5 new atomicMutate concurrency tests + key-registry
shape test); migrated test mocks to expose atomicMutate; tsc clean.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-21 00:09:51 +00:00
..