Ionic & SQLite - Structuring init load with promises - angularjs

I could not wrap my head around my problem, which I finally understand I believe.
I must initialize user data from a SQLite database, which I am running through the cordova sqlite plugin. My problem is that reading the DB is in an asynchronous deferrable this.loadData function (below). ( I may be "old fashioned", but my major complaint here - why can't DB access be in a synchronous function like all other sane implementations of databases? Without the deferrable it would be straight forward.)
My controller reacts to $stateChangeSuccess where I display a leaflet.js map, that is immediately started by $urlRouterProvider.otherwise('/app/map'); within the state controller.
My problem is that I do not know how to stall the view controller load until all my data is loaded. I tried putting loading the SQLite data into $ionicPlatform.ready so that load would be triggered immediately, but on the device loading the view overtakes the background SQLite thread.
I also tried to load the data within the module during module instantiation by calling this.loadData() at the end. However, the cordova module would not be initialized until then and fail.
What's the best way? Can I make a custom event that would fire when everything is loaded? Should I add a intermediate view after the splash screen that waits out all progress?
Essentially I have to make the whole app loading procedure wait till my data is loaded and do not know how to handle this when SQLite forces one to make all data access a promise. I really would prefer to have a choice of sync / async DB access somehow.
//this is in a module dataService
this.loadData = function(){
if(!myDB)
myDB = $cordovaSQLite.openDB('my.db');
var deferred = $q.defer();
myDB.transaction(function(transaction1) {
transaction1.executeSql('SELECT ID,Data FROM myDB;', [],
function(tx, result) {
console.log(result.rows.length);
var dataJSON = result.rows.item(0).Data;
var dataParsed = JSON.parse(dataJSON);
deferred.resolve(dataParsed);
},
function(error) {
console.log("SELECT failed. Must create table first");
//do other stuff ...
});
});
return deferred.promise;
}
// in my MapViewController the app reacts to
$scope.$on("$stateChangeSuccess", function() {
// init views with the data that should be available now
}
//this is in the starter module
angular.module('starter',[]).config(function($stateProvider, $urlRouterProvider){
\\... states ...
$urlRouterProvider.otherwise('/app/map');
}

As you return a promise, you have to resolve it on then statament, and inside it you can do a redirect.
loadData
.then(loadDataSuccess)
.catch(loadDataError)
function loadDataSuccess(data){
$state.go('map')
}
function loadDataError(error){
//do stuff
}

Related

Reopening PouchDB in a second Angular factory results in Pouch not using SQLite plugin

I'm writing an Ionic app that uses PouchDB and the SQLite plugin. I have a factory responsible for opening the db and exposing some methods for getting records etc:
.factory('PouchdbFactory', ['$q', function ($q) {
var db,
codes = [];
var initDB = function() {
db = new PouchDB('codes', {adapter: 'websql', auto_compaction: true, location: 2}); // location should be 2
// Listen for changes on the database
db.changes({ live: true, since: 'now', include_docs: true}).on('change', onDatabaseChange);
db.info().then(console.log.bind(console));
}
...etc...
And another factory that returns methods that are used to periodically get the records and change them.
.factory('OtpFactory', ['$rootScope', '$interval', 'PouchdbFactory', function($rootScope, $interval, PouchdbFactory) {
var otpCodes,
globalTimerPromise,
totpObj = new TOTP();
// this breaks sqllite plugin (db.info().then(console.log.bind(console)); returns sqlite_plugin: false )
PouchdbFactory.initDB();
PouchdbFactory.getAllCodes().then(function(codes) {
otpCodes = codes;
});
Calling PouchdbFactory.initDB(); in the second factory results in pouch not using the SQLite plugin (db.info().then(console.log.bind(console)); returns sqlite_plugin: false).
I'm still learning Angular, so perhaps this is not the way to structure it. I could move the db creation into $rootscope so the database can be access in all factories but I don't understand why calling PouchdbFactory.initDB(); a second time means no sqlite_plugin.
Any ideas?
Not that it's the reason for the error, but I would rather just return the db instance if it's initialized. Try a guard clause in init that's checks db has been initialized - if it has the return db straight away, if not then continue to initialize it.

Synchronous service using async service in Angular [duplicate]

This question already has answers here:
AngularJS : Initialize service with asynchronous data
(10 answers)
Closed 7 years ago.
I have a link generator service that is able to generate links to specific content types (users' details page, content items' details pages etc).
This service is really easy to use and has synchronous functions:
links.content(contentInstance); // /items/123
links.user(userInstance); // /users/234
I now have to introduce separate routing for logged in user to change from /users/id to /users/me.
The only change I'd need to add to my link generator service is to check whether userInstance.id == loggedInUser.id and return a different route URL. This is not a problem as long as my logged-in user's info would be synchronously available. but it's not...
I have a userService.getMyInfo() that returns a promise. The first time it's called it actually makes a server request but subsequent calls return a resolved promise with already cached data.
So how should I implement my user link URL generation in my link generator service?
Edit
Ok. So to see better what I have at the moment and where I'm having the problem. I'm pretty aware that async will stay async and that it can't be converted to synchronous (and it shouldn't be).
This is some more of my code, that will make it easier to understand.
linkGenerator.user
angular
.module("App.Globals")
.factory("linkGenerator", function(userService) {
...
user: function(userInstance) {
// I should be calling userService.getMyInfo() here
return "/users/{0}/{1}".format(userInstance.id, userInstance.name);
},
...
});
userService.getMyInfo
angular
.module("App.Globals")
.service("userService", function($q, cacheService, userResource) {
...
getMyInfo: function() {
if (cacheService.exists("USER_KEY"))
// return resolved promise
return $q.when(cacheService.get("USER_KEY"));
// get data
return userResource
.getCurrentUser()
.$promise
.then(function(userData) {
// cache it
cacheService.set("USER_KEY", userData);
});
},
...
});
Controller
angular
.module("App.Content")
.controller("ItemDetailsController", function(linkGenerator, ...) {
...
this.model = { ... };
this.helpers = {
...
links: linkGenerator,
...
};
...
});
View
View uses ItemDetailsController as context notation.
...
<a ng-href="{{::context.helpers.links(item.author)}}"
ng-bind="::item.author.name">
</a>
...
Notes
As you can see my view generates links to item authors. The problem is that my linkGenerator (as you can see from the code may not have the information yet whether it should generate one of the correct links to user details view.
I know I can't (and don't want to) change my async code to synchronous, but what would be the best way to make this thing work as expected?
One possible solution
For the time being I've come up with a solution that does the trick, but I don't really like it, as I have to supply my logged in user's ID to linkGenerator.user(userInstance, loggedInUserId) function. Then I set up my routing so that I add resolve to my route where I call userService.getMyInfo() which means that my controller is not being instantiated until all promises are resolved. Something along this line:
routeProvider
.when("...", {
templateUrl: "path/to/my/details/template",
controller: "ItemDetailsController".
controllerAs: "context",
resolve: {
myInfo: function(userService) {
return userService.getMyInfo();
}
}
})
...
Then I also add an additional helper to my controller
this.helpers = {
...
links: linkGenerator,
me: myInfo.id,
...
};
And then I also change link generator's function by adding the additional parameter that I then supply in the view.
linkGenerator.user = function(userInstance, loggedInUserId) {
if (userInstance.id === loggedInUserId)
return "users/me";
return "users/{0}/{1}".format(userInstance.id, userInstance.name);
}
and in the view
<a ng-href="{{::context.helpers.links.user(item.author, context.helpers.me)}}"...
And I don't to always supply logged in user's ID. I want my service to take care of this data on its own.
There is no way to make anything in JavaScript that is asynchronous at some point synchronous again. This is a ground rule of how concurrency works - no blocking for waiting for stuff is allowed.
Instead, you can make your new method return a promise and use the regular tools for waiting for it to resolve.
links.me = function(){
var info = userService.getMyInfo();
return info.then(info => { // or function(info){ if old browser
// generate link here
return `/users/${info.id}`; // or regular string concat if old browser
});
}
Which you'd have to use asynchronously as:
links.me().then(function(link){
// use link here
});

Accessing factories in the same Angular module

In my Angular app, I have some resource modules, each containing some cache factories.
For example,
projectRsrc.factory('getProjectCache', ['$cacheFactory', function($cacheFactory){
return $cacheFactory('getProjectCache');
}]);
I have a few of these to cache values received from the servers.
The problem is that at times I'd like to clear all the caches. So I want to put all the cacheFactories into one CacheCentralApp module and delete all the caches with a single call.
The trouble is, I don't know of any way to access other factories inside my module. So for instance, if I create a module CacheCentralApp, and in it, declare factories that provide cacheFactorys, how can I create a function in there that calls removeAll() on every cacheFactory?
I don't think it is possible to target all the factories of a certain module. I think however that another solution to your problem is to send a event that all factories has to be cleared. This will prevent that you will have to loop through all your factories and call a .clear() function on everyone.
You could send a event request with the following code:
$scope.$broadcast('clearAllCaches');
And listen to this event in every factory with:
$scope.$on('clearAllCaches', function() {
clearCache();
}
In a separate module you might create a factory for that:
var cacheModule = angular.module('CacheCentralApp', []);
cacheModule.factory('MyCacheFactory', ['$cacheFactory', function($cacheFactory) {
var cacheKeys = [];
return {
clearAll: function() {
angular.forEach(cacheKeys, function(key) {
$cacheFactory.get(key).removeAll();
});
},
get: function(key) {
if(cacheKeys.indexOf(key) == -1) {
cacheKeys.push(key);
return $cacheFactory(key);
} else {
return $cacheFactory.get(key);
}
}
}
}]);
To create new or get existing Cache you simply call MyCacheFactory.get(cacheName). To clear all the caches ever created in the factory you call MyCacheFactory.clearAll().
Note: I am not quite sure that Array.indexOf is available in every browser, you might want to use Lo-Dash or another library to make sure your code works.

Service or provider with cached data

On a server side I have a json file in a dictionary form:
{
"term1": "definition1",
"term2": "definition2",
"term3": "definition3"
}
I'm trying to create a service or provider (one of them is sufficient) which will have a cache of a data from this json file and will be able to use it.
The structures look like:
myApp.service('translateSrv', function() {
this.dictionaryData; // how to populate
this.translate = function(input) {
return this.dictionaryData[input];
};
});
myApp.provider('translateProvider', function() {
this.dictionaryData; // how to populate
this.$get = function() {
return {
translate: function() {
return this.dictionaryData[input];
}
}
};
});
My question is how to populate a dictionary data in this service or provider before the first call of translate() method (in a time of module creation/configuration)? I can't do it asynchronously while first method call.
I want to use one of this structure, among others, in a filter:
myApp.filter('translate', ['translateProvider', function(translateProvider) {
return function(input) {
return translateProvider.translate(input);
}
}]);
I've started recently my work with Angular so maybe my approach is wrong. I will appreciate every hint.
Provider name:
Do not suffix your provider name with 'Provider' since the name you will use to inject the provider in config functions will already be suffixed with 'Provider'
myApp.provider('translate', /*...*/);
// -> injectable provider is 'translateProvider'
// -> injectable instance is 'translate'
Populate the provider in a config function:
myApp.config(['translateProvider', function(translateProvider) {
translateProvider.dictionaryData = { /*...*/ };
});
Advice for performance!
If your translations are static per page view, please consider pre translating your templates.
If you really need it, prefer writing the whole translation js object in an inline script in the document
1 XHR less
no lazy loading deferring application load
Lazy loading
If you really have to lazy load those translations:
Either defer application loading with an external XHR before application load, while keeping the translationData at the provider configuration level,
Or take advantage of the "resolve" part of angular rooting or ui-router, while setting the translationData object on the instance (not on the provider)
How to choose between both?
The first option is quite easy and you won't have to couple application routing with your lazy load constraint.
For the second choice, prefer ui-router and declare an abstract state responsible for handling the lazy loaded data in the "resolve" state property and make other states be children of this abstract state, so that you won't have to add resolve constraints on each state that have a dependency on translations.

angularJS unit testing where run contains a HTTP request?

I am fairly new to AngularJS and am trying to learn some best practices. I have things working, but would like to start adding some unit tests to my modules and controllers. The first one I am looking to tackle is my AuthModule.
I have an AuthModule. This Module registers a Factory called "AuthModule" and exposes things like "setAuthenticatedUser" and also fields like "isLoggedIn" and "currentUser". I think this is a fairly common pattern in an AngularJS application, with some variations on the specific implementation details.
authModule.factory(
'AuthModule',
function(APIService, $rootScope) {
var _currentUser = null;
var _isLoggedIn = false;
return {
'setAuthenticatedUser' : function(currentUser) {
_currentUser = currentUser;
_isLoggedIn = currentUser == null ? false : true;
$rootScope.$broadcast('event:authenticatedUserChanged',
_currentUser);
if (_isLoggedIn == false) {
$rootScope.$broadcast('event:loginRequired')
}
$rootScope.authenticatedUser = _currentUser;
$rootScope.isLoggedIn = _isLoggedIn;
},
'isLoggedIn' : _isLoggedIn,
'currentUser' : _currentUser
}
});
The module does some other things like register a handler for the event "loginRequired" to send the person back to the home screen. These events are raised by the AuthModule factory.
authModule.run(function($rootScope, $log, $location) {
$rootScope.$on("event:loginRequired", function(event, data) {
$log.info("sending him home. Login is required");
$location.path("/");
});
});
Finally, the module has a run block which will use an API service I have to determine the current logged in user form the backend.
authModule.run(
function(APIService, $log, AuthModule) {
APIService.keepAlive().then(function(currentUser) {
AuthModule.setAuthenticatedUser(currentUser.user);
}, function(response) {
AuthModule.setAuthenticatedUser(null);
});
});
Here are some of my questions:
My question is how would you setup tests for this? I would think that I would need to Mock out the APIService? I'm having a hard time because I keep getting unexpected POST request to my /keepalive function (called within APIService.keepAlive())?
Is there any way to use $httpBackend in order to return the right response to the actual KeepAlive call? This would prevent me from having to mock-out the API service?
Should I pull the .run() block out which obtains the current logged in user out of the AuthModule and put it into the main application? It seems no matter where I put the run() block, I can't seem to initialize the $httpbackend before I load the module?
Should the AuthModule even be its own module at all? or should I just use the main application module and register the factory there?
Run blocks are the closest thing in Angular to the main method. A run block is the code which needs to run to kickstart the application. It is executed after all of the service have been configured and the injector has been created. Run blocks typically contain code which is hard to unit-test, and for this reason should be declared in isolated modules, so that they can be ignored in the unit-tests.angularjs docs
I suggest you take a look at this authentication service, using a service is the way to go.
Hopefully this would help ... Good luck

Resources