How to broadcast message to a single client using nestjs? - websocket

I am using nestjs websockets to build a chat app for a multi-vendor application but whenever I broadcast/emit a message from the server to the client, it gets broadcasted to all the clients. Is it possible to emit a message to a single client? And how to do that?
export class ChatGateway implements OnGatewayInit {
constructor(private conversationService: ConversationService) {}
#WebSocketServer()
server: Server;
#SubscribeMessage('sendMessage')
async handleMessage(
client: Socket,
payload: {
senderProfileId: ID;
recieverProfileId: ID;
conversationId: ID;
message: string;
createdAt: Date;
},
) {
await this.conversationService.createMessage({
message: payload.message,
createdAt: payload.createdAt,
senderProfileId: payload.senderProfileId as string,
conversationId: payload.conversationId as string,
});
// Want to emit this to a single client
this.server.emit('recieveMessage', payload);
}
}
Currently, I am using the recieverProfileId to make sure if the message is displayed to the correct reciever.

server side:
#SubscribeMessage('message')
onEnvent(#MessageBody() message: any){
this.server.to(message.to).emit("message", message);
}
client:
socket.emit('message', { to: socket.id, data: message })

Related

Nest.js Websocket Gateway loosing socket connecition using redid broker

I have to implement websocket communication in my nest.js app. I've successfully setup the websocket gateway and I have tested it with postman. My code looks like this
export class SocketIOAdapter extends IoAdapter {
constructor(private app: INestApplicationContext, private configService: ConfigService) {
super(app);
}
createIOServer(port: number, options: ServerOptions) {
const clientPort = parseInt(this.configService.getOrThrow("PORT"));
const cors = {
origin: [
`http://localhost:${clientPort}`,
new RegExp(`/^http:\/\/192\.168\.1\.([1-9]|[1-9]\d):${clientPort}$/`),
],
};
const optionsWithCORS: ServerOptions = {
...options,
cors,
};
const server: Server = super.createIOServer(port, optionsWithCORS);
const orderRepository = this.app.get(OrderRepository);
server
.of("/orders")
.use(createTokenMiddleware(orderRepository));
return server;
}
}
const createTokenMiddleware =
(orderRepository: OrderRepository) =>
async (socket: Socket, next) => {
// here I run some logic using my order repository
next();
} catch {
next(new Error("FORBIDDEN"));
}
};
And
#WebSocketGateway({
namespace: "/orders",
})
#Injectable()
export class OrderGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
private readonly logger = new Logger(OrderGateway.name);
#WebSocketServer() io: Namespace;
afterInit(): void {
this.logger.log("Websocket Gateway initialized.");
}
async handleConnection(client: Socket) {
const sockets = this.io.sockets;
// here I run some logic to know which rooms to use for this client
const roomsToJoin = [...]
await client.join(roomsToJoin);
}
async handleDisconnect(client: Socket) {
this.logger.log(`Disconnected socket id: ${client.id}`);
}
public emitOrderStatusChangeNotification(order: OrderDTO) {
this.io
.to("Here I put some roomId that depends on order")
.emit("order_status_changed", JSON.stringify(order));
}
}
Now, whenever I want to send a notification, I inject the OrderGateway and call emitOrderStatusChangeNotification. This works fine, however, my app is deployed on several instances behind a load balancer. The latter breaks this approach as socket clients may be connected to a different server from the one I'm sending the notification. So, the next step to scale web sockets (as far as I understand) is to use a broker. I tried to use Redis pub/sub in the following way. I have this two classes:
#Injectable()
export class NotificationPublisherService {
constructor(#Inject("ORDER_NOTIFICATION_SERVICE") private client: ClientProxy) {}
async publishEvent(order: OrderDTO) {
console.log("will emit to redis");
this.client.emit(Constants.notificationEventName, order);
}
}
#Controller()
export class NotificationSuscriberController {
private readonly logger = new Logger(NotificationSuscriberController.name);
constructor(private readonly orderGateway: OrderGateway) {}
#EventPattern(Constants.notificationEventName)
async handleOrderStatusChangeEvent(order: OrderDTO) {
try {
this.orderGateway.emitOrderStatusChangeNotification(order);
} catch (err) {
this.logger.log("error sending notification");
}
}
As you can see, I'm injecting orderGateway in the class that have the method that handles the data from redis and in that handler I send the notification. Finally, I replaced all the invocations of emitOrderStatusChangeNotification to the publishEvent method of NotificationPublisherService. After doing this, the flow works well except from the last step. This means, the data is put on redis and read by the suscriber, which tries to send the websocket notification. However, when logging the connected clients for that room in emitOrderStatusChangeNotification method, I'm getting that there are no connected clients, even though I confirmed there where connected clients on that room (I did this by logging the list of connected clients after doing client.join in the handleConnection method of OrderGateway). My best guess is that an instance of OrderGateway handles the socket connection and a different instance of OrderGateway is processing the data from Redis broker. I tried to explicitly set the scope of the Gateway to Default to guarantee that my app has only one instance of OrderGateway (I also confirmed that it has not any request scoped dependency that could bubble up and make it not default scoped). It did not work and I'm out of ideas. Does anyone know what could be happening? Thanks in advance
EDIT
As Gregorio suggested in the answers, I had to extend my adapter as explained in the docs, the following code worked for me
export class SocketIOAdapter extends IoAdapter {
private adapterConstructor: ReturnType<typeof createAdapter>;
constructor(private app: INestApplicationContext, private configService: ConfigService) {
super(app);
}
async connectToRedis(): Promise<void> {
const pubClient = createClient({ url: "redis://localhost:6379" });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
this.adapterConstructor = createAdapter(pubClient, subClient);
}
createIOServer(port: number, options: ServerOptions) {
const clientPort = parseInt(this.configService.getOrThrow("PORT"));
const cors = {
origin: [
`http://localhost:${clientPort}`,
new RegExp(`/^http:\/\/192\.168\.1\.([1-9]|[1-9]\d):${clientPort}$/`),
],
};
const optionsWithCORS: ServerOptions = {
...options,
cors,
};
const server: Server = super.createIOServer(port, optionsWithCORS);
const orderRepository = this.app.get(OrderRepository);
server
.adapter(this.adapterConstructor)
.of(`/orders`)
.use(createTokenMiddleware(orderRepository));
return server;
}
}
const createTokenMiddleware =
(orderRepository: OrderRepository) =>
async (socket: Socket, next) => {
// here I run some logic using my order repository
next();
} catch {
next(new Error("FORBIDDEN"));
}
};
}
and in my main.ts
const redisIoAdapter = new SocketIOAdapter(app, configService);
await redisIoAdapter.connectToRedis();
Have you tried following this page from the nest.js docs? I think it might help you in what you're looking for. You should write in your SocketIOAdapter what it says there in order to connect with Redis, it is not necessary to have the NotificationPublisherService or the NPController.
https://docs.nestjs.com/websockets/adapter

