aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.41k stars 2.11k forks source link

DataStore sometimes takes a long time to sync data, or doesn't sync it at all. #12355

Open duckbytes opened 10 months ago

duckbytes commented 10 months ago

Before opening, please confirm:

JavaScript Framework

React, React Native

Amplify APIs

Authentication, GraphQL API, DataStore

Amplify Categories

auth, api

Environment information

``` System: OS: Linux 6.5 Arch Linux CPU: (16) x64 AMD Ryzen 7 3700X 8-Core Processor Memory: 4.71 GB / 31.25 GB Container: Yes Shell: 5.9 - /bin/zsh Binaries: Node: 16.13.2 - ~/.nvm/versions/node/v16.13.2/bin/node Yarn: 1.22.15 - ~/.nvm/versions/node/v16.13.2/bin/yarn npm: 8.1.2 - ~/.nvm/versions/node/v16.13.2/bin/npm Browsers: Firefox: 118.0.2 npmPackages: @aws-amplify/ui-react: ^3.6.0 => 3.6.0 @aws-amplify/ui-react-internal: undefined () @aws-amplify/ui-react-legacy: undefined () @babel/cli: ^7.21.5 => 7.21.5 @babel/core: ^7.21.8 => 7.21.8 (7.12.3, 7.12.17) @babel/preset-env: ^7.21.5 => 7.21.5 (7.12.1) @cypress/react: ^5.10.3 => 5.10.3 @cypress/webpack-dev-server: ^1.7.0 => 1.7.0 @date-io/date-fns: ^2.11.0 => 2.11.0 (1.1.0) @date-io/moment: ^2.11.0 => 2.11.0 @emotion/react: ^11.4.1 => 11.10.4 @emotion/styled: ^11.3.0 => 11.3.0 @fast-csv/format: ^4.3.5 => 4.3.5 @fontsource/roboto: ^4.5.1 => 4.5.1 @mui/icons-material: ^5.11.11 => 5.11.11 @mui/lab: ^5.0.0-alpha.50 => 5.0.0-alpha.50 @mui/material: ^5.0.3 => 5.0.3 @mui/styled-engine-sc: ^5.0.3 => 5.0.3 @mui/styles: ^5.0.1 => 5.0.1 @peculiar/webcrypto: ^1.2.2 => 1.2.2 @reduxjs/toolkit: ^1.6.1 => 1.6.1 @reduxjs/toolkit-query: 1.0.0 @reduxjs/toolkit-query-react: 1.0.0 @testing-library/cypress: ^8.0.2 => 8.0.2 @testing-library/jest-dom: ^5.15.0 => 5.15.0 @testing-library/react: ^12.1.2 => 12.1.2 @testing-library/user-event: ^13.5.0 => 13.5.0 @types/autosuggest-highlight: ^3.2.0 => 3.2.0 @types/jest: ^29.0.1 => 29.0.1 @types/lodash: ^4.14.185 => 4.14.185 @types/node: ^18.7.18 => 18.7.18 (14.18.28) @types/react: ^17.0.49 => 17.0.49 @types/react-dom: ^18.0.6 => 18.0.6 @types/react-linkify: ^1.0.1 => 1.0.1 @types/react-router-dom: ^5.3.3 => 5.3.3 @types/uuid: ^8.3.4 => 8.3.4 @types/validator: ^13.7.17 => 13.7.17 @welldone-software/why-did-you-render: ^6.2.3 => 6.2.3 autosuggest-highlight: ^3.1.1 => 3.1.1 aws-amplify: ^4.3.46 => 4.3.46 axios: ^0.21.1 => 0.21.4 (0.26.0) canvas: ^2.8.0 => 2.8.0 css-mediaquery: ^0.1.2 => 0.1.2 cypress: ^9.0.0 => 9.0.0 cypress-localstorage-commands: ^1.7.0 => 1.7.0 date-fns: ^2.23.0 => 2.25.0 (2.0.0-alpha.27) downshift: ^6.1.7 => 6.1.7 faker: ^5.5.3 => 5.5.3 framer-motion: ^4.1.17 => 4.1.17 immutability-helper: ^3.1.1 => 3.1.1 intersection-observer: ^0.12.0 => 0.12.0 jest-canvas-mock: ^2.3.1 => 2.3.1 jest-fetch-mock: ^3.0.3 => 3.0.3 libphonenumber-js: ^1.9.25 => 1.9.25 libphonenumber-js-core: 1.0.0 libphonenumber-js-max: 1.0.0 libphonenumber-js-min: 1.0.0 libphonenumber-js-mobile: 1.0.0 lodash: ^4.17.21 => 4.17.21 (3.10.1) match-sorter: ^6.3.0 => 6.3.0 material-table: ^1.69.3 => 1.69.3 moment: ^2.29.4 => 2.29.4 moment-timezone: ^0.5.35 => 0.5.35 notistack: ^2.0.2 => 2.0.2 prettier: ^2.8.4 => 2.8.4 prop-types: ^15.7.2 => 15.8.1 (15.6.2) react: ^17.0.2 => 17.0.2 react-cropper: ^2.1.8 => 2.1.8 react-dom: ^17.0.2 => 17.0.2 react-easy-crop: ^4.0.1 => 4.0.1 react-helmet: ^6.1.0 => 6.1.0 react-horizontal-scrolling-menu: ^3.2.3 => 3.2.3 react-idle-timer: ^4.6.4 => 4.6.4 react-intersection-observer: ^8.33.1 => 8.33.1 react-linkify: ^1.0.0-alpha => 1.0.0-alpha react-moment: ^1.1.1 => 1.1.1 react-notifications-component: ^3.1.0 => 3.1.0 react-redux: ^7.2.4 => 7.2.4 react-router: ^5.2.0 => 5.2.0 react-router-dom: ^5.2.0 => 5.2.0 react-scripts: ^4.0.3 => 4.0.3 react-waypoint: ^10.1.0 => 10.1.0 redux: ^4.1.1 => 4.1.1 redux-saga: ^1.1.3 => 1.1.3 redux-saga-test-plan: ^4.0.3 => 4.0.3 redux-saga/effects: undefined () redux-toolkit: ^1.1.2 => 1.1.2 seedrandom: ^3.0.5 => 3.0.5 socket.io-client: ^4.1.3 => 4.1.3 styled-components: ^5.3.1 => 5.3.1 styled-components/macro: undefined () styled-components/native: undefined () styled-components/primitives: undefined () tss-react: ^4.6.0 => 4.6.0 typescript: ^4.8.3 => 4.8.3 uuid: ^8.3.2 => 8.3.2 (3.4.0, 3.3.2, 7.0.3) uuid-base62: ^0.1.0 => 0.1.0 validator: ^13.9.0 => 13.9.0 web-vitals: ^1.0.1 => 1.1.2 workbox-background-sync: ^5.1.4 => 5.1.4 workbox-broadcast-update: ^5.1.4 => 5.1.4 workbox-cacheable-response: ^5.1.4 => 5.1.4 workbox-core: ^5.1.4 => 5.1.4 workbox-expiration: ^5.1.4 => 5.1.4 workbox-google-analytics: ^5.1.4 => 5.1.4 workbox-navigation-preload: ^5.1.4 => 5.1.4 workbox-precaching: ^5.1.4 => 5.1.4 workbox-range-requests: ^5.1.4 => 5.1.4 workbox-routing: ^5.1.4 => 5.1.4 workbox-strategies: ^5.1.4 => 5.1.4 workbox-streams: ^5.1.4 => 5.1.4 npmGlobalPackages: @aws-amplify/cli: 12.2.3 @bubblewrap/cli: 1.18.1 @ionic/cli: 6.20.3 amplify-cli: 1.0.0 aws-cdk: 2.100.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 eas-cli: 5.2.0 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 sharp-cli: 4.1.1 ts-node: 10.9.1 typescript: 4.8.2 uglify-js: 3.17.4 uglifyjs: 2.4.11 ```
``` System: OS: Linux 6.5 Arch Linux CPU: (16) x64 AMD Ryzen 7 3700X 8-Core Processor Memory: 4.42 GB / 31.25 GB Container: Yes Shell: 5.9 - /bin/zsh Binaries: Node: 16.13.2 - ~/.nvm/versions/node/v16.13.2/bin/node Yarn: 1.22.15 - ~/.nvm/versions/node/v16.13.2/bin/yarn npm: 8.1.2 - ~/.nvm/versions/node/v16.13.2/bin/npm pnpm: 6.11.0 - ~/.nvm/versions/node/v16.13.2/bin/pnpm 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 @expo/webpack-config: ^19.0.0 => 19.0.0 @react-native-async-storage/async-storage: 1.18.2 => 1.18.2 @react-native-community/netinfo: 9.3.10 => 9.3.10 @react-native/gradle-plugin: ^0.72.11 => 0.72.11 @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.2.14 => 18.2.21 HelloWorld: 0.0.1 amazon-cognito-identity-js: ^6.3.1 => 6.3.6 amazon-cognito-identity-js/internals: undefined () aws-amplify: ^5.3.11 => 5.3.11 core-js: ^3.31.1 => 3.31.1 experiments-app: 1.0.0 expo: ^49.0.11 => 49.0.11 expo-file-system: ~15.4.4 => 15.4.4 expo-splash-screen: ~0.20.5 => 0.20.5 expo-sqlite: ~11.3.3 => 11.3.3 expo-status-bar: ~1.6.0 => 1.6.0 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.72.5 => 0.72.5 react-native-get-random-values: ~1.9.0 => 1.9.0 react-native-paper: ^5.9.1 => 5.9.1 react-native-paper-dates: ^0.18.12 => 0.18.12 react-native-safe-area-context: 4.6.3 => 4.6.3 react-native-screens: ~3.22.0 => 3.22.1 react-native-svg: 13.9.0 => 13.9.0 react-native-testing-library-website: 0.0.0 react-native-url-polyfill: ^2.0.0 => 2.0.0 (1.3.0) react-native-web: ~0.19.6 => 0.19.8 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: ^5.1.3 => 5.2.2 npmGlobalPackages: @aws-amplify/cli: 12.2.3 @bubblewrap/cli: 1.18.1 @ionic/cli: 6.20.3 amplify-cli: 1.0.0 aws-cdk: 2.100.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 eas-cli: 5.2.0 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 sharp-cli: 4.1.1 ts-node: 10.9.1 typescript: 4.8.2 uglify-js: 3.17.4 uglifyjs: 2.4.11 ```

