Comments (8)
The update changes the behavior of subscriptions. The new behavior is described here: https://aws.amazon.com/de/blogs/mobile/appsync-realtime/
The legacy AWS AppSync PubSub provider created MQTT connections via websockets. The new real-time AWS AppSync PubSub provider uses pure websockets. The legacy API was able to work with multiple root fields in subscriptions which are prohibited by GraphQL's spec (https://spec.graphql.org/June2018/#sec-Single-root-field). The pure websockets API seems to be more specification-compliant. Unfortunately, this breaks applications that rely on multi-field subscriptions.
I know you are using Vue but maybe you get (or anyone who stumbles upon this issue get) some inspiration to work around this problem with the solution that works for us: We created an adapted version of the Connect component (https://github.com/aws-amplify/amplify-js/blob/master/packages/aws-amplify-react/src/API/GraphQL/Connect.tsx) and implemented support for multiple subscriptions (and queries):
import { Component } from 'react';
import API from '@aws-amplify/api';
import merge from 'deepmerge';
function getNextToken(data) {
const keyWithNextToken = Object.keys(data).find(key => data[key].nextToken);
return (
keyWithNextToken &&
data[keyWithNextToken] &&
data[keyWithNextToken].nextToken
);
}
function recursiveQuery(operation) {
return new Promise(resolve => {
API.graphql(operation).then(response => {
const nextToken = getNextToken(response.data);
if (nextToken) {
recursiveQuery({
query: operation.query,
variables: {
...operation.variables,
nextToken
}
}).then(data => {
resolve(merge(response.data, data));
});
} else {
resolve(response.data);
}
});
});
}
export default class Connect extends Component {
constructor(props) {
super(props);
this.state = this.getInitialState();
this.subSubscriptions = [];
}
getInitialState() {
const { query } = this.props;
return {
loading: query && !!query.query,
data: [],
errors: [],
mutation: () => console.warn('Not implemented')
};
}
getDefaultState() {
return {
loading: false,
data: [],
errors: [],
mutation: () => console.warn('Not implemented')
};
}
async _fetchData() {
this._unsubscribe();
this.setState({ loading: true });
let {
query: { query: legacyQuery, variables: legacyVariables = {} } = {},
queries,
mutation: { query: mutation } = {},
subscription: legacySubscription,
subscriptions,
onSubscriptionMsg = prevData => prevData
} = this.props;
let { data, mutation: mutationProp, errors } = this.getDefaultState();
if (
!API ||
typeof API.graphql !== 'function' ||
typeof API.getGraphqlOperationType !== 'function'
) {
throw new Error(
'No API module found, please ensure @aws-amplify/api is imported'
);
}
if (legacySubscription) {
subscriptions = [legacySubscription];
console.warn(
'subscription and subscriptions are specified. subscription is ignored.'
);
}
let variables = [];
if (legacyQuery) {
queries = [legacyQuery];
variables = [legacyVariables];
console.warn('query and queries are specified. subscription is ignored.');
} else {
variables = queries.map(query => query.variables);
}
const hasValidQueries =
queries &&
queries.every(q => API.getGraphqlOperationType(q.query) === 'query');
const hasValidMutation =
mutation && API.getGraphqlOperationType(mutation) === 'mutation';
const hasValidSubscriptions =
subscriptions &&
subscriptions.every(
s => API.getGraphqlOperationType(s.query) === 'subscription'
);
if (!hasValidQueries && !hasValidMutation && !hasValidSubscriptions) {
console.warn('No query, mutation or subscription was specified');
}
if (hasValidQueries) {
data = [];
queries.forEach((query, index) => {
try {
recursiveQuery({
query: query.query,
variables: variables[index]
}).then(responseData => {
data[index] = responseData;
this.setState({ data });
});
} catch (err) {
data[index] = err.data;
errors[index] = err.errors;
}
});
}
if (hasValidMutation) {
mutationProp = async variables => {
const result = await API.graphql({
query: mutation,
variables: legacyVariables
});
this.forceUpdate();
return result;
};
}
if (hasValidSubscriptions) {
subscriptions.forEach((subscription, index) => {
const { query: subsQuery, variables: subsVars } = subscription;
try {
const observable = API.graphql({
query: subsQuery,
variables: subsVars
});
this.subSubscriptions.push(
observable.subscribe({
next: ({ value: { data } }) => {
const { data: prevData } = this.state;
const newData = onSubscriptionMsg(prevData, data);
if (this.mounted) {
this.setState({ data: newData });
}
},
error: err => console.error(err)
})
);
} catch (err) {
errors[index] = err.errors.concat(errors[index]);
}
});
}
this.setState({ data, errors, mutation: mutationProp, loading: false });
}
_unsubscribe() {
this.subSubscriptions.forEach(subSubscription =>
subSubscription.unsubscribe()
);
}
async componentDidMount() {
this._fetchData();
this.mounted = true;
}
componentWillUnmount() {
this._unsubscribe();
this.mounted = false;
}
componentDidUpdate(prevProps) {
const { loading } = this.state;
const { query: newQueryObj, mutation: newMutationObj } = this.props;
const { query: prevQueryObj, mutation: prevMutationObj } = prevProps;
// quer
const { query: newQuery, variables: newQueryVariables } = newQueryObj || {};
const { query: prevQuery, variables: prevQueryVariables } =
prevQueryObj || {};
const queryChanged =
prevQuery !== newQuery ||
JSON.stringify(prevQueryVariables) !== JSON.stringify(newQueryVariables);
// mutatio
const { query: newMutation, variables: newMutationVariables } =
newMutationObj || {};
const { query: prevMutation, variables: prevMutationVariables } =
prevMutationObj || {};
const mutationChanged =
prevMutation !== newMutation ||
JSON.stringify(prevMutationVariables) !==
JSON.stringify(newMutationVariables);
if (!loading && (queryChanged || mutationChanged)) {
this._fetchData();
}
}
render() {
const { data, loading, mutation, errors } = this.state;
return this.props.children({ data, errors, loading, mutation }) || null;
}
}
from amplify-ui.
As @visusnet pointed out, the new AppSync real-time endpoints do not support having multiple root fields in the subscription for the Connect
component. However, there does appear to be a few workarounds noted in this issue. My suggestion would just be to manage each subscription directly with the API
category (docs) or go with a custom solution provided here.
With that said, we are continuing build out our new UI Components, which provide a much more consistent experience across the major JavaScript frameworks. The Connect
component is not supported there yet, but it is on the roadmap. I am marking this as a "feature request" for now to be sure this is taken into account in the future.
from amplify-ui.
Is there a way I can know the status of this issue? Using PubSub from an older version on aws-amplify 3.3.18. However updating to the latest version stops all my AWS services on the mobile end. Tried reconfiguring Amplify by adding my IAM credentials. So far, no success.
from amplify-ui.
I've used this version of 'Connect' to get my multiple subscriptions working, but it throws two warnings, then an error 'TypeError: Must provide source. Received: Undefined.
This is my connect...
<Connect
key="CategoriesConnector"
query={graphqlOperation(listPosts, {
limit: 100,
})}
subscription={graphqlOperation(onCreateOrUpdateOrDelete)}
onSubscriptionMsg={(prev, {addedPost, editedPost, deletePost}) => {
console.log(prev);
if (addedPost) {
prev.listPosts.items.push(addedPost);
} else if (editedPost) {
prev.listPosts.items = prev.listPosts.items.map(post => post.id === editedPost.id ? editedPost : post);
} else if (deletePost) {
prev.listPosts.items = prev.listPosts.items.filter(post => post.id !== deletePost.id);
}
return prev;
}}
>
{({ data, loading, error }) => {
if (error) return <h3 key="Error">Error</h3>;
if (loading || !data) return <h3>No Posts to load</h3>;
return [
data.listPosts.items.map(item => (
<PostContainer key={item.id} post={item} />
))
];
}}
</Connect>
And my custom subscriptions...
export const onCreateOrUpdateOrDelete = /* GraphQL */ `
subscription OnCreateOrUpdateOrDelete {
addedPost {
id
date
data
}
editedPost {
id
date
data
}
deletePost {
id
date
data
}
}
`;
from amplify-ui.
I am having the same issue, can't do a simple crud with this component
from amplify-ui.
I'm having the same issue in React after migrating from "aws-amplify": "^1.1.32"
, "aws-amplify-react": "^2.3.11"
.
from amplify-ui.
I tried the Connect.tsx version above and got the same errors reported by others in this thread. So I created a version whereby you can pass in multiple subscriptions as an array - you can still pass in single a subscription too - as per the original version. Note: this version takes only a single query and a single onSubscriptionMessage - however your onSubscriptionMessage can be a wrapper function that examines the newData passed into it and calls the appropriate update depending on this data like this:
const onSubscriptionMessage = (prevQuery, newData) => {
if(newData && newData.onDeleteItem) {
return onRemoveItem(prevQuery, newData);
}
if(newData && newData.onCreateItem) {
return onAddItem(prevQuery, newData);
}
};
Connect.tsx for multiple subscriptions given a single query and a single onSubscriptionMessage handler that switches handling according to the newData.
import * as React from 'react';
import { API, GraphQLResult } from '@aws-amplify/api';
import Observable from 'zen-observable-ts';
export interface IConnectProps {
mutation?: any;
onSubscriptionMsg?: (prevData: any) => any;
query?: any;
subscription?: any;
}
export interface IConnectState {
loading: boolean;
data: any;
errors: any;
mutation: any;
}
export class Connect extends React.Component<IConnectProps, IConnectState> {
public subSubscriptions: Array<Promise<GraphQLResult<object>> | Observable<object>>;
private mounted: boolean;
constructor(props) {
super(props);
this.state = this.getInitialState();
this.subSubscriptions = [];
}
getInitialState() {
const { query } = this.props;
return {
loading: query && !!query.query,
data: {},
errors: [],
mutation: () => console.warn('Not implemented'),
};
}
getDefaultState() {
return {
loading: false,
data: {},
errors: [],
mutation: () => console.warn('Not implemented'),
};
}
async _fetchData() {
this._unsubscribe();
this.setState({ loading: true });
const {
// @ts-ignore
query: { query, variables = {} } = {},
// @ts-ignore
mutation: { query: mutation, mutationVariables = {} } = {},
subscription,
onSubscriptionMsg = prevData => prevData,
} = this.props;
let { data, mutation: mutationProp, errors } = this.getDefaultState();
if (
!API ||
typeof API.graphql !== 'function' ||
typeof API.getGraphqlOperationType !== 'function'
) {
throw new Error(
'No API module found, please ensure @aws-amplify/api is imported'
);
}
const hasValidQuery =
query && API.getGraphqlOperationType(query) === 'query';
const hasValidMutation =
mutation && API.getGraphqlOperationType(mutation) === 'mutation';
const validSubscription = subscription => subscription
&& subscription.query
&& API.getGraphqlOperationType(subscription.query) === 'subscription';
const validateSubscriptions = (subscription) => {
let valid = false;
if(Array.isArray(subscription)) {
valid = subscription.map(validSubscription).indexOf(false) === -1;
} else {
valid = validSubscription(subscription)
}
return valid;
};
const hasValidSubscriptions = validateSubscriptions(subscription);
if (!hasValidQuery && !hasValidMutation && !hasValidSubscriptions) {
console.warn('No query, mutation or subscription was specified correctly');
}
if (hasValidQuery) {
console.log('hasValidQuery');
try {
data = null;
const response = await API.graphql({ query, variables });
// @ts-ignore
data = response.data;
} catch (err) {
data = err.data;
errors = err.errors;
}
}
if (hasValidMutation) {
console.log('hasValidMutation');
// @ts-ignore
mutationProp = async variables => {
const result = await API.graphql({ query: mutation, variables });
this.forceUpdate();
return result;
};
}
if (hasValidSubscriptions) {
const connectSubscriptionToOnSubscriptionMessage = (subscription) => {
const {query: subsQuery, variables: subsVars} = subscription;
try {
const observable = API.graphql({
query: subsQuery,
variables: subsVars,
});
// @ts-ignore
this.subSubscriptions.push(observable.subscribe({
next: ({value: {data}}) => {
const {data: prevData} = this.state;
// @ts-ignore
const newData = onSubscriptionMsg(prevData, data);
if (this.mounted) {
this.setState({data: newData});
}
},
error: err => console.error(err),
}));
} catch (err) {
errors = err.errors;
}
};
if(Array.isArray(subscription)) {
subscription.forEach(connectSubscriptionToOnSubscriptionMessage);
} else {
connectSubscriptionToOnSubscriptionMessage(subscription)
}
}
this.setState({ data, errors, mutation: mutationProp, loading: false });
}
_unsubscribe() {
const __unsubscribe = subSubscription => {
if (subSubscription) {
subSubscription.unsubscribe();
}
};
this.subSubscriptions.map(__unsubscribe);
}
async componentDidMount() {
console.log('Connect - componentDidMount');
this._fetchData();
this.mounted = true;
}
componentWillUnmount() {
console.log('Connect - componentWillUnmount');
this._unsubscribe();
this.mounted = false;
}
componentDidUpdate(prevProps) {
const { loading } = this.state;
const { query: newQueryObj, mutation: newMutationObj, subscription: newSubscription} = this.props;
const { query: prevQueryObj, mutation: prevMutationObj, subscription: prevSubscription } = prevProps;
// query
// @ts-ignore
const { query: newQuery, variables: newQueryVariables } = newQueryObj || {};
// @ts-ignore
const { query: prevQuery, variables: prevQueryVariables } =
prevQueryObj || {};
const queryChanged =
prevQuery !== newQuery ||
JSON.stringify(prevQueryVariables) !== JSON.stringify(newQueryVariables);
// mutation
// @ts-ignore
const { query: newMutation, variables: newMutationVariables } =
newMutationObj || {};
// @ts-ignore
const { query: prevMutation, variables: prevMutationVariables } =
prevMutationObj || {};
const mutationChanged =
prevMutation !== newMutation ||
JSON.stringify(prevMutationVariables) !==
JSON.stringify(newMutationVariables);
// subscription
// @ts-ignore
const { query: newSubsQuery, variables: newSubsVars } = newSubscription || {};
// @ts-ignore
const { query: prevSubsQuery, variables: prevSubsVars } = prevSubscription || {};
const subscriptionChanged =
prevSubsQuery !== newSubsQuery ||
JSON.stringify(prevSubsVars) !==
JSON.stringify(newSubsVars);
if (!loading && (queryChanged || mutationChanged || subscriptionChanged)) {
this._fetchData();
}
}
render() {
const { data, loading, mutation, errors } = this.state;
// @ts-ignore
return this.props.children({ data, errors, loading, mutation }) || null;
}
}
/**
* @deprecated use named import
*/
export default Connect;
Usage (*example of onSubscriptionMessage is above)
<Connect
query={graphqlOperation(listTopics)}
subscription={[graphqlOperation(onCreateTopic), graphqlOperation(onDeleteTopic)]}
onSubscriptionMsg={onSubscriptionMessage}>
{.....}
</Connect>
from amplify-ui.
was the react Connect
component deprecated?
from amplify-ui.
Related Issues (20)
- [FR] Authenticator: hideSignIn HOT 1
- Hide "Confirm Password" field during sign up HOT 1
- [Bug] Vue Authenticator component exported as `any` type
- [FR]Storage Manager: Add transfer acceleration option HOT 1
- Menu cannot be disabled HOT 3
- @aws-amplify/ui-react-native withAuthenticator and <Authenticator.Provider> works on web but fails on mobile when using expo-router HOT 9
- [FR] Liveness - Proxy for APIs HOT 1
- Bug: Take it for a test drive feature is showing default component after minimizing the screen.
- Making a selection using a SelectField initiates text selection in Firefox on Windows HOT 1
- bug(react/a11y): Tabs primitive uses malformed id values with spaces in them
- 1
- front page icons
- Inconsistent frontend validation of certain Authenticator form fields HOT 2
- Session Not Found HOT 15
- Authenticator error message overflows if message is long HOT 2
- Issue with `withAuthenticator` hook integration in Next.js app HOT 3
- Ionic Chatbot UI [missing-page] HOT 2
- Ability to specify auth flow to trigger user migration events HOT 2
- Reset Password by Email not showing correct translations HOT 3
- SelectField SVG accessibility
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from amplify-ui.