Graphql-ws: separate websocket connection is opened for each subscription - reactjs

I'm a newbie in websockets.
I use urql and graphql-ws (migrated from subscriptions-transport-ws) to get graphql subscriptions. The code is following:
export const useUrqlClient = () => {
const headers = useHeaders();
const client = useMemo(() => createUrqlClient(headers), [headers]);
return client;
};
const createUrqlClient = (headers: any = defaultHeaders) => {
return createClient({
url: GRAPHQL_ENDPOINT,
fetchOptions: {
headers
},
exchanges: [
...defaultExchanges,
subscriptionExchange({
forwardSubscription: (operation) => {
return {
subscribe: (sink) => ({
unsubscribe: wsClient(headers).subscribe(operation, sink)
})
}
}
})
]
});
};
const wsClient = (headers: any) => createWSClient({
url: WS_GRAPHQL_ENDPOINT,
connectionParams: () => ({
headers
})
});
const useHeaders = () => {
const [authHeader, setAuthHeader] = useState<object>({});
const { selectedToken } = useAuth();
useEffect(() => {
if (selectedToken) {
setAuthHeader(selectedToken ? { authorization: `Bearer ${selectedToken}` } : {});
}
}, [selectedToken]);
return {
...defaultHeaders,
...authHeader
};
};
Everything works fine BUT is it okay that separate websocket connection is opened for each subscription?
They are closed on leaving the page and another are created but is it expected?
Note: the same behaviour was on approach using subscriptions-transport-ws.

You're running the client in lazy mode which establishes WebSocket connections on-demand, this is the default.
Consider one of the 2 options:
Use the client in lazy = false mode which will establish a connection with the server on create and keep it alive
Use the client's lazyCloseTimeout option which will delay closing the connection in case of new subscriptions.

Related

Next js: Hydration failed with React Query and React Cookie

So i have narrowed the issue down to the fact that I am using the cookie.logged_in to enable the react query call. If I remove the enabled: !!cookies.logged_in and cookies.logged_in in the if condition, then the code works properly
. I want a scenario where the code goes directly to show the children if the cookies.logged_in is unavailable and only tries to show the loading when both the query. loading is working and cookies.logged_in is actually available
This is my code
const AuthMiddleware: React.FC<AuthMiddlewareProps> = ({ children }) => {
const [cookies] = useCookies(['logged_in']);
const { setCurrentUser } = useContext(Context);
const query = useQuery(['authUser'], () => getCurrentUserFn(), {
enabled: !!cookies.logged_in,
select: (data) => data,
onSuccess: (response) => {
setCurrentUser(response);
},
onError: () => {
setCurrentUser({} as AuthUser);
},
});
if (query.isLoading && cookies.logged_in) {
return <LoadingScreen />;
}
return children;
};

Connection leak with a Socket.IO component in React

I have a React application that uses Socket.IO. The Socket instance is in a React component.
I have been noticing that the action of logging out and logging into my application, which should unmount the component and close the connection and then remount and reopen the connection leads to a socket leak/creation of duplicate socket connection. I have also managed to get the application into a state where it quickly spews off new connections leading to starvation, but have not been able to replicate. This hit production once.
Here is the client code:
const Socket = React.memo(() => {
const [isLoadingSocket, setIsLoadingSocket] = useState<boolean>(false)
const socketRef = useRef<SocketIO<ServerToClientEvents, ClientToServerEvents> | null>(null)
const socketNeedsRestart = isFocused ? !isLoadingSocket : false
async function initializeSocket() {
const token = await getToken()
setIsLoadingSocket(true)
if (socketRef.current) {
socketRef.current.disconnect()
}
socketRef.current = io(`${SOCKET_HOST}`, {
secure: true,
reconnectionDelay: 5000,
transports: ['websocket', 'polling'],
path: ENVIRONMENT !== Environment.local ? '/api/socket.io/' : '',
auth: {
token,
},
})
console.log(`socket initialized`)
}
useEffect(() => {
if (socketNeedsRestart) {
initializeSocket()
}
}, [socketNeedsRestart]) //eslint-disable-line
useEffect(() => {
if (socketRef.current) {
socketRef.current.on(SocketLifecycleEvent.Connect, () => {
console.log('socket connected')
setIsLoadingSocket(false)
})
socketRef.current.on(SocketMessage.UsersOnline, (message) => {
updateOnlineUsers(message.onlineUserIDs)
})
}
return () => {
if (socketRef.current) {
socketRef.current.off(SocketLifecycleEvent.Connect)
socketRef.current.off(SocketLifecycleEvent.ConnectionError)
socketRef.current.off(SocketLifecycleEvent.Disconnect)
}
}
}, [isLoadingSocket])
useEffect(() => {
socketRef.current?.disconnect()
}, [])
return <></>
})
export default Socket
The component is used once in the page that a user gets to after login. I can provide server code but it doesn't do anything except notify all users every time someone connects. What's causing the connection leak? How can I re-create the rapid-fire leak?
Is your last useEffect doing what you expect it to do? It looks like this is supposed to be the cleanup on unmount but you are not returning a function there.
Did you try something like this?:
useEffect( () => () => socketRef.current?.disconnect(), [] );

React/Socket.io chat app not working on heroku

