1// preferences.js 2// Canonical client-side preferences manager for Activity Center. 3// Single source of truth: window.orchestrator.activityCenter.termsFollowing 4// Shape: { "TypeName": [ { "id": "1", "name": "name1" }, ... ], ... } 5// termsFollowing may arrive as a JSON string OR an already-parsed object. 6// Provides simple APIs to ensure readiness, query topic ids, add/remove topics, 7// and notify listeners when preferences change. 8 9const PreferencesManager = (function () { 10 let _ready = false; 11 let _readyPromise = null; 12 13 // canonical topic ids as strings (flat, for fetchFeed / UI lookups) 14 let _topicIds = []; 15 16 // id -> { name, type }, preserved so serializeForServer can rebuild 17 // the full multi-type shape without dropping data on save. 18 let _topics = new Map(); 19 const _listeners = new Set(); 20 21 /** 22 * Build canonical from terms following 23 */ 24 function _buildCanonical() { 25 _topicIds = []; 26 _topics = new Map(); 27 28 try { 29 const raw = window.orchestrator?.activityCenter?.termsFollowing; 30 if (!raw) { 31 return; 32 } 33 34 // raw may be a JSON string or an already-parsed object. 35 const parsed = 'string' === typeof raw ? JSON.parse(raw) : raw; 36 if (!parsed || 'object' !== typeof parsed) { 37 return; 38 } 39
40 Object.keys(parsed).forEach((type) => { 41 const entries = parsed[type]; 42 if (!Array.isArray(entries)) { 43 return; 44 } 45 entries.forEach((t) => { 46 if (t && t.id !== undefined && null !== t.id) { 47 const sid = String(t.id); 48 _topicIds.push(sid); 49 _topics.set(sid, { name: t.name ?? null, type: type }); 50 } 51 }); 52 }); 53 } catch (e) { 54 console.warn( 55 '[PreferencesManager] _buildCanonical failed, treating as empty', 56 e, 57 ); 58 _topicIds = []; 59 _topics = new Map(); 60 } 61 } 62 63 function _notify() { 64 _listeners.forEach((fn) => { 65 try { 66 fn(getTopicsIds()); 67 } catch (e) { 68 console.warn('[PreferencesManager] listener error', e); 69 } 70 }); 71 } 72 73 function ensureReady() { 74 if (_ready) { 75 return Promise.resolve(true); 76 } 77 if (_readyPromise) { 78 return _readyPromise; 79 } 80 _readyPromise = new Promise((resolve) => { 81 try { 82 _buildCanonical(); 83 } finally { 84 _ready = true; 85 _readyPromise = null; 86 resolve(true); 87 } 88 }); 89 return _readyPromise; 90 } 91 92 function getTopicsIds() { 93 return _topicIds.slice(); 94 } 95 96 function setTopicsIds(ids) { 97 _topicIds = Array.isArray(ids) ? ids.map(String).filter(Boolean) : []; 98 99 // Drop type/name tracking for ids no longer present; keep it for ids that remain. 100 const kept = new Map(); 101 _topicIds.forEach((sid) => { 102 if (_topics.has(sid)) { 103 kept.set(sid, _topics.get(sid)); 104 } 105 }); 106 _topics = kept; 107 _notify(); 108 } 109 110 // NOTE: assumes 'Topics' as the type for newly-followed items (e.g. clicking 111 // a category in the feed UI). If callers ever need to add Trends/Organizations 112 // items, they should pass a type explicitly. 113 function addTopic(id, name = null, type = 'Topics') { 114 const sid = String(id); 115 if (!_topicIds.includes(sid)) { 116 _topicIds.push(sid); 117 _topics.set(sid, { name: name, type: type }); 118 _notify(); 119 } 120 } 121 122 function removeTopic(id) { 123 const sid = String(id); 124 if (_topicIds.includes(sid)) { 125 _topicIds = _topicIds.filter((x) => x !== sid); 126 _topics.delete(sid); 127 _notify(); 128 } 129 } 130 131 function onChange(fn) { 132 if ('function' === typeof fn) { 133 _listeners.add(fn); 134 } 135 return () => _listeners.delete(fn); 136 } 137 138 function serializeForServer() { 139 // Rebuild the full multi-type termsFollowing shape from canonical state, 140 // e.g. { Topics: [...], Trends: [...], Organizations: [...] } 141 try { 142 const grouped = {}; 143 _topicIds.forEach((sid) => { 144 const info = _topics.get(sid) || { name: null, type: 'Topics' }; 145 const type = info.type || 'Topics'; 146 if (!grouped[type]) { 147 grouped[type] = []; 148 } 149 grouped[type].push({ id: sid, name: info.name ?? null }); 150 }); 151 return JSON.stringify(grouped); 152 } catch (e) { 153 console.warn('[PreferencesManager] serializeForServer failed', e); 154 return JSON.stringify({ Topics: [] }); 155 } 156 } 157 158 return { 159 ensureReady, 160 getTopicsIds, 161 setTopicsIds, 162 addTopic, 163 removeTopic, 164 onChange, 165 serializeForServer, 166 }; 167})(); 168 169export default PreferencesManager;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.