Describe the bug

We have been using an app developed with both React and React Native. It uses DataStore to do the majority of communications with the API.

I've had several reports from users along these lines:

Incoming data does not sync, or takes a very long time (sometimes hours) to sync and appear on their devices.

Outgoing data does not sync, until the user logs out and logs back in again. Logging out clears the datastore. This means that their unsynced data is lost and they have to re-enter it. Then sync starts to work again.

There are the two most relevant models in my schema:

type Task
@auth(rules: [
  {allow: private, operations: [read]},
  {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update]},
])
@model {
  id: ID!
  tenantId: ID! @index(name: "byTenantId", queryField: "listTasksByTenantId", sortKeyFields: ["createdAt"])
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read]},
    ])
  createdAt: String @auth(rules: [{allow: private, operations: [read]}])
  createdBy: User @belongsTo
  dateCreated: AWSDate!
  timeOfCall: AWSDateTime
  timePickedUp: AWSDateTime
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update, delete]},
    ])
  timePickedUpSenderName: String
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update, delete]},
    ])
  timeDroppedOff: AWSDateTime
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update, delete]},
    ])
  timeDroppedOffRecipientName: String
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update, delete]},
    ])
  timeCancelled: AWSDateTime
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update, delete]},
    ])
  timeRejected: AWSDateTime
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update, delete]},
    ])
  timeRiderHome: AWSDateTime
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update, delete]},
    ])
  requesterContact: AddressAndContactDetails
  pickUpLocationId: ID @index(name: "byPickUpLocation")
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update, delete]},
    ])
  dropOffLocationId: ID @index(name: "byDropOffLocation")
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update, delete]},
    ])
  establishmentLocationId: ID @index(name: "byEstasblishmentLocation")
  pickUpLocation: Location @belongsTo(fields: ["pickUpLocationId"])
  dropOffLocation: Location @belongsTo(fields: ["dropOffLocationId"])
  establishmentLocation: Location @belongsTo(fields: ["establishmentLocationId"])
  riderResponsibility: String
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update, delete]},
    ])
  assignees: [TaskAssignee] @hasMany
  priority: Priority
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, update, delete]},
    ])
  deliverables: [Deliverable] @hasMany
  comments: [Comment] @hasMany(indexName: "byParent", fields: ["id"])
  status: TaskStatus @index(name: "byStatus", queryField: "tasksByStatus")
  isRiderUsingOwnVehicle: Int @default(value: "0")
  archived: Int @default(value: "0") @index(name: "byArchivedStatus", queryField: "tasksByArchivedStatus", sortKeyFields: ["status"])
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read]},
    ])
}

