styled error messages. bug where clicking to fetch above (and I assume below too) doesn't actually run anything
This commit is contained in:
parent
73e81f0bc9
commit
aa79034d7e
@ -12,7 +12,9 @@ $borderRadius: 8px;
|
||||
flex-direction: column-reverse;
|
||||
padding-bottom: calc(16px + 8px);
|
||||
margin-bottom: calc(65px - $scrollbarBottom - $borderRadius);
|
||||
height: calc(100vh - 71px - 65px + 4px + 8px); /* TODO: Going to have to find a way to do this without a fixed height since the message box needs to be scalable */
|
||||
height: calc(
|
||||
100vh - 71px - 65px + 4px + 8px
|
||||
); /* TODO: Going to have to find a way to do this without a fixed height since the message box needs to be scalable */
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
@ -44,6 +46,32 @@ $borderRadius: 8px;
|
||||
background-color: theme.$background-message-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.retry {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: theme.$background-secondary-alt;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
margin-top: 8px;
|
||||
&:first-child {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-right: 8px;
|
||||
color: theme.$text-normal;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 0.85em;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { MutableRefObject, ReactNode, RefObject, useCallback, useRef } from 'react';
|
||||
import React, { MutableRefObject, ReactNode } from 'react';
|
||||
|
||||
import { LoadableValueScrolling } from '../require/loadables';
|
||||
import { useScrollableCallables } from '../require/react-helper';
|
||||
@ -23,6 +23,7 @@ function InfiniteScrollRecoil<T>(props: InfiniteScrollRecoilProps<T[], T>) {
|
||||
<div className="infinite-scroll-elements">
|
||||
<Retry error={scrollable?.above?.error} text={aboveErrorMessage} retryFunc={fetchAboveCallable} />
|
||||
{children}
|
||||
<Retry error={scrollable?.below?.error} text={belowErrorMessage} retryFunc={fetchBelowCallable} />
|
||||
<Retry
|
||||
error={scrollable.error}
|
||||
text={initialErrorMessage}
|
||||
@ -33,7 +34,6 @@ function InfiniteScrollRecoil<T>(props: InfiniteScrollRecoilProps<T[], T>) {
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Retry error={scrollable?.below?.error} text={belowErrorMessage} retryFunc={fetchBelowCallable} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -23,9 +23,11 @@ const Retry: FC<RetryProps> = (props: RetryProps) => {
|
||||
return { result: null, errorMessage: null };
|
||||
},
|
||||
[retryFunc],
|
||||
{ start: 'Try Again', pending: 'Fetching...' },
|
||||
{ start: 'Try Again', pending: 'Fetching...', done: 'Try Again', error: 'Try Again' },
|
||||
);
|
||||
|
||||
// TODO: shaking isn't working
|
||||
|
||||
if (!error) return null;
|
||||
|
||||
return (
|
||||
|
@ -39,7 +39,11 @@ export type AtomEffectParam<T> = Arguments<AtomEffect<T>>[0];
|
||||
|
||||
// "initial" value loaders
|
||||
export type FetchValueFunc = () => Promise<void>;
|
||||
export function createFetchValueFunc<T>(atomEffectParam: AtomEffectParam<LoadableValue<T>>, guildId: number, fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>): FetchValueFunc {
|
||||
export function createFetchValueFunc<T>(
|
||||
atomEffectParam: AtomEffectParam<LoadableValue<T>>,
|
||||
guildId: number,
|
||||
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
|
||||
): FetchValueFunc {
|
||||
const { node, setSelf, getPromise } = atomEffectParam;
|
||||
const fetchValueFunc = async () => {
|
||||
const guild = await getPromise(guildState(guildId));
|
||||
@ -53,8 +57,8 @@ export function createFetchValueFunc<T>(atomEffectParam: AtomEffectParam<Loadabl
|
||||
const value = await fetchFunc(guild);
|
||||
setSelf(createLoadedValue(value, fetchValueFunc));
|
||||
} catch (e: unknown) {
|
||||
LOG.error('unable to fetch initial guild metadata', e);
|
||||
setSelf(createFailedValue(e, fetchValueFunc));
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
return fetchValueFunc;
|
||||
@ -84,10 +88,17 @@ export function createFetchValueScrollingFunc<T, E>(
|
||||
setSelf(DEF_PENDED_SCROLLING_VALUE);
|
||||
try {
|
||||
const result = await fetchFunc(guild, count);
|
||||
setSelf(createLoadedValueScrolling(result, fetchValueReferenceFunc, createAboveEndFunc(result), createBelowEndFunc(result)));
|
||||
setSelf(
|
||||
createLoadedValueScrolling(
|
||||
result,
|
||||
fetchValueReferenceFunc,
|
||||
createAboveEndFunc(result),
|
||||
createBelowEndFunc(result),
|
||||
),
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
LOG.error('unable to fetch value scrolling', e);
|
||||
setSelf(createFailedValueScrolling(e, fetchValueReferenceFunc));
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
return fetchValueReferenceFunc;
|
||||
@ -98,8 +109,15 @@ export function createFetchValueScrollingReferenceFunc<T>(
|
||||
atomEffectParam: AtomEffectParam<LoadableValueScrolling<T[], T>>,
|
||||
guildId: number,
|
||||
getFunc: (selfState: LoadedValueScrolling<T[], T>) => LoadableScrollingEnd<T>,
|
||||
applyEndToSelf: (selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => LoadedValueScrolling<T[], T>,
|
||||
applyResultToSelf: (selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => LoadedValueScrolling<T[], T>,
|
||||
applyEndToSelf: (
|
||||
selfState: LoadedValueScrolling<T[], T>,
|
||||
end: LoadableScrollingEnd<T>,
|
||||
) => LoadedValueScrolling<T[], T>,
|
||||
applyResultToSelf: (
|
||||
selfState: LoadedValueScrolling<T[], T>,
|
||||
end: LoadedScrollingEnd<T>,
|
||||
result: T[],
|
||||
) => LoadedValueScrolling<T[], T>,
|
||||
count: number,
|
||||
fetchReferenceFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[]>,
|
||||
): {
|
||||
@ -134,15 +152,22 @@ export function createFetchValueScrollingReferenceFunc<T>(
|
||||
setSelf(applyEndToSelf(selfState, createCancelledScrollingEnd(selfEnd)));
|
||||
} else {
|
||||
const hasMore = result.length >= count;
|
||||
setSelf(applyResultToSelf(selfState, createLoadedScrollingEnd(hasMore, fetchValueReferenceFunc, cancel), result));
|
||||
setSelf(
|
||||
applyResultToSelf(
|
||||
selfState,
|
||||
createLoadedScrollingEnd(hasMore, fetchValueReferenceFunc, cancel),
|
||||
result,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (canceled) {
|
||||
LOG.error('unable to fetch value based on reference (but we were canceled)', e);
|
||||
LOG.error('unable to fetch value based on reference (but we were canceled anyway)', e);
|
||||
setSelf(applyEndToSelf(selfState, createCancelledScrollingEnd(selfEnd)));
|
||||
} else {
|
||||
LOG.error('unable to fetch value based on reference', e);
|
||||
// xLOG.error('unable to fetch value based on reference', e);
|
||||
setSelf(applyEndToSelf(selfState, createFailedScrollingEnd(e, fetchValueReferenceFunc, cancel)));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -156,7 +181,11 @@ function createEventHandler<
|
||||
ArgsMapResult, // e.g. Member[]
|
||||
// eslint-disable-next-line space-before-function-paren
|
||||
XE extends keyof (Connectable | Conflictable), // e.g. new-members
|
||||
>(atomEffectParam: AtomEffectParam<V>, argsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => ArgsMapResult, applyFunc: (selfState: V, argsResult: ArgsMapResult) => V): (Connectable & Conflictable)[XE] {
|
||||
>(
|
||||
atomEffectParam: AtomEffectParam<V>,
|
||||
argsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => ArgsMapResult,
|
||||
applyFunc: (selfState: V, argsResult: ArgsMapResult) => V,
|
||||
): (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
|
||||
@ -198,8 +227,16 @@ export 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
|
||||
|
||||
onUpdateFunc = createEventHandler(atomEffectParam, eventMapping.updatedEvent.argsMap, eventMapping.updatedEvent.applyFunc);
|
||||
onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc);
|
||||
onUpdateFunc = createEventHandler(
|
||||
atomEffectParam,
|
||||
eventMapping.updatedEvent.argsMap,
|
||||
eventMapping.updatedEvent.applyFunc,
|
||||
);
|
||||
onConflictFunc = createEventHandler(
|
||||
atomEffectParam,
|
||||
eventMapping.conflictEvent.argsMap,
|
||||
eventMapping.conflictEvent.applyFunc,
|
||||
);
|
||||
|
||||
guild.on(eventMapping.updatedEvent.name, onUpdateFunc);
|
||||
guild.on(eventMapping.conflictEvent.name, onConflictFunc);
|
||||
@ -212,7 +249,14 @@ export function listenToSingle<
|
||||
return cleanup;
|
||||
}
|
||||
|
||||
export interface MultipleEventMappingParams<T, V, NE extends keyof Connectable, UE extends keyof Connectable, RE extends keyof Connectable, CE extends keyof Conflictable> {
|
||||
export interface MultipleEventMappingParams<
|
||||
T,
|
||||
V,
|
||||
NE extends keyof Connectable,
|
||||
UE extends keyof Connectable,
|
||||
RE extends keyof Connectable,
|
||||
CE extends keyof Conflictable,
|
||||
> {
|
||||
newEvent: {
|
||||
name: NE;
|
||||
argsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined<T[]>;
|
||||
@ -241,7 +285,11 @@ export function listenToMultiple<
|
||||
UE extends keyof Connectable, // update Event
|
||||
RE extends keyof Connectable, // remove Event
|
||||
CE extends keyof Conflictable, // conflict Event
|
||||
>(atomEffectParam: AtomEffectParam<V>, guildId: number, eventMapping: MultipleEventMappingParams<T, V, NE, UE, RE, CE>) {
|
||||
>(
|
||||
atomEffectParam: AtomEffectParam<V>,
|
||||
guildId: number,
|
||||
eventMapping: MultipleEventMappingParams<T, V, NE, UE, RE, CE>,
|
||||
) {
|
||||
const { getPromise } = atomEffectParam;
|
||||
// listen for updates
|
||||
let guild: CombinedGuild | null = null;
|
||||
@ -256,9 +304,21 @@ export function listenToMultiple<
|
||||
if (closed) return; // make sure not to bind events if this closed while we were fetching the guild state
|
||||
|
||||
onNewFunc = createEventHandler(atomEffectParam, eventMapping.newEvent.argsMap, eventMapping.newEvent.applyFunc);
|
||||
onUpdateFunc = createEventHandler(atomEffectParam, eventMapping.updatedEvent.argsMap, eventMapping.updatedEvent.applyFunc);
|
||||
onRemoveFunc = createEventHandler(atomEffectParam, eventMapping.removedEvent.argsMap, eventMapping.removedEvent.applyFunc);
|
||||
onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc);
|
||||
onUpdateFunc = createEventHandler(
|
||||
atomEffectParam,
|
||||
eventMapping.updatedEvent.argsMap,
|
||||
eventMapping.updatedEvent.applyFunc,
|
||||
);
|
||||
onRemoveFunc = createEventHandler(
|
||||
atomEffectParam,
|
||||
eventMapping.removedEvent.argsMap,
|
||||
eventMapping.removedEvent.applyFunc,
|
||||
);
|
||||
onConflictFunc = createEventHandler(
|
||||
atomEffectParam,
|
||||
eventMapping.conflictEvent.argsMap,
|
||||
eventMapping.conflictEvent.applyFunc,
|
||||
);
|
||||
|
||||
guild.on(eventMapping.newEvent.name, onNewFunc);
|
||||
guild.on(eventMapping.updatedEvent.name, onUpdateFunc);
|
||||
@ -279,7 +339,12 @@ export 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, LoadableValue<T>, UE, CE>, skipFunc?: () => boolean) {
|
||||
>(
|
||||
guildId: number,
|
||||
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
|
||||
eventMapping: SingleEventMappingParams<T, LoadableValue<T>, UE, CE>,
|
||||
skipFunc?: () => boolean,
|
||||
) {
|
||||
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)
|
||||
@ -288,7 +353,13 @@ export function guildDataSubscriptionLoadableSingleEffect<
|
||||
|
||||
// fetch initial value on first get
|
||||
if (trigger === 'get') {
|
||||
fetchValueFunc();
|
||||
(async () => {
|
||||
try {
|
||||
await fetchValueFunc();
|
||||
} catch (e: unknown) {
|
||||
LOG.error('error fetching initial value', e);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// listen to changes
|
||||
@ -301,7 +372,13 @@ export function guildDataSubscriptionLoadableSingleEffect<
|
||||
return effect;
|
||||
}
|
||||
|
||||
export function guildDataSubscriptionLoadableMultipleEffect<T extends { id: string }, NE extends keyof Connectable, UE extends keyof Connectable, RE extends keyof Connectable, CE extends keyof Conflictable>(
|
||||
export function guildDataSubscriptionLoadableMultipleEffect<
|
||||
T extends { id: string },
|
||||
NE extends keyof Connectable,
|
||||
UE extends keyof Connectable,
|
||||
RE extends keyof Connectable,
|
||||
CE extends keyof Conflictable,
|
||||
>(
|
||||
guildId: number,
|
||||
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T[]>>,
|
||||
eventMapping: MultipleEventMappingParams<T, LoadableValue<T[]>, NE, UE, RE, CE>,
|
||||
@ -313,7 +390,13 @@ export function guildDataSubscriptionLoadableMultipleEffect<T extends { id: stri
|
||||
|
||||
// fetch initial value on first get
|
||||
if (trigger === 'get') {
|
||||
fetchValueFunc();
|
||||
(async () => {
|
||||
try {
|
||||
await fetchValueFunc();
|
||||
} catch (e: unknown) {
|
||||
LOG.error('error fetching initial value', e);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// listen to changes
|
||||
@ -355,70 +438,84 @@ export function multipleScrollingGuildSubscriptionEffect<
|
||||
guildId,
|
||||
fetchCount,
|
||||
fetchFuncs.fetchBottomFunc,
|
||||
(result: T[]) => createLoadedScrollingEnd(result.length >= fetchCount, fetchValueAboveReferenceFunc, cancelAbove), // above end
|
||||
(result: T[]) =>
|
||||
createLoadedScrollingEnd(result.length >= fetchCount, fetchValueAboveReferenceFunc, cancelAbove), // above end
|
||||
(_result: T[]) => createLoadedScrollingEnd(false, fetchValueBelowReferenceFunc, cancelBelow), // below end
|
||||
);
|
||||
// fetch Above a Reference
|
||||
const { fetchValueReferenceFunc: fetchValueAboveReferenceFunc, cancel: cancelAbove } = createFetchValueScrollingReferenceFunc<T>(
|
||||
atomEffectParam,
|
||||
guildId,
|
||||
(selfState: LoadedValueScrolling<T[], T>) => selfState.above,
|
||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({ ...selfState, above: end }), // for "cancelled, pending, etc"
|
||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => {
|
||||
let nextValue = result.concat(selfState.value).sort(sortFunc);
|
||||
let sliced = false;
|
||||
if (nextValue.length > maxElements) {
|
||||
nextValue = nextValue.slice(undefined, maxElements);
|
||||
cancelBelow();
|
||||
sliced = true;
|
||||
}
|
||||
const loadedValue: LoadedValueScrolling<T[], T> = {
|
||||
const { fetchValueReferenceFunc: fetchValueAboveReferenceFunc, cancel: cancelAbove } =
|
||||
createFetchValueScrollingReferenceFunc<T>(
|
||||
atomEffectParam,
|
||||
guildId,
|
||||
(selfState: LoadedValueScrolling<T[], T>) => selfState.above,
|
||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({
|
||||
...selfState,
|
||||
value: nextValue,
|
||||
above: end,
|
||||
below: {
|
||||
...selfState.below,
|
||||
hasMore: sliced ? true : selfState.below.hasMore,
|
||||
} as LoadableScrollingEnd<T>, // this is OK since selfState.below is already a LoadableScrollingEnd and we are only modifying hasMore to potentially include a boolean
|
||||
};
|
||||
return loadedValue;
|
||||
},
|
||||
fetchCount,
|
||||
fetchFuncs.fetchAboveFunc,
|
||||
);
|
||||
}), // for "cancelled, pending, etc"
|
||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => {
|
||||
let nextValue = result.concat(selfState.value).sort(sortFunc);
|
||||
let sliced = false;
|
||||
if (nextValue.length > maxElements) {
|
||||
nextValue = nextValue.slice(undefined, maxElements);
|
||||
cancelBelow();
|
||||
sliced = true;
|
||||
}
|
||||
const loadedValue: LoadedValueScrolling<T[], T> = {
|
||||
...selfState,
|
||||
value: nextValue,
|
||||
above: end,
|
||||
below: {
|
||||
...selfState.below,
|
||||
hasMore: sliced ? true : selfState.below.hasMore,
|
||||
} as LoadableScrollingEnd<T>, // this is OK since selfState.below is already a LoadableScrollingEnd and we are only modifying hasMore to potentially include a boolean
|
||||
};
|
||||
return loadedValue;
|
||||
},
|
||||
fetchCount,
|
||||
fetchFuncs.fetchAboveFunc,
|
||||
);
|
||||
// fetch Below a Reference
|
||||
const { fetchValueReferenceFunc: fetchValueBelowReferenceFunc, cancel: cancelBelow } = createFetchValueScrollingReferenceFunc<T>(
|
||||
atomEffectParam,
|
||||
guildId,
|
||||
(selfState: LoadedValueScrolling<T[], T>) => selfState.below,
|
||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({ ...selfState, below: end }),
|
||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => {
|
||||
let nextValue = result.concat(selfState.value).sort(sortFunc);
|
||||
let sliced = false;
|
||||
if (nextValue.length > maxElements) {
|
||||
nextValue = nextValue.slice(nextValue.length - maxElements, undefined);
|
||||
cancelAbove();
|
||||
sliced = true;
|
||||
}
|
||||
const loadedValue: LoadedValueScrolling<T[], T> = {
|
||||
const { fetchValueReferenceFunc: fetchValueBelowReferenceFunc, cancel: cancelBelow } =
|
||||
createFetchValueScrollingReferenceFunc<T>(
|
||||
atomEffectParam,
|
||||
guildId,
|
||||
(selfState: LoadedValueScrolling<T[], T>) => selfState.below,
|
||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({
|
||||
...selfState,
|
||||
value: nextValue,
|
||||
above: {
|
||||
...selfState.above,
|
||||
hasMore: sliced ? true : selfState.above.hasMore,
|
||||
} as LoadableScrollingEnd<T>,
|
||||
below: end,
|
||||
};
|
||||
return loadedValue;
|
||||
},
|
||||
fetchCount,
|
||||
fetchFuncs.fetchBelowFunc,
|
||||
);
|
||||
}),
|
||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => {
|
||||
let nextValue = result.concat(selfState.value).sort(sortFunc);
|
||||
let sliced = false;
|
||||
if (nextValue.length > maxElements) {
|
||||
nextValue = nextValue.slice(nextValue.length - maxElements, undefined);
|
||||
cancelAbove();
|
||||
sliced = true;
|
||||
}
|
||||
const loadedValue: LoadedValueScrolling<T[], T> = {
|
||||
...selfState,
|
||||
value: nextValue,
|
||||
above: {
|
||||
...selfState.above,
|
||||
hasMore: sliced ? true : selfState.above.hasMore,
|
||||
} as LoadableScrollingEnd<T>,
|
||||
below: end,
|
||||
};
|
||||
return loadedValue;
|
||||
},
|
||||
fetchCount,
|
||||
fetchFuncs.fetchBelowFunc,
|
||||
);
|
||||
|
||||
// fetch bottom value on first get
|
||||
if (trigger === 'get') {
|
||||
LOG.debug('fetching scrolling bottom...');
|
||||
fetchValueBottomFunc();
|
||||
(async () => {
|
||||
try {
|
||||
await fetchValueBottomFunc();
|
||||
} catch (e: unknown) {
|
||||
LOG.error('error fetching bottom value', e);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// listen to changes
|
||||
@ -434,19 +531,33 @@ export function multipleScrollingGuildSubscriptionEffect<
|
||||
export function applyNewElements<T>(list: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] {
|
||||
return list.concat(newElements).sort(sortFunc);
|
||||
}
|
||||
export 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);
|
||||
export 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);
|
||||
}
|
||||
export 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));
|
||||
}
|
||||
|
||||
export function applyChangedElements<T extends { id: string }>(list: T[], changes: Changes<T>, sortFunc: (a: T, b: T) => number): T[] {
|
||||
export 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)
|
||||
.map(
|
||||
element =>
|
||||
changes.updated.find(updatedPair => updatedPair.newDataPoint.id === element.id)?.newDataPoint ??
|
||||
element,
|
||||
)
|
||||
.filter(element => !removedIds.has(element.id))
|
||||
.sort(sortFunc);
|
||||
}
|
||||
|
@ -102,6 +102,7 @@ export default class ElementsUtil {
|
||||
return src;
|
||||
} catch (e) {
|
||||
LOG.warn('unable to fetch and convert guild resource, showing error instead', e);
|
||||
//xLOG.warn('unable to fetch and convert guild resource, showing error instead', e);
|
||||
return './img/error.png';
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,20 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import { DependencyList, Dispatch, MutableRefObject, ReactNode, RefObject, SetStateAction, UIEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
DependencyList,
|
||||
Dispatch,
|
||||
MutableRefObject,
|
||||
ReactNode,
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
UIEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ShouldNeverHappenError } from '../../data-types';
|
||||
import Util from '../../util';
|
||||
import Globals from '../../globals';
|
||||
@ -46,7 +59,11 @@ function useShake(ms: number): [shaking: boolean, doShake: () => void] {
|
||||
}
|
||||
|
||||
// runs an Async action one time (updates when deps changes)
|
||||
export function useOneTimeAsyncAction<T, V>(actionFunc: () => Promise<T>, initialValue: V, deps: DependencyList): [value: T | V, error: unknown | null] {
|
||||
export function useOneTimeAsyncAction<T, V>(
|
||||
actionFunc: () => Promise<T>,
|
||||
initialValue: V,
|
||||
deps: DependencyList,
|
||||
): [value: T | V, error: unknown | null] {
|
||||
const isMounted = useRef(false);
|
||||
|
||||
const [value, setValue] = useState<T | V>(initialValue);
|
||||
@ -79,9 +96,11 @@ export function useOneTimeAsyncAction<T, V>(actionFunc: () => Promise<T>, initia
|
||||
|
||||
/** creates a callable async function that will not double-trigger and gives a result, error message, and shaking boolean */
|
||||
export function useAsyncCallback<ResultType>(
|
||||
actionFunc: (isMounted: MutableRefObject<boolean>) => Promise<{ errorMessage: string | null; result: ResultType | null }>,
|
||||
actionFunc: (
|
||||
isMounted: MutableRefObject<boolean>,
|
||||
) => Promise<{ errorMessage: string | null; result: ResultType | null }>,
|
||||
deps: DependencyList,
|
||||
): [callable: () => void, result: ResultType | null, errorMessage: string | null, shaking: boolean] {
|
||||
): [callable: () => void, result: ResultType | null, errorMessage: string | null, shaking: boolean, pending: boolean] {
|
||||
const isMounted = useIsMountedRef();
|
||||
|
||||
const [result, setResult] = useState<ResultType | null>(null);
|
||||
@ -103,20 +122,23 @@ export function useAsyncCallback<ResultType>(
|
||||
setPending(false);
|
||||
if (errorMessage) doShake();
|
||||
} catch (e: unknown) {
|
||||
LOG.error('unable to perform submit button actionFunc', e);
|
||||
LOG.error('unable to perform async callback', e);
|
||||
setResult(null);
|
||||
setErrorMessage('Unknown error');
|
||||
if (errorMessage) doShake();
|
||||
setErrorMessage('unknown error');
|
||||
doShake();
|
||||
setPending(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isMounted, pending, actionFunc, doShake, errorMessage, ...deps]);
|
||||
|
||||
return [callable, result, errorMessage, shaking];
|
||||
return [callable, result, errorMessage, shaking, pending];
|
||||
}
|
||||
|
||||
/** creates a callable async function that does not need to return a value (or shake) */
|
||||
export function useAsyncVoidCallback(actionFunc: (isMounted: MutableRefObject<boolean>) => Promise<void>, deps: DependencyList): [callable: () => void] {
|
||||
export function useAsyncVoidCallback(
|
||||
actionFunc: (isMounted: MutableRefObject<boolean>) => Promise<void>,
|
||||
deps: DependencyList,
|
||||
): [callable: () => void] {
|
||||
const [callable] = useAsyncCallback(async (isMounted: MutableRefObject<boolean>) => {
|
||||
await actionFunc(isMounted);
|
||||
return { errorMessage: null, result: null };
|
||||
@ -129,9 +151,28 @@ export function useDownloadButton(
|
||||
downloadName: string,
|
||||
fetchBuff: () => Promise<Buffer | null>,
|
||||
fetchBuffDeps: DependencyList,
|
||||
stateTextMapping?: { start?: string; pendingFetch?: string; errorFetch?: string; notReadyFetch?: string; pendingSave?: string; errorSave?: string; success?: string },
|
||||
stateTextMapping?: {
|
||||
start?: string;
|
||||
pendingFetch?: string;
|
||||
errorFetch?: string;
|
||||
notReadyFetch?: string;
|
||||
pendingSave?: string;
|
||||
errorSave?: string;
|
||||
success?: string;
|
||||
},
|
||||
): [callable: () => void, text: string, shaking: boolean] {
|
||||
const textMapping = { ...{ start: 'Download', pendingFetch: 'Downloading...', errorFetch: 'Try Again', notReadyFetch: 'Not Ready', pendingSave: 'Saving...', errorSave: 'Try Again', success: 'Open in Explorer' }, ...stateTextMapping };
|
||||
const textMapping = {
|
||||
...{
|
||||
start: 'Download',
|
||||
pendingFetch: 'Downloading...',
|
||||
errorFetch: 'Try Again',
|
||||
notReadyFetch: 'Not Ready',
|
||||
pendingSave: 'Saving...',
|
||||
errorSave: 'Try Again',
|
||||
success: 'Open in Explorer',
|
||||
},
|
||||
...stateTextMapping,
|
||||
};
|
||||
|
||||
const [filePath, setFilePath] = useState<string | null>(null);
|
||||
const [fileBuffer, setFileBuffer] = useState<Buffer | null>(null);
|
||||
@ -207,30 +248,25 @@ export function useDownloadButton(
|
||||
|
||||
/** creates useful state variables for a button intended call a submit action */
|
||||
export function useAsyncSubmitButton<ResultType>(
|
||||
actionFunc: (isMounted: MutableRefObject<boolean>) => Promise<{ errorMessage: string | null; result: ResultType | null }>,
|
||||
actionFunc: (
|
||||
isMounted: MutableRefObject<boolean>,
|
||||
) => Promise<{ errorMessage: string | null; result: ResultType | null }>,
|
||||
deps: DependencyList,
|
||||
stateTextMapping?: { start?: string; pending?: string; error?: string; done?: string },
|
||||
): [callable: () => void, text: string, shaking: boolean, result: ResultType | null, errorMessage: string | null] {
|
||||
const textMapping = { ...{ start: 'Submit', pending: 'Submitting...', error: 'Try Again', done: 'Done' }, ...stateTextMapping };
|
||||
const textMapping = {
|
||||
...{ start: 'Submit', pending: 'Submitting...', error: 'Try Again', done: 'Done' },
|
||||
...stateTextMapping,
|
||||
};
|
||||
|
||||
const [pending, setPending] = useState<boolean>(false);
|
||||
const [complete, setComplete] = useState<boolean>(false);
|
||||
|
||||
const [callable, result, errorMessage, shaking] = useAsyncCallback(
|
||||
const [callable, result, errorMessage, shaking, pending] = useAsyncCallback(
|
||||
async (isMounted: MutableRefObject<boolean>) => {
|
||||
try {
|
||||
setPending(true);
|
||||
const actionReturnValue = await actionFunc(isMounted);
|
||||
if (!isMounted) return { errorMessage: null, result: null };
|
||||
setPending(false);
|
||||
if (actionReturnValue.errorMessage === null) setComplete(true);
|
||||
return actionReturnValue;
|
||||
} catch (e) {
|
||||
LOG.error('Error submitting button', e);
|
||||
if (!isMounted) return { errorMessage: null, result: null };
|
||||
setPending(false);
|
||||
return { errorMessage: 'Error submitting button', result: null };
|
||||
}
|
||||
const actionReturnValue = await actionFunc(isMounted);
|
||||
if (!isMounted) return { errorMessage: null, result: null };
|
||||
if (actionReturnValue.errorMessage === null) setComplete(true);
|
||||
return actionReturnValue;
|
||||
},
|
||||
[...deps, actionFunc],
|
||||
);
|
||||
@ -325,9 +361,15 @@ export function useScrollableCallables<T>(
|
||||
|
||||
// makes sure to also allow you to fly-out a click starting inside of the ref'd element but was dragged outside
|
||||
/** calls the close action when you hit escape or click outside of the ref element */
|
||||
export function useActionWhenEscapeOrClickedOrContextOutsideEffect(ref: RefObject<HTMLElement>, actionFunc: () => void) {
|
||||
export function useActionWhenEscapeOrClickedOrContextOutsideEffect(
|
||||
ref: RefObject<HTMLElement>,
|
||||
actionFunc: () => void,
|
||||
) {
|
||||
// have to use a ref here and not states since we can't re-assign state between mouseup and click
|
||||
const mouseRef = useRef<{ mouseDownTarget: Node | null; mouseUpTarget: Node | null }>({ mouseDownTarget: null, mouseUpTarget: null });
|
||||
const mouseRef = useRef<{ mouseDownTarget: Node | null; mouseUpTarget: Node | null }>({
|
||||
mouseDownTarget: null,
|
||||
mouseUpTarget: null,
|
||||
});
|
||||
|
||||
const handleClickOutside = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
@ -453,7 +495,10 @@ export function useAlignment(
|
||||
}
|
||||
|
||||
/** creates useful context menu state */
|
||||
export function useContextMenu(createContextMenu: (close: () => void) => ReactNode, createContextMenuDeps: DependencyList): [contextMenu: ReactNode, toggle: () => void, close: () => void, open: () => void, isOpen: boolean] {
|
||||
export function useContextMenu(
|
||||
createContextMenu: (close: () => void) => ReactNode,
|
||||
createContextMenuDeps: DependencyList,
|
||||
): [contextMenu: ReactNode, toggle: () => void, close: () => void, open: () => void, isOpen: boolean] {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const close = useCallback(() => {
|
||||
@ -482,9 +527,15 @@ export function useContextClickContextMenu(
|
||||
|
||||
const alignment = useMemo(() => ({ top: 'centerY', left: 'centerX' }), []);
|
||||
|
||||
const createContextMenuWithRelativeToPos = useCallback((close: () => void) => createContextMenu(alignment, relativeToPos, close), [alignment, createContextMenu, relativeToPos]);
|
||||
const createContextMenuWithRelativeToPos = useCallback(
|
||||
(close: () => void) => createContextMenu(alignment, relativeToPos, close),
|
||||
[alignment, createContextMenu, relativeToPos],
|
||||
);
|
||||
|
||||
const [contextMenu, _toggle, _close, open] = useContextMenu(createContextMenuWithRelativeToPos, createContextMenuDeps);
|
||||
const [contextMenu, _toggle, _close, open] = useContextMenu(
|
||||
createContextMenuWithRelativeToPos,
|
||||
createContextMenuDeps,
|
||||
);
|
||||
|
||||
const onContextMenu = useCallback(
|
||||
(event: React.MouseEvent<HTMLElement>) => {
|
||||
@ -498,7 +549,10 @@ export function useContextClickContextMenu(
|
||||
}
|
||||
|
||||
/** creates context state for hovering over an element */
|
||||
export function useContextHover(createContextHover: () => ReactNode, createContextHoverDeps: DependencyList): [contextHover: ReactNode, mouseEnterCallable: () => void, mouseLeaveCallable: () => void] {
|
||||
export function useContextHover(
|
||||
createContextHover: () => ReactNode,
|
||||
createContextHoverDeps: DependencyList,
|
||||
): [contextHover: ReactNode, mouseEnterCallable: () => void, mouseLeaveCallable: () => void] {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const contextHover = useMemo(
|
||||
@ -518,7 +572,11 @@ export function useContextHover(createContextHover: () => ReactNode, createConte
|
||||
}
|
||||
|
||||
/** creates a drop target element that will appear when you drag a file over the document */
|
||||
export function useDocumentDropTarget(message: string, setBuff: Dispatch<SetStateAction<Buffer | null>>, setName: Dispatch<SetStateAction<string | null>>): [dropTarget: ReactNode] {
|
||||
export function useDocumentDropTarget(
|
||||
message: string,
|
||||
setBuff: Dispatch<SetStateAction<Buffer | null>>,
|
||||
setName: Dispatch<SetStateAction<string | null>>,
|
||||
): [dropTarget: ReactNode] {
|
||||
const [dragEnabled, setDragEnabled] = useState<boolean>(false);
|
||||
|
||||
// drag overlay
|
||||
|
@ -7,7 +7,18 @@ import * as socketio from 'socket.io-client';
|
||||
import PersonalDBGuild from './guild-personal-db';
|
||||
import RAMGuild from './guild-ram';
|
||||
import SocketGuild from './guild-socket';
|
||||
import { Changes, Channel, ConnectionInfo, GuildMetadata, GuildMetadataLocal, Member, Message, Resource, SocketConfig, Token } from './data-types';
|
||||
import {
|
||||
Changes,
|
||||
Channel,
|
||||
ConnectionInfo,
|
||||
GuildMetadata,
|
||||
GuildMetadataLocal,
|
||||
Member,
|
||||
Message,
|
||||
Resource,
|
||||
SocketConfig,
|
||||
Token,
|
||||
} from './data-types';
|
||||
|
||||
import MessageRAMCache from './message-ram-cache';
|
||||
import PersonalDB from './personal-db';
|
||||
@ -19,9 +30,13 @@ import EnsuredFetchable from './fetchable-ensured';
|
||||
import { EventEmitter } from 'tsee';
|
||||
import { AutoVerifierChangesType } from './auto-verifier';
|
||||
import { IDQuery, PartialMessageListQuery } from './auto-verifier-with-args';
|
||||
import Util from './util';
|
||||
|
||||
/** the general guild class. This handles connecting a RAM, PersonalDB, and Socket guild together using fetchable-pair-verifiers and some manual caching to nicely update messages */
|
||||
export default class CombinedGuild extends EventEmitter<Connectable & Conflictable> implements AsyncGuaranteedFetchable, AsyncRequestable {
|
||||
export default class CombinedGuild
|
||||
extends EventEmitter<Connectable & Conflictable>
|
||||
implements AsyncGuaranteedFetchable, AsyncRequestable
|
||||
{
|
||||
private readonly ramGuild: RAMGuild;
|
||||
private readonly personalDBGuild: PersonalDBGuild;
|
||||
private readonly socketGuild: SocketGuild;
|
||||
@ -29,7 +44,15 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
private readonly fetchable: AsyncGuaranteedFetchable;
|
||||
private readonly mainPairVerifier: PairVerifierFetchable;
|
||||
|
||||
constructor(public readonly id: number, public readonly memberId: string, socket: socketio.Socket, socketVerifier: SocketVerifier, messageRAMCache: MessageRAMCache, resourceRAMCache: ResourceRAMCache, personalDB: PersonalDB) {
|
||||
constructor(
|
||||
public readonly id: number,
|
||||
public readonly memberId: string,
|
||||
socket: socketio.Socket,
|
||||
socketVerifier: SocketVerifier,
|
||||
messageRAMCache: MessageRAMCache,
|
||||
resourceRAMCache: ResourceRAMCache,
|
||||
personalDB: PersonalDB,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.ramGuild = new RAMGuild(messageRAMCache, resourceRAMCache, this.id);
|
||||
@ -102,8 +125,18 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
for (const message of messages) {
|
||||
LOG.info(`g#${this.id} ${message}`);
|
||||
}
|
||||
this.ramGuild.handleMessagesAdded(messages, diskSocket.fetchMessagesRecentVerifier, diskSocket.fetchMessagesBeforeVerifier, diskSocket.fetchMessagesAfterVerifier);
|
||||
await this.personalDBGuild.handleMessagesAdded(messages, diskSocket.fetchMessagesRecentVerifier, diskSocket.fetchMessagesBeforeVerifier, diskSocket.fetchMessagesAfterVerifier);
|
||||
this.ramGuild.handleMessagesAdded(
|
||||
messages,
|
||||
diskSocket.fetchMessagesRecentVerifier,
|
||||
diskSocket.fetchMessagesBeforeVerifier,
|
||||
diskSocket.fetchMessagesAfterVerifier,
|
||||
);
|
||||
await this.personalDBGuild.handleMessagesAdded(
|
||||
messages,
|
||||
diskSocket.fetchMessagesRecentVerifier,
|
||||
diskSocket.fetchMessagesBeforeVerifier,
|
||||
diskSocket.fetchMessagesAfterVerifier,
|
||||
);
|
||||
const members = await this.grabRAMMembersMap();
|
||||
const channels = await this.grabRAMChannelsMap();
|
||||
for (const message of messages) message.fill(members, channels);
|
||||
@ -113,8 +146,18 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
for (const message of messages) {
|
||||
LOG.info(`g#${this.id} updated ${message}`);
|
||||
}
|
||||
this.ramGuild.handleMessagesChanged(messages, ramDiskSocket.fetchMessagesRecentVerifier, ramDiskSocket.fetchMessagesBeforeVerifier, ramDiskSocket.fetchMessagesAfterVerifier);
|
||||
await this.personalDBGuild.handleMessagesChanged(messages, diskSocket.fetchMessagesRecentVerifier, diskSocket.fetchMessagesBeforeVerifier, diskSocket.fetchMessagesAfterVerifier);
|
||||
this.ramGuild.handleMessagesChanged(
|
||||
messages,
|
||||
ramDiskSocket.fetchMessagesRecentVerifier,
|
||||
ramDiskSocket.fetchMessagesBeforeVerifier,
|
||||
ramDiskSocket.fetchMessagesAfterVerifier,
|
||||
);
|
||||
await this.personalDBGuild.handleMessagesChanged(
|
||||
messages,
|
||||
diskSocket.fetchMessagesRecentVerifier,
|
||||
diskSocket.fetchMessagesBeforeVerifier,
|
||||
diskSocket.fetchMessagesAfterVerifier,
|
||||
);
|
||||
const members = await this.grabRAMMembersMap();
|
||||
const channels = await this.grabRAMChannelsMap();
|
||||
for (const message of messages) message.fill(members, channels);
|
||||
@ -156,10 +199,13 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
});
|
||||
|
||||
// forward the conflict events from the last verifier in the chain
|
||||
ramDiskSocket.on('conflict-metadata', (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => {
|
||||
LOG.info(`g#${this.id} metadata conflict`, { oldGuildMeta, newGuildMeta });
|
||||
this.emit('conflict-metadata', changesType, oldGuildMeta, newGuildMeta);
|
||||
});
|
||||
ramDiskSocket.on(
|
||||
'conflict-metadata',
|
||||
(changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => {
|
||||
LOG.info(`g#${this.id} metadata conflict`, { oldGuildMeta, newGuildMeta });
|
||||
this.emit('conflict-metadata', changesType, oldGuildMeta, newGuildMeta);
|
||||
},
|
||||
);
|
||||
ramDiskSocket.on('conflict-members', (changesType: AutoVerifierChangesType, changes: Changes<Member>) => {
|
||||
LOG.info(`g#${this.id} members conflict`); // this should be pretty common since we manually update the status to "unknown" which will cause a conflict
|
||||
this.emit('conflict-members', changesType, changes);
|
||||
@ -168,28 +214,40 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
LOG.info(`g#${this.id} channels conflict`, { changes });
|
||||
this.emit('conflict-channels', changesType, changes);
|
||||
});
|
||||
ramDiskSocket.on('conflict-messages', async (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes<Message>) => {
|
||||
const members = await this.grabRAMMembersMap();
|
||||
const channels = await this.grabRAMChannelsMap();
|
||||
for (const message of changes.added) message.fill(members, channels);
|
||||
for (const dataPoint of changes.updated) dataPoint.newDataPoint.fill(members, channels);
|
||||
for (const message of changes.deleted) message.fill(members, channels);
|
||||
this.emit('conflict-messages', query, changesType, changes);
|
||||
});
|
||||
ramDiskSocket.on(
|
||||
'conflict-messages',
|
||||
async (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes<Message>) => {
|
||||
const members = await this.grabRAMMembersMap();
|
||||
const channels = await this.grabRAMChannelsMap();
|
||||
for (const message of changes.added) message.fill(members, channels);
|
||||
for (const dataPoint of changes.updated) dataPoint.newDataPoint.fill(members, channels);
|
||||
for (const message of changes.deleted) message.fill(members, channels);
|
||||
this.emit('conflict-messages', query, changesType, changes);
|
||||
},
|
||||
);
|
||||
ramDiskSocket.on('conflict-tokens', (changesType: AutoVerifierChangesType, changes: Changes<Token>) => {
|
||||
LOG.info(`g#${this.id} tokens conflict`, { changes });
|
||||
this.emit('conflict-tokens', changesType, changes);
|
||||
});
|
||||
ramDiskSocket.on('conflict-resource', (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => {
|
||||
LOG.warn(`g#${this.id} resource conflict`, { oldResource, newResource });
|
||||
this.emit('conflict-resource', query, changesType, oldResource, newResource);
|
||||
});
|
||||
ramDiskSocket.on(
|
||||
'conflict-resource',
|
||||
(query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => {
|
||||
LOG.warn(`g#${this.id} resource conflict`, { oldResource, newResource });
|
||||
this.emit('conflict-resource', query, changesType, oldResource, newResource);
|
||||
},
|
||||
);
|
||||
|
||||
this.fetchable = new EnsuredFetchable(ramDiskSocket);
|
||||
this.mainPairVerifier = ramDiskSocket;
|
||||
}
|
||||
|
||||
static async create(guildMetadata: GuildMetadataLocal, socketConfig: SocketConfig, messageRAMCache: MessageRAMCache, resourceRAMCache: ResourceRAMCache, personalDB: PersonalDB) {
|
||||
static async create(
|
||||
guildMetadata: GuildMetadataLocal,
|
||||
socketConfig: SocketConfig,
|
||||
messageRAMCache: MessageRAMCache,
|
||||
resourceRAMCache: ResourceRAMCache,
|
||||
personalDB: PersonalDB,
|
||||
) {
|
||||
if (guildMetadata.memberId === null) {
|
||||
throw new Error('trying to launch guild that we have never verified with');
|
||||
}
|
||||
@ -198,7 +256,15 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
ca: socketConfig.cert,
|
||||
});
|
||||
const socketVerifier = new SocketVerifier(socket, socketConfig.publicKey, socketConfig.privateKey);
|
||||
return new CombinedGuild(guildMetadata.id, guildMetadata.memberId, socket, socketVerifier, messageRAMCache, resourceRAMCache, personalDB);
|
||||
return new CombinedGuild(
|
||||
guildMetadata.id,
|
||||
guildMetadata.memberId,
|
||||
socket,
|
||||
socketVerifier,
|
||||
messageRAMCache,
|
||||
resourceRAMCache,
|
||||
personalDB,
|
||||
);
|
||||
}
|
||||
|
||||
public isSocketVerified(): boolean {
|
||||
@ -220,7 +286,8 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
if (this.ramGuild.getChannels().size === 0) {
|
||||
await this.fetchChannels();
|
||||
}
|
||||
if (this.ramGuild.getChannels().size === 0) throw new Error('RAM Channels was not updated through fetchChannels');
|
||||
if (this.ramGuild.getChannels().size === 0)
|
||||
throw new Error('RAM Channels was not updated through fetchChannels');
|
||||
}
|
||||
|
||||
async grabRAMMembersMap(): Promise<Map<string, Member>> {
|
||||
@ -261,7 +328,11 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
connection.rolePriority = member.rolePriority;
|
||||
connection.privileges = member.privileges;
|
||||
} else {
|
||||
LOG.warn(`g#${this.id} unable to find self in members (of ${members.size}, [${Array.from(members.keys()).join(',')}], ${member}, ${this.memberId})`);
|
||||
LOG.warn(
|
||||
`g#${this.id} unable to find self in members (of ${members.size}, [${Array.from(
|
||||
members.keys(),
|
||||
).join(',')}], ${member}, ${this.memberId})`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const members = await this.personalDBGuild.fetchMembers();
|
||||
@ -283,15 +354,19 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
|
||||
// fetched through the triple-cache system (RAM -> Disk -> Server)
|
||||
async fetchMetadata(): Promise<GuildMetadata> {
|
||||
// xUtil.failSometimes(0.05); // for testing
|
||||
return await this.fetchable.fetchMetadata();
|
||||
}
|
||||
async fetchMembers(): Promise<Member[]> {
|
||||
// xUtil.failSometimes(0.05); // for testing
|
||||
return await this.fetchable.fetchMembers();
|
||||
}
|
||||
async fetchChannels(): Promise<Channel[]> {
|
||||
// xUtil.failSometimes(0.05); // for testing
|
||||
return await this.fetchable.fetchChannels();
|
||||
}
|
||||
async fetchMessagesRecent(channelId: string, number: number): Promise<Message[]> {
|
||||
// xUtil.failSometimes(0.05); // for testing
|
||||
const members = await this.grabRAMMembersMap();
|
||||
const channels = await this.grabRAMChannelsMap();
|
||||
const messages = await this.fetchable.fetchMessagesRecent(channelId, number);
|
||||
@ -301,6 +376,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
return messages;
|
||||
}
|
||||
async fetchMessagesBefore(channelId: string, messageOrderId: string, number: number): Promise<Message[]> {
|
||||
Util.failSometimes(0.5); // for testing
|
||||
const members = await this.grabRAMMembersMap();
|
||||
const channels = await this.grabRAMChannelsMap();
|
||||
const messages = await this.fetchable.fetchMessagesBefore(channelId, messageOrderId, number);
|
||||
@ -310,6 +386,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
return messages;
|
||||
}
|
||||
async fetchMessagesAfter(channelId: string, messageOrderId: string, number: number): Promise<Message[]> {
|
||||
// xUtil.failSometimes(0.05); // for testing
|
||||
const members = await this.grabRAMMembersMap();
|
||||
const channels = await this.grabRAMChannelsMap();
|
||||
const messages = await this.fetchable.fetchMessagesAfter(channelId, messageOrderId, number);
|
||||
@ -319,9 +396,11 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
return messages;
|
||||
}
|
||||
async fetchResource(resourceId: string): Promise<Resource> {
|
||||
// xUtil.failSometimes(0.05); // for testing
|
||||
return await this.fetchable.fetchResource(resourceId);
|
||||
}
|
||||
async fetchTokens(): Promise<Token[]> {
|
||||
// xUtil.failSometimes(0.05); // for testing
|
||||
const members = await this.grabRAMMembersMap();
|
||||
const tokens = await this.fetchable.fetchTokens();
|
||||
for (const token of tokens) {
|
||||
@ -335,7 +414,12 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
await this.socketGuild.requestSendMessage(channelId, text);
|
||||
}
|
||||
// TODO: Change to "withAttachment"
|
||||
async requestSendMessageWithResource(channelId: string, text: string | null, resource: Buffer, resourceName: string): Promise<void> {
|
||||
async requestSendMessageWithResource(
|
||||
channelId: string,
|
||||
text: string | null,
|
||||
resource: Buffer,
|
||||
resourceName: string,
|
||||
): Promise<void> {
|
||||
await this.socketGuild.requestSendMessageWithResource(channelId, text, resource, resourceName);
|
||||
}
|
||||
async requestSetStatus(status: string): Promise<void> {
|
||||
|
@ -32,7 +32,32 @@ export default class Util {
|
||||
// solution: Replace reserved characters with empty string (''), bad characters with '_', and append '_' to bad names
|
||||
|
||||
// illegal File Names (Windows)
|
||||
if (['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'].indexOf(name) !== -1) {
|
||||
if (
|
||||
[
|
||||
'CON',
|
||||
'PRN',
|
||||
'AUX',
|
||||
'NUL',
|
||||
'COM1',
|
||||
'COM2',
|
||||
'COM3',
|
||||
'COM4',
|
||||
'COM5',
|
||||
'COM6',
|
||||
'COM7',
|
||||
'COM8',
|
||||
'COM9',
|
||||
'LPT1',
|
||||
'LPT2',
|
||||
'LPT3',
|
||||
'LPT4',
|
||||
'LPT5',
|
||||
'LPT6',
|
||||
'LPT7',
|
||||
'LPT8',
|
||||
'LPT9',
|
||||
].indexOf(name) !== -1
|
||||
) {
|
||||
// TODO: case insensitive?
|
||||
name += '_';
|
||||
}
|
||||
@ -72,6 +97,11 @@ export default class Util {
|
||||
});
|
||||
}
|
||||
|
||||
/** throws an error based on a ratio from 0-1 */
|
||||
static failSometimes(ratio: number): void {
|
||||
if (Math.random() < ratio) throw new Error('fail sometimes');
|
||||
}
|
||||
|
||||
// this function is a promise for error stack tracking purposes.
|
||||
// this function expects the last argument of args to be a callback(err, serverData)
|
||||
static socketEmitTimeout(
|
||||
|
Loading…
Reference in New Issue
Block a user