handy online tools for AT Protocol
boat.kelinci.net
atproto
bluesky
atcute
typescript
solidjs
1import { createRenderEffect, createSignal } from 'solid-js';
2
3import { makeAbortable } from './abortable';
4import { dequal } from './dequal';
5
6export interface SuccessQueryReturn<R> {
7 data: R;
8 error: undefined;
9 isSuccess: true;
10 isError: false;
11 isPending: false;
12 isIdle: false;
13 refetch(): void;
14}
15export interface ErrorQueryReturn {
16 data: undefined;
17 error: unknown;
18 isSuccess: false;
19 isError: true;
20 isPending: false;
21 isIdle: false;
22 refetch(): void;
23}
24export interface PendingQueryReturn {
25 data: undefined;
26 error: undefined;
27 isSuccess: false;
28 isError: false;
29 isPending: true;
30 isIdle: false;
31 refetch(): void;
32}
33export interface IdleQueryReturn {
34 data: undefined;
35 error: undefined;
36 isSuccess: false;
37 isError: false;
38 isPending: false;
39 isIdle: true;
40 refetch(): void;
41}
42
43export type QueryReturn<R> = SuccessQueryReturn<R> | ErrorQueryReturn | PendingQueryReturn;
44
45const enum QueryState {
46 IDLE,
47 PENDING,
48 SUCCESS,
49 ERROR,
50}
51
52const UNSET_QUERY_KEY = Symbol();
53
54export const createQuery = <K, R>(
55 keyFn: () => K | null | undefined,
56 queryFn: (key: K, signal: AbortSignal) => Promise<R>,
57): QueryReturn<R> => {
58 let currKey: any = UNSET_QUERY_KEY;
59
60 const [getSignal, cleanup] = makeAbortable();
61 const [state, setState] = createSignal<{ s: QueryState; d?: any; e?: any }>(
62 { s: QueryState.IDLE },
63 { equals: (prev, next) => prev.s === next.s },
64 );
65
66 const refetch = (force: boolean) => {
67 const nextKey = keyFn();
68 const prevKey = currKey;
69 currKey = nextKey;
70
71 if (nextKey == null) {
72 cleanup();
73 setState({ s: QueryState.IDLE });
74 } else if (force || !dequal(nextKey, prevKey)) {
75 setState({ s: QueryState.PENDING });
76
77 const signal = getSignal();
78
79 new Promise((resolve) => resolve(queryFn(nextKey, signal))).then(
80 (data) => {
81 if (signal.aborted) {
82 return;
83 }
84
85 setState({ s: QueryState.SUCCESS, d: data });
86 },
87 (err) => {
88 if (signal.aborted) {
89 return;
90 }
91
92 setState({ s: QueryState.ERROR, e: err });
93 },
94 );
95 }
96 };
97
98 createRenderEffect(() => refetch(false));
99
100 return {
101 get data() {
102 const $state = state();
103 if ($state.s === QueryState.SUCCESS) {
104 return $state.d;
105 }
106 },
107 get error() {
108 const $state = state();
109 if ($state.s === QueryState.ERROR) {
110 return $state.e;
111 }
112 },
113 get isSuccess() {
114 return state().s === QueryState.SUCCESS;
115 },
116 get isError() {
117 return state().s === QueryState.ERROR;
118 },
119 get isPending() {
120 return state().s === QueryState.PENDING;
121 },
122 get isIdle() {
123 return state().s === QueryState.IDLE;
124 },
125 refetch() {
126 refetch(true);
127 },
128 } as any;
129};