Closed sjdeak closed 8 months ago
Hi @sjdeak , thanks for raising this issue.
However, I was not able to reproduce the issue on the latest version of aws-amplify (v6.0.8). The same pattern of save, query, update resulted in the following graphql request. The version was correctly set to 1 and the mutation included the latest record data (name: "V2")
Looks like you're on 6.0.6 but I wasn't able to reproduce the issue when I downgraded either so you can upgrade but that wouldn't fix the issue. Might be configuration related.
Can you run amplify update api
> GraphQL
and confirm that Conflict detection
is enabled?
If it is enabled, what Conflict Resolution Strategy are you using? (ex. Auto Merge, Optimistic Concurrency, etc)
One thing I did notice though, is that the very first time I created a Habit
there was only a createHabit
Mutation with the latest name
value.
Afterwards, I was seeing the createHabit
with the initial name: "V1"
and then updateHabit
mutations with the updated name: "V2"
value.
Not representative of the issue, but I thought that was interesting.
Hi @chrisbonifacio Many thanks for the rapid response, did you enable the owner based policy when reproduce the issue? This issue only occurs when owner-based policy is enabled, you can also fork my reproduce repo and verify it.
I confirmed conflict resolution is enabled, output of amplify cli:
➜ amplify update api
? Select from one of the below mentioned services: GraphQL
General information
- Name: learn
- API endpoint: https://4gwicf2oezd6pe52nosfkahfm4.appsync-api.us-west-2.amazonaws.com/graphql
Authorization modes
- Default: API key expiring Thu Dec 28 2023 15:00:00 GMT+0800 (China Standard Time): <hide>
- Amazon Cognito User Pool
Conflict detection (required for DataStore)
- Conflict resolution strategy: Auto Merge
Also share the network request screenshot in my side:
Ah, I see. Yes, now that I include the owner auth rule, the datastore fields (_version, _lastChangedAt, _deleted) are not being sent in the mutations.
I will mark this as a bug for the team to investigate further as it is consistently reproducible.
Hi @sjdeak after discussing with the team, it seems that this behavior is a result of latency between the first create mutation being sent out and the record syncing back down to the client with the owner field set in the resolver.
On faster network connections, this seems to happen fast enough to not be an issue, but is reproducible on slightly slower connections. We are tracking this bug on a separate ticket (https://github.com/aws-amplify/amplify-js/issues/9979), and the team is working on a fix.
Is there a reason why you have to update the record immediately after it was created?
One way to work around this is by checking to make sure that the first mutation has been processed by the DataStore outbox prior to attempting a second mutation, or simply performing the update in a function or event separate from the creation.
In our docs, we provide some guidance around working with updates. For example, you can create a record, then set a copy of the record to state and update that until it's ready to be persisted to the server.
The most full-proof way to ensure that a rapid mutation will persist is to first check that the previous mutation has cleared the outbox. Here's an example of how you might do that:
/**
* Watches Hub events until an outBoxStatus with isEmpty is received.
*
* NOTICE: If the outbox is *already* empty, this will not resolve.
*
* @param verbose Whether to log hub events until empty
*/
export async function waitForEmptyOutbox(verbose = false) {
return new Promise<void>(resolve => {
const { Hub } = require('@aws-amplify/core');
const hubCallback = message => {
if (verbose) console.log('hub event', message);
if (
message.payload.event === 'outboxStatus' &&
message.payload.data.isEmpty
) {
removeListener();
resolve();
}
};
const removeListener = Hub.listen('datastore', hubCallback);
});
}
Hi Chris, I think this issue is not related to network latency, made a video demo (with voice explanation):
https://github.com/aws-amplify/amplify-js/assets/8575264/e6b95409-6c68-4135-a48a-f6a9d5e093f1
@chrisbonifacio Would you have any updates?
Hi @sjdeak, Happy New Year! Apologies for the delayed response. Thank you for sharing the video with more context. I don't have an update at the moment, just wanted to let you know this is on the team's radar and we are looking into a fix.
We'll post any updates as they happen, thank you for your patience 🙏
Were you able to resolve the issue temporarily using some of the guidance shared before?
Hi @chrisbonifacio Happy new year too! Looking for the good news. I can't temporarily solving this issue, in our application (https://www.gaminote.com) we let users create time labels and its very frequent that users will want to change the label property just after its creation, so it's very important to us, otherwise users will feel very frustrated as all their changes are not saved.
Reading through the correspondence here in more detail, I think I agree that the issue isn't resolved with the update sequencing fix that was merged earlier this week.
I just reproduced your issue and I think I understand whats happening here. Because habitJustSaved
is queried immediately after the create call to Datastore, it isn't updated when Datastore sends the information to AppSync and received the initial _version
back.
I would recommend querying for the current object just before each update to make sure your updating the latest version, similar to what you'll see in the docs. Using an older object means that in cases like this it will lose conflict resolution and your changes will be ignored preferring the existing field content.
I modified the example app with the following handleClick change, moving Step 2, which resolved the issue for me.
const handleClick = async () => {
const newHabit = await DataStore.save(new Habit({name: "V1", count: 1})); // Step 1
// const habitJustSaved = await DataStore.query(Habit, newHabit.id); // Step 2 <- Removed
console.log("Start to wait 5 seconds");
await new Promise(resolve => setTimeout(resolve, 5000));
console.log(await DataStore.query(Habit, newHabit.id));
console.log("Start to save modified habit");
const currentHabit = await DataStore.query(Habit, newHabit.id); // Step 2 <- Added
const modified = Habit.copyOf(currentHabit, updated => { // Step 3
updated.name = "V2";
});
console.log({modified});
await DataStore.save(modified);
};
Can you give this a try and let us know if its working as expected?
Thanks, Aaron
Hi @stocaaro thanks for the reply! Your workaround do work for me. But I think Amplify could improve this part.
DataStore.save
which can ease a lot of users.Hello @sjdeak , On the page you've linked, I believe the section callout titled "Avoid working with stale data!" is meant to document the problem and how to address it. It's a complicated enough problem, that I'm not sure how to phrase it more clearly.
You make a good point about there being an opportunity to improve Datastore.save
. On Android/Swift/Flutter, the record state is managed independently of versioning, which allows save to behave as you describe, and we have a backlog item to investigate porting this across to the typescript library.
Since your concern is covered in the docs and there is work in the backlog that would address the feature change your suggesting, I'm going to close this issue.
Appreciate your input!
Thanks, Aaron
Thanks Aaron for your kind support, hope the backlog could be implemented in the future 🤝
Before opening, please confirm:
JavaScript Framework
React
Amplify APIs
DataStore
Amplify Categories
api
Environment information
Describe the bug
the modification in step 3 will not be reflected.
Expected behavior
the modification in step 3 will not be reflected.
Reproduction steps
schema.graphql
JavaScript code
In the end, step 3 will have no effect, the saved habit name is still "V1" I observed that for step 3 there is graphql mutation request sent, however
_version
param is not included in step 3, which causes AppSync just returned original Habit data without modification applied.Code Snippet
No response
Log output
aws-exports.js
No response
Manual configuration
No response
Additional configuration
No response
Mobile Device
No response
Mobile Operating System
No response
Mobile Browser
No response
Mobile Browser Version
No response
Additional information and screenshots
No response