type TaskAssignee
@auth(rules: [
  {allow: private, operations: [read]},
  {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, delete]},
])
@model {
  id: ID!
  tenantId: ID! @index(name: "byTenantId")
  role: Role!
  task: Task! @belongsTo
  assignee: User! @belongsTo
  archived: Int @default(value: "0") @index(name: "byArchived")
    @auth(rules: [
      {allow: private, operations: [read]},
      {allow: groups, groups: ["COORDINATOR", "RIDER", "ADMIN"], operations: [create, read, delete]},
    ])
}

This is the entire schema: https://github.com/platelet-app/platelet/blob/master/amplify/backend/api/platelet/schema.graphql

The TaskAssignee record is created to assign a user to a Task. This is the model that the dashboard subscribes to with DataStore.observeQuery to show their tasks to the user, and could be why it takes a long time for incoming data to appear.

The Task record is what gets updated when the user updates their tasks through the dashboard. It is linked to the TaskAssignees model.

In the past I've experienced an issue with DataStore.observeQuery not working properly when {allow: private, operations: [read]}, is used instead of {allow: groups, groups: ["USER"], operations: [read]},. I would find that data did not appear if it was created by another user when the model also had owner auth. I'm not sure that this is related or not, but I could try using the groups auth instead of private auth on my other models. It'll just need to be tested before going into production, where the issues are happening.

There are two versions of Amplify being used, on the web version it uses ^4.3.46 and on the mobile ^5.3.11. I can't update the web version yet because of breaking changes. All records are created on the web version. The mobile version only reads and updates data. However I get similar reports from users using the web version for the same purposes as the mobile version (for updating Tasks while out and about).

I have x-ray set up and have looked through logs during times I know users have had issues, but nothing stands out. Every log entry says "OK". If I know what I need to be looking for I might be able to find some more useful information.

Expected behavior

I expect DataStore to always sync incoming and outgoing data.

Reproduction steps

It's difficult to specify steps for a basic reproduction. If it's helpful to, the project can be built and deployed from https://github.com/platelet-app/platelet and the mobile version is here: https://github.com/platelet-app/platelet-mobile

More or less it is an Amplify project with DataStore enabled and optimistic concurrency, using cognito for auth with user groups.

Code Snippet

I use this hook to synchronise data on my dashboard. It can be explored better here https://github.com/platelet-app/platelet/blob/master/src/hooks/useTasksColumnTasks.ts


import React from "react";
import * as models from "../models";
import { useSelector } from "react-redux";
import {
    getRoleView,
    getWhoami,
    taskAssigneesSelector,
    dashboardFilteredUserSelector,
    taskAssigneesReadyStatusSelector,
    dataStoreModelSyncedStatusSelector,
    selectionActionsPendingSelector,
} from "../redux/Selectors";
import getAllMyTasks from "./utilities/getAllMyTasks";
import getAllTasksByUser from "./utilities/getAllTasksByUser";
import getTasksAll from "./utilities/getTasksAll";
import { DataStore } from "aws-amplify";
import _ from "lodash";

export type TaskStateType = {
    [key: string]: models.Task;
};

export function convertTasksToStateType(tasks: models.Task[]): TaskStateType {
    const state: TaskStateType = {};
    tasks.forEach((task) => {
        state[task.id] = task;
    });
    return state;
}

