Angular2/Websocket: how to return an observable for incoming websocket messages - websocket

I'm going to use Angular2 to receive websocket incoming messages and update a webpage based on those received messages. Right now, I'm using a dummy echo websocket service and will replace it.
From my understanding, the function which receive websocket messages has to return an observable that is subscribed by a handler who will update the webpage. But I can't figure out how to return an observable.
Code snippet is attached below. The MonitorService creates a websocket connection and return an observable containing the received messages.
#Injectable()
export class MonitorService {
private actionUrl: string;
private headers: Headers;
private websocket: any;
private receivedMsg: any;
constructor(private http: Http, private configuration: AppConfiguration) {
this.actionUrl = configuration.BaseUrl + 'monitor/';
this.headers = new Headers();
this.headers.append('Content-Type', 'application/json');
this.headers.append('Accept', 'application/json');
}
public GetInstanceStatus = (): Observable<Response> => {
this.websocket = new WebSocket("ws://echo.websocket.org/"); //dummy echo websocket service
this.websocket.onopen = (evt) => {
this.websocket.send("Hello World");
};
this.websocket.onmessage = (evt) => {
this.receivedMsg = evt;
};
return new Observable(this.receivedMsg).share();
}
}
Below is another component which subscribes to the observable returned from above and updates webpages correspondingly.
export class InstanceListComponent {
private instanceStatus: boolean
private instanceName: string
private instanceIcon: string
constructor(private monitor: MonitorService) {
this.monitor.GetInstanceStatus().subscribe((result) => {
this.setInstanceProperties(result);
});
}
setInstanceProperties(res:any) {
this.instanceName = res.Instance.toUpperCase();
this.instanceStatus = res.Status;
if (res.Status == true)
{
this.instanceIcon = "images/icon/healthy.svg#Layer_1";
} else {
this.instanceIcon = "images/icon/cancel.svg#cancel";
}
}
}
Now, I'm running into this error in the browser console
TypeError: this._subscribe is not a function

I put it on a plunker and I added a function for sending message to the Websocket endpoint. Here is the important edit:
public GetInstanceStatus(): Observable<any>{
this.websocket = new WebSocket("ws://echo.websocket.org/"); //dummy echo websocket service
this.websocket.onopen = (evt) => {
this.websocket.send("Hello World");
};
return Observable.create(observer=>{
this.websocket.onmessage = (evt) => {
observer.next(evt);
};
})
.share();
}
Update
As you mentioned in your comment, a better alternative way is to use Observable.fromEvent()
websocket = new WebSocket("ws://echo.websocket.org/");
public GetInstanceStatus(): Observable<Event>{
return Observable.fromEvent(this.websocket,'message');
}
plunker example for Observable.fromEvent();
Also, you can do it using WebSocketSubject, although, it doesn't look like it's ready yet (as of rc.4):
constructor(){
this.websocket = WebSocketSubject.create("ws://echo.websocket.org/");
}
public sendMessage(text:string){
let msg = {msg:text};
this.websocket.next(JSON.stringify(msg));
}
plunker example

Get onMessage data from socket.
import { Injectable } from '#angular/core';
import {Observable} from 'rxjs/Rx';
#Injectable()
export class HpmaDashboardService {
private socketUrl: any = 'ws://127.0.0.0/util/test/dataserver/ws';
private websocket: any;
public GetAllInstanceStatus(objStr): Observable<any> {
this.websocket = new WebSocket(this.socketUrl);
this.websocket.onopen = (evt) => {
this.websocket.send(JSON.stringify(objStr));
};
return Observable.create(observer => {
this.websocket.onmessage = (evt) => {
observer.next(evt);
};
}).map(res => res.data).share();
}
**Get only single mesage from socket.**
public GetSingleInstanceStatus(objStr): Observable<any> {
this.websocket = new WebSocket(this.socketUrl);
this.websocket.onopen = (evt) => {
this.websocket.send(JSON.stringify(objStr));
};
return Observable.create(observer => {
this.websocket.onmessage = (evt) => {
observer.next(evt);
this.websocket.close();
};
}).map(res => res.data).share();
}
}

