UI Caching Design
What are we caching?
- session data - current user, access token, system notifications, permission strings
- offline data - requisitions, requisition templates
- metadata stored as session data - currency and locale settings, minimal facilities
What do we want to cache?
- session data - current user, access token, system notifications, permission strings
- offline data - requisitions, requisition templates
- metadata/dictionaries - currency and locale settings, minimal facilities, orderables
Why do want to cache?
- improved performance
- less network traffic (a big benefit for slow connections)
- fetching the same data is redundant
- offline capabilities
What is wrong with the current approach a.k.a. why do we need a new one?
- fragmented and spread - to cache a piece of data we need to add some code in multiple places
- not standardized - session data is following a pattern (could be improved) but storing offline requisition is a complete mess (https://github.com/OpenLMIS/openlmis-requisition-ui/blob/master/src/requisition/requisitions.service.js#L108)
- code duplication (especially for session data)
- https://github.com/OpenLMIS/openlmis-referencedata-ui/blob/master/src/openlmis-permissions/current-user-roles.service.js#L53
- https://github.com/OpenLMIS/openlmis-referencedata-ui/blob/master/src/referencedata-user/current-user.service.js#L53
- https://github.com/OpenLMIS/openlmis-referencedata-ui/blob/master/src/referencedata-system-notification/system-notification.service.js#L54
- some of the metadata is stored as session data
- below is an example of minimal facilities being stored as session data (and not even following the pattern for session data)
https://github.com/OpenLMIS/openlmis-referencedata-ui/blob/master/src/referencedata-facilities-cache/facility.service.decorator.js
- below is an example of minimal facilities being stored as session data (and not even following the pattern for session data)
Proposed redesign
We can put the data into two groups:
- session data - all the data that should be stored throughout the session but no longer, information like current user, its permissions, system notification
- metadata/dictionaries/offline data - data that should persist between multiple sessions so it is available offline and doesn't have to be fetched constantly as the data usually takes some time to load
Since we have two different groups of data that can't really be handled mechanism the proposed design describes two different mechanisms for dealing with each of the group.
Session caching
For caching session data the openlmisSessionCacheService is used. It has a simple interface as follows
angular .module('referencedata-user') .service('openlmisSessionCacheService', openlmisSessionCacheService); openlmisSessionCacheService.$inject = [/* Dependencies */]; function openlmisSessionCacheService(/* Dependencies */) { this.cache = cache; this.get = get; /** * Caches the response of the method throughout the session. * * @param {string} key the key the resolved value will be available under * @param {Function} fn the function to fetch the cached data, can return a Promise * @param {Object} options the options object, options: * - fetchOnLogin - defines when the data should be fetched (on login or on the first * call to the get method with respective key), defaults to true */ function cache(key, fn, options) { // method body } /** * Retrieves the cached value hidden under the given key * * @param {string} key the key of the cached value * @return {Promise} the promise resolved once the data is ready */ function get(key) { // method body } }
Underneath the service should rely on LocalDatabase class for storing data and registerPostLogin and registerPostLogout methods of the loginService to fetch data (either on login or on the first call to get) and clear it on logout.
Example usage
Instead of
- https://github.com/OpenLMIS/openlmis-referencedata-ui/blob/master/src/referencedata-user/current-user.service.js
- https://github.com/OpenLMIS/openlmis-referencedata-ui/blob/master/src/referencedata-user/referencedata-user.run.js
We would have the following
(function() { 'use strict'; angular .module('referencedata-user') .run(routes); routes.$inject = ['openlmisSessionCacheService', 'UserRepository', 'authorizationService']; function routes(openlmisSessionCacheService, UserRepository, authorizationService) { function getCurrentUser() { return new UserRepository().get(authorizationService.getUser().user_id); } openlmisSessionCacheService.cache('currentUser', getCurrentUser); } })();
To retrieve the data we would use the following snippet
openlmisSessionCacheService.get('currentUser') .then(function(currentUser) { // do something with the current user })
Metadata/Dictionary/Offline data caching
Caching this group is more complex as some of the resources are versioned and some are not. To achieve this a new layer above OpenlmisResource is added that deals with the caching of data. It has the following interface
angular .module('openlmis-cached-repository') .factory('OpenlmisCachedResource', OpenlmisCachedResource); OpenlmisCachedResource.$inject = [/* Dependencies */]; function OpenlmisCachedResource(/* Dependencies */) { OpenlmisCachedResource.prototype.get = get; OpenlmisCachedResource.prototype.query = query; OpenlmisCachedResource.prototype.getAll = getAll; OpenlmisCachedResource.prototype.update = update; OpenlmisCachedResource.prototype.create = create; OpenlmisCachedResource.prototype.delete = deleteObject; return OpenlmisCachedResource; /** * @ngdoc method * @methodOf openlmis-cached-repository.OpenlmisCachedResource * @name OpenlmisCachedResource * @constructor * * @description * Creates an instance of the OpenlmisCachedResource class. * * Configuration options: * - paginated - flag defining whether response returned by the query request is paginated; defaults to true * - versioned - flag defining whether handled resource is versioned; defaults to false * * @param {String} uri the URI pointing to the resource * @param {Object} config the optional configuration object, modifies the default behavior making this class * more flexible */ function OpenlmisCachedResource(uri, config) { // implementation } /** * @ngdoc method * @methodOf openlmis-cached-repository.OpenlmisCachedResource * @name get * * @description * Retrieves an object with the given ID from cache or from the server. * * @param {string} id the ID of the object * @param {strong} versionId (optional) the version of the object * @return {Promise} the promise resolving to matching object, rejects if ID is not given or if the * request fails */ function get(id, versionId) { // implementation } /** * @ngdoc method * @methodOf openlmis-cached-repository.OpenlmisCachedResource * @name query * * @description * Return the response of the GET request or cached value. Passes the given object as request parameters. * * @param {Object} params the map of request parameters * @return {Promise} the promise resolving to the server response or cached value, rejected if request fails */ function query(params) { // implementation } /** * @ngdoc method * @methodOf openlmis-cached-repository.OpenlmisCachedResource * @name getAll * * @description * Return the response of the GET request or cached value in a form of a list. Passes the given object as request * parameters. * * @param {Object} params the map of request parameters * @return {Promise} the promise resolving to the server response or cached value, rejected if request fails */ function getAll(params) { // implementation } /** * @ngdoc method * @methodOf openlmis-cached-repository.OpenlmisCachedResource * @name update * * @description * Saves the given object on the OpenLMIS server. Uses PUT method. Caches the result. * * @param {Object} object the object to be saved on the server * @return {Promise} the promise resolving to the server response, rejected if request fails or object is * undefined or if the ID is undefined */ function update(object) { // implementation } /** * @ngdoc method * @methodOf openlmis-cached-repository.OpenlmisCachedResource * @name create * * @description * Creates the given object on the OpenLMIS server. Uses POST method. Caches the result. * * @param {Object} object the object to be created on the server * @param {Object} params the parameters to be passed to the request * @return {Promise} the promise resolving to the server response, rejected if request fails */ function create(object, params) { // implementation } /** * @ngdoc method * @methodOf openlmis-cached-repository.OpenlmisCachedResource * @name delete * * @description * Deletes the object on the OpenLMIS server. Removes the cached object. * * @param {Object} object the object to be deleted from the server * @return {Promise} the promise resolving to the server response, rejected if request fails or object is * undefined or if the ID is undefined */ function deleteObject(object) { // implementation } }
Underneath the OpenlmisCachedResource will use LocalDatabase for caching the data and OpenlmisResource for communicating with the backend server. Following are the descriptions on how each of the methods should behave:
- OpenlmisCachedResource.get
- versioned
- if the version id is given the component will first try to fetch the matching object from the local storage, if none is found a request will be made
- if the version id is not given the component will first fetch the latest version of the matching object from the local storage, then a request with ETag will be sent, if the ETag matches the cached object will be returned, if not, the server response will be returned
- saves the response in the local database
- non-versioned
- the component will first fetch the matching object from the local storage, then a request with ETag will be sent, if the ETag matches the cached object will be returned, if not, the server response will be returned
- overrides the object in the local database
- versioned
- OpenlmisCachedResource.query
- saves results in the local database
- OpenlmisCachedResource.getAll
- saves results in the local database
- OpenlmisCachedResource.update
- versioned
- the newly-created version is cached in the local database
- no previous versions are removed
- non-versioned
- the newly-created version is cached in the local database
- the previously cached version is overridden
- versioned
- OpenlmisCachedResource.create
- the newly-created object is cached in the local database
- OpenlmisCachedResource.delete
- versioned
- all versions of the deleted resource are removed from the local database
- non-versioned
- the cached resource is removed from the local database
- versioned
Open questions
- How to prevent redundant calls when using getAll and query methods?
Next steps
- create a ticket for adding ETag support for endpoints which responses are supposed to be cached
- create a ticket for implementing openlmisSessionCacheService
- create a ticket for implementing OpenlmisCachedResource
- create a ticket for refactoring places that cache the session data to use the openlmisSessionCacheService
- create a ticket for refactoring requisition related communication to use OpenlmisCachedResource
OpenLMIS: the global initiative for powerful LMIS software