|
| 1 | +import { createContext } from 'react'; |
| 2 | +import type { MutatorCallback, MutatorOptions, SWRResponse } from 'swr'; |
| 3 | +import useSWR, { useSWRConfig } from 'swr'; |
| 4 | + |
| 5 | +/** |
| 6 | + * Context type for configuring react hooks. |
| 7 | + */ |
| 8 | +export type RequestHandlerContext = { |
| 9 | + endpoint: string; |
| 10 | +}; |
| 11 | + |
| 12 | +/** |
| 13 | + * Context for configuring react hooks. |
| 14 | + */ |
| 15 | +export const RequestHandlerContext = createContext<RequestHandlerContext>({ |
| 16 | + endpoint: '/api/model', |
| 17 | +}); |
| 18 | + |
| 19 | +/** |
| 20 | + * Context provider. |
| 21 | + */ |
| 22 | +export const Provider = RequestHandlerContext.Provider; |
| 23 | + |
| 24 | +/** |
| 25 | + * Client request options |
| 26 | + */ |
| 27 | +export type RequestOptions<T> = { |
| 28 | + // disable data fetching |
| 29 | + disabled?: boolean; |
| 30 | + initialData?: T; |
| 31 | +}; |
| 32 | + |
| 33 | +/** |
| 34 | + * Makes a GET request with SWR. |
| 35 | + * |
| 36 | + * @param url The request URL. |
| 37 | + * @param args The request args object, which will be superjson-stringified and appended as "?q=" parameter |
| 38 | + * @returns SWR response |
| 39 | + */ |
| 40 | +// eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 41 | +export function get<Data, Error = any>( |
| 42 | + url: string | null, |
| 43 | + args?: unknown, |
| 44 | + options?: RequestOptions<Data> |
| 45 | +): SWRResponse<Data, Error> { |
| 46 | + const reqUrl = options?.disabled ? null : url ? makeUrl(url, args) : null; |
| 47 | + return useSWR<Data, Error>(reqUrl, fetcher, { |
| 48 | + fallbackData: options?.initialData, |
| 49 | + }); |
| 50 | +} |
| 51 | + |
| 52 | +/** |
| 53 | + * Makes a POST request. |
| 54 | + * |
| 55 | + * @param url The request URL. |
| 56 | + * @param data The request data. |
| 57 | + * @param mutate Mutator for invalidating cache. |
| 58 | + */ |
| 59 | +export async function post<Data, Result>(url: string, data: Data, mutate: Mutator): Promise<Result> { |
| 60 | + const r: Result = await fetcher(url, { |
| 61 | + method: 'POST', |
| 62 | + headers: { |
| 63 | + 'content-type': 'application/json', |
| 64 | + }, |
| 65 | + body: marshal(data), |
| 66 | + }); |
| 67 | + mutate(); |
| 68 | + return r; |
| 69 | +} |
| 70 | + |
| 71 | +/** |
| 72 | + * Makes a PUT request. |
| 73 | + * |
| 74 | + * @param url The request URL. |
| 75 | + * @param data The request data. |
| 76 | + * @param mutate Mutator for invalidating cache. |
| 77 | + */ |
| 78 | +export async function put<Data, Result>(url: string, data: Data, mutate: Mutator): Promise<Result> { |
| 79 | + const r: Result = await fetcher(url, { |
| 80 | + method: 'PUT', |
| 81 | + headers: { |
| 82 | + 'content-type': 'application/json', |
| 83 | + }, |
| 84 | + body: marshal(data), |
| 85 | + }); |
| 86 | + mutate(); |
| 87 | + return r; |
| 88 | +} |
| 89 | + |
| 90 | +/** |
| 91 | + * Makes a DELETE request. |
| 92 | + * |
| 93 | + * @param url The request URL. |
| 94 | + * @param args The request args object, which will be superjson-stringified and appended as "?q=" parameter |
| 95 | + * @param mutate Mutator for invalidating cache. |
| 96 | + */ |
| 97 | +export async function del<Result>(url: string, args: unknown, mutate: Mutator): Promise<Result> { |
| 98 | + const reqUrl = makeUrl(url, args); |
| 99 | + const r: Result = await fetcher(reqUrl, { |
| 100 | + method: 'DELETE', |
| 101 | + }); |
| 102 | + const path = url.split('/'); |
| 103 | + path.pop(); |
| 104 | + mutate(); |
| 105 | + return r; |
| 106 | +} |
| 107 | + |
| 108 | +type Mutator = ( |
| 109 | + data?: unknown | Promise<unknown> | MutatorCallback, |
| 110 | + opts?: boolean | MutatorOptions |
| 111 | +) => Promise<unknown[]>; |
| 112 | + |
| 113 | +export function getMutate(prefixes: string[]): Mutator { |
| 114 | + // https://swr.vercel.app/docs/advanced/cache#mutate-multiple-keys-from-regex |
| 115 | + const { cache, mutate } = useSWRConfig(); |
| 116 | + return (data?: unknown | Promise<unknown> | MutatorCallback, opts?: boolean | MutatorOptions) => { |
| 117 | + if (!(cache instanceof Map)) { |
| 118 | + throw new Error('mutate requires the cache provider to be a Map instance'); |
| 119 | + } |
| 120 | + |
| 121 | + const keys = Array.from(cache.keys()).filter( |
| 122 | + (k) => typeof k === 'string' && prefixes.some((prefix) => k.startsWith(prefix)) |
| 123 | + ) as string[]; |
| 124 | + const mutations = keys.map((key) => mutate(key, data, opts)); |
| 125 | + return Promise.all(mutations); |
| 126 | + }; |
| 127 | +} |
| 128 | + |
| 129 | +export async function fetcher<R>(url: string, options?: RequestInit) { |
| 130 | + const res = await fetch(url, options); |
| 131 | + if (!res.ok) { |
| 132 | + const error: Error & { info?: unknown; status?: number } = new Error( |
| 133 | + 'An error occurred while fetching the data.' |
| 134 | + ); |
| 135 | + error.info = unmarshal(await res.text()); |
| 136 | + error.status = res.status; |
| 137 | + throw error; |
| 138 | + } |
| 139 | + |
| 140 | + const textResult = await res.text(); |
| 141 | + try { |
| 142 | + return unmarshal(textResult) as R; |
| 143 | + } catch (err) { |
| 144 | + console.error(`Unable to deserialize data:`, textResult); |
| 145 | + throw err; |
| 146 | + } |
| 147 | +} |
0 commit comments