# Codebase Context: Test Fixture This is a simplified test codebase that mirrors real-world patterns. Use this context to understand the architecture before making changes. ## Architecture Overview ``` src/ ├── broker/ │ └── events.ts # Broker event bus (Subject-based pub/sub) ├── services/ │ ├── user-block.service.ts # Blocking logic │ ├── voice-channel.service.ts # Voice channels and DM calls │ ├── chat.service.ts # Chat channels and messages │ └── team.service.ts # Team state and permission caching └── controllers/ └── roles.controller.ts # REST API for roles ``` ## Critical Pattern: Broker Events **All state changes that affect multiple services MUST emit broker events.** ### Available Events | Event | Emitted When | Expected Subscribers | |-------|--------------|---------------------| | `BusUserBlockChange` | User blocks/unblocks another | Voice services, DM services | | `BusTeamRoleChange` | Role created/updated/deleted | Permission caches | | `BusTeamMemberRoleChange` | User role assigned/removed | Permission caches | | `BusVoiceParticipant` | User joins/leaves voice | Voice UI components | | `BusDmCall` | DM call state changes | Call observers | ### Pattern Example ```typescript // CORRECT: Emit event after state change async updateRole(teamId, roleId, updates) { const role = await db.update(roleId, updates); BusTeamRoleChange.next({ // ← MUST emit event teamId, roleId, action: 'updated', timestamp: new Date(), }); return role; } ``` ## Critical Pattern: Permission vs Action Checks **Permission = Visibility. Action = Separate Check.** ### Why This Matters For DM channels, blocking creates a `'read'` permission, NOT `'denied'`. The user can still SEE the DM channel, but cannot SEND messages. ```typescript // Permission check (for visibility) if (isBlocked) { return { permission: 'read', reason: 'blocked' }; // ← 'read', not 'denied' } // Action check (separate from permission) async sendMessage(userId, channel, content) { if (await isBlockingEitherWay(userA, userB)) { throw new Error('Cannot send messages'); // ← Separate check } } ``` ### Voice Permission Pattern For DM channels, voice permissions are **always true**: ```typescript return { permission: 'read', voiceListen: true, // Always true for DM voiceTalk: true, // Blocking checked on JOIN, not here voiceWebcam: true, voiceScreenshare: true, }; ``` Blocking is enforced by **action checks** in: - `joinVoiceChannel()` - line 33 - `startDmCall()` - line 56 ## Critical Pattern: Cache Invalidation **Caches MUST subscribe to relevant broker events.** ### Current Bug Pattern ```typescript // BAD: Only clears on team switch constructor() { teamChange$.subscribe(() => { this.memoizedPermissions.clear(); }); } // GOOD: Also clear on role changes constructor() { teamChange$.subscribe(() => this.clearCache()); BusTeamRoleChange.subscribe(() => this.clearCache()); // ← ADD THIS BusTeamMemberRoleChange.subscribe(() => this.clearCache()); // ← AND THIS } ``` ## Checklist: Before Making Changes ### For ANY state change: 1. [ ] Does this change affect other services? 2. [ ] Is there a broker event for this? If not, should there be? 3. [ ] Are all relevant services subscribed to the event? ### For blocking-related changes: 1. [ ] Is blocking checked on all relevant ACTIONS (not just permissions)? 2. [ ] What happens if block is created DURING an action (e.g., mid-call)? 3. [ ] Are broker events emitted for blocking changes? 4. [ ] Do voice services subscribe to `BusUserBlockChange`? ### For permission/cache changes: 1. [ ] What events should invalidate this cache? 2. [ ] Is the cache subscribed to all relevant broker events? 3. [ ] What's the TTL? Is stale data acceptable? ## Files Quick Reference | File | Key Functions | Known Issues | |------|---------------|--------------| | `user-block.service.ts` | `blockUser()`, `unblockUser()` | Missing voice cleanup on block | | `voice-channel.service.ts` | `answerDmCall()`, `startDmCall()` | Missing blocking check on answer | | `team.service.ts` | `getPermission()`, `clearCache()` | Cache doesn't subscribe to role events | | `roles.controller.ts` | `createRole()`, `deleteRole()` | Missing broker events | | `chat.service.ts` | `getChannelPermission()` | **Reference implementation** (correct) | ## Anti-Patterns to Avoid 1. **Fixing in ONE place** - If blocking is checked in `startDmCall()`, it should also be in `answerDmCall()` 2. **Changing permissions** - Don't change `voiceListen: true` to `voiceListen: !isBlocked`. Use action checks instead. 3. **Forgetting broker events** - Every CRUD operation on roles/permissions should emit an event 4. **Assuming cache is fresh** - If an operation can change state, subscribe to its event