From 601a5bd3d6328874640efcef2fb7dd5e667901fb Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Sat, 5 Feb 2022 14:38:16 -0600 Subject: [PATCH] refactor listeners in atoms --- src/client/webapp/elements/require/atoms-2.ts | 549 ++++++++++-------- 1 file changed, 298 insertions(+), 251 deletions(-) diff --git a/src/client/webapp/elements/require/atoms-2.ts b/src/client/webapp/elements/require/atoms-2.ts index 17c43cf..78d2881 100644 --- a/src/client/webapp/elements/require/atoms-2.ts +++ b/src/client/webapp/elements/require/atoms-2.ts @@ -5,20 +5,19 @@ const LOG = Logger.create(__filename, electronConsole); import { ReactNode, useEffect } from "react"; import { atom, AtomEffect, atomFamily, GetRecoilValue, Loadable, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useRecoilValueLoadable, useSetRecoilState } from "recoil"; -import { Changes, Channel, GuildMetadata, Member, Message, Resource, ShouldNeverHappenError, Token } 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'; import { Conflictable, Connectable } from '../../guild-types'; -import { IDQuery, PartialMessageListQuery } from '../../auto-verifier-with-args'; import ElementsUtil from './elements-util'; -import Globals from '../../globals'; // General typescript type that infers the arguments of a function type Arguments = T extends (...args: infer A) => unknown ? A : never; // Ensures that a type is not undefined type Defined = T extends undefined ? never : T | Awaited; +type AtomEffectParam = Arguments>[0]; + export type UnloadedValue = { value: undefined; error: undefined; @@ -104,24 +103,19 @@ export const allGuildsState = atom({ dangerouslyAllowMutability: true }); -interface RecoilLoadableAtomEffectParams { - node: RecoilState>, - trigger: 'get' | 'set'; - setSelf: (loadableValue: LoadableValue) => void; - getPromise: (recoilValue: RecoilValue) => Promise; -} +// TODO: Consider using 'getCallback' for this and having atom-backed selectors instead of using the atoms directly +// Atoms would have to be set up with destructors that unsubscribe from the guild +type FetchValueFunc = () => Promise; function createFetchValueFunc( - getPromise: (recoilValue: RecoilValue) => Promise, + atomEffectParam: AtomEffectParam>, guildId: number, - node: RecoilState>, - setSelf: (loadableValue: LoadableValue) => void, fetchFunc: (guild: CombinedGuild) => Promise>, -): () => Promise { +): FetchValueFunc { + const { node, setSelf, getPromise } = atomEffectParam; const fetchValueFunc = async () => { - // TODO: Look into using getCallback in case guild is null https://recoiljs.org/docs/api-reference/core/selector#returning-objects-with-callbacks const guild = await getPromise(guildState(guildId)); - if (guild === null) return; // Can't send a request without an associated guild + if (guild === null) return; // NOTE: This would only happen if this atom is created before its corresponding guild exists in the guildsManager const selfState = await getPromise(node); if (isPended(selfState)) return; // Don't send another request if we're already loading @@ -138,47 +132,59 @@ function createFetchValueFunc( return fetchValueFunc; } -function applyNew(value: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] { - return value.concat(newElements).sort(sortFunc); -} -function applyUpdated(value: T[], updatedElements: T[], sortFunc: (a: T, b: T) => number): T[] { - return value.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc); -} -function applyRemoved(value: T[], removedElements: T[]): T[] { - const removedIds = new Set(removedElements.map(removedElement => removedElement.id)); - return value.filter(element => !removedIds.has(element.id)); +// Useful for new-xxx, update-xxx, remove-xxx, conflict-xxx list events +function createEventHandler< + V, // e.g. LoadableValue + ArgsMapResult, // e.g. Member[] + Addins, // e.g. FetchValueFunc to be called later to re-load members + XE extends keyof (Connectable | Conflictable) // e.g. new-members +>( + atomEffectParam: AtomEffectParam, + argsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => ArgsMapResult, + applyFunc: (selfState: V, argsResult: ArgsMapResult, addins: Addins) => V, + addins: Addins, +): (Connectable & Conflictable)[XE] { + const { node, setSelf, getPromise } = atomEffectParam; + // 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 (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => { + const selfState = await getPromise(node); + const argsResult = argsMap(...args); + setSelf(applyFunc(selfState, argsResult, addins)); + }) 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 - .concat(changes.added) - .map(element => changes.updated.find(updatedPair => updatedPair.newDataPoint.id === element.id)?.newDataPoint ?? element) - .filter(element => !removedIds.has(element.id)) - .sort(sortFunc); -} - -interface SingleEventMappingParams { - updatedEventName: UE; - updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined; - updatedEventCondition?: (value: T) => boolean; - conflictEventName: CE; - conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Defined; - conflictEventCondition?: (value: T) => boolean; +interface SingleEventMappingParams< + T, + V, + Addins, + UE extends keyof Connectable, + CE extends keyof Conflictable +> { + updatedEvent: { + name: UE; + argsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined; + applyFunc: (selfState: V, argsResult: Defined, addins: Addins) => V; + }, + conflictEvent: { + name: CE; + argsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Defined; + applyFunc: (selfState: V, argsResult: Defined, addins: Addins) => V; + } } function listenToSingle< T, // e.g. GuildMetadata + V, // e.g. LoadableValue UE extends keyof Connectable, // Update Event CE extends keyof Conflictable, // Conflict Event >( - getPromise: (recoilValue: RecoilValue) => Promise, + atomEffectParam: AtomEffectParam, guildId: number, - node: RecoilValue>, - setSelf: (loadableValue: LoadableValue) => void, - fetchValueFunc: () => Promise, - eventMapping: SingleEventMappingParams + eventMapping: SingleEventMappingParams, + fetchValueFunc: FetchValueFunc, ) { + const { getPromise } = atomEffectParam; // Listen for updates let guild: CombinedGuild | null = null; let onUpdateFunc: (Connectable & Conflictable)[UE] | null = null; @@ -189,72 +195,65 @@ function listenToSingle< if (guild === null) return; // NOTE: This would only happen if this atom is created before its corresponding guild exists in the guildsManager 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 (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => { - const selfState = await getPromise(node); - if (isLoaded(selfState)) { - const value = eventArgsMap(...args); - if (condition(value)) { - setSelf(createLoadedValue(value, fetchValueFunc)); - } - } - }) as (Connectable & Conflictable)[XE]; - } + onUpdateFunc = createEventHandler(atomEffectParam, eventMapping.updatedEvent.argsMap, eventMapping.updatedEvent.applyFunc, fetchValueFunc); + onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc, fetchValueFunc); - // 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 = createConnectableHandler(eventMapping.updatedEventArgsMap, eventMapping.updatedEventCondition ?? (() => true)); - onConflictFunc = createConnectableHandler(eventMapping.conflictEventArgsMap, eventMapping.conflictEventCondition ?? (() => true)); - guild.on(eventMapping.updatedEventName, onUpdateFunc); - guild.on(eventMapping.conflictEventName, onConflictFunc); + guild.on(eventMapping.updatedEvent.name, onUpdateFunc); + guild.on(eventMapping.conflictEvent.name, onConflictFunc); })(); const cleanup = () => { closed = true; - if (guild && onUpdateFunc) guild.off(eventMapping.updatedEventName, onUpdateFunc); - if (guild && onConflictFunc) guild.off(eventMapping.conflictEventName, onConflictFunc); + if (guild && onUpdateFunc) guild.off(eventMapping.updatedEvent.name, onUpdateFunc); + if (guild && onConflictFunc) guild.off(eventMapping.conflictEvent.name, onConflictFunc); } return cleanup; } + interface MultipleEventMappingParams< T, + V, + Addins, NE extends keyof Connectable, UE extends keyof Connectable, RE extends keyof Connectable, CE extends keyof Conflictable > { - newEventName: NE; - newEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined; - newEventCondition?: (eventArgsResult: Defined) => boolean; - updatedEventName: UE; - updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined; - updatedEventCondition?: (eventArgsResult: Defined) => boolean; - removedEventName: RE; - removedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined; - removedEventCondition?: (eventArgsResult: Defined) => boolean; - conflictEventName: CE; - conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes; - conflictEventCondition?: (eventArgsResult: Changes) => boolean; + newEvent: { + name: NE; + argsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined; + applyFunc: (selfState: V, argsResult: Defined, addins: Addins) => V; + }, + updatedEvent: { + name: UE; + argsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined; + applyFunc: (selfState: V, argsResult: Defined, addins: Addins) => V; + }, + removedEvent: { + name: RE; + argsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined; + applyFunc: (selfState: V, argsResult: Defined, addins: Addins) => V; + }, + conflictEvent: { + name: CE; + argsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes; + applyFunc: (selfState: V, argsResult: Changes, addins: Addins) => V; + }, } function listenToMultiple< - T extends { id: string }, + T extends { id: string }, // e.g. Member + V, // e.g. LoadableValue 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, + atomEffectParam: AtomEffectParam, guildId: number, - node: RecoilValue>, - setSelf: (loadableValue: LoadableValue) => void, - fetchValueFunc: () => Promise, - sortFunc: (a: T, b: T) => number, - eventMapping: MultipleEventMappingParams + eventMapping: MultipleEventMappingParams, + fetchValueFunc: FetchValueFunc, ) { + const { getPromise } = atomEffectParam; // Listen for updates let guild: CombinedGuild | null = null; let onNewFunc: (Connectable & Conflictable)[NE] | null = null; @@ -267,78 +266,42 @@ function listenToMultiple< if (guild === null) return; // NOTE: This would only happen if this atom is created before its corresponding guild exists in the guildsManager if (closed) return; // Make sure not to bind events if this closed while we were fetching the guild state - // Useful for new-xxx, update-xxx, remove-xxx list events - function createConnectableHandler( - eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined, - condition: (eventArgsResult: Defined) => boolean, - 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 (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => { - const selfState = await getPromise(node); - if (isLoaded(selfState)) { - const eventArgsResult = eventArgsMap(...args); - if (condition(eventArgsResult)) { - const value = applyFunc(selfState.value, eventArgsResult, sortFunc); - setSelf(createLoadedValue(value, fetchValueFunc)); - } - } - }) as (Connectable & Conflictable)[XE]; - } + onNewFunc = createEventHandler(atomEffectParam, eventMapping.newEvent.argsMap, eventMapping.newEvent.applyFunc, fetchValueFunc); + onUpdateFunc = createEventHandler(atomEffectParam, eventMapping.updatedEvent.argsMap, eventMapping.updatedEvent.applyFunc, fetchValueFunc); + onRemoveFunc = createEventHandler(atomEffectParam, eventMapping.removedEvent.argsMap, eventMapping.removedEvent.applyFunc, fetchValueFunc); + onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc, fetchValueFunc); - // Useful for conflict-xxx list events - function createConflictableHandler( - eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes, - condition: (eventArgsResult: Changes) => boolean, - applyFunc: (value: T[], eventArgsResult: Changes, sortFunc: (a: T, b: T) => number) => T[], - ): (Connectable & Conflictable)[XE] { - return (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => { - const selfState = await getPromise(node); - if (isLoaded(selfState)) { - const eventArgsResult = eventArgsMap(...args); - if (condition(eventArgsResult)) { - const value = applyFunc(selfState.value, eventArgsResult, sortFunc); - setSelf(createLoadedValue(value, fetchValueFunc)); - } - } - }) as (Connectable & Conflictable)[XE]; - } - - onNewFunc = createConnectableHandler(eventMapping.newEventArgsMap, eventMapping.newEventCondition ?? (() => true), applyNew); - onUpdateFunc = createConnectableHandler(eventMapping.updatedEventArgsMap, eventMapping.updatedEventCondition ?? (() => true), applyUpdated); - onRemoveFunc = createConnectableHandler(eventMapping.removedEventArgsMap, eventMapping.removedEventCondition ?? (() => true), applyRemoved); - onConflictFunc = createConflictableHandler(eventMapping.conflictEventArgsMap, eventMapping.conflictEventCondition ?? (() => true), applyChanges); - guild.on(eventMapping.newEventName, onNewFunc); - guild.on(eventMapping.updatedEventName, onUpdateFunc); - guild.on(eventMapping.removedEventName, onRemoveFunc); - guild.on(eventMapping.conflictEventName, onConflictFunc); + guild.on(eventMapping.newEvent.name, onNewFunc); + guild.on(eventMapping.updatedEvent.name, onUpdateFunc); + guild.on(eventMapping.removedEvent.name, onRemoveFunc); + guild.on(eventMapping.conflictEvent.name, onConflictFunc); })(); const cleanup = () => { closed = true; - if (guild && onNewFunc) guild.off(eventMapping.newEventName, onNewFunc); - if (guild && onUpdateFunc) guild.off(eventMapping.updatedEventName, onUpdateFunc); - if (guild && onRemoveFunc) guild.off(eventMapping.removedEventName, onRemoveFunc); - if (guild && onConflictFunc) guild.off(eventMapping.conflictEventName, onConflictFunc); + if (guild && onNewFunc) guild.off(eventMapping.newEvent.name, onNewFunc); + if (guild && onUpdateFunc) guild.off(eventMapping.updatedEvent.name, onUpdateFunc); + if (guild && onRemoveFunc) guild.off(eventMapping.removedEvent.name, onRemoveFunc); + if (guild && onConflictFunc) guild.off(eventMapping.conflictEvent.name, onConflictFunc); } return cleanup; } -function singleGuildSubscriptionEffect< +function guildDataSubscriptionLoadableSingleEffect< T, // e.g. GuildMetadata UE extends keyof Connectable, // Update Event CE extends keyof Conflictable, // Conflict Event >( guildId: number, fetchFunc: (guild: CombinedGuild) => Promise>, - eventMapping: SingleEventMappingParams, + eventMapping: SingleEventMappingParams, FetchValueFunc, UE, CE>, skipFunc?: () => boolean ) { - const effect: AtomEffect> = ({ node, trigger, setSelf, getPromise }) => { + const effect: AtomEffect> = (atomEffectParam: AtomEffectParam>) => { + const { trigger } = atomEffectParam; if (skipFunc && skipFunc()) return; // Don't run if this atom should be skipped for some reason (e.g. null resourceId) - const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc); + const fetchValueFunc = createFetchValueFunc(atomEffectParam, guildId, fetchFunc); // Fetch initial value on first get if (trigger === 'get') { @@ -346,7 +309,7 @@ function singleGuildSubscriptionEffect< } // Listen to changes - const cleanup = listenToSingle(getPromise, guildId, node, setSelf, fetchValueFunc, eventMapping); + const cleanup = listenToSingle(atomEffectParam, guildId, eventMapping, fetchValueFunc); return () => { cleanup(); @@ -355,7 +318,7 @@ function singleGuildSubscriptionEffect< return effect; } -function multipleGuildSubscriptionEffect< +function guildDataSubscriptionLoadableMultipleEffect< T extends { id: string }, NE extends keyof Connectable, UE extends keyof Connectable, @@ -364,11 +327,12 @@ function multipleGuildSubscriptionEffect< >( guildId: number, fetchFunc: (guild: CombinedGuild) => Promise>, - sortFunc: (a: T, b: T) => number, - eventMapping: MultipleEventMappingParams, + eventMapping: MultipleEventMappingParams, FetchValueFunc, NE, UE, RE, CE>, ) { - const effect: AtomEffect> = ({ node, trigger, setSelf, getPromise }) => { - const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc); + const effect: AtomEffect> = (atomEffectParam) => { + const { trigger } = atomEffectParam; + + const fetchValueFunc = createFetchValueFunc(atomEffectParam, guildId, fetchFunc); // Fetch initial value on first get if (trigger === 'get') { @@ -376,7 +340,7 @@ function multipleGuildSubscriptionEffect< } // Listen to changes - const cleanup = listenToMultiple(getPromise, guildId, node, setSelf, fetchValueFunc, sortFunc, eventMapping); + const cleanup = listenToMultiple(atomEffectParam, guildId, eventMapping, fetchValueFunc); return () => { cleanup(); @@ -385,36 +349,76 @@ function multipleGuildSubscriptionEffect< return effect; } -interface ScrollingList { - list: T[]; - hasMoreAbove: boolean; - hasMoreBelow: boolean; -} -function multipleScrollingGuildSubscriptionEffect< - 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 ->( - guildId: number, - fetchBottomFunc: (guild: CombinedGuild, count: number) => Promise, - fetchAboveFunc: (guild: CombinedGuild, reference: T, count: number) => Promise, - fetchBelowFunc: (guild: CombinedGuild, reference: T, count: number) => Promise, - fetchCount: number, // NOTE: If a fetch returns less than this number of elements, we will no longer try to get more above/below it - maxElements: number, // The maximum number of elements in the scroller. Must be greater than maxFetchElements - sortFunc: (a: T, b: T) => number, - eventMapping: MultipleEventMappingParams -) { - const effect: AtomEffect>> = ({ node, trigger, setSelf, getPromise }) => { - const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, async (guild: CombinedGuild) => { - const list = await fetchBottomFunc(guild, fetchCount); - return { list, hasMoreBelow: false, hasMoreAbove: list.length <= fetchCount }; - }); +// TODO: ScrollableLoadableValue +// interface ScrollingList { +// list: T[]; +// hasMoreAbove: boolean; +// hasMoreBelow: boolean; +// } +// function multipleScrollingGuildSubscriptionEffect< +// 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 +// >( +// guildId: number, +// fetchBottomFunc: (guild: CombinedGuild, count: number) => Promise, +// fetchAboveFunc: (guild: CombinedGuild, reference: T, count: number) => Promise, +// fetchBelowFunc: (guild: CombinedGuild, reference: T, count: number) => Promise, +// fetchCount: number, // NOTE: If a fetch returns less than this number of elements, we will no longer try to get more above/below it +// maxElements: number, // The maximum number of elements in the scroller. Must be greater than maxFetchElements +// sortFunc: (a: T, b: T) => number, +// eventMapping: MultipleEventMappingParams +// ) { +// const effect: AtomEffect>> = ({ node, trigger, setSelf, getPromise }) => { +// const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, async (guild: CombinedGuild) => { +// const list = await fetchBottomFunc(guild, fetchCount); +// return { list, hasMoreBelow: false, hasMoreAbove: list.length <= fetchCount }; +// }); +// +// // TODO: listen for changes +// }; +// return effect; +// } - // TODO: listen for changes +function applyNewElements(list: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] { + return list.concat(newElements).sort(sortFunc); +} +function applyUpdatedElements(list: T[], updatedElements: T[], sortFunc: (a: T, b: T) => number): T[] { + return list.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc); +} +function applyRemovedElements(list: T[], removedElements: T[]): T[] { + const removedIds = new Set(removedElements.map(removedElement => removedElement.id)); + return list.filter(element => !removedIds.has(element.id)); +} + +function applyChangedElements(list: T[], changes: Changes, sortFunc: (a: T, b: T) => number): T[] { + const removedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id)); + return list + .concat(changes.added) + .map(element => changes.updated.find(updatedPair => updatedPair.newDataPoint.id === element.id)?.newDataPoint ?? element) + .filter(element => !removedIds.has(element.id)) + .sort(sortFunc); +} + +function applyIfLoaded(selfState: LoadableValue, argsResult: Defined, fetchValueFunc: FetchValueFunc): LoadableValue { + if (!isLoaded(selfState)) return selfState; + return createLoadedValue(argsResult, fetchValueFunc); +} + +function applyListFuncIfLoaded( + applyFunc: (list: T[], argsResult: A, sortFunc: (a: T, b: T) => number) => T[], + sortFunc: (a: T, b: T) => number +): ( + selfState: LoadableValue, + argsResult: A, + fetchValueFunc: FetchValueFunc +) => LoadableValue { + return (selfState: LoadableValue, argsResult: A, fetchValueFunc: FetchValueFunc) => { + if (!isLoaded(selfState)) return selfState; + return createLoadedValue(applyFunc(selfState.value, argsResult, sortFunc), fetchValueFunc); }; - return effect; } // You probably want currGuildMetaState @@ -422,14 +426,20 @@ export const guildMetaState = atomFamily, number>({ key: 'guildMetaState', default: DEF_UNLOADED_VALUE, effects_UNSTABLE: (guildId: number) => [ - singleGuildSubscriptionEffect( + guildDataSubscriptionLoadableSingleEffect( guildId, async (guild: CombinedGuild) => await guild.fetchMetadata(), { - updatedEventName: 'update-metadata', - updatedEventArgsMap: (newMeta: GuildMetadata) => newMeta, - conflictEventName: 'conflict-metadata', - conflictEventArgsMap: (_changesType: AutoVerifierChangesType, _oldMeta: GuildMetadata, newMeta: GuildMetadata) => newMeta + updatedEvent: { + name: 'update-metadata', + argsMap: (newMeta: GuildMetadata) => newMeta, + applyFunc: applyIfLoaded + }, + conflictEvent: { + name: 'conflict-metadata', + argsMap: (_changesType, _oldMeta, newMeta: GuildMetadata) => newMeta, + applyFunc: applyIfLoaded + } } ) ], @@ -442,7 +452,7 @@ export const guildResourceState = atomFamily, { guildId: key: 'guildPotentialResourceState', default: DEF_UNLOADED_VALUE, effects_UNSTABLE: (param: { guildId: number, resourceId: string | null }) => [ - singleGuildSubscriptionEffect( + guildDataSubscriptionLoadableSingleEffect( param.guildId, async (guild: CombinedGuild) => { if (param.resourceId === null) { // Should never happen because of skipFunc (last argument) @@ -452,12 +462,16 @@ export const guildResourceState = atomFamily, { guildId: } }, { - 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, + updatedEvent: { + name: 'update-resource', + argsMap: (updatedResource: Resource) => updatedResource, + applyFunc: applyIfLoaded + }, + conflictEvent: { + name: 'conflict-resource', + argsMap: (_query, _changeType, _oldResource: Resource, newResource: Resource) => newResource, + applyFunc: applyIfLoaded + } }, () => param.resourceId === null // Never load/bind if resourceId === null ) @@ -479,19 +493,30 @@ const guildMembersState = atomFamily, number>({ key: 'guildMembersState', default: DEF_UNLOADED_VALUE, effects_UNSTABLE: (guildId: number) => [ - multipleGuildSubscriptionEffect( + guildDataSubscriptionLoadableMultipleEffect( guildId, async (guild: CombinedGuild) => await guild.fetchMembers(), - Member.sortForList, { - newEventName: 'new-members', - newEventArgsMap: (newMembers: Member[]) => newMembers, - updatedEventName: 'update-members', - updatedEventArgsMap: (updatedMembers: Member[]) => updatedMembers, - removedEventName: 'remove-members', - removedEventArgsMap: (removedMembers: Member[]) => removedMembers, - conflictEventName: 'conflict-members', - conflictEventArgsMap: (_changesType: AutoVerifierChangesType, changes: Changes) => changes, + newEvent: { + name: 'new-members', + argsMap: (newMembers: Member[]) => newMembers, + applyFunc: applyListFuncIfLoaded(applyNewElements, Member.sortForList) + }, + updatedEvent: { + name: 'update-members', + argsMap: (newMembers: Member[]) => newMembers, + applyFunc: applyListFuncIfLoaded(applyUpdatedElements, Member.sortForList) + }, + removedEvent: { + name: 'remove-members', + argsMap: (newMembers: Member[]) => newMembers, + applyFunc: applyListFuncIfLoaded(applyRemovedElements, Member.sortForList) + }, + conflictEvent: { + name: 'conflict-members', + argsMap: (_changeType, changes: Changes) => changes, + applyFunc: applyListFuncIfLoaded(applyChangedElements, Member.sortForList) + }, } ) ], @@ -526,19 +551,30 @@ const guildChannelsState = atomFamily, number>({ key: 'guildChannelsState', default: DEF_UNLOADED_VALUE, effects_UNSTABLE: (guildId: number) => [ - multipleGuildSubscriptionEffect( + guildDataSubscriptionLoadableMultipleEffect( guildId, async (guild: CombinedGuild) => await guild.fetchChannels(), - Channel.sortByIndex, { - newEventName: 'new-channels', - newEventArgsMap: (newChannels: Channel[]) => newChannels, - updatedEventName: 'update-channels', - updatedEventArgsMap: (updatedChannels: Channel[]) => updatedChannels, - removedEventName: 'remove-channels', - removedEventArgsMap: (removedChannels: Channel[]) => removedChannels, - conflictEventName: 'conflict-channels', - conflictEventArgsMap: (_changesType: AutoVerifierChangesType, changes: Changes) => changes + newEvent: { + name: 'new-channels', + argsMap: (newChannels: Channel[]) => newChannels, + applyFunc: applyListFuncIfLoaded(applyNewElements, Channel.sortByIndex) + }, + updatedEvent: { + name: 'update-channels', + argsMap: (updatedChannels: Channel[]) => updatedChannels, + applyFunc: applyListFuncIfLoaded(applyUpdatedElements, Channel.sortByIndex) + }, + removedEvent: { + name: 'remove-channels', + argsMap: (removedChannels: Channel[]) => removedChannels, + applyFunc: applyListFuncIfLoaded(applyRemovedElements, Channel.sortByIndex) + }, + conflictEvent: { + name: 'conflict-channels', + argsMap: (_changeType, changes: Changes) => changes, + applyFunc: applyListFuncIfLoaded(applyChangedElements, Channel.sortByIndex) + }, } ) ], @@ -569,57 +605,68 @@ const guildActiveChannelState = selectorFamily, number>({ dangerouslyAllowMutability: true }); -export const guildChannelMessagesState = atomFamily>, { guildId: number, channelId: string }>({ - key: 'guildChannelMessagesState', - default: DEF_UNLOADED_VALUE, - effects_UNSTABLE: ({ guildId, channelId }) => [ - multipleScrollingGuildSubscriptionEffect( - guildId, - async (guild: CombinedGuild, count: number) => await guild.fetchMessagesRecent(channelId, count), - async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesBefore(channelId, reference._order, count), - async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesAfter(channelId, reference._order, count), - Globals.MESSAGES_PER_REQUEST, - Globals.MAX_CURRENT_MESSAGES, - Message.sortOrder, - { - newEventName: 'new-messages', - newEventArgsMap: (newMessages: Message[]) => newMessages.filter(message => message.channel.id === channelId), - newEventCondition: (messages: Message[]) => messages.length > 0, - updatedEventName: 'update-messages', - updatedEventArgsMap: (updatedMessages: Message[]) => updatedMessages.filter(message => message.channel.id === channelId), - updatedEventCondition: (messages: Message[]) => messages.length > 0, - removedEventName: 'remove-messages', - removedEventArgsMap: (removedMessages: Message[]) => removedMessages.filter(message => message.channel.id === channelId), - removedEventCondition: (messages: Message[]) => messages.length > 0, - conflictEventName: 'conflict-messages', - conflictEventArgsMap: (_query: PartialMessageListQuery, _changesType: AutoVerifierChangesType, changes: Changes) => ({ - added: changes.added.filter(message => message.channel.id === channelId), - updated: changes.updated.filter(change => change.newDataPoint.channel.id === channelId), - deleted: changes.deleted.filter(message => message.channel.id === channelId), - }), - conflictEventCondition: (changes: Changes) => changes.added.length + changes.updated.length + changes.deleted.length > 0, - } - ) - ] -}); +// export const guildChannelMessagesState = atomFamily>, { guildId: number, channelId: string }>({ +// key: 'guildChannelMessagesState', +// default: DEF_UNLOADED_VALUE, +// effects_UNSTABLE: ({ guildId, channelId }) => [ +// multipleScrollingGuildSubscriptionEffect( +// guildId, +// async (guild: CombinedGuild, count: number) => await guild.fetchMessagesRecent(channelId, count), +// async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesBefore(channelId, reference._order, count), +// async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesAfter(channelId, reference._order, count), +// Globals.MESSAGES_PER_REQUEST, +// Globals.MAX_CURRENT_MESSAGES, +// Message.sortOrder, +// { +// newEventName: 'new-messages', +// newEventArgsMap: (newMessages: Message[]) => newMessages.filter(message => message.channel.id === channelId), +// newEventCondition: (messages: Message[]) => messages.length > 0, +// updatedEventName: 'update-messages', +// updatedEventArgsMap: (updatedMessages: Message[]) => updatedMessages.filter(message => message.channel.id === channelId), +// updatedEventCondition: (messages: Message[]) => messages.length > 0, +// removedEventName: 'remove-messages', +// removedEventArgsMap: (removedMessages: Message[]) => removedMessages.filter(message => message.channel.id === channelId), +// removedEventCondition: (messages: Message[]) => messages.length > 0, +// conflictEventName: 'conflict-messages', +// conflictEventArgsMap: (_query: PartialMessageListQuery, _changesType: AutoVerifierChangesType, changes: Changes) => ({ +// added: changes.added.filter(message => message.channel.id === channelId), +// updated: changes.updated.filter(change => change.newDataPoint.channel.id === channelId), +// deleted: changes.deleted.filter(message => message.channel.id === channelId), +// }), +// conflictEventCondition: (changes: Changes) => changes.added.length + changes.updated.length + changes.deleted.length > 0, +// } +// ) +// ] +// }); export const guildTokensState = atomFamily, number>({ key: 'guildTokensState', default: DEF_UNLOADED_VALUE, effects_UNSTABLE: (guildId: number) => [ - multipleGuildSubscriptionEffect( + guildDataSubscriptionLoadableMultipleEffect( 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 + newEvent: { + name: 'new-tokens', + argsMap: (newTokens: Token[]) => newTokens, + applyFunc: applyListFuncIfLoaded(applyNewElements, Token.sortRecentCreatedFirst) + }, + updatedEvent: { + name: 'update-tokens', + argsMap: (updatedTokens: Token[]) => updatedTokens, + applyFunc: applyListFuncIfLoaded(applyUpdatedElements, Token.sortRecentCreatedFirst) + }, + removedEvent: { + name: 'remove-tokens', + argsMap: (removedTokens: Token[]) => removedTokens, + applyFunc: applyListFuncIfLoaded(applyRemovedElements, Token.sortRecentCreatedFirst) + }, + conflictEvent: { + name: 'conflict-tokens', + argsMap: (_changeType, changes: Changes) => changes, + applyFunc: applyListFuncIfLoaded(applyChangedElements, Token.sortRecentCreatedFirst) + }, } ) ]