// The local cache of message-thread SHELLS, and the sign-out sweep that clears it. // // WHY THIS MODULE EXISTS AT ALL - the key is COMPUTED, and sign-out could not clean it up. // AuthService.logout() clears storage by naming each key literally (`${prefix}_accessToken` and its // ten siblings). That works for fixed keys and cannot work for this one: the thread cache key is // derived per identity (`:`), a staff member can accumulate more than one // bucket across a session, and the identity it was derived from is already gone by the time sign-out // runs. So the cache outlived every sign-out - tokens cleared, conversation list left on the disk. // // The prefix lives HERE, as the single definition both sides share, and the cleanup is a sweep // rather than another literal. Mirrors the clearCampaignCtaAttribution() seam in // utils/campaignCtaAttribution.ts: the module that owns the storage owns the way to clear it, and // AuthService just calls it - that module has no view into auth state, and auth has no business // knowing how a messaging key is spelled. // // SHARED-DEVICE CORRECTNESS, which is the whole point. What this cache holds is not message // bodies - Communication resources are never persisted - but the thread shells ARE identifying: // patientId, organizationId, the staff account, and the RESOLVED HUMAN NAMES of both parties // (CommunicationService enriches sender/recipient displays and writes them back on every load). // A treatment relationship with a named provider is PHI in itself. The sharp case is a practice's // front-desk workstation: staff cache the office's POOLED threads, which exist precisely to carry a // patientId, so a receptionist signing out was leaving a roster of patient names behind for whoever // used the browser next. // // The sweep is deliberately NOT scoped to the departing identity. Clearing "this user's bucket" // would leave every other bucket on the machine - which is the shared-device case, i.e. the one that // matters. Signing out clears the messaging cache for everyone on this browser; the next sign-in // re-fetches, which the code already handles because a cold browser has no cache either. // A demo tab keeps these in its own sessionStorage, so its sign-out sweeps only its own buckets. import { authStorageArea } from '../services/authStorage'; export const DIRECT_THREAD_CACHE_KEY_PREFIX = 'akashic:direct-threads:'; export const directThreadCacheKey = (resourceType: string, id: string): string => `${DIRECT_THREAD_CACHE_KEY_PREFIX}${resourceType}:${id}`; /** * Remove every cached thread bucket on this browser. Called from AuthService.logout(). * * Collects the keys before removing any of them: removeItem() during a live index walk over * localStorage re-indexes the remaining entries, which silently skips every other match. */ export const clearAllDirectThreadCaches = (): void => { try { const doomed: string[] = []; const area = authStorageArea(); if (!area) return; for (let index = 0; index < area.length; index += 1) { const key = area.key(index); if (key?.startsWith(DIRECT_THREAD_CACHE_KEY_PREFIX)) { doomed.push(key); } } doomed.forEach((key) => area.removeItem(key)); } catch { // localStorage unavailable (private mode / disabled) — non-fatal. There is nothing cached to // leak in that case either, since the writes would have failed the same way. } };