Lazy remote service access – REST and IndexedDB

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'
    }
  });
}]);
TrackRemoteService.jsview rawview file on Bitbucket

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;
  }
PersistenceService.jsview rawview file on Bitbucket

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:

GotoCo Overview

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;
  }
TrackServiceFacade.jsview rawview file on Bitbucket

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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

sechzehn − zwei =