Adding a function to a Vuex plugin? - javascript

I'm a beginner in vue and created my first application. I use vuex and I have a plugin to manage a webSocket to the server. Everything works fine as long as I only dealt with messages sent from the server to the browser.
I now would like to add a function to send messages through the socket if connected, but I'm not able to access the exported function. I'm also a beginner in Javascript programming.
Here is the plugin code:
var store = null;
var ws = null;
function startWebsocket() {
ws = new WebSocket(process.env.VUE_APP_WEBSOCKET_URL)
ws.onmessage = function (event) {
console.log("webSocket: on message: ", event.data);
store.dispatch('remoteMessage', event.data);
}
ws.onopen = function (event) {
console.log("webSocket: on open: ", event)
store.dispatch('connectionOpened');
}
ws.onclose = function (event) {
console.log("webSocket: on close: ", event)
store.dispatch('connectionClosed');
ws = null
setTimeout(startWebsocket, 5000)
}
ws.onerror = function (event) {
console.log("webSocket: on error: ", event)
}
}
export default function createWebSocketPlugin() {
return store_param => {
store = store_param;
startWebsocket();
};
}
I would like to add the following function to the plugin so that I can call it from a vuex action function.
export function sendWebSocketMsg(msg) {
if (ws) {
ws.sendMsg(msg)
}
}
In the vuex index.js file I have this:
. . .
import webSocket from '../plugins/webSocket'
. . .
export default new Vuex.Store({
. . .
actions: {
connectionOpened({ commit }) {
commit('SET_CONNECTION', true);
},
connectionClosed({ commit }) {
commit('SET_CONNECTION', false);
},
connectionError({ commit }, error) {
commit('SET_ERROR', error);
},
remoteMessage({commit}, message) {
commit('SET_MESSAGE', message);
},
pause() {
sendWebSocketMsg('{"pause":true}')
},
play() {
sendWebSocketMsg('{"pause":false}')
}
}
}
The webSocket works well and reconnects automatically.
The only thing that I'm missing is the ability to send a webSocket message.
How do I have to modify the webSocket plugin ?

I answer my question since I found the solution. It is partly given in the tutorial I followed.
I wasn't aware of it, but the plugin is a vuex plugin.
The solution is to subscribe to a vuex method. I added the empty method SEND_MESSAGE to the vuex mutations.
mutations: {
SET_ERROR(state, errStr) {
state.error = errStr;
},
SET_CONNECTION(state, status) {
state.connected = status;
},
SET_MESSAGE(state, message) {
let msg = JSON.parse(message);
. . .
},
SEND_MESSAGE() {
},
},
I also added the application specific actions:
pause({commit}) {
commit('SEND_MESSAGE', '{"pause":true}');
},
play({commit}) {
commit('SEND_MESSAGE', '{"pause":false}');
},
I call the store actions from my components like this:
methods: {
pause() {
this.$store.dispatch("pause");
},
play() {
this.$store.dispatch("play");
}
},
The only change left to do is in the plugin. I subscribe a method to call to the SEND_MESSAGE mutation. This is how it is done:
export default function createWebSocketPlugin() {
return store_param => {
store = store_param;
startWebsocket();
store.subscribe((mutation, state) => {
if (state.connected && mutation.type === 'SEND_MESSAGE' && ws) {
console.log("webSocket send "+mutation.payload);
ws.send(mutation.payload);
}
});
};
}
I added the store.subscribe instruction. We only perform the operation when the mutation is of the right type and the web socket is connected.

ws variable is local to the module it was defined, this requires to modify plugin module in order for a function to access ws, e.g.:
export function sendWebSocketMsg(msg) {
if (ws) {
ws.sendMsg(msg)
}
}
export default function createWebSocketPlugin() {...}
Then named export can be imported in module where it's used:
import webSocket, {sendWebSocketMsg} from '../plugins/webSocket'

Related

Service worker activate and push event don't get triggered even though the registration is successful and it's activated

I have built a React progressive web application that makes use of service workers.
The service worker gets registered and is activated:
I have been trying to detect the "activate" event using this:
service-worker.js
navigator.serviceWorker.addEventListener("activate", function (event) {
console.log("service worker activated");
});
I added that at the end of the service-worker file. But, this event never gets triggered and I have no idea why.
I also tried to implement push notifications and trigger the from the backend. For this, I needed a "push" event listener that would listen to these events from the server:
navigator.serviceWorker.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
This is how showPushNotification is defined:
export function showPushNotification(title, description, image) {
if (!("serviceWorker" in navigator)) {
console.log("Service Worker is not supported in this browser");
return;
}
navigator.serviceWorker.ready.then(function (registration) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
});
}
I tested calling that function manually and it successfully triggerss a push notification.
This is the server code that triggers the push notification:
const sendPushNotification = async (user_id, title, description, image) => {
const search_option = { user: user_id };
const users_subscriptions = await PushNotificationSubscription.find(
search_option
);
const number_of_users_subscriptions = users_subscriptions.length;
const options = {
vapidDetails: {
subject: "mailto:xxxx#xxxx.com",
publicKey: VAPID_PUBLIC_KEY,
privateKey: VAPID_PRIVATE_KEY,
},
};
let push_notif_sending_results = {};
for (let i = 0; i < number_of_users_subscriptions; i++) {
const user_subscription = users_subscriptions[i];
await webPush
.sendNotification(
user_subscription,
JSON.stringify({
title,
description,
image,
}),
options
)
.then((notif_send_result) => {
push_notif_sending_results[i] = { success: notif_send_result };
})
.catch((error) => {
push_notif_sending_results[i] = { error: error };
});
}
return push_notif_sending_results;
};
This is the part responsible for sending the push notification:
webPush
.sendNotification(
user_subscription,
JSON.stringify({
title,
description,
image,
}),
options
)
And it's successfully executed as it returns a 201 HTTP response.
So the "push" event listener is supposed to detect it and trigger a push notification.
I think everything regarding the push notification has been successfully implementing and the problem is how the "push" event listener is added since the "activate" event listener also doesn't work.
So I tried moving the two event listeners here right after the registration of the service worker is successful:
function registerValidSW(swUrl, config) {
navigator.serviceWorker.register(swUrl).then((registration) => {
registration.addEventListener("activate", (event) => {
console.log(
"🚀 ~ file: serviceWorker.js:159 ~ navigator.serviceWorker.register ~ event",
event
);
});
registration.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(
showPushNotification(title, description, image)
);
});
});
}
But, it's still the same result.
Neither the "push" nor the "activate" event listeners get triggered.
Any idea what's going on?
Here's the whole service-worker file:
service-worker.js
import axios from "axios";
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets;
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
} else {
// Is not localhost. Just register service worker
console.log(
"Is not localhost. Just register a service worker, by calling registerValidSW"
);
registerValidSW(swUrl, config);
}
});
}
}
async function subscribeToPushNotifications(serviceWorkerReg) {
let subscription = await serviceWorkerReg.pushManager.getSubscription();
if (subscription === null) {
const dev_public_vapid_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const prod_public_vapid_key =
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const public_vapid_key = isLocalhost
? dev_public_vapid_key
: prod_public_vapid_key;
subscription = await serviceWorkerReg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: public_vapid_key,
});
axios
.post("/api/push_notif_subscription/subscribe", subscription)
.then((response) => {})
.catch((error) => {});
}
}
export function showPushNotification(title, description, image) {
if (!("serviceWorker" in navigator)) {
console.log("Service Worker is not supported in this browser");
return;
}
navigator.serviceWorker.ready.then(function (registration) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
});
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker.register(swUrl).then((registration) => {
subscribeToPushNotifications(registration);
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (!installingWorker) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been preached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { "Service-Worker": "script" },
}).then((response) => {
// Ensure the service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get("content-type");
if (
response.status === 404 ||
(!!contentType && contentType.indexOf("javascript") === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
console.log("Service worker found, calling registerValidSW");
registerValidSW(swUrl, config);
}
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}
}
navigator.serviceWorker.addEventListener("activate", function (event) {
console.log("service worker activated");
});
navigator.serviceWorker.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
The events "push" and "activate" are part of the ServiceWorkerGlobalScope as within the Service Worker API.
Push notifications must be handled within the service worker itself.
Therefore only the service worker can register an "activate" event listener.
The same applies for a "push" listener.
Specially in terms of the "push" listener this makes sense.
The idea of push events is to receive them, even if the main app (in this case the website) has been closed.
The service worker is an exception, as it even runs without the page being loaded.
Therefore move the "push" event into your service worker.
Your code (within the service worker) may look like this:
this.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
function showPushNotification(title, description, image) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
}
The rest seems fine to me.
Update (Some more explanation)
I took a more careful look at your service-worker.js and it seems it contains general methods for registering the service worker.
As mentioned above the main app and the service worker are two completely separate chunks of code, running in different spaces. So this means everything which is not supposed to run in the service worker itself must be put outside of the service-worker.js. The service worker (in your case) should only contain the code for handling push notifications. It's important that you do not include the "service-worker.js" within your application.
In your case, you may seperate these functions into service-worker-register.js which contain all functions which are for managing the service worker registration but should not be executed within the service worker itself (isLocalhost, register, subscribeToPushNotifications, registerValidSW, checkValidServiceWorker, and unregister). Please note the code snippet from above and make changes accordingly to the code left within the service worker.
MDN has a pretty in depth tutorial on service workers (and there are a lot more) I recommend having a look at:
developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers

