react-redux-cache 0.6.0 → 0.7.0-rc.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/README.md CHANGED
@@ -41,7 +41,7 @@ Examples of states, generated by cache reducer from `/example` project:
41
41
  queries: {
42
42
  // each query has its own map of query states, stored by cache key, which is generated from query params
43
43
  getUser: {
44
- "2": {loading: false, error: undefined, result: 2, params: 2},
44
+ "2": {loading: false, result: 2, params: 2, expiresAt: 1727217298025},
45
45
  "3": {loading: true, params: 3}
46
46
  },
47
47
  getUsers: {
@@ -79,9 +79,9 @@ Examples of states, generated by cache reducer from `/example` project:
79
79
  getUser: {
80
80
  "2": {
81
81
  loading: false,
82
- error: undefined,
83
82
  result: {id: 2, bank: {id: "2", name: "Bank 2"}, name: "User 2"},
84
- params: 2
83
+ params: 2,
84
+ expiresAt: 1727217298025
85
85
  },
86
86
  "3": {loading: true, params: 3}
87
87
  },
@@ -122,6 +122,8 @@ Examples of states, generated by cache reducer from `/example` project:
122
122
  - [api.ts](https://github.com/gentlee/react-redux-cache#apits)
123
123
  - [Usage](https://github.com/gentlee/react-redux-cache#usage)
124
124
  - [Advanced](https://github.com/gentlee/react-redux-cache#advanced)
125
+ - [Error handling](https://github.com/gentlee/react-redux-cache#error-handling)
126
+ - [Invalidation](https://github.com/gentlee/react-redux-cache#invalidation)
125
127
  - [Extended cache policy](https://github.com/gentlee/react-redux-cache#extended-cache-policy)
126
128
  - [Infinite scroll pagination](https://github.com/gentlee/react-redux-cache#infinite-scroll-pagination)
127
129
  - [redux-persist](https://github.com/gentlee/react-redux-cache#redux-persist)
@@ -134,7 +136,7 @@ Examples of states, generated by cache reducer from `/example` project:
134
136
 
135
137
  `fast-deep-equal` is an optional peer dependency if `deepComparisonEnabled` cache option is enabled (default is true).
136
138
  ```sh
137
- npm add react-redux-cache react redux react-redux
139
+ npm add react-redux-cache react redux react-redux fast-deep-equal
138
140
  ```
139
141
  ### Initialization
140
142
  The only function that needs to be imported is `createCache`, which creates fully typed reducer, hooks, actions, selectors and utils to be used in the app. You can create as many caches as needed, but keep in mind that normalization is not shared between them.
@@ -156,7 +158,12 @@ export const {
156
158
  },
157
159
  queries: {
158
160
  getUsers: { query: getUsers },
159
- getUser: { query: getUser },
161
+ getUser: {
162
+ query: getUser,
163
+ // For each query `secondsToLive` option can be set, which is used to set expiration date of a cached result when query response is received.
164
+ // After expiration query result is considered invalidated and will be refetched on the next useQuery mount.
165
+ secondsToLive: 5 * 60 // Here cached result is valid for 5 minutes.
166
+ },
160
167
  },
161
168
  mutations: {
162
169
  updateUser: { mutation: updateUser },
@@ -201,37 +208,32 @@ Perfect implementation is when the backend already returns normalized data.
201
208
 
202
209
  // Example of query with normalization (recommended)
203
210
 
204
- export const getUser = async (id: number) => {
205
- const result = await ...
206
-
207
- const normalizedResult: {
208
- // result is id of the user
209
- result: number
210
- // entities contain all normalized objects
211
- entities: {
212
- users: Record<number, User>
213
- banks: Record<string, Bank>
214
- }
215
- } = normalize(result, getUserSchema)
211
+ export const getUser = async (id) => {
212
+ // Result can be get by any way - fetch, axios etc, even with database connection.
213
+ // There is no limitation here.
214
+ const response = await ...
216
215
 
217
- return normalizedResult
218
- }
216
+ // In this example normalizr package is used, but it is not necessary.
217
+ return normalize(response, getUserSchema)
218
+ // satisfies keyword is used here for proper typing of params and returned value.
219
+ } satisfies Query<number, CacheTypenames>
219
220
 
220
- // Example of query without normalization (not recommended)
221
+ // Example of query without normalization (not recommended), with selecting access token from the store
221
222
 
222
- export const getBank = (id: string) => {
223
+ export const getBank = (id, {getState}) => {
224
+ const token = tokenSelector(getState())
223
225
  const result: Bank = ...
224
226
  return {result} // result is bank object, no entities passed
225
- }
227
+ } satisfies Query<string>
226
228
 
227
229
  // Example of mutation with normalization
228
230
 
229
- export const removeUser = async (id: number) => {
231
+ export const removeUser = async (id, _, abortSignal) => {
230
232
  await ...
231
233
  return {
232
234
  remove: { users: [id] },
233
235
  }
234
- }
236
+ } satisfies Query<number, CacheTypenames>
235
237
  ```
236
238
 
237
239
  ### Usage
@@ -243,7 +245,7 @@ Please check `example/` folder (`npm run example` to run).
243
245
  export const UserScreen = () => {
244
246
  const {id} = useParams()
245
247
 
246
- // useQuery connects to redux state and if user with that id is already cached, fetch won't happen (with default cachePolicy 'cache-first')
248
+ // useQuery connects to redux state and if user with that id is already cached, fetch won't happen (with default cachePolicy 'cache-first').
247
249
  // Infers all types from created cache, telling here that params and result are of type `number`.
248
250
  const [{result: userId, loading, error}] = useQuery({
249
251
  query: 'getUser',
@@ -254,7 +256,7 @@ export const UserScreen = () => {
254
256
  mutation: 'updateUser',
255
257
  })
256
258
 
257
- // This selector is used for denormalization and returns entities with proper types - User and Bank
259
+ // This selector returns entities with proper types - User and Bank
258
260
  const user = useSelectEntityById(userId, 'users')
259
261
  const bank = useSelectEntityById(user?.bankId, 'banks')
260
262
 
@@ -268,9 +270,43 @@ export const UserScreen = () => {
268
270
 
269
271
  ### Advanced
270
272
 
273
+ #### Error handling
274
+
275
+ Queries and mutations are wrapped in try/catch, so any error will lead to cancelling of any updates to the state except `loading: false` and the caught error. If you still want to make some state updates, or just want to use thrown errors only for unexpected cases, consider returning expected errors as a part of the result:
276
+
277
+ ```typescript
278
+ export const updateBank = (bank) => {
279
+ const {httpError, response} = ...
280
+ return {
281
+ result: {
282
+ // Error is a part of the result, containing e.g. map of not valid fields and threir error messages
283
+ httpError,
284
+ // Bank still can be returned from the backend with error e.g. when only some of fields were udpated
285
+ bank: response?.bank
286
+ }
287
+ }
288
+ } satisfies Mutation<Partial<Bank>>
289
+
290
+ ```
291
+
292
+ #### Invalidation
293
+
294
+ `cache-first` cache policy (default) skips fetching on component mount if result is already cached, but we can invalidate cached query results using `invalidateQuery` action to make it run again on a next mount. Also, we can return this action as a part of query or mutation response, to perform it in one batch update.
295
+
296
+ ```typescript
297
+ const updateUser = (user: Partial<User>) => {
298
+ const response = ...
299
+ return {
300
+ result: response.result,
301
+ expiresAt: Date.now() + 10000, // optional, expiration time of current response
302
+ actions: [invalidateQuery([{ key: 'getUsers' }])] // optional, this action sets `expiresAt` field of `getUsers` states to Date.now()
303
+ }
304
+ }
305
+ ```
306
+
271
307
  #### Extended cache policy
272
308
 
273
- `cache-first` cache policy skips fetching if result is already cached, but sometimes it can't determine that we already have result in some other's query result or in normalized entities cache. In that case we can use `skip` parameter of a query:
309
+ `cache-first` cache policy (default) skips fetching on component mount if result is already cached, but sometimes it can't determine that we already have result in some other's query result or in normalized entities cache. In that case we can use `skip` parameter of a query:
274
310
 
275
311
  ```typescript
276
312
  export const UserScreen = () => {
@@ -294,7 +330,7 @@ We can additionally check that entity is full or "fresh" enough:
294
330
  skip: !!user && isFullUser(user)
295
331
  ```
296
332
 
297
- Another approach is to set `skip: true` and manually run `fetch` when needed:
333
+ Another approach is to set `skip: true` and manually run `fetch`. `onlyIfExpired` option can be also used:
298
334
 
299
335
  ```typescript
300
336
  export const UserScreen = () => {
@@ -308,7 +344,7 @@ export const UserScreen = () => {
308
344
 
309
345
  useEffect(() => {
310
346
  if (screenIsVisible) {
311
- fetchUser()
347
+ fetchUser({ onlyIfExpired: true }) // expiration happens if expiresAt was set before e.g. by secondsToLive option or invalidateQuery action. If result is not cached yet, it is also considered as expired.
312
348
  }
313
349
  }, [screenIsVisible])
314
350
 
@@ -439,4 +475,4 @@ As example, can be overriden when implementing pagination.
439
475
 
440
476
  **Queries:** Queries are throttled: query with the same cache key (generated from params by default) is cancelled if already running.
441
477
 
442
- **Mutations:** Mutations are debounced: previous similar mutation is aborted if it was running when the new one started. Second argument in mutations is `AbortSignal`, which can be used e.g. for cancelling http requests.
478
+ **Mutations:** Mutations are debounced: previous similar mutation is aborted if it was running when the new one started. Third argument in mutations is `AbortSignal`, which can be used e.g. for cancelling http requests.
@@ -1,23 +1,23 @@
1
- import type { EntityChanges, Key, QueryMutationState, Typenames } from './types';
1
+ import type { EntityChanges, Key, MutationState, QueryState, Typenames } from './types';
2
2
  export type ActionMap<N extends string, T extends Typenames, QP, QR, MP, MR> = ReturnType<typeof createActions<N, T, QP, QR, MP, MR>>;
3
3
  export declare const createActions: <N extends string, T extends Typenames, QP, QR, MP, MR>(name: N) => {
4
4
  /** Updates query state, and optionally merges entity changes in a single action. */
5
5
  updateQueryStateAndEntities: {
6
- <K extends keyof QP & keyof QR>(queryKey: K, queryCacheKey: Key, state?: Partial<QueryMutationState<QP[K], QR[K]>> | undefined, entityChanges?: EntityChanges<T> | undefined): {
6
+ <K extends keyof QP & keyof QR>(queryKey: K, queryCacheKey: Key, state?: Partial<QueryState<QP[K], QR[K]>> | undefined, entityChanges?: EntityChanges<T> | undefined): {
7
7
  type: `@rrc/${N}/updateQueryStateAndEntities`;
8
8
  queryKey: K;
9
9
  queryCacheKey: Key;
10
- state: Partial<QueryMutationState<QP[K], QR[K]>> | undefined;
10
+ state: Partial<QueryState<QP[K], QR[K]>> | undefined;
11
11
  entityChanges: EntityChanges<T> | undefined;
12
12
  };
13
13
  type: `@rrc/${N}/updateQueryStateAndEntities`;
14
14
  };
15
15
  /** Updates mutation state, and optionally merges entity changes in a single action. */
16
16
  updateMutationStateAndEntities: {
17
- <K_1 extends keyof MP & keyof MR>(mutationKey: K_1, state?: Partial<QueryMutationState<MP[K_1], MR[K_1]>> | undefined, entityChanges?: EntityChanges<T> | undefined): {
17
+ <K_1 extends keyof MP & keyof MR>(mutationKey: K_1, state?: Partial<MutationState<MP[K_1], MR[K_1]>> | undefined, entityChanges?: EntityChanges<T> | undefined): {
18
18
  type: `@rrc/${N}/updateMutationStateAndEntities`;
19
19
  mutationKey: K_1;
20
- state: Partial<QueryMutationState<MP[K_1], MR[K_1]>> | undefined;
20
+ state: Partial<MutationState<MP[K_1], MR[K_1]>> | undefined;
21
21
  entityChanges: EntityChanges<T> | undefined;
22
22
  };
23
23
  type: `@rrc/${N}/updateMutationStateAndEntities`;
@@ -30,16 +30,42 @@ export declare const createActions: <N extends string, T extends Typenames, QP,
30
30
  };
31
31
  type: `@rrc/${N}/mergeEntityChanges`;
32
32
  };
33
+ /** Invalidates query states. */
34
+ invalidateQuery: {
35
+ <K_2 extends keyof QP & keyof QR>(queries: {
36
+ /** Query key */
37
+ key: K_2;
38
+ /** Query cache key */
39
+ cacheKey?: Key | undefined;
40
+ /** Unix timestamp at which query expires. Is set to the query state. @default Date.now() */
41
+ expiresAt?: number | undefined;
42
+ }[]): {
43
+ type: `@rrc/${N}/invalidateQuery`;
44
+ queries: {
45
+ /** Query key */
46
+ key: K_2;
47
+ /** Query cache key */
48
+ cacheKey?: Key | undefined;
49
+ /** Unix timestamp at which query expires. Is set to the query state. @default Date.now() */
50
+ expiresAt?: number | undefined;
51
+ }[];
52
+ };
53
+ type: `@rrc/${N}/invalidateQuery`;
54
+ };
33
55
  /** Clear states for provided query keys and cache keys.
34
56
  * If cache key for query key is not provided, the whole state for query key is cleared. */
35
57
  clearQueryState: {
36
- <K_2 extends keyof QP & keyof QR>(queryKeys: {
37
- key: K_2;
58
+ <K_3 extends keyof QP & keyof QR>(queries: {
59
+ /** Query key */
60
+ key: K_3;
61
+ /** Query cache key */
38
62
  cacheKey?: Key | undefined;
39
63
  }[]): {
40
64
  type: `@rrc/${N}/clearQueryState`;
41
- queryKeys: {
42
- key: K_2;
65
+ queries: {
66
+ /** Query key */
67
+ key: K_3;
68
+ /** Query cache key */
43
69
  cacheKey?: Key | undefined;
44
70
  }[];
45
71
  };
@@ -47,9 +73,9 @@ export declare const createActions: <N extends string, T extends Typenames, QP,
47
73
  };
48
74
  /** Clear states for provided mutation keys. */
49
75
  clearMutationState: {
50
- <K_3 extends keyof MP & keyof MR>(mutationKeys: K_3[]): {
76
+ <K_4 extends keyof MP & keyof MR>(mutationKeys: K_4[]): {
51
77
  type: `@rrc/${N}/clearMutationState`;
52
- mutationKeys: K_3[];
78
+ mutationKeys: K_4[];
53
79
  };
54
80
  type: `@rrc/${N}/clearMutationState`;
55
81
  };
@@ -27,10 +27,16 @@ const createActions = (name) => {
27
27
  changes,
28
28
  });
29
29
  mergeEntityChanges.type = mergeEntityChangesType;
30
+ const invalidateQueryType = `${actionPrefix}invalidateQuery`;
31
+ const invalidateQuery = (queries) => ({
32
+ type: invalidateQueryType,
33
+ queries,
34
+ });
35
+ invalidateQuery.type = invalidateQueryType;
30
36
  const clearQueryStateType = `${actionPrefix}clearQueryState`;
31
- const clearQueryState = (queryKeys) => ({
37
+ const clearQueryState = (queries) => ({
32
38
  type: clearQueryStateType,
33
- queryKeys,
39
+ queries,
34
40
  });
35
41
  clearQueryState.type = clearQueryStateType;
36
42
  const clearMutationStateType = `${actionPrefix}clearMutationState`;
@@ -46,6 +52,8 @@ const createActions = (name) => {
46
52
  updateMutationStateAndEntities,
47
53
  /** Merge EntityChanges to the state. */
48
54
  mergeEntityChanges,
55
+ /** Invalidates query states. */
56
+ invalidateQuery,
49
57
  /** Clear states for provided query keys and cache keys.
50
58
  * If cache key for query key is not provided, the whole state for query key is cleared. */
51
59
  clearQueryState,
@@ -1,35 +1,42 @@
1
- import type { Cache, Key, MutationResult, OptionalPartial, QueryMutationState, QueryOptions, QueryResult, Typenames } from './types';
1
+ import type { Cache, Key, MutationResult, MutationState, OptionalPartial, QueryOptions, QueryResult, QueryState, Typenames } from './types';
2
2
  import { useMutation } from './useMutation';
3
3
  import { useQuery } from './useQuery';
4
4
  import { applyEntityChanges } from './utilsAndConstants';
5
5
  /**
6
6
  * Creates reducer, actions and hooks for managing queries and mutations through redux cache.
7
7
  */
8
- export declare const createCache: <N extends string, T extends Typenames, QP, QR, MP, MR>(partialCache: OptionalPartial<Cache<N, T, QP, QR, MP, MR>, "options" | "queries" | "mutations" | "cacheStateSelector">) => {
8
+ export declare const createCache: <N extends string, T extends Typenames, QP, QR, MP, MR>(partialCache: OptionalPartial<Cache<N, T, QP, QR, MP, MR>, "queries" | "options" | "mutations" | "cacheStateSelector">) => {
9
9
  /** Keeps all options, passed while creating the cache. */
10
10
  cache: Cache<N, T, QP, QR, MP, MR>;
11
11
  /** Reducer of the cache, should be added to redux store. */
12
12
  reducer: (state: {
13
13
  entities: import("./types").EntitiesMap<T>;
14
- queries: QP | QR extends infer T_1 ? { [QK in keyof T_1]: import("./types").Dict<QueryMutationState<QP[QK], QR[QK]>>; } : never;
15
- mutations: MP | MR extends infer T_2 ? { [MK in keyof T_2]: QueryMutationState<MP[MK], MR[MK]>; } : never;
14
+ queries: QP | QR extends infer T_1 ? { [QK in keyof T_1]: import("./types").Dict<QueryState<QP[QK], QR[QK]> | undefined>; } : never;
15
+ mutations: MP | MR extends infer T_2 ? { [MK in keyof T_2]: MutationState<MP[MK], MR[MK]>; } : never;
16
16
  } | undefined, action: {
17
17
  type: `@rrc/${N}/updateQueryStateAndEntities`;
18
18
  queryKey: keyof QP & keyof QR;
19
19
  queryCacheKey: Key;
20
- state: Partial<QueryMutationState<QP[keyof QP & keyof QR], QR[keyof QP & keyof QR]>> | undefined;
20
+ state: Partial<QueryState<QP[keyof QP & keyof QR], QR[keyof QP & keyof QR]>> | undefined;
21
21
  entityChanges: import("./types").EntityChanges<T> | undefined;
22
22
  } | {
23
23
  type: `@rrc/${N}/updateMutationStateAndEntities`;
24
24
  mutationKey: keyof MP & keyof MR;
25
- state: Partial<QueryMutationState<MP[keyof MP & keyof MR], MR[keyof MP & keyof MR]>> | undefined;
25
+ state: Partial<MutationState<MP[keyof MP & keyof MR], MR[keyof MP & keyof MR]>> | undefined;
26
26
  entityChanges: import("./types").EntityChanges<T> | undefined;
27
27
  } | {
28
28
  type: `@rrc/${N}/mergeEntityChanges`;
29
29
  changes: import("./types").EntityChanges<T>;
30
+ } | {
31
+ type: `@rrc/${N}/invalidateQuery`;
32
+ queries: {
33
+ key: keyof QP & keyof QR;
34
+ cacheKey?: Key | undefined;
35
+ expiresAt?: number | undefined;
36
+ }[];
30
37
  } | {
31
38
  type: `@rrc/${N}/clearQueryState`;
32
- queryKeys: {
39
+ queries: {
33
40
  key: keyof QP & keyof QR;
34
41
  cacheKey?: Key | undefined;
35
42
  }[];
@@ -38,25 +45,25 @@ export declare const createCache: <N extends string, T extends Typenames, QP, QR
38
45
  mutationKeys: (keyof MP & keyof MR)[];
39
46
  }) => {
40
47
  entities: import("./types").EntitiesMap<T>;
41
- queries: QP | QR extends infer T_3 ? { [QK in keyof T_3]: import("./types").Dict<QueryMutationState<QP[QK], QR[QK]>>; } : never;
42
- mutations: MP | MR extends infer T_4 ? { [MK in keyof T_4]: QueryMutationState<MP[MK], MR[MK]>; } : never;
48
+ queries: QP | QR extends infer T_3 ? { [QK in keyof T_3]: import("./types").Dict<QueryState<QP[QK], QR[QK]> | undefined>; } : never;
49
+ mutations: MP | MR extends infer T_4 ? { [MK in keyof T_4]: MutationState<MP[MK], MR[MK]>; } : never;
43
50
  };
44
51
  actions: {
45
52
  updateQueryStateAndEntities: {
46
- <K extends keyof QP & keyof QR>(queryKey: K, queryCacheKey: Key, state?: Partial<QueryMutationState<QP[K], QR[K]>> | undefined, entityChanges?: import("./types").EntityChanges<T> | undefined): {
53
+ <K extends keyof QP & keyof QR>(queryKey: K, queryCacheKey: Key, state?: Partial<QueryState<QP[K], QR[K]>> | undefined, entityChanges?: import("./types").EntityChanges<T> | undefined): {
47
54
  type: `@rrc/${N}/updateQueryStateAndEntities`;
48
55
  queryKey: K;
49
56
  queryCacheKey: Key;
50
- state: Partial<QueryMutationState<QP[K], QR[K]>> | undefined;
57
+ state: Partial<QueryState<QP[K], QR[K]>> | undefined;
51
58
  entityChanges: import("./types").EntityChanges<T> | undefined;
52
59
  };
53
60
  type: `@rrc/${N}/updateQueryStateAndEntities`;
54
61
  };
55
62
  updateMutationStateAndEntities: {
56
- <K_1 extends keyof MP & keyof MR>(mutationKey: K_1, state?: Partial<QueryMutationState<MP[K_1], MR[K_1]>> | undefined, entityChanges?: import("./types").EntityChanges<T> | undefined): {
63
+ <K_1 extends keyof MP & keyof MR>(mutationKey: K_1, state?: Partial<MutationState<MP[K_1], MR[K_1]>> | undefined, entityChanges?: import("./types").EntityChanges<T> | undefined): {
57
64
  type: `@rrc/${N}/updateMutationStateAndEntities`;
58
65
  mutationKey: K_1;
59
- state: Partial<QueryMutationState<MP[K_1], MR[K_1]>> | undefined;
66
+ state: Partial<MutationState<MP[K_1], MR[K_1]>> | undefined;
60
67
  entityChanges: import("./types").EntityChanges<T> | undefined;
61
68
  };
62
69
  type: `@rrc/${N}/updateMutationStateAndEntities`;
@@ -68,30 +75,45 @@ export declare const createCache: <N extends string, T extends Typenames, QP, QR
68
75
  };
69
76
  type: `@rrc/${N}/mergeEntityChanges`;
70
77
  };
71
- clearQueryState: {
72
- <K_2 extends keyof QP & keyof QR>(queryKeys: {
78
+ invalidateQuery: {
79
+ <K_2 extends keyof QP & keyof QR>(queries: {
73
80
  key: K_2;
74
81
  cacheKey?: Key | undefined;
82
+ expiresAt?: number | undefined;
75
83
  }[]): {
76
- type: `@rrc/${N}/clearQueryState`;
77
- queryKeys: {
84
+ type: `@rrc/${N}/invalidateQuery`;
85
+ queries: {
78
86
  key: K_2;
79
87
  cacheKey?: Key | undefined;
88
+ expiresAt?: number | undefined;
89
+ }[];
90
+ };
91
+ type: `@rrc/${N}/invalidateQuery`;
92
+ };
93
+ clearQueryState: {
94
+ <K_3 extends keyof QP & keyof QR>(queries: {
95
+ key: K_3;
96
+ cacheKey?: Key | undefined;
97
+ }[]): {
98
+ type: `@rrc/${N}/clearQueryState`;
99
+ queries: {
100
+ key: K_3;
101
+ cacheKey?: Key | undefined;
80
102
  }[];
81
103
  };
82
104
  type: `@rrc/${N}/clearQueryState`;
83
105
  };
84
106
  clearMutationState: {
85
- <K_3 extends keyof MP & keyof MR>(mutationKeys: K_3[]): {
107
+ <K_4 extends keyof MP & keyof MR>(mutationKeys: K_4[]): {
86
108
  type: `@rrc/${N}/clearMutationState`;
87
- mutationKeys: K_3[];
109
+ mutationKeys: K_4[];
88
110
  };
89
111
  type: `@rrc/${N}/clearMutationState`;
90
112
  };
91
113
  };
92
114
  selectors: {
93
115
  /** Selects query state. */
94
- selectQueryState: <QK_1 extends keyof QP | keyof QR>(state: unknown, query: QK_1, cacheKey: Key) => QueryMutationState<QK_1 extends keyof QP & keyof QR ? QP[QK_1] : never, QK_1 extends keyof QP & keyof QR ? QR[QK_1] : never>;
116
+ selectQueryState: <QK_1 extends keyof QP | keyof QR>(state: unknown, query: QK_1, cacheKey: Key) => QueryState<QK_1 extends keyof QP & keyof QR ? QP[QK_1] : never, QK_1 extends keyof QP & keyof QR ? QR[QK_1] : never>;
95
117
  /** Selects query latest result. */
96
118
  selectQueryResult: <QK_2 extends keyof QP | keyof QR>(state: unknown, query: QK_2, cacheKey: Key) => (QK_2 extends keyof QP & keyof QR ? QR[QK_2] : never) | undefined;
97
119
  /** Selects query loading state. */
@@ -100,8 +122,10 @@ export declare const createCache: <N extends string, T extends Typenames, QP, QR
100
122
  selectQueryError: <QK_4 extends keyof QP | keyof QR>(state: unknown, query: QK_4, cacheKey: Key) => Error | undefined;
101
123
  /** Selects query latest params. */
102
124
  selectQueryParams: <QK_5 extends keyof QP | keyof QR>(state: unknown, query: QK_5, cacheKey: Key) => (QK_5 extends keyof QP & keyof QR ? QP[QK_5] : never) | undefined;
125
+ /** Selects query expiresAt value. */
126
+ selectQueryExpiresAt: <QK_6 extends keyof QP | keyof QR>(state: unknown, query: QK_6, cacheKey: Key) => number | undefined;
103
127
  /** Selects mutation state. */
104
- selectMutationState: <MK_1 extends keyof MP | keyof MR>(state: unknown, mutation: MK_1) => QueryMutationState<MK_1 extends keyof MP & keyof MR ? MP[MK_1] : never, MK_1 extends keyof MP & keyof MR ? MR[MK_1] : never>;
128
+ selectMutationState: <MK_1 extends keyof MP | keyof MR>(state: unknown, mutation: MK_1) => MutationState<MK_1 extends keyof MP & keyof MR ? MP[MK_1] : never, MK_1 extends keyof MP & keyof MR ? MR[MK_1] : never>;
105
129
  /** Selects mutation latest result. */
106
130
  selectMutationResult: <MK_2 extends keyof MP | keyof MR>(state: unknown, mutation: MK_2) => (MK_2 extends keyof MP & keyof MR ? MR[MK_2] : never) | undefined;
107
131
  /** Selects mutation loading state. */
@@ -120,20 +144,18 @@ export declare const createCache: <N extends string, T extends Typenames, QP, QR
120
144
  hooks: {
121
145
  /** Returns client object with query and mutate functions. */
122
146
  useClient: () => {
123
- query: <QK_6 extends keyof QP | keyof QR>(options: Omit<QueryOptions<T, QP, QR, QK_6>, "cachePolicy">) => Promise<QueryResult<QK_6 extends keyof QP & keyof QR ? QR[QK_6] : never>>;
147
+ query: <QK_7 extends keyof QP | keyof QR>(options: QueryOptions<T, QP, QR, QK_7>) => Promise<QueryResult<QK_7 extends keyof QP & keyof QR ? QR[QK_7] : never>>;
124
148
  mutate: <MK_6 extends keyof MP | keyof MR>(options: {
125
149
  mutation: MK_6;
126
150
  params: MK_6 extends keyof MP & keyof MR ? MP[MK_6] : never;
127
151
  }) => Promise<MutationResult<MK_6 extends keyof MP & keyof MR ? MR[MK_6] : never>>;
128
152
  };
129
- /** Fetches query when params change and subscribes to query state. */
130
- useQuery: <QK_7 extends keyof QP | keyof QR>(options: import("./types").UseQueryOptions<T, QP, QR, QK_7>) => readonly [QueryMutationState<QK_7 extends keyof QP & keyof QR ? QP[QK_7] : never, QK_7 extends keyof QP & keyof QR ? QR[QK_7] : never>, (options?: {
131
- params: QK_7 extends keyof QP & keyof QR ? QP[QK_7] : never;
132
- } | undefined) => Promise<QueryResult<QK_7 extends infer T_5 ? T_5 extends QK_7 ? T_5 extends keyof QP & keyof QR ? QR[T_5] : never : never : never>>];
153
+ /** Fetches query when params change and subscribes to query state changes (except `expiresAt` field). */
154
+ useQuery: <QK_8 extends keyof QP | keyof QR>(options: import("./types").UseQueryOptions<T, QP, QR, QK_8>) => readonly [Omit<QueryState<QK_8 extends keyof QP & keyof QR ? QP[QK_8] : never, QK_8 extends keyof QP & keyof QR ? QR[QK_8] : never>, "expiresAt">, (options?: Partial<Pick<QueryOptions<T, QP, QR, QK_8>, "params" | "onlyIfExpired">> | undefined) => Promise<QueryResult<QK_8 extends infer T_5 ? T_5 extends QK_8 ? T_5 extends keyof QP & keyof QR ? QR[T_5] : never : never : never>>];
133
155
  /** Subscribes to provided mutation state and provides mutate function. */
134
156
  useMutation: <MK_7 extends keyof MP | keyof MR>(options: {
135
157
  mutation: MK_7;
136
- }) => readonly [(params: MK_7 extends keyof MP & keyof MR ? MP[MK_7] : never) => Promise<MutationResult<MK_7 extends infer T_6 ? T_6 extends MK_7 ? T_6 extends keyof MP & keyof MR ? MR[T_6] : never : never : never>>, QueryMutationState<MK_7 extends keyof MP & keyof MR ? MP[MK_7] : never, MK_7 extends keyof MP & keyof MR ? MP[MK_7] : never>, () => boolean];
158
+ }) => readonly [(params: MK_7 extends keyof MP & keyof MR ? MP[MK_7] : never) => Promise<MutationResult<MK_7 extends infer T_6 ? T_6 extends MK_7 ? T_6 extends keyof MP & keyof MR ? MR[T_6] : never : never : never>>, MutationState<MK_7 extends keyof MP & keyof MR ? MP[MK_7] : never, MK_7 extends keyof MP & keyof MR ? MP[MK_7] : never>, () => boolean];
137
159
  /** useSelector + selectEntityById. */
138
160
  useSelectEntityById: <TN_2 extends keyof T>(id: Key | null | undefined, typename: TN_2) => T[TN_2] | undefined;
139
161
  };
@@ -4,9 +4,9 @@ exports.createCache = void 0;
4
4
  const react_1 = require("react");
5
5
  const react_redux_1 = require("react-redux");
6
6
  const createActions_1 = require("./createActions");
7
+ const createCacheReducer_1 = require("./createCacheReducer");
7
8
  const mutate_1 = require("./mutate");
8
9
  const query_1 = require("./query");
9
- const reducer_1 = require("./reducer");
10
10
  const useMutation_1 = require("./useMutation");
11
11
  const useQuery_1 = require("./useQuery");
12
12
  const utilsAndConstants_1 = require("./utilsAndConstants");
@@ -48,7 +48,7 @@ const createCache = (partialCache) => {
48
48
  /** Keeps all options, passed while creating the cache. */
49
49
  cache,
50
50
  /** Reducer of the cache, should be added to redux store. */
51
- reducer: (0, reducer_1.createCacheReducer)(actions, cache.typenames, Object.keys(cache.queries), cache.options),
51
+ reducer: (0, createCacheReducer_1.createCacheReducer)(actions, cache.typenames, Object.keys(cache.queries), cache.options),
52
52
  actions,
53
53
  selectors: {
54
54
  /** Selects query state. */
@@ -69,6 +69,10 @@ const createCache = (partialCache) => {
69
69
  selectQueryParams: (state, query, cacheKey) => {
70
70
  return selectQueryState(state, query, cacheKey).params;
71
71
  },
72
+ /** Selects query expiresAt value. */
73
+ selectQueryExpiresAt: (state, query, cacheKey) => {
74
+ return selectQueryState(state, query, cacheKey).expiresAt;
75
+ },
72
76
  /** Selects mutation state. */
73
77
  selectMutationState,
74
78
  /** Selects mutation latest result. */
@@ -106,11 +110,13 @@ const createCache = (partialCache) => {
106
110
  const client = {
107
111
  query: (options) => {
108
112
  var _a;
109
- const { query: queryKey, params } = options;
113
+ const { query: queryKey, params, onlyIfExpired, secondsToLive, mergeResults } = options;
110
114
  const getCacheKey = (_a = cache.queries[queryKey].getCacheKey) !== null && _a !== void 0 ? _a : (utilsAndConstants_1.defaultGetCacheKey);
111
115
  // @ts-expect-error fix later
112
116
  const cacheKey = getCacheKey(params);
113
- return (0, query_1.query)('query', store, cache, actions, queryKey, cacheKey, params);
117
+ return (0, query_1.query)('query', store, cache, actions, queryKey, cacheKey, params, secondsToLive, onlyIfExpired,
118
+ // @ts-expect-error fix later
119
+ mergeResults);
114
120
  },
115
121
  mutate: (options) => {
116
122
  return (0, mutate_1.mutate)('mutate', store, cache, actions, options.mutation, options.params, abortControllers);
@@ -119,7 +125,7 @@ const createCache = (partialCache) => {
119
125
  return client;
120
126
  }, [store]);
121
127
  },
122
- /** Fetches query when params change and subscribes to query state. */
128
+ /** Fetches query when params change and subscribes to query state changes (except `expiresAt` field). */
123
129
  useQuery: (options) => (0, useQuery_1.useQuery)(cache, actions, options),
124
130
  /** Subscribes to provided mutation state and provides mutate function. */
125
131
  useMutation: (options) => (0, useMutation_1.useMutation)(cache, actions, options, abortControllers),
@@ -1,27 +1,34 @@
1
1
  import type { ActionMap } from './createActions';
2
- import type { CacheOptions, Dict, EntitiesMap, QueryMutationState, Typenames } from './types';
2
+ import type { CacheOptions, Dict, EntitiesMap, MutationState, QueryState, Typenames } from './types';
3
3
  export type ReduxCacheState<T extends Typenames, QP, QR, MP, MR> = ReturnType<ReturnType<typeof createCacheReducer<string, T, QP, QR, MP, MR>>>;
4
4
  export declare const createCacheReducer: <N extends string, T extends Typenames, QP, QR, MP, MR>(actions: ActionMap<N, T, QP, QR, MP, MR>, typenames: T, queryKeys: (keyof QP & keyof QR)[], cacheOptions: CacheOptions) => (state: {
5
5
  entities: EntitiesMap<T>;
6
- queries: QP | QR extends infer T_1 ? { [QK in keyof T_1]: Dict<QueryMutationState<QP[QK], QR[QK]>>; } : never;
7
- mutations: MP | MR extends infer T_2 ? { [MK in keyof T_2]: QueryMutationState<MP[MK], MR[MK]>; } : never;
6
+ queries: QP | QR extends infer T_1 ? { [QK in keyof T_1]: Dict<QueryState<QP[QK], QR[QK]> | undefined>; } : never;
7
+ mutations: MP | MR extends infer T_2 ? { [MK in keyof T_2]: MutationState<MP[MK], MR[MK]>; } : never;
8
8
  } | undefined, action: {
9
9
  type: `@rrc/${N}/updateQueryStateAndEntities`;
10
10
  queryKey: keyof QP & keyof QR;
11
11
  queryCacheKey: import("./types").Key;
12
- state: Partial<QueryMutationState<QP[keyof QP & keyof QR], QR[keyof QP & keyof QR]>> | undefined;
12
+ state: Partial<QueryState<QP[keyof QP & keyof QR], QR[keyof QP & keyof QR]>> | undefined;
13
13
  entityChanges: import("./types").EntityChanges<T> | undefined;
14
14
  } | {
15
15
  type: `@rrc/${N}/updateMutationStateAndEntities`;
16
16
  mutationKey: keyof MP & keyof MR;
17
- state: Partial<QueryMutationState<MP[keyof MP & keyof MR], MR[keyof MP & keyof MR]>> | undefined;
17
+ state: Partial<MutationState<MP[keyof MP & keyof MR], MR[keyof MP & keyof MR]>> | undefined;
18
18
  entityChanges: import("./types").EntityChanges<T> | undefined;
19
19
  } | {
20
20
  type: `@rrc/${N}/mergeEntityChanges`;
21
21
  changes: import("./types").EntityChanges<T>;
22
+ } | {
23
+ type: `@rrc/${N}/invalidateQuery`;
24
+ queries: {
25
+ key: keyof QP & keyof QR;
26
+ cacheKey?: import("./types").Key | undefined;
27
+ expiresAt?: number | undefined;
28
+ }[];
22
29
  } | {
23
30
  type: `@rrc/${N}/clearQueryState`;
24
- queryKeys: {
31
+ queries: {
25
32
  key: keyof QP & keyof QR;
26
33
  cacheKey?: import("./types").Key | undefined;
27
34
  }[];
@@ -30,6 +37,6 @@ export declare const createCacheReducer: <N extends string, T extends Typenames,
30
37
  mutationKeys: (keyof MP & keyof MR)[];
31
38
  }) => {
32
39
  entities: EntitiesMap<T>;
33
- queries: QP | QR extends infer T_1 ? { [QK in keyof T_1]: Dict<QueryMutationState<QP[QK], QR[QK]>>; } : never;
34
- mutations: MP | MR extends infer T_2 ? { [MK in keyof T_2]: QueryMutationState<MP[MK], MR[MK]>; } : never;
40
+ queries: QP | QR extends infer T_1 ? { [QK in keyof T_1]: Dict<QueryState<QP[QK], QR[QK]> | undefined>; } : never;
41
+ mutations: MP | MR extends infer T_2 ? { [MK in keyof T_2]: MutationState<MP[MK], MR[MK]>; } : never;
35
42
  };
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createCacheReducer = void 0;
4
4
  const utilsAndConstants_1 = require("./utilsAndConstants");
5
5
  const EMPTY_QUERY_STATE = Object.freeze({});
6
+ const optionalQueryKeys = ['error', 'expiresAt', 'result', 'params'];
7
+ const optionalMutationKeys = ['error', 'result', 'params'];
6
8
  const createCacheReducer = (actions, typenames, queryKeys, cacheOptions) => {
7
9
  const entitiesMap = {};
8
10
  for (const key in typenames) {
@@ -32,6 +34,14 @@ const createCacheReducer = (actions, typenames, queryKeys, cacheOptions) => {
32
34
  const { queryKey, queryCacheKey, state: queryState, entityChanges, } = action;
33
35
  const oldQueryState = (_a = state.queries[queryKey][queryCacheKey]) !== null && _a !== void 0 ? _a : utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE;
34
36
  let newQueryState = queryState && Object.assign(Object.assign({}, oldQueryState), queryState);
37
+ // remove undefined optional fields
38
+ if (newQueryState) {
39
+ for (const key of optionalQueryKeys) {
40
+ if (key in newQueryState && newQueryState[key] === undefined) {
41
+ delete newQueryState[key];
42
+ }
43
+ }
44
+ }
35
45
  if (deepEqual === null || deepEqual === void 0 ? void 0 : deepEqual(oldQueryState, newQueryState)) {
36
46
  newQueryState = undefined;
37
47
  }
@@ -51,6 +61,14 @@ const createCacheReducer = (actions, typenames, queryKeys, cacheOptions) => {
51
61
  const { mutationKey, state: mutationState, entityChanges, } = action;
52
62
  const oldMutationState = (_b = state.mutations[mutationKey]) !== null && _b !== void 0 ? _b : utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE;
53
63
  let newMutationState = mutationState && Object.assign(Object.assign({}, oldMutationState), mutationState);
64
+ // remove undefined optional fields
65
+ if (newMutationState) {
66
+ for (const key of optionalMutationKeys) {
67
+ if (key in newMutationState && newMutationState[key] === undefined) {
68
+ delete newMutationState[key];
69
+ }
70
+ }
71
+ }
54
72
  if (deepEqual === null || deepEqual === void 0 ? void 0 : deepEqual(oldMutationState, newMutationState)) {
55
73
  newMutationState = undefined;
56
74
  }
@@ -71,26 +89,71 @@ const createCacheReducer = (actions, typenames, queryKeys, cacheOptions) => {
71
89
  const newEntities = (0, utilsAndConstants_1.applyEntityChanges)(state.entities, changes, cacheOptions);
72
90
  return newEntities ? Object.assign(Object.assign({}, state), { entities: newEntities }) : state;
73
91
  }
92
+ case actions.invalidateQuery.type: {
93
+ const { queries: queriesToInvalidate } = action;
94
+ if (!queriesToInvalidate.length) {
95
+ return state;
96
+ }
97
+ const now = Date.now();
98
+ let newQueries = undefined;
99
+ for (const { key, cacheKey, expiresAt = now } of queriesToInvalidate) {
100
+ const queryStates = (newQueries !== null && newQueries !== void 0 ? newQueries : state.queries)[key];
101
+ if (cacheKey != null) {
102
+ if (queryStates[cacheKey]) {
103
+ const queryState = queryStates[cacheKey];
104
+ if (queryState && queryState.expiresAt !== expiresAt) {
105
+ newQueries !== null && newQueries !== void 0 ? newQueries : (newQueries = Object.assign({}, state.queries));
106
+ if (state.queries[key] === newQueries[key]) {
107
+ newQueries[key] = Object.assign({}, newQueries[key]);
108
+ }
109
+ // @ts-expect-error fix type later
110
+ newQueries[key][cacheKey] = Object.assign(Object.assign({}, queryState), { expiresAt });
111
+ if (expiresAt === undefined) {
112
+ delete newQueries[key][cacheKey].expiresAt;
113
+ }
114
+ }
115
+ }
116
+ }
117
+ else {
118
+ for (const cacheKey in queryStates) {
119
+ const queryState = queryStates[cacheKey];
120
+ if (queryState && queryState.expiresAt !== expiresAt) {
121
+ newQueries !== null && newQueries !== void 0 ? newQueries : (newQueries = Object.assign({}, state.queries));
122
+ if (state.queries[key] === newQueries[key]) {
123
+ newQueries[key] = Object.assign({}, newQueries[key]);
124
+ }
125
+ newQueries[key][cacheKey] = Object.assign(Object.assign({}, queryState), { expiresAt });
126
+ if (expiresAt === undefined) {
127
+ delete newQueries[key][cacheKey].expiresAt;
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ return !newQueries
134
+ ? state
135
+ : Object.assign(Object.assign({}, state), { queries: newQueries });
136
+ }
74
137
  case actions.clearQueryState.type: {
75
- const { queryKeys: queryKeysToClear } = action;
76
- if (!queryKeysToClear.length) {
138
+ const { queries: queriesToClear } = action;
139
+ if (!queriesToClear.length) {
77
140
  return state;
78
141
  }
79
142
  let newQueries = undefined;
80
- for (const query of queryKeysToClear) {
81
- const queryState = (newQueries !== null && newQueries !== void 0 ? newQueries : state.queries)[query.key];
82
- if (query.cacheKey != null) {
83
- if (queryState[query.cacheKey]) {
143
+ for (const { key, cacheKey } of queriesToClear) {
144
+ const queryStates = (newQueries !== null && newQueries !== void 0 ? newQueries : state.queries)[key];
145
+ if (cacheKey != null) {
146
+ if (queryStates[cacheKey]) {
84
147
  newQueries !== null && newQueries !== void 0 ? newQueries : (newQueries = Object.assign({}, state.queries));
85
- if (state.queries[query.key] === newQueries[query.key]) {
86
- newQueries[query.key] = Object.assign({}, newQueries[query.key]);
148
+ if (state.queries[key] === newQueries[key]) {
149
+ newQueries[key] = Object.assign({}, newQueries[key]);
87
150
  }
88
- delete newQueries[query.key][query.cacheKey];
151
+ delete newQueries[key][cacheKey];
89
152
  }
90
153
  }
91
- else if (queryState !== EMPTY_QUERY_STATE) {
154
+ else if (queryStates !== EMPTY_QUERY_STATE) {
92
155
  newQueries !== null && newQueries !== void 0 ? newQueries : (newQueries = Object.assign({}, state.queries));
93
- newQueries[query.key] = EMPTY_QUERY_STATE;
156
+ newQueries[key] = EMPTY_QUERY_STATE;
94
157
  }
95
158
  }
96
159
  return !newQueries
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { createCache } from './createCache';
2
- export type { ReduxCacheState } from './reducer';
2
+ export type { ReduxCacheState } from './createCacheReducer';
3
3
  export * from './types';
4
4
  export { defaultGetCacheKey, DEFAULT_QUERY_MUTATION_STATE as defaultQueryMutationState, } from './utilsAndConstants';
package/dist/index.js CHANGED
@@ -22,18 +22,20 @@ var utilsAndConstants_1 = require("./utilsAndConstants");
22
22
  Object.defineProperty(exports, "defaultGetCacheKey", { enumerable: true, get: function () { return utilsAndConstants_1.defaultGetCacheKey; } });
23
23
  Object.defineProperty(exports, "defaultQueryMutationState", { enumerable: true, get: function () { return utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE; } });
24
24
  // Backlog
25
- // ! high
26
- // optimistic response
27
- // type extractors?
28
- // skip -> enabled?
29
- // add full api docs
30
- // update readme with how to use mergeResults for invalidating / updating caches?
31
- // make query key / cache key difference more clear in the docs, and/or rename queryKey -> query?
25
+ // ! high (1.0.0)
26
+ // key -> query
27
+ // rca -> vite
28
+ // defaults
29
+ // remove cachePolicy? make skip/enabled a function? skip -> enabled/shouldFetch?
30
+ // optional typenames: {} as Typenames
31
+ // remove mergeResults? bcs store is passed to queries/mutations
32
+ // remove undefined optional fields & emtpy states
33
+ // generate full api docs
32
34
  // ! medium
35
+ // optimistic response
36
+ // make query key / cache key difference more clear in the docs
33
37
  // check type of function arguments in dev
34
38
  // allow multiple mutation with same keys?
35
- // type extractors from cache
36
- // custom useStore
37
39
  // return back deserialize selector?
38
40
  // selector for entities by typename
39
41
  // callback option on error / success?
@@ -42,6 +44,7 @@ Object.defineProperty(exports, "defaultQueryMutationState", { enumerable: true,
42
44
  // deep equal entities while merging state
43
45
  // make error type generic
44
46
  // ! low
47
+ // custom useStore & useSelector to support multiple stores?
45
48
  // access to currently loading queries and mutations?
46
49
  // add params to the state?
47
50
  // cancellation to queries
package/dist/mutate.js CHANGED
@@ -44,7 +44,7 @@ const mutate = (logTag, store, cache, { updateMutationStateAndEntities, }, mutat
44
44
  try {
45
45
  response = yield fetchFn(
46
46
  // @ts-expect-error fix later
47
- params, abortController.signal);
47
+ params, store, abortController.signal);
48
48
  }
49
49
  catch (e) {
50
50
  error = e;
@@ -67,12 +67,12 @@ const mutate = (logTag, store, cache, { updateMutationStateAndEntities, }, mutat
67
67
  return { error };
68
68
  }
69
69
  if (response) {
70
- store.dispatch(updateMutationStateAndEntities(mutationKey, {
70
+ const newState = {
71
71
  error: undefined,
72
72
  loading: false,
73
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
74
73
  result: response.result,
75
- }, response));
74
+ };
75
+ store.dispatch(updateMutationStateAndEntities(mutationKey, newState, response));
76
76
  // @ts-expect-error fix later
77
77
  return { result: response.result };
78
78
  }
package/dist/query.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { Store } from 'redux';
2
2
  import type { ActionMap } from './createActions';
3
3
  import type { Cache, Key, QueryResult, Typenames } from './types';
4
- export declare const query: <N extends string, T extends Typenames, QP, QR, MP, MR, QK extends keyof QP | keyof QR>(logTag: string, store: Store, cache: Cache<N, T, QP, QR, MP, MR>, { updateQueryStateAndEntities, }: Pick<ActionMap<N, T, QP, QR, unknown, unknown>, "updateQueryStateAndEntities">, queryKey: QK, cacheKey: Key, params: QK extends keyof QP & keyof QR ? QP[QK] : never) => Promise<QueryResult<QK extends keyof QP & keyof QR ? QR[QK] : never>>;
4
+ export declare const query: <N extends string, T extends Typenames, QP, QR, MP, MR, QK extends keyof QP | keyof QR>(logTag: string, store: Store, cache: Cache<N, T, QP, QR, MP, MR>, { updateQueryStateAndEntities, }: Pick<ActionMap<N, T, QP, QR, unknown, unknown>, "updateQueryStateAndEntities">, queryKey: QK, cacheKey: Key, params: QK extends keyof QP & keyof QR ? QP[QK] : never, secondsToLive: number | undefined, onlyIfExpired: boolean | undefined, mergeResults?: ((oldResult: QR[keyof QP & keyof QR & string] | undefined, response: import("./types").QueryResponse<T, QR[keyof QP & keyof QR & string]>, params: QP[keyof QP & keyof QR & string] | undefined, store: Store<any, import("redux").AnyAction>) => QR[keyof QP & keyof QR & string]) | ((oldResult: QR[keyof QP & keyof QR & number] | undefined, response: import("./types").QueryResponse<T, QR[keyof QP & keyof QR & number]>, params: QP[keyof QP & keyof QR & number] | undefined, store: Store<any, import("redux").AnyAction>) => QR[keyof QP & keyof QR & number]) | ((oldResult: QR[keyof QP & keyof QR & symbol] | undefined, response: import("./types").QueryResponse<T, QR[keyof QP & keyof QR & symbol]>, params: QP[keyof QP & keyof QR & symbol] | undefined, store: Store<any, import("redux").AnyAction>) => QR[keyof QP & keyof QR & symbol]) | undefined) => Promise<QueryResult<QK extends keyof QP & keyof QR ? QR[QK] : never>>;
package/dist/query.js CHANGED
@@ -11,11 +11,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.query = void 0;
13
13
  const utilsAndConstants_1 = require("./utilsAndConstants");
14
- const query = (logTag, store, cache, { updateQueryStateAndEntities, }, queryKey, cacheKey, params) => __awaiter(void 0, void 0, void 0, function* () {
15
- var _a;
14
+ const query = (logTag, store, cache, { updateQueryStateAndEntities, }, queryKey, cacheKey, params, secondsToLive = cache.queries[queryKey].secondsToLive, onlyIfExpired, mergeResults = cache.queries[queryKey].mergeResults) => __awaiter(void 0, void 0, void 0, function* () {
15
+ var _a, _b;
16
16
  const logsEnabled = cache.options.logsEnabled;
17
17
  const cacheStateSelector = cache.cacheStateSelector;
18
- const mergeResults = cache.queries[queryKey].mergeResults;
19
18
  const queryStateOnStart = cacheStateSelector(store.getState()).queries[queryKey][cacheKey];
20
19
  if (queryStateOnStart === null || queryStateOnStart === void 0 ? void 0 : queryStateOnStart.loading) {
21
20
  logsEnabled &&
@@ -26,17 +25,27 @@ const query = (logTag, store, cache, { updateQueryStateAndEntities, }, queryKey,
26
25
  });
27
26
  return CANCELLED_RESULT;
28
27
  }
28
+ if (onlyIfExpired && (queryStateOnStart === null || queryStateOnStart === void 0 ? void 0 : queryStateOnStart.expiresAt) != null && queryStateOnStart.expiresAt > Date.now()) {
29
+ logsEnabled &&
30
+ (0, utilsAndConstants_1.log)(`${logTag} cancelled: not expired yet`, {
31
+ queryStateOnStart,
32
+ params,
33
+ cacheKey,
34
+ onlyIfExpired,
35
+ });
36
+ return CANCELLED_RESULT;
37
+ }
29
38
  store.dispatch(updateQueryStateAndEntities(queryKey, cacheKey, {
30
39
  loading: true,
31
40
  params,
32
41
  }));
33
- logsEnabled && (0, utilsAndConstants_1.log)(`${logTag} started`, { queryStateOnStart, params, cacheKey });
42
+ logsEnabled && (0, utilsAndConstants_1.log)(`${logTag} started`, { queryKey, params, cacheKey, queryStateOnStart, onlyIfExpired });
34
43
  let response;
35
44
  const fetchFn = cache.queries[queryKey].query;
36
45
  try {
37
46
  response = yield fetchFn(
38
47
  // @ts-expect-error fix later
39
- params);
48
+ params, store);
40
49
  }
41
50
  catch (error) {
42
51
  store.dispatch(updateQueryStateAndEntities(queryKey, cacheKey, {
@@ -48,10 +57,11 @@ const query = (logTag, store, cache, { updateQueryStateAndEntities, }, queryKey,
48
57
  const newState = {
49
58
  error: undefined,
50
59
  loading: false,
60
+ expiresAt: (_a = response.expiresAt) !== null && _a !== void 0 ? _a : (secondsToLive != null ? Date.now() + secondsToLive * 1000 : undefined),
51
61
  result: mergeResults
52
62
  ? mergeResults(
53
63
  // @ts-expect-error fix later
54
- (_a = cacheStateSelector(store.getState()).queries[queryKey][cacheKey]) === null || _a === void 0 ? void 0 : _a.result, response, params, store)
64
+ (_b = cacheStateSelector(store.getState()).queries[queryKey][cacheKey]) === null || _b === void 0 ? void 0 : _b.result, response, params, store)
55
65
  : response.result,
56
66
  };
57
67
  store.dispatch(updateQueryStateAndEntities(queryKey, cacheKey, newState, response));
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Store } from 'redux';
2
- import type { ReduxCacheState } from './reducer';
2
+ import type { ReduxCacheState } from './createCacheReducer';
3
3
  export type Key = string | number | symbol;
4
4
  export type Dict<T> = Record<Key, T>;
5
5
  export type OptionalPartial<T, K extends keyof T> = Partial<{
@@ -69,14 +69,20 @@ export type EntitiesMap<T extends Typenames> = {
69
69
  export type EntityIds<T extends Typenames> = {
70
70
  [K in keyof T]?: Key[];
71
71
  };
72
- export type Query<T extends Typenames, P, R> = (params: P) => Promise<QueryResponse<T, R>>;
72
+ export type Query<P, T extends Typenames = Typenames, R = unknown> = (
73
+ /** Query parameters */
74
+ params: P,
75
+ /** Redux store */
76
+ store: Store) => Promise<QueryResponse<T, R>>;
73
77
  export type QueryInfo<T extends Typenames, P, R> = {
74
- query: Query<T, P, R>;
78
+ query: Query<P, T, R>;
75
79
  /**
76
80
  * Cache policy.
77
81
  * @default cache-first
78
82
  */
79
83
  cachePolicy?: QueryCachePolicy;
84
+ /** If set, this value updates expiresAt value of query state when query resut is received. */
85
+ secondsToLive?: number;
80
86
  /** Merges results before saving to the store. Default implementation is using the latest result. */
81
87
  mergeResults?: (oldResult: R | undefined, response: QueryResponse<T, R>, params: P | undefined, store: Store) => R;
82
88
  /**
@@ -86,34 +92,45 @@ export type QueryInfo<T extends Typenames, P, R> = {
86
92
  * */
87
93
  getCacheKey?: (params?: P) => Key;
88
94
  };
95
+ export type QueryState<P, R> = MutationState<P, R> & {
96
+ expiresAt?: number;
97
+ };
89
98
  export type UseQueryOptions<T extends Typenames, QP, QR, QK extends keyof (QP & QR)> = {
90
99
  query: QK;
91
100
  params: QK extends keyof (QP | QR) ? QP[QK] : never;
101
+ /** When true fetch is not performed. When switches to false fetch is performed depending on cache policy. */
92
102
  skip?: boolean;
93
- } & Pick<QueryInfo<T, unknown, unknown>, 'cachePolicy'>;
103
+ } & Pick<QueryInfo<T, QK extends keyof (QP | QR) ? QP[QK] : never, QK extends keyof (QP | QR) ? QR[QK] : never>, 'cachePolicy' | 'secondsToLive' | 'mergeResults'>;
94
104
  /**
95
105
  * @param cache-first for each params key fetch is not called if cache exists.
96
106
  * @param cache-and-fetch for each params key result is taken from cache and fetch is called.
97
107
  */
98
108
  export type QueryCachePolicy = 'cache-first' | 'cache-and-fetch';
99
109
  export type QueryResponse<T extends Typenames, R> = EntityChanges<T> & {
100
- /** Normalized result of a query. */
101
110
  result: R;
111
+ /** If defined, overrides this value for query state, ignoring `secondsToLive`. */
112
+ expiresAt?: number;
102
113
  };
103
114
  export type QueryResult<R> = {
104
115
  error?: unknown;
105
116
  cancelled?: true;
106
117
  result?: R;
107
118
  };
108
- export type QueryOptions<T extends Typenames, QP, QR, QK extends keyof (QP & QR)> = Omit<UseQueryOptions<T, QP, QR, QK>, 'skip'>;
109
- export type Mutation<T extends Typenames, P, R> = (params: P,
119
+ export type QueryOptions<T extends Typenames, QP, QR, QK extends keyof (QP & QR)> = Omit<UseQueryOptions<T, QP, QR, QK>, 'skip' | 'cachePolicy'> & {
120
+ /** If set to true, query will run only if it is expired or result not yet cached. */
121
+ onlyIfExpired?: boolean;
122
+ };
123
+ export type Mutation<P, T extends Typenames = Typenames, R = unknown> = (
124
+ /** Mutation parameters */
125
+ params: P,
126
+ /** Redux store */
127
+ store: Store,
110
128
  /** Signal is aborted for current mutation when the same mutation was called once again. */
111
129
  abortSignal: AbortSignal) => Promise<MutationResponse<T, R>>;
112
130
  export type MutationInfo<T extends Typenames, P, R> = {
113
- mutation: Mutation<T, P, R>;
131
+ mutation: Mutation<P, T, R>;
114
132
  };
115
133
  export type MutationResponse<T extends Typenames, R> = EntityChanges<T> & {
116
- /** Normalized result of a mutation. */
117
134
  result?: R;
118
135
  };
119
136
  export type MutationResult<R> = {
@@ -121,7 +138,7 @@ export type MutationResult<R> = {
121
138
  aborted?: true;
122
139
  result?: R;
123
140
  };
124
- export type QueryMutationState<P, R> = {
141
+ export type MutationState<P, R> = {
125
142
  /** `true` when query or mutation is currently in progress. */
126
143
  loading: boolean;
127
144
  /** Result of the latest successfull response. */
@@ -1,6 +1,6 @@
1
1
  import { Store } from 'redux';
2
2
  import { ActionMap } from './createActions';
3
- import { Cache, Key, QueryMutationState, Typenames } from './types';
3
+ import { Cache, Key, MutationState, Typenames } from './types';
4
4
  export declare const useMutation: <N extends string, T extends Typenames, MP, MR, MK extends keyof MP | keyof MR>(cache: Cache<N, T, unknown, unknown, MP, MR>, actions: Pick<ActionMap<N, T, unknown, unknown, MP, MR>, "updateMutationStateAndEntities">, options: {
5
5
  mutation: MK;
6
- }, abortControllers: WeakMap<Store, Record<Key, AbortController>>) => readonly [(params: MK extends keyof MP & keyof MR ? MP[MK] : never) => Promise<import("./types").MutationResult<MK extends infer T_1 ? T_1 extends MK ? T_1 extends keyof MP & keyof MR ? MR[T_1] : never : never : never>>, QueryMutationState<MK extends keyof MP & keyof MR ? MP[MK] : never, MK extends keyof MP & keyof MR ? MP[MK] : never>, () => boolean];
6
+ }, abortControllers: WeakMap<Store, Record<Key, AbortController>>) => readonly [(params: MK extends keyof MP & keyof MR ? MP[MK] : never) => Promise<import("./types").MutationResult<MK extends infer T_1 ? T_1 extends MK ? T_1 extends keyof MP & keyof MR ? MR[T_1] : never : never : never>>, MutationState<MK extends keyof MP & keyof MR ? MP[MK] : never, MK extends keyof MP & keyof MR ? MP[MK] : never>, () => boolean];
@@ -1,5 +1,3 @@
1
1
  import { ActionMap } from './createActions';
2
- import { Cache, QueryMutationState, Typenames, UseQueryOptions } from './types';
3
- export declare const useQuery: <N extends string, T extends Typenames, QP, QR, MP, MR, QK extends keyof QP | keyof QR>(cache: Cache<N, T, QP, QR, MP, MR>, actions: Pick<ActionMap<N, T, QP, QR, MP, MR>, "updateQueryStateAndEntities">, options: UseQueryOptions<T, QP, QR, QK>) => readonly [QueryMutationState<QK extends keyof QP & keyof QR ? QP[QK] : never, QK extends keyof QP & keyof QR ? QR[QK] : never>, (options?: {
4
- params: QK extends keyof QP & keyof QR ? QP[QK] : never;
5
- } | undefined) => Promise<import("./types").QueryResult<QK extends infer T_1 ? T_1 extends QK ? T_1 extends keyof QP & keyof QR ? QR[T_1] : never : never : never>>];
2
+ import { Cache, QueryOptions, QueryState, Typenames, UseQueryOptions } from './types';
3
+ export declare const useQuery: <N extends string, T extends Typenames, QP, QR, MP, MR, QK extends keyof QP | keyof QR>(cache: Cache<N, T, QP, QR, MP, MR>, actions: Pick<ActionMap<N, T, QP, QR, MP, MR>, "updateQueryStateAndEntities">, options: UseQueryOptions<T, QP, QR, QK>) => readonly [Omit<QueryState<QK extends keyof QP & keyof QR ? QP[QK] : never, QK extends keyof QP & keyof QR ? QR[QK] : never>, "expiresAt">, (options?: Partial<Pick<QueryOptions<T, QP, QR, QK>, "params" | "onlyIfExpired">> | undefined) => Promise<import("./types").QueryResult<QK extends infer T_1 ? T_1 extends QK ? T_1 extends keyof QP & keyof QR ? QR[T_1] : never : never : never>>];
package/dist/useQuery.js CHANGED
@@ -16,7 +16,7 @@ const query_1 = require("./query");
16
16
  const utilsAndConstants_1 = require("./utilsAndConstants");
17
17
  const useQuery = (cache, actions, options) => {
18
18
  var _a, _b, _c;
19
- const { query: queryKey, skip, params, cachePolicy = (_a = cache.queries[queryKey].cachePolicy) !== null && _a !== void 0 ? _a : 'cache-first', } = options;
19
+ const { query: queryKey, skip, params, secondsToLive, cachePolicy = (_a = cache.queries[queryKey].cachePolicy) !== null && _a !== void 0 ? _a : 'cache-first', mergeResults, } = options;
20
20
  const logsEnabled = cache.options.logsEnabled;
21
21
  const getCacheKey = (_b = cache.queries[queryKey].getCacheKey) !== null && _b !== void 0 ? _b : (utilsAndConstants_1.defaultGetCacheKey);
22
22
  const cacheStateSelector = cache.cacheStateSelector;
@@ -25,9 +25,13 @@ const useQuery = (cache, actions, options) => {
25
25
  const cacheKey = getCacheKey(params);
26
26
  /** Fetch query with the new parameters, or refetch with the same if parameters not provided. */
27
27
  const fetch = (0, react_1.useCallback)((options) => __awaiter(void 0, void 0, void 0, function* () {
28
+ const paramsPassed = options && 'params' in options;
28
29
  return yield (0, query_1.query)('useQuery.fetch', store, cache, actions, queryKey,
29
30
  // @ts-expect-error fix later
30
- options ? getCacheKey(options.params) : cacheKey, options ? options.params : params);
31
+ paramsPassed ? getCacheKey(options.params) : cacheKey, paramsPassed ? options.params : params, // params type can also have null | undefined, thats why we don't check for it here
32
+ secondsToLive, options === null || options === void 0 ? void 0 : options.onlyIfExpired,
33
+ // @ts-expect-error fix later
34
+ mergeResults);
31
35
  }),
32
36
  // eslint-disable-next-line react-hooks/exhaustive-deps
33
37
  [store, queryKey, cacheKey]);
@@ -35,16 +39,20 @@ const useQuery = (cache, actions, options) => {
35
39
  const queryState = (_c = (0, react_redux_1.useSelector)((state) => {
36
40
  const queryState = cacheStateSelector(state).queries[queryKey][cacheKey];
37
41
  return queryState; // TODO proper type
38
- })) !== null && _c !== void 0 ? _c : utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE;
42
+ }, (useQueryStateComparer))) !== null && _c !== void 0 ? _c : utilsAndConstants_1.DEFAULT_QUERY_MUTATION_STATE;
39
43
  (0, react_1.useEffect)(() => {
40
44
  if (skip) {
41
45
  logsEnabled && (0, utilsAndConstants_1.log)('useQuery.useEffect skip fetch', { skip, cacheKey });
42
46
  return;
43
47
  }
44
- if (queryState.result != null && cachePolicy === 'cache-first') {
48
+ if (queryState.result != null &&
49
+ cachePolicy === 'cache-first' &&
50
+ (queryState.expiresAt == null || queryState.expiresAt > Date.now())) {
45
51
  logsEnabled &&
46
- (0, utilsAndConstants_1.log)('useQuery.useEffect don`t fetch due to cache policy', {
52
+ (0, utilsAndConstants_1.log)('useQuery.useEffect no fetch due to cache policy', {
47
53
  result: queryState.result,
54
+ expiresAt: queryState.expiresAt,
55
+ now: Date.now(),
48
56
  cachePolicy,
49
57
  });
50
58
  return;
@@ -61,3 +69,16 @@ const useQuery = (cache, actions, options) => {
61
69
  return [queryState, fetch];
62
70
  };
63
71
  exports.useQuery = useQuery;
72
+ /** Omit `expiresAt` from comparison */
73
+ const useQueryStateComparer = (state1, state2) => {
74
+ if (state1 === state2) {
75
+ return true;
76
+ }
77
+ if (state1 === undefined || state2 === undefined) {
78
+ return false;
79
+ }
80
+ return (state1.error === state2.error &&
81
+ state1.loading === state2.loading &&
82
+ state1.params === state2.params &&
83
+ state1.result === state2.result);
84
+ };
@@ -4,9 +4,9 @@ export declare const optionalUtils: {
4
4
  deepEqual?: (a: any, b: any) => boolean;
5
5
  };
6
6
  export declare const IS_DEV: boolean;
7
- export declare const DEFAULT_QUERY_MUTATION_STATE: {
8
- readonly loading: false;
9
- };
7
+ export declare const DEFAULT_QUERY_MUTATION_STATE: Readonly<{
8
+ loading: false;
9
+ }>;
10
10
  export declare const defaultGetCacheKey: <P = unknown>(params: P) => Key;
11
11
  export declare const log: (tag: string, data?: unknown) => void;
12
12
  export declare const applyEntityChanges: <T extends Typenames>(entities: EntitiesMap<T>, changes: EntityChanges<T>, options: CacheOptions) => EntitiesMap<T> | undefined;
@@ -20,7 +20,7 @@ exports.IS_DEV = (() => {
20
20
  return process.env.NODE_ENV === 'development';
21
21
  }
22
22
  })();
23
- exports.DEFAULT_QUERY_MUTATION_STATE = { loading: false };
23
+ exports.DEFAULT_QUERY_MUTATION_STATE = Object.freeze({ loading: false });
24
24
  const defaultGetCacheKey = (params) => {
25
25
  switch (typeof params) {
26
26
  case 'string':
@@ -109,11 +109,9 @@ const applyEntityChanges = (entities, changes, options) => {
109
109
  options.logsEnabled &&
110
110
  (0, exports.log)('applyEntityChanges', {
111
111
  entities,
112
- // eslint-disable-next-line @typescript-eslint/no-var-requires
113
- changes: require('util').inspect(changes, { depth: 4 }),
112
+ changes,
114
113
  options,
115
- // eslint-disable-next-line @typescript-eslint/no-var-requires
116
- result: require('util').inspect(result, { depth: 4 }),
114
+ result,
117
115
  });
118
116
  return result;
119
117
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "react-redux-cache",
3
3
  "author": "Alexander Danilov",
4
4
  "license": "MIT",
5
- "version": "0.6.0",
5
+ "version": "0.7.0-rc.0",
6
6
  "description": "Powerful data fetching and caching library that supports normalization, built on top of redux",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
@@ -12,6 +12,8 @@
12
12
  "lint": "yarn eslint src",
13
13
  "build": "yarn clean && yarn lint && tsc",
14
14
  "test": "node node_modules/jest/bin/jest.js",
15
+ "preminor-rc": "yarn build && npm version preminor --preid rc",
16
+ "publish-rc": "npm publish --tag rc",
15
17
  "prepublishOnly": "yarn build && yarn test"
16
18
  },
17
19
  "peerDependencies": {