How to update React-Query data after socket signal? - react-query

I have app using React-Query, and I need to trigger data update after socket signal. That is, if I am on the user's page and I received a notification that the user's data has changed, then I need to send a request to receive updated data and write it. How to listen to socket events using useQuery hook?

You wouldn't do that with useQuery, but you would setup a socket subscription separately and then either write to the cache when you receive a message, or just invalidate the query to trigger a refetch.
I have a full blog post on this topic:
https://tkdodo.eu/blog/using-web-sockets-with-react-query
But the gist is:
const useReactQuerySubscription = () => {
const queryClient = useQueryClient()
React.useEffect(() => {
const websocket = new WebSocket('wss://echo.websocket.org/')
websocket.onopen = () => {
console.log('connected')
}
websocket.onmessage = (event) => {
const data = JSON.parse(event.data)
const queryKey = [...data.entity, data.id].filter(Boolean)
queryClient.invalidateQueries(queryKey)
}
return () => {
websocket.close()
}
}, [queryClient])
}

Related

Send message and leave server on ready event if not whitelisted (Discord.JS + MongoDB)

I'm coding a whitelist system for my discord bot that, on ready event (and after a 3 seconds delay), checks if every server it is in has it's ID added to the whitelist database on MongoDB. If not, the bot sends an embed and leaves the server. I managed to get it working on the guildCreate event, but on ready event it performs the message and leave actions on every single server without filtering conditions, even though those are added to the list. I cannot figure out why. Also, I'm still new to JavaScript, so it could be just a minor mistake.
//VARIABLES
const { Client, MessageEmbed } = require("discord.js")
const config = require('../../Files/Configuration/config.json');
const DB = require("../../Schemas/WhitelistDB");
//READY EVENT
module.exports = {
name: "ready",
once: false,
async execute(client) {
//[ ... ] <--- OTHER UNNECESSARY CODE IN BETWEEN
setTimeout(function() { // <--- 3 SECONDS DELAY
client.guilds.cache.forEach(async (guild) => { // <--- CHECK EVERY SERVER
await DB.find({}).then(whitelistServers => { // <--- CHECK MONGODB ID LIST
if(!whitelistServers.includes(guild.id)) {
const channel = guild.channels.cache.filter(c => c.type === 'GUILD_TEXT').random(1)[0]; // <--- SEND MESSAGE TO RANDOM TEXT CHANNEL (It is sending to every server, when it should be sending only to the not whitelisted ones)
if(channel) {
const WhitelistEmbed = new MessageEmbed()
WhitelistEmbed.setColor(config.colors.RED)
WhitelistEmbed.setDescription(`${config.symbols.ERROR} ${config.messages.SERVER_NOT_WHITELISTED}`)
channel.send({embeds: [WhitelistEmbed]});
}
client.guilds.cache.get(guild.id).leave(); // <--- LEAVE SERVER (It is leaving every server, when it should be leaving only the not whitelisted ones)
} else { return }
});
});
}, 1000 * 3);
}
}
I found the solution myself!
Instead of finding the array of whitelisted ID's for each guild, find one at a time and instead of checking the content of the array, check if the array exists. This is the updated code:
//WHITELIST
setTimeout(function() {
client.guilds.cache.forEach(async (guild) => {
await DB.findOne({ GuildID: guild.id }).then(whitelistServers => {
if(!whitelistServers) {
const channel = guild.channels.cache.filter(c => c.type === 'GUILD_TEXT').random(1)[0];
if(channel) {
const WhitelistEmbed = new MessageEmbed()
WhitelistEmbed.setColor(config.colors.RED)
WhitelistEmbed.setDescription(`${config.symbols.ERROR} ${config.messages.SERVER_NOT_WHITELISTED}`)
channel.send({embeds: [WhitelistEmbed]});
}
client.guilds.cache.get(guild.id).leave();
} else { return }
});
});
}, 1000 * 3);

Amplify Datastore subscription cost

