cordis/src/client/webapp/elements/require/guild-subscriptions.ts

159 lines
5.9 KiB
TypeScript
Raw Normal View History

2021-12-13 04:01:30 +00:00
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);
});
}
}