NestJS websocket Broadcast event to clients - websocket

I'm not able to send data from server NestJS to clients via websocket. Nothing is emitted.
My use case:
several clients connected to a server via websocket
client sends a message to the server via websocket
server broadcast the message to all client
My stack:
NestJS server with websocket
Angular client and other (like chrome extension for testing websockets)
My code:
simple-web-socket.gateway.ts:
import { SubscribeMessage, WebSocketGateway, WsResponse, WebSocketServer, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit } from '#nestjs/websockets';
#WebSocketGateway({ port: 9995, transports: ['websocket'] })
export class SimpleWebSocketGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
#WebSocketServer() private server: any;
wsClients=[];
afterInit() {
this.server.emit('testing', { do: 'stuff' });
}
handleConnection(client: any) {
this.wsClients.push(client);
}
handleDisconnect(client) {
for (let i = 0; i < this.wsClients.length; i++) {
if (this.wsClients[i].id === client.id) {
this.wsClients.splice(i, 1);
break;
}
}
this.broadcast('disconnect',{});
}
private broadcast(event, message: any) {
const broadCastMessage = JSON.stringify(message);
for (let c of this.wsClients) {
c.emit(event, broadCastMessage);
}
}
#SubscribeMessage('my-event')
onChgEvent(client: any, payload: any) {
this.broadcast('my-event',payload);
}
}
main.ts:
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { WsAdapter } from '#nestjs/websockets';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new WsAdapter());
await app.listen(3000);
}
bootstrap();
app.module.ts:
import { Module } from '#nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SimpleWebSocketGateway } from 'simple-web-socket/simple-web-socket.gateway';
#Module({
imports: [],
controllers: [AppController],
providers: [AppService, SimpleWebSocketGateway],
})
export class AppModule {}
Additionnal Informations:
Client emiting (with code line c.emit(event, broadCastMessage);) return false.
I suspect an error in the framework as my usage is quite simple. But I want to double-check with the community here if I'm doing something wrong.

As mentionned in the previous comment, c.send() works fine with the following snippet:
import { SubscribeMessage, WebSocketGateway, WsResponse, WebSocketServer, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit } from '#nestjs/websockets';
#WebSocketGateway({ port: 9995, transports: ['websocket'] })
export class SimpleWebSocketGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
#WebSocketServer() private server: any;
wsClients=[];
afterInit() {
this.server.emit('testing', { do: 'stuff' });
}
handleConnection(client: any) {
this.wsClients.push(client);
}
handleDisconnect(client) {
for (let i = 0; i < this.wsClients.length; i++) {
if (this.wsClients[i] === client) {
this.wsClients.splice(i, 1);
break;
}
}
this.broadcast('disconnect',{});
}
private broadcast(event, message: any) {
const broadCastMessage = JSON.stringify(message);
for (let c of this.wsClients) {
c.send(event, broadCastMessage);
}
}
#SubscribeMessage('my-event')
onChgEvent(client: any, payload: any) {
this.broadcast('my-event',payload);
}
}

I tried to broadcast a message to all connected clients using client.send():
broadcastMessage(event: string, payload: any) {
for (const client of this.clients) {
client.send(event, payload);
}
}
testBroadcast() {
this.broadcastMessage('msgInfo', { name: 'foo' });
}
the data sent ended up looking like this:
["message", "msgInfo", { "name": "foo" }]
The above did not work for me, so instead I used the client.emit() which works fine:
broadcastMessage(event: string, payload: any) {
for (const client of this.clients) {
client.emit(event, payload);
}
}
now the data looks like this:
["msgInfo", { "name": "foo" }]

With socket.io we could use socket.broadcast.emit()?:
#SubscribeMessage('my-event')
onChgEvent(
#MessageBody() message: any,
#ConnectedSocket() socket: Socket,
): void {
socket.broadcast.emit('my-event', message);
}

Related

Format request before send by ClientProxy

