Skip to content
Ryan Neufeld edited this page Jul 9, 2013 · 8 revisions

To follow along with this section, start with tag v2.0.12.

The focus of this tutorial is on the client side of a Pedestal application. To make this application work, it will need to have a service which will allow counters to be shared between clients.

This tutorial will not go into much detail about Pedestal services other than to show the steps for creating a new one. The main goal is to show how services interact with Pedestal clients and how to organize and integrate client and service code.

The following concepts will be introduced in this section:

  • Creating a Pedestal service
  • Using Server Sent Events
  • Testing a service with curl

Creating a service project

The tutorial service will be created as a new project. In the beginning of the tutorial you created the directory named pedestal-app-tutorial and then created the tutorial-client project. The tutorial-service project should be created right next to tutorial-client. In the pedestal-app-tutorial directory run:

lein new pedestal-service tutorial-service

This will create a working service. By default the service will print a lot of messages to the console when you run it. To fix this, edit the file config/logback.xml, changing the log level to WARN for the STDOUT appender.

<level>WARN</level>

If you would like to try out the service before you make changes to it, run

lein repl

and then

(use 'dev)
(start)

and then visit http://localhost:8080.

Add Support for Server Sent Events

All of the changes that you will make to the service will be made in the tutorial-service.service namespace. The goal of these changes is to add support for Server Sent Events and to support the kinds of messages that the application will both send and have pushed to it.

Before you make any changes, add the following required namespaces.

[io.pedestal.service.http.sse :as sse]
[io.pedestal.service.log :as log]
[io.pedestal.service.http.ring-middlewares :as middlewares]
[io.pedestal.service.interceptor :refer [definterceptor]]
[ring.middleware.session.cookie :as cookie]

Next, add the following code to the top of the source in the service namespace. Make sure to add this code before defroutes.

(def ^:private streaming-contexts (atom {}))

(defn- session-from-context
  "Extract the session id from the streaming context."
  [streaming-context]
  (get-in streaming-context [:request :cookies "client-id" :value]))

(defn- session-from-request
  "Extract the session id from a request."
  [request]
  (get-in request [:cookies "client-id" :value]))

(defn- clean-up
  "Remove the given streaming context and shutdown the event stream."
  [streaming-context]
  (swap! streaming-contexts dissoc (session-from-context streaming-context))
  (sse/end-event-stream streaming-context))

(defn- notify
  "Send event-data to the connected client."
  [session-id event-name event-data]
  (when-let [streaming-context (get @streaming-contexts session-id)]
    (try
      (sse/send-event streaming-context event-name event-data)
      (catch java.io.IOException ioe
        (clean-up streaming-context)))))

(defn- notify-all-others
  "Send event-data to all connected channels except for the given session-id."
  [sending-session-id event-name event-data]
  (doseq [session-id (keys @streaming-contexts)]
    (when (not= session-id sending-session-id)
      (notify session-id event-name event-data))))

(defn- store-streaming-context [streaming-context]
  (let [session-id (session-from-context streaming-context)]
    (swap! streaming-contexts assoc session-id streaming-context)))

(defn- session-id [] (.toString (java.util.UUID/randomUUID)))

(declare url-for)

(defn subscribe
  "Assign a session cookie to this request if one does not
  exist. Redirect to the events channel."
  [request]
  (let [session-id (or (session-from-request request)
                       (session-id))
        cookie {:client-id {:value session-id :path "/"}}]
    (-> (ring-resp/redirect (url-for ::events))
        (update-in [:cookies] merge cookie))))

(definterceptor session-interceptor
  (middlewares/session {:store (cookie/cookie-store)}))

(defn publish
  "Publish a message to all other connected clients."
  [{msg-data :edn-params :as request}]
  (log/info :message "received message"
            :request request
            :msg-data msg-data)
  (let [session-id (or (session-from-request request)
                       (session-id))]
    (notify-all-others session-id
                       "msg"
                       (pr-str (update-in msg-data
                                          [:io.pedestal.app.messages/topic]
                                          conj
                                          (subs session-id 0 8)))))
  (ring-resp/response ""))

That is a lot of code. The two most important functions are publish and subscribe. When the application starts, the client will subscribe. Each time the local counter is updated it will be published to the server.

The subscribe code will create a new session id if one does not exist and will then redirect the request to the ::events endpoint. The events endpoint is the Server Sent Events channel which can send and receive messages to and from the client.

The publish function will receive a message from a client and add the first 8 characters of the session id to the :io.pedestal.app.messages/topic value. This will serve as the client id. It will then forward that message to all of the other connected sessions.

All of the other code is the standard SSE code that is documented elsewhere.

With the addition of this code, you also need to modify the routes to properly route requests to these functions.

(defroutes routes
  [[["/" {:get home-page}
     ;; Set default interceptors for /about and any other paths under /
     ^:interceptors [(body-params/body-params) bootstrap/html-body session-interceptor]
     ["/about" {:get about-page}]
     ["/msgs" {:get subscribe :post publish}
      ["/events" {:get [::events (sse/start-event-stream store-streaming-context)]}]]]]])

This change adds the session-interceptor to the list of interceptors as well as adding handlers for /msgs and msgs/events. GET requests to /msgs will be routed to the subscribe function and POST requests to /msgs will be routed to the publish function.

With these changes in place, you can now test the service with curl.

Start the service

First, start the service, as described above.

lein repl
(use 'dev)
(start)

Testing the service with curl

To perform this test, you will need two terminal windows. One to act as the consumer of Server Sent Events and the other to send messages. As messages are sent, you should see them appear in the consumer.

Execute the command below in one terminal to establish a connection which will listen for events.

curl -L http://localhost:8080/msgs

In another terminal, execute the command below to send a message to be published.

curl \
 --data \
 "{:io.pedestal.app.messages/topic [:other-counters] \
   :io.pedestal.app.messages/type :swap \
   :value 42}" \
 --header "Content-Type:application/edn" \
 http://localhost:8080/msgs

In the first terminal, the following text should be printed.

event: msg
data:{:io.pedestal.app.messages/topic [:other-counters "b253ba47"],
      :io.pedestal.app.messages/type :swap,
      :value 42}

You now have a working service.

The Pedestal philosophy is to keep service and application projects separate and to test them independently. Including service and application code is a single project can lead to unnecessary coupling which makes it harder to understand how the whole system works.

Next steps

There is one remaining section in Part 1 of this tutorial. In this section you will connect the client application to this service and the result will be a working interactive application.

The tag for this section is v2.0.13.

Home | Connecting to the Service

Clone this wiki locally