I have a chat app I made using React for the frontend, DRF for the backend and I also have a node.js server within the React app for socket.io The issue is that the chat doesn't work basically (it works fine locally). When a message is sent it's not emitted and only shows up when I refresh since it's then pulled from the DB instead. I have gone through many threads on here for this issue but can't figure out what I'm doing wrong.
My server:
const server = require("http").createServer();
const io = require("socket.io")(server, {
cors: {
origin: "*",
},
});
const PORT = process.env.PORT || 5000;
const NEW_CHAT_MESSAGE_EVENT = "newChatMessage";
io.on("connection", (socket) => {
console.log('Client connected')
// Join a conversation.
const {roomId} = socket.handshake.query;
socket.join(roomId);
// Listen for new messages
socket.on(NEW_CHAT_MESSAGE_EVENT, (data) => {
io.in(roomId).emit(NEW_CHAT_MESSAGE_EVENT, data);
});
// Leave the room if the user closes the socket
socket.on("disconnect", () => {
socket.leave(roomId);
});
});
server.listen(PORT, (error) => {
if (error) throw error;
console.log(`Listening on port ${PORT}`);
});
Hook I made for the frontend:
const NEW_CHAT_MESSAGE_EVENT = "newChatMessage"; // Name of the event
const SOCKET_SERVER_URL = `https://<my-react-frontend>.herokuapp.com`;
export const useChat = () => {
const socketRef = useRef();
const {messages, setMessages, activeConvo, headerConvo, reloadSideBar, setReloadSideBar} = useActiveConvo()
const roomId = activeConvo
useEffect(() => {
console.log('useChat useEffect ran')
// Creates a WebSocket connection
socketRef.current = socketIOClient(SOCKET_SERVER_URL, {
query: {roomId},
});
// Listens for incoming messages
socketRef.current.on(NEW_CHAT_MESSAGE_EVENT, (message) => {
const incomingMessage = {
message: message.body,
created_by: localStorage.getItem('currentUserID'),
};
console.log('messages set in useChat useFfect')
setMessages((messages) => [...messages, incomingMessage]);
});
// Destroys the socket reference
// when the connection is closed
return () => {
socketRef.current.disconnect();
};
}, [roomId]);
// Sends a message to the server that
// forwards it to all users in the same room
const sendMessage = (messageBody) => {
socketRef.current.emit(NEW_CHAT_MESSAGE_EVENT, {
body: messageBody,
senderId: socketRef.current.id,
});
const fetchContents = {
message: messageBody,
created_by: localStorage.getItem('currentUserID'),
convo_id: activeConvo ? activeConvo : headerConvo
}
fetch(`https://<my-drf-backend>.herokuapp.com/api/save-message/`, authRequestOptions(('POST'), fetchContents))
.then(response => response.json())
.then(setReloadSideBar(reloadSideBar + 1))
.catch(error => console.log(error))
};
return {messages, sendMessage};
};
The issue in most of the threads appeared to be either still using the localhost url on the frontend or not using process.env.PORT in the server but it's still busted after fixing that. I also saw someone mention in another thread that the folder structure was the issue so I tried having the server file in the root of the react app and having it in it's own folder under "src", no dice.
In case anyone faces this same issue, I solved it by putting the server in a separate app on heroku

Compare SSE local and Global versions when using eventSource and Server Sent Events

Am using server sent events in an express server like this;
const sendEventDashboard = async (req, res) => {
try {
const orders = await Order.find({ agent_id: req.params.id })
.populate("agent_id")
.sort({ _id: -1 });
res.writeHead(200, {
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
Connection: "keep-alive",
});
const sseId = new Date().toDateString();
const intervalId = setInterval(() => {
writeEvent(res, sseId, JSON.stringify(orders));
}, SEND_INTERVAL);
res.on("close", () => {
clearInterval(intervalId);
res.end();
// console.log("Client closed connection browser");
});
} catch (error) {
console.log(error);
}
};
export const getOrdersStreamDashboard = async (req, res) => {
if (req.headers.accept === "text/event-stream") {
sendEventDashboard(req, res);
} else {
res.json({ message: "Okay" });
}
};
and this is how i use it in a react app using a useEffect hook;
useEffect(() => {
const es = new EventSource(
`${process.env.REACT_APP_SERVER_URL}/weborders/${agentId}/stream_dashboard`
);
es.addEventListener("open", () => {
console.log("Dashboard stream opened!");
});
es.addEventListener("message", (e) => {
const data = JSON.parse(e.data);
setTrackOrderCount(data);
});
return () => {
// es.removeAllEventListeners();
es.close();
es.removeEventListener("message", (e) => {
const data = JSON.parse(e.data);
setTrackOrderCount(data);
});
};
}, [trackOrderCount]);
Everything runs as desired apart from event source always running until when the app/browser crushes. I get no error when it stops running and have to refresh for it to start again. This happens like after 10mins of inactivity or being on that same page for a long duration. Is there a way I can only run sse only when the state in the server is different from that of the client because i think the browser crushes because server sent events continuously run even when there's no event. I tried to remove the dependency array [trackOrderCount] in the useEffect and the setInterval in the server but that didn't solve the issue.
The solution might be in comparing the local and global versions before the event is sent but i've failed to figure out where to put that logic! I the browser's console, this is what i get;
and this will run for sometime then crush!

Setup Contentful Javascript SDK in React Native

I'm trying to implement Contentful Javascript SDK on a React Native project (without Expo).
This is the code:
const {createClient} = require('contentful/dist/contentful.browser.min.js')
useEffect(() => {
getContentfulData()
}, [])
const getContentfulData = async () => {
var client = createClient({
adapter: (config) => {
config.adapter = null
return fetch(config)
},
space: '---',
accessToken: '---',
})
await client
.getEntries()
.then((entries) => {
console.log(entries)
})
.catch((error) => {
console.log(error)
})
}
But I'm getting TypeError: Network request failed over and over again.
Any ideas?
const { createClient } = require('contentful/dist/contentful.browser.min.js')
const client = createClient({
space: '*********',
accessToken: '****************************************',
})
client
.getEntries({
content_type: 'trendingBlogs',
})
.then(entry => console.log(entry))
.catch(err => console.log(err))
your missing the getEntries parameters.
i.e
{
content_type: 'trendingBlogs',
}

Resources