Symple

Repositories
Node.js Server: https://github.com/sourcey/symple-server-node
JavaScript Client: https://github.com/sourcey/symple-client
Ruby Client: https://github.com/sourcey/symple-client-ruby
C++ Client: https://github.com/sourcey/libsourcey
Licence
LGPL, Pro Licence

Symple Logo Symple is a messaging, presence and remoting protocol with implementations in C++ and JavaScript for fast real-time communication between native desktop and browser applications.

Symple is an unrestrictive peer messaging and presence protocol, as the name would suggest, it’s a minimal and flexible protocol that is well suited to a “do-it-yourself” developer mentality.

The message format is semantically similar to XMPP, except that it uses a vastly simplified syntax, and it uses JSON instead of XML for encoding messages. Symple has built-in support signalling support for messages, commands, forms and presence.

The flexible nature of the JSON format also make it great for remoting and serializing data structures such as nested polymorphic objects.

Client implementations

There are currently three Symple client implementations, one in Ruby, one in JavaScript, and one in C++.

The Ruby client is a single drop-in module that is perfect for server-side integration with a web framework like Rails or Sinatra.

The JavaScript client provides a flexible interface for receiving standard and arbitrary message types. The client also includes a web player interface which enables Symple to be used in conjunction with MJPEG, FLV, h264 and WebRTC for live video streaming.

LibSourcey also contains includes a native C++ client and including samples for using Symple in native applications.

Server implementations

The Symple node.js server repository includes sample server is implemented on a node.js stack.

The server supports SSL configuration, groups, users, x-real-ip detection, and pre-arranged sessions using short-term authentication tokens via Redis.

Why Symple

If you’ve had the pleasure of building a system to complex specifications then no doubt you’ll be familiar with the bloat indicative of such systems.

In some cases it’s a necessity, but when it comes to a task like messaging, we prefer freedom of implementation rather than incumbent structure.

Symple was created as a result of our work with Anionu, for which we require real-time messaging, presence and command sharing between the native Spot client and online dashboard. Beyond that we hope to see Symple continue to adapt and develop as a great open source messaging solution.

Protocol Specifications

Below are the protocol specifications for Symple’s core message types.

Address

A Symple ID is the destination address of a group user or peer node on the server. It consists of 3 namespace segments, each segment that is included in a message’s destination address refines the delivery scope for that message. The purpose of namespacing peer addresses is so peers can be privately grouped.

Group
Each peer on the Symple server belongs to a group namespace. A message directed to the group namespace will be dispatched to all nodes belonging to that group.

Example: { to: "group" }

User
Each peer on the Symple sever has an associated user name for the purposes of authentication and node grouping. A message directed to the user namespace will be dispatched to nodes belonging to that user. When directing a message to a user namespace, the group namespace MUST be included beforehand.

Example: { to: "[email protected]" }

Session ID
Each peer on the Symple sever has a session ID. The session ID acts is a unique identifier for each peer session. Messages may be directed to a specific peer by including the node ID in the destination address. When directing a message to a node ID, the node’s group and user namespaces MUST be included beforehand.

Example: { to: "[email protected]/sessionid" }

Message

A symple message is a JSON object which acts as a container for data that is sent over the wire. There are 3 required message attributes:

{
  "to": "[email protected]/sessionid",      // Recipient @see Symple ID (may be full or partial ID)
  "from": "[email protected]/sessionid",       // Sender @see Symple ID (always full ID)
  "type": "message",                      // The type of message being sent, maybe a polymorphic type.
}

Other frequently used but optional attributes include:

