Executing 3 asynchronous operations (where the 2nd one depends of a condition) - breeze

I am working on a durandal / breeze project.
I need to perform several things in my activate function. Thanks to promises I am able to chain asynchronous operations. Please note that I'm still a beginner with all these things so don't hesitate to correct my code.
I have the following activate function inside my viewModel:
var activate = function (routeData) {
initLookups()
var idTran = parseInt(routeData.idTran);
var idItin = parseInt(routeData.idItin);
return datacontext.getTransportById(idTran, transport)
.then(function () { return datacontext.getItineraryById(idItin, itinerary); });
}
So in the return statement:
I fill the transport observable thanks to getTransportById
then I fill the itinerary observable thanks to getItineraryById
-
So far so good. This code works and do his job as expected (maybe not optimised). Now I need to insert a condition between these 2 operations for the case of creating a new entity (if idItin==-1). In this case, a new entity is created. I try with the code below but it doesn't work: I mean the promise doesn't seems to do his job here and thus the view is displayed without waiting for the asynchronous operations to complete.
var activate = function (routeData) {
initLookups()
var idTran = parseInt(routeData.idTran);
var idItin = parseInt(routeData.idItin);
return datacontext.getTransportById(idTran, transport)
.then(function () { if (idItin == -1) return datacontext.createItineraryDetailTransport(idTran); })
.then(function () { return datacontext.getItineraryById(idItin, itinerary); });
}
And below is the createItineraryDetailTransport function of the datacontext:
var createItineraryDetailTransport = function (idTransport) {
var initialValues = ({
transportId: idTransport
});
var newItinerary = manager.createEntity(entityNames.itinerary, initialValues);
return manager.addEntity(newItinerary);
}
So does someone have an idea how to code this thing?

