All files / apps/web/src/handlers batch.ts

100% Statements 16/16
100% Branches 5/5
100% Functions 6/6
100% Lines 16/16

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111                            70x 70x                                                         70x                                 70x                                       191x 191x 897x 897x 897x   761x     761x                       897x 897x 897x 154x   897x      
import type {
	ProcedureResolverOptions,
	UnsetMarker,
} from "@trpc/server/unstable-core-do-not-import";
import type { Options } from "dataloader";
import Dataloader from "dataloader";
import { doNothing } from "remeda";
 
import type { MaybePromise } from "~utils/types";
import type {
	AuthorizedContext,
	UnauthorizedContext,
} from "~web/handlers/context";
 
const SCHEDULE_DELAY = 100;
const CLEAR_CACHE_DELAY = 2000;
 
type DefaultValue<TValue, TFallback> = TValue extends UnsetMarker
	? TFallback
	: TValue;
 
type ProcedureResolver<
	TContext,
	TMeta,
	TContextOverrides,
	TInputOut,
	TOutputParserIn,
	$Output,
> = (
	opts: ProcedureResolverOptions<TContext, TMeta, TContextOverrides, TInputOut>,
) => MaybePromise<
	// If an output parser is defined, we need to return what the parser expects, otherwise we return the inferred type
	DefaultValue<TOutputParserIn, $Output>
>;
 
type ProcedureResolverShort<C, I, O> = ProcedureResolver<
	C,
	unknown,
	object,
	I,
	UnsetMarker,
	O
>;
 
const defaultGetKey = <C extends UnauthorizedContext>(context: C): string => {
	// there are no batched requests with no auth yet
	/* c8 ignore start */
	if ("auth" in context) {
		return (context as AuthorizedContext).auth.userId;
	}
	return "anonymous";
	/* c8 ignore stop */
};
 
export type BatchLoadContextFn<
	C extends UnauthorizedContext,
	I,
	O,
	E extends Error = Error,
> = (ctx: C) => (keys: readonly I[]) => PromiseLike<(O | E)[]>;
 
export const queueCallFactory = <
	C extends UnauthorizedContext,
	I,
	O,
	E extends Error = Error,
>(
	batchLoadFn: BatchLoadContextFn<C, I, O, E>,
	{
		getKey,
		...batchOpts
	}: Options<I, O, string> & {
		getKey?: (ctx: C) => string;
	} = {},
): ProcedureResolverShort<C, I, O> => {
	const dataloaderStorage: Record<
		string,
		{
			dataloader: Dataloader<I, O, string>;
			removeTimeoutId: ReturnType<typeof setTimeout>;
		}
	> = {};
	return async (opts) => {
		const context = opts.ctx as C;
		const key = (getKey || defaultGetKey)(context);
		const dataloaderObject = dataloaderStorage[key] || {
			dataloader: new Dataloader<I, O, string>(
				(inputs) => batchLoadFn(context)(inputs),
				{
					batchScheduleFn: (callback) => {
						setTimeout(callback, SCHEDULE_DELAY);
					},
					cacheKeyFn: JSON.stringify,
					// Disable cache on test runs - subsequent calls with different data are happening in tests
					cache: !context.reqHeaders.get("x-test-id"),
					// Undocumented `opts.path` property
					name: (opts as unknown as { path: string }).path,
					...batchOpts,
				},
			),
			removeTimeoutId: setTimeout(doNothing(), 0),
		};
		dataloaderStorage[key] = dataloaderObject;
		clearTimeout(dataloaderObject.removeTimeoutId);
		dataloaderObject.removeTimeoutId = setTimeout(() => {
			delete dataloaderStorage[key];
		}, CLEAR_CACHE_DELAY);
		return dataloaderObject.dataloader.load(opts.input as I);
	};
};