I use microservices and RabbitMQ as a transporter. My microservices communicate with each other, so one of them can send message to another one. I use the following way to send messages:
await this.client.send(SOME_COMMAND, obj).toPromise();
Now I need to format objects I send in all requests to any microservice in one place. For example add reqId, or serialize Map. Is it possible?
1. Per controller solution. here for simplicity I removed the handler part:
#Controller()
export class AppController {
constructor(#Inject('MATH_SERVICE') private client: ClientProxy) {
let send = client.send.bind(client);
client.send = function (pattern, payload) {
return send(pattern, { payload, systemWideProp: ""})
}
}
sum() {
this.client.send(COMMAND, obj)
}
}
2. This could be as a provider for injecting it on each controller you want using your rabbitmq client service:
custom-client-proxy.ts
import { Inject, Injectable } from '#nestjs/common';
import { ClientProxy } from '#nestjs/microservices';
#Injectable()
export class CustomClientProxy {
constructor(#Inject('MATH_SERVICE') private client: ClientProxy) { }
send(pattern, payload) {
// payload and pattern manipulations such as:
// payload.say = "Hi";
// const scopePattern = { cmd: `${pattern.cmd}_dev` };
return this.client.send(pattern, payload)
}
}
app.module.ts
import { Module } from '#nestjs/common';
import { ClientsModule, Transport } from '#nestjs/microservices';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CustomClientProxy } from './custom-client-proxy';
#Module({
imports: [ClientsModule.register([{
name: 'MATH_SERVICE',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'math_queue'
},
}])],
controllers: [AppController],
providers: [AppService, CustomClientProxy],
exports: [CustomClientProxy] // export here for other modules of your app
})
export class AppModule { }
app.controller.ts
also any controller you have imported under app's modules
#Controller()
export class AppController {
constructor(private client: CustomClientProxy) {}
sum() {
this.client.send(COMMAND, obj)
}
}
3. When you need to use this functionality for more than one queue:
// custom-client-proxy.ts
#Injectable()
export class CustomClientProxy {
constructor() { }
send(client: ClientProxy, pattern, payload) {
// payload and pattern manipulations such as:
// payload.say = "Hi";
// const scopePattern = { cmd: `${pattern.cmd}_dev` };
return client.send(pattern, payload)
}
}
// app.module.ts
#Module({
imports: [ClientsModule.register([{
name: 'MATH_SERVICE',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'math_queue'
},
},
{
name: 'MESSAGE_SERVICE',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'message_queue',
},
}])],
controllers: [AppController],
providers: [AppService, CustomClientProxy],
exports: [CustomClientProxy]
})
export class AppModule { }
// app.controller.ts
#Controller()
export class AppController {
constructor(
#Inject('MATH_SERVICE') private mathClient: ClientProxy,
#Inject('MESSAGE_SERVICE') private messageClient: ClientProxy,
private client: CustomClientProxy) {}
sum(a) {
return this.client.sendTo(this.mathClient, pattern, payload);
}
}
The answer is pretty simple: you need to pass custom Serializer to the ClientProviderOptions and you can implement everything there

Angular, error 500 after sending the request in the header

I have a hard time passing the right angular request to the header. This is my service:
import { Injectable } from '#angular/core';
import { HttpClient, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpHeaders }
from '#angular/common/http';
import { Utente } from '../model/Utente ';
import { Prodotto } from '../model/Prodotto ';
import { OktaAuthService } from '#okta/okta-angular';
import { Observable, from } from 'rxjs';
import { Carrello } from '../model/Carrello ';
import { userInfo } from 'node:os';
import { getLocaleCurrencyCode } from '#angular/common';
const headers = new HttpHeaders().set('Accept', 'application/json');
#Injectable({
providedIn: 'root'
})
export class HttpClientService {
constructor(
private httpClient:HttpClient, private oktaAuth:OktaAuthService ) {}
getCarr(){
return this.httpClient.get<Carrello[]>('http://localhost:8080/prodotti/utente/vedicarrelloo', {headers} );
}
}
This is my spring method:
#Transactional(readOnly = true)
public List<Carrello> getCarrello(#AuthenticationPrincipal OidcUser utente){
Utente u= utenteRepository.findByEmail(utente.getEmail());
return carrelloRepository.findByUtente(u);
}
In console I get this error (error 500):
https://i.stack.imgur.com/BiONS.png
this error corresponds in my console to "java.lang.NullPointerException: null.
But if I access localhost: 8080, I can see the answer correctly, so I assume there is a problem in passing the request header in angular, can anyone tell me where am I wrong, please? I specify that I get this error only in the methods where the OidcUser is present, the rest works perfectly. Thank you!
You need to send an access token with your request. Like this:
import { Component, OnInit } from '#angular/core';
import { OktaAuthService } from '#okta/okta-angular';
import { HttpClient } from '#angular/common/http';
import sampleConfig from '../app.config';
interface Message {
date: string;
text: string;
}
#Component({
selector: 'app-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {
failed: Boolean;
messages: Array<Message> [];
constructor(public oktaAuth: OktaAuthService, private http: HttpClient) {
this.messages = [];
}
async ngOnInit() {
const accessToken = await this.oktaAuth.getAccessToken();
this.http.get(sampleConfig.resourceServer.messagesUrl, {
headers: {
Authorization: 'Bearer ' + accessToken,
}
}).subscribe((data: any) => {
let index = 1;
const messages = data.messages.map((message) => {
const date = new Date(message.date);
const day = date.toLocaleDateString();
const time = date.toLocaleTimeString();
return {
date: `${day} ${time}`,
text: message.text,
index: index++
};
});
[].push.apply(this.messages, messages);
}, (err) => {
console.error(err);
this.failed = true;
});
}
}
On the Spring side, if you want it to accept a JWT, you'll need to change to use Jwt instead of OidcUser. Example here.
#GetMapping("/")
public String index(#AuthenticationPrincipal Jwt jwt) {
return String.format("Hello, %s!", jwt.getSubject());
}

