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/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