Skip to content

DataStore React Native: updating a record before deleting results in repeated failed requests due to a conflict. #11708

Open
@duckbytes

Description

@duckbytes

Before opening, please confirm:

JavaScript Framework

React Native

Amplify APIs

GraphQL API, DataStore

Amplify Categories

auth, api

Environment information

  System:
    OS: Linux 6.4 Arch Linux
    CPU: (16) x64 AMD Ryzen 7 3700X 8-Core Processor
    Memory: 10.35 GB / 31.26 GB
    Container: Yes
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 16.13.2 - ~/.nvm/versions/node/v16.13.2/bin/node
    Yarn: 1.22.19 - /usr/bin/yarn
    npm: 8.1.2 - ~/.nvm/versions/node/v16.13.2/bin/npm
  npmPackages:
    @aws-amplify/datastore-storage-adapter: ^2.0.42 => 2.0.42
    @aws-amplify/ui-react-native: ^1.2.20 => 1.2.20
    @azure/core-asynciterator-polyfill: ^1.0.2 => 1.0.2
    @babel/core: ^7.20.0 => 7.22.9 (7.9.0)
    @expo/webpack-config: ^18.0.1 => 18.1.1
    @react-native-async-storage/async-storage: 1.17.11 => 1.17.11
    @react-native-community/netinfo: 9.3.7 => 9.3.7
    @react-navigation/bottom-tabs: ^6.5.8 => 6.5.8
    @react-navigation/material-bottom-tabs: ^6.2.16 => 6.2.16
    @react-navigation/native-stack: ^6.9.13 => 6.9.13
    @reduxjs/toolkit: ^1.9.5 => 1.9.5
    @reduxjs/toolkit-query:  1.0.0
    @reduxjs/toolkit-query-react:  1.0.0
    @testing-library/jest-native: ^5.4.2 => 5.4.2
    @testing-library/react-native: ^12.1.3 => 12.1.3
    @types/jest: ^29.5.3 => 29.5.3
    @types/react: ~18.0.14 => 18.0.38
    HelloWorld:  0.0.1
    amazon-cognito-identity-js: ^6.3.1 => 6.3.1
    amazon-cognito-identity-js/internals:  undefined ()
    aws-amplify: ^5.3.6 => 5.3.6
    core-js: ^3.31.1 => 3.31.1
    experiments-app:  1.0.0
    expo: ~48.0.18 => 48.0.20
    expo-blur: ~12.2.2 => 12.2.2
    expo-clipboard: ~4.1.2 => 4.1.2
    expo-file-system: ~15.2.2 => 15.2.2 (15.3.0, 11.1.3)
    expo-haptics: ~12.2.1 => 12.2.1
    expo-sqlite: ~11.1.1 => 11.1.1
    expo-status-bar: ~1.4.4 => 1.4.4
    faker: 5.5.3 => 5.5.3
    jest-expo: ^49.0.0 => 49.0.0
    mock-async-storage: ^2.2.0 => 2.2.0
    moment: ^2.29.4 => 2.29.4
    moment-timezone: ^0.5.43 => 0.5.43
    react: 18.2.0 => 18.2.0
    react-content-loader: ^6.2.1 => 6.2.1
    react-content-loader/native:  undefined ()
    react-native: 0.71.8 => 0.71.8
    react-native-dotenv: ^3.4.9 => 3.4.9
    react-native-gesture-handler: ~2.9.0 => 2.9.0
    react-native-get-random-values: ~1.8.0 => 1.8.0
    react-native-hold-menu: ^0.1.6 => 0.1.6
    react-native-paper: ^5.9.1 => 5.9.1
    react-native-paper-dates: ^0.18.12 => 0.18.12
    react-native-reanimated: ~2.14.4 => 2.14.4
    react-native-safe-area-context: 4.5.0 => 4.5.0
    react-native-screens: ~3.20.0 => 3.20.0
    react-native-svg: 13.4.0 => 13.4.0
    react-native-testing-library-website:  0.0.0
    react-native-unimodules: ^0.14.10 => 0.14.10
    react-native-url-polyfill: ^2.0.0 => 2.0.0 (1.3.0)
    react-native-web: ~0.18.10 => 0.18.12
    react-navigation-example:  0.0.1
    react-redux: ^8.1.1 => 8.1.1
    redux-example:  0.0.1
    redux-saga: ^1.2.3 => 1.2.3
    redux-saga/effects:  undefined ()
    typescript: ^4.9.4 => 4.9.5
  npmGlobalPackages:
    @aws-amplify/cli: 11.0.3
    @bubblewrap/cli: 1.18.1
    @ionic/cli: 6.20.3
    amplify-cli: 1.0.0
    cordova: 11.1.0
    corepack: 0.10.0
    create-react-native-app: 3.9.0
    deadfile: 2.0.1
    docsify-cli: 4.4.4
    graphql-language-service-cli: 3.3.16
    jsonminify: 0.4.2
    mjson: 0.4.2
    native-run: 1.7.1
    npm: 8.1.2
    prebuild-install: 7.1.1
    react_app: 0.1.0
    react-dom: 17.0.2
    react-js-to-ts: 1.4.0
    react: 18.2.0
    serve: 14.2.0
    ts-node: 10.9.1
    typescript: 4.8.2
    uglify-js: 3.17.4
    uglifyjs: 2.4.11

