I'm using Flux and WebSocket in my Reactjs application and during implementation I've encountered some problems.
Questions:
Assuming I have a set of a set of actioncreators and a store for managing the WebSocket connection, and that the connection is started in a actioncreator (open(token)), where should I put my conn.emit's and how do I get other actions access to my connection object so that they can send data to the backend?
Do I have to pass it as an argument to the actions that are called in the views (eg. TodoActions.create(conn, todo)) or is there a smarter way?
Current code is here
I'm using ES6 classes.
If I have omitted anything necessary in the gist, please let me know.
EDIT:
This is what I have concocted so far based on glortho's answer:
import { WS_URL } from "./constants/ws";
import WSActions from "./actions/ws";
class WSClient {
constructor() {
this.conn = null;
}
open(token) {
this.conn = new WebSocket(WS_URL + "?access_token=" + token);
this.conn.onopen = WSActions.onOpen;
this.conn.onclose = WSActions.onClose;
this.conn.onerror = WSActions.onError;
this.conn.addEventListener("action", (payload) => {
WSActions.onAction(payload);
});
}
close() {
this.conn.close();
}
send(msg) {
return this.conn.send(msg);
}
}
export default new WSClient();
You should have a singleton module (not a store or an action creator) that handles opening the socket and directing traffic through. Then any action creator that needs to send/receive data via the socket just requires the module and makes use of its generic methods.
Here's a quick and dirty untested example (assuming you're using CommonJS):
SocketUtils.js:
var SocketActions = require('../actions/SocketActions.js');
var socket = new WebSocket(...);
// your stores will be listening for these dispatches
socket.onmessage = SocketActions.onMessage;
socket.onerror = SocketActions.onError;
module.exports = {
send: function(msg) {
return socket.send(msg);
}
};
MyActionCreator.js
var SocketUtils = require('../lib/SocketUtils.js');
var MyActionCreator = {
onSendStuff: function(msg) {
SocketUtils.send(msg);
// maybe dispatch something here, though the incoming data dispatch will come via SocketActions.onMessage
}
};
Of course, in reality you'll be doing better and different things, but this gives you a sense of how you might structure it.
Related
Before you can create objects with user properties on frontend and assign it to the socket.user property for each connection using a code like this ex below in the backend.
socket.on("new_visitor", user => {
console.log("new_visitor", user);
socket.user = user;
emitVisitors();
});
then retrieve all these data through the sockets object eg.
const getVisitors = () => {
let clients = io.sockets.clients().connected;
let sockets = Object.values(clients);
let users = sockets.map(s => s.user);
return users;
};
//frontend
componentWillMount() {
axios.get('http://geoplugin.net/json.gp').then(res => {
const {
geoplugin_request,
geoplugin_countryCode,
geoplugin_city,
geoplugin_region,
geoplugin_countryName
} = res.data;
const visitor = {
ip: geoplugin_request,
countrycode: geoplugin_countryCode,
city: geoplugin_city,
state: geoplugin_region,
country: geoplugin_countryName
}
socket.emit("new_visitor", visitor);
socket.on("visitors", visitors => {
this.setState({
visitors: visitors
})
})
});
}
But now the io.sockets.clients is not working anymore and is not recognized as a function.Every API provided seem t return only the Id. For anyone who knows a workaround on this please let us know. Thanks a lot.
Problem : How to hold custom data for each socket (serverside)
For each socket that connects to your socket-io server you want to be able to store some custom data in reference to said socket, so at a later point, other sockets can retrieve this information.
Solution : Add a simple in-memory-store (serverside)
I strongly advise to not add anything or mutating the socket object. Instead use the socket id to maintain a simple in-memory store for all connected sockets.
🚧 Please note: the following snippets are just pointers and are not meant to just be copy pasted. Instead, try to use them to understand your problem and adjust them to your needs.
Server Side
const store = {};
io.on('connection', function (socket) {
// add socket to store, like this
// note 'data' is null at this point, yet needs to be set
store[socket.id] = {
socket : socket,
data : null
}
socket.on('SET_CLIENT_DATA', function (clientdata) {
// here we receive data from frontend, and add it to the serverside reference
store[socket.id].data = clientdata;
// once a socket updates his custom client data
// emit all custom data to all clients
io.emit('ALL_CONNECTED_CLIENTS', Object.values(store).map(e => e.data));
});
socket.on('disconnect', function () {
// if socket disconnects, make sure to remove the reference in your store
delete store[socket.id];
});
});
Client Side
socket.emit("SET_CLIENT_DATA", clientdata);
socket.on("ALL_CONNECTED_CLIENTS", (allclients) => {
/* here the client receives all custom client data that we kept serverside for each connected client */
/* do some more code here */
});
I have a small web application listening for incoming messages from a Websocket server. I receive them like so
const webSocket = new WebSocket("wss://echo.websocket.org");
webSocket.onopen = event => webSocket.send("test");
webSocket.onmessage = event => console.log(event.data);
but the sending server is more complex. There are multiple types of messages that could come e.g. "UserConnected", "TaskDeleted", "ChannelMoved"
How to detect which type of message was sent? For now I modified the code to
const webSocket = new WebSocket("wss://echo.websocket.org");
webSocket.onopen = event => {
const objectToSend = JSON.stringify({
message: "test-message",
data: "test"
});
webSocket.send(objectToSend);
};
webSocket.onmessage = event => {
const objectToRead = JSON.parse(event.data);
if (objectToRead.message === "test-message") {
console.log(objectToRead.data);
}
};
So do I have to send an object from the server containing the "method name" / "message type" e.g. "TaskDeleted" to identify the correct method to execute at the client? That would result in a big switch case statement, no?
Are there any better ways?
You can avoid the big switch-case statement by mapping the methods directly:
// List of white-listed methods to avoid any funny business
let allowedMethods = ["test", "taskDeleted"];
function methodHandlers(){
this.test = function(data)
{
console.log('test was called', data);
}
this.taskDeleted = function(data)
{
console.log('taskDeleted was called', data);
}
}
webSocket.onmessage = event => {
const objectToRead = JSON.parse(event.data);
let methodName = objectToRead.message;
if (allowerMethods.indexOf(methodName)>=0)
{
let handler = new methodHandlers();
handler[methodName](data);
}
else
{
console.error("Method not allowed: ", methodName)
}
};
As you have requested in one of your comments to have a fluent interface for the websockets like socket.io.
You can make it fluent by using a simple PubSub (Publish Subscribe) design pattern so you can subscribe to specific message types. Node offers the EventEmitter class so you can inherit the on and emit events, however, in this example is a quick mockup using a similar API.
In a production environment I would suggest using the native EventEmitter in a node.js environment, and a browser compatible npm package in the front end.
Check the comments for a description of each piece.
The subscribers are saved in a simple object with a Set of callbacks, you can add unsubscribe if you need it.
note: if you are using node.js you can just extend EventEmitter
// This uses a similar API to node's EventEmitter, you could get it from a node or a number of browser compatible npm packages.
class EventEmitter {
// { [event: string]: Set<(data: any) => void> }
__subscribers = {}
// subscribe to specific message types
on(type, cb) {
if (!this.__subscribers[type]) {
this.__subscribers[type] = new Set
}
this.__subscribers[type].add(cb)
}
// emit a subscribed callback
emit(type, data) {
if (typeof this.__subscribers[type] !== 'undefined') {
const callbacks = [...this.__subscribers[type]]
callbacks.forEach(cb => cb(data))
}
}
}
class SocketYO extends EventEmitter {
constructor({ host }) {
super()
// initialize the socket
this.webSocket = new WebSocket(host);
this.webSocket.onopen = () => {
this.connected = true
this.emit('connect', this)
}
this.webSocket.onerror = console.error.bind(console, 'SockyError')
this.webSocket.onmessage = this.__onmessage
}
// send a json message to the socket
send(type, data) {
this.webSocket.send(JSON.stringify({
type,
data
}))
}
on(type, cb) {
// if the socket is already connected immediately call the callback
if (type === 'connect' && this.connected) {
return cb(this)
}
// proxy EventEmitters `on` method
return super.on(type, cb)
}
// catch any message from the socket and call the appropriate callback
__onmessage = e => {
const { type, data } = JSON.parse(e.data)
this.emit(type, data)
}
}
// create your SocketYO instance
const socket = new SocketYO({
host: 'wss://echo.websocket.org'
})
socket.on('connect', (socket) => {
// you can only send messages once the socket has been connected
socket.send('myEvent', {
message: 'hello'
})
})
// you can subscribe without the socket being connected
socket.on('myEvent', (data) => {
console.log('myEvent', data)
})
I built a TS, MongoDB Client wrapper. for some reason when I call the function that gets the connection, its callback is called twice.
There are 2 calls in total to the get() function, 1 before the export as you can see and another from a mocha test.
I am pretty new to TS and JS in general, but this seems a bit off.
import {Db, MongoClient} from "mongodb";
import {MongoConfig} from '../config/config'
class DbClient {
private cachedDb : Db = null;
private async connectToDatabase() {
console.log('=> connect to database');
let connectionString : string = "mongodb://" + MongoConfig.host + ":" + MongoConfig.port;
return MongoClient.connect(connectionString)
.then(db => {
console.log('=> connected to database');
this.cachedDb = db.db(MongoConfig.database);
return this.cachedDb;
});
}
public async get() {
if (this.cachedDb) {
console.log('=> using cached database instance');
return Promise.resolve(this.cachedDb);
}else{
return this.connectToDatabase();
}
}
}
let client = new DbClient();
client.get();
export = client;
where the console output is:
=> connect to database
=> connected to database
=> connected to database
Any particular reason this is misbehaving?
There are 2 calls in total to the get() function, 1 before the export as you can see and another from a mocha test.
I suspect the output has an additional => connect to database. As I said in the comments: There's a "race condition" where get() could be called multiple times before this.cachedDb is set which would lead to multiple connections/instances of Db being created.
For example:
const a = client.get();
const b = client.get();
// then
a.then(resultA => {
b.then(resultB => {
console.log(resultA !== resultB); // true
});
});
Solution
The problem can be fixed by storing the promise as the cached value (also, no need to have the async keyword on the methods as Randy pointed out, as there's no values being awaited in any of the methods so you can just return the promises):
import {Db, MongoClient} from "mongodb";
import {MongoConfig} from '../config/config'
class DbClient {
private cachedGet: Promise<Db> | undefined;
private connectToDatabase() {
console.log('=> connect to database');
const connectionString = `mongodb://${MongoConfig.host}:${MongoConfig.port}`;
return MongoClient.connect(connectionString);
}
get() {
if (!this.cachedGet) {
this.cachedGet = this.connectToDatabase();
// clear the cached promise on failure so that if a caller
// calls this again, it will try to reconnect
this.cachedGet.catch(() => {
this.cachedGet = undefined;
});
}
return this.cachedGet;
}
}
let client = new DbClient();
client.get();
export = client;
Note: I'm not sure about the best way of using MongoDB (I've never used it), but I suspect connections should not be so long lived as to be cached like this (or should probably only be cached for a short time and then disconnected). You'll need to investigate that though.
I've been looking for a way to handle web socket disconnects in my React app with Apollo subscriptions and have not found a way to do so. The other examples I see in the apollo documentation show the below method for catching a reconnect:
const wsClient = process.browser ? new SubscriptionClient(WSendpoint, {
reconnect: true,
}) : null;
const wsLink = process.browser ? new WebSocketLink(wsClient) : null;
if (process.browser) {
wsLink.subscriptionClient.on(
'reconnected',
() => {
console.log('reconnected')
},
)
}
There are two issues with the above method:
is that is does not catch when the user disconnects from their internet (only from when the server restarts for whatever reason)
that the reconnect is triggered outside of my React apps components.
What I would like to be able to do is to is reload my "chat" component if the user either disconnects from their internet or if my express server goes down for any reason. For this to happen I would need my chat component to completely reload which i'm not sure would be possible from outside my component tree.
Is there a method in the Query or Subscription Apollo components to be able to capture this event and handle it accordingly from the component?
There are a few ways I can think of to handle these cases but none of them are a one-shot solution, each case needs to be handled independently.
Setup a online/offline listener (ref)
Setup an Apollo middleware to handle network errors from your server (ref)
Create a variable in your store, isOnline for example, which can hold a global reference of your app's state. Whenever the above two methods trigger, you could update the value of isOnline
Finally, to bundle all of it together. Create a react HOC which uses isOnline to handle the network state for each component. This can be used to handle network error messages, refresh components once network is restored.
You can use SubscriptionClient callbacks from subscriptions-transport-ws, like this:
const ws = require("ws");
const { SubscriptionClient } = require("subscriptions-transport-ws");
const { WebSocketLink } = require("apollo-link-ws");
const { ApolloClient } = require("apollo-client");
const { InMemoryCache } = require("apollo-cache-inmemory");
const subClient = new SubscriptionClient(
'ws://localhost:4000/graphql',
{ reconnect: true },
ws
);
subClient.onConnected(() => { console.log("onConnected") });
subClient.onReconnected(() => { console.log("onReconnected") });
subClient.onReconnecting(() => { console.log("onReconnecting") });
subClient.onDisconnected(() => { console.log("onDisconnected") });
subClient.onError(error => { console.log("onError", error.message) });
const wsLink = new WebSocketLink(subClient);
const client = new ApolloClient({
link: wsLink,
cache: new InMemoryCache()
});
I'm using this for Node.js, but it will probably work for React too.
I am currently developing a small app that basically copies whatsapp's functionality, beeing a simple chat.
For that I use websockets and I've stumbled upon several tutorials and stackoverflow posts that use this kind of code for the websocket, which I then used for my project.
private create(url) {
const websocket = new WebSocket(url);
const observable: Observable<MessageEvent> = Observable.create(
(observerInLambda: Observer<MessageEvent>) => {
websocket.onmessage = observerInLambda.next.bind(observerInLambda);
websocket.onerror = observerInLambda.error.bind(observerInLambda);
websocket.onclose = observerInLambda.complete.bind(observerInLambda);
return websocket.close.bind(websocket); // When unsubbing from the observable the websocket is closed
}
);
const observer = {
next: (data: Object) => {
if (websocket.readyState === WebSocket.OPEN) {
websocket.send(JSON.stringify(data));
}
},
error: (err: any) => {
console.log(err);
},
complete: () => {
console.log("complete");
}
};
return Subject.create(observer, observable);
}
This works mostly fine, appart from the disconnecting on unsubscribe, but thats not my question. I get that i have to use some sort of callback/event style code like websocket.onmessage = (data:Object) => doStuff(data); to read from the websocket, but what confuses me why I would have to use a an observer, whose .next function i call, which in turn calls websocket.send. Why not simply call websocket.send instead, which isn't a asynchronous function anyway...
To me it seems that the whole pattern including the subject is a bit overkill for what I am doing? Am I getting something wrong, or is simply not useful in my case?