Webflux RSocket Server using RSocketRequestor sending message to Rsocket-js client .Responder not receiving

My requirement is that as and when some event happens on server, it should push updates to client. Want to use RSocket and not SSE.
Will responder configured on the rsocket-js client respond to server request?
If yes then how should the rsocket-js responder be configured to accept message on a particular route? That part please clarify.
Not sure if my spring service is correct.
My Webflux Spring Boot RSocket server code-
#Service
#RequiredArgsConstructor
public class RsocketService {
private final RSocketRequester rSocketRequester;
public void serverToClientRequest(){
Mono.just("Your request is completed at "+ LocalDateTime.now())
.delayElement(Duration.ofSeconds(ThreadLocalRandom.current().nextInt(5, 10)))
.flatMap(m -> rSocketRequester.route("request.stream").data(m).send())
.subscribe();
}
}
I have a Controller -
#MessageMapping("request.stream")
public Flux<String> requestStream(#Payload String requestData) {
}
Client side I am using RSocketWebSocketClient from 'rsocket-websocket-client';
const client = new RSocketClient({
responder: new EchoResponder(),
transport: new RSocketWebSocketClient(
{
url: 'ws://localhost:7000/rsocket',
wsCreator: (url: string) => new WebSocket(url),
debug: true,
}
),
setup: {
dataMimeType: "text/plain",
metadataMimeType: 'message/x.rsocket.routing.v0',
keepAlive: 600000,
lifetime: 180000,
}
});
My reactjs component-
async componentDidMount() {
const rsocket= await client.connect();
console.log('rsocket client connected');
rsocket
.requestStream({
data: "client message",
metadata: String.fromCharCode('request.stream'.length) + 'request.stream'
})
.subscribe({
onComplete: () => {
console.log("request stream completed");
},
onNext: value => {
console.log("on next-->got data from sever");
console.log(value.data);
},
onError: (error: any) => {
console.log("got error with requestResponse");
console.error(error);
},
onSubscribe: sub => {
console.log("subscribe request Stream!");
sub.request(2147483647);
}
});
}
EchoResponder is taken from https://github.com/rsocket/rsocket-js/blob/master/packages/rsocket-examples/src/LeaseClientExample.js. But the responder does not get any message. Any help is appreciated please.

rsocket-js routing fireAndForget to Spring Boot #MessageMapping