A different approach I used is with subject:
export class WebSocketClient {
private client: WebSocket | undefined;
private subject = new Subject<string>();
...
private connect() {
const client = new WebSocket(fakeUrl);
const client.onmessage = (event) => {
this.subject.next(event.data);
};
}
private watch() { return this.subject } // can be mapped
}
And using it will be in my opinion clearer:
const client = new WebSocketClient(); // can also be injected
client.connect();
client.watch().subscribe(x => ...);
Happy coding!

Related

Observable subscribe vs pipe - rxjs

I have following function in service to retrieve data:
public getData(): Observable<dataObj> {
const endpointURL = 'xyz';
return response = this.responseHandler
.makeRequest<dataObj>('get', endpointURL)
.pipe(
startWithTap(() => this._requestsInProgress.next(++this.requestsInProgress)),
finalize(() => this._requestsInProgress.next(--this.requestsInProgress))
)
}
in different place I have something like this and I call that function:
export class SearchAndFilterService {
private _data: BehaviorSubject<dataObj> = new BehaviorSubject(null);
private _searchTerm: BehaviorSubject<string> = new BehaviorSubject(null);
private _filterTerms: BehaviorSubject<string[]> = new BehaviorSubject(null);
public readonly data$: Observable<dataObj>;
constructor(
private service: Service
) {
this.data$ = combineLatest([
this._data.asObservable(),
this._searchTerm.asObservable(),
this._filterTerms.asObservable()
]).pipe(
map((tuple) => {
const [data, searchTerm, filterTerms] = tuple;
if (data) {
return {
...data,
array: this.searchAndFilterData(
data?.array,
searchTerm,
filterTerms
)
};
}
return data;
}),
);
}
public updateDonorAppointments() {
this.service.getData().pipe(tap(
(data) => this._data.next(data)
))
}
I thought that pipe should create a subscription to returned observable and this will work. However instead of pipe I must use .subscribe to make it work. Like this:
public updateDonorAppointments() {
this.service.getData().subscribe(
(data) => this._data.next(data)
)
}
Am I missing something? Or is this approach totally wrong?
Thanks

websocket_sockJS : all transport error. after adding { transports: ['websocket']}

background:
whenever I open WebSocket page
I had a few XHR_SEND? 404 - error but finally XHR_SEND? got success response and connected to WebSocket.
So to avoid this 404 error, I decide to use WebSocket only. so I added this
: return new SockJS(connectionUrl,, null, { transports: ['websocket']});
then now..
XHR_SEND? are gone but it doesn't connect to server at all.
+FYI: I have 2 servers ..(i think because of this previously I got XHR_send error. )
The below screenshot is repeating. but never connected
JAVA
#Configuration
#EnableWebSocketMessageBroker
public class BatchSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/batch-socket");
registry.addEndpoint("/batch-socket").withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/socketio/");
}
ANGULAR7
import { Injectable, OnDestroy, Inject, Optional } from '#angular/core';
import * as SockJS from '../../../assets/lib/sockjs.min.js';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, first, switchMap } from 'rxjs/operators';
import { StompSubscription, Stomp, Client, Frame, Message, StompConfig, Versions } from '#stomp/stompjs';
#Injectable({
providedIn: 'root'
})
export class SocketService {
private client: Client;
private state: BehaviorSubject<any>;
private baseUrl: any = "/" + window.location.href.substr(0).split('/')[3] + "/";
constructor() {
}
init() {
let connectionUrl = this.baseUrl + "batch-socket";
console.log("MY URL is " + connectionUrl);
return new Promise((resolve, reject) => {
let config = new StompConfig();
config.heartbeatOutgoing = 10000;
config.heartbeatIncoming = 10000;
config.stompVersions = new Versions(['1.0', '1.1']);
config.webSocketFactory = function () {
return new SockJS(connectionUrl, null, { transports: ['websocket']});
//PREVIOUS : return new SockJS(connectionUrl)
}
config.debug = function (str) {
console.log("#socketDebug: " + str)
}
this.client = new Client();
this.client.configure(config);
console.log(this.client);
console.log("#socketSvc: starting connection...");
const _this = this;
this.client.onConnect = function (frame) {
console.log("#socketSvc: connection established.");
console.log(frame);
_this.state = new BehaviorSubject<any>(SocketClientState.ATTEMPTING);
_this.state.next(SocketClientState.CONNECTED);
resolve(frame.headers['user-name']);
}
this.client.onWebSocketClose = function (msg){
console.log("#socketSvc: connection closed.");
console.log(msg);
}
this.client.onWebSocketError = function(msg){
console.log("#socketSvc: connection error.");
console.log(msg);
}
this.client.onDisconnect = function(msg){
console.log("#socketSvc: socket disconnected.");
console.log(msg);
//this.init();
}
this.client.onStompError = function(msg){
console.log("#socketSvc: stomp error occurred.");
console.log(msg);
}
this.client.activate();
});
}
private connect(): Observable<Client> {
return new Observable<Client>(observer => {
this.state.pipe(filter(state => state === SocketClientState.CONNECTED)).subscribe(() => {
observer.next(this.client);
});
});
}
onPlainMessageReceived(topic: string): Observable<string> {
return this.onMessageReceived(topic, SocketService.textHandler);
}
onMessageReceived(topic: string, handler = SocketService.jsonHandler): Observable<any> {
return this.connect().pipe(first(), switchMap(client => {
return new Observable<any>(observer => {
const subscription: StompSubscription = client.subscribe(topic, message => {
observer.next(handler(message));
});
return () => client.unsubscribe(subscription.id);
});
}));
}
static jsonHandler(message: Message): any {
return JSON.parse(message.body);
}
static textHandler(message: Message): string {
return message.body;
}
disconnect() {
this.connect().pipe(first()).subscribe(client => client.deactivate());
this.client.deactivate();
}
}
export enum SocketClientState {
ATTEMPTING, CONNECTED
}
I found a reason for this issue.
I realized that I have 2 war files.
hence one has my code(socket connection) , the other one doesnt have a code (socket connection).
so it throws the error.
=> resolved by removing the war file that doesn't have socket connection.

