aws-mobile-appsync-sdk-js icon indicating copy to clipboard operation
aws-mobile-appsync-sdk-js copied to clipboard

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

Open matthiaszyx opened this issue 3 years ago • 4 comments

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/[email protected] 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
        }
    }
`);
  1. 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: "<API URL>",
	region: "us-west-2",
	auth: {
		type: AUTH_TYPE.API_KEY,
		apiKey: "<API KEY>"
	}
});

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 (
    <div className="App">
      <button onClick={createPost}>CREATE POST</button>
    </div>
  );
}

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

matthiaszyx avatar Jul 08 '20 14:07 matthiaszyx

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

yaronya avatar Sep 22 '20 17:09 yaronya

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 avatar Nov 06 '20 10:11 herzner

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!

herzner avatar Nov 11 '20 20:11 herzner

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.

cadam11 avatar Jan 12 '22 03:01 cadam11