Socket IO and Angular duplicate issue

I am getting duplicate issue when moving from one room to another. I use websocket in nestjs backend project and angular as a client.
Simply, I am trying to move from one room to another but socket.fromEvent returns duplicates and duplicates increase once I login to another rooms.
Server
import {
SubscribeMessage,
WebSocketGateway,
OnGatewayInit,
WebSocketServer,
OnGatewayConnection,
OnGatewayDisconnect,
} from '#nestjs/websockets';
import { Socket, Server } from 'socket.io';
#WebSocketGateway({ transports: ['websocket', 'polling'] })
export class AppGateway implements OnGatewayInit {
#WebSocketServer() wss: Server;
afterInit(server: any) {}
#SubscribeMessage('send-message')
handleMessage(client: Socket, payload: any) {
this.wss.to(payload.userroom).emit('message', { msg: payload.text, when: payload.when, user: payload.user })
}
#SubscribeMessage('join')
handleConnection(client: Socket, room: string) {
client.join(room)
}
#SubscribeMessage('leave')
handleDisconnect(client: Socket, room: string) {
client.leave(room)
}
}
Client
// connect, leave & join when moving from room to another
getChatMessagesAndStartSession(sessionId: number, userChat: number) {
this.clearChatContent();
this.activeSessionId = sessionId;
this.activeChat = userChat;
this.messages = messages;
this.socket.connect()
this.socket.emit('leave', this.beforesession);
this.socket.emit('join', this.activeSessionId);
this.beforesession = this.activeSessionId;
this.socket.fromEvent('message').subscribe((message: any) => {
console.log(message)
this.messages.push(message);
this.scrollToBottom();
});
}
// send message to
sendSocketMessage() {
this.socket.emit('send-message', { text: this.respondMessage, when: new Date, user: this.user_id })
}
Result:

Connect NestJS to a websocket server

How can NestJS be use as a websocket client? I want to connect to a remote websocket server as a client using NestJS, but I didn't find any information about this implementation in the framework.
As Nestjs is simply a framework for Nodejs, so you need to find an NPM package that supports Websocket. For example, I use ws with #types/ws type definition, and create a Websocket client as a Nestjs service class:
// socket-client.ts
import { Injectable } from "#nestjs/common";
import * as WebSocket from "ws";
#Injectable()
export class WSService {
// wss://echo.websocket.org is a test websocket server
private ws = new WebSocket("wss://echo.websocket.org");
constructor() {
this.ws.on("open", () => {
this.ws.send(Math.random())
});
this.ws.on("message", function(message) {
console.log(message);
});
}
send(data: any) {
this.ws.send(data);
}
onMessage(handler: Function) {
// ...
}
// ...
}
// app.module.ts
import { Module } from "#nestjs/common";
import { WSService } from "./socket-client";
#Module({
providers: [WSService]
})
export class AppModule {}
I try it by another way. I write an adapter with socket.io-client. Then use this adapter in boostrap by method useWebSocketAdapter. After that i can write handle websocket event in gateway like the way working with socket server (use decorator #SubscribeMessage)
My Adapter file
import { WebSocketAdapter, INestApplicationContext } from '#nestjs/common';
import { MessageMappingProperties } from '#nestjs/websockets'
import * as SocketIoClient from 'socket.io-client';
import { isFunction, isNil } from '#nestjs/common/utils/shared.utils';
import { fromEvent, Observable } from 'rxjs';
import { filter, first, map, mergeMap, share, takeUntil } from 'rxjs/operators';
export class IoClientAdapter implements WebSocketAdapter {
private io;
constructor(private app: INestApplicationContext) {
}
create(port: number, options?: SocketIOClient.ConnectOpts) {
const client = SocketIoClient("http://localhost:3000" , options || {})
this.io = client;
return client;
}
bindClientConnect(server: SocketIOClient.Socket, callback: Function) {
this.io.on('connect', callback);
}
bindClientDisconnect(client: SocketIOClient.Socket, callback: Function) {
console.log("it disconnect")
//client.on('disconnect', callback);
}
public bindMessageHandlers(
client: any,
handlers: MessageMappingProperties[],
transform: (data: any) => Observable<any>,
) {
const disconnect$ = fromEvent(this.io, 'disconnect').pipe(
share(),
first(),
);
handlers.forEach(({ message, callback }) => {
const source$ = fromEvent(this.io, message).pipe(
mergeMap((payload: any) => {
const { data, ack } = this.mapPayload(payload);
return transform(callback(data, ack)).pipe(
filter((response: any) => !isNil(response)),
map((response: any) => [response, ack]),
);
}),
takeUntil(disconnect$),
);
source$.subscribe(([response, ack]) => {
if (response.event) {
return client.emit(response.event, response.data);
}
isFunction(ack) && ack(response);
});
});
}
public mapPayload(payload: any): { data: any; ack?: Function } {
if (!Array.isArray(payload)) {
return { data: payload };
}
const lastElement = payload[payload.length - 1];
const isAck = isFunction(lastElement);
if (isAck) {
const size = payload.length - 1;
return {
data: size === 1 ? payload[0] : payload.slice(0, size),
ack: lastElement,
};
}
return { data: payload };
}
close(server: SocketIOClient.Socket) {
this.io.close()
}
}
main.js
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import {IoClientAdapter} from './adapters/ioclient.adapter'
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new IoClientAdapter(app))
await app.listen(3006);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
then Gateway
import {
MessageBody,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
WsResponse,
} from '#nestjs/websockets';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Server } from 'socket.io';
#WebSocketGateway()
export class EventsGateway {
#WebSocketServer()
server: Server;
#SubscribeMessage('hello')
async identity(#MessageBody() data: number): Promise<number> {
console.log(data)
return data;
}
}
It a trick, but look so cool. Message handler can write more like nestjs style.

