@pixotope/query 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +20 -0
- package/.turbo/turbo-check-types.log +4 -0
- package/.turbo/turbo-test.log +15 -0
- package/dist/index.cjs +693 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +179 -0
- package/dist/index.d.ts +179 -0
- package/dist/index.js +674 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
- package/src/factories.ts +166 -0
- package/src/index.ts +3 -0
- package/src/mock/useQuery.ts +48 -0
- package/src/useMutation.ts +228 -0
- package/src/useQuery.test.ts +282 -0
- package/src/useQuery.ts +302 -0
- package/src/useSubscription.ts +87 -0
- package/src/useSubscriptionCore.test.ts +360 -0
- package/src/useSubscriptionCore.ts +223 -0
- package/src/vitest-setup.ts +1 -0
- package/tsconfig.json +27 -0
- package/vitest.config.mts +21 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { fireEvent } from "@testing-library/dom";
|
|
2
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import { vi, describe, beforeEach, it, expect, test } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { useQuery } from "./useQuery";
|
|
6
|
+
import { MockUseQueryUtils } from "./mock/useQuery";
|
|
7
|
+
const mockQueryKey = ["dummyQK"];
|
|
8
|
+
|
|
9
|
+
function until(condition: boolean) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
if (condition) {
|
|
12
|
+
resolve(null);
|
|
13
|
+
} else {
|
|
14
|
+
reject();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("useQuery", () => {
|
|
20
|
+
let defaultQueryMock: MockUseQueryUtils;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
defaultQueryMock = new MockUseQueryUtils({});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should call queryFn by on mount by default", () => {
|
|
27
|
+
const mockFunction = vi.fn(() => defaultQueryMock.getMockedQueryFn());
|
|
28
|
+
renderHook(() =>
|
|
29
|
+
useQuery({
|
|
30
|
+
queryFn: mockFunction,
|
|
31
|
+
key: mockQueryKey,
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(mockFunction).toBeCalledTimes(1);
|
|
36
|
+
});
|
|
37
|
+
it("should be in loading state on mount", () => {
|
|
38
|
+
const { result } = renderHook(() =>
|
|
39
|
+
useQuery({
|
|
40
|
+
queryFn: () => defaultQueryMock.getMockedQueryFn(),
|
|
41
|
+
key: mockQueryKey,
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(result.current.status).toBe("loading");
|
|
46
|
+
});
|
|
47
|
+
it("should be in success state after queryFn resolves", async () => {
|
|
48
|
+
const { result } = renderHook(() =>
|
|
49
|
+
useQuery({
|
|
50
|
+
queryFn: () => defaultQueryMock.getMockedQueryFn(),
|
|
51
|
+
key: mockQueryKey,
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
act(() => {
|
|
55
|
+
defaultQueryMock.resolve();
|
|
56
|
+
});
|
|
57
|
+
await waitFor(() => until(result.current.status === "success"), {
|
|
58
|
+
timeout: 1000,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
it("should be in error state after queryFn rejects", async () => {
|
|
62
|
+
const { result } = renderHook(() =>
|
|
63
|
+
useQuery({
|
|
64
|
+
queryFn: () => defaultQueryMock.getMockedQueryFn(),
|
|
65
|
+
key: mockQueryKey,
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
act(() => defaultQueryMock.reject());
|
|
69
|
+
await waitFor(() => until(result.current.status === "error"), {
|
|
70
|
+
timeout: 1000,
|
|
71
|
+
});
|
|
72
|
+
expect(result.current.status).toBe("error");
|
|
73
|
+
});
|
|
74
|
+
it("should be in timeout state after queryFn rejects with timeout", async () => {
|
|
75
|
+
const { result } = renderHook(() =>
|
|
76
|
+
useQuery({
|
|
77
|
+
queryFn: () => defaultQueryMock.getMockedQueryFn(),
|
|
78
|
+
key: mockQueryKey,
|
|
79
|
+
timeout: 0.01,
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
await waitFor(() => until(result.current.status === "error"), {
|
|
83
|
+
timeout: 1000,
|
|
84
|
+
});
|
|
85
|
+
expect(result.current.status === "error");
|
|
86
|
+
});
|
|
87
|
+
it("should be in success state after queryFn resolves with initialData", async () => {
|
|
88
|
+
const { result } = renderHook(() =>
|
|
89
|
+
useQuery({
|
|
90
|
+
queryFn: () => defaultQueryMock.getMockedQueryFn(),
|
|
91
|
+
key: mockQueryKey,
|
|
92
|
+
initialData: "test-test",
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
defaultQueryMock.resolve();
|
|
96
|
+
await waitFor(() => until(result.current.status === "success"), {
|
|
97
|
+
timeout: 1000,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
it("should be in idle state if enabled is false", async () => {
|
|
101
|
+
const { result } = renderHook(() =>
|
|
102
|
+
useQuery({
|
|
103
|
+
queryFn: () => defaultQueryMock.getMockedQueryFn(),
|
|
104
|
+
key: mockQueryKey,
|
|
105
|
+
enabled: false,
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
expect(result.current.status).toBe("idle");
|
|
109
|
+
});
|
|
110
|
+
test("refetch should trigger query function", async () => {
|
|
111
|
+
const resolver = vi.fn(() => defaultQueryMock.getMockedQueryFn());
|
|
112
|
+
const { result } = await act(() =>
|
|
113
|
+
renderHook(() =>
|
|
114
|
+
useQuery({
|
|
115
|
+
queryFn: resolver,
|
|
116
|
+
key: mockQueryKey,
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
await act(() => defaultQueryMock.resolve());
|
|
121
|
+
await waitFor(() => until(result.current.status === "success"), {
|
|
122
|
+
timeout: 1000,
|
|
123
|
+
});
|
|
124
|
+
await act(() => result.current.refetch());
|
|
125
|
+
expect(resolver).toBeCalledTimes(2);
|
|
126
|
+
});
|
|
127
|
+
test("select should return selected right data", async () => {
|
|
128
|
+
const queryMock = new MockUseQueryUtils({
|
|
129
|
+
foo: "bar",
|
|
130
|
+
baz: "qux",
|
|
131
|
+
});
|
|
132
|
+
const { result } = renderHook(() =>
|
|
133
|
+
useQuery({
|
|
134
|
+
queryFn: () => queryMock.getMockedQueryFn(),
|
|
135
|
+
key: mockQueryKey,
|
|
136
|
+
select: (data) => data?.foo,
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
expect(result.current.data).toBeUndefined();
|
|
140
|
+
queryMock.resolve();
|
|
141
|
+
await waitFor(() => until(result.current.data !== undefined), {
|
|
142
|
+
timeout: 1000,
|
|
143
|
+
});
|
|
144
|
+
expect(result.current.data).toBe("bar");
|
|
145
|
+
});
|
|
146
|
+
test("onSuccess should be called after queryFn resolves", async () => {
|
|
147
|
+
const onSuccess = vi.fn();
|
|
148
|
+
renderHook(() =>
|
|
149
|
+
useQuery({
|
|
150
|
+
queryFn: () => defaultQueryMock.getMockedQueryFn(),
|
|
151
|
+
key: mockQueryKey,
|
|
152
|
+
onSuccess,
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
await act(() => defaultQueryMock.resolve());
|
|
156
|
+
await waitFor(() => {}, {
|
|
157
|
+
timeout: 1000,
|
|
158
|
+
});
|
|
159
|
+
expect(onSuccess).toBeCalledTimes(1);
|
|
160
|
+
});
|
|
161
|
+
test("onError should be called after queryFn rejects", async () => {
|
|
162
|
+
const onError = vi.fn();
|
|
163
|
+
renderHook(() =>
|
|
164
|
+
useQuery({
|
|
165
|
+
queryFn: () => defaultQueryMock.getMockedQueryFn(),
|
|
166
|
+
key: mockQueryKey,
|
|
167
|
+
onError,
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
await act(() => defaultQueryMock.reject());
|
|
171
|
+
await waitFor(() => {}, {
|
|
172
|
+
timeout: 1000,
|
|
173
|
+
});
|
|
174
|
+
expect(onError).toBeCalledTimes(1);
|
|
175
|
+
});
|
|
176
|
+
test("onSettled should be called after queryFn resolves", async () => {
|
|
177
|
+
const onSettled = vi.fn();
|
|
178
|
+
renderHook(() =>
|
|
179
|
+
useQuery({
|
|
180
|
+
queryFn: () => defaultQueryMock.getMockedQueryFn(),
|
|
181
|
+
key: mockQueryKey,
|
|
182
|
+
onSettled,
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
await act(() => defaultQueryMock.resolve());
|
|
186
|
+
await waitFor(() => {}, {
|
|
187
|
+
timeout: 1000,
|
|
188
|
+
});
|
|
189
|
+
expect(onSettled).toBeCalledTimes(1);
|
|
190
|
+
});
|
|
191
|
+
test("onSettled should be called after queryFn rejects", async () => {
|
|
192
|
+
const onSettled = vi.fn();
|
|
193
|
+
renderHook(() =>
|
|
194
|
+
useQuery({
|
|
195
|
+
queryFn: () => defaultQueryMock.getMockedQueryFn(),
|
|
196
|
+
key: mockQueryKey,
|
|
197
|
+
onSettled,
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
await act(() => defaultQueryMock.reject());
|
|
201
|
+
await waitFor(() => {}, {
|
|
202
|
+
timeout: 1000,
|
|
203
|
+
});
|
|
204
|
+
expect(onSettled).toBeCalledTimes(1);
|
|
205
|
+
});
|
|
206
|
+
test("refetch is triggered on window focus", async () => {
|
|
207
|
+
const resolver = vi.fn(() => defaultQueryMock.getMockedQueryFn());
|
|
208
|
+
const { result } = await act(() =>
|
|
209
|
+
renderHook(() =>
|
|
210
|
+
useQuery({
|
|
211
|
+
queryFn: resolver,
|
|
212
|
+
refetchOnWindowFocus: true,
|
|
213
|
+
key: mockQueryKey,
|
|
214
|
+
})
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
await act(() => defaultQueryMock.resolve());
|
|
218
|
+
await waitFor(() => until(result.current.status === "success"), {
|
|
219
|
+
timeout: 1000,
|
|
220
|
+
});
|
|
221
|
+
await act(() => fireEvent(window, new FocusEvent("focus")));
|
|
222
|
+
expect(resolver).toBeCalledTimes(2);
|
|
223
|
+
});
|
|
224
|
+
test("refetch interval should work", async () => {
|
|
225
|
+
const resolver = vi.fn(() => defaultQueryMock.getMockedQueryFn());
|
|
226
|
+
const { result } = renderHook(() =>
|
|
227
|
+
useQuery({
|
|
228
|
+
queryFn: resolver,
|
|
229
|
+
refetchInterval: 10,
|
|
230
|
+
key: mockQueryKey,
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
await act(() => defaultQueryMock.resolve());
|
|
234
|
+
await waitFor(() => until(result.current.status === "success"), {
|
|
235
|
+
timeout: 1000,
|
|
236
|
+
});
|
|
237
|
+
waitFor(() => until(resolver.call.length > 1), { timeout: 1000 });
|
|
238
|
+
});
|
|
239
|
+
test("refetch should automatically trigger if queryKey is changed", async () => {
|
|
240
|
+
const resolver = vi.fn(() => defaultQueryMock.getMockedQueryFn());
|
|
241
|
+
const { result, rerender } = await act(() =>
|
|
242
|
+
renderHook(
|
|
243
|
+
(props) =>
|
|
244
|
+
useQuery({
|
|
245
|
+
queryFn: props.resolver,
|
|
246
|
+
key: props.key,
|
|
247
|
+
}),
|
|
248
|
+
{
|
|
249
|
+
initialProps: {
|
|
250
|
+
key: ["dummyQK1"],
|
|
251
|
+
resolver,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
await act(() => defaultQueryMock.resolve());
|
|
257
|
+
await waitFor(() => until(result.current.status === "success"), {
|
|
258
|
+
timeout: 1000,
|
|
259
|
+
});
|
|
260
|
+
rerender({ key: ["dummyQK2"], resolver });
|
|
261
|
+
rerender({ key: ["dummyQK2"], resolver });
|
|
262
|
+
expect(resolver).toBeCalledTimes(2);
|
|
263
|
+
});
|
|
264
|
+
test("stateless mode", async () => {
|
|
265
|
+
const resolver = vi.fn(() => defaultQueryMock.getMockedQueryFn());
|
|
266
|
+
const { result } = renderHook(() =>
|
|
267
|
+
useQuery({
|
|
268
|
+
queryFn: resolver,
|
|
269
|
+
key: mockQueryKey,
|
|
270
|
+
stateless: true,
|
|
271
|
+
})
|
|
272
|
+
);
|
|
273
|
+
await act(() => defaultQueryMock.resolve());
|
|
274
|
+
await waitFor(() => until(result.current.status === "success"), {
|
|
275
|
+
timeout: 1000,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Here ts should kick in and complain about but for testing purposes, all good
|
|
279
|
+
// @ts-ignore
|
|
280
|
+
expect(result.current.data).toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
});
|
package/src/useQuery.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface QueryOptions<
|
|
4
|
+
TData = unknown,
|
|
5
|
+
TError = unknown,
|
|
6
|
+
TParams = void,
|
|
7
|
+
TSelectData = TData,
|
|
8
|
+
Stateless extends true | undefined = undefined,
|
|
9
|
+
> {
|
|
10
|
+
queryFn: (params: TParams | undefined | void) => Promise<TData>;
|
|
11
|
+
onSuccess?: (data: TData) => Promise<void> | void;
|
|
12
|
+
onError?: (error: TError) => Promise<void> | void;
|
|
13
|
+
onSettled?: (
|
|
14
|
+
data: TData | undefined,
|
|
15
|
+
error: TError | undefined
|
|
16
|
+
) => Promise<void> | void;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
initialData?: TData | Functionize<TData>;
|
|
20
|
+
select?: (data: TData) => TSelectData;
|
|
21
|
+
refetchOnWindowFocus?: boolean;
|
|
22
|
+
refetchInterval?: number;
|
|
23
|
+
compareOrThrow?: (data: TData) => boolean;
|
|
24
|
+
stateless?: Stateless;
|
|
25
|
+
key?: TQueryKey;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Functionize<T> = () => T;
|
|
29
|
+
type QueryStatus = "idle" | "loading" | "success" | "error" | "timeout";
|
|
30
|
+
type RaceErrorReason = "timeout";
|
|
31
|
+
type RacePromises<T> = Promise<T | RaceErrorReason>;
|
|
32
|
+
type WithData<TData> = { data: TData | undefined };
|
|
33
|
+
type TQueryKey = readonly any[];
|
|
34
|
+
|
|
35
|
+
export type UseQueryResult<
|
|
36
|
+
TData = unknown,
|
|
37
|
+
TError = unknown,
|
|
38
|
+
TParams = void,
|
|
39
|
+
TSelectData = TData,
|
|
40
|
+
Stateless extends true | undefined = undefined,
|
|
41
|
+
> = {
|
|
42
|
+
error: TError | null;
|
|
43
|
+
status: QueryStatus;
|
|
44
|
+
refetch: (params: TParams | void | undefined) => Promise<void>;
|
|
45
|
+
reset: () => void;
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
47
|
+
} & (Stateless extends true ? {} : WithData<TSelectData>);
|
|
48
|
+
|
|
49
|
+
function normalizeQueryKey(queryKey: TQueryKey): string {
|
|
50
|
+
return JSON.stringify(queryKey);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type TInternalCallbackRef<TData, TError, TSelectData> = {
|
|
54
|
+
onError?: (error: TError) => Promise<void> | void;
|
|
55
|
+
onSuccess?: (data: TData) => Promise<void> | void;
|
|
56
|
+
onSettled?: (
|
|
57
|
+
data: TData | undefined,
|
|
58
|
+
error: TError | undefined
|
|
59
|
+
) => Promise<void> | void;
|
|
60
|
+
compareOrThrow?: (data: TData) => boolean;
|
|
61
|
+
select?: (data: TData) => TSelectData;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type TInternalOptionsRef<TData> = {
|
|
65
|
+
timeout?: number;
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
refetchOnWindowFocus: boolean;
|
|
68
|
+
refetchInterval: number;
|
|
69
|
+
compareOrThrow?: (data: TData) => boolean;
|
|
70
|
+
stateless?: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const useQuery = <
|
|
74
|
+
TData = unknown,
|
|
75
|
+
TError = unknown,
|
|
76
|
+
TParams = void,
|
|
77
|
+
TSelected = TData,
|
|
78
|
+
Stateless extends true | undefined = undefined,
|
|
79
|
+
>({
|
|
80
|
+
queryFn,
|
|
81
|
+
...options
|
|
82
|
+
}: QueryOptions<TData, TError, TParams, TSelected, Stateless>): UseQueryResult<
|
|
83
|
+
TSelected,
|
|
84
|
+
TError,
|
|
85
|
+
TParams,
|
|
86
|
+
TSelected,
|
|
87
|
+
Stateless
|
|
88
|
+
> => {
|
|
89
|
+
const {
|
|
90
|
+
enabled = true,
|
|
91
|
+
refetchOnWindowFocus = false,
|
|
92
|
+
refetchInterval = 0,
|
|
93
|
+
initialData,
|
|
94
|
+
select,
|
|
95
|
+
onError,
|
|
96
|
+
onSuccess,
|
|
97
|
+
onSettled,
|
|
98
|
+
timeout,
|
|
99
|
+
compareOrThrow,
|
|
100
|
+
stateless,
|
|
101
|
+
key: queryKeyPassed,
|
|
102
|
+
} = options;
|
|
103
|
+
const queryKey = normalizeQueryKey(queryKeyPassed ?? []);
|
|
104
|
+
const promiseResolutionInProgress = useRef(false);
|
|
105
|
+
const lastFetchedAt = useRef<number | null>(null);
|
|
106
|
+
const lastQueryKey = useRef<string | undefined>(undefined);
|
|
107
|
+
const queryFnRef = useRef(queryFn);
|
|
108
|
+
queryFnRef.current = queryFn;
|
|
109
|
+
const internalCallbackRef = useRef<
|
|
110
|
+
TInternalCallbackRef<TData, TError, TSelected>
|
|
111
|
+
>({});
|
|
112
|
+
internalCallbackRef.current = {
|
|
113
|
+
onError,
|
|
114
|
+
onSuccess,
|
|
115
|
+
onSettled,
|
|
116
|
+
compareOrThrow,
|
|
117
|
+
select,
|
|
118
|
+
};
|
|
119
|
+
const internalOptionsRef = useRef<TInternalOptionsRef<TData>>({
|
|
120
|
+
enabled,
|
|
121
|
+
refetchOnWindowFocus,
|
|
122
|
+
refetchInterval,
|
|
123
|
+
compareOrThrow,
|
|
124
|
+
stateless,
|
|
125
|
+
timeout,
|
|
126
|
+
});
|
|
127
|
+
internalOptionsRef.current = {
|
|
128
|
+
enabled,
|
|
129
|
+
refetchOnWindowFocus,
|
|
130
|
+
refetchInterval,
|
|
131
|
+
compareOrThrow,
|
|
132
|
+
stateless,
|
|
133
|
+
timeout,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const [status, setStatus] = useState<QueryStatus>(() => {
|
|
137
|
+
return "idle";
|
|
138
|
+
});
|
|
139
|
+
const [error, setError] = useState<TError | null>(null);
|
|
140
|
+
const [data, setData] = useState<TData | undefined>(() => {
|
|
141
|
+
if (initialData !== undefined) {
|
|
142
|
+
if (typeof initialData === "function") {
|
|
143
|
+
return (initialData as Functionize<TData>)();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return initialData as TData;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return undefined;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const handleSetData = useCallback((data: TData) => {
|
|
153
|
+
if (internalOptionsRef.current.stateless) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setData(data);
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
const shouldRefetch = useCallback(() => {
|
|
161
|
+
if (!internalOptionsRef.current.enabled) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (lastFetchedAt.current === null) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
new Date().getTime() - lastFetchedAt.current >
|
|
171
|
+
internalOptionsRef.current?.refetchInterval
|
|
172
|
+
);
|
|
173
|
+
}, []);
|
|
174
|
+
|
|
175
|
+
const refetch = useCallback(
|
|
176
|
+
async (params: TParams | void | undefined = undefined) => {
|
|
177
|
+
if (
|
|
178
|
+
promiseResolutionInProgress.current ||
|
|
179
|
+
!internalOptionsRef.current.enabled
|
|
180
|
+
) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
setStatus("loading");
|
|
185
|
+
setError(null);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const promises: RacePromises<TData>[] = [
|
|
189
|
+
queryFnRef.current(params),
|
|
190
|
+
...(internalOptionsRef.current.timeout
|
|
191
|
+
? [
|
|
192
|
+
new Promise<RaceErrorReason>((resolve) =>
|
|
193
|
+
setTimeout(
|
|
194
|
+
() => resolve("timeout"),
|
|
195
|
+
internalOptionsRef.current.timeout
|
|
196
|
+
)
|
|
197
|
+
),
|
|
198
|
+
]
|
|
199
|
+
: []),
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
promiseResolutionInProgress.current = true;
|
|
203
|
+
const res = await Promise.race(promises);
|
|
204
|
+
promiseResolutionInProgress.current = false;
|
|
205
|
+
|
|
206
|
+
if (res === "timeout") {
|
|
207
|
+
throw new Error("timeout");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
handleSetData(res);
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
internalCallbackRef.current.compareOrThrow &&
|
|
214
|
+
internalCallbackRef.current.compareOrThrow(res) === true
|
|
215
|
+
) {
|
|
216
|
+
throw new Error("compareOrThrow");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
setStatus("success");
|
|
220
|
+
lastFetchedAt.current = new Date().getTime();
|
|
221
|
+
internalCallbackRef.current.onSuccess?.(res);
|
|
222
|
+
internalCallbackRef.current.onSettled?.(res, undefined);
|
|
223
|
+
} catch (error: any) {
|
|
224
|
+
setError(error);
|
|
225
|
+
setStatus("error");
|
|
226
|
+
internalCallbackRef.current.onError?.(error);
|
|
227
|
+
internalCallbackRef.current.onSettled?.(undefined, error);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
[handleSetData]
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const reset = useCallback(() => {
|
|
234
|
+
setData(undefined);
|
|
235
|
+
setStatus("idle");
|
|
236
|
+
setError(null);
|
|
237
|
+
refetch();
|
|
238
|
+
}, [refetch]);
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (enabled) {
|
|
242
|
+
refetch();
|
|
243
|
+
}
|
|
244
|
+
}, [enabled, refetch, queryKey]);
|
|
245
|
+
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (lastQueryKey.current === undefined) {
|
|
248
|
+
lastQueryKey.current = queryKey;
|
|
249
|
+
refetch();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (queryKey !== lastQueryKey.current) {
|
|
254
|
+
lastQueryKey.current = queryKey;
|
|
255
|
+
refetch();
|
|
256
|
+
}
|
|
257
|
+
}, [queryKey, refetch, enabled]);
|
|
258
|
+
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (refetchOnWindowFocus) {
|
|
261
|
+
const queryHandleFocus = () => {
|
|
262
|
+
if (shouldRefetch()) {
|
|
263
|
+
refetch();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
window.addEventListener("focus", queryHandleFocus);
|
|
268
|
+
|
|
269
|
+
return () => {
|
|
270
|
+
window.removeEventListener("focus", queryHandleFocus);
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}, [refetchOnWindowFocus, refetch, shouldRefetch]);
|
|
274
|
+
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
if (refetchInterval !== 0) {
|
|
277
|
+
const interval = setInterval(
|
|
278
|
+
() => {
|
|
279
|
+
if (shouldRefetch()) {
|
|
280
|
+
refetch();
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
Math.max(refetchInterval, 1000)
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
return () => {
|
|
287
|
+
clearInterval(interval);
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}, [refetch, refetchInterval, shouldRefetch]);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
data:
|
|
294
|
+
select && data !== undefined
|
|
295
|
+
? select(data as TData)
|
|
296
|
+
: (data as unknown as TSelected),
|
|
297
|
+
error,
|
|
298
|
+
status,
|
|
299
|
+
refetch,
|
|
300
|
+
reset,
|
|
301
|
+
};
|
|
302
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { DHClientWebSocketProxy } from "@pixotope/zmq-client";
|
|
2
|
+
import {
|
|
3
|
+
SubscriptionCoreOptions,
|
|
4
|
+
useSubscriptionCore,
|
|
5
|
+
} from "./useSubscriptionCore";
|
|
6
|
+
|
|
7
|
+
export type SubscriptionOptions<
|
|
8
|
+
TData,
|
|
9
|
+
TMessage = unknown,
|
|
10
|
+
Stateless extends true | undefined = undefined,
|
|
11
|
+
> = Omit<
|
|
12
|
+
SubscriptionCoreOptions<TData, TMessage, Stateless>,
|
|
13
|
+
"subscriberFn"
|
|
14
|
+
> & {
|
|
15
|
+
service: string;
|
|
16
|
+
topic: string;
|
|
17
|
+
maxDownwardDepth?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type TSubscriptionHandlerFunction<TMessage> = (opts: {
|
|
21
|
+
service: string;
|
|
22
|
+
topic: string;
|
|
23
|
+
callback: (message: TMessage) => void;
|
|
24
|
+
maxDownwardDepth?: number;
|
|
25
|
+
}) => () => void;
|
|
26
|
+
|
|
27
|
+
export function useSubscription<
|
|
28
|
+
TData = unknown,
|
|
29
|
+
TMessage = TData,
|
|
30
|
+
Stateless extends true | undefined = undefined,
|
|
31
|
+
>(
|
|
32
|
+
subscriptionHandler: TSubscriptionHandlerFunction<TMessage>,
|
|
33
|
+
options: SubscriptionOptions<TData, TMessage, Stateless>
|
|
34
|
+
) {
|
|
35
|
+
const { topic, service, maxDownwardDepth = 1000 } = options;
|
|
36
|
+
|
|
37
|
+
return useSubscriptionCore<TData, TMessage, Stateless>({
|
|
38
|
+
...options,
|
|
39
|
+
subscriberFn: (handler) => {
|
|
40
|
+
const unsubscribe = subscriptionHandler({
|
|
41
|
+
service,
|
|
42
|
+
topic,
|
|
43
|
+
callback: (message: TMessage) => {
|
|
44
|
+
handler(message);
|
|
45
|
+
},
|
|
46
|
+
maxDownwardDepth,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return unsubscribe;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildServiceSubscriptionHook(
|
|
55
|
+
subscriptionHandler: TSubscriptionHandlerFunction<unknown>,
|
|
56
|
+
targetMachine: (machine: string | undefined) => string
|
|
57
|
+
) {
|
|
58
|
+
return function useSubscriptionBuilder<
|
|
59
|
+
TData,
|
|
60
|
+
TMessage = TData,
|
|
61
|
+
Stateless extends true | undefined = undefined,
|
|
62
|
+
>(
|
|
63
|
+
options: Omit<
|
|
64
|
+
SubscriptionOptions<TData, TMessage, Stateless>,
|
|
65
|
+
"service"
|
|
66
|
+
> & {
|
|
67
|
+
machine?: string;
|
|
68
|
+
}
|
|
69
|
+
) {
|
|
70
|
+
const targetService = targetMachine(options.machine);
|
|
71
|
+
return useSubscription<TData, TMessage, Stateless>(
|
|
72
|
+
<TSubscriptionHandlerFunction<TMessage>>subscriptionHandler,
|
|
73
|
+
{
|
|
74
|
+
service: targetService,
|
|
75
|
+
...options,
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function bindClientSocketProxy(
|
|
82
|
+
socketClientProxy: DHClientWebSocketProxy
|
|
83
|
+
) {
|
|
84
|
+
return (
|
|
85
|
+
...args: Parameters<Parameters<typeof buildServiceSubscriptionHook>[0]>
|
|
86
|
+
) => socketClientProxy.mirror(...args)[0];
|
|
87
|
+
}
|