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');
}
}
Related
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,
I have an async function getIdentByInfo and in the console i get the right output if i log it in this function. As soon as i call it in another component it doesnt work and i only get 'undefined'. I know it has something to do with beeing ssynchrone and Promises but i cant figure out how to solve my issue. I need the Model class filled with attributes coming from the http request in another component to send them to another service
import { EventEmitter, Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { IdentModel } from "../models/identmodel.model";
import { IdentteilComponent } from "../pages/identteil/identteil.component";
#Injectable({
providedIn: 'root',
})
export class InfoWebservice {
url = 'http://localhost:4201';
ident: IdentModel[];
constructor(private http: HttpClient) { }
// promise vom typ IdentModel zurückgeben
getIdentByInfo(id: string, vwk: string) {
this.http.get(this.url).toPromise().then(data => {
for (let i in data){
this.ident.push(data[i])
if ( this.ident[i].identNr == id && this.ident[i].vwk == vwk){
return this.ident[i];
}
}
});
}
}
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
import { InfoWebservice } from '../../webservices/info.webservice'
import { ImageWebservice } from '../../webservices/image.webservice'
import { IdentModel } from "../../models/identmodel.model";
#Component({
selector: 'app-identteil',
templateUrl: './identteil.component.html',
styleUrls: ['./identteil.component.scss']
})
export class IdentteilComponent implements OnInit {
ident = [];
identNr:string;
vwk:string;
imgFrontLink:string;
imgBackLink:string;
constructor(private router: Router, private service: InfoWebservice, private image: ImageWebservice) { }
getIdentNr() : string {
var split = this.router.url.split("/");
this.identNr = split[2];
return this.identNr;
}
//return type is STRING
getVwk() {
// output von window.location.host = repapp-maw.dbl.de
// var splitHost = window.location.host.split(".");
var splitHost = 'repapp-maw';
var splitV = splitHost.split("-");
this.vwk = splitV[1];
return this.vwk;
}
callInfoService = async () => {
return await this.service.getIdentByInfo(this.getIdentNr(), this.getVwk());
}
ngOnInit() {
console.log(this.callInfoService());
}
}
When you use angular, its always preferred not to use await/Promise. Angular has an in-built RX-JS library which has tonnes of super-awesome functionalities that you can use.
For Example, in your case, you can do something like this:
// Your Service File can make use of 'Behavior Subject'
// Please read more about it here: https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject
import { EventEmitter, Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { IdentModel } from "../models/identmodel.model";
import { IdentteilComponent } from "../pages/identteil/identteil.component";
#Injectable({
providedIn: 'root',
})
export class InfoWebservice {
url = 'http://localhost:4201';
ident: IdentModel[];
initialIdentValues: IdentModel = [];
private identSource: BehaviorSubject<IdentModel[]> = new BehaviorSubject<IdentModel[]>(this.initialIdentValues);
public identValuesObs$: Observable<IdentModel[]> = this.identSource.asObservable();
// Create a method to set the values in component-1
setIdentValues(identValues: IdentModel[]) {
this.identSource.next(identValues);
}
// Create a method to return values in component-2 or any component
returnIdentValues() {
return this.identValuesObs$;
}
constructor(private http: HttpClient) { }
// Change your service call to this:
getIdentByInfo(id: string, vwk: string): Observable<any> {
return this.http.get(this.url);
}
}
Now in your component-1 where you want to set the values of this identvalues:
// Component-1
constructor(private infoWebService: InfoWebService){}
// Create a method where you get the values
someMethod() {
// Call the API method here and subscribe and then set the values
this.infoWebService.getIdentInfoById(id, vwk).subscribe((data: any) => {
// Your logic goes here ANDD
if (data) {
for (let i in data){
this.ident.push(data[i])
let localIdentsWithRequiredLogic = [];
if ( this.ident[i].identNr == id && this.ident[i].vwk == vwk){
localIdentsWithRequiredLogic.push(this.ident[i]);
}
// THIS IS IMPORTANT
this.infoWebService.setIdentValues(localIdentsWithRequiredLogic);
}
}
})
}
Then in component-2 or whatever component you want, you can retrieve it using the returnIdentValues method like this:
// In component-2
inSomeMethodWhereYouRequireIdentValues() {
this.infoWebService.returnIdentValues().subscribe(data => {
console.log(data) // this is data that you set in component one
})
}
I am working on a simple app where events are triggered once user logs in successfully.
All the events are working fine in my laptop but when i login from my phone, no events are getting triggered? Any possible reason?
Added Angular socket service code below:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import * as io from 'socket.io-client';
import { Observable } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class SocketService {
public prod = 'https://afad.sdfsd.com';
public dev = 'http://localhost:3001';
public baseUrl = this.dev;
private socket;
constructor(public http: HttpClient) {
this.socket=io('https://dfdsfs.sdfs.com')
}
public verifyUser=()=>{
return Observable.create((observer)=>{
this.socket.on('verifyUser',(data)=>{
observer.next(data);
})
})
}
public setUser=(authToken)=>{
this.socket.emit("set-user",authToken)
}
public userOffline=()=>{
return Observable.create((observer)=>{
this.socket.on('userOffline',(data)=>{
observer.next(data);
})
})
}
public userList=()=>{
return Observable.create((observer)=>{
this.socket.on('userlist',(data)=>{
observer.next(data);
})
})
}
public welcomeUser=(userid)=>{
return Observable.create((observer)=>{
this.socket.on(userid,(data)=>{
observer.next(data);
})
})
}
public userOnline=()=>{
return Observable.create((observer)=>{
this.socket.on('userOnline',(data)=>{
observer.next(data);
})
})
}
public disconnectUser = () => {
return Observable.create((observer) => {
this.socket.on('disconnect', () => {
observer.next()
})
})
}
}
I have installed socketio-client to achieve this functionality.
Any breakthrough will be helpful
I guess your laptop and phone are connected via WIFI to the router. If so you won't be able to use https://afad.sdfsd.com or http://localhost:3001 due to them only being available only on the laptop.
Try pulling the hostname from the browser and use it as the base URL (window.location.host)
when you are using the laptop it will be localhost:3001 and for the phone it will be IP_OF_THE_3001.
...
export class SocketService {
private socket;
constructor(public http: HttpClient) {
this.socket=io(window.location.host)
}
...
}
I was able to figure out the issue here,
socket.emit will emit an event to only current user, We should use myio.emit in this case.
I am editing using kendo ui angular grid in using rest api, that opens the edit component in the popup.
I am referring https://www.telerik.com/kendo-angular-ui/components/grid/editing/external-editing/
When I edit the record, the grid does not gets refreshed, only after if I F5 the page, the updated data is shown.
//Emp-List Component
private view: Observable<GridDataResult>;
constructor(private service: EmployeeService) {
this.view = this.service;
this.service.getEmpList();
}
public saveHandler(employee: Employee) {
this.service.updateEmployee(employee);
this.service.getEmpList();
this.editDataItem = undefined;
}
export class EmployeeService extends BehaviorSubject<GridDataResult> {
constructor(private http: HttpClient) {
super(null);
}
readonly APIUrl = 'http://localhost:12121/api/employee';
private getEmployee(): Observable<GridDataResult> {
return this.http.get(`${this.APIUrl}`).pipe(
map(
(response) =>
<GridDataResult>{
data: response,
total: 100,
}
)
);
}
public getEmpList(): void {
this.getEmployee().subscribe((x) => super.next(x));
}
updateEmployee(employee: Employee) {
return this.http
.put(`${this.APIUrl}/${employee.employeeID}`, employee)
.subscribe((data) => {
console.log('Employee updated ', data);
});
}
}
I have a function in which I'm calling an instance of Manager's onSpecificData() to which I'm subscribing in order to update my application's state (I'm managing a state on the server-side as well).
The problem is that in the SomeManager's implementation of onSpecificData() I'm merging 3 different Observables using merge() operator, which for some reason triggers the invocation of all the underlying Observable's operators even though only 1 of the sources is the one that's emitting a value
SomeManager.ts
export class DerivedManager implements Manager {
private driver: SomeDriver;
constructor(...) {
this.driver = new SomeDriver(...);
}
public onSpecificData(): Observable<DataType> {
return merge(
this.driver.onSpecificData(Sources.Source1).map((value) => {
return {source1: value};
}),
this.driver.onSpecificData(Sources.Source2).map((value) => {
return {source2: value};
}),
this.driver.onSpecificData(Sources.Source3).map((value) => {
return {source3: value};
})
);
}
Manager.ts
export type DataType = Partial<{value1: number, value2: number, value3: number}>;
export interface Manager {
onSpecificData(): Observable<DataType>;
}
SomeDriver.ts
export const enum Sources {
Source1,
Source2,
Source3,
}
export class SomeDriver extends Driver {
private static specificDataId = 1337; // some number
private handler: Handler;
constructor(...) {
super(...);
this.handler = new Handler(this.connection, ...);
// ...
}
// ...
onSpecificData(source: Sources): Observable<number> {
return this.handler
.listenToData<SpecificDataType>(
SomeDriver.specificDataId,
(data) => data.source === source)
).map((data) => data.value);
}
}
Driver.ts
export abstract class Driver {
protected connection: Duplex;
constructor(...) {
// init connection, etc...
}
public abstract onSpecificData(source: number);
// some implementations and more abstract stuff...
}
Handler.ts
export class Handler {
private data$: Observable<Buffer>;
constructor(private connection: Duplex, ...) {
this.data$ = Observable.fromEvent<Buffer>(connection as any, 'data');
}
listenToData<T>(dataId: number, filter?: (data: T) => boolean) {
return this.data$
.map((data) => {
// decode and transform
})
.filter((decodedData) => !decodedData.error && decodedData.value.id)
.do((decodedData) => {
console.log(`Got ${decodedData.value.id}`);
})
.map((decodedData) => decodedData.value.value as T)
.filter(filter || () => true);
}
}
And finally, subscribe()-ing:
export default function(store: Store<State>, manager: Manager) {
// ...
manager.onSpecificData()
.subscribe((data) => {
// update state according to returned data
});
}
As you can see, there is only 1 underlying Observable (data$) but apparently the operator chain in listenToData<T>() is invoked 3 times for each value emitted by it. I already know this is because of SomeManager#onSpecificData()'s merge of those 3 Observables, but I don't know why this happens. I want it to be invoked once for each value.
Help will be much appreciated.
I solved this in a "hacky" way, in my opinion. I replaced data$ with a Subject, created an observable from stream's 'data' event, moving all the shared logic to that observable and emit a value from the subject, like so:
export class Handler {
private dataSrc = new Subject<DecodedData>();
constructor(private connection: Duplex, ...) {
Observable.fromEvent<Buffer>(connection as any, 'data')
.map((data) => {
// decode and transform
})
.filter((decodedData) => !decodedData.error)
.do((decodedData) => {
console.log(`Got ${decodedData.value.id}`);
})
.subscribe((decodedData) => {
this.dataSrc.next(decodedData);
});
}
listenToData<T>(dataId: number, filter?: (data: T) => boolean) {
return this.dataSrc
.filter((decodedData) => decodedData.value.id === dataId)
.map((decodedData) => decodedData.value.value as T)
.filter(filter || () => true);
}
}
Not exactly the solution I was looking for, but it works. If anyone has a better solution, which better suits the "Rx way" to do stuff, I'd love to hear it.