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 { Changes, GuildMetadata, Resource } from "../../data-types"; import CombinedGuild from "../../guild-combined"; import React, { DependencyList, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AutoVerifierChangesType } from "../../auto-verifier"; import { Conflictable, Connectable } from "../../guild-types"; import { EventEmitter } from 'tsee'; import { IDQuery } from '../../auto-verifier-with-args'; import { Token } from '../../data-types'; export type SingleSubscriptionEvents = { 'fetch': () => void; 'updated': () => void; 'conflict': () => void; 'fetch-error': () => void; } export type MultipleSubscriptionEvents = { 'fetch': () => void; 'fetch-error': () => void; 'new': (newValues: T[]) => void; 'updated': (updatedValues: T[]) => void; 'removed': (removedValues: T[]) => void; 'conflict': (changes: Changes) => void; } interface EffectParams { guild: CombinedGuild; onFetch: (value: T | null) => void; onFetchError: (e: unknown) => void; bindEventsFunc: () => void; unbindEventsFunc: () => void; } type Arguments = T extends (...args: infer A) => unknown ? A : never; interface SingleEventMappingParams { updatedEventName: UE; updatedEventArgsMap: (...args: Arguments) => T; conflictEventName: CE; conflictEventArgsMap: (...args: Arguments) => T; } interface MultipleEventMappingParams< T, NE extends keyof Connectable, UE extends keyof Connectable, RE extends keyof Connectable, CE extends keyof Conflictable > { newEventName: NE; newEventArgsMap: (...args: Arguments) => T[]; // list of new elements updatedEventName: UE; updatedEventArgsMap: (...args: Arguments) => T[]; // list of updated elements removedEventName: RE; removedEventArgsMap: (...args: Arguments) => T[]; // list of removed elements conflictEventName: CE; conflictEventArgsMap: (...args: Arguments) => Changes; sortFunc: (a: T, b: T) => number; } export default class GuildSubscriptions { private static useGuildSubscriptionEffect( isMountedRef: React.MutableRefObject, subscriptionParams: EffectParams, fetchFunc: (() => Promise) | (() => Promise) ) { const { guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc } = subscriptionParams; const fetchManagerFunc = useMemo(() => { return async () => { if (!isMountedRef.current) return; try { const value = await fetchFunc(); if (!isMountedRef.current) return; onFetch(value); } catch (e: unknown) { LOG.error('error fetching for subscription', e); if (!isMountedRef.current) return; onFetchError(e); } } }, [ fetchFunc ]); useEffect(() => { isMountedRef.current = true; // Bind guild events to make sure we have the most up to date information guild.on('connect', fetchManagerFunc); bindEventsFunc(); // Fetch the data once fetchManagerFunc(); return () => { isMountedRef.current = false; // Unbind the events so that we don't have any memory leaks guild.off('connect', fetchManagerFunc); unbindEventsFunc(); } }, [ fetchManagerFunc ]); } private static useSingleGuildSubscription( guild: CombinedGuild, eventMappingParams: SingleEventMappingParams, fetchFunc: (() => Promise) | (() => Promise) ): [value: T | null, fetchError: unknown | null, events: EventEmitter] { const { updatedEventName, updatedEventArgsMap, conflictEventName, conflictEventArgsMap } = eventMappingParams; const isMountedRef = useRef(false); const [ fetchError, setFetchError ] = useState(null); const [ value, setValue ] = useState(null); const events = useMemo(() => new EventEmitter(), []); const onFetch = useCallback((fetchValue: T | null) => { setValue(fetchValue); setFetchError(null); events.emit('fetch'); }, []); const onFetchError = useCallback((e: unknown) => { setFetchError(e); setValue(null); events.emit('fetch-error'); }, []); const onUpdated = useCallback((updateValue: T) => { setValue(updateValue); events.emit('updated'); }, []); const onConflict = useCallback((conflictValue: T) => { setValue(conflictValue); events.emit('conflict'); }, []); // I think the typed EventEmitter class isn't ready for this level of insane type safety // otherwise, I may have done this wrong. Forcing it to work with these calls const boundUpdateFunc = useCallback((...args: Arguments): void => { if (!isMountedRef.current) return; const value = updatedEventArgsMap(...args); onUpdated(value); }, []) as (Connectable & Conflictable)[UE]; const boundConflictFunc = useCallback((...args: Arguments): void => { if (!isMountedRef.current) return; const value = conflictEventArgsMap(...args); onConflict(value); }, []) as (Connectable & Conflictable)[CE]; const bindEventsFunc = useCallback(() => { guild.on(updatedEventName, boundUpdateFunc); guild.on(conflictEventName, boundConflictFunc); }, []); const unbindEventsFunc = useCallback(() => { guild.off(updatedEventName, boundUpdateFunc); guild.off(conflictEventName, boundConflictFunc); }, []); GuildSubscriptions.useGuildSubscriptionEffect(isMountedRef, { guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc }, fetchFunc); return [ value, fetchError, events ]; } private static useMultipleGuildSubscription< T extends { id: string }, NE extends keyof Connectable, UE extends keyof Connectable, RE extends keyof Connectable, CE extends keyof Conflictable >( guild: CombinedGuild, eventMappingParams: MultipleEventMappingParams, fetchFunc: (() => Promise) | (() => Promise) ): [value: T[] | null, fetchError: unknown | null, events: EventEmitter>] { const { newEventName, newEventArgsMap, updatedEventName, updatedEventArgsMap, removedEventName, removedEventArgsMap, conflictEventName, conflictEventArgsMap, sortFunc } = eventMappingParams; const isMountedRef = useRef(false); const [ fetchError, setFetchError ] = useState(null); const [ value, setValue ] = useState(null); const events = useMemo(() => new EventEmitter>(), []); const onFetch = useCallback((fetchValue: T[] | null) => { setValue(fetchValue); setFetchError(null); events.emit('fetch'); }, []); const onFetchError = useCallback((e: unknown) => { setFetchError(e); setValue(null); events.emit('fetch-error'); }, []); const onNew = useCallback((newElements: T[]) => { setValue(currentValue => { if (currentValue === null) return null; return currentValue.concat(newElements).sort(sortFunc); }) events.emit('new', newElements); }, []); const onUpdated = useCallback((updatedElements: T[]) => { setValue(currentValue => { if (currentValue === null) return null; return currentValue.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc); }); events.emit('updated', updatedElements); }, []); const onRemoved = useCallback((removedElements: T[]) => { setValue(currentValue => { if (currentValue === null) return null; const deletedIds = new Set(removedElements.map(deletedElement => deletedElement.id)); return currentValue.filter(element => !deletedIds.has(element.id)).sort(sortFunc); }); events.emit('removed', removedElements); }, []); const onConflict = useCallback((changes: Changes) => { setValue(currentValue => { if (currentValue === null) return null; const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id)); return currentValue .concat(changes.added) .map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element) .filter(element => !deletedIds.has(element.id)) .sort(sortFunc); }); events.emit('conflict', changes); }, []); // I think the typed EventEmitter class isn't ready for this level of insane type safety // otherwise, I may have done this wrong. Forcing it to work with these calls const boundNewFunc = useCallback((...args: Arguments): void => { if (!isMountedRef.current) return; onNew(newEventArgsMap(...args)); }, []) as (Connectable & Conflictable)[NE]; const boundUpdateFunc = useCallback((...args: Arguments): void => { if (!isMountedRef.current) return; onUpdated(updatedEventArgsMap(...args)); }, []) as (Connectable & Conflictable)[UE]; const boundRemovedFunc = useCallback((...args: Arguments): void => { if (!isMountedRef.current) return; onRemoved(removedEventArgsMap(...args)); }, []) as (Connectable & Conflictable)[RE]; const boundConflictFunc = useCallback((...args: Arguments): void => { if (!isMountedRef.current) return; onConflict(conflictEventArgsMap(...args)); }, []) as (Connectable & Conflictable)[CE]; const bindEventsFunc = useCallback(() => { guild.on(newEventName, boundNewFunc); guild.on(updatedEventName, boundUpdateFunc); guild.on(removedEventName, boundRemovedFunc); guild.on(conflictEventName, boundConflictFunc); }, []); const unbindEventsFunc = useCallback(() => { guild.off(newEventName, boundNewFunc); guild.off(updatedEventName, boundUpdateFunc); guild.off(removedEventName, boundRemovedFunc); guild.off(conflictEventName, boundConflictFunc); }, []); GuildSubscriptions.useGuildSubscriptionEffect(isMountedRef, { guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc }, fetchFunc); return [ value, fetchError, events ]; } static useGuildMetadataSubscription(guild: CombinedGuild) { const fetchMetadataFunc = useCallback(async () => { //LOG.silly('fetching metadata for subscription'); return await guild.fetchMetadata(); }, [ guild ]); return GuildSubscriptions.useSingleGuildSubscription(guild, { updatedEventName: 'update-metadata', updatedEventArgsMap: (guildMeta: GuildMetadata) => guildMeta, conflictEventName: 'conflict-metadata', conflictEventArgsMap: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => newGuildMeta }, fetchMetadataFunc); } static useResourceSubscription(guild: CombinedGuild, resourceId: string | null) { const fetchResourceFunc = useCallback(async () => { //LOG.silly('fetching resource for subscription (resourceId: ' + resourceId + ')'); if (resourceId === null) return null; return await guild.fetchResource(resourceId); }, [ guild, resourceId ]); return GuildSubscriptions.useSingleGuildSubscription(guild, { updatedEventName: 'update-resource', updatedEventArgsMap: (resource: Resource) => resource, conflictEventName: 'conflict-resource', conflictEventArgsMap: (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => newResource }, fetchResourceFunc); } static useTokensSubscription(guild: CombinedGuild) { const fetchTokensFunc = useCallback(async () => { //LOG.silly('fetching tokens for subscription'); return await guild.fetchTokens(); }, [ guild ]); return GuildSubscriptions.useMultipleGuildSubscription(guild, { newEventName: 'new-tokens', newEventArgsMap: (tokens: Token[]) => tokens, updatedEventName: 'update-tokens', updatedEventArgsMap: (updatedTokens: Token[]) => updatedTokens, removedEventName: 'remove-tokens', removedEventArgsMap: (removedTokens: Token[]) => removedTokens, conflictEventName: 'conflict-tokens', conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes) => changes, sortFunc: Token.sortFurthestExpiresFirst }, fetchTokensFunc); } }