-
Notifications
You must be signed in to change notification settings - Fork 4
Getting Started
First, install the package engine.js
with NPM
$ npm install engine.js
Now we need to start intake
, cylinder
, and exhaust
components. These three components make up the "server-side" of an
Engine.js application.
Create a new file app.js
and place the following code in
the new file.
var engine = require("engine.js").engine,
intake = engine.intake.create(),
cylinder = engine.cylinder.create(),
exhaust = engine.exhaust.create();
Note: For the sake of simplicity we're starting the server components from within the application. Normally these components are started in a separate process(es).
Now we need to create a client. In your app.js
file:
var client = engine.client.create();
The client
will create task
objects and send them through the
server components (intake
, cylinder
, exhaust
). Results from the
task
will then be sent back to the client
.
A unit of work in an Engine.js application is a task
. A task
contains all the information it needs to execute.
In your app.js
file add the following:
var task = client.createTask();
task.setCode("1+1");
By default, a task's code
is executed in an empty Javascript
environment. The code
only has access to
built-in Javascript objects. Later we will
see how to add custom objects to execution environment.
A task
is run asynchronously. When the task
has completed the
task
will emit an eval
event passing:
-
err
:null
unless there was a problem before a task was executed. -
response
: An instance of a TaskResponse object.
In your app.js
file add the following:
task.on('eval', function(err, response){
console.log("task evaluation:", response.getEvaluation());
});
task.run();
Your app.js
file should look like this:
var engine = require("engine.js").engine,
intake = engine.intake.create(),
cylinder = engine.cylinder.create(),
exhaust = engine.exhaust.create();
var client = engine.client.create();
var task = client.createTask();
task.setCode("1+1");
task.on("eval", function(err, response){
console.log("task evaluation:",response.getEvaluation());
});
task.run();
If you run node app.js
you should see task evaluation: 2
You should also notice that the process does not immediately
exit. This is because the client and server components are still
connected. To fix this, close the client and server components within
the "eval"
event's callback:
task.on("eval", function(err, response){
console.log("task evaluation:", response.getEvaluation());
client.close();
intake.close();
cylinder.close();
exhaust.close();
});
Note: Normally the server components are started in separate process(es) so only closing the
client
is usually necessary.
In the last chapter we showed how to execute code in an empty Javascript environment. In this chapter will see show how to add your own functions and runtime data to the execution environment.
An Engine.js Task should include all the information it needs to execute. This includes three things:
- An Execution Context (
Context
) - Local Variable Bindings (
Locals
) - User Code (
Code
)
Context: A task's Context
explicitly defines the global
object
in which a task's Code
is run. A task's Code
does not have access
to anything except the defined global
object and
built-in Javascript objects.
A Context
is a Javascript function value (an anonymous function
wrapped in parens) that, when invoked, returns an object-literal. This
process is known as "Rendering the Context". The Context
takes one
argument (locals
) which binds when the Context
is rendered.
For example:
(function(locals){
return {
foo: "bar",
hello: locals.world,
qux: function() {
return "qal";
}
};
})
In this example the global
object will be populated with a foo
object whose value is "bar"
, a hello
object whose value is derived
from the locals
variable hash (see below), and a qux
object which
is a function. The task's Code
will only have access to these three
objects (in addition to the built-in objects like Date
, Array
, etc.).
A Context
is set on a Task
object using the setContext
method. It expects a String
.
Local Variable Bindings: There are times when your task will
depend on runtime information. The locals
argument of the Context
function allows you to pass-in an object literal of key/value pairs
which will be injected into your rendered Context
.
In the example above, our Context
references locals.world
. If we
set the task's locals
property to:
{ world: "Mundo" }
then the locals.world
object in the rendered Context
would be "Mundo"
Locals
are set on a Task
object using the setLocals
method. It expects an object-literal.
Code: The core of the task
is the Code
to be executed. Code
is just Plain Old Javascript (tm). It is executed in the context of
the task's corresponding rendered Context
. It is set on the task
object as a String.
Code
is set on a Task
object using the setCode
method. It expects a String
.
Your app.js file from before should look like this:
var engine = require("engine.js").engine,
intake = engine.intake.create(),
cylinder = engine.cylinder.create(),
exhaust = engine.exhaust.create();
var client = engine.client.create();
var task = client.createTask();
task.setCode("1+1");
task.on("eval", function(err, response){
console.log("task evaluation:",response.getEvaluation());
client.close();
intake.close();
cylinder.close();
exhaust.close();
});
task.run();
Lets define an increment function that will increment a value by 1. The definition of the function looks like this:
function(value){
return value + 1;
}
Recall that a Context
must be a function that returns an object-literal. An empty Context
looks like this:
(function(locals){
return {
}
})
To add the increment
function to our Context
we must add an
increment
key to the returned object-literal. Use the previously
mentioned function definition as the increment
key's value.
(function(locals){
return {
increment: function(value){
return value + 1;
}
}
})
Put the above in a new file called context.js
. Like a task's code, a
task's context is set on the task object as a string. Keeping the code
and context in seperate files allows us to easily read them as strings
while keeping their contents managable in a text editor.
To set the Context
on the task
, read in the contents of the
context.js
file and set the Context
on the task
:
var fs = require("fs");
var context = fs.readFileSync(__dirname + "/context.js","utf-8");
task.setContext(context);
Now you may call your new increment
function from your Code
:
task.setCode("increment(2)");
Your app.js file should look like this now:
var engine = require("engine.js").engine,
intake = engine.intake.create(),
cylinder = engine.cylinder.create(),
exhaust = engine.exhaust.create();
var client = engine.client.create();
var task = client.createTask();
task.setCode("increment(2)");
var fs = require("fs");
var context = fs.readFileSync(__dirname + "/context.js","utf-8");
task.setContext(context);
task.on("eval", function(err, response){
console.log("task evaluation:",response.getEvaluation());
client.close();
intake.close();
cylinder.close();
exhaust.close();
});
task.run();
Run node app.js
and you should see task evaluation: 3
Sometimes the execution of a task will depend on information supplied
at runtime. This is what Locals
are intended for.
Lets modify our existing increment
function. Rather than increment
the passed-in value by 1, we will determine the value to increment by
at runtime.
Modify your context.js
file to look like this:
(function(locals){
return {
increment: function(value){
return locals.increment_by + value;
}
}
})
The value of the locals
argument is set on the task
object with
the setLocals
method. Unlike the values accepted by setCode
and
setContext
, the setLocals
method accepts an object-literal of
key/value pairs. Since the increment
function in our context
references locals.increment_by
, our Locals
must have an
increment_by
key.
Set the Locals
for your task in app.js
:
var engine = require("engine.js").engine,
intake = engine.intake.create(),
cylinder = engine.cylinder.create(),
exhaust = engine.exhaust.create();
var client = engine.client.create();
var task = client.createTask();
task.setCode("increment(2)");
var fs = require("fs");
var context = fs.readFileSync(__dirname + "/context.js","utf-8");
task.setContext(context);
task.setLocals({
increment_by: 3
});
task.on("eval", function(err, response){
console.log("task evaluation:",response.getEvaluation());
client.close();
intake.close();
cylinder.close();
exhaust.close();
});
task.run();
Run node app.js
and you should see task evaluation: 5
Locals
cannot be directly accessed by Code
. Properties on the
locals
argument can only be accessed my functions in the Context
.
In the last chapter we saw how to run a Task with Engine.js. In this chapter we're going to look at how Engine.js is architected and how it can be configured.
As you might have guessed, Engine.js uses the metaphor of a combustion engine to name its internal components.
-
Client: The
Client
is responsible for create newTask
objects and sending them theIntake
. It is also responsible for receivingTaskResponse
objects from theExhaust
-
Intake: The
Intake
is responsible for accepting incomingTask
messages from many clients and forwarding them to manyCylinders
-
Cylinder: When a
Cylinder
is created it starts its ownPiston
(in a separate child process). When aCylinder
receives aTask
it starts an execution timer and sends theTask
to itsPiston
for execution. When thePiston
returns a result theCylinder
forwards it to theExhaust
- If the execution timer finishes before a response from the
Piston
is received, thePiston
process is killed and an error is sent back to theClient
(via theExhaust
).
- If the execution timer finishes before a response from the
-
Piston: The
Piston
is responsible for creating a code sandbox from the receivedTask
messages's context and run-time variable bindings. ThePiston
then runs the user-code against the sandbox and returns the results to its parent Cylinder. -
Exhaust: The
Exhaust
receivesTask
result messages from manyCylinders
and forwards then back to the appropriateClient
.
All components use ZeroMQ to communicate with each other. This allows
us to start the components (Intake
, Cylinder
, and Exhaust
) in
individual processes, even on different machines (as long as they are
in the same network). In the previous chapter we started all of the
components in the same process, but this was only for convenience. The
ZeroMQ endpoints are
configurable for most components.
+------------+ +------------+
-----| | | |
+------+ | | Cylinder |<---->| Piston |
| Task +-+ +------------+ +------------+ | | | | |
+------+ | | | | | | +------------+ +------------+
(n) +---+ Client +----+ +--->----| Intake |-->-+ | (n)
+------+ | | | | | | | | | +------------+ +------------+
| Task +-+ +------------+ | | +------------+ | | | | | |
+------+ (n) | | (1) +-->---+ | Cylinder |<---->+ Piston |
(n) +-----+ | | | | |
| | +--<---+ +------------+ +------------+
+------------+ | | +------------+ | | (n)
+------+ | | | | | | | | +------------+ +------------+
| Task +-----+ Client +----+ +---<----| Exhaust +-<--+ | | | | |
+------+ | | | | | | Cylinder |<---->+ Piston |
(n) +------------+ +------------+ | | | | |
(n) (1) | +------------+ +------------+
| (n)
| +------------+ +------------+
| | | | |
+----| Cylinder |<---->| Piston |
| | | |
+------------+ +------------+
(n)
- Start multiple
Cylinders
- ZeroMQ round-robin balancing
- Standard Logger
- Custom Logger
TaskResponse.getDebug()
There are several measures taken to secure Engine.js
- Child process execution
- V8 Context membrane
- Execution timeouts
-
Function.toString()
removal
- Asynchronous Functions
- Using 3rd Party Modules