From 54ad823ebee0944d56997a56f8b7cb59a840a80d Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Sat, 5 Feb 2022 09:01:50 -0600 Subject: [PATCH] function-scoping functions for less globalism in atoms-2 --- .../displays/display-guild-invites.tsx | 18 +- src/client/webapp/elements/require/atoms-2.ts | 186 +++++++++++------- .../elements/require/guild-subscriptions.ts | 2 +- 3 files changed, 117 insertions(+), 89 deletions(-) diff --git a/src/client/webapp/elements/displays/display-guild-invites.tsx b/src/client/webapp/elements/displays/display-guild-invites.tsx index 8ea1c76..7b0824a 100644 --- a/src/client/webapp/elements/displays/display-guild-invites.tsx +++ b/src/client/webapp/elements/displays/display-guild-invites.tsx @@ -12,11 +12,10 @@ import { Duration } from 'moment'; import moment from 'moment'; import DropdownInput from '../components/input-dropdown'; import Button from '../components/button'; -import { useTokensSubscription } from '../require/guild-subscriptions'; import TokenRow from '../components/token-row'; import CombinedGuild from '../../guild-combined'; import { useRecoilValue } from 'recoil'; -import { guildMetaState, guildResourceSoftImgSrcState, isLoaded, useRecoilValueSoftImgSrc } from '../require/atoms-2'; +import { guildMetaState, guildResourceSoftImgSrcState, guildTokensState, isFailed, isLoaded, useRecoilValueSoftImgSrc } from '../require/atoms-2'; export interface GuildInvitesDisplayProps { guild: CombinedGuild; @@ -25,12 +24,10 @@ const GuildInvitesDisplay: FC = (props: GuildInvitesDi const { guild } = props; const guildMeta = useRecoilValue(guildMetaState(guild.id)); + const tokens = useRecoilValue(guildTokensState(guild.id)); const url = 'https://localhost:3030'; // TODO: this will likely be a dropdown list at some point - // TODO: Recoilize tokens :) - const [ _fetchRetryCallable, tokensResult, tokensError ] = useTokensSubscription(guild); - const [ expiresFromNow, setExpiresFromNow ] = useState(moment.duration(1, 'day')); const [ expiresFromNowText, setExpiresFromNowText ] = useState('1 day'); @@ -66,13 +63,10 @@ const GuildInvitesDisplay: FC = (props: GuildInvitesDi }, [ createTokenFailMessage ]); const tokenElements = useMemo(() => { - if (tokensError) { - // TODO: Try Again - return
Unable to load tokens
; - } - // TODO: Try again? - return tokensResult?.value?.map((token: Token) => ); - }, [ url, guild, tokensResult, tokensError ]); + if (isFailed(tokens)) return
Unable to load tokens
; // TODO: Try Again + if (!isLoaded(tokens)) return null; // TODO: Pending indicator + return tokens.value.map((token: Token) => ); + }, [ url, guild, tokens ]); return ( ( return fetchValueFunc; } -// Creates an event handler that directly applies the result of the eventArgsMap as a loadedValue into self -function createDirectMappedEventHandler( - getPromise: (recoilValue: RecoilValue) => Promise, - node: RecoilValue>, - setSelf: (loadableValue: LoadableValue) => void, - fetchValueFunc: () => Promise, - eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined, - condition: (value: T) => boolean -): (Connectable & Conflictable)[XE] { - return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { - (async () => { - const selfState = await getPromise(node); - if (isLoaded(selfState)) { - const value = eventArgsMap(...args); - if (condition(value)) { - setSelf(createLoadedValue(value, fetchValueFunc)); - } - } - })(); - }) as (Connectable & Conflictable)[XE]; -} - function applyNew(value: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] { return value.concat(newElements).sort(sortFunc); } @@ -170,30 +148,6 @@ function applyRemoved(value: T[], removedElements: T[] return value.filter(element => !removedIds.has(element.id)); } -// Useful for new-xxx, update-xxx, remove-xxx list events -function createListConnectableMappedEventHandler( - getPromise: (recoilValue: RecoilValue) => Promise, - node: RecoilValue>, - setSelf: (loadableValue: LoadableValue) => void, - fetchValueFunc: () => Promise, - eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined, - sortFunc: (a: T, b: T) => number, - applyFunc: (value: T[], eventArgsResult: T[], sortFunc: (a: T, b: T) => number) => T[], -): (Connectable & Conflictable)[XE] { - // 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 - return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { - (async () => { - const selfState = await getPromise(node); - if (isLoaded(selfState)) { - const eventArgsResult = eventArgsMap(...args); - const value = applyFunc(selfState.value, eventArgsResult, sortFunc); - setSelf(createLoadedValue(value, fetchValueFunc)); - } - })(); - }) as (Connectable & Conflictable)[XE]; -} - function applyChanges(value: T[], changes: Changes, sortFunc: (a: T, b: T) => number): T[] { const removedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id)); return value @@ -203,28 +157,6 @@ function applyChanges(value: T[], changes: Changes, .sort(sortFunc); } -// Useful for conflict-xxx list events -function createListConflictableMappedEventHandler( - getPromise: (recoilValue: RecoilValue) => Promise, - node: RecoilValue>, - setSelf: (loadableValue: LoadableValue) => void, - fetchValueFunc: () => Promise, - eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes, - sortFunc: (a: T, b: T) => number, - applyFunc: (value: T[], eventArgsResult: Changes, sortFunc: (a: T, b: T) => number) => T[], -): (Connectable & Conflictable)[XE] { - return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { - (async () => { - const selfState = await getPromise(node); - if (isLoaded(selfState)) { - const eventArgsResult = eventArgsMap(...args); - const value = applyFunc(selfState.value, eventArgsResult, sortFunc); - setSelf(createLoadedValue(value, fetchValueFunc)); - } - })(); - }) as (Connectable & Conflictable)[XE]; -} - interface SingleEventMappingParams { updatedEventName: UE; updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined; @@ -253,13 +185,31 @@ function listenToSingle< let closed = false; (async () => { guild = await getPromise(guildState(guildId)); - if (guild === null) return; + if (guild === null) return; // TODO: This would put the atom in an infinite loading state. Look into useCallback for a potential way to prevent this... if (closed) return; // Make sure not to bind events if this closed while we were fetching the guild state + // Creates an event handler that directly applies the result of the eventArgsMap as a loadedValue into self + function createConnectableHandler( + eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined, + condition: (value: T) => boolean + ): (Connectable & Conflictable)[XE] { + return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { + (async () => { + const selfState = await getPromise(node); + if (isLoaded(selfState)) { + const value = eventArgsMap(...args); + if (condition(value)) { + setSelf(createLoadedValue(value, fetchValueFunc)); + } + } + })(); + }) as (Connectable & Conflictable)[XE]; + } + // 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 - onUpdateFunc = createDirectMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.updatedEventArgsMap, eventMapping.updatedEventCondition ?? (() => true)); - onConflictFunc = createDirectMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.conflictEventArgsMap, eventMapping.conflictEventCondition ?? (() => true)); + onUpdateFunc = createConnectableHandler(eventMapping.updatedEventArgsMap, eventMapping.updatedEventCondition ?? (() => true)); + onConflictFunc = createConnectableHandler(eventMapping.conflictEventArgsMap, eventMapping.conflictEventCondition ?? (() => true)); guild.on(eventMapping.updatedEventName, onUpdateFunc); guild.on(eventMapping.conflictEventName, onConflictFunc); })(); @@ -314,11 +264,49 @@ function listenToMultiple< if (guild === null) return; if (closed) return; // Make sure not to bind events if this closed while we were fetching the guild state - onNewFunc = createListConnectableMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.newEventArgsMap, sortFunc, applyNew); - onUpdateFunc = createListConnectableMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.updatedEventArgsMap, sortFunc, applyUpdated); - onRemoveFunc = createListConnectableMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.updatedEventArgsMap, sortFunc, applyRemoved); - onConflictFunc = createListConflictableMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.conflictEventArgsMap, sortFunc, applyChanges); + // Useful for new-xxx, update-xxx, remove-xxx list events + function createConnectableHandler( + eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined, + applyFunc: (value: T[], eventArgsResult: T[], sortFunc: (a: T, b: T) => number) => T[], + ): (Connectable & Conflictable)[XE] { + // 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 + return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { + (async () => { + const selfState = await getPromise(node); + if (isLoaded(selfState)) { + const eventArgsResult = eventArgsMap(...args); + const value = applyFunc(selfState.value, eventArgsResult, sortFunc); + setSelf(createLoadedValue(value, fetchValueFunc)); + } + })(); + }) as (Connectable & Conflictable)[XE]; + } + + // Useful for conflict-xxx list events + function createConflictableHandler( + eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes, + applyFunc: (value: T[], eventArgsResult: Changes, sortFunc: (a: T, b: T) => number) => T[], + ): (Connectable & Conflictable)[XE] { + return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { + (async () => { + const selfState = await getPromise(node); + if (isLoaded(selfState)) { + const eventArgsResult = eventArgsMap(...args); + const value = applyFunc(selfState.value, eventArgsResult, sortFunc); + setSelf(createLoadedValue(value, fetchValueFunc)); + } + })(); + }) as (Connectable & Conflictable)[XE]; + } + + onNewFunc = createConnectableHandler(eventMapping.newEventArgsMap, applyNew); + onUpdateFunc = createConnectableHandler(eventMapping.updatedEventArgsMap, applyUpdated); + onRemoveFunc = createConnectableHandler(eventMapping.removedEventArgsMap, applyRemoved); + onConflictFunc = createConflictableHandler(eventMapping.conflictEventArgsMap, applyChanges); + guild.on(eventMapping.newEventName, onNewFunc); guild.on(eventMapping.updatedEventName, onUpdateFunc); + guild.on(eventMapping.removedEventName, onRemoveFunc); guild.on(eventMapping.conflictEventName, onConflictFunc); })(); const cleanup = () => { @@ -331,6 +319,26 @@ function listenToMultiple< return cleanup; } +// function listenToMultipleScrolling< +// T extends { id: string }, +// NE extends keyof Connectable, // New Event +// UE extends keyof Connectable, // Update Event +// RE extends keyof Connectable, // Remove Event +// CE extends keyof Conflictable // Conflict Event +// >( +// getPromise: (recoilValue: RecoilValue) => Promise, +// guildId: number, +// node: RecoilValue>, +// setSelf: (loadableValue: LoadableValue) => void, +// fetchValueFunc: () => Promise, +// fetchAboveFunc: (reference: T) => Promise, +// fetchBelowFunc: (reference: T) => Promise, +// sortFunc: (a: T, b: T) => number, +// eventMapping: MultipleEventMappingParams +// ) { +// // TODO +// } + function singleGuildSubscriptionEffect< T, // e.g. GuildMetadata UE extends keyof Connectable, // Update Event @@ -543,6 +551,28 @@ const guildActiveChannelState = selectorFamily, number>({ dangerouslyAllowMutability: true }); +export const guildTokensState = atomFamily, number>({ + key: 'guildTokensState', + default: DEF_UNLOADED_VALUE, + effects_UNSTABLE: (guildId: number) => [ + multipleGuildSubscriptionEffect( + guildId, + async (guild: CombinedGuild) => await guild.fetchTokens(), + Token.sortRecentCreatedFirst, + { + newEventName: 'new-tokens', + newEventArgsMap: (newTokens: Token[]) => newTokens, + updatedEventName: 'update-tokens', + updatedEventArgsMap: (updatedTokens: Token[]) => updatedTokens, + removedEventName: 'remove-tokens', + removedEventArgsMap: (removedTokens: Token[]) => removedTokens, + conflictEventName: 'conflict-tokens', + conflictEventArgsMap: (_changesType: AutoVerifierChangesType, changes: Changes) => changes + } + ) + ] +}); + const guildState = selectorFamily({ key: 'guildState', get: (guildId: number) => ({ get }) => { @@ -640,6 +670,10 @@ export const currGuildActiveChannelState = selector>({ get: createCurrentGuildLoadableStateGetter(guildActiveChannelState), dangerouslyAllowMutability: true }); +export const currGuildTokensState = selector>({ + key: 'currGuildTokensState', + get: createCurrentGuildLoadableStateGetter(guildTokensState) +}); // Helper functions for using softImgSrc states function useLoadedOrElse(loadable: Loadable, ifLoading: T, ifError: T) { diff --git a/src/client/webapp/elements/require/guild-subscriptions.ts b/src/client/webapp/elements/require/guild-subscriptions.ts index afe631c..92a8187 100644 --- a/src/client/webapp/elements/require/guild-subscriptions.ts +++ b/src/client/webapp/elements/require/guild-subscriptions.ts @@ -869,7 +869,7 @@ function useSelfMemberSubscription(guild: CombinedGuild): [ * fetchError: Any error from fetching * ] */ -export function useTokensSubscription(guild: CombinedGuild) { +function useTokensSubscription(guild: CombinedGuild) { const fetchTokensFunc = useCallback(async () => { //LOG.silly('fetching tokens for subscription'); return await guild.fetchTokens();