const useTasksColumnTasks = (taskStatusKey: models.TaskStatus[]) => {
    const [state, setState] = React.useState<TaskStateType>({});
    const dashboardFilteredUser = useSelector(dashboardFilteredUserSelector);
    const roleView = useSelector(getRoleView);
    const dataStoreModelSynced = useSelector(
        dataStoreModelSyncedStatusSelector
    ).Task;
    const selectionActionsPending = useSelector(
        selectionActionsPendingSelector
    );
    const tasksSubscription = React.useRef({
        unsubscribe: () => {},
    });
    const stateRef = React.useRef<TaskStateType>({});
    const locationsSubscription = React.useRef({
        unsubscribe: () => {},
    });
    const whoami = useSelector(getWhoami);
    const taskAssignees = useSelector(taskAssigneesSelector);
    const taskAssigneesReady = useSelector(taskAssigneesReadyStatusSelector);
    const [isFetching, setIsFetching] = React.useState(true);
    const [error, setError] = React.useState(false);
    stateRef.current = state;
    const tasksKeyJSON = JSON.stringify(taskStatusKey);

    let myTaskAssigneeIds = taskAssignees.items
        .filter(
            (a: models.TaskAssignee) =>
                a?.assignee?.id === whoami?.id && a?.role === roleView
        )
        .map((a2: models.TaskAssignee) => a2?.task?.id);

    function addTaskToState(newTask: models.Task) {
        setState((prevState) => {
            return { ...prevState, [newTask.id]: newTask };
        });
    }

    function removeTaskFromState(newTask: models.Task) {
        setState((prevState) => {
            if (prevState[newTask.id]) return _.omit(prevState, newTask.id);
            else return prevState;
        });
    }

    if (dashboardFilteredUser && roleView === models.Role.COORDINATOR) {
        const theirAssignments = taskAssignees.items.filter(
            (a: models.TaskAssignee) =>
                a.role === models.Role.RIDER &&
                a.task &&
                a.assignee?.id === dashboardFilteredUser
        );
        const theirTaskIds = theirAssignments.map(
            (a: models.TaskAssignee) => a.task?.id
        );
        const intersectingTasksIds = _.intersection(
            myTaskAssigneeIds,
            theirTaskIds
        );
        myTaskAssigneeIds = intersectingTasksIds;
    }

    const sortedMyTaskAssigneeIds = myTaskAssigneeIds.sort(
        (a: string, b: string) => {
            return a.localeCompare(b);
        }
    );
    const taskIdsJson = JSON.stringify(sortedMyTaskAssigneeIds);

    const getTasks = React.useCallback(async () => {
        if (
            !roleView ||
            !taskAssigneesReady ||
            selectionActionsPending ||
            !taskStatusKey
        ) {
            return;
        } else {
            try {
                if (
                    taskStatusKey.includes(models.TaskStatus.PENDING) ||
                    (roleView === "ALL" && !dashboardFilteredUser)
                ) {
                    setState(await getTasksAll(taskStatusKey));
                } else if (roleView === "ALL" && dashboardFilteredUser) {
                    setState(
                        await getAllTasksByUser(
                            taskStatusKey,
                            dashboardFilteredUser,
                            models.Role.RIDER,
                            taskAssignees.items
                        )
                    );
                } else if (roleView !== "ALL") {
                    setState(
                        await getAllMyTasks(taskStatusKey, myTaskAssigneeIds)
                    );
                }

                setIsFetching(false);
            } catch (error) {
                setError(true);
                setIsFetching(false);
                console.log(error);
            }
        }
    }, [
        dashboardFilteredUser,
        tasksKeyJSON,
        roleView,
        selectionActionsPending,
        taskIdsJson,
        taskAssigneesReady,
        whoami.id,
    ]);

    React.useEffect(() => {
        getTasks();
    }, [getTasks, dataStoreModelSynced]);

    const selectionActionsPendingRef = React.useRef(false);
    selectionActionsPendingRef.current = selectionActionsPending;

    const setUpObservers = React.useCallback(
        (roleView, taskKey) => {
            tasksSubscription.current.unsubscribe();
            tasksSubscription.current = DataStore.observe(
                models.Task
            ).subscribe((newTask) => {
                if (selectionActionsPendingRef.current) return;
                try {
                    if (newTask.opType === "UPDATE") {
                        if (
                            newTask.element.status &&
                            taskKey.includes(newTask.element.status) &&
                            !(newTask.element.id in stateRef.current)
                        ) {
                            getTasks();
                            return;
                        } else if (
                            newTask.element.status &&
                            !taskKey.includes(newTask.element.status)
                        ) {
                            removeTaskFromState(newTask.element);
                            return;
                        } else if (newTask.element.id in stateRef.current) {
                            addTaskToState(newTask.element);
                        }
                    } else {
                        // if roleView is rider or coordinator, let the assignments observer deal with it
                        if (
                            roleView !== "ALL" &&
                            !taskKey.includes(models.TaskStatus.PENDING)
                        )
                            return;
                        if (taskKey.includes(newTask.element.status)) {
                            getTasks();
                        }
                    }
                } catch (error) {
                    console.log(error);
                }
            });
            locationsSubscription.current.unsubscribe();
            locationsSubscription.current = DataStore.observe(
                models.Location
            ).subscribe(async (location) => {
                try {
                    if (location.opType === "UPDATE") {
                        for (const task of Object.values(stateRef.current)) {
                            if (
                                task.pickUpLocation &&
                                task.pickUpLocation.id === location.element.id
                            ) {
                                setState((prevState) => ({
                                    ...prevState,
                                    [task.id]: {
                                        ...prevState[task.id],
                                        pickUpLocation: location.element,
                                    },
                                }));
                            }
                            if (
                                task.dropOffLocation &&
                                task.dropOffLocation.id === location.element.id
                            ) {
                                setState((prevState) => ({
                                    ...prevState,
                                    [task.id]: {
                                        ...prevState[task.id],
                                        dropOffLocation: location.element,
                                    },
                                }));
                            }
                        }
                    }
                } catch (error) {
                    console.log(error);
                }
            });
        },
        [getTasks]
    );

    React.useEffect(() => {
        setUpObservers(roleView, taskStatusKey);
    }, [roleView, setUpObservers, taskStatusKey]);

    React.useEffect(() => {
        return () => {
            tasksSubscription.current.unsubscribe();
            locationsSubscription.current.unsubscribe();
        };
    }, []);
    return { state, isFetching, error };
};

export default useTasksColumnTasks;

