WebSockets on specific route in Nest.js - websocket

I'd like to create specific API route which will be used only WebSocket (/api/events) but in all examples of implementing WebSockets on Nest.js I stumbled upon module is imported in AppModule and client is emitting events toward the root URL, which I can't do because I have this middleware;
frontend.middleware.ts
import { Request, Response } from 'express';
import { AppModule } from '../../app.module';
export function FrontendMiddleware(
req: Request,
res: Response,
next: Function,
) {
const { baseUrl } = req;
if (baseUrl.indexOf('/api') === 0) {
next();
} else {
res.sendFile('index.html', { root: AppModule.getStaticAssetsRootPath() });
}
}
Here is the EventGateway and EventModule:
event.gateway.ts
import {
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
WsResponse,
} from '#nestjs/websockets';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Client, Server } from 'socket.io';
#WebSocketGateway({ namespace: 'events' })
export class EventGateway {
#WebSocketServer()
server: Server;
#SubscribeMessage('events')
findAll(client: Client, data: any): Observable<WsResponse<number>> {
return from([1, 2, 3]).pipe(map(item => ({ event: 'events', data: item })));
}
#SubscribeMessage('identity')
async identity(client: Client, data: number): Promise<number> {
return data;
}
}
event.module.ts
import { Module } from '#nestjs/common';
import { EventGateway } from './event.gateway';
#Module({
components: [EventGateway],
})
export class EventModule {}
Is there a way to create controller which will allow server-client communication via /api/events?

Yes, it is possible to create the WebsocketGateway on another path. You can just use the options of the WebsocketGateway to configure the underlying IO-Connection:
E.g:
import {
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
WsResponse,
} from '#nestjs/websockets';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Client, Server } from 'socket.io';
#WebSocketGateway({ path: '/api/events', namespace: 'events' })
export class EventGateway {
#WebSocketServer()
server: Server;
#SubscribeMessage('events')
findAll(client: Client, data: any): Observable<WsResponse<number>> {
return from([1, 2, 3]).pipe(map(item => ({ event: 'events', data: item })));
}
#SubscribeMessage('identity')
async identity(client: Client, data: number): Promise<number> {
return data;
}
}
This will start the IO-Connection on http://localhost/api/events
Remember to change the connection-path also in your client. It won't be the default /socket.io path anymore, it will be /api/events in your sample.

Websocket is running on the server, not an endpoint. Therefore you cannot have it listen to requests under a specific route, rather just a port, which for Nest's default configuration happens to be the same as the HTTP one.
You could use a reverse proxy like Nginx to redirect the requests towards /api/events facing the Websocket server and also handle the redirection to index.html without changing even the Websocket server's port. Then you would not need the FrontendMiddleware class at all. It is also better since the application does not take the burden of managing request redirections.

Related

nest js websocket connection is not working with angular 11