{
  "data": {},                             // We recommend placing arbitrary message data inside a "data" attribute, but this is optional.  
  "notes": [                              // Notes can be added to messages like so.
    {
      "type": "info",                     // Note types: "info", "warn", "error"
      "text": "ummm..."
    }
}          

From these meagre beginnings we can rapidly develop just about any kind of messaging system we want. As long as this simple message structure is adhered to data can be broadcast between scoped peers.

Command

Command messages allow us to send arbitrary commands peer. They are suited for sending either short or long running commands, and allow for status callbacks. A command request looks like this:

{
  "to": "responder",                      // Responder
  "from": "initiator",                    // Initiator
  "type": "command",                      // Message type
  "node": "some:node",                    // The target command node or namespace
  "action": "execute"                     // An optional command action
}

A command response looks like this:

{
  "to": "initiator",                      // Initiator
  "from": "responder",                    // Responder
  "type": "command",
  "node": "some:node",
  "status": 200                           // HTTP status codes are employed to describe the status of the command.
}                                         // HTTP codes are generally 200, 202, 404, 406, 500 *

Command status callbacks for long running commands should use status 202 (Accepted) and 406 (Not accepted) to describe the command’s running status. All other response status codes will result in completion and termination of the command session.

Event

Events allow peers to broadcast information about their running state to other peers.

{
  "to": "recipient",                      // Recipient
  "from": "sender",                       // Sender
  "type": "event",
  "node": "some:node"                     // The target event node
}

Form

Forms allow for peers to engage in configuration sessions. The forms data structure is designed to be easily interpreted by GUI renderers. Our JavaScript library includes support for displaying Symple forms as HTML. Forms objects generally piggyback on command messages. A basic form session is illustrated below:

A form session is requested by the initiator:

{
  "to": "responder",                      // Responder
  "from": "initiator",                    // Initiator
  "type": "command"
  "node": "some:node:config"
}

The responder returns an empty form.

{
  "to": "initiator",                      // Initiator
  "from": "responder",                    // Responder
  "type": "command",
  "node": "some:node:config",
  "status": 202,                          // Status 202 indicates the session is in progress
  "form": {
    "type": "form",                       // Type "forms" identifies the object 
    "action": "form",                     // Actions: "form", "submit", "result"
    "elements": [                         // Forms support nested element arrays
      {
        "type": "page",                   // Form fields optionally belong to a "page" or "section" element 
        "label": "page 1",                // Every element has a label
        "elements": [
          {
            "id": "field1",               // Uniquely identified this form field (like HTML's "name" attribute)
            "type": "text",               // Form field types are as follows: 
            "label": "field 1",           //  "text", "text-multi", "list", "list-multi", "boolean", "media", "custom"              
            "options": {                                            
              "key1": "value1",           // The options hash holds key value data for "list" and "list-multi" types.             
              "key2": "value2",        
              "key3": "value3",                
            }
          }
        ],
        "values": [ "key1" ]              // The values array include the current value(s) for this field.
      }
    ]
  }
}

The initiator receives and displays the form the responder. Once complete and the form data is updated an submit back to responder for processing and validation. Note the only fields that change here are the form’s “type” attribute which is set to “submit”, status code is removed, and the field value now equals “key2”.

{
  "to": "responder",                      // Responder
  "from": "initiator",                    // Initiator
  "type": "command",
  "node": "some:node:config",
  "status": 202,                          // Status 202 indicates the session is in progress
  "form": {
    "type": "form",
    "action": "submit",
    "elements": [
      {
        "type": "page",
        "label": "page 1",
        "elements": [
          {
            "id": "field1",
            "type": "text",
            "label": "field 1",
            "options": {                                            
              "key1": "value1",
              "key2": "value2",
              "key3": "value3",
            }
          }
        ],
        "values": [ "key2" ]              // The values now equals "key2".
      }
    ]
  }
}

The responder receives and and processes the form data. If any errors are encountered during validation, an error message may added to the field and form returned to the requester for review.

For the purpose of our example validation failed and case the responder was unable to set the value to key2

{
  "to": "initiator",                      // Initiator
  "from": "responder",                    // Responder
  "type": "command",
  "node": "some:node:config",
  "status": 202,                          // Status 202 indicates the session is in progress
  "form": {
    "type": "form",
    "action": "result",
    "elements": [
      {
        "type": "page",
        "label": "page 1",
        "elements": [
          {
            "id": "field1",
            "type": "page",
            "label": "text",
            "error": "key2 is invalid",   // An error message described the problem 
            "options": {                                            
              "key1": "value1",
              "key2": "value2",     
              "key3": "value3",                
            }
          }
        ],
        "values": [ "key1" ]              // The values now equals "key1" again since validation failed.
      }
    ]
  }
}

The requester receives the form and displays the errors to the user. Once rectified the user submits the form again, this time selecting “key3”.

{
  "to": "responder",                      // Responder
  "from": "initiator",                    // Initiator
  "type": "command",
  "node": "some:node:config",
  "form": {
    "type": "form",
    "action": "submit",
    "elements": [
      {
        "type": "page",
        "label": "page 1",
        "elements": [
          {
            "id": "field1",
            "type": "page",
            "label": "text",
            "options": {                                            
              "key1": "value1",
              "key2": "value2",
              "key3": "value3",                
            }
          }
        ],
        "values": [ "key3" ]              // The values now equals "key3"
      }
    ]
  }
}

Finally both peers have come to an agreement, the info is valid, contracts are signed, and the data is saved.

The responding peer terminates the session by saying thanks:

{
  "to": "initiator",                      // Initiator
  "from": "responder",                    // Responder
  "type": "command",
  "node": "some:node:config",
  "form": { ... }                         // Form data is included in the response with "result" action.
  "status": 200                           // Status 200 signifies the session has ended in success. 
  "notes": [                              // Status 404 or 500 would signify an unrecoverable session error.
    {
      "type": "info",                     
      "text": "Form saved successfully!"
    }
  ]
}

Contributing

  1. Fork it on Github
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Issues

If you come across any bugs or issues then please use the Github issue tracker.