The same hook for populating the dashboard on the mobile version. This one is a bit simpler and cleaner than the web one.

import * as React from "react";
import * as models from "../models";
import { useSelector } from "react-redux";
import { getWhoami } from "../redux/Selectors";
import { DataStore } from "aws-amplify";
import convertModelListToTypedObject from "./utilities/convertModelListToTypedObject";
import _ from "lodash";
import useAppActiveStatus from "./useAppActiveStatus";

export type ResolvedTask = Omit<
    models.Task,
    "pickUpLocation" | "dropOffLocation"
> & {
    pickUpLocation: models.Location | null;
    dropOffLocation: models.Location | null;
};

type StateType = {
    [id: string]: ResolvedTask;
};

const log = (message: any) => {
    console.log(`[useMyAssignedTasks] ${message}`);
};

const useMyAssignedTasks = (
    status: models.TaskStatus[] | models.TaskStatus,
    role: models.Role.COORDINATOR | models.Role.RIDER,
    limit: boolean = false
) => {
    const whoami = useSelector(getWhoami);
    const assigneeObserver = React.useRef({ unsubscribe: () => {} });
    const tasksObserver = React.useRef({ unsubscribe: () => {} });
    const locationObserver = React.useRef({ unsubscribe: () => {} });
    const stateRef = React.useRef<StateType>({});
    const taskIdsRef = React.useRef<string[] | null>(null);
    const [taskIds, setTaskIds] = React.useState<string[] | null>(null);
    const [state, setState] = React.useState<StateType>({});
    const [error, setError] = React.useState<Error | null>(null);
    const [isFetching, setIsFetching] = React.useState(true);
    const isFetchingAssigneesRef = React.useRef(true);
    const appStatus = useAppActiveStatus();

    stateRef.current = state;
    taskIdsRef.current = taskIds;

    let actualStatus: models.TaskStatus[] = React.useMemo(() => {
        if (!Array.isArray(status)) {
            return [status];
        } else {
            return status;
        }
    }, [status]);

    const setUpTasksObserver = React.useCallback(() => {
        // when appStatus changes back to the foreground, we want to restart the observer
        if (appStatus !== "active") {
            tasksObserver.current.unsubscribe();
            return;
        }
        if (isFetchingAssigneesRef.current) {
            return;
        }
        log("setting up tasks observer");
        const oneWeekAgo = new Date();
        oneWeekAgo.setHours(0, 0, 0, 0);
        oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
        const oneWeekAgoString = oneWeekAgo.toISOString();
        try {
            tasksObserver.current.unsubscribe();
            if (limit) {
                tasksObserver.current = DataStore.observeQuery(
                    models.Task,
                    (t) =>
                        t.and((t) => [
                            t.or((t) =>
                                actualStatus.map((s) => t.status.eq(s))
                            ),
                            t.or((t) => [
                                t.createdAt.eq(undefined),
                                t.createdAt.gt(oneWeekAgoString),
                            ]),
                        ]),
                    { sort: (s) => s.createdAt("DESCENDING") }
                ).subscribe(async ({ items }) => {
                    const filtered = items.filter((t) =>
                        taskIdsRef.current?.includes(t.id)
                    );
                    const resolvedTasks: ResolvedTask[] = await Promise.all(
                        filtered.map(async (t) => {
                            const pickUpLocation =
                                (await t.pickUpLocation) || null;
                            const dropOffLocation =
                                (await t.dropOffLocation) || null;
                            return {
                                ...t,
                                pickUpLocation,
                                dropOffLocation,
                            };
                        })
                    );
                    setState(
                        convertModelListToTypedObject<ResolvedTask>(
                            resolvedTasks
                        )
                    );
                    setIsFetching(false);
                });
            } else {
                tasksObserver.current = DataStore.observeQuery(
                    models.Task,
                    (t) =>
                        t.and((t) => [
                            t.or((t) =>
                                actualStatus.map((s) => t.status.eq(s))
                            ),
                        ]),
                    { sort: (s) => s.createdAt("DESCENDING") }
                ).subscribe(async ({ items }) => {
                    const filtered = items.filter((t) =>
                        taskIdsRef.current?.includes(t.id)
                    );
                    const resolvedTasks: ResolvedTask[] = await Promise.all(
                        filtered.map(async (t) => {
                            const pickUpLocation =
                                (await t.pickUpLocation) || null;
                            const dropOffLocation =
                                (await t.dropOffLocation) || null;
                            return {
                                ...t,
                                pickUpLocation,
                                dropOffLocation,
                            };
                        })
                    );
                    setState(
                        convertModelListToTypedObject<ResolvedTask>(
                            resolvedTasks
                        )
                    );
                    setIsFetching(false);
                });
            }
        } catch (error: unknown) {
            if (error instanceof Error) {
                setError(error);
                log(error);
            }
            setIsFetching(false);
        }
    }, [taskIds, actualStatus, limit, appStatus]);

    React.useEffect(() => {
        setUpTasksObserver();
        return () => {
            tasksObserver.current.unsubscribe();
        };
    }, [setUpTasksObserver]);

    const setUpLocationObserver = React.useCallback(() => {
        // when appStatus changes back to the foreground, we want to restart the observer
        if (appStatus !== "active") {
            locationObserver.current.unsubscribe();
            return;
        }
        log("setting up location observer");
        locationObserver.current.unsubscribe();
        locationObserver.current = DataStore.observe(models.Location).subscribe(
            async (location) => {
                try {
                    if (location.opType === "UPDATE") {
                        for (const task of Object.values(stateRef.current)) {
                            if (
                                task.pickUpLocation &&
                                task.pickUpLocation.id === location.element.id
                            ) {
                                setState((prevState) => ({
                                    ...prevState,
                                    [task.id]: {
                                        ...prevState[task.id],
                                        pickUpLocation: location.element,
                                    },
                                }));
                            }
                            if (
                                task.dropOffLocation &&
                                task.dropOffLocation.id === location.element.id
                            ) {
                                setState((prevState) => ({
                                    ...prevState,
                                    [task.id]: {
                                        ...prevState[task.id],
                                        dropOffLocation: location.element,
                                    },
                                }));
                            }
                        }
                    }
                } catch (error) {
                    log(error);
                }
            }
        );
    }, [appStatus]);

    React.useEffect(() => {
        setUpLocationObserver();
        return () => {
            locationObserver.current.unsubscribe();
        };
    }, [setUpLocationObserver]);

    const setUpAssignedTasksObserver = React.useCallback(async () => {
        // when appStatus changes back to the foreground, we want to restart the observer
        if (appStatus !== "active") {
            assigneeObserver.current.unsubscribe();
            return;
        }
        log("setting up assigned tasks observer");
        try {
            assigneeObserver.current.unsubscribe();
            assigneeObserver.current = DataStore.observeQuery(
                models.TaskAssignee,
                (a) => a.role.eq(role)
            ).subscribe(async ({ items }) => {
                const resolved = await Promise.all(
                    items.map(async (a) => {
                        const assignee = await a.assignee;
                        const task = await a.task;
                        return { ...a, assignee, task };
                    })
                );
                const filtered = resolved.filter(
                    (a) => a.assignee.id === whoami?.id
                );
                const taskIds = filtered.map((t) => t.task.id);
                if (_.isEqual(taskIds, taskIdsRef.current)) {
                    return;
                } else {
                    setTaskIds(taskIds);
                }
                isFetchingAssigneesRef.current = false;
            });
            return;

        } catch (error: unknown) {
            if (error instanceof Error) {
                log(error);
                setError(error);
            }
        }
    }, [whoami?.id, role, appStatus]);

    React.useEffect(() => {
        setUpAssignedTasksObserver();
        return () => {
            assigneeObserver.current.unsubscribe();
        };
    }, [setUpAssignedTasksObserver]);

    return { state: Object.values(state), isFetching, error };
};

