more specific about single vs multiple subscriptions

This commit is contained in:
Michael Peters 2021-12-12 22:34:29 -06:00
parent 2ef1af3eff
commit 2a79af8a0e
2 changed files with 55 additions and 47 deletions

View File

@ -81,9 +81,7 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
const [ avatarInputValid, setAvatarInputValid ] = useState<boolean>(false);
const [ exampleAvatarBuff, exampleAvatarBuffError ] = ReactHelper.useAsyncActionSubscription(
async () => {
return await fs.readFile(exampleAvatarPath);
},
async () => await fs.readFile(exampleAvatarPath),
null,
[ exampleAvatarPath ]
);

View File

@ -22,9 +22,9 @@ export type SubscriptionEvents = {
interface EffectParams<T> {
guild: CombinedGuild;
onFetch: (value: T | null) => void;
onUpdate: (value: T) => void;
onConflict: (value: T) => void;
onFetchError: (e: unknown) => void;
bindEventsFunc: () => void;
unbindEventsFunc: () => void;
}
type Arguments<T> = T extends (...args: infer A) => unknown ? A : never;
@ -34,67 +34,57 @@ interface EventMappingParams<T, UE extends keyof Connectable, CE extends keyof C
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>)
private static useGuildSubscriptionEffect<T>(
isMountedRef: React.MutableRefObject<boolean>,
subscriptionParams: EffectParams<T>,
fetchFunc: (() => Promise<T>) | (() => Promise<T | null>),
fetchDeps: DependencyList
) {
const { guild, onFetch, onUpdate, onConflict, onFetchError } = subscriptionParams;
const { updateEventName, updateEventArgsMap, conflictEventName, conflictEventArgsMap, fetchDeps } = eventMappingParams;
const isMounted = useRef(false);
const { guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc } = subscriptionParams;
const fetchManagerFunc = useCallback(async () => {
if (!isMounted.current) return;
if (!isMountedRef.current) return;
try {
const value = await fetchFunc();
if (!isMounted.current) return;
if (!isMountedRef.current) return;
onFetch(value);
} catch (e: unknown) {
LOG.error('error fetching for subscription', e);
if (!isMounted.current) return;
if (!isMountedRef.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;
isMountedRef.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);
bindEventsFunc();
// Fetch the data once
fetchManagerFunc();
return () => {
isMounted.current = false;
isMountedRef.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);
unbindEventsFunc();
}
}, [ 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>)
private static useSingleGuildSubscription<T, UE extends keyof Connectable, CE extends keyof Conflictable>(
guild: CombinedGuild, eventMappingParams: EventMappingParams<T, UE, CE>, fetchFunc: (() => Promise<T>) | (() => Promise<T | null>), fetchDeps: DependencyList
): [value: T | null, fetchError: unknown | null, events: EventEmitter<SubscriptionEvents>] {
const { updateEventName, updateEventArgsMap, conflictEventName, conflictEventArgsMap } = eventMappingParams;
const isMountedRef = useRef<boolean>(false);
const [ fetchError, setFetchError ] = useState<unknown | null>(null);
const [ value, setValue ] = useState<T | null>(null);
@ -122,37 +112,57 @@ export default class GuildSubscriptions {
events.emit('conflict');
}, []);
GuildSubscriptions.useGuildSubscriptionEffect({
// 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
const boundUpdateFunc = useCallback((...args: Arguments<Connectable[UE]>): void => {
if (!isMountedRef.current) return;
const value = updateEventArgsMap(...args);
onUpdate(value);
}, []) as (Connectable & Conflictable)[UE];
const boundConflictFunc = useCallback((...args: Arguments<Conflictable[CE]>): void => {
if (!isMountedRef.current) return;
const value = conflictEventArgsMap(...args);
onConflict(value);
}, []) as (Connectable & Conflictable)[CE];
const bindEventsFunc = useCallback(() => {
guild.on(updateEventName, boundUpdateFunc);
guild.on(conflictEventName, boundConflictFunc);
}, []);
const unbindEventsFunc = useCallback(() => {
guild.off(updateEventName, boundUpdateFunc);
guild.off(conflictEventName, boundConflictFunc);
}, []);
GuildSubscriptions.useGuildSubscriptionEffect(isMountedRef, {
guild,
onFetch,
onUpdate,
onConflict,
onFetchError
}, eventMappingParams, fetchFunc);
onFetchError,
bindEventsFunc,
unbindEventsFunc
}, fetchFunc, fetchDeps);
return [ value, fetchError, events ];
}
static useGuildMetadataSubscription(guild: CombinedGuild) {
return GuildSubscriptions.useGuildSubscription<GuildMetadata, 'update-metadata', 'conflict-metadata'>(guild, {
return GuildSubscriptions.useSingleGuildSubscription<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());
conflictEventArgsMap: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => newGuildMeta
}, async () => await guild.fetchMetadata(), [ guild ]);
}
static useResourceSubscription(guild: CombinedGuild, resourceId: string | null) {
return GuildSubscriptions.useGuildSubscription<Resource, 'update-resource', 'conflict-resource'>(guild, {
return GuildSubscriptions.useSingleGuildSubscription<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 ]
conflictEventArgsMap: (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => newResource
}, async () => {
if (resourceId === null) return null;
return await guild.fetchResource(resourceId);
});
}, [ guild, resourceId ]);
}
}