Socket IO and Angular duplicate issue - websocket

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:

Related

Could anyone provide a fastapi websocket endpoint which could connect with the example given for RTK Query streaming updates

I'm trying to get my head around RTK Query as it applies to websockets. The example given is
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react'
import { createEntityAdapter, EntityState } from '#reduxjs/toolkit'
import { isMessage } from './schemaValidators'
export type Channel = 'redux' | 'general'
export interface Message {
id: number
channel: Channel
userName: string
text: string
}
const messagesAdapter = createEntityAdapter<Message>()
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getMessages: build.query<EntityState<Message>, Channel>({
query: (channel) => `messages/${channel}`,
transformResponse(response: Message[]) {
return messagesAdapter.addMany(
messagesAdapter.getInitialState(),
response
)
},
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
const ws = new WebSocket('ws://localhost:8080')
try {
await cacheDataLoaded
const listener = (event: MessageEvent) => {
const data = JSON.parse(event.data)
if (!isMessage(data) || data.channel !== arg) return
updateCachedData((draft) => {
messagesAdapter.upsertOne(draft, data)
})
}
ws.addEventListener('message', listener)
} catch {}
await cacheEntryRemoved
ws.close()
},
}),
}),
})
export const { useGetMessagesQuery } = api
for the frontend. It looks as though the idea is to make a request to /messages/{channel} and on successful receipt and caching of these messages to connect to a websocket api. I'm struggling to create a fastapi app that connects with this example so I can figure out the workings. Does anyone have an example they might be willing to please share?

Angular 9 - how to properly encode + sign in URL parameter

I spent many hours without success. I know it's a common problem, many solutions but for me works only Interceptor that I want to avoid.
My service - here I get email with plus like john.doe+100#gmail.com
#Injectable({
providedIn: 'root',
})
export class UsersHttpService {
httpParams = new HttpParams({encoder: new CustomEncoder()});
removeUsersFromGroup(groupId: string, email: string): Observable<any> {
console.log(email); //john.doe+100#gmail.com
let parsedEmail = encodeURI(email); //one of many attempts
return this.http.delete(`${this.env.URI}/monitoring/api/v1/groups/${groupId}/users/`, {
params: {
groupId,
email: email.replace(' ', '+')
},
});
}
And my CustomEncoder:
export class CustomEncoder implements HttpParameterCodec {
encodeKey(key: string): string {
return encodeURIComponent(key);
}
encodeValue(value: string): string {
// console.log('encodeValue encodeValue');
// console.log(value);
// console.log(encodeURIComponent(value));
return encodeURIComponent(value);
}
decodeKey(key: string): string {
return decodeURIComponent(key);
}
decodeValue(value: string): string {
// console.log('decodeValue decodeValue');
// console.log(value);
// console.log(decodeURIComponent(value));
return decodeURIComponent(value);
}
}
When I send request from Angular then in the Network tab in web browser I see:
DELETE https://myapp/groups/d39a4f50-8ebd-11ea-a9ae-5103b15ad73b/users/?groupId=d39a4f50-8ebd-11ea-a9ae-5103b15ad73b&email=john.doe 100#gmail.com
with a space! What's wrong? Were is the problem? IN the console I get email with + but in Network tab without space instead of + sign.
My params are properly encoded (there is 200 status from backend (spring boot), email with +) ONLY when I use global interceptor (which should be avoided):
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpParams,
HttpRequest,
} from "#angular/common/http";
import {Injectable} from "#angular/core";
import {Observable} from "rxjs";
import {CustomEncoder} from "./customEncoder";
#Injectable()
export class EncodeHttpParamsInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const params = new HttpParams({
encoder: new CustomEncoder(),
fromString: req.params.toString(),
});
return next.handle(req.clone({params}));
}
}
Does anyone have any idea??? I tried to use:
return this.http.delete(${this.env.ORBITAL_URI}/monitoring/api/v1/groups/${groupId}/users/, {
params: {
groupId,
email: encodeURI(email) //or encodeURIComponent(email)
},
});
and then in Network tab I see something like john.doe%2B%40gmail.com but I get 500 error from backend
My solution - without any interceptor:
removeUsersFromGroup(groupId: string, email: string): Observable<any> {
const params = new HttpParams({
encoder: new CustomEncoder(),
fromObject: {
groupId,
email,
},
});
return this.http.delete(`${this.env.URI}/myapp/v1/groups/${groupId}/users/`, {
params: params,
});
}
Now it works as expected:)

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.

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

Handling Nest.js app on Redis cache store disconnection

When I setup Redis as cache store like:
import { Module, CacheModule } from '#nestjs/common';
import * as redisStore from 'cache-manager-redis-store';
#Module({
imports: [
CacheModule.register({
store: redisStore,
host: 'localhost',
port: 6379,
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
It works as expected. However, if for some reason the connection to Redis goes down, the whole application will crash:
Error: Redis connection to localhost:6379 failed - connect ECONNREFUSED
127.0.0.1:6379
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1113:14)
How can I gracefully handle these errors or even try to reconnect?
Ok, found it myself. I need to inject the exposed CACHE_MANAGER in the module constructor to get my hands on the client:
import { CACHE_MANAGER, Inject } from '#nestjs/common';
...
export class AppModule {
constructor(#Inject(CACHE_MANAGER) cacheManager) {
const client = cacheManager.store.getClient();
client.on('error', (error) => {
console.info(error);
});
}
}
you can handle or you can check connections whether is connected or not. you can also use nestjs/bull module Click here
Redis Service Ready = queue.clients[0].status === 'ready'
Redis Service Disconnect = queue.clients[0].status === 'reconnecting'
import { InjectQueue } from '#nestjs/bull';
import { BadRequestException, HttpException, HttpStatus, Injectable, Logger } from '#nestjs/common';
import { Queue } from 'bull';
import CONFIG from 'src/config/config';
import { CreateEmailServiceDto } from './dto/create-email-service.dto';
#Injectable()
export class EmailServiceService {
private readonly logger = new Logger(this.constructor.name);
constructor(
#InjectQueue(CONFIG.REDIS_QUEUE_NAME)
private mailQueue: Queue,
) {
}
async sendConfirmationEmail(user: CreateEmailServiceDto): Promise<any> {
try {
const queue: any = this.mailQueue;
if (queue.clients[0].status === 'reconnecting') {
throw new HttpException("Redis Service is unavailable!",
HttpStatus.SERVICE_UNAVAILABLE);
}
await this.mailQueue
.add('confirmation', {
user,
});
return {
message: 'Email Sent Successfully!',
status: 201,
};
} catch (error) {
console.log(error);
this.logger.error(`Error queueing confirmation email to user
${user.to}`);
if (error?.response && error?.status)
throw new HttpException(error.response, error.status);
throw new BadRequestException(error);
}
}
}
You can use the redisCache class as follows
redisCache.set('foo', 'bar', { ttl: ttl }, (err) => {
if (err) {
throw err;
}
redisCache.get('foo', (err, result) => {
console.log(result);
// >> 'bar'
redisCache.del('foo', (err) => {
});
});
});

Resources