improved single/multiple guild subscription effect - use the { node } destructed vaglue from the effect parameter

This commit is contained in:
Michael Peters 2022-02-03 23:08:02 -06:00
parent 09390fad8f
commit 5b708f2a94
2 changed files with 58 additions and 38 deletions

View File

@ -5,14 +5,16 @@ const LOG = Logger.create(__filename, electronConsole);
import { ReactNode, useEffect } from "react"; import { ReactNode, useEffect } from "react";
import { atom, atomFamily, GetCallback, GetRecoilValue, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useSetRecoilState } from "recoil"; import { atom, atomFamily, GetCallback, GetRecoilValue, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useSetRecoilState } from "recoil";
import { Changes, Channel, GuildMetadata, Member } from "../../data-types"; import { Changes, Channel, GuildMetadata, Member, Resource } from "../../data-types";
import CombinedGuild from "../../guild-combined"; import CombinedGuild from "../../guild-combined";
import GuildsManager from "../../guilds-manager"; import GuildsManager from "../../guilds-manager";
import { AutoVerifierChangesType } from '../../auto-verifier'; import { AutoVerifierChangesType } from '../../auto-verifier';
import { Conflictable, Connectable } from '../../guild-types'; import { Conflictable, Connectable } from '../../guild-types';
import { IDQuery } from '../../auto-verifier-with-args';
// General typescript type that infers the arguments of a function // General typescript type that infers the arguments of a function
type Arguments<T> = T extends (...args: infer A) => unknown ? A : never; type Arguments<T> = T extends (...args: infer A) => unknown ? A : never;
// Ensures that a type is not undefined
type Defined<T> = T extends undefined ? never : T | Awaited<T>; type Defined<T> = T extends undefined ? never : T | Awaited<T>;
export type UnloadedValue = { export type UnloadedValue = {
@ -101,6 +103,7 @@ export const allGuildsState = atom<CombinedGuild[] | null>({
}); });
interface RecoilLoadableAtomEffectParams<T> { interface RecoilLoadableAtomEffectParams<T> {
node: RecoilState<LoadableValue<T>>,
trigger: 'get' | 'set'; trigger: 'get' | 'set';
setSelf: (loadableValue: LoadableValue<T>) => void; setSelf: (loadableValue: LoadableValue<T>) => void;
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>; getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>;
@ -109,15 +112,15 @@ interface RecoilLoadableAtomEffectParams<T> {
function createFetchValueFunc<T>( function createFetchValueFunc<T>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>, getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number, guildId: number,
node: RecoilValue<LoadableValue<T>>,
setSelf: (loadableValue: LoadableValue<T>) => void, setSelf: (loadableValue: LoadableValue<T>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T>>,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>, fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
): () => Promise<void> { ): () => Promise<void> {
const fetchValueFunc = async () => { const fetchValueFunc = async () => {
const guild = await getPromise(guildState(guildId)); const guild = await getPromise(guildState(guildId));
if (guild === null) return; // Can't send a request without an associated guild if (guild === null) return; // Can't send a request without an associated guild
const selfState = await getPromise(stateAtomFamily(guildId)); const selfState = await getPromise(node);
if (isPended(selfState)) return; // Don't send another request if we're already loading if (isPended(selfState)) return; // Don't send another request if we're already loading
setSelf(DEF_PENDED_VALUE); setSelf(DEF_PENDED_VALUE);
@ -135,18 +138,20 @@ function createFetchValueFunc<T>(
// Creates an event handler that directly applies the result of the eventArgsMap as a loadedValue into self // 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)>( function createDirectMappedEventHandler<T, XE extends keyof (Connectable | Conflictable)>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>, getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number, node: RecoilValue<LoadableValue<T>>,
setSelf: (loadableValue: LoadableValue<T>) => void, setSelf: (loadableValue: LoadableValue<T>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T>>,
fetchValueFunc: () => Promise<void>, fetchValueFunc: () => Promise<void>,
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T> eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T>,
condition: (value: T) => boolean
): (Connectable & Conflictable)[XE] { ): (Connectable & Conflictable)[XE] {
return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => { (async () => {
const selfState = await getPromise(stateAtomFamily(guildId)); const selfState = await getPromise(node);
if (isLoaded(selfState)) { if (isLoaded(selfState)) {
const value = eventArgsMap(...args); const value = eventArgsMap(...args);
setSelf(createLoadedValue(value, fetchValueFunc)); if (condition(value)) {
setSelf(createLoadedValue(value, fetchValueFunc));
}
} }
})(); })();
}) as (Connectable & Conflictable)[XE]; }) as (Connectable & Conflictable)[XE];
@ -166,9 +171,8 @@ function applyRemoved<T extends { id: string }>(value: T[], removedElements: T[]
// Useful for new-xxx, update-xxx, remove-xxx list events // Useful for new-xxx, update-xxx, remove-xxx list events
function createListConnectableMappedEventHandler<T, XE extends keyof (Connectable | Conflictable)>( function createListConnectableMappedEventHandler<T, XE extends keyof (Connectable | Conflictable)>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>, getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number, node: RecoilValue<LoadableValue<T[]>>,
setSelf: (loadableValue: LoadableValue<T[]>) => void, setSelf: (loadableValue: LoadableValue<T[]>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T[]>>,
fetchValueFunc: () => Promise<void>, fetchValueFunc: () => Promise<void>,
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T[]>, eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T[]>,
sortFunc: (a: T, b: T) => number, sortFunc: (a: T, b: T) => number,
@ -178,7 +182,7 @@ function createListConnectableMappedEventHandler<T, XE extends keyof (Connectabl
// otherwise, I may have done this wrong. Forcing it to work with these calls // otherwise, I may have done this wrong. Forcing it to work with these calls
return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => { (async () => {
const selfState = await getPromise(stateAtomFamily(guildId)); const selfState = await getPromise(node);
if (isLoaded(selfState)) { if (isLoaded(selfState)) {
const eventArgsResult = eventArgsMap(...args); const eventArgsResult = eventArgsMap(...args);
const value = applyFunc(selfState.value, eventArgsResult, sortFunc); const value = applyFunc(selfState.value, eventArgsResult, sortFunc);
@ -200,9 +204,8 @@ function applyChanges<T extends { id: string }>(value: T[], changes: Changes<T>,
// Useful for conflict-xxx list events // Useful for conflict-xxx list events
function createListConflictableMappedEventHandler<T, XE extends keyof (Connectable | Conflictable)>( function createListConflictableMappedEventHandler<T, XE extends keyof (Connectable | Conflictable)>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>, getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number, node: RecoilValue<LoadableValue<T[]>>,
setSelf: (loadableValue: LoadableValue<T[]>) => void, setSelf: (loadableValue: LoadableValue<T[]>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T[]>>,
fetchValueFunc: () => Promise<void>, fetchValueFunc: () => Promise<void>,
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes<T>, eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes<T>,
sortFunc: (a: T, b: T) => number, sortFunc: (a: T, b: T) => number,
@ -210,7 +213,7 @@ function createListConflictableMappedEventHandler<T, XE extends keyof (Connectab
): (Connectable & Conflictable)[XE] { ): (Connectable & Conflictable)[XE] {
return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => { (async () => {
const selfState = await getPromise(stateAtomFamily(guildId)); const selfState = await getPromise(node);
if (isLoaded(selfState)) { if (isLoaded(selfState)) {
const eventArgsResult = eventArgsMap(...args); const eventArgsResult = eventArgsMap(...args);
const value = applyFunc(selfState.value, eventArgsResult, sortFunc); const value = applyFunc(selfState.value, eventArgsResult, sortFunc);
@ -223,8 +226,10 @@ function createListConflictableMappedEventHandler<T, XE extends keyof (Connectab
interface SingleEventMappingParams<T, UE extends keyof Connectable, CE extends keyof Conflictable> { interface SingleEventMappingParams<T, UE extends keyof Connectable, CE extends keyof Conflictable> {
updatedEventName: UE; updatedEventName: UE;
updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T>; updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T>;
updatedEventCondition?: (value: T) => boolean;
conflictEventName: CE; conflictEventName: CE;
conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Defined<T>; conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Defined<T>;
conflictEventCondition?: (value: T) => boolean;
} }
function listenToSingle< function listenToSingle<
@ -234,8 +239,8 @@ function listenToSingle<
>( >(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>, getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number, guildId: number,
node: RecoilValue<LoadableValue<T>>,
setSelf: (loadableValue: LoadableValue<T>) => void, setSelf: (loadableValue: LoadableValue<T>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T>>,
fetchValueFunc: () => Promise<void>, fetchValueFunc: () => Promise<void>,
eventMapping: SingleEventMappingParams<T, UE, CE> eventMapping: SingleEventMappingParams<T, UE, CE>
) { ) {
@ -251,8 +256,8 @@ function listenToSingle<
// I think the typed EventEmitter class isn't ready for this level of insane type safety // 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 // otherwise, I may have done this wrong. Forcing it to work with these calls
onUpdateFunc = createDirectMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.updatedEventArgsMap); onUpdateFunc = createDirectMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.updatedEventArgsMap, eventMapping.updatedEventCondition ?? (() => true));
onConflictFunc = createDirectMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.conflictEventArgsMap); onConflictFunc = createDirectMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.conflictEventArgsMap, eventMapping.conflictEventCondition ?? (() => true));
guild.on(eventMapping.updatedEventName, onUpdateFunc); guild.on(eventMapping.updatedEventName, onUpdateFunc);
guild.on(eventMapping.conflictEventName, onConflictFunc); guild.on(eventMapping.conflictEventName, onConflictFunc);
})(); })();
@ -289,8 +294,8 @@ function listenToMultiple<
>( >(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>, getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number, guildId: number,
node: RecoilValue<LoadableValue<T[]>>,
setSelf: (loadableValue: LoadableValue<T[]>) => void, setSelf: (loadableValue: LoadableValue<T[]>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T[]>>,
fetchValueFunc: () => Promise<void>, fetchValueFunc: () => Promise<void>,
sortFunc: (a: T, b: T) => number, sortFunc: (a: T, b: T) => number,
eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE> eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>
@ -307,10 +312,10 @@ function listenToMultiple<
if (guild === null) return; if (guild === null) return;
if (closed) return; // Make sure not to bind events if this closed while we were fetching the guild state if (closed) return; // Make sure not to bind events if this closed while we were fetching the guild state
onNewFunc = createListConnectableMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.newEventArgsMap, sortFunc, applyNew); onNewFunc = createListConnectableMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.newEventArgsMap, sortFunc, applyNew);
onUpdateFunc = createListConnectableMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.updatedEventArgsMap, sortFunc, applyUpdated); onUpdateFunc = createListConnectableMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.updatedEventArgsMap, sortFunc, applyUpdated);
onRemoveFunc = createListConnectableMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.updatedEventArgsMap, sortFunc, applyRemoved); onRemoveFunc = createListConnectableMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.updatedEventArgsMap, sortFunc, applyRemoved);
onConflictFunc = createListConflictableMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.conflictEventArgsMap, sortFunc, applyChanges); onConflictFunc = createListConflictableMappedEventHandler(getPromise, node, setSelf, fetchValueFunc, eventMapping.conflictEventArgsMap, sortFunc, applyChanges);
guild.on(eventMapping.updatedEventName, onUpdateFunc); guild.on(eventMapping.updatedEventName, onUpdateFunc);
guild.on(eventMapping.conflictEventName, onConflictFunc); guild.on(eventMapping.conflictEventName, onConflictFunc);
})(); })();
@ -330,13 +335,12 @@ function singleGuildSubscriptionEffect<
CE extends keyof Conflictable, // Conflict Event CE extends keyof Conflictable, // Conflict Event
>( >(
guildId: number, guildId: number,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T>>,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>, fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
eventMapping: SingleEventMappingParams<T, UE, CE> eventMapping: SingleEventMappingParams<T, UE, CE>
) { ) {
return (params: RecoilLoadableAtomEffectParams<T>) => { return (params: RecoilLoadableAtomEffectParams<T>) => {
const { trigger, setSelf, getPromise } = params; const { node, trigger, setSelf, getPromise } = params;
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, setSelf, stateAtomFamily, fetchFunc); const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc);
// Fetch initial value on first get // Fetch initial value on first get
if (trigger === 'get') { if (trigger === 'get') {
@ -344,7 +348,7 @@ function singleGuildSubscriptionEffect<
} }
// Listen to changes // Listen to changes
const cleanup = listenToSingle(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping); const cleanup = listenToSingle(getPromise, guildId, node, setSelf, fetchValueFunc, eventMapping);
return () => { return () => {
cleanup(); cleanup();
@ -360,14 +364,13 @@ function multipleGuildSubscriptionEffect<
CE extends keyof Conflictable CE extends keyof Conflictable
>( >(
guildId: number, guildId: number,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T[]>>,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T[]>>, fetchFunc: (guild: CombinedGuild) => Promise<Defined<T[]>>,
sortFunc: (a: T, b: T) => number, sortFunc: (a: T, b: T) => number,
eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>, eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>,
) { ) {
return (params: RecoilLoadableAtomEffectParams<T[]>) => { return (params: RecoilLoadableAtomEffectParams<T[]>) => {
const { trigger, setSelf, getPromise } = params; const { node, trigger, setSelf, getPromise } = params;
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, setSelf, stateAtomFamily, fetchFunc); const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc);
// Fetch initial value on first get // Fetch initial value on first get
if (trigger === 'get') { if (trigger === 'get') {
@ -375,7 +378,7 @@ function multipleGuildSubscriptionEffect<
} }
// Listen to changes // Listen to changes
const cleanup = listenToMultiple(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, sortFunc, eventMapping); const cleanup = listenToMultiple(getPromise, guildId, node, setSelf, fetchValueFunc, sortFunc, eventMapping);
return () => { return () => {
cleanup(); cleanup();
@ -384,13 +387,12 @@ function multipleGuildSubscriptionEffect<
} }
// You probably want currGuildMetaState // You probably want currGuildMetaState
export const guildMetaState: (guildId: number) => RecoilState<LoadableValue<GuildMetadata>> = atomFamily<LoadableValue<GuildMetadata>, number>({ export const guildMetaState = atomFamily<LoadableValue<GuildMetadata>, number>({
key: 'guildMetaState', key: 'guildMetaState',
default: DEF_UNLOADED_VALUE, default: DEF_UNLOADED_VALUE,
effects_UNSTABLE: (guildId: number) => [ effects_UNSTABLE: (guildId: number) => [
singleGuildSubscriptionEffect( singleGuildSubscriptionEffect(
guildId, guildId,
guildMetaState,
async (guild: CombinedGuild) => await guild.fetchMetadata(), async (guild: CombinedGuild) => await guild.fetchMetadata(),
{ {
updatedEventName: 'update-metadata', updatedEventName: 'update-metadata',
@ -403,13 +405,32 @@ export const guildMetaState: (guildId: number) => RecoilState<LoadableValue<Guil
dangerouslyAllowMutability: true dangerouslyAllowMutability: true
}); });
const guildMembersState: (guildId: number) => RecoilState<LoadableValue<Member[]>> = atomFamily<LoadableValue<Member[]>, number>({ export const guildResourceState = atomFamily<LoadableValue<Resource>, { guildId: number, resourceId: string }>({
key: 'guildResourceState',
default: DEF_UNLOADED_VALUE,
effects_UNSTABLE: (param: { guildId: number, resourceId: string }) => [
singleGuildSubscriptionEffect(
param.guildId,
async (guild: CombinedGuild) => await guild.fetchResource(param.resourceId),
{
updatedEventName: 'update-resource',
updatedEventArgsMap: (updatedResource: Resource) => updatedResource,
updatedEventCondition: (resource: Resource) => resource.id === param.resourceId,
conflictEventName: 'conflict-resource',
conflictEventArgsMap: (_query: IDQuery, _changeType: AutoVerifierChangesType, _oldResource: Resource, newResource: Resource) => newResource,
conflictEventCondition: (resource: Resource) => resource.id === param.resourceId
}
)
],
dangerouslyAllowMutability: true
});
const guildMembersState = atomFamily<LoadableValue<Member[]>, number>({
key: 'guildMembersState', key: 'guildMembersState',
default: DEF_UNLOADED_VALUE, default: DEF_UNLOADED_VALUE,
effects_UNSTABLE: (guildId: number) => [ effects_UNSTABLE: (guildId: number) => [
multipleGuildSubscriptionEffect( multipleGuildSubscriptionEffect(
guildId, guildId,
guildMembersState,
async (guild: CombinedGuild) => await guild.fetchMembers(), async (guild: CombinedGuild) => await guild.fetchMembers(),
Member.sortForList, Member.sortForList,
{ {
@ -451,13 +472,12 @@ export const guildSelfMemberState = selectorFamily<LoadableValue<Member>, number
dangerouslyAllowMutability: true dangerouslyAllowMutability: true
}); });
const guildChannelsState: (guildId: number) => RecoilState<LoadableValue<Channel[]>> = atomFamily<LoadableValue<Channel[]>, number>({ const guildChannelsState = atomFamily<LoadableValue<Channel[]>, number>({
key: 'guildChannelsState', key: 'guildChannelsState',
default: DEF_UNLOADED_VALUE, default: DEF_UNLOADED_VALUE,
effects_UNSTABLE: (guildId: number) => [ effects_UNSTABLE: (guildId: number) => [
multipleGuildSubscriptionEffect( multipleGuildSubscriptionEffect(
guildId, guildId,
guildChannelsState,
async (guild: CombinedGuild) => await guild.fetchChannels(), async (guild: CombinedGuild) => await guild.fetchChannels(),
Channel.sortByIndex, Channel.sortByIndex,
{ {

View File

@ -788,7 +788,7 @@ export function useSoftImageSrcResourceSubscription(guild: CombinedGuild, resour
* fetchError: Any error from fetching * fetchError: Any error from fetching
* ] * ]
*/ */
export function useChannelsSubscription(guild: CombinedGuild) { function useChannelsSubscription(guild: CombinedGuild) {
const fetchChannelsFunc = useCallback(async () => { const fetchChannelsFunc = useCallback(async () => {
return await guild.fetchChannels(); return await guild.fetchChannels();
}, [ guild ]); }, [ guild ]);
@ -838,7 +838,7 @@ function useMembersSubscription(guild: CombinedGuild) {
* TODO: __fetchError: Any error from fetching * TODO: __fetchError: Any error from fetching
* ] * ]
*/ */
export function useSelfMemberSubscription(guild: CombinedGuild): [ function useSelfMemberSubscription(guild: CombinedGuild): [
selfMemberResult: SubscriptionResult<Member | null> | null selfMemberResult: SubscriptionResult<Member | null> | null
] { ] {
const [ _fetchRetryCallable, membersResult, _fetchError ] = useMembersSubscription(guild); const [ _fetchRetryCallable, membersResult, _fetchError ] = useMembersSubscription(guild);