react-smart-query 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 +130 -0
- package/dist/cache.service-MR6EEYM4.mjs +4 -0
- package/dist/cache.service-MR6EEYM4.mjs.map +1 -0
- package/dist/chunk-KLJQATIV.mjs +170 -0
- package/dist/chunk-KLJQATIV.mjs.map +1 -0
- package/dist/chunk-KSLDOL27.mjs +133 -0
- package/dist/chunk-KSLDOL27.mjs.map +1 -0
- package/dist/chunk-QRCVY7UR.mjs +137 -0
- package/dist/chunk-QRCVY7UR.mjs.map +1 -0
- package/dist/index.d.mts +545 -0
- package/dist/index.d.ts +545 -0
- package/dist/index.js +1533 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1018 -0
- package/dist/index.mjs.map +1 -0
- package/dist/storage.adapter-PJCVI4DE.mjs +3 -0
- package/dist/storage.adapter-PJCVI4DE.mjs.map +1 -0
- package/dist/testing.d.mts +89 -0
- package/dist/testing.d.ts +89 -0
- package/dist/testing.js +272 -0
- package/dist/testing.js.map +1 -0
- package/dist/testing.mjs +78 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-XXiTKLnh.d.mts +134 -0
- package/dist/types-XXiTKLnh.d.ts +134 -0
- package/dist/utils/debug.d.mts +2 -0
- package/dist/utils/debug.d.ts +2 -0
- package/dist/utils/debug.js +208 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/debug.mjs +40 -0
- package/dist/utils/debug.mjs.map +1 -0
- package/docs/API_REFERENCE.md +149 -0
- package/docs/GUIDELINES.md +23 -0
- package/docs/TESTING.md +61 -0
- package/package.json +136 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
import { init_requestLock_service, requestLock_service_exports, fetchWithLock, enqueueMutation, processQueue } from './chunk-KLJQATIV.mjs';
|
|
2
|
+
export { clearQueue, enqueueMutation, fetchWithLock, getQueue, getQueueLength, inFlightCount, inFlightKeys, initQueue, processQueue, registerExecutor } from './chunk-KLJQATIV.mjs';
|
|
3
|
+
import { cacheKeyFor, readCache, writeCache, isCacheStale, emit, deleteCache } from './chunk-KSLDOL27.mjs';
|
|
4
|
+
export { CURRENT_CACHE_VERSION, addObserver, cacheKeyFor, clearObservers, deleteCache, getPartialCache, isCacheStale, readCache, removeObserver, setMaxCacheEntries, writeCache } from './chunk-KSLDOL27.mjs';
|
|
5
|
+
import { __toCommonJS, storage } from './chunk-QRCVY7UR.mjs';
|
|
6
|
+
import { useMemo, useState, useRef, useEffect, useCallback, useSyncExternalStore } from 'react';
|
|
7
|
+
import { useQueryClient, useQuery } from '@tanstack/react-query';
|
|
8
|
+
import equal from 'fast-deep-equal';
|
|
9
|
+
|
|
10
|
+
init_requestLock_service();
|
|
11
|
+
function smartCompare(oldData, newData, options = {}) {
|
|
12
|
+
const idField = options.idField ?? "id";
|
|
13
|
+
const versionField = options.versionField === void 0 ? "updatedAt" : options.versionField;
|
|
14
|
+
if (oldData === newData) return { isEqual: true, tier: 1 };
|
|
15
|
+
const oldIsArr = Array.isArray(oldData);
|
|
16
|
+
const newIsArr = Array.isArray(newData);
|
|
17
|
+
if (oldIsArr !== newIsArr) return { isEqual: false, tier: 2 };
|
|
18
|
+
if (!oldIsArr) return { isEqual: equal(oldData, newData), tier: 5 };
|
|
19
|
+
const o = oldData;
|
|
20
|
+
const n = newData;
|
|
21
|
+
if (o.length !== n.length) return { isEqual: false, tier: 2 };
|
|
22
|
+
if (o.length === 0) return { isEqual: true, tier: 2 };
|
|
23
|
+
let oldIds = "";
|
|
24
|
+
let newIds = "";
|
|
25
|
+
for (let i = 0; i < o.length; i++) {
|
|
26
|
+
oldIds += String(o[i][idField] ?? i) + "|";
|
|
27
|
+
newIds += String(n[i][idField] ?? i) + "|";
|
|
28
|
+
}
|
|
29
|
+
if (oldIds !== newIds) return { isEqual: false, tier: 3 };
|
|
30
|
+
if (versionField !== null) {
|
|
31
|
+
let xorOld = 0;
|
|
32
|
+
let xorNew = 0;
|
|
33
|
+
for (let i = 0; i < o.length; i++) {
|
|
34
|
+
xorOld ^= Number(o[i][versionField] ?? 0) ^ i;
|
|
35
|
+
xorNew ^= Number(n[i][versionField] ?? 0) ^ i;
|
|
36
|
+
}
|
|
37
|
+
if (xorOld !== xorNew) return { isEqual: false, tier: 4 };
|
|
38
|
+
}
|
|
39
|
+
return { isEqual: equal(oldData, newData), tier: 5 };
|
|
40
|
+
}
|
|
41
|
+
function isDataEqual(oldData, newData, options) {
|
|
42
|
+
return smartCompare(oldData, newData, options).isEqual;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/utils/normalize.ts
|
|
46
|
+
function emptyList() {
|
|
47
|
+
return { byId: /* @__PURE__ */ Object.create(null), allIds: [] };
|
|
48
|
+
}
|
|
49
|
+
function fromArray(items, getItemId, comparator) {
|
|
50
|
+
const byId = /* @__PURE__ */ Object.create(null);
|
|
51
|
+
const allIds = new Array(items.length);
|
|
52
|
+
for (let i = 0; i < items.length; i++) {
|
|
53
|
+
const item = items[i];
|
|
54
|
+
const id = getItemId(item);
|
|
55
|
+
byId[id] = item;
|
|
56
|
+
allIds[i] = id;
|
|
57
|
+
}
|
|
58
|
+
if (comparator) allIds.sort((a, b) => comparator(byId[a], byId[b]));
|
|
59
|
+
return { byId, allIds };
|
|
60
|
+
}
|
|
61
|
+
function toArray(list) {
|
|
62
|
+
const out = new Array(list.allIds.length);
|
|
63
|
+
for (let i = 0; i < list.allIds.length; i++) out[i] = list.byId[list.allIds[i]];
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function binaryIdx(allIds, byId, item, getItemId, cmp) {
|
|
67
|
+
const itemId = getItemId(item);
|
|
68
|
+
let lo = 0, hi = allIds.length;
|
|
69
|
+
while (lo < hi) {
|
|
70
|
+
const mid = lo + hi >>> 1;
|
|
71
|
+
const midItem = byId[allIds[mid]];
|
|
72
|
+
let res = cmp(midItem, item);
|
|
73
|
+
if (res === 0) {
|
|
74
|
+
res = allIds[mid].localeCompare(itemId);
|
|
75
|
+
}
|
|
76
|
+
res <= 0 ? lo = mid + 1 : hi = mid;
|
|
77
|
+
}
|
|
78
|
+
return lo;
|
|
79
|
+
}
|
|
80
|
+
function normalizedAdd(list, item, getItemId, comparator, getItemVersion) {
|
|
81
|
+
const id = getItemId(item);
|
|
82
|
+
const existing = list.byId[id];
|
|
83
|
+
if (existing && getItemVersion) {
|
|
84
|
+
const vExisting = getItemVersion(existing);
|
|
85
|
+
const vNew = getItemVersion(item);
|
|
86
|
+
if (vNew < vExisting) return list;
|
|
87
|
+
}
|
|
88
|
+
const newById = { ...list.byId, [id]: item };
|
|
89
|
+
const existingIdx = list.allIds.indexOf(id);
|
|
90
|
+
const workingIds = list.allIds.slice();
|
|
91
|
+
if (existingIdx !== -1) workingIds.splice(existingIdx, 1);
|
|
92
|
+
const insertIdx = binaryIdx(workingIds, newById, item, getItemId, comparator);
|
|
93
|
+
workingIds.splice(insertIdx, 0, id);
|
|
94
|
+
return { byId: newById, allIds: workingIds };
|
|
95
|
+
}
|
|
96
|
+
function normalizedUpdate(list, item, getItemId, comparator, getItemVersion) {
|
|
97
|
+
const id = getItemId(item);
|
|
98
|
+
const oldItem = list.byId[id];
|
|
99
|
+
if (!oldItem) return list;
|
|
100
|
+
if (getItemVersion) {
|
|
101
|
+
const vExisting = getItemVersion(oldItem);
|
|
102
|
+
const vNew = getItemVersion(item);
|
|
103
|
+
if (vNew < vExisting) return list;
|
|
104
|
+
}
|
|
105
|
+
if (comparator(oldItem, item) === 0) {
|
|
106
|
+
return {
|
|
107
|
+
allIds: list.allIds,
|
|
108
|
+
// Keep same reference if possible, but immutable is safer
|
|
109
|
+
byId: { ...list.byId, [id]: item }
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return normalizedAdd(list, item, getItemId, comparator, getItemVersion);
|
|
113
|
+
}
|
|
114
|
+
function normalizedRemove(list, id) {
|
|
115
|
+
if (!(id in list.byId)) return list;
|
|
116
|
+
const newById = { ...list.byId };
|
|
117
|
+
delete newById[id];
|
|
118
|
+
const idx = list.allIds.indexOf(id);
|
|
119
|
+
const newAllIds = list.allIds.slice();
|
|
120
|
+
if (idx !== -1) newAllIds.splice(idx, 1);
|
|
121
|
+
return { byId: newById, allIds: newAllIds };
|
|
122
|
+
}
|
|
123
|
+
function mergeNormalizedData(existing, incomingIds, incomingById, getItemId, comparator) {
|
|
124
|
+
const newById = { ...existing.byId, ...incomingById };
|
|
125
|
+
const allIds = [...existing.allIds];
|
|
126
|
+
const seen = new Set(existing.allIds);
|
|
127
|
+
for (const id of incomingIds) {
|
|
128
|
+
if (seen.has(id)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
seen.add(id);
|
|
132
|
+
if (comparator) {
|
|
133
|
+
const item = incomingById[id];
|
|
134
|
+
const insertIdx = binaryIdx(allIds, newById, item, getItemId, comparator);
|
|
135
|
+
allIds.splice(insertIdx, 0, id);
|
|
136
|
+
} else {
|
|
137
|
+
allIds.push(id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (comparator && incomingIds.some((id) => existing.byId[id])) {
|
|
141
|
+
allIds.sort((a, b) => comparator(newById[a], newById[b]));
|
|
142
|
+
}
|
|
143
|
+
return { byId: newById, allIds };
|
|
144
|
+
}
|
|
145
|
+
function getPage(allIds, byId, pageIndex, pageSize) {
|
|
146
|
+
const start = pageIndex * pageSize;
|
|
147
|
+
const ids = allIds.slice(start, start + pageSize);
|
|
148
|
+
return ids.map((id) => byId[id]);
|
|
149
|
+
}
|
|
150
|
+
function derivePages(allIds, byId, pageSize) {
|
|
151
|
+
const totalItems = allIds.length;
|
|
152
|
+
const totalPages = Math.ceil(totalItems / pageSize);
|
|
153
|
+
const pages = [];
|
|
154
|
+
for (let i = 0; i < totalPages; i++) {
|
|
155
|
+
pages.push(getPage(allIds, byId, i, pageSize));
|
|
156
|
+
}
|
|
157
|
+
return pages;
|
|
158
|
+
}
|
|
159
|
+
function isNormalizedEmpty(list) {
|
|
160
|
+
return list.allIds.length === 0;
|
|
161
|
+
}
|
|
162
|
+
function trimNormalizedList(list, maxItems) {
|
|
163
|
+
if (list.allIds.length <= maxItems) return list;
|
|
164
|
+
const removeCount = Math.max(
|
|
165
|
+
Math.ceil(list.allIds.length * 0.2),
|
|
166
|
+
list.allIds.length - maxItems
|
|
167
|
+
);
|
|
168
|
+
const newSize = list.allIds.length - removeCount;
|
|
169
|
+
const newAllIds = list.allIds.slice(0, newSize);
|
|
170
|
+
const newById = {};
|
|
171
|
+
for (const id of newAllIds) {
|
|
172
|
+
newById[id] = list.byId[id];
|
|
173
|
+
}
|
|
174
|
+
return { byId: newById, allIds: newAllIds };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/registry/smartQueryRegistry.ts
|
|
178
|
+
var liveRegistry = /* @__PURE__ */ new Map();
|
|
179
|
+
var sortConfigRegistry = /* @__PURE__ */ new Map();
|
|
180
|
+
function _registerUpdater(storageKey, updater, config) {
|
|
181
|
+
liveRegistry.set(storageKey, updater);
|
|
182
|
+
if (config) sortConfigRegistry.set(storageKey, config);
|
|
183
|
+
}
|
|
184
|
+
function _unregisterUpdater(storageKey) {
|
|
185
|
+
liveRegistry.delete(storageKey);
|
|
186
|
+
}
|
|
187
|
+
async function mutateStorageOnly(storageKey, queryKey, fn) {
|
|
188
|
+
const config = sortConfigRegistry.get(storageKey);
|
|
189
|
+
if (!config) return;
|
|
190
|
+
const entry = await readCache(storageKey, queryKey);
|
|
191
|
+
if (!entry) return;
|
|
192
|
+
const rawData = entry.data;
|
|
193
|
+
const isUnified = "data" in rawData && "meta" in rawData;
|
|
194
|
+
const currentList = isUnified ? rawData.data : rawData;
|
|
195
|
+
const nextList = fn(currentList, config);
|
|
196
|
+
const nextData = isUnified ? { ...rawData, data: nextList } : nextList;
|
|
197
|
+
await writeCache(storageKey, nextData, queryKey);
|
|
198
|
+
}
|
|
199
|
+
function getSmartQueryActions(queryKey) {
|
|
200
|
+
const storageKey = cacheKeyFor(queryKey);
|
|
201
|
+
return {
|
|
202
|
+
isActive: () => liveRegistry.has(storageKey),
|
|
203
|
+
addItem: async (item) => {
|
|
204
|
+
const live = liveRegistry.get(storageKey);
|
|
205
|
+
if (live) {
|
|
206
|
+
live.add(item);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
await mutateStorageOnly(
|
|
210
|
+
storageKey,
|
|
211
|
+
queryKey,
|
|
212
|
+
(list, { comparator, getItemId }) => normalizedAdd(list, item, getItemId, comparator)
|
|
213
|
+
);
|
|
214
|
+
},
|
|
215
|
+
updateItem: async (item) => {
|
|
216
|
+
const live = liveRegistry.get(storageKey);
|
|
217
|
+
if (live) {
|
|
218
|
+
live.update(item);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
await mutateStorageOnly(
|
|
222
|
+
storageKey,
|
|
223
|
+
queryKey,
|
|
224
|
+
(list, { comparator, getItemId }) => normalizedUpdate(list, item, getItemId, comparator)
|
|
225
|
+
);
|
|
226
|
+
},
|
|
227
|
+
removeItem: async (id) => {
|
|
228
|
+
const live = liveRegistry.get(storageKey);
|
|
229
|
+
if (live) {
|
|
230
|
+
live.remove(id);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
await mutateStorageOnly(
|
|
234
|
+
storageKey,
|
|
235
|
+
queryKey,
|
|
236
|
+
(list) => normalizedRemove(list, id)
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
var smartQueryDebug = {
|
|
242
|
+
/** Get the current normalized state for a query key. */
|
|
243
|
+
getNormalizedState: async (queryKey) => {
|
|
244
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return null;
|
|
245
|
+
const storageKey = cacheKeyFor(queryKey);
|
|
246
|
+
const entry = await readCache(storageKey, queryKey);
|
|
247
|
+
return entry ? entry.data : null;
|
|
248
|
+
},
|
|
249
|
+
/** Log a detailed summary of the cache entry to the console. */
|
|
250
|
+
inspectCache: async (queryKey) => {
|
|
251
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return;
|
|
252
|
+
const storageKey = cacheKeyFor(queryKey);
|
|
253
|
+
const entry = await readCache(storageKey, queryKey);
|
|
254
|
+
if (!entry) {
|
|
255
|
+
console.log(`[SmartQuery Debug] Cache MISS for:`, queryKey);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
console.log(`[SmartQuery Debug] Cache HIT for:`, queryKey, {
|
|
259
|
+
cachedAt: new Date(entry.cachedAt).toISOString(),
|
|
260
|
+
size: JSON.stringify(entry.data).length,
|
|
261
|
+
data: entry.data
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
/** Clear the cache entry for a query key. */
|
|
265
|
+
clearCache: async (queryKey) => {
|
|
266
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return;
|
|
267
|
+
const storageKey = cacheKeyFor(queryKey);
|
|
268
|
+
const { deleteCache: deleteCache2 } = await import('./cache.service-MR6EEYM4.mjs');
|
|
269
|
+
await deleteCache2(storageKey);
|
|
270
|
+
},
|
|
271
|
+
/** Get the current state of the offline mutation queue. */
|
|
272
|
+
getQueue: async () => {
|
|
273
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return [];
|
|
274
|
+
try {
|
|
275
|
+
const { getStorage } = await import('./storage.adapter-PJCVI4DE.mjs');
|
|
276
|
+
const raw = await getStorage().get("sq_mutation_queue");
|
|
277
|
+
return raw ? JSON.parse(raw) : [];
|
|
278
|
+
} catch {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
/** Get list of all storage keys currently being fetched. */
|
|
283
|
+
inFlightRequests: () => {
|
|
284
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return [];
|
|
285
|
+
try {
|
|
286
|
+
const { inFlightKeys: inFlightKeys2 } = (init_requestLock_service(), __toCommonJS(requestLock_service_exports));
|
|
287
|
+
return inFlightKeys2();
|
|
288
|
+
} catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
async function batchUpdate(queryKey, fn) {
|
|
294
|
+
const storageKey = cacheKeyFor(queryKey);
|
|
295
|
+
const actions = getSmartQueryActions(queryKey);
|
|
296
|
+
const live = liveRegistry.has(storageKey);
|
|
297
|
+
if (live) {
|
|
298
|
+
await fn(actions);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const config = sortConfigRegistry.get(storageKey);
|
|
302
|
+
if (!config) return;
|
|
303
|
+
const entry = await readCache(storageKey, queryKey);
|
|
304
|
+
if (!entry) return;
|
|
305
|
+
let currentData = entry.data;
|
|
306
|
+
const isUnified = "data" in currentData && "meta" in currentData;
|
|
307
|
+
let currentList = isUnified ? currentData.data : currentData;
|
|
308
|
+
const batchActions = {
|
|
309
|
+
isActive: () => false,
|
|
310
|
+
addItem: async (item) => {
|
|
311
|
+
currentList = normalizedAdd(currentList, item, config.getItemId, config.comparator);
|
|
312
|
+
},
|
|
313
|
+
updateItem: async (item) => {
|
|
314
|
+
currentList = normalizedUpdate(currentList, item, config.getItemId, config.comparator);
|
|
315
|
+
},
|
|
316
|
+
removeItem: async (id) => {
|
|
317
|
+
currentList = normalizedRemove(currentList, id);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
await fn(batchActions);
|
|
321
|
+
const nextData = isUnified ? { ...currentData, data: currentList } : currentList;
|
|
322
|
+
await writeCache(storageKey, nextData, queryKey);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/hooks/useSmartQuery.ts
|
|
326
|
+
function normalizeError(cause) {
|
|
327
|
+
if (cause instanceof Error) {
|
|
328
|
+
const err = cause;
|
|
329
|
+
const status = err.status;
|
|
330
|
+
return {
|
|
331
|
+
cause,
|
|
332
|
+
message: cause.message,
|
|
333
|
+
retryable: !status || status >= 500 || status === 429,
|
|
334
|
+
statusCode: status
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return { cause, message: String(cause), retryable: true };
|
|
338
|
+
}
|
|
339
|
+
var DEFAULT_TTL = 5 * 6e4;
|
|
340
|
+
function useSmartQuery(options) {
|
|
341
|
+
const {
|
|
342
|
+
queryKey,
|
|
343
|
+
queryFn,
|
|
344
|
+
select,
|
|
345
|
+
getItemId,
|
|
346
|
+
sortComparator,
|
|
347
|
+
cacheTtl = DEFAULT_TTL,
|
|
348
|
+
maxItems = 1e3,
|
|
349
|
+
getItemVersion,
|
|
350
|
+
strictFreshness = false,
|
|
351
|
+
fallbackData,
|
|
352
|
+
compareOptions,
|
|
353
|
+
onSuccess,
|
|
354
|
+
onError,
|
|
355
|
+
queryOptions = {}
|
|
356
|
+
} = options;
|
|
357
|
+
const queryClient = useQueryClient();
|
|
358
|
+
const storageKey = useMemo(
|
|
359
|
+
() => cacheKeyFor(queryKey),
|
|
360
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
361
|
+
[JSON.stringify(queryKey)]
|
|
362
|
+
);
|
|
363
|
+
const listMode = typeof sortComparator === "function" && typeof getItemId === "function";
|
|
364
|
+
const [viewData, setViewData] = useState(void 0);
|
|
365
|
+
const [isCacheLoading, setIsCacheLoading] = useState(true);
|
|
366
|
+
const [isFromCache, setIsFromCache] = useState(false);
|
|
367
|
+
const [shouldFetch, setShouldFetch] = useState(false);
|
|
368
|
+
const [smartError, setSmartError] = useState(null);
|
|
369
|
+
const prevRawRef = useRef(void 0);
|
|
370
|
+
const normalizedRef = useRef(emptyList());
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
let cancelled = false;
|
|
373
|
+
setIsCacheLoading(true);
|
|
374
|
+
readCache(storageKey, queryKey).then((entry) => {
|
|
375
|
+
if (cancelled) return;
|
|
376
|
+
if (entry !== null) {
|
|
377
|
+
const isStale = isCacheStale(entry, cacheTtl);
|
|
378
|
+
const usable = !strictFreshness || !isStale;
|
|
379
|
+
if (usable) {
|
|
380
|
+
if (listMode) {
|
|
381
|
+
let normalized;
|
|
382
|
+
if (Array.isArray(entry.data)) {
|
|
383
|
+
normalized = fromArray(
|
|
384
|
+
entry.data,
|
|
385
|
+
getItemId,
|
|
386
|
+
sortComparator
|
|
387
|
+
);
|
|
388
|
+
} else {
|
|
389
|
+
normalized = entry.data;
|
|
390
|
+
}
|
|
391
|
+
normalizedRef.current = normalized;
|
|
392
|
+
const view = toArray(normalized);
|
|
393
|
+
prevRawRef.current = normalized;
|
|
394
|
+
queryClient.setQueryData(queryKey, view);
|
|
395
|
+
setViewData(view);
|
|
396
|
+
} else {
|
|
397
|
+
prevRawRef.current = entry.data;
|
|
398
|
+
queryClient.setQueryData(queryKey, entry.data);
|
|
399
|
+
setViewData(entry.data);
|
|
400
|
+
}
|
|
401
|
+
setIsFromCache(true);
|
|
402
|
+
setShouldFetch(isStale);
|
|
403
|
+
} else {
|
|
404
|
+
setShouldFetch(true);
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
setShouldFetch(true);
|
|
408
|
+
}
|
|
409
|
+
setIsCacheLoading(false);
|
|
410
|
+
});
|
|
411
|
+
return () => {
|
|
412
|
+
cancelled = true;
|
|
413
|
+
};
|
|
414
|
+
}, [storageKey]);
|
|
415
|
+
const wrappedQueryFn = useCallback(
|
|
416
|
+
async (ctx) => {
|
|
417
|
+
emit({ type: "fetch_start", queryKey });
|
|
418
|
+
const start = Date.now();
|
|
419
|
+
try {
|
|
420
|
+
const raw = await fetchWithLock(storageKey, () => queryFn(ctx));
|
|
421
|
+
const transformed = select ? select(raw) : raw;
|
|
422
|
+
emit({
|
|
423
|
+
type: "fetch_success",
|
|
424
|
+
queryKey,
|
|
425
|
+
durationMs: Date.now() - start
|
|
426
|
+
});
|
|
427
|
+
return transformed;
|
|
428
|
+
} catch (err) {
|
|
429
|
+
emit({ type: "fetch_error", queryKey, error: err });
|
|
430
|
+
throw err;
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
434
|
+
[storageKey, queryFn, select]
|
|
435
|
+
);
|
|
436
|
+
const {
|
|
437
|
+
data: freshData,
|
|
438
|
+
isFetching,
|
|
439
|
+
error: tqError,
|
|
440
|
+
refetch: tqRefetch
|
|
441
|
+
} = useQuery({
|
|
442
|
+
queryKey,
|
|
443
|
+
queryFn: wrappedQueryFn,
|
|
444
|
+
enabled: !isCacheLoading && shouldFetch,
|
|
445
|
+
staleTime: cacheTtl,
|
|
446
|
+
gcTime: cacheTtl * 2,
|
|
447
|
+
refetchOnWindowFocus: false,
|
|
448
|
+
refetchOnReconnect: true,
|
|
449
|
+
networkMode: "offlineFirst",
|
|
450
|
+
...queryOptions
|
|
451
|
+
});
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
if (!tqError) {
|
|
454
|
+
setSmartError(null);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const structured = normalizeError(tqError);
|
|
458
|
+
const suppressed = onError?.(structured);
|
|
459
|
+
if (!suppressed) setSmartError(structured);
|
|
460
|
+
}, [tqError, onError]);
|
|
461
|
+
useEffect(() => {
|
|
462
|
+
if (freshData === void 0 || isFetching) return;
|
|
463
|
+
if (listMode) {
|
|
464
|
+
const freshNormalized = fromArray(
|
|
465
|
+
freshData,
|
|
466
|
+
getItemId,
|
|
467
|
+
sortComparator
|
|
468
|
+
);
|
|
469
|
+
if (!isDataEqual(prevRawRef.current, freshNormalized, compareOptions)) {
|
|
470
|
+
prevRawRef.current = freshNormalized;
|
|
471
|
+
normalizedRef.current = freshNormalized;
|
|
472
|
+
const view = toArray(freshNormalized);
|
|
473
|
+
queryClient.setQueryData(queryKey, view);
|
|
474
|
+
setViewData(() => view);
|
|
475
|
+
setIsFromCache(false);
|
|
476
|
+
const trimmed = trimNormalizedList(freshNormalized, maxItems);
|
|
477
|
+
void writeCache(storageKey, trimmed, queryKey);
|
|
478
|
+
onSuccess?.(view);
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
if (!isDataEqual(prevRawRef.current, freshData, compareOptions)) {
|
|
482
|
+
prevRawRef.current = freshData;
|
|
483
|
+
queryClient.setQueryData(queryKey, freshData);
|
|
484
|
+
setViewData(() => freshData);
|
|
485
|
+
setIsFromCache(false);
|
|
486
|
+
void writeCache(storageKey, freshData, queryKey);
|
|
487
|
+
onSuccess?.(freshData);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}, [freshData, isFetching]);
|
|
491
|
+
const applyMutation = useCallback(
|
|
492
|
+
(fn) => {
|
|
493
|
+
const next = trimNormalizedList(fn(normalizedRef.current), maxItems);
|
|
494
|
+
normalizedRef.current = next;
|
|
495
|
+
prevRawRef.current = next;
|
|
496
|
+
const view = toArray(next);
|
|
497
|
+
queryClient.setQueryData(queryKey, view);
|
|
498
|
+
setViewData(view);
|
|
499
|
+
void writeCache(storageKey, next, queryKey);
|
|
500
|
+
},
|
|
501
|
+
[queryClient, queryKey, storageKey, maxItems]
|
|
502
|
+
);
|
|
503
|
+
const addItem = useCallback(
|
|
504
|
+
(item) => {
|
|
505
|
+
if (!listMode) return;
|
|
506
|
+
applyMutation(
|
|
507
|
+
(cur) => normalizedAdd(
|
|
508
|
+
cur,
|
|
509
|
+
item,
|
|
510
|
+
getItemId,
|
|
511
|
+
sortComparator,
|
|
512
|
+
getItemVersion
|
|
513
|
+
)
|
|
514
|
+
);
|
|
515
|
+
},
|
|
516
|
+
[listMode, getItemId, sortComparator, getItemVersion, applyMutation]
|
|
517
|
+
);
|
|
518
|
+
const updateItem = useCallback(
|
|
519
|
+
(item) => {
|
|
520
|
+
if (!listMode) return;
|
|
521
|
+
applyMutation(
|
|
522
|
+
(cur) => normalizedUpdate(
|
|
523
|
+
cur,
|
|
524
|
+
item,
|
|
525
|
+
getItemId,
|
|
526
|
+
sortComparator,
|
|
527
|
+
getItemVersion
|
|
528
|
+
)
|
|
529
|
+
);
|
|
530
|
+
},
|
|
531
|
+
[listMode, getItemId, sortComparator, getItemVersion, applyMutation]
|
|
532
|
+
);
|
|
533
|
+
const removeItem = useCallback(
|
|
534
|
+
(id) => {
|
|
535
|
+
if (!listMode) return;
|
|
536
|
+
applyMutation((cur) => normalizedRemove(cur, id));
|
|
537
|
+
},
|
|
538
|
+
[listMode, applyMutation]
|
|
539
|
+
);
|
|
540
|
+
const addRef = useRef(addItem);
|
|
541
|
+
const updateRef = useRef(updateItem);
|
|
542
|
+
const removeRef = useRef(removeItem);
|
|
543
|
+
useEffect(() => {
|
|
544
|
+
addRef.current = addItem;
|
|
545
|
+
}, [addItem]);
|
|
546
|
+
useEffect(() => {
|
|
547
|
+
updateRef.current = updateItem;
|
|
548
|
+
}, [updateItem]);
|
|
549
|
+
useEffect(() => {
|
|
550
|
+
removeRef.current = removeItem;
|
|
551
|
+
}, [removeItem]);
|
|
552
|
+
useEffect(() => {
|
|
553
|
+
_registerUpdater(
|
|
554
|
+
storageKey,
|
|
555
|
+
{
|
|
556
|
+
add: (item) => addRef.current(item),
|
|
557
|
+
update: (item) => updateRef.current(item),
|
|
558
|
+
remove: (id) => removeRef.current(id)
|
|
559
|
+
},
|
|
560
|
+
listMode && !!sortComparator && !!getItemId ? {
|
|
561
|
+
comparator: sortComparator,
|
|
562
|
+
getItemId
|
|
563
|
+
} : null
|
|
564
|
+
);
|
|
565
|
+
return () => _unregisterUpdater(storageKey);
|
|
566
|
+
}, [storageKey]);
|
|
567
|
+
const data = viewData ?? (tqError ? fallbackData : void 0);
|
|
568
|
+
const isLoading = isCacheLoading || data === void 0 && isFetching;
|
|
569
|
+
const refetch = useCallback(() => {
|
|
570
|
+
setShouldFetch(true);
|
|
571
|
+
tqRefetch();
|
|
572
|
+
}, [tqRefetch]);
|
|
573
|
+
return {
|
|
574
|
+
data,
|
|
575
|
+
isLoading,
|
|
576
|
+
isFetching,
|
|
577
|
+
isFromCache,
|
|
578
|
+
isCacheLoading,
|
|
579
|
+
error: smartError,
|
|
580
|
+
refetch,
|
|
581
|
+
addItem,
|
|
582
|
+
updateItem,
|
|
583
|
+
removeItem
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
var invalidateSmartCache = (queryKey) => deleteCache(cacheKeyFor(queryKey));
|
|
587
|
+
var clearAllSmartCache = () => storage.clearAll();
|
|
588
|
+
function defaultIsOnline() {
|
|
589
|
+
if (typeof navigator !== "undefined" && "onLine" in navigator) {
|
|
590
|
+
return navigator.onLine;
|
|
591
|
+
}
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
function normalizeError2(cause) {
|
|
595
|
+
if (cause instanceof Error) {
|
|
596
|
+
const err = cause;
|
|
597
|
+
const status = err.status;
|
|
598
|
+
return {
|
|
599
|
+
cause,
|
|
600
|
+
message: cause.message,
|
|
601
|
+
retryable: !status || status >= 500 || status === 429,
|
|
602
|
+
statusCode: status
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
return { cause, message: String(cause), retryable: true };
|
|
606
|
+
}
|
|
607
|
+
function useSmartMutation(options) {
|
|
608
|
+
const {
|
|
609
|
+
queryKey,
|
|
610
|
+
mutationType,
|
|
611
|
+
mutationFn,
|
|
612
|
+
getItemId,
|
|
613
|
+
toItem,
|
|
614
|
+
enableOfflineQueue = true,
|
|
615
|
+
getEntityKey,
|
|
616
|
+
onSuccess,
|
|
617
|
+
onError,
|
|
618
|
+
isOnline = defaultIsOnline
|
|
619
|
+
} = options;
|
|
620
|
+
const [isPending, setIsPending] = useState(false);
|
|
621
|
+
const [error, setError] = useState(null);
|
|
622
|
+
const isMounted = useRef(true);
|
|
623
|
+
const getActions = useCallback(
|
|
624
|
+
() => getSmartQueryActions(queryKey),
|
|
625
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
626
|
+
[JSON.stringify(queryKey)]
|
|
627
|
+
);
|
|
628
|
+
const mutateAsync = useCallback(
|
|
629
|
+
async (item) => {
|
|
630
|
+
const actions = getActions();
|
|
631
|
+
const itemId = getItemId(item);
|
|
632
|
+
if (isMounted.current) {
|
|
633
|
+
setIsPending(true);
|
|
634
|
+
setError(null);
|
|
635
|
+
}
|
|
636
|
+
if (mutationType === "ADD_ITEM" || mutationType === "CUSTOM") {
|
|
637
|
+
await actions.addItem(item);
|
|
638
|
+
} else if (mutationType === "UPDATE_ITEM") {
|
|
639
|
+
await actions.updateItem(item);
|
|
640
|
+
} else if (mutationType === "REMOVE_ITEM") {
|
|
641
|
+
await actions.removeItem(itemId);
|
|
642
|
+
}
|
|
643
|
+
if (!isOnline()) {
|
|
644
|
+
if (enableOfflineQueue) {
|
|
645
|
+
await enqueueMutation({
|
|
646
|
+
type: mutationType,
|
|
647
|
+
queryKey,
|
|
648
|
+
payload: item,
|
|
649
|
+
entityKey: getEntityKey?.(item)
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (isMounted.current) setIsPending(false);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const response = await mutationFn(item);
|
|
657
|
+
const confirmedItem = toItem ? toItem(response) : response;
|
|
658
|
+
if (mutationType !== "REMOVE_ITEM") {
|
|
659
|
+
await actions.updateItem(confirmedItem);
|
|
660
|
+
}
|
|
661
|
+
emit({
|
|
662
|
+
type: "queue_success",
|
|
663
|
+
mutationId: `${mutationType}:${itemId}`
|
|
664
|
+
});
|
|
665
|
+
onSuccess?.(response, item);
|
|
666
|
+
void processQueue();
|
|
667
|
+
} catch (err) {
|
|
668
|
+
if (mutationType === "ADD_ITEM" || mutationType === "CUSTOM") {
|
|
669
|
+
await actions.removeItem(itemId);
|
|
670
|
+
} else if (mutationType === "UPDATE_ITEM") ; else if (mutationType === "REMOVE_ITEM") {
|
|
671
|
+
await actions.addItem(item);
|
|
672
|
+
}
|
|
673
|
+
const structured = normalizeError2(err);
|
|
674
|
+
if (isMounted.current) setError(structured);
|
|
675
|
+
onError?.(structured, item);
|
|
676
|
+
} finally {
|
|
677
|
+
if (isMounted.current) setIsPending(false);
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
[
|
|
681
|
+
getActions,
|
|
682
|
+
getItemId,
|
|
683
|
+
mutationType,
|
|
684
|
+
mutationFn,
|
|
685
|
+
toItem,
|
|
686
|
+
enableOfflineQueue,
|
|
687
|
+
getEntityKey,
|
|
688
|
+
isOnline,
|
|
689
|
+
onSuccess,
|
|
690
|
+
onError,
|
|
691
|
+
queryKey
|
|
692
|
+
]
|
|
693
|
+
);
|
|
694
|
+
const mutate = useCallback(
|
|
695
|
+
(item) => {
|
|
696
|
+
void mutateAsync(item);
|
|
697
|
+
},
|
|
698
|
+
[mutateAsync]
|
|
699
|
+
);
|
|
700
|
+
const reset = useCallback(() => {
|
|
701
|
+
setError(null);
|
|
702
|
+
setIsPending(false);
|
|
703
|
+
}, []);
|
|
704
|
+
return { mutate, mutateAsync, isPending, error, reset };
|
|
705
|
+
}
|
|
706
|
+
init_requestLock_service();
|
|
707
|
+
function normalizeError3(cause) {
|
|
708
|
+
if (cause instanceof Error) {
|
|
709
|
+
const err = cause;
|
|
710
|
+
const status = err.status;
|
|
711
|
+
return {
|
|
712
|
+
cause,
|
|
713
|
+
message: cause.message,
|
|
714
|
+
retryable: !status || status >= 500 || status === 429,
|
|
715
|
+
statusCode: status
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
return { cause, message: String(cause), retryable: true };
|
|
719
|
+
}
|
|
720
|
+
var DEFAULT_TTL2 = 5 * 6e4;
|
|
721
|
+
function useInfiniteSmartQuery(options) {
|
|
722
|
+
const {
|
|
723
|
+
queryKey,
|
|
724
|
+
queryFn,
|
|
725
|
+
getNextCursor,
|
|
726
|
+
select,
|
|
727
|
+
getItemId,
|
|
728
|
+
getItemVersion,
|
|
729
|
+
sortComparator,
|
|
730
|
+
initialPageParam = void 0,
|
|
731
|
+
paginationMode = "normalized",
|
|
732
|
+
pageSize: pageSizeProp,
|
|
733
|
+
maxItems = 1e3,
|
|
734
|
+
cacheTtl = DEFAULT_TTL2,
|
|
735
|
+
strictFreshness = false,
|
|
736
|
+
onError
|
|
737
|
+
} = options;
|
|
738
|
+
const storageKey = useMemo(
|
|
739
|
+
() => cacheKeyFor(queryKey),
|
|
740
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
741
|
+
[JSON.stringify(queryKey)]
|
|
742
|
+
);
|
|
743
|
+
const [infiniteData, setInfiniteData] = useState({
|
|
744
|
+
data: emptyList(),
|
|
745
|
+
meta: {
|
|
746
|
+
nextCursor: initialPageParam ?? null,
|
|
747
|
+
pageParams: []
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
const [pageSize, setPageSize] = useState(pageSizeProp);
|
|
751
|
+
const [isCacheLoading, setIsCacheLoading] = useState(true);
|
|
752
|
+
const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
|
|
753
|
+
const [isFetching, setIsFetching] = useState(false);
|
|
754
|
+
const [error, setError] = useState(null);
|
|
755
|
+
const infiniteRef = useRef(infiniteData);
|
|
756
|
+
function setAndNotify(next) {
|
|
757
|
+
infiniteRef.current = next;
|
|
758
|
+
setInfiniteData(next);
|
|
759
|
+
}
|
|
760
|
+
useEffect(() => {
|
|
761
|
+
let cancelled = false;
|
|
762
|
+
setIsCacheLoading(true);
|
|
763
|
+
readCache(storageKey, queryKey).then((entry) => {
|
|
764
|
+
if (cancelled) return;
|
|
765
|
+
if (entry !== null) {
|
|
766
|
+
const isStale = isCacheStale(entry, cacheTtl);
|
|
767
|
+
if (!strictFreshness || !isStale) {
|
|
768
|
+
setAndNotify(entry.data);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
setIsCacheLoading(false);
|
|
772
|
+
});
|
|
773
|
+
return () => {
|
|
774
|
+
cancelled = true;
|
|
775
|
+
};
|
|
776
|
+
}, [storageKey]);
|
|
777
|
+
const fetchPage = useCallback(
|
|
778
|
+
async (cursor) => {
|
|
779
|
+
const lockKey = `${storageKey}:${JSON.stringify(cursor)}`;
|
|
780
|
+
emit({ type: "fetch_start", queryKey });
|
|
781
|
+
try {
|
|
782
|
+
const raw = await fetchWithLock(lockKey, () => queryFn({ pageParam: cursor }));
|
|
783
|
+
const items = select(raw);
|
|
784
|
+
const nextCursor = getNextCursor(raw);
|
|
785
|
+
const pageIds = items.map(getItemId);
|
|
786
|
+
const pageById = fromArray(items, getItemId).byId;
|
|
787
|
+
const now = Date.now();
|
|
788
|
+
emit({ type: "fetch_success", queryKey, durationMs: 0 });
|
|
789
|
+
const current = infiniteRef.current;
|
|
790
|
+
const alreadyFetched = current.meta.pageParams.includes(cursor);
|
|
791
|
+
if (!pageSize && items.length > 0) {
|
|
792
|
+
setPageSize(items.length);
|
|
793
|
+
}
|
|
794
|
+
let mergedList = mergeNormalizedData(
|
|
795
|
+
current.data,
|
|
796
|
+
pageIds,
|
|
797
|
+
pageById,
|
|
798
|
+
getItemId,
|
|
799
|
+
sortComparator
|
|
800
|
+
);
|
|
801
|
+
mergedList = trimNormalizedList(mergedList, maxItems);
|
|
802
|
+
const next = {
|
|
803
|
+
data: mergedList,
|
|
804
|
+
meta: {
|
|
805
|
+
pageParams: alreadyFetched ? current.meta.pageParams : [...current.meta.pageParams, cursor],
|
|
806
|
+
nextCursor,
|
|
807
|
+
lastFetchedAt: now
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
setAndNotify(next);
|
|
811
|
+
void writeCache(storageKey, next, queryKey);
|
|
812
|
+
setError(null);
|
|
813
|
+
} catch (err) {
|
|
814
|
+
emit({ type: "fetch_error", queryKey, error: err });
|
|
815
|
+
const structured = normalizeError3(err);
|
|
816
|
+
setError(structured);
|
|
817
|
+
onError?.(structured);
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
821
|
+
[storageKey, queryFn, select, getNextCursor, getItemId, sortComparator, maxItems]
|
|
822
|
+
);
|
|
823
|
+
useEffect(() => {
|
|
824
|
+
if (isCacheLoading) return;
|
|
825
|
+
void (async () => {
|
|
826
|
+
setIsFetching(true);
|
|
827
|
+
await fetchPage(initialPageParam);
|
|
828
|
+
setIsFetching(false);
|
|
829
|
+
})();
|
|
830
|
+
}, [isCacheLoading]);
|
|
831
|
+
const inFlightRef = useRef(/* @__PURE__ */ new Set());
|
|
832
|
+
const fetchNextPage = useCallback(() => {
|
|
833
|
+
const { nextCursor, lastFetchedAt } = infiniteRef.current.meta;
|
|
834
|
+
if (nextCursor === null || isFetchingNextPage) return;
|
|
835
|
+
const cursorKey = JSON.stringify(nextCursor);
|
|
836
|
+
if (inFlightRef.current.has(cursorKey)) return;
|
|
837
|
+
if (lastFetchedAt && Date.now() - lastFetchedAt < 500) return;
|
|
838
|
+
void (async () => {
|
|
839
|
+
inFlightRef.current.add(cursorKey);
|
|
840
|
+
setIsFetchingNextPage(true);
|
|
841
|
+
await fetchPage(nextCursor);
|
|
842
|
+
setIsFetchingNextPage(false);
|
|
843
|
+
inFlightRef.current.delete(cursorKey);
|
|
844
|
+
})();
|
|
845
|
+
}, [fetchPage, isFetchingNextPage]);
|
|
846
|
+
const refetch = useCallback(() => {
|
|
847
|
+
void (async () => {
|
|
848
|
+
setIsFetching(true);
|
|
849
|
+
await fetchPage(initialPageParam);
|
|
850
|
+
setIsFetching(false);
|
|
851
|
+
})();
|
|
852
|
+
}, [fetchPage, initialPageParam]);
|
|
853
|
+
const applyItemMutation = useCallback(
|
|
854
|
+
(fn) => {
|
|
855
|
+
const current = infiniteRef.current;
|
|
856
|
+
const next = {
|
|
857
|
+
...current,
|
|
858
|
+
data: fn(current.data)
|
|
859
|
+
};
|
|
860
|
+
setAndNotify(next);
|
|
861
|
+
void writeCache(storageKey, next, queryKey);
|
|
862
|
+
},
|
|
863
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
864
|
+
[storageKey]
|
|
865
|
+
);
|
|
866
|
+
const updateItem = useCallback(
|
|
867
|
+
(item) => {
|
|
868
|
+
applyItemMutation(
|
|
869
|
+
(data) => normalizedUpdate(
|
|
870
|
+
data,
|
|
871
|
+
item,
|
|
872
|
+
getItemId,
|
|
873
|
+
sortComparator ?? ((a, b) => 0),
|
|
874
|
+
getItemVersion
|
|
875
|
+
)
|
|
876
|
+
);
|
|
877
|
+
},
|
|
878
|
+
[getItemId, sortComparator, getItemVersion, applyItemMutation]
|
|
879
|
+
);
|
|
880
|
+
const addItem = useCallback(
|
|
881
|
+
(item) => {
|
|
882
|
+
const id = getItemId(item);
|
|
883
|
+
const current = infiniteRef.current;
|
|
884
|
+
if (id in current.data.byId) {
|
|
885
|
+
updateItem(item);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
applyItemMutation((data) => {
|
|
889
|
+
const next = normalizedAdd(
|
|
890
|
+
data,
|
|
891
|
+
item,
|
|
892
|
+
getItemId,
|
|
893
|
+
sortComparator ?? ((a, b) => 0),
|
|
894
|
+
getItemVersion
|
|
895
|
+
);
|
|
896
|
+
return trimNormalizedList(next, maxItems);
|
|
897
|
+
});
|
|
898
|
+
},
|
|
899
|
+
[getItemId, sortComparator, getItemVersion, applyItemMutation, updateItem, maxItems]
|
|
900
|
+
);
|
|
901
|
+
const removeItem = useCallback(
|
|
902
|
+
(id) => {
|
|
903
|
+
applyItemMutation((data) => normalizedRemove(data, id));
|
|
904
|
+
},
|
|
905
|
+
[applyItemMutation]
|
|
906
|
+
);
|
|
907
|
+
const flatData = useMemo(() => toArray(infiniteData.data), [infiniteData.data]);
|
|
908
|
+
const isLoading = isCacheLoading || flatData.length === 0 && isFetching;
|
|
909
|
+
const isRefreshing = !!(flatData.length > 0 && isFetching && !isFetchingNextPage);
|
|
910
|
+
const hasNextPage = infiniteRef.current.meta.nextCursor !== null;
|
|
911
|
+
const totalCount = infiniteData.data.allIds.length;
|
|
912
|
+
const derivedData = useMemo(() => {
|
|
913
|
+
if (paginationMode === "pages") {
|
|
914
|
+
return {
|
|
915
|
+
pages: derivePages(
|
|
916
|
+
infiniteData.data.allIds,
|
|
917
|
+
infiniteData.data.byId,
|
|
918
|
+
pageSize ?? flatData.length
|
|
919
|
+
)
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
return flatData;
|
|
923
|
+
}, [paginationMode, infiniteData.data.allIds, infiniteData.data.byId, pageSize, flatData]);
|
|
924
|
+
return {
|
|
925
|
+
data: derivedData,
|
|
926
|
+
isLoading,
|
|
927
|
+
isFetchingNextPage,
|
|
928
|
+
isFetching,
|
|
929
|
+
isRefreshing,
|
|
930
|
+
hasNextPage,
|
|
931
|
+
error,
|
|
932
|
+
totalCount,
|
|
933
|
+
fetchNextPage,
|
|
934
|
+
refetch,
|
|
935
|
+
addItem,
|
|
936
|
+
updateItem,
|
|
937
|
+
removeItem
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function useSmartQuerySelector(queryKey, selector, equalityFn = equal) {
|
|
941
|
+
const queryClient = useQueryClient();
|
|
942
|
+
const selectedRef = useRef(selector(queryClient.getQueryData(queryKey)));
|
|
943
|
+
const subscribe = useCallback(
|
|
944
|
+
(onStoreChange) => {
|
|
945
|
+
return queryClient.getQueryCache().subscribe((event) => {
|
|
946
|
+
if (event.type !== "updated" && event.type !== "added" && event.type !== "removed") return;
|
|
947
|
+
const cacheKey = JSON.stringify(queryKey);
|
|
948
|
+
const eventKey = JSON.stringify(event.query.queryKey);
|
|
949
|
+
if (cacheKey !== eventKey) return;
|
|
950
|
+
const newSelected = selector(queryClient.getQueryData(queryKey));
|
|
951
|
+
if (!equalityFn(selectedRef.current, newSelected)) {
|
|
952
|
+
selectedRef.current = newSelected;
|
|
953
|
+
onStoreChange();
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
},
|
|
957
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
958
|
+
[queryClient, JSON.stringify(queryKey), selector, equalityFn]
|
|
959
|
+
);
|
|
960
|
+
const getSnapshot = useCallback(
|
|
961
|
+
() => selectedRef.current,
|
|
962
|
+
[]
|
|
963
|
+
);
|
|
964
|
+
const getServerSnapshot = useCallback(
|
|
965
|
+
() => selector(void 0),
|
|
966
|
+
[selector]
|
|
967
|
+
);
|
|
968
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/factory/createTypedQuery.ts
|
|
972
|
+
function createTypedQuery(config) {
|
|
973
|
+
return {
|
|
974
|
+
useQuery(...args) {
|
|
975
|
+
const qk = config.queryKey(...args);
|
|
976
|
+
return useSmartQuery({
|
|
977
|
+
queryKey: qk,
|
|
978
|
+
queryFn: () => config.queryFn(...args),
|
|
979
|
+
select: config.select,
|
|
980
|
+
getItemId: config.getItemId,
|
|
981
|
+
sortComparator: config.sortComparator,
|
|
982
|
+
cacheTtl: config.cacheTtl,
|
|
983
|
+
strictFreshness: config.strictFreshness,
|
|
984
|
+
...config.defaultOptions
|
|
985
|
+
});
|
|
986
|
+
},
|
|
987
|
+
useMutation(...argsAndOptions) {
|
|
988
|
+
const mutationOptions = argsAndOptions[argsAndOptions.length - 1];
|
|
989
|
+
const args = argsAndOptions.slice(0, -1);
|
|
990
|
+
const qk = config.queryKey(...args);
|
|
991
|
+
if (!config.getItemId) {
|
|
992
|
+
throw new Error(
|
|
993
|
+
"[SmartQuery] useMutation requires getItemId to be defined in createTypedQuery"
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
return useSmartMutation({
|
|
997
|
+
queryKey: qk,
|
|
998
|
+
getItemId: config.getItemId,
|
|
999
|
+
...mutationOptions
|
|
1000
|
+
});
|
|
1001
|
+
},
|
|
1002
|
+
getActions(...args) {
|
|
1003
|
+
const qk = config.queryKey(...args);
|
|
1004
|
+
return getSmartQueryActions(qk);
|
|
1005
|
+
},
|
|
1006
|
+
invalidate(...args) {
|
|
1007
|
+
const qk = config.queryKey(...args);
|
|
1008
|
+
return invalidateSmartCache(qk);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/index.ts
|
|
1014
|
+
init_requestLock_service();
|
|
1015
|
+
|
|
1016
|
+
export { batchUpdate, clearAllSmartCache, createTypedQuery, emptyList, fromArray, getSmartQueryActions, invalidateSmartCache, isDataEqual, isNormalizedEmpty, normalizedAdd, normalizedRemove, normalizedUpdate, smartCompare, smartQueryDebug, toArray, trimNormalizedList, useInfiniteSmartQuery, useSmartMutation, useSmartQuery, useSmartQuerySelector };
|
|
1017
|
+
//# sourceMappingURL=index.mjs.map
|
|
1018
|
+
//# sourceMappingURL=index.mjs.map
|