import type { AnyFunction, AnyFunctionMap } from 'apollo-server-types'; type Args = F extends (...args: infer A) => any ? A : never; type AsFunction = F extends AnyFunction ? F : never; type StripPromise = T extends Promise ? U : never; type DidEndHook = (...args: TArgs) => void; type AsyncDidEndHook = (...args: TArgs) => Promise; export class Dispatcher { constructor(protected targets: T[]) {} private callTargets( methodName: TMethodName, ...args: Args ): ReturnType>[] { return this.targets.map((target) => { const method = target[methodName]; if (typeof method === 'function') { return method.apply(target, args); } }); } public hasHook(methodName: keyof T): boolean { return this.targets.some( (target) => typeof target[methodName] === 'function', ); } public async invokeHook< TMethodName extends keyof T, THookReturn extends StripPromise>>, >( methodName: TMethodName, ...args: Args ): Promise { return Promise.all(this.callTargets(methodName, ...args)); } public async invokeHooksUntilNonNull( methodName: TMethodName, ...args: Args ): Promise>> | null> { for (const target of this.targets) { const method = target[methodName]; if (typeof method !== 'function') { continue; } const value = await method.apply(target, args); if (value !== null) { return value; } } return null; } public async invokeDidStartHook< TMethodName extends keyof T, TEndHookArgs extends Args< StripPromise>> >, >( methodName: TMethodName, ...args: Args ): Promise> { const hookReturnValues: (AsyncDidEndHook | void)[] = await Promise.all(this.callTargets(methodName, ...args)); const didEndHooks = hookReturnValues.filter( (hook): hook is AsyncDidEndHook => !!hook, ); didEndHooks.reverse(); return async (...args: TEndHookArgs) => { await Promise.all(didEndHooks.map((hook) => hook(...args))); }; } // Almost all hooks are async, but as a special case, willResolveField is sync // due to performance concerns. public invokeSyncDidStartHook< TMethodName extends keyof T, TEndHookArgs extends Args>>, >( methodName: TMethodName, ...args: Args ): DidEndHook { const didEndHooks: DidEndHook[] = []; for (const target of this.targets) { const method = target[methodName]; if (typeof method === 'function') { const didEndHook = method.apply(target, args); if (didEndHook) { didEndHooks.push(didEndHook); } } } didEndHooks.reverse(); return (...args: TEndHookArgs) => { for (const didEndHook of didEndHooks) { didEndHook(...args); } }; } }