loading...
Cover image for Javascript APIs with async properties

Javascript APIs with async properties

omrilotan profile image omrilotan Originally published at Medium on ・2 min read

Getter properties are a great way to mask logic and expose what appears as static values. I find it extremely elegant to differentiate functionality from attributes. This logic may hide lazy calculations to be performed only on-demand, or it may hide logic that’s based on object’s, or even application state.

For example, a User object may have functionality like user.goOnline(), it should have attributes like user.isOnline. The check if a user is online should be performed on demand because this status may change from moment of instantiation to the moment of querying.

With the emerging of async/await in Javascript, it is now possible to create such lazy getters by pointing them to promises, creating more semantic APIs.

class User {
    constructor(id) { ... }
        goOnline() { ... }
        addBadge(type) { ... }
        get _isOnline() {  
        return fetch(`/${this.username}/online`)
            .then(response => response.json())  
            .then(data => data.isOnline)  
            .catch(error => { throw error; });  
    } 
}

const myUser = new User ('acb33259');

// Functionality
myUser.goOnline();

// Attribute
if (await myUser.isOnline) {

    // Functionality
    myUser.addBadge('online');
}

Static object can also leverage this way of typing. An API object pointing to async properties may also appear more readable.

const api = {
    get request() {
        return new Promise (resolve => setTimeout(() => resolve({status: 200}), 1000))
    }
};

(async () => {
    const { status } = await api.request;
    console.log(status); // 200
})();

When importing between modules — I feel the appeal is even greater.

module.exports = async () => {
    const { request } = require('./api');
    const { data } = await request;

    // Do amazing things with the data
};

And this just opens up endless sugary syntactic possibilities.

The following is a (simplified) real life example where I found using async getters made the final logic cleaner.

git tag -a ${tag} -m "${await message}"

const asyncExec = require('util').promisify(require('child_process').exec);

/**
 * Execute command line in a child process
 * @param  {...string} args Commands
 * @return {string}
 */
async function exec (...args) {
  const { stdout, stderr } = await asyncExec(...args);

  if (stderr) {
    throw new Error(stderr);
  }

  return stdout.trim();
}

/**
 * @typedef           gitData
 * @description       Git data getters
 * @type     {Object}
 * @property {string} author  Author of the last commit
 * @property {string} email   Git user email
 * @property {string} message Most recent commit message
 */
const gitData = Object.defineProperties({}, {
  author:  { get: async () => await exec('git log -1 --pretty=%an') },
  email:   { get: async () => await exec('git log -1 --pretty=%ae') },
  message: { get: async () => await exec('git log -1 --pretty=%B') },
});

/**
 * Create a tag by the last commit's author with it's message
 * @param  {string} tag Tag name (e.g. v1.1.0)
 * @return {void}
 */
module.exports = async (tag) => {
  const { message, author, email } = gitData;

  try {
    await exec(`git config --global user.name "${await author}"`);
    await exec(`git config --global user.email "${await email}"`);
    await exec(`git tag -a ${tag} -m "${await message}"`);
    await exec(`git push origin refs/tags/${tag}`);
  } catch (error) {
    console.error(error);
    throw error;
  }
};

Discussion

pic
Editor guide