/actorjs

Message-driven JavaScript actor hierarchies

Primary LanguageJavaScript

ActorJS

ActorJS is a JavaScript framework that structures apps as actors that communicate via asynchronous JSON-RPC and publish-subscribe messaging. With ActorJS, you define actor types then fire calls to instantiate them, invoke their methods, subscribe to their publications, delete them, and so forth.

ActorJS is being developed as the backbone of xeoEngine, a WebGL-based engine which lets us assemble and drive 3D worlds over a network.

Contents


Features:

Documentation

Examples

Example 1: Hello, World

Check out this super basic example - we'll define a simple actor type which will publish whatever we tell it to say:

ActorJS.addActorType("person",
    function (cfg) {

        var myName = cfg.myName;

        this.saySomething = function (params) {
            this.publish("saidSomething", {
                message:myName + " says: " + params.message
            });
        };
    });

Now create a stage and add an instance of the actor type:

var stage = ActorJS.createStage();

stage.call("addActor", {
    id:"dilbert",
    type:"person",
    myName:"Dilbert"
});

Subscribe to the message the actor will publish:

stage.subscribe("dilbert.saidSomething",
   function (params) {
       alert(params.message);
});

Call the actor's 'saySomething' method, which publishes that message back at us:

stage.call("dilbert.saySomething", {
    message:"Hello, World!"
});

[Run this example]

Example 2: Actor Hierarchies

Typically you would compose actors into hierarchies, then use paths into the hierarchies to resolve actor methods and topics. As before, define the "person" actor type:

ActorJS.addActorType("person",
    function (cfg) {

        var myName = cfg.myName;

        this.saySomething = function (params) {
            this.publish("saidSomething", {
                message:myName + " says: " + params.message
            });
        };
    });

Then define a "group" actor type, which creates and manages two "person" actors:

ActorJS.addActorType("group",
    function (cfg) {

        var myName = cfg.myName;

        this.addActor({
            id:"foo",
            type:"person",
            myName:"Foo"
        });

        this.addActor({
            id:"bar",
            type:"person",
            myName:"Bar"
        });

        this.saySomething = function (params) {

            this.call("foo.saySomething", params);
            this.call("bar.saySomething", params);

            this.publish("saidSomething", {
                message:myName + " says: " + params.message
            });
        };
    });

Create a stage and add the "group" actor:

var stage = ActorJS.createStage();

stage.call("addActor", {
    id:"group",
    type:"group",
    myName:"Group"
});

Subscribe to the message the 'group' actor will publish on behalf of either of its child 'person' actors:

stage.subscribe("group.saidSomething",
     function (params) {
         alert(params.message);
     });

Subscribe to the message the 'group' actor's first child 'person' actor will publish

stage.subscribe("group.foo.saidSomething",
     function (params) {
         alert(params.message);
     });

Subscribe to the message the 'group' actor's second child 'person' actor will publish

stage.subscribe("group.bar.saidSomething",
    function (params) {
        alert(params.message);
    });

Call the 'group' actor's 'saySomething' method, which calls that method in turn on both of its child 'person'actors

stage.call("group.saySomething", {
     message:"Hello, World!"
});

[Run this example]

Example 3: Using RequireJS

ActorJS encourages you create libraries of reusable actor types, to instantiate as required for each application.

Lets do a variation on Example 1, this time providing the "person" actor type as an AMD module (in actors/people/person.js):

define(function () {

    return function (cfg) {

        var myName = cfg.myName;

        this.saySomething = function (params) {
            this.publish("saidSomething", { message:myName + " says: " + params.message });
        };
    };
});

Point RequireJS at the base directory:

requirejs.config({
    baseUrl:"."
});

Configure ActorJS with a loader that wraps RequireJS:

ActorJS.configure({
    typeLoader:function (path, ok, error) {
        require(["actors/" + path], ok, error);
    }
});

Now create a stage and add an instance of our actor type. See how the type property resolves to our AMD module. You can configure ActorJS to use slashes to delimit paths, but I found that dots just look nicer and have a more objecty-feel.

var stage = ActorJS.createStage();

stage.call("addActor", {
    id:"foo",
    type:"people.person",
    myName:"Foo"
});

Subscribe to the message the actor will publish:

