/** * - WHICH CACHED THREADS SURVIVE A SERVER LISTING. * * THE DEFECT IT CLOSES, measured 2026-08-24: a referral whose queue item the server had retired kept * showing on the front desk **through a reload and a brand-new tab**. The retire had fired and the server * had stopped listing it - the desk was rendering the local thread cache, and **nothing ever removed * anything from that cache.** It only grew. A coordinator books the patient, the referral correctly * reads `Scheduled`, and the queue item they were told would leave is still sitting there. * * PURE, so the rule can be reasoned about without a service tree or `localStorage`. The decision about * WHEN to apply it is the dangerous half and lives at the call site - see the warning below. */ /** Only the two fields the rule reads. Deliberately structural: the cache carries far more than this. */ export interface ReconcilableThread { threadId?: string; threadType?: string; } /** * Keep the cached threads the server still lists, and drop the in-scope ones it does not. * * **CALL THIS ONLY WITH A LIST THAT ACTUALLY ARRIVED.** An empty set is read as *"the server has none * of these"* and clears every in-scope entry - which is correct for a real empty answer and catastrophic * for a failed fetch. Never call it from a `catch`. The listing calls in `CommunicationService` are * wrapped in `try` blocks that deliberately continue on cached data precisely so a network blip cannot * empty a practice's inbox, and this function **cannot tell the two cases apart** - only the caller can. * * `isInScope` FORCES THE CALLER TO SAY WHAT THE LISTING WAS AUTHORITATIVE FOR. The staff pool listing * asks for pool threads only; letting it retire a receptionist's own conversations because they were not * mentioned would be a much worse bug than the stale row being fixed. * * An entry with no `threadId` is KEPT: it cannot be matched against the list, and silently discarding * what you cannot evaluate is how a cache loses something nobody meant to remove. */ export const reconcileThreadCache = ( cached: T[], liveThreadIds: Set, isInScope: (thread: T) => boolean, ): T[] => cached.filter( (thread) => !isInScope(thread) || !thread.threadId || liveThreadIds.has(thread.threadId), );