swr-catalyst 0.2.2 → 0.3.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.
Files changed (30) hide show
  1. package/README.md +1 -0
  2. package/dist/__tests__/integration/errors/index.d.ts +12 -0
  3. package/dist/__tests__/integration/helpers/fetchers.d.ts +12 -0
  4. package/dist/__tests__/integration/helpers/index.d.ts +4 -0
  5. package/dist/__tests__/integration/helpers/testHelpers.d.ts +17 -0
  6. package/dist/__tests__/integration/hooks/concurrent.integration.test.d.ts +1 -0
  7. package/dist/__tests__/integration/hooks/useSWRCreate.integration.test.d.ts +1 -0
  8. package/dist/__tests__/integration/hooks/useSWRDelete.integration.test.d.ts +1 -0
  9. package/dist/__tests__/integration/hooks/useSWRUpdate.integration.test.d.ts +1 -0
  10. package/dist/__tests__/integration/scenarios/cacheConsistency.integration.test.d.ts +1 -0
  11. package/dist/__tests__/integration/scenarios/errorRecovery.integration.test.d.ts +1 -0
  12. package/dist/__tests__/integration/setup/basic.integration.test.d.ts +1 -0
  13. package/dist/__tests__/integration/setup/server.d.ts +3 -0
  14. package/dist/__tests__/integration/setup/vitest.setup.d.ts +1 -0
  15. package/dist/__tests__/integration/types/index.d.ts +10 -0
  16. package/dist/__tests__/integration/utilities/mutateByGroup.integration.test.d.ts +1 -0
  17. package/dist/__tests__/integration/utilities/mutateById.integration.test.d.ts +1 -0
  18. package/dist/__tests__/integration/utilities/resetCache.integration.test.d.ts +1 -0
  19. package/dist/errors/index.d.ts +1 -1
  20. package/dist/hooks/shared/helpers/createMutationError/index.d.ts +1 -1
  21. package/dist/hooks/useSWRCreate/index.d.ts +2 -2
  22. package/dist/hooks/useSWRCreate/types.d.ts +3 -4
  23. package/dist/hooks/useSWRDelete/index.d.ts +2 -2
  24. package/dist/hooks/useSWRDelete/types.d.ts +5 -7
  25. package/dist/hooks/useSWRUpdate/index.d.ts +2 -2
  26. package/dist/hooks/useSWRUpdate/types.d.ts +4 -5
  27. package/dist/index.d.ts +1 -1
  28. package/dist/swr-catalyst.js +50 -50
  29. package/dist/swr-catalyst.umd.cjs +1 -1
  30. package/package.json +10 -7
