I am newbie trying out rxjs and nestjs. The use case that I am currently trying to accomplish is for educational purpose. So I wanted to read a json file (throw an observable error in case of the file being empty or cannot be read) using the "fs" module. Now I create an observable by reading the file asynchronously, set the observer in the subject and then subscribe to the subject in the controller. Here is my code in the service
#Injectable()
export class NewProviderService {
private serviceSubject: BehaviorSubject<HttpResponseModel[]>;
// this is the variable that should be exposed. make the subject as private
// this allows the service to be the sole propertier to modify the stream and
// not the controller or components
serviceSubject$: Observable<HttpResponseModel[]>;
private serviceErrorSubject: BehaviorSubject<any>;
serviceErrorSubject$: Observable<any>;
filePath: string;
httpResponseObjectArray: HttpResponseModel[];
constructor() {
this.serviceSubject = new BehaviorSubject<HttpResponseModel[]>([]);
this.serviceSubject$ = this.serviceSubject.asObservable();
this.serviceErrorSubject = new BehaviorSubject<any>(null);
this.serviceErrorSubject$ = this.serviceErrorSubject.asObservable();
this.filePath = path.resolve(__dirname, './../../shared/assets/httpTest.json');
}
readFileFromJson() {
return new Promise((resolve, reject) => {
fs.exists(this.filePath.toString(), exists => {
if (exists) {
fs.readFile(this.filePath.toString(), 'utf-8' , (err, data) => {
if (err) {
logger.info('error in reading file', err);
return reject('Error in reading the file' + err.message);
}
logger.info('file read without parsing fg', data.length);
if ((data.length !== 0) && !isNullOrUndefined(data) && data !== null) {
// this.httpResponseObjectArray = JSON.parse(data).HttpTestResponse;
// logger.info('array obj is:', this.httpResponseObjectArray);
logger.info('file read after parsing new', JSON.parse(data));
return resolve(JSON.parse(data).HttpTestResponse);
} else {
return reject(new FileExceptionHandler('no data in file'));
}
});
} else {
return reject(new FileExceptionHandler('file cannot be read at the moment'));
}
});
});
}
getData() {
from(this.readFileFromJson()).pipe(map(data => {
logger.info('data in obs', data);
this.httpResponseObjectArray = data as HttpResponseModel[];
return this.httpResponseObjectArray;
}), catchError(error => {
return Observable.throw(error);
}))
.subscribe(actualData => {
this.serviceSubject.next(actualData);
}, err => {
logger.info('err in sub', typeof err, err);
this.serviceErrorSubject.next(err);
});
}
Now this is the controller class
#Get('/getJsonData')
public async getJsonData(#Req() requestAnimationFrame,#Req() req, #Res() res) {
await this.newService.getData();
this.newService.serviceSubject$.subscribe(data => {
logger.info('data subscribed', data, _.isEmpty(data));
if (!isNullOrUndefined(data) && !_.isEmpty(data)) {
logger.info('coming in');
res.status(HttpStatus.OK).send(data);
res.end();
}
});
}
The problem I face is that I can get the file details for the first time and the subscription is getting called once > its working fine. On the subsequent requests
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (_http_outgoing.js:470:11)
at ServerResponse.header (C:\personal\Node\test-nest.js\prj-sample\node_modules\express\lib\response.js:767:10)
at Ser
and the endpoint /getJsonData results in an error. Could someone help me out. i believe the subscription is not getting properly after the first call, but not sure how to end that and how to resolve that
The problem is that you're subscribing to your serviceSubject in your controller. Every time a new value is emitted, it will try to send the response. This works the first time, but the second time it will tell you it can't send the same response again; the request has already been handled.
You can use the pipeable first() operator to complete the Observable after the first value:
#Get('/getJsonData')
public async getJsonData() {
await this.newService.getData();
return this.newService.serviceSubject$.pipe(first())
}
You want your Observable to be shared (hot), so that every subscriber always gets the same, latest value. That's exactly what a BehaviourSubject does. So you should not convert your Subject to an Observable when you expose it publicly because you will lose this desired behavior. Instead, you can just cast your Subject to Observable, so that internally it is still a subject but it will not expose the next() method to emit new values publicly:
private serviceSubject: BehaviorSubject<HttpResponseModel[]>;
get serviceSubject$(): Observable<HttpResponseModel[]> {
return this.serviceSubject;
}
I think trying to convert the cold observable (the one that I created) to a hot/warm observable might help to plugin to a single source and emit and complete its execution and maintain the last emitted data to any cloned values. So I make the cold observable to a warm observable using the publishLast(), refCount() operators, and I could achieve the single subscription and the execution completion of the observable. Here are the change I made to work.
This is the service class change I made
getData() {
return from(this.readFileFromJson()).pipe(map(data => {
logger.info('data in obs', data);
this.httpResponseObjectArray = data as HttpResponseModel[];
return this.httpResponseObjectArray;
}), publishLast(), refCount()
, catchError(error => {
return Observable.throw(error);
}));
// .subscribe(actualData => {
// this.serviceSubject.next(actualData);
// }, err => {
// logger.info('err in sub', typeof err, err);
// this.serviceErrorSubject.next(err);
// });
}
And this is the change I made in the controller
public async getJsonData(#Req() req, #Res() res) {
let jsonData: HttpResponseModel[];
await this.newService.getData().subscribe(data => {
logger.info('dddd', data);
res.send(data);
});
}
Any answers that allow the observables to be first subscribed to subjects and then subscribing that subject in the controller is also welcome.
I found a great post on hot vs cold observables and how to make an observable subscribe to a single source and convert a cold, to a hot/warm observable - https://blog.thoughtram.io/angular/2016/06/16/cold-vs-hot-observables.html
I would recommend to return the Promise directly to the controller. Here, you don't need an Observable. For the subscribers, you additionally emit the value of the Promise to your serviceSubject.
async getData() {
try {
const data = await this.readFileFromJson();
this.serviceSubject.next(data as HttpResponseModel[]);
return data;
} catch (error) {
// handle error
}
}
In your controller you can just return the Promise:
#Get('/getJsonData')
public async getJsonData() {
return this.newService.getData();
}
Related
I have a set of functions in Node.js that I would like to load in a certain order. I will provide some mockup code abstracted and simplified:
function updateMyApp() {
loadDataToServer()
.then(() => useData())
.then(() => saveData())
.then(() => { console.log("updateMyApp done") })
}
function loadDataToServer() {
return new Promise( (resolve, reject) {
...preparing data and save file to cloud...
resolve()})
}
function handleDataItem(item) {
// Function that fetches data item from database and updates each data item
console.log("Name", item.name)
}
function saveData() {
// Saves the altered data to some place
}
useData is a bit more complex. In it I would like to, in order:
console.log('Starting alterData()')
Load data, as json, from the cloud data source
Iterate through every item in the json file and do handleDataItem(item) on it.
When #2 is done -> console.log('alterData() done')
Return a resolved promise back to updateMyApp
Go on with saveData() with all data altered.
I want the logs to show:
Starting useData()
Name: Adam
Name: Ben
Name: Casey
useData() done
my take on this is the following:
function useData() {
console.log('Starting useData()')
return new Promise( function(resolve, reject) {
readFromCloudFileserver()
.then(jsonListFromCloud) => {
jsonListFromCloud.forEach((item) => {
handleDataItem(item)
}
})
.then(() => {
resolve() // I put resolve here because it is not until everything is finished above that this function is finished
console.log('useData() done')
}).catch((error) => { console.error(error.message) })
})
}
which seems to work but, as far as I understand this is not how one is supposed to do it. Also, this seems to do the handleDataItem outside of this chain so the logs look like this:
Starting useData()
useData() done
Name: Adam
Name: Ben
Name: Casey
In other words. It doesn't seem like the handleDataItem() calls are finished when the chain has moved on to the next step (.then()). In other words, I can not be sure all items have been updated when it goes on to the saveData() function?
If this is not a good way to handle it, then how should these functions be written? How do I chain the functions properly to make sure everything is done in the right order (as well as making the log events appear in order)?
Edit: As per request, this is handleDataItem less abstracted.
function handleDataItem(data) {
return new Promise( async function (resolve) {
data['member'] = true
if (data['twitter']) {
const cleanedUsername = twitterApi.cleanUsername(data['twitter']).toLowerCase()
if (!data['twitter_numeric']) {
var twitterId = await twitterApi.getTwitterIdFromUsername(cleanedUsername)
if (twitterId) {
data['twitter_numeric'] = twitterId
}
}
if (data['twitter_numeric']) {
if (data['twitter_protected'] != undefined) {
var twitterInfo = await twitterApi.getTwitterGeneralInfoToDb(data['twitter_numeric'])
data['twitter_description'] = twitterInfo.description
data['twitter_protected'] = twitterInfo.protected
data['twitter_profile_pic'] = twitterInfo.profile_image_url.replace("_normal", '_bigger')
data['twitter_status'] = 2
console.log("Tweeter: ", data)
}
} else {
data['twitter_status'] = 1
}
}
resolve(data)
}).then( (data) => {
db.collection('people').doc(data.marker).set(data)
db.collection('people').doc(data.marker).collection('positions').doc(data['report_at']).set(
{
"lat":data['lat'],
"lon":data['lon'],
}
)
}).catch( (error) => { console.log(error) })
}
The twitterAPI functions called:
cleanUsername: function (givenUsername) {
return givenUsername.split('/').pop().replace('#', '').replace('#', '').split(" ").join("").split("?")[0].trim().toLowerCase()
},
getTwitterGeneralInfoToDb: async function (twitter_id) {
var endpointURL = "https://api.twitter.com/2/users/" + twitter_id
var params = {
"user.fields": "name,description,profile_image_url,protected"
}
// this is the HTTP header that adds bearer token authentication
return new Promise( (resolve,reject) => {
needle('get', endpointURL, params, {
headers: {
"User-Agent": "v2UserLookupJS",
"authorization": `Bearer ${TWITTER_TOKEN}`
}
}).then( (res) => {
console.log("result.body", res.body);
if (res.body['errors']) {
if (res.body['errors'][0]['title'] == undefined) {
reject("Twitter API returns undefined error for :'", cleanUsername, "'")
} else {
reject("Twitter API returns error:", res.body['errors'][0]['title'], res.body['errors'][0]['detail'])
}
} else {
resolve(res.body.data)
}
}).catch( (error) => { console.error(error.message) })
})
},
// Get unique id from Twitter user
// Twitter API
getTwitterIdFromUsername: async function (cleanUsername) {
const endpointURL = "https://api.twitter.com/2/users/by?usernames="
const params = {
usernames: cleanUsername, // Edit usernames to look up
}
// this is the HTTP header that adds bearer token authentication
const res = await needle('get', endpointURL, params, {
headers: {
"User-Agent": "v2UserLookupJS",
"authorization": `Bearer ${TWITTER_TOKEN}`
}
})
if (res.body['errors']) {
if (res.body['errors'][0]) {
if (res.body['errors'][0]['title'] == undefined) {
console.error("Twitter API returns undefined error for :'", cleanUsername, "'")
} else {
console.error("Twitter API returns error:", res.body['errors'][0]['title'], res.body['errors'][0]['detail'])
}
} else {
console.error("Twitter API special error:", res.body)
}
} else {
if (res.body['data']) {
return res.body['data'][0].id
} else {
//console.log("??? Could not return ID, despite no error. See: ", res.body)
}
}
},
You have 3 options to deal with your main issue of async methods in a loop.
Instead of forEach, use map and return promises. Then use Promise.all on the returned promises to wait for them to all complete.
Use a for/of loop in combination with async/await.
Use a for await loop.
It sounds like there's a problem in the implementation of handleDataItem() and the promise that it returns. To help you with that, we need to see the code for that function.
You also need to clean up useData() so that it properly returns a promise that propagates both completion and errors.
And, if handleDataItem() returns a promise that is accurate, then you need to change how you do that in a loop here also.
Change from this:
function useData() {
console.log('Starting useData()')
return new Promise( function(resolve, reject) {
readFromCloudFileserver()
.then(jsonListFromCloud) => {
jsonListFromCloud.forEach((item) => {
handleDataItem(item)
}
})
.then(() => {
resolve() // I put resolve here because it is not until everything is finished above that this function is finished
console.log('useData() done')
}).catch((error) => { console.error(error.message) })
})
}
to this:
async function useData() {
try {
console.log('Starting useData()')
const jsonListFromCloud = await readFromCloudFileserver();
for (let item of jsonListFromCloud) {
await handleDataItem(item);
}
console.log('useData() done');
} catch (error) {
// log error and rethrow so caller gets the error
console.error(error.message)
throw error;
}
}
The structural changes here are:
Switch to use async/await to more easily handle the asynchronous items in a loop
Remove the promise anti-pattern that wraps new Promise() around an existing promise - no need for that AND you weren't capturing or propagating rejections from readFromCloudFileServer() which is a common mistake when using that anti-pattern.
rethrow the error inside your catch after logging the error so the error gets propagated back to the caller
I have a webhook to recieve facebook messenger events in a cloud function like so:
export const facebookMessengerHook = functions.https.onRequest(async (req: express.Request, res: express.Response) => {
console.log(req);
console.log(req.method);
console.log(req.body);
if (req.method == "POST") {
const body = req.body;
console.log(body);
// Checks this is an event from a page subscription
if (body.object === 'page') {
res.status(200).send('EVENT_RECEIVED');
// Iterates over each entry - there may be multiple if batched
for (const entry of body.entry) {
// will only ever contain one message, so we get index 0
const webhook_data = entry.messaging[0];
console.log(webhook_data);
try {
// v THAT PART HERE v
const user = await admin.firestore().collection('users')
.where('facebookMessengerId', '==', webhook_data.sender.id)
.get();
// ^ THAT PART HERE ^
console.log(user);
} catch (e) {
console.log('No user');
}
}
}
else {
// Returns a '404 Not Found' if event is not from a page subscription
res.sendStatus(404);
}
}
});
It does not log anything, unless I comment out the marked part in the snippet.
Can someone please explain to me why and how to fix this, because I need to make a call to firestore and I also need the console.log for debug purposes?
Thanks for any help!
The problem most probably comes from the fact that by doing
res.status(200).send('EVENT_RECEIVED');
you actually indicate to the Cloud Function platform that the Cloud Function can be terminated before the rest of the asynchronous work (the set of calls to the get() method) is done. See the following official video form more detail. In other words, the Cloud Function is terminated before the promises returned by the get() method are resolved.
So you should modify your code as follows:
//....
if (body.object === 'page') {
// Iterates over each entry - there may be multiple if batched
for (const entry of body.entry) {
// will only ever contain one message, so we get index 0
const webhook_data = entry.messaging[0];
console.log(webhook_data);
try {
const user = await admin.firestore().collection('users')
.where('facebookMessengerId', '==', webhook_data.sender.id)
.get();
console.log(user);
} catch (e) {
console.log('No user');
//Here throw an error to be catched at an upper level
}
}
res.status(200).send('EVENT_RECEIVED');
}
//....
Note that you may use Promise.all() since you issue a series of fetch to the database. But with your code it is impossible to confirm that, because it does not show the exact use of these fetches.
I need to poll the data until the response.Status === 'UpdatesComplete'.
I have written this node js API function which basically polls the data -
const getResults = async (location) => {
try {
const response = await poll(location);
if (response.Status && response.Status === 'UpdatesComplete') {
return response;
}
return await getResults(location);
} catch (err) {
throw err;
}
};
app.get('/url', async (req, res) => {
try {
const results = await getResults(req.query);
res.json(formatData(results));
} catch (err) {
res.status(500).send(err);
console.error(err);
}
});
I am calling this API from ReactJS class component inside ComponentDidMount lifecycle method -
componentDidMount = () => {
axios.get('url', {
params: params
})
.then(response => {
console.log(response.data, 'data');
// setting the data on state
this.setState({ filteredList: response.data });
})
.catch(err => {
this.setState({ error: true });
});
};
This is working fine. But since the API is returning data till all the data has been fetched(after doing polling), it's taking a very long time on view to load the data. I am basically looking for returning the polling data to view as soon as the data fetched and simultaneously polling the API until all the data has been fetched. In this case, data will keep on updating after each polling on the view which will improve the user experience.
Thanks in advance.
You are finding the lazy loading or infinite scroll solution on the server-side. There is no simple way to do this.
The basic idea of the solution is to paginate your result with polling. ie.
call url?size=2&offset=0 from the client-side. Then on the server-side just poll first 2 results and return. next time call url?size=2&offset=2 and so-on.
I am trying a use case of reading a json file asynchronously and sending it out as a response (as a rxjs observable data). Here is the service that I use
import { logger } from './../../shared/utils/logger';
import { Injectable } from '#nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
import { BehaviorSubject, Observable, pipe, of, from, throwError, merge} from 'rxjs';
import { map, filter, scan, take, debounce, switchMap, retry, catchError, mergeMap, delay, zip, tap, mapTo } from 'rxjs/operators';
import { HttpResponseModel } from '../model/config.model';
import { isNullOrUndefined } from 'util';
#Injectable()
export class NewProviderService {
serviceSubject: BehaviorSubject<HttpResponseModel[]>;
filePath: string;
httpResponseObjectArray: HttpResponseModel[];
constructor() {
this.serviceSubject = new BehaviorSubject<HttpResponseModel[]>([]);
this.filePath = path.resolve(__dirname, './../../shared/assets/httpTest.json');
this.setSubject();
}
readFileFromJSON() {
this.readFileFromJsonSync();
fs.exists(this.filePath.toString(), exists => {
if (exists) {
fs.readFile(this.filePath.toString(), 'utf-8', (err, data) => {
logger.info('file read without parsin', data);
this.httpResponseObjectArray = JSON.parse(data).HttpTestResponse;
logger.info('array obj is:', this.httpResponseObjectArray);
logger.info('file read after parsing', JSON.parse(data));
return this.httpResponseObjectArray;
});
} else {
return null;
}
});
}
getObservable(): Observable<HttpResponseModel[]> {
// create an observable
// return Observable.create(observer => {
// observer.next(this.readFileFromJSON());
// });
return of(this.readFileFromJsonSync()).pipe(map(data => {
logger.info('inside obs methid', data);
return data;
}));
}
setSubject() {
this.getObservable().subscribe(data => {
logger.info('data before setting in sub', data);
this.serviceSubject.next(data);
});
}
}
So I wanted to subscribe to this emitted observable in the controller, but the values are getting read after I have subscribed and read the subject (BehaviorSubject). I understand that I am kind of doing something wrong with the subscription and emitting of data, but couldn't understand where I am doing wrong. Every time the controller prints 'data subscribed undefined' and then continues to read the file and emit the observable
This is the controller data
#Get('/getJsonData')
public async getJsonData(#Req() requestAnimationFrame, #Res() res) {
this.newService.serviceSubject.subscribe(data => {
logger.info('data subscribed', data);
res.status(HttpStatus.OK).send(data);
});
}
It works well if I read the file synchronously
replace readFileFromJSON() with the following method and it works well
readFileFromJsonSync(): HttpResponseModel[] {
const objRead = JSON.parse(fs.readFileSync(this.filePath.toString(), {encoding: 'utf-8'}));
logger.info('object read is', objRead.HttpTestResponse);
return objRead.HttpTestResponse;
}
So I am missing something while reading the file async. I am not sure what am I doing wrong. Could someone please help?
The problem is that you don't actually return anything in readFileFromJSON. It will asynchronously run fs.exists and fs.readFile and the corresponding callbacks but the result from the callbacks is ignored.
You should use Promises instead. You can either create a Promise yourself or use a library like bluebird that transforms fs from a callback based API to a Promise based API. For more information see this thread.
return new Promise(function(resolve, reject) {
fs.readFile(this.filePath.toString(), 'utf-8', (err, data) => {
if (err) {
reject(err);
} else {
const httpResponseObjectArray = JSON.parse(data).HttpTestResponse;
resolve(httpResponseObjectArray);
}
});
});
Actual Log order:
('ngOnInit started')
('after me aaya', this.policydetails)
('Here', Object.keys(this.policy).length)
Expected Log order:
('ngOnInit started')
('Here', Object.keys(this.policy).length)
('after me aaya', this.policydetails)
Component.ts file snippet below:
ngOnInit() {
console.log('ngOnInit started');
this.route.params.subscribe(params => {
this.getPoliciesService.getPolicyDetails(params.policyNo)
.subscribe((data: PoliciesResponse) => {
this.policy = data.data[0];
this.flattenPolicy();
console.log('Here', Object.keys(this.policy).length);
});
});
this.makePolicyTable();
}
ngAfterViewInit() {
console.log('after me aaya', this.policydetails);
const table = this.policydetails.nativeElement;
table.innerHTML = '';
console.log(table);
console.log(this.table);
table.appendChild(this.table);
console.log(table);
}
Service.ts file snippet below:
getPolicyDetails(policyNo) {
const serviceURL = 'http://localhost:7001/getPolicyDetails';
console.log('getPolicyDetails service called, policyNo:', policyNo);
const params = new HttpParams()
.set('policyNo', policyNo);
console.log(params);
return this.http.get<PoliciesResponse>(serviceURL, {params} );
}
JS file snippet corresponding to the API call below:
router.get('/getPolicyDetails', async function(req, res) {
let policyNo = (req.param.policyNo) || req.query.policyNo;
console.log('policyNo', typeof policyNo);
await helper.getPolicyDetails({'policyNo' : policyNo},
function(err, data) {
console.log(err, data)
if (err) {
return res.send({status : false, msg : data});
}
return res.send({status : true, data : data});
});
});
Can anyone please suggest where exactly do i need to async-await for expected log order?
If you want this.makePolicyTable() to be called only after the web request (getPolicyDetails) completes, then you should place that call inside of the .subscribe() block:
ngOnInit() {
console.log('ngOnInit started');
this.route.params.subscribe(params => {
this.getPoliciesService.getPolicyDetails(params.policyNo)
.subscribe((data: PoliciesResponse) => {
this.policy = data.data[0];
this.flattenPolicy();
console.log('Here', Object.keys(this.policy).length);
this.makePolicyTable();
});
});
}
You'll probably also want to move the table logic that's in ngAfterViewInit() inside the subscribe() block, too.
Basically, any logic that needs to wait for the asynchronous call to complete should be triggered inside the .subscribe() block. Otherwise, as you're seeing, it can be run before the web request gets back.
Finally, I would move this web service call into ngAfterViewInit() instead of ngOnInit(). Then you can be sure that the Angular components and views are all set up for you to manipulate them when the web service call completes.
You could also set a flag variable to false in the component and then when async calls finishes set it to true and render the HTML based on that flag variable with *ngIf syntax.