@wisemen/vue-core-api-utils 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +254 -287
- package/dist/index.mjs +13162 -353
- package/package.json +2 -2
- package/skills/asyncresult-handling/SKILL.md +6 -8
- package/skills/cache-management/SKILL.md +113 -76
- package/skills/foundations/SKILL.md +1 -2
- package/skills/getting-started/SKILL.md +55 -19
- package/skills/optimistic-uis/SKILL.md +107 -164
- package/skills/writing-infinitequeries/SKILL.md +108 -42
- package/skills/writing-mutations/SKILL.md +59 -34
- package/skills/writing-queries/SKILL.md +26 -14
|
@@ -1,28 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: optimistic-uis
|
|
3
3
|
description: >
|
|
4
|
-
Combining mutations,
|
|
4
|
+
Combining mutations, QueryClient.update() with built-in rollback, and AsyncResult to create responsive UIs with instant feedback; optimistic updates with automatic error reversal.
|
|
5
5
|
type: core
|
|
6
6
|
library: vue-core-api-utils
|
|
7
|
-
library_version: "
|
|
7
|
+
library_version: "1.2.0"
|
|
8
8
|
sources:
|
|
9
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/mutation.md"
|
|
10
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/query-client.md"
|
|
11
9
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/mutation/mutation.composable.ts"
|
|
12
10
|
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/utils/query-client/queryClient.ts"
|
|
13
11
|
---
|
|
14
12
|
|
|
15
13
|
# @wisemen/vue-core-api-utils — Optimistic UIs
|
|
16
14
|
|
|
17
|
-
Create fast, responsive UIs by updating the cache immediately while mutations execute in the background. Combine `useMutation()`, `
|
|
15
|
+
Create fast, responsive UIs by updating the cache immediately while mutations execute in the background. Combine `useMutation()`, `QueryClient`, and `AsyncResult` pattern matching to provide instant feedback to users.
|
|
18
16
|
|
|
19
17
|
## Setup
|
|
20
18
|
|
|
21
19
|
```typescript
|
|
22
|
-
|
|
20
|
+
// src/api/queryClient.ts
|
|
21
|
+
import { QueryClient, getTanstackQueryClient } from '@wisemen/vue-core-api-utils'
|
|
22
|
+
import type { ProjectQueryKeys } from '@/types/queryKey.type'
|
|
23
|
+
|
|
24
|
+
export function useQueryClient() {
|
|
25
|
+
return new QueryClient<ProjectQueryKeys>(getTanstackQueryClient())
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { useQuery, useMutation } from '@/api'
|
|
31
|
+
import { useQueryClient } from '@/api/queryClient'
|
|
23
32
|
import { computed } from 'vue'
|
|
24
33
|
|
|
25
34
|
const queryClient = useQueryClient()
|
|
35
|
+
|
|
26
36
|
const { result: contact } = useQuery('contactDetail', {
|
|
27
37
|
params: {
|
|
28
38
|
contactUuid: computed(() => contactUuid),
|
|
@@ -30,27 +40,24 @@ const { result: contact } = useQuery('contactDetail', {
|
|
|
30
40
|
queryFn: () => ContactService.getDetail(contactUuid),
|
|
31
41
|
})
|
|
32
42
|
|
|
33
|
-
const { execute,
|
|
43
|
+
const { execute, result: mutationResult } = useMutation({
|
|
34
44
|
queryFn: ({ body }) => ContactService.updateContact(contactUuid, body),
|
|
35
45
|
queryKeysToInvalidate: { contactList: {} },
|
|
36
46
|
})
|
|
37
47
|
|
|
38
48
|
async function handleSubmit(formData) {
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
// Optimistic update: immediate cache change
|
|
43
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
44
|
-
by: (c) => true,
|
|
49
|
+
// Optimistic update — returns rollback function
|
|
50
|
+
const { rollback } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
51
|
+
by: () => true,
|
|
45
52
|
value: (c) => ({ ...c, ...formData }),
|
|
46
53
|
})
|
|
47
54
|
|
|
48
55
|
// Execute mutation
|
|
49
|
-
const result = await execute(formData)
|
|
56
|
+
const result = await execute({ body: formData })
|
|
50
57
|
|
|
51
|
-
//
|
|
58
|
+
// Revert cache on error
|
|
52
59
|
if (result.isErr()) {
|
|
53
|
-
|
|
60
|
+
rollback()
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
```
|
|
@@ -60,72 +67,65 @@ async function handleSubmit(formData) {
|
|
|
60
67
|
### Immediate cache update while request pending
|
|
61
68
|
|
|
62
69
|
```typescript
|
|
63
|
-
const
|
|
64
|
-
params: computed(() => ({ contactUuid })),
|
|
65
|
-
queryFn: () => ContactService.getDetail(contactUuid),
|
|
66
|
-
})
|
|
70
|
+
const queryClient = useQueryClient()
|
|
67
71
|
|
|
68
|
-
const { execute
|
|
72
|
+
const { execute } = useMutation({
|
|
69
73
|
queryFn: (data) => ContactService.updateContact(contactUuid, data),
|
|
70
|
-
queryKeysToInvalidate: {
|
|
74
|
+
queryKeysToInvalidate: { contactList: {} },
|
|
71
75
|
})
|
|
72
76
|
|
|
73
77
|
async function handleSave(formData) {
|
|
74
|
-
// Cache update happens immediately
|
|
75
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
76
|
-
by: (
|
|
78
|
+
// Cache update happens immediately — UI shows new data right away
|
|
79
|
+
const { rollback } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
80
|
+
by: () => true,
|
|
77
81
|
value: (c) => ({ ...c, ...formData }),
|
|
78
82
|
})
|
|
79
83
|
|
|
80
84
|
// Mutation executes in background
|
|
81
|
-
await execute(formData)
|
|
85
|
+
const result = await execute({ body: formData })
|
|
82
86
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
// On error, revert to the previous state
|
|
88
|
+
if (result.isErr()) {
|
|
89
|
+
rollback()
|
|
90
|
+
}
|
|
86
91
|
}
|
|
87
92
|
```
|
|
88
93
|
|
|
89
|
-
Users see changes instantly.
|
|
94
|
+
Users see changes instantly. No perceived latency. If the server rejects the change, `rollback()` restores the previous cache state automatically.
|
|
90
95
|
|
|
91
96
|
### Error handling with AsyncResult
|
|
92
97
|
|
|
93
98
|
```typescript
|
|
94
99
|
async function handleSave(formData) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
98
|
-
by: (c) => true,
|
|
100
|
+
const { rollback } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
101
|
+
by: () => true,
|
|
99
102
|
value: (c) => ({ ...c, ...formData }),
|
|
100
103
|
})
|
|
101
104
|
|
|
102
|
-
const result = await execute(formData)
|
|
105
|
+
const result = await execute({ body: formData })
|
|
103
106
|
|
|
104
|
-
// Match on mutation result
|
|
105
107
|
result.match({
|
|
106
108
|
ok: () => {
|
|
107
109
|
// Server confirmed the update
|
|
108
|
-
// Cache already reflects the change
|
|
110
|
+
// Cache already reflects the change from the optimistic update
|
|
109
111
|
showSuccessMessage('Contact updated')
|
|
110
112
|
},
|
|
111
113
|
err: (error) => {
|
|
112
114
|
// Revert optimistic update
|
|
113
|
-
|
|
114
|
-
showErrorMessage(`Failed: ${error.message}`)
|
|
115
|
+
rollback()
|
|
116
|
+
showErrorMessage(`Failed: ${'errors' in error ? error.errors[0].detail : error.message}`)
|
|
115
117
|
},
|
|
116
118
|
loading: () => {
|
|
117
|
-
//
|
|
119
|
+
// Won't happen after await, but required by match()
|
|
118
120
|
},
|
|
119
121
|
})
|
|
120
122
|
}
|
|
121
123
|
```
|
|
122
124
|
|
|
123
|
-
When mutation fails, revert the optimistic change using the saved original value.
|
|
124
|
-
|
|
125
125
|
### Composable combining query + mutation + optimistic UI
|
|
126
126
|
|
|
127
127
|
```typescript
|
|
128
|
-
export function useContactEditor(contactUuid) {
|
|
128
|
+
export function useContactEditor(contactUuid: string) {
|
|
129
129
|
const queryClient = useQueryClient()
|
|
130
130
|
|
|
131
131
|
const { result: contact } = useQuery('contactDetail', {
|
|
@@ -133,29 +133,26 @@ export function useContactEditor(contactUuid) {
|
|
|
133
133
|
queryFn: () => ContactService.getDetail(contactUuid),
|
|
134
134
|
})
|
|
135
135
|
|
|
136
|
-
const { execute,
|
|
137
|
-
queryFn: (
|
|
136
|
+
const { execute, result: mutationResult } = useMutation({
|
|
137
|
+
queryFn: ({ body }) => ContactService.updateContact(contactUuid, body),
|
|
138
138
|
queryKeysToInvalidate: {
|
|
139
|
-
contactList:
|
|
140
|
-
'contact-stats': () => true,
|
|
139
|
+
contactList: {},
|
|
141
140
|
},
|
|
142
141
|
})
|
|
143
142
|
|
|
144
143
|
async function saveContact(formData) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Optimistic
|
|
148
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
144
|
+
// Optimistic update
|
|
145
|
+
const { rollback } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
149
146
|
by: () => true,
|
|
150
147
|
value: (c) => ({ ...c, ...formData }),
|
|
151
148
|
})
|
|
152
149
|
|
|
153
150
|
// Execute
|
|
154
|
-
const result = await execute(formData)
|
|
151
|
+
const result = await execute({ body: formData })
|
|
155
152
|
|
|
156
153
|
// Rollback on error
|
|
157
154
|
if (result.isErr()) {
|
|
158
|
-
|
|
155
|
+
rollback()
|
|
159
156
|
}
|
|
160
157
|
|
|
161
158
|
return result
|
|
@@ -164,7 +161,6 @@ export function useContactEditor(contactUuid) {
|
|
|
164
161
|
return {
|
|
165
162
|
contact,
|
|
166
163
|
saveContact,
|
|
167
|
-
isLoading,
|
|
168
164
|
mutationResult,
|
|
169
165
|
}
|
|
170
166
|
}
|
|
@@ -174,227 +170,174 @@ Encapsulate the full flow in a composable for reusability across components.
|
|
|
174
170
|
|
|
175
171
|
## Common Mistakes
|
|
176
172
|
|
|
177
|
-
### HIGH:
|
|
173
|
+
### HIGH: Ignore the rollback return from update(); can't revert on error
|
|
178
174
|
|
|
179
175
|
```typescript
|
|
180
|
-
// ❌ Wrong:
|
|
181
|
-
const { execute, result } = useMutation({
|
|
182
|
-
queryFn: (data) => ContactService.updateContact(data),
|
|
183
|
-
queryKeysToInvalidate: { contactList: () => true },
|
|
184
|
-
})
|
|
185
|
-
|
|
176
|
+
// ❌ Wrong: discarding rollback
|
|
186
177
|
async function handleSave(formData) {
|
|
187
|
-
|
|
188
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
178
|
+
queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
189
179
|
by: () => true,
|
|
190
180
|
value: (c) => ({ ...c, ...formData }),
|
|
191
181
|
})
|
|
182
|
+
// rollback discarded!
|
|
192
183
|
|
|
193
|
-
const result = await execute(formData)
|
|
184
|
+
const result = await execute({ body: formData })
|
|
194
185
|
|
|
195
186
|
if (result.isErr()) {
|
|
196
187
|
// No way to undo the optimistic change!
|
|
197
188
|
showErrorMessage('Save failed')
|
|
189
|
+
// UI now shows wrong data permanently
|
|
198
190
|
}
|
|
199
191
|
}
|
|
200
192
|
```
|
|
201
193
|
|
|
202
194
|
```typescript
|
|
203
|
-
// ✅ Correct:
|
|
195
|
+
// ✅ Correct: capture and use rollback
|
|
204
196
|
async function handleSave(formData) {
|
|
205
|
-
|
|
206
|
-
const originalContact = contact.value?.getValue()
|
|
207
|
-
|
|
208
|
-
// Update cache
|
|
209
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
197
|
+
const { rollback } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
210
198
|
by: () => true,
|
|
211
199
|
value: (c) => ({ ...c, ...formData }),
|
|
212
200
|
})
|
|
213
201
|
|
|
214
|
-
const result = await execute(formData)
|
|
202
|
+
const result = await execute({ body: formData })
|
|
215
203
|
|
|
216
204
|
if (result.isErr()) {
|
|
217
|
-
//
|
|
218
|
-
queryClient.set(['contactDetail', { contactUuid }], originalContact)
|
|
205
|
+
rollback() // Restores previous state automatically
|
|
219
206
|
showErrorMessage('Save failed, changes reverted')
|
|
220
207
|
}
|
|
221
208
|
}
|
|
222
209
|
```
|
|
223
210
|
|
|
224
|
-
|
|
211
|
+
`update()` captures the previous state internally and exposes it via `rollback()`. Always capture and use it when doing optimistic updates.
|
|
225
212
|
|
|
226
|
-
Source: `
|
|
213
|
+
Source: `src/utils/query-client/queryClient.ts` — `QueryClientUpdateResult`
|
|
227
214
|
|
|
228
|
-
### CRITICAL:
|
|
215
|
+
### CRITICAL: Stale optimistic update if component unmounts during pending mutation
|
|
229
216
|
|
|
230
217
|
```typescript
|
|
231
|
-
// ❌ Wrong:
|
|
218
|
+
// ❌ Wrong: mutation pending, user navigates away, rollback never called
|
|
232
219
|
async function handleSave(formData) {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
220
|
+
const { rollback } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
236
221
|
by: () => true,
|
|
237
222
|
value: (c) => ({ ...c, ...formData }),
|
|
238
223
|
})
|
|
239
224
|
|
|
240
|
-
// User navigates away while mutation pending
|
|
225
|
+
// User navigates away while mutation is pending
|
|
241
226
|
// Cache still has optimistic data
|
|
242
|
-
|
|
243
|
-
const result = await execute(formData)
|
|
227
|
+
const result = await execute({ body: formData })
|
|
244
228
|
if (result.isErr()) {
|
|
245
|
-
//
|
|
246
|
-
queryClient.set(['contactDetail', { contactUuid }], originalContact)
|
|
229
|
+
rollback() // User is gone — rollback still works but user won't see it
|
|
247
230
|
}
|
|
248
231
|
}
|
|
249
232
|
```
|
|
250
233
|
|
|
251
234
|
```typescript
|
|
252
|
-
// ✅ Correct:
|
|
235
|
+
// ✅ Correct: rollback on unmount if mutation is still in flight
|
|
253
236
|
import { onUnmounted } from 'vue'
|
|
254
237
|
|
|
255
|
-
let
|
|
256
|
-
let isSaving = false
|
|
238
|
+
let pendingRollback: (() => void) | null = null
|
|
257
239
|
|
|
258
240
|
async function handleSave(formData) {
|
|
259
|
-
|
|
260
|
-
isSaving = true
|
|
261
|
-
|
|
262
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
241
|
+
const { rollback } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
263
242
|
by: () => true,
|
|
264
243
|
value: (c) => ({ ...c, ...formData }),
|
|
265
244
|
})
|
|
245
|
+
pendingRollback = rollback
|
|
266
246
|
|
|
267
|
-
const result = await execute(formData)
|
|
268
|
-
|
|
247
|
+
const result = await execute({ body: formData })
|
|
248
|
+
pendingRollback = null
|
|
269
249
|
|
|
270
|
-
if (result.isErr()
|
|
271
|
-
|
|
250
|
+
if (result.isErr()) {
|
|
251
|
+
rollback()
|
|
272
252
|
}
|
|
273
253
|
}
|
|
274
254
|
|
|
275
255
|
onUnmounted(() => {
|
|
276
|
-
// If still saving and user leaves, rollback
|
|
277
|
-
|
|
278
|
-
queryClient.set(['contactDetail', { contactUuid }], originalContact)
|
|
279
|
-
}
|
|
256
|
+
// If still saving and user leaves, rollback stale optimistic update
|
|
257
|
+
pendingRollback?.()
|
|
280
258
|
})
|
|
281
259
|
```
|
|
282
260
|
|
|
283
|
-
If user navigates away while a mutation is pending, rollback the optimistic change using component lifecycle hooks. Otherwise, stale data persists in the cache.
|
|
261
|
+
If the user navigates away while a mutation is pending, rollback the optimistic change using component lifecycle hooks. Otherwise, stale unconfirmed data persists in the global cache.
|
|
284
262
|
|
|
285
263
|
Source: Architectural consideration from cache invalidation patterns
|
|
286
264
|
|
|
287
|
-
### HIGH:
|
|
265
|
+
### HIGH: Optimistic update on list query with wrong predicate; patches wrong item
|
|
288
266
|
|
|
289
267
|
```typescript
|
|
290
|
-
// ❌ Wrong: update
|
|
291
|
-
const { result: paginatedContacts } = useQuery('contactList', {
|
|
292
|
-
params: computed(() => ({ limit: 20, offset: 0 })),
|
|
293
|
-
queryFn: () => ContactService.list({ limit: 20, offset: 0 }),
|
|
294
|
-
})
|
|
295
|
-
|
|
268
|
+
// ❌ Wrong: update without proper predicate
|
|
296
269
|
queryClient.update('contactList', {
|
|
297
|
-
by: (
|
|
298
|
-
value: (
|
|
270
|
+
by: () => true, // Matches ALL contacts in the list!
|
|
271
|
+
value: (c) => ({ ...c, name: 'Updated' }),
|
|
299
272
|
})
|
|
300
|
-
//
|
|
273
|
+
// Every contact in the cached list gets "Updated" as name
|
|
301
274
|
```
|
|
302
275
|
|
|
303
276
|
```typescript
|
|
304
|
-
// ✅ Correct:
|
|
305
|
-
const { result: paginatedContacts } = useQuery('contactList', {
|
|
306
|
-
params: computed(() => ({ limit: 20, offset: 0 })),
|
|
307
|
-
queryFn: () => ContactService.list({ limit: 20, offset: 0 }),
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
// QueryClient infers the data structure from query keys
|
|
311
|
-
// If contactList query returns { data: Contact[], meta: {...} }
|
|
312
|
-
// QueryClient knows to iterate contact.data and apply the predicate
|
|
277
|
+
// ✅ Correct: use a specific predicate
|
|
313
278
|
queryClient.update('contactList', {
|
|
314
|
-
by: (contact) => contact.
|
|
315
|
-
value: (contact) => ({ ...contact, name:
|
|
279
|
+
by: (contact) => contact.uuid === contactUuid, // Only match the specific item
|
|
280
|
+
value: (contact) => ({ ...contact, name: formData.name }),
|
|
316
281
|
})
|
|
317
282
|
```
|
|
318
283
|
|
|
319
|
-
|
|
284
|
+
When updating lists, always use a predicate that uniquely identifies the item you're changing. `by: () => true` is only appropriate for single-entity queries.
|
|
320
285
|
|
|
321
|
-
Source: `
|
|
286
|
+
Source: `src/utils/query-client/queryClient.ts` — `updateEntity` iterates arrays
|
|
322
287
|
|
|
323
|
-
### MEDIUM: Race condition: multiple mutations affect the same query
|
|
288
|
+
### MEDIUM: Race condition: multiple mutations affect the same query
|
|
324
289
|
|
|
325
290
|
```typescript
|
|
326
291
|
// ❌ Wrong: two mutations in flight affecting the same cache
|
|
327
292
|
async function handleMultipleSaves() {
|
|
328
|
-
// Mutation 1
|
|
329
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
293
|
+
// Mutation 1 optimistic update
|
|
294
|
+
const { rollback: rollback1 } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
330
295
|
by: () => true,
|
|
331
296
|
value: (c) => ({ ...c, tags: [...c.tags, 'new-tag'] }),
|
|
332
297
|
})
|
|
333
|
-
|
|
298
|
+
// Don't await — start Mutation 2 immediately!
|
|
299
|
+
execute1({ body: { tags: [...contact.tags, 'new-tag'] } })
|
|
334
300
|
|
|
335
|
-
// Mutation 2
|
|
336
|
-
|
|
337
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
301
|
+
// Mutation 2 on same cache entry
|
|
302
|
+
const { rollback: rollback2 } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
338
303
|
by: () => true,
|
|
339
304
|
value: (c) => ({ ...c, name: 'Updated' }),
|
|
340
305
|
})
|
|
341
|
-
|
|
306
|
+
await execute2({ body: { name: 'Updated' } })
|
|
342
307
|
|
|
343
|
-
// If Mutation 2 completes before Mutation 1, the tags
|
|
308
|
+
// If Mutation 2 completes before Mutation 1, the tags may be lost
|
|
344
309
|
}
|
|
345
310
|
```
|
|
346
311
|
|
|
347
312
|
```typescript
|
|
348
|
-
// ✅ Correct: serialize mutations
|
|
313
|
+
// ✅ Correct: serialize mutations to avoid ordering issues
|
|
349
314
|
async function handleMultipleSaves() {
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
// Either: await each mutation before starting the next
|
|
353
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
315
|
+
const { rollback: rollback1 } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
354
316
|
by: () => true,
|
|
355
317
|
value: (c) => ({ ...c, tags: [...c.tags, 'new-tag'] }),
|
|
356
318
|
})
|
|
357
|
-
const result1 = await
|
|
319
|
+
const result1 = await execute1({ body: { tags: [...contact.tags, 'new-tag'] } })
|
|
358
320
|
if (result1.isErr()) {
|
|
359
|
-
|
|
321
|
+
rollback1()
|
|
360
322
|
return
|
|
361
323
|
}
|
|
362
324
|
|
|
363
|
-
//
|
|
364
|
-
const
|
|
365
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
325
|
+
// Only start Mutation 2 after Mutation 1 finishes
|
|
326
|
+
const { rollback: rollback2 } = queryClient.update(['contactDetail', { contactUuid }] as const, {
|
|
366
327
|
by: () => true,
|
|
367
328
|
value: (c) => ({ ...c, name: 'Updated' }),
|
|
368
329
|
})
|
|
369
|
-
const result2 = await
|
|
330
|
+
const result2 = await execute2({ body: { name: 'Updated' } })
|
|
370
331
|
if (result2.isErr()) {
|
|
371
|
-
|
|
332
|
+
rollback2()
|
|
372
333
|
}
|
|
373
334
|
}
|
|
374
335
|
```
|
|
375
336
|
|
|
376
|
-
|
|
337
|
+
Serialize mutations that affect the same cache entry. Concurrent mutations on the same key can lead to lost updates or incorrect rollbacks.
|
|
377
338
|
|
|
378
339
|
Source: Architectural consideration from query lifecycle patterns
|
|
379
340
|
|
|
380
|
-
## Rollback Strategy
|
|
381
|
-
|
|
382
|
-
Rollback is enabled by saving the original data before the optimistic update:
|
|
383
|
-
|
|
384
|
-
```typescript
|
|
385
|
-
const original = contact.value?.getValue()
|
|
386
|
-
queryClient.update(['contactDetail', { contactUuid }], {
|
|
387
|
-
by: () => true,
|
|
388
|
-
value: (c) => ({ ...c, ...formData }),
|
|
389
|
-
})
|
|
390
|
-
const result = await execute(formData)
|
|
391
|
-
if (result.isErr()) {
|
|
392
|
-
queryClient.set(['contactDetail', { contactUuid }], original)
|
|
393
|
-
}
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
However, rollback patterns for complex scenarios (partial field updates, nested objects) are being refined in the library. For now, save and restore the entire entity.
|
|
397
|
-
|
|
398
341
|
## See Also
|
|
399
342
|
|
|
400
343
|
- [Writing Mutations](../writing-mutations/SKILL.md) — The `execute()` and result handling that pairs with optimistic updates
|