diff --git a/src/app/api/base.ts b/src/app/api/base.ts index f125c42d20..a9cb2dc892 100644 --- a/src/app/api/base.ts +++ b/src/app/api/base.ts @@ -21,9 +21,21 @@ type ApiEndpoint = typeof API_ENDPOINTS; export type ApiEndpointKey = keyof ApiEndpoint; type ApiUrl = `${typeof SERVICE_API}${ApiEndpoint[ApiEndpointKey]}`; +/** + * Constructs a complete API URL from a given endpoint key. + * @param endpoint The endpoint key + * @returns A full API URL (string) + */ export const getFullApiUrl = (endpoint: ApiEndpointKey): ApiUrl => `${SERVICE_API}${API_ENDPOINTS[endpoint]}`; +/** + * Runs a given fetch request with the appropriate headers and CSRF token. + * + * @param url The URL to fetch + * @param options RequestInit options + * @returns The JSON response from the fetch + */ export const fetchWithAuth = async (url: string, options: RequestInit = {}) => { const csrftoken = getCookie("csrftoken"); const headers = { diff --git a/src/app/api/endpoints.ts b/src/app/api/endpoints.ts index bd877e9b1d..04d12da4b1 100644 --- a/src/app/api/endpoints.ts +++ b/src/app/api/endpoints.ts @@ -1,5 +1,10 @@ import { fetchWithAuth, getFullApiUrl } from "@/app/api/base"; import type { Zone } from "@/app/store/zone/types"; +/** + * Fetches a list of zones. + * + * @returns The list of zones + */ export const fetchZones = (): Promise => fetchWithAuth(getFullApiUrl("zones")); diff --git a/src/app/api/query-client.ts b/src/app/api/query-client.ts index 823a9e72dc..23964d4544 100644 --- a/src/app/api/query-client.ts +++ b/src/app/api/query-client.ts @@ -1,5 +1,6 @@ import { QueryClient } from "@tanstack/react-query"; +// Different query keys for different methods. export const queryKeys = { zones: { list: ["zones"], @@ -10,23 +11,28 @@ type QueryKeys = typeof queryKeys; type QueryKeyCategories = keyof QueryKeys; type QueryKeySubcategories = keyof QueryKeys[T]; +// This basically exists to get us the query key as it appears in react query, i.e. a string in an array. export type QueryKey = QueryKeys[QueryKeyCategories][QueryKeySubcategories]; // first element of the queryKeys array +// Idk what Peter meant by the above comment, but QueryModel is basically QueryKey but not in an array from what I understand. export type QueryModel = QueryKey[number]; +// 5 minutes feels rather long for default stale time. export const defaultQueryOptions = { staleTime: 5 * 60 * 1000, // 5 minutes cacheTime: 15 * 60 * 1000, // 15 minutes refetchOnWindowFocus: true, } as const; +// 0 is far too quick lol but at least we're not using this. export const realTimeQueryOptions = { staleTime: 0, cacheTime: 60 * 1000, // 1 minute } as const; +// This just creates a query client and provides the options specified above. export const createQueryClient = () => new QueryClient({ defaultOptions: { diff --git a/src/app/api/query/base.ts b/src/app/api/query/base.ts index a879b69b01..47be9e3a98 100644 --- a/src/app/api/query/base.ts +++ b/src/app/api/query/base.ts @@ -11,6 +11,11 @@ import statusSelectors from "@/app/store/status/selectors"; import type { WebSocketEndpointModel } from "@/websocket-client"; import { WebSocketMessageType } from "@/websocket-client"; +/** + * Provides a hook to subscribe to NOTIFY messages from the websocket. + * + * @returns An object with a subscribe function that takes a callback to run when a NOTIFY message is received. + */ export const useWebSocket = () => { const websocketClient = useContext(WebSocketContext); @@ -18,15 +23,20 @@ export const useWebSocket = () => { throw new Error("useWebSocket must be used within a WebSocketProvider"); } + // Listen for NOTIFY messages and run a callback when received const subscribe = useCallback( (callback: (msg: any) => void) => { if (!websocketClient.rws) return; const messageHandler = (messageEvent: MessageEvent) => { const data = JSON.parse(messageEvent.data); + // if we get a NOTIFY, run the provided callback if (data.type === WebSocketMessageType.NOTIFY) callback(data); }; + // add an event listener for NOTIFY messages websocketClient.rws.addEventListener("message", messageHandler); + + // this is a function to remove that event listener, it gets called in a cleanup effect down below. return () => websocketClient.rws?.removeEventListener("message", messageHandler); }, @@ -40,6 +50,16 @@ const wsToQueryKeyMapping: Partial> = { zone: "zones", // Add more mappings as needed } as const; + +/** + * A function to run a query which invalidates the query cache when a + * websocket message is received, or when the websocket reconnects. + * + * @param queryKey The query key to use + * @param queryFn The query function to run + * @param options Options for useQuery + * @returns The return value of useQuery + */ export function useWebsocketAwareQuery< TQueryFnData = unknown, TError = unknown, @@ -60,13 +80,17 @@ export function useWebsocketAwareQuery< const previousConnectedCount = usePrevious(connectedCount); useEffect(() => { + // connectedCount will change if the websocket reconnects - if this happens, we should invalidate the query if (connectedCount !== previousConnectedCount) { queryClient.invalidateQueries({ queryKey }); } }, [connectedCount, previousConnectedCount, queryClient, queryKey]); useEffect(() => { + // subscribe returns a function to remove the event listener for NOTIFY messages; + // This function will be used as the cleanup function for the effect. return subscribe( + // This callback function will be called when a NOTIFY message is received ({ name: model }: { action: string; name: WebSocketEndpointModel }) => { const mappedKey = wsToQueryKeyMapping[model]; const modelQueryKey = queryKey[0]; diff --git a/src/app/api/query/zones.ts b/src/app/api/query/zones.ts index 64f7403208..e20dacf0cf 100644 --- a/src/app/api/query/zones.ts +++ b/src/app/api/query/zones.ts @@ -8,15 +8,29 @@ const zoneKeys = { list: ["zones"] as const, }; +/** + * Fetches a list of zones. + * + * @returns The zones list as a UseQueryResult + */ export const useZones = () => { return useWebsocketAwareQuery(zoneKeys.list, fetchZones); }; +/** + * Fetches the number of zones. + * @returns The number of zones as a UseQueryResult + */ export const useZoneCount = () => useWebsocketAwareQuery(["zones"], fetchZones, { select: (data) => data?.length ?? 0, }); +/** + * Get a zone by its ID from the list result. + * @param id The zone's ID + * @returns A Zone, or null as a UseQueryResult + */ export const useZoneById = (id?: ZonePK | null) => useWebsocketAwareQuery(zoneKeys.list, fetchZones, { select: selectById(id ?? null),