Implementing Asynchronous Actions Based On User Preferences Client Side

On my current project, we needed a way to check a user set preference before taking action on behalf of the user. To be specific, we wanted to check if the user prefers for us to post an open graph action to facebook when they favorite a meme on our site. The trick is the user preferences are stored in our profile database and all of our open graph work is purely client side.

In our open graph module, we really didn't want to care how the user preferences were stored. We simply wanted to consume them in a clean way. An example looks like this:

f.onFavorite = function (postUrl) {
        var options = {
            data: postUrl,
            allowed: function(postUrl){
                f.postFavorite(postUrl);
            },
            unknown: function(postUrl){
                $('#js-og-enable-favorite-btn').data('postUrl', postUrl);
                $('#js-og-favorite-gate').modal("show");
            }
        };
        preferences.userPrefers('favoriting', options );
    };

This is the click handler for a favorite button. We pass in the url to the meme the user favorited and construct an options object. The options object defines data associated with the preference as well as a function to perform if the user allows the action. We also include a function to execute if the preference is not currently set. This way we can prompt the user to make a preference. Finally, we call the preferences module with the preference in question and the options.

Deep in the bowels of our preferences module, is the userPrefers method. It looks like this.

f.userPrefers = function(preferenceName, options){
         f.withCurrentPreferences(function(preferences){
         if(preferences[preferenceName])
            options.allowed(options.data);

         if(preferences[preferenceName] == null)
            options.unknown(options.data);
        });
    };

This function calls withCurrentPreferences and passes in a function describing what to do with a set of current preferences. We check to see if the preference we are checking is enabled and call the allowed method passing along the data if it is. Finally, it check is the preference is explicitly null and calls the unknown method if it is.

So far fairly clear and concise. But what magic is this withCurrentPreferences method?

f.withCurrentPreferences = function(action){
        var preferences = f.getPreferencesCookie();
        if(preferences)
            action(preferences);
        else
            f.getPreferences(action);
    };

f.getPreferences = function(action) {
        $.ajax({
            dataType: "jsonp",
            url: cfg.ProfileDomain + '/' + cfg.Username + '/Preferences',
            success: function(preferences){
                f.setPreferencesCookie(preferences);
                if(action)
                    action(preferences);
            }
        });
    };

The method takes an action to execute with preferences and attempts to read a locally stored preference cookie. We cache preferences locally to not bombard our app servers with unneeded calls. If the cookie based preference exists, we simply call the action passing along the preference. If not, we call getPreferences passing along the action. Finally the getPreferences function makes a ajax call out to our app server to get the preferences. On success it saves a preference cookie and if an action was passed in it calls it.

And there you have it a nice clean asynchronous method of taking actions based on a users preference that is managed completely client side and it uses a local caching mechanism to make it zippy.

Here is the full source of the AMD module.

define(['jquery', 'mods/ono-config', 'mods/utils/utils'], function ($, config, cookieJar) {
    var cfg = config.getConfig();
    var f = {};

    f.getPreferences = function(action) {
        $.ajax({
            dataType: "jsonp",
            url: cfg.ProfileDomain + '/' + cfg.Username + '/Preferences',
            success: function(preferences){
                f.setPreferencesCookie(preferences);
                if(action)
                    action(preferences);
            }
        });
    };

    f.setPreferencesCookie = function (preferences) {
       cookieJar.destroyCookie('preferences', cfg.CookieHostname);
       cookieJar.setCookie('preferences', JSON.stringify(preferences), 1000, cfg.CookieHostname);
    };

    f.getPreferencesCookie = function(){
      return JSON.parse(cookieJar.getCookie('preferences'));
    };

    f.userPrefers = function(preferenceName, options){
         f.withCurrentPreferences(function(preferences){
         if(preferences[preferenceName])
            options.allowed(options.data);

         if(preferences[preferenceName] == null)
            options.unknown(options.data);
        });
    };

    f.withCurrentPreferences = function(action){
        var preferences = f.getPreferencesCookie();
        if(preferences)
            action(preferences);
        else
            f.getPreferences(action);
    };

    f.savePreference = function(preferenceName, value){
        f.withCurrentPreferences(function(preferences){
            preferences[preferenceName] = value;
            f.setPreferencesCookie(preferences);
            f.setPreference(preferenceName, value);
        });
    };

    f.setPreference = function (preferenceName, value) {
        $.ajax({
            dataType: "jsonp",
            url: cfg.ProfileDomain + '/' + cfg.Username + '/SetPreference',
            data: {
                preferenceToSet:preferenceName,
                preferenceValue: value
            }
        });    
    };

    return f;
});
Follow me on Mastodon!