Visiting the Goto Conference in Berlin let me code a quick hack of a personal conference planner GotoCo . GotoCo is a small mobile application based on web technologies using the Ionic framework. It features to access the conference information, store them locally for later use and build your personal conference schedule. Visit http://apps.mindcrime-ilab.de/gotoco/index.html to check out the app – but due to the conference is already over it might not that useful anymore.
Conference sessions and tracks become more or less fixed after some point and network usage is always critical on mobile devices (limited speed or transfer volume). Applying a cache mechanims seems appropriate in order to make the app more responsive and mobile friendly.
Asynchronous resources
The access to the remote API is encapsulated by a service using Angular’s $resource
service. The $resource
service wraps an REST interface asynchronously returning promises on future results.
'use strict'; var app = angular.module('gotoco.services'); app.factory('TrackRemoteService', ['$resource', '$log', 'CONFERENCE_ID', function ($resource, $log, CONFERENCE_ID) { $log.debug('Fetching data'); return $resource('https://app.gotocon.com/structr/rest/conferences/' + CONFERENCE_ID + '/tracks/:trackId', {trackId: '@trackId'}, { 'getTracks': { isArray: false }, 'getTrack': { method: 'GET' } }); }]);
Angular takes care of unwrapping the promise if it is assigned to a scope object and becomes resolved:
$scope.tracks = TrackRemoteService.query();
If there are subsequent or dependent operations or error handling required the $promise
object allows to react on resolved or failed promises:
TrackRemoteService.query().$promise .then(function(data) { // do someting with the resolved data }) .catch(function(error) { // handle error })
Local storage – IndexedDB
Modern browsers provide a variety of choices for storing information locally. IndexedDB is an object store inside your browser. Almost every operation is designed to be asynchronous issuing an callback if it is finished:
function all(store, mapper) { var deferred = $q.defer(); database.promise .then(function (db) { $log.debug('[Persistence] Database connected'); var objectStore = db.transaction([store]).objectStore(store); $log.debug('[Persistence] execute query on store ['+store+']... '); var request = objectStore.openCursor(); var result = []; request.onsuccess = function (evt) { var cursor = evt.target.result; if (cursor) { var val = cursor.value; result.push(val); cursor.continue(); } else { $log.debug('No more entries!'); if( typeof(mapper) === 'function') { deferred.resolve(mapper(result)); } else { deferred.resolve(result); } } }; request.onerror = function (evt) { $log.warn('[Persistence] Query failed' + evt.target.error.message + ' : ' + evt.target.error.code); deferred.reject(evt.target.error.message); }; }); return deferred.promise; }
Folding those callbacks into promise chains keep the code more understandable and tidy. Promise chains allow the automatic resolution of dependent promises:
var deferred = $q.defer(); deferred.promise() .then(function(a) { // do something with 'a' and return the result 'x' var x = ... return x; }) .then(function(b){ // 'b' contains the result from the former then callback .- means the value of 'x' // do something with b var y = ... return y }) .then(function(c){...}) .catch(function(error){/* handle error*/});
Wrapping up the interface to the IndexedDB into promises builds a nice analogy to the service interface for remote service provided by Angular. The generic persistence interface is provided by the ‚PersistenceService‘, which also contains generic get
and query
methods.
Putting all together
Both the Angular remote service interfaces as well as the persistence service are based on promises. Making them interchangeable allows to load an object regardless of its local or remote availability:
The controllers accessing the data objects via the facades with provide domain specific operations and are responsible for loading the data either from the local IndexedDB store or from the originating remote service. Being an object store IndexedDB is very well suited to store the remote JSON objects building a local caching instance without the need of any further information.
app.factory('TrackServiceFacade', ['$q', '$log', 'TrackRemoteService', 'PersistenceService', function($q, $log, TrackService, PersistenceService) { /** * Query all Tracks from database or load from remote if there are not tracks in the store * * @returns {{$promise}} angular like promise */ function queryForTracks() { $log.debug('[CacheSevice] tracks...'); var value = {}; var deferred = $q.defer(); PersistenceService.getTracks() .then(function (data) { $log.debug('[CacheService] Got cached data:' + data); if (typeof(data) === 'undefined' || typeof(data.result) === 'undefined' || data.result.length === 0) { TrackService.getTracks().$promise .then(deferred.resolve) .catch(deferred.reject); } else { deferred.resolve(data); } }) .catch(deferred.reject); value.$promise = deferred.promise; return value; }
Note that the originating promise is wrapped into an object with $promise
field to enable the seamless usage in place of Angular services. So it is possible easily switching between an version which uses locally cached object or another one always fetching it from remote (eg. if the targeted browser does not support IndexedDB). To do so simply change the declared dependency and let Angulars dependency injection does the work.
TL;DR
The asynchronous nature of calling a REST service and loading data from the IndexedDB object stores as well as the fact that the nature of IndexedDB allows to save data as objects facilitates the cooperation of both forming an transparent and easy to use local cache. Pluggable interchangeability could be easily achieved be a unitary API design.