Function Binding Issues on Callbacks

I'm working with WebSocket and having an issue with a function showing incorrect data, All my code used to work with a Class-based component, I'm only trying to convert a class component that actually calls the connect method of Websocket to a functional-based component. The Websocket is class-based for instance.
So I have a WebSocket which on socket New Message sets callbacks.
socketNewMessage(data) {
console.log(data);
const parsedData = JSON.parse(data);
const command = parsedData.command;
if (command === "userChatGroups") {
this.callback[command](parsedData.chatGRoups);
}
if (command === "new_userChatGroups") {
this.callback[command](parsedData.chatGRoup);
}
}
and here are the callbacks defined -
addCallbacks(userChatGroups, newUserChatGroup) {
this.callback["userChatGroups"] = userChatGroups;
this.callback["new_userChatGroups"] = newUserChat;
}
and then websocket finally return -
const WebSocketInstance = Websocketservice.getInstance();
export default WebSocketInstance;
Now the class-based component which I'm trying to convert to functional based -
let's call this component Chats-
this calls the connect method and a Websocket instance is returned.
useEffect(() => {
if (loggedInUserDetail) {
WebSocketInstance.connect(loggedInUserDetail[0].id);
}
}, [loggedInUserDetail]);
I have a helper function which checks the status of websocket -
function waitForSocketConnection(callback) {
setTimeout(() => {
if (WebSocketInstance.state() === 1) {
console.log("connection is secure");
callback();
return;
} else {
console.log("waiting for connection");
waitForSocketConnection(callback);
}
}, 10);
}
and I check the status and map my callbacks - here is the problem-
useEffect(() => {
waitForSocketConnection(() => {
WebSocketInstance.addCallbacks(
setChatGroups,
addNewChatGroup
);
});
}, [loggedInUserDetail]);
I have a state to manage CHatGroups -
const [groups, setGroups] = useState([]);
the setChatGroups (which initially loads all the groups the users are associated with works fine and sets the state of chatsGroups) as -
const setChatGroups = useCallback((userChatGroups) => {
setGroups(userChatGroups); //whatever data it recieved from the websocket.
}, []);
but the function addNewChatGroup always shows groups value as an empty array [] (it was updated earlier with setChatGroups). If I manually check the value of groups it is an array of n length, but in addNewChatGroup function, it always shows an empty array with the initial value [].
const addNewChatGroup = useCallback(
(newCHatGroup) => {
console.log(groups); **error -> this is always empty array**
// here I have to update. add the received value with the previous
},
[groups] // callbacks used hoping this will bind
);
In the class-based component I used to set callbacks on the constructor and used to bind with this, but I'm not able to do it here, can anyone help what I'm missing?
I'm sure it is a binding issue. maybe. May I know the reason for this binding failure?
Well if I understood correctly your problem, it could be link to multiple things.
The problem actually is I don't have a clear view on all your components, maybe you can try to paste a sandbox link or something like that with a "simple structure".
I tried to reproduced a typescript version, I don't know if it could help:
class MyWebSocket {
private static _instance: MyWebSocket;
public callbacks: any = {};
public connected: boolean = false;
public socketNewMessage(data: any): void {
const parsedData = JSON.parse(data);
console.log('new message received:', parsedData);
const command = parsedData.command;
if (command === "new_userChatGroups") {
this.callbacks[command](parsedData.newGroupAdded);
}
}
public addCallbacks(elements: {command: string, func: Function}[]) {
console.log('adding callbacks...', elements);
elements.forEach(element => {
this.callbacks[element.command] = element.func;
});
}
public connect(): void {
setTimeout(() => this.connected = true, 1100);
}
public static getInstance(): MyWebSocket {
return this._instance || (this._instance = new MyWebSocket());
}
}
class SocketUtils {
static waitForSocketConnection(callback: any): void {
const waitingInterval = setInterval(() => {
if (MyWebSocket.getInstance().connected) {
console.log('socket is connected! processing callback...');
clearInterval(waitingInterval);
callback();
return;
} else {
console.log('socket is not connected after 1sec, waiting...');
}
}, 1000);
}
}
class Chat {
groups: string[] = ['group of JStw'];
new_userChatGroups(group: string) {
this.groups.push(group);
}
}
class Main {
constructor() {
const myChat = new Chat();
MyWebSocket.getInstance().connect();
// waiting connections.
SocketUtils.waitForSocketConnection(() => {
console.log('waitForSocketConnection is triggered, adding callbacks...');
// adding callbacks
MyWebSocket.getInstance().addCallbacks([{command: 'new_userChatGroups', func: myChat.new_userChatGroups.bind(myChat)}]);
});
// Waiting 5min to dispatch an message
setTimeout(() => {
// testing eventing after getting connection
MyWebSocket.getInstance().socketNewMessage(JSON.stringify({command: 'new_userChatGroups', newGroupAdded: 'group of Ranu Vijay'}));
}, 5000);
setTimeout(() => {
console.log('program finished, results of chat groups:', myChat.groups);
}, 10000);
}
}
new Main();
Output:
I'm more specialized on functional component by using react so without a link to investigate all your code, it will be complex to help.
I don't know if you are using a library, but there is this one: https://www.npmjs.com/package/react-use-websocket which seems to be really ultra simple to use without managing socket connection/disconnection.
For me if I had to implement it, I would say:
Component ChatGroup which is using websocket hook const { sendMessage, lastMessage, readyState } = useWebSocket(socketUrl); and contains the state groups, setGroups.
Component Chat which can use the sendMessage from props of ChatGroup component and call it from this component if a join a group.
Then your "parent" component is managing the state and is controlling the data.

