Skip to content
Source
Sign inSign up

query.ts

98 lines3.5 KB

1import { RegistryContext, useAtomRefresh, useAtomValue } from "@effect/atom-react";
2import * as Cause from "effect/Cause";
3import * as Option from "effect/Option";
4import { AsyncResult, Atom } from "effect/reactivity";
5import { useContext } from "react";
6​
7const EMPTY_ASYNC_RESULT_ATOM = Atom.make(AsyncResult.initial<never, never>(false)).pipe(
8 Atom.withLabel("web-environment-query:empty"),
9);
10​
11export interface EnvironmentQueryView<A, E = unknown> {
12 readonly data: A | null;
13 readonly dataUpdatedAt: number;
14 readonly error: string | null;
15 readonly failure: E | null;
16 readonly isPending: boolean;
17 readonly isSuccess: boolean;
18 readonly refresh: () => void;
20​
21export function formatEnvironmentQueryError(cause: Cause.Cause<unknown>): string {
22 const error = Cause.squash(cause);
23 return error instanceof Error && error.message.trim().length > 0
24 ? error.message
25 : "The environment request failed.";
27​
28function queryUpdatedAt(result: AsyncResult.AsyncResult<unknown, unknown>): number {
29 return result._tag === "Success"
30 ? result.timestamp
31 : result._tag === "Failure"
32 ? (Option.getOrNull(result.previousSuccess)?.timestamp ?? 0)
33 : 0;
35​
36export function useEnvironmentQuery<A, E>(
37 atom: Atom.Atom<AsyncResult.AsyncResult<A, E>> | null,
38): EnvironmentQueryView<A, E> {
39 const selectedAtom = atom ?? EMPTY_ASYNC_RESULT_ATOM;
40 const result = useAtomValue(selectedAtom);
41 const refresh = useAtomRefresh(selectedAtom);
42 return {
43 data: AsyncResult.getOrElse(result, () => null),
44 dataUpdatedAt: queryUpdatedAt(result),
45 error: result._tag === "Failure" ? formatEnvironmentQueryError(result.cause) : null,
46 failure:
47 result._tag === "Failure" ? Option.getOrNull(Cause.findErrorOption(result.cause)) : null,
48 isPending: atom !== null && result.waiting,
49 isSuccess: result._tag === "Success",
50 refresh,
51 };
53​
54/** Keep select outside the component; a new function rebuilds the derived atom on every render. */
55export function useEnvironmentQuerySelector<A, E, B>(
56 atom: Atom.Atom<AsyncResult.AsyncResult<A, E>> | null,
57 select: (result: AsyncResult.AsyncResult<A, E>) => B,
58): B {
59 return useAtomValue(atom ?? EMPTY_ASYNC_RESULT_ATOM, select);
61​
62type QueryResultAtom = Atom.Atom<AsyncResult.AsyncResult<unknown, unknown>>;
63const queryIsNewerAtoms = new WeakMap<
64 QueryResultAtom,
65 WeakMap<QueryResultAtom, Atom.Atom<boolean>>
66>();
67function queryIsNewerAtom(query: QueryResultAtom, than: QueryResultAtom): Atom.Atom<boolean> {
68 let byThan = queryIsNewerAtoms.get(query);
69 if (byThan === undefined) {
70 byThan = new WeakMap();
71 queryIsNewerAtoms.set(query, byThan);
72 }
73 let atom = byThan.get(than);
74 if (atom === undefined) {
75 atom = Atom.make((get) => queryUpdatedAt(get(query)) >= queryUpdatedAt(get(than)));
76 byThan.set(than, atom);
77 }
78 return atom;
80​
81export function useEnvironmentQueryIsNewer(
82 query: QueryResultAtom | null,
83 than: QueryResultAtom | null,
84): boolean {
85 return useAtomValue(
86 queryIsNewerAtom(query ?? EMPTY_ASYNC_RESULT_ATOM, than ?? EMPTY_ASYNC_RESULT_ATOM),
87 );
89​
90/** Returns refresh and call-time pending state without subscribing to it. */
91export function useEnvironmentQueryRefresh<A, E>(
92 atom: Atom.Atom<AsyncResult.AsyncResult<A, E>> | null,
93): { readonly refresh: () => void; readonly isPending: () => boolean } {
94 const registry = useContext(RegistryContext);
95 const selectedAtom = atom ?? EMPTY_ASYNC_RESULT_ATOM;
96 const refresh = useAtomRefresh(selectedAtom);
97 return { refresh, isPending: () => atom !== null && registry.get(selectedAtom).waiting };