NestJs #Sse - event is consumed only by one client - nestjs

I tried the sample SSE application provided with nest.js (28-SSE), and modified the sse endpoint to send a counter:
#Sse('sse')
sse(): Observable<MessageEvent> {
return interval(5000).pipe(
map((_) => ({ data: { hello: `world - ${this.c++}` }} as MessageEvent)),
);
}
I expect that each client that is listening to this SSE will receive the message, but when opening multiple browser tabs I can see that each message is consumed only by one browser, so if I have three browsers open I get the following:
How can I get the expected behavior?

To achieve the behavior you're expecting you need to create a separate stream for each connection and push the data stream as you wish.
One possible minimalistic solution is below
import { Controller, Get, MessageEvent, OnModuleDestroy, OnModuleInit, Res, Sse } from '#nestjs/common';
import { readFileSync } from 'fs';
import { join } from 'path';
import { Observable, ReplaySubject } from 'rxjs';
import { map } from 'rxjs/operators';
import { Response } from 'express';
#Controller()
export class AppController implements OnModuleInit, OnModuleDestroy {
private stream: {
id: string;
subject: ReplaySubject<unknown>;
observer: Observable<unknown>;
}[] = [];
private timer: NodeJS.Timeout;
private id = 0;
public onModuleInit(): void {
this.timer = setInterval(() => {
this.id += 1;
this.stream.forEach(({ subject }) => subject.next(this.id));
}, 1000);
}
public onModuleDestroy(): void {
clearInterval(this.timer);
}
#Get()
public index(): string {
return readFileSync(join(__dirname, 'index.html'), 'utf-8').toString();
}
#Sse('sse')
public sse(#Res() response: Response): Observable<MessageEvent> {
const id = AppController.genStreamId();
// Clean up the stream when the client disconnects
response.on('close', () => this.removeStream(id));
// Create a new stream
const subject = new ReplaySubject();
const observer = subject.asObservable();
this.addStream(subject, observer, id);
return observer.pipe(map((data) => ({
id: `my-stream-id:${id}`,
data: `Hello world ${data}`,
event: 'my-event-name',
}) as MessageEvent));
}
private addStream(subject: ReplaySubject<unknown>, observer: Observable<unknown>, id: string): void {
this.stream.push({
id,
subject,
observer,
});
}
private removeStream(id: string): void {
this.stream = this.stream.filter(stream => stream.id !== id);
}
private static genStreamId(): string {
return Math.random().toString(36).substring(2, 15);
}
}
You can make a separate service for it and make it cleaner and push stream data from different places but as an example showcase this would result as shown in the screenshot below

This behaviour is correct. Each SSE connection is a dedicated socket and handled by a dedicated server process. So each client can receive different data.
It is not a broadcast-same-thing-to-many technology.
How can I get the expected behavior?
Have a central record (e.g. in an SQL DB) of the desired value you want to send out to all the connected clients.
Then have each of the SSE server processes watch or poll that central record
and send out an event each time it changes.

you just have to generate a new observable for each sse connection of the same subject
private events: Subject<MessageEvent> = new Subject();
constuctor(){
timer(0, 1000).pipe(takeUntil(this.destroy)).subscribe(async (index: any)=>{
let event: MessageEvent = {
id: index,
type: 'test',
retry: 30000,
data: {index: index}
} as MessageEvent;
this.events.next(event);
});
}
#Sse('sse')
public sse(): Observable<MessageEvent> {
return this.events.asObservable();
}
Note: I'm skipping the rest of the controller code.
Regards,

Related

How to add JSON data to Jetstream strams?