stage.subscribe("foo.saidSomething",
    function (params) {
        alert(params.message);
    });

And finally, fire a call at the actor to make it say hello:

stage.call("foo.saySomething", {
    message:"Hello, World!"
});

[Run this example]

  • ActorJS will cache the people.person actor type the first time it's loaded, which offsets the XHR overhead if we instantiate the type many times.
  • Still, if you want better performance and don't care about hot-loading actor types on demand, then you can define your actor types as plain JavaScript libs that use ActorJS.addActorType, then compress and concatenate them into one lib and load that statically.
  • ActorJS uses promises for calls and subscriptions. That means we can just instantiate actor types and use them immediately without having to synchronise with their appearance. Behind the scenes, ActorJS will buffer everything until the actor types load and instances exist.

Example 4: JSON Includes

For a higher level of reuse, we can create libraries of JSON components then pull them into our actor graphs as includes.

First, let's create a people/person actor type. Just for fun, let's make it an AMD module like before (in actors/people/person.js):

define(function () {

    return function (cfg) {

        var myName = cfg.myName;

        this.saySomething = function (params) {
            this.publish("saidSomething", { message:myName + " says: " + params.message });
        };
    };
});

Next, lets create a JSON component (in includes/people/pointyHairedBoss.json) that defines a hierarchy containing three of those actor types:

{
    "type":"people.person",
    "myName":"Pointy Haired Boss",

    "actors":[
        {
            "id":"dilbert",
            "type":"people.person",
            "myName":"Dilbert"
        },
        {
            "id":"phil",
            "type":"people.person",
            "myName":"Phil"
        }
    ]
}
  • See how each actor in this component will be an instance of the people/person type we created.
  • The root actor in this component has no ID - each time we include one of these, we're creating a separate instance of it, which will get its own ID.

Then point RequireJS at the base directory:

requirejs.config({
    baseUrl:"."
});

Configure ActorJS with a loader that wraps RequireJS:

ActorJS.configure({
    typeLoader:function (path, ok, error) {
        require(["actors/" + path], ok, error);
    }
});

Configure ActorJS with the base directory where our JSON components live:

ActorJS.configure({
    includePath:"includes/"
});

Now create a stage and include the component. See how the include property resolves to our JSON file:

var stage = ActorJS.createStage();

stage.call("addActor", {
    id:"boss",
    include:"people.pointyHairedBoss",
    myName:"Boss"
});

Subscribe to the message the actor will publish:

stage.subscribe("foo.saidSomething",
    function (params) {
        alert(params.message);
    });

And finally, fire a call at the actor to make it say hello:

stage.call("foo.saySomething", {
    message:"Hello, World!"
});

[Run this example]

Example 5: Client/server on HTML5 Web Messaging API

ActorJS's API allows (encourages) us to drive everything remotely via messages.

Let's create an example on the HTML5 Web Messaging API, building on concepts introduced in the previous examples.

First, whip up a page that exposes an ActorJS stage to Web Message clients:

<html>
<head>
    <script src="../build/actorjs.js"></script>
    <script src="lib/require.js"></script>
</head>
<body>
<script>

    // We'll use RequireJS to hot-load actor types.
    // Point it at the base directory:
    requirejs.config({
        baseUrl:"."
    });

    // Plug a loader into ActorJS:
    ActorJS.configure({
        typeLoader:function (path, ok, error) {
            require(["actors/" + path], ok, error);
        }
    });

    // Create a stage to contain actors:
    var stage = ActorJS.createStage();

    // Serve the stage to Web Message clients:
    var server = new ActorJS.WebMessageServer(stage);

</script>
</body>
</html>

Then we'll make a client page which will embed our server page in an iframe and drive it remotely, just as if if the ActorJS environment was actually in the client page. We'll make a variation on Example 2:

<html>
<head>
    <script src="../build/actorjs-webMessageClient.js"></script>
