@navios/react-query 0.7.0 → 1.0.0-alpha.1

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 (124) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +152 -4
  3. package/dist/src/__tests__/errorSchema.spec.d.mts +2 -0
  4. package/dist/src/__tests__/errorSchema.spec.d.mts.map +1 -0
  5. package/dist/src/client/__type-tests__/from-endpoint.spec-d.d.mts +2 -0
  6. package/dist/src/client/__type-tests__/from-endpoint.spec-d.d.mts.map +1 -0
  7. package/dist/src/client/__type-tests__/infinite-query.spec-d.d.mts +2 -0
  8. package/dist/src/client/__type-tests__/infinite-query.spec-d.d.mts.map +1 -0
  9. package/dist/src/client/__type-tests__/multipart-mutation.spec-d.d.mts +2 -0
  10. package/dist/src/client/__type-tests__/multipart-mutation.spec-d.d.mts.map +1 -0
  11. package/dist/src/client/__type-tests__/mutation.spec-d.d.mts +2 -0
  12. package/dist/src/client/__type-tests__/mutation.spec-d.d.mts.map +1 -0
  13. package/dist/src/client/__type-tests__/query.spec-d.d.mts +2 -0
  14. package/dist/src/client/__type-tests__/query.spec-d.d.mts.map +1 -0
  15. package/dist/src/client/declare-client.d.mts +15 -8
  16. package/dist/src/client/declare-client.d.mts.map +1 -1
  17. package/dist/src/client/types/from-endpoint.d.mts +130 -0
  18. package/dist/src/client/types/from-endpoint.d.mts.map +1 -0
  19. package/dist/src/client/types/helpers.d.mts +74 -0
  20. package/dist/src/client/types/helpers.d.mts.map +1 -0
  21. package/dist/src/client/types/index.d.mts +21 -0
  22. package/dist/src/client/types/index.d.mts.map +1 -0
  23. package/dist/src/client/types/infinite-query.d.mts +61 -0
  24. package/dist/src/client/types/infinite-query.d.mts.map +1 -0
  25. package/dist/src/client/types/multipart-mutation.d.mts +98 -0
  26. package/dist/src/client/types/multipart-mutation.d.mts.map +1 -0
  27. package/dist/src/client/types/mutation.d.mts +75 -0
  28. package/dist/src/client/types/mutation.d.mts.map +1 -0
  29. package/dist/src/client/types/query.d.mts +65 -0
  30. package/dist/src/client/types/query.d.mts.map +1 -0
  31. package/dist/src/client/types.d.mts +1 -608
  32. package/dist/src/client/types.d.mts.map +1 -1
  33. package/dist/src/common/types.d.mts +30 -3
  34. package/dist/src/common/types.d.mts.map +1 -1
  35. package/dist/src/mutation/index.d.mts +1 -0
  36. package/dist/src/mutation/index.d.mts.map +1 -1
  37. package/dist/src/mutation/make-hook.d.mts +42 -16
  38. package/dist/src/mutation/make-hook.d.mts.map +1 -1
  39. package/dist/src/mutation/optimistic.d.mts +166 -0
  40. package/dist/src/mutation/optimistic.d.mts.map +1 -0
  41. package/dist/src/mutation/types.d.mts +51 -19
  42. package/dist/src/mutation/types.d.mts.map +1 -1
  43. package/dist/src/query/index.d.mts +1 -0
  44. package/dist/src/query/index.d.mts.map +1 -1
  45. package/dist/src/query/key-creator.d.mts.map +1 -1
  46. package/dist/src/query/make-infinite-options.d.mts +3 -2
  47. package/dist/src/query/make-infinite-options.d.mts.map +1 -1
  48. package/dist/src/query/make-options.d.mts +42 -12
  49. package/dist/src/query/make-options.d.mts.map +1 -1
  50. package/dist/src/query/prefetch.d.mts +245 -0
  51. package/dist/src/query/prefetch.d.mts.map +1 -0
  52. package/dist/src/query/types.d.mts +35 -17
  53. package/dist/src/query/types.d.mts.map +1 -1
  54. package/dist/tsconfig.tsbuildinfo +1 -1
  55. package/lib/index.cjs +454 -37
  56. package/lib/index.cjs.map +1 -1
  57. package/lib/index.d.cts +1021 -598
  58. package/lib/index.d.cts.map +1 -1
  59. package/lib/index.d.mts +1019 -596
  60. package/lib/index.d.mts.map +1 -1
  61. package/lib/index.mjs +441 -29
  62. package/lib/index.mjs.map +1 -1
  63. package/package.json +8 -8
  64. package/src/__tests__/declare-client.spec.mts +1 -2
  65. package/src/__tests__/errorSchema.spec.mts +391 -0
  66. package/src/__tests__/make-mutation.spec.mts +6 -5
  67. package/src/__tests__/makeDataTag.spec.mts +2 -1
  68. package/src/__tests__/makeQueryOptions.spec.mts +2 -1
  69. package/src/client/__type-tests__/from-endpoint.spec-d.mts +550 -0
  70. package/src/client/__type-tests__/infinite-query.spec-d.mts +648 -0
  71. package/src/client/__type-tests__/multipart-mutation.spec-d.mts +725 -0
  72. package/src/client/__type-tests__/mutation.spec-d.mts +757 -0
  73. package/src/client/__type-tests__/query.spec-d.mts +701 -0
  74. package/src/client/declare-client.mts +59 -34
  75. package/src/client/types/from-endpoint.mts +345 -0
  76. package/src/client/types/helpers.mts +140 -0
  77. package/src/client/types/index.mts +26 -0
  78. package/src/client/types/infinite-query.mts +133 -0
  79. package/src/client/types/multipart-mutation.mts +264 -0
  80. package/src/client/types/mutation.mts +176 -0
  81. package/src/client/types/query.mts +132 -0
  82. package/src/client/types.mts +1 -1935
  83. package/src/common/types.mts +48 -3
  84. package/src/mutation/index.mts +1 -0
  85. package/src/mutation/make-hook.mts +171 -63
  86. package/src/mutation/optimistic.mts +294 -0
  87. package/src/mutation/types.mts +102 -29
  88. package/src/query/index.mts +1 -0
  89. package/src/query/key-creator.mts +24 -13
  90. package/src/query/make-infinite-options.mts +53 -10
  91. package/src/query/make-options.mts +184 -43
  92. package/src/query/prefetch.mts +326 -0
  93. package/src/query/types.mts +76 -16
  94. package/dist/src/declare-client.d.mts +0 -31
  95. package/dist/src/declare-client.d.mts.map +0 -1
  96. package/dist/src/make-infinite-query-options.d.mts +0 -13
  97. package/dist/src/make-infinite-query-options.d.mts.map +0 -1
  98. package/dist/src/make-mutation.d.mts +0 -15
  99. package/dist/src/make-mutation.d.mts.map +0 -1
  100. package/dist/src/make-query-options.d.mts +0 -15
  101. package/dist/src/make-query-options.d.mts.map +0 -1
  102. package/dist/src/types/client-endpoint-helper.d.mts +0 -8
  103. package/dist/src/types/client-endpoint-helper.d.mts.map +0 -1
  104. package/dist/src/types/client-instance.d.mts +0 -211
  105. package/dist/src/types/client-instance.d.mts.map +0 -1
  106. package/dist/src/types/index.d.mts +0 -8
  107. package/dist/src/types/index.d.mts.map +0 -1
  108. package/dist/src/types/mutation-args.d.mts +0 -10
  109. package/dist/src/types/mutation-args.d.mts.map +0 -1
  110. package/dist/src/types/mutation-helpers.d.mts +0 -10
  111. package/dist/src/types/mutation-helpers.d.mts.map +0 -1
  112. package/dist/src/types/query-args.d.mts +0 -8
  113. package/dist/src/types/query-args.d.mts.map +0 -1
  114. package/dist/src/types/query-helpers.d.mts +0 -14
  115. package/dist/src/types/query-helpers.d.mts.map +0 -1
  116. package/dist/src/types/query-url-params-args.d.mts +0 -5
  117. package/dist/src/types/query-url-params-args.d.mts.map +0 -1
  118. package/dist/src/types.d.mts +0 -49
  119. package/dist/src/types.d.mts.map +0 -1
  120. package/dist/src/utils/mutation-key.creator.d.mts +0 -39
  121. package/dist/src/utils/mutation-key.creator.d.mts.map +0 -1
  122. package/dist/src/utils/query-key-creator.d.mts +0 -24
  123. package/dist/src/utils/query-key-creator.d.mts.map +0 -1
  124. package/src/client/__type-tests__/client-instance.spec-d.mts +0 -852
