tanstack-cacher 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,518 @@
1
+ # React Query Cache Manager
2
+
3
+ [![npm version](https://img.shields.io/npm/v/tanstack-cacher.svg)](https://www.npmjs.com/package/tanstack-cacher)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
6
+
7
+ ## Introduction
8
+
9
+ A robust, TypeScript-first cache manager for React Query that works with **ANY response structure**. Using simple path-based configuration, it handles simple arrays, deeply nested data, paginated responses, and any custom format you throw at it.
10
+
11
+ **Key Features:**
12
+ - Path-based configuration (e.g., `itemsPath: "data.content"`)
13
+ - Automatic handling of missing data and nested structures
14
+ - Built-in pagination metadata management
15
+ - Works with any response shape
16
+ - Full TypeScript support with type inference
17
+ - Zero dependencies (except React Query)
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @your-scope/tanstack-cacher
23
+ # or
24
+ yarn add @your-scope/tanstack-cacher
25
+ # or
26
+ pnpm add @your-scope/tanstack-cacher
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ### 1. Simple Array Response
32
+
33
+ ```typescript
34
+ import { QueryCacheManager } from '@your-scope/tanstack-cacher';
35
+ import { useQueryClient, useMutation } from '@tanstack/react-query';
36
+
37
+ interface Todo {
38
+ id: string;
39
+ title: string;
40
+ }
41
+
42
+ // API returns: Todo[]
43
+ function TodoList() {
44
+ const queryClient = useQueryClient();
45
+
46
+ const cache = new QueryCacheManager<Todo[], Todo>({
47
+ queryClient,
48
+ queryKey: ['todos'],
49
+ itemsPath: '', // Empty string = data IS the array
50
+ });
51
+
52
+ const createMutation = useMutation({
53
+ mutationFn: createTodo,
54
+ onMutate: (newTodo) => cache.add(newTodo),
55
+ onError: () => cache.invalidate(),
56
+ });
57
+
58
+ const updateMutation = useMutation({
59
+ mutationFn: updateTodo,
60
+ onMutate: (todo) => cache.update(todo),
61
+ onError: () => cache.invalidate(),
62
+ });
63
+
64
+ const deleteMutation = useMutation({
65
+ mutationFn: deleteTodo,
66
+ onMutate: (id) => cache.delete(id),
67
+ onError: () => cache.invalidate(),
68
+ });
69
+ }
70
+ ```
71
+
72
+ ### 2. Nested Response
73
+
74
+ ```typescript
75
+ interface ApiResponse {
76
+ success: true;
77
+ data: {
78
+ items: User[];
79
+ };
80
+ }
81
+
82
+ const cache = new QueryCacheManager<ApiResponse, User>({
83
+ queryClient,
84
+ queryKey: ['users'],
85
+ itemsPath: 'data.items', // Path to the array
86
+ });
87
+ ```
88
+
89
+ ### 3. Paginated Response (Automatic Metadata Updates!)
90
+
91
+ ```typescript
92
+ interface PaginatedResponse {
93
+ content: User[];
94
+ page: {
95
+ number: number;
96
+ size: number;
97
+ totalElements: number;
98
+ totalPages: number;
99
+ };
100
+ }
101
+
102
+ const cache = new QueryCacheManager<PaginatedResponse, User>({
103
+ queryClient,
104
+ queryKey: ['users', { page: 0 }],
105
+ itemsPath: 'content', // Path to items
106
+ pagination: {
107
+ totalElementsPath: 'page.totalElements', // Auto-updates on add/delete
108
+ totalPagesPath: 'page.totalPages', // Auto-recalculated on add/delete
109
+ currentPagePath: 'page.number',
110
+ pageSizePath: 'page.size', // Required for totalPages calculation
111
+ },
112
+ });
113
+
114
+ // Now when you add/delete, totalElements AND totalPages update automatically!
115
+ cache.add(newUser); // totalElements++, totalPages recalculated
116
+ cache.delete(userId); // totalElements--, totalPages recalculated
117
+ ```
118
+
119
+ ### 4. Different Pagination Format
120
+
121
+ ```typescript
122
+ interface Response {
123
+ items: Product[];
124
+ meta: {
125
+ total: number;
126
+ currentPage: number;
127
+ totalPages: number;
128
+ };
129
+ }
130
+
131
+ const cache = new QueryCacheManager<Response, Product>({
132
+ queryClient,
133
+ queryKey: ['products'],
134
+ itemsPath: 'items',
135
+ pagination: {
136
+ totalElementsPath: 'meta.total',
137
+ totalPagesPath: 'meta.totalPages',
138
+ currentPagePath: 'meta.currentPage',
139
+ pageSizePath: 'meta.pageSize', // For auto totalPages calculation
140
+ },
141
+ });
142
+ ```
143
+
144
+ ### 5. Deeply Nested Response
145
+
146
+ ```typescript
147
+ interface ComplexResponse {
148
+ status: 'ok';
149
+ result: {
150
+ data: {
151
+ users: User[];
152
+ };
153
+ metadata: {
154
+ count: number;
155
+ };
156
+ };
157
+ }
158
+
159
+ const cache = new QueryCacheManager<ComplexResponse, User>({
160
+ queryClient,
161
+ queryKey: ['users'],
162
+ itemsPath: 'result.data.users', // Navigate with dots
163
+ pagination: {
164
+ totalElementsPath: 'result.metadata.count',
165
+ },
166
+ });
167
+ ```
168
+
169
+ ## API Reference
170
+
171
+ ### Constructor
172
+
173
+ ```typescript
174
+ new QueryCacheManager<TData, TItem>(config)
175
+ ```
176
+
177
+ **Config Options:**
178
+
179
+ | Option | Type | Required | Description |
180
+ |--------|------|----------|-------------|
181
+ | `queryClient` | `QueryClient` | Yes | React Query client instance |
182
+ | `queryKey` | `QueryKey` | Yes | Query key for the cache |
183
+ | `itemsPath` | `string` | Yes | Dot-separated path to items array (empty string if data IS array) |
184
+ | `pagination` | `PaginationConfig` | No | Pagination metadata paths |
185
+ | `keyExtractor` | `(item: TItem) => string \| number` | No | Extract unique ID (default: `item.id`) |
186
+ | `initialData` | `TData` | No | Initial structure when cache is empty |
187
+
188
+ **PaginationConfig:**
189
+
190
+ | Option | Type | Description |
191
+ |--------|------|-------------|
192
+ | `totalElementsPath` | `string` | Path to total count (auto-updated on add/delete) |
193
+ | `totalPagesPath` | `string` | Path to total pages (auto-recalculated when pageSizePath provided) |
194
+ | `currentPagePath` | `string` | Path to current page number |
195
+ | `pageSizePath` | `string` | Path to page size (required for automatic totalPages recalculation) |
196
+
197
+ ### Methods
198
+
199
+ #### `add(newItem, position?)`
200
+ Add new item to cache
201
+ ```typescript
202
+ cache.add({ id: '1', name: 'John' }); // Adds at start (default)
203
+ cache.add({ id: '2', name: 'Jane' }, 'end'); // Adds at end
204
+ ```
205
+
206
+ #### `update(updatedItem, matcher?)`
207
+ Update existing item
208
+ ```typescript
209
+ // Update by ID (default)
210
+ cache.update({ id: '1', name: 'Johnny' });
211
+
212
+ // Custom matcher
213
+ cache.update(
214
+ { status: 'active' },
215
+ (item) => item.email === 'user@example.com'
216
+ );
217
+ ```
218
+
219
+ #### `delete(itemOrId, matcher?)`
220
+ Remove item from cache
221
+ ```typescript
222
+ // Delete by ID
223
+ cache.delete('1');
224
+
225
+ // Delete by object
226
+ cache.delete({ id: '1', name: 'John' });
227
+
228
+ // Custom matcher
229
+ cache.delete(null, (item) => item.isDeleted);
230
+ ```
231
+
232
+ #### `replace(newData)`
233
+ Replace entire cache data
234
+ ```typescript
235
+ cache.replace(newFullData);
236
+ ```
237
+
238
+ #### `clear()`
239
+ Clear all items (resets array to empty, updates pagination to 0)
240
+ ```typescript
241
+ cache.clear();
242
+ ```
243
+
244
+ #### `getItemsFromCache()`
245
+ Get current items array
246
+ ```typescript
247
+ const items = cache.getItemsFromCache(); // returns TItem[]
248
+ ```
249
+
250
+ #### `getDataFromCache()`
251
+ Get full cache data
252
+ ```typescript
253
+ const fullData = cache.getDataFromCache(); // returns TData | undefined
254
+ ```
255
+
256
+ #### `invalidate()`
257
+ Trigger refetch from server
258
+ ```typescript
259
+ cache.invalidate();
260
+ ```
261
+
262
+ #### `createHandlers()`
263
+ Get handlers for mutation callbacks
264
+ ```typescript
265
+ const handlers = cache.createHandlers();
266
+
267
+ useMutation({
268
+ mutationFn: createUser,
269
+ onMutate: handlers.onAdd,
270
+ onError: () => cache.invalidate(),
271
+ });
272
+ ```
273
+
274
+ ## Real-World Example
275
+
276
+ ```typescript
277
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
278
+ import { QueryCacheManager } from '@your-scope/tanstack-cacher';
279
+
280
+ interface Todo {
281
+ id: string;
282
+ title: string;
283
+ completed: boolean;
284
+ }
285
+
286
+ function TodoApp() {
287
+ const queryClient = useQueryClient();
288
+
289
+ // Setup cache manager
290
+ const todoCache = new QueryCacheManager<Todo[], Todo>({
291
+ queryClient,
292
+ queryKey: ['todos'],
293
+ itemsPath: '', // Data IS the array
294
+ });
295
+
296
+ // Fetch todos
297
+ const { data: todos } = useQuery({
298
+ queryKey: ['todos'],
299
+ queryFn: () => fetch('/api/todos').then(r => r.json()),
300
+ });
301
+
302
+ // Mutations with optimistic updates
303
+ const createMutation = useMutation({
304
+ mutationFn: async (title: string) => {
305
+ const res = await fetch('/api/todos', {
306
+ method: 'POST',
307
+ body: JSON.stringify({ title }),
308
+ });
309
+ return res.json();
310
+ },
311
+ onMutate: (title) => {
312
+ // Optimistically add with temp ID
313
+ todoCache.add({
314
+ id: `temp-${Date.now()}`,
315
+ title,
316
+ completed: false,
317
+ });
318
+ },
319
+ onError: () => todoCache.invalidate(),
320
+ });
321
+
322
+ const toggleMutation = useMutation({
323
+ mutationFn: async (todo: Todo) => {
324
+ const res = await fetch(`/api/todos/${todo.id}`, {
325
+ method: 'PUT',
326
+ body: JSON.stringify({ ...todo, completed: !todo.completed }),
327
+ });
328
+ return res.json();
329
+ },
330
+ onMutate: (todo) => {
331
+ // Optimistically update
332
+ todoCache.update({ id: todo.id, completed: !todo.completed });
333
+ },
334
+ onError: () => todoCache.invalidate(),
335
+ });
336
+
337
+ const deleteMutation = useMutation({
338
+ mutationFn: (id: string) => fetch(`/api/todos/${id}`, { method: 'DELETE' }),
339
+ onMutate: (id) => {
340
+ // Optimistically remove
341
+ todoCache.delete(id);
342
+ },
343
+ onError: () => todoCache.invalidate(),
344
+ });
345
+
346
+ return (
347
+ <div>
348
+ <input
349
+ onKeyDown={(e) => {
350
+ if (e.key === 'Enter') {
351
+ createMutation.mutate(e.currentTarget.value);
352
+ e.currentTarget.value = '';
353
+ }
354
+ }}
355
+ placeholder="Add todo..."
356
+ />
357
+ <ul>
358
+ {todos?.map((todo) => (
359
+ <li key={todo.id}>
360
+ <input
361
+ type="checkbox"
362
+ checked={todo.completed}
363
+ onChange={() => toggleMutation.mutate(todo)}
364
+ />
365
+ <span>{todo.title}</span>
366
+ <button onClick={() => deleteMutation.mutate(todo.id)}>
367
+ Delete
368
+ </button>
369
+ </li>
370
+ ))}
371
+ </ul>
372
+ </div>
373
+ );
374
+ }
375
+ ```
376
+
377
+ ## Next.js Usage
378
+
379
+ Works seamlessly with Next.js App Router:
380
+
381
+ ```typescript
382
+ // app/users/page.tsx
383
+ 'use client';
384
+
385
+ import { QueryCacheManager } from '@your-scope/tanstack-cacher';
386
+
387
+ export default function UsersPage() {
388
+ const queryClient = useQueryClient();
389
+
390
+ const cache = new QueryCacheManager({
391
+ queryClient,
392
+ queryKey: ['users'],
393
+ itemsPath: 'data.users',
394
+ });
395
+
396
+ // ... rest of component
397
+ }
398
+ ```
399
+
400
+ ## Advanced Examples
401
+
402
+ ### Handling Missing Data
403
+
404
+ The manager automatically creates the structure if data is missing:
405
+
406
+ ```typescript
407
+ const cache = new QueryCacheManager<ApiResponse, User>({
408
+ queryClient,
409
+ queryKey: ['users'],
410
+ itemsPath: 'data.content',
411
+ // If cache is empty, this structure is created:
412
+ initialData: {
413
+ data: {
414
+ content: [],
415
+ },
416
+ page: {
417
+ totalElements: 0,
418
+ totalPages: 0,
419
+ },
420
+ },
421
+ });
422
+
423
+ // Even if cache is empty, this works fine!
424
+ cache.add(newUser); // Creates structure + adds item
425
+ ```
426
+
427
+ ### Custom Key Extractor
428
+
429
+ ```typescript
430
+ interface User {
431
+ userId: string; // Not "id"
432
+ name: string;
433
+ }
434
+
435
+ const cache = new QueryCacheManager<User[], User>({
436
+ queryClient,
437
+ queryKey: ['users'],
438
+ itemsPath: '',
439
+ keyExtractor: (user) => user.userId, // Custom ID field
440
+ });
441
+ ```
442
+
443
+ ### Multiple Cache Managers
444
+
445
+ ```typescript
446
+ function App() {
447
+ const queryClient = useQueryClient();
448
+
449
+ // One manager per query
450
+ const usersCache = new QueryCacheManager({
451
+ queryClient,
452
+ queryKey: ['users'],
453
+ itemsPath: 'data.users',
454
+ });
455
+
456
+ const postsCache = new QueryCacheManager({
457
+ queryClient,
458
+ queryKey: ['posts'],
459
+ itemsPath: 'posts',
460
+ });
461
+
462
+ // Use independently
463
+ usersCache.add(newUser);
464
+ postsCache.add(newPost);
465
+ }
466
+ ```
467
+
468
+ ## Best Practices
469
+
470
+ 1. **Always handle errors** - Call `invalidate()` on mutation errors to refetch from server
471
+ 2. **Use temporary IDs** - For optimistic creates, use `temp-${Date.now()}` or similar
472
+ 3. **One manager per query** - Create separate instances for different queries
473
+ 4. **Specify paths clearly** - Use dot notation for nested paths: `"data.result.items"`
474
+ 5. **Type everything** - Provide TypeScript types for full type safety
475
+
476
+ ## Why This Package?
477
+
478
+ Traditional cache managers assume your API structure. This package:
479
+
480
+ - **Works with ANY structure** - Just tell it the path to your data
481
+ - **Handles edge cases** - Missing data, nested objects, pagination metadata
482
+ - **Zero configuration overhead** - No complex setup or boilerplate
483
+ - **Type-safe** - Full TypeScript support with inference
484
+ - **Framework agnostic** - Works anywhere React Query works
485
+
486
+ ## Migration from Old API
487
+
488
+ If you were using the old `getItems`/`setItems` approach:
489
+
490
+ ```typescript
491
+ // Old way ❌
492
+ const cache = new QueryCacheManager({
493
+ getItems: (data) => data.content,
494
+ setItems: (data, items) => ({ ...data, content: items }),
495
+ onItemsAdd: (data, count) => ({
496
+ ...data,
497
+ page: { ...data.page, totalElements: data.page.totalElements + count }
498
+ }),
499
+ });
500
+
501
+ // New way ✅
502
+ const cache = new QueryCacheManager({
503
+ itemsPath: 'content',
504
+ pagination: {
505
+ totalElementsPath: 'page.totalElements',
506
+ },
507
+ });
508
+ ```
509
+
510
+ Much simpler and safer!
511
+
512
+ ## License
513
+
514
+ MIT
515
+
516
+ ## Contributing
517
+
518
+ Issues and PRs welcome!
@@ -0,0 +1,43 @@
1
+ import { QueryClient, QueryKey } from '@tanstack/react-query';
2
+ export * from '@tanstack/react-query';
3
+
4
+ interface PaginationConfig {
5
+ totalElementsPath?: string;
6
+ totalPagesPath?: string;
7
+ currentPagePath?: string;
8
+ pageSizePath?: string;
9
+ }
10
+ interface CacheConfig<TData, TItem> {
11
+ queryClient: QueryClient;
12
+ queryKey: QueryKey;
13
+ itemsPath: string;
14
+ pagination?: PaginationConfig;
15
+ keyExtractor?: (item: TItem) => string | number;
16
+ initialData?: TData;
17
+ }
18
+ interface CacheHandlers<TItem> {
19
+ onAdd: (newItem: TItem, position?: 'start' | 'end') => void;
20
+ onUpdate: (updatedItem: Partial<TItem>, matcher?: (item: TItem) => boolean) => void;
21
+ onDelete: (itemOrId: TItem | string | number, matcher?: (item: TItem) => boolean) => void;
22
+ }
23
+ type InsertPosition = 'start' | 'end';
24
+
25
+ declare class QueryCacheManager<TData, TItem> {
26
+ private config;
27
+ constructor(config: CacheConfig<TData, TItem>);
28
+ private getItems;
29
+ private setItems;
30
+ private updatePaginationOnAdd;
31
+ private updatePaginationOnRemove;
32
+ add(newItem: TItem, position?: InsertPosition): void;
33
+ update(updatedItem: Partial<TItem>, matcher?: (item: TItem) => boolean): void;
34
+ delete(itemOrId: TItem | string | number, matcher?: (item: TItem) => boolean): void;
35
+ replace(newData: TData): void;
36
+ clear(): void;
37
+ getItemsFromCache(): TItem[];
38
+ getDataFromCache(): TData | undefined;
39
+ invalidate(): void;
40
+ createHandlers(): CacheHandlers<TItem>;
41
+ }
42
+
43
+ export { type CacheConfig, type CacheHandlers, type InsertPosition, type PaginationConfig, QueryCacheManager };
@@ -0,0 +1,43 @@
1
+ import { QueryClient, QueryKey } from '@tanstack/react-query';
2
+ export * from '@tanstack/react-query';
3
+
4
+ interface PaginationConfig {
5
+ totalElementsPath?: string;
6
+ totalPagesPath?: string;
7
+ currentPagePath?: string;
8
+ pageSizePath?: string;
9
+ }
10
+ interface CacheConfig<TData, TItem> {
11
+ queryClient: QueryClient;
12
+ queryKey: QueryKey;
13
+ itemsPath: string;
14
+ pagination?: PaginationConfig;
15
+ keyExtractor?: (item: TItem) => string | number;
16
+ initialData?: TData;
17
+ }
18
+ interface CacheHandlers<TItem> {
19
+ onAdd: (newItem: TItem, position?: 'start' | 'end') => void;
20
+ onUpdate: (updatedItem: Partial<TItem>, matcher?: (item: TItem) => boolean) => void;
21
+ onDelete: (itemOrId: TItem | string | number, matcher?: (item: TItem) => boolean) => void;
22
+ }
23
+ type InsertPosition = 'start' | 'end';
24
+
25
+ declare class QueryCacheManager<TData, TItem> {
26
+ private config;
27
+ constructor(config: CacheConfig<TData, TItem>);
28
+ private getItems;
29
+ private setItems;
30
+ private updatePaginationOnAdd;
31
+ private updatePaginationOnRemove;
32
+ add(newItem: TItem, position?: InsertPosition): void;
33
+ update(updatedItem: Partial<TItem>, matcher?: (item: TItem) => boolean): void;
34
+ delete(itemOrId: TItem | string | number, matcher?: (item: TItem) => boolean): void;
35
+ replace(newData: TData): void;
36
+ clear(): void;
37
+ getItemsFromCache(): TItem[];
38
+ getDataFromCache(): TData | undefined;
39
+ invalidate(): void;
40
+ createHandlers(): CacheHandlers<TItem>;
41
+ }
42
+
43
+ export { type CacheConfig, type CacheHandlers, type InsertPosition, type PaginationConfig, QueryCacheManager };
package/dist/index.js ADDED
@@ -0,0 +1,292 @@
1
+ 'use strict';
2
+
3
+ var reactQuery = require('@tanstack/react-query');
4
+
5
+ // src/index.ts
6
+
7
+ // src/managers/QueryCacheManager/QueryCache.utils.ts
8
+ function getAtPath(obj, path, defaultValue) {
9
+ if (!obj || !path) return defaultValue;
10
+ const keys = path.split(".");
11
+ let result = obj;
12
+ for (const key of keys) {
13
+ if (result === null || result === void 0) {
14
+ return defaultValue;
15
+ }
16
+ result = result[key];
17
+ }
18
+ return result !== void 0 ? result : defaultValue;
19
+ }
20
+ function setAtPath(obj, path, value) {
21
+ if (!path) return obj;
22
+ const keys = path.split(".");
23
+ const root = obj ? { ...obj } : {};
24
+ let current = root;
25
+ for (let i = 0; i < keys.length - 1; i++) {
26
+ const key = keys[i];
27
+ if (!current[key] || typeof current[key] !== "object") {
28
+ current[key] = {};
29
+ } else {
30
+ current[key] = { ...current[key] };
31
+ }
32
+ current = current[key];
33
+ }
34
+ current[keys[keys.length - 1]] = value;
35
+ return root;
36
+ }
37
+ function incrementAtPath(obj, path, increment) {
38
+ const currentValue = getAtPath(obj, path, 0);
39
+ const newValue = currentValue + increment;
40
+ return setAtPath(obj, path, newValue);
41
+ }
42
+
43
+ // src/managers/QueryCacheManager/QueryCache.manager.ts
44
+ var QueryCacheManager = class {
45
+ constructor(config) {
46
+ this.config = {
47
+ ...config,
48
+ keyExtractor: config.keyExtractor || ((item) => item.id)
49
+ };
50
+ }
51
+ /**
52
+ * Get items array from data
53
+ * Returns empty array if path doesn't exist or data is null
54
+ */
55
+ getItems(data) {
56
+ if (!data) return [];
57
+ if (!this.config.itemsPath) {
58
+ return Array.isArray(data) ? data : [];
59
+ }
60
+ const items = getAtPath(data, this.config.itemsPath, []);
61
+ return Array.isArray(items) ? items : [];
62
+ }
63
+ /**
64
+ * Set items array in data
65
+ * Creates nested structure if it doesn't exist
66
+ */
67
+ setItems(data, items) {
68
+ if (!data) {
69
+ if (this.config.initialData) {
70
+ data = this.config.initialData;
71
+ } else {
72
+ if (!this.config.itemsPath) {
73
+ return items;
74
+ }
75
+ data = {};
76
+ }
77
+ }
78
+ if (!this.config.itemsPath) {
79
+ return items;
80
+ }
81
+ return setAtPath(data, this.config.itemsPath, items);
82
+ }
83
+ /**
84
+ * Update pagination metadata after adding items
85
+ */
86
+ updatePaginationOnAdd(data, addedCount) {
87
+ if (!this.config.pagination) return data;
88
+ let result = data;
89
+ if (this.config.pagination.totalElementsPath) {
90
+ result = incrementAtPath(
91
+ result,
92
+ this.config.pagination.totalElementsPath,
93
+ addedCount
94
+ );
95
+ }
96
+ if (this.config.pagination.totalPagesPath && this.config.pagination.pageSizePath && this.config.pagination.totalElementsPath) {
97
+ const pageSize = getAtPath(result, this.config.pagination.pageSizePath, 0);
98
+ const totalElements = getAtPath(
99
+ result,
100
+ this.config.pagination.totalElementsPath,
101
+ 0
102
+ );
103
+ if (pageSize > 0) {
104
+ const totalPages = Math.ceil(totalElements / pageSize);
105
+ result = setAtPath(
106
+ result,
107
+ this.config.pagination.totalPagesPath,
108
+ totalPages
109
+ );
110
+ }
111
+ }
112
+ return result;
113
+ }
114
+ /**
115
+ * Update pagination metadata after removing items
116
+ */
117
+ updatePaginationOnRemove(data, removedCount) {
118
+ if (!this.config.pagination) return data;
119
+ let result = data;
120
+ if (this.config.pagination.totalElementsPath) {
121
+ result = incrementAtPath(
122
+ result,
123
+ this.config.pagination.totalElementsPath,
124
+ -removedCount
125
+ );
126
+ }
127
+ if (this.config.pagination.totalPagesPath && this.config.pagination.pageSizePath && this.config.pagination.totalElementsPath) {
128
+ const pageSize = getAtPath(result, this.config.pagination.pageSizePath, 0);
129
+ const totalElements = getAtPath(
130
+ result,
131
+ this.config.pagination.totalElementsPath,
132
+ 0
133
+ );
134
+ if (pageSize > 0) {
135
+ const totalPages = Math.ceil(Math.max(0, totalElements) / pageSize);
136
+ result = setAtPath(
137
+ result,
138
+ this.config.pagination.totalPagesPath,
139
+ totalPages
140
+ );
141
+ }
142
+ }
143
+ return result;
144
+ }
145
+ /**
146
+ * Add item to cache
147
+ *
148
+ * @param newItem - The item to add
149
+ * @param position - Where to add: 'start' or 'end'
150
+ */
151
+ add(newItem, position = "start") {
152
+ try {
153
+ this.config.queryClient.setQueryData(this.config.queryKey, (oldData) => {
154
+ const items = this.getItems(oldData);
155
+ const updatedItems = position === "start" ? [newItem, ...items] : [...items, newItem];
156
+ let result = this.setItems(oldData, updatedItems);
157
+ result = this.updatePaginationOnAdd(result, 1);
158
+ return result;
159
+ });
160
+ } catch (error) {
161
+ console.error("[QueryCacheManager] Add failed:", error);
162
+ this.invalidate();
163
+ }
164
+ }
165
+ /**
166
+ * Update existing item
167
+ *
168
+ * @param updatedItem - Partial item data to update
169
+ * @param matcher - Optional custom matcher function. Defaults to matching by key
170
+ */
171
+ update(updatedItem, matcher) {
172
+ try {
173
+ this.config.queryClient.setQueryData(this.config.queryKey, (oldData) => {
174
+ const items = this.getItems(oldData);
175
+ const matchFn = matcher || ((item) => this.config.keyExtractor(item) === this.config.keyExtractor(updatedItem));
176
+ const updatedItems = items.map(
177
+ (item) => matchFn(item) ? { ...item, ...updatedItem } : item
178
+ );
179
+ return this.setItems(oldData, updatedItems);
180
+ });
181
+ } catch (error) {
182
+ console.error("[QueryCacheManager] Update failed:", error);
183
+ this.invalidate();
184
+ }
185
+ }
186
+ /**
187
+ * Remove item from cache
188
+ *
189
+ * @param itemOrId - Item object or ID to remove
190
+ * @param matcher - Optional custom matcher function. Defaults to matching by key
191
+ */
192
+ delete(itemOrId, matcher) {
193
+ try {
194
+ this.config.queryClient.setQueryData(this.config.queryKey, (oldData) => {
195
+ const items = this.getItems(oldData);
196
+ const matchFn = matcher || ((item) => {
197
+ if (typeof itemOrId === "object") {
198
+ return this.config.keyExtractor(item) === this.config.keyExtractor(itemOrId);
199
+ }
200
+ return this.config.keyExtractor(item) === itemOrId;
201
+ });
202
+ const originalLength = items.length;
203
+ const updatedItems = items.filter((item) => !matchFn(item));
204
+ const removedCount = originalLength - updatedItems.length;
205
+ let result = this.setItems(oldData, updatedItems);
206
+ if (removedCount > 0) {
207
+ result = this.updatePaginationOnRemove(result, removedCount);
208
+ }
209
+ return result;
210
+ });
211
+ } catch (error) {
212
+ console.error("[QueryCacheManager] Delete failed:", error);
213
+ this.invalidate();
214
+ }
215
+ }
216
+ /**
217
+ * Replace full data
218
+ *
219
+ * @param newData - Complete new data to replace cache
220
+ */
221
+ replace(newData) {
222
+ try {
223
+ this.config.queryClient.setQueryData(this.config.queryKey, newData);
224
+ } catch (error) {
225
+ console.error("[QueryCacheManager] Replace failed:", error);
226
+ this.invalidate();
227
+ }
228
+ }
229
+ /**
230
+ * Clear all items (keeps structure, empties array)
231
+ */
232
+ clear() {
233
+ try {
234
+ this.config.queryClient.setQueryData(this.config.queryKey, (oldData) => {
235
+ let result = this.setItems(oldData, []);
236
+ if (this.config.pagination?.totalElementsPath) {
237
+ result = setAtPath(result, this.config.pagination.totalElementsPath, 0);
238
+ }
239
+ if (this.config.pagination?.totalPagesPath) {
240
+ result = setAtPath(result, this.config.pagination.totalPagesPath, 0);
241
+ }
242
+ return result;
243
+ });
244
+ } catch (error) {
245
+ console.error("[QueryCacheManager] Clear failed:", error);
246
+ this.invalidate();
247
+ }
248
+ }
249
+ /**
250
+ * Get current items from cache
251
+ *
252
+ * @returns Current items array or empty array if no data
253
+ */
254
+ getItemsFromCache() {
255
+ const data = this.config.queryClient.getQueryData(this.config.queryKey);
256
+ return this.getItems(data);
257
+ }
258
+ /**
259
+ * Get full data from cache
260
+ *
261
+ * @returns Current full data or undefined if no data
262
+ */
263
+ getDataFromCache() {
264
+ return this.config.queryClient.getQueryData(this.config.queryKey);
265
+ }
266
+ /**
267
+ * Invalidate query to trigger refetch
268
+ */
269
+ invalidate() {
270
+ this.config.queryClient.invalidateQueries({ queryKey: this.config.queryKey });
271
+ }
272
+ /**
273
+ * Get handlers for use with mutations
274
+ *
275
+ * @returns Object with onAdd, onUpdate, onDelete handlers
276
+ */
277
+ createHandlers() {
278
+ return {
279
+ onAdd: (newItem, position) => this.add(newItem, position),
280
+ onUpdate: (updatedItem, matcher) => this.update(updatedItem, matcher),
281
+ onDelete: (itemOrId, matcher) => this.delete(itemOrId, matcher)
282
+ };
283
+ }
284
+ };
285
+
286
+ exports.QueryCacheManager = QueryCacheManager;
287
+ Object.keys(reactQuery).forEach(function (k) {
288
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
289
+ enumerable: true,
290
+ get: function () { return reactQuery[k]; }
291
+ });
292
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,284 @@
1
+ export * from '@tanstack/react-query';
2
+
3
+ // src/index.ts
4
+
5
+ // src/managers/QueryCacheManager/QueryCache.utils.ts
6
+ function getAtPath(obj, path, defaultValue) {
7
+ if (!obj || !path) return defaultValue;
8
+ const keys = path.split(".");
9
+ let result = obj;
10
+ for (const key of keys) {
11
+ if (result === null || result === void 0) {
12
+ return defaultValue;
13
+ }
14
+ result = result[key];
15
+ }
16
+ return result !== void 0 ? result : defaultValue;
17
+ }
18
+ function setAtPath(obj, path, value) {
19
+ if (!path) return obj;
20
+ const keys = path.split(".");
21
+ const root = obj ? { ...obj } : {};
22
+ let current = root;
23
+ for (let i = 0; i < keys.length - 1; i++) {
24
+ const key = keys[i];
25
+ if (!current[key] || typeof current[key] !== "object") {
26
+ current[key] = {};
27
+ } else {
28
+ current[key] = { ...current[key] };
29
+ }
30
+ current = current[key];
31
+ }
32
+ current[keys[keys.length - 1]] = value;
33
+ return root;
34
+ }
35
+ function incrementAtPath(obj, path, increment) {
36
+ const currentValue = getAtPath(obj, path, 0);
37
+ const newValue = currentValue + increment;
38
+ return setAtPath(obj, path, newValue);
39
+ }
40
+
41
+ // src/managers/QueryCacheManager/QueryCache.manager.ts
42
+ var QueryCacheManager = class {
43
+ constructor(config) {
44
+ this.config = {
45
+ ...config,
46
+ keyExtractor: config.keyExtractor || ((item) => item.id)
47
+ };
48
+ }
49
+ /**
50
+ * Get items array from data
51
+ * Returns empty array if path doesn't exist or data is null
52
+ */
53
+ getItems(data) {
54
+ if (!data) return [];
55
+ if (!this.config.itemsPath) {
56
+ return Array.isArray(data) ? data : [];
57
+ }
58
+ const items = getAtPath(data, this.config.itemsPath, []);
59
+ return Array.isArray(items) ? items : [];
60
+ }
61
+ /**
62
+ * Set items array in data
63
+ * Creates nested structure if it doesn't exist
64
+ */
65
+ setItems(data, items) {
66
+ if (!data) {
67
+ if (this.config.initialData) {
68
+ data = this.config.initialData;
69
+ } else {
70
+ if (!this.config.itemsPath) {
71
+ return items;
72
+ }
73
+ data = {};
74
+ }
75
+ }
76
+ if (!this.config.itemsPath) {
77
+ return items;
78
+ }
79
+ return setAtPath(data, this.config.itemsPath, items);
80
+ }
81
+ /**
82
+ * Update pagination metadata after adding items
83
+ */
84
+ updatePaginationOnAdd(data, addedCount) {
85
+ if (!this.config.pagination) return data;
86
+ let result = data;
87
+ if (this.config.pagination.totalElementsPath) {
88
+ result = incrementAtPath(
89
+ result,
90
+ this.config.pagination.totalElementsPath,
91
+ addedCount
92
+ );
93
+ }
94
+ if (this.config.pagination.totalPagesPath && this.config.pagination.pageSizePath && this.config.pagination.totalElementsPath) {
95
+ const pageSize = getAtPath(result, this.config.pagination.pageSizePath, 0);
96
+ const totalElements = getAtPath(
97
+ result,
98
+ this.config.pagination.totalElementsPath,
99
+ 0
100
+ );
101
+ if (pageSize > 0) {
102
+ const totalPages = Math.ceil(totalElements / pageSize);
103
+ result = setAtPath(
104
+ result,
105
+ this.config.pagination.totalPagesPath,
106
+ totalPages
107
+ );
108
+ }
109
+ }
110
+ return result;
111
+ }
112
+ /**
113
+ * Update pagination metadata after removing items
114
+ */
115
+ updatePaginationOnRemove(data, removedCount) {
116
+ if (!this.config.pagination) return data;
117
+ let result = data;
118
+ if (this.config.pagination.totalElementsPath) {
119
+ result = incrementAtPath(
120
+ result,
121
+ this.config.pagination.totalElementsPath,
122
+ -removedCount
123
+ );
124
+ }
125
+ if (this.config.pagination.totalPagesPath && this.config.pagination.pageSizePath && this.config.pagination.totalElementsPath) {
126
+ const pageSize = getAtPath(result, this.config.pagination.pageSizePath, 0);
127
+ const totalElements = getAtPath(
128
+ result,
129
+ this.config.pagination.totalElementsPath,
130
+ 0
131
+ );
132
+ if (pageSize > 0) {
133
+ const totalPages = Math.ceil(Math.max(0, totalElements) / pageSize);
134
+ result = setAtPath(
135
+ result,
136
+ this.config.pagination.totalPagesPath,
137
+ totalPages
138
+ );
139
+ }
140
+ }
141
+ return result;
142
+ }
143
+ /**
144
+ * Add item to cache
145
+ *
146
+ * @param newItem - The item to add
147
+ * @param position - Where to add: 'start' or 'end'
148
+ */
149
+ add(newItem, position = "start") {
150
+ try {
151
+ this.config.queryClient.setQueryData(this.config.queryKey, (oldData) => {
152
+ const items = this.getItems(oldData);
153
+ const updatedItems = position === "start" ? [newItem, ...items] : [...items, newItem];
154
+ let result = this.setItems(oldData, updatedItems);
155
+ result = this.updatePaginationOnAdd(result, 1);
156
+ return result;
157
+ });
158
+ } catch (error) {
159
+ console.error("[QueryCacheManager] Add failed:", error);
160
+ this.invalidate();
161
+ }
162
+ }
163
+ /**
164
+ * Update existing item
165
+ *
166
+ * @param updatedItem - Partial item data to update
167
+ * @param matcher - Optional custom matcher function. Defaults to matching by key
168
+ */
169
+ update(updatedItem, matcher) {
170
+ try {
171
+ this.config.queryClient.setQueryData(this.config.queryKey, (oldData) => {
172
+ const items = this.getItems(oldData);
173
+ const matchFn = matcher || ((item) => this.config.keyExtractor(item) === this.config.keyExtractor(updatedItem));
174
+ const updatedItems = items.map(
175
+ (item) => matchFn(item) ? { ...item, ...updatedItem } : item
176
+ );
177
+ return this.setItems(oldData, updatedItems);
178
+ });
179
+ } catch (error) {
180
+ console.error("[QueryCacheManager] Update failed:", error);
181
+ this.invalidate();
182
+ }
183
+ }
184
+ /**
185
+ * Remove item from cache
186
+ *
187
+ * @param itemOrId - Item object or ID to remove
188
+ * @param matcher - Optional custom matcher function. Defaults to matching by key
189
+ */
190
+ delete(itemOrId, matcher) {
191
+ try {
192
+ this.config.queryClient.setQueryData(this.config.queryKey, (oldData) => {
193
+ const items = this.getItems(oldData);
194
+ const matchFn = matcher || ((item) => {
195
+ if (typeof itemOrId === "object") {
196
+ return this.config.keyExtractor(item) === this.config.keyExtractor(itemOrId);
197
+ }
198
+ return this.config.keyExtractor(item) === itemOrId;
199
+ });
200
+ const originalLength = items.length;
201
+ const updatedItems = items.filter((item) => !matchFn(item));
202
+ const removedCount = originalLength - updatedItems.length;
203
+ let result = this.setItems(oldData, updatedItems);
204
+ if (removedCount > 0) {
205
+ result = this.updatePaginationOnRemove(result, removedCount);
206
+ }
207
+ return result;
208
+ });
209
+ } catch (error) {
210
+ console.error("[QueryCacheManager] Delete failed:", error);
211
+ this.invalidate();
212
+ }
213
+ }
214
+ /**
215
+ * Replace full data
216
+ *
217
+ * @param newData - Complete new data to replace cache
218
+ */
219
+ replace(newData) {
220
+ try {
221
+ this.config.queryClient.setQueryData(this.config.queryKey, newData);
222
+ } catch (error) {
223
+ console.error("[QueryCacheManager] Replace failed:", error);
224
+ this.invalidate();
225
+ }
226
+ }
227
+ /**
228
+ * Clear all items (keeps structure, empties array)
229
+ */
230
+ clear() {
231
+ try {
232
+ this.config.queryClient.setQueryData(this.config.queryKey, (oldData) => {
233
+ let result = this.setItems(oldData, []);
234
+ if (this.config.pagination?.totalElementsPath) {
235
+ result = setAtPath(result, this.config.pagination.totalElementsPath, 0);
236
+ }
237
+ if (this.config.pagination?.totalPagesPath) {
238
+ result = setAtPath(result, this.config.pagination.totalPagesPath, 0);
239
+ }
240
+ return result;
241
+ });
242
+ } catch (error) {
243
+ console.error("[QueryCacheManager] Clear failed:", error);
244
+ this.invalidate();
245
+ }
246
+ }
247
+ /**
248
+ * Get current items from cache
249
+ *
250
+ * @returns Current items array or empty array if no data
251
+ */
252
+ getItemsFromCache() {
253
+ const data = this.config.queryClient.getQueryData(this.config.queryKey);
254
+ return this.getItems(data);
255
+ }
256
+ /**
257
+ * Get full data from cache
258
+ *
259
+ * @returns Current full data or undefined if no data
260
+ */
261
+ getDataFromCache() {
262
+ return this.config.queryClient.getQueryData(this.config.queryKey);
263
+ }
264
+ /**
265
+ * Invalidate query to trigger refetch
266
+ */
267
+ invalidate() {
268
+ this.config.queryClient.invalidateQueries({ queryKey: this.config.queryKey });
269
+ }
270
+ /**
271
+ * Get handlers for use with mutations
272
+ *
273
+ * @returns Object with onAdd, onUpdate, onDelete handlers
274
+ */
275
+ createHandlers() {
276
+ return {
277
+ onAdd: (newItem, position) => this.add(newItem, position),
278
+ onUpdate: (updatedItem, matcher) => this.update(updatedItem, matcher),
279
+ onDelete: (itemOrId, matcher) => this.delete(itemOrId, matcher)
280
+ };
281
+ }
282
+ };
283
+
284
+ export { QueryCacheManager };
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "tanstack-cacher",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight cache management utility for TanStack Query that simplifies adding, updating, deleting, and synchronizing cached data",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/hacagahasanli/tanstack-cacher"
9
+ },
10
+ "author": {
11
+ "name": "Hasanli Hajagha",
12
+ "email": "hacagahasanli@gmail.com",
13
+ "url": "https://github.com/hacagahasanli"
14
+ },
15
+ "maintainers": [
16
+ {
17
+ "name": "Hasanli Hajagha",
18
+ "email": "hacagahasanli@gmail.com",
19
+ "web": "http://github.com/hacagahasanli"
20
+ }
21
+ ],
22
+ "homepage": "https://github.com/hacagahasanli/tanstack-cacher",
23
+ "keywords": [
24
+ "react",
25
+ "react-query",
26
+ "tanstack-query",
27
+ "cache",
28
+ "cache-manager",
29
+ "optimistic-updates"
30
+ ],
31
+ "main": "./dist/index.js",
32
+ "module": "./dist/index.mjs",
33
+ "types": "./dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.mjs",
38
+ "require": "./dist/index.js"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "README.md",
44
+ "LICENSE"
45
+ ],
46
+ "engines": {
47
+ "node": ">=16"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsup --watch",
52
+ "lint": "eslint src --ext .ts,.tsx",
53
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
54
+ "type-check": "tsc --noEmit",
55
+ "prepublishOnly": "npm run build",
56
+ "test": "echo \"No tests yet\" && exit 0"
57
+ },
58
+ "dependencies": {
59
+ "@tanstack/react-query": "^5.62.11"
60
+ },
61
+ "peerDependencies": {
62
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@types/react": "^18.3.12",
66
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
67
+ "@typescript-eslint/parser": "^7.18.0",
68
+ "eslint": "^8.57.1",
69
+ "eslint-config-prettier": "^9.1.0",
70
+ "eslint-plugin-prettier": "^5.2.1",
71
+ "prettier": "^3.4.2",
72
+ "react": "^18.3.1",
73
+ "tsup": "^8.3.5",
74
+ "typescript": "^5.7.2"
75
+ }
76
+ }