Connect NestJS to a websocket server - websocket

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.

Related

How to pass a dynamic port to the Websockets-gateway in NestJS?

I wanted to dynamically set the Websockets-gateway port from config in NestJS. Below is my websockets-gateway code.
import { WebSocketGateway } from '#nestjs/websockets';
const WS_PORT = parseInt(process.env.WS_PORT);
#WebSocketGateway(WS_PORT)
export class WsGateway {
constructor() {
console.log(WS_PORT);
}
}
But the WS_PORT is always NaN.
This is my bootstrap function insdie main.ts :
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: false });
const configService = app.get(ConfigService);
initAdapters(app);
await app.listen(configService.get(HTTP_PORT), () => {
console.log('Listening on port ' + configService.get(HTTP_PORT));
});
}
Below is my app.module.ts :
#Module({
imports: [
ConfigModule.forRoot({
envFilePath: './src/config/dev.env',
isGlobal: true,
}),
RedisModule,
SocketStateModule,
RedisPropagatorModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>(JWT_SECRET_KEY),
}),
inject: [ConfigService],
}),
],
controllers: [AppController],
providers: [WsGateway, AppService],
})
export class AppModule {}
I put a console log in the Gateway constructor to print the value of 'WS_PORT' but it's always NaN.
[Nest] 13252 - 10/04/2021, 5:05:34 PM LOG [NestFactory] Starting Nest application...
NaN
Thanks in advance.
I could not find a way to add dynamic data to the decorator. So to be able to dynamically choose the port and other configurations I had to:
Create an adapter for socket-io:
Tell NestJs to use the new adapter
SocketIoAdapter.ts
import { INestApplicationContext } from '#nestjs/common';
import { IoAdapter } from '#nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { ConfigService } from '#nestjs/config';
export class SocketIoAdapter extends IoAdapter {
constructor(
private app: INestApplicationContext,
private configService: ConfigService,
) {
super(app);
}
createIOServer(port: number, options?: ServerOptions) {
port = this.configService.get<number>('SOCKETIO.SERVER.PORT');
const path = this.configService.get<string>('SOCKETIO.SERVER.PATH');
const origins = this.configService.get<string>(
'SOCKETIO.SERVER.CORS.ORIGIN',
);
const origin = origins.split(',');
options.path = path;
options.cors = { origin };
const server = super.createIOServer(port, options);
return server;
}
}
Now, you need to edit the main.ts to use the adapter
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '#nestjs/config';
import { SocketIoAdapter } from './socket-io/socket-io.adapter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const hosts = configService.get<string>('CORS.HOST');
const hostsArray = hosts.split(',');
app.enableCors({
origin: hostsArray,
credentials: true,
});
//Here you use the adapter and sent the config service
app.useWebSocketAdapter(new SocketIoAdapter(app, configService));
await app.listen(4300);
}
bootstrap();
In this case I set the port and the cors origin, here an example of the conf file (using .env)
env.local
SOCKETIO.SERVER.PORT=4101
SOCKETIO.SERVER.PATH=
SOCKETIO.SERVER.CORS.ORIGIN=http://localhost:4200,http://localhost.com:8080
Here a link to config service Config Service NestJs
You can do it relatively straightforward if you decorate the Gateway before app.init is called:
Import the class in main.ts
Get an instance of your ConfigurationService
Manually call the decorator on the class with the config data
function decorateGateway(class_, config) {
// Just calling the decorator as a function with the class
// as argument does the same as `#WebSocketGateway`
WebSocketGateway({
cors: {
origin: config.get("websocket.cors.origin"),
}
})(class_)
}
async function bootstrap() {
const app = await NestFactory.create(AppModule, {});
const config = app.get(ConfigService);
decorateGateway(ChatGateway, config);
...
app.init();
}
The tricky part with a Gateway is that it starts up together with the server, and the decorator metadata needs to be applied to the class earlier than for other components. You can do this in main.ts before app.init.
port = this.configService.get<number>('SOCKETIO.SERVER.PORT');
In my case I found its return string(from .env), port gets 'string' but not 'number',
but if put parseInt(this.configService.get<number>('SOCKETIO.SERVER.PORT'), 10);
then it is ok
Mind that socket-io ports must be the same on server & client side

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());
}

How to hook up a Long running Process in NestJS with ws ( Websockets ) and RxJs

I want to run code in a function - and then return as a websocket Observable. Effectively monitoring a long running process. I can not figure out how to return the values correctly through the websockets in this format.
My long-running process: ( obviously not going to actually take a long time )
import { Observable } from 'rxjs';
export function longRunningProcess (): Observable<unknown> {
return new Observable(subscriber => {
subscriber.next('End of step 1');
subscriber.next('End of step 2');
subscriber.next('End of step 3');
setTimeout(() => {
subscriber.next('End of Step 4');
subscriber.complete();
}, 1000);
});
}
My NestJS endpoint that returns to the ws ( Websocket )
import { WsAdapter } from '#nestjs/platform-ws';
import {
MessageBody,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
WsResponse,
} from '#nestjs/websockets';
import { from, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { Server } from 'ws';
import { longRunningProcess } from './test'
#WebSocketGateway()
export class EventsGateway {
#WebSocketServer()
server: Server;
#SubscribeMessage('events')
// send {"event":"events","data":"test"} in websockets
findAll (#MessageBody() data: any): Observable<WsResponse<unknown>> {
return from(longRunningProcess) // Not really sure how to return this
//return from([1, 2, 3]).pipe(map(item => ({ event: 'events', data: item }))); //<< this works from the sample
}
#SubscribeMessage('identity')
async identity (#MessageBody() data: number): Promise<number> {
return data;
}
}
just map your result from the longRunningProcess like you've did for the numbers array.
#SubscribeMessage('events')
findAll (#MessageBody() data: any): Observable<WsResponse<unknown>> {
return longRunningProcess().pipe(map(item => ({ event: 'events', data: item })));
}

Nest JS Microservice TCP E2E Test

does someone know how to write E2E test for nest microservices? Giving this code?
main.ts
import { NestFactory } from '#nestjs/core';
import { Transport } from '#nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice(AppModule, {
transport: Transport.TCP,
});
app.listen(() => console.log('Microservice is listening'));
}
bootstrap();
app.controller.ts
import { Controller } from '#nestjs/common';
import { MessagePattern } from '#nestjs/microservices';
#Controller()
export class MathController {
#MessagePattern({ cmd: 'sum' })
accumulate(data: number[]): number {
return (data || []).reduce((a, b) => a + b);
}
}
This should work for you:
import { INestApplication } from '#nestjs/common';
import { Test, TestingModule } from '#nestjs/testing';
import { ClientsModule, Transport, ClientProxy } from '#nestjs/microservices';
import * as request from 'supertest';
import { Observable } from 'rxjs';
describe('Math e2e test', () => {
let app: INestApplication;
let client: ClientProxy;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
MathModule,
ClientsModule.register([
{ name: 'MathService', transport: Transport.TCP },
]),
],
}).compile();
app = moduleFixture.createNestApplication();
app.connectMicroservice({
transport: Transport.TCP,
});
await app.startAllMicroservicesAsync();
await app.init();
client = app.get('MathService');
await client.connect();
});
afterAll(async () => {
await app.close();
client.close();
});
it('test accumulate', done => {
const response: Observable<any> = client.send(
{ cmd: 'sum' },
{ data: [1, 2, 3] },
);
response.subscribe(sum=> {
expect(sum).toBe(6);
done();
});
});
});

NestJS websocket Broadcast event to clients

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);
}

Resources