@@ -1,4 +1,9 @@
1
- import type { BuilderInstance } from '@navios/builder'
1
+ import type {
2
+ BuilderInstance,
3
+ ErrorSchemaRecord,
4
+ InferErrorSchemaOutput,
5
+ } from '@navios/builder'
6
+ import type { z, ZodType } from 'zod/v4'
2
7
 
3
8
  /**
4
9
  * Splits a string by a delimiter into a tuple type.
@@ -21,11 +26,51 @@ export type ProcessResponseFunction<TData = unknown, TVariables = unknown> = (
21
26
 
22
27
  /**
23
28
  * Options for creating a client instance.
29
+ *
30
+ * @template UseDiscriminator - When `true`, errors are returned as union types.
31
+ * When `false` (default), errors are thrown.
24
32
  */
25
- export type ClientOptions = {
26
- api: BuilderInstance
33
+ export type ClientOptions<UseDiscriminator extends boolean = false> = {
34
+ api: BuilderInstance<UseDiscriminator>
27
35
  defaults?: {
28
36
  keyPrefix?: string[]
29
37
  keySuffix?: string[]
30
38
  }
31
39
  }
40
+
41
+ /**
42
+ * Infers the full response type from an endpoint configuration.
43
+ * Returns `ResponseType | ErrorTypes` if errorSchema exists,
44
+ * otherwise just `ResponseType`.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * type Response = InferEndpointResponse<{
49
+ * responseSchema: z.ZodObject<{ data: z.ZodString }>,
50
+ * errorSchema: { 400: z.ZodObject<{ error: z.ZodString }> }
51
+ * }>
52
+ * // Result: { data: string } | { error: string }
53
+ * ```
54
+ */
55
+ export type InferEndpointResponse<
56
+ Config extends {
57
+ responseSchema: ZodType
58
+ errorSchema?: ErrorSchemaRecord
59
+ },
60
+ > = Config['errorSchema'] extends ErrorSchemaRecord
61
+ ? z.output<Config['responseSchema']> | InferErrorSchemaOutput<Config['errorSchema']>
62
+ : z.output<Config['responseSchema']>
63
+
64
+ /**
65
+ * Computes the Result type, applying processResponse transformation
66
+ * to the full response (including error union when present).
67
+ */
68
+ export type ComputeResultType<
69
+ ResponseSchema extends ZodType,
70
+ ErrorSchema extends ErrorSchemaRecord | undefined,
71
+ ProcessedResult,
72
+ > = ProcessedResult extends undefined
73
+ ? ErrorSchema extends ErrorSchemaRecord
74
+ ? z.output<ResponseSchema> | InferErrorSchemaOutput<ErrorSchema>
75
+ : z.output<ResponseSchema>
76
+ : ProcessedResult
@@ -1,3 +1,4 @@
1
1
  export * from './types.mjs'
2
2
  export * from './key-creator.mjs'
3
3
  export * from './make-hook.mjs'
4
+ export * from './optimistic.mjs'
@@ -1,22 +1,105 @@
1
1
  import type {
2
- AbstractEndpoint,
3
2
  AnyEndpointConfig,
4
- NaviosZodRequest,
3
+ BaseEndpointConfig,
4
+ ErrorSchemaRecord,
5
+ HttpMethod,
6
+ InferErrorSchemaOutput,
5
7
  UrlHasParams,
6
8
  UrlParams,
7
9
  } from '@navios/builder'
8
10
  import type {
9
11
  MutationFunctionContext,
12
+ UseMutationOptions,
10
13
  UseMutationResult,
11
14
  } from '@tanstack/react-query'
12
- import type { z } from 'zod/v4'
15
+ import type { z, ZodObject, ZodType } from 'zod/v4'
13
16
 
14
17
  import { useIsMutating, useMutation } from '@tanstack/react-query'
15
18
 
16
- import type { MutationParams } from './types.mjs'
19
+ import type { ProcessResponseFunction } from '../common/types.mjs'
20
+ import type { MutationHelpers } from './types.mjs'
17
21
 
18
22
  import { createMutationKey } from './key-creator.mjs'
19
23
 
24
+ /**
25
+ * Helper type for endpoint with config property
26
+ */
27
+ type EndpointWithConfig<Config extends AnyEndpointConfig> = ((
28
+ params: any,
29
+ ) => Promise<any>) & {
30
+ config: Config
31
+ }
32
+
33
+ /**
34
+ * Helper type for response input when errorSchema is present
35
+ */
36
+ type ResponseInput<
37
+ ResponseSchema extends ZodType,
38
+ ErrorSchema extends ErrorSchemaRecord | undefined,
39
+ > = ErrorSchema extends ErrorSchemaRecord
40
+ ? z.output<ResponseSchema> | InferErrorSchemaOutput<ErrorSchema>
41
+ : z.output<ResponseSchema>
42
+
43
+ /**
44
+ * Options type for makeMutation
45
+ */
46
+ type MakeMutationParams<
47
+ Config extends AnyEndpointConfig,
48
+ ResponseSchema extends ZodType,
49
+ ErrorSchema extends ErrorSchemaRecord | undefined,
50
+ TData,
51
+ TVariables,
52
+ TOnMutateResult,
53
+ TContext,
54
+ UseKey extends boolean,
55
+ > = Omit<
56
+ UseMutationOptions<TData, Error, TVariables>,
57
+ | 'mutationKey'
58
+ | 'mutationFn'
59
+ | 'onMutate'
60
+ | 'onSuccess'
61
+ | 'onError'
62
+ | 'onSettled'
63
+ | 'scope'
64
+ > & {
65
+ processResponse?: ProcessResponseFunction<TData, ResponseInput<ResponseSchema, ErrorSchema>>
66
+ useContext?: () => TContext
67
+ onSuccess?: (
68
+ data: TData,
69
+ variables: TVariables,
70
+ context: TContext &
71
+ MutationFunctionContext & { onMutateResult: TOnMutateResult | undefined },
72
+ ) => void | Promise<void>
73
+ onError?: (
74
+ err: unknown,
75
+ variables: TVariables,
76
+ context: TContext &
77
+ MutationFunctionContext & { onMutateResult: TOnMutateResult | undefined },
78
+ ) => void | Promise<void>
79
+ onMutate?: (
80
+ variables: TVariables,
81
+ context: TContext & MutationFunctionContext,
82
+ ) => TOnMutateResult | Promise<TOnMutateResult>
83
+ onSettled?: (
84
+ data: TData | undefined,
85
+ error: Error | null,
86
+ variables: TVariables,
87
+ context: TContext &
88
+ MutationFunctionContext & { onMutateResult: TOnMutateResult | undefined },
89
+ ) => void | Promise<void>
90
+ useKey?: UseKey
91
+ keyPrefix?: UseKey extends true
92
+ ? UrlHasParams<Config['url']> extends true
93
+ ? string[]
94
+ : never
95
+ : never
96
+ keySuffix?: UseKey extends true
97
+ ? UrlHasParams<Config['url']> extends true
98
+ ? string[]
99
+ : never
100
+ : never
101
+ }
102
+
20
103
  /**
21
104
  * Creates a mutation hook for a given endpoint.
22
105
  *
@@ -27,44 +110,87 @@ import { createMutationKey } from './key-creator.mjs'
27
110
  * @param options - Mutation configuration including processResponse and callbacks
28
111
  * @returns A hook function that returns mutation result with attached helpers
29
112
  */
113
+ // Overload: WITH errorSchema
30
114
  export function makeMutation<
31
- Config extends AnyEndpointConfig,
32
- TData = unknown,
33
- TVariables extends NaviosZodRequest<Config> = NaviosZodRequest<Config>,
34
- TResponse = z.output<Config['responseSchema']>,
115
+ Method extends HttpMethod,
116
+ Url extends string,
117
+ QuerySchema extends ZodObject | undefined,
118
+ ResponseSchema extends ZodType,
119
+ RequestSchema extends ZodType,
120
+ ErrorSchema extends ErrorSchemaRecord,
121
+ TData,
122
+ TOnMutateResult = unknown,
123
+ TContext = unknown,
124
+ UseKey extends boolean = false,
125
+ >(
126
+ endpoint: EndpointWithConfig<
127
+ BaseEndpointConfig<Method, Url, QuerySchema, ResponseSchema, RequestSchema, ErrorSchema>
128
+ >,
129
+ options: MakeMutationParams<
130
+ BaseEndpointConfig<Method, Url, QuerySchema, ResponseSchema, RequestSchema, ErrorSchema>,
131
+ ResponseSchema,
132
+ ErrorSchema,
133
+ TData,
134
+ any,
135
+ TOnMutateResult,
136
+ TContext,
137
+ UseKey
138
+ >,
139
+ ): ((
140
+ keyParams: UseKey extends true
141
+ ? UrlHasParams<Url> extends true
142
+ ? { urlParams: UrlParams<Url> }
143
+ : never
144
+ : never,
145
+ ) => UseMutationResult<TData, Error, any, TOnMutateResult>) &
146
+ MutationHelpers<Url, TData>
147
+
148
+ // Overload: WITHOUT errorSchema
149
+ export function makeMutation<
150
+ Method extends HttpMethod,
151
+ Url extends string,
152
+ QuerySchema extends ZodObject | undefined,
153
+ ResponseSchema extends ZodType,
154
+ RequestSchema extends ZodType | undefined,
155
+ TData,
35
156
  TOnMutateResult = unknown,
36
157
  TContext = unknown,
37
158
  UseKey extends boolean = false,
38
159
  >(
39
- endpoint: AbstractEndpoint<Config>,
40
- options: MutationParams<
41
- Config,
160
+ endpoint: EndpointWithConfig<
161
+ BaseEndpointConfig<Method, Url, QuerySchema, ResponseSchema, RequestSchema, undefined>
162
+ >,
163
+ options: MakeMutationParams<
164
+ BaseEndpointConfig<Method, Url, QuerySchema, ResponseSchema, RequestSchema, undefined>,
165
+ ResponseSchema,
166
+ undefined,
42
167
  TData,
43
- TVariables,
44
- TResponse,
168
+ any,
45
169
  TOnMutateResult,
46
170
  TContext,
47
171
  UseKey
48
172
  >,
49
- ) {
173
+ ): ((
174
+ keyParams: UseKey extends true
175
+ ? UrlHasParams<Url> extends true
176
+ ? { urlParams: UrlParams<Url> }
177
+ : never
178
+ : never,
179
+ ) => UseMutationResult<TData, Error, any, TOnMutateResult>) &
180
+ MutationHelpers<Url, TData>
181
+
182
+ // Implementation
183
+ export function makeMutation(
184
+ endpoint: EndpointWithConfig<AnyEndpointConfig>,
185
+ options: any,
186
+ ): any {
50
187
  const config = endpoint.config
51
188
 
52
189
  const mutationKey = createMutationKey(config, {
53
190
  ...options,
54
- processResponse: options.processResponse ?? ((data) => data),
191
+ processResponse: options.processResponse ?? ((data: any) => data),
55
192
  })
56
- const result = (
57
- keyParams: UseKey extends true
58
- ? UrlHasParams<Config['url']> extends true
59
- ? { urlParams: UrlParams<Config['url']> }
60
- : never
61
- : never,
62
- ): UseMutationResult<
63
- TData,
64
- Error,
65
- NaviosZodRequest<Config>,
66
- TOnMutateResult
67
- > => {
193
+ const result = (keyParams: any): any => {
68
194
  const {
69
195
  useKey,
70
196
  useContext,
@@ -78,9 +204,8 @@ export function makeMutation<
78
204
  ...rest
79
205
  } = options
80
206
 
81
- const ownContext = (useContext?.() as TContext) ?? {}
207
+ const ownContext = useContext?.() ?? {}
82
208
 
83
- // @ts-expect-error The types match
84
209
  return useMutation({
85
210
  ...rest,
86
211
  mutationKey: useKey ? mutationKey(keyParams) : undefined,
@@ -89,89 +214,72 @@ export function makeMutation<
89
214
  id: JSON.stringify(mutationKey(keyParams)),
90
215
  }
91
216
  : undefined,
92
- async mutationFn(params: TVariables) {
217
+ async mutationFn(params: any) {
93
218
  const response = await endpoint(params)
94
219
 
95
- return (processResponse ? processResponse(response) : response) as TData
220
+ return processResponse ? processResponse(response) : response
96
221
  },
97
222
  onSuccess: onSuccess
98
223
  ? (
99
- data: TData,
100
- variables: TVariables,
101
- onMutateResult: TOnMutateResult | undefined,
224
+ data: any,
225
+ variables: any,
226
+ onMutateResult: any,
102
227
  context: MutationFunctionContext,
103
228
  ) => {
104
229
  return onSuccess?.(data, variables, {
105
230
  ...ownContext,
106
231
  ...context,
107
232
  onMutateResult,
108
- } as TContext &
109
- MutationFunctionContext & {
110
- onMutateResult: TOnMutateResult | undefined
111
- })
233
+ })
112
234
  }
113
235
  : undefined,
114
236
  onError: onError
115
237
  ? (
116
238
  err: Error,
117
- variables: TVariables,
118
- onMutateResult: TOnMutateResult | undefined,
239
+ variables: any,
240
+ onMutateResult: any,
119
241
  context: MutationFunctionContext,
120
242
  ) => {
121
243
  return onError?.(err, variables, {
122
244
  onMutateResult,
123
245
  ...ownContext,
124
246
  ...context,
125
- } as TContext &
126
- MutationFunctionContext & {
127
- onMutateResult: TOnMutateResult | undefined
128
- })
247
+ })
129
248
  }
130
249
  : undefined,
131
250
  onMutate: onMutate
132
- ? (variables: TVariables, context: MutationFunctionContext) => {
251
+ ? (variables: any, context: MutationFunctionContext) => {
133
252
  return onMutate(variables, {
134
253
  ...ownContext,
135
254
  ...context,
136
- } as TContext & MutationFunctionContext)
255
+ })
137
256
  }
138
257
  : undefined,
139
258
  onSettled: onSettled
140
259
  ? (
141
- data: TData | undefined,
260
+ data: any,
142
261
  error: Error | null,
143
- variables: TVariables,
144
- onMutateResult: TOnMutateResult | undefined,
262
+ variables: any,
263
+ onMutateResult: any,
145
264
  context: MutationFunctionContext,
146
265
  ) => {
147
266
  return onSettled(data, error, variables, {
148
267
  ...ownContext,
149
268
  ...context,
150
269
  onMutateResult,
151
- } as TContext &
152
- MutationFunctionContext & {
153
- onMutateResult: TOnMutateResult | undefined
154
- })
270
+ })
155
271
  }
