Tuesday, 8 July 2008

Using orbited Javascript STOMP client with Haxe


I am developing the early idea behind a message-driven SaaS application that will require the application UI to update in real time. I was aware of the cometd protocol implementations, but then came across orbited , and TCP sockets in the browser! Even better, I then discovered Apache ActiveMQ JMS broker, which hopefully will handle all the message queuing and subscription logic for my system, rather than having to roll my own. More on this on another blog entry soon.

To test this stack, I decided to adapt the STOMP protocol demo included in the v0.5 orbited distribution to also test the Haxe wrapper of the orbited STOMP protocol js library. Here's my adapted code.



  1. First, I created a Haxe extern wrapper class, STOMPClient.hx, for the orbited STOMP protocol js library:


    // Extern class for orbited Stomp.js protocol library object
    // Hosting html file must have "Stomp = stomp;" line so that Haxe code can access the js "stomp" object as named "Stomp"

    extern class STOMPClient

    {
    public function new():Void;

    // Call back functions
    // "msg" parameter is an anonymous object containing:
    // type:String
    // headers:Hash
    // body:String

    public function onmessage(msg:Dynamic):Void;

    public function onerror(msg:Dynamic):Void;

    public function onopen():Void;

    public function messageReceived(msg:String):Void;

    // Client methods

    public function connect(domain:String, port:Int, user:String, ?password:String):Void;

    public function disconnect():Void;

    public function send(msg:String, destination:String, ?custom_headers:Hash):Void;

    public function subscribe(destination:String):Void;

    public function unsubscribe(destination:String):Void;

    public function begin(id:String):Void;

    public function commit(id:String):Void;

    public function abort(id:String):Void;

    }




  2. I also created a small wrapper for the Shell.js library associated with the orbited demos:


    import js.Dom;

    /**
    * ...
    * @author Richard J Smith
    */

    // Extern class wraper for orbited client js library Shell.js

    extern class Shell

    {

    public function new(output:HtmlDom):Void;

    public function print(data:String):Void;

    }





  3. I created a Main.hx Haxe main application file to hold the initialisation logic and create the HTML button handler logic. The first bit sets up imports and the main entry method, which using dojo to provide the addOnLoad handler when the page has finished loading into the browser:


    **
    * ...
    * @author Richard J Smith
    */

    package test;

    import Dojo;
    import Shell;
    import STOMPClient;
    import js.Lib;
    import js.Dom;
    import test.DomListener;

    class Main
    {

    public static function main():Void {

    untyped __js__(" // assign "Dojo" javascript object to actual "dojo" object. Workround because Haxe requires class names to start with uppercase character

    Dojo.addOnLoad(Main.initStompPage );

    }


    Next, the first part of the application method sets up references to Shell and STOMPClients (this is following the original javascript contained in the orbited STOMP demo index.htm file):


    public static function initStompPage():Void {

    Lib.document.domain = Lib.document.domain;

    // Setup shell

    var output = Lib.document.getElementById("

    var s = new Shell(output);

    untyped __js__("ORBITED_PORT = 8000");

    var stomp = new STOMPClient();



    The method sets up callbacks for the onmessage, onerror and onopen events generated from the STOMPClient object:



    stomp.onmessage = function(msg)
    {
    s.print(msg.body);
    };

    stomp.onerror = function(msg)
    {
    Lib.alert(msg.body);
    }

    stomp.onopen = function() {

    var usernameField:Text = cast js.Lib.document.getElementById('username');

    s.print("Connected to ActiveMQ as user " + usernameField.value);

    }




  4. Finally for the Main.hx file, set up button event listeners (replacing the embedded onclick handlers in the original index.html file):


    /****** Set up button listeners ***************/

    // Connect

    var btn1:Button = cast js.Lib.document.getElementById('connectBtn');

    DomListener.add(btn1, "
    {
    var usernameField:Text = cast js.Lib.document.getElementById('username');

    stomp.connect('localhost', 61613, usernameField.value);
    });

    // Subscribe

    var btn2:Button = cast js.Lib.document.getElementById('subscribeBtn');

    DomListener.add(btn2, "click", function(e)
    {
    var subChannelField:Text = cast js.Lib.document.getElementById('sub_channel');

    stomp.subscribe(subChannelField.value);
    });

    // Unsubscribe

    var btn3:Button = cast js.Lib.document.getElementById('unsubscribeBtn');

    DomListener.add(btn3, "click", function(e)
    {
    var subChannelField:Text = cast js.Lib.document.getElementById('sub_channel');

    stomp.unsubscribe(subChannelField.value);
    });

    // Send

    var btn4:Button = cast js.Lib.document.getElementById('sendBtn');

    DomListener.add(btn4, "click", function(e)
    {
    var inputField:Text = cast js.Lib.document.getElementById('input');

    var pubChannelField:Text = cast js.Lib.document.getElementById('pub_channel');

    stomp.send(inputField.value,pubChannelField.value);
    });

    // Send With Transaction ID

    var btn5:Button = cast js.Lib.document.getElementById('sendWithTransactionIDBtn');

    DomListener.add(btn5, "click", function(e)
    {
    var inputField:Text = cast js.Lib.document.getElementById('input');

    var pubChannelField:Text = cast js.Lib.document.getElementById('pub_channel');

    var transPubIDField:Text = cast js.Lib.document.getElementById('trans_pub_id');

    var headers:Hash = new Hash();

    headers.set("transaction", transPubIDField.value);

    stomp.send(inputField.value,pubChannelField.value,headers);
    });

    // Begin Transaction

    var btn6:Button = cast js.Lib.document.getElementById('beginTranBtn');

    DomListener.add(btn6, "click", function(e)
    {
    var transIDField:Text = cast js.Lib.document.getElementById('trans_id');

    stomp.begin(transIDField.value);
    });

    // Commit Transaction

    var btn7:Button = cast js.Lib.document.getElementById('commitTranBtn');

    DomListener.add(btn7, "click", function(e)
    {
    var transIDField:Text = cast js.Lib.document.getElementById('trans_id');

    stomp.commit(transIDField.value);
    });

    // Commit Transaction

    var btn7:Button = cast js.Lib.document.getElementById('commitTranBtn');

    DomListener.add(btn7, "click", function(e)
    {
    var transIDField:Text = cast js.Lib.document.getElementById('trans_id');

    stomp.commit(transIDField.value);
    });

    // Abort Transaction

    var btn8:Button = cast js.Lib.document.getElementById('abortTranBtn');

    DomListener.add(btn8, "click", function(e)
    {

    var transIDField:Text = cast js.Lib.document.getElementById('trans_id');

    stomp.abort(transIDField.value);
    });

    }
    }




  5. To finish the picture, here's the containing index.html file:


    <!DOCTYPE html>

    <html>

    <head>

    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">

    <link rel="stylesheet" href="style.css">

    <title>JavaScript Stomp Test</title>

    <script src="http://ajax.googleapis.com/ajax/libs/dojo/1.1.1/dojo/dojo.xd.js"></script>

    <script src="/static/BinaryTCPSocket.js"></script>

    <script src="/static/shell.js"></script>

    <script src="/static/protocols/stomp/stomp.js"></script>

    </head>

    <body>

    <h1>STOMP Test</h1>

    <div id="output" style="background-color: #eee; overflow-y: scroll; height : 200px;"></div>

    <button id="connectBtn">Connect</button>Name: <input id="username" value="frank">

    <br>

    <button id="subscribeBtn">Subscribe</button>

    <button id="unsubscribeBtn">Unsubscribe</button>

    channel:

    <input id="sub_channel" value="/topic/home"><br>

    <button id="sendBtn">Send</button>

    <button id="sendWithTransactionIDBtn">Send with Transaction Id</button>

    <input id="input" value="hello">

    channel:

    <input id="pub_channel" value="/topic/home">

    transaction id:

    <input id="trans_pub_id" value="trans1">

    <br>

    <br>

    <button id="beginTranBtn">Begin Transaction</button>

    <button id="commitTranBtn">Commit</button>

    <button id="abortTranBtn">Abort</button>

    transaction id:

    <input id="trans_id" value="trans1">

    <!-- Insert Haxe - generated JS file -->

    <script src="/static/main.js"></script>

    </body>

    </html>




  6. The index.html file is placed in the webroot folder, all the js files included in a "/static" subfolder.


  7. The Haxe files are compiled using the javascript compiler directive. Here's my ant build script:


    <project name="StompTest" basedir="." default="buildAll">

    <property name="home.dir" value="F:\orbited\Haxe"/>

    <property name="project.dir" value="${home.dir}\StompTest" />

    <!--<taskdef name="haxe" classpath="../../antHaxe.jar" classname="com.relivethefuture.ant.Haxe"></taskdef>-->

    <taskdef name="haxe" classname="com.relivethefuture.ant.Haxe"></taskdef>

    <!-- buildALL -->

    <target name="buildAll" depends="buildTests">

    </target>

    <!-- StompTest-->

    <!-- StompTest Tests-->

    <target name="buildTests" depends="cleanTests,compileTests">

    </target>

    <target name="cleanTests" description="Clean the directories for building">

    <delete file="${project.dir}\bin\Main.js"/>

    </target>

    <target name="compileTests">

    <haxe classpath="${project.dir}\src;${project.dir}\lib\dojo\src;${project.dir}\lib\orbited\src" javascript="${project.dir}\bin\Main.js"

    flags="debug,verbose" main="test.Main" />

    </target>

    </project>




  8. I launch nekotools server -p 80 from the webroot directory. I launch the orbited server from the same folder (defaults to port 8000) . And launch a standard install of Apache ActiveMQ 5 (port 61616)


  9. Launch a web browser, point it at http://localhost You should the browser screen at the top of this post
  10. Click the Connect button, then Subscribe to a topic and send some messages. The messages will get thrown back to the same browser by the ActiveMQ server via the orbited server. Welcome to real-time JMS in the browser using Haxe!

No comments: