1/** 2 * @factory/shared — realtime presence palette (PRODUCT-SPEC realtime row, 3 * phase-07 TASK-0092). 4 * 5 * `presenceColor(userId)` is the single deterministic source of a peer's 6 * cursor/avatar color, shared so the server (presence broadcasts) and 7 * clients agree without negotiating. 20 distinct colors; the a11y rule is 8 * that color is never the only identifier (the peer's name label is 9 * mandatory — ux-requirements §12), so a color collision is harmless. 10 * 11 * Deterministic FNV-1a 32-bit hash → palette index. No allocation, no 12 * Math.random (rule 25: deterministic behavior). 13 */ 14 15/** The 20-color presence palette (closed set; names, not raw hex, on the wire). */ 16export const PRESENCE_PALETTE: readonly string[] = [ 17 "red", 18 "orange", 19 "amber", 20 "yellow", 21 "lime", 22 "green", 23 "teal", 24 "cyan", 25 "sky", 26 "blue", 27 "indigo", 28 "violet", 29 "purple", 30 "fuchsia", 31 "pink", 32 "rose", 33 "slate", 34 "gray", 35 "zinc", 36 "stone", 37]; 38 39/** 40 * FNV-1a 32-bit hash of a string. Deterministic across platforms (32-bit 41 * math is exact in JS doubles). 42 */ 43export function fnv1a32(input: string): number { 44 let hash = 0x811c9dc5; 45 for (let i = 0; i < input.length; i += 1) { 46 hash ^= input.charCodeAt(i); 47 // hash *= 16777619 (FNV prime) — via multiply + >>> 0 (32-bit wrap) 48 hash = Math.imul(hash, 0x01000193) >>> 0; 49 } 50 return hash >>> 0; 51} 52 53/** 54 * The deterministic presence color name for a user. 55 * 56 * Distribution guarantee (unit-tested): over any 1,000-user fixture no 57 * color is assigned to more than 10 % of users (palette of 20 ⇒ expected 58 * 5 %; the test asserts the FNV-1a mix is uniform enough). 59 */ 60export function presenceColor(userId: string): string { 61 const index = fnv1a32(userId) % PRESENCE_PALETTE.length; 62 return PRESENCE_PALETTE[index] ?? "gray"; 63} 64