As I understand RSocket-JS supports routing messages using encodeCompositeMetadata and encodeRoute, however, I cannot get the server to accept a fireAndForget message. The server constantly logs the following message:
o.s.m.r.a.support.RSocketMessageHandler : No handler for fireAndForget to ''
This is the server mapping I am trying to trigger:
#Controller
public class MockController {
private static final Logger LOGGER = LoggerFactory.getLogger(MockController.class);
#MessageMapping("fire-and-forget")
public Mono<Void> fireAndForget(MockData mockData) {
LOGGER.info("fireAndForget: {}", mockData);
return Mono.empty();
}
}
This is the TypeScript code that's trying to make the connection:
client.connect().subscribe({
onComplete: socket => {
console.log("Connected to socket!")
socket.fireAndForget({
data: { someData: "Hello world!" },
metadata: encodeCompositeMetadata([[MESSAGE_RSOCKET_ROUTING, encodeRoute("fire-and-forget")]])
});
},
onError: error => console.error(error),
onSubscribe: cancel => {/* call cancel() to abort */ }
});
I've also tried adding the route in other ways (metadata: String.fromCharCode('route'.length)+'route') I found on the internet, but none seem to work.
What do I need to do to format the route in a way that the Spring Boot server recognizes it and can route the message correctly?
Binary only communication when using CompositeMetadata
Please make sure that you have configured your ClientTransport with binary codecs as follows:
new RSocketWebSocketClient(
{
url: 'ws://<host>:<port>'
},
BufferEncoders,
),
Having Binary encoders you will be able to properly send your routes using composite metadata.
Also, please make sure that you have configured metadataMimeType as:
...
const metadataMimeType = MESSAGE_RSOCKET_COMPOSITE_METADATA.string; // message/x.rsocket.composite-metadata.v0
new RSocketClient<Buffer, Buffer>({
setup: {
...
metadataMimeType,
},
transport: new RSocketWebSocketClient(
{
url: 'ws://<host>:<port>',
},
BufferEncoders,
),
});
Note, once you enabled BufferEncoders your JSONSeriallizer will not work and you would need to encode your JSON to binary yours selves ( I suggest doing that since in the future versions we will remove support of Serializers concept completely). Therefore, your request has to be adjusted as it is in the following example:
client.connect().subscribe({
onComplete: socket => {
console.log("Connected to socket!")
socket.fireAndForget({
data: Buffer.from(JSON.stringify({ someData: "Hello world!" })),
metadata: encodeCompositeMetadata([[MESSAGE_RSOCKET_ROUTING, encodeRoute("fire-and-forget")]])
});
},
onError: error => console.error(error),
onSubscribe: cancel => {/* call cancel() to abort */ }
});
Use #Payload annotation for your payload at spring backend
Also, to handle any data from the client and to let Spring know that the specified parameter argument is your incoming request data, you have to annotate it with the #Payload annotation:
#Controller
public class MockController {
private static final Logger LOGGER = LoggerFactory.getLogger(MockController.class);
#MessageMapping("fire-and-forget")
public Mono<Void> fireAndForget(#Payload MockData mockData) {
LOGGER.info("fireAndForget: {}", mockData);
return Mono.empty();
}
}

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

React and rSocket REQUEST_CHANNEL error with Spring Boot

We have a working demo between React and Spring Boot Data Geode using rSocket for fire & forget, request response and request stream but when we try and use request channel we get error:
org.springframework.messaging.MessageDeliveryException: Destination 'quotes' does not support REQUEST_CHANNEL. Supported interaction(s): [REQUEST_STREAM]
So far on web it looks like this ought to be possible from RSocket Git
It's a simple spring boot app with #Controller endpoints that run over rSocket like this:
#Controller
public class RSocketController {
private static final Logger log = LogManager.getLogger(RSocketController.class);
#Autowired
PriceService priceService;
#MessageMapping(value = "quotes")
public Flux<Quote> getQuotes() {
log.info("In getQuotes");
return priceService.generatePrices();
}
}
The generatePrices returns a Flux of prices which works fine in request stream but we would prefer to use request channel for bi-directional comms.
Client versions
"rsocket-core": "0.0.19"
"rsocket-flowable": "0.0.14"
"rsocket-tcp-client": "0.0.19"
"rsocket-websocket-client": "0.0.19"
Client code
const transport = new RSocketWebSocketClient(transportOptions);
const rSocketClient = new RSocketClient({serializers, setup, transport});
​
rSocketClient.connect().subscribe({
onComplete: socket => {
console.log('Client connected to the RSocket Server');
​
socket.requestChannel(Flowable.just({
data: 'foyss',
metadata: String.fromCharCode(6) + 'quotes'
})).subscribe({
onComplete: function() {
console.log(`Channel received end of server stream`);
},
onError: function(err) {
console.log("err", err);
},
onNext: payload => {
console.log(payload);
},
onSubscribe: function(subscription) {
console.log("got subscription");
subscription.request(0x7fffffff);
},
onError: error => {
console.log(error);
},
onSubscribe: cancel => {
console.log('onSubscribe cancel');
}})
},
onError: error => {
console.log(error);
},
onSubscribe: cancel => {
// console.log(cancel);
console.log('onSubscribe cancel');
}
})
Some JS libraries still don't support the request-channel model. Please check the official documentation for your JS lib, eg, for: https://www.npmjs.com/package/ng-rsocket-rxjs
Missing:
Lease Handling
Server Setup
Resume Support
Request Channel
.....

Resources