API index and search · Build metadata
Source snapshot
packages/optimistic/src/client-id.ts
1import type { TicketPersistence } from "@rindle/remote";23// The connection's stable clientID — kept in its own (wasm-free) module so it can be unit-tested4// without pulling the engine in. A per-ORIGIN base (localStorage) shared by every tab and kept5// across reloads, a per-TAB suffix (sessionStorage) that survives a reload within a tab but is6// distinct per tab, and a per-LOAD instance suffix that keeps two clients in one tab distinct.7// Without these two tabs (or two clients in one tab) would share a clientID and collide on the8// server's per-clientID mid sequence (a confirmed-mid-ahead-of-issued throw, OPTIMISTIC-WRITES §8.2).910/** The per-ORIGIN base id (localStorage): one logical identity shared by every tab and kept11 * across reloads. */12const CLIENT_ID_KEY = "rindle-client-id";13/** The per-TAB suffix (sessionStorage): survives a reload within a tab but is distinct per tab. */14const TAB_ID_KEY = "rindle-tab-id";15/** The per-TAB follower-affinity ticket (sessionStorage): kept across a reload WITHIN a tab (so a16 * reload lands back on the same follower — monotone reads survive it, FOLLOWER-AFFINITY-DESIGN.md17 * §8), but DISTINCT per tab so two tabs can pin two regions (design §13; a shared localStorage18 * ticket would force both onto one follower). The ticket is opaque and short-lived — the follower19 * re-mints one on every connect — so losing it (no sessionStorage) merely re-anycasts. */20const AFFINITY_TICKET_KEY = "rindle-affinity-ticket";2122/** Read-or-mint a value in a web Storage area, tolerating its absence (SSR, privacy mode). Returns23 * `undefined` when the area is unavailable so the caller can pick a fallback. */24function persistedId(area: "localStorage" | "sessionStorage", key: string, mint: () => string): string | undefined {25 try {26 const storage = (globalThis as unknown as Record<string, Storage | undefined>)[area];27 if (!storage) return undefined;28 const existing = storage.getItem(key);29 if (existing) return existing;30 const fresh = mint();31 storage.setItem(key, fresh);32 return fresh;33 } catch {34 return undefined;35 }36}3738/** Per-PAGE-LOAD instance counter. The FIRST client created in a tab keeps the bare tab suffix (so a39 * reload resumes its mid sequence and adds no row); each ADDITIONAL client constructed in the same40 * load gets a distinct instance suffix, so two clients in one tab never share a mid stream. Held in41 * memory so it resets on reload — the sole client then reclaims the bare, reload-stable suffix. */42let tabInstance = 0;4344/** Test-only: reset the per-load instance counter to simulate a fresh page load. Deliberately NOT45 * re-exported from the package barrel. */46export function resetTabInstanceForTests(): void {47 tabInstance = 0;48}4950/** Clear the persisted client identity so the next page load starts a fresh mid/lmid stream.51 * Intended for dev recovery after out-of-band server state loss, not for normal operation. */52export function resetStableClientID(): void {53 try {54 globalThis.localStorage?.removeItem(CLIENT_ID_KEY);55 } catch {56 // Storage can throw in private modes or test shims; best-effort reset.57 }58 try {59 globalThis.sessionStorage?.removeItem(TAB_ID_KEY);60 } catch {61 // Storage can throw in private modes or test shims; best-effort reset.62 }63 tabInstance = 0;64}6566/** A sessionStorage-backed {@link TicketPersistence} for the affinity ticket — per-tab, survives a67 * reload (see {@link AFFINITY_TICKET_KEY}). Every access tolerates web storage being absent (SSR,68 * private mode): the store then behaves as pure in-memory, re-anycasting on each fresh load. */69export function sessionTicketPersistence(): TicketPersistence {70 const area = () => {71 try {72 return (globalThis as unknown as Record<string, Storage | undefined>).sessionStorage;73 } catch {74 return undefined;75 }76 };77 return {78 load: () => {79 try {80 return area()?.getItem(AFFINITY_TICKET_KEY) ?? undefined;81 } catch {82 return undefined;83 }84 },85 save: (ticket) => {86 try {87 area()?.setItem(AFFINITY_TICKET_KEY, ticket);88 } catch {89 // Best-effort persistence; a full/blocked store just means we re-anycast on the next load.90 }91 },92 clear: () => {93 try {94 area()?.removeItem(AFFINITY_TICKET_KEY);95 } catch {96 // Best-effort.97 }98 },99 };100}101102/** The connection's stable clientID. A per-origin base (localStorage) keeps one logical identity103 * across reloads; a per-tab suffix (sessionStorage) gives each tab its OWN mid/lmid stream; a104 * per-load instance suffix keeps two clients constructed in ONE tab from sharing that stream. Falls105 * back to a fresh random id when web storage is unavailable.106 *107 * Note: an explicit "duplicate tab" copies sessionStorage, so the clone briefly shares its source's108 * suffix until reloaded — the one residual case of the shared-clientID collision. */109export function stableClientID(): string {110 const base = persistedId("localStorage", CLIENT_ID_KEY, () => crypto.randomUUID());111 if (!base) return crypto.randomUUID(); // no localStorage: session-scoped, unique per load112 // With localStorage but no sessionStorage, mint a fresh suffix each load so tabs still differ.113 const tab =114 persistedId("sessionStorage", TAB_ID_KEY, () => crypto.randomUUID().slice(0, 8)) ??115 crypto.randomUUID().slice(0, 8);116 // Instance 0 keeps the bare tab suffix (reload-stable, no extra row); extra concurrent clients in117 // one tab get `.N` so they can't collide on the server's per-clientID mid sequence.118 const instance = tabInstance++;119 const suffix = instance === 0 ? tab : `${tab}.${instance}`;120 return `${base}-${suffix}`;121}122