-
Notifications
You must be signed in to change notification settings - Fork 0
Making the Service
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
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.
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.
First, start the service, as described above.
lein repl
(use 'dev)
(start)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/msgsIn 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/msgsIn 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.
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.