refactor listeners in atoms
This commit is contained in:
parent
dfd50c7e20
commit
601a5bd3d6
@ -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)
|
||||
},
|
||||
}
|
||||
)
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user