From 64d5b399c57a3e0b3711e367f1dcd9fc0698a274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 23 Sep 2015 09:29:31 +0200 Subject: [PATCH] Experiment with angular-cache One prominent example where we can employ frontend-side caching is the fetching of associated users in activities. For a large number of comments of the same user, that user is retrieved once per activity, which causes a drastic overhead. This commit introduces a simple wrapper around angular-cache to store values in the sessionStorage. --- frontend/app/openproject-app.js | 4 +- frontend/app/services/cache-service.js | 77 +++++++++++++++++++ frontend/app/services/index.js | 8 ++ frontend/app/services/user-service.js | 14 +++- .../app/work_packages/activities/index.js | 1 + .../activities/user-activity-directive.js | 3 +- frontend/bower.json | 1 + .../user-activity-directive-test.js | 2 +- 8 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 frontend/app/services/cache-service.js diff --git a/frontend/app/openproject-app.js b/frontend/app/openproject-app.js index c08134c08d..edd96d684a 100644 --- a/frontend/app/openproject-app.js +++ b/frontend/app/openproject-app.js @@ -59,6 +59,7 @@ require('angular-busy/dist/angular-busy.css'); require('angular-context-menu'); require('angular-elastic'); +require('angular-cache'); require('mousetrap'); require('ngFileUpload'); @@ -190,7 +191,8 @@ var openprojectApp = angular.module('openproject', [ 'cgBusy', 'openproject.api', 'openproject.templates', - 'monospaced.elastic' + 'monospaced.elastic', + 'angular-cache' ]); window.appBasePath = jQuery('meta[name=app_base_path]').attr('content') || diff --git a/frontend/app/services/cache-service.js b/frontend/app/services/cache-service.js new file mode 100644 index 0000000000..e316bdcbb2 --- /dev/null +++ b/frontend/app/services/cache-service.js @@ -0,0 +1,77 @@ +//-- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See doc/COPYRIGHT.rdoc for more details. +//++ + +module.exports = function( + HALAPIResource, + $http, + $q, + CacheFactory) { + + var cacheName = 'openproject-cache'; + var _cache = CacheFactory(cacheName, { + maxAge: 30 * 60 * 1000, // 30 mins + storageMode: 'sessionStorage' + }); + + var CacheService = { + + cache: function(key, value) { + _cache.put(key, value); + }, + + get: function(key) { + return _cache.get(key); + }, + + loadResource: function(resource, force) { + var deferred = $q.defer(), + key = resource.props.href, + cached = CacheService.get(key); + + // Return an existing promise if it exists + // Avoids intermittent requests while a first + // is already underway. + if (cached && !force) { + return cached; + } + + var promise = deferred.promise; + CacheService.cache(key, promise); + + resource.fetch().then(function(data) { + deferred.resolve(data); + }, function() { + deferred.reject(); + }); + + return promise; + }, + }; + + return CacheService; +}; diff --git a/frontend/app/services/index.js b/frontend/app/services/index.js index ba9db6d562..b891e4012a 100644 --- a/frontend/app/services/index.js +++ b/frontend/app/services/index.js @@ -35,6 +35,13 @@ angular.module('openproject.services') require('./activity-service') ]) .service('AuthorisationService', require('./authorisation-service')) + .service('CacheService', [ + 'HALAPIResource', + '$http', + '$q', + 'CacheFactory', + require('./cache-service') + ]) .service('GroupService', ['$http', 'PathHelper', require('./group-service')]) .service('HookService', require('./hook-service')) .service('KeyboardShortcutService', [ @@ -83,6 +90,7 @@ angular.module('openproject.services') 'HALAPIResource', '$http', 'PathHelper', + 'CacheService', require('./user-service') ]) .service('VersionService', ['$http', 'PathHelper', require( diff --git a/frontend/app/services/user-service.js b/frontend/app/services/user-service.js index 38502b53af..122a18a5f0 100644 --- a/frontend/app/services/user-service.js +++ b/frontend/app/services/user-service.js @@ -26,16 +26,22 @@ // See doc/COPYRIGHT.rdoc for more details. //++ -module.exports = function(HALAPIResource, $http, PathHelper) { - - var registeredUserIds = [], cachedUsers = {}; +module.exports = function( + HALAPIResource, + $http, + PathHelper, + CacheService) { + var registeredUserIds = []; var UserService = { getUser: function(id) { var path = PathHelper.apiV3UserPath(id), resource = HALAPIResource.setup(path); - return resource.fetch(); + return getUserByResource(resource); + + getUserByResource: function(user, force) { + return CacheService.loadResource(user, force); }, getUsers: function(projectIdentifier) { diff --git a/frontend/app/work_packages/activities/index.js b/frontend/app/work_packages/activities/index.js index 41e28d6333..9c00b18145 100644 --- a/frontend/app/work_packages/activities/index.js +++ b/frontend/app/work_packages/activities/index.js @@ -39,6 +39,7 @@ angular.module('openproject.workPackages.activities') 'PathHelper', 'ActivityService', 'UsersHelper', + 'UserService', 'ConfigurationService', 'AutoCompleteHelper', 'EditableFieldsState', diff --git a/frontend/app/work_packages/activities/user-activity-directive.js b/frontend/app/work_packages/activities/user-activity-directive.js index 1e9b169777..dd8b20bf7e 100644 --- a/frontend/app/work_packages/activities/user-activity-directive.js +++ b/frontend/app/work_packages/activities/user-activity-directive.js @@ -34,6 +34,7 @@ module.exports = function($uiViewScroll, PathHelper, ActivityService, UsersHelper, + UserService, ConfigurationService, AutoCompleteHelper, EditableFieldsState, @@ -74,7 +75,7 @@ module.exports = function($uiViewScroll, scope.userCanQuote = !!scope.workPackage.links.addComment; scope.accessibilityModeEnabled = ConfigurationService.accessibilityModeEnabled(); - scope.activity.links.user.fetch().then(function(user) { + UserService.getUserByResource(scope.activity.links.user).then(function(user) { scope.userId = user.props.id; scope.userName = user.props.name; scope.userAvatar = user.props.avatar; diff --git a/frontend/bower.json b/frontend/bower.json index c893df3bdb..3ef10338b0 100644 --- a/frontend/bower.json +++ b/frontend/bower.json @@ -18,6 +18,7 @@ "angular-truncate": "sparkalow/angular-truncate#fdf60fda265042d12e9414b5354b2cc52f1419de", "angular-feature-flags": "mjt01/angular-feature-flags", "angular-elastic": "2.5.0", + "angular-cache": "~4.3.2", "jquery-migrate": "~1.2.1", "moment": "~2.10.6", "moment-timezone": "0.4.x", diff --git a/frontend/tests/unit/tests/work_packages/activities/user-activity-directive-test.js b/frontend/tests/unit/tests/work_packages/activities/user-activity-directive-test.js index 0f8f2ba4ec..0bd151ec8c 100644 --- a/frontend/tests/unit/tests/work_packages/activities/user-activity-directive-test.js +++ b/frontend/tests/unit/tests/work_packages/activities/user-activity-directive-test.js @@ -63,7 +63,7 @@ describe('userActivity Directive', function() { describe('element', function() { describe('with a valid user', function(){ - beforeEach(function() { + beforeEach(inject(function($q) { scope.workPackage = { links: { addComment: true