I have a code written in node-nats-streaming and trying to convert it to newer jetstream. A part of code looks like this:
import { Message, Stan } from 'node-nats-streaming';
import { Subjects } from './subjects';
interface Event {
subject: Subjects;
data: any;
}
export abstract class Listener<T extends Event> {
abstract subject: T['subject'];
abstract queueGroupName: string;
abstract onMessage(data: T['data'], msg: Message): void;
private client: Stan;
protected ackWait = 5 * 1000;
constructor(client: Stan) {
this.client = client;
}
subscriptionOptions() {
return this.client
.subscriptionOptions()
.setDeliverAllAvailable()
.setManualAckMode(true)
.setAckWait(this.ackWait)
.setDurableName(this.queueGroupName);
}
listen() {
const subscription = this.client.subscribe(
this.subject,
this.queueGroupName,
this.subscriptionOptions()
);
subscription.on('message', (msg: Message) => {
console.log(`Message received: ${this.subject} / ${this.queueGroupName}`);
const parsedData = this.parseMessage(msg);
this.onMessage(parsedData, msg);
});
}
parseMessage(msg: Message) {
const data = msg.getData();
return typeof data === 'string'
? JSON.parse(data)
: JSON.parse(data.toString('utf8'));
}
}
As I searched through the documents it seems I can do something like following:
import { connect } from "nats";
const jsm = await nc.jetstreamManager();
const cfg = {
name: "EVENTS",
subjects: ["events.>"],
};
await jsm.streams.add(cfg);
But it seems there are only name and subject options available. But from my original code I need a data property it can handle JSON objects. Is there a way I can convert this code to a Jetstream code or I should change the logic of the whole application as well?

NestJs, RabbitMq, CQRS & BFF: Listening for event inside the bff

I'm about to implement a Microservice Architecture with CQRS Design Pattern. The Microservices are communicating with RMQ.
Additionally, I'm adding a BFF for my Application UI.
In this scenario, the BFF needs to listen to certain domain events.
For instance: After the user sends an request to the BFF, the BFF calls a method of a Microservice which invokes an asynchronous event.
The events result will go back to the BFF and then to the user.
I'm thinking of different ways I might be able to implement this and I came up with this concept:
// BFF Application: sign-up.controller.ts
import { Controller, Post, Body } from '#nestjs/common';
import { Client, ClientProxy, Transport } from '#nestjs/microservices';
#Controller('signup')
export class SignupController {
#Client({ transport: Transport.RMQ, options: { urls: ['amqp://localhost:5672'], queue: 'signup_request' } })
client: ClientProxy;
#Post()
async signup(#Body() body: any) {
// Generate a unique identifier for the request
const requestId = uuid();
// Send the request with the unique identifier
const response = await this.client.send<any>({ cmd: 'signup', requestId }, body).toPromise();
// Wait for the SignUpEvent to be emitted before sending the response
return new Promise((resolve, reject) => {
this.client.subscribe<any>('signup_response', (response: any) => {
// Check the unique identifier to ensure the event corresponds to the original request
if (response.requestId === requestId) {
resolve(response);
}
});
});
}
}
After sending the request with the unique identifier, the microservice will execute a sign up command:
// Microservice Application: sign-up.handler.ts
import { CommandHandler, ICommandHandler } from '#nestjs/cqrs';
import { SignUpCommand } from './commands/sign-up.command';
import { SignUpEvent } from './events/sign-up.event';
import { EventBus } from '#nestjs/cqrs';
#CommandHandler(SignUpCommand)
export class SignUpCommandHandler implements ICommandHandler<SignUpCommand> {
constructor(private readonly eventBus: EventBus) {}
async execute(command: SignUpCommand) {
// Validating the user/account aggregate
// ...
// Emit the SignUpEvent with the unique identifier
this.eventBus.publish(new SignUpEvent(user, command.requestId));
}
}
Now the Event Handler gets called:
// Microservice Application: signed-up.handler.ts
import { EventsHandler, IEventHandler } from '#nestjs/cqrs';
import { SignUpEvent } from './events/sign-up.event';
import { ClientProxy, Transport } from '#nestjs/microservices';
#EventsHandler(SignUpEvent)
export class SignUpEventHandler implements IEventHandler<SignUpEvent> {
#Client({ transport: Transport.RMQ, options: { urls: ['amqp://localhost:5672'], queue: 'signup_response' } })
client: ClientProxy;
async handle(event: SignUpEvent) {
// Persist the user
const user = await this.persistUser(event.user);
// Generate access and refresh tokens
const tokens = this.generateTokens(user);
// Emit the SignUpResponse event with a unique identifier
await this.client.emit('signup_response', { user, tokens, requestId: event.requestId });
}
}
Is this, a valid way to implement this type of behaviour?
Thank you in advance.

