Introduction

postal is a simple postal protocol client for ClojureScript.

Rationale

The typical web application usually follows the API REST architecture, but the main problem of that is that is bound directly to the HTTP semantics that are not always coherent or not always clear how to use.

Postal is a client library that implements the postal protocol. And allows consume APIs using rich and user defined messages on top of HTTP.

The general idea behind the protocol and this library was inspired by Facebook Relay and Netflix’s Falcor. The main differents with them is that this library only represents the transport layer, so it’s is nonobstructive and not coupled with the concrete framework.

Postal is intended for use with any postal protocol compliant servers. An exceptional example of where this library fits in is a transport layer for Om.Next.

Project Maturity

Since postal is a young project, there can be some API breakage.

Install

The simplest way to use postal in a Clojure project is by including it in the dependency vector on your project.clj file:

[funcool/postal "0.7.0"]

Getting Started

The postal interface is simple. The first step for use it, just define a client instance:

(require '[postal.client :as pc])

(def c (pc/client "http://yourser.ver/end/point"))

The client is stateless so that you can define it globally without any problem. It only stores the initial configuration.

By default, it uses "PUT" HTTP connection to send messages to backend. "PUT" is a good default value in the backend but if you feel otherwise you can change it using the second optional the parameter to the client function:

(def c (pc/client uri {:method :get}))

Using the "GET" method allows sending the messages using HTTP GET request using as well as a query string parameter. In some circumstances using GET (as opposed to PUT) is beneficial; for example, when making cross domain without specially setup CORS.

Additionally, you can setup initial HTTP headers to use, using the :headers option. And they can be updated or reset after client definition using postal.client/update-headers! or postal.client/reset-headers!.

And finally, just use it using the public API. It there an example performing a :query request:

(require '[promesa.core :as p])

(p/then (pc/query c :users {:id 1})
        (fn [{:keys [data] :as msg}]
          (println "User: " data)))

Notice that query function returns a promise that will be resolved with the response message or rejected with a message if an :error message is returned. The promise may also be rejected with an exception object if something foreign to transport happens (message serialization error as an example).

Additionally to query, there is also novelty function, that sends :novelty type frame in the same way as with query.

User Guide

User defined frames

At this moment, the postal library only implements query and novelty specialized functions for send corresponding kind of frames. Surely, you may want to use own messages if the provided are not enough.

For that purpose, postal library exposes a send! function that just exposes as a low level api to define your own helpers for send frames. Let us imagine you want to send a :take type message; to do so let’s define a helper that uses send! function for send that frame:

(defn take
  "Sends a query message to a server."
  ([client dest]
   (take client dest nil {}))
  ([client dest data]
   (take client dest data {}))
  ([client dest data opts]
   (send! client (merge {:type :query
                         :dest dest
                         :data data}
                        opts))))

Now, you can use the new defined take function in the same way as you will use the query and novelty functions.

Server Push

In some circumstances, the request/response patterns are not enough. Maybe you want to get notifications from the backend in a server-push manner or full bi-directional communication with the backend (as opposed to having the client pull as all of the examples previously demonstrate).

For this, a WebSocket approach is chosen because is more widely supported by browsers then Server-Sent-Events and supports bi-directional communication with the server.

The difference from plain WebSockets, you will be able to send more rich messages because behind the scenes, messages are serialized using transit serialization format. Additionally, handling socket reconnection automatic (via postal).

There two ways to open a WebSocket to the server, in a unidirectional or a bidirectional way. The unidirectional approach serves just for cases where you only need server push. The bi-directional option allows you to also send messages to the server using the web socket connection. This library exposes a simple API for both approaches using beicon library (wrapper over rxjs).

Let see how it can be used:

(require '[beicon.core :as s])

(def c (pc/client "http://yourser.ver/api"))
(def bus (pc/subscribe c :timeupdate {:some "additional data"}))
(def stream (s/throttle bus 1000)))

(s/on-value stream (fn [message]
                     (println "Received: " message)))

You can close a subscription just ending the message bus:

(s/end! bus)

calling beicon.core/end! will close the connection and the stream, removing all existing stream subscriptions.

Bi-directional sockets with server

You also can open a bi-directional socket to the backend using socket function:

(require '[beicon.core :as s])

(def c (pc/client "http://yourser.ver/api"))

(let [[instream outbus] (pc/socket c :timeupdate)]
  (def in instream)
  (def out outbus))

;; Send messages to the backend
;; Literally can be any kind of messages that
;; can be encoded using transit-clj.
(s/push! out {:type :message :data "Hello"})

(def stream (s/throttle in 1000)))
(s/on-value stream (fn [message]
                     (println "Received: " message)))

And you can close the socket just ending the output bus with s/end! function.

FAQ

Is it a replacement for WebSockets?

In general NO. This library/protocol does not intend to replace any existing bi-directional protocols/messaging-systems. In fact, it lives together with WebSockets.

With the upcoming http2 and already existing spdy, most of the performance problems of the http1.x are solved. So, the majority of the standard use of WebSockets can be easily solved using HTTP (http2/spdy).

Should I use HTTP2/SPDY?

No, but is highly recommended.

At this moment, it is not necessary that your server has http2/spdy support. Putting your application behind an HTTP proxy like nginx that already supports SPDY and http2 support is an option.

http2/spdy offers connection multiplexing allowing use one unique persistent connection handle all required context, eliminating the overhead of creating and destroying connections. With that, you can make multiple and repeated HTTP connections without performance issues.

Is there any way to handle auth with sockets?

The downside of using WebSocket (like as with EventSource) is that its API does not allow append additional headers. The API is very limited for that. So you have two ways to do it:

  1. Using the implicit authentication using cookies; that only works if your endpoint is in the same domain (no cookies send in a cross domain request).

  2. Using a ticket based communication with single-use tokens (just make a query request for obtaining a token and later making the subscription request passing the token using and additional query parameter:

(def sub (pc/subscribe :timeupdate nil {:params {:token my-token}}))

Is there any server supports that protocol?

At this moment, the unique backend/server implementation for this library is catacumba. And you can find the related documentation for the backend handlers setup.

Developers Guide

Philosophy

Five most important rules:

  • Beautiful is better than ugly.

  • Explicit is better than implicit.

  • Simple is better than complex.

  • Complex is better than complicated.

  • Readability counts.

All contributions to postal should keep these important rules in mind.

Contributing

Unlike Clojure and other Clojure contributed libraries postal does not have many restrictions on contributions. Just open an issue or pull request.

Source Code

postal is open source and can be found on github.

You can clone the public repository with this command:

git clone https://github.com/funcool/postal

Run tests

For running tests just execute this:

./scripts/build
node ./out/tests.js

License

postal is under public domain:

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>