0

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..

ChrisGPT was on strike
  • 127,765
  • 105
  • 273
  • 257

1 Answers1

0

I finally found the solution here : Using Socket.io and Redis on Heroku with Node.js

I need to use an adapter as when there multiple Dynos Heroku doesn't always use the same one, which makes sens.