1import { RegistryContext } from "@effect/atom-react";
2import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime";
3import { type AsyncResult, type Atom } from "effect/reactivity";
4import { use, useCallback, useEffect, useRef, useState } from "react";
5
6/** Only a user's pull owns the refresh indicator; query invalidation stays silent. */
7export function usePullToRefresh<A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>> | null) {
8 const registry = use(RegistryContext);
9 const request = useRef<{ atom: NonNullable<typeof atom>; controller: AbortController } | null>(
10 null,
11 );
12 const [refreshingAtom, setRefreshingAtom] = useState<typeof atom>(null);
13 if (refreshingAtom !== null && refreshingAtom !== atom) setRefreshingAtom(null);
14 useEffect(
15 () => () => {
16 if (request.current?.atom === atom) request.current.controller.abort();
17 },
18 [atom],
19 );
20
21 const onRefresh = useCallback(() => {
22 if (!atom || (request.current?.atom === atom && !request.current.controller.signal.aborted))
23 return;
24 request.current?.controller.abort();
25 const controller = new AbortController();
26 request.current = { atom, controller };
27 setRefreshingAtom(atom);
28 void executeAtomQuery(registry, atom, {
29 refresh: true,
30 signal: controller.signal,
31 reportFailure: false,
32 reportDefect: false,
33 }).finally(() => {
34 if (controller.signal.aborted || request.current?.controller !== controller) return;
35 request.current = null;
36 setRefreshingAtom(null);
37 });
38 }, [atom, registry]);
39
40 return { refreshing: atom !== null && refreshingAtom === atom, onRefresh };
41}