Describe the bug

I would like to modify a record before deleting it with DataStore. In my case, I have a Comment model, and I would like to strip the body before deleting it. I am using optimistic concurrency.

If I only delete the record, everything works fine. If I modify the record before deleting it, I get conflict errors that are never resolved.

On the web version of my app, I was able to work around this issue by returning the remote model in the conflict resolver. This is using amplify 4.3.46. I'm now developing a mobile version with amplify 5.3.6 using the same backend. The workaround no longer seems to work, and DataStore continually tries to send the mutation.

However the data on the backend does seem to update successfully as reflected in dynamodb.

I thought a better solution to this problem would be to instead delete the body during the delete mutation with a custom override, but I wasn't able to figure out how to do it.

I'm not able to look at the actual network data as it seems to be quite difficult in React Native, but here are the responses I see in the web version, which complete successfully after one failed attempt:

Screenshot from 2023-08-02 21-48-21
Screenshot from 2023-08-02 21-48-28
Screenshot from 2023-08-02 21-48-43
Screenshot from 2023-08-02 21-48-50
Screenshot from 2023-08-02 21-48-59
Screenshot from 2023-08-02 21-49-05

Expected behavior

I would expect datastore to not continually try to send mutations after deleting a record.

Reproduction steps

  1. create a project with expo
  2. npx expo install @aws-amplify/datastore-storage-adapter @aws-amplify/ui-react-native @azure/core-asynciterator-polyfill @react-native-async-storage/async-storage amazon-cognito-identity-js expo-sqlite
  3. use Authenticator component
  4. initialise amplify
  5. create a graphql API with DataStore enabled and select optimistic concurrency

Code Snippet

My model in the schema:

type Comment
@auth(rules: [
  {allow: groups, groups: ["USER"], operations: [read]},
  {allow: owner, operations: [create, read, delete, update]},
  {allow: groups, groups: ["ADMIN"], operations: [create, read, delete, update]},
])
@model {
  id: ID!
  parentId: ID @index(name: "byParent")
  owner: String
    @auth(rules: [
      {allow: groups, groups: ["USER"], operations: [read]},
      {allow: owner, operations: [create, read, delete]},
      {allow: groups, groups: ["ADMIN"], operations: [create, read, delete]},
    ])
  tenantId: ID! @index(name: "byTenantId")
    @auth(rules: [
      {allow: groups, groups: ["USER"], operations: [read]},
      {allow: owner, operations: [create, read, delete]},
      {allow: groups, groups: ["ADMIN"], operations: [create, read, delete]},
    ])
  body: String
  author: User @belongsTo
  visibility: CommentVisibility
  archived: Int @default(value: "0") @index(name: "byArchived")
    @auth(rules: [
      {allow: groups, groups: ["USER"], operations: [read]},
      {allow: owner, operations: [create, read, delete]},
      {allow: groups, groups: ["ADMIN"], operations: [create, read, delete]},
    ])
}

App.tsx

import * as React from "react";
import "@azure/core-asynciterator-polyfill";
import { DataStore } from "aws-amplify";
import { ExpoSQLiteAdapter } from "@aws-amplify/datastore-storage-adapter/ExpoSQLiteAdapter";
import { Authenticator } from "@aws-amplify/ui-react-native";
import { Amplify } from "aws-amplify";
import config from "./src/aws-exports";

Amplify.configure(config);

DataStore.configure({
    storageAdapter: ExpoSQLiteAdapter,
});





const App = () => {    
        return (
            <Authenticator.Provider>
                <Authenticator loginMechanisms={["email"]}>
                            <Main />
                </Authenticator>
            </Authenticator.Provider>
        );
};

export default App;

deleteComment function

    const deleteComment = async () => {
        try {
            if (selectedComment) {
                const existingComment = await DataStore.query(
                    models.Comment,
                    selectedComment.id
                );
                if (existingComment) {
                    // if this part is removed, everything works
                    const updated = await DataStore.save(
                        models.Comment.copyOf(existingComment, (upd) => {
                            upd.body = "";
                        })
                    );
                    await DataStore.delete(updated);
                }
            }
        } catch (e) {
            console.log(e);
        }

    };

