PageSourceSearch

https://www.housingwire.com/wp-content/themes/housingwire/js/activ…/activity-feed/preferences.js?v=9.3

js housingwire.com collected 2026-10-09 22:41:28 UTC 4,213 bytes, 169 lines download raw bytes

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.