I am trying to understand the cost of Datastore. It seems that it subscribes to all Mutations. So if there are 50 users, then each message will be send 50 times, even if it not required.
As each real time mutation costs money, we will be paying unnecessary 49 times for this real time message mutation.
Also , it seems to me SyncExpression doesn't have any effect on this Subscription.
I am really stuck here. It will be great of someone can clarify
Amplify generates the datastore boilerplate code for you, but you still need to call it. You won't pay for every user and every mutation.
You will only subscribe to a mutation (explicitly call the code to listen for changes) on a per-user basis for things that user is interested in. e.g. if you are viewing a TODO item, you'd subscribe the user to that item and they'll immediately see if someone else modify it on another device.
UPDATE
Long story... I was triggering back-end computation via GraphQL by making a lambda resolver. The computation took too long and the GQL call would timeout. I updated the code so the GQL call called itself asynchronously (re-trigger the lambda), and returned immediately. Then when the long-running task completed in the spun-up lambda, I updated the a record in the database.
I update the record using AppSync instead of direct GQL so it would trigger mutations, and in the react client, I listen to a mutation for the specific record that will be updated. This way, there is just 1 user listening (if they've triggered the long running action) and that user is only notified about changes to the single DB record they're interested in, and not receiving other user's updates.
I don't know if all this is applicable to your situation. The code snippets below may help you, but they're somewhat out of context.
// In amplify/backend/api/projectname/schema.graphql
type Subscription {
onCouponWithIdUpdated(id: ID!): Coupon #aws_subscribe(mutations: ["updateCoupon"])
}
// In my useSendCoupon hook...
// Subscribe to coupon updates
useEffect(() => {
if (0 === couponId) {
return
}
console.log(`subscribe to coupon updates for couponId:`, couponId)
const onCouponWithIdUpdated = /* GraphQL */ `
subscription OnCouponWithIdUpdated($id: ID!) {
onCouponWithIdUpdated(id: $id) {
id
proofLink
owner
}
}
`
const subscription = API
.graphql(graphqlOperation(onCouponWithIdUpdated, { id: couponId }))
.subscribe({
next: ({ provider, value }) => {
const coupon = value.data.onCouponWithIdUpdated
//console.log(`Proof Link:`, coupon.proofLink)
setProofLinks([coupon.proofLink])
setSendCouponState(COUPON_STATE_PREVIEW_SUCCESS)
},
error: error => console.warn(error)
})
console.log('subscribed: ', subscription)
return () => {
console.log(`unsubscribe to coupon updates`)
subscription.unsubscribe()
}
}, [couponId])
// inside a lambda...
const updateCouponWithProof = async (authorization, couponId, proofLink) => {
const initializeClient = () => new AWSAppSyncClient({
url: process.env.API_XXXX_GRAPHQLAPIENDPOINTOUTPUT,
region: process.env.REGION,
auth: {
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
jwtToken: authorization
},
disableOffline: true,
})
const executeMutation = async (mutation, operationName, variables) => {
const client = initializeClient()
try {
const response = await client.mutate({
mutation: gql(mutation),
variables,
fetchPolicy: "no-cache",
})
return response.data[operationName]
} catch (err) {
console.log("Error while trying to mutate data", err)
throw JSON.stringify(err)
}
}
const updateCoupon = /* GraphQL */ `
mutation UpdateCoupon(
$input: UpdateCouponInput!
$condition: ModelCouponConditionInput
) {
updateCoupon(input: $input, condition: $condition) {
id
proofLink
owner
}
}
`
const variables = { input: { id: couponId, proofLink } }
try {
return await executeMutation(updateCoupon, 'updateCoupon', variables)
} catch (error) {
console.log(`executeMutation error`, error)
}
}

Flutter Cloud Messaging: how to send notification from the app (not from firebase console)

Is it possible to send the notification from within the app instead of a cloud function on firebase?
The reason is, I want to do something similar to: FCM Push Notifications for Flutter, where they have this function that will be deployed to firebase:
export const sendToTopic = functions.firestore
.document('puppies/{puppyId}')
.onCreate(async snapshot => {
const puppy = snapshot.data();
const payload: admin.messaging.MessagingPayload = {
notification: {
title: 'New Puppy!',
body: `${puppy.name} is ready for adoption`,
icon: 'your-icon-url',
click_action: 'FLUTTER_NOTIFICATION_CLICK' // required only for onResume or onLaunch callbacks
}
};
return fcm.sendToTopic('puppies', payload);
});
this method works as intended on firebase cloud functions, however I need the path
.document('puppies/{puppyId}')
to be dynamic depending on which chatroom a user is in, so he would get a notification everytime i new message is send, so the 'chatroom22' would be a variable:
.document('chatroom22/{roomId}')
So is it possible to do this in the app-code, or can this be done in the deployed function?
In response to Doug Stevensons answer
Okay, that makes sence, and works. However, now everybody get the notifications. I want only the people in a given chatroom to receive the notification. I've tried something like this, where the users device token is saved for a given chat-room, then I want to notiffy all those tokens:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().functions);
var newData;
exports.myTrigger = functions.firestore.document('messages/{messageId}/room/{roomId}/message/{messageId2}').onCreate(async (snapshot, context) => {
//
if (snapshot.empty) {
console.log('No Devices');
return;
}
newData = snapshot.data();
const deviceIdTokens = await admin
.firestore()
.collection('messages/{messageId}/room/{roomId}/tokens/{tokenId}')
.get();
var tokens = [];
for (var token of deviceIdTokens.docs) {
tokens.push(token.data().device_token);
}
var payload = {
notification: {
title: `${newData.sender}`,
body: `${newData.message}`,
sound: 'default',
},
data: {
push_key: 'Push Key Value',
key1: newData.message,
},
};
try {
const response = await admin.messaging().sendToDevice(tokens, payload);
console.log('Notification sent successfully');
} catch (err) {
console.log(err);
}
});
But it doesnt seem to work with wildcards.. how can I get the specific destination for each chatroom?
You can use a wildcard for the collection in the path:
functions.firestore.document('{collectionId}/{documentId}')
But this will trigger for all documents in all top-level collecitons, which is probably not what you want.
In fact, using variable names for top-level collections is actually not the preferred way to model data in Firestore. Consider instead having a top-level collection for all rooms, then use subcollections to contain their messages. If you do that, then you function becomes more more clearly defined as:
functions.firestore.document('rooms/{roomId}/messages/{messageId}')
Cloud Fuctions only allows wildcards for full path segments like this. There are no other patterns or regular expressions.

axios - requests pool implementation

I'm looking for a way to create a pool of HTTP requests and have a timer that will send them every x seconds.
I'm using axios as my HTTP client and wonder if there is a hook in axios to handle a request before it being sent, save it in this pool and resolve immediately, then my timer will do the actual request asynchronously.
I imagine to have some ability like that like that:
const pool = [];
setInterval(() => {
const promises = [];
while(pool.length){
const { url, data, config } = pool.pop();
delete config.usePool;
// This request will be send to the server
promises.push(axios.post(url, data, config);
}
await Promise.all(promises);
}, 5000);
// this `axios.hook` not really exists, im looking for a way to implement it (this is my actual question)
axios.hook = (url, data, config) => {
if(config?.usePool){
pool.push({ url, data, config });
}
};
// This request won't send but save in the pool
const res = axios.post('/my/url', { a: 1, b: 1 }, { usePool: true });
For a request pool of size 8, you can use the following implementation:
const axios = require('axios');
const { Agent } = require('https');
export default axios.create({
httpsAgent: new Agent({
maxSockets: 8
})
});
You can find more configuration options for the https agent here:
https://nodejs.org/api/http.html#new-agentoptions
There isn't an option available to space out the requests in time as you desired, although the usefulness of that approach is debatable.

What is correct way to respond from webhook running nodejs?

Trying to implement web-hook (with V2 dialogflow) running nodejs. Received response "MalformedResponse 'final_response' must be set.". Below is the code. To the end of POST (app.post) code block was expecting conv.close would send SimpleResponse. But that's not happening. Need help understand why this error is seen and probable direction to solve it.
Thanks
const express = require('express');
const {
dialogflow,
Image,
SimpleResponse,
} = require('actions-on-google')
const bodyParser = require('body-parser');
const request = require('request');
const https = require("https");
const app = express();
const Map = require('es6-map');
// Pretty JSON output for logs
const prettyjson = require('prettyjson');
const toSentence = require('underscore.string/toSentence');
app.use(bodyParser.json({type: 'application/json'}));
// http://expressjs.com/en/starter/static-files.html
app.use(express.static('public'));
// http://expressjs.com/en/starter/basic-routing.html
app.get("/", function (request, response) {
console.log("Received GET request..!!");
//response.sendFile(__dirname + '/views/index.html');
response.end("Response from my server..!!");
});
// Handle webhook requests
app.post('/', function(req, res, next) {
console.log("Received POST request..!!");
// Log the request headers and body, to aide in debugging. You'll be able to view the
// webhook requests coming from API.AI by clicking the Logs button the sidebar.
console.log('======Req HEADERS================================================');
logObject('Request headers: ', req.headers);
console.log('======Req BODY================================================');
logObject('Request body: ', req.body);
console.log('======Req END================================================');
// Instantiate a new API.AI assistant object.
const assistant = dialogflow({request: req, response: res});
// Declare constants for your action and parameter names
//const PRICE_ACTION = 'price'; // The action name from the API.AI intent
const PRICE_ACTION = 'revenue'; // The action name from the API.AI intent
var price = 0.0
// Create functions to handle intents here
function getPrice(assistant) {
console.log('** Handling action: ' + PRICE_ACTION);
let requestURL = 'https://blockchain.info/q/24hrprice';
request(requestURL, function(error, response) {
if(error) {
console.log("got an error: " + error);
next(error);
} else {
price = response.body;
logObject('the current bitcoin price: ' , price);
// Respond to the user with the current temperature.
//assistant.tell("The demo price is " + price);
}
});
}
getPrice(assistant);
var reponseText = 'The demo price is ' + price;
// Leave conversation with SimpleResponse
assistant.intent(PRICE_ACTION, conv => {
conv.close(new SimpleResponse({
speech: responseText,
displayText: responseText,
}));
});
}); //End of app.post
// Handle errors.
app.use(function (err, req, res, next) {
console.error(err.stack);
res.status(500).send('Oppss... could not check the price');
})
// Pretty print objects for logging.
function logObject(message, object, options) {
console.log(message);
console.log(prettyjson.render(object, options));
}
// Listen for requests.
let server = app.listen(process.env.PORT || 3000, function () {
console.log('Your app is listening on ' + JSON.stringify(server.address()));
});
In general, The "final_response" must be set error is because you didn't send anything back. You have a lot going on in your code, and while you're on the right track, there are a few things in the code that could be causing this error.
First - in the code, it looks like you are confused about how to send a response. You have both a call to conv.close() and the commented out assistant.tell(). The conv.close() or conv.ask() methods are the way to send a reply using this version of the library. The tell() method was used by a previous version and is no longer supported.
Next, your code looks like it is only setting up the assistant object when the routing function is called. While this can be done, it is not the usual way to do it. Typically you'll create the assistant object and setup the Intent handlers (using assistant.intent()) as part of the program initialization. This is a rough equivalent to setting up the express app and the routes for it before the request itself comes in.
The portion that sets up the Assistant and then hooks it into a route might look something like this:
const assistant = dialogflow();
app.post('/', assistant);
If you really wanted to examine the request and response objects first, you might do this as something like
const assistant = dialogflow();
app.post('/', function( req, res ){
console.log(JSON.stringify(req.body,null,1));
assistant( req, res );
});
Related to this appears to be that you're trying to execute code in the route handler and then trying to call the intent handler. Again, this might be possible, but isn't the suggested way to use the library. (And I haven't tried to debug your code to see if there are problems in how you're doing it to see if you're doing it validly.) More typical would be to call getPrice() from inside the Intent handler instead of trying to call it from inside the route handler.
But this leads to another problem. The getPrice() function calls request(), which is an asynchronous call. Async calls are one of the biggest problems that causes an empty response. If you are using an async call, you must return a Promise. The easiest way to use a Promise with request() is to use the request-promise-native package instead.
So that block of code might look something (very roughly) like this:
const rp = require('request-promise-native');
function getPrice(){
return rp.get(url)
.then( body => {
// In this case, the body is the value we want, so we'll just return it.
// But normally we have to get some part of the body returned
return body;
});
}
assistant.intent(PRICE_ACTION, conv => {
return getPrice()
.then( price => {
let msg = `The price is ${price}`;
conv.close( new SimpleResponse({
speech: msg,
displayText: msg
});
});
});
The important thing to note about both getPrice() and the intent handler are that they both return a Promise.
Finally, there are some odd aspects in your code. Lines such as res.status(500).send('Oppss... could not check the price'); probably won't do what you think they will do. It won't, for example, send a message to be spoken. Instead, the Assistant will just close the connection and say that something went wrong.
Many thanks to #Prisoner. Below is the V2 working solution based on above comments. Same has been verified on nodejs webhook (without firebase). V1 version of the code was referenced from https://glitch.com/~aog-template-1
Happy coding..!!
// init project pkgs
const express = require('express');
const rp = require('request-promise-native');
const {
dialogflow,
Image,
SimpleResponse,
} = require('actions-on-google')
const bodyParser = require('body-parser');
const request = require('request');
const app = express().use(bodyParser.json());
// Instantiate a new API.AI assistant object.
const assistant = dialogflow();
// Handle webhook requests
app.post('/', function(req, res, next) {
console.log("Received POST request..!!");
console.log('======Req HEADERS============================================');
console.log('Request headers: ', req.headers);
console.log('======Req BODY===============================================');
console.log('Request body: ', req.body);
console.log('======Req END================================================');
assistant(req, res);
});
// Declare constants for your action and parameter names
const PRICE_ACTION = 'revenue'; // The action name from the API.AI intent
var price = 0.0
// Invoke http request to obtain blockchain price
function getPrice(){
console.log('getPrice is invoked');
var url = 'https://blockchain.info/q/24hrprice';
return rp.get(url)
.then( body => {
// In this case, the body is the value we want, so we'll just return it.
// But normally we have to get some part of the body returned
console.log('The demo price is ' + body);
return body;
});
}
// Handle AoG assistant intent
assistant.intent(PRICE_ACTION, conv => {
console.log('intent is triggered');
return getPrice()
.then(price => {
let msg = 'The demo price is ' + price;
conv.close( new SimpleResponse({
speech: msg,
}));
});
});
// Listen for requests.
let server = app.listen(process.env.PORT || 3000, function () {
console.log('Your app is listening on ' + JSON.stringify(server.address()));
});