web sockets rooms not works in nestjs

I'm creating a simple chat app with rooms and I meet the problem, that, seems like socket.io can not join the room, I have schemas in mongodb, those are users, messages and rooms, for room id I provide the id of rooms schema and then want to join it with one event for example, I'm logging in from react app, then by user id I'm finding all rooms that contains my id and then want to join them all.
For the second step I want to send message and I'm targeting the room that I want send the message, with .to(roomId).emit(..., ...)
but all of this tries are useless, it not works
here is the nestjs gateway code:
import { Logger } from '#nestjs/common';
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayDisconnect,
OnGatewayInit,
OnGatewayConnection,
MessageBody,
ConnectedSocket,
WsResponse,
} from '#nestjs/websockets';
import { Socket, Server } from 'socket.io';
import { UserService } from 'src/user/user.service';
import { ChatService } from './chat.service';
import { CreateChatDto } from './dto/create-chat.dto';
import { UpdateChatDto } from './dto/update-chat.dto';
#WebSocketGateway({
cors: {
origin: '*',
},
})
export class ChatGateway
implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit
{
constructor(
private readonly chatService: ChatService,
private userService: UserService,
) {}
private readonly logger: Logger = new Logger(ChatGateway.name);
#WebSocketServer() server: Server;
afterInit(client: Socket) {
this.logger.log('Initialized SocketGateway');
}
handleConnection(client: Socket) {
this.logger.log(`[connection] from client (${client.id})`);
}
handleDisconnect(client: Socket) {
this.logger.log(`[disconnection] from client(${client.id})`);
}
//this works when user logs is
#SubscribeMessage('setup')
async handleJoinRoom(client: Socket, #MessageBody() userData) {
//here I'm getting all rooms that contains my user id
const rooms = await this.userService.findAll(userData);
await this.logger.log(
`[joinWhiteboard] ${userData}(${client.id}) joins ${userData}`,
);
await rooms.map((item) => {
client.join(item._id.toString());
//joining all rooms that I'm in
client.to(item._id.toString()).emit('joined');
});
}
#SubscribeMessage('new message')
create(client: Socket, #MessageBody() recievedMessage) {
this.logger.log(
`[sent a new message] from (${client.id}) to ${recievedMessage.chatRoomId}`,
);
//sending message to the room
client
.to(recievedMessage.chatRoomId)
.emit('message recieved', recievedMessage);
}
}
in my github is the full code(react part also), please feel free if you need to see it
https://github.com/Code0Breaker/chat

Emit events between different tabs