reconnect web socket if it is closed

My web socket connection code :
public connect(): Subject<MessageEvent> {
if (!this.subject) {
this.subject = this.create(this.url);
}
this.ws.onerror = () => {
this.close();
let refresh = setInterval(() => {
this.subject = null;
this.connect();
this.ws.onopen = () => {
clearInterval(refresh)
}
}, 5000);
}
return this.subject;
}
private create(url: string){
this.ws = new WebSocket(url);
const observable = Observable.create((obs: Subject<MessageEvent>) => {
this.ws.onmessage = obs.next.bind(obs);
this.ws.onerror = obs.error.bind(obs);
this.ws.onclose = obs.complete.bind(obs);
this.ws.onclose = function () {
console.log("trying to reconnect");
this.connect();
}
return this.ws.close.bind(this.ws);
});
const observer = {
next: (data: any) => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
};
return Subject.create(observer, observable);
}
I want to reconnect web socket if connection closes. At the moment the function gets truggered when i stop the web socket. BUt is not connecting again .I See error "this.connect is not a function" .How to work with angular recursive functions?
Don't use function keyword to create your callback when using this inside of it if you aren't aware of how it changes the this reference depending on the execution context, use arrow function instead
To make it reconnect, change this
this.ws.onclose = function () {
console.log("trying to reconnect");
this.connect();
}
To this
this.ws.onclose = () => {
console.log("trying to reconnect");
this.subject = null;
this.connect();
}

