Skip to content

Architecture

Pat Grasso edited this page Oct 20, 2017 · 1 revision

Architecture

The TaylorFit system is divided into two parts, like any other web application. The front end (interface) and the back end (engine). TaylorFit is isomorphic, which means the "back end" can either sit on a server in the cloud, or it can be used on the client's machine, with no additional need for a server. Currently, TaylorFit is deployed as a static web app (the engine is packaged with the client code).

The engine and the interface communicate with one-way messages. This allows the two parts to operate asynchronously, only updating the other when something important happens. This allows the engine to do expensive computation without making the interface wait for it to complete. This one-way messaging is inherent with Web Workers. Web Workers use the postMessage() and onmessage() functions to communicate by passing messages.

Each web worker has its own thread of execution, which means the engine and the interface operate on different threads. If it were not for this, the expensive computations the engine needs to perform would halt the UI, making for a terrible user experience.

For example, this is what happens when a user clicks to enable an exponent:

                interface                |                engine
             ===============             |            ==============
        1. user clicks exponent          |
        2. worker.postMessage(setExponents) ---------------->
                                         |  3. onmessage(setExponents) is called
            (user does other things)     |  4. candidates & model are recomputed
                    <---------------------- 5. postMessage(candidates)
                    <---------------------- 6. postMessage(model)
        7. UI is updated with the new    |
           model and candidates          |

While the engine is quietly computing in the background, the interface is able to run smoothly.

Where the magic happens

Once you're well versed with Web Workers, take a look at engine/worker/engine-worker.js. This is the "main()" for the engine web worker. It instantiates the model, sets up listeners for events coming from the UI, and forwards events emitted by the model to the UI. This is where all of the wiring between the UI and the model happens. You can think of it like a "router" in an API. It handles requests.

Once the engine worker is instantiated and supplied with a message handler

const engineWorker = new Worker('engine-worker.js');
engineWorker.onmessage = (e) => onMessageFromEngine;

you can send requests to the engine.

engineWorker.postMessage({ type: 'setData', data: {
  data: [[0, 1], [1, 2]],
  label: 'fit'
} });

engineWorker.postMessage({ type: 'setExponents', data: [1, 3] });

The engine will take these requests, process them, apply them to the model, and eventually send a message in response.

function onMessageFromEngine(e) {
  const data = e.data.data
      , type = e.data.type;

  console.log('type:', type);
  console.log('data:', JSON.stringify(data));
}

So far, this is what we would probably see in the console:

type: candidates
data: [[[0, 0, 0]], [[0, 1, 0]]]
type: model
data: < see Model#getModel() >
type: candidates
data: [[[0, 0, 0]], [[0, 1, 0]], [[0, 3, 0]]]
type: model
data: < see Model#getModel() >

Notice that for each message we send, we get two in response. One of type candidates and one of type model. This is intentional. Once again, by using a message passing model instead of a request-response model, we're able to do things like this.

What the engine worker understands

Every message that the engine can receive, as well as the messages it sends in response to what it receives, is outlined in detail in the Engine API Wiki.

See how the interface does it

The interface's handling of the engine worker is all done in interface/adapter/worker.coffee. The worker is instantiated here and the message handlers are set up to receive messages, which get propagated to the UI.

The pub-sub pattern

The publisher-subscriber design pattern allows objects to "follow" other objects in the Facebook or Twitter sense. An object can decide to fire an event, and if another object is listening to that event, they are notified. The Observable class can be extended, which gives the subclass two methods:

  • on(String event, Function fn) - registers fn to be called whenever event fires
  • fire(String event, Object data) - dispatches event to all listeners of that event, calling the registered function with data as the argument

Observable is extended by Model, which means you can subscribe to events emitted by the model. Within the model class, you'll find this.fire(<event>, <data>) frequently. This is the model saying "hey, to whom it may concern, this happened and here's the result."

Again, this lends to asynchronous behavior. Instead of waiting for a call to model.getCandidates() to resolve completely (which could take a while), we can just subscribe to the getCandidates.end event, which fires when all of the candidate terms have been computed.

const m = new Model();
m.on('getCandidates.start', () => console.log('starting computation');
m.on('getCandidates.each', (cand) => console.log('here\'s one:', cand));
m.on('getCandidates.end', (candidates) => console.log('hooray!', candidates));
m.getCandidates(); // this will take a while