Angular Transfer State not preventing repeat http calls - javascript

I have the http request being made a service which is injected onto my component and subscribed to from there. Since I introduced server side rendering with angular universal to my application, the results on the page are repeated at least twice.
I have method which is called on click, which performs the http request to facebook's api
getAlbum(albumId: number) {
this.albumPhotos = this.state.get(ALBUM_PHOTOS_KEY, null as any);
if (!this.albumPhotos) {
this.facebookService.getBachadiffAlbumPhotos(albumId).subscribe(res => {
this.bachataPicsArray = res;
this.state.set(ALBUM_PHOTOS_KEY, res as any);
});
}
}
I declared the const variable below the imports
const ALBUM_PHOTOS_KEY = makeStateKey('albumPhotos');
And I also declared the property
albumNames: any;
I am assuming I have done all of the imports right I have the code on github in the gallery component.

You are on the right pass, you just need to handle your service differently if you are on the server or the browser side to perform your queries only once and not twice.
Pseudo logic:
If server -> Do http request -> Set value in transfer-state
If browser -> Get value from transfer-state
To do so, you could for example enhance your Service like following:
#Injectable()
export class FacebookEventsService {
const ALBUM_PHOTOS_KEY: StateKey<number>;
constructor(#Inject(PLATFORM_ID) private platformId: Object, private http: HttpClient) {
this.ALBUM_PHOTOS_KEY = makeStateKey('albumPhotos');
}
getBachaDiffFacebookEvents(): Observable<CalendarEvent[]> {
// Here we check if server or browser side
if (isPlatformServer(this.platformId)) {
return this.getServerBachaDiffFacebookEvents();
} else {
return this.getBrowserBachaDiffFacebookEvents();
}
}
getServerBachaDiffFacebookEvents(): Observable<CalendarEvent[]> {
return this.http.get(this.facebookEventsUrl)
.map(res => {
// Here save also result in transfer-state
this.transferState.set(ALBUM_PHOTOS_KEY, calendarEvents);
});
}
getBrowserBachaDiffFacebookEvents(): Observable<CalendarEvent[]> {
return new Observable(observer => {
observer.next(this.transferState.get(ALBUM_PHOTOS_KEY, null));
});
}
}
UPDATE
To use this logic you would also need:
TransferHttpCacheModule (to be initialized in app.module.ts).
TransferHttpCacheModule installs a Http interceptor that avoids
duplicate HttpClient requests on the client, for requests that were
already made when the application was rendered on the server side.
https://github.com/angular/universal/tree/master/modules/common
ServerTransferStateModule on the server side and BrowserTransferStateModule on the client side to use TransferState
https://angular.io/api/platform-browser/TransferState
P.S.: Note that if you do so and enhance your server, of course you would not need anymore to set the value in transfer-state in your getAlbum() method you displayed above
UPDATE 2
If you want to handle the server and browser side as you did in your gallery.component.ts, you could do something like the following:
getAlbum(albumId: number) {
if (isPlatformServer(this.platformId)) {
if (!this.albumPhotos) {
this.facebookService.getBachadiffAlbumPhotos(albumId).subscribe(res => {
this.bachataPicsArray = res;
this.state.set(ALBUM_PHOTOS_KEY, null);
});
}
} else {
this.albumPhotos = this.state.get(ALBUM_PHOTOS_KEY,null);
}
}
UPDATE 3
The thing is, your action getAlbum is never called on the server side. This action is only used on the browser side, once the page is rendered, when the user click on a specific action. Therefore, using transfer-state in that specific case isn't correct/needed.
Furthermore not sure that the Observable in your service was correctly subscribed.
Here what to change to make it running:
gallery.component.ts
getAlbum(albumId: number) {
this.facebookService.getBachadiffAlbumPhotos(albumId).subscribe(res => {
this.albumPhotos = res;
});
}
facebook-events.service.ts
getBachadiffAlbumPhotos(albumId: number): Observable<Object> {
this.albumId = albumId;
this.facebookAlbumPhotosUrl = `https://graph.facebook.com/v2.11/${this.albumId}/photos?limit=20&fields=images,id,link,height,width&access_token=${this.accessToken}`;
return Observable.fromPromise(this.getPromiseBachaDiffAlbumPhotos(albumId));
}
private getPromiseBachaDiffAlbumPhotos(albumId: number): Promise<{}> {
return new Promise((resolve, reject) => {
this.facebookAlbumPhotosUrl = `https://graph.facebook.com/v2.11/${this.albumId}/photos?limit=20&fields=images,id,link,height,width&access_token=${this.accessToken}`;
let facebookPhotos: FacebookPhoto[] = new Array();
let facebookPhoto: FacebookPhoto;
const params: HttpParams = new HttpParams();
this.http.get(this.facebookAlbumPhotosUrl, {params: params})
.subscribe(res => {
let facebookPhotoData = res['data'];
for (let photo of facebookPhotoData) {
facebookPhotos.push(
facebookPhoto = {
id: photo.id,
image: photo.images[3].source,
link: photo.link,
height: photo.height,
width: photo.width
});
}
resolve(facebookPhotos);
}, (error) => {
reject(error);
});
});
}
UPDATE 4
ngOnInit is executed on the server side, this means that my very first answer here has to be use in this case.
Furthermore, also note that on the server side you doesn't have access to the window, therefore calling $
With gallery.component.ts you could do something like this to run only the http queries once but this won't solve all your problems, I think it will still need further improvements.
ngOnInit() {
if (isPlatformServer(this.platformId)) {
this.facebookService.getBachadiffFacebookVideos().subscribe(res => {
this.bachataVidsArray = res;
this.state.set(VIDEOS_KEY, res as any);
});
this.facebookService.getBachadiffFacebookLastClassPictures().subscribe(res => {
this.bachataPicsArray = res;
this.state.set(LAST_CLASS_PICTURES_KEY, res as any);
});
this.facebookService.getBachadiffAlbumNames().subscribe(res => {
this.bachataAlbumHeaderNames = res;
this.state.set(ALBUM_NAMES_KEY, res as any);
});
} else {
$('ul.tabs').tabs();
this.bachataVidsArray = this.state.get(VIDEOS_KEY, null as any);
this.bachataPicsArray = this.state.get(LAST_CLASS_PICTURES_KEY, null as any);
this.bachataAlbumHeaderNames = this.state.get(ALBUM_NAMES_KEY, null as any);
}
}

