159 lines
5.9 KiB
TypeScript
159 lines
5.9 KiB
TypeScript
|
import * as electronRemote from '@electron/remote';
|
||
|
const electronConsole = electronRemote.getGlobal('console') as Console;
|
||
|
import Logger from '../../../../logger/logger';
|
||
|
const LOG = Logger.create(__filename, electronConsole);
|
||
|
|
||
|
import { GuildMetadata, Resource } from "../../data-types";
|
||
|
import CombinedGuild from "../../guild-combined";
|
||
|
|
||
|
import React, { DependencyList, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||
|
import { AutoVerifierChangesType } from "../../auto-verifier";
|
||
|
import { Conflictable, Connectable } from "../../guild-types";
|
||
|
import { EventEmitter } from 'tsee';
|
||
|
import { IDQuery } from '../../auto-verifier-with-args';
|
||
|
|
||
|
export type SubscriptionEvents = {
|
||
|
'fetch': () => void;
|
||
|
'update': () => void;
|
||
|
'conflict': () => void;
|
||
|
'fetch-error': () => void;
|
||
|
}
|
||
|
|
||
|
interface EffectParams<T> {
|
||
|
guild: CombinedGuild;
|
||
|
onFetch: (value: T | null) => void;
|
||
|
onUpdate: (value: T) => void;
|
||
|
onConflict: (value: T) => void;
|
||
|
onFetchError: (e: unknown) => void;
|
||
|
}
|
||
|
|
||
|
type Arguments<T> = T extends (...args: infer A) => unknown ? A : never;
|
||
|
|
||
|
interface EventMappingParams<T, UE extends keyof Connectable, CE extends keyof Conflictable> {
|
||
|
updateEventName: UE;
|
||
|
updateEventArgsMap: (...args: Arguments<Connectable[UE]>) => T; // should be same as the params list from Connectable
|
||
|
conflictEventName: CE;
|
||
|
conflictEventArgsMap: (...args: Arguments<Conflictable[CE]>) => T; // should be the same as the params list from Conflictable
|
||
|
fetchDeps: DependencyList;
|
||
|
}
|
||
|
|
||
|
export default class GuildSubscriptions {
|
||
|
private static useGuildSubscriptionEffect<T, UE extends keyof Connectable, CE extends keyof Conflictable>(
|
||
|
subscriptionParams: EffectParams<T>, eventMappingParams: EventMappingParams<T, UE, CE>, fetchFunc: (() => Promise<T>) | (() => Promise<T | null>)
|
||
|
) {
|
||
|
const { guild, onFetch, onUpdate, onConflict, onFetchError } = subscriptionParams;
|
||
|
const { updateEventName, updateEventArgsMap, conflictEventName, conflictEventArgsMap, fetchDeps } = eventMappingParams;
|
||
|
|
||
|
const isMounted = useRef(false);
|
||
|
|
||
|
const fetchManagerFunc = useCallback(async () => {
|
||
|
if (!isMounted.current) return;
|
||
|
try {
|
||
|
const value = await fetchFunc();
|
||
|
if (!isMounted.current) return;
|
||
|
onFetch(value);
|
||
|
} catch (e: unknown) {
|
||
|
LOG.error('error fetching for subscription', e);
|
||
|
if (!isMounted.current) return;
|
||
|
onFetchError(e);
|
||
|
}
|
||
|
}, [ ...fetchDeps, fetchFunc ]);
|
||
|
|
||
|
const boundUpdateFunc = useCallback((...args: Arguments<Connectable[UE]>): void => {
|
||
|
if (!isMounted.current) return;
|
||
|
const value = updateEventArgsMap(...args);
|
||
|
onUpdate(value);
|
||
|
}, []) as (Connectable & Conflictable)[UE]; // I think the typed EventEmitter class isn't ready for this level of type safety
|
||
|
const boundConflictFunc = useCallback((...args: Arguments<Conflictable[CE]>): void => {
|
||
|
if (!isMounted.current) return;
|
||
|
const value = conflictEventArgsMap(...args); // otherwise, I may have done this wrong. Using never to force it to work
|
||
|
onConflict(value);
|
||
|
}, []) as (Connectable & Conflictable)[CE];
|
||
|
|
||
|
useEffect(() => {
|
||
|
isMounted.current = true;
|
||
|
|
||
|
// Bind guild events to make sure we have the most up to date information
|
||
|
guild.on('connect', fetchManagerFunc);
|
||
|
guild.on(updateEventName, boundUpdateFunc);
|
||
|
guild.on(conflictEventName, boundConflictFunc);
|
||
|
|
||
|
// Fetch the data once
|
||
|
fetchManagerFunc();
|
||
|
|
||
|
return () => {
|
||
|
isMounted.current = false;
|
||
|
|
||
|
// Unbind the events so that we don't have any memory leaks
|
||
|
guild.off('connect', fetchManagerFunc);
|
||
|
guild.off(updateEventName, boundUpdateFunc);
|
||
|
guild.off(conflictEventName, boundConflictFunc);
|
||
|
}
|
||
|
}, [ fetchManagerFunc ]);
|
||
|
}
|
||
|
|
||
|
private static useGuildSubscription<T, UE extends keyof Connectable, CE extends keyof Conflictable>(
|
||
|
guild: CombinedGuild, eventMappingParams: EventMappingParams<T, UE, CE>, fetchFunc: (() => Promise<T>) | (() => Promise<T | null>)
|
||
|
): [value: T | null, fetchError: unknown | null, events: EventEmitter<SubscriptionEvents>] {
|
||
|
const [ fetchError, setFetchError ] = useState<unknown | null>(null);
|
||
|
const [ value, setValue ] = useState<T | null>(null);
|
||
|
|
||
|
const events = useMemo(() => new EventEmitter<SubscriptionEvents>(), []);
|
||
|
|
||
|
const onFetch = useCallback((fetchValue: T | null) => {
|
||
|
setValue(fetchValue);
|
||
|
setFetchError(null);
|
||
|
events.emit('fetch');
|
||
|
}, []);
|
||
|
|
||
|
const onFetchError = useCallback((e: unknown) => {
|
||
|
setFetchError(e);
|
||
|
setValue(null);
|
||
|
events.emit('fetch-error');
|
||
|
}, []);
|
||
|
|
||
|
const onUpdate = useCallback((updateValue: T) => {
|
||
|
setValue(updateValue);
|
||
|
events.emit('update');
|
||
|
}, []);
|
||
|
|
||
|
const onConflict = useCallback((conflictValue: T) => {
|
||
|
setValue(conflictValue);
|
||
|
events.emit('conflict');
|
||
|
}, []);
|
||
|
|
||
|
GuildSubscriptions.useGuildSubscriptionEffect({
|
||
|
guild,
|
||
|
onFetch,
|
||
|
onUpdate,
|
||
|
onConflict,
|
||
|
onFetchError
|
||
|
}, eventMappingParams, fetchFunc);
|
||
|
|
||
|
return [ value, fetchError, events ];
|
||
|
}
|
||
|
|
||
|
static useGuildMetadataSubscription(guild: CombinedGuild) {
|
||
|
return GuildSubscriptions.useGuildSubscription<GuildMetadata, 'update-metadata', 'conflict-metadata'>(guild, {
|
||
|
updateEventName: 'update-metadata',
|
||
|
updateEventArgsMap: (guildMeta: GuildMetadata) => guildMeta,
|
||
|
conflictEventName: 'conflict-metadata',
|
||
|
conflictEventArgsMap: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => newGuildMeta,
|
||
|
fetchDeps: [ guild ]
|
||
|
}, async () => await guild.fetchMetadata());
|
||
|
}
|
||
|
|
||
|
static useResourceSubscription(guild: CombinedGuild, resourceId: string | null) {
|
||
|
return GuildSubscriptions.useGuildSubscription<Resource, 'update-resource', 'conflict-resource'>(guild, {
|
||
|
updateEventName: 'update-resource',
|
||
|
updateEventArgsMap: (resource: Resource) => resource,
|
||
|
conflictEventName: 'conflict-resource',
|
||
|
conflictEventArgsMap: (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => newResource,
|
||
|
fetchDeps: [ guild, resourceId ]
|
||
|
}, async () => {
|
||
|
if (resourceId === null) return null;
|
||
|
return await guild.fetchResource(resourceId);
|
||
|
});
|
||
|
}
|
||
|
}
|