-
Notifications
You must be signed in to change notification settings - Fork 11
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.
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.
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.
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 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)
- registersfn
to be called wheneverevent
fires -
fire(String event, Object data)
- dispatchesevent
to all listeners of that event, calling the registered function withdata
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