AWS Amplify Show AppSync/GraphQL Subscription Connection Status - aws-amplify

After creating an AWS AppSync/GraphQL subscription -- how do I get the subscriptions connection status and any future connection status changes?
The AWS Amplify documentation is clear on how to create a subscription:
const subscription = API.graphql(
graphqlOperation(subscriptions.onCreateTodo)
).subscribe({
next: ({ provider, value }) => console.log({ provider, value }),
error: (error) => console.warn(error)
});
The documentation is also clear on how to get subscription connection statuses using hub:
Hub.listen('api', (data: any) => {
const { payload } = data;
if (payload.event === CONNECTION_STATE_CHANGE) {
const connectionState = payload.data.connectionState as ConnectionState;
console.log(connectionState);
}
});
But this hub example listens to all GraphQL subscriptions. How can I listen to a specific subscription? Or when listening to all subscriptions -- is there a way to associate a hub connection with a specific GraphQL subscription?
A simplified example: In an app, I have a chat page and a GraphQL subscription that listens for new chats. I also have a page that shows the results of a long running backend process and a GraphQL subscription that listens for process complete. Hub listens to all GraphQL subscriptions and one of the subscriptions has been disconnected. Which GraphQL subscription has been disconnected so the app can display a message to the user?
The object returned by hub looks like this. I'm guessing the _linked properties link back to the specific GraphQL subscription. But I'm not seeing any human readable properties to determine which GraphQL subscription it's linked to.
{
"channel": "api",
"payload": {
"event": "ConnectionStateChange",
"data": {
"provider": {
"socketStatus": 0,
"keepAliveTimeout": 300000,
"subscriptionObserverMap": {},
"promiseArray": [],
"connectionStateMonitor": {
"_linkedConnectionState": {
"networkState": "connected",
"connectionState": "disconnected",
"intendedConnectionState": "disconnected",
"keepAliveState": "healthy"
},
"_linkedConnectionStateObservable": {},
"_linkedConnectionStateObserver": {
"_subscription": {
"_observer": {},
"_state": "ready"
}
}
},
"keepAliveTimeoutId": 108,
"keepAliveAlertTimeoutId": 109
},
"connectionState": "Connecting"
},
"message": "Connection state is Connecting"
},
"source": "PubSub",
"patternInfo": []
}
The GraphQL subscription returns a provider object that looks like this. It also has a connectionStateMonitor property. But when trying to display this property it errors with: Property 'connectionStateMonitor' is private and only accessible within class 'AWSAppSyncRealTimeProvider'
{
"provider": {
"socketStatus": 0,
"keepAliveTimeout": 300000,
"subscriptionObserverMap": {},
"promiseArray": [],
"connectionStateMonitor": {
"_linkedConnectionState": {
"networkState": "connected",
"connectionState": "disconnected",
"intendedConnectionState": "disconnected",
"keepAliveState": "healthy"
},
"_linkedConnectionStateObservable": {},
"_linkedConnectionStateObserver": {
"_subscription": {
"_observer": {},
"_state": "ready"
}
}
},
"keepAliveTimeoutId": 1056,
"keepAliveAlertTimeoutId": 1057
},
"value": {
"data": {
"BSubscribeToTest": {
"matchIt": 10,
"item": {
"rateRequestId": "123"
}
}
}
}
}

You need a combination of:
Hub.listen('some-channel', callback); and
Hub.dispatch('some-channel', {event, data, message});
That channel name (first parameter) can be added dynamically so that you are monitoring multiple channels in an application. Each channel can then be listened to and subscribed to separately and you can check the status, data, etc for each one.
Because you are listening on the example "api' channel above you would need to add that channel to this line:
graphqlOperation(subscriptions.onCreateTodo, 'api') Take note of the 'api' at the end.

Related

cloud run api service response broken when I use firebase rewrites

