-
Notifications
You must be signed in to change notification settings - Fork 0
Making the Service
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 we 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 console when we 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 we make changes to it, run
lein repl
and then
(use 'dev)
(start)and then visit http://localhost:8080.
All of the changes that we 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 our application both send and have pushed to it.
Before we 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 top-level functions are publish and
subscribe. When we start our application, we will subscribe. Each
time we update our counter 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 from the client.
The publish code will receive a message from a client, add the first
8 characters of the session id to the
:io.pedestal.app.messages/topic value in the messages 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 this addition of this code, we 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)]}]]]]])We add 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, we can now test our new service with curl.
First, start the service, as described above.
lein repl
(use 'dev)
(start)To perform our test, we 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, we should see them appear in the consumer.
Execute this command in one terminal to establish a connection which will listen for events
curl -L http://localhost:8080/msgsIn another terminal, execute this command 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}We now have a working service.
In the next section we will connect the client application and this service to create a working interactive application.
The tag for this step is v0.0.11.