Conflict resolver:

import { DISCARD } from "@aws-amplify/datastore";
import {
    SyncConflict,
    PersistentModel,
    PersistentModelConstructor,
} from "@aws-amplify/datastore";
import * as models from "../../models";
import determineTaskStatus from "../../utilities/determineTaskStatus";

const dataStoreConflictHandler = async (
    conflict: SyncConflict
): Promise<symbol | PersistentModel> => {
    const { modelConstructor, localModel, remoteModel } = conflict;
    console.log(
        "DataStore has found a conflict",
        modelConstructor,
        remoteModel,
        localModel
    );
    
     if (
        modelConstructor ===
        (models.Comment as PersistentModelConstructor<models.Comment>)
    ) {
        return remoteModel;
    }
    return DISCARD;
};

export default dataStoreConflictHandler;

DataStore.configure (run in redux-saga)

                yield call([DataStore, DataStore.configure], {
                    errorHandler: (err) => {
                        console.log("DataStore error:", err);
                        console.log("Cause:", err.cause);
                    },
                    syncExpressions: [
                        ...modelsToSync.map((model) =>
                            syncExpression(
                                model,
                                () => (m) => m.tenantId.eq(tenantId)
                            )
                        ),
                        ...archivedModels.map((model) =>
                            syncExpression(
                                model,
                                () => (m) =>
                                    m.and((m) => [
                                        m.tenantId.eq(tenantId),
                                        m.archived.eq(0),
                                    ])
                            )
                        ),
                        syncExpression(
                            models.Tenant,
                            () => (m) => m.id.eq(tenantId)
                        ),
                    ],
                    conflictHandler: dataStoreConflictHandler,
                });

Log output

This is the error I see when immediately trying to delete a comment:

 LOG  DataStore has found a conflict [Function Comment] {"_deleted": null, "_lastChangedAt": 1691010033913, "_version": 2, "archived": 0, "body": "", "createdAt": "2023-08-02T21:00:29.636Z", "id": "f373b642-6982-4ed1-8029-c2aff869b0c1", "owner": null, "parentId": "1f40c987-c109-4b7d-8b1a-188080f86f3d", "tenantId": "7b18c148-6259-4adf-948b-257756e6eb4e", "updatedAt": "2023-08-02T21:00:33.878Z", "userCommentsId": undefined, "visibility": "EVERYONE"} {"_deleted": undefined, "_lastChangedAt": undefined, "_version": 1, "archived": null, "body": null, "createdAt": null, "id": "f373b642-6982-4ed1-8029-c2aff869b0c1", "owner": null, "parentId": null, "tenantId": null, "updatedAt": null, "visibility": null}
 LOG  DataStore error: {"cause": [Error: RetryMutation], "errorType": "Unknown", "localModel": null, "message": "RetryMutation", "model": "Comment", "operation": undefined, "process": "sync", "recoverySuggestion": "Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues", "remoteModel": null}
 LOG  Cause: [Error: RetryMutation]

On reloading the app I see the same error:

 LOG  DataStore has found a conflict [Function Comment] {"_deleted": true, "_lastChangedAt": 1691010034378, "_version": 3, "archived": 0, "body": "", "createdAt": "2023-08-02T21:00:29.636Z", "id": "f373b642-6982-4ed1-8029-c2aff869b0c1", "owner": null, "parentId": "1f40c987-c109-4b7d-8b1a-188080f86f3d", "tenantId": "7b18c148-6259-4adf-948b-257756e6eb4e", "updatedAt": "2023-08-02T21:00:33.878Z", "userCommentsId": undefined, "visibility": "EVERYONE"} {"_deleted": undefined, "_lastChangedAt": undefined, "_version": 1, "archived": null, "body": null, "createdAt": null, "id": "f373b642-6982-4ed1-8029-c2aff869b0c1", "owner": null, "parentId": null, "tenantId": null, "updatedAt": null, "visibility": null}
 LOG  DataStore error: {"cause": [Error: RetryMutation], "errorType": "Unknown", "localModel": null, "message": "RetryMutation", "model": "Comment", "operation": undefined, "process": "sync", "recoverySuggestion": "Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues", "remoteModel": null}
 LOG  Cause: [Error: RetryMutation]

and this repeats every time I load up the app.

I tried to include a debug log, but it was too long and GitHub rejected the issue.

aws-exports.js

