refactor listeners in atoms

This commit is contained in:
Michael Peters 2022-02-05 14:38:16 -06:00
parent dfd50c7e20
commit 601a5bd3d6

View File

@ -5,20 +5,19 @@ const LOG = Logger.create(__filename, electronConsole);
import { ReactNode, useEffect } from "react"; import { ReactNode, useEffect } from "react";
import { atom, AtomEffect, atomFamily, GetRecoilValue, Loadable, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useRecoilValueLoadable, useSetRecoilState } from "recoil"; 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 CombinedGuild from "../../guild-combined";
import GuildsManager from "../../guilds-manager"; import GuildsManager from "../../guilds-manager";
import { AutoVerifierChangesType } from '../../auto-verifier';
import { Conflictable, Connectable } from '../../guild-types'; import { Conflictable, Connectable } from '../../guild-types';
import { IDQuery, PartialMessageListQuery } from '../../auto-verifier-with-args';
import ElementsUtil from './elements-util'; import ElementsUtil from './elements-util';
import Globals from '../../globals';
// 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 // 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>;
type AtomEffectParam<T> = Arguments<AtomEffect<T>>[0];
export type UnloadedValue = { export type UnloadedValue = {
value: undefined; value: undefined;
error: undefined; error: undefined;
@ -104,24 +103,19 @@ export const allGuildsState = atom<CombinedGuild[] | null>({
dangerouslyAllowMutability: true dangerouslyAllowMutability: true
}); });
interface RecoilLoadableAtomEffectParams<T> { // TODO: Consider using 'getCallback' for this and having atom-backed selectors instead of using the atoms directly
node: RecoilState<LoadableValue<T>>, // Atoms would have to be set up with destructors that unsubscribe from the guild
trigger: 'get' | 'set';
setSelf: (loadableValue: LoadableValue<T>) => void;
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>;
}
type FetchValueFunc = () => Promise<void>;
function createFetchValueFunc<T>( function createFetchValueFunc<T>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>, atomEffectParam: AtomEffectParam<LoadableValue<T>>,
guildId: number, guildId: number,
node: RecoilState<LoadableValue<T>>,
setSelf: (loadableValue: LoadableValue<T>) => void,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>, fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
): () => Promise<void> { ): FetchValueFunc {
const { node, setSelf, getPromise } = atomEffectParam;
const fetchValueFunc = async () => { 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)); 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); 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
@ -138,47 +132,59 @@ function createFetchValueFunc<T>(
return fetchValueFunc; return fetchValueFunc;
} }
function applyNew<T>(value: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] { // Useful for new-xxx, update-xxx, remove-xxx, conflict-xxx list events
return value.concat(newElements).sort(sortFunc); function createEventHandler<
} V, // e.g. LoadableValue<Member[]>
function applyUpdated<T extends { id: string }>(value: T[], updatedElements: T[], sortFunc: (a: T, b: T) => number): T[] { ArgsMapResult, // e.g. Member[]
return value.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc); Addins, // e.g. FetchValueFunc to be called later to re-load members
} XE extends keyof (Connectable | Conflictable) // e.g. new-members
function applyRemoved<T extends { id: string }>(value: T[], removedElements: T[]): T[] { >(
const removedIds = new Set<string>(removedElements.map(removedElement => removedElement.id)); atomEffectParam: AtomEffectParam<V>,
return value.filter(element => !removedIds.has(element.id)); 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<T extends { id: string }>(value: T[], changes: Changes<T>, sortFunc: (a: T, b: T) => number): T[] { interface SingleEventMappingParams<
const removedIds = new Set<string>(changes.deleted.map(deletedElement => deletedElement.id)); T,
return value V,
.concat(changes.added) Addins,
.map(element => changes.updated.find(updatedPair => updatedPair.newDataPoint.id === element.id)?.newDataPoint ?? element) UE extends keyof Connectable,
.filter(element => !removedIds.has(element.id)) CE extends keyof Conflictable
.sort(sortFunc); > {
updatedEvent: {
name: UE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T>;
applyFunc: (selfState: V, argsResult: Defined<T>, addins: Addins) => V;
},
conflictEvent: {
name: CE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Defined<T>;
applyFunc: (selfState: V, argsResult: Defined<T>, addins: Addins) => V;
} }
interface SingleEventMappingParams<T, UE extends keyof Connectable, CE extends keyof Conflictable> {
updatedEventName: UE;
updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T>;
updatedEventCondition?: (value: T) => boolean;
conflictEventName: CE;
conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Defined<T>;
conflictEventCondition?: (value: T) => boolean;
} }
function listenToSingle< function listenToSingle<
T, // e.g. GuildMetadata T, // e.g. GuildMetadata
V, // e.g. LoadableValue<GuildMetadata>
UE extends keyof Connectable, // Update Event UE extends keyof Connectable, // Update Event
CE extends keyof Conflictable, // Conflict Event CE extends keyof Conflictable, // Conflict Event
>( >(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>, atomEffectParam: AtomEffectParam<V>,
guildId: number, guildId: number,
node: RecoilValue<LoadableValue<T>>, eventMapping: SingleEventMappingParams<T, V, FetchValueFunc, UE, CE>,
setSelf: (loadableValue: LoadableValue<T>) => void, fetchValueFunc: FetchValueFunc,
fetchValueFunc: () => Promise<void>,
eventMapping: SingleEventMappingParams<T, UE, CE>
) { ) {
const { getPromise } = atomEffectParam;
// Listen for updates // Listen for updates
let guild: CombinedGuild | null = null; let guild: CombinedGuild | null = null;
let onUpdateFunc: (Connectable & Conflictable)[UE] | 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 (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 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 onUpdateFunc = createEventHandler(atomEffectParam, eventMapping.updatedEvent.argsMap, eventMapping.updatedEvent.applyFunc, fetchValueFunc);
function createConnectableHandler<XE extends keyof (Connectable | Conflictable)>( onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc, fetchValueFunc);
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T>,
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];
}
// I think the typed EventEmitter class isn't ready for this level of insane type safety guild.on(eventMapping.updatedEvent.name, onUpdateFunc);
// otherwise, I may have done this wrong. Forcing it to work with these calls guild.on(eventMapping.conflictEvent.name, onConflictFunc);
onUpdateFunc = createConnectableHandler(eventMapping.updatedEventArgsMap, eventMapping.updatedEventCondition ?? (() => true));
onConflictFunc = createConnectableHandler(eventMapping.conflictEventArgsMap, eventMapping.conflictEventCondition ?? (() => true));
guild.on(eventMapping.updatedEventName, onUpdateFunc);
guild.on(eventMapping.conflictEventName, onConflictFunc);
})(); })();
const cleanup = () => { const cleanup = () => {
closed = true; closed = true;
if (guild && onUpdateFunc) guild.off(eventMapping.updatedEventName, onUpdateFunc); if (guild && onUpdateFunc) guild.off(eventMapping.updatedEvent.name, onUpdateFunc);
if (guild && onConflictFunc) guild.off(eventMapping.conflictEventName, onConflictFunc); if (guild && onConflictFunc) guild.off(eventMapping.conflictEvent.name, onConflictFunc);
} }
return cleanup; return cleanup;
} }
interface MultipleEventMappingParams< interface MultipleEventMappingParams<
T, T,
V,
Addins,
NE extends keyof Connectable, NE extends keyof Connectable,
UE extends keyof Connectable, UE extends keyof Connectable,
RE extends keyof Connectable, RE extends keyof Connectable,
CE extends keyof Conflictable CE extends keyof Conflictable
> { > {
newEventName: NE; newEvent: {
newEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined<T[]>; name: NE;
newEventCondition?: (eventArgsResult: Defined<T[]>) => boolean; argsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined<T[]>;
updatedEventName: UE; applyFunc: (selfState: V, argsResult: Defined<T[]>, addins: Addins) => V;
updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T[]>; },
updatedEventCondition?: (eventArgsResult: Defined<T[]>) => boolean; updatedEvent: {
removedEventName: RE; name: UE;
removedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined<T[]>; argsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T[]>;
removedEventCondition?: (eventArgsResult: Defined<T[]>) => boolean; applyFunc: (selfState: V, argsResult: Defined<T[]>, addins: Addins) => V;
conflictEventName: CE; },
conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes<T>; removedEvent: {
conflictEventCondition?: (eventArgsResult: Changes<T>) => boolean; name: RE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined<T[]>;
applyFunc: (selfState: V, argsResult: Defined<T[]>, addins: Addins) => V;
},
conflictEvent: {
name: CE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes<T>;
applyFunc: (selfState: V, argsResult: Changes<T>, addins: Addins) => V;
},
} }
function listenToMultiple< function listenToMultiple<
T extends { id: string }, T extends { id: string }, // e.g. Member
V, // e.g. LoadableValue<Member[]>
NE extends keyof Connectable, // New Event NE extends keyof Connectable, // New Event
UE extends keyof Connectable, // Update Event UE extends keyof Connectable, // Update Event
RE extends keyof Connectable, // Remove Event RE extends keyof Connectable, // Remove Event
CE extends keyof Conflictable // Conflict Event CE extends keyof Conflictable // Conflict Event
>( >(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>, atomEffectParam: AtomEffectParam<V>,
guildId: number, guildId: number,
node: RecoilValue<LoadableValue<T[]>>, eventMapping: MultipleEventMappingParams<T, V, FetchValueFunc, NE, UE, RE, CE>,
setSelf: (loadableValue: LoadableValue<T[]>) => void, fetchValueFunc: FetchValueFunc,
fetchValueFunc: () => Promise<void>,
sortFunc: (a: T, b: T) => number,
eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>
) { ) {
const { getPromise } = atomEffectParam;
// Listen for updates // Listen for updates
let guild: CombinedGuild | null = null; let guild: CombinedGuild | null = null;
let onNewFunc: (Connectable & Conflictable)[NE] | 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 (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 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 onNewFunc = createEventHandler(atomEffectParam, eventMapping.newEvent.argsMap, eventMapping.newEvent.applyFunc, fetchValueFunc);
function createConnectableHandler<XE extends keyof (Connectable | Conflictable)>( onUpdateFunc = createEventHandler(atomEffectParam, eventMapping.updatedEvent.argsMap, eventMapping.updatedEvent.applyFunc, fetchValueFunc);
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T[]>, onRemoveFunc = createEventHandler(atomEffectParam, eventMapping.removedEvent.argsMap, eventMapping.removedEvent.applyFunc, fetchValueFunc);
condition: (eventArgsResult: Defined<T[]>) => boolean, onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc, fetchValueFunc);
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];
}
// Useful for conflict-xxx list events guild.on(eventMapping.newEvent.name, onNewFunc);
function createConflictableHandler<XE extends keyof (Connectable | Conflictable)>( guild.on(eventMapping.updatedEvent.name, onUpdateFunc);
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes<T>, guild.on(eventMapping.removedEvent.name, onRemoveFunc);
condition: (eventArgsResult: Changes<T>) => boolean, guild.on(eventMapping.conflictEvent.name, onConflictFunc);
applyFunc: (value: T[], eventArgsResult: Changes<T>, 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);
})(); })();
const cleanup = () => { const cleanup = () => {
closed = true; closed = true;
if (guild && onNewFunc) guild.off(eventMapping.newEventName, onNewFunc); if (guild && onNewFunc) guild.off(eventMapping.newEvent.name, onNewFunc);
if (guild && onUpdateFunc) guild.off(eventMapping.updatedEventName, onUpdateFunc); if (guild && onUpdateFunc) guild.off(eventMapping.updatedEvent.name, onUpdateFunc);
if (guild && onRemoveFunc) guild.off(eventMapping.removedEventName, onRemoveFunc); if (guild && onRemoveFunc) guild.off(eventMapping.removedEvent.name, onRemoveFunc);
if (guild && onConflictFunc) guild.off(eventMapping.conflictEventName, onConflictFunc); if (guild && onConflictFunc) guild.off(eventMapping.conflictEvent.name, onConflictFunc);
} }
return cleanup; return cleanup;
} }
function singleGuildSubscriptionEffect< function guildDataSubscriptionLoadableSingleEffect<
T, // e.g. GuildMetadata T, // e.g. GuildMetadata
UE extends keyof Connectable, // Update Event UE extends keyof Connectable, // Update Event
CE extends keyof Conflictable, // Conflict Event CE extends keyof Conflictable, // Conflict Event
>( >(
guildId: number, guildId: number,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>, fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
eventMapping: SingleEventMappingParams<T, UE, CE>, eventMapping: SingleEventMappingParams<T, LoadableValue<T>, FetchValueFunc, UE, CE>,
skipFunc?: () => boolean skipFunc?: () => boolean
) { ) {
const effect: AtomEffect<LoadableValue<T>> = ({ node, trigger, setSelf, getPromise }) => { const effect: AtomEffect<LoadableValue<T>> = (atomEffectParam: AtomEffectParam<LoadableValue<T>>) => {
const { trigger } = atomEffectParam;
if (skipFunc && skipFunc()) return; // Don't run if this atom should be skipped for some reason (e.g. null resourceId) 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 // Fetch initial value on first get
if (trigger === 'get') { if (trigger === 'get') {
@ -346,7 +309,7 @@ function singleGuildSubscriptionEffect<
} }
// Listen to changes // Listen to changes
const cleanup = listenToSingle(getPromise, guildId, node, setSelf, fetchValueFunc, eventMapping); const cleanup = listenToSingle(atomEffectParam, guildId, eventMapping, fetchValueFunc);
return () => { return () => {
cleanup(); cleanup();
@ -355,7 +318,7 @@ function singleGuildSubscriptionEffect<
return effect; return effect;
} }
function multipleGuildSubscriptionEffect< function guildDataSubscriptionLoadableMultipleEffect<
T extends { id: string }, T extends { id: string },
NE extends keyof Connectable, NE extends keyof Connectable,
UE extends keyof Connectable, UE extends keyof Connectable,
@ -364,11 +327,12 @@ function multipleGuildSubscriptionEffect<
>( >(
guildId: number, guildId: number,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T[]>>, fetchFunc: (guild: CombinedGuild) => Promise<Defined<T[]>>,
sortFunc: (a: T, b: T) => number, eventMapping: MultipleEventMappingParams<T, LoadableValue<T[]>, FetchValueFunc, NE, UE, RE, CE>,
eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>,
) { ) {
const effect: AtomEffect<LoadableValue<T[]>> = ({ node, trigger, setSelf, getPromise }) => { const effect: AtomEffect<LoadableValue<T[]>> = (atomEffectParam) => {
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc); const { trigger } = atomEffectParam;
const fetchValueFunc = createFetchValueFunc(atomEffectParam, guildId, fetchFunc);
// Fetch initial value on first get // Fetch initial value on first get
if (trigger === 'get') { if (trigger === 'get') {
@ -376,7 +340,7 @@ function multipleGuildSubscriptionEffect<
} }
// Listen to changes // Listen to changes
const cleanup = listenToMultiple(getPromise, guildId, node, setSelf, fetchValueFunc, sortFunc, eventMapping); const cleanup = listenToMultiple(atomEffectParam, guildId, eventMapping, fetchValueFunc);
return () => { return () => {
cleanup(); cleanup();
@ -385,36 +349,76 @@ function multipleGuildSubscriptionEffect<
return effect; return effect;
} }
interface ScrollingList<T> { // TODO: ScrollableLoadableValue
list: T[]; // interface ScrollingList<T> {
hasMoreAbove: boolean; // list: T[];
hasMoreBelow: boolean; // hasMoreAbove: boolean;
} // hasMoreBelow: boolean;
function multipleScrollingGuildSubscriptionEffect< // }
T extends { id: string }, // function multipleScrollingGuildSubscriptionEffect<
NE extends keyof Connectable, // New Event // T extends { id: string },
UE extends keyof Connectable, // Update Event // NE extends keyof Connectable, // New Event
RE extends keyof Connectable, // Remove Event // UE extends keyof Connectable, // Update Event
CE extends keyof Conflictable // Conflict Event // RE extends keyof Connectable, // Remove Event
>( // CE extends keyof Conflictable // Conflict Event
guildId: number, // >(
fetchBottomFunc: (guild: CombinedGuild, count: number) => Promise<T[]>, // guildId: number,
fetchAboveFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[] | null>, // fetchBottomFunc: (guild: CombinedGuild, count: number) => Promise<T[]>,
fetchBelowFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[] | null>, // fetchAboveFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[] | null>,
fetchCount: number, // NOTE: If a fetch returns less than this number of elements, we will no longer try to get more above/below it // fetchBelowFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[] | null>,
maxElements: number, // The maximum number of elements in the scroller. Must be greater than maxFetchElements // fetchCount: number, // NOTE: If a fetch returns less than this number of elements, we will no longer try to get more above/below it
sortFunc: (a: T, b: T) => number, // maxElements: number, // The maximum number of elements in the scroller. Must be greater than maxFetchElements
eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE> // sortFunc: (a: T, b: T) => number,
) { // eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>
const effect: AtomEffect<LoadableValue<ScrollingList<T>>> = ({ node, trigger, setSelf, getPromise }) => { // ) {
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, async (guild: CombinedGuild) => { // const effect: AtomEffect<LoadableValue<ScrollingList<T>>> = ({ node, trigger, setSelf, getPromise }) => {
const list = await fetchBottomFunc(guild, fetchCount); // const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, async (guild: CombinedGuild) => {
return { list, hasMoreBelow: false, hasMoreAbove: list.length <= fetchCount }; // 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<T>(list: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] {
return list.concat(newElements).sort(sortFunc);
}
function applyUpdatedElements<T extends { id: string }>(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<T extends { id: string }>(list: T[], removedElements: T[]): T[] {
const removedIds = new Set<string>(removedElements.map(removedElement => removedElement.id));
return list.filter(element => !removedIds.has(element.id));
}
function applyChangedElements<T extends { id: string }>(list: T[], changes: Changes<T>, sortFunc: (a: T, b: T) => number): T[] {
const removedIds = new Set<string>(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<T>(selfState: LoadableValue<T>, argsResult: Defined<T>, fetchValueFunc: FetchValueFunc): LoadableValue<T> {
if (!isLoaded(selfState)) return selfState;
return createLoadedValue(argsResult, fetchValueFunc);
}
function applyListFuncIfLoaded<T extends { id: string }, A>(
applyFunc: (list: T[], argsResult: A, sortFunc: (a: T, b: T) => number) => T[],
sortFunc: (a: T, b: T) => number
): (
selfState: LoadableValue<T[]>,
argsResult: A,
fetchValueFunc: FetchValueFunc
) => LoadableValue<T[]> {
return (selfState: LoadableValue<T[]>, argsResult: A, fetchValueFunc: FetchValueFunc) => {
if (!isLoaded(selfState)) return selfState;
return createLoadedValue(applyFunc(selfState.value, argsResult, sortFunc), fetchValueFunc);
}; };
return effect;
} }
// You probably want currGuildMetaState // You probably want currGuildMetaState
@ -422,14 +426,20 @@ 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( guildDataSubscriptionLoadableSingleEffect(
guildId, guildId,
async (guild: CombinedGuild) => await guild.fetchMetadata(), async (guild: CombinedGuild) => await guild.fetchMetadata(),
{ {
updatedEventName: 'update-metadata', updatedEvent: {
updatedEventArgsMap: (newMeta: GuildMetadata) => newMeta, name: 'update-metadata',
conflictEventName: 'conflict-metadata', argsMap: (newMeta: GuildMetadata) => newMeta,
conflictEventArgsMap: (_changesType: AutoVerifierChangesType, _oldMeta: GuildMetadata, 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<LoadableValue<Resource>, { guildId:
key: 'guildPotentialResourceState', key: 'guildPotentialResourceState',
default: DEF_UNLOADED_VALUE, default: DEF_UNLOADED_VALUE,
effects_UNSTABLE: (param: { guildId: number, resourceId: string | null }) => [ effects_UNSTABLE: (param: { guildId: number, resourceId: string | null }) => [
singleGuildSubscriptionEffect( guildDataSubscriptionLoadableSingleEffect(
param.guildId, param.guildId,
async (guild: CombinedGuild) => { async (guild: CombinedGuild) => {
if (param.resourceId === null) { // Should never happen because of skipFunc (last argument) if (param.resourceId === null) { // Should never happen because of skipFunc (last argument)
@ -452,12 +462,16 @@ export const guildResourceState = atomFamily<LoadableValue<Resource>, { guildId:
} }
}, },
{ {
updatedEventName: 'update-resource', updatedEvent: {
updatedEventArgsMap: (updatedResource: Resource) => updatedResource, name: 'update-resource',
updatedEventCondition: (resource: Resource) => resource.id === param.resourceId, argsMap: (updatedResource: Resource) => updatedResource,
conflictEventName: 'conflict-resource', applyFunc: applyIfLoaded
conflictEventArgsMap: (_query: IDQuery, _changeType: AutoVerifierChangesType, _oldResource: Resource, newResource: Resource) => newResource, },
conflictEventCondition: (resource: Resource) => resource.id === param.resourceId, conflictEvent: {
name: 'conflict-resource',
argsMap: (_query, _changeType, _oldResource: Resource, newResource: Resource) => newResource,
applyFunc: applyIfLoaded
}
}, },
() => param.resourceId === null // Never load/bind if resourceId === null () => param.resourceId === null // Never load/bind if resourceId === null
) )
@ -479,19 +493,30 @@ 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( guildDataSubscriptionLoadableMultipleEffect(
guildId, guildId,
async (guild: CombinedGuild) => await guild.fetchMembers(), async (guild: CombinedGuild) => await guild.fetchMembers(),
Member.sortForList,
{ {
newEventName: 'new-members', newEvent: {
newEventArgsMap: (newMembers: Member[]) => newMembers, name: 'new-members',
updatedEventName: 'update-members', argsMap: (newMembers: Member[]) => newMembers,
updatedEventArgsMap: (updatedMembers: Member[]) => updatedMembers, applyFunc: applyListFuncIfLoaded(applyNewElements, Member.sortForList)
removedEventName: 'remove-members', },
removedEventArgsMap: (removedMembers: Member[]) => removedMembers, updatedEvent: {
conflictEventName: 'conflict-members', name: 'update-members',
conflictEventArgsMap: (_changesType: AutoVerifierChangesType, changes: Changes<Member>) => changes, 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<Member>) => changes,
applyFunc: applyListFuncIfLoaded(applyChangedElements, Member.sortForList)
},
} }
) )
], ],
@ -526,19 +551,30 @@ 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( guildDataSubscriptionLoadableMultipleEffect(
guildId, guildId,
async (guild: CombinedGuild) => await guild.fetchChannels(), async (guild: CombinedGuild) => await guild.fetchChannels(),
Channel.sortByIndex,
{ {
newEventName: 'new-channels', newEvent: {
newEventArgsMap: (newChannels: Channel[]) => newChannels, name: 'new-channels',
updatedEventName: 'update-channels', argsMap: (newChannels: Channel[]) => newChannels,
updatedEventArgsMap: (updatedChannels: Channel[]) => updatedChannels, applyFunc: applyListFuncIfLoaded(applyNewElements, Channel.sortByIndex)
removedEventName: 'remove-channels', },
removedEventArgsMap: (removedChannels: Channel[]) => removedChannels, updatedEvent: {
conflictEventName: 'conflict-channels', name: 'update-channels',
conflictEventArgsMap: (_changesType: AutoVerifierChangesType, changes: Changes<Channel>) => changes 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<Channel>) => changes,
applyFunc: applyListFuncIfLoaded(applyChangedElements, Channel.sortByIndex)
},
} }
) )
], ],
@ -569,57 +605,68 @@ const guildActiveChannelState = selectorFamily<LoadableValue<Channel>, number>({
dangerouslyAllowMutability: true dangerouslyAllowMutability: true
}); });
export const guildChannelMessagesState = atomFamily<LoadableValue<ScrollingList<Message>>, { guildId: number, channelId: string }>({ // export const guildChannelMessagesState = atomFamily<LoadableValue<ScrollingList<Message>>, { guildId: number, channelId: string }>({
key: 'guildChannelMessagesState', // key: 'guildChannelMessagesState',
default: DEF_UNLOADED_VALUE, // default: DEF_UNLOADED_VALUE,
effects_UNSTABLE: ({ guildId, channelId }) => [ // effects_UNSTABLE: ({ guildId, channelId }) => [
multipleScrollingGuildSubscriptionEffect( // multipleScrollingGuildSubscriptionEffect(
guildId, // guildId,
async (guild: CombinedGuild, count: number) => await guild.fetchMessagesRecent(channelId, count), // 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.fetchMessagesBefore(channelId, reference._order, count),
async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesAfter(channelId, reference._order, count), // async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesAfter(channelId, reference._order, count),
Globals.MESSAGES_PER_REQUEST, // Globals.MESSAGES_PER_REQUEST,
Globals.MAX_CURRENT_MESSAGES, // Globals.MAX_CURRENT_MESSAGES,
Message.sortOrder, // Message.sortOrder,
{ // {
newEventName: 'new-messages', // newEventName: 'new-messages',
newEventArgsMap: (newMessages: Message[]) => newMessages.filter(message => message.channel.id === channelId), // newEventArgsMap: (newMessages: Message[]) => newMessages.filter(message => message.channel.id === channelId),
newEventCondition: (messages: Message[]) => messages.length > 0, // newEventCondition: (messages: Message[]) => messages.length > 0,
updatedEventName: 'update-messages', // updatedEventName: 'update-messages',
updatedEventArgsMap: (updatedMessages: Message[]) => updatedMessages.filter(message => message.channel.id === channelId), // updatedEventArgsMap: (updatedMessages: Message[]) => updatedMessages.filter(message => message.channel.id === channelId),
updatedEventCondition: (messages: Message[]) => messages.length > 0, // updatedEventCondition: (messages: Message[]) => messages.length > 0,
removedEventName: 'remove-messages', // removedEventName: 'remove-messages',
removedEventArgsMap: (removedMessages: Message[]) => removedMessages.filter(message => message.channel.id === channelId), // removedEventArgsMap: (removedMessages: Message[]) => removedMessages.filter(message => message.channel.id === channelId),
removedEventCondition: (messages: Message[]) => messages.length > 0, // removedEventCondition: (messages: Message[]) => messages.length > 0,
conflictEventName: 'conflict-messages', // conflictEventName: 'conflict-messages',
conflictEventArgsMap: (_query: PartialMessageListQuery, _changesType: AutoVerifierChangesType, changes: Changes<Message>) => ({ // conflictEventArgsMap: (_query: PartialMessageListQuery, _changesType: AutoVerifierChangesType, changes: Changes<Message>) => ({
added: changes.added.filter(message => message.channel.id === channelId), // added: changes.added.filter(message => message.channel.id === channelId),
updated: changes.updated.filter(change => change.newDataPoint.channel.id === channelId), // updated: changes.updated.filter(change => change.newDataPoint.channel.id === channelId),
deleted: changes.deleted.filter(message => message.channel.id === channelId), // deleted: changes.deleted.filter(message => message.channel.id === channelId),
}), // }),
conflictEventCondition: (changes: Changes<Message>) => changes.added.length + changes.updated.length + changes.deleted.length > 0, // conflictEventCondition: (changes: Changes<Message>) => changes.added.length + changes.updated.length + changes.deleted.length > 0,
} // }
) // )
] // ]
}); // });
export const guildTokensState = atomFamily<LoadableValue<Token[]>, number>({ export const guildTokensState = atomFamily<LoadableValue<Token[]>, number>({
key: 'guildTokensState', key: 'guildTokensState',
default: DEF_UNLOADED_VALUE, default: DEF_UNLOADED_VALUE,
effects_UNSTABLE: (guildId: number) => [ effects_UNSTABLE: (guildId: number) => [
multipleGuildSubscriptionEffect( guildDataSubscriptionLoadableMultipleEffect(
guildId, guildId,
async (guild: CombinedGuild) => await guild.fetchTokens(), async (guild: CombinedGuild) => await guild.fetchTokens(),
Token.sortRecentCreatedFirst,
{ {
newEventName: 'new-tokens', newEvent: {
newEventArgsMap: (newTokens: Token[]) => newTokens, name: 'new-tokens',
updatedEventName: 'update-tokens', argsMap: (newTokens: Token[]) => newTokens,
updatedEventArgsMap: (updatedTokens: Token[]) => updatedTokens, applyFunc: applyListFuncIfLoaded(applyNewElements, Token.sortRecentCreatedFirst)
removedEventName: 'remove-tokens', },
removedEventArgsMap: (removedTokens: Token[]) => removedTokens, updatedEvent: {
conflictEventName: 'conflict-tokens', name: 'update-tokens',
conflictEventArgsMap: (_changesType: AutoVerifierChangesType, changes: Changes<Token>) => changes 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<Token>) => changes,
applyFunc: applyListFuncIfLoaded(applyChangedElements, Token.sortRecentCreatedFirst)
},
} }
) )
] ]