    // [...] - boilerplate code to connect to Crossbar.io

    function main (session) {
       // subscribe to future vote event
       session.subscribe("http://crossbar.io/crossbar/demo/vote#onvote",
          function(topicUri, event) {
             document.getElementById("votes" + event.subject).value =
                event.votes;
          });

       // get the current vote count
       session.call("http://crossbar.io/crossbar/demo/vote#get").then(
          function(res){
             for(var i = 0; i < res.length; i++) {
                document.getElementById("votes" + res[i].subject).value =
                   res[i].votes;
             }
       }, ab.log);

       // wire up vote buttons
       var voteButtons = document.getElementById("voteContainer").
          getElementsByTagName("button");
       for(var i = 0; i < voteButtons.length; i++) {
          voteButtons[i].onclick = function(evt) {
             session.call("http://crossbar.io/crossbar/demo/vote#vote",
                   evt.target.id).then(ab.log, ab.log);
          }
       }

       // subscribe to vote reset event
       session.subscribe("http://crossbar.io/crossbar/demo/vote#onreset",
          function() {
             var voteCounters = document.getElementById("voteContainer").
                getElementsByTagName("input");
             for(var i = 0; i < voteCounters.length; i++) {
                voteCounters[i].value = 0;
             }
          });

       // wire up reset button
       document.getElementById("resetVotes").onclick = function() {
          session.call("http://crossbar.io/crossbar/demo/vote#reset").
             then(ab.log, ab.log);
       };
    }

    CREATE or REPLACE PACKAGE BODY votes_api
    AS

       FUNCTION get RETURN JSON_LIST
       AS
          l_res       SYS_REFCURSOR;
       BEGIN
          -- open cursor from query
          --
          OPEN l_res FOR
             SELECT subject AS "subject",
                    votes   AS "votes"
               FROM votes ORDER BY subject;

          -- transform result set into JSON list of object
          --
          RETURN json_dyn.executeList(l_res);
       END get;

       FUNCTION vote (p_subject VARCHAR2) RETURN NUMBER
       IS
          l_votes     NUMBER;
          l_res       JSON := JSON();
       BEGIN
          -- check args
          --
          IF p_subject NOT IN ('Banana', 'Lemon',
             'Chocolate') THEN
             -- raise custom exception, this gets transformed
             -- into a proper RPC error return
             --
             webmq.raise(BASEURI || 'invalid_argument',
                'No subject "' || p_subject || '" to vote on.');
          END IF;

          -- update votes, returing new count
          --
          UPDATE votes SET votes = votes + 1
             WHERE subject = p_subject
                RETURNING votes INTO l_votes;
          COMMIT;

          -- create and publish PubSub event
          --
          l_res.put('subject', p_subject);
          l_res.put('votes', l_votes);
          webmq.publish(BASEURI || 'onvote', l_res);

          RETURN l_votes;
       END vote;

       PROCEDURE reset
       AS
       BEGIN
          -- reset all votes
          --
          UPDATE votes SET votes = 0;
          COMMIT;

          -- publish event with no payload
          --
          webmq.publish(BASEURI || 'onreset');
       END reset;

    END;