Add the ability to stream events on rpc methods

This commit is contained in:
Andrey Antukh 2023-12-01 16:18:39 +01:00 committed by Andrés Moya
parent f3e9efa6fe
commit 03518a8da1
21 changed files with 408 additions and 106 deletions

View file

@ -10,6 +10,7 @@
[app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.files.helpers :as cfh]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.uri :as u]
[app.common.uuid :as uuid]
@ -25,6 +26,7 @@
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.sse :as sse]
[app.util.time :as dt]
[app.util.timers :as tm]
[app.util.webapi :as wapi]
@ -32,6 +34,8 @@
[clojure.set :as set]
[potok.core :as ptk]))
(log/set-level! :warn)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -949,7 +953,17 @@
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :clone-template {:project-id project-id :template-id template-id})
(->> (rp/cmd! ::sse/clone-template {:project-id project-id
:template-id template-id})
(rx/tap (fn [event]
(let [payload (sse/get-payload event)
type (sse/get-type event)]
(if (= type "event")
(log/dbg :hint "clone-template: progress" :section (:section payload) :name (:name payload))
(log/dbg :hint "clone-template: end")))))
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/tap on-success)
(rx/catch on-error))))))

View file

@ -7,9 +7,11 @@
(ns app.main.repo
(:require
[app.common.data :as d]
[app.common.transit :as t]
[app.common.uri :as u]
[app.config :as cf]
[app.util.http :as http]
[app.util.sse :as sse]
[beicon.core :as rx]
[cuerdas.core :as str]))
@ -56,8 +58,14 @@
{:query-params [:file-id :revn]
:form-data? true}
::sse/clone-template
{:response-type ::sse/stream}
::sse/import-binfile
{:response-type ::sse/stream
:form-data? true}
:export-binfile {:response-type :blob}
:import-binfile {:form-data? true}
:retrieve-list-of-builtin-templates {:query-params :all}
})
@ -85,9 +93,9 @@
:else :post)
request {:method method
:uri (u/join cf/public-uri "api/rpc/command/" (name id))
:uri (u/join cf/public-uri "api/rpc/command/" nid)
:credentials "include"
:headers {"accept" "application/transit+json"}
:headers {"accept" "application/transit+json,text/event-stream,*/*"}
:body (when (= method :post)
(if form-data?
(http/form-data params)
@ -97,11 +105,21 @@
(if query-params
(select-keys params query-params)
nil))
:response-type (or response-type :text)}]
(->> (http/send! request)
(rx/map decode-fn)
(rx/mapcat handle-response))))
:response-type
(if (= response-type ::sse/stream)
:stream
(or response-type :text))}
result (->> (http/send! request)
(rx/map decode-fn)
(rx/mapcat handle-response))]
(cond->> result
(= ::sse/stream response-type)
(rx/mapcat (fn [body]
(-> (sse/create-stream body)
(sse/read-stream t/decode-str)))))))
(defmulti cmd! (fn [id _] id))

View file

@ -147,8 +147,10 @@
(mf/use-fn
(mf/deps template default-project-id)
(fn []
(let [mdata {:on-success on-template-cloned-success :on-error on-template-cloned-error}
params {:project-id default-project-id :template-id (:id template)}]
(let [mdata {:on-success on-template-cloned-success
:on-error on-template-cloned-error}
params {:project-id default-project-id
:template-id (:id template)}]
(swap! state #(assoc % :status :importing))
(st/emit! (with-meta (dd/clone-template (with-meta params mdata))
{::ev/origin "get-started-hero-block"})))))]

View file

@ -105,17 +105,22 @@
(defn send!
[{:keys [response-type] :or {response-type :text} :as params}]
(letfn [(on-response [response]
(let [body (case response-type
:json (.json ^js response)
:text (.text ^js response)
:blob (.blob ^js response))]
(->> (rx/from body)
(rx/map (fn [body]
{::response response
:status (.-status ^js response)
:headers (parse-headers (.-headers ^js response))
:body body})))))]
(letfn [(on-response [^js response]
(if (= :stream response-type)
(rx/of {:status (.-status response)
:headers (parse-headers (.-headers response))
:body (.-body response)
::response response})
(let [body (case response-type
:json (.json ^js response)
:text (.text ^js response)
:blob (.blob ^js response))]
(->> (rx/from body)
(rx/map (fn [body]
{::response response
:status (.-status ^js response)
:headers (parse-headers (.-headers ^js response))
:body body}))))))]
(->> (fetch params)
(rx/mapcat on-response))))

View file

@ -0,0 +1,54 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.util.sse
(:require
["eventsource-parser/stream" :as sse]
[beicon.core :as rx]))
(defn create-stream
[^js/ReadableStream stream]
(.. stream
(pipeThrough (js/TextDecoderStream.))
(pipeThrough (sse/EventSourceParserStream.))))
(defn read-stream
[^js/ReadableStream stream decode-fn]
(letfn [(read-items [^js reader]
(->> (rx/from (.read reader))
(rx/mapcat (fn [result]
(if (.-done result)
(rx/empty)
(rx/concat
(rx/of (.-value result))
(read-items reader)))))))]
(->> (read-items (.getReader stream))
(rx/mapcat (fn [^js event]
(let [type (.-event event)
data (.-data event)
data (decode-fn data)]
(if (= "error" type)
(rx/throw (ex-info "stream exception" data))
(rx/of #js {:type type :data data}))))))))
(defn get-type
[event]
(unchecked-get event "type"))
(defn get-payload
[event]
(unchecked-get event "data"))
(defn end-of-stream?
[event]
(= "end" (get-type event)))
(defn event?
[event]
(= "event" (get-type event)))

View file

@ -22,6 +22,7 @@
[app.util.i18n :as i18n :refer [tr]]
[app.util.import.parser :as cip]
[app.util.json :as json]
[app.util.sse :as sse]
[app.util.webapi :as wapi]
[app.util.zip :as uz]
[app.worker.impl :as impl]
@ -329,7 +330,7 @@
(map #(assoc % :type :fill)))
stroke-images-data (->> (cip/get-stroke-images-data node)
(map #(assoc % :type :stroke)))
images-data (concat
fill-images-data
stroke-images-data
@ -709,15 +710,22 @@
:response-type :blob
:method :get})
(rx/map :body)
(rx/mapcat #(rp/cmd! :import-binfile {:file % :project-id project-id}))
(rx/map (fn [_]
{:status :import-finish
:file-id (:file-id data)}))
(rx/mapcat (fn [file]
(->> (rp/cmd! ::sse/import-binfile {:file file :project-id project-id})
(rx/tap (fn [event]
(let [payload (sse/get-payload event)
type (sse/get-type event)]
(if (= type "event")
(log/dbg :hint "import-binfile: progress" :section (:section payload) :name (:name payload))
(log/dbg :hint "import-binfile: end")))))
(rx/filter sse/end-of-stream?)
(rx/map (fn [_]
{:status :import-finish
:file-id (:file-id data)})))))
(rx/catch (fn [cause]
(log/error :hint "unexpected error on import process"
:project-id project-id
::log/sync? true)
;; TODO: consider do thi son logging directly ?
(when (map? cause)
(println "Error data:")