Reconnect WebSocket in Angular 5 with Rxjs observable and observer

In any case, if my application got disconnected from WebSocket I am not able to reconnect it. I am attaching the sample code please suggest me the idea to how I can reconnect WebSocket and initialize my identity on web socket server again.
I have made my application with the help of this tutorial.
https://tutorialedge.net/typescript/angular/angular-websockets-tutorial/
I have written the same code in my application except for my application requirement.
The tutorial that I have been followed does not have the feature to reconnect the WebSocket in any case like internet break or by some reason our WebSocket server got restart because I am running my WebSocket server with SupervisorD and it will automatically restart if WebSocket server get to stop in any case
My application is in production and many customers are using now so I can not change all flow and recreate the code for WebSocket in this application.
I am adding all code that I am using
websocket.service.ts
import { Injectable } from '#angular/core';
import * as Rx from 'rxjs/Rx';
#Injectable()
export class WebsocketService {
connected: boolean = false;
initialized: boolean= false;
constructor() { }
private subject: Rx.Subject<MessageEvent>;
public connect(url): Rx.Subject<MessageEvent> {
if (!this.subject) {
this.subject = this.create(url);
// console.log("Successfully connected: " + url);
}
return this.subject;
}
private create(url): Rx.Subject<MessageEvent> {
let ws = new WebSocket(url);
// here i am trying to reconnect my websocket
// setInterval (function () {
// if (ws.readyState !== 1) {
// ws = new WebSocket(url);
// this.initialized = false;
// }
// console.log(this.initialized);
// if (ws.readyState == 1 && this.initialized == false) {
// ws.send('{"type":"add",
"t":"14bfa6xxx", "from_numbers":
["xxxx","xxxxx"], "platform":"xxxx"}');
// this.initialized = true;
// }
// console.log(this.initialized);
// }, 4000);
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) {
if (data['type'] == 'add') {
console.log("Connection Initialized");
}
ws.send(JSON.stringify(data));
}
}
}
return Rx.Subject.create(observer, observable);
}
}
Chat.service.ts
import { Injectable } from '#angular/core';
import { Observable, Subject } from 'rxjs/Rx';
import { WebsocketService } from './websocket.service';
#Injectable()
export class ChatService {
public messages: Subject<Message>;
constructor(wsService: WebsocketService, private authService: AuthService) {
this.messages = <Subject<Message>>wsService
.connect(socket_url)
.map((response: MessageEvent): Message => {
const data = JSON.parse(response.data);
console.log(data);
return data;
});
}
}
and finality I have used this in our component to subscribe message.
constructor(private chatService: ChatService,) {
this.socketMessages();
}
socketMessages() {
this.chatService.messages.subscribe(msg => {
console.log(msg)
});
}

Spring Boot sockjs + stomp, cannot get a connection

