Sunday, November 30, 2014

FRP using RxJS and Firebase

FRP using RxJS and Firebase

FRP (Functional Reactive Programming) is the new cool thing. It allows elegant solutions without relying on complex state variable logic and allowing a pure functional approach to writing clean code that is much more error resistant.

RxJS

There are several Javascript FRP implementations available. The most common ones being BaconJS, RxJS and Kefir. Bacon seems a little bit old and slow, whereas Kefir seems very promising and fast but there isn’t much adoption and therefore little in the way of help on-line when you get stuck. RxJS has been developed by Microsoft and is seemingly the most popular (next to BaconJS).

Firebase

Firebase is an excellent cloud-based data storage and synchronisation service that provides a very nice API in various forms (JS, RESTful etc) to interact with the store. It works like a large JSON structure that you can store, query at any node … and is extremely fast and scalable.

The Application [live demo here ]

To get to know FRP a little bit I decided to write a simple multi-user Scribble program using these two libaries (plus JQuery). Using an HTML5 canvas we can set-up some event streams from the mouse interaction. First let’s create the HTML

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="X-UA-Compatible" content="IE=edge" >
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta charset="utf-8">
        <meta name="description" content="">
        <meta name="author" content="">
        <title>Rx for JavaScript Rocks!</title>
        <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css">
        <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css">
        <link rel="stylesheet" href="binding.css">
    </head>
    <body>
        <div class="container">
            <div class="page-header">
                <h1>Shared Whiteboard</h1>
            </div>
            <div class="pull-left controls-group">
               <label>Colour:</label> 
               <input id="colour" value="#df4b26">
            </div>
            <button class="pull-right btn btn-primary" id="clear">
                Clear Canvas
            </button>
            <div class="row-fluid">
                <canvas id="tutorial" width="640" height="480"></canvas>
            </div>
        </div>
        <script src="bower_components/jquery/dist/jquery.min.js"></script>
        <script src="bower_components/firebase/firebase.js"></script>
        <script src="bower_components/rxjs/dist/rx.lite.compat.js"></script>
        <script src="rx.firebase.js"></script>

        <script src="main.js"></script>
    </body>
</html>

We set up a simple bootstrap view here with a canvas element, a text field for setting the scribble colour, and a button to clear the canvas.

Now to the Javascript. For those not familiar with FRP methodologies, we want to handle as much as we can as streams of data (Observables) that we perform functional processes on to do something sensible with them. Instead of triggering state changes and switching on different states, we want a continuous approach where the decision making is done by the flow of the events through our functional pipeline.

To achieve this we first have to create Observables from our mouse interactions.

var mouseDowns  = Rx.Observable.fromEvent(canvas, 'mousedown');
var mouseUps    = Rx.Observable.fromEvent(document, 'mouseup');
var mouseMoves  = Rx.Observable.fromEvent(canvas, 'mousemove');

We can model a mouse drag event by pulling mouse-move events when we get a mouse-down event, until we get a mouse-up event. In RxJS this is simply

var mouseDrags = mouseDowns.select(function (downEvent) {
    return mouseMoves.takeUntil(mouseUps).select(function (drag) {
        return functionToGetStuffWeWant(drag);
    });
});

This creates an Observable with events for each drag event, inside each drag event is an Observable of move events, which each contain some stuff we want. In our case we want to store the mouse positions, which can be retrieved from the move event that we pass to our function.

OK, so now we want to do something with the drag events containing mouse positions; we want to send them to Firebase. First we will set up a Firebase reference

var _ref = new Firebase('https://conseal.firebaseio.com/scribble');

and now we will subscribe to the mouse drag Observable and push points to the Firebase

mouseDrags.subscribe(function (drags) {
    $colour = $('#colour').val();
    $colour = $colour ? $colour : '#df4b26';
    var _dragref = _ref.push({colour: $colour});
    drags.subscribe(function (move) {
        _dragref.ref().child('points').push({x: move.x, y: move.y});
    });
});