</head>
<body>
    <iframe id="myIFrame" src="server.html"></iframe>
    <script>

        var client = new ActorJSWebMessageClient({
            iframe:"myIFrame"
        });

        // We can create whole actor hierarchies in one call:
        client.call("addActor", {
            id:"boss",
            type:"people/person",
            myName:"Boss",
            actors: [
                {
                    id: "dilbert",
                    type: "people/person",
                    myName: "Dilbert"
                },
                {
                    id: "phil",
                    type: "people/person",
                    myName: "Phil"
                }
            ]
        });

        client.subscribe("boss.dilbert.saidSomething",
            function (params) {
                alert(params.message);
            });

        client.call("boss.dilbert.saySomething", {
            message:"Hello, World!"
        });

    </script>
</body>
</html>

The main coolness here is that the client page only depends on the ActorJS client library, meaning that the client bits can be embedded in blogs and code sharing sites like CodePen, without having to upload all your actors' dependencies there (image files etc). This has proved useful for sharing examples for xeoEngine (built on ActorJS) like this.

[Run this example]

Example 6: Injecting Resources

Actors can manage JavaScript objects for their children to use. We'll build on example 2, this time with a resource object that the children will send messages through.

Define a "person" actor type like before, but this time it's going to say something its thing via a resource:

ActorJS.addActorType("person",
    function (cfg) {

        var myName = cfg.myName;

        var myResource = this.getResource("myResource");

        this.saySomething = function (params) {
            myResource.saySomething(myName + " says: " + params.message);
        };
});

Next, define a "group" actor which injects the resource where the children can get it. The resource is just a simple JavaScript object with a saySomething method:

ActorJS.addActorType("group",
    function (cfg) {

        var myName = cfg.myName;

        this.setResource("myResource", {
            saySomething:function (sayWhat) {
                alert(sayWhat);
            }
        });

        this.addActor({
            id:"foo",
            type:"person",
            myName:"Foo"
        });

        this.addActor({
            id:"bar",
            type:"person",
            myName:"Bar"
        });

        this.saySomething = function (params) {

            this.call("foo.saySomething", params);
            this.call("bar.saySomething", params);

            this.publish("saidSomething", { message: myName + " says: " + params.message });
        };
    });

And finally, just like before, create the "group" actor, do the subscriptions and call:

var stage = ActorJS.createStage();

stage.call("addActor", {
    id:"group",
    type:"group",
    myName:"Group"
});

stage.subscribe("group.saidSomething",
     function (params) {
         alert(params.message);
     });

stage.subscribe("group.foo.saidSomething",
     function (params) {
         alert(params.message);
     });

stage.subscribe("group.bar.saidSomething",
    function (params) {
        alert(params.message);
    });

stage.call("group.saySomething", {
     message:"Hello, World!"
});

[Run this example]

Configuration

The examples above show most of the configurables, but here's some examples of those in an orderly fashion:

Path Delimiters

By default, ActorJS uses '/' as the delimiter character on paths to everything (actor types, JSON includes, actor methods, subscription topics etc.). You can configure that char to be something else, like this:

ActorJS.configure({
    pathSeparator:"/"
});

JSON Include Loader

By default, ActorJS has it's own JSON include loader, which uses an XMLHTTPRequest. You can plug in your own like this (which happens to be the same as ActorJS' default one):

ActorJS.configure({
    includeLoader:function (path, ok, error) {
        var jsonFile = new XMLHttpRequest();
        jsonFile.overrideMimeType("application/json");
        jsonFile.open("GET", ActorJS._includePath + path, true);
        jsonFile.onreadystatechange = function () {
            if (jsonFile.readyState == 4) {
                var json = JSON.parse(jsonFile.responseText);
                ok(json);
            }
        };
        jsonFile.send(null);
    }
});

JSON Include Path

Configure ActorJS to find JSON includes relative to the given base directory:

ActorJS.configure({
    includePath:"includes/"
});

Actor Type Loader

By default, ActorJS has no actor type loader. If you want to load types on demand from resources like AMD modules, instead of defining them in-code, you'll need to configure a loader. As an example - configure ActorJS to load actor types from AMD modules via RequireJS:

ActorJS.configure({
    typeLoader:function (path, ok, error) {
        require([path], ok, error);
    }
});

License

ActorJS is licensed under both the GPL and MIT licenses. Pick whichever of those fits your needs.

Inspirations

  • The JSON-based actor hierarchy is inspired in part by Unveil.js
  • The asynchronous JSON-RPC + publish/subscribe is inspired by Autobahn.JS