I'm trying to get a sockjs + stomp connection to my spring boot websockets. This is my configuration:
#Configuration
#EnableWebSocketMessageBroker
public class WebsocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
private final String MESSAGE_BROKER_PREFIX = "/topic";
private final String WEBSOCKET_PREFIX = "/sockjs-node";
private final String REQUEST_PREFIX = "/";
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(WEBSOCKET_PREFIX)
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker(MESSAGE_BROKER_PREFIX);
config.setApplicationDestinationPrefixes(REQUEST_PREFIX);
}
}
And and my endpoint definition:
#Controller
public class Foo {
#SubscribeMapping("/{pipelineId}/{topic}")
private void subscribe(
HttpSession session,
#PathVariable String pipelineId,
#PathVariable String topic
) {
System.out.println(session.getId());
}
#EventListener
public void onApplicationEvent(SessionConnectEvent event) {
System.out.println(event.getSource());
}
#EventListener
public void onApplicationEvent(SessionDisconnectEvent event) {
System.out.println(event.getSessionId());
}
}
And from the javascript side:
var ws = new SockJS('/sockjs-node');
var client = Stomp.over(ws);
var subscription = client.subscribe("/topic/foo/bar", () => {
console.log("asdas");
});
but the connection does not happen and none of the methods get invoked. In the javascript console I can see:
>>> SUBSCRIBE
id:sub-0
destination:/topic/lala
stomp.js:199 Uncaught TypeError: Cannot read property 'send' of undefined
at Client._transmit (webpack:///./node_modules/#stomp/stompjs/lib/stomp.js?:199:26)
at Client.subscribe (webpack:///./node_modules/#stomp/stompjs/lib/stomp.js?:468:12)
at Object.eval (webpack:///./src/index.js?:128:27)
I am able to connect using wscat --connect ws://localhost:8080/sockjs-node/902/phebsu4o/websocket, but interestingly enough only the disconnect handler gets invoked and the connect handler doesn't. What am I missing here?
I found a js client which actually works on github.
import React from "react";
import SockJS from "sockjs-client";
import Stomp from "stompjs";
import PropTypes from "prop-types";
class SockJsClient extends React.Component {
static defaultProps = {
onConnect: () => {},
onDisconnect: () => {},
getRetryInterval: (count) => {return 1000 * count;},
headers: {},
autoReconnect: true,
debug: false
}
static propTypes = {
url: PropTypes.string.isRequired,
topics: PropTypes.array.isRequired,
onConnect: PropTypes.func,
onDisconnect: PropTypes.func,
getRetryInterval: PropTypes.func,
onMessage: PropTypes.func.isRequired,
headers: PropTypes.object,
autoReconnect: PropTypes.bool,
debug: PropTypes.bool
}
constructor(props) {
super(props);
this.state = {
connected: false
};
this.subscriptions = new Map();
this.retryCount = 0;
}
componentDidMount() {
this.connect();
}
componentWillUnmount() {
this.disconnect();
}
render() {
return (<div></div>);
}
_initStompClient = () => {
// Websocket held by stompjs can be opened only once
this.client = Stomp.over(new SockJS(this.props.url));
if (!this.props.debug) {
this.client.debug = () => {};
}
}
_cleanUp = () => {
this.setState({ connected: false });
this.retryCount = 0;
this.subscriptions.clear();
}
_log = (msg) => {
if (this.props.debug) {
console.log(msg);
}
}
connect = () => {
this._initStompClient();
this.client.connect(this.props.headers, () => {
this.setState({ connected: true });
this.props.topics.forEach((topic) => {
this.subscribe(topic);
});
this.props.onConnect();
}, (error) => {
if (this.state.connected) {
this._cleanUp();
// onDisconnect should be called only once per connect
this.props.onDisconnect();
}
if (this.props.autoReconnect) {
this._timeoutId = setTimeout(this.connect, this.props.getRetryInterval(this.retryCount++));
}
});
}
disconnect = () => {
// On calling disconnect explicitly no effort will be made to reconnect
// Clear timeoutId in case the component is trying to reconnect
if (this._timeoutId) {
clearTimeout(this._timeoutId);
}
if (this.state.connected) {
this.subscriptions.forEach((subid, topic) => {
this.unsubscribe(topic);
});
this.client.disconnect(() => {
this._cleanUp();
this.props.onDisconnect();
this._log("Stomp client is successfully disconnected!");
});
}
}
subscribe = (topic) => {
let sub = this.client.subscribe(topic, (msg) => {
this.props.onMessage(JSON.parse(msg.body));
});
this.subscriptions.set(topic, sub);
}
unsubscribe = (topic) => {
let sub = this.subscriptions.get(topic);
sub.unsubscribe();
this.subscriptions.delete(topic);
}
// Below methods can be accessed by ref attribute from the parent component
sendMessage = (topic, msg, opt_headers = {}) => {
if (this.state.connected) {
this.client.send(topic, opt_headers, msg);
} else {
console.error("Send error: SockJsClient is disconnected");
}
}
}
export default SockJsClient;

Resources