Referencing a Vue component (from a socket function)

so what I am currently working on is using vue with socketIO. So I want a component that updates its data when the websocket emits something.
I looked at the following tutorial: https://medium.com/#michaelmangial1/getting-started-with-vue-js-socket-io-8d385ffb9782.
The connection to the websocket works fine however I don't know how I can change a data variable of the component or call a method of the component from the websocket function as I get the error that e.g. this.getAllLayouts is not a function.
In the tutorial they just set app.title = data.title; in socket.on() as the Vue instance can be addresses as app.
But now I am confused. I wanted to use this in an component which I defined as a single-file component. So I can't use this app reference. But how can I reference the component in that case?
Below you can see the most important parts of this component. I defined the socket connection in the created() of the component
<script>
import axios from 'axios';
// eslint-disable-next-line
import io from 'socket.io-client';
import FirstPage from './FirstPage.vue';
import DNDAssign from './DNDAssign.vue';
export default {
name: 'ControlCenter',
data() {
return {
devices: [{ ip: 'yet unknown' }], // placeholder so line 12 does not throw error before actual device info fetched
thisDeviceIndex: 0,
currentLayoutIndex: 0,
layouts: [],
};
},
components: {
DNDAssign,
FirstPage,
},
methods: {
getAllLayouts() {
const path = 'http://192.168.0.38:5000/layouts';
axios.get(path)
.then((response) => {
console.log(response);
this.layouts = response.data.layouts;
this.devices = response.data.devices;
this.thisDeviceIndex = response.data.your_device_index;
console.log(this.layouts);
})
.catch((error) => {
// eslint-disable-next-line
console.log(error);
});
},
},
created() {
// inital fetching of layouts
console.log('fetching layouts from backend');
this.getAllLayouts();
// test websocket connection
const socket = io.connect('http://192.168.0.38:5000');
// eslint-disable-next-line
socket.on('connect', function () {
console.error('connected to webSocket');
socket.emit('my event', { data: 'I\'m connected!' });
});
// eslint-disable-next-line
socket.on('my response', function(data){
console.log('got response');
console.log(data.data);
});
// eslint-disable-next-line
socket.on('update_on_layouts', function(data){
this.getAllLayouts(); //this does not work? How can I make it work or do something like
// this.layouts = data;
});
},
};
</script>
Issue is that this inside that fn is not binded to Vue, you need to lexically bind it
Use arrow method to bind this to method
socket.on('update_on_layouts', data => {
this.getAllLayouts(); //this does not work? How can I make it work or do something like
// this.layouts = data;
});