156
272
  : undefined,
157
273
  })
158
274
  }
159
- result.useIsMutating = (
160
- keyParams: UseKey extends true
161
- ? UrlHasParams<Config['url']> extends true
162
- ? UrlParams<Config['url']>
163
- : never
164
- : never,
165
- ): boolean => {
275
+ result.useIsMutating = (keyParams: any): boolean => {
166
276
  if (!options.useKey) {
167
277
  throw new Error(
168
278
  'useIsMutating can only be used when useKey is set to true',
169
279
  )
170
280
  }
171
281
  const isMutating = useIsMutating({
172
- mutationKey: mutationKey({
173
- urlParams: keyParams,
174
- }),
282
+ mutationKey: mutationKey(keyParams),
175
283
  })
176
284
  return isMutating > 0
177
285
  }
@@ -0,0 +1,294 @@
1
+ import type { QueryClient } from '@tanstack/react-query'
2
+
3
+ /**
4
+ * Configuration for creating optimistic update callbacks.
5
+ *
6
+ * @template TData - The mutation response data type
7
+ * @template TVariables - The mutation variables type
8
+ * @template TQueryData - The query cache data type
9
+ */
10
+ export interface OptimisticUpdateConfig<
11
+ TData,
12
+ TVariables,
13
+ TQueryData,
14
+ > {
15
+ /**
16
+ * The query key to optimistically update.
17
+ * This should match the query key used for the affected query.
18
+ */
19
+ queryKey: readonly unknown[]
20
+
21
+ /**
22
+ * Function to compute the optimistic cache value.
23
+ * Receives the current cache data and mutation variables.
24
+ *
25
+ * @param oldData - Current data in the cache (may be undefined if not cached)
26
+ * @param variables - The mutation variables being submitted
27
+ * @returns The new optimistic cache value
28
+ */
29
+ updateFn: (oldData: TQueryData | undefined, variables: TVariables) => TQueryData
30
+
31
+ /**
32
+ * Whether to rollback on error.
33
+ * Defaults to true.
34
+ */
35
+ rollbackOnError?: boolean
36
+
37
+ /**
38
+ * Whether to invalidate the query on settlement.
39
+ * Defaults to true.
40
+ */
41
+ invalidateOnSettled?: boolean
42
+ }
43
+
44
+ /**
45
+ * Return type for optimistic update callbacks.
46
+ */
47
+ export interface OptimisticUpdateCallbacks<TData, TVariables, TQueryData> {
48
+ /**
49
+ * Called before the mutation starts. Cancels outgoing refetches,
50
+ * snapshots the current cache value, and applies the optimistic update.
51
+ */
52
+ onMutate: (
53
+ variables: TVariables,
54
+ context: { queryClient: QueryClient },
55
+ ) => Promise<{ previousData: TQueryData | undefined }>
56
+
57
+ /**
58
+ * Called when the mutation fails. Rolls back the cache to the previous
59
+ * value if rollbackOnError is enabled.
60
+ */
61
+ onError: (
62
+ err: Error,
63
+ variables: TVariables,
64
+ context: { previousData?: TQueryData; queryClient: QueryClient },
65
+ ) => void
66
+
67
+ /**
68
+ * Called when the mutation completes (success or error).
69
+ * Invalidates the query if invalidateOnSettled is enabled.
70
+ */
71
+ onSettled: (
72
+ data: TData | undefined,
73
+ error: Error | null,
74
+ variables: TVariables,
75
+ context: { queryClient: QueryClient },
76
+ ) => void
77
+ }
78
+
79
+ /**
80
+ * Creates type-safe optimistic update callbacks for mutations.
81
+ *
82
+ * This helper generates the onMutate, onError, and onSettled callbacks
83
+ * that implement the standard optimistic update pattern:
84
+ *
85
+ * 1. onMutate: Cancel refetches, snapshot cache, apply optimistic update
86
+ * 2. onError: Rollback cache to previous value on failure
87
+ * 3. onSettled: Invalidate query to refetch fresh data
88
+ *
89
+ * @param config - Configuration for the optimistic update
90
+ * @returns Object containing onMutate, onError, and onSettled callbacks
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * // Create a mutation with optimistic updates
95
+ * const updateUser = client.mutation({
96
+ * method: 'PATCH',
97
+ * url: '/users/$userId',
98
+ * requestSchema: updateUserSchema,
99
+ * responseSchema: userSchema,
100
+ * processResponse: (data) => data,
101
+ * ...createOptimisticUpdate({
102
+ * queryKey: ['users', userId],
103
+ * updateFn: (oldData, variables) => ({
104
+ * ...oldData,
105
+ * ...variables.data,
106
+ * }),
107
+ * }),
108
+ * })
109
+ * ```
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * // Optimistic update for adding an item to a list
114
+ * const addTodo = client.mutation({
115
+ * method: 'POST',
116
+ * url: '/todos',
117
+ * requestSchema: createTodoSchema,
118
+ * responseSchema: todoSchema,
119
+ * processResponse: (data) => data,
120
+ * ...createOptimisticUpdate({
121
+ * queryKey: ['todos'],
122
+ * updateFn: (oldData, variables) => [
123
+ * ...(oldData ?? []),
124
+ * { id: 'temp-id', ...variables.data, createdAt: new Date() },
125
+ * ],
126
+ * }),
127
+ * })
128
+ * ```
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * // Optimistic delete
133
+ * const deleteTodo = client.mutation({
134
+ * method: 'DELETE',
135
+ * url: '/todos/$todoId',
136
+ * responseSchema: z.object({ success: z.boolean() }),
137
+ * processResponse: (data) => data,
138
+ * ...createOptimisticUpdate({
139
+ * queryKey: ['todos'],
140
+ * updateFn: (oldData, variables) =>
141
+ * (oldData ?? []).filter((t) => t.id !== variables.urlParams.todoId),
142
+ * }),
143
+ * })
144
+ * ```
145
+ */
146
+ export function createOptimisticUpdate<
147
+ TData,
148
+ TVariables,
149
+ TQueryData,
150
+ >(config: OptimisticUpdateConfig<TData, TVariables, TQueryData>): OptimisticUpdateCallbacks<TData, TVariables, TQueryData> {
151
+ const {
152
+ queryKey,
153
+ updateFn,
154
+ rollbackOnError = true,
155
+ invalidateOnSettled = true,
156
+ } = config
157
+
158
+ return {
159
+ onMutate: async (
160
+ variables: TVariables,
161
+ context: { queryClient: QueryClient },
162
+ ) => {
163
+ // Cancel any outgoing refetches to prevent overwriting optimistic update
164
+ await context.queryClient.cancelQueries({ queryKey })
165
+
166
+ // Snapshot the previous value
167
+ const previousData = context.queryClient.getQueryData<TQueryData>(queryKey)
168
+
169
+ // Optimistically update the cache
170
+ context.queryClient.setQueryData<TQueryData>(
171
+ queryKey,
172
+ (old) => updateFn(old, variables),
173
+ )
174
+
175
+ // Return context with the previous data for potential rollback
176
+ return { previousData }
177
+ },
178
+
179
+ onError: (
180
+ _err: Error,
181
+ _variables: TVariables,
182
+ context: { previousData?: TQueryData; queryClient: QueryClient },
183
+ ) => {
184
+ // Rollback to the previous value on error
185
+ if (rollbackOnError && context?.previousData !== undefined) {
186
+ context.queryClient.setQueryData(queryKey, context.previousData)
187
+ }
188
+ },
189
+
190
+ onSettled: (
191
+ _data: TData | undefined,
192
+ _error: Error | null,
193
+ _variables: TVariables,
194
+ context: { queryClient: QueryClient },
195
+ ) => {
196
+ // Always invalidate to ensure we have the correct server state
197
+ if (invalidateOnSettled) {
198
+ void context.queryClient.invalidateQueries({ queryKey })
199
+ }
200
+ },
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Creates optimistic update callbacks that work with multiple query keys.
206
+ *
207
+ * Useful when a mutation affects multiple cached queries.
208
+ *
209
+ * @param configs - Array of optimistic update configurations
210
+ * @returns Combined callbacks that handle all specified queries
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * // Updating a user affects both user detail and user list queries
215
+ * const updateUser = client.mutation({
216
+ * method: 'PATCH',
217
+ * url: '/users/$userId',
218
+ * requestSchema: updateUserSchema,
219
+ * responseSchema: userSchema,
220
+ * processResponse: (data) => data,
221
+ * ...createMultiOptimisticUpdate([
222
+ * {
223
+ * queryKey: ['users', userId],
224
+ * updateFn: (oldData, variables) => ({ ...oldData, ...variables.data }),
225
+ * },
226
+ * {
227
+ * queryKey: ['users'],
228
+ * updateFn: (oldList, variables) =>
229
+ * (oldList ?? []).map((u) =>
230
+ * u.id === userId ? { ...u, ...variables.data } : u
231
+ * ),
232
+ * },
233
+ * ]),
234
+ * })
235
+ * ```
236
+ */
237
+ export function createMultiOptimisticUpdate<TData, TVariables>(
238
+ configs: Array<OptimisticUpdateConfig<TData, TVariables, unknown>>,
239
+ ): OptimisticUpdateCallbacks<TData, TVariables, Map<string, unknown>> {
240
+ return {
241
+ onMutate: async (
242
+ variables: TVariables,
243
+ context: { queryClient: QueryClient },
244
+ ) => {
245
+ // Cancel and snapshot all queries
246
+ const previousData = new Map<string, unknown>()
247
+
248
+ for (const config of configs) {
249
+ await context.queryClient.cancelQueries({ queryKey: config.queryKey })
250
+ const key = JSON.stringify(config.queryKey)
251
+ previousData.set(key, context.queryClient.getQueryData(config.queryKey))
252
+ context.queryClient.setQueryData(
253
+ config.queryKey,
254
+ (old: unknown) => config.updateFn(old, variables),
255
+ )
256
+ }
257
+
258
+ return { previousData }
259
+ },
260
+
261
+ onError: (
262
+ _err: Error,
263
+ _variables: TVariables,
264
+ context: { previousData?: Map<string, unknown>; queryClient: QueryClient },
265
+ ) => {
266
+ // Rollback all queries
267
+ if (context?.previousData) {
268
+ for (const config of configs) {
269
+ if (config.rollbackOnError !== false) {
270
+ const key = JSON.stringify(config.queryKey)
271
+ const previous = context.previousData.get(key)
272
+ if (previous !== undefined) {
273
+ context.queryClient.setQueryData(config.queryKey, previous)
274
+ }
275
+ }
276
+ }
277
+ }
278
+ },
279
+
280
+ onSettled: (
281
+ _data: TData | undefined,
282
+ _error: Error | null,
283
+ _variables: TVariables,
284
+ context: { queryClient: QueryClient },
285
+ ) => {
286
+ // Invalidate all queries
287
+ for (const config of configs) {
288
+ if (config.invalidateOnSettled !== false) {
289
+ void context.queryClient.invalidateQueries({ queryKey: config.queryKey })
290
+ }
291
+ }
292
+ },
293
+ }
294
+ }