How to set up a socket connection on a strapi server - socket.io

I am trying to integrate socket.io with strapi. But unfortunately I have been unable to do so without any proper tutorial or documentation covering this aspect.
I followed along with the only resource I found online which is:
https://medium.com/strapi/strapi-socket-io-a9c856e915a6
But I think the article is outdated. I can't seem to run the code mentioned in it without running into tonnes of errors.
Below is my attempt to implement it and I have been trying to connect it through a chrome websocket plugin smart websocket client But I am not getting any response when I try to run the server.
I'm totally in the dark. Any help will be appreciated
module.exports = ()=> {
// import socket io
var io = require('socket.io')(strapi.server)
console.log(strapi.server) //undefined
// listen for user connection
io.on('connect', socket => {
socket.send('Hello!');
console.log("idit")
// or with emit() and custom event names
socket.emit('greetings', 'Hey!', { 'ms': 'jane' }, Buffer.from([4, 3, 3, 1]));
// handle the event sent with socket.send()
socket.on('message', (data) => {
console.log(data);
});
// handle the event sent with socket.emit()
socket.on('salutations', (elem1, elem2, elem3) => {
console.log(elem1, elem2, elem3);
});
});
};

So I found the solution. Yay. I'll put it here just in case anybody needs it.
boostrap.js
module.exports = async () => {
process.nextTick(() =>{
var io = require('socket.io')(strapi.server);
io.on('connection', async function(socket) {
console.log(`a user connected`)
// send message on user connection
socket.emit('hello', JSON.stringify({message: await strapi.services.profile.update({"posted_by"})}));
// listen for user diconnect
socket.on('disconnect', () =>{
console.log('a user disconnected')
});
});
strapi.io = io; // register socket io inside strapi main object to use it globally anywhere
})
};
Found this at: https://github.com/strapi/strapi/issues/5869#issuecomment-619508153_
Apparently, socket.server is not available when the server starts. So you have to make use of process.nextTick that waits for the socket.server to initialize.
I'll also add a few questions that I faced when setting this up.
How do i connect from an external client like nuxt,vue or react?
You just have to connect through "http://localhost:1337" that is my usual address for strapi.
I am using nuxt as my client side and this is how set up my socketio on the client side
I first installed nuxt-socket-io through npm
Edited the nuxt.config file as per it's documention
modules:[
...
'nuxt-socket-io',
...
],
io: {
// module options
sockets: [
{
name: 'main',
url: 'http://localhost:1337',
},
],
},
And then i finally added a listener in one of my pages.
created() {
this.socket = this.$nuxtSocket({})
this.socket.on('hello', (msg, cb) => {
console.log('SOCKET HI')
console.log(msg)
})
},
And it works.

A clean way to integrate third-party services into Strapi is to use hooks. They are loaded once during the server boot. In this case, we will create a local hook.
The following example has worked with strapi#3.6.
Create a hook for socket.io at ./hooks/socket.io/index.js
module.exports = strapi => {
return {
async initialize() {
const ioServer = require('socket.io')(strapi.server, {
cors: {
origin: process.env['FRONT_APP_URL'],
methods: ['GET', 'POST'],
/* ...other cors options */
}
})
ioServer.on('connection', function(socket) {
socket.emit('hello', `Welcome ${socket.id}`)
})
/* HANDLE CLIENT SOCKET LOGIC HERE */
// store the server.io instance to global var to use elsewhere
strapi.services.ioServer = ioServer
},
}
}
Enable the new hook in order for Strapi to load it - ./config/hook.js
module.exports = {
settings: {
'socket.io': {
enabled: true,
},
},
};
That's done. You can access the websocket server inside ./config/functions/bootstrap.js or models' lifecycle hooks.
// ./api/employee/models/employee.js
module.exports = {
lifecycles: {
async afterUpdate(result, params, data) {
strapi.services.ioServer.emit('update:employee', result)
},
},
};

For those who are looking the answer using Strapi version 4
var io = require("socket.io")(strapi.server.httpServer)

Related