Unable to connect to WebSocket with Vue plugin

I'd like to write a vue-plugin to get handy WebSocket methods like connect() and subscribe() in my Vue application. I've got a problem with connecting to WebSocket, it only works when I call connect() method in the mounted hook and load the whole page (like with the browser refresh button). In another case, when I first load the page and then call the connect() method explicitly by the button click, the connection isn't established.
My vue-plugin code:
import SockJS from "sockjs-client";
import Stomp from "webstomp-client";
const WebSocketTester = {
install(Vue, options) {
console.log("websocket tester launched");
let connected = false;
const ws = {
connected: () => connected
};
const stompClient = getStompClient("http://localhost:8080/ws");
const connect = () => {
return new Promise((resolve, reject) => {
if (connected) {
reject("Already connected !");
return;
}
console.log("trying to connect websocket");
stompClient.connect({}, frame => {
console.log("got websocket frame:");
console.log(frame);
if (frame.command == "CONNECTED") {
connected = true;
resolve();
} else {
reject("Could not connect with " + url);
}
});
});
};
ws.connect = () => {
return connect();
};
Vue.prototype.$ws = ws;
}
};
const getStompClient = webSocketUrl => {
const socket = new SockJS(webSocketUrl);
return Stomp.over(socket);
};
export default WebSocketTester;
My vue component:
<template>
<div class="hello">
<button #click="connect">Connect with websocket</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String
},
methods: {
connect() {
console.log("connecting...");
this.$ws.connect().catch(error => {
console.log("could not connect by click");
console.log(error);
});
}
},
mounted() {
// this works well
// this.$ws.connect().catch(error => {
// console.log("could not connect in mounted");
// console.log(error);
// });
}
};
</script>
In the case, I uncomment the mounted hook, after page load I see the console log like this:
websocket tester launched
trying to connect websocket
Opening Web Socket...
Web Socket Opened...
DEPRECATED: undefined is not a recognized STOMP version. In next major client version, this will close the connection.
>>> CONNECT
>>> length 52
<<< CONNECTED
connected to server undefined
got websocket frame:
Frame {command: "CONNECTED", headers: {…}, body: ""}
And everything works correct. But, if I comment the mounted hook and want to connect with the WebSocket by the button click, the console log looks like this:
websocket tester launched
connecting...
trying to connect websocket
Opening Web Socket...
and that's it, the connection isn't established. Why this happens and how to fix it?
OK I figured it out. The problem line was const stompClient = getStompClient("http://localhost:8080/ws"); in the plugin. I've moved it to the connect method and store as ws.object.
if (connected) {
reject("Already connected !");
return;
}
ws.stompClient = getStompClient("http://localhost:8080/ws");
console.log("trying to connect websocket");
ws.stompClient.connect({}, frame => {
Later, I use ws.stompClient and it works fine.

How to import SignalR in React Component?

I have used create-react-app to scaffold the initial react application.
My DashBoard component:
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import $ from 'jquery';
import 'signalr';
class Dashboard extends Component {
constructor(props) {
super(props);
var connection = $.hubConnection('http://[address]:[port]');
var proxy = connection.createHubProxy('[hubname]');
// atempt connection, and handle errors
connection.start()
.done(function(){ console.log('Now connected, connection ID=' + connection.id); })
.fail(function(){ console.log('Could not connect'); });
}
render() {
return (...);
}
}
export default Dashboard;
Now I get the below error from SignalR saying jQuery is not added, but I have imported it in the line above:
Error: jQuery was not found. Please ensure jQuery is referenced before
the SignalR client JavaScript file.
If I comment out import "signalr"; jQuery gets loaded correctly and i can access the $ inside the module. Why does this happen?
This is how we do it now (year 2020) with the new package #microsoft/signalr.
We use Redux, but you don't have to use Redux to be able to utilize this method.
If you are using #microsoft/signalr package instead of #aspnet/signalr, then this is how you can set it up. This is our working code in prod:
import {
JsonHubProtocol,
HubConnectionState,
HubConnectionBuilder,
LogLevel
} from '#microsoft/signalr';
const isDev = process.env.NODE_ENV === 'development';
const startSignalRConnection = async connection => {
try {
await connection.start();
console.assert(connection.state === HubConnectionState.Connected);
console.log('SignalR connection established');
} catch (err) {
console.assert(connection.state === HubConnectionState.Disconnected);
console.error('SignalR Connection Error: ', err);
setTimeout(() => startSignalRConnection(connection), 5000);
}
};
// Set up a SignalR connection to the specified hub URL, and actionEventMap.
// actionEventMap should be an object mapping event names, to eventHandlers that will
// be dispatched with the message body.
export const setupSignalRConnection = (connectionHub, actionEventMap = {}, getAccessToken) => (dispatch, getState) => {
const options = {
logMessageContent: isDev,
logger: isDev ? LogLevel.Warning : LogLevel.Error,
accessTokenFactory: () => getAccessToken(getState())
};
// create the connection instance
// withAutomaticReconnect will automatically try to reconnect
// and generate a new socket connection if needed
const connection = new HubConnectionBuilder()
.withUrl(connectionHub, options)
.withAutomaticReconnect()
.withHubProtocol(new JsonHubProtocol())
.configureLogging(LogLevel.Information)
.build();
// Note: to keep the connection open the serverTimeout should be
// larger than the KeepAlive value that is set on the server
// keepAliveIntervalInMilliseconds default is 15000 and we are using default
// serverTimeoutInMilliseconds default is 30000 and we are using 60000 set below
connection.serverTimeoutInMilliseconds = 60000;
// re-establish the connection if connection dropped
connection.onclose(error => {
console.assert(connection.state === HubConnectionState.Disconnected);
console.log('Connection closed due to error. Try refreshing this page to restart the connection', error);
});
connection.onreconnecting(error => {
console.assert(connection.state === HubConnectionState.Reconnecting);
console.log('Connection lost due to error. Reconnecting.', error);
});
connection.onreconnected(connectionId => {
console.assert(connection.state === HubConnectionState.Connected);
console.log('Connection reestablished. Connected with connectionId', connectionId);
});
startSignalRConnection(connection);
connection.on('OnEvent', res => {
const eventHandler = actionEventMap[res.eventType];
eventHandler && dispatch(eventHandler(res));
});
return connection;
};
Then you would call like the following. Please note that this a pseudo code. You may have to call it differently depending on your project setup.
import { setupSignalRConnection } from 'fileAbove.js';
const connectionHub = '/hub/service/url/events';
export const setupEventsHub = setupSignalRConnection(connectionHub, {
onMessageEvent: someMethod
}, getAccessToken);
export default () => dispatch => {
dispatch(setupEventsHub); // dispatch is coming from Redux
};
Let me know if it helped by up-voting. Thank you
UPDATE: Please note that if you are using Redux in your ReactJS app, the solution below is not necessarily the best solution. It is better to implement signalR as a middleware. You can find the best answer here.
If you are not using Redux, or you still want to implement it in a React component, then read on:
For people that are using the latest version of signalR (core v2.1), since jQuery is not a dependency of signalR any more, you can import it like:
import * as signalR from '#aspnet/signalr';
NOTE: there is now a newer version of signalr available (#microsoft/signalr) that requires a different setup. This solution only works with #aspnet/signalr. (UPDATE June 2020)
And then use it like:
signalR.HubConnectionBuilder()
Here is an example:
import React, { PureComponent } from 'react';
import { string } from 'prop-types';
import * as signalR from '#aspnet/signalr';
class SignalR extends PureComponent {
constructor (props) {
super(props);
this.connection = null;
this.onNotifReceived = this.onNotifReceived.bind(this);
}
componentDidMount () {
const protocol = new signalR.JsonHubProtocol();
const transport = signalR.HttpTransportType.WebSockets;
const options = {
transport,
logMessageContent: true,
logger: signalR.LogLevel.Trace,
accessTokenFactory: () => this.props.accessToken,
};
// create the connection instance
this.connection = new signalR.HubConnectionBuilder()
.withUrl(this.props.connectionHub, options)
.withHubProtocol(protocol)
.build();
this.connection.on('DatabaseOperation', this.onNotifReceived);
this.connection.on('DownloadSession', this.onNotifReceived);
this.connection.on('UploadSession', this.onNotifReceived);
this.connection.start()
.then(() => console.info('SignalR Connected'))
.catch(err => console.error('SignalR Connection Error: ', err));
}
componentWillUnmount () {
this.connection.stop();
}
onNotifReceived (res) {
console.info('Yayyyyy, I just received a notification!!!', res);
}
render () {
return <span />;
};
};
SignalR.propTypes = {
connectionHub: string.isRequired,
accessToken: string.isRequired
};
export default SignalR;
UPDATE: in 2020, you can use "withAutomaticReconnect()":
const connection = new HubConnectionBuilder()
.withUrl(connectionHub, options)
.withAutomaticReconnect()
.withHubProtocol(new JsonHubProtocol())
.configureLogging(LogLevel.Information)
.build();
What I figured out Signalr has dependency on jQuery. For some reason import $ from 'jquery' doesn't set window.jQuery. That's why need to do it explicitly.
I solved the issue this way:
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import $ from 'jquery';
window.jQuery = $;
require('signalr');
class Dashboard extends Component {
// .....
}
export default Dashboard;
Check out SignalR no jQuery
npm i -D signalr-no-jquery
import { hubConnection } from 'signalr-no-jquery';
const connection = hubConnection('http://[address]:[port]', options);
const hubProxy = connection.createHubProxy('hubNameString');
// set up event listeners i.e. for incoming "message" event
hubProxy.on('message', function(message) {
console.log(message);
});
// connect
connection.start({ jsonp: true })
.done(function(){ console.log('Now connected, connection ID=' + connection.id); })
.fail(function(){ console.log('Could not connect'); });
https://www.npmjs.com/package/signalr-no-jquery

Categories