From eeb999db791487db43e926faea49e892e807a60a Mon Sep 17 00:00:00 2001 From: Ryan Hopper-Lowe Date: Thu, 30 Jan 2025 23:04:12 -0600 Subject: [PATCH] chore: document service-primitives.ts --- .../app/lib/service/api/service-primitives.ts | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/ui/admin/app/lib/service/api/service-primitives.ts b/ui/admin/app/lib/service/api/service-primitives.ts index f73a1c914..9f3de71ea 100644 --- a/ui/admin/app/lib/service/api/service-primitives.ts +++ b/ui/admin/app/lib/service/api/service-primitives.ts @@ -5,8 +5,30 @@ type FetcherConfig = { signal?: AbortSignal; }; -export const RevalidateSkip = Symbol("RevalidateSkip"); +/** + * Allows us to skip matching on specific key segments + * @example + * revalidateArray(["Agents", SkipKey]) + * // will revalidate the following keys: + * ["Agents", "1234"] + * ["Agents", "5678"] + * // but not: + * ["Agents", "1234", "Threads"] + * ["Agents", "1234", "Threads", "5678"] + * + * // If exact is false: + * revalidateArray(["Agents", SkipKey], false) + * // will also revalidate the following keys: + * ["Agents", "1234", "Threads"] + * ["Agents", "1234", "Threads", "5678"] + */ +export const SkipKey = Symbol("SkipKey"); +/** + * Revalidates all keys that match the given key + * @param key - The key to match. Use SkipKey to skip matching on specific segments + * @param exact - Whether the key must match exactly + */ export const revalidateArray = ( key: TKey, exact = true @@ -15,11 +37,18 @@ export const revalidateArray = ( if (!Array.isArray(cacheKey)) return false; return ( - key.every((k, i) => [cacheKey[i], RevalidateSkip].includes(k)) && + key.every((k, i) => [cacheKey[i], SkipKey].includes(k)) && (!exact || cacheKey.length === key.length) ); }); +/** + * Creates a fetcher for a given API function + * @param input - The input schema + * @param handler - The API function + * @param key - The function that generates the UNIQUE key for the given params. This should include all dependencies of the method + * @returns The fetcher + */ export const createFetcher = < TInput extends ZodObject, TParams extends z.infer, @@ -32,28 +61,32 @@ export const createFetcher = < ) => { type KeyParams = NullishPartial; - const buildKey = (params: KeyParams) => { - const { data } = input.safeParse(params); - return data ? key(data as TParams) : null; - }; + /** Creates a closure to trigger abort controller on consecutive requests */ + let abortController: AbortController; + // this schema will skip any parameters that would cause an error const skippedSchema = z.object( Object.fromEntries( Object.entries(input.shape).map(([key, schema]) => [ key, - schema.catch(RevalidateSkip), + // this means that if a parameter would cause an error, it will be skipped + schema.catch(SkipKey), ]) ) ); + // this function will return null if the params are invalid + // SWR will not call the handler if the key is null + const buildKey = (params: KeyParams) => { + const { data } = input.safeParse(params); + return data ? key(data as TParams) : null; + }; + const buildRevalidator = (params: KeyParams, exact?: boolean) => { const data = skippedSchema.parse(params); revalidateArray(key(data as TParams), exact); }; - // create a closure to trigger abort controller on consecutive requests - let abortController: AbortController; - const handleFetch = (params: TParams, config: FetcherConfig) => { abortController?.abort(); @@ -64,9 +97,11 @@ export const createFetcher = < return { handler, key, + /** Creates a SWR key and fetcher for the given params. This works for both `useSWR` and `prefetch` from SWR */ swr: (params: KeyParams, config: FetcherConfig = {}) => { return [ buildKey(params), + // casting (params as TParams) is safe here because handleFetch will never be called when params are invalid () => handleFetch(params as TParams, config), ] as const; }, @@ -75,9 +110,15 @@ export const createFetcher = < }; }; +/** + * Creates a mutator for a given API function + * @param fn - The API function + * @returns The mutator + */ export const createMutator = ( fn: (params: TInput, config: FetcherConfig) => Promise ) => { + /** Creates a closure to trigger abort controller on consecutive requests */ let abortController: AbortController; return (params: TInput, config: FetcherConfig = {}) => {