Error during WebSocket handshake: Unexpected response code: 502 in angular

I've followed the tutorial from https://tutorialedge.net/typescript/angular/angular-websockets-tutorial/
as I'm trying to setup a websocket connection but I'm getting an error after the socket gets connected. The error says "Error during WebSocket handshake: Unexpected response code: 502"
I've followed the tutorial and followed the steps but i still get the error after the socket gets connected.
websocket.service.ts
import { Injectable } from '#angular/core';
import * as Rx from 'rxjs/Rx';
#Injectable({
providedIn: 'root'
})
export class WebsocketService {
constructor() { }
private subject: Rx.Subject<MessageEvent>;
public connect(url): Rx.Subject<MessageEvent> {
if (!this.subject) {
this.subject =
this.create('ws://61c725fa.ngrok.io/auth/forgot_password/');
console.log("Successfully connected: " + url);
}
return this.subject;
}
private create(url): Rx.Subject<MessageEvent> {
let ws = new WebSocket(url);
let observable = Rx.Observable.create((obs:
Rx.Observer<MessageEvent>) => {
ws.onmessage = obs.next.bind(obs);
ws.onerror = obs.error.bind(obs);
ws.onclose = obs.complete.bind(obs);
return ws.close.bind(ws);
});
let observer = {
next: (data: Object) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
}
};
return Rx.Subject.create(observer, observable);
}
}
notifier.service.ts
import { Injectable } from '#angular/core';
import { Observable, Subject } from "rxjs-compat/Rx";
import { WebsocketService } from "./websocket.service";
import { map } from 'rxjs/operators';
let ws_scheme = window.location.protocol == "https:" ? "wss" :
"ws";
let ws_path = ws_scheme + '://' +"61c725fa.ngrok.io" +
"/auth/forgot_password/";
const URL = ws_path;
export interface Message {
username: string;
}
#Injectable({
providedIn: 'root'
})
export class NotifierService {
public messages: Subject<Message>;
constructor(wsService: WebsocketService) {
this.messages = <Subject<Message>>wsService
.connect(URL).pipe(map((response: MessageEvent): Message => {
let data = JSON.parse(response.data);
return {
username: data.message
}
}));
}
}
component.ts
import { Component } from '#angular/core';
import { NotifierService } from './notifier.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'ang-socket';
constructor(private notifier:NotifierService){
notifier.messages.subscribe(msg=>{
console.log("response from websocket:" + msg);
})
}
private message = {
username: "abc#gmail.com"
};
sendMsg() {
console.log("new message from client to websocket: ",
this.message);
this.notifier.messages.next(this.message);
this.message.username = "";
}
}
I would like to fix this issue. Great if someone helps.

Resources