How to push notifications from server to client using Spring Boot RSocket (Backend) and Angular (rsocket-js)?

I am planning to use RSocket for my notifications system. I wanted to use Spring Boot RSocket for my backend (Java) while for my frontend, I will be using Angular using rsocket-js.
I was able to quickly spin-up a request-stream interaction model wherein I can pull-in all the notifications within my system. See code snippet for my backend:
#MessageMapping("streams")
public Flux<Notification> requestStream() {
log.info("Streaming to notifications...");
return streamEventService.retrieveAllNotifications().log();
}
Now on my frontend, I have the following code snippet:
export class RsocketClientService {
// backend ws endpoint
private readonly wsURL = 'ws://localhost:7000/notification';
client: any;
socket: any
constructor() {
this.client = new RSocketClient({
serializers: {
data: JsonSerializer,
metadata: IdentitySerializer
},
setup: {
keepAlive: 10000,
lifetime: 180000,
dataMimeType: 'application/json',
metadataMimeType: 'message/x.rsocket.routing.v0',
payload: {
data: 23
}
},
transport: new RSocketWebSocketClient({
url: this.wsURL
}),
responder: new EchoResponder()
});
}
public connect() {
console.log("initializeSocket...")
this.client.connect().subscribe({
onComplete: (socket: any) => {
this.socket = socket;
this.socket.connectionStatus().subscribe( (status: any) => {
console.log("Connection status? ", status);
});
},
onError: (error: any) => {
console.error("Connection onError? " + error);
},
onSubscribe: (cancel: any) => {
console.log("Connection onSubscribe? cancel?");
}
});
}
public retrieveNotifications() {
this.socket.requestStream({
data: null,
metadata: String.fromCharCode('streams'.length) + 'streams'
})
.subscribe({
onComplete: () => {
console.log("onComplete?");
},
onError: (error: any) => {
console.error("onError? error: " + error);
},
onNext: (payload: any) => {
console.log("onNext? payload: ", payload);
},
onSubscribe: (subscription: any) => {
console.log("onSubscribe?");
subscription.request(1000000);
},
});
}
I have a button in the UI that if clicked will call the method retrieveNotifications which will subscribe to the rsocket message mapping method in my backend requestStream.
Everything is working fine and I could see my responses coming in. Now my question would be, what if on my server there is a new data inserted into the database for example, then how can I send a notification message from my backend server to the frontend saying that "Hey! new data was pushed into the database." I am kind of stuck on how the server will be able to use a somehow fire and forget to the client side.
You want to server-side send request to client-side when connect established.
You can get this connect's RSocketRequester from server then using it create one of four method(FNF, Request-reponse, request-stream, stream-stream) to send request to client.
In client-side, you can receive data in EchoResponder class in one of four method above.
It looks like you need to create a new controller function that returns a void and when you insert an object in the DB you pass that object to the front end from this function and in angular you connect to it as you did up...try to check this link for fire and forget approach ... hope this helps https://www.baeldung.com/spring-boot-rsocket

How can I get socket.io to work with deno?

My aim is to get socket.io to work with deno. Deno does have a standard library for web sockets, but that does not have a fail-over.
I think it is possible to use the UNPKG service to use an NPM package in you deno program, but I seem to get the syntax wrong for importing socket.io:
import { serve } from "https://deno.land/std/http/server.ts";
import {Socket} from "https://unpkg.com/browse/socket.io#3.0.1/dist/index.d.ts";
new Worker(new URL("worker.js", import.meta.url).href, { type: "module" });
const server = serve({ port: 3001 });
const app = new Application();
const io = Socket(3001);
// serve index page
if (req.url === "/") {
req.respond({
status: 200,
body: await Deno.open("./public/index.html"),
});
}
io.on("connection", (socket) => {
// either with send()
socket.send("Hello!");
// or with emit() and custom event names
socket.emit("greetings", "Hey!", { "ms": "jane" }, Buffer.from([4, 3, 3, 1]));
// handle the event sent with socket.send()
socket.on("message", (data) => {
console.log(data);
});
// handle the event sent with socket.emit()
socket.on("salutations", (elem1, elem2, elem3) => {
console.log(elem1, elem2, elem3);
});
});
I get the following error:
error: An unsupported media type was attempted to be imported as a module.
Specifier: https://unpkg.com/browse/socket.io#3.0.1/dist/index.d.ts
MediaType: Unknown
It fails because you are importing a .d.ts file, which is a declaration file and essentially you can't run any code from - it's purely to support types.
Instead you should replace index.d.ts with index.js
Try this:
import Socket from 'https://cdn.esm.sh/v9/socket.io#3.0.3/esnext/socket.io.js';
As #Indecisive said, you are importing a .d.ts file.
You can use #Marcos Casagrande: https://stackoverflow.com/a/61821141/6250402 (remember install socket.io module with npm)
Or use https://deno.land/std#0.79.0/ws/mod.ts

Add identifier to websocket

I am using the Node.js ws library, to listen to events in user accounts on a 3rd party API. For each user, I open a websocket to listen to the events in the user's account.
Turns out, the 3rd-party API doesn't provide a userID for each event, so if I have 10 websocket connections to user-accounts, I cannot determine which account an event came from.
I have access to a unique userId prior to starting each of my connections.
Is there a way to append or wrap the websocket connection with the userId identifier, to each connection I make, such that when I receive an event, I can access the custom identifier, and subsequently know which user's account the event came from?
The code below is a mix of real code, and pseudocode (i.e customSocket)
const ws = new WebSocket('wss://thirdparty-api.com/accounts', {
port: 8080,
});
ws.send(
JSON.stringify({
action: 'authenticate',
data: {
oauth_token: access_token,
},
})
);
// wrap and attach data here (pseudocode at top-level)
customSocket.add({userId,
ws.send(
JSON.stringify({
action: 'listen',
data: {
streams: ['action_updates'],
},
})
)
})
// listen for wrapper data here, pseudocode at top level
customSocket.emit((customData) {
ws.on('message', function incoming(data) {
console.log('incoming -> data', data.toString());
})
console.log('emit -> customData', customData);
})
Looking at the socket.io library, the namespace feature may solve for this, but I can't determine if that's true or not. Below is an example in their documentation:
// your application has multiple tenants so you want to dynamically create one namespace per tenant
const workspaces = io.of(/^\/\w+$/);
workspaces.on('connection', socket => {
const workspace = socket.nsp;
workspace.emit('hello');
});
// this middleware will be assigned to each namespace
workspaces.use((socket, next) => {
// ensure the user has access to the workspace
next();
});
I found a solution to this which is fairly simple. First create a message handler function:
const eventHandler = (uid, msg) => {
console.log(`${uid} did ${msg}`);
};
Then, when you create the websocket for the given user, wrap the .on event with the handler:
const createSocketForUser = (uid, eventHandler) => {
const socket = new WebSocket(/* ... */);
socket.onmessage = (msg) => {
eventHandler(uid, msg)
};
return socket;
}

Using Cypress with `mock-sockets`

I am trying to use mock-sockets with Cypress, setting up the mock in the onBeforeLoad hook for cy.visit() in my beforeEach block. I can get one test to work but when the mock setup runs on the next beforeEach I get an error that A mock server is already listening on this url.
code under test:
(called from my React app's componentDidiMount)
subscribeToSettings(url: string): W3CWebSocket {
let settingsSubscription = new W3CWebSocket(url);
settingsSubscription.onopen = () => console.log('WebSocket Client Connected (settings)');
settingsSubscription.onclose = () => console.log('WebSocket Client Disconnected (settings)');
settingsSubscription.onmessage = (message: MessageEvent) => this.handleSettingsMessage(message);
return settingsSubscription;
}
/**
* Handler for websocket settings messages, which updates the local settings values.
* #param message the websocket message
*/
handleSettingsMessage(message: MessageEvent) {
const updatedValues = JSON.parse(message.data);
console.log('A message was received on the settings channel.', updatedValues);
this.props.updateSettingsFromBackend(updatedValues);
}
cypress tests
import { Server } from 'mock-socket'
import { defaultSettingsState } from "../../src/reducers/settings.reducer";
import { _createSettingsApiPutPayload } from "../../src/actions/settings.actions";
describe('mock socket method 1', () => {
let mockSocket;
let mockServer;
beforeEach(() => {
cy.visit('/', {
onBeforeLoad(win: Window): void {
// #ts-ignore
cy.stub(win, 'WebSocket', url => {
mockServer = new Server(url)
mockServer.on('connection', socket => {
console.log('mock socket connected');
mockSocket = socket;
});
mockSocket = new WebSocket(url);
return mockSocket
});
},
});
});
afterEach(() => {
mockSocket.close()
mockServer.stop()
});
it('gets a message', () => {
cy.contains('SETTINGS').click()
const object = _createSettingsApiPutPayload(defaultSettingsState)
mockSocket.send(JSON.stringify(object));
cy.contains('Motion threshold')
});
it('gets another message', () => {
cy.contains('SETTINGS').click()
const object = _createSettingsApiPutPayload(defaultSettingsState)
mockSocket.send(JSON.stringify(object));
cy.contains('Motion threshold')
});
});
Here are the logs from my console:
WebSocket Client Connected (settings)
mock socket connected at url ws://localhost:8702/PM_Settings
A message was received on the settings channel. {…}
mock socket connected at url ws://localhost:3000/sockjs-node/949/mhuyekl3/websocket
The development server has disconnected.
Refresh the page if necessary.
Uncaught Error: A mock server is already listening on this url
I wonder if it has to do with that second call which is for some mystery url.
(Note: calling cy.contains('SETTINGS').click() at the end of beforeEach somehow doesn't work, even in that first test. Even when I have my app set to start on the settings page (instead of having to click to it from inside the tests), clicking on SETTINGS from beforeEach still doesn't work even though we're already there. So that's kind of weird)
These cypress logs may also be helpful:
It only worked for me, when I moved server stopping into WebSocket stub:
cy.stub(window, 'WebSocket', url => {
if (mockServer) {
mockServer.stop();
}
mockServer = new Server(url);
mockServer.on('connection', socket => {
mockSocket = socket;
});
mockSocket = new WebSocket(url);
return mockSocket;
});
Im probably wrong, but I guess afterEach or mockServer.stop(); is async thats why mock server fails to stop before new init

Issue Broadcasting to Socket.io Rooms of A Namespace

I'm trying to set up a server that can dynamically create many rooms for many namespaces. I'm currently just trying to broadcast to sockets of a room, when a new socket has joined that room.
So far I have been able to broadcast to a specific namespace and my event listeners on the client receives the message. However when I try to broadcast to a room, of a specific namespace, my event listener doesn't receive that message.
I've turned on the Debugger mode and see the socket.io-client:socket emitting the event with the right payload and event type. So I am not sure what I am missing since the documentation also seems fairly straightforward. Any help would be much appreciated. Below is my code.
Server
const colorNs = io.of('/color');
colorNs.on('connection', (socket) => {
const { id } = socket.handshake.query;
const { id:connId } = socket.conn;
if(id) {
socket.join(id);
socket.broadcast.to(id).emit('user:connect', { id: connId });
}
socket.on('disconnect', () => {
const { id } = socket.handshake.query;
const { id:connId } = socket.conn;
socket.broadcast.to(id).emit('user:disconnect', { id: connId });
});
});
Client
const socket = io('/color?id="123"');
socket.on('user:connect', () => console.log('data', data));
Client - Debug Trace
socket.io-parser decoded 2/color,["user:connect",{"id":"IZTTPidF121JCzf9AAAO"}] as {"type":2,"nsp":"/color","data":["user:connect",{"id":"IZTTPidF121JCzf9AAAO"}]} +1ms
browser.js:133
socket.io-client:socket emitting event ["user:connect",{"id":"IZTTPidF121JCzf9AAAO"}] +3ms

Resources