diff --git a/deps.edn b/deps.edn index b3e687b..86641e1 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,11 @@ {:paths ["src" "resources"] - :deps {org.clojure/clojure {:mvn/version "1.11.1"} - io.replikativ/datahike {:mvn/version "0.6.1592"} + :deps {com.hyperfiddle/electric {:git/url "https://github.com/hyperfiddle/electric" :git/sha "b32ac98df7d7ec87f225d47354671be172ffa87e"} + ring/ring {:mvn/version "1.11.0"} ; comes with Jetty + org.clojure/tools.logging {:mvn/version "1.2.4"} + ch.qos.logback/logback-classic {:mvn/version "1.4.14"} + + org.clojure/clojure {:mvn/version "1.11.1"} + io.replikativ/datahike {:mvn/version "0.6.1594"} clj-python/libpython-clj {:mvn/version "2.025"} morse/morse {:mvn/version "0.4.3"} io.replikativ/kabel {:mvn/version "0.2.2"} @@ -15,21 +20,35 @@ remus/remus {:mvn/version "0.2.4"} nrepl/nrepl {:mvn/version "1.1.1"} cider/cider-nrepl {:mvn/version "0.47.1"} + io.replikativ/datahike-dynamodb {:mvn/version "0.1.8"} + io.replikativ/datahike-s3 {:mvn/version "0.1.13"} ;; exploratory - missionary/missionary {:mvn/version "b.34"} + missionary/missionary {:mvn/version "b.41"} io.github.jbellis/jvector {:mvn/version "3.0.2"} pangloss/pattern {:git/url "https://github.com/pangloss/pattern" :sha #_"affc7f3ac907f5b98de6638574a741e4693f1648" "93fb43e3223bbcfe08c4e37414709021d8a99604"} - anglican/anglican {:mvn/version "1.1.0"}} - :jvm-opts ["-Xmx1g" "--add-modules jdk.incubator.vector"] + anglican/anglican {:mvn/version "1.1.0"} + + reagent/reagent {:mvn/version "1.1.1"}} :aliases - {:run {:main-opts ["-m" "is.simm.simmis" "--middleware" "[cider.nrepl/cider-middleware]"]} + {:dev {;:main-opts ["-m" "is.simm.simmis" "--middleware" "[cider.nrepl/cider-middleware]"] + ;:jvm-opts ["--add-modules jdk.incubator.vector"] + :extra-paths ["src-dev"] + :extra-deps {thheller/shadow-cljs {:mvn/version "2.26.2"} + io.github.clojure/tools.build {:mvn/version "0.9.6" + :exclusions [com.google.guava/guava ; Guava version conflict between tools.build and clojurescript. + org.slf4j/slf4j-nop]}}} ; clashes with app logger + :build {:deps {io.github.clojure/tools.build {:mvn/version "0.9.6"}} - :ns-default build} + :ns-default build + ;:jvm-opts ["--add-modules jdk.incubator.vector"] + } :test {:extra-paths ["test"] :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"} io.github.cognitect-labs/test-runner - {:git/tag "v0.5.1" :git/sha "dfb30dd"}}}}} + {:git/tag "v0.5.1" :git/sha "dfb30dd"}} + ;:jvm-opts ["--add-modules jdk.incubator.vector"] + }}} diff --git a/package.json b/package.json new file mode 100644 index 0000000..08af348 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "recharts": "^2.1.9" + } +} diff --git a/resources/public/electric_starter_app/child-left.png b/resources/public/electric_starter_app/child-left.png new file mode 100644 index 0000000..7c29fb4 Binary files /dev/null and b/resources/public/electric_starter_app/child-left.png differ diff --git a/resources/public/electric_starter_app/child-right.png b/resources/public/electric_starter_app/child-right.png new file mode 100644 index 0000000..353a44d Binary files /dev/null and b/resources/public/electric_starter_app/child-right.png differ diff --git a/resources/public/electric_starter_app/child.png b/resources/public/electric_starter_app/child.png new file mode 100644 index 0000000..817e518 Binary files /dev/null and b/resources/public/electric_starter_app/child.png differ diff --git a/resources/public/electric_starter_app/index.html b/resources/public/electric_starter_app/index.html new file mode 100644 index 0000000..6fa093a --- /dev/null +++ b/resources/public/electric_starter_app/index.html @@ -0,0 +1,20 @@ + + + + + + + Simmis + + + + + + + + + + + + + diff --git a/resources/public/electric_starter_app/main-left.png b/resources/public/electric_starter_app/main-left.png new file mode 100644 index 0000000..588cd8d Binary files /dev/null and b/resources/public/electric_starter_app/main-left.png differ diff --git a/resources/public/electric_starter_app/main-right.png b/resources/public/electric_starter_app/main-right.png new file mode 100644 index 0000000..9a19caf Binary files /dev/null and b/resources/public/electric_starter_app/main-right.png differ diff --git a/resources/public/electric_starter_app/masterplan.css b/resources/public/electric_starter_app/masterplan.css new file mode 100644 index 0000000..ac4d889 --- /dev/null +++ b/resources/public/electric_starter_app/masterplan.css @@ -0,0 +1,103 @@ +/* CSS for masterplan */ + +#content { + margin-top: 30px; +} + +.message { + font-size: 60px; + font-weight: bold; + margin-top: 50px; +} + +.timeline { + height: 30px; + vertical-align: middle; + + border-style: solid; + border-width: 3px; + margin-top: 5px; +} + +.timeline-left { + display: inline-block; + position: fixed; + margin-top: -3px; + margin-left: -15px; +} + +.timeline-right { + display: inline-block; + position: fixed; + margin-right: -20px; + margin-top: -3px; +} + +.markers { + padding-top: 5px; + background-color: #ffffff; +} + +.marker { + color: #4d4d4d; + display: inline-block; + font-weight: bold; +} + +.parent { + color: #666666; + background-color: #f6f6f6; +} + +.parent-timeline-text { + display: inline-block; +/* visibility: hidden; */ + width: calc(100% - 8px); + vertical-align: middle; + + font-size: 16px; + font-weight: bold; + text-align: center; +} + +.main { + width:90%; +/* left: 0; + right: 0; needed for browser compat? */ + margin: auto; + + color: #666666; + background-color: #e6e6e6; + border-color: #666666; +} + +.main-timeline-text { + display: inline-block; + width: calc(100% - 22px); + vertical-align: middle; + + font-size: 16px; + font-weight: bold; + text-align: center; +} + +.child { + width: 20%; + margin-top: 15px; + margin-left: 60%; + + color: #333333; + background-color: #b3b3b3; + border-color: #333333; +} + +.child-timeline-text { + display: inline-block; +/* visibility: hidden; */ + width: calc(100% - 8px); + vertical-align: middle; + + font-size: 16px; + font-weight: bold; + text-align: center; +} diff --git a/resources/public/electric_starter_app/mockup1.svg b/resources/public/electric_starter_app/mockup1.svg new file mode 100644 index 0000000..ec38ea1 --- /dev/null +++ b/resources/public/electric_starter_app/mockup1.svg @@ -0,0 +1,353 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + 1.10.13 + 31.10.13 + + + + Monatsplan + + + + + Planbenennungen? + + + + + + + + diff --git a/resources/public/electric_starter_app/parent-left.png b/resources/public/electric_starter_app/parent-left.png new file mode 100644 index 0000000..d97feb5 Binary files /dev/null and b/resources/public/electric_starter_app/parent-left.png differ diff --git a/resources/public/electric_starter_app/parent-left.svg b/resources/public/electric_starter_app/parent-left.svg new file mode 100644 index 0000000..97e5130 --- /dev/null +++ b/resources/public/electric_starter_app/parent-left.svg @@ -0,0 +1,67 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/resources/public/electric_starter_app/parent-right.png b/resources/public/electric_starter_app/parent-right.png new file mode 100644 index 0000000..2da43cd Binary files /dev/null and b/resources/public/electric_starter_app/parent-right.png differ diff --git a/resources/public/electric_starter_app/simmis.png b/resources/public/electric_starter_app/simmis.png new file mode 100644 index 0000000..e534e9a Binary files /dev/null and b/resources/public/electric_starter_app/simmis.png differ diff --git a/shadow-cljs.edn b/shadow-cljs.edn new file mode 100644 index 0000000..42293a2 --- /dev/null +++ b/shadow-cljs.edn @@ -0,0 +1,12 @@ +{:builds + {:dev {:target :browser + :devtools {:loader-mode :default, :watch-dir "resources/public/electric_starter_app"} + :output-dir "resources/public/electric_starter_app/js" + :asset-path "/js" + :modules {:main {:entries [dev] :init-fn dev/start!}} + :build-hooks [(hyperfiddle.electric.shadow-cljs.hooks/reload-clj)]} + :prod {:target :browser + :output-dir "resources/public/electric_starter_app/js" + :asset-path "/js" + :modules {:main {:entries [prod] :init-fn prod/start!}} + :module-hash-names true}}} diff --git a/src-dev/dev.cljc b/src-dev/dev.cljc new file mode 100644 index 0000000..a079cca --- /dev/null +++ b/src-dev/dev.cljc @@ -0,0 +1,49 @@ +(ns dev + (:require + is.simm.main + [hyperfiddle.electric :as e] + #?(:clj [is.simm.server-jetty :as jetty]) + #?(:clj [shadow.cljs.devtools.api :as shadow]) + #?(:clj [shadow.cljs.devtools.server :as shadow-server]) + #?(:clj [clojure.tools.logging :as log]))) + +(comment (-main)) ; repl entrypoint + +#?(:clj ;; Server Entrypoint + (do + (def config + {:host "0.0.0.0" + :port 8081 + :resources-path "public/electric_starter_app" + :manifest-path ; contains Electric compiled program's version so client and server stays in sync + "public//electric_starter_app/js/manifest.edn"}) + + (defn -main [& args] + (log/info "Starting Electric compiler and server...") + + (shadow-server/start!) + (shadow/watch :dev) + (comment (shadow-server/stop!)) + + (def server (jetty/start-server! + (fn [ring-request] + (e/boot-server {} is.simm.main/Main ring-request)) + config)) + + (comment (.stop server)) + ))) + +#?(:cljs ;; Client Entrypoint + (do + (def electric-entrypoint (e/boot-client {} is.simm.main/Main nil)) + + (defonce reactor nil) + + (defn ^:dev/after-load ^:export start! [] + (set! reactor (electric-entrypoint + #(js/console.log "Reactor success:" %) + #(js/console.error "Reactor failure:" %)))) + + (defn ^:dev/before-load stop! [] + (when reactor (reactor)) ; stop the reactor + (set! reactor nil)))) diff --git a/src-dev/logback.xml b/src-dev/logback.xml new file mode 100644 index 0000000..d2c20b2 --- /dev/null +++ b/src-dev/logback.xml @@ -0,0 +1,16 @@ + + + + + %highlight(%-5level) %logger: %msg%n + + + + + + + + + + + diff --git a/src-dev/user.clj b/src-dev/user.clj new file mode 100644 index 0000000..7e98b0b --- /dev/null +++ b/src-dev/user.clj @@ -0,0 +1 @@ +(ns user (:require [dev])) diff --git a/src/is/simm/main.cljc b/src/is/simm/main.cljc new file mode 100644 index 0000000..d35592f --- /dev/null +++ b/src/is/simm/main.cljc @@ -0,0 +1,356 @@ +(ns is.simm.main + #?(:cljs (:require-macros [is.simm.main :refer [with-reagent]])) + (:require [hyperfiddle.electric :as e] + [hyperfiddle.electric-dom2 :as dom] + [hyperfiddle.electric-svg :as svg] + #?(:clj [datahike.api :as d]) + #?(:cljs [reagent.core :as r]) + [missionary.core :as m] + [clojure.string :as str] + #?(:cljs [reagent.core :as r]) + #?(:cljs ["recharts" :refer [ScatterChart Scatter LineChart Line + XAxis YAxis CartesianGrid]]) + #?(:cljs ["react-dom/client" :as ReactDom]))) + + + +;; Saving this file will automatically recompile and update in your browser + +(def task-cfg {:store {:backend :mem :id "task-manager"}}) + +(def task-schema [{:db/ident :title + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + {:db/ident :start + :db/valueType :db.type/instant + :db/cardinality :db.cardinality/one} + {:db/ident :end + :db/valueType :db.type/instant + :db/cardinality :db.cardinality/one} + {:db/ident :children + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/many}]) + +(def user-cfg {:store {:backend :mem :id "user-manager"}}) + +(def user-schema [{:db/ident :user + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + {:db/ident :password + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + {:db/ident :role + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one}]) + +#?(:clj + (defn init-db [cfg schema] + (try + (d/create-database cfg) + (defonce conn (d/connect cfg)) + (d/transact conn schema) + (catch Exception e + (println "Database already exists") + (defonce conn (d/connect cfg)))))) + +#?(:clj + (do (init-db task-cfg task-schema) + (init-db user-cfg user-schema))) + +(comment + (d/delete-database cfg) + + (d/transact conn [{:age 42 :name "Alice"}]) + + (d/transact conn [{:age 42 :name "Beatrix"}]) + + (d/transact conn [{:title "Nachhaltiges Mannheim 2014" + :start #inst "2014-01-01T00:00:00.000-00:00" + :end #inst "2014-12-31T23:59:59.999-00:00" + :children [{:title "Urban gardening" + :start #inst "2014-01-01T00:00:00.000-00:00" + :end #inst "2014-12-31T23:59:59.999-00:00" + :children [{:title "Gartenprojekt" + :start #inst "2014-01-01T00:00:00.000-00:00" + :end #inst "2014-12-31T23:59:59.999-00:00"}]}]}]) + + (d/datoms @conn :eavt) + + (d/transact conn schema) + + (d/q '[:find ?t ?ct + :where + [?e :title ?t] + [?e :children ?c] + [?c :title ?ct]] + @conn)) + +;; a user management system + +;; schema containing user, password, and role + +(e/defn QueryParentTimeline [title] + (e/server (d/q '[:find ?title ?start ?end + :in $ ?parent-title + :where + [?t :title ?parent-title] + [?pt :children ?t] + [?pt :title ?title] + [?pt :start ?start] + [?pt :end ?end]] + (e/watch conn) + title))) + +(e/defn QueryTimeline [title] + (e/server (d/q '[:find ?title ?start ?end + :in $ ?title + :where + [?t :title ?title] + [?t :start ?start] + [?t :end ?end]] + (e/watch conn) + title))) + +(e/defn QueryChildrenTimeline [parent-title] + (e/server (d/q '[:find ?title ?start ?end + :in $ ?parent-title + :where + [?pt :title ?parent-title] + [?pt :children ?t] + [?t :title ?title] + [?t :start ?start] + [?t :end ?end]] + (e/watch conn) + parent-title))) + +(e/defn Task [active-task text start end] + (e/client + (dom/div (dom/props {:class "parent" :style {:opacity (if (empty? text) "0.5" "1")}}) + (dom/div (dom/props {:class "timeline"}) + (dom/div (dom/props {:class "timeline-left"}) + (dom/img (dom/props {:src "parent-left.png" + :style {:height "30px"} + :alt (str start)}))) + (dom/div (dom/props {:class "parent-timeline-text"}) + (dom/text text) + (dom/on "click" (e/fn [e] + (.log js/console "click" e) + (reset! active-task text)))) + (dom/div (dom/props {:class "timeline-right"}) + (dom/img (dom/props {:src "parent-right.png" + :style {:height "30px"} + :alt (str end)}))))))) + +#?(:cljs + (def !view-state (atom :main))) + +#?(:cljs + (def !user (atom nil))) + +(e/defn Header [] + (e/client + (dom/header (dom/props {:class "bg-gray-600 p-4 shadow"}) + (dom/div (dom/props {:class "container mx-auto flex justify-between items-center"}) + (dom/div (dom/props {:class "flex items-center"}) + (dom/a (dom/props {:class "text-white text-xl font-bold"}) + (dom/on "click" (e/fn [_e] (reset! !view-state :main))) + (dom/img (dom/props {:src "/simmis.png" :alt "Logo" :class "h-8"})))) + (dom/nav (dom/props {:class "flex space-x-4"}) + (dom/a (dom/props {:class "text-white" :href "#about"}) + (dom/on "click" (e/fn [_e] (reset! !view-state :about))) + (dom/text "About")) + (dom/a (dom/props {:class "text-white" :href "#taskmanager"}) + (dom/on "click" (e/fn [_e] (reset! !view-state :taskmanager))) + (dom/text "Task Manager")) + (dom/a (dom/props {:class "text-white" :href "#screenshare"}) + (dom/on "click" (e/fn [_e] (reset! !view-state :screenshare))) + (dom/text "Screen Share")) + (if-let [user (e/watch !user)] + (dom/a (dom/props {:class "text-white" :href "#user"}) + (dom/on "click" (e/fn [_e] (js/alert "not implemented"))) + (dom/text (:user user))) + (dom/a (dom/props {:class "text-white" :href "#login"}) + (dom/on "click" (e/fn [_e] (reset! !view-state :login))) + (dom/text "Login")))))))) + + +(e/defn Footer [] + (e/client + (dom/footer (dom/props {:class "bg-gray-200 p-4"}) + (dom/div (dom/props {:class "container mx-auto flex justify-between text-center"}) + (dom/p (dom/text "Powered by ") + (dom/a (dom/props {:href "https://clojure.org/" :target "_blank" :class "text-blue-400"}) + (dom/text "Clojure")) + (dom/text ", ") + (dom/a (dom/props {:href "https://datahike.io" :target "_blank" :class "text-blue-400"}) + (dom/text "Datahike")) + (dom/text " and ") + (dom/a (dom/props {:href "https://hyperfiddle.net/" :target "_blank" :class "text-blue-400"}) + (dom/text "Electric"))))))) + +#?(:cljs + (def !active-task (atom "Urban gardening"))) + +(e/defn TaskManager [] + (let [active-task-title (e/client (e/watch !active-task)) + [parent-title parent-start parent-end] (first (QueryParentTimeline. active-task-title)) + [title start end] (first (QueryTimeline. active-task-title))] + (e/client (dom/div (dom/props {:class "flex flex-col items-center justify-center"}) + (dom/div (dom/props {:class "container mx-auto text-center"}) + (Task. !active-task parent-title parent-start parent-end) + (Task. !active-task title start end) + (dom/div (dom/props {:class "container w-11/12"}) + (e/for [[title start end] (QueryChildrenTimeline. title)] + (Task. !active-task title start end)))))))) + +(e/defn ScreenShare [] + (e/client + (dom/div (dom/props {:class "flex flex-col items-center justify-center bg-gray-100"}) + (dom/div (dom/props {:class "max-w-2xl p-6 bg-white rounded-lg shadow-md text-center"}) + (dom/h1 (dom/props {:class "text-4xl font-bold mb-4 text-gray-800"}) (dom/text "ScreenShare")) + (dom/p (dom/props {:class "text-lg font-medium text-gray-700 mb-2"}) + (dom/text "This is a simple screen sharing application using Electric.")) + (dom/p (dom/props {:class "text-gray-600"}) + (dom/text "It demonstrates how to use Electric to build a simple web application.")))))) + + +(e/defn CheckLogin? [user password] + (e/server + (prn user password) + true)) + +(e/defn Login [] + (e/client + (let [submit-fn (e/fn [e] + (.preventDefault e) + (let [user (.-value (.getElementById js/document "user")) + password (.-value (.getElementById js/document "password"))] + (if (CheckLogin?. user password) + (do + (reset! !user {:user user :password password}) + (reset! !view-state :main)) + (do + (reset! !user nil) + (js/alert "Invalid username or password.")))))] + (dom/div (dom/props {:class "flex flex-col items-center justify-center"}) + (dom/div (dom/props {:class "w-full max-w-md"}) + (dom/h1 (dom/props {:class "text-3xl font-bold mb-6"}) (dom/text "Signup")) + (dom/form (dom/props {:class "bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"}))) + (dom/on "submit" submit-fn) + (dom/div (dom/props {:class "mb-4"}) + (dom/label (dom/props {:class "block text-gray-700 text-sm font-bold mb-2"} (dom/text "Username"))) + (dom/input (dom/props {:id "user" :class "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" :type "text" :placeholder "Username"}))) + (dom/div (dom/props {:class "mb-6"}) + (dom/label (dom/props {:class "block text-gray-700 text-sm font-bold mb-2"} (dom/text "Password"))) + (dom/input (dom/props {:id "password" :class "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" :type "password" :placeholder "Password"}))) + (dom/div (dom/props {:class "flex items-center justify-between"}) + (dom/button (dom/props {:class "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" :type "submit"}) + (dom/on "click" submit-fn) + (dom/text "Login"))))))) + +(e/defn About [] + (e/client + (dom/section (dom/props {:class "flex flex-col items-center justify-center bg-gray-100 grow"}) + (dom/div (dom/props {:class "max-w-2xl p-6 bg-white rounded-lg shadow-md"}) + (dom/h1 (dom/props {:class "text-4xl font-bold mb-4 text-center text-gray-800"}) (dom/text "About")) + (dom/p (dom/props {:class "text-lg font-medium text-gray-700 mb-2 text-center"}) + (dom/text "This is a simple task manager application using Electric.")) + (dom/p (dom/props {:class "text-gray-600 text-center"}) + (dom/text "It demonstrates how to use Electric to build a simple web application.")))))) + + + +#?(:cljs (def ReactRootWrapper + (r/create-class + {:component-did-mount (fn [this] (js/console.log "mounted")) + :render (fn [this] + (let [[_ Component & args] (r/argv this)] + (into [Component] args)))}))) + +#?(:cljs (defn create-root + "See https://reactjs.org/docs/react-dom-client.html#createroot" + ([node] (create-root node (str (gensym)))) + ([node id-prefix] + (ReactDom/createRoot node #js {:identifierPrefix id-prefix})))) + +#?(:cljs (defn render [root & args] + (.render root (r/as-element (into [ReactRootWrapper] args))))) + +(defmacro with-reagent [& args] + `(dom/div ; React will hijack this element and empty it. + (let [root# (create-root dom/node)] + (render root# ~@args) + (e/on-unmount #(.unmount root#))))) + +;; Reagent World + +(defn TinyLineChart [data] + #?(:cljs + [:> LineChart {:width 400 :height 200 :data (clj->js data)} + [:> CartesianGrid {:strokeDasharray "3 3"}] + [:> XAxis {:dataKey "name"}] + [:> YAxis] + [:> Line {:type "monotone", :dataKey "pv", :stroke "#8884d8"}] + [:> Line {:type "monotone", :dataKey "uv", :stroke "#82ca9d"}]])) + +(defn MousePosition [x y] + #?(:cljs + [:> ScatterChart {:width 300 :height 300 + :margin #js{:top 20, :right 20, :bottom 20, :left 20}} + [:> CartesianGrid {:strokeDasharray "3 3"}] + [:> XAxis {:type "number", :dataKey "x", :unit "px", :domain #js[0 2000]}] + [:> YAxis {:type "number", :dataKey "y", :unit "px", :domain #js[0 2000]}] + [:> Scatter {:name "Mouse position", + :data (clj->js [{:x x, :y y}]), :fill "#8884d8"}]])) + +;; Electric Clojure + +(e/defn ReagentInterop [] + (e/client + (let [[x y] (dom/on! js/document "mousemove" + (fn [e] [(.-clientX e) (.-clientY e)]))] + (with-reagent MousePosition x y) ; reactive + ;; Adapted from https://recharts.org/en-US/examples/TinyLineChart + (with-reagent TinyLineChart + [{:name "Page A" :uv 4000 :amt 2400 :pv 2400} + {:name "Page B" :uv 3000 :amt 2210 :pv 1398} + {:name "Page C" :uv 2000 :amt 2290 :pv (+ 6000 (* -5 y))} ; reactive + {:name "Page D" :uv 2780 :amt 2000 :pv 3908} + {:name "Page E" :uv 1890 :amt 2181 :pv 4800} + {:name "Page F" :uv 2390 :amt 2500 :pv 3800} + {:name "Page G" :uv 3490 :amt 2100 :pv 4300}])))) + +(e/defn Main [ring-request] + (let [view-state (e/client (e/watch !view-state))] + (e/client + (binding [dom/node js/document.body] + (dom/section (dom/props {:class "flex flex-col min-h-screen"}) + (Header.) + (dom/div (dom/props {:class "flex-grow"}) + (ReagentInterop.) + #_(case view-state + :main (About.) + :screenshare (ScreenShare.) + :taskmanager (TaskManager.) + :login (Login.) + :about (About.))) + (Footer.)))))) + + +(comment + + (require #_[app.config :as config] + 'clojure.edn + 'contrib.ednish + '[contrib.str :refer [any-matches?]] + '[contrib.data :refer [unqualify treelister]] + ;;#?(:clj '[contrib.datomic-contrib :as dx]) + '[contrib.datomic-m #?(:clj :as :cljs :as-alias) d] + '[contrib.gridsheet :as gridsheet :refer [Explorer]] + '[hyperfiddle.electric :as e] + '[hyperfiddle.electric-dom2 :as dom] + '[hyperfiddle.history :as history] + '[missionary.core :as m]) + + + ) \ No newline at end of file diff --git a/src/is/simm/runtimes/rustdesk.clj b/src/is/simm/runtimes/rustdesk.clj index 0e70a1e..9cabc9d 100644 --- a/src/is/simm/runtimes/rustdesk.clj +++ b/src/is/simm/runtimes/rustdesk.clj @@ -8,14 +8,16 @@ [is.simm.website :refer [md-render default-chrome base-url]] [is.simm.runtimes.openai :refer [text-chat chat]] [is.simm.parse :refer [parse-json]] - [clojure.core.async :refer [timeout put! chan pub sub close! take! poll!] :as async] + [clojure.core.async :refer [timeout put! chan pub sub close! take! poll! go-loop go] :as async] [superv.async :refer [go-try S flow [ch] + (m/observe (fn [!] + (go-loop [m (chan [flow] + (let [ch (chan)] + ((m/reduce #(put! ch %2) nil flow) + prn prn) + ch)) + + (def test-ch (flow->chan (m/seed [1 2 3]))) + + (put! test-ch 42) + + (take! test-ch (fn [v] (prn "go received" v))) + + ((let [flow (m/observe (fn [!] (defn send! [m] (! m)) #()))] + (m/ap (prn "received" (m/?> ##Inf flow)))) + prn prn) + + (m/observe (fn [!] + (defn foo [m] (! m)) + #_(! 42) + #_(go-loop [m (flow test-ch) + (fn [tag stream] (m/ap (prn tag '- (m/?> stream))))) + prn prn) + + + + + (defn stream-groups " +For each key+flow pair consumed from input, call given function with the key, the flow published as a stream, and +optional arguments. Merge and emit values produced by resulting flows. Each stream is kept alive for the time extent of +the flow process. +" [input f & args] + (m/ap + (let [[tag group] (m/?> ##Inf input) + event-stream (m/stream group)] + (m/amb= (do (m/?> event-stream) (m/amb)) + (m/?> (apply f tag event-stream args)))))) + + ((stream-groups (m/seed [[1 (m/seed [1])] [2 (m/seed [2])] [1 (m/seed [3])]]) + (fn [tag stream] (m/ap (prn tag '- (m/?> stream))))) + prn prn) + + (defn topic-store " +For each key+flow pair consumed from input, accumulate the successive maps associating each key with its stream, call +given function with states published as a signal, and optional arguments. Emit values produced by resulting flow. The +signal is kept alive for the time extent of the flow process. +" [input f & args] + (m/ap + (let [topics (m/signal (m/reductions conj {} (stream-groups input #(m/ap [%1 %2]))))] + (m/amb= (do (m/?> topics) (m/amb)) + (m/?> (apply f topics args)))))) + + (defn get-topic " +Subscribe to the stream associated with given key in given signal of maps, as soon as it's available. +" [topics tag] + (m/ap (m/?> (m/? (m/reduce (comp reduced {}) nil + (m/eduction (keep tag) topics)))))) + + (defn user-subs " +For each user in given flow, call given function with the user name, a map associating each topic of interest to its +stream of events, and optional arguments. Merge and emit values produced by resulting flows. +" [topics users f & args] + (m/ap + (let [{:keys [name interests]} (m/?> ##Inf users)] + (m/?> (apply f name (into {} (map (juxt identity (partial get-topic topics))) interests) args))))) + + + ((m/ap + (m/amb= + (do (Thread/sleep 1000) "Option 1") ; Resolves in 1 second + (do (Thread/sleep 500) "Option 2"))) + prn prn) ; Resolves in 0.5 seconds + + + `(comment + (def ps ((m/reduce {} nil + (topic-store + (m/group-by :tag + (m/observe + (fn [!] + (defn create-post [tag text] + (! {:tag tag :text text})) + #()))) + user-subs + (m/seed #{{:name "alice" :interests #{:math :science}} + {:name "bob" :interests #{:math :clojure}}}) + (fn [name interests] + (m/ap (let [[tag msgs] (m/?> (count interests) (m/seed interests))] + (prn name '- (m/?> msgs)) (m/amb)))))) + prn prn)) + + (create-post :math "linear algebra") + ;; "alice" - {:tag :math, :text "linear algebra"} + ;; "bob" - {:tag :math, :text "linear algebra"} + + (create-post :clojure "transducers") + ;; "bob" - {:tag :clojure, :text "transducers"} + + (ps)) + + ) \ No newline at end of file diff --git a/src/is/simm/server_jetty.clj b/src/is/simm/server_jetty.clj new file mode 100644 index 0000000..6612e61 --- /dev/null +++ b/src/is/simm/server_jetty.clj @@ -0,0 +1,120 @@ +(ns is.simm.server-jetty + "Electric integrated into a sample ring + jetty app." + (:require + [clojure.edn :as edn] + [clojure.java.io :as io] + [clojure.string :as str] + [clojure.tools.logging :as log] + [contrib.assert :refer [check]] + [hyperfiddle.electric-ring-adapter :as electric-ring] + [ring.adapter.jetty :as ring] + [ring.middleware.content-type :refer [wrap-content-type]] + [ring.middleware.cookies :as cookies] + [ring.middleware.params :refer [wrap-params]] + [ring.middleware.resource :refer [wrap-resource]] + [ring.util.response :as res]) + (:import + (org.eclipse.jetty.server.handler.gzip GzipHandler) + (org.eclipse.jetty.websocket.server.config JettyWebSocketServletContainerInitializer JettyWebSocketServletContainerInitializer$Configurator))) + +;;; Electric integration + +(defn electric-websocket-middleware + "Open a websocket and boot an Electric server program defined by `entrypoint`. + Takes: + - a ring handler `next-handler` to call if the request is not a websocket upgrade (e.g. the next middleware in the chain), + - a `config` map eventually containing {:hyperfiddle.electric/user-version } to ensure client and server share the same version, + - see `hyperfiddle.electric-ring-adapter/wrap-reject-stale-client` + - an Electric `entrypoint`: a function (fn [ring-request] (e/boot-server {} my-ns/My-e-defn ring-request)) + " + [next-handler config entrypoint] + ;; Applied bottom-up + (-> (electric-ring/wrap-electric-websocket next-handler entrypoint) ; 5. connect electric client + ; 4. this is where you would add authentication middleware (after cookie parsing, before Electric starts) + (cookies/wrap-cookies) ; 3. makes cookies available to Electric app + (electric-ring/wrap-reject-stale-client config) ; 2. reject stale electric client + (wrap-params))) ; 1. parse query params + +(defn get-modules [manifest-path] + (when-let [manifest (io/resource manifest-path)] + (let [manifest-folder (when-let [folder-name (second (rseq (str/split manifest-path #"\/")))] + (str "/" folder-name "/"))] + (->> (slurp manifest) + (edn/read-string) + (reduce (fn [r module] (assoc r (keyword "hyperfiddle.client.module" (name (:name module))) + (str manifest-folder (:output-name module)))) {}))))) + +(defn template + "In string template `
$:foo/bar$
`, replace all instances of $key$ +with target specified by map `m`. Target values are coerced to string with `str`. + E.g. (template \"
$:foo$
\" {:foo 1}) => \"
1
\" - 1 is coerced to string." + [t m] (reduce-kv (fn [acc k v] (str/replace acc (str "$" k "$") (str v))) t m)) + +;;; Template and serve index.html + +(defn wrap-index-page + "Server the `index.html` file with injected javascript modules from `manifest.edn`. +`manifest.edn` is generated by the client build and contains javascript modules +information." + [next-handler config] + (fn [ring-req] + (if-let [response (res/resource-response (str (check string? (:resources-path config)) "/index.html"))] + (if-let [bag (merge config (get-modules (check string? (:manifest-path config))))] + (-> (res/response (template (slurp (:body response)) bag)) ; TODO cache in prod mode + (res/content-type "text/html") ; ensure `index.html` is not cached + (res/header "Cache-Control" "no-store") + (res/header "Last-Modified" (get-in response [:headers "Last-Modified"]))) + (-> (res/not-found (pr-str ::missing-shadow-build-manifest)) ; can't inject js modules + (res/content-type "text/plain"))) + ;; index.html file not found on classpath + (next-handler ring-req)))) + +(defn not-found-handler [_ring-request] + (-> (res/not-found "Not found") + (res/content-type "text/plain"))) + +(defn http-middleware [config] + ;; these compose as functions, so are applied bottom up + (-> not-found-handler + (wrap-index-page config) ; 3. otherwise fallback to default page file + (wrap-resource (:resources-path config)) ; 2. serve static file from classpath + (wrap-content-type) ; 1. detect content (e.g. for index.html) + )) + +(defn middleware [config entrypoint] + (-> (http-middleware config) ; 2. otherwise, serve regular http content + (electric-websocket-middleware config entrypoint))) ; 1. intercept websocket upgrades and maybe start Electric + +(defn- add-gzip-handler! + "Makes Jetty server compress responses. Optional but recommended." + [server] + (.setHandler server + (doto (GzipHandler.) + #_(.setIncludedMimeTypes (into-array ["text/css" "text/plain" "text/javascript" "application/javascript" "application/json" "image/svg+xml"])) ; only compress these + (.setMinGzipSize 1024) + (.setHandler (.getHandler server))))) + +(defn- configure-websocket! + "Tune Jetty Websocket config for Electric compat." [server] + (JettyWebSocketServletContainerInitializer/configure + (.getHandler server) + (reify JettyWebSocketServletContainerInitializer$Configurator + (accept [_this _servletContext wsContainer] + (.setIdleTimeout wsContainer (java.time.Duration/ofSeconds 60)) + (.setMaxBinaryMessageSize wsContainer (* 100 1024 1024)) ; 100M - temporary + (.setMaxTextMessageSize wsContainer (* 100 1024 1024)) ; 100M - temporary + )))) + +(defn start-server! [entrypoint + {:keys [port host] + :or {port 8080, host "0.0.0.0"} + :as config}] + (let [server (ring/run-jetty (middleware config entrypoint) + (merge {:port port + :join? false + :configurator (fn [server] + (configure-websocket! server) + (add-gzip-handler! server))} + config))] + (log/info "👉" (str "http://" host ":" (-> server (.getConnectors) first (.getPort)))) + server)) \ No newline at end of file diff --git a/src/is/simm/simmis.clj b/src/is/simm/simmis.clj index 41427c3..95b2f59 100644 --- a/src/is/simm/simmis.clj +++ b/src/is/simm/simmis.clj @@ -4,6 +4,7 @@ [superv.async :refer [S go-try (get-in @peer [:http :routes]) + ring-handler + wrap-session) server (run-jetty ring {:port 8080 :join? false})] (swap! peer assoc-in [:http :server] server) (log/info "Server started.") diff --git a/src/is/simm/towers.clj b/src/is/simm/towers.clj index b409494..6d4858a 100644 --- a/src/is/simm/towers.clj +++ b/src/is/simm/towers.clj @@ -4,6 +4,7 @@ [is.simm.runtimes.report :refer [report]] [is.simm.runtimes.etaoin :refer [etaoin]] [is.simm.runtimes.notes :refer [notes]] + [is.simm.runtimes.users :refer [users]] [is.simm.runtimes.rustdesk :refer [rustdesk]] [is.simm.runtimes.telegram :refer [telegram long-polling]] [is.simm.runtimes.text-extractor :refer [text-extractor]] @@ -20,7 +21,7 @@ [S peer [in out]]) (defn default [] - (comp drain brave etaoin openai notes assistance text-extractor rustdesk telegram codrain)) + (comp drain brave etaoin openai notes assistance text-extractor rustdesk #_users telegram codrain)) (defn debug [] (comp drain @@ -38,7 +39,9 @@ text-extractor (partial report #(println "text-extractor: " (:type %) (:request-id %))) rustdesk - (partial report #(println "rustdesk: " (:type %) (:request-id %))) + (partial report #(println "users: " (:type %) (:request-id %))) + #_users + #_(partial report #(println "rustdesk: " (:type %) (:request-id %))) telegram (partial report #(println "telegram: " (:type %) (:request-id %))) codrain)) @@ -60,6 +63,8 @@ (partial report #(println "text-extractor: " (:type %) (:request-id %))) rustdesk (partial report #(println "rustdesk: " (:type %) (:request-id %))) + #_(partial report #(println "users: " (:type %) (:request-id %))) + #_users (partial telegram long-polling) (partial report #(println "telegram: " (:type %) (:request-id %))) codrain)) \ No newline at end of file