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 @@
+
+
+
+
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 @@
+
+
+
+
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 ? go-loop-try put? go-for]]
[taoensso.timbre :refer [debug]]
[missionary.core :as m]
[datahike.api :as d]
[clojure.data :refer [diff]]
- [clojure.java.io :as io])
+ [clojure.java.io :as io]
+ [konserve.core :as k]
+ [konserve.filestore :as kf])
(:import [java.nio.file Files Paths]
[java.util Base64]))
@@ -130,6 +132,261 @@
(m/? (add-screenshot! conn x)))))
nil 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 ? on-abort] :as sasync]
[is.simm.http :refer [ring-handler]]
[ring.adapter.jetty :refer [run-jetty]]
+ [ring.middleware.session :refer [wrap-session]]
[clojure.core.async :refer [chan close!]]
[is.simm.towers :refer [default debug test-tower]]
[nrepl.server :refer [start-server stop-server]]
@@ -26,7 +27,9 @@
(def peer (atom {}))
(sasync/restarting-supervisor (fn [S] (go-try S
((debug) [S peer chans])
- (let [ring (ring-handler (get-in @peer [:http :routes]))
+ (let [ring (-> (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