且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

Apollo mutate 为单个突变调用更新四次

更新时间:2023-02-06 16:38:37

我刚刚与负责此代码的工程师聊了聊.您看到的是 AWS AppSync 开发工具包流程在幕后使用的簿记流程,以确保数据完整性.它并非实际上针对您的 API 运行了 4 次变异.

当 AppSync 客户端获得乐观响应时,更新功能会运行两次 - 一次用于本地响应,一次用于网络响应.这是标准的 Apollo 行为.AppSync 客户端在幕后所做的是第一个乐观响应,我们将其视为网络响应并将数据存储在持久存储介质中(Web 本地存储,React Native 异步存储)以允许乐观 UI处于离线状态时.这本质上是一个发件箱",数据在离线时首先写入(当前实现使用 Redux Offline),如果您使用 disableOffline:true 禁用离线,您将不再看到这种行为.>

当您重新上线时,同步过程将被执行,您会看到客户端向服务器发送的另一个突变消息(实际上是原始突变)和相应的响应.

请注意,如果您在客户端的乐观响应中创建唯一 ID 并且还在服务器上创建唯一 ID,例如使用 $util.autoId() 那么您可能会有重复的记录因为我们不会覆盖您明确分配了 ID 的任何本地数据.如果您愿意,您可以使用 AppSync 的 DynamoDB 解析器中的支持离线的放置项支持离线的响应模板来使任何本地创建的 ID 失效,这些模板使用名为 relayState 的临时键(您需要将其添加为您正在创建的类型的字段),您可以使用它来跟踪本地 ID 并将其与创建的 ID 匹配服务器.

我们将在未来对该簿记过程进行更多补充,欢迎在我们的 GitHub 问题存储库中提出建议:https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues

Every time I add a new item in my app, the mutation calls update() four times, for some reason. The first two are optimistic data, and in the second batch, one is optimistic and one is real data from the network. I can't make any sense of this. The new item being created shows up twice on the page.

This is my mutation:

mutation CreateTrack($name: String!, $trackNum: Int, $s3Key: String!) {
  createTrack(name: $name, trackNum: $trackNum, s3Key: $s3Key) {
    trackId
    name
    createdAt
    duration
    trackNum
    s3Key
    isProcessing
    didProcessingFail
  }
}

And this is the mutation code:

createTrack({ name, s3Key }) {
  const newTrack = {
    name,
    s3Key,
  };

  this.$apollo
    .mutate({
      mutation: createTrackMutation,
      variables: newTrack,
      update: (store, { data: { createTrack } }) => {
        console.log('this is dumb', JSON.stringify(createTrack, null, 2));
        const variables = {
          limit: this.pageSize,
          order: this.order === 'ascending' ? 'asc' : 'desc',
          sortBy: this.sortBy,
        };
        const data = store.readQuery({
          query: listTracksQuery,
          variables,
        });
        data.listTracks.items.push(createTrack);
        store.writeQuery({
          query: listTracksQuery,
          variables,
          data,
        });
      },
      optimisticResponse: {
        __typename: 'Mutation',
        createTrack: {
          __typename: 'Track',
          ...newTrack,
          trackId: '??',
          createdAt: new Date().toISOString(),
          isProcessing: true,
          didProcessingFail: false,
          duration: null,
          trackNum: 999,
        },
      },
    })
    .then(data => {
      console.log('done!', data);
    })
    .catch(err => {
      console.log('error', err);
    });
},

And finally, here are the console logs for calling mutate once:

this is dumb {
  "__typename": "Track",
  "name": "small2.wav",
  "s3Key": "staging/audio/10e3e675-e7a6-41dc-a8fb-686ad683e40e.wav",
  "trackId": "??",
  "createdAt": "2018-03-05T03:30:18.246Z",
  "isProcessing": true,
  "didProcessingFail": false,
  "duration": null,
  "trackNum": 999
}

this is dumb {
  "__typename": "Track",
  "name": "small2.wav",
  "s3Key": "staging/audio/10e3e675-e7a6-41dc-a8fb-686ad683e40e.wav",
  "trackId": "??",
  "createdAt": "2018-03-05T03:30:18.246Z",
  "isProcessing": true,
  "didProcessingFail": false,
  "duration": null,
  "trackNum": 999
}

done! {data: {...}}

this is dumb {
  "__typename": "Track",
  "name": "small2.wav",
  "s3Key": "staging/audio/10e3e675-e7a6-41dc-a8fb-686ad683e40e.wav",
  "trackId": "??",
  "createdAt": "2018-03-05T03:30:18.246Z",
  "isProcessing": true,
  "didProcessingFail": false,
  "duration": null,
  "trackNum": 999
}

this is dumb {
  "trackId": "2b3de8ac-d145-4da6-b522-27e5413d43e1",
  "name": "small2.wav",
  "createdAt": "2018-03-05T03:30:18.627Z",
  "duration": null,
  "trackNum": 999,
  "s3Key": "staging/audio/10e3e675-e7a6-41dc-a8fb-686ad683e40e.wav",
  "isProcessing": true,
  "didProcessingFail": null,
  "__typename": "Track"
}

What am I doing wrong here?

I just chatted through this with the Engineer that worked on this code. What you're seeing is the bookeeping process that the AWS AppSync SDK process uses under the covers to ensure data integrity. It is NOT actually running a mutation 4 times against your API.

When the AppSync client gets an optimistic response the update function runs twice - once for the local response and once for the network response. This is standard Apollo behavior. What the AppSync client does under the covers is on the first optimistic response, we treat it as if it were the network response and store the data in a persistent storage medium (local storage for web, Async Storage for React Native) to allow optimistic UI when in an offline state. This is essentially an "outbox" that the data first gets written to when offline (currently the implementation uses Redux Offline) and if you disable offline with disableOffline:true you will no longer see this behavior.

When you come back online, the synchronization process gets executed and you see another mutation message (which is actually the original mutation) of the client sending this to the server and the appropriate response.

Note that if you are creating unique IDs in your optimistic response on the client AND also creating unique IDs on the server, for instance using the $util.autoId() then you could have duplicate records as we will not overwrite any local data you've explicitly assigned an ID. You can do invalidation of any locally-created IDs if you wish by using the Offline-enabled put item and Offline-enabled response templates in the DynamoDB resolvers for AppSync which use an ephemeral key called relayState (that you'll need to add as a field for the type you're creating) that you can use to track the local ID and match it up with the ID that was created at the server.

There are more additions we're going to be making to this bookeeping process in the future and would welcome suggestions in our GitHub issues repo: https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues