refactor: extract generic TTLStore to replace duplicate in-memory stores (#24)
* refactor: extract generic TTLStore to replace duplicate in-memory store logic
OAuthStateStore, OAuthSessionStore, and CookieSessionStore all implemented
the same pattern: Map + set/get/del + periodic cleanup interval + destroy.
Extract that shared logic into a generic TTLStore<V> class with a configurable
isExpired predicate, and refactor all three stores to delegate to it.
- TTLStore provides: set, get (lazy eviction), getRaw (no eviction), delete,
destroy, and background cleanup with structured logging
- OAuthStateStore/OAuthSessionStore wrap TTLStore with async methods for
@atproto/oauth-client-node SimpleStore compatibility
- CookieSessionStore wraps TTLStore with null-returning get for its API
- app-context.ts unchanged (same public interface for all three stores)
- 18 new tests for TTLStore covering expiration, cleanup, logging, destroy
* fix: address critical adapter testing and lifecycle issues in TTLStore refactoring
**Issue #1: Adapter layer completely untested (Critical)** ✅
- Added 19 comprehensive adapter tests (11 OAuth + 8 cookie session)
- OAuthStateStore: async interface, StateEntry wrapping, 10-min TTL verification
- OAuthSessionStore: getUnchecked() bypass, complex refresh token expiration logic
- CookieSessionStore: Date comparison, null mapping, expiration boundary testing
- Tests verify adapters correctly wrap TTLStore with collection-specific behavior
**Issue #2: Complex expiration logic untested (Critical)** ✅
- Added 3 critical tests for OAuthSessionStore refresh token handling:
- Sessions with refresh tokens never expire (even if access token expired)
- Sessions without refresh token expire when access token expires
- Sessions missing expires_at never expire (defensive)
- Verifies the conditional expiration predicate works correctly
**Issue #3: Post-destruction usage creates zombie stores (Critical)** ✅
- Added destroyed state tracking to TTLStore
- CRUD operations now throw after destroy() is called
- destroy() is idempotent (safe to call multiple times)
- Prevents memory leaks from zombie stores with no cleanup
**Issue #4: getRaw() violates TTL contract (Important)** ✅
- Renamed getRaw() to getUnchecked() to make danger explicit
- Added UNSAFE JSDoc warning about bypassing expiration
- Updated all callers (OAuthSessionStore, ttl-store.test.ts)
Test count: 171 passed (was 152)
Addresses PR #24 review feedback from type-design-analyzer, code-reviewer, and pr-test-analyzer agents.
---------
Co-authored-by: Claude <noreply@anthropic.com>