UNPKG

elhala-query

Version:

A lightweight, educational implementation of React Query concepts with Suspense support.

408 lines (398 loc) 10.6 kB
// src/useQuery.tsx import { useSyncExternalStore, useRef, useCallback as useCallback2 } from "react"; // src/client-provider.tsx import { useContext, createContext, useCallback, useEffect } from "react"; import { jsx } from "react/jsx-runtime"; var elhalaClientContext = createContext(null); var useElhalaClient = () => { const client = useContext(elhalaClientContext); if (!client) { throw new Error( "useElhalaClient must be used within an ElhalaClientProvider" ); } return client; }; var ElhalaProvider = ({ client, children }) => { const onFocus = useCallback(() => { client.queries.forEach((query) => { query.subscribers.forEach((sub) => sub.notify()); }); }, [client]); useEffect(() => { const controller = new AbortController(); window.addEventListener("focus", onFocus, { signal: controller.signal }); window.addEventListener("visibilitychange", onFocus, { signal: controller.signal }); return () => controller.abort(); }, [onFocus]); return /* @__PURE__ */ jsx(elhalaClientContext.Provider, { value: client, children }); }; // src/queryObserver.ts function createQueryObserver(client, { queryFn, queryKey, cacheTime, staleTime = 0 }) { const query = client.getQuery({ queryFn, queryKey, cacheTime }); let staleCheckTimer = null; const setupStaleCheck = () => { if (staleTime <= 0 || !query.state.lastUpdated) return; if (staleCheckTimer) { clearTimeout(staleCheckTimer); staleCheckTimer = null; } const timeUntilStale = staleTime - (Date.now() - query.state.lastUpdated); if (timeUntilStale > 0) { staleCheckTimer = setTimeout(() => { query.fetch(); }, timeUntilStale); } }; const observer = { notify: () => { setupStaleCheck(); return query.state; }, getResults: () => query.state, fetch: () => { if (!query.state.lastUpdated || Date.now() - query.state.lastUpdated > staleTime) { query.fetch(); } }, subscribe: (callback) => { observer.notify = () => { setupStaleCheck(); callback(); return query.state; }; const unsub = query.subscribe(observer); observer.fetch(); setupStaleCheck(); return () => { unsub(); if (staleCheckTimer) { clearTimeout(staleCheckTimer); staleCheckTimer = null; } }; }, fetchOptimistic: () => { return query.fetch(); } }; return observer; } // src/useQuery.tsx function useQuery(options) { const client = useElhalaClient(); const observer = useRef(null); if (!observer.current) { observer.current = createQueryObserver(client, options); } useSyncExternalStore( useCallback2((onStoreChange) => { return observer.current.subscribe(onStoreChange); }, []), () => observer.current?.getResults(), () => observer.current?.getResults() ); return observer.current.getResults(); } // src/useMutation.ts import { useSyncExternalStore as useSyncExternalStore2, useRef as useRef2, useCallback as useCallback3 } from "react"; // src/mutationObserver.ts function createMutationObserver(client, options) { const mutation = client.getMutation(options); const observer = { notify: () => mutation.state, getResults: () => mutation.state, subscribe: (callback) => { observer.notify = callback; const unsub = mutation.subscribe(observer); return unsub; } }; return observer; } // src/useMutation.ts function useMutation(options) { const client = useElhalaClient(); const observer = useRef2(null); if (!observer.current) { observer.current = createMutationObserver(client, options); } useSyncExternalStore2( useCallback3((onStoreChange) => { return observer.current.subscribe(onStoreChange); }, []), () => observer.current?.getResults(), () => observer.current?.getResults() ); return observer.current.getResults(); } // src/mutation.ts function createMutation({ mutationFn, mutationKey, onError, onMutate, onSettled, onSuccess }) { let mutation = { subscribers: [], subscribe: (subscriber) => { mutation.subscribers.push(subscriber); return () => { mutation.subscribers = mutation.subscribers.filter( (sub) => sub !== subscriber ); }; }, setState: (updater) => { mutation.state = updater(mutation.state); mutation.subscribers.forEach((sub) => sub.notify()); }, options: { mutationFn, mutationKey, onError, onMutate, onSettled, onSuccess, mutationHash: JSON.stringify(mutationKey) }, state: { data: void 0, error: void 0, isLoading: false, isSuccess: false, isError: false, status: "idle", mutate: async (variables) => { mutation.setState((state) => ({ ...state, isLoading: true, isSuccess: false, isError: false, status: "loading" })); mutation.options.onMutate?.(); try { const data = await mutation.options.mutationFn(variables); mutation.setState((state) => ({ ...state, data, isSuccess: true, isError: false, status: "success" })); mutation.options.onSuccess?.(data); } catch (error) { mutation.setState((state) => ({ ...state, isSuccess: false, isError: true, status: "error", error })); mutation.options.onError?.(error); } finally { mutation.setState((state) => ({ ...state, isLoading: false, status: "idle" })); mutation.options.onSettled?.(); } } } }; return mutation; } // src/query.ts var DEFAULT_CACHE_TIME = 1e3 * 60 * 5; function createQuery(client, { queryFn, queryKey, cacheTime = DEFAULT_CACHE_TIME }) { let queryHash = JSON.stringify(queryKey); let query = { subscribers: [], state: { data: void 0, error: void 0, isLoading: true, isFetching: true, isError: false, status: "loading", lastUpdated: void 0, refetch: () => { query.fetch(); } }, promise: void 0, options: { queryFn, queryHash, queryKey }, gcTimeout: void 0, scheduleGC: () => { query.gcTimeout = setTimeout(() => { client.removeQuery(query); }, cacheTime); }, unscheduleGC: () => { clearTimeout(query.gcTimeout); }, subscribe: (subscriber) => { query.subscribers.push(subscriber); query.unscheduleGC(); return () => { query.subscribers = query.subscribers.filter( (sub) => sub !== subscriber ); if (query.subscribers.length === 0) { query.scheduleGC(); } }; }, setState: (updater) => { query.state = updater(query.state); query.subscribers.forEach((sub) => sub.notify()); }, fetch: () => { if (!query.promise) { query.promise = (async () => { query.setState((state) => ({ ...state, isFetching: true })); try { let data = await query.options.queryFn({ queryKey: query.options.queryKey }); query.setState((state) => ({ ...state, data, status: "success", isError: false, lastUpdated: Date.now() })); } catch (error) { query.setState((state) => ({ ...state, error, isError: true, status: "error" })); } finally { query.promise = void 0; query.setState((state) => ({ ...state, isFetching: false, isLoading: false })); } })(); } return query.promise; } }; return query; } // src/elhalaClient.ts var ElhalaClient = class { constructor() { this.queries = /* @__PURE__ */ new Map(); this.mutations = /* @__PURE__ */ new Map(); } getQuery(options) { let queryHash = JSON.stringify(options.queryKey); if (this.queries.has(queryHash)) { return this.queries.get(queryHash); } else { let query = createQuery(this, options); this.queries.set(queryHash, query); return query; } } getQueryData(options) { return this.getQuery(options).state.data; } setQueryData(options, data) { let query = this.getQuery(options); query.setState((state) => ({ ...state, data })); } removeQuery(query) { this.queries.delete(query.options.queryHash); } invalidateQueries({ queryKey }) { this.queries.forEach((query) => { if (queryKey.length === 0 || queryKey.some((key) => query.options.queryKey.includes(key))) { query.setState((state) => ({ ...state, isLoading: true, isFetching: true })); query.fetch(); } }); } clearQueriesCache() { this.queries.clear(); } getMutation(options) { let mutationHash = JSON.stringify(options.mutationKey); if (this.mutations.has(mutationHash)) { return this.mutations.get(mutationHash); } else { let mutation = createMutation(options); this.mutations.set(mutationHash, mutation); return mutation; } } clearMutationsCache() { this.mutations.clear(); } }; // src/useSuspenseQuery.ts import { useSyncExternalStore as useSyncExternalStore3, useRef as useRef3, useCallback as useCallback4 } from "react"; function useSuspenseQuery(options) { const client = useElhalaClient(); const observer = useRef3(null); if (!observer.current) { observer.current = createQueryObserver(client, options); } const state = useSyncExternalStore3( useCallback4( (onStoreChange) => observer.current.subscribe(onStoreChange), [] ), () => observer.current?.getResults(), () => observer.current?.getResults() ); if (state?.isFetching) { const promise = observer.current.fetchOptimistic(); if (promise) throw promise; throw new Promise(() => { }); } if (state?.status === "error") { throw state.error; } return state; } export { ElhalaClient, ElhalaProvider, useElhalaClient, useMutation, useQuery, useSuspenseQuery }; //# sourceMappingURL=index.mjs.map