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);
import { ReactNode, useEffect } from "react";
import { atom, atomFamily, GetRecoilValue, Loadable, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useRecoilValueLoadable, useSetRecoilState } from "recoil";
import { Changes, Channel, GuildMetadata, Member, Resource, ShouldNeverHappenError, Token } from "../../data-types";
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 CombinedGuild from "../../guild-combined";
import GuildsManager from "../../guilds-manager";
import { AutoVerifierChangesType } from '../../auto-verifier';
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 Globals from '../../globals';
// General typescript type that infers the arguments of a function
type Arguments<T> = T extends (...args: infer A) => unknown ? A : never;
@ -113,7 +114,7 @@ interface RecoilLoadableAtomEffectParams<T> {
function createFetchValueFunc<T>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number,
node: RecoilValue<LoadableValue<T>>,
node: RecoilState<LoadableValue<T>>,
setSelf: (loadableValue: LoadableValue<T>) => void,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
): () => Promise<void> {
@ -185,7 +186,7 @@ function listenToSingle<
let closed = false;
(async () => {
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
// Creates an event handler that directly applies the result of the eventArgsMap as a loadedValue into self
@ -193,16 +194,14 @@ function listenToSingle<
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T>,
condition: (value: T) => boolean
): (Connectable & Conflictable)[XE] {
return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => {
const selfState = await getPromise(node);
if (isLoaded(selfState)) {
const value = eventArgsMap(...args);
if (condition(value)) {
setSelf(createLoadedValue(value, fetchValueFunc));
}
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];
}
@ -230,12 +229,16 @@ interface MultipleEventMappingParams<
> {
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;
}
function listenToMultiple<
T extends { id: string },
@ -261,49 +264,51 @@ function listenToMultiple<
let closed = false;
(async () => {
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
// 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 ((...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => {
const selfState = await getPromise(node);
if (isLoaded(selfState)) {
const eventArgsResult = eventArgsMap(...args);
return (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => {
const selfState = await getPromise(node);
if (isLoaded(selfState)) {
const eventArgsResult = eventArgsMap(...args);
if (condition(eventArgsResult)) {
const value = applyFunc(selfState.value, eventArgsResult, sortFunc);
setSelf(createLoadedValue(value, fetchValueFunc));
}
})();
}
}) as (Connectable & Conflictable)[XE];
}
// Useful for conflict-xxx list events
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 ((...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => {
const selfState = await getPromise(node);
if (isLoaded(selfState)) {
const eventArgsResult = eventArgsMap(...args);
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, applyNew);
onUpdateFunc = createConnectableHandler(eventMapping.updatedEventArgsMap, applyUpdated);
onRemoveFunc = createConnectableHandler(eventMapping.removedEventArgsMap, applyRemoved);
onConflictFunc = createConflictableHandler(eventMapping.conflictEventArgsMap, applyChanges);
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);
@ -319,25 +324,6 @@ function listenToMultiple<
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<
T, // e.g. GuildMetadata
@ -349,10 +335,9 @@ function singleGuildSubscriptionEffect<
eventMapping: SingleEventMappingParams<T, UE, CE>,
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)
const { node, trigger, setSelf, getPromise } = params;
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc);
// Fetch initial value on first get
@ -367,6 +352,7 @@ function singleGuildSubscriptionEffect<
cleanup();
}
}
return effect;
}
function multipleGuildSubscriptionEffect<
@ -381,8 +367,7 @@ function multipleGuildSubscriptionEffect<
sortFunc: (a: T, b: T) => number,
eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>,
) {
return (params: RecoilLoadableAtomEffectParams<T[]>) => {
const { node, trigger, setSelf, getPromise } = params;
const effect: AtomEffect<LoadableValue<T[]>> = ({ node, trigger, setSelf, getPromise }) => {
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc);
// Fetch initial value on first get
@ -396,7 +381,40 @@ function multipleGuildSubscriptionEffect<
return () => {
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
@ -528,7 +546,7 @@ const guildChannelsState = atomFamily<LoadableValue<Channel[]>, number>({
});
// 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',
default: null,
});
@ -551,6 +569,40 @@ 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 guildTokensState = atomFamily<LoadableValue<Token[]>, number>({
key: 'guildTokensState',
default: DEF_UNLOADED_VALUE,