Starlord
Starlord2w ago

mutation doesnt rollback query state on failure

The issue is that as soon as the applyOptimisticUpdate function is called to modify the localstore, the remoteQuerySet map within the RemoteQuerySet class is immediately updated to match the localstore values—this happens before the mutation is executed. In the first screenshot, you can see that the backend still holds the unmodified value. In the second screenshot, however, the remoteQuerySet already reflects the updated value, even though the mutation hasn’t been executed yet. Mutation would throw an exception on the backend and values are not rollbacked. I did not find the reason why remoteQuerySet is set to localstore values before mutation is even executed.
No description
No description
16 Replies
sujayakar
sujayakar2w ago
hi @Starlord ! do you have a small repro of the behavior you're observing? I'd be curious what your optimistic update looks like, in particular. one potential issue is that optimistic updates must not mutate the query values they observe (like setState in react) -- that might be causing this anomaly.
Starlord
StarlordOP2w ago
can i share some screenshots? because cant share codebase
sujayakar
sujayakar2w ago
sure!
Starlord
StarlordOP2w ago
No description
No description
No description
Starlord
StarlordOP2w ago
so once localstore setquery is called RemoteQuerySet is updated instantly to those values the problem happens in mutationInternal between applyOptimisticUpdate and MutationRequest
sujayakar
sujayakar2w ago
ah, yeah it looks like updateTaskCompletion is mutating taskStatus, which originates from localStore instead, you can make a shallow copy of the object and then pass that to updateTaskStatus
Starlord
StarlordOP2w ago
its the same way like in docs no?
No description
sujayakar
sujayakar2w ago
ah since currentValue is a number it doesn't get mutated (currentValue + increment creates a new primitive) in this case, it should work if you change updateTaskCompletion to
const newStatus = { ...taskStatus, taskState: TaskState.COMPLETING, checkStartTime: Date.now };
this.updateTaskStatus(newStatus);
const newStatus = { ...taskStatus, taskState: TaskState.COMPLETING, checkStartTime: Date.now };
this.updateTaskStatus(newStatus);
so it's creating a new JS object rather than updating the original taskStatus object that lives within the remote query set
Starlord
StarlordOP2w ago
ah ok let me try
sujayakar
sujayakar2w ago
btw, one library I really like for working with this type of stuff is https://immerjs.github.io/immer/ -- it makes it look like you're just updating stuff directly but creates new objects under the hood as needed
Introduction to Immer | Immer
Immer (German for: always) is a tiny package that allows you to work with immutable state in a more convenient way.
sujayakar
sujayakar2w ago
makes it less annoying (both for convex optimistic updates but also react, redux, etc.)
Starlord
StarlordOP2w ago
oh you were right. its working now
sujayakar
sujayakar2w ago
amazing! sorry you ran into this, it's definitely a gotcha with these types of APIs in JS
Starlord
StarlordOP2w ago
thank you
sujayakar
sujayakar2w ago
I know some libraries have a dev mode where they freeze objects (so it'll throw an error if you try to mutate them), but there's a performance penalty to doing so
Starlord
StarlordOP2w ago
thank you for the help