Skip to content

Lattice_Connected API

jasonl99 edited this page Feb 3, 2017 · 2 revisions

Lattice::Connected

The Lattice::Connected module is made of two classes (WebSocket and WebObject) that communicate events between one or more objects on a web page,. and the server instance of that connected object. This connectivity is set up in the HTML of the page through data- attributes, and some javascript glue to tie it all together. For example, the card_game sets up a connection using a slang template, very similar to Ruby's slim:

div data-item=dom_id data-subscribe=""
  div#hand
    - hand.each_with_index do |card,idx|
      span.card-holder
        img.card src=card_image(card) data-item="#{dom_id}-card-#{idx}" data-event-attributes="src" data-events="click,mouseenter,mouseleave"
  div#chat-room
    == chat_room.content
  div#stats
    span.game-name = "game: #{name}"
    span.cards-remaining
      | Cards Remaining in deck: 
      span data-item="#{dom_id}-cards-remaining"
        = deck.size

There are currently for data- attributes used to establish communcation between the objects:

  • data-subscribe : Subscribes to events emitted from the server for the dom element with the given data-item attribute
  • data-item : a server-unique id for this item (different from the dom id) that is sent back in any events as the source, and send to the client as a target.
  • data-events : the type of events that are to send back to the server as they occur.
  • data-event-attributes : The html attributes (for example, id, class, src) to include as parameters sent back to the server when events occur.

