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 { 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> = 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 AtomEffectParam<T> = Arguments<AtomEffect<T>>[0];
export type UnloadedValue = {
value: undefined;
error: undefined;
@ -104,24 +103,19 @@ export const allGuildsState = atom<CombinedGuild[] | null>({
dangerouslyAllowMutability: true
});
interface RecoilLoadableAtomEffectParams<T> {
node: RecoilState<LoadableValue<T>>,
trigger: 'get' | 'set';
setSelf: (loadableValue: LoadableValue<T>) => void;
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>;
}
// 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<void>;
function createFetchValueFunc<T>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
atomEffectParam: AtomEffectParam<LoadableValue<T>>,
guildId: number,
node: RecoilState<LoadableValue<T>>,
setSelf: (loadableValue: LoadableValue<T>) => void,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
): () => Promise<void> {
): 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<T>(
return fetchValueFunc;
}
function applyNew<T>(value: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] {
return value.concat(newElements).sort(sortFunc);
}
function applyUpdated<T extends { id: string }>(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<T extends { id: string }>(value: T[], removedElements: T[]): T[] {
const removedIds = new Set<string>(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<Member[]>
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<V>,
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[] {
const removedIds = new Set<string>(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<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;
interface SingleEventMappingParams<
T,
V,
Addins,
UE extends keyof Connectable,
CE extends keyof Conflictable
> {
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;
}
}
function listenToSingle<
T, // e.g. GuildMetadata
V, // e.g. LoadableValue<GuildMetadata>
UE extends keyof Connectable, // Update Event
CE extends keyof Conflictable, // Conflict Event
>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
atomEffectParam: AtomEffectParam<V>,
guildId: number,
node: RecoilValue<LoadableValue<T>>,
setSelf: (loadableValue: LoadableValue<T>) => void,
fetchValueFunc: () => Promise<void>,
eventMapping: SingleEventMappingParams<T, UE, CE>
eventMapping: SingleEventMappingParams<T, V, FetchValueFunc, UE, CE>,
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<XE extends keyof (Connectable | Conflictable)>(
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];
}
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<T[]>;
newEventCondition?: (eventArgsResult: Defined<T[]>) => boolean;
updatedEventName: UE;
updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T[]>;
updatedEventCondition?: (eventArgsResult: Defined<T[]>) => boolean;
removedEventName: RE;
removedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined<T[]>;
removedEventCondition?: (eventArgsResult: Defined<T[]>) => boolean;
conflictEventName: CE;
conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes<T>;
conflictEventCondition?: (eventArgsResult: Changes<T>) => boolean;
newEvent: {
name: NE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined<T[]>;
applyFunc: (selfState: V, argsResult: Defined<T[]>, addins: Addins) => V;
},
updatedEvent: {
name: UE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T[]>;
applyFunc: (selfState: V, argsResult: Defined<T[]>, addins: Addins) => V;
},
removedEvent: {
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<
T extends { id: string },
T extends { id: string }, // e.g. Member
V, // e.g. LoadableValue<Member[]>
NE extends keyof Connectable, // New Event
UE extends keyof Connectable, // Update Event
RE extends keyof Connectable, // Remove Event
CE extends keyof Conflictable // Conflict Event
>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
atomEffectParam: AtomEffectParam<V>,
guildId: number,
node: RecoilValue<LoadableValue<T[]>>,
setSelf: (loadableValue: LoadableValue<T[]>) => void,
fetchValueFunc: () => Promise<void>,
sortFunc: (a: T, b: T) => number,
eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>
eventMapping: MultipleEventMappingParams<T, V, FetchValueFunc, NE, UE, RE, CE>,
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<XE extends keyof (Connectable | Conflictable)>(
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T[]>,
condition: (eventArgsResult: Defined<T[]>) => 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<XE extends keyof (Connectable | Conflictable)>(
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes<T>,
condition: (eventArgsResult: Changes<T>) => boolean,
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);
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<Defined<T>>,
eventMapping: SingleEventMappingParams<T, UE, CE>,
eventMapping: SingleEventMappingParams<T, LoadableValue<T>, FetchValueFunc, UE, CE>,
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)
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<Defined<T[]>>,
sortFunc: (a: T, b: T) => number,
eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>,
eventMapping: MultipleEventMappingParams<T, LoadableValue<T[]>, FetchValueFunc, NE, UE, RE, CE>,
) {
const effect: AtomEffect<LoadableValue<T[]>> = ({ node, trigger, setSelf, getPromise }) => {
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc);
const effect: AtomEffect<LoadableValue<T[]>> = (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<T> {
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<T[]>,
fetchAboveFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[] | null>,
fetchBelowFunc: (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
maxElements: number, // The maximum number of elements in the scroller. Must be greater than maxFetchElements
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 list = await fetchBottomFunc(guild, fetchCount);
return { list, hasMoreBelow: false, hasMoreAbove: list.length <= fetchCount };
});
// TODO: ScrollableLoadableValue
// interface ScrollingList<T> {
// 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<T[]>,
// fetchAboveFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[] | null>,
// fetchBelowFunc: (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
// maxElements: number, // The maximum number of elements in the scroller. Must be greater than maxFetchElements
// 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 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
@ -422,14 +426,20 @@ export const guildMetaState = atomFamily<LoadableValue<GuildMetadata>, 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<LoadableValue<Resource>, { 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<LoadableValue<Resource>, { 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<LoadableValue<Member[]>, 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<Member>) => 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<Member>) => changes,
applyFunc: applyListFuncIfLoaded(applyChangedElements, Member.sortForList)
},
}
)
],
@ -526,19 +551,30 @@ const guildChannelsState = atomFamily<LoadableValue<Channel[]>, 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<Channel>) => 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<Channel>) => changes,
applyFunc: applyListFuncIfLoaded(applyChangedElements, Channel.sortByIndex)
},
}
)
],
@ -569,57 +605,68 @@ const guildActiveChannelState = selectorFamily<LoadableValue<Channel>, number>({
dangerouslyAllowMutability: true
});
export const guildChannelMessagesState = atomFamily<LoadableValue<ScrollingList<Message>>, { 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<Message>) => ({
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<Message>) => changes.added.length + changes.updated.length + changes.deleted.length > 0,
}
)
]
});
// export const guildChannelMessagesState = atomFamily<LoadableValue<ScrollingList<Message>>, { 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<Message>) => ({
// 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<Message>) => changes.added.length + changes.updated.length + changes.deleted.length > 0,
// }
// )
// ]
// });
export const guildTokensState = atomFamily<LoadableValue<Token[]>, 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<Token>) => 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<Token>) => changes,
applyFunc: applyListFuncIfLoaded(applyChangedElements, Token.sortRecentCreatedFirst)
},
}
)
]