I am finally upgrading my code to Parse JS SDK > 2.0 and am struggling to convert an existing function that returns an Observable after a query. I know that my issue is that query.find() now needs to be async/await but really lost on the best practices here. I've tried creating the observable first and using toPromise(), using .defer and also tried returning the observable inside query.find().then but I'm sure that's laughable.
Any guidance on this would be most appreciated!
getAttending(eventId) {
const event = new this.Event();
event.id = eventId;
const query = new Parse.Query(this._parseInvites);
query.limit(1000);
query.equalTo("event_objectId", event);
return Observable.fromPromise(query.find())
.map((arrEventInvites) => {
return arrEventInvites.reduce((total, invite) => {
var countGuest = parseInt(invite.get("invite_guest"), 10);
var countChild = parseInt(invite.get("invite_childcount"), 10);
total += 1;
if (isNaN(countGuest) || (!isNaN(countGuest) && !countGuest)) {
total += 0;
} else {
total += countGuest;
}
if (isNaN(countChild) || (!isNaN(countChild) && !countChild)) {
total += 0;
} else {
total += countChild;
}
return total;
}, 0);
})
.catch((err) => {
this.parseProvider.handleError(err);
return Observable.throw(0);
});
}
Related
I am new to Angular and i am facing some difficulties with a task. I have an array of IDs that i want to execute the same GET Call over. And for every GET call result i have to do some operations and then add the result of every operation to some arrays. I managed to find a way to do it correctly. But my problem is, i can't manage to wait for the final result to be ready (after all the GET calls are done and the operations too) before giving it as an argument to another method that will send it with a POST call.
the method where i do the GET calls and the operations over every call's result (the problem occurs when i am in the rollBackSPN condition).
async getComponentIds(taskName: String, selectedComponents: IComponent[]) {
const componentsId: number[] = [];
const componentsWithoutParams: IComponent[] = [];
let sendPortaPrecedente : boolean;
if(taskName == "rollBackSPN"){
from(selectedComponents).pipe(
concatMap(component =>{
return this.http.get<any>("Url"+component.idComponent).pipe(
tap(val => {
sendPortaPrecedente = true;
for(const obj of val){
if((obj.name == "z0bpqPrevious" && obj.value == null) || (obj.name == "datePortaPrevious" && obj.value == null) || (obj.name == "typePortaPrevious" && obj.value == null)){
sendPortaPrecedente = false;
}
}
if(sendPortaPrecedente){
componentsId.push(component.idComponent);
}else{
componentsWithoutParams.push(component);
}
}),
catchError(err => {
return of(err);
})
)
})
).subscribe(val => {
return { componentsId : componentsId, componentsWithoutParams : componentsWithoutParams, sendPortaPrecedente : sendPortaPrecedente};
});
}else{
for (const component of selectedComponents) {
componentsId.push(component.idComponent)
return { componentsId : componentsId, componentsWithoutParams : componentsWithoutParams, sendPortaPrecedente : sendPortaPrecedente};
}
}
}
The method where i pass the getComponentIds(taskName: String, selectedComponents: IComponent[]) result so it can be send with a POST call (again when i am in the rollBackSPN condition)
executeTask(serviceIdSi: string, actionIdSi: string, actionClassName: string, componentName: string, taskName: string,
componentsId: number[], componentsWithoutParams: IComponent[], sendPortaPrecedente: boolean): Observable<any> {
const url = this.taskUrl + `?serviceId=${serviceIdSi}` + `&actionId=${actionIdSi}` + `&actionClassName=${actionClassName}`
+ `&componentName=${componentName}` + `&taskName=${taskName}`;
if(taskName == "rollBackSPN"){
if(sendPortaPrecedente && componentsWithoutParams.length == 0){
return this.http.post<any>(url, componentsId);
}else{
let errMessage = "Some Error Message"
for(const component of componentsWithoutParams){
errMessage = errMessage + component.idComponent +"\n";
}
throw throwError(errMessage);
}
}else{
return this.http.post<any>(url, componentsId);
}
}
Both these methods are defined in a service called TaskService.
And the service is called like this in a component UnitTaskButtonsComponent.
async launchUnitTask() {
this.isLoading = true;
this.isClosed = false;
this.appComponent.currentComponentIndex = this.componentIndex;
let res = await this.taskService.getComponentIds(this.unitTaskLabel, this.selectedComponents);
this.taskService.executeTask(this.appComponent.currentService.identifiantSi,
this.appComponent.currentAction.identifiantSi,
this.appComponent.currentAction.className,
this.selectedComponents[0].name,
this.unitTaskLabel,
res.componentsId,
res.componentsWithoutParams,
res.sendPortaPrecedente).subscribe(
data => this.executeTaskSuccess(),
error => this.executeTaskError());
}
"res" properties are always undefined when it's a rollBackSPN task.
The main issue here is that getComponentIds does not return a Promise. So awaiting does not work. I would suggest to change getComponentIds so that it returns an Observable instead.
getComponentIds(taskName: string, selectedComponents: IComponent[]) {
// ^^^^^^ use string instead of String
return forkJoin(
selectedComponents.map((component) => {
return this.http.get<any>("Url" + component.idComponent).pipe(
map((val) => {
let sendPortaPrecedente = true;
for (const obj of val) {
if (
(obj.name == "z0bpqPrevious" && obj.value == null) ||
(obj.name == "datePortaPrevious" && obj.value == null) ||
(obj.name == "typePortaPrevious" && obj.value == null)
) {
sendPortaPrecedente = false;
}
}
return { component, sendPortaPrecedente }
}),
catchError((err) => of(err))
);
})
).pipe(
map((result) => {
const componentsId: number[] = [];
const componentsWithoutParams: IComponent[] = [];
for (const val of result) {
if (val.sendPortaPrecedente) {
componentsId.push(val.component.idComponent);
} else {
componentsWithoutParams.push(val.component);
}
}
return { componentsId, componentsWithoutParams };
})
);
}
Instead of using concatMap, let's use a forkJoin. The forkJoin allows sending all requests in parallel and returns the result in an array. But we have to pass in an array of Observables. That's why we map over the selectedComponents.
In the lower map, we can now get the complete result of the http calls in the result parameter. Here we do the processing of the data. I was not really sure how to handle the sendPortaPrecedente. You will have to fill that in.
We simply return the whole Observable
async launchUnitTask() {
this.taskService
.getComponentIds(this.unitTaskLabel, this.selectedComponents)
.pipe(
switchMap((res) => {
this.taskService
.executeTask(
this.appComponent.currentService.identifiantSi,
this.appComponent.currentAction.identifiantSi,
this.appComponent.currentAction.className,
this.selectedComponents[0].name,
this.unitTaskLabel,
res.componentsId,
res.componentsWithoutParams,
res.sendPortaPrecedente
)
})
).subscribe(
(data) => this.executeTaskSuccess(),
(error) => this.executeTaskError()
);
}
In the launchUnitTask method, we don't use await anymore. Instead, we call getComponentIds and chain the call of executeTask with a switchMap.
I have the next code, and it was working properly. to execute a request to my method fetchDropdownDataByFederationId, but now I have a requirement to execute the same method x number of times.
fetchInProgress(queryString?): Observable<IPerson[]> {
let PersonList: IPerson[] = [];
return this.getItems<IPerson[]>('', queryString).pipe(
take(1),
switchMap((wls: IPerson[]) => {
PersonList = [...wls];
//const createdbyIds = [...new Set(wls.map((f) => f.createdBy))];
return this.teamPageService.getInformation(wls.createdBy);
}),
map((teams:any) => {
console.log('> teams', teams);
for (let i = 0; i < PersonList.length; i++) {
//update information
}
//console.log('> Final value: ', PersonList);
return PersonList;
})
);
}
But, I'm not finding a way to execute my SwitchMap x number of times and get the results back to use them in my map method to parse the information.
I just moved my SwitchMap to mergeMap, something like this:
mergeMap((wls: IWalklist[]) => {
//let allIds = wls.contact.map(id => this.getSingleData(id._id) );
let drops: Dropdown[] = [];
walklistList = [...wls];
const allIds = [...new Set(wls.map((f) => f.createdBy))];
return forkJoin(...allIds).pipe(
map((idDataArray) => {
drops.push(
this.teamPageService.getInformation('');
);
return drops;
})
)
}),
But still no luck.
Can some help me? how can I fix it?
I have following code:
private getUsers(page, result) {
result = result||[];
return this.http.get(API_URL + '/users?page=1')
.pipe(map(response => {
const response_filter = response.json();
const users = response_filter['data'];
const pages = response_filter['total_pages'];
Array.prototype.push.apply(result, users.map((user) => new User(user)));
while (page != pages)
{
this.http.get(API_URL + '/users?page=' + page)
.pipe(map(resp => {
console.log('test');
const response_filter = resp.json();
const users = response_filter['data'];
Array.prototype.push.apply(result, users.map((user) => new User(user)));
return result;
}))
.pipe(catchError(val => of(`Caught inner error: ${val}`)));
page += 1;
}
return result;
}))
.pipe(catchError(val => of(`Caught error: ${val}`)));
}
Code works good until console.log('test'). This log doesn't get shown, but while loop iterates fine.
Previously i tried the same function, but in recursive way. There was the same problem.
The best way to do this is to create a single observable which represents all of the requests you want to make, using flatMap and forkJoin operators. There are a number of problems with the asynchronous operations in your code, meaning that the returned result will not include the results of the inner HTTP requests.
I would propose the following:
private getUsers(page, result) {
return this.http.get(API_URL + '/users?page=1')
.pipe(
flatMap((response) => {
const response_filter = response.json();
const users = response_filter['data'];
const pages = response_filter['total_pages'];
let firstPageUsers: User[] = users.map((user) => new User(user));
let getAllUsers: Observable<User[]>[];
getAllUsers.push(of(firstPageUsers));
while (page < pages)
{
getAllUsers.push(this.http.get(API_URL + '/users?page=' + page)
.pipe(
map(resp => {
console.log('test');
const response_filter = resp.json();
const users = response_filter['data'];
return users.map((user) => new User(user));
}),
// You need to decide if this is how you want errors
// handled, it doesn't seem too sensible to me:
catchError((err) => {
console.log(`Caught inner error: ${err}`);
return of([]); // needs to return type Observable<User[]>
})
)
);
page += 1;
}
return forkJoin(getAllUsers);
}),
map((allResponses) => {
// allResponses will be an array of User arrays from
// all of the observables within the forkJoin, so now
// we can iterate over all of those to create a single
// array containing all of the results.
result = result||[];
allResponses.forEach((responseUsers) => {
Array.prototype.push.apply(result, responseUsers);
});
return result;
}),
catchError((err) => {
console.log(`Caught outer error: ${err}`);
of(null); // Or whatever - again, think about your error cases.
})
);
}
Now wherever you are calling getUsers, when you subscribe to this observable it should resolve all of the inner queries as well.
Marks answer is great, but I already solved my problem (maybe not in the good way, but solved it) using Martin comment (using subscribe). Firstly I subscribe for a "get pages count" request and then I'm subscribing to "get users" request in a while loop.
I'm new in angular, so maybe someone will answer a question "Must I use unsubscribe here?"
this._dataSub0 = this.userDataService.getPages().subscribe((pages) => {
var page_num = pages;
var i = 1;
while (i < page_num) {
this._dataSub = this.userDataService
.getAllUsers()
.subscribe(
(users) => {
for (let us of users) {
this.users.push(us);
}
}
);
i++;
}
});
public getAllUsers(page): Observable<User[]> {
return this.getUsers(page);
}
private getUsers(page) {
var result = result||[];
return this.http.get(API_URL + '/users?page=' + page)
.pipe(map(response => {
const response_filter = response.json();
const users = response_filter['data'];
const pages = response_filter['total_pages']
if(pages == page)
return null;
Array.prototype.push.apply(result, users.map((user) => new User(user)));
return result;
}))
.pipe(catchError(val => of(`Caught error: ${val}`)));
}
public getPages(): Observable<number> {
var result;
return this.http.get(API_URL + '/users?page=0')
.pipe(map(response => {
const response_filter = response.json();
const pages = response_filter['total_pages']
return pages;
}))
.pipe(catchError(val => of(`Caught error: ${val}`)));
}
I am using ag-grid with angular 4.
I am using infinite scrolling as the rowModelType. But since my data is huge, we want to first call just 100 records in the first ajax call and when the scroll reaches the end, the next ajax call needs to be made with the next 100 records? How can i do this using ag-grid in angular 4.
This is my current code
table-component.ts
export class AssaysTableComponent implements OnInit{
//private rowData;
private gridApi;
private gridColumnApi;
private columnDefs;
private rowModelType;
private paginationPageSize;
private components;
private rowData: any[];
private cacheBlockSize;
private infiniteInitialRowCount;
allTableData : any[];
constructor(private http:HttpClient, private appServices:AppServices) {
this.columnDefs = [
{
headerName: "Date/Time",
field: "createdDate",
headerCheckboxSelection: true,
headerCheckboxSelectionFilteredOnly: true,
checkboxSelection: true,
width: 250,
cellRenderer: "loadingRenderer"
},
{headerName: 'Assay Name', field: 'assayName', width: 200},
{headerName: 'Samples', field: 'sampleCount', width: 100}
];
this.components = {
loadingRenderer: function(params) {
if (params.value !== undefined) {
return params.value;
} else {
return '<img src="../images/loading.gif">';
}
}
};
this.rowModelType = "infinite";
//this.paginationPageSize = 10;
this.cacheBlockSize = 10;
this.infiniteInitialRowCount = 1;
//this.rowData = this.appServices.assayData;
}
ngOnInit(){
}
onGridReady(params) {
this.gridApi = params.api;
this.gridColumnApi = params.columnApi;
//const allTableData:string[] = [];
//const apiCount = 0;
//apiCount++;
console.log("assayApiCall>>",this.appServices.assayApiCall);
const assaysObj = new Assays();
assaysObj.sortBy = 'CREATED_DATE';
assaysObj.sortOder = 'desc';
assaysObj.count = "50";
if(this.appServices.assayApiCall>0){
console.log("this.allTableData >> ",this.allTableData);
assaysObj.startEvalulationKey = {
}
}
this.appServices.downloadAssayFiles(assaysObj).subscribe(
(response) => {
if (response.length > 0) {
var dataSource = {
rowCount: null,
getRows: function (params) {
console.log("asking for " + params.startRow + " to " + params.endRow);
setTimeout(function () {
console.log("response>>",response);
if(this.allTableData == undefined){
this.allTableData = response;
}
else{
this.allTableData = this.allTableData.concat(response);
}
var rowsThisPage = response.slice(params.startRow, params.endRow);
var lastRow = -1;
if (response.length <= params.endRow) {
lastRow = response.length;
}
params.successCallback(rowsThisPage, lastRow);
}, 500);
}
}
params.api.setDatasource(dataSource);
this.appServices.setIsAssaysAvailable(true);
this.appServices.assayApiCall +=1;
}
else{
this.appServices.setIsAssaysAvailable(false)
}
}
)
}
}
I will need to call this.appServices.downloadAssayFiles(assaysObj) at the end of 100 rows again to get the next set of 100 rows.
Please suggest a method of doing this.
Edit 1:
private getRowData(startRow: number, endRow: number): Observable<any[]> {
var rowData =[];
const assaysObj = new Assays();
assaysObj.sortBy = 'CREATED_DATE';
assaysObj.sortOder = 'desc';
assaysObj.count = "10";
this.appServices.downloadAssayFiles(assaysObj).subscribe(
(response) => {
if (response.length > 0) {
console.log("response>>",response);
if(this.allTableData == undefined){
this.allTableData = response;
}
else{
rowData = response;
this.allTableData = this.allTableData.concat(response);
}
this.appServices.setIsAssaysAvailable(true);
}
else{
this.appServices.setIsAssaysAvailable(false)
}
console.log("rowdata>>",rowData);
});
return Observable.of(rowData);
}
onGridReady(params: any) {
console.log("onGridReady");
var dataSource = {
getRows: (params: IGetRowsParams) => {
this.info = "Getting datasource rows, start: " + params.startRow + ", end: " + params.endRow;
console.log(this.info);
this.getRowData(params.startRow, params.endRow)
.subscribe(data => params.successCallback(data));
}
};
params.api.setDatasource(dataSource);
}
Result 1 : The table is not loaded with the data. Also for some reason the service call this.appServices.downloadAssayFiles is being made thrice . Is there something wrong with my logic here.
There's an example of doing exactly this on the ag-grid site: https://www.ag-grid.com/javascript-grid-infinite-scrolling/.
How does your code currently act? It looks like you're modeling yours from the ag-grid docs page, but that you're getting all the data at once instead of getting only the chunks that you need.
Here's a stackblitz that I think does what you need. https://stackblitz.com/edit/ag-grid-infinite-scroll-example?file=src/app/app.component.ts
In general you want to make sure you have a service method that can retrieve just the correct chunk of your data. You seem to be setting the correct range of data to the grid in your code, but the issue is that you've already spent the effort of getting all of it.
Here's the relevant code from that stackblitz. getRowData is the service call that returns an observable of the records that the grid asks for. Then in your subscribe method for that observable, you supply that data to the grid.
private getRowData(startRow: number, endRow: number): Observable<any[]> {
// This is acting as a service call that will return just the
// data range that you're asking for. In your case, you'd probably
// call your http-based service which would also return an observable
// of your data.
var rowdata = [];
for (var i = startRow; i <= endRow; i++) {
rowdata.push({ one: "hello", two: "world", three: "Item " + i });
}
return Observable.of(rowdata);
}
onGridReady(params: any) {
console.log("onGridReady");
var datasource = {
getRows: (params: IGetRowsParams) => {
this.getRowData(params.startRow, params.endRow)
.subscribe(data => params.successCallback(data));
}
};
params.api.setDatasource(datasource);
}
have an observable that returns arrays/lists of things: Observable
And I have a usecase where is is a pretty costly affair for the downstream consumer of this observable to have more items added to this list. So I'd like to slow down the amount of additions that are made to this list, but not loose any.
Something like an operator that takes this observable and returns another observable with the same signature, but whenever a new list gets pushed on it and it has more items than last time, then only one or a few are added at a time.
So if the last push was a list with 3 items and next push has 3 additional items with 6 items in total, and the batch size is 1, then this one list push gets split into 3 individual pushes of lists with lengths: 4, 5, 6
So additions are batched, and this way the consumer can more easily keep up with new additions to the list. Or the consumer doesn't have to stall for so long each time while processing additional items in the array/list, because the additions are split up and spread over a configurable size of batches.
I made an addAdditionalOnIdle operator that you can apply to any rxjs observable using the pipe operator. It takes a batchSize parameter, so you can configure the batch size. It also takes a dontBatchAfterThreshold, which stops batching of the list after a certain list size, which was useful for my purposes. The result also contains a morePending value, which you can use to show a loading indicator while you know more data is incomming.
The implementation uses the new requestIdleCallback function internally to schedule the batched pushes of additional items when there is idle time in the browser. This function is not available in IE or Safari yet, but I found this excelent polyfill for it, so you can use it today anyways: https://github.com/aFarkas/requestIdleCallback :)
See the implementation and example usage of addAdditionalOnIdle below:
const { NEVER, of, Observable } = rxjs;
const { concat } = rxjs.operators;
/**
* addAdditionalOnIdle
*
* Only works on observables that produce values that are of type Array.
* Adds additional elements on window.requestIdleCallback
*
* #param batchSize The amount of values that are added on each idle callback
* #param dontBatchAfterThreshold Return all items after amount of returned items passes this threshold
*/
function addAdditionalOnIdle(
batchSize = 1,
dontBatchAfterThreshold = 22,
) {
return (source) => {
return Observable.create((observer) => {
let idleCallback;
let currentPushedItems = [];
let lastItemsReceived = [];
let sourceSubscription = source
.subscribe({
complete: () => {
observer.complete();
},
error: (error) => {
observer.error(error);
},
next: (items) => {
lastItemsReceived = items;
if (idleCallback) {
return;
}
if (lastItemsReceived.length > currentPushedItems.length) {
const idleCbFn = () => {
if (currentPushedItems.length > lastItemsReceived.length) {
observer.next({
morePending: false,
value: lastItemsReceived,
});
idleCallback = undefined;
return;
}
const to = currentPushedItems.length + batchSize;
const last = lastItemsReceived.length;
if (currentPushedItems.length < dontBatchAfterThreshold) {
for (let i = 0 ; i < to && i < last ; i++) {
currentPushedItems[i] = lastItemsReceived[i];
}
} else {
currentPushedItems = lastItemsReceived;
}
if (currentPushedItems.length < lastItemsReceived.length) {
idleCallback = window.requestIdleCallback(() => {
idleCbFn();
});
} else {
idleCallback = undefined;
}
observer.next({
morePending: currentPushedItems.length < lastItemsReceived.length,
value: currentPushedItems,
});
};
idleCallback = window.requestIdleCallback(() => {
idleCbFn();
});
} else {
currentPushedItems = lastItemsReceived;
observer.next({
morePending: false,
value: currentPushedItems,
});
}
},
});
return () => {
sourceSubscription.unsubscribe();
sourceSubscription = undefined;
lastItemsReceived = undefined;
currentPushedItems = undefined;
if (idleCallback) {
window.cancelIdleCallback(idleCallback);
idleCallback = undefined;
}
};
});
};
}
function sleep(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if ((new Date().getTime() - start) > milliseconds){
break;
}
}
}
let testSource = of(
[1,2,3],
[1,2,3,4,5,6],
).pipe(
concat(NEVER)
);
testSource
.pipe(addAdditionalOnIdle(2))
.subscribe((list) => {
// Simulate a slow synchronous consumer with a busy loop sleep implementation
sleep(1000);
document.body.innerHTML += "<p>" + list.value + "</p>";
});
<script src="https://unpkg.com/rxjs#6.5.3/bundles/rxjs.umd.js"></script>