Why does the websocket close and open another one? - reactjs

I'm having a problem with the ws . In principle I should only have one ws running in the application. But in certain situations (which I don't understand) two things happen:
More than one ws are started , which have no messages ( only header ) and the last one is the one that connects / subscribes.
The ws starts normally ( only 1 ) but suddenly ( without any clear trigger ) unsubscribes from everything and opens another ws . Which by the logic of my application generates me problems.
Here is the code, this would be the one that creates the Ws with rxjs :
import { Observable } from "rxjs";
/**
* Create a stomp connection to a given url
*
* #param url base url to connect.
* #param debug (Optional) flag to set debug traces
*/
export function createStompConnection(url: string, debug = false) {
// create the main connection and return and observable
const observerStomp: Observable<{ connected: boolean }> = Observable.create(
(observer: Observer<any>) => {
// define callbacks for the stomp connection
const _connectCallback = () => {
observer.next({ connected: true });
};
const _errorCallback = () => {
observer.next({ connected: false });
};
const _closeEventCallback = () => {
observer.next({ connected: false });
};
connectCallback.setCallback(_connectCallback);
errorCallback.setCallback(_errorCallback);
closeEventCallback.setCallback(_closeEventCallback);
// connect to client
connect();
// set reconnect delay
client.reconnect_delay = 1000;
// return disconnection handle, should be client.disconnect()
return () => console.warn("client disconnected");
}
);
and here is where you call and pass the subscribers ( in the redux store ) .
const stomp = createStompConnection(proxyWs);
I am not very clear about the management that makes the ws in the front end, so ask me and I will post any part of code that is necessary and I have missed.
Any idea why this behaviour is the expected one? Thx !

Related

React SocketContext: 2 Components using 1 Websocket

MY PROBLEM
I have a server which receives streaming data over UDP and rebroadcasts the data over websockets. In my front-end I have 2 different components which pick up the data and render the values. Whenever my server broadcast the object, key A is picked up by component 1. And key B should be picked up by component B.
For debugging purposes, the code below works smooth if I were to use a single component, but as soon as I use both only 1 is able to fetch and update data correctly while the other does nothing. What am I doing wrong?
MY SERVER (Just the important stuff)
# Dict to hold values
var data = {"score" : 0, "session" : 0,}
# handle different incoming UDP packets
client.on(PACKETS.session, handle_session);
client.on(PACKETS.score, handle_score);
#update data.session object with new values
function handle_session(data) {
data['session'] = data.session
websocket.broadcast(data);
}
#update data.score object with new values
function handle_score(data) {
data['score'] = data.score
websocket.broadcast(data);
}
# Broadcast data function gets called in handle functions.
websocket.broadcast = function broadcast(data) {
websocket.clients.forEach(function each(client) {
console.log(data);
client.send(data);
});
};
MY REACT APP
CONTEXT.JS - Create my socket
export const socket = new WebSocket("ws://localhost:8000")
export const SocketContext = React.createContext();
APP.JS - I wrap both my components in SocketContext
function App() {
return (
<div className="App">
<SocketContext.Provider value={socket}>
< Score />
< Session />
</SocketContext.Provider>
</div>
SCORE.JS - Component number 1 loads the socket from context.
function Score() {
const [score, setScore] = useState(0)
const websocket = useContext(SocketContext);
useEffect(() => {
websocket.onmessage = function(e) {
var packet = JSON.parse(e.data);
setScore(packet.score)
}
}, [websocket,]);
return (
<h6>{score}</>
);
}
export default Score;
SESSION.JS - Component number 2 loads the same socket from context.
function Session() {
const [session, setSession] = useState(0)
const websocket = useContext(SocketContext);
useEffect(() => {
websocket.onmessage = function(e) {
var packet = JSON.parse(e.data);
setSession(packet.session)
}
}, [websocket,]);
return (
<h6>{session}</>
);
}
export default Session;
So when running the frontend and adding console logging in useEffect for both components, Only one runs at the time. Then when reloading the app. Another one runs. They never run both at the same time.
Do you have any specific error messages in the console?
This bit on the server-side seems problematic:
data['score'] = {data.score}
It seems that should be:
data['score'] = data.score;
Edit*
Also another issue I see is variable scope overwriting:
You set:
var data = {"score" : 0, "session" : 0,}
But later both your functions use data in their local scope which is not good and would cause issues:
function handle_session(data) {
Should probably be:
function handle_session(d) {
Also this:
websocket.broadcast = function broadcast(data) {
Should probably be:
websocket.broadcast = function broadcast(d) {
These functions can share the same inner scope variable, d because they are siblings.
***EDIT, UPDATED ANSWER
The issue is with the use of websocket.onmessage. Since websocket is a single instance shared between components, once the listener is set on the onmessage property inside App.js, subsequent children components will be ignored when they try to set the same listener on the same property on the same websocket instance.
The key is to use addListener instead!
https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event
I created a simple working React <> Express <> Websocket App example of your situation, resolved by using addListener. I reproduced your exact problem.
Now all 3 component's useEffects will properly listen as you expected.
Open a terminal and run: npm run dev
https://codesandbox.io/p/sandbox/bitter-mountain-q3x118
Hope that helps!

SvelteKit form get request

I am trying to set up a simple endpoint in SvelteKit that reads the input given in a form and returns the result of an SQL query. In my first attempt I used the form actions and wrote the following code in the +page.server.js file:
export const actions = {
default: async (event) => {
let form_input = await event.request.formData();
let query = {
text: "select * from ux_return_shipments($1, $2, $3, $4)",
values: [form_input.get('beg-dep-date') || null, form_input.get('end-dep-date') || null, form_input.get('beg-arr-date') || null, form_input.get('end-arr-date') || null
}
try {
const result = await event.locals.pool.query(query);
return result.rows;
} catch (e) {
console.log(e);
}
}
};
I am now trying to set up the same process using a GET request instead of a POST one but I am having difficulties setting up the endpoint. I tried to replace the above code with this template but it looks like the endpoint is not getting activated since I see no activity server side:
export function GET({ url }) {
console.log(url);
return new Response("Test response");
};
What am I doing wrong? I see that using this code for an API endpoint (+server.js file) works correctly. I also checked the form element and the URL looks correct.
In case someone has the same problem I managed to solve it using this template:
export const load = async (event) => {
return ...
};
Using the load function I was able to set up a get endpoint and pass some data to the frontend using a return.

Subscribing to the socket uses the state of the variable as empty. After Updating the state it's not using the state after updating

I'm trying to connect to a socket endpoint that sends a JSON object every time either of the users sends messages back and forth.
I'm connecting to the socket and subscribing to a topic. Whenever the message is received it needs to add the object to the state. Since I'm using useEffect to connect the socket, it is called first and the socket gets subscribed to the topic with the empty state. So whenever a message is received, it is getting added to the empty array resulting in only one object.
Here's my code,
The socket code
const [chatData,setChatData] = useState([]);
function connect() {
var socket = new SockJS(""); //I've connected to my backend socket
console.log(chatData);
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
isConnected = true;
console.log("Connected: " + frame);
stompClient.subscribe(
"/topic/oneToOneChat/" + user.id,
function (message) {
const m = JSON.stringify(message.body);
const socketMessage = JSON.parse(JSON.parse(m));
chatData.unshift(socketMessage); //Adding the object to the top of the array
setChatData([...chatData]); //This is adding the object to the empty array
}
);
stompClient.subscribe("/topic/oneToOneChat/" + id, function (message) {
const m = JSON.stringify(message.body);
const socketMessage = JSON.parse(JSON.parse(m));
chatData.unshift(socketMessage);
setChatData([...chatData]);
});
stompClient.subscribe(
"/topic/deleteOneToOneChat/" + id,
function (message) {
const m = JSON.stringify(message.body);
const socketMessage = JSON.parse(JSON.parse(m));
deleteMessageFromChatData(socketMessage.id);
}
);
stompClient.subscribe(
"/topic/deleteOneToOneChat/" + user.id,
function (message) {
const m = JSON.stringify(message.body);
const socketMessage = JSON.parse(JSON.parse(m));
deleteMessageFromChatData(socketMessage.id);
}
);
});
}
useEffect(() => {
connect();
}, [chatData]);
chatData is my state where I update my array of objects. After updating all the chats are showing on my screen. But when I send or receive the message, the socket is adding the message object to the chatData and removing all the previous messages. Since it is appending to the empty chatData.
I've solved this by adding the below lines inside the socket subscription
setChatData((chats) => [socketMessage,...chats])

Invalid value for transfer with ipcRenderer.postMessage in Electron

I'm getting this error message : Invalid value for transfer when trying to use, for the very first time, the message-ports-reply-streams.
In preload.js I defined this api:
contextBridge.exposeInMainWorld(
"api", {
electronIpcPostMessage: (channel: string, message: any, transfer?: MessagePort[]) => {
ipcRenderer.postMessage(channel, message, transfer)
},
}
)
declare global {
interface Window {
api: {
electronIpcPostMessage: (channel: string, message: any, transfer?: MessagePort[]) => void;
}
}
And , following the example found here: https://www.electronjs.org/docs/latest/tutorial/message-ports#reply-streams , in the renderer React component I defined the streaming request as follows:
const Layers = () => {
const componentIsMounted = React.useRef(true)
React.useEffect(() => {
const cb = (event, args) => {
try {
if (componentIsMounted.current) {
console.log("react-leaflet-layers-args: ", args)
}
} catch (err) {
console.log("err: ", err)
}
}
const makeStreamingRequest = (element, cb) => {
// MessageChannels are lightweight--it's cheap to create a new one for each request.
const { port1, port2 } = new MessageChannel()
// We send one end of the port to the main process ...
window.api.electronIpcPostMessage(
'give-me-a-stream',
{ element, count: 10 },
[port2]
)
// ... and we hang on to the other end.
// The main process will send messages to its end of the port,
// and close it when it's finished.
port1.onmessage = (event) => {
cb(event.data)
}
port1.onclose = () => {
console.log('stream ended')
}
}
makeStreamingRequest(42, (data) => {
console.log('got response data:', event.data)
})
// We will see "got response data: 42" 10 times.
return () => { // clean-up function
componentIsMounted.current = false
window.api.electronIpcRemoveListener(
"give-me-a-stream",
cb,
)
}
}, [])
As said, when running Electron-React app the error message I get when accessing the page rendered by that component, is : Invalid value for transfer .
From this StackOverflow question : Invalid value for transfer while using ipcRenderer.postMessage of electron, it seems that I'm not the only one stumbling on this type of error, but I didn't find any solutions yet.
What am I doing wrongly or missing? How to solve the problem?
My objective is to send, better in a streaming fashion, a very big geojson file from the main process to the renderer process. That's why I thought to try to use ipcRenderer.postMessage.
By the way, any other working solutions that accomplish this goal, are welcomed.
Other info:
electron: v. 16
node: v. 16.13.0
O.S: Ubuntu 20.04
Looking forward to hints and help
I also encountered the same problem. In https://www.electronjs.org/docs/latest/api/context-bridge, it is mentioned that the types of parameters, errors and return values in functions bound with contextBridge are restricted, and MessagePort is one of the types that cannot be transported, so it doesn't recognize the MessagePort you passed in and throw this error.
If you want to use MessageChannel for communication, you can provide some proxy functions through contextBridge in preload.js, call these functions in renderer.js and pass in copyable parameters.
Hope my answer helps you.

Relay Modern: Connecting websocket to network layer

I’m having issues figuring out how to connect the Relay Modern network layer with my websocket instance.
I’m currently instantiating a websocket instance as:
const subscriptionWebSocket = new ReconnectingWebSocket('ws://url.url/ws/subscriptions/', null, options);
I'm specifying the subscription and creating a new instance of requestSubscription:
const subscription = graphql`
subscription mainSubscription {
testData {
anotherNode {
data
}
}
}
`;
requestSubscription(
environment,
{
subscription,
variables: {},
onComplete: () => {...},
onError: (error) => {...},
onNext: (response) => {...},
updater: (updaterStoreConfig) => {...},
},
);
Which then allows me to send any subscription requests:
function subscriptionHandler(subscriptionConfig, variables, cacheConfig, observer) {
subscriptionWebSocket.send(JSON.stringify(subscriptionConfig.text));
return {
dispose: () => {
console.log('subscriptionHandler: Disposing subscription');
},
};
}
const network = Network.create(fetchQuery, subscriptionHandler);
through to my server (currently using Graphene-python), and I’m able to interpret the received message on the server.
However, what I’m having issues figuring out is how to respond to a subscription; for example, when something changes in my DB, I want to generate a response and return to any potential subscribers.
The question being, how do I connect the onMessage event from my websocket instance into my Relay Modern Network Layer? I've browsed through the source for relay but can't seem to figure out what callback, or what method should be implementing an onreceive.
Any tips are appreciated.
I've managed to make subscriptions with Relay Modern work as well and wanted to share my minimal setup, maybe it's helpful for someone!
Note that I'm not using WebSocket but the SubscriptionClient that can be found in subscriptions-transport-ws to manage the connection to the server.
Here's my minimal setup code:
Environment.js
import { SubscriptionClient } from 'subscriptions-transport-ws'
const {
Environment,
Network,
RecordSource,
Store,
} = require('relay-runtime')
const store = new Store(new RecordSource())
const fetchQuery = (operation, variables) => {
return fetch('https://api.graph.cool/relay/v1/__GRAPHCOOL_PROJECT_ID__', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: operation.text,
variables,
}),
}).then(response => {
return response.json()
})
}
const websocketURL = 'wss://subscriptions.graph.cool/v1/__GRAPHCOOL_PROJECT_ID__'
function setupSubscription(
config,
variables,
cacheConfig,
observer,
) {
const query = config.text
const subscriptionClient = new SubscriptionClient(websocketURL, {reconnect: true})
const id = subscriptionClient.subscribe({query, variables}, (error, result) => {
observer.onNext({data: result})
})
}
const network = Network.create(fetchQuery, setupSubscription)
const environment = new Environment({
network,
store,
})
export default environment
NewLinkSubscription.js
import {
graphql,
requestSubscription
} from 'react-relay'
import environment from '../Environment'
const newLinkSubscription = graphql`
subscription NewLinkSubscription {
Link {
mutation
node {
id
description
url
createdAt
postedBy {
id
name
}
}
}
}
`
export default (onNext, onError, onCompleted, updater) => {
const subscriptionConfig = {
subscription: newLinkSubscription,
variables: {},
onError,
onNext,
onCompleted,
updater
}
requestSubscription(
environment,
subscriptionConfig
)
}
Now you can simply use the exported function to subscribe. For example, in one of my React components in componentDidMount I can now do the following:
componentDidMount() {
NewLinkSubscription(
response => console.log(`Received data: `, response),
error => console.log(`An error occurred:`, error),
() => console.log(`Completed`)
)
}
Note that the SubscriptionClient can only be used if your server implements this protocol!
If you want to learn more, check out the fullstack How to GraphQL tutorial that describes in detail how to make subscriptions work with Relay Modern.
I’ll just write down how I’ve approached this issue after the assistance found in this thread. It might be usable for someone else. This is very dependent on the server-side solution that you've chosen.
My approach:
Firstly I built a SubscriptionHandler that will handle the requestStream#subscribeFunction through SubscriptionHandler#setupSubscription.
The SubscriptionHandler instantiates a WebSocket (using a custom version of ReconnectingWebSockets) and attaches the onmessage event to an internal method (SubscriptionHandler#receiveSubscriptionPayload) which will add the payload to the corresponding request.
We create new subscriptions through SubscriptionHandler#newSubscription which will use the internal attribute SubscriptionHandler.subscriptions to add a keyed entry of this subscription (we use an MD5-hash util over the query and variables); meaning the object will come out as:
SubscriptionHandler.subscriptions = {
[md5hash]: {
query: QueryObject,
variables: SubscriptionVariables,
observer: Observer (contains OnNext method)
}
Whenever the server sends a subscription response the SubscriptionHandler#receiveSubscriptionPayload method will be called and it will identify what subscription the payload belongs to by using the query/variables md5 hash, then use the SubscriptionHandler.subscriptions observer onNext method.
This approach requires the server to return a message such as:
export type ServerResponseMessageParsed = {
payload: QueryPayload,
request: {
query: string,
variables: Object,
}
}
I do not know if this is a great way of handling subscriptions, but it works for now with my current setup.
SubscriptionHandler.js
class SubscriptionHandler {
subscriptions: Object;
subscriptionEnvironment: RelayModernEnvironment;
websocket: Object;
/**
* The SubscriptionHandler constructor. Will setup a new websocket and bind
* it to internal method to handle receving messages from the ws server.
*
* #param {string} websocketUrl - The WebSocket URL to listen to.
* #param {Object} webSocketSettings - The options object.
* See ReconnectingWebSocket.
*/
constructor(websocketUrl: string, webSocketSettings: WebSocketSettings) {
// All subscription hashes and objects will be stored in the
// this.subscriptions attribute on the subscription handler.
this.subscriptions = {};
// Store the given environment internally to be reused when registering new
// subscriptions. This is required as per the requestRelaySubscription spec
// (method requestSubscription).
this.subscriptionEnvironment = null;
// Create a new WebSocket instance to be able to receive messages on the
// given URL. Always opt for default protocol for the RWS, second arg.
this.websocket = new ReconnectingWebSocket(
websocketUrl,
null, // Protocol.
webSocketSettings,
);
// Bind an internal method to handle incoming messages from the websocket.
this.websocket.onmessage = this.receiveSubscriptionPayload;
}
/**
* Method to attach the Relay Environment to the subscription handler.
* This is required as the Network needs to be instantiated with the
* SubscriptionHandler's methods, and the Environment needs the Network Layer.
*
* #param {Object} environment - The apps environment.
*/
attachEnvironment = (environment: RelayModernEnvironment) => {
this.subscriptionEnvironment = environment;
}
/**
* Generates a hash from a given query and variable pair. The method
* used is a recreatable MD5 hash, which is used as a 'key' for the given
* subscription. Using the MD5 hash we can identify what subscription is valid
* based on the query/variable given from the server.
*
* #param {string} query - A string representation of the subscription.
* #param {Object} variables - An object containing all variables used
* in the query.
* #return {string} The MD5 hash of the query and variables.
*/
getHash = (query: string, variables: HashVariables) => {
const queryString = query.replace(/\s+/gm, '');
const variablesString = JSON.stringify(variables);
const hash = md5(queryString + variablesString).toString();
return hash;
}
/**
* Method to be bound to the class websocket instance. The method will be
* called each time the WebSocket receives a message on the subscribed URL
* (see this.websocket options).
*
* #param {string} message - The message received from the websocket.
*/
receiveSubscriptionPayload = (message: ServerResponseMessage) => {
const response: ServerResponseMessageParsed = JSON.parse(message.data);
const { query, variables } = response.request;
const hash = this.getHash(query, variables);
// Fetch the subscription instance from the subscription handlers stored
// subscriptions.
const subscription = this.subscriptions[hash];
if (subscription) {
// Execute the onNext method with the received payload after validating
// that the received hash is currently stored. If a diff occurs, meaning
// no hash is stored for the received response, ignore the execution.
subscription.observer.onNext(response.payload);
} else {
console.warn(Received payload for unregistered hash: ${hash});
}
}
/**
* Method to generate new subscriptions that will be bound to the
* SubscriptionHandler's environment and will be stored internally in the
* instantiated handler object.
*
* #param {string} subscriptionQuery - The query to subscribe to. Needs to
* be a validated subscription type.
* #param {Object} variables - The variables for the passed query.
* #param {Object} configs - A subscription configuration. If
* override is required.
*/
newSubscription = (
subscriptionQuery: GraphQLTaggedNode,
variables: Variables,
configs: GraphQLSubscriptionConfig,
) => {
const config = configs || DEFAULT_CONFIG;
requestSubscription(
this.subscriptionEnvironment,
{
subscription: subscriptionQuery,
variables: {},
...config,
},
);
}
setupSubscription = (
config: ConcreteBatch,
variables: Variables,
cacheConfig: ?CacheConfig,
observer: Observer,
) => {
const query = config.text;
// Get the hash from the given subscriptionQuery and variables. Used to
// identify this specific subscription.
const hash = this.getHash(query, variables);
// Store the newly created subscription request internally to be re-used
// upon message receival or local data updates.
this.subscriptions[hash] = { query, variables };
const subscription = this.subscriptions[hash];
subscription.observer = observer;
// Temp fix to avoid WS Connection state.
setTimeout(() => {
this.websocket.send(JSON.stringify({ query, variables }));
}, 100);
}
}
const subscriptionHandler = new SubscriptionHandler(WS_URL, WS_OPTIONS);
export default subscriptionHandler;
For anyone stumbling across this recently, I did not have success with either the solutions above because of recent updates in the libraries involved. Yet they were a great source to start and I put up together a small example based on the official relay modern todo example, it is very minimalistic and uses helpers libraries from Apollo but works well with relay modern:
https://github.com/jeremy-colin/relay-examples-subscription
It includes both server and client
Hope it can help
I think this repo would fit your needs.
Helps you creating your subscriptions server-side

Resources