/* eslint-disable */
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
    "aws_project_region": "eu-west-1",
    "aws_appsync_graphqlEndpoint": "https://lwmusnla5bfhrjmqhjm7miauxa.appsync-api.eu-west-1.amazonaws.com/graphql",
    "aws_appsync_region": "eu-west-1",
    "aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS",
    "aws_cognito_identity_pool_id": "eu-west-1:7b0db40b-57bf-4438-9c96-6e1a94777d3a",
    "aws_cognito_region": "eu-west-1",
    "aws_user_pools_id": "eu-west-1_TE5NS3Bn1",
    "aws_user_pools_web_client_id": "4nr073c46hm9jcdcpnv6a9uhqm",
    "oauth": {},
    "aws_cognito_username_attributes": [],
    "aws_cognito_social_providers": [],
    "aws_cognito_signup_attributes": [
        "EMAIL"
    ],
    "aws_cognito_mfa_configuration": "OFF",
    "aws_cognito_mfa_types": [
        "SMS"
    ],
    "aws_cognito_password_protection_settings": {
        "passwordPolicyMinLength": 8,
        "passwordPolicyCharacters": []
    },
    "aws_cognito_verification_mechanisms": [
        "EMAIL"
    ],
    "aws_user_files_s3_bucket": "platelet26fb7449fb884a3eb4c5fd7539c78dd301103-deev",
    "aws_user_files_s3_bucket_region": "eu-west-1",
    "geo": {
        "amazon_location_service": {
            "region": "eu-west-1",
            "search_indices": {
                "items": [
                    "plateletPlace-deev"
                ],
                "default": "plateletPlace-deev"
            }
        }
    }
};


export default awsmobile;

Manual configuration

No response

Additional configuration

No response

Mobile Device

Android Emulator: Samsung_Galaxy_S8_API_30

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

package.json

{
    "name": "mobile",
    "version": "1.0.0",
    "main": "node_modules/expo/AppEntry.js",
    "scripts": {
        "start": "expo start",
        "android": "expo start --android",
        "ios": "expo start --ios",
        "web": "expo start --web",
        "test": "jest --watch --testTimeout=40000"
    },
    "dependencies": {
        "@aws-amplify/datastore-storage-adapter": "^2.0.42",
        "@aws-amplify/ui-react-native": "^1.2.20",
        "@azure/core-asynciterator-polyfill": "^1.0.2",
        "@expo/webpack-config": "^18.0.1",
        "@react-native-async-storage/async-storage": "1.17.11",
        "@react-native-community/netinfo": "9.3.7",
        "@react-navigation/bottom-tabs": "^6.5.8",
        "@react-navigation/material-bottom-tabs": "^6.2.16",
        "@react-navigation/native-stack": "^6.9.13",
        "@reduxjs/toolkit": "^1.9.5",
        "amazon-cognito-identity-js": "^6.3.1",
        "aws-amplify": "^5.3.6",
        "core-js": "^3.31.1",
        "expo": "~48.0.18",
        "expo-blur": "~12.2.2",
        "expo-clipboard": "~4.1.2",
        "expo-file-system": "~15.2.2",
        "expo-haptics": "~12.2.1",
        "expo-sqlite": "~11.1.1",
        "expo-status-bar": "~1.4.4",
        "faker": "5.5.3",
        "moment": "^2.29.4",
        "moment-timezone": "^0.5.43",
        "react": "18.2.0",
        "react-content-loader": "^6.2.1",
        "react-native": "0.71.8",
        "react-native-gesture-handler": "~2.9.0",
        "react-native-get-random-values": "~1.8.0",
        "react-native-hold-menu": "^0.1.6",
        "react-native-paper": "^5.9.1",
        "react-native-paper-dates": "^0.18.12",
        "react-native-reanimated": "~2.14.4",
        "react-native-safe-area-context": "4.5.0",
        "react-native-screens": "~3.20.0",
        "react-native-svg": "13.4.0",
        "react-native-unimodules": "^0.14.10",
        "react-native-url-polyfill": "^2.0.0",
        "react-native-web": "~0.18.10",
        "react-redux": "^8.1.1",
        "redux-saga": "^1.2.3"
    },
    "devDependencies": {
        "@babel/core": "^7.20.0",
        "@testing-library/jest-native": "^5.4.2",
        "@testing-library/react-native": "^12.1.3",
        "@types/jest": "^29.5.3",
        "@types/react": "~18.0.14",
        "jest-expo": "^49.0.0",
        "mock-async-storage": "^2.2.0",
        "react-native-dotenv": "^3.4.9",
        "typescript": "^4.9.4"
    },
    "private": true
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    DataStoreRelated to DataStore categorybugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions