aws-mobile-appsync-sdk-js
aws-mobile-appsync-sdk-js copied to clipboard
DeltaSync with subscriptionQuery does not work in aws-appsync >3.0.2
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:
- Setup server: https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-delta-sync.html One Step Setup Launch Stack
- Create React App: npx create-react-app my-app
- Install dependencies: yarn add @react-native-community/[email protected] aws-appsync graphql-tag
- 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
}
}
`);
- 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
I'm having the same issue as well. Also happens on 4.0.1
.
Any plans on addressing this bug?
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 "
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!
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.