awslabs / aws-mobile-appsync-sdk-js

JavaScript library files for Offline, Sync, Sigv4. includes support for React Native
Apache License 2.0
921 stars 266 forks source link

DeltaSync with subscriptionQuery does not work in aws-appsync >3.0.2 #573

Open matthiaszyx opened 4 years ago

matthiaszyx commented 4 years ago

Do you want to request a feature or report a bug? Bug

What is the current behavior? Delta sync works up to aws-appsync version 3.0.1 but does not work (nothing happens, no result, no error) for version 3.0.2 or greater when a subscriptionQuery is set. Without subscriptionQuery it always works in all versions.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. I took the server code from the official delta sync example https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-delta-sync.html and the client code from here https://docs.amplify.aws/lib/graphqlapi/advanced-workflows/q/platform/js#react-example

Steps to reproduce:

  1. Setup server: https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-delta-sync.html One Step Setup Launch Stack
  2. Create React App: npx create-react-app my-app
  3. Install dependencies: yarn add @react-native-community/netinfo@4.7.0 aws-appsync graphql-tag
  4. Add file queries.js:
    
    import gql from 'graphql-tag';

export const createPostMutation = gql( mutation createPost($input:CreatePostInput!) { createPost(input:$input) { id author title content } } );

export const syncPostsQuery = gql( query syncPosts { syncPosts { items { id author title content } startedAt nextToken } } );

export const onCreatePostSubscription = gql( subscription onCreatePost { onCreatePost { id author title content } } );

5. Add code to App.js:

import React from 'react'; import './App.css'; import AWSAppSyncClient, {AUTH_TYPE} from 'aws-appsync'; import { syncPostsQuery, onCreatePostSubscription, createPostMutation } from "./queries";`

const client = new AWSAppSyncClient({ url: "", region: "us-west-2", auth: { type: AUTH_TYPE.API_KEY, apiKey: "" } });

client.sync({ baseQuery: { query: syncPostsQuery, update: (cache, data) => { console.log("syncPosts baseQuery", data); } }, deltaQuery: { query: syncPostsQuery, update: (cache, data) => { console.log("syncPosts deltaQuery", data); } }, subscriptionQuery: { query: onCreatePostSubscription, update: (cache, data) => { console.log("syncPosts subscriptionQuery", data); } } });

const createPost = () => { client.mutate({ mutation: createPostMutation, variables: { input: { author: "Author", title: "Title", content: "Content" } }, update: (cache, data) => { console.log("createPost", data); } }); }

function App() { return (

); }

export default App;



**What is the expected behavior?**
Should work in all versions.

**Which versions and which environment (browser, react-native, nodejs) / OS are affected by this issue? Did this work in previous versions?**
bugged: aws-appsync > 3.0.2
working: aws-appsync < 3.0.1
yaronya commented 4 years ago

I'm having the same issue as well. Also happens on 4.0.1. Any plans on addressing this bug?

herzner commented 3 years ago

I also have this issue. The sync behaves fine as long as you don't specify a subscriptionQuery. I played around with the browsers offline mode (Firefox) and found out that even 3.0.1 seems to have an issue with re-connects.

Suppose that there is no subscriptionQuery defined the deltaQuery gets active whenever the browser is back online again - just as expected. When there is subscriptionQuery the sync function breaks when switching the browser to offline mode. The console output just states that the websocket-connection was interrupted.

According to the docs I would expect that the client re-subscribes (https://docs.amplify.aws/lib/graphqlapi/advanced-workflows/q/platform/js#delta-sync): "However, when the device transitions from offline to online, to account for high velocity writes the client will execute the resubscription along with synchronization and message processing "

herzner commented 3 years ago

I did some investigation and found that there is a promise that never gets resolved. When creating a subscription it waits for a control message confirming that the connection has been established.

See https://github.com/awslabs/aws-mobile-appsync-sdk-js/blob/master/packages/aws-appsync/src/deltaSync.ts

await new Promise(resolve => {
            if (subscriptionQuery && subscriptionQuery.query) {
                const { query, variables } = subscriptionQuery;

                subscription = client.subscribe<FetchResult, any>({
                    query: query,
                    variables: {
                        ...variables,
                        [SKIP_RETRY_KEY]: true,
                        [CONTROL_EVENTS_KEY]: true,
                    },
                }).filter(data => {
                    const { extensions: { controlMsgType = undefined, controlMsgInfo = undefined } = {} } = data;
                    const isControlMsg = typeof controlMsgType !== 'undefined';

                    if (controlMsgType) {
                        subsControlLogger(controlMsgType, controlMsgInfo);

                        if (controlMsgType === 'CONNECTED') {
                            resolve();
                        }
                    }

                    return !isControlMsg;
                }).subscribe({
                    // ...
                });
            } else {
                resolve();
            }
        });

See: https://github.com/awslabs/aws-mobile-appsync-sdk-js/blob/master/packages/aws-appsync-subscription-link/src/realtime-subscription-handshake-link.ts

Here the expected control message gets created. However there is a filter that ignores the control message because of the undefined controlEvents flag.

When creating the subscription the flag CONTROL_EVENTS_KEY is passed with the variables property. But it is not correctly evaluated when reading the operation's context.

request(operation: Operation) {
    const { query, variables } = operation;
    const {
      controlMessages: { [CONTROL_EVENTS_KEY]: controlEvents } = {
        [CONTROL_EVENTS_KEY]: undefined
      },
      headers
    } = operation.getContext();
    return new Observable<FetchResult>(observer => {

    // ...

    }).filter(data => {
      const { extensions: { controlMsgType = undefined } = {} } = data;
      const isControlMsg = typeof controlMsgType !== "undefined";

      return controlEvents === true || !isControlMsg;
    });
  }

Would be great if you could provide a solution for this. Thx!

cadam11 commented 2 years ago

About 14 months later, I found this works for setting the control flag. Have to pass in a context object as part of the subscription options (Apollo 3 client):

const { error } = useSubscription(
    gql`
      subscription Events {
        onEvent {
          id
          time
          eventName
        }
      }
    `,
    {
      variables: {},
      context: {
        controlMessages: {
          [CONTROL_EVENTS_KEY]: true,
        },
      },
      onSubscriptionData: (data) => {
        console.log('raw data from subscription', data);

        const ext = (
          data.subscriptionData as {
            extensions?: { controlMsgType?: string; controlMsgInfo?: unknown };
          }
        ).extensions;

        console.log('extension data', ext);
      },
    }
  );

Doesn't work when passing that flag in via variables since it's destructured from the result of getContext() on the operation object.