Home Blog CV Projects Patterns Notes Book Colophon Search

Functional ES6 Promise Experiments

WARNING: This is a work in progress. The core ideas are sound, the detail I haven't explained well enough yet.

The key idea here is that you have things called actions that make up part of your platform, that a function can call with an optional arguments object to get something done. Actions always return a promise that resolves to a JSON serialisable object, even if it is resolves to {}. They can't return a promise that contains a structure with other promises, just JSON serialisable data. The arguments object is also a plain, JSON serialisable object, even if it is just {};

Actions can only call other action and I/O functions via a context that is explicitly given to them. They don't call other functions or import modules. What they need to get their work done must be in the context they receive.

Actions can derive new contexts from the original one to pass to other actions. They might add new functionality or remove existing functionality.

With this set up:

Also, because dependencies resolve themselves when the promises resolve, each action doesn't need to know about the order things happen in within other actions, so code stays super-simple and business logic is explicit.

With this setup, any action function could be moved to a separate process/server and the calling code wouldn't need to change. This makes it easy to change the implementation of action-based code as things grow and change.

The only helper you need to make this work smoothly is some code to derive one context from another:

function deriveContext(currentContext, actionsToAdd, actionsToDelete) {
  let context = Object.assign({}, currentContext, actionsToAdd);
  for (let i=0; i<actionsToDelete.length; i++) {
    delete context[actionsToDelete[i]];
  }
  return context;
}

Here's an action:

function getActors(ctx, args) {
    return Promise.resolve({actors: ["Sean Penn", "Rachel McAdams"]);
}

This is an action because it takes a context and returns a promise. It doesn't use any arguments, but if it did, it would also have an args argument.

Actions are never allowed to call any functions directly unless they are pure (i.e. just do something without any network access, promises, side-effects etc).

To use this getTime() method we'd create a context, and then pass it to main() action:

function main(ctx, args) {
    Promise.all([
      getTime(ctx, null),
    ]).then((results) => {
        return {"actors": results[0]};
    })
  );
}

const ctx = {
    time: getTime,
    log: console.log,
}
main(ctx, null).then((result) => {
   ctx.log({}, {message: 'Actors: ' + JSON.stringify(results[0])});
   return Promise.resolve(null);
});

The beauty of this is that we can test getTime() and main() by simply creating different contexts. None of their dependencies are explicitly hard-coded.

We can test like this:

const time = require('./time');

describe('main', () => {
  it('called', (done) => {
    let msg = ''
    const ctxt = {
        time: time.getTime
        log: (ctx, args) => {
            msg += args.message;
        },
    };
    time.main(ctx, null).then((r) => {
      expect(r.actors).toBe(["Sean Penn", "Rachel McAdams"]);
      expect(msg).toBe('Actors: ["Sean Penn", "Rachel McAdams"]');
      done();
    });
  });
});

Ways to structure ES6 code that works in the browser, and in node.

Copyright James Gardner 1996-2020 All Rights Reserved. Admin.