export default useMyAssignedTasks;

This is the component on mobile for updating tasks. It's very similar on the web version and can be looked at in more detail here: https://github.com/platelet-app/platelet/blob/mobile/mobile/src/screens/Task/components/TaskActions.tsx

import { DataStore } from "aws-amplify";
import * as React from "react";
import { Card, ToggleButton, Text } from "react-native-paper";
import { TouchableOpacity, View } from "react-native";
import useModelSubscription from "../../../hooks/useModelSubscription";
import * as models from "../../../models";
import determineTaskStatus, {
    TaskInterface,
} from "../../../utilities/determineTaskStatus";
import TaskTimePicker from "./TaskTimePicker";
import TaskActionsConfirmationDialog from "./TaskActionsConfirmationDialog";
import GenericErrorSnack from "../../../snacks/GenericErrorSnack";

type TaskActionsProps = {
    taskId: string;
};

const fields = {
    timePickedUp: "Picked up",
    timeDroppedOff: "Delivered",
    timeCancelled: "Cancelled",
    timeRejected: "Rejected",
    timeRiderHome: "Rider home",
};

export type TaskUpdateKey = keyof Omit<TaskInterface, "id">;

const TaskActions: React.FC<TaskActionsProps> = ({ taskId }) => {
    const [buttonsState, setButtonsState] = React.useState<TaskUpdateKey[]>([]);
    const [isPosting, setIsPosting] = React.useState(false);
    const [confirmationKey, setConfirmationKey] =
        React.useState<TaskUpdateKey | null>(null);
    const [editKey, setEditKey] = React.useState<TaskUpdateKey | null>(null);
    const { state, setState, isFetching, notFound, error } =
        useModelSubscription<models.Task>(models.Task, taskId);
    const [snackVisible, setSnackVisible] = React.useState(false);

    function onClickToggle(key: TaskUpdateKey) {
        setConfirmationKey(key);
    }

    function onClickEdit(key: TaskUpdateKey) {
        setEditKey(key);
    }

    async function saveValues(values: Partial<TaskInterface>) {
        setIsPosting(true);
        setConfirmationKey(null);
        try {
            const existingTask = await DataStore.query(models.Task, taskId);
            if (!existingTask) {
                throw new Error("Task not found");
            }
            const status = await determineTaskStatus({
                ...existingTask,
                ...values,
            });
            const updatedTask = await DataStore.save(
                models.Task.copyOf(existingTask, (upd) => {
                    upd.status = status;
                    for (const key in values) {
                        upd[key as keyof Omit<TaskInterface, "id">] =
                            values[key as keyof TaskInterface];
                    }
                })
            );
            setState(updatedTask);
        } catch (e) {
            console.log(e);
            setSnackVisible(true);
        } finally {
            setIsPosting(false);
            setEditKey(null);
        }
    }

    function calculateState() {
        if (!state) return;
        const result = Object.keys(fields).filter((key) => {
            return !!state[key as keyof typeof fields];
        });
        setButtonsState(result as TaskUpdateKey[]);
    }
    React.useEffect(calculateState, [state]);

    const getIcon = (key: TaskUpdateKey) => {
        if (buttonsState.includes(key)) return "checkbox-marked-outline";
        return "checkbox-blank-outline";
    };

    function checkDisabled(key: TaskUpdateKey) {
        if (
            notFound ||
            error ||
            isFetching ||
            isPosting ||
            state?.status === models.TaskStatus.PENDING
        )
            return true;
        const stopped =
            buttonsState.includes("timeCancelled") ||
            buttonsState.includes("timeRejected");
        if (key === "timeDroppedOff")
            return (
                buttonsState.includes("timeRiderHome") ||
                !buttonsState.includes("timePickedUp") ||
                stopped
            );
        else if (key === "timePickedUp") {
            return buttonsState.includes("timeDroppedOff") || stopped;
        } else if (key === "timeRiderHome") {
            if (state?.status === models.TaskStatus.NEW) return true;
            return !buttonsState.includes("timeDroppedOff");
        } else if (key === "timeRejected") {
            if (buttonsState.includes("timeRejected")) return false;
            return (
                (buttonsState.includes("timePickedUp") &&
                    buttonsState.includes("timeDroppedOff")) ||
                stopped
            );
        } else if (key === "timeCancelled") {
            if (buttonsState.includes("timeCancelled")) return false;
            return (
                (buttonsState.includes("timePickedUp") &&
                    buttonsState.includes("timeDroppedOff")) ||
                stopped
            );
        } else return false;
    }

    let nameKey:
        | "timePickedUpSenderName"
        | "timeDroppedOffRecipientName"
        | null = null;
    if ([confirmationKey, editKey].includes("timePickedUp"))
        nameKey = "timePickedUpSenderName";
    else if ([confirmationKey, editKey].includes("timeDroppedOff"))
        nameKey = "timeDroppedOffRecipientName";

    return (
        <>
            <Card>
                <Card.Content>
                    {Object.entries(fields).map(([key, value], index) => {
                        let borderTopLeftRadius = 0;
                        let borderTopRightRadius = 0;
                        let borderBottomLeftRadius = 0;
                        let borderBottomRightRadius = 0;
                        if (index === 0) {
                            borderTopLeftRadius = 8;
                            borderTopRightRadius = 8;
                        }
                        if (index === Object.entries(fields).length - 1) {
                            borderBottomLeftRadius = 8;
                            borderBottomRightRadius = 8;
                        }
                        const disabled = checkDisabled(key as TaskUpdateKey);

                        let showInfo = false;
                        if (
                            key === "timePickedUp" &&
                            state?.timePickedUpSenderName
                        ) {
                            showInfo = true;
                        } else if (
                            key === "timeDroppedOff" &&
                            state?.timeDroppedOffRecipientName
                        ) {
                            showInfo = true;
                        }

                        return (
                            <View
                                style={{
                                    flexDirection: "row",
                                    alignItems: "center",
                                    justifyContent: "space-between",
                                    gap: 8,
                                }}
                                key={key}
                            >
                                <View
                                    style={{
                                        flexDirection: "row",
                                        alignItems: "center",
                                        gap: 8,
                                    }}
                                >
                                    <ToggleButton
                                        size={30}
                                        disabled={checkDisabled(
                                            key as TaskUpdateKey
                                        )}
                                        aria-label={value}
                                        onPress={() => {
                                            onClickToggle(key as TaskUpdateKey);
                                        }}
                                        style={{
                                            height: 53,
                                            width: 53,
                                            borderWidth: 0.4,
                                            borderTopLeftRadius,
                                            borderTopRightRadius,
                                            borderBottomLeftRadius,
                                            borderBottomRightRadius,
                                        }}
                                        icon={getIcon(key as TaskUpdateKey)}
                                        value={key}
                                        status={
                                            buttonsState.includes(
                                                key as TaskUpdateKey
                                            )
                                                ? "checked"
                                                : "unchecked"
                                        }
                                    />
                                    <TouchableOpacity
                                        disabled={disabled}
                                        onPress={() => {
                                            onClickToggle(key as TaskUpdateKey);
                                        }}
                                    >
                                        <Text
                                            style={{
                                                fontSize: 16,
                                                opacity: disabled ? 0.5 : 1,
                                                textTransform: "uppercase",
                                            }}
                                        >
                                            {value}
                                        </Text>
                                    </TouchableOpacity>
                                </View>
                                <TouchableOpacity
                                    onPress={() => {
                                        onClickEdit(key as TaskUpdateKey);
                                    }}
                                >
                                    <TaskTimePicker
                                        time={
                                            state?.[key as keyof TaskInterface]
                                        }
                                        label={`Edit ${value}`}
                                        onClickEdit={() =>
                                            onClickEdit(key as TaskUpdateKey)
                                        }
                                        infoIcon={showInfo}
                                    />
                                </TouchableOpacity>
                            </View>
                        );
                    })}
                </Card.Content>
            </Card>
            <TaskActionsConfirmationDialog
                key={confirmationKey || "confirmationDialog"}
                nullify={!!state?.[confirmationKey as keyof TaskInterface]}
                taskKey={confirmationKey as TaskUpdateKey}
                nameKey={nameKey}
                open={!!confirmationKey}
                onClose={() => setConfirmationKey(null)}
                onConfirm={saveValues}
            />
            <TaskActionsConfirmationDialog
                key={editKey || "editDialog"}
                startingValue={state?.[editKey as keyof TaskInterface]}
                nullify={false}
                startingNameValue={state?.[nameKey as keyof TaskInterface]}
                taskKey={editKey as TaskUpdateKey}
                nameKey={nameKey}
                open={!!editKey}
                onClose={() => setEditKey(null)}
                onConfirm={saveValues}
            />
            <GenericErrorSnack
                visible={snackVisible}
                onDismiss={() => setSnackVisible(false)}
            />
        </>
    );
};

