diff --git a/src/client/webapp/elements/overlays/overlay-add-guild.tsx b/src/client/webapp/elements/overlays/overlay-add-guild.tsx index ba6b8df..58acdd1 100644 --- a/src/client/webapp/elements/overlays/overlay-add-guild.tsx +++ b/src/client/webapp/elements/overlays/overlay-add-guild.tsx @@ -81,9 +81,7 @@ const AddGuildOverlay: FC = (props: AddGuildOverlayProps) const [ avatarInputValid, setAvatarInputValid ] = useState(false); const [ exampleAvatarBuff, exampleAvatarBuffError ] = ReactHelper.useAsyncActionSubscription( - async () => { - return await fs.readFile(exampleAvatarPath); - }, + async () => await fs.readFile(exampleAvatarPath), null, [ exampleAvatarPath ] ); diff --git a/src/client/webapp/elements/require/guild-subscriptions.ts b/src/client/webapp/elements/require/guild-subscriptions.ts index 5b1233d..f301d4c 100644 --- a/src/client/webapp/elements/require/guild-subscriptions.ts +++ b/src/client/webapp/elements/require/guild-subscriptions.ts @@ -22,9 +22,9 @@ export type SubscriptionEvents = { interface EffectParams { guild: CombinedGuild; onFetch: (value: T | null) => void; - onUpdate: (value: T) => void; - onConflict: (value: T) => void; onFetchError: (e: unknown) => void; + bindEventsFunc: () => void; + unbindEventsFunc: () => void; } type Arguments = T extends (...args: infer A) => unknown ? A : never; @@ -34,67 +34,57 @@ interface EventMappingParams) => T; // should be same as the params list from Connectable conflictEventName: CE; conflictEventArgsMap: (...args: Arguments) => T; // should be the same as the params list from Conflictable - fetchDeps: DependencyList; } export default class GuildSubscriptions { - private static useGuildSubscriptionEffect( - subscriptionParams: EffectParams, eventMappingParams: EventMappingParams, fetchFunc: (() => Promise) | (() => Promise) + private static useGuildSubscriptionEffect( + isMountedRef: React.MutableRefObject, + subscriptionParams: EffectParams, + fetchFunc: (() => Promise) | (() => Promise), + fetchDeps: DependencyList ) { - const { guild, onFetch, onUpdate, onConflict, onFetchError } = subscriptionParams; - const { updateEventName, updateEventArgsMap, conflictEventName, conflictEventArgsMap, fetchDeps } = eventMappingParams; - - const isMounted = useRef(false); + const { guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc } = subscriptionParams; const fetchManagerFunc = useCallback(async () => { - if (!isMounted.current) return; + if (!isMountedRef.current) return; try { const value = await fetchFunc(); - if (!isMounted.current) return; + if (!isMountedRef.current) return; onFetch(value); } catch (e: unknown) { LOG.error('error fetching for subscription', e); - if (!isMounted.current) return; + if (!isMountedRef.current) return; onFetchError(e); } }, [ ...fetchDeps, fetchFunc ]); - const boundUpdateFunc = useCallback((...args: Arguments): void => { - if (!isMounted.current) return; - const value = updateEventArgsMap(...args); - onUpdate(value); - }, []) as (Connectable & Conflictable)[UE]; // I think the typed EventEmitter class isn't ready for this level of type safety - const boundConflictFunc = useCallback((...args: Arguments): void => { - if (!isMounted.current) return; - const value = conflictEventArgsMap(...args); // otherwise, I may have done this wrong. Using never to force it to work - onConflict(value); - }, []) as (Connectable & Conflictable)[CE]; - useEffect(() => { - isMounted.current = true; + isMountedRef.current = true; // Bind guild events to make sure we have the most up to date information guild.on('connect', fetchManagerFunc); - guild.on(updateEventName, boundUpdateFunc); - guild.on(conflictEventName, boundConflictFunc); + bindEventsFunc(); // Fetch the data once fetchManagerFunc(); return () => { - isMounted.current = false; + isMountedRef.current = false; // Unbind the events so that we don't have any memory leaks guild.off('connect', fetchManagerFunc); - guild.off(updateEventName, boundUpdateFunc); - guild.off(conflictEventName, boundConflictFunc); + unbindEventsFunc(); } }, [ fetchManagerFunc ]); } - private static useGuildSubscription( - guild: CombinedGuild, eventMappingParams: EventMappingParams, fetchFunc: (() => Promise) | (() => Promise) + private static useSingleGuildSubscription( + guild: CombinedGuild, eventMappingParams: EventMappingParams, fetchFunc: (() => Promise) | (() => Promise), fetchDeps: DependencyList ): [value: T | null, fetchError: unknown | null, events: EventEmitter] { + const { updateEventName, updateEventArgsMap, conflictEventName, conflictEventArgsMap } = eventMappingParams; + + const isMountedRef = useRef(false); + const [ fetchError, setFetchError ] = useState(null); const [ value, setValue ] = useState(null); @@ -122,37 +112,57 @@ export default class GuildSubscriptions { events.emit('conflict'); }, []); - GuildSubscriptions.useGuildSubscriptionEffect({ + // 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 = updateEventArgsMap(...args); + onUpdate(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(updateEventName, boundUpdateFunc); + guild.on(conflictEventName, boundConflictFunc); + }, []); + const unbindEventsFunc = useCallback(() => { + guild.off(updateEventName, boundUpdateFunc); + guild.off(conflictEventName, boundConflictFunc); + }, []); + + GuildSubscriptions.useGuildSubscriptionEffect(isMountedRef, { guild, onFetch, - onUpdate, - onConflict, - onFetchError - }, eventMappingParams, fetchFunc); + onFetchError, + bindEventsFunc, + unbindEventsFunc + }, fetchFunc, fetchDeps); return [ value, fetchError, events ]; } static useGuildMetadataSubscription(guild: CombinedGuild) { - return GuildSubscriptions.useGuildSubscription(guild, { + return GuildSubscriptions.useSingleGuildSubscription(guild, { updateEventName: 'update-metadata', updateEventArgsMap: (guildMeta: GuildMetadata) => guildMeta, conflictEventName: 'conflict-metadata', - conflictEventArgsMap: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => newGuildMeta, - fetchDeps: [ guild ] - }, async () => await guild.fetchMetadata()); + conflictEventArgsMap: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => newGuildMeta + }, async () => await guild.fetchMetadata(), [ guild ]); } static useResourceSubscription(guild: CombinedGuild, resourceId: string | null) { - return GuildSubscriptions.useGuildSubscription(guild, { + return GuildSubscriptions.useSingleGuildSubscription(guild, { updateEventName: 'update-resource', updateEventArgsMap: (resource: Resource) => resource, conflictEventName: 'conflict-resource', - conflictEventArgsMap: (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => newResource, - fetchDeps: [ guild, resourceId ] + conflictEventArgsMap: (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => newResource }, async () => { if (resourceId === null) return null; return await guild.fetchResource(resourceId); - }); + }, [ guild, resourceId ]); } }