I'm using NodeJs for my backend and NextJs for my frontend. The backend is on Heroku and the front on Vercel. I'm building an app and want to show a popup toast that displays a progress bar when an action is being processed by the server.
So for the past seven days I tried to achieve that with Socket.io. Everything works fine on localhost, but on Heroku, only the first Emit goes to the front.
On my backend I have :
Index.js ( io connection )
io.on("connection", async (socket) => {
try {
const tokenfull = socket.handshake.headers?.cookie?.split(';').filter(item => item.includes('access_token'));
const token = tokenfull? tokenfull[0].split('=')[1] : null
if (token){
jwt.verify(token, privateKey, (err, decoded) => {
if (err){
console.log(err)
} else {
console.log('sokettoken ' + token)
socket.join(decoded.id);
}
})
}
} catch(e){
console.log(e)
}
Index.js : When a user submit a new task, the server emit an event to tell the frontend that the task is "Queued". The server then queue the task with Bulljs.
The first even "status: Queued" always works.
app.post('/newtask', verifyJWT, async (req, res) => {
try {
try {
io.to(req.user.id).emit('enrichProgress', {percentage: 0, type: "list", itemid: newlist._id, itemname: newlist.name, status: "queued"});
/////// THIS ONE ALWAYS WORK EVEN ON HEROKU
}
catch(e){
console.log(e)
}
await EnrichQueue.add({ listid: req.body.listid, newlist: newlist, listname: req.body.listname, userid: req.user.id }, {removeOnComplete: true});
res.json({ message: "Success" });
} catch(e) {
console.log(e)
}
})
Then in my EnrichQueue.process the server emit progress ( 10%, 20% etc ... )
EnrichQueue.process(async (job, done) => {
console.log('EnrichQueue.process')
const userid = job.data.userid
try {
io.to(userid).emit('enrichProgress', {percentage: 0, type: "list", itemid: job.data.newlist._id, itemname: job.data.newlist.name, status: "processing"});
} catch(e) {
console.log(e)
}
for (let i=0 ; i< item.length; i++) {
await dosomething(i)
io.to(userid).emit('enrichProgress', {percentage: 10, type: "list", itemid: job.data.newlist._id, itemname: job.data.newlist.name, status: "processing"});
}
ETC... .... DO some other tasks
Emit from the Queue.Process almost never work on Heroku. It works fine when I restart the server but after a few attempt it just stops working and only the first emit ('status: queued' from the app.post/newtask ) is received by the frontend.
When I debug Socket.io on Heroku logs I can see that events are emitted with status 101 so I don't get what is going wrong.
If I have multiple devices connect to my account sometime one of them receives emits but they should all get it.
On my frontend I have as follow :
SocketContext.js
import React, { useState, useEffect, useCallback } from 'react'
import socketio from "socket.io-client";
import { baseurl } from "src/env";
export const socket = socketio.connect(baseurl, {
//forceNew: true,
transports: [ "websocket" ],
withCredentials: true
});
export const SocketContext = React.createContext();
Socket.js
import React, { useState, useEffect, useCallback, useContext } from 'react'
import toast from 'react-hot-toast'
import Box from '@mui/material/Box'
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import Typography from '@mui/material/Typography'
import IconButton from '@mui/material/IconButton'
import Close from 'mdi-material-ui/Close'
import { SocketContext }from '../context/SocketContext'
import { styled } from '@mui/material/styles';
import LinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress';
import ErrorIcon from '@mui/icons-material/Error';
const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({
height: 10,
borderRadius: 5,
[`&.${linearProgressClasses.colorPrimary}`]: {
backgroundColor: theme.palette.grey[theme.palette.mode === 'light' ? 200 : 800],
},
[`& .${linearProgressClasses.bar}`]: {
borderRadius: 5,
backgroundColor: theme.palette.mode === 'light' ? '#1a90ff' : '#308fe8',
},
}));
function Socket() {
const socket = useContext(SocketContext);
useEffect(() => {
socket.on('uploadProgress', (data) => {
console.log(data);
});
socket.on('enrichProgress', (data) => {
console.log(data);
if (data.status === 'processing' || data.status === 'queued') {
toast(
t => (
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/*<Avatar alt='Victor Anderson' src='/demo/materialize-mui-react-nextjs-admin-template/demo-1/images/avatars/1.png' sx={{ mr: 3, width: 40, height: 40 }} />*/}
<Box >
<Box>
<Typography sx={{ fontWeight: 500 }}>Enrichment in progress: {data.itemname}</Typography>
</Box>
<Box sx={{marginBottom: '10px'}}>
<div>
<Typography variant='caption'>Status: {data.status}</Typography>
</div>
<div >
<Typography variant='caption'>{(data.percentage*100).toFixed(0)}% Done</Typography>
</div>
</Box>
<BorderLinearProgress variant="determinate" value={data.percentage*100} />
</Box>
</Box>
</Box>
),
{
id: data.itemid,
position: 'bottom-left',
duration:'1000000',
style: {
minWidth: '300px'
}
}
)
}
if (data.status === 'finished') {
toast(
t => (
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CheckCircleIcon sx={{ mr: 3, width: 40, height: 40, color:'#0ACF83' }}/>
{/*<Avatar alt='Victor Anderson' src='/demo/materialize-mui-react-nextjs-admin-template/demo-1/images/avatars/1.png' sx={{ mr: 3, width: 40, height: 40 }} />*/}
<Box >
<Box>
<Typography sx={{ fontWeight: 500 }}>All good ! </Typography>
<Typography variant='caption'>{data.itemname}</Typography>
</Box>
<Box sx={{marginBottom: '10px'}}>
<div>
<Typography variant='caption'>Status: {data.status}</Typography>
</div>
<div >
<Typography variant='caption'>{100}% Done</Typography>
</div>
</Box>
</Box>
</Box>
<IconButton onClick={() => toast.dismiss(t.id)}>
<Close fontSize='small' />
</IconButton>
</Box>
),
{
id: data.itemid,
position: 'bottom-left',
duration:'1000000',
style: {
minWidth: '350px'
}
}
)
}
});
socket.on('emailverifProgress', (data) => {
console.log(data);
if (data.status === 'processing' || data.status === 'queued') {
toast(
t => (
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/*<Avatar alt='Victor Anderson' src='/demo/materialize-mui-react-nextjs-admin-template/demo-1/images/avatars/1.png' sx={{ mr: 3, width: 40, height: 40 }} />*/}
<Box >
<Box>
<Typography sx={{ fontWeight: 500 }}>Email verification: {data.itemname}</Typography>
</Box>
<Box sx={{marginBottom: '10px'}}>
<div>
<Typography variant='caption'>Status: {data.status}</Typography>
</div>
<div >
<Typography variant='caption'>{(data.percentage*100).toFixed(0)}% Done</Typography>
</div>
</Box>
<BorderLinearProgress variant="determinate" value={data.percentage*100} />
</Box>
</Box>
</Box>
),
{
id: data.itemid,
position: 'bottom-left',
duration:'1000000',
style: {
minWidth: '300px'
}
}
)
}
if (data.status === 'finished') {
toast(
t => (
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CheckCircleIcon sx={{ mr: 3, width: 40, height: 40, color:'#0ACF83' }}/>
{/*<Avatar alt='Victor Anderson' src='/demo/materialize-mui-react-nextjs-admin-template/demo-1/images/avatars/1.png' sx={{ mr: 3, width: 40, height: 40 }} />*/}
<Box >
<Box>
<Typography sx={{ fontWeight: 500 }}>All good ! </Typography>
<Typography variant='caption'>{data.itemname}</Typography>
</Box>
<Box sx={{marginBottom: '10px'}}>
<div>
<Typography variant='caption'>Status: {data.status}</Typography>
</div>
<div >
<Typography variant='caption'>{100}% Done</Typography>
</div>
</Box>
</Box>
</Box>
<IconButton onClick={() => toast.dismiss(t.id)}>
<Close fontSize='small' />
</IconButton>
</Box>
),
{
id: data.itemid,
position: 'bottom-left',
duration:'1000000',
style: {
minWidth: '350px'
}
}
)
}
});
socket.on('exportProgress', (data) => {
console.log(data);
if (data.status === 'processing' || data.status === 'queued') {
toast(
t => (
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/*<Avatar alt='Victor Anderson' src='/demo/materialize-mui-react-nextjs-admin-template/demo-1/images/avatars/1.png' sx={{ mr: 3, width: 40, height: 40 }} />*/}
<Box >
<Box>
<Typography sx={{ fontWeight: 500 }}>Linkedin Export in progress: {data.itemname}</Typography>
</Box>
<Box sx={{marginBottom: '10px'}}>
<div>
<Typography variant='caption'>Status: {data.status}</Typography>
</div>
<div >
<Typography variant='caption'>{(data.percentage*100).toFixed(0)}% Done</Typography>
</div>
</Box>
<BorderLinearProgress variant="determinate" value={data.percentage*100} />
</Box>
</Box>
</Box>
),
{
id: data.itemid,
position: 'bottom-left',
duration:'1000000',
style: {
minWidth: '300px'
}
}
)
}
if (data.status === 'finished') {
toast(
t => (
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CheckCircleIcon sx={{ mr: 3, width: 40, height: 40, color:'#0ACF83' }}/>
{/*<Avatar alt='Victor Anderson' src='/demo/materialize-mui-react-nextjs-admin-template/demo-1/images/avatars/1.png' sx={{ mr: 3, width: 40, height: 40 }} />*/}
<Box >
<Box>
<Typography sx={{ fontWeight: 500 }}>All good ! </Typography>
<Typography variant='caption'>{data.itemname}</Typography>
</Box>
<Box sx={{marginBottom: '10px'}}>
<div>
<Typography variant='caption'>Status: {data.status}</Typography>
</div>
<div >
<Typography variant='caption'>{100}% Done</Typography>
</div>
</Box>
</Box>
</Box>
<IconButton onClick={() => toast.dismiss(t.id)}>
<Close fontSize='small' />
</IconButton>
</Box>
),
{
id: data.itemid,
position: 'bottom-left',
duration:'1000000',
style: {
minWidth: '350px'
}
}
)
}
});
socket.on('facebookexportProgress', (data) => {
console.log(data);
if (data.status === 'processing') {
toast(
t => (
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/*<Avatar alt='Victor Anderson' src='/demo/materialize-mui-react-nextjs-admin-template/demo-1/images/avatars/1.png' sx={{ mr: 3, width: 40, height: 40 }} />*/}
<Box >
<Box>
<Typography sx={{ fontWeight: 500 }}>Facebook Export: {data.itemname}</Typography>
</Box>
<Box sx={{marginBottom: '10px'}}>
<div>
<Typography variant='caption'>Status: {data.status}</Typography>
</div>
<div >
<Typography variant='caption'>{(data.percentage).toFixed(0)}% Done</Typography>
</div>
</Box>
<BorderLinearProgress variant="determinate" value={data.percentage} />
</Box>
</Box>
</Box>
),
{
id: data.itemid,
position: 'bottom-left',
duration:'1000000',
style: {
minWidth: '300px'
}
}
)
}
if (data.status === 'scrolling') {
toast(
t => (
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/*<Avatar alt='Victor Anderson' src='/demo/materialize-mui-react-nextjs-admin-template/demo-1/images/avatars/1.png' sx={{ mr: 3, width: 40, height: 40 }} />*/}
<Box >
<Box>
<Typography sx={{ fontWeight: 500 }}>Facebook Export: {data.itemname}</Typography>
</Box>
<Box sx={{marginBottom: '10px'}}>
<div>
<Typography variant='caption'>Status: {data.status}</Typography>
</div>
<div >
<Typography variant='caption'>{(data.percentage).toFixed(0)}% Done</Typography>
</div>
</Box>
<BorderLinearProgress variant="determinate" value={data.percentage} />
</Box>
</Box>
</Box>
),
{
id: data.itemid,
position: 'bottom-left',
duration:'1000000',
style: {
minWidth: '300px'
}
}
)
}
if (data.status === 'scraping members') {
toast(
t => (
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/*<Avatar alt='Victor Anderson' src='/demo/materialize-mui-react-nextjs-admin-template/demo-1/images/avatars/1.png' sx={{ mr: 3, width: 40, height: 40 }} />*/}
<Box >
<Box>
<Typography sx={{ fontWeight: 500 }}>Facebook Export: {data.itemname}</Typography>
</Box>
<Box sx={{marginBottom: '10px'}}>
<div>
<Typography variant='caption'>Status: {data.status}</Typography>
</div>
<div >
<Typography variant='caption'>{(data.percentage).toFixed(0)}% Done</Typography>
</div>
</Box>
<BorderLinearProgress variant="determinate" value={data.percentage} />
</Box>
</Box>
</Box>
),
{
id: data.itemid,
position: 'bottom-left',
duration:'1000000',
style: {
minWidth: '300px'
}
}
)
}
if (data.status === 'finished') {
toast(
t => (
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CheckCircleIcon sx={{ mr: 3, width: 40, height: 40, color:'#0ACF83' }}/>
{/*<Avatar alt='Victor Anderson' src='/demo/materialize-mui-react-nextjs-admin-template/demo-1/images/avatars/1.png' sx={{ mr: 3, width: 40, height: 40 }} />*/}
<Box >
<Box>
<Typography sx={{ fontWeight: 500 }}>All good ! </Typography>
<Typography variant='caption'>{data.itemname}</Typography>
</Box>
<Box sx={{marginBottom: '10px'}}>
<div>
<Typography variant='caption'>Status: {data.status}</Typography>
</div>
<div >
<Typography variant='caption'>{100}% Done</Typography>
</div>
</Box>
</Box>
</Box>
<IconButton onClick={() => toast.dismiss(t.id)}>
<Close fontSize='small' />
</IconButton>
</Box>
),
{
id: data.itemid,
position: 'bottom-left',
duration:'1000000',
style: {
minWidth: '350px'
}
}
)
}
if (data.status === 'error') {
toast(
t => (
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<ErrorIcon sx={{ mr: 3, width: 40, height: 40, color:'#666CFF' }}/>
{/*<Avatar alt='Victor Anderson' src='/demo/materialize-mui-react-nextjs-admin-template/demo-1/images/avatars/1.png' sx={{ mr: 3, width: 40, height: 40 }} />*/}
<Box >
<Box>
<Typography sx={{ fontWeight: 500 }}>Something went wrong !</Typography>
<Typography variant='caption'>{data.itemname}</Typography>
</Box>
<Box sx={{marginBottom: '10px'}}>
<div>
<Typography variant='caption'>Status: {data.errormessage}</Typography>
</div>
<div >
<Typography variant='caption'>{100}% Done</Typography>
</div>
</Box>
</Box>
</Box>
<IconButton onClick={() => toast.dismiss(t.id)}>
<Close fontSize='small' />
</IconButton>
</Box>
),
{
id: data.itemid,
position: 'bottom-left',
duration:'1000000',
style: {
minWidth: '350px'
}
}
)
}
});
return () => {
socket.off('uploadProgress');
socket.off('enrichProgress');
socket.off('emailverifProgress');
socket.off('exportProgress');
socket.off('facebookexportProgress');
};
}, []);
return (
null
)
}
export default Socket
And on _app.js
const App = props => {
const [showChild, setShowChild] = useState(false);
useEffect(() => {
setShowChild(true);
}, []);
if (!showChild) {
return null;
}
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props
// Variables
const getLayout = Component.getLayout ?? (page => <UserLayout>{page}</UserLayout>)
const setConfig = Component.setConfig ?? undefined
return (
<Provider store={store}>
<CacheProvider value={emotionCache}>
<AuthProvider>
<SocketContext.Provider value={socket}>
<SettingsProvider {...(setConfig ? { pageSettings: setConfig() } : {})}>
<SettingsConsumer>
{({ settings }) => {
return (
<ThemeComponent settings={settings}>
<WindowWrapper>
<Socket/>
{getLayout(<Component {...pageProps} />)}
</WindowWrapper>
<ReactHotToast>
<Toaster position={settings.toastPosition} toastOptions={{ className: 'react-hot-toast' }} />
</ReactHotToast>
</ThemeComponent>
)
}}
</SettingsConsumer>
</SettingsProvider>
</SocketContext.Provider>
</AuthProvider>
</CacheProvider>
</Provider>
)
}
export default App
If someone finds a solution I would love to hear it as I absolutely can't find any clue.. Thanks for taking the time to read !
I tried to to remove change to transport: web socket only, and several option in the front such as forceNew: true but it didn't change anything..