@structyl/api-client 1.0.0
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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/devtools.cjs +181 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.cts +8 -0
- package/dist/devtools.d.ts +8 -0
- package/dist/devtools.mjs +179 -0
- package/dist/devtools.mjs.map +1 -0
- package/dist/index.cjs +905 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.mjs +887 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server-BdrIj1E3.d.cts +208 -0
- package/dist/server-BdrIj1E3.d.ts +208 -0
- package/dist/server.cjs +230 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.mjs +225 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +72 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createContext, useContext, useRef, useCallback, useSyncExternalStore, useEffect, useState } from 'react';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
var ApiClient = class {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.instance = axios.create({
|
|
10
|
+
baseURL: config.baseURL,
|
|
11
|
+
headers: config.headers,
|
|
12
|
+
timeout: config.timeout ?? 1e4
|
|
13
|
+
});
|
|
14
|
+
this.setupInterceptors();
|
|
15
|
+
}
|
|
16
|
+
setupInterceptors() {
|
|
17
|
+
this.instance.interceptors.request.use(async (requestConfig) => {
|
|
18
|
+
if (this.config.getAuthToken) {
|
|
19
|
+
const token = await this.config.getAuthToken();
|
|
20
|
+
if (token) {
|
|
21
|
+
requestConfig.headers.Authorization = `Bearer ${token}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return requestConfig;
|
|
25
|
+
});
|
|
26
|
+
this.instance.interceptors.response.use(
|
|
27
|
+
(response) => response,
|
|
28
|
+
async (error) => {
|
|
29
|
+
const retriableConfig = error.config;
|
|
30
|
+
if (error.response?.status === 401 && this.config.refreshToken && retriableConfig && !retriableConfig._retried) {
|
|
31
|
+
retriableConfig._retried = true;
|
|
32
|
+
try {
|
|
33
|
+
const newToken = await this.config.refreshToken();
|
|
34
|
+
retriableConfig.headers["Authorization"] = `Bearer ${newToken}`;
|
|
35
|
+
return await this.instance.request(retriableConfig);
|
|
36
|
+
} catch (refreshErr) {
|
|
37
|
+
this.config.onRefreshError?.(refreshErr);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return Promise.reject(normalizeError(error));
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function normalizeError(error) {
|
|
46
|
+
if (error.response) {
|
|
47
|
+
const responseData = error.response.data;
|
|
48
|
+
return {
|
|
49
|
+
status: error.response.status,
|
|
50
|
+
message: (typeof responseData?.["message"] === "string" ? responseData["message"] : null) ?? error.message,
|
|
51
|
+
data: error.response.data
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { status: 0, message: "No response received from server" };
|
|
55
|
+
}
|
|
56
|
+
function createApiClient(config) {
|
|
57
|
+
return new ApiClient(config);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/cache.ts
|
|
61
|
+
function serializeKey(key) {
|
|
62
|
+
return JSON.stringify(key);
|
|
63
|
+
}
|
|
64
|
+
var QueryCache = class {
|
|
65
|
+
constructor(gcTime = 5 * 6e4) {
|
|
66
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
67
|
+
this.subscribers = /* @__PURE__ */ new Map();
|
|
68
|
+
this.inFlight = /* @__PURE__ */ new Map();
|
|
69
|
+
// Discards responses from requests that were superseded (e.g. after cancelQueries)
|
|
70
|
+
this.generations = /* @__PURE__ */ new Map();
|
|
71
|
+
// Evicts entries when no subscriber remains for longer than gcTime
|
|
72
|
+
this.gcTimers = /* @__PURE__ */ new Map();
|
|
73
|
+
// AbortControllers for in-flight requests
|
|
74
|
+
this.abortControllers = /* @__PURE__ */ new Map();
|
|
75
|
+
// Global subscribers (for persistence and devtools)
|
|
76
|
+
this.globalSubscribers = /* @__PURE__ */ new Set();
|
|
77
|
+
this.gcTime = gcTime;
|
|
78
|
+
}
|
|
79
|
+
get(key) {
|
|
80
|
+
return this.entries.get(key);
|
|
81
|
+
}
|
|
82
|
+
subscribe(key, fn) {
|
|
83
|
+
const pending = this.gcTimers.get(key);
|
|
84
|
+
if (pending !== void 0) {
|
|
85
|
+
clearTimeout(pending);
|
|
86
|
+
this.gcTimers.delete(key);
|
|
87
|
+
}
|
|
88
|
+
if (!this.subscribers.has(key)) this.subscribers.set(key, /* @__PURE__ */ new Set());
|
|
89
|
+
this.subscribers.get(key).add(fn);
|
|
90
|
+
return () => {
|
|
91
|
+
const set = this.subscribers.get(key);
|
|
92
|
+
if (!set) return;
|
|
93
|
+
set.delete(fn);
|
|
94
|
+
if (set.size === 0) {
|
|
95
|
+
this.subscribers.delete(key);
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
this.entries.delete(key);
|
|
98
|
+
this.generations.delete(key);
|
|
99
|
+
this.gcTimers.delete(key);
|
|
100
|
+
}, this.gcTime);
|
|
101
|
+
this.gcTimers.set(key, timer);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
subscribeGlobal(fn) {
|
|
106
|
+
this.globalSubscribers.add(fn);
|
|
107
|
+
return () => this.globalSubscribers.delete(fn);
|
|
108
|
+
}
|
|
109
|
+
notify(key) {
|
|
110
|
+
this.subscribers.get(key)?.forEach((fn) => fn());
|
|
111
|
+
this.globalSubscribers.forEach((fn) => fn());
|
|
112
|
+
}
|
|
113
|
+
setData(key, data, staleTime) {
|
|
114
|
+
this.entries.set(key, {
|
|
115
|
+
data,
|
|
116
|
+
error: void 0,
|
|
117
|
+
status: "success",
|
|
118
|
+
updatedAt: Date.now(),
|
|
119
|
+
staleTime
|
|
120
|
+
});
|
|
121
|
+
this.inFlight.delete(key);
|
|
122
|
+
this.notify(key);
|
|
123
|
+
}
|
|
124
|
+
setError(key, error) {
|
|
125
|
+
const existing = this.entries.get(key);
|
|
126
|
+
this.entries.set(key, {
|
|
127
|
+
data: existing?.data,
|
|
128
|
+
error,
|
|
129
|
+
status: "error",
|
|
130
|
+
updatedAt: Date.now(),
|
|
131
|
+
staleTime: 0
|
|
132
|
+
});
|
|
133
|
+
this.inFlight.delete(key);
|
|
134
|
+
this.notify(key);
|
|
135
|
+
}
|
|
136
|
+
setLoading(key) {
|
|
137
|
+
const existing = this.entries.get(key);
|
|
138
|
+
if (existing?.status === "loading") return;
|
|
139
|
+
this.entries.set(key, {
|
|
140
|
+
data: existing?.data,
|
|
141
|
+
error: void 0,
|
|
142
|
+
status: "loading",
|
|
143
|
+
updatedAt: existing?.updatedAt ?? 0,
|
|
144
|
+
staleTime: existing?.staleTime ?? 0
|
|
145
|
+
});
|
|
146
|
+
this.notify(key);
|
|
147
|
+
}
|
|
148
|
+
isStale(key) {
|
|
149
|
+
const e = this.entries.get(key);
|
|
150
|
+
if (!e || e.status === "idle" || e.status === "error") return true;
|
|
151
|
+
if (e.status === "loading") return false;
|
|
152
|
+
return e.updatedAt === 0 || Date.now() - e.updatedAt > e.staleTime;
|
|
153
|
+
}
|
|
154
|
+
// True only when markStale() was explicitly called on a successful entry.
|
|
155
|
+
// Used to detect *external* invalidation (e.g. after a mutation) vs time-based
|
|
156
|
+
// staleness, so the re-fetch effect doesn't loop when staleTime is 0.
|
|
157
|
+
isExternallyInvalidated(key) {
|
|
158
|
+
const e = this.entries.get(key);
|
|
159
|
+
return !!e && e.status === "success" && e.updatedAt === 0;
|
|
160
|
+
}
|
|
161
|
+
getInFlight(key) {
|
|
162
|
+
return this.inFlight.get(key);
|
|
163
|
+
}
|
|
164
|
+
setInFlight(key, p) {
|
|
165
|
+
this.inFlight.set(key, p);
|
|
166
|
+
}
|
|
167
|
+
clearInFlight(key) {
|
|
168
|
+
this.inFlight.delete(key);
|
|
169
|
+
}
|
|
170
|
+
// Returns the new generation. Does NOT touch inFlight — call clearInFlight separately.
|
|
171
|
+
bumpGeneration(key) {
|
|
172
|
+
const g = (this.generations.get(key) ?? 0) + 1;
|
|
173
|
+
this.generations.set(key, g);
|
|
174
|
+
return g;
|
|
175
|
+
}
|
|
176
|
+
getGeneration(key) {
|
|
177
|
+
return this.generations.get(key) ?? 0;
|
|
178
|
+
}
|
|
179
|
+
// Sets updatedAt = 0 as the sentinel for "externally invalidated"
|
|
180
|
+
markStale(key) {
|
|
181
|
+
const e = this.entries.get(key);
|
|
182
|
+
if (e) {
|
|
183
|
+
this.entries.set(key, { ...e, updatedAt: 0 });
|
|
184
|
+
this.notify(key);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// AbortController management
|
|
188
|
+
setAbortController(key, ctrl) {
|
|
189
|
+
this.abortControllers.set(key, ctrl);
|
|
190
|
+
}
|
|
191
|
+
getAbortController(key) {
|
|
192
|
+
return this.abortControllers.get(key);
|
|
193
|
+
}
|
|
194
|
+
clearAbortController(key) {
|
|
195
|
+
this.abortControllers.delete(key);
|
|
196
|
+
}
|
|
197
|
+
clear() {
|
|
198
|
+
for (const timer of this.gcTimers.values()) clearTimeout(timer);
|
|
199
|
+
this.gcTimers.clear();
|
|
200
|
+
for (const ctrl of this.abortControllers.values()) ctrl.abort();
|
|
201
|
+
this.abortControllers.clear();
|
|
202
|
+
this.entries.clear();
|
|
203
|
+
this.inFlight.clear();
|
|
204
|
+
this.generations.clear();
|
|
205
|
+
this.subscribers.clear();
|
|
206
|
+
}
|
|
207
|
+
snapshot() {
|
|
208
|
+
const result = {};
|
|
209
|
+
for (const [key, entry] of this.entries) {
|
|
210
|
+
if (entry.status === "success") result[key] = entry;
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
restore(state) {
|
|
215
|
+
for (const [key, entry] of Object.entries(state)) {
|
|
216
|
+
if (!this.entries.has(key)) this.entries.set(key, entry);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
var QueryClient = class {
|
|
221
|
+
constructor(config) {
|
|
222
|
+
this.config = config ?? {};
|
|
223
|
+
this.cache = new QueryCache(config?.gcTime);
|
|
224
|
+
}
|
|
225
|
+
getQueryData(key) {
|
|
226
|
+
return this.cache.get(serializeKey(key))?.data;
|
|
227
|
+
}
|
|
228
|
+
setQueryData(key, updater) {
|
|
229
|
+
const k = serializeKey(key);
|
|
230
|
+
const existing = this.cache.get(k);
|
|
231
|
+
const newData = typeof updater === "function" ? updater(existing?.data) : updater;
|
|
232
|
+
this.cache.setData(k, newData, existing?.staleTime ?? 6e4);
|
|
233
|
+
this.config.onSuccess?.(newData, k);
|
|
234
|
+
}
|
|
235
|
+
invalidateQueries(options) {
|
|
236
|
+
this.cache.markStale(serializeKey(options.queryKey));
|
|
237
|
+
}
|
|
238
|
+
async cancelQueries(options) {
|
|
239
|
+
const k = serializeKey(options.queryKey);
|
|
240
|
+
this.cache.getAbortController(k)?.abort();
|
|
241
|
+
this.cache.clearAbortController(k);
|
|
242
|
+
this.cache.bumpGeneration(k);
|
|
243
|
+
this.cache.clearInFlight(k);
|
|
244
|
+
this.cache.markStale(k);
|
|
245
|
+
}
|
|
246
|
+
async prefetchQuery(options) {
|
|
247
|
+
const k = serializeKey(options.queryKey);
|
|
248
|
+
if (!this.cache.isStale(k)) return;
|
|
249
|
+
try {
|
|
250
|
+
const data = await options.queryFn();
|
|
251
|
+
this.cache.setData(k, data, options.staleTime ?? 6e4);
|
|
252
|
+
this.config.onSuccess?.(data, k);
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Called internally after setData / setError to fire global callbacks
|
|
257
|
+
notifySuccess(data, key) {
|
|
258
|
+
this.config.onSuccess?.(data, key);
|
|
259
|
+
}
|
|
260
|
+
notifyError(error, key) {
|
|
261
|
+
this.config.onError?.(error, key);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
var ApiContext = createContext(null);
|
|
265
|
+
function useApiContext() {
|
|
266
|
+
const ctx = useContext(ApiContext);
|
|
267
|
+
if (!ctx) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
"[api-client] Hooks must be used within <ApiProvider>. Wrap your app root with <ApiProvider client={api}>."
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return ctx;
|
|
273
|
+
}
|
|
274
|
+
function useApiClient() {
|
|
275
|
+
return useApiContext().apiClient;
|
|
276
|
+
}
|
|
277
|
+
function makeQueryClient(config) {
|
|
278
|
+
return new QueryClient(config);
|
|
279
|
+
}
|
|
280
|
+
var browserQueryClient;
|
|
281
|
+
function getQueryClient(config) {
|
|
282
|
+
if (typeof window === "undefined") return makeQueryClient(config);
|
|
283
|
+
if (!browserQueryClient) browserQueryClient = makeQueryClient(config);
|
|
284
|
+
return browserQueryClient;
|
|
285
|
+
}
|
|
286
|
+
function ApiProvider({
|
|
287
|
+
client,
|
|
288
|
+
children,
|
|
289
|
+
queryClient,
|
|
290
|
+
gcTime,
|
|
291
|
+
hydratedState,
|
|
292
|
+
queryClientConfig
|
|
293
|
+
}) {
|
|
294
|
+
const qcRef = useRef(null);
|
|
295
|
+
if (!qcRef.current) {
|
|
296
|
+
const config = {
|
|
297
|
+
gcTime,
|
|
298
|
+
...queryClientConfig
|
|
299
|
+
};
|
|
300
|
+
qcRef.current = queryClient ?? getQueryClient(config);
|
|
301
|
+
if (hydratedState) {
|
|
302
|
+
qcRef.current.cache.restore(hydratedState.entries);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const ctxRef = useRef(null);
|
|
306
|
+
if (!ctxRef.current || ctxRef.current.apiClient !== client) {
|
|
307
|
+
ctxRef.current = { apiClient: client, queryClient: qcRef.current };
|
|
308
|
+
}
|
|
309
|
+
return /* @__PURE__ */ jsx(ApiContext.Provider, { value: ctxRef.current, children });
|
|
310
|
+
}
|
|
311
|
+
ApiProvider.displayName = "ApiProvider";
|
|
312
|
+
|
|
313
|
+
// src/utils.ts
|
|
314
|
+
async function withRetry(fn, retries) {
|
|
315
|
+
let lastErr;
|
|
316
|
+
for (let i = 0; i <= retries; i++) {
|
|
317
|
+
try {
|
|
318
|
+
return await fn();
|
|
319
|
+
} catch (err) {
|
|
320
|
+
lastErr = err;
|
|
321
|
+
if (i < retries) {
|
|
322
|
+
await new Promise(
|
|
323
|
+
(res) => setTimeout(res, Math.min(1e3 * 2 ** i, 3e4))
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
throw lastErr;
|
|
329
|
+
}
|
|
330
|
+
function toApiError(err) {
|
|
331
|
+
if (err != null && typeof err.status === "number" && typeof err.message === "string") {
|
|
332
|
+
return err;
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
status: -1,
|
|
336
|
+
message: err instanceof Error ? err.message : String(err)
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/hooks/useApiQuery.ts
|
|
341
|
+
function useApiQuery(keyOrUrl, urlOrFnOrOptions, maybeOptions) {
|
|
342
|
+
const { apiClient, queryClient } = useApiContext();
|
|
343
|
+
let serializedKey;
|
|
344
|
+
let options;
|
|
345
|
+
const fetcherRef = useRef(null);
|
|
346
|
+
if (typeof urlOrFnOrOptions === "string") {
|
|
347
|
+
const url = urlOrFnOrOptions;
|
|
348
|
+
serializedKey = serializeKey(
|
|
349
|
+
typeof keyOrUrl === "string" ? [keyOrUrl] : keyOrUrl
|
|
350
|
+
);
|
|
351
|
+
fetcherRef.current = (signal) => apiClient.instance.get(url, { signal }).then((r) => r.data);
|
|
352
|
+
options = maybeOptions;
|
|
353
|
+
} else if (typeof urlOrFnOrOptions === "function") {
|
|
354
|
+
const fn = urlOrFnOrOptions;
|
|
355
|
+
serializedKey = serializeKey(
|
|
356
|
+
typeof keyOrUrl === "string" ? [keyOrUrl] : keyOrUrl
|
|
357
|
+
);
|
|
358
|
+
fetcherRef.current = (_signal) => fn(apiClient.instance);
|
|
359
|
+
options = maybeOptions;
|
|
360
|
+
} else {
|
|
361
|
+
const url = keyOrUrl;
|
|
362
|
+
serializedKey = serializeKey([url]);
|
|
363
|
+
fetcherRef.current = (signal) => apiClient.instance.get(url, { signal }).then((r) => r.data);
|
|
364
|
+
options = urlOrFnOrOptions;
|
|
365
|
+
}
|
|
366
|
+
const retries = options?.retry === false ? 0 : options?.retry ?? 1;
|
|
367
|
+
const staleTime = options?.staleTime ?? 6e4;
|
|
368
|
+
if (options?.initialData !== void 0 && !queryClient.cache.get(serializedKey)) {
|
|
369
|
+
queryClient.cache.setData(serializedKey, options.initialData, staleTime);
|
|
370
|
+
}
|
|
371
|
+
const previousDataRef = useRef(void 0);
|
|
372
|
+
const debounceTimerRef = useRef(void 0);
|
|
373
|
+
const doFetch = useCallback(
|
|
374
|
+
async (force = false) => {
|
|
375
|
+
if (!force && !queryClient.cache.isStale(serializedKey)) return;
|
|
376
|
+
if (queryClient.cache.getInFlight(serializedKey)) return;
|
|
377
|
+
const ctrl = new AbortController();
|
|
378
|
+
queryClient.cache.setAbortController(serializedKey, ctrl);
|
|
379
|
+
queryClient.cache.setLoading(serializedKey);
|
|
380
|
+
const gen = queryClient.cache.bumpGeneration(serializedKey);
|
|
381
|
+
const signal = ctrl.signal;
|
|
382
|
+
const promise = withRetry(() => fetcherRef.current(signal), retries);
|
|
383
|
+
queryClient.cache.setInFlight(serializedKey, promise);
|
|
384
|
+
try {
|
|
385
|
+
const data2 = await promise;
|
|
386
|
+
if (queryClient.cache.getGeneration(serializedKey) !== gen) return;
|
|
387
|
+
queryClient.cache.setData(serializedKey, data2, staleTime);
|
|
388
|
+
queryClient.notifySuccess(data2, serializedKey);
|
|
389
|
+
queryClient.cache.clearAbortController(serializedKey);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
if (queryClient.cache.getGeneration(serializedKey) !== gen) return;
|
|
392
|
+
if (err instanceof Error && err.name === "CanceledError") return;
|
|
393
|
+
const apiError = toApiError(err);
|
|
394
|
+
queryClient.cache.setError(serializedKey, apiError);
|
|
395
|
+
queryClient.notifyError(apiError, serializedKey);
|
|
396
|
+
queryClient.cache.clearAbortController(serializedKey);
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
[serializedKey, staleTime, retries, queryClient]
|
|
400
|
+
);
|
|
401
|
+
const debouncedDoFetch = useCallback(
|
|
402
|
+
(force = false) => {
|
|
403
|
+
const debounceMs = options?.debounce;
|
|
404
|
+
if (debounceMs) {
|
|
405
|
+
if (debounceTimerRef.current !== void 0) {
|
|
406
|
+
clearTimeout(debounceTimerRef.current);
|
|
407
|
+
}
|
|
408
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
409
|
+
debounceTimerRef.current = void 0;
|
|
410
|
+
void doFetch(force);
|
|
411
|
+
}, debounceMs);
|
|
412
|
+
} else {
|
|
413
|
+
void doFetch(force);
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
[doFetch, options?.debounce]
|
|
417
|
+
);
|
|
418
|
+
const entry = useSyncExternalStore(
|
|
419
|
+
(notify) => queryClient.cache.subscribe(serializedKey, notify),
|
|
420
|
+
() => queryClient.cache.get(serializedKey),
|
|
421
|
+
() => queryClient.cache.get(serializedKey)
|
|
422
|
+
);
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
if (options?.enabled === false) return;
|
|
425
|
+
debouncedDoFetch();
|
|
426
|
+
return () => {
|
|
427
|
+
if (debounceTimerRef.current !== void 0) {
|
|
428
|
+
clearTimeout(debounceTimerRef.current);
|
|
429
|
+
debounceTimerRef.current = void 0;
|
|
430
|
+
}
|
|
431
|
+
queryClient.cache.getAbortController(serializedKey)?.abort();
|
|
432
|
+
queryClient.cache.clearAbortController(serializedKey);
|
|
433
|
+
};
|
|
434
|
+
}, [debouncedDoFetch, options?.enabled]);
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
if (options?.enabled === false) return;
|
|
437
|
+
if (queryClient.cache.isExternallyInvalidated(serializedKey)) debouncedDoFetch();
|
|
438
|
+
}, [entry, debouncedDoFetch, serializedKey, options?.enabled, queryClient]);
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (!options?.pollInterval) return;
|
|
441
|
+
const id = setInterval(() => debouncedDoFetch(true), options.pollInterval);
|
|
442
|
+
return () => clearInterval(id);
|
|
443
|
+
}, [debouncedDoFetch, options?.pollInterval]);
|
|
444
|
+
useEffect(() => {
|
|
445
|
+
const enabled = options?.refetchOnWindowFocus ?? true;
|
|
446
|
+
if (!enabled || typeof window === "undefined") return;
|
|
447
|
+
const handler = () => {
|
|
448
|
+
if (queryClient.cache.isStale(serializedKey)) debouncedDoFetch();
|
|
449
|
+
};
|
|
450
|
+
window.addEventListener("focus", handler);
|
|
451
|
+
return () => window.removeEventListener("focus", handler);
|
|
452
|
+
}, [debouncedDoFetch, serializedKey, options?.refetchOnWindowFocus, queryClient]);
|
|
453
|
+
const rawData = entry?.data;
|
|
454
|
+
let data = options?.select !== void 0 && rawData !== void 0 ? options.select(rawData) : rawData;
|
|
455
|
+
let isPlaceholderData = false;
|
|
456
|
+
if (data === void 0 && entry?.status === "loading" && options?.placeholderData !== void 0) {
|
|
457
|
+
data = options.placeholderData;
|
|
458
|
+
isPlaceholderData = true;
|
|
459
|
+
}
|
|
460
|
+
if (data !== void 0) {
|
|
461
|
+
previousDataRef.current = data;
|
|
462
|
+
} else if (options?.keepPreviousData && previousDataRef.current !== void 0) {
|
|
463
|
+
data = previousDataRef.current;
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
data,
|
|
467
|
+
isLoading: entry?.status === "loading" && entry?.data === void 0,
|
|
468
|
+
isRefetching: entry?.status === "loading" && entry?.data !== void 0,
|
|
469
|
+
isFetching: entry?.status === "loading",
|
|
470
|
+
isPlaceholderData,
|
|
471
|
+
isSuccess: entry?.status === "success",
|
|
472
|
+
isError: entry?.status === "error",
|
|
473
|
+
error: entry?.error ?? null,
|
|
474
|
+
status: entry?.status ?? "idle",
|
|
475
|
+
refetch: () => void doFetch(true)
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function useApiMutation(urlOrFn, options) {
|
|
479
|
+
const { apiClient, queryClient } = useApiContext();
|
|
480
|
+
const optionsRef = useRef(options);
|
|
481
|
+
optionsRef.current = options;
|
|
482
|
+
const [state, setState] = useState({
|
|
483
|
+
data: void 0,
|
|
484
|
+
error: null,
|
|
485
|
+
status: "idle"
|
|
486
|
+
});
|
|
487
|
+
const mutateAsync = useCallback(
|
|
488
|
+
async (variables) => {
|
|
489
|
+
const { method = "POST", onSuccess, onError, invalidates, optimistic, onUploadProgress } = optionsRef.current ?? {};
|
|
490
|
+
setState({ data: void 0, error: null, status: "pending" });
|
|
491
|
+
let previousData;
|
|
492
|
+
if (optimistic) {
|
|
493
|
+
await queryClient.cancelQueries({ queryKey: optimistic.queryKey });
|
|
494
|
+
previousData = queryClient.getQueryData(optimistic.queryKey);
|
|
495
|
+
queryClient.setQueryData(
|
|
496
|
+
optimistic.queryKey,
|
|
497
|
+
(old) => optimistic.updater(old, variables)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
const data = typeof urlOrFn === "function" ? await urlOrFn(apiClient.instance, variables) : await apiClient.instance.request({
|
|
502
|
+
method,
|
|
503
|
+
url: urlOrFn,
|
|
504
|
+
data: variables,
|
|
505
|
+
onUploadProgress: onUploadProgress ? (evt) => {
|
|
506
|
+
const pct = evt.total ? Math.round(evt.loaded / evt.total * 100) : 0;
|
|
507
|
+
onUploadProgress(pct);
|
|
508
|
+
} : void 0
|
|
509
|
+
}).then((r) => r.data);
|
|
510
|
+
setState({ data, error: null, status: "success" });
|
|
511
|
+
if (invalidates?.length) {
|
|
512
|
+
await Promise.all(
|
|
513
|
+
invalidates.map((key) => queryClient.invalidateQueries({ queryKey: key }))
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
await onSuccess?.(data, variables);
|
|
517
|
+
return data;
|
|
518
|
+
} catch (err) {
|
|
519
|
+
if (optimistic) {
|
|
520
|
+
queryClient.setQueryData(optimistic.queryKey, previousData);
|
|
521
|
+
}
|
|
522
|
+
const apiError = toApiError(err);
|
|
523
|
+
setState({ data: void 0, error: apiError, status: "error" });
|
|
524
|
+
onError?.(apiError);
|
|
525
|
+
throw apiError;
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
[apiClient, queryClient, urlOrFn]
|
|
529
|
+
);
|
|
530
|
+
const mutate = useCallback(
|
|
531
|
+
(variables) => {
|
|
532
|
+
void mutateAsync(variables).catch(() => void 0);
|
|
533
|
+
},
|
|
534
|
+
[mutateAsync]
|
|
535
|
+
);
|
|
536
|
+
const reset = useCallback(() => {
|
|
537
|
+
setState({ data: void 0, error: null, status: "idle" });
|
|
538
|
+
}, []);
|
|
539
|
+
return {
|
|
540
|
+
mutate,
|
|
541
|
+
mutateAsync,
|
|
542
|
+
data: state.data,
|
|
543
|
+
isPending: state.status === "pending",
|
|
544
|
+
isSuccess: state.status === "success",
|
|
545
|
+
isError: state.status === "error",
|
|
546
|
+
error: state.error,
|
|
547
|
+
reset
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function useInfiniteApiQuery(url, options) {
|
|
551
|
+
const { apiClient, queryClient } = useApiContext();
|
|
552
|
+
const cacheKey = serializeKey(["__inf__", url]);
|
|
553
|
+
const retries = options.retry === false ? 0 : options.retry ?? 1;
|
|
554
|
+
const staleTime = options.staleTime ?? 6e4;
|
|
555
|
+
const optionsRef = useRef(options);
|
|
556
|
+
optionsRef.current = options;
|
|
557
|
+
const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
|
|
558
|
+
const [isFetchingPreviousPage, setIsFetchingPreviousPage] = useState(false);
|
|
559
|
+
const entry = useSyncExternalStore(
|
|
560
|
+
(notify) => queryClient.cache.subscribe(cacheKey, notify),
|
|
561
|
+
() => queryClient.cache.get(cacheKey),
|
|
562
|
+
() => queryClient.cache.get(cacheKey)
|
|
563
|
+
);
|
|
564
|
+
const buildUrl = (pageParam) => {
|
|
565
|
+
if (pageParam == null) return url;
|
|
566
|
+
return url + (url.includes("?") ? "&" : "?") + "cursor=" + String(pageParam);
|
|
567
|
+
};
|
|
568
|
+
const fetchOnePage = useCallback(
|
|
569
|
+
(pageParam) => {
|
|
570
|
+
const currentOptions = optionsRef.current;
|
|
571
|
+
if (currentOptions.fetchPage) {
|
|
572
|
+
return withRetry(() => currentOptions.fetchPage(pageParam, apiClient.instance), retries);
|
|
573
|
+
}
|
|
574
|
+
return withRetry(
|
|
575
|
+
() => apiClient.instance.get(buildUrl(pageParam)).then((r) => r.data),
|
|
576
|
+
retries
|
|
577
|
+
);
|
|
578
|
+
},
|
|
579
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
580
|
+
[apiClient, retries, url]
|
|
581
|
+
);
|
|
582
|
+
const doInitialFetch = useCallback(async () => {
|
|
583
|
+
if (queryClient.cache.getInFlight(cacheKey)) return;
|
|
584
|
+
queryClient.cache.setLoading(cacheKey);
|
|
585
|
+
const gen = queryClient.cache.bumpGeneration(cacheKey);
|
|
586
|
+
const promise = fetchOnePage(void 0);
|
|
587
|
+
queryClient.cache.setInFlight(cacheKey, promise);
|
|
588
|
+
try {
|
|
589
|
+
const firstPage = await promise;
|
|
590
|
+
if (queryClient.cache.getGeneration(cacheKey) !== gen) return;
|
|
591
|
+
const infiniteData2 = {
|
|
592
|
+
pages: [firstPage],
|
|
593
|
+
pageParams: [void 0]
|
|
594
|
+
};
|
|
595
|
+
queryClient.cache.setData(cacheKey, infiniteData2, staleTime);
|
|
596
|
+
} catch (err) {
|
|
597
|
+
if (queryClient.cache.getGeneration(cacheKey) !== gen) return;
|
|
598
|
+
const apiError = toApiError(err);
|
|
599
|
+
queryClient.cache.setError(cacheKey, apiError);
|
|
600
|
+
}
|
|
601
|
+
}, [cacheKey, fetchOnePage, staleTime, queryClient]);
|
|
602
|
+
useEffect(() => {
|
|
603
|
+
if (options.enabled === false) return;
|
|
604
|
+
if (!queryClient.cache.isStale(cacheKey)) return;
|
|
605
|
+
void doInitialFetch();
|
|
606
|
+
}, [doInitialFetch, cacheKey, options.enabled, queryClient]);
|
|
607
|
+
const fetchNextPage = useCallback(() => {
|
|
608
|
+
const currentData = queryClient.cache.get(cacheKey)?.data;
|
|
609
|
+
if (!currentData || currentData.pages.length === 0) return;
|
|
610
|
+
const lastPage = currentData.pages[currentData.pages.length - 1];
|
|
611
|
+
const nextPageParam = optionsRef.current.getNextPageParam(lastPage, currentData.pages);
|
|
612
|
+
if (nextPageParam === void 0) return;
|
|
613
|
+
setIsFetchingNextPage(true);
|
|
614
|
+
void fetchOnePage(nextPageParam).then((newPage) => {
|
|
615
|
+
const latestData = queryClient.cache.get(cacheKey)?.data;
|
|
616
|
+
if (!latestData) return;
|
|
617
|
+
const updated = {
|
|
618
|
+
pages: [...latestData.pages, newPage],
|
|
619
|
+
pageParams: [...latestData.pageParams, nextPageParam]
|
|
620
|
+
};
|
|
621
|
+
queryClient.cache.setData(cacheKey, updated, staleTime);
|
|
622
|
+
}).catch((err) => {
|
|
623
|
+
const apiError = toApiError(err);
|
|
624
|
+
queryClient.cache.setError(cacheKey, apiError);
|
|
625
|
+
}).finally(() => {
|
|
626
|
+
setIsFetchingNextPage(false);
|
|
627
|
+
});
|
|
628
|
+
}, [cacheKey, fetchOnePage, staleTime, queryClient]);
|
|
629
|
+
const fetchPreviousPage = useCallback(() => {
|
|
630
|
+
const currentData = queryClient.cache.get(cacheKey)?.data;
|
|
631
|
+
if (!currentData || currentData.pages.length === 0) return;
|
|
632
|
+
const getPreviousPageParam = optionsRef.current.getPreviousPageParam;
|
|
633
|
+
if (!getPreviousPageParam) return;
|
|
634
|
+
const firstPage = currentData.pages[0];
|
|
635
|
+
const prevPageParam = getPreviousPageParam(firstPage, currentData.pages);
|
|
636
|
+
if (prevPageParam === void 0) return;
|
|
637
|
+
setIsFetchingPreviousPage(true);
|
|
638
|
+
void fetchOnePage(prevPageParam).then((newPage) => {
|
|
639
|
+
const latestData = queryClient.cache.get(cacheKey)?.data;
|
|
640
|
+
if (!latestData) return;
|
|
641
|
+
const updated = {
|
|
642
|
+
pages: [newPage, ...latestData.pages],
|
|
643
|
+
pageParams: [prevPageParam, ...latestData.pageParams]
|
|
644
|
+
};
|
|
645
|
+
queryClient.cache.setData(cacheKey, updated, staleTime);
|
|
646
|
+
}).catch((err) => {
|
|
647
|
+
const apiError = toApiError(err);
|
|
648
|
+
queryClient.cache.setError(cacheKey, apiError);
|
|
649
|
+
}).finally(() => {
|
|
650
|
+
setIsFetchingPreviousPage(false);
|
|
651
|
+
});
|
|
652
|
+
}, [cacheKey, fetchOnePage, staleTime, queryClient]);
|
|
653
|
+
const refetch = useCallback(() => {
|
|
654
|
+
queryClient.cache.bumpGeneration(cacheKey);
|
|
655
|
+
queryClient.cache.clearInFlight(cacheKey);
|
|
656
|
+
void doInitialFetch();
|
|
657
|
+
}, [cacheKey, doInitialFetch, queryClient]);
|
|
658
|
+
const infiniteData = entry?.data;
|
|
659
|
+
let hasNextPage = false;
|
|
660
|
+
let hasPreviousPage = false;
|
|
661
|
+
if (infiniteData && infiniteData.pages.length > 0) {
|
|
662
|
+
const lastPage = infiniteData.pages[infiniteData.pages.length - 1];
|
|
663
|
+
hasNextPage = optionsRef.current.getNextPageParam(lastPage, infiniteData.pages) !== void 0;
|
|
664
|
+
if (optionsRef.current.getPreviousPageParam) {
|
|
665
|
+
const firstPage = infiniteData.pages[0];
|
|
666
|
+
hasPreviousPage = optionsRef.current.getPreviousPageParam(firstPage, infiniteData.pages) !== void 0;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
data: infiniteData,
|
|
671
|
+
isLoading: entry?.status === "loading" && entry?.data === void 0,
|
|
672
|
+
isFetching: entry?.status === "loading",
|
|
673
|
+
isFetchingNextPage,
|
|
674
|
+
isFetchingPreviousPage,
|
|
675
|
+
isSuccess: entry?.status === "success",
|
|
676
|
+
isError: entry?.status === "error",
|
|
677
|
+
error: entry?.error ?? null,
|
|
678
|
+
hasNextPage,
|
|
679
|
+
hasPreviousPage,
|
|
680
|
+
fetchNextPage,
|
|
681
|
+
fetchPreviousPage,
|
|
682
|
+
refetch
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function useApiQueries(queries) {
|
|
686
|
+
const { apiClient, queryClient } = useApiContext();
|
|
687
|
+
const keys = queries.map((q) => serializeKey(q.key ?? [q.url]));
|
|
688
|
+
const keysJoined = keys.join(",");
|
|
689
|
+
const prevSnapshotRef = useRef([]);
|
|
690
|
+
const previousDataRef = useRef([]);
|
|
691
|
+
const getSnapshot = useCallback(() => {
|
|
692
|
+
const next = keys.map((k) => queryClient.cache.get(k));
|
|
693
|
+
if (next.length === prevSnapshotRef.current.length && next.every((e, i) => e === prevSnapshotRef.current[i])) {
|
|
694
|
+
return prevSnapshotRef.current;
|
|
695
|
+
}
|
|
696
|
+
prevSnapshotRef.current = next;
|
|
697
|
+
return next;
|
|
698
|
+
}, [keysJoined, queryClient]);
|
|
699
|
+
const subscribe = useCallback(
|
|
700
|
+
(notify) => {
|
|
701
|
+
const unsubs = keys.map((k) => queryClient.cache.subscribe(k, notify));
|
|
702
|
+
return () => unsubs.forEach((u) => u());
|
|
703
|
+
},
|
|
704
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
705
|
+
[keysJoined, queryClient]
|
|
706
|
+
);
|
|
707
|
+
const entries = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
708
|
+
const doFetchOne = useCallback(
|
|
709
|
+
(query, key, force = false) => {
|
|
710
|
+
const opts = query.options;
|
|
711
|
+
if (!force && !queryClient.cache.isStale(key)) return;
|
|
712
|
+
if (queryClient.cache.getInFlight(key)) return;
|
|
713
|
+
const retries = opts?.retry === false ? 0 : opts?.retry ?? 1;
|
|
714
|
+
const st = opts?.staleTime ?? 6e4;
|
|
715
|
+
queryClient.cache.setLoading(key);
|
|
716
|
+
const gen = queryClient.cache.bumpGeneration(key);
|
|
717
|
+
const promise = withRetry(
|
|
718
|
+
() => apiClient.instance.get(query.url).then((r) => r.data),
|
|
719
|
+
retries
|
|
720
|
+
);
|
|
721
|
+
queryClient.cache.setInFlight(key, promise);
|
|
722
|
+
void promise.then((data) => {
|
|
723
|
+
if (queryClient.cache.getGeneration(key) !== gen) return;
|
|
724
|
+
queryClient.cache.setData(key, data, st);
|
|
725
|
+
}).catch((err) => {
|
|
726
|
+
if (queryClient.cache.getGeneration(key) !== gen) return;
|
|
727
|
+
const apiError = toApiError(err);
|
|
728
|
+
queryClient.cache.setError(key, apiError);
|
|
729
|
+
});
|
|
730
|
+
},
|
|
731
|
+
[apiClient, queryClient]
|
|
732
|
+
);
|
|
733
|
+
useEffect(() => {
|
|
734
|
+
queries.forEach((query, i) => {
|
|
735
|
+
if (query.options?.enabled === false) return;
|
|
736
|
+
doFetchOne(query, keys[i]);
|
|
737
|
+
});
|
|
738
|
+
}, [keysJoined, queryClient, apiClient, doFetchOne]);
|
|
739
|
+
return entries.map((entry, i) => {
|
|
740
|
+
const query = queries[i];
|
|
741
|
+
const opts = query?.options;
|
|
742
|
+
const key = keys[i];
|
|
743
|
+
const rawData = entry?.data;
|
|
744
|
+
let data = opts?.select !== void 0 && rawData !== void 0 ? opts.select(rawData) : rawData;
|
|
745
|
+
let isPlaceholderData = false;
|
|
746
|
+
if (data === void 0 && entry?.status === "loading" && opts?.placeholderData !== void 0) {
|
|
747
|
+
data = opts.placeholderData;
|
|
748
|
+
isPlaceholderData = true;
|
|
749
|
+
}
|
|
750
|
+
if (data !== void 0) {
|
|
751
|
+
previousDataRef.current[i] = data;
|
|
752
|
+
} else if (opts?.keepPreviousData && previousDataRef.current[i] !== void 0) {
|
|
753
|
+
data = previousDataRef.current[i];
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
data,
|
|
757
|
+
isLoading: entry?.status === "loading" && entry?.data === void 0,
|
|
758
|
+
isRefetching: entry?.status === "loading" && entry?.data !== void 0,
|
|
759
|
+
isFetching: entry?.status === "loading",
|
|
760
|
+
isPlaceholderData,
|
|
761
|
+
isSuccess: entry?.status === "success",
|
|
762
|
+
isError: entry?.status === "error",
|
|
763
|
+
error: entry?.error ?? null,
|
|
764
|
+
status: entry?.status ?? "idle",
|
|
765
|
+
refetch: () => {
|
|
766
|
+
if (!key || !query) return;
|
|
767
|
+
doFetchOne(query, key, true);
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
function useSuspenseApiQuery(url, options) {
|
|
773
|
+
const { apiClient, queryClient } = useApiContext();
|
|
774
|
+
const serializedKey = serializeKey([url]);
|
|
775
|
+
const retries = options?.retry === false ? 0 : options?.retry ?? 1;
|
|
776
|
+
const staleTime = options?.staleTime ?? 6e4;
|
|
777
|
+
const entry = useSyncExternalStore(
|
|
778
|
+
(notify) => queryClient.cache.subscribe(serializedKey, notify),
|
|
779
|
+
() => queryClient.cache.get(serializedKey),
|
|
780
|
+
() => queryClient.cache.get(serializedKey)
|
|
781
|
+
);
|
|
782
|
+
if (!entry || entry.status === "loading" || entry.status === "idle" && !entry.data) {
|
|
783
|
+
const existing = queryClient.cache.getInFlight(serializedKey);
|
|
784
|
+
if (existing) {
|
|
785
|
+
throw existing;
|
|
786
|
+
}
|
|
787
|
+
const gen = queryClient.cache.bumpGeneration(serializedKey);
|
|
788
|
+
queryClient.cache.setLoading(serializedKey);
|
|
789
|
+
const p = withRetry(
|
|
790
|
+
() => apiClient.instance.get(url).then((r) => r.data),
|
|
791
|
+
retries
|
|
792
|
+
).then((data) => {
|
|
793
|
+
if (queryClient.cache.getGeneration(serializedKey) === gen) {
|
|
794
|
+
queryClient.cache.setData(serializedKey, data, staleTime);
|
|
795
|
+
}
|
|
796
|
+
}).catch((err) => {
|
|
797
|
+
if (queryClient.cache.getGeneration(serializedKey) === gen) {
|
|
798
|
+
const apiError = toApiError(err);
|
|
799
|
+
queryClient.cache.setError(serializedKey, apiError);
|
|
800
|
+
throw apiError;
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
queryClient.cache.setInFlight(serializedKey, p);
|
|
804
|
+
throw p;
|
|
805
|
+
}
|
|
806
|
+
if (entry.status === "error") {
|
|
807
|
+
throw entry.error;
|
|
808
|
+
}
|
|
809
|
+
const refetch = () => {
|
|
810
|
+
queryClient.cache.clearInFlight(serializedKey);
|
|
811
|
+
const gen = queryClient.cache.bumpGeneration(serializedKey);
|
|
812
|
+
const p = withRetry(
|
|
813
|
+
() => apiClient.instance.get(url).then((r) => r.data),
|
|
814
|
+
retries
|
|
815
|
+
).then((data) => {
|
|
816
|
+
if (queryClient.cache.getGeneration(serializedKey) === gen) {
|
|
817
|
+
queryClient.cache.setData(serializedKey, data, staleTime);
|
|
818
|
+
}
|
|
819
|
+
}).catch((err) => {
|
|
820
|
+
if (queryClient.cache.getGeneration(serializedKey) === gen) {
|
|
821
|
+
queryClient.cache.setError(serializedKey, toApiError(err));
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
queryClient.cache.setInFlight(serializedKey, p);
|
|
825
|
+
};
|
|
826
|
+
return {
|
|
827
|
+
data: entry.data,
|
|
828
|
+
isFetching: false,
|
|
829
|
+
isRefetching: false,
|
|
830
|
+
isSuccess: true,
|
|
831
|
+
refetch
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function usePrefetch(url, options) {
|
|
835
|
+
const { apiClient, queryClient } = useApiContext();
|
|
836
|
+
const prefetch = useCallback(() => {
|
|
837
|
+
void queryClient.prefetchQuery({
|
|
838
|
+
queryKey: [url],
|
|
839
|
+
queryFn: () => apiClient.instance.get(url).then((r) => r.data),
|
|
840
|
+
staleTime: options?.staleTime
|
|
841
|
+
});
|
|
842
|
+
}, [url, queryClient, apiClient, options?.staleTime]);
|
|
843
|
+
return prefetch;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/persistence.ts
|
|
847
|
+
async function persistCache(queryClient, config) {
|
|
848
|
+
const storageKey = config.key ?? "structyl-cache";
|
|
849
|
+
const maxAge = config.maxAge ?? 24 * 60 * 6e4;
|
|
850
|
+
try {
|
|
851
|
+
const raw = await config.storage.getItem(storageKey);
|
|
852
|
+
if (raw) {
|
|
853
|
+
const parsed = JSON.parse(raw);
|
|
854
|
+
if (parsed.savedAt + maxAge > Date.now()) {
|
|
855
|
+
queryClient.cache.restore(parsed.entries);
|
|
856
|
+
} else {
|
|
857
|
+
void config.storage.removeItem(storageKey);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
} catch {
|
|
861
|
+
}
|
|
862
|
+
let debounceTimer;
|
|
863
|
+
const write = () => {
|
|
864
|
+
if (debounceTimer !== void 0) clearTimeout(debounceTimer);
|
|
865
|
+
debounceTimer = setTimeout(() => {
|
|
866
|
+
debounceTimer = void 0;
|
|
867
|
+
const snapshot = queryClient.cache.snapshot();
|
|
868
|
+
const payload = {
|
|
869
|
+
entries: snapshot,
|
|
870
|
+
savedAt: Date.now()
|
|
871
|
+
};
|
|
872
|
+
void config.storage.setItem(storageKey, JSON.stringify(payload));
|
|
873
|
+
}, 1e3);
|
|
874
|
+
};
|
|
875
|
+
const unsubscribe = queryClient.cache.subscribeGlobal(write);
|
|
876
|
+
return () => {
|
|
877
|
+
unsubscribe();
|
|
878
|
+
if (debounceTimer !== void 0) {
|
|
879
|
+
clearTimeout(debounceTimer);
|
|
880
|
+
debounceTimer = void 0;
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export { ApiClient, ApiProvider, QueryClient, createApiClient, persistCache, useApiClient, useApiContext, useApiMutation, useApiQueries, useApiQuery, useInfiniteApiQuery, usePrefetch, useSuspenseApiQuery };
|
|
886
|
+
//# sourceMappingURL=index.mjs.map
|
|
887
|
+
//# sourceMappingURL=index.mjs.map
|