Wednesday, November 4, 2015

Sneaky JavaScript Technics II

How do you secure an asynchronous method to make sure that the code is executed only once? Here is an example using promises.

The context

I often talk about promises as a way to handle asynchronous calls. In this short article, I would like to illustrate an advanced technique that allows you to synchronize concurrent calls to an asynchronous API with promises.

Let consider the following API:

/** * Intialize the API. * WARNING: must be called only once! * * @param {Function} callback Will be triggered when the API is ready * it receives a dictionary exposing the api. */ function intializeApi (callback) { /*...*/ }

As a client of this API, you need to find a place in your code where you would call it this way:

// Intialize the API intializeApi(function (api) { // Use the API });

Handling concurrent calls

As the comment says, you can call this function only once. What happens if you develop several modules that requires this API? You have to wrap the function in a module that is loaded before any other and expose a method to secure the call.

(function (context) { "use strict"; /*global intializeApi*/ var _api; context.safeInitializeApi = function () { if (_api) { return Promise.resolve(_api); } return new Promise(function (resolve/*, reject*/) { intializeApi(function (api) { _api = api; resolve(api); }); }); }; }(this));

This sounds great: when the callback is triggered, it keeps track of the returned object and any subsequent call will use the cached result.

But...

What if the you consider the following code:

// module1 safeInitializeApi() .then(function (api) { // Use the API }); // module2 safeInitializeApi() .then(function (api) { // Use the API });

The function safeInitializeApi is called twice sequentially. Because the initializeApi is asynchronous, it won't have the possibility to execute the fulfillment handler (that sets _api) before the second call.

As a result, you still call the API twice.

Solution

The following part was based on an incorrect assumption. However, even if the proposed solution is useless (and I will explain the reason at the end of the article), is remains valid (it works). To understand where I come from, the

Here is a more sophisticated version of the safeInitializeApi:

(function (context) { "use strict"; /*global intializeApi*/ var _api, _pendingPromises; context.safeInitializeApi = function () { var deferred; if (_api) { return Promise.resolve(_api); } if (!_pendingPromises) { _pendingPromises = []; return intializeApi() .then(function (api) { _api = api; _pendingPromises.forEach(function (deferred) { deferred.resolve(api); }); return api; }); } // Build a new promise resolved upon API fulfillment deferred = {}; deferred.promise = new Promise(function (resolve, reject) { deferred.resolve = resolve; deferred.reject = reject; }); _pendingPromises.push(deferred); return deferred.promise; }; }(this));

The trick consists in creating new promises that will be resolved when the initial one is fulfilled. The array of promises (_pendingPromises) acts as a critical section to know if the method has already been called or not.

Handling errors

How do you handle errors?

Unlike fulfillment handlers that can be chained (.then() returns a promise), rejection handlers can't be. Hence, we need to create another promise to wrap the initial one and forward fulfillment or rejection on all pending promises.

This leads to this wrapper:

(function (context) { "use strict"; /*global intializeApi*/ var _api, _pendingPromises; context.safeInitializeApi = function () { var deferred; if (_api) { return Promise.resolve(_api); } deferred = {}; deferred.promise = new Promise(function (resolve, reject) { deferred.resolve = resolve; deferred.reject = reject; }); if (!_pendingPromises) { _pendingPromises = [deferred]; intializeApi() .then(function (api) { _api = api; _pendingPromises.forEach(function (deferred) { deferred.resolve(api); }); }) .catch(function (reason) { _pendingPromises.forEach(function (deferred) { deferred.reject(reason); }); }); } else { _pendingPromises.push(deferred); } return deferred.promise; }; }(this));

Final solution

If you want this trick to be reusable, here is a function that wraps any function or method to ensure that it can be called only once. This sample page will allow you to see it in action.

/*global intializeApi*/ function onceWrapper (callback) { "use strict"; var _result, _pendingPromises; return function () { var deferred; if (_result) { return Promise.resolve(_result); } deferred = {}; deferred.promise = new Promise(function (resolve, reject) { deferred.resolve = resolve; deferred.reject = reject; }); if (!_pendingPromises) { _pendingPromises = [deferred]; callback.apply(this, arguments) .then(function (result) { _result = result; _pendingPromises.forEach(function (deferred) { deferred.resolve(result); }); }) .catch(function (reason) { _pendingPromises.forEach(function (deferred) { deferred.reject(reason); }); }); } else { _pendingPromises.push(deferred); } return deferred.promise; }; } // Example of use var safeInitializeApi = onceWrapper(intializeApi);