import { useNavigate } from "@solidjs/router"; import { createResource, onCleanup, Resource, useContext } from "solid-js"; import type { ResourceOptions, ResourceSource } from "solid-js/types/reactive/signal"; import { isServer } from "solid-js/web"; import { isRedirectResponse, LocationHeader } from "../server/responses"; import { ServerContext } from "../server/ServerContext"; import { FETCH_EVENT, ServerFunctionEvent } from "../server/types"; interface RouteDataEvent extends ServerFunctionEvent {} type RouteResourceSource = S | false | null | undefined | (() => S | false | null | undefined); type RouteResourceFetcher = (source: S, event: RouteDataEvent) => T | Promise; const resources = new Set<(k: any) => void>(); export function createRouteData( fetcher: RouteResourceFetcher, options?: ResourceOptions ): Resource; export function createRouteData( fetcher: RouteResourceFetcher, options: ResourceOptions ): Resource; export function createRouteData( source: RouteResourceSource, fetcher: RouteResourceFetcher, options?: ResourceOptions ): Resource; export function createRouteData( source: RouteResourceSource, fetcher: RouteResourceFetcher, options: ResourceOptions ): Resource; export function createRouteData( source: RouteResourceSource | RouteResourceFetcher, fetcher?: RouteResourceFetcher | ResourceOptions | ResourceOptions, options?: ResourceOptions | ResourceOptions ): Resource | Resource { if (arguments.length === 2) { if (typeof fetcher === "object") { options = fetcher as ResourceOptions | ResourceOptions; fetcher = source as RouteResourceFetcher; source = true as ResourceSource; } } else if (arguments.length === 1) { fetcher = source as RouteResourceFetcher; source = true as ResourceSource; } const navigate = useNavigate(); const pageEvent = useContext(ServerContext); function handleResponse(response: Response) { if (isRedirectResponse(response)) { let url = response.headers.get(LocationHeader); if (url.startsWith("/")) { navigate(response.headers.get(LocationHeader), { replace: true }); } else { if (!isServer) { window.location.href = response.headers.get(LocationHeader); } } if (isServer) { pageEvent.setStatusCode(response.status); response.headers.forEach((head, value) => { pageEvent.responseHeaders.set(value, head); }); } } } const [resource, { refetch }] = createResource( source as RouteResourceSource, async (key, info) => { try { if (info.refetching && info.refetching !== true && !partialMatch(key, info.refetching)) { return info.value; } let event = pageEvent as RouteDataEvent; if (isServer) { event = Object.freeze({ request: pageEvent.request, env: pageEvent.env, $type: FETCH_EVENT, fetch: pageEvent.fetch }); } let response = await (fetcher as any).call(event, key, event); if (response instanceof Response) { if (isServer) { handleResponse(response); } else { setTimeout(() => handleResponse(response), 0); } } return response; } catch (e) { if (e instanceof Response) { if (isServer) { handleResponse(e); } else { setTimeout(() => handleResponse(e), 0); } return e; } throw e; } }, options ); resources.add(refetch); onCleanup(() => resources.delete(refetch)); return resource; } export function refetchRouteData(key?: string | any[] | void) { for (let refetch of resources) refetch(key); } /* React Query key matching https://github.com/tannerlinsley/react-query */ function partialMatch(a, b) { return partialDeepEqual(ensureQueryKeyArray(a), ensureQueryKeyArray(b)); } function ensureQueryKeyArray(value) { return Array.isArray(value) ? value : [value]; } /** * Checks if `b` partially matches with `a`. */ function partialDeepEqual(a, b) { if (a === b) { return true; } if (typeof a !== typeof b) { return false; } if (a.length && !b.length) return false; if (a && b && typeof a === "object" && typeof b === "object") { return !Object.keys(b).some(key => !partialDeepEqual(a[key], b[key])); } return false; }