Here we store the current colour from the view in a new line element by pushing a new object to the Firebase each time we get a drag event. Then we subscribe to the move events and push each move position that we stored in the Observable earlier to a child of the new object we just created. We then end up with something like this on Firebase

{
    "conseal": {
        "scribble": {
            "-Jc2M-VOB-iH5s7sbKvg": {
                "colour": 'black',
                "points": {
                    "-Jc2Q8N7sBLhOB3pzWzF" : {
                        "x" : 226,
                        "y" : 293
                    },
                    "-Jc2Q8Ncwp_O_3zyQXXZ" : {
                        "x" : 227,
                        "y" : 293
                    },
                    "-Jc2Q8O-9DCgdE8Xdspx" : {
                        "x" : 228,
                        "y" : 293
                    }
                }
            }
        }
    }
}

So now we want to draw our points on the canvas by responding to Firebase events and drawing the points we get from there, so that any user’s points will be retrieved and drawn.
I decided to use a simple helper library called rx.firebase.js, which allows us to treat Firebase as RxJS Observables.

We then register a subscriber that picks up when a new line is drawn, i.e. a child is added to the “scribble” location. We then want to register another subscriber to get added points. When a new point is added, we recover the parent node to get the colour and draw a line between the previous node and the current node. Doing it this way allows the code to respond to events and draw them in context without any reference to state. This means that multiple users can draw at the same time in different colours and the program will handle them all independently.

Below is the full javascript code used.

(function (window, undefined) {
    // Calculate offset either layerX/Y or offsetX/Y
    function getOffset(event) {
        return {
            x: event.offsetX === undefined ? event.layerX : event.offsetX,
            y: event.offsetY === undefined ? event.layerY : event.offsetY
        };
    }

    function main() {
        var _ref = new Firebase('https://conseal.firebaseio.com/scribble');
        var canvas = document.getElementById('tutorial');

        if (canvas.getContext) {
            var ctx = canvas.getContext('2d');
            ctx.lineWidth = 3;

            var mouseDowns  = Rx.Observable.fromEvent(canvas, 'mousedown');
            var mouseUps    = Rx.Observable.fromEvent(document, 'mouseup');
            var mouseMoves  = Rx.Observable.fromEvent(canvas, 'mousemove');
            var clearButton = Rx.Observable.fromEvent($('#clear'), 'click');

            var mouseDrags = mouseDowns.select(function (downEvent) {
                return mouseMoves.takeUntil(mouseUps).select(function (drag) {
                    return getOffset(drag);
                });
            });

            // UI EVENTS
            mouseDrags.subscribe(function (drags) {
                $colour = $('#colour').val();
                $colour = $colour ? $colour : '#df4b26';
                var _dragref = _ref.push({colour: $colour});
                drags.subscribe(function (move) {
                    _dragref.ref().child('points').push({x: move.x, y: move.y});
                });
            });

            clearButton.subscribe(function () {
                _ref.remove();
            });

            // DRAWING CODE - called from Firebase event
            var drawLine = function (data) {
                // get current point
                var coordsTo = data.snapshot.val();
                // get colour
                data.snapshot.ref().parent().parent().child('colour')
                .once('value', function (snap) {
                    var colour = snap.val();
                    // get previous point
                    data.snapshot.ref().parent().child(data.prevName)
                    .once('value', function (snap) {
                        var coordsFrom = snap.val();
                        ctx.beginPath();
                        ctx.strokeStyle = colour;
                        ctx.moveTo(coordsFrom.x, coordsFrom.y);
                        ctx.lineTo(coordsTo.x, coordsTo.y);
                        ctx.stroke();
                    });
                });
            };

            // FIRBASE EVENTS
            _ref.observe('child_added')
            .subscribe(function (newLine) {
                newLine.snapshot.child('points').ref()
                .observe('child_added')
                .filter(function (data) { return data.prevName !== null;})
                .subscribe(drawLine);
            });

            _ref.on('child_removed', function (snap) {
                canvas.width = canvas.width;
                ctx.lineWidth = 3;
            });
        }
    }

    main();
}(window));

Written with StackEdit.

No comments: