preparing for channel messages

This commit is contained in:
Michael Peters 2022-02-05 11:05:31 -06:00
parent 54ad823ebe
commit dfd50c7e20

View File

@ -4,14 +4,15 @@ import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole); const LOG = Logger.create(__filename, electronConsole);
import { ReactNode, useEffect } from "react"; import { ReactNode, useEffect } from "react";
import { atom, 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, Resource, ShouldNeverHappenError, Token } from "../../data-types"; import { Changes, Channel, GuildMetadata, Member, Message, 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 { AutoVerifierChangesType } from '../../auto-verifier';
import { Conflictable, Connectable } from '../../guild-types'; import { Conflictable, Connectable } from '../../guild-types';
import { IDQuery } from '../../auto-verifier-with-args'; 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;
@ -113,7 +114,7 @@ interface RecoilLoadableAtomEffectParams<T> {
function createFetchValueFunc<T>( function createFetchValueFunc<T>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>, getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number, guildId: number,
node: RecoilValue<LoadableValue<T>>, node: RecoilState<LoadableValue<T>>,
setSelf: (loadableValue: LoadableValue<T>) => void, setSelf: (loadableValue: LoadableValue<T>) => void,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>, fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
): () => Promise<void> { ): () => Promise<void> {
@ -185,7 +186,7 @@ function listenToSingle<
let closed = false; let closed = false;
(async () => { (async () => {
guild = await getPromise(guildState(guildId)); guild = await getPromise(guildState(guildId));
if (guild === null) return; // TODO: This would put the atom in an infinite loading state. Look into useCallback for a potential way to prevent this... 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 // Creates an event handler that directly applies the result of the eventArgsMap as a loadedValue into self
@ -193,8 +194,7 @@ function listenToSingle<
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T>, eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T>,
condition: (value: T) => boolean condition: (value: T) => boolean
): (Connectable & Conflictable)[XE] { ): (Connectable & Conflictable)[XE] {
return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { return (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => {
const selfState = await getPromise(node); const selfState = await getPromise(node);
if (isLoaded(selfState)) { if (isLoaded(selfState)) {
const value = eventArgsMap(...args); const value = eventArgsMap(...args);
@ -202,7 +202,6 @@ function listenToSingle<
setSelf(createLoadedValue(value, fetchValueFunc)); setSelf(createLoadedValue(value, fetchValueFunc));
} }
} }
})();
}) as (Connectable & Conflictable)[XE]; }) as (Connectable & Conflictable)[XE];
} }
@ -230,12 +229,16 @@ interface MultipleEventMappingParams<
> { > {
newEventName: NE; newEventName: NE;
newEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined<T[]>; newEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined<T[]>;
newEventCondition?: (eventArgsResult: Defined<T[]>) => boolean;
updatedEventName: UE; updatedEventName: UE;
updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T[]>; updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T[]>;
updatedEventCondition?: (eventArgsResult: Defined<T[]>) => boolean;
removedEventName: RE; removedEventName: RE;
removedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined<T[]>; removedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined<T[]>;
removedEventCondition?: (eventArgsResult: Defined<T[]>) => boolean;
conflictEventName: CE; conflictEventName: CE;
conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes<T>; conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes<T>;
conflictEventCondition?: (eventArgsResult: Changes<T>) => boolean;
} }
function listenToMultiple< function listenToMultiple<
T extends { id: string }, T extends { id: string },
@ -261,49 +264,51 @@ function listenToMultiple<
let closed = false; let closed = false;
(async () => { (async () => {
guild = await getPromise(guildState(guildId)); guild = await getPromise(guildState(guildId));
if (guild === null) return; 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 // Useful for new-xxx, update-xxx, remove-xxx list events
function createConnectableHandler<XE extends keyof (Connectable | Conflictable)>( function createConnectableHandler<XE extends keyof (Connectable | Conflictable)>(
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T[]>, 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[], applyFunc: (value: T[], eventArgsResult: T[], sortFunc: (a: T, b: T) => number) => T[],
): (Connectable & Conflictable)[XE] { ): (Connectable & Conflictable)[XE] {
// I think the typed EventEmitter class isn't ready for this level of insane type safety // I think the typed EventEmitter class isn't ready for this level of insane type safety
// otherwise, I may have done this wrong. Forcing it to work with these calls // otherwise, I may have done this wrong. Forcing it to work with these calls
return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { return (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => {
const selfState = await getPromise(node); const selfState = await getPromise(node);
if (isLoaded(selfState)) { if (isLoaded(selfState)) {
const eventArgsResult = eventArgsMap(...args); const eventArgsResult = eventArgsMap(...args);
if (condition(eventArgsResult)) {
const value = applyFunc(selfState.value, eventArgsResult, sortFunc); const value = applyFunc(selfState.value, eventArgsResult, sortFunc);
setSelf(createLoadedValue(value, fetchValueFunc)); setSelf(createLoadedValue(value, fetchValueFunc));
} }
})(); }
}) as (Connectable & Conflictable)[XE]; }) as (Connectable & Conflictable)[XE];
} }
// Useful for conflict-xxx list events // Useful for conflict-xxx list events
function createConflictableHandler<XE extends keyof (Connectable | Conflictable)>( function createConflictableHandler<XE extends keyof (Connectable | Conflictable)>(
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes<T>, 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[], applyFunc: (value: T[], eventArgsResult: Changes<T>, sortFunc: (a: T, b: T) => number) => T[],
): (Connectable & Conflictable)[XE] { ): (Connectable & Conflictable)[XE] {
return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { return (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => {
const selfState = await getPromise(node); const selfState = await getPromise(node);
if (isLoaded(selfState)) { if (isLoaded(selfState)) {
const eventArgsResult = eventArgsMap(...args); const eventArgsResult = eventArgsMap(...args);
if (condition(eventArgsResult)) {
const value = applyFunc(selfState.value, eventArgsResult, sortFunc); const value = applyFunc(selfState.value, eventArgsResult, sortFunc);
setSelf(createLoadedValue(value, fetchValueFunc)); setSelf(createLoadedValue(value, fetchValueFunc));
} }
})(); }
}) as (Connectable & Conflictable)[XE]; }) as (Connectable & Conflictable)[XE];
} }
onNewFunc = createConnectableHandler(eventMapping.newEventArgsMap, applyNew); onNewFunc = createConnectableHandler(eventMapping.newEventArgsMap, eventMapping.newEventCondition ?? (() => true), applyNew);
onUpdateFunc = createConnectableHandler(eventMapping.updatedEventArgsMap, applyUpdated); onUpdateFunc = createConnectableHandler(eventMapping.updatedEventArgsMap, eventMapping.updatedEventCondition ?? (() => true), applyUpdated);
onRemoveFunc = createConnectableHandler(eventMapping.removedEventArgsMap, applyRemoved); onRemoveFunc = createConnectableHandler(eventMapping.removedEventArgsMap, eventMapping.removedEventCondition ?? (() => true), applyRemoved);
onConflictFunc = createConflictableHandler(eventMapping.conflictEventArgsMap, applyChanges); onConflictFunc = createConflictableHandler(eventMapping.conflictEventArgsMap, eventMapping.conflictEventCondition ?? (() => true), applyChanges);
guild.on(eventMapping.newEventName, onNewFunc); guild.on(eventMapping.newEventName, onNewFunc);
guild.on(eventMapping.updatedEventName, onUpdateFunc); guild.on(eventMapping.updatedEventName, onUpdateFunc);
guild.on(eventMapping.removedEventName, onRemoveFunc); guild.on(eventMapping.removedEventName, onRemoveFunc);
@ -319,25 +324,6 @@ function listenToMultiple<
return cleanup; return cleanup;
} }
// function listenToMultipleScrolling<
// 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
// >(
// getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
// guildId: number,
// node: RecoilValue<LoadableValue<T[]>>,
// setSelf: (loadableValue: LoadableValue<T[]>) => void,
// fetchValueFunc: () => Promise<void>,
// fetchAboveFunc: (reference: T) => Promise<T | null>,
// fetchBelowFunc: (reference: T) => Promise<T | null>,
// sortFunc: (a: T, b: T) => number,
// eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>
// ) {
// // TODO
// }
function singleGuildSubscriptionEffect< function singleGuildSubscriptionEffect<
T, // e.g. GuildMetadata T, // e.g. GuildMetadata
@ -349,10 +335,9 @@ function singleGuildSubscriptionEffect<
eventMapping: SingleEventMappingParams<T, UE, CE>, eventMapping: SingleEventMappingParams<T, UE, CE>,
skipFunc?: () => boolean skipFunc?: () => boolean
) { ) {
return (params: RecoilLoadableAtomEffectParams<T>) => { const effect: AtomEffect<LoadableValue<T>> = ({ node, trigger, setSelf, getPromise }) => {
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 { node, trigger, setSelf, getPromise } = params;
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc); const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc);
// Fetch initial value on first get // Fetch initial value on first get
@ -367,6 +352,7 @@ function singleGuildSubscriptionEffect<
cleanup(); cleanup();
} }
} }
return effect;
} }
function multipleGuildSubscriptionEffect< function multipleGuildSubscriptionEffect<
@ -381,8 +367,7 @@ function multipleGuildSubscriptionEffect<
sortFunc: (a: T, b: T) => number, sortFunc: (a: T, b: T) => number,
eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>, eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>,
) { ) {
return (params: RecoilLoadableAtomEffectParams<T[]>) => { const effect: AtomEffect<LoadableValue<T[]>> = ({ node, trigger, setSelf, getPromise }) => {
const { node, trigger, setSelf, getPromise } = params;
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc); const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc);
// Fetch initial value on first get // Fetch initial value on first get
@ -396,7 +381,40 @@ function multipleGuildSubscriptionEffect<
return () => { return () => {
cleanup(); cleanup();
} }
} };
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: listen for changes
};
return effect;
} }
// You probably want currGuildMetaState // You probably want currGuildMetaState
@ -528,7 +546,7 @@ const guildChannelsState = atomFamily<LoadableValue<Channel[]>, number>({
}); });
// You probably want currGuildActiveChannel // You probably want currGuildActiveChannel
export const guildActiveChannelIdState: (guildId: number) => RecoilState<string | null> = atomFamily<string | null, number>({ export const guildActiveChannelIdState = atomFamily<string | null, number>({
key: 'guildActiveChannelIdState', key: 'guildActiveChannelIdState',
default: null, default: null,
}); });
@ -551,6 +569,40 @@ const guildActiveChannelState = selectorFamily<LoadableValue<Channel>, number>({
dangerouslyAllowMutability: true 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 guildTokensState = atomFamily<LoadableValue<Token[]>, number>({ export const guildTokensState = atomFamily<LoadableValue<Token[]>, number>({
key: 'guildTokensState', key: 'guildTokensState',
default: DEF_UNLOADED_VALUE, default: DEF_UNLOADED_VALUE,