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.d.ts
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { QueryKey, QueryFunction, UseQueryOptions } from '@tanstack/react-query';
|
|
2
|
+
import { a as AnyItem, G as GetItemId, S as SortComparator, b as GetItemVersion, c as SmartQueryError, M as MutationType, P as PaginationMode, C as CacheEntry, N as NormalizedList, U as UnifiedNormalizedInfiniteData, Q as QueuedMutation, O as ObserverFn } from './types-XXiTKLnh.js';
|
|
3
|
+
export { A as AsyncStorage, I as InfinitePagedData, d as ObservabilityEvent } from './types-XXiTKLnh.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* src/utils/smartCompare.ts
|
|
7
|
+
*
|
|
8
|
+
* Hybrid 5-tier comparison — short-circuits as early as possible.
|
|
9
|
+
*
|
|
10
|
+
* Tier 1 reference equality O(1)
|
|
11
|
+
* Tier 2 type / length O(1)
|
|
12
|
+
* Tier 3 id-set fingerprint O(n) — detects add / remove / reorder
|
|
13
|
+
* Tier 4 version XOR fingerprint O(n) — detects field updates
|
|
14
|
+
* Tier 5 fast-deep-equal fallback O(n*f) — schema-less safety net
|
|
15
|
+
*/
|
|
16
|
+
interface SmartCompareOptions {
|
|
17
|
+
idField?: string;
|
|
18
|
+
versionField?: string | null;
|
|
19
|
+
}
|
|
20
|
+
interface CompareResult {
|
|
21
|
+
isEqual: boolean;
|
|
22
|
+
tier: 1 | 2 | 3 | 4 | 5;
|
|
23
|
+
}
|
|
24
|
+
declare function smartCompare(oldData: unknown, newData: unknown, options?: SmartCompareOptions): CompareResult;
|
|
25
|
+
declare function isDataEqual(oldData: unknown, newData: unknown, options?: SmartCompareOptions): boolean;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* src/hooks/useSmartQuery.ts
|
|
29
|
+
*
|
|
30
|
+
* Core data-fetching hook — V2.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
interface SmartQueryOptions<TRaw, TData, TItem extends AnyItem = TData extends AnyItem[] ? TData[number] : AnyItem> {
|
|
34
|
+
queryKey: QueryKey;
|
|
35
|
+
/**
|
|
36
|
+
* Fetches raw data from the network.
|
|
37
|
+
* Automatically wrapped in fetchWithLock — concurrent calls are deduplicated.
|
|
38
|
+
*/
|
|
39
|
+
queryFn: QueryFunction<TRaw>;
|
|
40
|
+
/**
|
|
41
|
+
* Transform the raw API response before caching and exposing to the component.
|
|
42
|
+
*/
|
|
43
|
+
select?: (raw: TRaw) => TData;
|
|
44
|
+
/**
|
|
45
|
+
* Extract a stable string id from a list item.
|
|
46
|
+
*/
|
|
47
|
+
getItemId?: TData extends AnyItem[] ? GetItemId<TItem> : never;
|
|
48
|
+
/**
|
|
49
|
+
* Sort comparator for list data.
|
|
50
|
+
*/
|
|
51
|
+
sortComparator?: TData extends AnyItem[] ? SortComparator<TItem> : never;
|
|
52
|
+
/** Cache TTL in ms. Default: 5 minutes. */
|
|
53
|
+
cacheTtl?: number;
|
|
54
|
+
/** Maximum items to keep in the normalized list. Default: 1000 */
|
|
55
|
+
maxItems?: number;
|
|
56
|
+
/** Optional. Extracts version/timestamp for conflict resolution. */
|
|
57
|
+
getItemVersion?: TData extends AnyItem[] ? GetItemVersion<TItem> : never;
|
|
58
|
+
/**
|
|
59
|
+
* When true, stale cache is never served — waits for a fresh fetch.
|
|
60
|
+
*/
|
|
61
|
+
strictFreshness?: boolean;
|
|
62
|
+
/** Data to show when there is no cache and the fetch fails */
|
|
63
|
+
fallbackData?: TData;
|
|
64
|
+
/** Smart diff configuration */
|
|
65
|
+
compareOptions?: SmartCompareOptions;
|
|
66
|
+
/** Called when a fresh fetch succeeds */
|
|
67
|
+
onSuccess?: (data: TData) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Called when a fetch fails.
|
|
70
|
+
*/
|
|
71
|
+
onError?: (error: SmartQueryError) => boolean | void;
|
|
72
|
+
/** Extra TanStack Query options */
|
|
73
|
+
queryOptions?: Omit<UseQueryOptions<TRaw>, "queryKey" | "queryFn" | "initialData" | "enabled" | "select">;
|
|
74
|
+
}
|
|
75
|
+
interface SmartQueryResult<TData, TItem extends AnyItem = TData extends AnyItem[] ? TData[number] : AnyItem> {
|
|
76
|
+
data: TData | undefined;
|
|
77
|
+
isLoading: boolean;
|
|
78
|
+
isFetching: boolean;
|
|
79
|
+
isFromCache: boolean;
|
|
80
|
+
isCacheLoading: boolean;
|
|
81
|
+
error: SmartQueryError | null;
|
|
82
|
+
refetch: () => void;
|
|
83
|
+
addItem: TData extends AnyItem[] ? (item: TItem) => void : never;
|
|
84
|
+
updateItem: TData extends AnyItem[] ? (item: TItem) => void : never;
|
|
85
|
+
removeItem: TData extends AnyItem[] ? (id: string) => void : never;
|
|
86
|
+
}
|
|
87
|
+
declare function useSmartQuery<TRaw, TData, TItem extends AnyItem = TData extends AnyItem[] ? TData[number] : AnyItem>(options: SmartQueryOptions<TRaw, TData, TItem>): SmartQueryResult<TData, TItem>;
|
|
88
|
+
declare const invalidateSmartCache: (queryKey: QueryKey) => Promise<void>;
|
|
89
|
+
declare const clearAllSmartCache: () => Promise<void>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* src/hooks/useSmartMutation.ts
|
|
93
|
+
*
|
|
94
|
+
* First-class write hook — the companion to useSmartQuery.
|
|
95
|
+
*
|
|
96
|
+
* Handles the full mutation lifecycle:
|
|
97
|
+
* 1. Optimistic update via getSmartQueryActions (instant UI)
|
|
98
|
+
* 2. API call via mutationFn
|
|
99
|
+
* 3. On success: optionally update the optimistic item with the server response
|
|
100
|
+
* 4. On error: automatic rollback of the optimistic update
|
|
101
|
+
* 5. Offline: enqueue to the persistent mutation queue
|
|
102
|
+
*
|
|
103
|
+
* Usage:
|
|
104
|
+
* const { mutate, mutateAsync, isLoading, error } = useSmartMutation<Expense>({
|
|
105
|
+
* queryKey: ["expenses", tripId],
|
|
106
|
+
* mutationType: "ADD_ITEM",
|
|
107
|
+
* mutationFn: (expense) => api.post("/expenses", expense),
|
|
108
|
+
* getItemId: (e) => e.id,
|
|
109
|
+
* });
|
|
110
|
+
*
|
|
111
|
+
* mutate(newExpense); // optimistic + API + queue if offline
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
interface SmartMutationOptions<TItem extends AnyItem, TResponse = TItem> {
|
|
115
|
+
/** The query key this mutation targets — must match the useSmartQuery key */
|
|
116
|
+
queryKey: readonly unknown[];
|
|
117
|
+
/** "ADD_ITEM" | "UPDATE_ITEM" | "REMOVE_ITEM" | custom string */
|
|
118
|
+
mutationType: MutationType | string;
|
|
119
|
+
/**
|
|
120
|
+
* The API call. Receives the item and returns the server-confirmed version.
|
|
121
|
+
* If the app is offline, this is skipped and the mutation is queued.
|
|
122
|
+
*/
|
|
123
|
+
mutationFn: (item: TItem) => Promise<TResponse>;
|
|
124
|
+
/**
|
|
125
|
+
* Extract the stable id from an item.
|
|
126
|
+
* Used to roll back or replace the optimistic item with the server response.
|
|
127
|
+
*/
|
|
128
|
+
getItemId: GetItemId<TItem>;
|
|
129
|
+
/**
|
|
130
|
+
* Map the server response back to a TItem for the final cache update.
|
|
131
|
+
* If omitted, the server response is used as-is (assumes TResponse = TItem).
|
|
132
|
+
*/
|
|
133
|
+
toItem?: (response: TResponse) => TItem;
|
|
134
|
+
/**
|
|
135
|
+
* Whether to enqueue the mutation when offline.
|
|
136
|
+
* Default: true.
|
|
137
|
+
*/
|
|
138
|
+
enableOfflineQueue?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Optional entity key for queue coalescing.
|
|
141
|
+
* Example: (item) => `expense:${item.id}`
|
|
142
|
+
*/
|
|
143
|
+
getEntityKey?: (item: TItem) => string;
|
|
144
|
+
/** Called after the API call succeeds */
|
|
145
|
+
onSuccess?: (response: TResponse, item: TItem) => void;
|
|
146
|
+
/** Called when the API call fails (after rollback) */
|
|
147
|
+
onError?: (error: SmartQueryError, item: TItem) => void;
|
|
148
|
+
/** Check network status — defaults to navigator.onLine on web, true on native */
|
|
149
|
+
isOnline?: () => boolean;
|
|
150
|
+
}
|
|
151
|
+
interface SmartMutationResult<TItem extends AnyItem> {
|
|
152
|
+
/** Fire-and-forget mutation */
|
|
153
|
+
mutate(item: TItem): void;
|
|
154
|
+
/** Async mutation — resolves after the API call completes */
|
|
155
|
+
mutateAsync(item: TItem): Promise<void>;
|
|
156
|
+
isPending: boolean;
|
|
157
|
+
error: SmartQueryError | null;
|
|
158
|
+
reset(): void;
|
|
159
|
+
}
|
|
160
|
+
declare function useSmartMutation<TItem extends AnyItem, TResponse = TItem>(options: SmartMutationOptions<TItem, TResponse>): SmartMutationResult<TItem>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* src/hooks/useInfiniteSmartQuery.ts
|
|
164
|
+
*
|
|
165
|
+
* Cursor-based infinite scroll hook.
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
interface InfiniteSmartQueryOptions<TRaw, TItem extends AnyItem> {
|
|
169
|
+
queryKey: readonly unknown[];
|
|
170
|
+
/**
|
|
171
|
+
* Fetches one page. Receives `pageParam` (the cursor).
|
|
172
|
+
* First call receives `initialPageParam`.
|
|
173
|
+
*/
|
|
174
|
+
queryFn: (ctx: {
|
|
175
|
+
pageParam: unknown;
|
|
176
|
+
}) => Promise<TRaw>;
|
|
177
|
+
/**
|
|
178
|
+
* Extract the cursor for the NEXT page from the raw page response.
|
|
179
|
+
* Return null when there are no more pages.
|
|
180
|
+
*/
|
|
181
|
+
getNextCursor: (raw: TRaw) => unknown | null;
|
|
182
|
+
/**
|
|
183
|
+
* Transform raw page response → TItem[].
|
|
184
|
+
* Use to unwrap response envelopes.
|
|
185
|
+
*/
|
|
186
|
+
select: (raw: TRaw) => TItem[];
|
|
187
|
+
/** Extract stable string id from an item */
|
|
188
|
+
getItemId: GetItemId<TItem>;
|
|
189
|
+
/** Sort order within each page (and across pages) */
|
|
190
|
+
sortComparator?: SortComparator<TItem>;
|
|
191
|
+
/** Cursor to use for the first page. Default: undefined */
|
|
192
|
+
initialPageParam?: unknown;
|
|
193
|
+
/**
|
|
194
|
+
* "normalized" (default) -> data: TItem[]
|
|
195
|
+
* "pages" -> data: { pages: TItem[][] }
|
|
196
|
+
*/
|
|
197
|
+
paginationMode?: PaginationMode;
|
|
198
|
+
/** Optional. Inferred from first page if not provided. */
|
|
199
|
+
pageSize?: number;
|
|
200
|
+
/** Maximum items to keep in the unified normalized list. Default: 1000 */
|
|
201
|
+
maxItems?: number;
|
|
202
|
+
/** Optional. Extracts version/timestamp for conflict resolution. */
|
|
203
|
+
getItemVersion?: GetItemVersion<TItem>;
|
|
204
|
+
cacheTtl?: number;
|
|
205
|
+
strictFreshness?: boolean;
|
|
206
|
+
onError?: (error: SmartQueryError) => void;
|
|
207
|
+
}
|
|
208
|
+
interface InfiniteSmartQueryResult<TItem extends AnyItem> {
|
|
209
|
+
/**
|
|
210
|
+
* If paginationMode: "normalized", data is TItem[]
|
|
211
|
+
* If paginationMode: "pages", data is { pages: TItem[][] }
|
|
212
|
+
*/
|
|
213
|
+
data: TItem[] | {
|
|
214
|
+
pages: TItem[][];
|
|
215
|
+
};
|
|
216
|
+
isLoading: boolean;
|
|
217
|
+
isFetchingNextPage: boolean;
|
|
218
|
+
isFetching: boolean;
|
|
219
|
+
isRefreshing: boolean;
|
|
220
|
+
hasNextPage: boolean;
|
|
221
|
+
error: SmartQueryError | null;
|
|
222
|
+
totalCount: number;
|
|
223
|
+
fetchNextPage(): void;
|
|
224
|
+
refetch(): void;
|
|
225
|
+
addItem(item: TItem): void;
|
|
226
|
+
updateItem(item: TItem): void;
|
|
227
|
+
removeItem(id: string): void;
|
|
228
|
+
}
|
|
229
|
+
declare function useInfiniteSmartQuery<TRaw, TItem extends AnyItem>(options: InfiniteSmartQueryOptions<TRaw, TItem>): InfiniteSmartQueryResult<TItem>;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* src/hooks/useSmartQuerySelector.ts
|
|
233
|
+
*
|
|
234
|
+
* Derived data selector — subscribe to a slice of a cached list.
|
|
235
|
+
*
|
|
236
|
+
* Problem:
|
|
237
|
+
* When an expense list of 500 items has one item updated, every component
|
|
238
|
+
* calling useSmartQuery rerenders. For a row component this is O(n) renders.
|
|
239
|
+
*
|
|
240
|
+
* Solution:
|
|
241
|
+
* useSmartQuerySelector subscribes to the TanStack Query cache for a specific
|
|
242
|
+
* queryKey and applies a selector function. The component only rerenders
|
|
243
|
+
* when the SELECTOR OUTPUT changes (checked with fast-deep-equal).
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* // Only rerenders when THIS expense changes
|
|
247
|
+
* const expense = useSmartQuerySelector(
|
|
248
|
+
* ["expenses", tripId],
|
|
249
|
+
* (expenses: Expense[]) => expenses.find((e) => e.id === expenseId)
|
|
250
|
+
* );
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* // Total amount — only rerenders when the total changes
|
|
254
|
+
* const total = useSmartQuerySelector(
|
|
255
|
+
* ["expenses", tripId],
|
|
256
|
+
* (expenses: Expense[]) => expenses.reduce((s, e) => s + e.amount, 0)
|
|
257
|
+
* );
|
|
258
|
+
*/
|
|
259
|
+
/**
|
|
260
|
+
* Subscribe to a derived slice of a TanStack Query cache entry.
|
|
261
|
+
*
|
|
262
|
+
* @param queryKey Must match a useSmartQuery / useInfiniteSmartQuery key.
|
|
263
|
+
* @param selector Pure function — receives the cached data, returns derived value.
|
|
264
|
+
* @param equalityFn Override the equality check. Defaults to fast-deep-equal.
|
|
265
|
+
*/
|
|
266
|
+
declare function useSmartQuerySelector<TData, TSelected>(queryKey: readonly unknown[], selector: (data: TData | undefined) => TSelected, equalityFn?: (a: TSelected, b: TSelected) => boolean): TSelected;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* src/services/cache.service.ts
|
|
270
|
+
*
|
|
271
|
+
* Versioned, LRU-aware cache layer.
|
|
272
|
+
*
|
|
273
|
+
* Features:
|
|
274
|
+
* • Schema versioning — auto-invalidates stale entries on version bump
|
|
275
|
+
* • lastAccessedAt tracking — enables LRU eviction
|
|
276
|
+
* • Configurable max entries per prefix — prevents unbounded growth
|
|
277
|
+
* • Partial hydration — read a subset of a NormalizedList by ids
|
|
278
|
+
* • Observability events on hit / miss / write / quota exceeded
|
|
279
|
+
*/
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Bump when CacheEntry shape or NormalizedList schema changes in a
|
|
283
|
+
* breaking way. Any stored entry with a lower version is silently discarded.
|
|
284
|
+
*/
|
|
285
|
+
declare const CURRENT_CACHE_VERSION = 2;
|
|
286
|
+
/** Override the global max entries limit (call before any reads/writes) */
|
|
287
|
+
declare function setMaxCacheEntries(n: number): void;
|
|
288
|
+
declare function cacheKeyFor(queryKey: readonly unknown[]): string;
|
|
289
|
+
declare function readCache<T>(key: string, queryKey?: readonly unknown[]): Promise<CacheEntry<T> | null>;
|
|
290
|
+
declare function writeCache<T>(key: string, data: T, queryKey?: readonly unknown[]): Promise<void>;
|
|
291
|
+
declare function deleteCache(key: string): Promise<void>;
|
|
292
|
+
/**
|
|
293
|
+
* Read a subset of a NormalizedList cache entry by item ids.
|
|
294
|
+
*
|
|
295
|
+
* Use for pagination, lazy loading, or detail views that only need
|
|
296
|
+
* a handful of items from a large cached list.
|
|
297
|
+
*
|
|
298
|
+
* @returns null if the cache entry doesn't exist.
|
|
299
|
+
* Empty array if none of the requested ids are cached.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* const items = await getPartialCache<Expense>(
|
|
303
|
+
* cacheKeyFor(["expenses", tripId]),
|
|
304
|
+
* ["exp_1", "exp_2"]
|
|
305
|
+
* );
|
|
306
|
+
*/
|
|
307
|
+
declare function getPartialCache<T extends AnyItem>(key: string, ids: string[]): Promise<T[] | null>;
|
|
308
|
+
declare function isCacheStale(entry: CacheEntry<unknown>, ttlMs: number): boolean;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* src/registry/smartQueryRegistry.ts
|
|
312
|
+
*
|
|
313
|
+
* Global mutation registry.
|
|
314
|
+
* Call addItem / updateItem / removeItem from anywhere — no hook required.
|
|
315
|
+
*/
|
|
316
|
+
|
|
317
|
+
interface SmartQueryActions<TItem extends AnyItem> {
|
|
318
|
+
addItem(item: TItem): Promise<void>;
|
|
319
|
+
updateItem(item: TItem): Promise<void>;
|
|
320
|
+
removeItem(id: string): Promise<void>;
|
|
321
|
+
isActive(): boolean;
|
|
322
|
+
}
|
|
323
|
+
declare function getSmartQueryActions<TItem extends AnyItem>(queryKey: readonly unknown[]): SmartQueryActions<TItem>;
|
|
324
|
+
/**
|
|
325
|
+
* Dev-only debug API. All methods are no-ops in production.
|
|
326
|
+
*/
|
|
327
|
+
declare const smartQueryDebug: {
|
|
328
|
+
/** Get the current normalized state for a query key. */
|
|
329
|
+
getNormalizedState: <T extends AnyItem>(queryKey: readonly unknown[]) => Promise<NormalizedList<T> | UnifiedNormalizedInfiniteData<T> | null>;
|
|
330
|
+
/** Log a detailed summary of the cache entry to the console. */
|
|
331
|
+
inspectCache: (queryKey: readonly unknown[]) => Promise<void>;
|
|
332
|
+
/** Clear the cache entry for a query key. */
|
|
333
|
+
clearCache: (queryKey: readonly unknown[]) => Promise<void>;
|
|
334
|
+
/** Get the current state of the offline mutation queue. */
|
|
335
|
+
getQueue: () => Promise<any[]>;
|
|
336
|
+
/** Get list of all storage keys currently being fetched. */
|
|
337
|
+
inFlightRequests: () => string[];
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* Executes multiple mutations in a single logical batch.
|
|
341
|
+
* If the hook is unmounted, it performs a single storage write.
|
|
342
|
+
*/
|
|
343
|
+
declare function batchUpdate(queryKey: readonly unknown[], fn: (actions: SmartQueryActions<any>) => void | Promise<void>): Promise<void>;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* src/factory/createTypedQuery.ts
|
|
347
|
+
*
|
|
348
|
+
* Type-safe query factory — inspired by tRPC's router pattern.
|
|
349
|
+
*
|
|
350
|
+
* Problem:
|
|
351
|
+
* getSmartQueryActions<Expense>(["expenses", tripId]) — the Expense type
|
|
352
|
+
* is not connected to the queryKey. You can pass the wrong type silently.
|
|
353
|
+
* Every call site must repeat the type annotation and queryFn.
|
|
354
|
+
*
|
|
355
|
+
* Solution:
|
|
356
|
+
* Define the query once at module level (queryKey shape + queryFn + config).
|
|
357
|
+
* Call sites get pre-typed hooks and actions with full inference.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* // Define once (e.g. src/queries/expense.query.ts)
|
|
361
|
+
* export const expenseQuery = createTypedQuery({
|
|
362
|
+
* queryKey: (tripId: string) => ["expenses", tripId] as const,
|
|
363
|
+
* queryFn: (tripId: string) =>
|
|
364
|
+
* api.get(`/trips/${tripId}/expenses`).then((r) => r.data as Expense[]),
|
|
365
|
+
* getItemId: (e: Expense) => e.id,
|
|
366
|
+
* sortComparator: (a: Expense, b: Expense) => b.createdAt - a.createdAt,
|
|
367
|
+
* cacheTtl: 5 * 60_000,
|
|
368
|
+
* });
|
|
369
|
+
*
|
|
370
|
+
* // Use anywhere — fully typed, no annotation needed
|
|
371
|
+
* const { data, addItem } = expenseQuery.useQuery(tripId);
|
|
372
|
+
* const actions = expenseQuery.getActions(tripId);
|
|
373
|
+
* await actions.addItem(newExpense); // TItem = Expense — inferred!
|
|
374
|
+
*/
|
|
375
|
+
|
|
376
|
+
type AnyArgs = readonly unknown[];
|
|
377
|
+
interface TypedQueryDefinition<TArgs extends AnyArgs, TRaw, TData, TItem extends AnyItem = TData extends AnyItem[] ? TData[number] : AnyItem> {
|
|
378
|
+
/**
|
|
379
|
+
* Pre-typed useSmartQuery hook.
|
|
380
|
+
* Pass the same args you defined in `queryKey(args)`.
|
|
381
|
+
*/
|
|
382
|
+
useQuery(...args: TArgs): SmartQueryResult<TData, TItem>;
|
|
383
|
+
/**
|
|
384
|
+
* Pre-typed useSmartMutation hook.
|
|
385
|
+
* Requires only the mutation-specific options (queryKey is pre-filled).
|
|
386
|
+
*/
|
|
387
|
+
useMutation(...args: [...TArgs, Omit<SmartMutationOptions<TItem>, "queryKey" | "getItemId">]): SmartMutationResult<TItem>;
|
|
388
|
+
/**
|
|
389
|
+
* Get pre-typed global actions for this query key.
|
|
390
|
+
* Call from anywhere — no React required.
|
|
391
|
+
*/
|
|
392
|
+
getActions(...args: TArgs): SmartQueryActions<TItem>;
|
|
393
|
+
/** Invalidate this query's cache entry */
|
|
394
|
+
invalidate(...args: TArgs): Promise<void>;
|
|
395
|
+
}
|
|
396
|
+
interface CreateTypedQueryConfig<TArgs extends AnyArgs, TRaw, TData, TItem extends AnyItem = TData extends AnyItem[] ? TData[number] : AnyItem> {
|
|
397
|
+
/** Build the query key array from args */
|
|
398
|
+
queryKey(...args: TArgs): readonly unknown[];
|
|
399
|
+
/** Async function that fetches data — receives the same args */
|
|
400
|
+
queryFn(...args: TArgs): Promise<TRaw>;
|
|
401
|
+
/** Transform raw response (optional) */
|
|
402
|
+
select?: (raw: TRaw) => TData;
|
|
403
|
+
/** Required for list queries */
|
|
404
|
+
getItemId?: TData extends AnyItem[] ? GetItemId<TItem> : never;
|
|
405
|
+
/** Required for list queries */
|
|
406
|
+
sortComparator?: TData extends AnyItem[] ? SortComparator<TItem> : never;
|
|
407
|
+
cacheTtl?: number;
|
|
408
|
+
strictFreshness?: boolean;
|
|
409
|
+
/** Extra SmartQueryOptions applied to every call */
|
|
410
|
+
defaultOptions?: Partial<SmartQueryOptions<TRaw, TData, TItem>>;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Create a type-safe query definition.
|
|
414
|
+
* Returns a typed query object with useQuery, useMutation, getActions, invalidate.
|
|
415
|
+
*/
|
|
416
|
+
declare function createTypedQuery<TArgs extends AnyArgs, TRaw, TData, TItem extends AnyItem = TData extends AnyItem[] ? TData[number] : AnyItem>(config: CreateTypedQueryConfig<TArgs, TRaw, TData, TItem>): TypedQueryDefinition<TArgs, TRaw, TData, TItem>;
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* src/services/queue.service.ts
|
|
420
|
+
*
|
|
421
|
+
* Offline Mutation Queue — persist-first, retry-on-reconnect, with coalescing.
|
|
422
|
+
*
|
|
423
|
+
* Key design decisions:
|
|
424
|
+
* • Sequential FIFO processing preserves causal ordering
|
|
425
|
+
* • Coalescing: two mutations with the same entityKey are merged before send
|
|
426
|
+
* (prevents stale optimistic updates from racing each other)
|
|
427
|
+
* • Exponential backoff with full jitter — no thundering herd on reconnect
|
|
428
|
+
* • Observability events on enqueue / success / failure / drain
|
|
429
|
+
* • clearQueue() on logout prevents cross-user mutation leakage
|
|
430
|
+
*/
|
|
431
|
+
|
|
432
|
+
type Executor<P = unknown> = (mutation: QueuedMutation<P>) => Promise<void>;
|
|
433
|
+
/**
|
|
434
|
+
* Register an executor for a mutation type.
|
|
435
|
+
* Must be called at app startup before mutations are enqueued.
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* registerExecutor("ADD_ITEM", async (m) => {
|
|
439
|
+
* await api.post("/expenses", m.payload);
|
|
440
|
+
* });
|
|
441
|
+
*/
|
|
442
|
+
declare function registerExecutor<TPayload>(type: string, fn: Executor<TPayload>): void;
|
|
443
|
+
declare function loadQueue(): Promise<QueuedMutation[]>;
|
|
444
|
+
/**
|
|
445
|
+
* Process all pending mutations in FIFO order.
|
|
446
|
+
* Coalesces before sending to minimize network calls.
|
|
447
|
+
* Safe to call concurrently — guarded by isProcessing flag.
|
|
448
|
+
*/
|
|
449
|
+
declare function processQueue(): Promise<void>;
|
|
450
|
+
/**
|
|
451
|
+
* Add a mutation to the persistent queue.
|
|
452
|
+
*
|
|
453
|
+
* @param entityKey Optional logical key for coalescing (e.g. "expense:exp_123").
|
|
454
|
+
* Mutations with the same entityKey are merged before sending.
|
|
455
|
+
*/
|
|
456
|
+
declare function enqueueMutation<TPayload>(options: {
|
|
457
|
+
type: MutationType | string;
|
|
458
|
+
queryKey: readonly unknown[];
|
|
459
|
+
payload: TPayload;
|
|
460
|
+
entityKey?: string;
|
|
461
|
+
maxRetries?: number;
|
|
462
|
+
}): Promise<void>;
|
|
463
|
+
/** Process queue on app startup */
|
|
464
|
+
declare function initQueue(): Promise<void>;
|
|
465
|
+
/** Clear all pending mutations — call on logout */
|
|
466
|
+
declare function clearQueue(): Promise<void>;
|
|
467
|
+
declare const getQueue: typeof loadQueue;
|
|
468
|
+
declare const getQueueLength: () => Promise<number>;
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* src/services/observer.service.ts
|
|
472
|
+
*
|
|
473
|
+
* Pluggable observability — emit structured events to any analytics backend.
|
|
474
|
+
*
|
|
475
|
+
* Zero coupling: the library emits; you decide where it goes.
|
|
476
|
+
* Attach observers at app startup; they receive every internal event.
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* // Sentry breadcrumbs
|
|
480
|
+
* addObserver((event) => {
|
|
481
|
+
* if (event.type === "fetch_error") {
|
|
482
|
+
* Sentry.addBreadcrumb({ message: event.type, data: event });
|
|
483
|
+
* }
|
|
484
|
+
* });
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* // Datadog / Mixpanel
|
|
488
|
+
* addObserver((event) => {
|
|
489
|
+
* analytics.track(event.type, event);
|
|
490
|
+
* });
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* // Simple console logger in dev
|
|
494
|
+
* if (__DEV__) addObserver(console.log);
|
|
495
|
+
*/
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Register an observer. Returns an unsubscribe function.
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* const unsub = addObserver(myLogger);
|
|
502
|
+
* // Later:
|
|
503
|
+
* unsub();
|
|
504
|
+
*/
|
|
505
|
+
declare function addObserver(fn: ObserverFn): () => void;
|
|
506
|
+
/** Remove a specific observer */
|
|
507
|
+
declare function removeObserver(fn: ObserverFn): void;
|
|
508
|
+
/** Remove all observers */
|
|
509
|
+
declare function clearObservers(): void;
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* src/utils/normalize.ts
|
|
513
|
+
*
|
|
514
|
+
* NormalizedList — O(1) lookup, O(log n) sorted mutations.
|
|
515
|
+
*
|
|
516
|
+
* The `getItemId` function is passed explicitly — no hardcoded field name.
|
|
517
|
+
* This lets you use `_id`, `uuid`, numeric ids, or composite keys.
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
declare function emptyList<T extends AnyItem>(): NormalizedList<T>;
|
|
521
|
+
declare function fromArray<T extends AnyItem>(items: T[], getItemId: GetItemId<T>, comparator?: SortComparator<T>): NormalizedList<T>;
|
|
522
|
+
declare function toArray<T extends AnyItem>(list: NormalizedList<T>): T[];
|
|
523
|
+
declare function normalizedAdd<T extends AnyItem>(list: NormalizedList<T>, item: T, getItemId: GetItemId<T>, comparator: SortComparator<T>, getItemVersion?: GetItemVersion<T>): NormalizedList<T>;
|
|
524
|
+
declare function normalizedUpdate<T extends AnyItem>(list: NormalizedList<T>, item: T, getItemId: GetItemId<T>, comparator: SortComparator<T>, getItemVersion?: GetItemVersion<T>): NormalizedList<T>;
|
|
525
|
+
declare function normalizedRemove<T extends AnyItem>(list: NormalizedList<T>, id: string): NormalizedList<T>;
|
|
526
|
+
declare function isNormalizedEmpty<T extends AnyItem>(list: NormalizedList<T>): boolean;
|
|
527
|
+
/**
|
|
528
|
+
* Soft Trim list to maxItems.
|
|
529
|
+
* Instead of a hard cut every time, we remove 20% of the oldest items
|
|
530
|
+
* to avoid constant array slicing on every single insert.
|
|
531
|
+
* Assumes list is already sorted.
|
|
532
|
+
*/
|
|
533
|
+
declare function trimNormalizedList<T extends AnyItem>(list: NormalizedList<T>, maxItems: number): NormalizedList<T>;
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* src/services/requestLock.service.ts
|
|
537
|
+
*
|
|
538
|
+
* In-flight request deduplication.
|
|
539
|
+
* Concurrent calls with the same key share one Promise — only one fetch fires.
|
|
540
|
+
*/
|
|
541
|
+
declare function fetchWithLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
542
|
+
declare const inFlightCount: () => number;
|
|
543
|
+
declare const inFlightKeys: () => string[];
|
|
544
|
+
|
|
545
|
+
export { AnyItem, CURRENT_CACHE_VERSION, CacheEntry, type CompareResult, type CreateTypedQueryConfig, GetItemId, GetItemVersion, type InfiniteSmartQueryOptions, type InfiniteSmartQueryResult, MutationType, NormalizedList, ObserverFn, QueuedMutation, type SmartCompareOptions, type SmartMutationOptions, type SmartMutationResult, type SmartQueryActions, SmartQueryError, type SmartQueryOptions, type SmartQueryResult, SortComparator, type TypedQueryDefinition, addObserver, batchUpdate, cacheKeyFor, clearAllSmartCache, clearObservers, clearQueue, createTypedQuery, deleteCache, emptyList, enqueueMutation, fetchWithLock, fromArray, getPartialCache, getQueue, getQueueLength, getSmartQueryActions, inFlightCount, inFlightKeys, initQueue, invalidateSmartCache, isCacheStale, isDataEqual, isNormalizedEmpty, normalizedAdd, normalizedRemove, normalizedUpdate, processQueue, readCache, registerExecutor, removeObserver, setMaxCacheEntries, smartCompare, smartQueryDebug, toArray, trimNormalizedList, useInfiniteSmartQuery, useSmartMutation, useSmartQuery, useSmartQuerySelector, writeCache };
|