export default TaskActions;

And this is my conflict handler, which I don't know if could be a cause for issues:

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 (remoteModel.archived === 1) {
        return DISCARD;
    }
    if (
        modelConstructor ===
        (models.Task as PersistentModelConstructor<models.Task>)
    ) {
        const remote = remoteModel as models.Task;
        const local = localModel as models.Task;
        let newModel = models.Task.copyOf(remote, (task) => {
            task.timePickedUp = remote.timePickedUp || local.timePickedUp;
            task.timeDroppedOff = remote.timeDroppedOff || local.timeDroppedOff;
            task.timeRiderHome = remote.timeRiderHome || local.timeRiderHome;
            task.timeCancelled = remote.timeCancelled || local.timeCancelled;
            task.timeRejected = remote.timeRejected || local.timeRejected;
            task.timePickedUpSenderName =
                remote.timePickedUpSenderName || local.timePickedUpSenderName;
            task.timeDroppedOffRecipientName =
                remote.timeDroppedOffRecipientName ||
                local.timeDroppedOffRecipientName;
        });
        console.log("Resolved task conflict result:", newModel);
        const status = await determineTaskStatus(newModel);
        console.log("Updating task status to", status);
        newModel = models.Task.copyOf(newModel, (task) => {
            task.status = status;
        });
        const { createdAt, updatedAt, tenantId, archived, ...rest } = newModel;
        return rest;
    } else if (
        modelConstructor ===
        (models.Comment as PersistentModelConstructor<models.Comment>)
    ) {
        const { createdAt, updatedAt, tenantId, archived, ...rest } =
            remoteModel;
        return rest;
    }
    return DISCARD;
};

