cordis/archive/setup-guild-recoil.ts
2022-02-02 23:05:38 -06:00

212 lines
8.8 KiB
TypeScript

import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import { useCallback, useEffect, useRef } from "react";
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import { Changes, GuildMetadata, Member } from "../../data-types";
import GuildsManager from "../../guilds-manager";
import { guildsManagerState, guildsState, GuildWithErrorableValue, selectedGuildIdState, selectedGuildState, selectedGuildWithMembersState } from "./atoms";
import { useIsMountedRef } from "./react-helper";
import CombinedGuild from '../../guild-combined';
import { AutoVerifierChangesType } from '../../auto-verifier';
import * as uuid from 'uuid';
/** Manages the guildsManager to ensure that guildsState is up to date with a list of guilds
* and that the guilds attempt to fetch their corresponding guildMetas
* This class it a bit more complicated since it has to fetch/listen to guild metadata changes on many guilds at once */
function useManagedGuildsManager(guildsManager: GuildsManager) {
const isMounted = useIsMountedRef();
const metadataInProgress = useRef<Set<number>>(new Set<number>());
const [ guildsWithMeta, setGuildsWithMeta ] = useRecoilState(guildsState);
// Update the guild list
// If new guilds are added, add them with null metadata
const onChange = useCallback(() => {
const guilds = guildsManager.guilds;
setGuildsWithMeta((prevGuildsWithMeta) => {
if (prevGuildsWithMeta === null) {
return guilds.map(guild => ({ guild, value: undefined, hasValueError: false, valueError: undefined }));
}
const newGuilds = guilds.filter(guild => !prevGuildsWithMeta.find(guildWithMeta => guildWithMeta.guild.id === guild.id));
const newGuildsWithMeta = prevGuildsWithMeta
.filter(guildWithMeta => guilds.find(guild => guildWithMeta.guild.id === guild.id)) // Remove old guilds
.concat(newGuilds.map(guild => ({ guild, value: undefined, hasValueError: false, valueError: undefined }))); // Add new guilds
return newGuildsWithMeta;
});
}, [ guildsManager, setGuildsWithMeta ]);
// Listen for changes to the guild manager
useEffect(() => {
// Set initial guilds
onChange();
// Listen for updates
guildsManager.on('update-guilds', onChange);
return () => {
guildsManager.off('update-guilds', onChange);
}
}, [ guildsManager, onChange ]);
const setGuildWithMeta = useCallback((guildId: number, newGuildWithMeta: GuildWithErrorableValue<GuildMetadata>) => {
setGuildsWithMeta(prevGuildsWithMeta => {
if (!prevGuildsWithMeta) return null;
return prevGuildsWithMeta.map(guildWithMeta => guildWithMeta.guild.id === guildId ? newGuildWithMeta : guildWithMeta);
});
}, [ setGuildsWithMeta ]);
useEffect(() => {
if (guildsWithMeta === null) return;
// Load guild metadatas
// TODO: How do we add a retry for guild metadata? It's used in a lot of places!
for (const { guild, value, hasValueError } of guildsWithMeta) {
// Only fetch if the value has not been fetched yet and we are not currently fetching it.
if (!metadataInProgress.current.has(guild.id) && value === undefined && hasValueError === false) {
(async () => {
metadataInProgress.current.add(guild.id);
try {
const guildMeta = await guild.fetchMetadata();
if (!isMounted.current) return;
LOG.debug('loaded metadata for g#' + guild.id);
setGuildWithMeta(guild.id, { guild, value: guildMeta, hasValueError: false, valueError: undefined });
} catch (e: unknown) {
LOG.error(`error fetching metadata for g#${guild.id}`, e);
if (!isMounted.current) return;
setGuildWithMeta(guild.id, { guild, value: undefined, hasValueError: true, valueError: e });
}
metadataInProgress.current.delete(guild.id);
})();
}
}
}, [ guildsWithMeta, setGuildWithMeta ]);
// Listen for changes to the guild metadata
useEffect(() => {
if (guildsWithMeta === null) return;
// Listen for changes to metadata
function handleUpdateOrConflict(guild: CombinedGuild, guildMeta: GuildMetadata) {
setGuildWithMeta(guild.id, { guild, value: guildMeta, hasValueError: false, valueError: undefined });
}
const callbacks = new Map<CombinedGuild, {
updateCallback: (guildMeta: GuildMetadata) => void,
conflictCallback: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, guildMeta: GuildMetadata) => void
}>();
for (const { guild } of guildsWithMeta) {
const updateCallback = (guildMeta: GuildMetadata) => handleUpdateOrConflict(guild, guildMeta);
const conflictCallback = (_changesType: AutoVerifierChangesType, _oldGuildMeta: GuildMetadata, guildMeta: GuildMetadata) => handleUpdateOrConflict(guild, guildMeta);
guild.on('update-metadata', updateCallback);
guild.on('conflict-metadata', conflictCallback);
callbacks.set(guild, { updateCallback, conflictCallback });
}
return () => {
for (const { guild } of guildsWithMeta) {
const { updateCallback, conflictCallback } = callbacks.get(guild) as {
updateCallback: (guildMeta: GuildMetadata) => void,
conflictCallback: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, guildMeta: GuildMetadata) => void
};
guild.off('update-metadata', updateCallback);
guild.off('conflict-metadata', conflictCallback);
}
}
}, [ guildsWithMeta ]);
//useEffect(() => {
// LOG.debug(`guildsWithMeta: `, { guildsWithMeta });
//}, [ guildsWithMeta ]);
}
function applyChanges<T extends { id: string }>(src: T[], changes: Changes<T>) {
const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id))
return src.concat(changes.added)
.map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element)
.filter(element => !deletedIds.has(element.id));
}
export function useRecoilSelectedGuildMembers(): void {
const isMounted = useIsMountedRef();
const guild = useRecoilValue(selectedGuildState);
const setGuildWithMembers = useSetRecoilState(selectedGuildWithMembersState);
const lastFetchId = useRef<string | null>(null);
// TODO: Test this custom effect out
// Fetch initial value
useEffect(() => {
if (guild === null) return;
(async () => {
// TODO: Set value to null if the fetch takes > 300 ms
const fetchId = uuid.v4();
try {
lastFetchId.current = fetchId;
const members = await guild.fetchMembers();
if (!isMounted.current) return;
if (lastFetchId.current !== fetchId) return; // Another fetch happened while we were fetching
setGuildWithMembers({ guild, value: members, hasValueError: false, valueError: undefined });
} catch (e: unknown) {
LOG.error('unable to perform members fetch', e);
if (!isMounted.current) return;
if (lastFetchId.current !== fetchId) return; // Another fetch happened while we were fetching
setGuildWithMembers({ guild, value: undefined, hasValueError: true, valueError: e });
}
})();
}, [ guild, isMounted ]);
// Listen for Changes
useEffect(() => {
if (guild === null) return;
const updateCallback = (updatedMembers: Member[]) => setGuildWithMembers({ guild, value: updatedMembers, hasValueError: false, valueError: undefined });
const conflictCallback = (_changesType: AutoVerifierChangesType, changes: Changes<Member>) => {
setGuildWithMembers(prevGuildWithMembers => {
if (prevGuildWithMembers === null) return null;
if (prevGuildWithMembers.valueError) return prevGuildWithMembers;
return {
guild,
value: applyChanges(prevGuildWithMembers.value ?? [], changes),
hasValueError: false,
valueError: undefined
};
});
};
guild.on('update-members', updateCallback);
guild.on('conflict-members', conflictCallback);
return () => {
guild.off('update-members', updateCallback);
guild.off('conflict-members', conflictCallback);
}
}, [ guild ]);
}
// Entrypoint for recoil setup with guilds
export function useGuildsManagerWithRecoil(guildsManager: GuildsManager): void {
const setGuildsManager = useSetRecoilState(guildsManagerState);
const guilds = useRecoilValue(guildsState);
const [ selectedGuildId, setSelectedGuildId ] = useRecoilState(selectedGuildIdState);
// Set the guilds manager atom
useEffect(() => { setGuildsManager(guildsManager); }, [ guildsManager, setGuildsManager ]);
// Manage the guilds within the manager (adds them to the guilds atom and automatically loads and updates their metadata)
useManagedGuildsManager(guildsManager);
// Make sure that the first guild is set to the active guild once we get some guilds
useEffect(() => {
if (guilds && guilds.length > 0 && selectedGuildId === null) {
setSelectedGuildId((guilds[0] as GuildWithErrorableValue<GuildMetadata>).guild.id);
}
}, [ guilds, selectedGuildId ]);
// Automatically load members for the selected guild
useRecoilSelectedGuildMembers();
}