Related

fetch data async right after save and reload the component

I'm retrieving data from an API, right now there are only two endpoints, one for adding users and other one for get the users added. The users are added using a button and its textbox.
let [showAlert, setAlertState] = useState(false);
let [twitterAccount, setTwitterAcount] = useState("Twitter Account here");
let [dataInfo, setDataInfo] = useState([]);
const getCurrentSpiedUsers = async () => {
fetch("https://localhost:7021/GetCurrentSpiedUsers")
.then((res) => res.json())
.then((res) => {setDataInfo(res)});
}
useEffect(function () {
getCurrentSpiedUsers();
}, []);
This part works as expected, the first time I enter into the website it loads data from the API.
This one is the other method for adding users.
const addTwitterAccount = (account) => {
fetch(`https://localhost:7021/AddTwitterAccount?account=${account}`, {
method:'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
}
)
.then((res) => res.json())
.then((res) => { });
}
and this is the function inside of the onClick event button.
const activeAlert = () => {
addTwitterAccount(twitterAccount);
getCurrentSpiedUsers();
setAlertState(true);
setTimeout(() => {
setAlertState(false);
}, 5000);
};
but this is not working as expected, once I click the button I add the account correctly, but can't reload the component using the getCurrentSpiedUsers(); function. I noticed that when I click twice, I can get the last one but not the actual one, so I assume it's because the code it's executed faster than my function is retrieving the data from the API.
I tried using async/await for both methods, but the result is always the same. What can I do?
Updated with the server-side:
AddTwitterAcccount endpoint:
[ApiController]
[Route("[controller]")]
public class AddTwitterAccount : ControllerBase
{
private readonly ISpiedAccounts _spiedAccounts;
public AddTwitterAccount(ISpiedAccounts spiedAccounts)
{
_spiedAccounts = spiedAccounts;
}
[HttpPost(Name = "AddTwitterAccount")]
public void AddAccount([FromQuery] string account)
{
_spiedAccounts.AddTwitterAccount(account);
}
}
SpiedAccounts class:
public interface ISpiedAccounts
{
public void AddTwitterAccount(string accountName);
public List<TwitterAccount> GetTwitterAccounts();
}
public class SpiedAccounts : ISpiedAccounts
{
private List<TwitterAccount> accounts = new();
private ResponseMessage _responseMessage;
public void AddTwitterAccount(string accountName)
{
if (accounts.Any(account => account.ScreenName == accountName))
{
_responseMessage = ResponseMessage.UserExists;
return;
}
int maxUsersSpied = 5;
if (accounts.Count <= maxUsersSpied)
{
accounts.Add(new TwitterAccount
{
ScreenName = accountName
});
_responseMessage = ResponseMessage.Added;
return;
}
_responseMessage = ResponseMessage.LimitUsersExceeded;
}
public string GetResponseMessage()
{
return _responseMessage switch
{
ResponseMessage.Added
=> "The account was added correctly.",
ResponseMessage.LimitUsersExceeded
=> "Only can be spied 6 accounts at the time. Please, wait for one of them to be free.",
ResponseMessage.UserExists
=> "This account is currently being spied.",
_
=> string.Empty
};
}
public List<TwitterAccount> GetTwitterAccounts()
=> accounts;
}
public enum ResponseMessage
{
Added,
LimitUsersExceeded,
UserExists
}
There's a race condition between the WRITE (addTwitterAccount) and the READ (getCurrentSpiedUsers).
In other words, when you call addTwitterAccount() it will request the addition of a new twitter account, but nothing guarantees such operation has been completed (aka the account has been added) before getCurrentSpiedUsers() reads info from the datasource.
In fact, given you don't await for addTwitterAccount(), getCurrentSpiedUsers() will likely never bring the new account in its results.
What to do?
First, await for the result of the POST (addTwitterAccount()) before proceeding to the GET (addTwitterAccount()):
const activeAlert = async () => { // added async here
await addTwitterAccount(twitterAccount); // added await here
getCurrentSpiedUsers();
...
For that to work, make sure the addTwitterAccount() returns the Promise:
const addTwitterAccount = (account) => {
return fetch(`https://loca......ount?account=${account}`, { // added return
Second, edit the (server-side) endpoint at https://localhost:7021/AddTwitterAccount to guarantee** it only returns after the account has been completely added.
** There are other alternatives to this. You can make this endpoint be async, but you would need to use other tactics to know the user has been added. But all these other tactics would involve way more work, and you likely don't need the scalability they would provide you at the moment. Example tactics: WebSockets, SSE, long-polling. Making the endpoint block until the account is created, as I suggested above, is likely the more cost-effective solution at this point.
you can't use multiple setState in one event it will do only the first one,
my suggestion is to use only one state with an object that contains your data

How to detect which message was sent from the Websocket server

I have a small web application listening for incoming messages from a Websocket server. I receive them like so
const webSocket = new WebSocket("wss://echo.websocket.org");
webSocket.onopen = event => webSocket.send("test");
webSocket.onmessage = event => console.log(event.data);
but the sending server is more complex. There are multiple types of messages that could come e.g. "UserConnected", "TaskDeleted", "ChannelMoved"
How to detect which type of message was sent? For now I modified the code to
const webSocket = new WebSocket("wss://echo.websocket.org");
webSocket.onopen = event => {
const objectToSend = JSON.stringify({
message: "test-message",
data: "test"
});
webSocket.send(objectToSend);
};
webSocket.onmessage = event => {
const objectToRead = JSON.parse(event.data);
if (objectToRead.message === "test-message") {
console.log(objectToRead.data);
}
};
So do I have to send an object from the server containing the "method name" / "message type" e.g. "TaskDeleted" to identify the correct method to execute at the client? That would result in a big switch case statement, no?
Are there any better ways?
You can avoid the big switch-case statement by mapping the methods directly:
// List of white-listed methods to avoid any funny business
let allowedMethods = ["test", "taskDeleted"];
function methodHandlers(){
this.test = function(data)
{
console.log('test was called', data);
}
this.taskDeleted = function(data)
{
console.log('taskDeleted was called', data);
}
}
webSocket.onmessage = event => {
const objectToRead = JSON.parse(event.data);
let methodName = objectToRead.message;
if (allowerMethods.indexOf(methodName)>=0)
{
let handler = new methodHandlers();
handler[methodName](data);
}
else
{
console.error("Method not allowed: ", methodName)
}
};
As you have requested in one of your comments to have a fluent interface for the websockets like socket.io.
You can make it fluent by using a simple PubSub (Publish Subscribe) design pattern so you can subscribe to specific message types. Node offers the EventEmitter class so you can inherit the on and emit events, however, in this example is a quick mockup using a similar API.
In a production environment I would suggest using the native EventEmitter in a node.js environment, and a browser compatible npm package in the front end.
Check the comments for a description of each piece.
The subscribers are saved in a simple object with a Set of callbacks, you can add unsubscribe if you need it.
note: if you are using node.js you can just extend EventEmitter
// This uses a similar API to node's EventEmitter, you could get it from a node or a number of browser compatible npm packages.
class EventEmitter {
// { [event: string]: Set<(data: any) => void> }
__subscribers = {}
// subscribe to specific message types
on(type, cb) {
if (!this.__subscribers[type]) {
this.__subscribers[type] = new Set
}
this.__subscribers[type].add(cb)
}
// emit a subscribed callback
emit(type, data) {
if (typeof this.__subscribers[type] !== 'undefined') {
const callbacks = [...this.__subscribers[type]]
callbacks.forEach(cb => cb(data))
}
}
}
class SocketYO extends EventEmitter {
constructor({ host }) {
super()
// initialize the socket
this.webSocket = new WebSocket(host);
this.webSocket.onopen = () => {
this.connected = true
this.emit('connect', this)
}
this.webSocket.onerror = console.error.bind(console, 'SockyError')
this.webSocket.onmessage = this.__onmessage
}
// send a json message to the socket
send(type, data) {
this.webSocket.send(JSON.stringify({
type,
data
}))
}
on(type, cb) {
// if the socket is already connected immediately call the callback
if (type === 'connect' && this.connected) {
return cb(this)
}
// proxy EventEmitters `on` method
return super.on(type, cb)
}
// catch any message from the socket and call the appropriate callback
__onmessage = e => {
const { type, data } = JSON.parse(e.data)
this.emit(type, data)
}
}
// create your SocketYO instance
const socket = new SocketYO({
host: 'wss://echo.websocket.org'
})
socket.on('connect', (socket) => {
// you can only send messages once the socket has been connected
socket.send('myEvent', {
message: 'hello'
})
})
// you can subscribe without the socket being connected
socket.on('myEvent', (data) => {
console.log('myEvent', data)
})

Why the GET request from Angular in the NodeJS sometimes come as null although there are data

I am having sometimes a little problem with the GET request in Angular.
I am using ReplaySubject and return the Observable but sometimes in the reload of the app the get request it is working but it is getting null data or the message No content although there are the data.
In 3-4 tries then it shows the data.
The request get works but sometimes is null and sometimes give me the data.
Can someone help me on this ?
And if it possible to give any idea to use the ReplaySubject or something like, because i need to reload page everytime to fetch new data.
This is my Frontend part.
export class ModelDataService {
public baseUrl = environment.backend;
private data = new ReplaySubject<any>(1);
public userID = this.authService.userID;
constructor(private http: HttpClient, private authService: AuthService) {
}
public getJSON() {
return this.http.get(`${this.baseUrl}/data/${this.userID}`).subscribe(res => this.data.next(res));
}
public dataModel(): Observable<any> {
return this.data.asObservable();
}
public setData(data: Model) {
const api = `${this.baseUrl}/data`;
const user_id = this.authService.userID;
this.http.post(api, data, {
headers: {user_id}
}).subscribe(res => this.data.next(res));
}
public updateDate(id: string, dataModel: Model) {
const api = `${this.baseUrl}/data/${id}`;
return this.http.put(api, dataModel).subscribe(res => res);
}
}
This is the component which I get data
ngOnInit() {
this.authService.getUserProfile(this.userID).subscribe((res) => {
this.currentUser = res.msg;
});
this.modelDataService.getJSON();
this.model$ = this.modelDataService.dataModel();
this.model$.subscribe((test) => {
this.model = test;
});
this.model$.subscribe((test) => {
this.model = test;
});
}
This is the backend part.
const ModelData = require("../models/data");
async show(req, res) {
let modelData;
await ModelData.findOne({user: req.params.id}, (error, user) => {
modelData = user;
});
if (!modelData) {
res.status(204).json({error: "No Data"});
return;
}
return res.status(200).send(modelData);
},
routes.get("/data/:id", ModelDataController.show);
routes.post("/data", ModelDataController.store);
routes.put("/data/:id", ModelDataController.update);
If you pass in a callback function, Mongoose will execute the query asynchronously and pass the results to the callback. Sometimes you don't get the data because the query is not finished excuting. To fix this, you can change your code to:
let modelData = await ModelData.findOne({user: req.params.id});
if (!modelData)...

javascript callback is called twice in an impossible invocation

I built a TS, MongoDB Client wrapper. for some reason when I call the function that gets the connection, its callback is called twice.
There are 2 calls in total to the get() function, 1 before the export as you can see and another from a mocha test.
I am pretty new to TS and JS in general, but this seems a bit off.
import {Db, MongoClient} from "mongodb";
import {MongoConfig} from '../config/config'
class DbClient {
private cachedDb : Db = null;
private async connectToDatabase() {
console.log('=> connect to database');
let connectionString : string = "mongodb://" + MongoConfig.host + ":" + MongoConfig.port;
return MongoClient.connect(connectionString)
.then(db => {
console.log('=> connected to database');
this.cachedDb = db.db(MongoConfig.database);
return this.cachedDb;
});
}
public async get() {
if (this.cachedDb) {
console.log('=> using cached database instance');
return Promise.resolve(this.cachedDb);
}else{
return this.connectToDatabase();
}
}
}
let client = new DbClient();
client.get();
export = client;
where the console output is:
=> connect to database
=> connected to database
=> connected to database
Any particular reason this is misbehaving?
There are 2 calls in total to the get() function, 1 before the export as you can see and another from a mocha test.
I suspect the output has an additional => connect to database. As I said in the comments: There's a "race condition" where get() could be called multiple times before this.cachedDb is set which would lead to multiple connections/instances of Db being created.
For example:
const a = client.get();
const b = client.get();
// then
a.then(resultA => {
b.then(resultB => {
console.log(resultA !== resultB); // true
});
});
Solution
The problem can be fixed by storing the promise as the cached value (also, no need to have the async keyword on the methods as Randy pointed out, as there's no values being awaited in any of the methods so you can just return the promises):
import {Db, MongoClient} from "mongodb";
import {MongoConfig} from '../config/config'
class DbClient {
private cachedGet: Promise<Db> | undefined;
private connectToDatabase() {
console.log('=> connect to database');
const connectionString = `mongodb://${MongoConfig.host}:${MongoConfig.port}`;
return MongoClient.connect(connectionString);
}
get() {
if (!this.cachedGet) {
this.cachedGet = this.connectToDatabase();
// clear the cached promise on failure so that if a caller
// calls this again, it will try to reconnect
this.cachedGet.catch(() => {
this.cachedGet = undefined;
});
}
return this.cachedGet;
}
}
let client = new DbClient();
client.get();
export = client;
Note: I'm not sure about the best way of using MongoDB (I've never used it), but I suspect connections should not be so long lived as to be cached like this (or should probably only be cached for a short time and then disconnected). You'll need to investigate that though.

not able to access class methods using the class instance returned by a static method

I have created a subscriber class to store subscriber details and use a static method to return the instance of the class, but I am not able to set the values using the instance
Here is the subscriber class:
let _instance;
export class Subscriber {
constructor(username, password) {
this._username = username;
this._password = password;
}
setSubscriberId(subscriberId) {
cy.log(subscriberId);
this._subscriberId = subscriberId;
}
setSessionId(sessionId) {
this.sessionId = sessionId;
}
getUserName = () => {
return this._username;
}
getPassword = () => {
return this._password;
}
getSubsciberId() {
return this._subscriberId;
}
getSessionId() {
return this.sessionId;
}
static createSubscriber(username, password) {
if (!_instance) {
_instance = new Subscriber(username, password);
}
return _intance;
}
static getSubscriber() {
return _instance;
}
}
I am creating a instance of the class in before block and accessing the instance in Given block
before("Create a new subscriber before the tests and set local storage", () => {
const username = `TestAutomation${Math.floor(Math.random() * 1000)}#sharklasers.com`;
const password = "test1234";
subscriberHelpers.createSubscriber(username, password, true).then((response) => {
cy.log(response);
Subscriber.createSubscriber(username, password);
Subscriber.getSubscriber().setSubscriberId(response.Subscriber.Id);
Subscriber.getSubscriber().setSessionId(response.SessionId);
}).catch((error) => {
cy.log(error);
});
});
Given(/^I launch selfcare app$/, () => {
cy.launchApp();
});
Given(/^I Set the environemnt for the test$/, () => {
cy.log(Subscriber.getSubscriber());
cy.log(Subscriber.getSubscriber().getSubsciberId());
});
here is the output on the cypress console
Questions:
Why the subscriberID is null even though I am setting it in the before block
if I print the subscriber Object why am I not seeing subscriberID
Here is the output of subscriber object
Properties username and password are defined synchronously in before(), so are present on the object when tested.
But subscriberId is obtained asynchronously, so you will need to wait for completion inside the test, e.g
cy.wrap(Subscriber.getSubscriber()).should(function(subscriber){
expect(subscriber.getSubsciberId()).not.to.be.null
})
Refer to wrap - Objects to see how to handle an object with Cypress commands.
and see should - Differences
When using a callback function with .should() or .and(), on the other hand, there is special logic to rerun the callback function until no assertions throw within it.
In other words, should will retry (up to 5 seconds) until the expect inside the callback does not fail (i.e in your case the async call has completed).

Categories

Resources