export default dataStoreConflictHandler;

Log output

``` // Put your logs below this line ```

aws-exports.js

This is just an example from my dev aws-exports.

const awsmobile = {
    "aws_project_region": "eu-west-1",
    "aws_appsync_graphqlEndpoint": "https://zevrqqcatbadzlgyzfiwalvac4.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:be212540-2dd5-4cc4-a3f4-c067a7b11e5c",
    "aws_cognito_region": "eu-west-1",
    "aws_user_pools_id": "eu-west-1_02UkIHRM9",
    "aws_user_pools_web_client_id": "74qtr5truikqtdd5gavut5r4o9",
    "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": "platelet26fb7449fb884a3eb4c5fd7539c78dd3211255-whoa",
    "aws_user_files_s3_bucket_region": "eu-west-1",
    "geo": {
        "amazon_location_service": {
            "region": "eu-west-1",
            "search_indices": {
                "items": [
                    "plateletPlace-whoa"
                ],
                "default": "plateletPlace-whoa"
            }
        }
    }
};

export default awsmobile;

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

Android, iOS of various versions

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

duckbytes commented 10 months ago

I had another report from a user today, they were the first one to test out the native version on iOS. The said they had to go back to using the web version, because their tasks were just not appearing for them.

Some possibly related issues: https://github.com/aws-amplify/amplify-flutter/issues/3865 https://github.com/aws-amplify/amplify-js/issues/11708

cwomack commented 10 months ago

Hey, @duckbytes 👋. We're looking into this and related issues (like you've referenced above). Greatly appreciate the details and context you put into the issues you open, especially when they are difficult to reproduce like this one! If there's any further questions or progress, we'll update this issue as soon as we can.

damir-fell commented 9 months ago

@cwomack Do you have any more information about this or especially the related issue (https://github.com/aws-amplify/amplify-flutter/issues/3865) ? I am trying to get some information and there has been no updates in a month.

Is this being prioritized? For our app the effect of the related issue is severe.

duckbytes commented 9 months ago

It was suggested to me that this issue might be related https://github.com/aws-amplify/amplify-category-api/issues/1853

Apparently it was fixed in the most recent version of the amplify cli.

I'm going to try and push an update this week and ask users for feedback.

darylteo commented 9 months ago

If you're here from Google trying to solve this, you might have created the app with an old version of Amplify. Check the AmplifyDataStore table on DynamoDB, and make sure that the PartitionKey and SortKey are both "ds_pk" and "ds_sk" respectively.

Specifically you should be getting a "DeltaSyncWriteError" or "Failed to write Delta Sync Record" error.

Screenshot 2023-11-14 at 9 42 34 pm

For some reason, on my version it was set to "pk" and "sk" even though it was created by the CLI. Manually deleting and then recreating the table resolved the issue for me.

duckbytes commented 9 months ago

It looks like all my tables have ds_pk and ds_sk, although they were created with an old version (11.0.3).

What's interesting is I don't think I've ever seen any entries in those tables, except for today I suddenly noticed there were a few items in just one of them (I have multiple DataStore tables to serve different tenants). The other ones (including the tenant who are most active) are empty.

I'm updating the backend today with the latest version of Amplify, so I'm going to observe and see if anything starts appearing in those tables.

EDIT: Few hours later and I'm already seeing new entries in that table now. Though that doesn't mean for sure they weren't appearing before! Just the first time I've ever looked and it wasn't empty.

darylteo commented 9 months ago

Per my understanding, there is a TTL on the delta sync table. They're removed automatically after a period of time. Screenshot 2023-11-15 at 11 11 16 am

If there are entries that does imply that it is syncing.

It did take me awhile, but Amplify.Logger.LOG_LEVEL = 'DEBUG'; as well as enabling Cloudwatch Logging on the Amplify API was incredibly helpful in helping me finally figuring out what was happening.

duckbytes commented 7 months ago

Several of my users are still reporting a lot of issues with this. Apparently they have to log out and log in several times during their shift to receive updates from other users. This is happening with both the web version and mobile version. I've found it very hard to re-create at home so I don't know what is going on.

Are there any thoughts or advice on what I can do to try and figure this out? Any logs that might have a hint?

I should probably try to capture logs created on the devices themselves as well. Not sure if there is a good way to do this with Amplify or if I should look for another way.

duckbytes commented 7 months ago

I had a conversation with a user working with another tenant who cover a different geographical area.

They say they've had no problems at all. They do have a smaller call volume, so less data needs to be synced. They also cover a smaller and less rural area.

This makes me think of three possibilities:

It's possible that the user experience goes something like this:

I'll try and communicate to the users and figure out if this is the case.