I think that the rigth way is:
return datacontext.getTransportById(idTran, transport).then(function () {
return datacontext.getItineraryById(idItin, itinerary).then(function(){
if (idItin == -1) return datacontext.createItineraryDetailTransport(idTran);
//You have to return something different tu null,false,undefined... or a promise
return true;
);
});
Also, if there is not needed to do this calls sequently, you can do in parallel using Q library (https://github.com/kriskowal/q). The solution could be:
if(idItin ==-1){
//CreateItineraryDetailTransport don't return a promise.
//You can call that like a normal methond.
//It is not a asyncronous methond
datacontext.createItineraryDetailTransport(idTran);
}
return Q.all([datacontext.getTransportById(idTran, transport),datacontext.getItineraryById(idItin, itinerary)]);
-----EDIT-----
Solution doing the creation between the two other calls:
return datacontext.getTransportById(idTran, transport).then(function () {
if (idItin == -1) return datacontext.createItineraryDetailTransport(idTran).then(function(){
return datacontext.getItineraryById(idItin, itinerary);
});
return datacontext.getItineraryById(idItin, itinerary)
});
// I am using Q to do this method return a promise
var createItineraryDetailTransport = function (idTransport) {
var deferred = Q.defer();
var initialValues = ({
transportId: idTransport
});
var newItinerary = manager.createEntity(entityNames.itinerary, initialValues);
manager.addEntity(newItinerary);
deferred.resolve(true);
return deferred.promise;
}
Also you can use Q.fcall:
return Q.fcall(function(){ datacontext.createItineraryDetailTransport(idTran);};
I think that maybe, you don't need to do createItineraryDetailTransport return a promise. Maybe this solution is also possible:
return datacontext.getTransportById(idTran, transport).then(function () {
if (idItin == -1) datacontext.createItineraryDetailTransport(idTran);
return datacontext.getItineraryById(idItin, itinerary);
});

Related

Mapping a Stream<List> to another type is returning a Stream<Null>

I'm trying to transform a Stream of a list of one type into a Stream of a list of another type, and having an issue with this.
I have this list of Habits that I'm streaming from Firebase, and I want to accept that stream in a function, and return a new stream that is a list of ViewModels of another type from it. But my function is returning a stream of the wrong type.
Here is my code:
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
var result = habitsStream.map((habitsList) {
habitsList.map(
(habit) async {
await _getHabitCompletionsCurrent(habit);
HabitCompletion completion = habit.completions!.firstWhere(
(completion) => completion.date
.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
},
).toList();
});
return result;
}
I am getting a compile error because the result variable is showing as type Stream<Null> when I hover over it, where I would expect it to be Stream<List<HabitCompletionViewModel>>. Any idea what I'm doing wrong?
Your outer .map call does not have a return statement which is why you are getting a Stream<Null>.
So add a return statement like so:
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
var result = habitsStream.map((habitsList) {
// added return statement here
return habitsList.map(
(habit) async {
await _getHabitCompletionsCurrent(habit);
HabitCompletion completion = habit.completions!.firstWhere(
(completion) =>
completion.date.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
},
).toList();
});
return result;
}
However the above code still has an error because it is now returning a Stream<List<Future<HabitCompletionViewModel>>> instead of the desired Stream<List<HabitCompletionViewModel>>. To solve this you can use .asyncMap instead of .map.
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
var result = habitsStream.asyncMap((habitsList) {
return Stream.fromIterable(habitsList).asyncMap(
(habit) async {
await _getHabitCompletionsCurrent(habit);
HabitCompletion completion = habit.completions!.firstWhere(
(completion) =>
completion.date.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
},
).toList();
});
return result;
}

How to import entities after save changes with breeze across two entity managers

I've implemented repository pattern with two entity managers,
mainManager is for read only and delete, and updateManager is used for edit and add new entities. I use createEmptyCopy() to create updateManager.
Before i update an entity i export the entity from mainManager and import into the updateManager, after the change i call to updateManager.saveChanges() method.
I've noticed that i get back the updated entities in the promise response. i wonder what is the best practice to import those entities back into the mainManager?
here is my code:
function ($q, $http, entityManagerFactory) {
var self = this;
self.mainManager = entityManagerFactory.newManager();
self.updateManager = entityManagerFactory.newManager();
self.saveChanges = function () {
return self.updateManager.saveChanges();
};
self.rejectChanges = function() {
self.updateManager.rejectChanges();
};
self.getDomains = function () {
self.mainManager.clear();
var query = new breeze.EntityQuery()
.from('Domains')
.orderBy('name');
return self.mainManager.executeQuery(query);
};
self.createEmptyDomain = function () {
var domain = self.updateManager.createEntity('Domain');
return domain;
};
self.editDomain = function(domain) {
var exported = self.mainManager.exportEntities([domain]);
return self.updateManager.importEntities(exported).entities[0];
}
self.addDomain = function (domain) {
self.updateManager.addEntity(domain);
return self.updateManager.saveChanges();
};
self.deleteDomain = function (domain) {
domain.entityAspect.setDeleted();
var deferred = $q.defer();
self.mainManager.saveChanges().then(
function(data) {
deferred.resolve(data);
},
function (reason) {
console.log(reason);
self.mainManager.rejectChanges();
deferred.reject(reason);
});
return deferred.promise;
};
}
Right now i'm calling mainManager.clear() and get the data again from the server as you can see above in getDomains function.
But i think this is too expansive, why call the server if i already have the updated entities from the saveChanges promise?
i've also tried to import those entities back to mainManager using:
mainManager.importEntities(data.entities, { mergeStrategy: breeze.MergeStrategy.OverwriteChanges });
but i get an internal null breeze exception:
TypeError: Cannot read property 'forEach' of undefined
at EntityManager.proto.importEntities (breeze.debug.js:13081)
at self.importEntities (domain-list.service.js:22)
at domain-list.controller.js:70
at processQueue (angular.js:13170)
at angular.js:13186
at Scope.promises.$get.Scope.$eval (angular.js:14383)
at Scope.promises.$get.Scope.$digest (angular.js:14199)
at Scope.promises.$get.Scope.$apply (angular.js:14488)
at done (angular.js:9646)
at completeRequest (angular.js:9836)
the error is from this line breeze.debug.js:13081
13080: var tempKeyMap = {};
13081: json.tempKeys.forEach(function (k) {
13082: var oldKey = EntityKey.fromJSON(k, that.metadataStore);
13083: // try to use oldKey if not already used in this keyGenerator. 13084: tempKeyMap[oldKey.toString()] = new EntityKey(oldKey.entityType,
13085: that.keyGenerator.generateTempKeyValue(oldKey.entityType, oldKey.values[0]));
13086: });
var exportData = updateManager.exportEntities(data.entities, false);
mainManager.importEntities(exportData,
{ mergeStrategy: breeze.MergeStrategy.OverwriteChanges });

Failing Parse background job when using beforesave with thousands of objects

I am using a background job to query a json with thousands of objects to initially populate my database. I have also implemented the beforesave function to prevent any duplicate entries. However, once I implemented this, it seems my background job called response.error and does not save all objects. It looks like I might be exceeding the requests/sec? I would really appreciate if someone could take a look at my code and tell me why it is not saving all entries successfully.
Here is my background job:
Parse.Cloud.job("testing", function(request, response) {
var json;
Parse.Cloud.httpRequest({
url: stringURL + pageNumber.toString(),
success: function(httpResponse) {
json = httpResponse.data;
console.log("total is: " + json["meta"].total);
console.log("object 1 is: " + json["events"][1].title);
return json;
}
//after getting the json, save all 1000
}).then(function() {
//helper function called
saveObjects(json).then(function() {
response.success("success");
},
function(error) {
response.error("nooooo");
});
});
});
function saveObjects(json) {
var promises = [];
for(var i = 0; i < 1000; i++) {
var newEvent = new Event();
promises.push(newEvent.save(new Event(json["events"][i])));
}
return Parse.Promise.when(promises);
}
Here is my beforesave code:
Parse.Cloud.beforeSave("Event", function(request, response) {
var newEvent = request.object;
var Event = Parse.Object.extend("Event");
var query = new Parse.Query("Event");
query.equalTo("title", newEvent.get("title"));
query.equalTo("datetime_utc", newEvent.get("datetime_utc"));
query.equalTo("url", newEvent.get("url"));
query.first({
success: function(temp) {
response.error({errorCode:123,errorMsg:"Event already exist!"});
},
error: function(error) {
response.success();
}
});
});
Thanks I really appreciate any help... I've been stuck for a while.
If it's a request rate issue, then you could probably use something like node-function-rate-limit but it's fairly simple to write your own rate limiting batcher. See doInBatches() below.
Also, when using promise-returning methods that also offer a "success:..." callback, it's better not to mix the two styles. It may behave as expected but you are denied the opportunity to pass results from the "success:..." callback to the rest of the promise chain. As you can see below, the "success:..." code has simply been shuffled into the .then() callback.
Parse.Cloud.job("testing", function(request, response) {
Parse.Cloud.httpRequest({
url: stringURL + pageNumber.toString()
}).then(function(httpResponse) {
var json = httpResponse.data;
// console.log("total is: " + json.meta.total);
// console.log("object 1 is: " + json.events[1].title);
/* helper function called */
doInBatches(json.events, 30, 1000, function(evt, i) {
var newEvent = new Event();
return newEvent.save(new Event(evt));
}).then(function() {
response.success('success');
}, function(error) {
response.error('nooooo');
});
});
});
// Async batcher.
function doInBatches(arr, batchSize, delay, fn) {
function delayAsync() {
var p = new Parse.Promise();
setTimeout(p.resolve, delay);
return p;
}
function saveBatch(start) {
if(start < arr.length) {
return Parse.Promise.when(arr.slice(start, start+batchSize).map(fn))
.then(delayAsync) // delay between batches
.then(function() {
return saveBatch(start + batchSize);
});
} else {
return Parse.Promise.as();
}
}
return saveBatch(0);
}
I can't see how or why the beforesave code might affect things.

breeze observableArray binding - are properties observable?

I have a viewmodel which consists of a list(foreach loop) of DoctorPrices and when clicking on an item in the list it open up a CRUD form on the side. However when i update the values on the CRUD the observableArray that is bound to the foreach is not refreshing? (although the values are updates in the DB correctly)
From my data access module i call the following query.
function getDoctorServices(doctorId) {
var query = breeze.EntityQuery
.from('DoctorPrices')
.where('DoctorID', 'eq', doctorId).orderBy('ListOrder');
return manager.executeQueryLocally(query);
}
In my viewmodel i have the following code:
this.services = ko.computed(function() {
return doctorServices.getDoctorServices(doctorList.viewModel.instance.currentDoctorID());
});
services is bound using a foreach loop (not posting here as the code is simple and works)
When i click on a one of the DoctorPrices it gets the data as follows and places it in an observable:
this.selectedPrice = function (data, event) {
self.currentService(data);
self.showEdit(true);
};
I then bind selectPrice to a simple form that has the properties on it to be modified by the user. I then call manager.SaveChanges().
This results in the following problem: the value is being updated correctly but the GUI / Original List that is bound in the foreach is not being updated? Are the properties in breeze not observables? What is the best way to work with something like this.
I thought of a workaround and changing the code with something like this:
doctorList.viewModel.instance.currentDoctorID.subscribe(function() {
self.services([]);
self.services(doctorServices.getDoctorServices(doctorList.viewModel.instance.currentDoctorID()));
});
But i feel that clearing the array in that way is sloppy and not the right way of doing things specially with long lists.
Can someone please point me in the right direction on how to bind observableArray properties properly so they are updated?
Additional code my VM Component:
function services() {
var self = this;
this.showForm = ko.observable(false);
this.currentService = ko.observable();
this.services = ko.observableArray(doctorServices.getDoctorServices(doctorList.viewModel.instance.currentDoctorID()));
this.title = ko.observable();
doctorList.viewModel.instance.currentDoctorID.subscribe(function() {
self.services([]);
self.services(doctorServices.getDoctorServices(doctorList.viewModel.instance.currentDoctorID()));
self.showDetails(false);
});
this.show = function (value) {
self.showForm(value);
};
this.showDetails = ko.observable(false);
this.addNewService = function() {
self.currentService(doctorServices.createService(doctorList.viewModel.instance.currentDoctorID()));
console.log(self.currentService().entityAspect.entityState);
self.showDetails(true);
};
this.showDelete = ko.computed(function() {
if (self.currentService() == null)
return false;
else if (self.currentService().entityAspect.entityState.isDetached()) {
self.title('Add new service');
return false;
} else {
self.title('Edit service');
return true;
}
});
this.deleteService = function() {
self.currentService().entityAspect.setDeleted();
doctorServices.saveChanges();
doctorList.viewModel.instance.currentDoctorID.notifySubscribers();
};
this.closeDetails = function () {
doctorServices.manager.rejectChanges();
doctorList.viewModel.instance.currentDoctorID.notifySubscribers();
self.showDetails(false);
};
this.selectService = function (data, event) {
self.currentService(data);
self.showDetails(true);
};
this.saveChanges = function () {
console.log(self.currentService().entityAspect.entityState);
if (self.currentService().entityAspect.entityState.isDetached()) {
doctorServices.attachEntity(self.currentService());
}
console.log(self.currentService().entityAspect.entityState);
doctorServices.saveChanges();
doctorList.viewModel.instance.currentDoctorID.notifySubscribers();
self.currentService.notifySubscribers();
self.showDetails(true);
};
}
return {
viewModel: {
instance: new services()
},
template: servicesTemplate,
};
Below is my Breeze Data Class:
define('data/doctorServices', ['jquery', 'data/dataManager', 'knockout','mod/medappBase', 'breeze', 'breeze.savequeuing'], function ($, manager, ko,base, breeze, savequeuing) {
var services = ko.observableArray([]);
return {
attachEntity:attachEntity,
getServices: getServices,
services: services,
manager:manager,
getDoctorServices: getDoctorServices,
getServiceById: getServiceById,
createService:createService,
hasChanges: hasChanges,
saveChanges: saveChanges
};
function getServices() {
var query = breeze.EntityQuery.from("DoctorPrices");
return manager.executeQuery(query).then(function (data) {
services(data.results);
}).fail(function (data) {
console.log('fetch failed...');
console.log(data);
});;
}
function getDoctorServices(doctorId) {
var query = breeze.EntityQuery
.from('DoctorPrices')
.where('DoctorID', 'eq', doctorId).orderBy('ListOrder');
var set = manager.executeQueryLocally(query);
return set;
}
function getServiceById(serviceId) {
return manager.createEntity('DoctorPrice', serviceId);
//return manager.getEntityByKey('DoctorPrice', serviceId);
}
function handleSaveValidationError(error) {
var message = "Not saved due to validation error";
try { // fish out the first error
var firstErr = error.innerError.entityErrors[0];
message += ": " + firstErr.errorMessage;
base.addNotify('error', 'Could not save.', message);
} catch (e) { /* eat it for now */ }
return message;
}
function hasChanges() {
return manager.hasChanges();
}
function attachEntity(entity) {
manager.addEntity(entity);
}
function createService(doctorId) {
return manager.createEntity('DoctorPrice', { DoctorPricingID: breeze.core.getUuid(), DoctorID:doctorId }, breeze.EntityState.Detached);
};
function saveChanges() {
return manager.saveChanges()
.then(saveSucceeded)
.fail(saveFailed);
function saveSucceeded(saveResult) {
base.addNotify('success', 'Saved.', 'Your updates have been saved.');
}
function saveFailed(error) {
var reason = error.message;
var detail = error.detail;
if (error.innerError.entityErrors) {
reason = handleSaveValidationError(error);
} else if (detail && detail.ExceptionType &&
detail.ExceptionType.indexOf('OptimisticConcurrencyException') !== -1) {
// Concurrency error
reason =
"Another user, perhaps the server, " +
"may have deleted one or all of the settings." +
" You may have to restart the app.";
} else {
reason = "Failed to save changes: " + reason +
" You may have to restart the app.";
}
console.log(error);
console.log(reason);
}
}
});
Please note this is my frist attempt at both a data class and VM. At the moment i am relying heavily on clearing the array ([]) and using notifySubscribers to make the array refresh :(
I bet you're missing an observable somewhere. I can't tell because you keep hopping from property to property whose definition is not shown.
For example, I don't know how you defined this.currentService.
I'm confused by this:
this.services = ko.computed(function() {
return doctorServices.getDoctorServices(doctorList.viewModel.instance.currentDoctorID());
});
Why is it a ko.computed? Why not just make it an observable array.
self.service = ko.observableArray();
// ... later replace the inner array in one step ...
self.service(doctorServices.getDoctorServices(
doctorList.viewModel.instance.currentDoctorID()));
I urge you to follow the observability trail, confident that your Breeze entity properties are indeed observable.
vm.selectedPrice = ko.dependentObservable(function () {
return doctorServices.getDoctorServices(doctorList.viewModel.instance.currentDoctorID());
}, vm);
vm is ur model on which u applied bindings , try this it will work.

How to create a "loading" spinner in Breeze?

I'm trying to create a loading spinner that will be displayed when breeze is communicating with the server. Is there some property in Breeze that is 'true' only when breeze is sending data to the server, receiving data, or waiting for a response (e.g. after an async call has been made but no response yet)? I thought of binding this data to a knockout observable and binding the spinner to this observable,
Thanks,
Elior
Use spin.js
http://fgnass.github.io/spin.js/
Its so simple..make it visible before you execute the query and disable it after the query succeeds or fails.
I don't see any property that is set or observable while Breeze is querying, but if you are using a datacontext, or some JavaScript module for your data calls, this is what you can do -
EDIT
Taking John's comments into account, I added a token'd way of tracking each query.
var activeQueries = ko.observableArray();
var isQuerying = ko.computed(function () {
return activeQueries().length !== 0;
});
var toggleQuery = function (token) {
if (activeQueries.indexOf(token) === -1)
{ activeQueries.push(token); }
else { activeQueries.remove(token); }
};
var getProducts = function (productsObservable, forceRemote) {
// Don't toggle if you aren't getting it remotely since this is synchronous
if (!forceRemote) {
var p = getLocal('Products', 'Product','product_id');
if (p.length > 0) {
productsObservable(p);
return Q.resolve();
}
}
// Create a token and toggle it
var token = 'products' + new Date().getTime();
toggleQuery(token);
var query = breeze.EntityQuery
.from("Products");
return manager.executeQuery(query).then(querySucceeded).fail(queryFailed);
function querySucceeded(data) {
var s = data.results;
log('Retrieved [Products] from remote data source', s, true);
// Toggle it off
toggleQuery(token);
return productsObservable(s);
}
};
You will need to make sure all of your fail logic toggles the query as well.
Then in your view where you want to place the spinner
var spinnerState = ko.computed(function () {
datacontext.isQuerying();
};

Resources