com.github.jlangch.venice.server-side-events.venice Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of venice Show documentation
Show all versions of venice Show documentation
Venice, a sandboxed Lisp implemented in Java.
;;;; __ __ _
;;;; \ \ / /__ _ __ (_) ___ ___
;;;; \ \/ / _ \ '_ \| |/ __/ _ \
;;;; \ / __/ | | | | (_| __/
;;;; \/ \___|_| |_|_|\___\___|
;;;;
;;;;
;;;; Copyright 2017-2024 Venice
;;;;
;;;; Licensed under the Apache License, Version 2.0 (the "License");
;;;; you may not use this file except in compliance with the License.
;;;; You may obtain a copy of the License at
;;;;
;;;; http://www.apache.org/licenses/LICENSE-2.0
;;;;
;;;; Unless required by applicable law or agreed to in writing, software
;;;; distributed under the License is distributed on an "AS IS" BASIS,
;;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;;;; See the License for the specific language governing permissions and
;;;; limitations under the License.
;;;; Venice server side events (SSE)
;;;; see https://html.spec.whatwg.org/multipage/server-sent-events.html
(ns server-side-events)
(import :java.io.BufferedReader)
(defn
^{ :arglists '("(render event)")
:doc """
Renders a server side event to a string.
Returns the event as string or `nil` if the event is `nil` or all its
fields are empty or `nil`,
Note: SSE is restricted to transporting UTF-8 messages.
The event is a map. E.g. :
```
{ :id "1"
:event "score"
:data [ "GOAL Liverpool 1 - 1 Arsenal"
"GOAL Manchester United 3 - 3 Manchester City" ] }
```
with the text representation
```
id: 1\\n
event: score\\n
data: GOAL Liverpool 1 - 1 Arsenal\\n
data: GOAL Manchester United 3 - 3 Manchester City\\n\\n
```
The event fields :id, :event, and :data must not contain newline,
carriage return, backspace, or formfeed characters!
A HTTP request to initiate SSE streaming looks like:
```
GET /api/v1/live-scores
Accept: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
```
"""
:examples '(
"""
(do
(load-module :server-side-events ['server-side-events :as 'sse])
(sse/render { :id "100"
:event "scores"
:data ["100" "200"] } ))
""" )
:see-also '(
"server-side-events/parse" ) }
render [event]
(assert (map? event))
(assert (or (nil? (:id event)) (string? (:id event)) (long? (:id event))))
(assert (or (nil? (:event event)) (string? (:event event))))
(assert (or (nil? (:data event))
(and (sequential? (:data event)) (every? string? (:data event)))))
(validate event)
(try-with [sw (io/string-writer)]
(when-let [s (str/trim-to-nil (str (:id event)))]
(print sw (str "id: " s "\r\n")))
(when-let [s (str/trim-to-nil (:event event))]
(print sw (str "event: " s "\r\n")))
(when-let [data (:data event)]
(->> (map str/trim-to-nil data)
(filter some?)
(map #(str "data: " %))
(str/join "\r\n")
(print sw)))
(if-let [s @sw]
(str s "\r\n\r\n")
nil)))
(defn
^{ :arglists '("(parse s)")
:doc """
Parses a server side event in string representation to a map.
"""
:examples '(
"""
(do
(load-module :server-side-events ['server-side-events :as 'sse])
(-> (sse/render { :id "100"
:event "scores"
:data ["100" "200"] } )
(sse/parse)))
""" )
:see-also '(
"server-side-events/render" ) }
parse [s]
(assert (or (nil? s) (string? s)))
(let [valid-field-names #{"id" "event" "data" "retry"}
lines (->> (str/split-lines s)
(filter #(not (str/starts-with? % ":"))))]
(loop [lines lines, data {}]
(if (empty? lines)
data
(let [line (first lines)
[name value] (map str/trim (str/split line ":" 2))]
(if (contains? valid-field-names name)
(if (= name "data")
(let [items (conj (:data data []) value)]
(recur (rest lines) (assoc data :data items)))
(recur (rest lines)
(assoc data (keyword name) value)))
(recur (rest lines) data)))))))
(defn
^{ :arglists '(
"(read-event rd)")
:doc """
Read a single event from a :java.io.BufferedReader.
Returns the event or `nil` if the underlying stream has been closed.
"""
:examples '(
"""
(do
(load-module :server-side-events ['server-side-events :as 'sse])
(defn sample-events []
(str (sse/render { :id "100" :event "scores" :data ["100"] } )
(sse/render { :id "101" :event "scores" :data ["101"] } )
(sse/render { :id "102" :event "scores" :data ["102"] } )))
(try-with [is (io/string-in-stream (sample-events))
rd (io/wrap-is-with-buffered-reader is :utf-8)]
(sse/read-event rd)))
""" )
:see-also '(
"server-side-events/read-events" ) }
read-event [rd]
(assert (io/reader? rd))
;; read the response line by line
(try
(loop [line (read-line rd) lines []]
(if (nil? line)
nil ;; end of stream, no complete event availale
(if (empty? line)
(parse (str/join "\r\n" lines)) ;; return the event
(recur (read-line rd) (conj lines line))))) ;; capture next event line
(catch [:cause-type :java.io.IOException] e
;; underlying stream error/closed -> return nil
nil)))
(defn
^{ :arglists '(
"(read-events rd limit)")
:doc """
Reads multiple events from a :java.io.BufferedReader.
Returns a list of events. Stops reading events if the limit is reached
or the underlying stream has been closed.
"""
:examples '(
"""
(do
(load-module :server-side-events ['server-side-events :as 'sse])
(defn sample-events []
(str (sse/render { :id "100" :event "scores" :data ["100"] } )
(sse/render { :id "101" :event "scores" :data ["101"] } )
(sse/render { :id "102" :event "scores" :data ["102"] } )
(sse/render { :id "103" :event "scores" :data ["103"] } )))
(try-with [is (io/string-in-stream (sample-events))
rd (io/wrap-is-with-buffered-reader is :utf-8)]
(sse/read-events rd 3)))
""" )
:see-also '(
"server-side-events/read-event" ) }
read-events [rd limit]
(assert (io/reader? rd))
(assert (long? limit))
(assert (pos? limit))
(loop [event (read-event rd) events []]
(if event
(let [events (conj events event)]
(if (>= (count events) limit)
events ;; return the events, limit
(recur (read-event rd) events))) ;; parse next event
events))) ;; return the events, stream closed
(defn- validate [event]
(assert (some? event))
(when (contains-invalid-chars? (:id event))
(throw (ex :AssertionException (make-validation-err-ms ":id"))))
(when (contains-invalid-chars? (:event event))
(throw (ex :AssertionException (make-validation-err-ms ":event"))))
(doseq [d (or (:data event) [])]
(when (contains-invalid-chars? d)
(throw (ex :AssertionException (make-validation-err-ms ":data"))))))
(defn- contains-invalid-chars? [value]
(and (some? value)
(some? (str/index-of-char value [#\newline #\return]))))
(defn- make-validation-err-msg [field]
(str/format """
The event field %s must not contain newline or carriage return \
characters!
"""
field))