import { clientLogin, getLoggedInUserName, getPeople, getRelatives as getRelativesApi, Person, } from 'wikitree-js'; import {TopolaError} from '../util/error'; const WIKITREE_APP_ID = 'topola-viewer'; /** Prefix for IDs of private individuals. */ export const PRIVATE_ID_PREFIX = '~Private'; const ANCESTORS_GENERATION_LIMIT = 5; const DESCENDANT_GENERATION_LIMIT = 5; function getSessionStorageItem(key: string): string | null { try { return sessionStorage.getItem(key); } catch (e) { console.warn('Failed to load data from session storage: ' + e); } return null; } function setSessionStorageItem(key: string, value: string) { try { return sessionStorage.setItem(key, value); } catch (e) { console.warn('Failed to store data in session storage: ' + e); } } function getCacheItem(key: string): T | null { const cachedData = getSessionStorageItem(key); if (cachedData) { try { return JSON.parse(cachedData) as T; } catch (e) { console.warn(`Failed to parse cached data for key ${key}: ${e}`); } } return null; } function setCacheItem(key: string, value: T): void { setSessionStorageItem(key, JSON.stringify(value)); } function getApiOptions(handleCors: boolean) { return Object.assign( {appId: WIKITREE_APP_ID}, handleCors ? { apiUrl: 'https://topolaproxy.bieda.it/https://api.wikitree.com/api.php', } : {}, ); } async function getAncestors( key: string, handleCors: boolean, ): Promise { const cacheKey = `wikitree:ancestors:${key}`; const cachedData = getCacheItem(cacheKey); if (cachedData) { return cachedData; } const result = await getPeople( [key], {ancestors: ANCESTORS_GENERATION_LIMIT}, getApiOptions(handleCors), ); setCacheItem(cacheKey, result); return result; } async function getRelatives( keys: string[], handleCors: boolean, ): Promise { const result: Person[] = []; const keysToFetch: string[] = []; // Deduplicate keys const uniqueKeys: string[] = []; keys.forEach((key) => { if (!uniqueKeys.includes(key)) { uniqueKeys.push(key); } }); uniqueKeys.forEach((key) => { const cachedData = getCacheItem(`wikitree:relatives:${key}`); if (cachedData) { result.push(cachedData); } else { keysToFetch.push(key); } }); if (keysToFetch.length === 0) { return result; } const response = await getRelativesApi( keysToFetch, {getChildren: true, getSpouses: true}, getApiOptions(handleCors), ); if (!response) { const id = keysToFetch[0]; throw new TopolaError( 'WIKITREE_PROFILE_NOT_FOUND', `WikiTree profile ${id} not found`, {id}, ); } response.forEach((person) => { setCacheItem(`wikitree:relatives:${person.Name}`, person); }); return result.concat(response); } async function logInIfNeeded( authcode: string | undefined, handleCors: boolean, ) { if (!handleCors && !getLoggedInUserName() && authcode) { const loginResult = await clientLogin(authcode, {appId: WIKITREE_APP_ID}); if (loginResult.result === 'Success') { sessionStorage.clear(); } } } async function getFirstPerson(key: string, handleCors: boolean) { const person = (await getRelatives([key], handleCors))[0]; if (!person?.Name) { const id = key; throw new TopolaError( 'WIKITREE_PROFILE_NOT_ACCESSIBLE', `WikiTree profile ${id} is not accessible. Try logging in.`, {id}, ); } return person; } function getSpouseKeys(person: Person) { return Object.values(person.Spouses || {}).map((s) => s.Name); } async function getAllAncestors(keys: string[], handleCors: boolean) { // Deduplicate input keys const uniqueKeys: string[] = []; keys.forEach((k) => { if (!uniqueKeys.includes(k)) { uniqueKeys.push(k); } }); const ancestors = await Promise.all( uniqueKeys.map((key) => getAncestors(key, handleCors)), ); const ancestorKeys = ancestors .flat() .map((person) => person.Name) .filter((key) => !!key); // Deduplicate ancestorKeys as well before calling getRelatives const uniqueAncestorKeys: string[] = []; ancestorKeys.forEach((k) => { if (!uniqueAncestorKeys.includes(k)) { uniqueAncestorKeys.push(k); } }); const ancestorDetails = await getRelatives(uniqueAncestorKeys, handleCors); // Map from person id to father id if the father profile is private. const privateFathers: Map = new Map(); // Map from person id to mother id if the mother profile is private. const privateMothers: Map = new Map(); // Adjust private individual ids so that there are no collisions in the case // that ancestors were collected for more than one person. ancestors.forEach((ancestorList, index) => { const offset = 1000 * index; // Adjust ids by offset. ancestorList.forEach((person) => { if (person.Id < 0) { person.Id -= offset; person.Name = `${PRIVATE_ID_PREFIX}${person.Id}`; } if (person.Father < 0) { person.Father -= offset; privateFathers.set(person.Id, person.Father); } if (person.Mother < 0) { person.Mother -= offset; privateMothers.set(person.Id, person.Mother); } }); }); // Set the Father and Mother fields again because getRelatives doesn't return // private parents. ancestorDetails.forEach((person) => { const privateFather = privateFathers.get(person.Id); if (privateFather) { person.Father = privateFather; } const privateMother = privateMothers.get(person.Id); if (privateMother) { person.Mother = privateMother; } }); // Collect private individuals. const privateAncestors = ancestors.flat().filter((person) => person.Id < 0); return ancestorDetails.concat(privateAncestors); } async function getAllDescendants(key: string, handleCors: boolean) { const everyone: Person[] = []; // Fetch descendants recursively. let toFetch = [key]; let generation = 0; while (toFetch.length > 0 && generation <= DESCENDANT_GENERATION_LIMIT) { const people = await getRelatives(toFetch, handleCors); everyone.push(...people); const allSpouses = people.flatMap((person) => Object.values(person.Spouses || {}), ); everyone.push(...allSpouses); // Fetch all children. const nextToFetch = people.flatMap((person) => Object.values(person.Children || {}).map((c) => c.Name), ); // Deduplicate nextToFetch and filter out already fetched people const uniqueNextToFetch: string[] = []; nextToFetch.forEach((name) => { if ( !uniqueNextToFetch.includes(name) && !everyone.some((p) => p.Name === name) ) { uniqueNextToFetch.push(name); } }); toFetch = uniqueNextToFetch; generation++; } return everyone; } /** * Loads data from the WikiTree API for a given person key. * * @param key The WikiTree profile ID to load. * @param authcode Optional authentication code. * @returns A unique list of WikiTree `Person` records. */ export async function loadData( key: string, authcode?: string, ): Promise { // Work around CORS if not in apps.wikitree.com domain. const handleCors = window.location.hostname !== 'apps.wikitree.com'; await logInIfNeeded(authcode, handleCors); const firstPerson = await getFirstPerson(key, handleCors); const spouseKeys = getSpouseKeys(firstPerson); // Fetch the ancestors of the input person and ancestors of his/her spouses. const allAncestors = getAllAncestors([key].concat(spouseKeys), handleCors); // Fetch descendants and their spouses. const allDescendants = getAllDescendants(key, handleCors); const everyone: Person[] = [ ...(await allAncestors), ...(await allDescendants), ]; // Make sure the list contains unique elements. return Array.from( new Map(everyone.map((person) => [person.Id, person])).values(), ); }