Created a nest js websocket and trying to connect to that from angular app version 11. Not able to connect to socket from angular 11. I am using latest version of socket.io-client.
In websocket server log says connect and disconnects.
nest js websocket file:
import { Logger } from '#nestjs/common';
import { OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WsResponse } from '#nestjs/websockets';
import { Socket,Server } from 'socket.io';
import { EventPattern } from '#nestjs/microservices';
#WebSocketGateway(3001)
export class AppGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect{
private Logger = new Logger('AppGateway');
afterInit(server: Server) {
this.Logger.log("App Gateway Initialized");
}
handleConnection(client: Socket, ...args: any[]){
this.Logger.log(`New client connected...: ${client.id}`);
client.emit('connected', 'Successfully connected to the server.');
}
handleDisconnect(client: Socket) {
this.Logger.log(`Client disconnected: ${client.id}`);
}
#SubscribeMessage('msgToServer')
handleMessage(client:Socket, text:string):WsResponse<string> {
this.Logger.log(`got new event`);
return {event: 'msgToClient', 'data': text};
}
}
angular websocket service file:
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { io } from 'socket.io-client';
#Injectable({
providedIn: 'root'
})
export class WebSocketService {
socket: any;
constructor() {
this.socket = io('http://localhost:3001');
debugger;
this.socket.on('connected', function() {
console.log("connected !");
});
}
listen(eventName: string) {
return new Observable((subscriber) => {
this.socket.on(eventName, (data) => {
subscriber.next(data);
})
});
}
emit(eventName: string, data:any) {
this.socket.emit(eventName, data);
}
}
angular app.component.ts file:
import { Component, OnInit } from '#angular/core';
import { WebSocketService } from './web-socket.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit{
title = 'dashboard';
constructor(
private webSocketService: WebSocketService
) {}
ngOnInit() {
/*this.webSocketService.listen('msgToClient').subscribe((data) => {
console.log(data);
})*/
this.webSocketService.listen('connected').subscribe((data) => {
console.log(data);
})
}
}
I am using latest version of socket.io-client.
I believe you're using socketIO client v4. (If you're working with v3, the following would still be true)
Based on NestJS Websocket documentation, the NestJS socketIO server is still in v2.
#nestjs/platform-socket.io currently depends on socket.io v2.3 and socket.io v3.0 client and server are not backward compatible. However, you can still implement a custom adapter to use socket.io v3.0. Please refer to this issue for further information.
If you check the version compatibility, you will see that socketIO server v2 is not compatible with socketIO client v4.
However, socketIO server v3 is compatible with socketIO client v4. So I believe you can take a look into this issue (as mentioned in NestJS docs) and try to convert your NestJS socketIO server to support socketIO client v3. Hopefully, that would also support socketIO client v4 as well. (I didn't test this though!)
Hope this helps you. Cheers 🍻 !!!

Vuex-ORM GraphQL installation troubles

I installed the Vuex-ORM Graphql Plugin into an existing Nuxt project with Laravel/GraphQL API, so that I could try avoiding using the Apollo Cache. In one of my components though, I'm running:
<script>
import Notification from '~/data/models/notification';
export default {
computed: {
notifications: () => Notification.all()
},
async mounted () {
await Notification.fetch();
}
}
</script>
however I'm receiving the error [vuex] unknown action type: entities/notifications/fetch.
I looked through the debug log and found several available getters (entities/notifications/query, entities/notifications/all, entities/notifications/find, and entities/notifications/findIn). I tried running await Notification.all() in the mounted method which removed the error, however looking in Vuex the Notifications data object is empty.
Here is the rest of my setup:
nuxt.config.js
plugins: [
'~/plugins/vuex-orm',
'~/plugins/graphql'
],
plugins/vuex-orm.js
import VuexORM from '#vuex-orm/core';
import database from '~/data/database';
export default ({ store }) => {
VuexORM.install(database)(store);
};
plugins/graphql.js
/* eslint-disable import/no-named-as-default-member */
import VuexORM from '#vuex-orm/core';
import VuexORMGraphQL from '#vuex-orm/plugin-graphql';
import { HttpLink } from 'apollo-link-http';
import fetch from 'node-fetch';
import CustomAdapter from '~/data/adapter';
import database from '~/data/database';
// The url can be anything, in this example we use the value from dotenv
export default function ({ app, env }) {
const apolloClient = app?.apolloProvider?.defaultClient;
const options = {
adapter: new CustomAdapter(),
database,
url: env.NUXT_ENV_BACKEND_API_URL,
debug: process.env.NODE_ENV !== 'production'
};
if (apolloClient) {
options.apolloClient = apolloClient;
} else {
options.link = new HttpLink({ uri: options.url, fetch });
}
VuexORM.use(VuexORMGraphQL, options);
};
/data/adapter.js
import { DefaultAdapter, ConnectionMode, ArgumentMode } from '#vuex-orm/plugin-graphql';
export default class CustomAdapter extends DefaultAdapter {
getConnectionMode () {
return ConnectionMode.PLAIN;
}
getArgumentMode () {
return ArgumentMode.LIST;
}
};
/data/database.js
import { Database } from '#vuex-orm/core';
// import models
import Notification from '~/data/models/notification';
import User from '~/data/models/user';
const database = new Database();
database.register(User);
database.register(Notification);
export default database;
/data/models/user.js
import { Model } from '#vuex-orm/core';
import Notification from './notification';
export default class User extends Model {
static entity = 'users';
static eagerLoad = ['notifications'];
static fields () {
return {
id: this.attr(null),
email: this.string(''),
first_name: this.string(''),
last_name: this.string(''),
// relationships
notifications: this.hasMany(Notification, 'user_id')
};
}
};
/data/models/notification.js
import { Model } from '#vuex-orm/core';
import User from './user';
export default class Notification extends Model {
static entity = 'notifications';
static fields () {
return {
id: this.attr(null),
message: this.string(''),
viewed: this.boolean(false),
// relationships
user: this.belongsTo(User, 'user_id')
};
}
};
package.json
"#vuex-orm/plugin-graphql": "^1.0.0-rc.41"
So in a Hail Mary throw to get this working, I ended up making a couple of changes that actually worked!
If other people come across this having similar issues, here's what I did...
In my nuxt.config.js, swapped the order of the two plugins to this:
plugins: [
'~/plugins/graphql',
'~/plugins/vuex-orm',
],
In my graphql.js plugin, I rearranged the order of the options to this (database first, followed by adapter):
const options = {
database,
adapter: new CustomAdapter(),
url: env.NUXT_ENV_BACKEND_API_URL,
debug: process.env.NODE_ENV !== 'production'
};

Possible memory leak in NativeScript app if user reopens his app multiple times

I'm not sure where is the bug, maybe I'm using rxjs in a wrong way. ngDestroy is not working to unsubscribe observables in NativeScript if you want to close and back to your app. I tried to work with takeUntil, but with the same results. If the user close/open the app many times, it can cause a memory leak (if I understand the mobile environment correctly). Any ideas? This code below it's only a demo. I need to use users$ in many places in my app.
Tested with Android sdk emulator and on real device.
AppComponent
import { Component, OnDestroy, OnInit } from '#angular/core';
import { Subscription, Observable } from 'rxjs';
import { AppService } from './app.service';
import { AuthenticationService } from './authentication.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnDestroy, OnInit {
public user$: Observable<any>;
private subscriptions: Subscription[] = [];
constructor(private appService: AppService, private authenticationService: AuthenticationService) {}
public ngOnInit(): void {
this.user$ = this.authenticationService.user$;
this.subscriptions.push(
this.authenticationService.user$.subscribe((user: any) => {
console.log('user', !!user);
})
);
}
public ngOnDestroy(): void {
if (this.subscriptions) {
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
}
}
async signIn() {
await this.appService.signIn();
}
async signOut() {
await this.appService.signOut();
}
}
AuthenticationService
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { AppService } from './app.service';
#Injectable({
providedIn: 'root',
})
export class AuthenticationService {
public user$: Observable<any>;
constructor(private appService: AppService) {
this.user$ = this.appService.authState().pipe(shareReplay(1)); // I'm using this.users$ in many places in my app, so I need to use sharereplay
}
}
AppService
import { Injectable, NgZone } from '#angular/core';
import { addAuthStateListener, login, LoginType, logout, User } from 'nativescript-plugin-firebase';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
const user$ = new BehaviorSubject<User>(null);
#Injectable({
providedIn: 'root',
})
export class AppService {
constructor(private ngZone: NgZone) {
addAuthStateListener({
onAuthStateChanged: ({ user }) => {
this.ngZone.run(() => {
user$.next(user);
});
},
});
}
public authState(): Observable<User> {
return user$.asObservable().pipe(distinctUntilChanged());
}
async signIn() {
return await login({ type: LoginType.PASSWORD, passwordOptions: { email: 'xxx', password: 'xxx' } }).catch(
(error: string) => {
throw {
message: error,
};
}
);
}
signOut() {
logout();
}
}
ngOnDestroy is called whenever a component is destroyed (following regular Angular workflow). If you have navigated forward in your app, previous views would still exist and would be unlikely to be destroyed.
If you are seeing multiple ngOnInit without any ngOnDestroy, then you have instantiated multiple components through some navigation, unrelated to your subscriptions. You should not expect the same instance of your component to be reused once ngOnDestroy has been called, so having a push to a Subscription[] array will only ever have one object.
If you are terminating the app (i.e. force quit swipe away), the whole JavaScript context is thrown out and memory is cleaned up. You won't run the risk of leaking outside of your app's context.
Incidentally, you're complicating your subscription tracking (and not just in the way that I described above about only ever having one pushed). A Subscription is an object that can have other Subscription objects attached for termination at the same time.
const subscription: Subscription = new Subscription();
subscription.add(interval(100).subscribe((n: number) => console.log(`first sub`));
subscription.add(interval(200).subscribe((n: number) => console.log(`second sub`));
subscription.add(interval(300).subscribe((n: number) => console.log(`third sub`));
timer(5000).subscribe(() => subscription.unsubscribe()); // terminates all added subscriptions
Be careful to add the subscribe call directly in .add and not with a closure. Annoyingly, this is exactly the same function call to make when you want to add a completion block to your subscription, passing a block instead:
subscription.add(() => console.log(`everybody's done.`));
One way to detect when the view comes from the background is to set callbacks on the router outlet (in angular will be)
<page-router-outlet
(loaded)="outletLoaded($event)"
(unloaded)="outletUnLoaded($event)"></page-router-outlet>
Then you cn use outletLoaded(args: EventData) {} to initialise your code
respectively outletUnLoaded to destroy your subscriptions.
This is helpful in cases where you have access to the router outlet (in App Component for instance)
In case when you are somewhere inside the navigation tree you can listen for suspend event
Application.on(Application.suspendEvent, (data: EventData) => {
this.backFromBackground = true;
});
Then when opening the app if the flag is true it will give you a hint that you are coming from the background rather than opening for the first time.
It works pretty well for me.
Hope that help you as well.

Navigate to another route from a service

I have service that intercepts all my Http requests so I can check if the user's token is valid or not. When a Http response is 401, I want the current user to be logged out of the application:
import { Http, ConnectionBackend, RequestOptions, RequestOptionsArgs, Request, Response } from '#angular/http'
import { Router } from '#angular/router'
import { Observable } from 'rxjs/Observable'
import { Injectable } from '#angular/core'
import { Config } from './shared/config'
import { RouterExtensions } from 'nativescript-angular/router'
import 'rxjs/add/observable/throw'
#Injectable()
export class RequestInterceptorService extends Http {
private config: Config = new Config()
constructor(
backend: ConnectionBackend,
defaultOptions: RequestOptions,
private router: Router,
private routerExtensions: RouterExtensions,
) {
super(backend, defaultOptions)
}
request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
return this.intercept(super.request(url, options))
}
intercept(observable: Observable<Response>): Observable<Response> {
return observable.catch((err, source) => {
if (err.status === 401) {
this.logout()
return Observable.empty()
} else {
return Observable.throw(err)
}
})
}
logout() {
this.config.clear()
this.routerExtensions.navigate(["/signin"], {
clearHistory: true
})
}
}
My issue is that router or routerExtensions are always undefined, so I can't really redirect the user to any other path at all using this service.
So, I found the solution. It was just a mater of understanding all of what I was doing.
I created a RequestInterceptor service that extends Http module. It includes a interceptor that checks if the status code is equal to 401. In order to make it compatible with my project, I decided to provide it as custom implementation of Http. The following code on my #NgModule does just that:
providers: [
{
provide: Http,
useClass: RequestInterceptorService
}
]
But the thing RequestInterceptorService does not have access to Http dependencies: ConnectionBackend and RequestOptions, so I had to inject them using the deps property, which is an array where I can identify an array of dependencies that will get injected. In my case, I need to inject not only the dependencies that Http requires (XHRBackend, RequestOptions), but also the one that my service will use: RouterExtensions.
My providers declaration looks like this:
providers: [
{
provide: Http,
useClass: RequestInterceptorService,
deps: [XHRBackend, RequestOptions, RouterExtensions]
}
]
After doing this, I was able to successfully redirect a user to the login page when a 401 status code is returned from any request.

admin-on-rest / restClient : call a resource with no auth

I made a register page that use restClient to send a POST to /users api.
But my problem is that the only way to send a POST is to be logged first as I receive this error log from the restClient :
'Could not find stored JWT and no authentication strategy was given'
Is there a way to desactivate the authentication middleware for a specific api call ?
// registerActions.js
import { CREATE } from 'admin-on-rest'
export const USER_REGISTER = 'AOR/USER_REGISTER'
export const USER_REGISTER_LOADING = 'AOR/USER_REGISTER_LOADING'
export const USER_REGISTER_FAILURE = 'AOR/USER_REGISTER_FAILURE'
export const USER_REGISTER_SUCCESS = 'AOR/USER_REGISTER_SUCCESS'
export const userRegister = (data, basePath) => ({
type: USER_REGISTER,
payload: { data: { email: data.username, ...data } },
meta: { resource: 'users', fetch: CREATE, auth: true },
})
//registerSaga.js
import { put, takeEvery, all } from 'redux-saga/effects'
import { push } from 'react-router-redux'
import { showNotification } from 'admin-on-rest'
import {
USER_REGISTER,
USER_REGISTER_LOADING,
USER_REGISTER_SUCCESS,
USER_REGISTER_FAILURE
} from './registerActions'
function* registerSuccess() {
yield put(showNotification('Register approved'))
yield put(push('/'))
}
function* registerFailure({ error }) {
yield put(showNotification('Error: register not approved', 'warning'))
console.error(error)
}
export default function* commentSaga() {
yield all([
takeEvery(USER_REGISTER_SUCCESS, registerSuccess),
takeEvery(USER_REGISTER_FAILURE, registerFailure),
])
}
You'll probably have to make your own feathers client and explicitly bypass the call to authenticate for this specific request
You can also write a rest wrappper this will intercept the call for this particular case and bypass auth
https://marmelab.com/admin-on-rest/RestClients.html#decorating-your-rest-client-example-of-file-upload
So something like below
const restWrapper = requestHandler => (type, resource, params) => {
import { fetchUtils } from 'admin-on-rest';
if (type === 'CREATE' && resource === 'users') {
return fetchUtils.fetchJson(url, params)
.then((response) => {
const {json} = response;
return { data: json };
})
}
Eliminates the need of rewriting an entire Rest Client when you only want to override the default behaviour for a single case

Resources