package/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  [![npm downloads](https://img.shields.io/npm/dm/swr-catalyst)](https://www.npmjs.com/package/swr-catalyst)
12
12
  [![CI](https://github.com/pedroab0/swr-catalyst/actions/workflows/ci.yml/badge.svg)](https://github.com/pedroab0/swr-catalyst/actions/workflows/ci.yml)
13
13
  [![codecov](https://codecov.io/github/pedroab0/swr-catalyst/graph/badge.svg?token=IL8XFDVYHU)](https://codecov.io/github/pedroab0/swr-catalyst)
14
+ [![CodeScene Average Code Health](https://codescene.io/projects/73545/status-badges/average-code-health)](https://codescene.io/projects/73545)
14
15
 
15
16
  [Features](#features) • [Installation](#installation) • [Quick Start](#quick-start) • [API Reference](#api-reference)
16
17
 
@@ -0,0 +1,12 @@
1
+ import { ValidationErrorResponse } from '../types';
2
+ /**
3
+ * Custom error class for API errors with status code and response body
4
+ */
5
+ export declare class ApiError extends Error {
6
+ status: number;
7
+ code?: string;
8
+ details?: Record<string, string>;
9
+ constructor(message: string, status: number, body?: ValidationErrorResponse);
10
+ isValidationError(): boolean;
11
+ isNotFound(): boolean;
12
+ }
@@ -0,0 +1,12 @@
1
+ import { Todo } from '../types';
2
+ export declare function fetchTodos(): Promise<Todo[]>;
3
+ export declare function fetchTodoById(id: number): Promise<Todo>;
4
+ export declare function createTodo(todo: Todo): Promise<Todo>;
5
+ export declare function updateTodo(id: string | number, data: Partial<{
6
+ title: string;
7
+ completed: boolean;
8
+ }>): Promise<Todo>;
9
+ export declare function deleteTodo(id: string | number): Promise<{
10
+ success: boolean;
11
+ id: number;
12
+ }>;
@@ -0,0 +1,4 @@
1
+ export type { Todo, ValidationErrorResponse } from '../types';
2
+ export { ApiError } from '../errors';
3
+ export { createTodo, deleteTodo, fetchTodoById, fetchTodos, updateTodo, } from './fetchers';
4
+ export { createMockTodo, renderHookWithGlobalCache, renderHookWithSWR, uniqueKeyId, } from './testHelpers';
@@ -0,0 +1,17 @@
1
+ import { Todo } from '../types';
2
+ /**
3
+ * Renders a hook with an isolated SWR cache.
4
+ * Use for hook tests where all hooks share the same wrapper context.
5
+ */
6
+ export declare function renderHookWithSWR<T>(hook: () => T): import('@testing-library/react').RenderHookResult<T, unknown>;
7
+ /**
8
+ * Renders a hook using the global SWR cache.
9
+ * Use for utility tests (mutateById, mutateByGroup, resetCache)
10
+ * where utilities operate on the global cache via `import { mutate } from 'swr'`.
11
+ */
12
+ export declare function renderHookWithGlobalCache<T>(hook: () => T): import('@testing-library/react').RenderHookResult<T, unknown>;
13
+ /**
14
+ * Generates a unique key ID to prevent cache collisions in global cache tests.
15
+ */
16
+ export declare function uniqueKeyId(prefix: string): string;
17
+ export declare function createMockTodo(overrides?: Partial<Todo>): Todo;
@@ -0,0 +1,3 @@
1
+ export declare const handlers: import('msw').HttpHandler[];
2
+ export declare const server: import('msw/node').SetupServerApi;
3
+ export declare function resetServerData(): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ export type Todo = {
2
+ id: number;
3
+ title: string;
4
+ completed: boolean;
5
+ };
6
+ export type ValidationErrorResponse = {
7
+ error: string;
8
+ code: string;
9
+ details?: Record<string, string>;
10
+ };
@@ -1,2 +1,2 @@
1
- export { MutationError } from './MutationErrors';
2
1
  export type { MutationErrorContext, MutationOperation, } from './MutationErrors/types';
2
+ export { MutationError } from './MutationErrors';
@@ -1,5 +1,5 @@
1
- import { MutationError } from '../../../../errors';
2
1
  import { SWRKey } from '../../../../types';
2
+ import { MutationError } from '../../../../errors';
3
3
  /**
4
4
  * Creates a standardized MutationError with contextual information.
5
5
  *
@@ -82,8 +82,8 @@ import { CreateFunction } from './types';
82
82
  *
83
83
  * // Later, you can use mutateByGroup('user-data') to revalidate all related caches
84
84
  */
85
- export declare function useSWRCreate<TData = unknown, TCache = unknown, TError = Error>(key: SWRKey<TData>, createFunction: CreateFunction<TData, TCache>, options?: MutateOptions<TCache, TData>): {
86
- trigger: (data: TData) => Promise<TCache | undefined>;
85
+ export declare function useSWRCreate<TData = unknown, TCache = unknown, TError = Error>(key: SWRKey<TData>, createFunction: CreateFunction<TData>, options?: MutateOptions<TCache, TData>): {
86
+ trigger: (data: TData) => Promise<unknown>;
87
87
  isMutating: boolean;
88
88
  error: TError | null;
89
89
  };
@@ -2,14 +2,13 @@
2
2
  * Function type for creating a new resource via API.
3
3
  *
4
4
  * @template TData - The type of data being sent to create the resource
5
- * @template TResult - The type of data returned from the API
6
5
  *
7
6
  * @param data - The data for the new resource
8
7
  * @returns Promise resolving to the created resource from the server
9
8
  *
10
9
  * @example
11
10
  * // Basic create function
12
- * const createTodo: CreateFunction<TodoInput, Todo> = async (data) => {
11
+ * const createTodo: CreateFunction<TodoInput> = async (data) => {
13
12
  * const response = await fetch('/api/todos', {
14
13
  * method: 'POST',
15
14
  * body: JSON.stringify(data),
@@ -19,9 +18,9 @@
19
18
  *
20
19
  * @example
21
20
  * // With axios
22
- * const createUser: CreateFunction<UserInput, User> = async (data) => {
21
+ * const createUser: CreateFunction<UserInput> = async (data) => {
23
22
  * const { data: user } = await axios.post('/api/users', data);
24
23
  * return user;
25
24
  * };
26
25
  */
27
- export type CreateFunction<TData, TResult> = (data: TData) => Promise<TResult>;
26
+ export type CreateFunction<TData> = (data: TData) => Promise<unknown>;
@@ -96,8 +96,8 @@ import { DeleteFunction } from './types';
96
96
  *
97
97
  * // Later, you can use mutateByGroup('user-data') to revalidate all related caches
98
98
  */
99
- export declare function useSWRDelete<TCache = unknown, TResult = unknown, TError = Error>(key: SWRKey, deleteFunction: DeleteFunction<TResult>, options?: MutateOptions<TCache, string | number>): {
100
- trigger: (id: string | number) => Promise<TResult | undefined>;
99
+ export declare function useSWRDelete<TCache = unknown, TError = Error>(key: SWRKey, deleteFunction: DeleteFunction, options?: MutateOptions<TCache, string | number>): {
100
+ trigger: (id: string | number) => Promise<unknown>;
101
101
  isMutating: boolean;
102
102
  error: TError | null;
103
103
  };
@@ -1,14 +1,12 @@
1
1
  /**
2
2
  * Function type for deleting a resource via API.
3
3
  *
4
- * @template TResult - The type of data returned from the API (e.g., confirmation message, deleted item)
5
- *
6
4
  * @param id - The unique identifier of the resource to delete
7
5
  * @returns Promise resolving to the API response (often a success message or the deleted item)
8
6
  *
9
7
  * @example
10
8
  * // Basic delete function
11
- * const deleteTodo: DeleteFunction<{ success: boolean }> = async (id) => {
9
+ * const deleteTodo: DeleteFunction = async (id) => {
12
10
  * const response = await fetch(`/api/todos/${id}`, {
13
11
  * method: 'DELETE',
14
12
  * });
@@ -17,14 +15,14 @@
17
15
  *
18
16
  * @example
19
17
  * // With axios, returning deleted item
20
- * const deleteUser: DeleteFunction<User> = async (id) => {
18
+ * const deleteUser: DeleteFunction = async (id) => {
21
19
  * const { data } = await axios.delete(`/api/users/${id}`);
22
20
  * return data;
23
21
  * };
24
22
  *
25
23
  * @example
26
24
  * // With error handling
27
- * const deletePost: DeleteFunction<void> = async (id) => {
25
+ * const deletePost: DeleteFunction = async (id) => {
28
26
  * const response = await fetch(`/api/posts/${id}`, {
29
27
  * method: 'DELETE',
30
28
  * });
@@ -35,11 +33,11 @@
35
33
  *
36
34
  * @example
37
35
  * // Returning just status code
38
- * const deleteComment: DeleteFunction<number> = async (id) => {
36
+ * const deleteComment: DeleteFunction = async (id) => {
39
37
  * const response = await fetch(`/api/comments/${id}`, {
40
38
  * method: 'DELETE',
41
39
  * });
42
40
  * return response.status;
43
41
  * };
44
42
  */
45
- export type DeleteFunction<TResult> = (id: string | number) => Promise<TResult>;
43
+ export type DeleteFunction = (id: string | number) => Promise<unknown>;
@@ -111,11 +111,11 @@ import { UpdateFunction } from './types';
111
111
  *
112
112
  * // Later, you can use mutateByGroup('user-data') to revalidate all related caches
113
113
  */
114
- export declare function useSWRUpdate<TData = unknown, TCache = unknown, TError = Error>(key: SWRKey, updateFunction: UpdateFunction<TData, TCache>, options?: MutateOptions<TCache, {
114
+ export declare function useSWRUpdate<TData = unknown, TCache = unknown, TError = Error>(key: SWRKey, updateFunction: UpdateFunction<TData>, options?: MutateOptions<TCache, {
115
115
  id: string | number;
116
116
  data: TData;
117
117
  }>): {
118
- trigger: (id: string | number, data: TData) => Promise<TCache | undefined>;
118
+ trigger: (id: string | number, data: TData) => Promise<unknown>;
119
119
  isMutating: boolean;
120
120
  error: TError | null;
121
121
  };
@@ -2,7 +2,6 @@
2
2
  * Function type for updating an existing resource via API.
3
3
  *
4
4
  * @template TData - The type of data being sent to update the resource
5
- * @template TResult - The type of data returned from the API
6
5
  *
7
6
  * @param id - The unique identifier of the resource to update
8
7
  * @param data - The partial or complete data to update the resource with
@@ -10,7 +9,7 @@
10
9
  *
11
10
  * @example
12
11
  * // Basic update function with PATCH
13
- * const updateTodo: UpdateFunction<TodoUpdate, Todo> = async (id, data) => {
12
+ * const updateTodo: UpdateFunction<TodoUpdate> = async (id, data) => {
14
13
  * const response = await fetch(`/api/todos/${id}`, {
15
14
  * method: 'PATCH',
16
15
  * body: JSON.stringify(data),
@@ -20,14 +19,14 @@
20
19
  *
21
20
  * @example
22
21
  * // With axios and PUT
23
- * const updateUser: UpdateFunction<UserUpdate, User> = async (id, data) => {
22
+ * const updateUser: UpdateFunction<UserUpdate> = async (id, data) => {
24
23
  * const { data: user } = await axios.put(`/api/users/${id}`, data);
25
24
  * return user;
26
25
  * };
27
26
  *
28
27
  * @example
29
28
  * // With error handling
30
- * const updatePost: UpdateFunction<PostUpdate, Post> = async (id, data) => {
29
+ * const updatePost: UpdateFunction<PostUpdate> = async (id, data) => {
31
30
  * const response = await fetch(`/api/posts/${id}`, {
32
31
  * method: 'PATCH',
33
32
  * body: JSON.stringify(data),
@@ -38,4 +37,4 @@
38
37
  * return response.json();
39
38
  * };
40
39
  */
41
- export type UpdateFunction<TData, TResult> = (id: string | number, data: TData) => Promise<TResult>;
40
+ export type UpdateFunction<TData> = (id: string | number, data: TData) => Promise<unknown>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from './errors';
2
+ export * from './utils';
2
3
  export * from './hooks';
3
4
  export * from './types';
4
- export * from './utils';
@@ -1,5 +1,5 @@
1
- import { useRef as S, useState as p, useEffect as $, useCallback as k } from "react";
2
- import { mutate as R, useSWRConfig as U } from "swr";
1
+ import { mutate as $, useSWRConfig as k } from "swr";
2
+ import { useRef as S, useState as p, useEffect as R, useCallback as U } from "react";
3
3
  class h extends Error {
4
4
  name = "MutationError";
5
5
  context;
@@ -141,44 +141,24 @@ class h extends Error {
141
141
  return !1;
142
142
  }
143
143
  }
144
- function v(r, t) {
145
- if (r === t)
146
- return !0;
147
- if (r == null || t == null)
148
- return r === t;
149
- if (typeof r != "object" || typeof t != "object")
150
- return !1;
151
- try {
152
- return JSON.stringify(r) === JSON.stringify(t);
153
- } catch {
154
- return !1;
155
- }
156
- }
157
- function x(r) {
158
- const t = r?.id, e = r?.group, i = r?.data, n = S(null);
159
- if (!r || !(t && i))
160
- return n.current = null, n.current;
161
- const a = n.current;
162
- return (!a || a.id !== t || a.group !== e || !v(a.data, i)) && (n.current = { id: t, group: e, data: i }), n.current;
163
- }
164
- const A = /#id:"((?:[^"\\]|\\.)*)"/, j = /group:"((?:[^"\\]|\\.)*)"/, I = /data:"((?:[^"\\]|\\.)*)"/;
144
+ const v = /#id:"((?:[^"\\]|\\.)*)"/, A = /group:"((?:[^"\\]|\\.)*)"/, j = /data:"((?:[^"\\]|\\.)*)"/;
165
145
  function M(r) {
166
146
  return r.replace(/\\(.)/g, "$1");
167
147
  }
168
- function C(r) {
148
+ function I(r) {
169
149
  if (!r.startsWith("#id:"))
170
150
  return null;
171
- const t = r.match(A);
151
+ const t = r.match(v);
172
152
  if (!t)
173
153
  return null;
174
- const e = r.match(j), i = r.match(I);
154
+ const e = r.match(A), i = r.match(j);
175
155
  return {
176
156
  id: M(t[1]),
177
157
  group: e ? M(e[1]) : void 0,
178
158
  data: i ? M(i[1]) : void 0
179
159
  };
180
160
  }
181
- function P(r) {
161
+ function C(r) {
182
162
  try {
183
163
  const t = JSON.parse(r);
184
164
  if (t && typeof t == "object" && "id" in t)
@@ -187,23 +167,23 @@ function P(r) {
187
167
  }
188
168
  return null;
189
169
  }
190
- function O(r) {
170
+ function x(r) {
191
171
  if (typeof r == "object" && r !== null && "id" in r)
192
172
  return r;
193
173
  if (typeof r != "string")
194
174
  return null;
195
- const t = C(r);
175
+ const t = I(r);
196
176
  if (t)
197
177
  return t;
198
- const e = P(r);
178
+ const e = C(r);
199
179
  return e || null;
200
180
  }
201
181
  async function L(r, t, e) {
202
182
  const i = Array.isArray(r) ? r : [r];
203
183
  try {
204
- await R(
184
+ await $(
205
185
  (n) => {
206
- const a = O(n);
186
+ const a = x(n);
207
187
  return a?.group ? i.includes(a.group) : !1;
208
188
  },
209
189
  t ? () => t : void 0,
@@ -228,9 +208,9 @@ async function L(r, t, e) {
228
208
  async function q(r, t, e) {
229
209
  const i = typeof r == "string" ? [r] : r;
230
210
  try {
231
- await R(
211
+ await $(
232
212
  (n) => {
233
- const a = O(n);
213
+ const a = x(n);
234
214
  return a ? i.includes(a?.id) : !1;
235
215
  },
236
216
  t ? () => t : void 0,
@@ -256,11 +236,11 @@ const B = async (r = []) => {
256
236
  let t;
257
237
  Array.isArray(r) ? t = r : r ? t = [r] : t = null;
258
238
  try {
259
- await R(
239
+ await $(
260
240
  (e) => {
261
241
  if (!t)
262
242
  return !0;
263
- const i = O(e);
243
+ const i = x(e);
264
244
  return i ? !t.includes(i.id) : !0;
265
245
  },
266
246
  void 0,
@@ -278,7 +258,7 @@ const B = async (r = []) => {
278
258
  );
279
259
  }
280
260
  };
281
- function W(r, t) {
261
+ function P(r, t) {
282
262
  if (t)
283
263
  return r.get(t);
284
264
  }
@@ -296,8 +276,28 @@ async function _(r) {
296
276
  return [null, t];
297
277
  }
298
278
  }
279
+ function W(r, t) {
280
+ if (r === t)
281
+ return !0;
282
+ if (r == null || t == null)
283
+ return r === t;
284
+ if (typeof r != "object" || typeof t != "object")
285
+ return !1;
286
+ try {
287
+ return JSON.stringify(r) === JSON.stringify(t);
288
+ } catch {
289
+ return !1;
290
+ }
291
+ }
292
+ function O(r) {
293
+ const t = r?.id, e = r?.group, i = r?.data, n = S(null);
294
+ if (!r || !(t && i))
295
+ return n.current = null, n.current;
296
+ const a = n.current;
297
+ return (!a || a.id !== t || a.group !== e || !W(a.data, i)) && (n.current = { id: t, group: e, data: i }), n.current;
298
+ }
299
299
  async function T(r, t, e, i) {
300
- const n = W(r, e)?.data, a = i.optimisticUpdateFn(n, i.data);
300
+ const n = P(r, e)?.data, a = i.optimisticUpdateFn(n, i.data);
301
301
  return await y(t, e, a, !1), n;
302
302
  }
303
303
  function b(r, t, e, i) {
@@ -317,14 +317,14 @@ async function D(r, t, e) {
317
317
  await y(r, t, e, !1);
318
318
  }
319
319
  function V(r, t, e = {}) {
320
- const { cache: i, mutate: n } = U(), { optimisticUpdate: a, rollbackOnError: l = !0 } = e, [E, d] = p(!1), [w, m] = p(null), o = S(!0), s = x(r);
321
- return $(
320
+ const { cache: i, mutate: n } = k(), { optimisticUpdate: a, rollbackOnError: l = !0 } = e, [E, d] = p(!1), [w, m] = p(null), o = S(!0), s = O(r);
321
+ return R(
322
322
  () => () => {
323
323
  o.current = !1;
324
324
  },
325
325
  []
326
326
  ), {
327
- trigger: k(
327
+ trigger: U(
328
328
  async (u) => {
329
329
  if (!o.current)
330
330
  return;
@@ -361,14 +361,14 @@ function V(r, t, e = {}) {
361
361
  };
362
362
  }
363
363
  function z(r, t, e = {}) {
364
- const { cache: i, mutate: n } = U(), { optimisticUpdate: a, rollbackOnError: l = !0 } = e, [E, d] = p(!1), [w, m] = p(null), o = S(!0), s = x(r);
365
- return $(
364
+ const { cache: i, mutate: n } = k(), { optimisticUpdate: a, rollbackOnError: l = !0 } = e, [E, d] = p(!1), [w, m] = p(null), o = S(!0), s = O(r);
365
+ return R(
366
366
  () => () => {
367
367
  o.current = !1;
368
368
  },
369
369
  []
370
370
  ), {
371
- trigger: k(
371
+ trigger: U(
372
372
  async (u) => {
373
373
  if (!o.current)
374
374
  return;
@@ -405,14 +405,14 @@ function z(r, t, e = {}) {
405
405
  };
406
406
  }
407
407
  function H(r, t, e = {}) {
408
- const { cache: i, mutate: n } = U(), { optimisticUpdate: a, rollbackOnError: l = !0 } = e, [E, d] = p(!1), [w, m] = p(null), o = S(!0), s = x(r);
409
- return $(
408
+ const { cache: i, mutate: n } = k(), { optimisticUpdate: a, rollbackOnError: l = !0 } = e, [E, d] = p(!1), [w, m] = p(null), o = S(!0), s = O(r);
409
+ return R(
410
410
  () => () => {
411
411
  o.current = !1;
412
412
  },
413
413
  []
414
414
  ), {
415
- trigger: k(
415
+ trigger: U(
416
416
  async (u, g) => {
417
417
  if (!o.current)
418
418
  return;
@@ -451,15 +451,15 @@ function H(r, t, e = {}) {
451
451
  }
452
452
  export {
453
453
  h as MutationError,
454
- O as extractSWRKey,
454
+ x as extractSWRKey,
455
455
  L as mutateByGroup,
456
456
  q as mutateById,
457
457
  B as resetCache,
458
- W as swrGetCache,
458
+ P as swrGetCache,
459
459
  y as swrMutate,
460
460
  _ as to,
461
461
  V as useSWRCreate,
462
462
  z as useSWRDelete,
463
463
  H as useSWRUpdate,
464
- x as useStableKey
464
+ O as useStableKey
465
465
  };
@@ -1 +1 @@
1
- (function(s,o){typeof exports=="object"&&typeof module<"u"?o(exports,require("react"),require("swr")):typeof define=="function"&&define.amd?define(["exports","react","swr"],o):(s=typeof globalThis<"u"?globalThis:s||self,o(s["swr-catalyst"]={},s.React,s.SWR))})(this,(function(s,o,E){"use strict";class y extends Error{name="MutationError";context;originalError;constructor(t,e,i){super(t),this.context=e,this.originalError=i,"captureStackTrace"in Error&&Error.captureStackTrace(this,y)}getUserMessage(){const{operation:t,key:e}=this.context,i=e?.id||"resource";return`Failed to ${{create:"add",update:"update",delete:"delete"}[t]} ${i}. Please try again.`}toJSON(){return{name:this.name,message:this.message,context:this.context,originalError:this.originalError instanceof Error?{name:this.originalError.name,message:this.originalError.message,stack:this.originalError.stack}:String(this.originalError),stack:this.stack}}isNetworkError(){if(this.originalError instanceof Error){const t=this.originalError.message.toLowerCase();return this.originalError.name==="NetworkError"||t.includes("fetch")||t.includes("network")||t.includes("timeout")}return!1}isValidationError(){if(this.originalError instanceof Error){const t=this.originalError.message.toLowerCase();return t.includes("validation")||t.includes("invalid")||t.includes("required")}return!1}}function D(r,t){if(r===t)return!0;if(r==null||t==null)return r===t;if(typeof r!="object"||typeof t!="object")return!1;try{return JSON.stringify(r)===JSON.stringify(t)}catch{return!1}}function M(r){const t=r?.id,e=r?.group,i=r?.data,n=o.useRef(null);if(!r||!(t&&i))return n.current=null,n.current;const a=n.current;return(!a||a.id!==t||a.group!==e||!D(a.data,i))&&(n.current={id:t,group:e,data:i}),n.current}const j=/#id:"((?:[^"\\]|\\.)*)"/,v=/group:"((?:[^"\\]|\\.)*)"/,F=/data:"((?:[^"\\]|\\.)*)"/;function $(r){return r.replace(/\\(.)/g,"$1")}function N(r){if(!r.startsWith("#id:"))return null;const t=r.match(j);if(!t)return null;const e=r.match(v),i=r.match(F);return{id:$(t[1]),group:e?$(e[1]):void 0,data:i?$(i[1]):void 0}}function A(r){try{const t=JSON.parse(r);if(t&&typeof t=="object"&&"id"in t)return t}catch{}return null}function k(r){if(typeof r=="object"&&r!==null&&"id"in r)return r;if(typeof r!="string")return null;const t=N(r);if(t)return t;const e=A(r);return e||null}async function I(r,t,e){const i=Array.isArray(r)?r:[r];try{await E.mutate(n=>{const a=k(n);return a?.group?i.includes(a.group):!1},t?()=>t:void 0,{revalidate:e?.revalidate??!t,...e})}catch(n){throw new y(`Failed to mutate cache by group(s): ${i.join(", ")}. ${n instanceof Error?n.message:String(n)}`,{operation:"update",key:null,data:t,timestamp:Date.now()},n)}}async function P(r,t,e){const i=typeof r=="string"?[r]:r;try{await E.mutate(n=>{const a=k(n);return a?i.includes(a?.id):!1},t?()=>t:void 0,{revalidate:e?.revalidate??!t,...e})}catch(n){throw new y(`Failed to mutate cache by ID(s): ${i.join(", ")}. ${n instanceof Error?n.message:String(n)}`,{operation:"update",key:null,data:t,timestamp:Date.now()},n)}}const G=async(r=[])=>{let t;Array.isArray(r)?t=r:r?t=[r]:t=null;try{await E.mutate(e=>{if(!t)return!0;const i=k(e);return i?!t.includes(i.id):!0},void 0,!1)}catch(e){throw new y(`Failed to reset cache${t?` (preserving: ${t.join(", ")})`:""}. ${e instanceof Error?e.message:String(e)}`,{operation:"delete",key:null,timestamp:Date.now()},e)}};function U(r,t){if(t)return r.get(t)}function w(r,t,e,i){return r(t,e,i)}async function J(r){try{return[await r,null]}catch(t){return[null,t]}}async function b(r,t,e,i){const n=U(r,e)?.data,a=i.optimisticUpdateFn(n,i.data);return await w(t,e,a,!1),n}function T(r,t,e,i){const n=t?.id||"unknown",a=i?.id?` with ID ${i.id}`:"",d=`Failed to ${r} resource "${n}"${a}. ${e instanceof Error?e.message:String(e)}`;return new y(d,{operation:r,key:t,...i,timestamp:Date.now()},e)}async function C(r,t,e){await w(r,t,e,!1)}function q(r,t,e={}){const{cache:i,mutate:n}=E.useSWRConfig(),{optimisticUpdate:a,rollbackOnError:d=!0}=e,[S,p]=o.useState(!1),[R,h]=o.useState(null),u=o.useRef(!0),c=M(r);return o.useEffect(()=>()=>{u.current=!1},[]),{trigger:o.useCallback(async f=>{if(!u.current)return;p(!0),h(null);let m;a&&(m=await b(i,n,c,{data:f,optimisticUpdateFn:a}));try{const l=await t(f);return u.current&&await w(n,c),l}catch(l){d&&a&&await C(n,c,m);const g=T("create",c,l,{data:f});throw u.current&&h(g),g}finally{u.current&&p(!1)}},[c,t,n,i,a,d]),isMutating:S,error:R}}function B(r,t,e={}){const{cache:i,mutate:n}=E.useSWRConfig(),{optimisticUpdate:a,rollbackOnError:d=!0}=e,[S,p]=o.useState(!1),[R,h]=o.useState(null),u=o.useRef(!0),c=M(r);return o.useEffect(()=>()=>{u.current=!1},[]),{trigger:o.useCallback(async f=>{if(!u.current)return;p(!0),h(null);let m;a&&(m=await b(i,n,c,{data:f,optimisticUpdateFn:a}));try{const l=await t(f);return u.current&&await w(n,c),l}catch(l){d&&a&&await C(n,c,m);const g=T("delete",c,l,{id:f});throw u.current&&h(g),g}finally{u.current&&p(!1)}},[c,t,n,i,a,d]),isMutating:S,error:R}}function L(r,t,e={}){const{cache:i,mutate:n}=E.useSWRConfig(),{optimisticUpdate:a,rollbackOnError:d=!0}=e,[S,p]=o.useState(!1),[R,h]=o.useState(null),u=o.useRef(!0),c=M(r);return o.useEffect(()=>()=>{u.current=!1},[]),{trigger:o.useCallback(async(f,m)=>{if(!u.current)return;p(!0),h(null);let l;a&&(l=await b(i,n,c,{data:{id:f,data:m},optimisticUpdateFn:a}));try{const g=await t(f,m);return u.current&&await w(n,c),g}catch(g){d&&a&&await C(n,c,l);const O=T("update",c,g,{data:m,id:f});throw u.current&&h(O),O}finally{u.current&&p(!1)}},[c,t,n,i,a,d]),isMutating:S,error:R}}s.MutationError=y,s.extractSWRKey=k,s.mutateByGroup=I,s.mutateById=P,s.resetCache=G,s.swrGetCache=U,s.swrMutate=w,s.to=J,s.useSWRCreate=q,s.useSWRDelete=B,s.useSWRUpdate=L,s.useStableKey=M,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"})}));
1
+ (function(o,d){typeof exports=="object"&&typeof module<"u"?d(exports,require("swr"),require("react")):typeof define=="function"&&define.amd?define(["exports","swr","react"],d):(o=typeof globalThis<"u"?globalThis:o||self,d(o["swr-catalyst"]={},o.SWR,o.React))})(this,(function(o,d,c){"use strict";class E extends Error{name="MutationError";context;originalError;constructor(t,e,i){super(t),this.context=e,this.originalError=i,"captureStackTrace"in Error&&Error.captureStackTrace(this,E)}getUserMessage(){const{operation:t,key:e}=this.context,i=e?.id||"resource";return`Failed to ${{create:"add",update:"update",delete:"delete"}[t]} ${i}. Please try again.`}toJSON(){return{name:this.name,message:this.message,context:this.context,originalError:this.originalError instanceof Error?{name:this.originalError.name,message:this.originalError.message,stack:this.originalError.stack}:String(this.originalError),stack:this.stack}}isNetworkError(){if(this.originalError instanceof Error){const t=this.originalError.message.toLowerCase();return this.originalError.name==="NetworkError"||t.includes("fetch")||t.includes("network")||t.includes("timeout")}return!1}isValidationError(){if(this.originalError instanceof Error){const t=this.originalError.message.toLowerCase();return t.includes("validation")||t.includes("invalid")||t.includes("required")}return!1}}const D=/#id:"((?:[^"\\]|\\.)*)"/,j=/group:"((?:[^"\\]|\\.)*)"/,v=/data:"((?:[^"\\]|\\.)*)"/;function $(r){return r.replace(/\\(.)/g,"$1")}function F(r){if(!r.startsWith("#id:"))return null;const t=r.match(D);if(!t)return null;const e=r.match(j),i=r.match(v);return{id:$(t[1]),group:e?$(e[1]):void 0,data:i?$(i[1]):void 0}}function N(r){try{const t=JSON.parse(r);if(t&&typeof t=="object"&&"id"in t)return t}catch{}return null}function M(r){if(typeof r=="object"&&r!==null&&"id"in r)return r;if(typeof r!="string")return null;const t=F(r);if(t)return t;const e=N(r);return e||null}async function A(r,t,e){const i=Array.isArray(r)?r:[r];try{await d.mutate(n=>{const a=M(n);return a?.group?i.includes(a.group):!1},t?()=>t:void 0,{revalidate:e?.revalidate??!t,...e})}catch(n){throw new E(`Failed to mutate cache by group(s): ${i.join(", ")}. ${n instanceof Error?n.message:String(n)}`,{operation:"update",key:null,data:t,timestamp:Date.now()},n)}}async function I(r,t,e){const i=typeof r=="string"?[r]:r;try{await d.mutate(n=>{const a=M(n);return a?i.includes(a?.id):!1},t?()=>t:void 0,{revalidate:e?.revalidate??!t,...e})}catch(n){throw new E(`Failed to mutate cache by ID(s): ${i.join(", ")}. ${n instanceof Error?n.message:String(n)}`,{operation:"update",key:null,data:t,timestamp:Date.now()},n)}}const P=async(r=[])=>{let t;Array.isArray(r)?t=r:r?t=[r]:t=null;try{await d.mutate(e=>{if(!t)return!0;const i=M(e);return i?!t.includes(i.id):!0},void 0,!1)}catch(e){throw new E(`Failed to reset cache${t?` (preserving: ${t.join(", ")})`:""}. ${e instanceof Error?e.message:String(e)}`,{operation:"delete",key:null,timestamp:Date.now()},e)}};function U(r,t){if(t)return r.get(t)}function w(r,t,e,i){return r(t,e,i)}async function G(r){try{return[await r,null]}catch(t){return[null,t]}}function J(r,t){if(r===t)return!0;if(r==null||t==null)return r===t;if(typeof r!="object"||typeof t!="object")return!1;try{return JSON.stringify(r)===JSON.stringify(t)}catch{return!1}}function k(r){const t=r?.id,e=r?.group,i=r?.data,n=c.useRef(null);if(!r||!(t&&i))return n.current=null,n.current;const a=n.current;return(!a||a.id!==t||a.group!==e||!J(a.data,i))&&(n.current={id:t,group:e,data:i}),n.current}async function b(r,t,e,i){const n=U(r,e)?.data,a=i.optimisticUpdateFn(n,i.data);return await w(t,e,a,!1),n}function T(r,t,e,i){const n=t?.id||"unknown",a=i?.id?` with ID ${i.id}`:"",g=`Failed to ${r} resource "${n}"${a}. ${e instanceof Error?e.message:String(e)}`;return new E(g,{operation:r,key:t,...i,timestamp:Date.now()},e)}async function C(r,t,e){await w(r,t,e,!1)}function q(r,t,e={}){const{cache:i,mutate:n}=d.useSWRConfig(),{optimisticUpdate:a,rollbackOnError:g=!0}=e,[S,h]=c.useState(!1),[R,y]=c.useState(null),s=c.useRef(!0),u=k(r);return c.useEffect(()=>()=>{s.current=!1},[]),{trigger:c.useCallback(async f=>{if(!s.current)return;h(!0),y(null);let p;a&&(p=await b(i,n,u,{data:f,optimisticUpdateFn:a}));try{const l=await t(f);return s.current&&await w(n,u),l}catch(l){g&&a&&await C(n,u,p);const m=T("create",u,l,{data:f});throw s.current&&y(m),m}finally{s.current&&h(!1)}},[u,t,n,i,a,g]),isMutating:S,error:R}}function B(r,t,e={}){const{cache:i,mutate:n}=d.useSWRConfig(),{optimisticUpdate:a,rollbackOnError:g=!0}=e,[S,h]=c.useState(!1),[R,y]=c.useState(null),s=c.useRef(!0),u=k(r);return c.useEffect(()=>()=>{s.current=!1},[]),{trigger:c.useCallback(async f=>{if(!s.current)return;h(!0),y(null);let p;a&&(p=await b(i,n,u,{data:f,optimisticUpdateFn:a}));try{const l=await t(f);return s.current&&await w(n,u),l}catch(l){g&&a&&await C(n,u,p);const m=T("delete",u,l,{id:f});throw s.current&&y(m),m}finally{s.current&&h(!1)}},[u,t,n,i,a,g]),isMutating:S,error:R}}function L(r,t,e={}){const{cache:i,mutate:n}=d.useSWRConfig(),{optimisticUpdate:a,rollbackOnError:g=!0}=e,[S,h]=c.useState(!1),[R,y]=c.useState(null),s=c.useRef(!0),u=k(r);return c.useEffect(()=>()=>{s.current=!1},[]),{trigger:c.useCallback(async(f,p)=>{if(!s.current)return;h(!0),y(null);let l;a&&(l=await b(i,n,u,{data:{id:f,data:p},optimisticUpdateFn:a}));try{const m=await t(f,p);return s.current&&await w(n,u),m}catch(m){g&&a&&await C(n,u,l);const O=T("update",u,m,{data:p,id:f});throw s.current&&y(O),O}finally{s.current&&h(!1)}},[u,t,n,i,a,g]),isMutating:S,error:R}}o.MutationError=E,o.extractSWRKey=M,o.mutateByGroup=A,o.mutateById=I,o.resetCache=P,o.swrGetCache=U,o.swrMutate=w,o.to=G,o.useSWRCreate=q,o.useSWRDelete=B,o.useSWRUpdate=L,o.useStableKey=k,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swr-catalyst",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "A lightweight, declarative library to simplify data mutations with SWR.",
5
5
  "author": "Pedro Barbosa (https://github.com/pedroab0)",
6
6
  "license": "MIT",
@@ -33,6 +33,8 @@
33
33
  "scripts": {
34
34
  "build": "vite build",
35
35
  "test": "vitest",
36
+ "test:unit": "vitest run --exclude 'src/__tests__/**'",
37
+ "test:integration": "vitest run src/__tests__/integration",
36
38
  "test:coverage": "vitest run --coverage",
37
39
  "lint": "biome check .",
38
40
  "lint:fix": "biome check --write .",
@@ -48,19 +50,20 @@
48
50
  "swr": "^2.0.0"
49
51
  },
50
52
  "devDependencies": {
51
- "@biomejs/biome": "^2.3.4",
53
+ "@biomejs/biome": "^2.3.7",
52
54
  "@size-limit/preset-small-lib": "^11.2.0",
53
55
  "@testing-library/react": "^16.3.0",
54
- "@types/node": "^24.10.0",
55
- "@vitest/coverage-v8": "^4.0.7",
56
- "jsdom": "^27.1.0",
56
+ "@types/node": "^24.10.1",
57
+ "@vitest/coverage-v8": "^4.0.13",
58
+ "jsdom": "^27.2.0",
59
+ "msw": "^2.12.2",
57
60
  "react": "^19.2.0",
58
61
  "react-dom": "^19.2.0",
59
62
  "size-limit": "^11.2.0",
60
63
  "swr": "^2.3.6",
61
64
  "typescript": "^5.9.3",
62
- "ultracite": "^6.3.2",
63
- "vite": "^7.2.1",
65
+ "ultracite": "^6.3.6",
66
+ "vite": "^7.2.4",
64
67
  "vite-plugin-dts": "^4.5.4",
65
68
  "vitest": "^4.0.7"
66
69
  }