function-scoping functions for less globalism in atoms-2

This commit is contained in:
Michael Peters 2022-02-05 09:01:50 -06:00
parent 6da480c36e
commit 54ad823ebe
3 changed files with 117 additions and 89 deletions

View File

@ -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<GuildInvitesDisplayProps> = (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<Duration | null>(moment.duration(1, 'day'));
const [ expiresFromNowText, setExpiresFromNowText ] = useState<string>('1 day');
@ -66,13 +63,10 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
}, [ createTokenFailMessage ]);
const tokenElements = useMemo(() => {
if (tokensError) {
// TODO: Try Again
return <div className="tokens-failed">Unable to load tokens</div>;
}
// TODO: Try again?
return tokensResult?.value?.map((token: Token) => <TokenRow key={guild.id + token.token} url={url} token={token} guild={guild} />);
}, [ url, guild, tokensResult, tokensError ]);
if (isFailed(tokens)) return <div className="tokens-failed">Unable to load tokens</div>; // TODO: Try Again
if (!isLoaded(tokens)) return null; // TODO: Pending indicator
return tokens.value.map((token: Token) => <TokenRow key={guild.id + token.token} url={url} token={token} guild={guild} />);
}, [ url, guild, tokens ]);
return (
<Display

View File

@ -5,7 +5,7 @@ const LOG = Logger.create(__filename, electronConsole);
import { ReactNode, useEffect } from "react";
import { atom, atomFamily, GetRecoilValue, Loadable, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useRecoilValueLoadable, useSetRecoilState } from "recoil";
import { Changes, Channel, GuildMetadata, Member, Resource, ShouldNeverHappenError } from "../../data-types";
import { Changes, Channel, GuildMetadata, Member, Resource, ShouldNeverHappenError, Token } from "../../data-types";
import CombinedGuild from "../../guild-combined";
import GuildsManager from "../../guilds-manager";
import { AutoVerifierChangesType } from '../../auto-verifier';
@ -137,28 +137,6 @@ function createFetchValueFunc<T>(
return fetchValueFunc;
}
// Creates an event handler that directly applies the result of the eventArgsMap as a loadedValue into self
function createDirectMappedEventHandler<T, XE extends keyof (Connectable | Conflictable)>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
node: RecoilValue<LoadableValue<T>>,
setSelf: (loadableValue: LoadableValue<T>) => void,
fetchValueFunc: () => Promise<void>,
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T>,
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<T>(value: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] {
return value.concat(newElements).sort(sortFunc);
}
@ -170,30 +148,6 @@ function applyRemoved<T extends { id: string }>(value: T[], removedElements: T[]
return value.filter(element => !removedIds.has(element.id));
}
// Useful for new-xxx, update-xxx, remove-xxx list events
function createListConnectableMappedEventHandler<T, XE extends keyof (Connectable | Conflictable)>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
node: RecoilValue<LoadableValue<T[]>>,
setSelf: (loadableValue: LoadableValue<T[]>) => void,
fetchValueFunc: () => Promise<void>,
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T[]>,
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<T extends { id: string }>(value: T[], changes: Changes<T>, sortFunc: (a: T, b: T) => number): T[] {
const removedIds = new Set<string>(changes.deleted.map(deletedElement => deletedElement.id));
return value
@ -203,28 +157,6 @@ function applyChanges<T extends { id: string }>(value: T[], changes: Changes<T>,
.sort(sortFunc);
}
// Useful for conflict-xxx list events
function createListConflictableMappedEventHandler<T, XE extends keyof (Connectable | Conflictable)>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
node: RecoilValue<LoadableValue<T[]>>,
setSelf: (loadableValue: LoadableValue<T[]>) => void,
fetchValueFunc: () => Promise<void>,
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes<T>,
sortFunc: (a: T, b: T) => number,
applyFunc: (value: T[], eventArgsResult: Changes<T>, 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<T, UE extends keyof Connectable, CE extends keyof Conflictable> {
updatedEventName: UE;
updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T>;
@ -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<XE extends keyof (Connectable | Conflictable)>(
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T>,
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<XE extends keyof (Connectable | Conflictable)>(
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T[]>,
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<XE extends keyof (Connectable | Conflictable)>(
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes<T>,
applyFunc: (value: T[], eventArgsResult: Changes<T>, 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: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
// guildId: number,
// node: RecoilValue<LoadableValue<T[]>>,
// setSelf: (loadableValue: LoadableValue<T[]>) => void,
// fetchValueFunc: () => Promise<void>,
// fetchAboveFunc: (reference: T) => Promise<T | null>,
// fetchBelowFunc: (reference: T) => Promise<T | null>,
// sortFunc: (a: T, b: T) => number,
// eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>
// ) {
// // TODO
// }
function singleGuildSubscriptionEffect<
T, // e.g. GuildMetadata
UE extends keyof Connectable, // Update Event
@ -543,6 +551,28 @@ const guildActiveChannelState = selectorFamily<LoadableValue<Channel>, number>({
dangerouslyAllowMutability: true
});
export const guildTokensState = atomFamily<LoadableValue<Token[]>, 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<Token>) => changes
}
)
]
});
const guildState = selectorFamily<CombinedGuild | null, number>({
key: 'guildState',
get: (guildId: number) => ({ get }) => {
@ -640,6 +670,10 @@ export const currGuildActiveChannelState = selector<LoadableValue<Channel>>({
get: createCurrentGuildLoadableStateGetter(guildActiveChannelState),
dangerouslyAllowMutability: true
});
export const currGuildTokensState = selector<LoadableValue<Token[]>>({
key: 'currGuildTokensState',
get: createCurrentGuildLoadableStateGetter(guildTokensState)
});
// Helper functions for using softImgSrc states
function useLoadedOrElse<T>(loadable: Loadable<T>, ifLoading: T, ifError: T) {

View File

@ -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();