doxa-maps/library/lists/peopleGroupList.js
Doxa Maps 3cd8a8cd30
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source c2af6b16
2026-09-26 07:01:59 +00:00

97 lines
4.7 KiB
JavaScript

/**
* peopleGroupList.js — the people-group LIST, a universal prop like engine, locale and token.
*
* Every map shows people groups, and until now always DOXA's (the live API's 2,107). The same maps
* must work for any project's list: Project 7000 (unreached), 4700 (frontier), 700 (frontier of
* 500,000 or more), 2000 (DOXA). A list is chosen the way the engine is, first answer wins:
* 1. profile-config `list` — this embed, decided by whoever mounted the element
* 2. ?list= in the page URL — any generated leaf can be switched from the URL
* 3. window.MAP_LIST — a host global, for a host that sets it programmatically
* 4. 'doxa' — the default: exactly today's behaviour, nothing fetched, nothing filtered
* An id or an alias ('700', 'project-700') is accepted; anything unknown falls back to the default
* and says so once in the console.
*
* WHAT A MAP DOES WITH IT (Tier A). Rows keep coming from the DOXA API, because that is where the
* prayer fields live. applyPeopleGroupList() keeps the rows whose slug belongs to the list, from a
* membership file the build writes (data/people-groups/lists/<id>.json, served under the asset
* pool). So under Project 700 a map shows the DOXA groups that are Project 700 groups. The list's
* groups that DOXA does not carry are Tier B, per map (SPEC-PEOPLE-GROUP-LIST-PROP.md).
*
* FAILS CLOSED. If a non-default list's membership cannot be loaded, the map gets NO rows and the
* console names the list and the URL. A map that silently showed all 2,107 while an embed asked for
* Project 700 would be showing the wrong list with no sign of it.
*
* Pure except for the fetch; safe outside a Vue setup().
*/
import registry from './lists.json'
import { resolveAssetsBase } from '../utils/embedBase.js'
export const LIST_DEFAULT = registry.default
const byKey = new Map()
for (const l of registry.lists) {
byKey.set(l.id, l)
for (const a of l.aliases || []) byKey.set(String(a).toLowerCase(), l)
}
/** Every list the registry defines: { id, label, definition?, source, filter }. */
export function peopleGroupLists() { return registry.lists }
/** The registry entry for an id or alias, or null. */
export function listInfo(idOrAlias) {
return byKey.get(String(idOrAlias ?? '').toLowerCase().trim()) || null
}
/** Unwrap a ref/computed to its config object; parse a JSON string; pass plain objects through. */
function unwrapConfig(profileConfig) {
let cfg = profileConfig
if (cfg && typeof cfg === 'object' && 'value' in cfg && !('list' in cfg) && !('profile' in cfg)) cfg = cfg.value
if (typeof cfg === 'string') { try { cfg = JSON.parse(cfg) } catch { cfg = null } }
return cfg && typeof cfg === 'object' ? cfg : null
}
const warned = new Set()
/** The list this map should show, as a registry id. Always a known id. */
export function resolvePeopleGroupList(profileConfig) {
const cfg = unwrapConfig(profileConfig)
const fromConfig = cfg?.list
const fromUrl = typeof location !== 'undefined' ? new URLSearchParams(location.search).get('list') : null
const fromWindow = typeof window !== 'undefined' ? window.MAP_LIST : undefined
const raw = fromConfig || fromUrl || fromWindow || LIST_DEFAULT
const hit = listInfo(raw)
if (hit) return hit.id
if (!warned.has(raw)) {
warned.add(raw)
console.warn(`[list] unknown people-group list "${raw}", showing "${LIST_DEFAULT}". Known: ${registry.lists.map((l) => l.id).join(', ')}`)
}
return LIST_DEFAULT
}
const membership = new Map()
/** The set of DOXA slugs in a list (fetched once per URL). Rejects if it cannot be loaded. */
export function loadListMembership(id, profileConfig) {
const url = `${resolveAssetsBase(unwrapConfig(profileConfig))}/people-groups/lists/${id}.json`
if (!membership.has(url)) {
membership.set(url, fetch(url)
.then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json() })
.then((d) => new Set(Array.isArray(d?.slugs) ? d.slugs : []))
.catch((e) => { membership.delete(url); throw new Error(`list "${id}" membership ${url}: ${e.message}`) }))
}
return membership.get(url)
}
/**
* Keep the rows that belong to the resolved list. The default list returns `rows` itself, untouched
* and without a fetch. `key` reads a row's slug (default: row.slug).
*/
export async function applyPeopleGroupList(rows, profileConfig, { key = (r) => r?.slug } = {}) {
const id = resolvePeopleGroupList(profileConfig)
if (id === LIST_DEFAULT || !Array.isArray(rows)) return rows
try {
const slugs = await loadListMembership(id, profileConfig)
return rows.filter((r) => slugs.has(key(r)))
} catch (e) {
console.error(`[list] ${e.message}. Showing no people groups rather than the wrong list.`)
return []
}
}