All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jlangch.venice.server-side-events.venice Maven / Gradle / Ivy

There is a newer version: 1.12.34
Show newest version
;;;;   __    __         _
;;;;   \ \  / /__ _ __ (_) ___ ___
;;;;    \ \/ / _ \ '_ \| |/ __/ _ \
;;;;     \  /  __/ | | | | (_|  __/
;;;;      \/ \___|_| |_|_|\___\___|
;;;;
;;;;
;;;; 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))




© 2015 - 2024 Weber Informatics LLC | Privacy Policy