There are also two different types of messages:

  • Those coming from the browser going to the server (set up in app.js through handleEvent )
  • Those coming from the server going to the browser (reactions to events received in WebObject#subscriber_action)

Each side of the equation (server, browser) has it's own API since the types of messages serve completely different purposes. Somewhat arbitrarily, let's call these ClientServer and ServerClient APIs, so we know which direction they messages are going.

data-subscribe

The card_game's slang starts with the following line

div data-item=dom_id data-subscribe=""

This instructs the associated javascript to send a subscribe action over the socket, sending the instantiated card game's dom_id (for example, cardgame-1928592839829) as the target.

A quick aside, the need to put an empty string in data-subscribe is more an anachronism of the templating language, if this were pure html, it would be perfectly fine to have the element as <div data-item="card-game 1928592839829" data-subscribe>

Here's what an example socket message would look like as a result of this slang file. Since this is coming from the browser, and going to the server, this would be a ClientServer message.

{
  "cardgame-93972704197200": {
   "action":"subscribe",
   "params":
        {"session_id":"d1a602d22520ce3308427eee55376461"}
   }
}

The ClientServer JSON must be structured as follows:

A single key, which is the data-item of the element that created the event

The value, which is a JSON object, has two keys: action & params. The action is always a string, the params are a JSON object with string keys and any value needed.

At the server, the message is received in the Connected::WebSocket class in on_message, where it is validated by #validate_message. The id cardgame-93972704197200 id parsed and used to find an instantiated object (currently, the id being used for cardgames is just the object_id)

WebSocket#validate_message returns everything it has processed, including a reference to the object's instance, the session_id of the user whose socket sent the message, and the parameters sent as part of the incoming message.

If the message is a subscribe action, WebSocket adds a REGISTERED_SESSIONS item (which keeps track of which session_id belongs to each socket), and it calls the identified object's subscribe which performs the following tasks:

  • Adds the socket's object_id to @subscribers property
  • Calls the identified object's subscribed method with the session_id and socket.

In the card_game demo, the subscribed method is used add the new player to the chat_room that's already part of the CardGame instance:

class CardGame
  property chat_room = ChatRoom.new(name: dom_id)  # creates a chat room named "cardgame-93972704197200"
  #... other code
  def subscribed( session_id, socket)
    chat_room.subscribe(socket, session_id)
  end
end

In summary, a subscription associates individual sockets from individual browsers to individual WebObject instances.

Why make this sort of association? Because now when the server instances changes (someone drew a card), we can send messages back to every browser that has this object loaded - our instance has a direct line to each of them via the WebSocket.

Subscribing, by itself, does nothing to to the dom; it exists simply to notify the server that we want messages back about changes to that particular object. There is also no heirarchy to subscriptions - although the DOM may have a well-defined hierarchy (a game has hands which have cards), messages have no such hierarchy. A single socket receives all messages for all subscribed objects. Since incoming messages contain the data-item identifier, we can easily determine which item is the target of the incoming message and act upon it directly.

data-item

This attribute is intended to work as an id for this node, with the id being something unique to the server (a particular cardgame, a particular chatroom, etc). The WebSocket and WebObject classes must be able to use this id to find an already-instantiated object, or failing to find one, optionally instantiate one from the id (think order ID where the order number is looked up from the data-item id). This object search occurs in WebSocket#validate_message.

data-events

These are the types of events to track on the client browser. The event types are specific to Lattice::Connect, and not javascript event types, but the intent is to keep the names as close as possible to their javascript counterparts. For example, click, submit, input are all events that can be used with addEventListener so Lattice has these same events.

Each event we define is mapped to a corresponding handler in the handleEvent function in app.js, with behavior specific to that event type. For example, a "submit" event prevents default actions, gathers up all the input elements into a JSON object, and sends it as params for the outgoing message, then resets the form. On the other hand, a click simply sends a click event and does nothing else. Here's the specific app.js code to set up handling of each of these events:

function handleEvent(event_type, el, socket) {
  switch (event_type) {
    case "click":
      el.addEventListener("click", function(evt) {
        msg = baseEvent(evt,"click")
        sendEvent(msg,socket)
        break; })
    case "submit":
      el.addEventListener("submit", function(evt) {
        evt.preventDefault();
        evt.stopPropagation();
        msg = baseEvent(evt, "submit", formToJSON(el))
        sendEvent(msg,socket)
        el.reset();
        break; })
    // ...other events
   }
 }

There are several examples in the card_game demo that show this. Let's look at the slang for an idividual card (I've moved this across several lines for ease of commenting, so this isn't actually valid slang syntax)

img.card src=card_image(card) 
  data-item="#{dom_id}-card-#{idx}" 
  data-event-attributes="src" 
  data-events="click,mouseenter,mouseleave"

In this example, we're tracking three different events: click, mouseenter, and mouseleave. So every time and user clicks, or mouses into our out of a card, we send a message back to the server over the socket.

We'll do a little preview of the another data- attribute: data-event-attributes copies each of the element's HTML attributes (as a comma-delimited list like data-events), and sends it back as part of the params ClientServer api. In fact, here's what the messages would look like as I mouse int the first card, click on it, and mouse out:

{"cardgame-93972704197200-card-0":
  {"action":"mouseenter",
   "params":{"src":"/images/10_of_diamonds.png"}
  }
}

{"cardgame-93972704197200-card-0":
  {"action":"click",
   "params":{"src":"/images/10_of_diamonds.png"}
  }
}

{"cardgame-93972704197200-card-0":
  {"action":"mouseleave",
   "params":{"src":"/images/king_of_diamonds.png"}
  }
}

It's a little much to wrap your head around at first - remember, while you're looking at events that happened on the browser, these events have been reveived at the server, sent to the correct card game, and that particular card game knows exactly which user sent that message.

You can also see, from params["src"], that after I clicked, I mousedout when the card's <img src> changed to the king_of_diamonds. So our click send a message to the server, which sent out a response to change the card's image, and another message was send back after that all occurred.

So suppose we had four players connected to this game when I clicked to draw that card. The other three players would also have received the message from the server to the browsers to change the card image. What does that message look like? It's the first ServerClient API message we've seen:

{
  "dom":{
    "id":"cardgame-93972704197200-card-0", 
    "attribute":"src",
    "value":"/images/king_of_diamonds.png",
    "action":"update_attribute",
    }
  }
}

Here's a bit of graphical representation of this occurring card_click

The dom key directs the associated javascript to perform an action on the id given (note that the id is the data-item of the element, not the dom id), and in this case, the action is update_attribute. The javascript code, in app.js for this particular change is pretty straightforward:

function handleSocketMessage(message) {
  payload = JSON.parse(message);
  if ("dom" in payload) {
    modifyDOM(payload.dom);
  }
}

// modify the dom based on the imformation contained in domData
function modifyDOM(domData) {
  if (matches = document.querySelectorAll("[data-item='" + domData.id + "']" )) {
    for (var i=0; i<matches.length; i++) {
      el = matches[i];
      switch (domData.action) {
        case "update_attribute":
          el.setAttribute(domData.attribute, domData.value)
          break;
     // ... other case statements
     }
   }
 }

This message is sent to all subscribers (players) to the object, meaning that all for players' card changed visually as soon as one user clicked. There is very little overhead, so the change is about as close to the best speed we can hope to accomplish since WebSockets are the fastest, most efficient communication between server & browser, and we have no control over any other potentially limiting factor (network latency and computing power of the browser and server). The API messages could arguably be made more concise, but that's a tiny in comparison to network latency.

Clone this wiki locally