I'm writing a web app where tab1 opens tab2 and needs to reload when tab2 is closed but nothing happens when I send the next() value (in debug I saw it get executed only once when the component is initialized). I assume it has something to do with the different browser tabs
the shared service that should allow communication between the two:
private tabClosedSource = new ReplaySubject<boolean>();
tabClosedEvent = this.tabClosedSource.asObservable();
toggleTabClosed() {
this.tabClosedSource.next(true);
}
on tab1 :
constructor(private service: ExampleService) {}
ngOnInit() {
this.service.tabClosedEvent.subscribe(
event => {
if (event) {
location.reload();
}
});
// had some issues with the path starting with '#' so I ended up replacing values
openTab2() {
window.open(window.location.href.replace('tab1', 'tab2'));
}
on tab2:
constructor(private service: ExampleService) {}
async onContinueClicked() {
api requests...
procces data...
this.service.toggleTabClosed();
window.close();
}
Your requirement is achievable with the help of Broadcast Channel API. Full credit goes to another blog, I've just updated it to suit your needs and by the way I learned some new things too.
First you need to create a service to communicate.
import { Injectable, NgZone } from '#angular/core';
import { MonoTypeOperatorFunction, Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';
// helper method to notify zone
function runInZone<T>(zone: NgZone): MonoTypeOperatorFunction<T> {
return (source) => {
return new Observable(observer => {
const onNext = (value: T) => zone.run(() => observer.next(value));
const onError = (e:any) => zone.run(()=> observer.error(e));
const onComplete = () => zone.run(()=> observer.complete());
return source.subscribe(onNext, onError, onComplete);
});
};
}
#Injectable({
providedIn: 'root'
})
export class BroadcastHelperService {
private broadcastChannel: BroadcastChannel;
private onMessage = new Subject<any>();
constructor(
private ngZone: NgZone) {
this.broadcastChannel = new BroadcastChannel('testChannel');
this.broadcastChannel.onmessage = (message: any) => { this.onMessage.next(message) }
}
publish(message: string): void {
this.broadcastChannel.postMessage(message);
}
getMessage(): Observable<any> {
return this.onMessage.pipe(
runInZone(this.ngZone),
map((message: any) => message.data));
}
}
In tab1 component write code to reload page.
export class Tab1Component implements OnInit {
worker: any;
constructor(
public broadcastHelper: BroadcastHelperService
) { }
ngOnInit(): void {
this.broadcastHelper.getMessage().subscribe((message: any) => {
console.log(message);
if(message === 'close') {
window.location.reload();
}
})
}
}
In tab2 component, write code to broadcast message when the tab is closed.
export class Tab2Component {
constructor(
public broadcastHelper: BroadcastHelperService
) { }
#HostListener('window:beforeunload', ['$event'])
beforeUnloadHander(event: any) {
this.broadcastHelper.publish('close');
}
}

Angular2 - Handling API Response

Good afternoon! I'm new in Angular 2, so I'm sorry in advance if my question is generic. I cannot figure out how to handle an API response.
My NodeJS Server API function is (Checked and works fine):
router.get('/appointment/:iatreio/:time', function(req, res, next) {
var paramIatreio = req.params.iatreio;
var paramTime = req.params.time;
db.appointments.findOne({iatreio: paramIatreio, time: req.params.time}, function(err, resultFound) {
if (err) { res.send(err); }
if (resultFound) {
res.json(true); // 1st Question: For best practice, res.json(true) or res.send(true)?
} else {
res.json(false);
}
});
});
My Angular2 Service:
import { Injectable } from '#angular/core';
import { Headers , Http } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
#Injectable()
export class AppointmentService {
constructor(private http: Http) { }
isBooked(iatreio: string, time: string): Observable<boolean> {
return this.http
.get('http://localhost:3000/appointment/'+iatreio+'/'+time)
.map(); //2nd Question: What inside map()?
}
} // end of Service
Component Function
isBooked(selectedIatreio: string, selectedTime: string): boolean {
this.appointmentService
.isBooked(selectedIatreio, selectedTime)
.subscribe(() => {}); //3rd Question: What inside subscribe()?
}
My final goal is the "isBooked(...)" function of my Component to be called and to return true or false. I have seen the code in the examples in the Angular2 site, but I'm a little confused on my case.
Can Service function return directly a true or false value or it has to be an Observable?? Map() function is necessary??
Generally, my thinking is right?? Or my goal can be accomplished more easily??
Thank you a lot for your time!!
map is used to convert the response into the model which you look for
isBooked(iatreio: string, time: string): Observable<boolean> {
return this.http
.get('http://localhost:3000/appointment/'+iatreio+'/'+time)
.map((response)=><boolean>response.json());
}
subscribe will return the data emitted by the service
isBooked(selectedIatreio: string, selectedTime: string): boolean {
this.appointmentService
.isBooked(selectedIatreio, selectedTime)
.subscribe((data) => {
//your operation
console.log(data);
});
}

Resources