Something tells me there are already solutions like this out there that are written by much smarter people who have tests and package.json etc. Perhaps my Friday-brain failed at googling them up.
So, the issue I'm having is an angular app that uses a ui-router to switch between controllers.
In almost every controller it looks something like this:
app.controller('Ctrl', function($scope, $http) {
/* The form that needs this looks something like this:
<input name="first_name" ng-model="stuff.first_name">
*/
$scope.stuff = {};
$http.get('/stuff/')
.success(function(response) {
$scope.stuff = response.stuff;
})
.error(function() {
console.error.apply(console, arguments);
});
})
(note; ideally you push this stuff into a service, but doing it here in the controller illustrates what matters in this point)
So far so good. But so far so slow.
Every time the controller is activated, the AJAX GET is fired and it might be slow because of network latency.
And I might switch to this controller repeatedly within one request/response session of loading the app.
So I wrote this:
app.service('localProxy',
['$q', '$http', '$timeout',
function($q, $http, $timeout) {
var service = {};
var memory = {};
service.get = function(url, store, once) {
var deferred = $q.defer();
var already = memory[url] || null;
if (already !== null) {
$timeout(function() {
if (once) {
deferred.resolve(already);
} else {
deferred.notify(already);
}
});
} else if (store) {
already = sessionStorage.getItem(url);
if (already !== null) {
already = JSON.parse(already);
$timeout(function() {
if (once) {
deferred.resolve(already);
} else {
deferred.notify(already);
}
});
}
}
$http.get(url)
.success(function(r) {
memory[url] = r;
deferred.resolve(r);
if (store) {
sessionStorage.setItem(url, JSON.stringify(r));
}
})
.error(function() {
deferred.reject(arguments);
});
return deferred.promise;
};
service.remember = function(url, data, store) {
memory[url] = data;
if (store) {
sessionStorage.setItem(url, JSON.stringify(data));
}
};
return service;
}]
)
And the way you use it is that it basically returns twice. First from the "cache", then from the network request response.
So, after you've used it at least once, when you request data from it, you first get the cached stuff (from memory or from the browser's sessionStorage
) then a little bit later you get the latest and greatest response from the server. For example:
app.controller('Ctrl', function($scope, $http, localProxy) {
$scope.stuff = {};
localProxy('/stuff/')
.then(function(response) {
// network response
$scope.stuff = response.stuff;
}, function() {
// reject/error
console.error.apply(console, arguments);
}, function(response) {
// update
$scope.stuff = response.stuff;
});
})
Note how it sets $scope.stuff = response.stuff
twice. That means that the page can load first with the cached data and shortly after the latest and greatest from the server.
You get to look at something whilst waiting for the server but you don't have to worry too much about cache invalidation.
Sure, there is a risk. If your server response is multiple seconds slow, your user might for example, start typing something into a form (once it's loaded from cache) and when the network request finally resolves, what xhe typed in is overwritten or conflicting.
The solution to that problem is that you perhaps put the form in a read-only mode until the network request resolves. At least you get something to look at sooner rather than later.
The default implementation above doesn't store things in sessionStorage
. It just stores it in memory as you're flipping between controllers. Alternatively, you might want to use a more persistent approach so then you instead use:
controller( // same as above
localProxy('/stuff/', true)
// same as above
)
Sometimes there's data that is very unlikely to change. Perhaps you just need the payload for a big drop-down widget or something. In that case, it's fine if it exists in the cache and you don't need a server response. Then set the third parameter to true, like this:
controller( // same as above
localProxy('/stuff/', true, true)
// same as above
)
This way, it won't fire twice. Just once.
Another interesting expansion on this is, if you change the data after it comes back. A good example is if you request data to fill in a form that user updates. After the user has changed some of it, you might want to pre-emptivly cache that too. Here's an example:
app.controller('Ctrl', function($scope, $http, localProxy) {
$scope.stuff = {};
var url = '/stuff/';
localProxy(url)
.then(function(response) {
// network response
$scope.stuff = response.stuff;
}, function() {
// reject/error
console.error.apply(console, arguments);
}, function(response) {
// update
$scope.stuff = response.stuff;
});
$scope.save = function() {
// update the cache
localProxy.remember(url, $scope.stuff);
$http.post(url, $scope.stuff);
};
})
What do you think? Is it useful? Is it "bonkers"?
I can think of one possible beautification, but I'm not entirely sure how to accomplish it.
Thing is, I like the API of $http.get
that it returns a promise with a function called success
, error
and finally
. The ideal API would look something like this:
app.controller('Ctrl', function($scope, $http) {
$scope.stuff = {};
// angular's $http service expanded
$http.getLocalProxy('/stuff/')
.success(function(cached, response) {
/* Imagine something like:
<p class="warning" ng-if="from_cache">Results you see come from caching</p>
*/
$scope.from_cache = cached;
$scope.stuff = response.stuff;
})
.error(function() {
console.error.apply(console, arguments);
});
})
That API looks and feels just like the regular $http.get
function but with an additional first argument to the success
promise callback.