The firebase Sveltekit client app and server api use a google cloud run hosting container. This works fine when I use the cloud run url: https://app...-4ysldefc4nq-uc.a.run.app/
But when I use firebase rewriting the client works fine using: https://vc-ticker.web.app/... but receives 502 and 504 responses from the API service. The cloud run log does not show any errors, receives the client fetch POST request and returns a Readablestream response.
But this API service response stream never arrives when using rewrites.
firebase.json
{
"hosting": {
"public": "public", !! NOT used, cloud run hosts the app
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"run": {
"serviceId": "vc-ticker-app",
"region": "us-central1"
}
}
]
}
}
+page.svelte client API request:
const logging = true;
const controller = new AbortController();
let reader = null;
const signal = controller.signal;
async function streamer(params) {
console.log("stream with logging:", logging, JSON.stringify(params));
try {
const response = await fetch("api/my-ticker", {
method: "POST",
body: JSON.stringify(params),
headers: {
"content-type": "application/json",
},
signal: signal,
});
const stream = response.body.pipeThrough(new TextDecoderStream("utf-8"));
reader = stream.getReader();
while (true) {
const { value, done } = await reader.read();
if (done || response.status !== 200) {
console.log("done response", response.status, done, value);
await reader.cancel(`reader done or invalid response: ${response.status}`);
reader = null;
break;
}
// response ok: parse multi json chunks => array => set store
const quotes = {};
JSON.parse(`[${value.replaceAll("}{", "},{")}]`).forEach((each, idx) => {
quotes[each.id] = [each.price, each.changePercent];
console.log(`quote-${idx}:`, quotes[each.id]);
});
positions.set(quotes);
}
} catch (err) {
console.log("streamer exception", err.name, err);
if (reader) {
await reader.cancel(`client exception: ${err.name}`);
reader = null;
}
}
}
$: if ($portfolio?.coins) {
const params = {
logging,
symbols: Object.values($portfolio.symbols),
};
streamer(params);
}
onDestroy(async () => {
if (reader) await reader.cancel("client destroyed");
controller.abort();
console.log("finished");
});
I use the Sveltekit adapter-node to build the app.
With rewrite rules, you can direct requests that match specific patterns to a single destination.Check your firebase.json file and verify if the rewrite configuration in the hosting section has the redirect serviceId name same as that from the deployed container image,as per below example
"hosting": {// ...
// Add the "rewrites" attribute within "hosting"
"rewrites": [ {
"source": "/helloworld",
"run": {
"serviceId": "helloworld", // "service name" (from when you [deployed the container image][3])
"region": "us-central1" // optional (if omitted, default is us-central1)
}
} ]
}
It is important to note that Firebase Hosting is subject to a 60-second request timeout. If your app requires more than 60 seconds to run, you'll receive an HTTPS status code 504 (request timeout). To support dynamic content that requires longer compute time, consider using an App Engine flexible environment.
You should also check the Hosting configuration page for more details about rewrite rules. You can also learn about the priority order of responses for various Hosting configurations.
I made it work with an external link to the cloud run api service (cors).
But I still do not understand why It can't be done without cors using only firebase rewrites.
+page.svelte client API request update:
Now using GET and an auth token to verify the api request on the endpoint server
const search = new URLSearchParams(params);
const apiHost = "https://fs-por....-app-4y...q-uc.a.run.app/api/yahoo-finance-streamer";
const response = await fetch(`${apiHost}?${search.toString()}`, {
method: "GET",
headers: {
"auth-token": await getIdToken(),
},
signal: signal,
});
And a handle hook to verify the auth token and handle cors:
const logging = true;
const reqUnauthorized = { status: 403, statusText: 'Unauthorized!' };
/** #type {import('#sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
let response;
if (event.request.method !== "OPTIONS") {
if (event.url.pathname.startsWith('/api')) {
const authToken = event.request.headers.get("auth-token")
const { error = null, decodedToken } = await decodeIdToken(logging, authToken)
if (error) return new Response(error.message, reqUnauthorized);
if (verifyUser(logging, decodedToken) === false) {
return new Response(`user auth failed for: ${decodedToken.email}`, reqUnauthorized);
}
}
response = await resolve(event);
} else { // handle cors preflight OPTIONS
response = new Response("", { status: 200 });
}
response.headers.append('Access-Control-Allow-Headers', "*");
response.headers.append('Access-Control-Allow-Origin', "*");
return response;
}
From firebase support:
I got an answer from the engineering team. Unfortunately Firebase Hosting does not support streaming responses at the moment. I’ve created a feature request so they will consider implementing it.
Please be informed that submitting a feature request doesn’t guarantee that it will be implemented. Keep an eye on the release notes.
I realize that this is not the answer you expected from me, but unfortunately there is nothing I can do about it.

Test cloud functions with authentication context

I have a cloud function that uses the context field that contains the user authentication context:
export const updateLocation = functions
.region(functionsRegion)
.https.onCall(async (
data : { location : firestore.GeoPoint },
context
) => { ... }
How do I specify that context with the GCP cloud function testing tab or Postman? The first only lets me specify the data segment, the second doesn't recognize an auth field, i.e. a body of:
{
"data": {
"location": [50.021376, 14.464792]
},
"auth": {
"uid": "1Rxn217sSoffpGXfzdWU"
}
}
results in Request body has extra fields: auth; Invalid request, unable to process.

AppSync client query not returning complete data response

I am having an issue getting results back from my AppSync API via AWSAppSyncClient. I can run the query in the AWS AppSync console and get the complete results, however when I run the query from my client the portion of the results I am looking for returns an empty array.
I have tried slimming down the query to return less results, as I read at one point that dynamo will run a filter on the results being returned if you do not provide your own. I have also read this could have something to do with the partition keys used in the dynamoDB table, however AppSync provisioned that resource for me and handled the initial config. I am new to working with AppSync so I am sort of drawing a blank on where to even start looking for the issue because there is not even an error message.
The Query I am running
export const getUserConversations = `query getUser($id: ID!) {
getUser(id: $id) {
id
conversations {
items {
conversation{
id
associated{
items{
convoLinkUserId
}
}
}
}
}
}
}
`;
Call being made in a redux actions file
export const getUserConvos = (id) => async dispatch => {
AppSyncClient.query({
query: gql(getUserConversations),
variables: {
id: id
}
}).then(res => {
console.log("RES FROM CONVO QUERY", res)
})
}
This is the response I am getting in the browser
Notice conversations.items returns an empty array.
getUser:
conversations:
items: []
__typename: "ModelConvoLinkConnection"
__proto__: Object
id: "HIDDEN_ID"
__typename: "User"
__proto__: Object
__proto__: Object
However if i run the exact same query in the playground on the AppSync console I get this...
{
"data": {
"getUser": {
"id": "HIDDEN_ID",
"conversations": {
"items": [
{
"conversation": {
"id": "HIDDEN_ID",
"associated": {
"items": [
{
"convoLinkUserId": "HIDDEN_ID"
},
{
"convoLinkUserId": "HIDDEN_ID"
}
]
}
}
},
{
"conversation": {
"id": "HIDDEN_ID",
"associated": {
"items": [
{
"convoLinkUserId": "HIDDEN_ID"
},
{
"convoLinkUserId": "HIDDEN_ID"
}
]
}
}
}
]
}
}
}
}
*HIDDEN_ID is a placeholder
I know that the objects are in my DB, however if i run the query via my react application I get nothing, and if I run it in the console on AWS I get another. I need to be able to have access to these conversations via the client. What could be causing this?

Firebase.auth error with 400 bad request

I found a strange error while I developing system using Firebase with service url contains user data.
User data is below.
{
"uid": "kt9Hcp2FbYbBvvIeSHHa1RbvHcv2",
"displayName": "Anonymous 901",
"photoURL": null,
"email": null,
"emailVerified": false,
"identifierNumber": null,
"isAnonymous": true,
"providerData": [
],
"apiKey": "MyApiKeyString",
"appName": "MyAppName",
"authDomain": "my.auth.domain",
"stsTokenManager": {
"apiKey": "MyApiKeyString",
"refreshToken": "refreshTokenString",
"accessToken": "accessTokenString",
"expirationTime": 1532451863076
},
"redirectEventId": null
}
I encode the above anonymous user data and include it in the service url.
( http://myserviceurl?userdata=encodedUserData )
Inside the system receives that url, firebase creates a user object with that user data contained in the url.
The purpose of this url is to use specific user's information in any browser.
However, when I call that service url, sometimes system creates user object well, sometimes got error -
400 Bad request errors with
https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccountInfo?key=MyApiKeyString
And error data is below,
{
"error": {
"code": 400,
"message": "TOKEN_EXPIRED",
"errors": [
{
"message": "TOKEN_EXPIRED",
"domain": "global",
"reason": "invalid"
}
]
}
}
Few hours later it works well, I changed nothing though.
I could not find the exact error point, but I suspect error occurs while observing authentication state or before this step.
Here is code snipets
#bind
private makeUserLoadingPromise(): Promise<void> {
let unSubscribe: () => void;
return new Promise<void>((resolve, _reject) => {
const onInitialized = this.makeOnInitializedAuthStateChanged(resolve);
unSubscribe = this.auth.onAuthStateChanged(onInitialized);
}).then(() => {
unSubscribe();
this.auth.onAuthStateChanged(this.onAuthStateChanged);
});
}
#bind
private makeOnInitializedAuthStateChanged(resolve: () => void) {
return (user: firebase.User | null) => {
this.user = user;
resolve();
};
}
#bind
private onAuthStateChanged(user: firebase.User | null) {
this.user = user;
}
Or maybe it relates with expirationTime?
I couldn't find any hints about this situation.
Any advice would be appreciated.
It is not clear what you are doing, but it appears that you are using the API incorrectly and insecurely. The plain user object contains a refresh token that is indefinite. Passing it around via URL is a really bad idea.
First don't rely on internal implementations, it is subject to change.
To get the user's information on your backend, the right way to do it, is to get the user's ID token using officially supported API, eg user.getIdToken(), then pass it to your server.
On your server, you verify it via the Firebase Admin SDK: admin.auth().verifyIdToken(idToken). Then you know this is a real authenticated user. If you need the full user info, you can then look it up using the decoded user id in the token: admin.auth().getUser(decodedIdToken.sub).

How to not use Firebase Topics Messaging and Device Group Messaging?

Wanted to ask in my special case about sending FCM. I read about the Firebase Topic messaging and Device group messaging but dont think it will work in my scenario.
I have a Firebase connected app that need help!
I want to create a place where the app user can send a chat message and there is nobody listening, or there could be a million+ people listening.
There are millions or even billions of chat rooms and there is no way to tell how many are listening on any chat, there could be miljons billions listening, there could be this many notifications to inform users that a new message is posted
When a user sends the chat a message, the notification must be sent to everyone who is listening.
how could I do this?
You can simply use Firebase database to create a chat app as you seek. There are tons of tutorials for that specific application actually.
Typically, the structure would look something like this:
{
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
"timestamp": 1459361875666
},
"two": { ... },
"three": { ... }
},
"members": {
"one": {
"ghopper": true,
"alovelace": true,
"eclarke": true
},
"two": { ... },
"three": { ... }
},
"messages": {
"one": {
"m1": {
"name": "eclarke",
"message": "The relay seems to be malfunctioning.",
"timestamp": 1459361875337
},
"m2": { ... },
"m3": { ... }
},
"two": { ... },
"three": { ... }
}
}
(source)
So whenever a user sends a message through your app, you'd just create a new node under messages and update the appropriate entry under chats. I'd post more about the client-side process, but I'm not sure which platform you're using
EDIT
To clarify a bit further, you should listen to the chats node on your platform for any changes, once the timestamp of the last message changes from what you have stored on device, you check messages. That, or you can simply listen for childAdded in messages.
Regarding your comment, you don't really need FCM in this case unless you want to push notifications, in which case you'll need your own server for that to listen for the changes as well and dispatch the notifications appropriately.

Resources