AngularJS promises as cache objects

Caching is a handy approach for keeping values that take a good amount of time to be computed.

Given the design in AngularJS, a promise can serve the purpose of being the cache object, so that it’s able to address as many invocation to return the cached value, saving some lines of code.

To exercise that concept, an application to show lucky numbers below

1st value Show me
2nd value Show me
3rd value Show me

Each lucky number is revealed on the user click to the corresponding Show me button.

Suppose all lucky numbers are provided by a third party service. All numbers are computed at once, but that operation takes some time to complete. To improve user experience, all lucky numbers are preloaded and cached, so that the user click on any button can have instant effect on the screen

Some HTML code:

<body ng-app="LuckyNumbersApp">

<table ng-controller="LuckyNumbersValuesCachingCtrl">
    <thead>
        <tr>
            <th colspan="3">Lucky numbers - caching values</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>1st value</td>
            <td>{{luckyNumbers[0]}}</td>
            <td>
                <button ng-click="getLuckyNumber(0)">Show me</button>
            </td>
        </tr>
        <tr>
            <td>2nd value</td>
            <td>{{luckyNumbers[1]}}</td>
            <td>
                <button ng-click="getLuckyNumber(1)">Show me</button>
            </td>
        </tr>
        <tr>
            <td>3rd value</td>
            <td>{{luckyNumbers[2]}}</td>
            <td>
                <button ng-click="getLuckyNumber(2)">Show me</button>
            </td>
        </tr>
    </tbody>
</table>

And the AngularJS counterpart:

var appModule = angular.module('LuckyNumbersApp', []);

appModule.controller('LuckyNumbersValuesCachingCtrl', function ($scope, $q, luckyNumbersService) {
    //  The values retrieved from outside the application are cached
    var cachedValues;
    $scope.luckyNumbers = [];

    function getAllLuckyNumbers() {
        var newPromise = $q.defer();

        if (cachedValues) {
            newPromise.resolve(cachedValues);
        }
        else {
            luckyNumbersService.computeLuckyNumbers().then(function (data) {
                cachedValues = data;
                newPromise.resolve(data);
            });
        }

        return newPromise.promise;
    }

    $scope.getLuckyNumber = function (num) {
        console.log('[Values caching] Loading lucky number ' + (num + 1));
        getAllLuckyNumbers().then(function (result) {
            console.log('[Values caching] Lucky number ' + (num + 1) + ' : ' + result[num]);
            $scope.luckyNumbers[num] = result[num];
        });
    };

    getAllLuckyNumbers();
});

The retrieval the numbers from the service (luckyNumbersService) is implemented inside function getAllLuckyNumbers, which extracts the array of numbers from a promise returned by service and wraps the array in a new promise, that is returned to the function caller. The array is kept in cachedValues variable, to avoid more invocations to the service in further function calls. Each number can then consumed, to populate the screen: user clicks on button, getLuckyNumber function is called, then getAllLuckyNumber is called (if the preloading of numbers has already completed, all calls here would here will already be handled by the cache).

So far, so good. It all works.

A fact: the way promises were designed in AngularJS allow the then method to be called many times. Documentation already gives in a hint (On the initial description of then method: This method returns a new promise…) and I confirmed that by looking at the $q service source code. In order not to extend this post too much, I won’t go deep.

Having that said, rather than caching the array of numbers, extracted from the promise returned by luckyNumbersService, the promise itself can be cached, so the code can be polished a bit.

A second version of the controller:

appModule.controller('LuckyNumbersPromiseCachingCtrl', function ($scope, $q, luckyNumbersService) {
    // Simpler approach: the promise itself returned by the service is cached
    var cachedPromise;
    $scope.luckyNumbers = [];

    function getAllLuckyNumbers() {
        if (!cachedPromise) {
            cachedPromise = luckyNumbersService.computeLuckyNumbers();
        }

        return cachedPromise;
    }

    $scope.getLuckyNumber = function (num) {
        console.log('[Promise caching] Loading lucky number ' + (num + 1));
        getAllLuckyNumbers().then(function (result) {
            console.log('[Promise caching] Lucky number ' + (num + 1) + ' : ' + result[num]);
            $scope.luckyNumbers[num] = result[num];
        });
    };

    getAllLuckyNumbers();
});

Now, getAllLuckyNumbers caches and forwards the promise returned by the service. The rest of the code stays the same, where each user click to a button makes the corresponding numbers provided by the service to populate the screen. And for multiple button clicks, the promise instance cached by the controller will be able to handle each then call made inside getLuckyNumber, invoking the callback with the array of numbers computed by the service.

Even if getAllLuckyNumbers had to create a new promise, to wrap result of perhaps some transformation, validation, filtering or any other operation over the numbers provided by the service, that promise could be kept and reused later.

For a running version of this example, access this link

References

AngularJS documentation: $q service