| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const SCOPED_PREFIX = 'medos:u:'; |
| const ANON_PREFIX = 'medos:anon:'; |
| const LEGACY_PREFIX = 'medos_'; |
| const CONTEXT_KEY = 'medos:__context__'; |
|
|
| type Ctx = |
| | { kind: 'user'; userId: string; prefix: string } |
| | { kind: 'anon'; anonId: string; prefix: string }; |
|
|
| let currentCtx: Ctx | null = null; |
|
|
| function hasLocalStorage(): boolean { |
| return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'; |
| } |
|
|
| function hasSessionStorage(): boolean { |
| return typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined'; |
| } |
|
|
| function randomId(): string { |
| |
| return ( |
| Date.now().toString(36) + |
| Math.random().toString(36).slice(2, 10) |
| ); |
| } |
|
|
| function getOrCreateAnonCtx(): Ctx { |
| if (!hasSessionStorage()) { |
| return { kind: 'anon', anonId: 'ssr', prefix: `${ANON_PREFIX}ssr:` }; |
| } |
| const existing = sessionStorage.getItem('medos:__anon_id__'); |
| const anonId = existing || randomId(); |
| if (!existing) sessionStorage.setItem('medos:__anon_id__', anonId); |
| return { kind: 'anon', anonId, prefix: `${ANON_PREFIX}${anonId}:` }; |
| } |
|
|
| function getCtx(): Ctx { |
| if (currentCtx) return currentCtx; |
| |
| |
| |
| if (hasLocalStorage()) { |
| try { |
| const raw = localStorage.getItem(CONTEXT_KEY); |
| if (raw) { |
| const parsed = JSON.parse(raw) as { userId?: string }; |
| if (parsed.userId) { |
| currentCtx = { |
| kind: 'user', |
| userId: parsed.userId, |
| prefix: `${SCOPED_PREFIX}${parsed.userId}:`, |
| }; |
| return currentCtx; |
| } |
| } |
| } catch { |
| |
| } |
| } |
| currentCtx = getOrCreateAnonCtx(); |
| return currentCtx; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function migrateLegacyIfNeeded(suffix: string, scoped: string): void { |
| if (!hasLocalStorage()) return; |
| const ctx = getCtx(); |
| if (ctx.kind !== 'user') return; |
| const legacyKey = `${LEGACY_PREFIX}${suffix}`; |
| try { |
| const legacyVal = localStorage.getItem(legacyKey); |
| if (legacyVal == null) return; |
| |
| if (localStorage.getItem(scoped) == null) { |
| localStorage.setItem(scoped, legacyVal); |
| } |
| localStorage.removeItem(legacyKey); |
| } catch { |
| |
| } |
| } |
|
|
| |
| |
| |
| |
| export function scopedKey(suffix: string): string { |
| const ctx = getCtx(); |
| const scoped = `${ctx.prefix}${suffix}`; |
| migrateLegacyIfNeeded(suffix, scoped); |
| return scoped; |
| } |
|
|
| |
| |
| |
| |
| export function isAuthenticatedContext(): boolean { |
| return getCtx().kind === 'user'; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function wipeUserScopedStorage(userId?: string): void { |
| if (hasLocalStorage()) { |
| const victim = userId ? `${SCOPED_PREFIX}${userId}:` : SCOPED_PREFIX; |
| const toDelete: string[] = []; |
| for (let i = 0; i < localStorage.length; i++) { |
| const k = localStorage.key(i); |
| if (!k) continue; |
| if (k.startsWith(victim) || k.startsWith(LEGACY_PREFIX)) toDelete.push(k); |
| } |
| toDelete.forEach((k) => localStorage.removeItem(k)); |
| } |
| if (hasSessionStorage()) { |
| const toDelete: string[] = []; |
| for (let i = 0; i < sessionStorage.length; i++) { |
| const k = sessionStorage.key(i); |
| if (!k) continue; |
| if (k.startsWith(ANON_PREFIX) || k.startsWith('medos:')) toDelete.push(k); |
| } |
| toDelete.forEach((k) => sessionStorage.removeItem(k)); |
| } |
| } |
|
|
| |
| |
| |
| |
| export function wipeAllMedosStorage(): void { |
| if (hasLocalStorage()) { |
| const toDelete: string[] = []; |
| for (let i = 0; i < localStorage.length; i++) { |
| const k = localStorage.key(i); |
| if (!k) continue; |
| if ( |
| k.startsWith(SCOPED_PREFIX) || |
| k.startsWith(ANON_PREFIX) || |
| k.startsWith(LEGACY_PREFIX) || |
| k.startsWith('medos:') |
| ) { |
| toDelete.push(k); |
| } |
| } |
| toDelete.forEach((k) => localStorage.removeItem(k)); |
| } |
| if (hasSessionStorage()) { |
| const toDelete: string[] = []; |
| for (let i = 0; i < sessionStorage.length; i++) { |
| const k = sessionStorage.key(i); |
| if (!k) continue; |
| if (k.startsWith('medos:')) toDelete.push(k); |
| } |
| toDelete.forEach((k) => sessionStorage.removeItem(k)); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function setStorageUserContext(userId: string | null | undefined): void { |
| const prev = currentCtx; |
| const prevUserId = prev && prev.kind === 'user' ? prev.userId : null; |
|
|
| if (userId) { |
| if (prevUserId && prevUserId !== userId) { |
| |
| wipeUserScopedStorage(prevUserId); |
| } |
| |
| if (hasSessionStorage()) { |
| const toDelete: string[] = []; |
| for (let i = 0; i < sessionStorage.length; i++) { |
| const k = sessionStorage.key(i); |
| if (!k) continue; |
| if (k.startsWith(ANON_PREFIX) || k === 'medos:__anon_id__') toDelete.push(k); |
| } |
| toDelete.forEach((k) => sessionStorage.removeItem(k)); |
| } |
| currentCtx = { |
| kind: 'user', |
| userId, |
| prefix: `${SCOPED_PREFIX}${userId}:`, |
| }; |
| if (hasLocalStorage()) { |
| try { |
| localStorage.setItem(CONTEXT_KEY, JSON.stringify({ userId })); |
| } catch { |
| |
| } |
| } |
| return; |
| } |
|
|
| |
| if (prevUserId) wipeUserScopedStorage(prevUserId); |
| if (hasLocalStorage()) { |
| try { |
| localStorage.removeItem(CONTEXT_KEY); |
| } catch { |
| |
| } |
| } |
| |
| |
| if (hasSessionStorage()) { |
| try { |
| sessionStorage.removeItem('medos:__anon_id__'); |
| } catch { |
| |
| } |
| } |
| currentCtx = getOrCreateAnonCtx(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function __resetStorageNamespaceForTests(): void { |
| currentCtx = null; |
| } |
|
|