├── .clj-kondo ├── config.edn └── net.clojars.john │ ├── cljs_thread │ ├── cljs_thread │ │ ├── core.clj │ │ └── core.clj_kondo │ └── config.edn │ └── injest │ ├── config.edn │ └── injest │ └── path.clj ├── .gitignore ├── LICENSE ├── README.md ├── build.clj ├── deps.edn ├── docs ├── core.js ├── index.html ├── manifest.edn ├── repl.js ├── screen.js ├── shared.js └── sw.js ├── resources ├── calva.exports │ └── config.edn └── clj-kondo.exports │ └── net.clojars.john │ └── cljs_thread │ ├── cljs_thread │ └── core.clj_kondo │ └── config.edn ├── shadow_dashboard ├── .DS_Store ├── deps.edn ├── node_modules │ └── .package-lock.json ├── package-lock.json ├── package.json ├── resources │ ├── .DS_Store │ └── public │ │ └── index.html ├── shadow-cljs.edn └── src │ ├── lib │ └── cljs_thread │ │ ├── re_frame.cljs │ │ └── re_state.cljs │ ├── main │ └── dashboard │ │ ├── core.cljs │ │ └── regs │ │ ├── db.cljs │ │ ├── home_panel.cljs │ │ ├── shell.cljs │ │ └── sign_in.cljs │ └── screen │ └── dashboard │ ├── footer.cljs │ ├── routes.cljs │ ├── screen.cljs │ ├── shell.cljs │ └── views │ ├── home_panel.cljs │ └── sign_in.cljs └── src └── cljs_thread ├── core.clj ├── core.cljs ├── db.cljs ├── env.cljs ├── future.clj ├── future.cljs ├── id.cljs ├── idb.cljs ├── in.clj ├── in.cljs ├── injest.clj ├── injest.cljs ├── macro_impl.clj ├── msg.cljs ├── nrepl.clj ├── on_when.clj ├── on_when.cljs ├── pmap.clj ├── pmap.cljs ├── repl.clj ├── repl.cljs ├── root.cljs ├── spawn.clj ├── spawn.cljs ├── state.cljs ├── sw.cljs ├── sync.cljs └── util.cljs /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {cljs-thread.core/=>> clojure.core/->>} 2 | :hooks {:macroexpand {cljs-thread.core/in cljs-thread.core/in 3 | cljs-thread.core/future cljs-thread.core/future 4 | cljs-thread.core/spawn cljs-thread.core/spawn 5 | cljs-thread.core/wait cljs-thread.core/wait 6 | cljs-thread.core/in? cljs-thread.core/in?}} 7 | :config-in-call 8 | {cljs-thread.core/in 9 | {:linters {:unresolved-symbol {:exclude [yield]}}} 10 | cljs-thread.core/future 11 | {:linters {:unresolved-symbol {:exclude [yield]}}} 12 | cljs-thread.core/wait 13 | {:linters {:unresolved-symbol {:exclude [yield]}}} 14 | cljs-thread.core/spawn 15 | {:linters {:unresolved-symbol {:exclude [yield]}}}}} 16 | -------------------------------------------------------------------------------- /.clj-kondo/net.clojars.john/cljs_thread/cljs_thread/core.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.core 2 | (:refer-clojure :exclude [future])) 3 | 4 | (defn yield []) 5 | 6 | (defmacro in [_id & x] 7 | `(let [~'yield clojure.core/identity 8 | yield# clojure.core/identity] 9 | (yield# 1) 10 | (~'yield 1) 11 | ~@x)) 12 | 13 | (defmacro future [& _x]) 14 | 15 | (defmacro wait [& x] 16 | `(let [~'yield clojure.core/identity 17 | yield# clojure.core/identity] 18 | (yield# 1) 19 | (~'yield 1) 20 | ~@x)) 21 | 22 | (defmacro spawn [& x] 23 | `(let [~'yield clojure.core/identity 24 | yield# clojure.core/identity] 25 | (yield# 1) 26 | (~'yield 1) 27 | ~@x)) 28 | 29 | (defn get-symbols [body] 30 | (->> body 31 | (tree-seq coll? seq) 32 | (rest) 33 | (filter (complement coll?)) 34 | (filter symbol?) 35 | vec)) 36 | 37 | (defmacro in? [& x] 38 | (let [[syms body] (if (and (second x) (vector? (first x))) 39 | [(first x) `~(second x)] 40 | [(get-symbols x) `(do ~@x)])] 41 | `(do '~syms (fn ~syms ~body)))) 42 | -------------------------------------------------------------------------------- /.clj-kondo/net.clojars.john/cljs_thread/cljs_thread/core.clj_kondo: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.core 2 | (:refer-clojure :exclude [future])) 3 | 4 | (defn yield []) 5 | 6 | (defmacro in [_id & x] 7 | `(let [~'yield clojure.core/identity 8 | yield# clojure.core/identity] 9 | (yield# 1) 10 | (~'yield 1) 11 | ~@x)) 12 | 13 | (defmacro future [& _x]) 14 | 15 | (defmacro wait [& x] 16 | `(let [~'yield clojure.core/identity 17 | yield# clojure.core/identity] 18 | (yield# 1) 19 | (~'yield 1) 20 | ~@x)) 21 | 22 | (defmacro spawn [& x] 23 | `(let [~'yield clojure.core/identity 24 | yield# clojure.core/identity] 25 | (yield# 1) 26 | (~'yield 1) 27 | ~@x)) 28 | 29 | (defn get-symbols [body] 30 | (->> body 31 | (tree-seq coll? seq) 32 | (rest) 33 | (filter (complement coll?)) 34 | (filter symbol?) 35 | vec)) 36 | 37 | (defmacro in? [& x] 38 | (let [[syms body] (if (and (second x) (vector? (first x))) 39 | [(first x) `~(second x)] 40 | [(get-symbols x) `(do ~@x)])] 41 | `(do '~syms (fn ~syms ~body)))) 42 | -------------------------------------------------------------------------------- /.clj-kondo/net.clojars.john/cljs_thread/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {cljs-thread.core/=>> clojure.core/->>} 2 | :hooks {:macroexpand {cljs-thread.core/in cljs-thread.core/in 3 | cljs-thread.core/future cljs-thread.core/future 4 | cljs-thread.core/spawn cljs-thread.core/spawn 5 | cljs-thread.core/wait cljs-thread.core/wait 6 | cljs-thread.core/in? cljs-thread.core/in?}} 7 | :config-in-call 8 | {cljs-thread.core/in 9 | {:linters {:unresolved-symbol {:exclude [yield]}}} 10 | cljs-thread.core/future 11 | {:linters {:unresolved-symbol {:exclude [yield]}}} 12 | cljs-thread.core/wait 13 | {:linters {:unresolved-symbol {:exclude [yield]}}} 14 | cljs-thread.core/spawn 15 | {:linters {:unresolved-symbol {:exclude [yield]}}}}} 16 | -------------------------------------------------------------------------------- /.clj-kondo/net.clojars.john/injest/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {injest.core/x> clojure.core/-> 2 | injest.core/x>> clojure.core/->> 3 | injest.core/=> clojure.core/-> 4 | injest.core/=>> clojure.core/->> 5 | injest.core/|> clojure.core/-> 6 | injest.core/|>> clojure.core/->> 7 | 8 | injest.path/+> clojure.core/-> 9 | injest.path/+>> clojure.core/->> 10 | injest.path/x> clojure.core/-> 11 | injest.path/x>> clojure.core/->> 12 | injest.path/=> clojure.core/-> 13 | injest.path/=>> clojure.core/->> 14 | injest.path/|> clojure.core/-> 15 | injest.path/|>> clojure.core/->>} 16 | 17 | :hooks {:macroexpand {injest.path/+> injest.path/+> 18 | injest.path/+>> injest.path/+>> 19 | injest.path/x> injest.path/+> 20 | injest.path/x>> injest.path/+>> 21 | injest.path/=> injest.path/+> 22 | injest.path/=>> injest.path/+>>}} 23 | 24 | :linters {:injest.path/+> {:level :error} 25 | :injest.path/+>> {:level :error} 26 | :unused-binding {:level :off}} 27 | 28 | } 29 | -------------------------------------------------------------------------------- /.clj-kondo/net.clojars.john/injest/injest/path.clj: -------------------------------------------------------------------------------- 1 | (ns injest.path) 2 | 3 | (def protected-fns #{`fn 'fn 'fn* 'partial}) 4 | 5 | (defn get-or-nth [m-or-v aval] 6 | (if (associative? m-or-v) 7 | (get m-or-v aval) 8 | (nth m-or-v aval))) 9 | 10 | (defn path-> [form x] 11 | (cond (and (seq? form) (not (protected-fns (first form)))) 12 | (with-meta `(~(first form) ~x ~@(next form)) (meta form)) 13 | (or (string? form) (nil? form) (boolean? form)) 14 | (list x form) 15 | (int? form) 16 | (list 'injest.path/get-or-nth x form) 17 | :else 18 | (list form x))) 19 | 20 | (defn path->> [form x] 21 | (cond (and (seq? form) (not (protected-fns (first form)))) 22 | (with-meta `(~(first form) ~@(next form) ~x) (meta form)) 23 | (or (string? form) (nil? form) (boolean? form)) 24 | (list x form) 25 | (int? form) 26 | (list 'injest.path/get-or-nth x form) 27 | :else 28 | (list form x))) 29 | 30 | (defn +> 31 | [x & forms] 32 | (loop [x x, forms forms] 33 | (if forms 34 | (recur (path-> (first forms) x) (next forms)) 35 | x))) 36 | 37 | (defn +>> 38 | [x & forms] 39 | (loop [x x, forms forms] 40 | (if forms 41 | (recur (path->> (first forms) x) (next forms)) 42 | x))) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .calva 2 | .clj-kondo/* 3 | !.clj-kondo/config.edn 4 | !.clj-kondo/net.clojars.john 5 | .cpcache 6 | /out 7 | .nrep-port 8 | target 9 | shadow_dashboard/node_modules 10 | shadow_dashboard/resources/public 11 | !shadow_dashboard/resources/public/index.html 12 | shadow_dashboard/.shadow-cljs 13 | .lsp/ 14 | .portal 15 | .vscode 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 John Newman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `cljs-thread`: `spawn`, `in`, `future`, `pmap`, `pcalls`, `pvalues` and `=>>` 2 | 3 | ## _"One step closer to threads on the web"_ 4 | 5 | `cljs-thread` makes using webworkers take less... work. Eventually, I'd like it to be able to minimize the amount of build tool configuration it takes to spawn workers in Clojurescript too, but that's a longer term goal. 6 | 7 | When you `spawn` a node, it automatically creates connections to all the other nodes, creating a fully connected mesh. 8 | 9 | The `in` macro then abstracts over the message passing infrastructure, with implicit binding conveyance and blocking semantics, allowing you to do work on threads in a manner similar to what you would experience with threads in Clojure and other languages. `cljs-thread` provides familiar constructs like `future`, `pmap`, `pcalls` and `pvalues`. For transducing over large sequences in parallel, the `=>>` thread-last macro is provided. 10 | 11 | ## Getting Started 12 | 13 | ### deps.edn 14 | Place the following in the `:deps` map of your `deps.edn` file: 15 | ``` 16 | ... 17 | net.clojars.john/cljs-thread {:mvn/version "0.1.0-alpha.4"} 18 | ... 19 | ``` 20 | ### Build Tools 21 | #### Shadow-cljs 22 | You'll want to put something like this in the build section of your `shadow-cljs.edn` file: 23 | ``` 24 | :builds 25 | {:repl ; <- just for getting a stable connection for repling, optional 26 | {:target :browser 27 | :output-dir "resources/public" 28 | :modules {:repl {:entries [dashboard.core] 29 | :web-worker true}}} 30 | :sw 31 | {:target :browser 32 | :output-dir "resources/public" 33 | :modules {:sw {:entries [dashboard.core] 34 | :web-worker true}}} 35 | :core 36 | {:target :browser 37 | :output-dir "resources/public" 38 | :modules 39 | {:shared {:entries []} 40 | :screen 41 | {:init-fn dashboard.screen/init! 42 | :depends-on #{:shared}} 43 | :core 44 | {:init-fn dashboard.core/init! 45 | :depends-on #{:shared} 46 | :web-worker true}}}}} 47 | ``` 48 | You can get by with less, but if you want a comfortable repling experience then you want your build file to look something similar to the above. Technically, you can get get by with just a single build artifact if you're careful enough in your worker code to never access `js/window`, `js/document`, etc. But, by having a `screen.js` artifact, you can more easily separate your "main thread" code from your web worker code. Having a separate service worker artifact (`:sw`) is fine because it doesn't need your deps - we only use it for getting blocking semantics in web workers. Having a separate `:repl` artifact is helpful for when you're using an IDE that only allows you to select repl connections on a per-build-id basis (such as VsCode Calva, which I use). 49 | 50 | The sub-project in this repo - `shadow_dashboard` - has an example project with a working build config (similar to the above) that you can use as an example to get started. 51 | 52 | To launch the project in Calva, type `shift-cmd-p` and choose _"Start a Project REPL and Connect"_ and then enable the three build options that come up. When it asks which build you want to connect to, select `:repl`. You can also connect to `:screen` and that will be a stable connection as well. For `:core`, however, in the above configuration, there will be lots of web worker connections pointed to it and you can't control which one will have ended up as the current connection. 53 | 54 | You can choose in Calva which build your are currently connected to by typing `shift-cmd-p` and choosing _"Select CLJS Build Connection"_. 55 | 56 | There are lots of possibilities with build configurations around web workers and eventually there will be an entire wiki article here on just that topic. Please file an issue if you find improved workflows or have questions about how to get things working. 57 | 58 | #### Figwheel 59 | There currently isn't a figwheel build configuration example provided in this repo, but I've had prior versions of this library working on figwheel and I'm hoping to have examples here soon - I just haven't had time. Please submit a PR if you get a great build configuration similar to the one above for shadow. 60 | 61 | #### cljs.main 62 | As with figwheel, a solid set of directions for getting this working with the default `cljs.main` build tools is forthcoming - PRs welcome! 63 | 64 | ### cljs-thread.core/init! 65 | Eventually, once all the different build tools have robust configurations, I would like to iron out a set of default configurations within `cljs-thread` such that things Just Work - just like spawning threads on the JVM. For now, you have to provide `cljs-thread` details on what your build configuration is _in code_ with `cljs-thread.core/init!` like so: 66 | ``` 67 | (thread/init! 68 | {:sw-connect-string "/sw.js" 69 | :repl-connect-string "/repl.js" 70 | :core-connect-string "/core.js"}) 71 | ``` 72 | `:sw-connect-string` defines where your service worker artifact is found, relative to the base directory of your server. Same goes for `:repl-connect-string` and `:core-connect-string`. You can also provide a `:root-connect-string`, `:future-connect-string` and `:injest-connect-string` - if you don't, they will default to your `:core-connect-string`. 73 | 74 | ## Demo 75 | https://johnmn3.github.io/cljs-thread/ 76 | 77 | The `shadow-dashboard` example project contains a standard dashboard demo built on re-frame/mui-v5/comp.el/sync-IndexedDB, with application logic (reg-subs & reg-event functions) moved into a webworker, where only react rendering is handled on the screen thread, allowing for buttery-smooth components backed by large data transformations in the workers. 78 | 79 | Screen Shot 2022-07-30 at 5 46 23 PM 80 | 81 | ## `spawn` 82 | There are a few ways you can `spawn` a worker. 83 | 84 | _No args_: 85 | ```clojure 86 | (def s1 (spawn)) 87 | ``` 88 | This will create a webworker and you'll be able to do something with it afterwards. 89 | 90 | _Only a body_: 91 | ```clojure 92 | (spawn (println :addition (+ 1 2 3))) 93 | ;:addition 6 94 | ``` 95 | This will create a webworker, run the code in it (presumably for side effects) and then terminate the worker. This is considered an _ephemeral worker_. 96 | 97 | _Named worker_: 98 | ```clojure 99 | (def s2 (spawn {:id :s2} (println :hi :from thread/id))) 100 | ;:hi :from :s2 101 | ``` 102 | This creates a webworker named `:s2` and you'll be able to do something with `s2` afterwards. 103 | 104 | _Ephemeral deref_: 105 | ```clojure 106 | (println :ephemeral :result @(spawn (+ 1 2 3))) 107 | ;:ephemeral :result 6 108 | ``` 109 | In workers, `spawn` returns a derefable, which returns the body's return value. In the main/screen thread, it returns a promise: 110 | ```clojure 111 | (-> @(spawn (+ 1 2 3)) 112 | (.then #(println :ephemeral :result %))) 113 | ;:ephemeral :result 6 114 | ``` 115 | > Note: The deref (`@(spawn...`) forces the result to be resolved for `.then`ing. Choosing not to deref on the main thread, as with on workers, implies the evaluation is side effecting and that we don't care about returning the result. 116 | 117 | In a worker, you can force the return of a promise with `(spawn {:promise? true} (+ 1 2 3))` if you'd rather treat it like a promise: 118 | ```clojure 119 | (-> @(js/Promise.all #js [(spawn {:promise? true} 1) (spawn {:promise? true} 2)]) 120 | (.then #(println :res (js->clj %)))) 121 | ;:res [1 2] 122 | ``` 123 | `spawn` has more features, but they mostly match the features of `in` and `future` which we'll go over below. `spawn` has a startup cost that we don't want to have to pay all the time, so you should use it sparingly. 124 | ## `in` 125 | Now that we have some workers, let's do some stuff `in` them: 126 | ```clojure 127 | (in s1 (println :hi :from :s1)) 128 | ;:hi :from :s1 129 | ``` 130 | We can also make chains of execution across multiple workers: 131 | ```clojure 132 | (in s1 133 | (println :now :we're :in :s1) 134 | (in s2 135 | (println :now :we're :in :s2 :through :s1))) 136 | ;:now :we're :in :s1 137 | ;:now :we're :in :s2 :through :s1 138 | ``` 139 | You can also deref the return value of `in`: 140 | ```clojure 141 | @(in s1 (+ 1 @(in s2 (+ 2 3)))) 142 | ;=> 6 143 | ``` 144 | ### Binding conveyance 145 | For most functions, `cljs-thread` will try to automatically convey local bindings, as well as vars local to the invoking namespace, across workers: 146 | ```clojure 147 | (let [x 3] 148 | @(in s1 (+ 1 @(in s2 (+ 2 x))))) 149 | ;=> 6 150 | ``` 151 | That works for both symbols bound to the local scope of the form and to top level defs of the current namespace. So this will work: 152 | ```clojure 153 | (def x 3) 154 | @(in s1 (+ 1 @(in s2 (+ 2 x)))) 155 | ``` 156 | Some things however cannot be transmitted. This will not work: 157 | ```clojure 158 | (def y (atom 3)) 159 | @(in s1 (+ 1 @(in s2 (+ 2 @y)))) 160 | ``` 161 | Atoms will not be serialized. That would break the identity semantics that atoms provide. If the current namespace is being shared between both sides of the invocation and you want to reference an atom that lives on the remote side without conveying the local one, you can either: 162 | - Define it in another namespace, so the local version is not conveyed (it's not a bad idea to define stateful things in a special namespace anyway); or 163 | - Declare the invocation with `:no-globals?` like `@(in s1 {:no-globals? true} (+ 1 @(in s2 (+ 2 @y))))`. This way, you can have `y` defined in the same namespace on both ends of the invocation but you'll be explicitly referencing the one on the remote side; or 164 | - Use an explicit conveyence vector that does not include the local symbol, like `@(in s1 [s2] (+ 1 @(in s2 (+ 2 @y))))`. Using explicit conveyance vectors disables implicit conveyance altogether. 165 | 166 | As mentioned above, you can also explicity define a _conveyance vector_: 167 | ```clojure 168 | @(in s1 [x s2] (+ 1 @(in s2 (+ 2 x)))) 169 | ;=> 6 170 | ``` 171 | Here, `[x s2]` declares that we want to pass `x` (here defined as `3`) through to `s2`. We don't need to declare it again in `s2` because now it is implicitly conveyed as it is in the local scope of the form. 172 | 173 | We could also avoid passing `s2` by simpling referencing it by its `:id`: 174 | ```clojure 175 | @(in s1 [x] (+ 1 @(in :s2 (+ 2 x)))) 176 | ;=> 6 177 | ``` 178 | However, you can't mix both implicit and explicit binding conveyance: 179 | ```clojure 180 | (let [z 3] 181 | @(in s1 [x] (+ 1 @(in :s2 (+ x z))))) 182 | ;=> nil 183 | ``` 184 | Rather, this would work: 185 | ```clojure 186 | (let [z 3] 187 | @(in s1 [x y] (+ 1 @(in :s2 (+ x z))))) 188 | ;=> 7 189 | ``` 190 | The explicit conveyance vector is essentially your escape hatch, for when the simple implicit conveyance isn't enough or is too much. 191 | ### `yield` 192 | 193 | When you want to convert an async javascript function into a synchronous one, `yield` is especially useful: 194 | ```clojure 195 | (->> @(in s1 (-> (js/fetch "http://api.open-notify.org/iss-now.json") 196 | (.then #(.json %)) 197 | (.then #(yield (js->clj % :keywordize-keys true))))) 198 | :iss_position 199 | (println "ISS Position:")) 200 | ;ISS Position: {:latitude 44.4403, :longitude 177.0011} 201 | ``` 202 | > Note: binding conveyance and `yield` also work with `spawn` 203 | > ```clojure 204 | > (let [x 6] 205 | > @(spawn (yield (+ x 2)) (println :i'm :ephemeral))) 206 | > ;:i'm :ephemeral 207 | > ;=> 8 208 | >``` 209 | > You can also nest spawns 210 | >```clojure 211 | > @(spawn (+ 1 @(spawn (+ 2 3)))) 212 | > ;=> 6 213 | >``` 214 | > But that will take 10 to 100 times longer, due to worker startup delay, so make sure that your work is truly heavy and ephemeral. With re-frame, react and a few other megabytes of dev-time dependencies loaded in `/core.js`, that call took me about 1 second to complete - not very fast. 215 | 216 | > Also note: You can use `yield` to temporarily prevent the closing of an ephemeral `spawn` as well: 217 | >```clojure 218 | > @(spawn (js/setTimeout 219 | > #(yield (println :finally!) (+ 1 2 3)) 220 | > 5000)) 221 | > ;:finally! 222 | > ;=> 6 223 | >``` 224 | > Where `6` took 5 seconds to return - handy for async tasks in ephemeral workers. 225 | ## `future` 226 | You don't have to create new workers though. `cljs-thread` comes with a thread pool of workers which you can invoke `future` on. Once invoked, it will grab one of the available workers, do the work on it and then free it when it's done. 227 | ```clojure 228 | (let [x 2] 229 | @(future (+ 1 @(future (+ x 3))))) 230 | ;=> 6 231 | ``` 232 | That took about 20 milliseconds. 233 | 234 | > Note: A single synchronous `future` call will cost you around 8 to 10 milliseconds. A single synchronous `in` call will cost you around 4 to 5 milliseconds, depending on if it needs to be proxied. 235 | 236 | Again, all of these constructs return promises on the main/screen thread: 237 | ```clojure 238 | (-> @(future (-> (js/fetch "http://api.open-notify.org/iss-now.json") 239 | (.then #(.json %)) 240 | (.then #(yield (js->clj % :keywordize-keys true))))) 241 | (.then #(println "ISS Position:" (:iss_position %)))) 242 | ;ISS Position: {:latitude 45.3612, :longitude -110.6497} 243 | ``` 244 | You wouldn't want to do this for such a lite-weight api call, but if you have some large payloads that you need fetched and normalized, it can be convenient to run them in futures for handling off the main thread. 245 | 246 | `cljs-thread`'s blocking semantics are great for achieving synchronous control flow when you need it, but as shown above, it has a performance cost of having to wait on the service worker to proxy results. Therefore, you wouldn't want to use them in very hot loops or for implementing tight algorithms. We can beat single threaded performance though if we're smart about chunking work up into large pieces and fanning it across a pool of workers. You can design your own system for doing that, but `cljs-thread` comes with a solution for pure functions: `=>>`. It also comes with a version of `pmap`. (see the official [`clojure.core/pmap`](https://clojuredocs.org/clojure.core/pmap) for more info) 247 | 248 | ## `pmap` 249 | `pmap` lazily consumes one or more collections and maps a function across them in parallel. 250 | ```clojure 251 | (def z inc) 252 | (let [i +] 253 | (->> [1 2 3 4] 254 | (pmap (fn [x y] (pr :x x :y y) (z (i x y))) [9 8 7 6]) 255 | (take 2))) 256 | ;:x 9 :y 1 257 | ;:x 8 :y 2 258 | ;=> (11 11) 259 | ``` 260 | Taking an example from clojuredocs.org: 261 | ```clojure 262 | ;; A function that simulates a long-running process by calling thread/sleep: 263 | (defn long-running-job [n] 264 | (thread/sleep 1000) ; wait for 1 second 265 | (+ n 10)) 266 | 267 | ;; Use `doall` to eagerly evaluate `map`, which evaluates lazily by default. 268 | 269 | ;; With `map`, the total elapsed time is just over 4 seconds: 270 | user=> (time (doall (map long-running-job (range 4)))) 271 | "Elapsed time: 4012.500000 msecs" 272 | (10 11 12 13) 273 | 274 | ;; With `pmap`, the total elapsed time is just over 1 second: 275 | user=> (time (doall (pmap long-running-job (range 4)))) 276 | "Elapsed time: 1021.500000 msecs" 277 | (10 11 12 13) 278 | ``` 279 | ## `=>>` 280 | [`injest`](https://github.com/johnmn3/injest) is a library that makes it easier to work with transducers. It provides a `x>>` macro for Clojure and Clojurescript that converts thread-last macros (`->>`) into transducer chains. For Clojure, it provides a `=>>` variant that also parallelizes the transducers across a fork-join pool with `r/fold`. However, because we've been lacking blocking semantics in the browser, it was unable to provide the same macro to Clojurescript. 281 | 282 | `cljs-thread` provides the auto-transducifying, auto-parallelizing `=>>` macro that `injest` was missing. 283 | 284 | So, suppose you have some non-trivial work: 285 | ```clojure 286 | (defn flip [n] 287 | (apply comp (take n (cycle [inc dec])))) 288 | ``` 289 | On a single thread, in Chrome, this takes between 16 and 20 seconds (on this computer): 290 | ```clojure 291 | (->> (range) 292 | (map (flip 100)) 293 | (map (flip 100)) 294 | (map (flip 100)) 295 | (take 1000000) 296 | (apply +) 297 | time) 298 | ``` 299 | On Safari and Firefox, that will take between 60 and 70 seconds. 300 | 301 | Let's try it with `=>>`: 302 | ```clojure 303 | (=>> (range) 304 | (map (flip 100)) 305 | (map (flip 100)) 306 | (map (flip 100)) 307 | (take 1000000) 308 | (apply +) 309 | time) 310 | ``` 311 | On Chrome, that'll take only about 8 to 10 seconds. On Safari it takes about 30 seconds and in Firefox it takes around 20 seconds. 312 | 313 | So in Chrome and Safari, you can roughly double your speed and in Firefox you can go three or more times faster. 314 | 315 | By changing only one character, we can double or triple our performance, all while leaving the main thread free to render at 60 frames per second. Notice also how it's lazy :) 316 | 317 | > Note: On the main/screen thread, `=>>` returns a promise. `=>>` defaults to a chunk size of 512. 318 | 319 | ## Stepping debugger 320 | 321 | The blocking semantics that `cljs-thread` provides open up the doors to a lot of things that weren't possible in Clojurescript/Javascript and the browser in general. One of these things is a stepping debugger in the runtime (outside of the JS console debugger). `cljs-thread` ships with a simple example of a stepping debugger: 322 | ```clojure 323 | (dbg 324 | (let [x 1 y 3 z 5] 325 | (println :starting) 326 | (dotimes [i z] 327 | (break (= i y)) 328 | (println :i i)) 329 | (println :done) 330 | x)) 331 | ;:starting 332 | ;:i 0 333 | ;:i 1 334 | ;:i 2 335 | ;=> :starting-dbg 336 | ``` 337 | `dbg` is a convenience macro for sending a form to a debug worker that is constantly listening for new forms to evaluate for the purpose of debugging. `break` stops the execution from running beyond a particular location in the code. It also takes an optional form that defines _when_ the `break` should stop the execution. Upon entering the break, the debugger enters a sub-loop, waiting for forms which can inspect the local variables of the form in the context of the `break`. 338 | 339 | The `in?` macro allows you to send forms to the `break` context within the debugger: 340 | ```clojure 341 | (in? z) 342 | ;=> 5 343 | (in? i) 344 | ;=> 3 345 | (in? [i x y z]) 346 | ;=> [3 1 3 5] 347 | (in? [z y x]) 348 | ;=> [5 3 1] 349 | (in? a) 350 | ;=> nil 351 | ``` 352 | For forms that have symbols that are not locally bound variables in the remote form, you must declare an explicit conveyance vector containing the variables that should be referenced: 353 | ```clojure 354 | (in? [x i] (+ x i)) 355 | ;=> 4 356 | ``` 357 | The `in?` macro above cannot know ahead of time that the form in the `dbg` instance hasn't locally re-bound the `+` symbol. Therefore, for non-simple forms, the conveyance vector is necessary to disambiguate which symbols require resolving in the local context of the remote form and which don't. 358 | 359 | By evaluating `:in/exit`, the running break context exits and the execution procedes to either the next break or until completion. 360 | ```clojure 361 | (in? :in/exit) 362 | ;:i 3 363 | ;:i 4 364 | ;:done 365 | ;=> 1 366 | ``` 367 | 368 | This is just a rudimentary implementation of a stepping debugger. I've added keybindings for usage in Calva. 369 | 370 | It would be nice to implement a sub-repl that wrapped repl evaluations in the `in?` macro until exit. It would also be nice to implement an nrepl middle where for the same thing, transparently filling in the missing bits for cider's debugging middleware, such that editors like emacs and calva can automatically use their debugging workflows in a Clojurescript context. There's [a github issue for this feature](https://github.com/clojure-emacs/cider/issues/1416) in the cider repo and it would be nice to finally be able to unlock this capability for browser development. PRs welcome! 371 | 372 | > Note: There are a host of other use cases that weren't previously possible that become possible with blocking semantics. Another example might be porting Datascript to IndexedDB using a synchronous set/get interface. If there are any other possibilities that come to mind - things you've always wanted to be able to do but weren't able to due to the lack of blocking semantics in the browser - feel free to drop a request in the issues and we can explore it. 373 | 374 | ## Some history 375 | 376 | `cljs-thread` is derived from [`tau.alpha`](https://github.com/johnmn3/tau.alpha) which I released about four years ago. That project evolved towards working with SharedArrayBuffers (SABs). A slightly update version of `tau.alpha` is available here: https://gitlab.com/johnmn3/tau and you can see a demo of the benefits of SABs here: https://simultaneous.netlify.app/ 377 | 378 | At an early point during the development of `tau.alpha` about four years ago, I got blocking semantics to work with these synchronous XHRs and hacking the response from a sharedworker. I eventually abandoned this strategy when I discovered you could get blocking semantics and better performance out of SABs and `js/Atomics`. 379 | 380 | Unfortunately there was lot's of drama around the security of SABs and, years later, they require very constraining security settings, making their usage impractical for some deployment situations. Compared to using typed arrays in `tau.alpha`, you'll never get that same performance in `cljs-thread`, in terms of worker-to-worker communication - in `tau.alpha` you're literally using shared memory - but there's no reason these other features shouldn't be available in non-SAB scenarios, so I figured it would make sense to extract these other bits out into `cljs-thread` and build V2 of `tau.alpha` on top of it. With `tau.beta`, built on `cljs-thread`, I'll be implementing SAB-less variants of `atom`s and `agent`s, with similar semantics to that of Clojure's. Then I'll be implementing SAB-based versions that folks can opt in to if desired. 381 | -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:refer-clojure :exclude [test]) 3 | (:require [org.corfield.build :as bb])) 4 | 5 | (def lib 'net.clojars.john/cljs-thread) 6 | (def version "0.1.0-alpha.3") 7 | 8 | ;; clojure -T:build ci 9 | ;; clojure -T:build deploy 10 | 11 | (def url "https://github.com/johnmn3/cljs-thread") 12 | 13 | (def scm {:url url 14 | :connection "scm:git:git://github.com/johnmn3/cljs-thread.git" 15 | :developerConnection "scm:git:ssh://git@github.com/johnmn3/cljs-thread.git" 16 | :tag version}) 17 | 18 | (defn test "Run the tests." [opts] 19 | (bb/run-tests opts)) 20 | 21 | (defn ci "Run the CI pipeline of tests (and build the JAR)." [opts] 22 | (-> opts 23 | (assoc :lib lib :version version :scm scm) 24 | (bb/run-tests) 25 | (bb/clean) 26 | (bb/jar))) 27 | 28 | (defn deploy "Deploy the JAR to Clojars." [opts] 29 | (-> opts 30 | (assoc :lib lib :version version) 31 | (bb/deploy))) 32 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojurescript {:mvn/version "1.11.60"} 2 | net.clojars.john/injest {:mvn/version "0.1.0-beta.8"}} 3 | :aliases 4 | {:test 5 | {:extra-paths ["test"] 6 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.0"} 7 | io.github.cognitect-labs/test-runner 8 | {:git/tag "v0.5.0" :git/sha "48c3c67"}}} 9 | :build {:deps {io.github.seancorfield/build-clj 10 | {:git/tag "v0.3.1" :git/sha "996ddfa"}} 11 | :ns-default build}}} 12 | -------------------------------------------------------------------------------- /docs/core.js: -------------------------------------------------------------------------------- 1 | importScripts("shared.js"); 2 | (function(){ 3 | 'use strict';var O2=function(a){$APP.Iu($APP.IC,$APP.I([new $APP.X(null,2,5,$APP.Y,["local-store",a],null),$APP.r.g(function(b,c){return $APP.RN.j(b,c,$APP.I([$APP.he]))}),$APP.V.i($APP.R,$APP.hu,!1)]));return a},P2=function(){var a=$APP.t($APP.Iu($APP.IC,$APP.I([new $APP.X(null,1,5,$APP.Y,["local-store"],null),$APP.r.g(function(b){return function(c){return $APP.NC(c,function(d){return $APP.Kt(new $APP.m(null,2,[$APP.Eu,d,$APP.Ft,b],null))})}}),$APP.V.i($APP.R,$APP.hu,!0)])));return $APP.Sd(a,$APP.GC)? 4 | $APP.GC.g(a):null},Q2=function(a){return a},R2=function(a){return $APP.tI.g(a)},T2=function(a){return S2.g(a)},U2=function(a){return $APP.$I.g(a)},V2=function(a){return $APP.rI.g(a)},W2=function(a){return $APP.XG.g(a)},X2=function(a){return $APP.xH.h(a,!1)},Y2=function(a){return $APP.CF.g(a)},Z2=new $APP.M(null,"initialize-db","initialize-db",230998432),$2=new $APP.M(null,"description","description",-1428560544),a3=new $APP.M("pricing","footers","pricing/footers",-1207489408),b3=new $APP.M(null,"auth", 5 | "auth",1389754926),c3=new $APP.M(null,"subheader","subheader",-1028810273),d3=new $APP.M("account-menu","open","account-menu/open",-653810079),e3=new $APP.M(null,"price","price",22129180),f3=new $APP.M("account-menu","close","account-menu/close",735320589),g3=new $APP.M("album","cards","album/cards",344121253),S2=new $APP.M(null,"errors","errors",-908790718),h3=new $APP.M(null,"button-text","button-text",-1800441720),i3=new $APP.M(null,"user-id","user-id",-206822291),j3=new $APP.M(null,"app-state", 6 | "app-state",-1509963278),k3=new $APP.M("home-panel","randomize-chart","home-panel/randomize-chart",-1785812427),l3=new $APP.M(null,"button-variant","button-variant",-939473245),m3=new $APP.M(null,"local-store","local-store",1708979092),n3=new $APP.M(null,"time","time",1385887882),o3=new $APP.M("pricing","tiers","pricing/tiers",186057845);var p3=function(a){return $APP.nE.j($APP.I([$APP.Rl,$APP.LD,$APP.LD,function(b){var c=$APP.Sd($APP.DE.g(b),$APP.IC)?$APP.ag(b,new $APP.X(null,2,5,$APP.Y,[$APP.DE,$APP.IC],null)):$APP.BD(b,$APP.IC),d=$APP.BD(b,$APP.HD);a.h?a.h(c,d):a.call(null,c,d);return b}]))}(function(a){return O2(a)}),q3=new $APP.X(null,2,5,$APP.Y,[p3,$APP.dO],null);$APP.Us()&&$APP.zD($APP.oE,m3,function(a){var b=P2();return $APP.V.i(a,j3,$APP.p(b)?b:$APP.R)}); 7 | var r3=new $APP.m(null,7,[$APP.tI,!1,$APP.XG,!0,$APP.$I,new $APP.X(null,5,5,$APP.Y,[new $APP.m(null,6,[$APP.Rl,0,$APP.zF,"16 July, 2022",$APP.Sj,"Restarted",$APP.iM,"Acme Global",$APP.wF,"5bc114e6-15cf-4e99-8251-6c2e0c543337",$APP.aG,"e59b5976-256a-4ecc-aeea-a926461c71cd"],null),new $APP.m(null,6,[$APP.Rl,1,$APP.zF,"16 July, 2022",$APP.Sj,"Heartbeat failed",$APP.iM,"Energy Enterprise",$APP.wF,"b73ff5ab-9fe3-4395-8564-c01f77cb5dac",$APP.aG,"d460d8df-132d-4712-a780-641114d9dcf5"],null),new $APP.m(null, 8 | 6,[$APP.Rl,2,$APP.zF,"16 July, 2022",$APP.Sj,"Authentication failed",$APP.iM,"General Statistics",$APP.wF,"f4a15ae0-59a5-478a-9958-1ff6a617c363",$APP.aG,"d460d8df-132d-4712-a780-641114d9dcf5"],null),new $APP.m(null,6,[$APP.Rl,3,$APP.zF,"16 July, 2022",$APP.Sj,"Reconnecting to DB",$APP.iM,"Fusion Star Inc",$APP.wF,"34c50198-c808-4841-b2e1-434be4f09534",$APP.aG,"2425173b-4659-49f6-a4ce-8448dcae4475"],null),new $APP.m(null,6,[$APP.Rl,4,$APP.zF,"15 July, 2022",$APP.Sj,"Purging logs",$APP.iM,"Big Agri Corp", 9 | $APP.wF,"a769f187-ee18-46f4-8d65-a2141b4634e2",$APP.aG,"2425173b-4659-49f6-a4ce-8448dcae4475"],null)],null),$APP.rI,new $APP.X(null,9,5,$APP.Y,[new $APP.m(null,2,[n3,"00:00",$APP.aG,0],null),new $APP.m(null,2,[n3,"03:00",$APP.aG,300],null),new $APP.m(null,2,[n3,"06:00",$APP.aG,600],null),new $APP.m(null,2,[n3,"09:00",$APP.aG,800],null),new $APP.m(null,2,[n3,"12:00",$APP.aG,1500],null),new $APP.m(null,2,[n3,"15:00",$APP.aG,2E3],null),new $APP.m(null,2,[n3,"18:00",$APP.aG,2400],null),new $APP.m(null, 10 | 2,[n3,"21:00",$APP.aG,2400],null),new $APP.m(null,2,[n3,"24:00",$APP.aG,null],null)],null),g3,$APP.Th(1,10),o3,new $APP.X(null,3,5,$APP.Y,[new $APP.m(null,5,[$APP.lK,"Free",e3,"0",$2,new $APP.X(null,4,5,$APP.Y,["10 users included","2 GB of storage","Help center access","Email support"],null),h3,"Sign up for free",l3,"outlined"],null),new $APP.m(null,6,[$APP.lK,"Pro",c3,"Most popular",e3,"15",$2,new $APP.X(null,4,5,$APP.Y,["20 users included","10 GB of storage","Help center access","Priority email support"], 11 | null),h3,"Get started",l3,"contained"],null),new $APP.m(null,5,[$APP.lK,"Enterprise",e3,"30",$2,new $APP.X(null,4,5,$APP.Y,["50 users included","30 GB of storage","Help center access","Phone \x26 email support"],null),h3,"Contact us",l3,"outlined"],null)],null),a3,new $APP.X(null,4,5,$APP.Y,[new $APP.m(null,2,[$APP.lK,"Company",$2,new $APP.X(null,4,5,$APP.Y,["Team","History","Contact us","Locations"],null)],null),new $APP.m(null,2,[$APP.lK,"Features",$2,new $APP.X(null,5,5,$APP.Y,["Cool stuff","Random feature", 12 | "Team feature","Developer stuff","Another one"],null)],null),new $APP.m(null,2,[$APP.lK,"Resources",$2,new $APP.X(null,4,5,$APP.Y,["Resource","Resource name","Another resource","Final resource"],null)],null),new $APP.m(null,2,[$APP.lK,"Legal",$2,new $APP.X(null,2,5,$APP.Y,["Privacy policy","Terms of use"],null)],null)],null)],null);$APP.gO.h?$APP.gO.h($APP.IC,Q2):$APP.gO.call(null,$APP.IC,Q2);$APP.gO.h?$APP.gO.h($APP.tI,R2):$APP.gO.call(null,$APP.tI,R2); 13 | $APP.gO.h?$APP.gO.h(S2,T2):$APP.gO.call(null,S2,T2);$APP.KE(Z2,new $APP.X(null,1,5,$APP.Y,[$APP.pE(m3)],null),function(a){a=$APP.U(a);$APP.K.h(a,$APP.IC);a=$APP.K.h(a,j3);a=$APP.dl.j($APP.I([r3,$APP.p(a)?a:$APP.R]));return new $APP.m(null,1,[$APP.IC,a],null)});$APP.eO.i($APP.kJ,q3,function(a,b){$APP.J(b,0,null);$APP.J(b,1,null);return $APP.at.i(a,$APP.tI,$APP.$a)});$APP.gO.h?$APP.gO.h($APP.$I,U2):$APP.gO.call(null,$APP.$I,U2);$APP.gO.h?$APP.gO.h($APP.rI,V2):$APP.gO.call(null,$APP.rI,V2); 14 | $APP.eO.h(k3,function(a){return $APP.at.i(a,$APP.rI,function(b){return function e(d){return new $APP.Ee(null,function(){for(;;){var f=$APP.B(d);if(f){if($APP.Ld(f)){var g=$APP.yc(f),h=$APP.F(g),k=$APP.He(h);a:for(var n=0;;)if(n 2 | 3 | 4 | 5 | 7 | 9 | 10 | 11 |
12 |

Loading Dashboard...

13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/manifest.edn: -------------------------------------------------------------------------------- 1 | [{:module-id :repl, :name :repl, :output-name "repl.js", :entries [dashboard.core], :depends-on nil, :sources ["goog/base.js" "goog/debug/error.js" "goog/dom/nodetype.js" "goog/asserts/asserts.js" "goog/reflect/reflect.js" "goog/math/long.js" "goog/math/integer.js" "goog/dom/asserts.js" "goog/functions/functions.js" "goog/string/typedstring.js" "goog/string/const.js" "goog/i18n/bidi.js" "goog/html/trustedtypes.js" "goog/html/safescript.js" "goog/fs/url.js" "goog/fs/blob.js" "goog/html/trustedresourceurl.js" "goog/string/internal.js" "goog/html/safeurl.js" "goog/html/safestyle.js" "goog/object/object.js" "goog/html/safestylesheet.js" "goog/dom/htmlelement.js" "goog/dom/tagname.js" "goog/array/array.js" "goog/labs/useragent/useragent.js" "goog/labs/useragent/util.js" "goog/labs/useragent/browser.js" "goog/dom/tags.js" "goog/html/safehtml.js" "goog/html/uncheckedconversions.js" "goog/dom/safe.js" "goog/string/string.js" "goog/collections/maps.js" "goog/structs/structs.js" "goog/uri/utils.js" "goog/uri/uri.js" "goog/string/stringbuffer.js" "cljs/core.cljs" "goog/debug/entrypointregistry.js" "goog/labs/useragent/engine.js" "goog/labs/useragent/platform.js" "goog/useragent/useragent.js" "goog/dom/browserfeature.js" "goog/math/math.js" "goog/math/coordinate.js" "goog/math/size.js" "goog/dom/dom.js" "goog/async/nexttick.js" "goog/debug/errorcontext.js" "goog/debug/debug.js" "goog/disposable/idisposable.js" "goog/disposable/dispose.js" "goog/disposable/disposeall.js" "goog/disposable/disposable.js" "goog/events/eventid.js" "goog/events/event.js" "goog/events/browserfeature.js" "goog/events/eventtype.js" "goog/events/browserevent.js" "goog/events/eventlike.js" "goog/events/listenablekey.js" "goog/events/listenable.js" "goog/events/listener.js" "goog/events/listenermap.js" "goog/debug/errorhandler.js" "goog/events/eventtarget.js" "goog/events/eventhandler.js" "goog/events/eventwrapper.js" "goog/events/events.js" "shadow/js.js" "node_modules/object-assign/index.js" "node_modules/react/cjs/react.production.min.js" "node_modules/react/index.js" "clojure/string.cljs" "clojure/walk.cljs" "reagent/debug.cljs" "reagent/impl/util.cljs" "reagent/impl/batching.cljs" "reagent/impl/protocols.cljs" "clojure/set.cljs" "reagent/ratom.cljs" "reagent/impl/component.cljs" "reagent/impl/input.cljs" "reagent/impl/template.cljs" "reagent/core.cljs" "re_frame/interop.cljs" "re_frame/db.cljc" "re_frame/loggers.cljc" "re_frame/utils.cljc" "re_frame/settings.cljc" "re_frame/registrar.cljc" "re_frame/trace.cljc" "re_frame/interceptor.cljc" "re_frame/events.cljc" "re_frame/subs.cljc" "re_frame/router.cljc" "re_frame/fx.cljc" "re_frame/cofx.cljc" "clojure/data.cljs" "re_frame/std_interceptors.cljc" "re_frame/core.cljc" "cljs/tools/reader/impl/utils.cljs" "cljs/tools/reader/reader_types.cljs" "cljs/tools/reader/impl/inspect.cljs" "cljs/tools/reader/impl/errors.cljs" "cljs/tools/reader/impl/commons.cljs" "cljs/tools/reader.cljs" "cljs/tools/reader/edn.cljs" "cljs/reader.cljs" "clojure/edn.cljs" "cljs_thread/util.cljs" "cljs_thread/env.cljs" "cljs_thread/re_state.cljs" "cljs_thread/state.cljs" "cljs_thread/id.cljs" "cljs/pprint.cljs" "cljs_thread/msg.cljs" "cljs_thread/sync.cljs" "cljs_thread/spawn.cljs" "cljs_thread/on_when.cljs" "cljs_thread/in.cljs" "cljs_thread/future.cljs" "cljs/analyzer/impl.cljc" "cljs/analyzer/impl/namespaces.cljc" "cljs/analyzer/passes.cljc" "cljs/analyzer/passes/and_or.cljc" "cljs/env.cljc" "cljs/tagged_literals.cljc" "cljs/analyzer.cljc" "cljs/analyzer/api.cljc" "injest/util.cljc" "injest/data.cljc" "injest/state.cljc" "injest/impl.cljc" "injest/path.cljc" "cljs_thread/injest.cljs" "cljs_thread/repl.cljs" "cljs_thread/root.cljs" "cljs_thread/idb.cljs" "cljs_thread/db.cljs" "cljs_thread/pmap.cljs" "cljs_thread/core.cljs" "cljs_thread/re_frame.cljs" "dashboard/regs/db.cljs" "dashboard/regs/home_panel.cljs" "dashboard/regs/shell.cljs" "dashboard/regs/sign_in.cljs" "dashboard/core.cljs" "shadow/module/repl/append.js"]}] -------------------------------------------------------------------------------- /resources/calva.exports/config.edn: -------------------------------------------------------------------------------- 1 | {:customREPLCommandSnippets 2 | [{:name "cljs-thread send top-level form to debugger" 3 | :repl "cljs" 4 | :snippet "(dbg $top-level-form)" 5 | :key "i"} 6 | {:name "cljs-thread inspect current local in break" 7 | :repl "cljs" 8 | :snippet "(in? $current-form)" 9 | :key "l"} 10 | {:name "cljs-thread exit break" 11 | :repl "cljs" 12 | :snippet "(in? :in/exit)" 13 | :key "e"}]} 14 | ;; TODO: this is a bit too gratuitous - runs on every hover, over everything. Some conditionals down work in snippet code though 15 | ;; :customREPLHoverSnippets 16 | ;; [{:name "eval text on hover" 17 | ;; :repl "cljs" 18 | ;; :snippet "(or (in? $hover-current-form) (quote $hover-current-form))"}]} 19 | -------------------------------------------------------------------------------- /resources/clj-kondo.exports/net.clojars.john/cljs_thread/cljs_thread/core.clj_kondo: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.core 2 | (:refer-clojure :exclude [future])) 3 | 4 | (defn yield []) 5 | 6 | (defmacro in [_id & x] 7 | `(let [~'yield clojure.core/identity 8 | yield# clojure.core/identity] 9 | (yield# 1) 10 | (~'yield 1) 11 | ~@x)) 12 | 13 | (defmacro future [& _x]) 14 | 15 | (defmacro wait [& x] 16 | `(let [~'yield clojure.core/identity 17 | yield# clojure.core/identity] 18 | (yield# 1) 19 | (~'yield 1) 20 | ~@x)) 21 | 22 | (defmacro spawn [& x] 23 | `(let [~'yield clojure.core/identity 24 | yield# clojure.core/identity] 25 | (yield# 1) 26 | (~'yield 1) 27 | ~@x)) 28 | 29 | (defn get-symbols [body] 30 | (->> body 31 | (tree-seq coll? seq) 32 | (rest) 33 | (filter (complement coll?)) 34 | (filter symbol?) 35 | vec)) 36 | 37 | (defmacro in? [& x] 38 | (let [[syms body] (if (and (second x) (vector? (first x))) 39 | [(first x) `~(second x)] 40 | [(get-symbols x) `(do ~@x)])] 41 | `(do '~syms (fn ~syms ~body)))) 42 | -------------------------------------------------------------------------------- /resources/clj-kondo.exports/net.clojars.john/cljs_thread/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {cljs-thread.core/=>> clojure.core/->>} 2 | :hooks {:macroexpand {cljs-thread.core/in cljs-thread.core/in 3 | cljs-thread.core/future cljs-thread.core/future 4 | cljs-thread.core/spawn cljs-thread.core/spawn 5 | cljs-thread.core/wait cljs-thread.core/wait 6 | cljs-thread.core/in? cljs-thread.core/in?}} 7 | :config-in-call 8 | {cljs-thread.core/in 9 | {:linters {:unresolved-symbol {:exclude [yield]}}} 10 | cljs-thread.core/future 11 | {:linters {:unresolved-symbol {:exclude [yield]}}} 12 | cljs-thread.core/wait 13 | {:linters {:unresolved-symbol {:exclude [yield]}}} 14 | cljs-thread.core/spawn 15 | {:linters {:unresolved-symbol {:exclude [yield]}}}}} 16 | -------------------------------------------------------------------------------- /shadow_dashboard/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnmn3/cljs-thread/8eaad5da548a71674f11406d229326e71e4ab284/shadow_dashboard/.DS_Store -------------------------------------------------------------------------------- /shadow_dashboard/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/lib" 2 | "src/main" 3 | "src/screen"] 4 | :deps {reagent/reagent {:mvn/version "1.1.0"} 5 | re-frame/re-frame {:mvn/version "1.3.0-rc3"} 6 | arttuka/reagent-material-ui {:mvn/version "5.6.2-1"} 7 | net.cgrand/macrovich {:mvn/version "0.2.1"} 8 | org.clojure/tools.logging {:mvn/version "1.1.0"} 9 | binaryage/devtools {:mvn/version "1.0.4"} 10 | metosin/reitit {:mvn/version "0.5.5"} 11 | day8/shadow-git-inject {:mvn/version "0.0.5"} 12 | johnmn3/comp.el {:git/url "https://github.com/johnmn3/comp.el.git" 13 | :sha "8dc9f904d2328cbe62230044d299f71734df0168"} 14 | BrianChevalier/radiant {:git/url "https://github.com/johnmn3/radiant.git" 15 | :sha "20e5dedd09275f27b2146a629a0b92078963db7b"} 16 | cljs-thread/cljs-thread {:local/root "../"}} 17 | :aliases 18 | {:cljs 19 | {:extra-deps {thheller/shadow-cljs {:mvn/version "2.21.0"} 20 | binaryage/devtools {:mvn/version "1.0.4"}}}}} 21 | -------------------------------------------------------------------------------- /shadow_dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard-cljs-thread-re-frame-comp.el", 3 | "scripts": { 4 | "watch": "npx shadow-cljs watch client", 5 | "release": "npx shadow-cljs release client", 6 | "build-report": "npx shadow-cljs run shadow.cljs.build-report client target/build-report.html" 7 | }, 8 | "devDependencies": { 9 | "karma": "6.3.7", 10 | "karma-chrome-launcher": "3.1.0", 11 | "karma-cljs-test": "0.1.0", 12 | "karma-junit-reporter": "2.0.1", 13 | "karma-phantomjs-launcher": "1.0.4", 14 | "shadow-cljs": "2.21.0" 15 | }, 16 | "dependencies": { 17 | "@emotion/react": "11.9.0", 18 | "@emotion/styled": "11.8.1", 19 | "@mui/material": "5.6.2", 20 | "core-js": "^3.6.5", 21 | "create-react-class": "^15.6.3", 22 | "events": "^3.2.0", 23 | "highlight.js": "11.1.0", 24 | "process": "^0.11.10", 25 | "react": "17.0.2", 26 | "react-chartjs-2": "^4.3.1", 27 | "react-dom": "17.0.2", 28 | "react-transition-group": "^4.4.2", 29 | "recharts": "^2.1.12" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /shadow_dashboard/resources/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnmn3/cljs-thread/8eaad5da548a71674f11406d229326e71e4ab284/shadow_dashboard/resources/.DS_Store -------------------------------------------------------------------------------- /shadow_dashboard/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 11 |
12 |

Loading Dashboard...

13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /shadow_dashboard/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:nrepl 2 | {:port 8777} 3 | 4 | :dev-http 5 | {8280 6 | {:root "resources/public" 7 | :push-state/headers {"content-type" "text/html; charset=utf-8" 8 | "Cross-Origin-Opener-Policy" "same-origin" 9 | "Cross-Origin-Embedder-Policy" "credentialless" #_"require-corp"}}} 10 | 11 | :deps {:aliases [:cljs]} 12 | 13 | 14 | :builds 15 | {:repl ; <- just for getting a stable connection for repling, optional 16 | {:target :browser 17 | :output-dir "resources/public" 18 | :modules {:repl {:entries [dashboard.core] 19 | :web-worker true}}} 20 | :sw 21 | {:target :browser 22 | :output-dir "resources/public" 23 | :modules {:sw {:entries [cljs-thread.sw] 24 | :web-worker true}}} 25 | :core 26 | {:target :browser 27 | :output-dir "resources/public" ; <- necessary because sw.js must be in root, but output-dir doesn't work per module 28 | :js-options {:resolve {"highlight.js" {:target :npm :require "highlight.js/lib/core"} 29 | "lowlight" {:target :npm :require "lowlight/lib/core"}}} 30 | :modules 31 | {:shared {:entries []} 32 | :screen 33 | {:init-fn dashboard.screen/init! 34 | :depends-on #{:shared}} 35 | :core 36 | {:init-fn dashboard.core/init! 37 | :depends-on #{:shared} 38 | :web-worker true}}}}} 39 | -------------------------------------------------------------------------------- /shadow_dashboard/src/lib/cljs_thread/re_frame.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.re-frame 2 | (:require 3 | [reagent.ratom :as ra] 4 | [re-frame.core :as re-frame] 5 | [reagent.core :as r] 6 | [cljs-thread.re-state :as state] 7 | [cljs-thread.core :refer [in]] 8 | [cljs-thread.env :as env])) 9 | 10 | (def ^:export reg-sub 11 | (if-not (env/in-core?) 12 | identity 13 | (fn [& args] 14 | (apply re-frame/reg-sub args)))) 15 | 16 | (defn ^:export dispatch 17 | [event] 18 | (if (env/in-core?) 19 | (re-frame/dispatch event) 20 | (in :core (re-frame.core/dispatch event)))) 21 | 22 | (defonce ^:export trackers 23 | (atom {})) 24 | 25 | (defn- ^:export reg-tracker 26 | [ts id sub-v] 27 | (if ts 28 | (update ts :subscribers conj id) 29 | (let [new-sub (re-frame/subscribe sub-v)] 30 | {:tracker (r/track! 31 | #(let [sub @new-sub] 32 | @(in :screen (swap! state/subscriptions assoc sub-v sub))) 33 | []) 34 | :subscribers #{id}}))) 35 | 36 | (defn ^:export add-sub 37 | [[id sub-v]] 38 | (swap! trackers update sub-v reg-tracker id sub-v)) 39 | 40 | (defn- ^:export unreg-tracker 41 | [ts id sub-v] 42 | (if-let [t (get ts sub-v)] 43 | (let [{:keys [tracker subscribers]} t 44 | new-subscribers (disj subscribers id)] 45 | (if (< 0 (count new-subscribers)) 46 | (assoc ts sub-v {:tracker tracker :subscribers new-subscribers}) 47 | (do 48 | (r/dispose! tracker) 49 | @(in :screen (swap! state/subscriptions dissoc sub-v)) 50 | (dissoc ts sub-v)))) 51 | ts)) 52 | 53 | (defn ^:export dispose-sub 54 | [[id sub-v]] 55 | (swap! trackers unreg-tracker id sub-v)) 56 | 57 | (defn ^:export subscribe 58 | [sub-v & [alt]] 59 | (if (env/in-core?) 60 | (re-frame/subscribe sub-v) 61 | (let [id (str (random-uuid))] 62 | (in :core (add-sub [id sub-v])) 63 | (ra/make-reaction 64 | #(get @state/subscriptions sub-v alt) 65 | :on-dispose 66 | #(in :core (dispose-sub [id sub-v])))))) 67 | -------------------------------------------------------------------------------- /shadow_dashboard/src/lib/cljs_thread/re_state.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.re-state 2 | (:require 3 | [reagent.core :as r])) 4 | 5 | (defonce ^:export subscriptions 6 | (r/atom {})) 7 | -------------------------------------------------------------------------------- /shadow_dashboard/src/main/dashboard/core.cljs: -------------------------------------------------------------------------------- 1 | (ns dashboard.core 2 | (:require 3 | [re-frame.core :as rf] 4 | [dashboard.regs.db] 5 | [dashboard.regs.home-panel] 6 | [dashboard.regs.shell] 7 | [dashboard.regs.sign-in] 8 | [cljs-thread.env :as env] 9 | [cljs-thread.core :as thread :refer [future spawn in =>> dbg break in? pmap pcalls pvalues]])) 10 | 11 | (enable-console-print!) 12 | 13 | (comment 14 | 15 | (println :sab? (exists? js/SharedArrayBuffer)) 16 | 17 | :end) 18 | 19 | (defn init! [] 20 | (when (env/in-core?) 21 | (rf/dispatch-sync [:initialize-db]) 22 | (rf/clear-subscription-cache!))) 23 | 24 | (defn flip [n] 25 | (apply comp (take n (cycle [inc inc dec])))) 26 | 27 | ;; uncomment for advanced compile tests 28 | #_ 29 | (when (env/in-core?) 30 | (future 31 | 32 | (->> @s/peers 33 | keys 34 | (filter (complement #{:sw})) 35 | (mapv #(in % (println thread/id :here)))) 36 | 37 | (->> @s/peers 38 | keys 39 | (filter (complement #{:sw})) 40 | (mapv #(in % (str thread/id :here))) 41 | (map deref) 42 | (println :rollcall)) 43 | 44 | (print '(=>> (range) (map (flip 10)) (map (flip 10)) (map (flip 10)) (take 10)) \newline 45 | (=>> (range) 46 | (map (flip 10)) 47 | (map (flip 10)) 48 | (map (flip 10)) 49 | (take 10))) 50 | 51 | #_ 52 | (print '->> 'large-computation \newline 53 | (->> (range) 54 | (map (flip 100)) 55 | (map (flip 100)) 56 | (map (flip 100)) 57 | (take 1000000) 58 | (apply +) 59 | time)) 60 | 61 | #_ 62 | (print '=>> 'large-computation \newline 63 | (=>> (range) 64 | (map (flip 100)) 65 | (map (flip 100)) 66 | (map (flip 100)) 67 | (take 1000000) 68 | (apply +) 69 | time)))) 70 | 71 | (comment 72 | 73 | (def z inc) 74 | (let [i +] 75 | (->> [1 2 3 4] 76 | (pmap (fn [x y] (pr :x x :y y) (z (i x y))) [9 8 7 6]) 77 | (take 2))) 78 | 79 | (defn long-running-job [n] 80 | (thread/sleep 1000) ; wait for 1 second 81 | (+ n 10)) 82 | 83 | (future (println :res (time (doall (map long-running-job (range 4)))))) 84 | 85 | (future (println :res (time (doall (pmap long-running-job (range 4)))))) 86 | 87 | (pcalls #(long-running-job 1) #(long-running-job 2)) 88 | 89 | (pvalues 90 | (long-running-job 1) 91 | (long-running-job 2) 92 | (long-running-job 3) 93 | (long-running-job 4) 94 | (long-running-job 5)) 95 | 96 | (dbg 97 | (let [x 1 y 3 z 5] 98 | (println :starting) 99 | (dotimes [i z] 100 | (break (= i y)) 101 | (println :i i)) 102 | (println :done) 103 | 4)) 104 | ;:starting 105 | ;:i 0 106 | ;:i 1 107 | ;:i 2 108 | ;=> :starting-dbg 109 | 110 | (in? [x i] (+ x i)) 111 | ;=> 4 112 | (in? z) 113 | ;=> 5 114 | (in? i) 115 | ;=> 3 116 | (in? [i x y z]) 117 | ;=> [3 1 3 5] 118 | (in? [z y x]) 119 | ;=> [5 3 1] 120 | (in? a) 121 | ;=> nil 122 | (in? :in/exit) 123 | ;:i 3 124 | ;:i 4 125 | ;:done 126 | ;=> 4 127 | 128 | (def s1 (spawn)) 129 | 130 | (spawn (println :addition (+ 1 2 3))) 131 | 132 | (def s2 (spawn {:id :s2} (println :hi :from thread/id))) 133 | 134 | (println :ephemeral :result @(spawn (+ 1 2 3))) 135 | 136 | (in :screen 137 | (-> @(spawn (println :working) (+ 1 2 3)) 138 | (.then #(println :ephemeral :result %)))) 139 | 140 | (-> (js/Promise.all #js [(spawn {:promise? true} 1) 141 | (spawn {:promise? true} 2)]) 142 | (.then #(println :res (js->clj %)))) 143 | 144 | (in s1 (println :hi :from :s1)) 145 | 146 | (in s1 147 | (println ":now :we're :in :s1") 148 | (in s2 149 | (println ":now :we're :in :s2 :through :s1"))) 150 | 151 | @(in s1 (+ 1 @(in s2 (+ 2 3)))) 152 | 153 | (let [x 3] 154 | @(in s1 (+ 1 @(in s2 (+ 2 x))))) 155 | 156 | (def x 3) 157 | @(in s1 (+ 1 @(in s2 (+ 2 x)))) 158 | 159 | (def x 3) 160 | @(in s1 (+ 1 @(in s2 (+ 2 x)))) 161 | 162 | @(in s1 [x s2] (+ 1 @(in s2 (+ 2 x)))) 163 | 164 | (let [y 3] 165 | @(in s1 (+ 1 @(in s2 (+ x y))))) 166 | 167 | (let [y 3] 168 | @(in s1 [x y s2] (+ 1 @(in s2 (+ x y))))) 169 | 170 | (let [x 6] 171 | @(spawn (yield (+ x 2)) (println :i'm :ephemeral :in thread/id))) 172 | 173 | @(spawn (+ 1 @(spawn (+ 2 3)))) 174 | 175 | (->> @(in s1 (-> (js/fetch "http://api.open-notify.org/iss-now.json") 176 | (.then #(.json %)) 177 | (.then #(yield (js->clj % :keywordize-keys true))))) 178 | :iss_position 179 | (println "ISS Position:")) 180 | 181 | @(spawn (js/setTimeout 182 | #(yield (println :finally!) (+ 1 2 3)) 183 | 5000)) 184 | 185 | @(in :screen 186 | (-> @(spawn (js/setTimeout 187 | #(yield (println :finally!) (+ 1 2 3)) 188 | 5000)) 189 | (.then #(yield %)))) 190 | 191 | @(future (+ 1 @(future (+ 2 3)))) 192 | 193 | (in :screen 194 | (-> @(future (-> (js/fetch "http://api.open-notify.org/iss-now.json") 195 | (.then #(.json %)) 196 | (.then #(yield (js->clj % :keywordize-keys true))))) 197 | (.then #(println "ISS Position:" (:iss_position %))))) 198 | 199 | (=>> (range 10000) 200 | (map inc) 201 | (filter odd?) 202 | (map (flip 1000)) 203 | (mapcat #(do [% (dec %)])) 204 | (partition-by #(= 0 (mod % 5))) 205 | (map (partial apply +)) 206 | (map (partial + 10)) 207 | (map (flip 1000)) 208 | (map #(do {:temp-value %})) 209 | (map :temp-value) 210 | (map (flip 1000)) 211 | (filter even?) 212 | (apply +) 213 | time) 214 | 215 | (->> (range 10000) 216 | (map (flip 10000)) 217 | (apply +) 218 | time) 219 | 220 | (=>> (range 10000) 221 | (map (flip 10000)) 222 | (apply +) 223 | time) 224 | 225 | (def y dec) 226 | (let [x inc] 227 | (=>> (range 10) 228 | (map (comp x y)) 229 | (apply +))) 230 | 231 | (=>> (range 1000) 232 | (map inc) 233 | (filter odd?) 234 | (map (flip 100000)) 235 | (mapcat #(do [% (dec %)])) 236 | (partition-by #(= 0 (mod % 5))) 237 | (map (partial apply +)) 238 | (map (partial + 10)) 239 | (map (flip 100000)) 240 | (map #(do {:temp-value %})) 241 | (map :temp-value) 242 | (map (flip 100000)) 243 | (filter even?) 244 | (apply +) 245 | time) 246 | 247 | (time @(spawn (+ 1 @(spawn (+ 2 3))))) 248 | 249 | (let [x 2] 250 | @(future (+ 1 @(future (+ x 3))))) 251 | 252 | (time @(future (+ 1 @(future (+ 2 3))))) 253 | 254 | (time @(future (+ 2 3))) 255 | 256 | (time @(in :core (+ 2 3))) 257 | 258 | ;; work in progress - transferring typed arrays 259 | (def ui8a (js/Uint8Array. 8)) 260 | (amap ui8a i ret (aset ui8a i i)) 261 | (def a {:ar ui8a :afn #(amap % i ret (aset % i (* 20 (aget % i))))}) 262 | (println :ar (let [{:keys [afn ar]} a] 263 | (afn ar))) 264 | ;:ar #js [0 20 40 60 80 100 120 140] 265 | 266 | (future (println :res (let [{:keys [afn ar]} a] 267 | (afn ar)))) 268 | ;:res #js [nil] ;; broken :( 269 | ;; Also can't do `(future (println :a a :a-again a))` 270 | 271 | ;; build again, because transfer neutered buffer 272 | (def ui8a (js/Uint8Array. 8)) 273 | (amap ui8a i ret (aset ui8a i i)) 274 | (def a {:ar ui8a :afn #(amap % i ret (aset % i (* 20 (aget % i))))}) 275 | 276 | (def s1 (spawn)) 277 | 278 | (in s1 (println :res (let [{:keys [afn ar]} a] 279 | (afn ar)))) 280 | ;:res #js [nil] ;; also broken 281 | (in s1 (println :res (let [{:keys [afn ar]} a] 282 | ar))) 283 | ;:res #object [Uint8Array 0 1 2 3 4 5 6 7] ;; prints correctly though 284 | :end) 285 | -------------------------------------------------------------------------------- /shadow_dashboard/src/main/dashboard/regs/db.cljs: -------------------------------------------------------------------------------- 1 | (ns dashboard.regs.db 2 | (:require 3 | [re-frame.core :as rf] 4 | [cljs-thread.env :refer [in-core?]] 5 | [cljs-thread.re-frame :refer [reg-sub]] 6 | [cljs-thread.db :refer [db-get db-set!]])) 7 | 8 | (def ls-key "local-store") 9 | 10 | (defn app-state->local-store 11 | "Puts app-state into local forage" 12 | [app-state] 13 | (db-set! ls-key app-state)) 14 | 15 | (def ->local-store (rf/after app-state->local-store)) 16 | 17 | (def app-state-interceptors 18 | [->local-store 19 | rf/trim-v]) 20 | 21 | (when (in-core?) 22 | (rf/reg-cofx 23 | :local-store 24 | (fn [cofx second-param] 25 | (let [local-store (or (db-get ls-key) {})] 26 | (assoc cofx :app-state local-store))))) 27 | 28 | (def default-db 29 | {:dark-theme? false 30 | :drawer/open? true 31 | :home-panel/orders [{:id 0 32 | :date "16 July, 2022" 33 | :name "Restarted" 34 | :ship-to "Acme Global" 35 | :payment-method "5bc114e6-15cf-4e99-8251-6c2e0c543337" 36 | :amount "e59b5976-256a-4ecc-aeea-a926461c71cd"} 37 | {:id 1 38 | :date "16 July, 2022" 39 | :name "Heartbeat failed" 40 | :ship-to "Energy Enterprise" 41 | :payment-method "b73ff5ab-9fe3-4395-8564-c01f77cb5dac" 42 | :amount "d460d8df-132d-4712-a780-641114d9dcf5"} 43 | {:id 2 44 | :date "16 July, 2022" 45 | :name "Authentication failed" 46 | :ship-to "General Statistics" 47 | :payment-method "f4a15ae0-59a5-478a-9958-1ff6a617c363" 48 | :amount "d460d8df-132d-4712-a780-641114d9dcf5"} 49 | {:id 3 50 | :date "16 July, 2022" 51 | :name "Reconnecting to DB" 52 | :ship-to "Fusion Star Inc" 53 | :payment-method "34c50198-c808-4841-b2e1-434be4f09534" 54 | :amount "2425173b-4659-49f6-a4ce-8448dcae4475"} 55 | {:id 4 56 | :date "15 July, 2022" 57 | :name "Purging logs" 58 | :ship-to "Big Agri Corp" 59 | :payment-method "a769f187-ee18-46f4-8d65-a2141b4634e2" 60 | :amount "2425173b-4659-49f6-a4ce-8448dcae4475"}] 61 | :home-panel/chart-data [{:time "00:00" :amount 0} 62 | {:time "03:00" :amount 300} 63 | {:time "06:00" :amount 600} 64 | {:time "09:00" :amount 800} 65 | {:time "12:00" :amount 1500} 66 | {:time "15:00" :amount 2000} 67 | {:time "18:00" :amount 2400} 68 | {:time "21:00" :amount 2400} 69 | {:time "24:00" :amount nil}] 70 | :album/cards (range 1 10) 71 | :pricing/tiers [{:title "Free" 72 | :price "0" 73 | :description ["10 users included" 74 | "2 GB of storage" 75 | "Help center access" 76 | "Email support"] 77 | :button-text "Sign up for free" 78 | :button-variant "outlined"} 79 | {:title "Pro" 80 | :subheader "Most popular" 81 | :price "15" 82 | :description ["20 users included" 83 | "10 GB of storage" 84 | "Help center access" 85 | "Priority email support"] 86 | :button-text "Get started" 87 | :button-variant "contained"} 88 | {:title "Enterprise" 89 | :price "30" 90 | :description ["50 users included" 91 | "30 GB of storage" 92 | "Help center access" 93 | "Phone & email support"] 94 | :button-text "Contact us" 95 | :button-variant "outlined"}] 96 | :pricing/footers [{:title "Company" 97 | :description ["Team" "History" "Contact us" "Locations"]} 98 | {:title "Features" 99 | :description ["Cool stuff" "Random feature" 100 | "Team feature" "Developer stuff" "Another one"]} 101 | {:title "Resources" 102 | :description ["Resource" "Resource name" "Another resource" "Final resource"]} 103 | {:title "Legal" 104 | :description ["Privacy policy" "Terms of use"]}]}) 105 | 106 | ;; subs 107 | 108 | (reg-sub 109 | :db 110 | (fn [db] 111 | db)) 112 | 113 | (reg-sub 114 | :dark-theme? 115 | (fn [db] 116 | (:dark-theme? db))) 117 | 118 | (reg-sub 119 | :errors 120 | (fn [db] 121 | (:errors db))) 122 | 123 | ;;; events 124 | 125 | (rf/reg-event-fx 126 | :initialize-db 127 | [(rf/inject-cofx :local-store)] 128 | (fn [{:as cofx :keys [db app-state]} second-param] 129 | (let [new-db (merge default-db (or app-state {}))] 130 | {:db new-db}))) 131 | 132 | (rf/reg-event-db 133 | :toggle-dark-theme 134 | app-state-interceptors 135 | (fn [app-state [stuff s2]] 136 | (update app-state :dark-theme? not))) 137 | -------------------------------------------------------------------------------- /shadow_dashboard/src/main/dashboard/regs/home_panel.cljs: -------------------------------------------------------------------------------- 1 | (ns dashboard.regs.home-panel 2 | (:require 3 | [re-frame.core :as rf] 4 | [cljs-thread.re-frame :refer [reg-sub]])) 5 | 6 | ;;; Subs 7 | 8 | (reg-sub 9 | :home-panel/orders 10 | (fn [db] 11 | (:home-panel/orders db))) 12 | 13 | (reg-sub 14 | :home-panel/chart-data 15 | (fn [db] 16 | (:home-panel/chart-data db))) 17 | 18 | ;;; Events 19 | 20 | (rf/reg-event-db 21 | :home-panel/randomize-chart 22 | (fn [db _] 23 | (update db :home-panel/chart-data 24 | (fn [data] 25 | (for [d data 26 | :let [time (:time d) 27 | amount (rand-int 3000)]] 28 | {:time time :amount amount}))))) 29 | -------------------------------------------------------------------------------- /shadow_dashboard/src/main/dashboard/regs/shell.cljs: -------------------------------------------------------------------------------- 1 | (ns dashboard.regs.shell 2 | (:require 3 | [dashboard.regs.db :as db] 4 | [re-frame.core :as rf] 5 | [cljs-thread.re-frame :refer [reg-sub]])) 6 | 7 | ;; subs 8 | (reg-sub 9 | :drawer/open? 10 | (fn [db] 11 | (:drawer/open? db))) 12 | 13 | (reg-sub 14 | :account-menu/open? 15 | (fn [db] 16 | (:account-menu/open? db false))) 17 | 18 | ;; events 19 | 20 | (rf/reg-event-db 21 | :drawer/open 22 | db/app-state-interceptors 23 | (fn [db _] 24 | (assoc db :drawer/open? true))) 25 | 26 | (rf/reg-event-db 27 | :drawer/close 28 | db/app-state-interceptors 29 | (fn [db _] 30 | (assoc db :drawer/open? false))) 31 | 32 | (rf/reg-event-db 33 | :account-menu/open 34 | (fn [db _] 35 | (assoc db :account-menu/open? true))) 36 | 37 | (rf/reg-event-db 38 | :account-menu/close 39 | (fn [db _] 40 | (assoc db :account-menu/open? false))) 41 | 42 | (rf/reg-event-db 43 | :account-menu/toggle 44 | (fn [db _] 45 | (update db :account-menu/open? not))) 46 | -------------------------------------------------------------------------------- /shadow_dashboard/src/main/dashboard/regs/sign_in.cljs: -------------------------------------------------------------------------------- 1 | (ns dashboard.regs.sign-in 2 | (:require 3 | [re-frame.core :as rf] 4 | [cljs-thread.re-frame :refer [reg-sub]])) 5 | 6 | ;;; Subs 7 | 8 | (reg-sub 9 | :sign-in/errors 10 | :<- [:errors] 11 | (fn [errors _] 12 | (:sign-in errors))) 13 | 14 | ;;; Events 15 | 16 | (rf/reg-event-fx 17 | :sign-in 18 | (fn [{:keys [db]} [_ {:keys [userid password remember?]}]] 19 | (if (= password "top-secret") 20 | {:db (-> db 21 | (assoc-in [:auth :user-id] userid) 22 | (assoc-in [:auth :remember?] remember?)) 23 | :navigate! [:routes/home]} 24 | {:db (assoc-in db [:errors :sign-in] 25 | {:password "Wrong Password! (should be \"top-secret\")"})}))) 26 | 27 | (rf/reg-event-db 28 | :sign-in/clear-errors 29 | (fn [db [_ field]] 30 | (update-in db [:errors :sign-in] dissoc field))) 31 | -------------------------------------------------------------------------------- /shadow_dashboard/src/screen/dashboard/footer.cljs: -------------------------------------------------------------------------------- 1 | (ns dashboard.footer 2 | (:require 3 | [comp.el :as c])) 4 | 5 | (defn copyright [] 6 | [c/text {:variant "body2" :color "textSecondary" :align "center" :padding 5} 7 | [c/link {:color "inherit" :href "https://github.com/johnmn3/cljs-thread"} 8 | "cljs-thread dashboard "] 9 | (.getFullYear (js/Date.))]) 10 | -------------------------------------------------------------------------------- /shadow_dashboard/src/screen/dashboard/routes.cljs: -------------------------------------------------------------------------------- 1 | (ns dashboard.routes 2 | (:require 3 | [re-frame.core :as rf] 4 | [reitit.frontend] 5 | [reitit.frontend.easy :as rfe] 6 | [reitit.frontend.controllers :as rfc] 7 | [dashboard.views.sign-in :as sign-in] 8 | [dashboard.views.home-panel :as home-panel])) 9 | 10 | 11 | (defn log-fn [& args] 12 | (fn [& _] 13 | #_(apply js/console.log args))) 14 | 15 | 16 | ;;; Subs 17 | 18 | (rf/reg-sub 19 | :current-route 20 | (fn [db] 21 | (:current-route db))) 22 | 23 | ;;; Events 24 | 25 | (rf/reg-event-fx 26 | :navigate 27 | (fn [_cofx [_ & route]] 28 | {:navigate! route})) 29 | 30 | ;; Triggering navigation from events. 31 | (rf/reg-fx 32 | :navigate! 33 | (fn [route] 34 | (apply rfe/push-state route))) 35 | 36 | (rf/reg-event-db 37 | :navigated 38 | (fn [db [_ new-match]] 39 | (let [old-match (:current-route db) 40 | controllers (rfc/apply-controllers (:controllers old-match) new-match)] 41 | (assoc db :current-route (assoc new-match :controllers controllers))))) 42 | 43 | ;;; Routes 44 | 45 | (def routes 46 | ["/" 47 | ["" 48 | {:name :routes/home 49 | :view home-panel/main 50 | :link-text "Home" 51 | :icon home-panel/drawer-icon 52 | :controllers 53 | [{:start (log-fn "Entering home page") 54 | :stop (log-fn "Leaving home page")}]}] 55 | ["sign-in" 56 | {:name :routes/sign-in 57 | :view sign-in/main 58 | :link-text "Sign In" 59 | :icon sign-in/drawer-icon 60 | :controllers 61 | [{:start (log-fn "Entering sign-in") 62 | :stop (log-fn "Leaving sign-in")}]}]]) 63 | 64 | (def router 65 | (reitit.frontend/router 66 | routes 67 | {})) 68 | 69 | (defn on-navigate [new-match] 70 | (when new-match 71 | (rf/dispatch [:navigated new-match]))) 72 | 73 | (defn init-routes! [] 74 | (rfe/start! 75 | router 76 | on-navigate 77 | {:use-fragment true})) 78 | -------------------------------------------------------------------------------- /shadow_dashboard/src/screen/dashboard/screen.cljs: -------------------------------------------------------------------------------- 1 | (ns dashboard.screen 2 | (:require 3 | [reagent.dom :as rdom] 4 | [comp.el :as c] 5 | [cljs-thread.core :as thread] 6 | [cljs-thread.re-frame :refer [subscribe]] 7 | [dashboard.routes :as routes] 8 | [dashboard.shell :as shell])) 9 | 10 | ;;; Config 11 | (enable-console-print!) 12 | ; for docs release 13 | ;; #_ 14 | (thread/init! 15 | {:sw-connect-string "/cljs-thread/sw.js" 16 | :repl-connect-string "/cljs-thread/repl.js" 17 | :core-connect-string "/cljs-thread/core.js"}) 18 | 19 | #_ 20 | (thread/init! 21 | {:sw-connect-string "/sw.js" 22 | :repl-connect-string "/repl.js" 23 | :core-connect-string "/core.js"}) 24 | 25 | (def debug? 26 | ^boolean goog.DEBUG) 27 | 28 | (defn dev-setup [] 29 | (when debug? 30 | (println "dev mode"))) 31 | 32 | ;; Styles 33 | (defn custom-theme [dark-theme?] 34 | {:palette {:mode (if dark-theme? "dark" "light") 35 | :primary {:main "#ef5350"} 36 | :secondary {:main "#3f51b5"}} 37 | :status {:danger "red"}}) 38 | 39 | ;; Views 40 | 41 | (defn main-shell [{:keys [router]}] 42 | (let [dark-theme? @(subscribe [:dark-theme?])] 43 | [:<> 44 | [c/css-baseline] 45 | [c/theme-provider (c/create-theme (custom-theme dark-theme?)) 46 | [shell/styled-dashboard]]])) 47 | 48 | ;;; Setup on screen 49 | 50 | (defn ^{:after-load true, :dev/after-load true} mount-root [] 51 | (routes/init-routes!) 52 | (rdom/render [main-shell] 53 | (.getElementById js/document "app"))) 54 | 55 | (defn init! [] 56 | (dev-setup) 57 | (mount-root)) 58 | -------------------------------------------------------------------------------- /shadow_dashboard/src/screen/dashboard/shell.cljs: -------------------------------------------------------------------------------- 1 | (ns dashboard.shell 2 | (:require 3 | [reagent.core :as reagent] 4 | [comp.el :as c] 5 | [re-frame.core] 6 | [cljs-thread.re-frame :refer [dispatch subscribe]] 7 | [reitit.frontend.easy :as rfe] 8 | [dashboard.routes :as routes] 9 | [reitit.core :as reitit])) 10 | 11 | ;; styles 12 | 13 | (def classes 14 | (let [prefix "rmui-dash"] 15 | {:root (str prefix "-root") 16 | :toolbar (str prefix "-toolbar") 17 | :toolbar-icon (str prefix "-toolbar-icon") 18 | :app-bar (str prefix "-app-bar") 19 | :app-bar-shift (str prefix "-app-bar-shift") 20 | :menu-button (str prefix "-menu-button") 21 | :menu-button-hidden (str prefix "-menu-button-hidden") 22 | :title (str prefix "-title") 23 | :drawer-paper (str prefix "-drawer-paper") 24 | :drawer-paper-close (str prefix "-drawer-paper-close") 25 | :app-bar-spacer (str prefix "-app-bar-spacer") 26 | :content (str prefix "-content") 27 | :container (str prefix "-container") 28 | :paper (str prefix "-paper") 29 | :fixed-height (str prefix "-fixed-height")})) 30 | 31 | (def drawer-width 240) 32 | 33 | (defn custom-styles [{:as theme-m :keys [theme]}] 34 | (let [spacing (:spacing theme) 35 | create-transitions (fn [& args] 36 | (apply (-> theme :transitions :create) 37 | (apply clj->js args)))] 38 | {(str "&." (:root classes)) {:display "flex"} 39 | (str "&." (:toolbar classes)) {:padding-right 24} 40 | (str "&." (:toolbar-icon classes)) {:display "flex" 41 | :align-items "center" 42 | :justify-content "flex-end" 43 | :padding "0 8px"} 44 | (str "& ." (:app-bar classes)) {:z-index (+ (-> theme :z-index :drawer) 1) 45 | :transition (let [create (-> theme :transitions :create)] 46 | (create #js ["width" "margin"] 47 | #js {:easing (-> theme :transitions :easing :sharp) 48 | :duration (-> theme :transitions :duration :leaving-screen)}))} 49 | (str "& ." (:app-bar-shift classes)) {:margin-left drawer-width 50 | :width (str "calc(100% - " drawer-width "px)") 51 | :transition (let [create (-> theme :transitions :create)] 52 | (create #js ["width" "margin"] 53 | #js {:easing (-> theme :transitions :easing :sharp) 54 | :duration (-> theme :transitions :duration :entering-screen)}))} 55 | (str "& ." (:menu-button classes)) {:margin-right 36} 56 | (str "& ." (:title classes)) {:flexGrow 1} 57 | (str "& ." (:drawer-paper classes)) {:position "relative" 58 | :white-space "nowrap" 59 | :top (-> theme :mixins :toolbar :min-height (+ 8)) 60 | :width drawer-width 61 | :transition (let [create (-> theme :transitions :create)] 62 | (create "width" 63 | #js {:easing (-> theme :transitions :easing :sharp) 64 | :duration (-> theme :transitions :duration :leaving-screen)}))} 65 | (str "& ." (:drawer-paper-close classes)) {:overflow-x "hidden" 66 | :transition (let [create (-> theme :transitions :create)] 67 | (create "width" 68 | #js {:easing (-> theme :transitions :easing :sharp) 69 | :duration (-> theme :transitions :duration :entering-screen)})) 70 | :width 0 #_(spacing 0) 71 | ((-> theme :breakpoints :up) "sm") {:width (spacing 6)}} 72 | (str "& ." (:app-bar-spacer classes)) (-> theme :mixins :toolbar) 73 | (str "& ." (:content classes)) {:flexGrow 1 74 | :margin 20 75 | :height "100vh" 76 | :overflow "auto"} 77 | (str "& ." (:container classes)) {:padding-top (spacing 4) 78 | :padding-bottom (spacing 4)} 79 | (str "& ." (:paper classes)) {:padding (spacing 4) 80 | :display "flex" 81 | :overflow "auto" 82 | :flex-direction "column"} 83 | (str "& ." (:fixed-height classes)) {:height 240}})) 84 | 85 | ;;; Components 86 | 87 | (defn list-item [{:keys [selected route-name text icon]}] 88 | [c/list-item {:style {:padding 10} 89 | :button true 90 | :selected selected 91 | :on-click #(rfe/push-state route-name)} 92 | [c/list-item-icon [icon]] 93 | [c/list-item-text {:primary text}]]) 94 | 95 | (defn account-menu [] 96 | (let [anchor-el (reagent/atom nil)] 97 | (fn [] 98 | (let [a-open? @(subscribe [:account-menu/open?]) 99 | a-close #(dispatch [:account-menu/toggle])] 100 | [:div 101 | [c/icon-button {:on-click #(do (dispatch [:account-menu/toggle]) 102 | (reset! anchor-el (.-currentTarget %)))} 103 | [c/avatar]] 104 | [c/menu {:anchor-el @anchor-el 105 | :open (boolean a-open?) 106 | :on-close a-close 107 | :on-click a-close 108 | :keepMounted true 109 | :transformOrigin {:horizontal "right" :vertical "top"} 110 | :anchorOrigin {:horizontal "right" :vertical "bottom"}} 111 | [c/menu-item 112 | [c/list-item-icon 113 | [c/account-circle]] 114 | "Sign In"] 115 | [c/divider] 116 | [c/menu-item 117 | [c/list-item-icon 118 | [c/exit-to-app {:fontSize "small"}]] 119 | "Logout"]]])))) 120 | 121 | (defn dashboard [{:keys [class-name]}] 122 | (let [open? @(subscribe [:drawer/open?]) 123 | dark-theme? @(subscribe [:dark-theme?]) 124 | router routes/router 125 | current-route @(re-frame.core/subscribe [:current-route])] 126 | [c/box {:class [class-name (:root classes)]} 127 | [c/app-bar {:position "absolute" 128 | :class [(:app-bar classes)]} 129 | [c/toolbar {:class (:toolbar classes)} 130 | [c/icon-button {:edge "start" 131 | :color "inherit" 132 | :aria-label "open drawer" 133 | :on-click #(if open? 134 | (dispatch [:drawer/close]) 135 | (dispatch [:drawer/open])) 136 | :class [(:menu-button classes)]} 137 | [c/menu-icon]] 138 | [c/text {:component "h1" 139 | :variant "h6" 140 | :color "inherit" 141 | :no-wrap true 142 | :class (:title classes)} 143 | "Acme Analytics"] 144 | [c/icon-button {:color "inherit"} 145 | [c/badge {:badgeContent 4 :color "secondary"}] 146 | [c/notifications]] 147 | [account-menu {}]]] 148 | 149 | [c/drawer {:variant "permanent" 150 | :classes {:paper (str (:drawer-paper classes) " " 151 | (if open? "" (:drawer-paper-close classes)))} 152 | :open open?} 153 | [c/divider] 154 | [c/list-items 155 | (for [route-name (reitit/route-names router) 156 | :let [route (reitit/match-by-name router route-name) 157 | text (-> route :data :link-text) 158 | icon (-> route :data :icon) 159 | selected? (= route-name (-> current-route :data :name))]] 160 | ^{:key route-name} [list-item {:text text 161 | :icon icon 162 | :route-name route-name 163 | :selected selected?}])] 164 | [c/divider] 165 | [c/list-items 166 | [c/list-item {:button true 167 | :style {:left 5 :top 10} 168 | :on-click #(dispatch [:toggle-dark-theme])} 169 | [c/list-item-icon [c/switch {:size "small" :checked (or dark-theme? false)}]] 170 | [c/list-item-text {:primary "Toggle Theme"}]]]] 171 | [:main {:class (:content classes)} 172 | [:div {:class (:app-bar-spacer classes)}] 173 | (when current-route 174 | [(-> current-route :data :view) {:classes classes}])]])) 175 | 176 | (def styled-dashboard (c/styled dashboard custom-styles)) 177 | -------------------------------------------------------------------------------- /shadow_dashboard/src/screen/dashboard/views/home_panel.cljs: -------------------------------------------------------------------------------- 1 | (ns dashboard.views.home-panel 2 | (:require 3 | [cljs-thread.re-frame :refer [subscribe]] 4 | [dashboard.footer :refer [copyright]] 5 | ["react-chartjs-2" :refer [Line]] 6 | [comp.el :as c])) 7 | 8 | ;; Components 9 | 10 | (defn drawer-icon [] 11 | [c/home]) 12 | 13 | (def data-6-hours 14 | (clj->js 15 | {:labels ["9:00" "10:00" "11:00" "12:00" "1:00" "2:00" "3:00"] 16 | :datasets 17 | [{:label "Events Last 6 Hours" 18 | :fill false 19 | :lineTension "0.1" 20 | :borderColor "#ef5350" 21 | :borderCapStyle "butt" 22 | :borderDash [] 23 | :borderDashOffset "0.0" 24 | :borderJoinStyle "miter" 25 | :pointBorderColor "#ef5350" 26 | :pointBackgroundColor "#ef5350" 27 | :pointBorderWidth 1 28 | :pointHoverRadius 5 29 | :pointHoverBackgroundColor "rgba(75,192,192,1)"' 30 | :pointHoverBorderColor "rgba(220,220,220,1)" 31 | :pointHoverBorderWidth 2 32 | :pointRadius 1 33 | :pointHitRadius 10 34 | :data [65, 59, 80, 81, 56, 55, 40]}]})) 35 | 36 | (def data-week 37 | (clj->js 38 | {:labels ["Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday" "Sunday"] 39 | :options {:legend {:display false}} 40 | :datasets 41 | [{:label "Events Last Week" 42 | :fill false 43 | :lineTension "0.1" 44 | :borderColor "#ef5350" 45 | :borderCapStyle "butt" 46 | :borderDash [] 47 | :borderDashOffset "0.0" 48 | :borderJoinStyle "miter" 49 | :pointBorderColor "#ef5350" 50 | :pointBackgroundColor "#ef5350" 51 | :pointBorderWidth 1 52 | :pointHoverRadius 5 53 | :pointHoverBackgroundColor "rgba(75,192,192,1)"' 54 | :pointHoverBorderColor "rgba(220,220,220,1)" 55 | :pointHoverBorderWidth 2 56 | :pointRadius 1 57 | :pointHitRadius 10 58 | :data [124, 144, 133, 277, 156, 188, 140]}]})) 59 | 60 | (defn chart [{:keys [^js classes]}] 61 | (let [chart-data (subscribe [:home-panel/chart-data])] 62 | [:<> 63 | [c/container {:justifyContent "space-between"} 64 | [c/item 65 | [c/text {:component "h2" :variant "h6" :color "primary" :gutter-bottom true} 66 | "Events"]] 67 | [c/item]] 68 | [c/container {:align-items "center" :spacing {:xs 2} :columns {:xs 6 :sm 6 :md 12}} ;{:style {:height 200 :width 800}} 69 | [c/item {:xs 6 :sm 6 :md 6 :style {:min-width 250}} 70 | [:> Line {:ref "chart" :data data-6-hours :options (clj->js {:plugins {:legend {:labels {:boxWidth 0}}}})}]] 71 | [c/item {:xs 6 :sm 6 :md 6 :style {:min-width 250}} 72 | [:> Line {:ref "chart" :data data-week :options (clj->js {:plugins {:legend {:labels {:boxWidth 0}}}})}]]]])) 73 | 74 | (defn deposits [{:keys [^js classes]}] 75 | [:<> 76 | [c/text {:component "h2" :variant "h6" :color "primary" :gutter-bottom true} 77 | "Last 168 Hours"] 78 | [c/text {:component "p" :variant "h4"} 79 | "1,237"] 80 | [c/text {:color "textSecondary" :style {:flex 1}} ; :class (.-depositContext classes) 81 | "Total Events in the Last Week"] 82 | [:div 83 | [c/link {:color "primary" :href "#" :on-click #(.preventDefault %)} 84 | "View events"]]]) 85 | 86 | (defn table-row [{:keys [date name ship-to payment-method amount]}] 87 | [c/table-row 88 | [c/table-cell date] 89 | [c/table-cell name] 90 | [c/table-cell ship-to] 91 | [c/table-cell payment-method] 92 | [c/table-cell {:align "right"} (str (apply str (take 20 amount)) "...")]]) 93 | 94 | 95 | (defn orders [{:keys [^js classes]}] 96 | (let [orders (subscribe [:home-panel/orders])] 97 | [:<> 98 | [c/text {:component "h2" :variant "h6" :color "primary" :gutter-bottom true} 99 | "Log"] 100 | [c/table {:size "small"} 101 | [c/table-head 102 | [c/table-row 103 | [c/table-cell "Date"] 104 | [c/table-cell "Message"] 105 | [c/table-cell "Customer"] 106 | [c/table-cell "GUID"] 107 | [c/table-cell {:align "right"} "Session ID"]]] 108 | [c/table-body 109 | (for [order @orders] 110 | ^{:key (:id order)} [table-row order])]]])) 111 | 112 | (defn main [{:keys [^js classes]}] 113 | [c/grid-container {:max-width "lg" :class (:container classes)} 114 | [c/css-baseline] 115 | [c/container {:spacing 3} 116 | [c/item {:xs 12 :md 8 :lg 9} 117 | [c/paper {:class [(:paper classes) (:fixedHeight classes)]} 118 | [chart {:classes classes}]]] 119 | [c/item {:xs 12 :md 4 :lg 3} 120 | [c/paper {:class [(:paper classes) (:fixedHeight classes)]} 121 | [deposits {:classes classes}]]] 122 | [c/item {:xs 12} 123 | [c/paper {:class (:paper classes)} 124 | [orders {:classes classes}]]]] 125 | [c/container {:spacing 3}] 126 | 127 | [c/box {:pt 4} 128 | [copyright]]]) 129 | -------------------------------------------------------------------------------- /shadow_dashboard/src/screen/dashboard/views/sign_in.cljs: -------------------------------------------------------------------------------- 1 | (ns dashboard.views.sign-in 2 | (:require 3 | [reagent.core :as reagent] 4 | [comp.el :as c] 5 | [cljs-thread.re-frame :refer [dispatch subscribe]] 6 | [dashboard.footer :refer [copyright]])) 7 | 8 | ;;; Styles 9 | (def classes 10 | (let [prefix "rmui-dash-sign-in"] 11 | {:avatar (str prefix "-avatar") 12 | :paper (str prefix "-paper") 13 | :form (str prefix "-form") 14 | :submit (str prefix "-submit")})) 15 | 16 | (defn custom-sign-in-styles [{:keys [theme]}] 17 | (let [spacing (:spacing theme) 18 | create-transitions (fn [& args] 19 | (apply (-> theme :transitions :create) 20 | (apply clj->js args)))] 21 | {(str "& ." (:paper classes)) {:marginTop (spacing 8) 22 | :display "flex" 23 | :flexDirection "column" 24 | :alignItems "center"} 25 | (str "& ." (:avatar classes)) {:margin (spacing 1) 26 | :backgroundColor (-> theme :palette :secondary :main)} 27 | (str "& ." (:form classes)) {:width "100%" ; Fix IE 11 issue 28 | :marginTop (spacing 1)} 29 | (str "& ." (:submit classes)) {:margin (spacing 3 0 2)}})) 30 | 31 | ;; Components 32 | 33 | (defn drawer-icon [] 34 | [c/lock-icon]) 35 | 36 | (defn sign-in [{:keys [^js classes] :as props}] 37 | (let [form (reagent/atom {:userid "" :password "" :remember? false}) 38 | errors (subscribe [:sign-in/errors])] 39 | (fn [] 40 | [c/css-baseline] 41 | [:div {:class (:paper classes) 42 | :style {:marginTop (str (* 8 8) "px") 43 | :display "flex" 44 | :flexDirection "column" 45 | :alignItems "center"}} 46 | [c/avatar {:class (:avatar classes)} 47 | [c/lock-icon]] 48 | [c/text {:component "h1" :variant "h5"} 49 | "Sign in"] 50 | [c/list-items 51 | {:style {:padding 10} 52 | :padding 10} 53 | [:form {:class (:form classes) 54 | :on-submit (fn [e] 55 | (js/console.log e) 56 | (.preventDefault e) 57 | (dispatch [:sign-in @form]) 58 | (reset! form {:userid "" :password "" :remember? (:remember? @form)})) 59 | :no-validate true} 60 | [c/input {:style {:width "100%"} 61 | :variant "outlined" 62 | :margin "normal" 63 | :required true 64 | :on-change #(swap! form assoc :userid (-> % .-target .-value)) 65 | :value (:userid @form) 66 | :id "email" 67 | :label "Email Address" 68 | :name "email" 69 | :autoComplete "email" 70 | :autoFocus true}] 71 | [c/input {:style {:width "100%"} 72 | :variant "outlined" 73 | :margin "normal" 74 | :error (:password @errors) 75 | :helper-text (:password @errors) 76 | :required true 77 | :on-focus #(dispatch [:sign-in/clear-errors :password]) 78 | :on-change #(swap! form assoc :password (-> % .-target .-value)) 79 | :value (:password @form) 80 | :id "password" 81 | :label "Password" 82 | :name "password" 83 | :autoComplete "current-password"}]]] 84 | [c/list-item 85 | ;; {:align-items "flex-start"} 86 | [c/form-control-label 87 | {:control (reagent/as-element 88 | [c/checkbox {:checked (:remember? @form) 89 | :on-change #(swap! form assoc :remember? (-> % .-target .-checked)) 90 | :color "primary"}]) 91 | :label "Remember me"}]] 92 | [c/button {:type "submit" 93 | :style {:width "100%" :margin 20} 94 | :variant "contained" 95 | :color "primary" 96 | :class (:submit classes)} 97 | "Sign In"] 98 | [c/container 99 | [c/item {:xs true} 100 | [c/link {:href "#" :variant "body2"} 101 | "Forgot password?"]] 102 | [c/item 103 | [c/link {:href "#" :variant "body2"} 104 | "Don't have an account? Sign Up"]]]]))) 105 | 106 | (defn main [{:keys [^js classes]}] 107 | [c/grid-container {:component "main" :max-width "xs"} 108 | [c/css-baseline] 109 | [(c/styled sign-in custom-sign-in-styles) classes] 110 | [c/box {:mt 8} 111 | [copyright]]]) 112 | -------------------------------------------------------------------------------- /src/cljs_thread/core.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.core 2 | (:refer-clojure :exclude [future pmap pcalls pvalues]) 3 | (:require 4 | [injest.path])) 5 | 6 | (defmacro in [& x] 7 | `(cljs-thread.in/in ~@x)) 8 | 9 | (defmacro future [& x] 10 | `(cljs-thread.future/future ~@x)) 11 | 12 | (defmacro spawn [& x] 13 | `(cljs-thread.spawn/spawn ~@x)) 14 | 15 | (defmacro =>> 16 | "Just like x>> but first composes stateless transducers into a function that 17 | transduces in parallel the values flowing through the thread. Remaining 18 | stateful transducers are composed just like x>>." 19 | [& x] 20 | `(cljs-thread.injest/=>> ~@x)) 21 | 22 | (defmacro on-when [& x] 23 | `(cljs-thread.on-when/on-when ~@x)) 24 | 25 | (defmacro on-watch [& x] 26 | `(cljs-thread.on-when/on-watch ~@x)) 27 | 28 | (defmacro dbg [& x] 29 | `(cljs-thread.repl/dbg ~@x)) 30 | 31 | (defmacro break [& x] 32 | `(cljs-thread.repl/break ~@x)) 33 | 34 | (defmacro in? [& x] 35 | `(cljs-thread.repl/in? ~@x)) 36 | 37 | (defmacro pmap [& x] 38 | `(cljs-thread.pmap/pmap ~@x)) 39 | 40 | (defmacro pcalls [& x] 41 | `(cljs-thread.pmap/pcalls ~@x)) 42 | 43 | (defmacro pvalues [& x] 44 | `(cljs-thread.pmap/pvalues ~@x)) 45 | -------------------------------------------------------------------------------- /src/cljs_thread/core.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.core 2 | (:require-macros [cljs-thread.core :refer [spawn]]) 3 | (:require 4 | [cljs-thread.util :as u] 5 | [cljs-thread.env :as e] 6 | [cljs-thread.state :as s] 7 | [cljs-thread.spawn :as sp] 8 | [cljs-thread.on-when] 9 | [cljs-thread.in] 10 | [cljs-thread.root :as r] 11 | [cljs-thread.db] 12 | [cljs-thread.msg :as m] 13 | [cljs-thread.sync] 14 | [cljs-thread.repl] 15 | [cljs-thread.future] 16 | [cljs-thread.injest] 17 | [cljs-thread.pmap])) 18 | 19 | (enable-console-print!) 20 | 21 | (def sleep cljs-thread.sync/sleep) 22 | 23 | (def ^:export id (:id e/data)) 24 | 25 | (defn init! [& [config-map]] 26 | (assert (e/in-screen?)) 27 | (when config-map 28 | (swap! s/conf merge config-map)) 29 | (let [config @s/conf] 30 | (if-not (:sw-connect-string config) 31 | (spawn {:id :root :no-globals? true} 32 | (r/init-root! config)) 33 | (do (sp/spawn-sw 34 | #(spawn {:id :root :no-globals? true} 35 | (spawn {:id :core :no-globals? true}) 36 | (spawn {:id :db :no-globals? true}) 37 | (m/pair-ids :core :db) 38 | (r/init-root! config))) 39 | (when-not (u/in-safari?) 40 | (sp/on-sw-registration-reload)))))) 41 | 42 | ;; ephemeral spawns 43 | (when (and (not (e/in-sw?)) (not (e/in-screen?))) 44 | (def e-fn (:efn e/data)) 45 | (def e-args (:eargs e/data)) 46 | (def sargs (->> e-args (mapv #(if (fn? %) (str "#cljs-thread/arg-fn " %) %)))) 47 | (when e-fn (cljs-thread.in/do-call 48 | {:data {:sfn e-fn 49 | :sargs sargs 50 | :in-id (:in-id e/data) 51 | :opts {:request-id (:id e/data) :atom? true :yield? (:yield? e/data)}}})) 52 | (when (and (not (:yield? e/data)) (not (:deamon? e/data))) 53 | (.close js/self)) 54 | :end) 55 | -------------------------------------------------------------------------------- /src/cljs_thread/db.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.db 2 | (:require 3 | [cljs-thread.in :refer [in]] 4 | [cljs-thread.sync] 5 | [cljs-thread.idb :refer [idb-set! idb-get]])) 6 | 7 | (defn db-set! [k data] 8 | (in :db (idb-set! k data identity)) 9 | data) 10 | 11 | (defn db-get [k] 12 | (let [res @(in :db (idb-get k yield))] 13 | (if-not (contains? res :res) 14 | nil 15 | (:res res)))) 16 | -------------------------------------------------------------------------------- /src/cljs_thread/env.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.env 2 | (:require 3 | [cljs-thread.util :as u])) 4 | 5 | (defn in-screen? [] (-> js/self .-document undefined? not)) 6 | 7 | (def data 8 | (let [loc-search js/location.search] 9 | (if-not (seq loc-search) 10 | (if (in-screen?) 11 | {:id :screen} 12 | {:id :root}) 13 | (u/decode-qp loc-search)))) 14 | 15 | (defn in-root? [] 16 | (-> data :id (= :root))) 17 | 18 | (defn in-sw? [] 19 | (-> data :id (= :sw))) 20 | 21 | (defn in-core? [] 22 | (-> data :id (= :core))) 23 | 24 | (defn in-future? [] 25 | (-> data :id (= :future))) 26 | 27 | (defn in-branch? [] 28 | (and (not (in-screen?)) (not (in-root?)))) 29 | 30 | (def current-browser 31 | (u/browser-type)) 32 | -------------------------------------------------------------------------------- /src/cljs_thread/future.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.future 2 | (:refer-clojure :exclude [future]) 3 | (:require 4 | [cljs-thread.macro-impl :as i])) 5 | 6 | (defmacro future [& x] 7 | (let [[conveyer names opts body] (i/globals-locals-and-args &env x) 8 | yield? (i/yields? x) 9 | afn (if-not yield? 10 | `(fn ~names 11 | (fn [yield*#] 12 | (yield*# (do ~@body)))) 13 | `(fn ~names 14 | (fn [yield*#] 15 | (let [~'yield yield*#] 16 | ~@body))))] 17 | `(cljs-thread.future/do-future ~conveyer ~afn ~opts))) 18 | -------------------------------------------------------------------------------- /src/cljs_thread/future.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.future 2 | (:require-macros 3 | [cljs-thread.future]) 4 | (:require 5 | [cljs-thread.util :as u] 6 | [cljs-thread.env :as e] 7 | [cljs-thread.spawn :refer [spawn]] 8 | [cljs-thread.on-when :refer [on-when]] 9 | [cljs-thread.in :refer [in]] 10 | [cljs-thread.state :as s] 11 | [cljs-thread.sync :as sync])) 12 | 13 | (defn take-worker! [] 14 | (when-let [p (some->> @s/future-pool :available first)] 15 | (swap! s/future-pool 16 | (fn [{:keys [available in-use]}] 17 | {:available (disj available p) 18 | :in-use (conj in-use p)})) 19 | p)) 20 | 21 | (defn put-back-worker! [p] 22 | (swap! s/future-pool 23 | (fn [{:keys [available in-use]}] 24 | {:available (conj available p) 25 | :in-use (disj in-use p)})) 26 | nil) 27 | 28 | (defn mk-worker-ids [n] 29 | (let [ws (-> n (or (inc (u/num-cores))) (/ 2) int)] 30 | (->> ws range (map #(keyword (str "fp-" %)))))) 31 | 32 | (defn init-future! [& [{:as config-map :keys [future-ids]}]] 33 | (assert (e/in-future?)) 34 | (when config-map 35 | (s/update-conf! config-map) 36 | (when future-ids 37 | (swap! s/future-pool update :available into future-ids)))) 38 | 39 | (defn start-futures [configs] 40 | (let [future-ids (mk-worker-ids (:future-count configs)) 41 | future-conf (assoc configs :future-ids future-ids)] 42 | (spawn {:id :future :no-globals? true} 43 | (init-future! future-conf)) 44 | (->> future-ids 45 | (mapv (fn [fid] 46 | (spawn {:id fid :no-globals? true} 47 | (s/update-conf! future-conf))))))) 48 | 49 | (defn do-future [args afn opts] 50 | (let [fut-id (u/gen-id)] 51 | (in :future [args afn fut-id] ;; <- TODO: prevent implicit conveyer param duplication (dissallowed for arraybuffer transfers) 52 | (on-when (-> @s/future-pool :available seq) {:duration 5} 53 | (let [worker (take-worker!)] 54 | (in worker [args afn fut-id worker] 55 | (try 56 | (if (seq args) 57 | ((apply afn args) 58 | #(sync/send-response {:request-id fut-id :response %})) 59 | ((afn) 60 | #(sync/send-response {:request-id fut-id :response %}))) 61 | (catch :default e 62 | (sync/send-response {:request-id fut-id :response {:error (pr-str e)}}))) 63 | (in :future 64 | (put-back-worker! worker)))))) 65 | (sync/wrap-derefable (merge opts {:id fut-id})))) 66 | -------------------------------------------------------------------------------- /src/cljs_thread/id.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.id) 2 | 3 | (defprotocol IDable 4 | "Protocol for types which have an ID" 5 | (get-id [x] 6 | "Returns id if a value has an ID.")) 7 | -------------------------------------------------------------------------------- /src/cljs_thread/idb.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.idb 2 | (:require 3 | [clojure.edn :as edn] 4 | [cljs-thread.env :as e] 5 | [cljs-thread.state :as s] 6 | [cljs-thread.on-when :refer [on-watch]])) 7 | 8 | (def idb-key "cljs-thread.db") 9 | 10 | (def open? (atom false)) 11 | 12 | (defn ^:export idb-set! [k data & [yield]] 13 | (on-watch open? true? 14 | (let [adb @s/idb 15 | transaction (.transaction adb #js [idb-key] "readwrite") 16 | os (.objectStore transaction idb-key) 17 | req (.put os (pr-str data) (pr-str k))] 18 | (set! (.-onerror req) #(println :idb-set! :error)) 19 | (set! (.-onsuccess req) #(let [res (some-> % .-target .-result)] 20 | (yield res))) 21 | (set! (.-oncomplete req) #(println :set :complete!))))) 22 | 23 | (defn ^:export idb-get [k yield] 24 | (on-watch open? true? 25 | (let [adb @s/idb 26 | req (-> adb 27 | (.transaction #js [idb-key]) 28 | (.objectStore idb-key) 29 | (.get (pr-str k)))] 30 | (when req 31 | (set! (.-onerror req) #(do (yield nil) 32 | (throw (ex-info (str "idb-get failed getting " k) 33 | {:k k :error %})))) 34 | (set! (.-onsuccess req) #(let [res (-> % .-target .-result edn/read-string)] 35 | (yield {:res res}))))))) 36 | 37 | (defn startup [] 38 | (let [request (.open js/indexedDB idb-key 1)] 39 | (set! (.-onerror request) #(do (println :idb-open-error %))) 40 | (set! (.-onsuccess request) 41 | #(do (reset! s/idb (-> % .-target .-result)) 42 | (reset! open? true))) 43 | (set! (.-onupgradeneeded request) 44 | #(let [db (-> % .-target .-result) 45 | os (.createObjectStore db idb-key)] 46 | (reset! s/idb db) 47 | (set! (-> os .-transaction .-oncomplete) 48 | (fn [e] 49 | (let [init-os 50 | (-> db 51 | (.transaction idb-key "readwrite") 52 | (.objectStore idb-key))] 53 | (reset! open? true)))))))) 54 | 55 | (when (= :db (:id e/data)) 56 | (swap! open? (constantly false)) 57 | (on-watch open? false? 58 | (startup)) 59 | (swap! open? identity)) 60 | -------------------------------------------------------------------------------- /src/cljs_thread/in.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.in 2 | (:require 3 | [cljs-thread.macro-impl :as i])) 4 | 5 | (defmacro in [id & x] 6 | (let [[conveyer names opts body] (i/globals-locals-and-args &env x) 7 | yield? (i/yields? x) 8 | yfn (if-not yield? 9 | `(fn ~names ~@body) 10 | `(fn [in-id#] 11 | (fn ~names 12 | (let [~'yield (fn [res#] 13 | (cljs-thread.sync/send-response 14 | {:request-id in-id# :response res#}))] 15 | ~@body))))] 16 | `(cljs-thread.in/do-in ~id ~conveyer (clojure.core/str ~yfn) (assoc ~opts :yield? ~yield?)))) 17 | -------------------------------------------------------------------------------- /src/cljs_thread/in.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.in 2 | (:require-macros 3 | [cljs-thread.in]) 4 | (:require [clojure.edn :as edn] 5 | [clojure.walk :refer [postwalk]] 6 | [cljs-thread.env :as e] 7 | [cljs-thread.id :refer [get-id IDable]] 8 | [cljs-thread.msg :as m] 9 | [cljs-thread.state :as s] 10 | [cljs-thread.sync :as sync] 11 | [cljs-thread.util :as u])) 12 | 13 | (defn instr-body [transfer-atom pl] 14 | (let [body (postwalk 15 | #(cond (fn? %) (str "#cljs-thread/arg-fn " %) 16 | (u/typed-array? %) 17 | (let [c-tag (:count (swap! transfer-atom update :count inc)) 18 | t (type %)] 19 | (swap! transfer-atom assoc-in [:transfers c-tag] 20 | (merge 21 | {:obj %} 22 | (when-not (and (exists? js/SharedArrayBuffer) (= (type %) js/SharedArrayBuffer)) 23 | {:transfer (.-buffer %)}))) 24 | (str "#cljs-thread/transferable " {:c-tag c-tag :transfer-type t})) 25 | :else %) 26 | pl)] 27 | body)) 28 | 29 | (defn unstr-body [transfers pl] 30 | (postwalk 31 | #(if-not (and (string? %) (or (.startsWith % "#cljs-thread/transferable") 32 | (.startsWith % "#cljs-thread/arg-fn"))) 33 | % 34 | (cond (.startsWith % "#cljs-thread/arg-fn") 35 | (js/eval (str "(function () {return (" (apply str (drop 20 %)) ");})();")) 36 | (.startsWith % "#cljs-thread/transferable") 37 | (let [transfer-map (edn/read-string (apply str (drop 26 %))) 38 | {:keys [c-tag transfer-type]} transfer-map 39 | transfers-clj (js->clj transfers :keywordize-keys true) 40 | {:keys [obj]} (get transfers-clj c-tag)] 41 | obj) 42 | :else %)) 43 | pl)) 44 | 45 | (defn do-call 46 | [{:keys [data] :as outer-data}] 47 | (let [{:keys [sfn sargs opts in-id local? transfers]} data 48 | res (try 49 | (if (or (nil? sfn) (= sfn "nil")) 50 | nil 51 | (if-not sargs 52 | (if (and in-id (:yield? opts)) 53 | ((js/eval (str "(" sfn ")();")) in-id) 54 | (js/eval (str "(" sfn ")();"))) 55 | (apply (if (and in-id (:yield? opts)) 56 | ((js/eval (str "(function () {return (" sfn ");})();")) in-id) 57 | (js/eval (str "(function () {return (" sfn ");})();"))) 58 | (if (vector? sargs) 59 | (->> sargs (mapv (partial unstr-body transfers))) 60 | (js/eval (str "(" sargs ")();")))))) 61 | (catch :default e 62 | (println :error-in (:id e/data)) 63 | (println :error-in-do-call e) 64 | (println :error (.-error e)) 65 | (println :data data) 66 | (when-not (e/in-sw?) 67 | (sync/send-response {:request-id (:request-id opts) :response {:error (pr-str e)}}))))] 68 | (when (:atom? opts) 69 | (reset! s/local-val res)) 70 | (if local? 71 | res 72 | (when (and (not (:yield? opts)) (not (e/in-sw?))) 73 | (sync/send-response {:request-id (:request-id opts) :response res}))))) 74 | 75 | 76 | (defmethod m/dispatch :call 77 | [data] 78 | (do-call data)) 79 | 80 | (defn do-in [id & [args afn opts]] 81 | (let [[afn args] (if afn [afn args] [args nil]) 82 | in-id (u/gen-id) 83 | transfer-atom (atom {:count 0 :transfers {}}) 84 | sargs (->> args (mapv (partial instr-body transfer-atom))) 85 | id (if (satisfies? IDable id) 86 | (get-id id) 87 | id) 88 | id (if (keyword id) 89 | id 90 | (pr-str id)) 91 | post-in #(m/post id 92 | {:dispatch :call 93 | :data (merge 94 | {:sfn afn :to id :in-id in-id} 95 | (when args 96 | {:sargs sargs}) 97 | (when-let [transfers (:transfers @transfer-atom)] 98 | {:transfers transfers}) 99 | (when opts 100 | {:opts (assoc opts :request-id in-id)}))})] 101 | (post-in) 102 | (sync/wrap-derefable (merge opts {:id in-id})))) 103 | -------------------------------------------------------------------------------- /src/cljs_thread/injest.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.injest 2 | (:require 3 | [injest.impl] 4 | [injest.util :as util] 5 | [cljs-thread.macro-impl :as i])) 6 | 7 | (defmacro tfan [conveyer names xf-group] 8 | (let [yfn `(clojure.core/fn ~names (injest.impl/compose-transducer-group ~xf-group))] 9 | `(do (fn [args#] 10 | (cljs-thread.injest/fan ~conveyer ~yfn args#))))) 11 | 12 | (defn pre-transducify-thread [conveyer names env minimum-group-size t-fn t-pred thread] 13 | (->> thread 14 | (util/qualify-thread env) 15 | (partition-by #(t-pred %)) 16 | (mapv #(if-not (and (t-pred (first %)) 17 | (not (< (count %) minimum-group-size))) 18 | % 19 | (list (list `(~t-fn ~conveyer ~names ~(mapv vec %)))))) 20 | (apply concat))) 21 | 22 | (defmacro =>> 23 | "Just like x>> but first composes stateless transducers into a function that 24 | `r/fold`s in parallel the values flowing through the thread. Remaining 25 | stateful transducers are composed just like x>>." 26 | [x & thread] 27 | (let [[conveyer names opts body] (i/globals-locals-and-args &env thread)] 28 | `(if (cljs-thread.env/in-screen?) 29 | (cljs-thread.in/in :core (injest.path/x>> ~x ~@(->> thread (pre-transducify-thread conveyer names &env 1 `cljs-thread.injest/tfan injest.impl/par-transducable?)))) 30 | (injest.path/x>> ~x ~@(->> thread (pre-transducify-thread conveyer names &env 1 `cljs-thread.injest/tfan injest.impl/par-transducable?)))))) 31 | -------------------------------------------------------------------------------- /src/cljs_thread/injest.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.injest 2 | (:require-macros 3 | [cljs-thread.injest]) 4 | (:require 5 | [cljs-thread.util :as u] 6 | [cljs-thread.state :as s] 7 | [cljs-thread.spawn :refer [spawn]] 8 | [cljs-thread.in :refer [in]] 9 | [injest.impl] 10 | [injest.path])) 11 | 12 | (defn mk-injest-ids [& [n]] 13 | (let [ws (-> n (or (inc (u/num-cores))) (/ 2) int)] 14 | (->> ws range (map #(keyword (str "injest-" %)))))) 15 | 16 | (defn start-injests [configs] 17 | (let [injests (-> configs :injest-count mk-injest-ids)] 18 | (->> injests 19 | (mapv (fn [wid] 20 | (spawn {:id wid} 21 | (s/update-conf! configs))))))) 22 | 23 | (def ^:dynamic *par* nil) 24 | (def ^:dynamic *chunk* nil) 25 | (defn fan [conveyer xf args & {:keys [par chunk]}] 26 | (let [injest-count (:injest-count @s/conf 4) 27 | n-pws (or *par* par (* injest-count 8)) 28 | pws (take n-pws (cycle (mk-injest-ids (:injest-count @s/conf)))) 29 | args-parts (partition-all n-pws (partition-all (or *chunk* chunk 512) args))] 30 | (->> args-parts 31 | (map (fn [ags] 32 | (->> ags 33 | (mapv (fn [p a] 34 | (in p 35 | (sequence (apply xf conveyer) a))) 36 | pws)))) 37 | (mapcat #(map deref %)) 38 | (apply concat)))) 39 | -------------------------------------------------------------------------------- /src/cljs_thread/macro_impl.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.macro-impl) 2 | 3 | (defn get-symbols [body] 4 | (let [body (if (symbol? body) 5 | [body] 6 | body)] 7 | (->> body 8 | (tree-seq coll? seq) 9 | (rest) 10 | (filter (complement coll?)) 11 | (filter symbol?) 12 | vec))) 13 | 14 | (defn get-locals [env body] 15 | (let [body (if (symbol? body) 16 | [body] 17 | body)] 18 | (->> (filter (complement coll?) 19 | (rest (tree-seq coll? seq body))) 20 | (filter symbol?) 21 | (map (:locals env)) 22 | (map :name) 23 | (filter (comp not nil?)) 24 | vec 25 | (#(do [% %]))))) 26 | 27 | (defn get-locals-and-globals [env body] 28 | (let [defs-and-locals (merge (:locals env) 29 | (into {} (map (fn [[k v]] [k {:name k}]) (:defs (:ns env))))) 30 | body (if (symbol? body) 31 | [body] 32 | body)] 33 | (->> (filter (complement coll?) 34 | (rest (tree-seq coll? seq body))) 35 | (filter symbol?) 36 | (map defs-and-locals) 37 | (map :name) 38 | (filter (comp not nil?)) 39 | vec 40 | (#(do [% %]))))) 41 | 42 | (defn parse-in [x] 43 | (if-not (coll? x) 44 | [[] {} x] 45 | (let [f (first x) 46 | s (second x)] 47 | (cond (and (vector? f) (map? s)) [f s (rest (rest x))] 48 | (vector? f) [f {} (rest x)] 49 | (map? f) [[] f (rest x)] 50 | :else [[] {} (vec x)])))) 51 | 52 | (defn yield-form? [form] 53 | (when (seq? form) 54 | (not 55 | (empty? 56 | (filter #(or (= % 'yield) (= % 'cljs-thread.core/yield)) form))))) 57 | 58 | (defn yields? [expr] 59 | (when (coll? expr) 60 | (not 61 | (empty? 62 | (->> expr 63 | (tree-seq coll? seq) 64 | (filter yield-form?)))))) 65 | 66 | (defn globals-locals-and-args [env body] 67 | (let [[args opts body*] (parse-in body) 68 | no-globals? (:no-globals? opts) 69 | [conveyer names] 70 | (if (seq args) 71 | [(mapv symbol args) args] 72 | (if no-globals? 73 | (get-locals env body*) 74 | (get-locals-and-globals env body*)))] 75 | [conveyer names opts body*])) 76 | -------------------------------------------------------------------------------- /src/cljs_thread/msg.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.msg 2 | (:require 3 | [cljs-thread.state :as s] 4 | [cljs-thread.env :as e] 5 | [cljs-thread.id :refer [IDable get-id]] 6 | [clojure.edn :as edn] 7 | [clojure.pprint :refer [pprint]])) 8 | 9 | (def event-message "message") 10 | 11 | (defmulti dispatch :dispatch) 12 | 13 | (defn read-id [id] 14 | (if (.startsWith id ":") 15 | (edn/read-string id) 16 | (str id))) 17 | 18 | (defn message-handler [^js e] 19 | (let [data (.-msg (.-data e)) 20 | receive-port? (-> ^js data .-dispatch (= "receive-port")) 21 | data (if receive-port? 22 | (-> data (js->clj :keywordize-keys true) 23 | (update :dispatch keyword) 24 | (update-in [:data :id] read-id)) 25 | (-> data edn/read-string))] 26 | (dispatch data))) 27 | 28 | (if (not (e/in-screen?)) 29 | (.addEventListener js/self event-message message-handler) 30 | (.addEventListener js/window event-message message-handler)) 31 | 32 | (defn do-pprint [s] 33 | (pprint s)) 34 | 35 | (defmethod dispatch :pprint 36 | [{:keys [data]}] 37 | (do-pprint data)) 38 | 39 | (defmethod dispatch :receive-port 40 | [{{:keys [id port]} :data}] 41 | (swap! s/peers assoc-in [id :port] port) 42 | (set! (.-onmessage port) message-handler)) 43 | 44 | (defn when-peer-ready [id afn & [watch-key]] 45 | (let [watch-key (or watch-key (str id "-" (hash afn) "-" (gensym)))] 46 | (if (get @s/peers id) 47 | (afn) 48 | (add-watch 49 | s/peers 50 | watch-key 51 | #(do (remove-watch s/peers watch-key) 52 | (when-peer-ready id afn watch-key)))))) 53 | 54 | (defn post [worker-id {:as data {:keys [transfers]} :data} & [transferables]] 55 | (let [transferables (->> transfers (mapv (fn [[_k {:keys [transfer]}]] 56 | transfer))) 57 | id (if (and (not (keyword? worker-id)) 58 | (instance? IDable worker-id)) 59 | (get-id worker-id) 60 | worker-id) 61 | data (assoc data :from (:id e/data))] 62 | (if (= :here id) 63 | (dispatch data) 64 | (let [w (or (-> @s/peers (get-in [id :port])) 65 | (-> @s/peers (get-in [id :w])))] 66 | (try 67 | (if (and (not (e/in-screen?)) (not w)) 68 | (let [w (or (-> @s/peers (get-in [:parent :port])) 69 | (-> @s/peers (get-in [:parent :w])))] 70 | (.postMessage w 71 | #js {:transfers transfers 72 | :msg 73 | ((if (-> data :dispatch (= :receive-port)) clj->js pr-str) 74 | {:dispatch :proxy 75 | :data data})} 76 | (if transferables 77 | (clj->js transferables) 78 | #js []))) 79 | (when-peer-ready id 80 | #(let [w (or (-> @s/peers (get-in [id :port])) 81 | (-> @s/peers (get-in [id :w])))] 82 | (.postMessage w 83 | (if (-> data :dispatch (= :receive-port)) 84 | #js {:transfers transfers 85 | :msg (clj->js data)} 86 | #js {:transfers transfers 87 | :msg (str data)}) 88 | (if transferables 89 | (clj->js transferables) 90 | #js []))))) 91 | (catch :default e 92 | (println :id (:id e/data)) 93 | (println :e e) 94 | (println :w w) 95 | (println :worker-id worker-id) 96 | (println :data data) 97 | (when-peer-ready id 98 | #(post worker-id data transferables)))))))) 99 | 100 | (defmethod dispatch :proxy 101 | [{data :data}] 102 | (let [id (-> data :data :to)] 103 | (if (= id (:id e/data)) 104 | (post :here data) 105 | (post id data)))) 106 | 107 | (defn mk-chan-pair [] 108 | (let [c (js/MessageChannel.) 109 | c1 (.-port1 c) 110 | c2 (.-port2 c)] 111 | [c1 c2])) 112 | 113 | (defn send-port [id c1] 114 | (post id {:dispatch :receive-port 115 | :data {:port c1 116 | :transfers {1 {:transfer c1}} 117 | :id (str (:id e/data))}})) 118 | 119 | (defn dist-port [id1 id2 c1 c2] 120 | (post id1 {:dispatch :receive-port 121 | :data {:port c1 122 | :transfers {1 {:transfer c1}} 123 | :id (str id2)}}) 124 | (post id2 {:dispatch :receive-port 125 | :data {:port c2 126 | :transfers {1 {:transfer c2}} 127 | :id (str id1)}})) 128 | 129 | (defn pair-ids [id1 id2] 130 | (let [[c1 c2] (mk-chan-pair)] 131 | (dist-port id1 id2 c1 c2))) 132 | 133 | (defn add-port [id p] 134 | (swap! s/peers assoc-in [id :port] p) 135 | (set! (.-onmessage p) message-handler)) 136 | -------------------------------------------------------------------------------- /src/cljs_thread/nrepl.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.nrepl) -------------------------------------------------------------------------------- /src/cljs_thread/on_when.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.on-when 2 | (:require 3 | [cljs.analyzer])) 4 | 5 | (defmacro on-when 6 | [pred & body] 7 | (let [[opts body] (if (map? (first body)) 8 | [(first body) (rest body)] 9 | [{} body])] 10 | `(cljs-thread.on-when/do-on-when 11 | (fn [] ~pred) 12 | (assoc ~opts 13 | :timeout-data 14 | {:ns ~(name cljs.analyzer/*cljs-ns*) 15 | :line ~(:line (meta &form))}) 16 | (fn [] ~@body)))) 17 | 18 | (defmacro on-watch 19 | [atm pred & body] 20 | (let [[opts body] (if (map? (first body)) 21 | [(first body) (rest body)] 22 | [{} body])] 23 | `(cljs-thread.on-when/do-on-watch 24 | ~atm 25 | ~pred 26 | (assoc ~opts 27 | :wkey (str ~(str atm "-" pred) "-" (gensym)) 28 | :timeout-data 29 | {:ns ~(name cljs.analyzer/*cljs-ns*) 30 | :line ~(:line (meta &form))}) 31 | (fn [] ~@body)))) 32 | -------------------------------------------------------------------------------- /src/cljs_thread/on_when.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.on-when 2 | (:require-macros 3 | [cljs-thread.on-when]) 4 | (:require 5 | [cljs-thread.env :as e])) 6 | 7 | (defn wait-until [condition {:keys [resolve duration max-time timeout-resolve timeout-data]}] 8 | (let [duration (or duration 2) 9 | max-time (or max-time 10000) 10 | start-time (.getTime (js/Date.))] 11 | (if-let [result (condition)] 12 | (if resolve 13 | (js/Promise.resolve (resolve result)) 14 | (js/Promise.resolve result)) 15 | (js/Promise. 16 | (fn [resolve* reject] 17 | (let [timer-id (atom nil) 18 | interval-fn 19 | #(let [time-passed (-> (js/Date.) .getTime (- start-time))] 20 | (if-let [result (condition)] 21 | (do (js/clearInterval @timer-id) 22 | (if resolve 23 | (resolve* (resolve result)) 24 | (resolve* result))) 25 | (when (-> time-passed (> max-time)) 26 | (js/clearInterval @timer-id) 27 | (if timeout-resolve 28 | (resolve (timeout-resolve)) 29 | (reject (js/Error. (str "Timed out: \n" 30 | "in: " (:id e/data) "\n" 31 | "Condition:\n" 32 | condition 33 | "\nTimeout-data:\n" 34 | timeout-data)))) 35 | #_#_:else (println :RUNNING_INTERVAL :SECONDS_PASSED (/ time-passed 1000)))))] 36 | (reset! timer-id (js/setInterval interval-fn duration)))))))) 37 | 38 | (defn do-on-when [pred opts afn] 39 | (-> (wait-until pred opts) 40 | (.then afn))) 41 | 42 | (defn watch-until [atm pred {:as props :keys [resolve wkey timeout-data max-time]}] 43 | (let [max-time (or max-time 30000) 44 | start-time (.getTime (js/Date.)) 45 | watch-key (or wkey (str "wkey-" (hash pred) "-" (gensym)))] 46 | (if (pred @atm) 47 | (if resolve 48 | (js/Promise.resolve (resolve true)) 49 | (js/Promise.resolve true)) 50 | (js/Promise. 51 | (fn [resolve* reject] 52 | (add-watch 53 | atm watch-key 54 | #(let [time-passed (-> (js/Date.) .getTime (- start-time))] 55 | (if-not (-> time-passed (< max-time)) 56 | (let [error-msg (str "Watch check timed out: \n" 57 | "in: " (:id e/data) "\nCondition:\n" pred "\nTimeout-data:\n" timeout-data)] 58 | (remove-watch atm watch-key) 59 | (println error-msg) 60 | (reject (js/Error. error-msg))) 61 | (when (pred %4) 62 | (remove-watch atm watch-key) 63 | (if resolve 64 | (resolve* (resolve true)) 65 | (resolve* true))))))))))) 66 | 67 | (defn do-on-watch [atm pred opts afn] 68 | (-> (watch-until atm pred opts) 69 | (.then afn))) 70 | -------------------------------------------------------------------------------- /src/cljs_thread/pmap.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.pmap 2 | (:refer-clojure :exclude [pmap pcalls pvalues]) 3 | (:require 4 | [cljs-thread.macro-impl :as i])) 5 | 6 | (defmacro pmap [afn & args] 7 | (let [[conveyer names opts body] (i/globals-locals-and-args &env afn) 8 | yfn `(clojure.core/fn ~names ~afn)] 9 | `(cljs-thread.pmap/do-pmap ~conveyer ~yfn ~@args))) 10 | 11 | (defmacro pcalls [& fns] 12 | (let [[conveyer names opts body] (i/globals-locals-and-args &env fns) 13 | conveyer-fns (->> body 14 | (mapv (fn [afn] 15 | `(clojure.core/fn ~names (~afn)))))] 16 | `(do (cljs-thread.pmap/pmap #(apply % ~conveyer) ~conveyer-fns)))) 17 | 18 | (defmacro pvalues 19 | [& exprs] 20 | (let [[conveyer names opts body] (i/globals-locals-and-args &env exprs) 21 | conveyer-exprs (->> body 22 | (mapv (fn [expr] 23 | `(clojure.core/fn ~names ~expr))))] 24 | `(do (cljs-thread.pmap/pmap #(apply % ~conveyer) ~conveyer-exprs)))) 25 | -------------------------------------------------------------------------------- /src/cljs_thread/pmap.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.pmap 2 | (:require-macros [cljs-thread.pmap]) 3 | (:require 4 | [cljs-thread.util :as u] 5 | [cljs-thread.env :as e] 6 | [cljs-thread.in :refer [in]] 7 | [cljs-thread.future :refer [future]] 8 | [cljs-thread.injest :refer [mk-injest-ids]])) 9 | 10 | (defn zipall [& colls] 11 | (->> colls 12 | ((fn [vs] 13 | (let [vc (mapv count vs) 14 | c (apply min vc)] 15 | (repeat c vs)))) 16 | (map-indexed (fn [i v] 17 | (map #(% i) v))))) 18 | 19 | (defn do-pmap [conveyer afn & args] 20 | (let [pws (cycle (mk-injest-ids)) 21 | zipargs (if (= 1 (count args)) 22 | (map #(do [%]) (first args)) 23 | (apply zipall args)) 24 | pa (map (fn [p a] [p a]) pws zipargs) 25 | pas (partition-all (inc (u/num-cores)) pa)] 26 | (if (or (e/in-screen?) (e/in-root?)) 27 | (future 28 | (->> pas 29 | (map (fn [pa] 30 | (->> pa 31 | (mapv (fn [[p a]] 32 | (in p (apply (apply afn conveyer) a))))))) 33 | (mapcat #(map deref %)))) 34 | (->> pas 35 | (map (fn [pa] 36 | (->> pa 37 | (mapv (fn [[p a]] 38 | (in p (apply (apply afn conveyer) a))))))) 39 | (mapcat #(map deref %)))))) 40 | -------------------------------------------------------------------------------- /src/cljs_thread/repl.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.repl 2 | (:require 3 | [cljs-thread.macro-impl :as i])) 4 | 5 | (defmacro dbg [& x] 6 | (let [afn `(fn [] ~@x)] 7 | `(cljs-thread.repl/do-dbg ~afn))) 8 | 9 | (defmacro break [& x] 10 | (let [names (vec (keys (:locals &env))) 11 | symvals (zipmap (map (fn [sym] `(quote ~sym)) (mapv symbol names)) names) 12 | opts (if (map? (first x)) (first x) {})] 13 | `(cljs-thread.repl/do-break ~symvals ~opts (fn ~names ~@x)))) 14 | 15 | (defmacro in? [& x] 16 | (let [[syms body] (if (and (second x) (vector? (first x))) 17 | [(first x) `~(second x)] 18 | [(i/get-symbols x) `(do ~@x)])] 19 | `(cljs-thread.repl/dbg-> '~syms (fn ~syms ~body)))) 20 | -------------------------------------------------------------------------------- /src/cljs_thread/repl.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.repl 2 | (:require-macros 3 | [cljs-thread.repl]) 4 | (:require 5 | [cljs-thread.env :as e] 6 | [cljs-thread.state :as s] 7 | [cljs-thread.spawn :refer [spawn]] 8 | [cljs-thread.on-when :refer [on-when]] 9 | [cljs-thread.sync :as sync] 10 | [cljs-thread.in] 11 | [cljs-thread.util :as u])) 12 | 13 | (def dbg-atom (atom {:running #{}})) 14 | 15 | (def local-dbg-id (atom nil)) 16 | 17 | (defn dbg-repl [] 18 | (when (= :repl-sync (:id e/data)) 19 | (let [dbg-id (u/gen-id)] 20 | (swap! dbg-atom update :running conj dbg-id) 21 | (loop [] 22 | (let [{:keys [sfn sargs break dbg-id]} (sync/request :dbg-req {:id "dbg-repl"})] 23 | (if break 24 | (do (sync/send-response {:request-id :dbg-res :response :no-break-running!}) 25 | (recur)) 26 | (let [_ (reset! local-dbg-id dbg-id) 27 | result (cljs-thread.in/do-call 28 | {:data {:sfn sfn 29 | :sargs sargs 30 | :local? true}})] 31 | (sync/send-response {:request-id :dbg-res :response {:end-session? true :res result}}) 32 | (recur)))))))) 33 | 34 | (defn do-break [symvals ctx expr] 35 | (when (= :repl-sync (:id e/data)) 36 | (let [when-res (cljs-thread.in/do-call 37 | {:data {:sfn (str expr) 38 | :sargs (vec (vals symvals)) 39 | :local? true}})] 40 | (if-not when-res 41 | expr 42 | (let [dbg-id (u/gen-id)] 43 | (swap! dbg-atom update :running conj dbg-id) 44 | (sync/send-response {:request-id :dbg-res :response :starting-dbg}) 45 | (loop [] 46 | (let [{:keys [sfn sargs break]} (sync/request :dbg-req {:park true :max-time (* 1000 60 60) :duration 500})] 47 | (if (not break) 48 | (sync/send-response {:request-id :dbg-res :response :dbg-already-running!}) 49 | (let [conveyer (mapv symvals sargs) 50 | result (cljs-thread.in/do-call 51 | {:data {:sfn sfn 52 | :sargs conveyer 53 | :local? true}})] 54 | (if (= :in/exit result) 55 | (do (swap! dbg-atom update :running disj dbg-id) 56 | expr) 57 | (do (sync/send-response {:request-id :dbg-res :response result}) 58 | (recur)))))))))))) 59 | 60 | (def remote-break (atom nil)) 61 | 62 | (defn break-running? [] 63 | (not (nil? @remote-break))) 64 | 65 | (defn do-dbg [afn] 66 | (let [sfn (str afn) 67 | break-id (u/gen-id)] 68 | (if (break-running?) 69 | (println :break-running!) 70 | (do (reset! remote-break break-id) 71 | (sync/send-response {:request-id :dbg-req :dbg-id break-id :response {:sfn sfn :sargs [] :request-id :dbg-res}}) 72 | (let [result (sync/request :dbg-res)] 73 | result))))) 74 | 75 | (defn dbg-> [symbols afn] 76 | (if-not (break-running?) 77 | (afn) 78 | (let [sfn (str afn)] 79 | (sync/send-response {:request-id :dbg-req :response {:break true :sfn sfn :sargs symbols :request-id :dbg-res}}) 80 | (let [{:keys [end-session? res] :as result} (sync/request :dbg-res)] 81 | (if end-session? 82 | (do (reset! remote-break nil) 83 | res) 84 | result))))) 85 | 86 | (defn start-repl [configs] 87 | (when (and goog/DEBUG (:repl-connect-string configs)) 88 | (spawn {:id :repl} 89 | (s/update-conf! configs)) 90 | (spawn {:id :repl-sync :no-globals? true} 91 | (on-when (contains? @s/peers :core) 92 | (dbg-repl))))) 93 | -------------------------------------------------------------------------------- /src/cljs_thread/root.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.root 2 | (:require 3 | [cljs-thread.env :as e] 4 | [cljs-thread.state :as s] 5 | [cljs-thread.on-when :refer [on-watch]] 6 | [cljs-thread.future :as f] 7 | [cljs-thread.injest :as i])) 8 | 9 | (defn ^:export init-root! [& [config-map]] 10 | (assert (e/in-root?)) 11 | (when config-map 12 | (swap! s/conf merge config-map)) 13 | (let [config @s/conf] 14 | (on-watch s/peers :db 15 | (f/start-futures config) 16 | (i/start-injests config)))) 17 | -------------------------------------------------------------------------------- /src/cljs_thread/spawn.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.spawn 2 | (:require 3 | [cljs-thread.macro-impl :as i])) 4 | 5 | (defmacro spawn [& x] 6 | (let [[conveyer names opts body] (i/globals-locals-and-args &env x) 7 | yield? (i/yields? x) 8 | yfn (if-not yield? 9 | `(fn ~names ~@body) 10 | `(fn [in-id#] 11 | (fn ~names 12 | (let [~'yield (fn [& res#] 13 | (cljs-thread.sync/send-response 14 | {:request-id in-id# :response (last res#)}) 15 | (when (not (:deamon? cljs-thread.env/data)) 16 | (js/setTimeout #(.close js/self) 100)))] 17 | ~@body))))] 18 | (if-not (seq body) 19 | `(cljs-thread.spawn/do-spawn [] ~opts nil) 20 | `(cljs-thread.spawn/do-spawn ~conveyer (assoc ~opts :yield? ~yield?) (clojure.core/str ~yfn))))) 21 | -------------------------------------------------------------------------------- /src/cljs_thread/spawn.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.spawn 2 | (:require-macros 3 | [cljs-thread.spawn]) 4 | (:require 5 | [cljs-thread.util :as u] 6 | [cljs-thread.state :as s] 7 | [cljs-thread.env :as e] 8 | [cljs-thread.msg :as m] 9 | [cljs-thread.sync :as sync])) 10 | 11 | (defn after-sw-registration [p afn] 12 | (-> p 13 | (.then #(if (or (.-active %) (.-installing %)) 14 | (afn %) 15 | (when (.-installing %) 16 | (.addEventListener 17 | (.-installing %) "onstatechange" 18 | (partial afn %))))))) 19 | 20 | (defn on-sw-registration-reload [] 21 | (-> (js/navigator.serviceWorker.getRegistration) 22 | (.then #(when-not (.-controller js/navigator.serviceWorker) 23 | (.reload js/window.location))))) 24 | 25 | (defn on-sw-registration [cb else-cb] 26 | (-> (js/navigator.serviceWorker.getRegistration) 27 | (.then #(if (.-controller js/navigator.serviceWorker) 28 | (cb) 29 | (else-cb))))) 30 | 31 | (defn link [id] 32 | (when-not (-> @s/peers (get-in [id :port])) 33 | (if (e/in-screen?) 34 | (when-not (= id :sw) 35 | (m/add-port id (-> @s/peers (get-in [id :w])))) 36 | (let [[c1 c2] (m/mk-chan-pair)] 37 | (m/send-port id c1) 38 | (m/add-port id c2))))) 39 | 40 | (defn get-connection-string [{:keys [id]}] 41 | (let [sw-override (:sw-connect-string @s/conf "/sw.js") 42 | core-override (:core-connect-string @s/conf "/core.js") 43 | root-override (:root-connect-string @s/conf core-override) 44 | future-override (:future-connect-string @s/conf root-override) 45 | future-override (:injest-connect-string @s/conf root-override) 46 | repl-override (:repl-connect-string @s/conf core-override)] 47 | (cond 48 | (= :sw id) sw-override 49 | (= :repl id) repl-override 50 | (= :root id) root-override 51 | (= :future id) future-override 52 | (= :injest id) future-override 53 | (= :core id) core-override 54 | :else root-override))) 55 | 56 | (declare do-spawn) 57 | 58 | (defn spawn-sw [init-callback] 59 | (let [url (str (get-connection-string {:id :sw}) 60 | (u/encode-qp {:id :sw}))] 61 | (on-sw-registration 62 | #(init-callback) 63 | #(-> (js/navigator.serviceWorker.register url) 64 | (after-sw-registration 65 | (fn [] 66 | (if (.-active %) 67 | (m/add-port :sw (.-active %)) 68 | (do (m/add-port :sw (.-installing %)) 69 | (.reload js/window.location))) 70 | (init-callback))))))) 71 | 72 | 73 | (defn root-spawn [{:as data :keys [deamon?]}] 74 | (let [id (u/gen-id data) 75 | url (str (get-connection-string data) 76 | (u/encode-qp (merge {:id id :conf @s/conf} data))) 77 | w (js/Worker. url)] 78 | (set! (.-onmessage w) m/message-handler) 79 | (when deamon? 80 | (swap! s/peers assoc id {:w w :id id}) 81 | (when-not (= id :sw) 82 | (link id) 83 | (m/post id 84 | {:dispatch :call 85 | :data {:sfn (str (fn [])) :from (:id e/data) :to id}}))) 86 | id)) 87 | 88 | (defn pair-ids [id1 id2] 89 | (let [[c1 c2] (m/mk-chan-pair)] 90 | (m/dist-port id1 id2 c1 c2))) 91 | 92 | (defn meshify [id] 93 | (let [peer-ids (filter (complement #{id :parent}) (keys @s/peers))] 94 | (when (seq peer-ids) 95 | (->> peer-ids 96 | (mapv (partial pair-ids id)))))) 97 | 98 | (defn local-spawn [{:as data :keys [deamon?]}] 99 | (assert (or (and (e/in-root?) (not (u/in-safari?)) (not (= (:id data) :sw))) 100 | (e/in-screen?))) 101 | (let [id (root-spawn data)] 102 | (when-not (= id :sw) 103 | (when deamon? (meshify id))) 104 | id)) 105 | 106 | (defmethod m/dispatch :spawn 107 | [{:keys [data]}] 108 | (if-not (or (e/in-screen?) (and (not (u/in-safari?)) (e/in-root?))) 109 | (throw (js/Error. "Can't spawn locally if not on root or screen")) 110 | (local-spawn data))) 111 | 112 | (defn send-spawn [id data] 113 | (m/post id 114 | {:dispatch :spawn 115 | :data data})) 116 | 117 | (defn ^:export do-spawn [eargs {:as data :keys [caller id yield?]} efn] 118 | (let [{:as data :keys [id]} 119 | (merge data 120 | {:from (:id e/data) :to :root} 121 | (if (and (not id) efn) 122 | (if yield? 123 | (let [in-id (u/gen-id data)] 124 | {:id in-id :in-id in-id}) 125 | {:id (u/gen-id data)}) 126 | {:deamon? true 127 | :id (or id (u/gen-id data))}) 128 | (when-not caller {:caller (:id e/data)}) 129 | (when efn {:efn (str efn) :eargs eargs}))] 130 | (if (or (u/in-safari?) (= :sw id)) 131 | (if (e/in-screen?) 132 | (local-spawn data) 133 | (send-spawn :screen data)) 134 | (if (e/in-root?) 135 | (local-spawn data) 136 | (if (and (e/in-screen?) (or (= id :core) (= id :root))) 137 | (local-spawn data) 138 | (send-spawn :root data)))) 139 | (sync/wrap-derefable data))) 140 | -------------------------------------------------------------------------------- /src/cljs_thread/state.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.state 2 | (:require 3 | [cljs-thread.env :as e])) 4 | 5 | (defn shake 6 | [atm & {:keys [seconds msg limit effect] 7 | :or {seconds 30}}] 8 | (let [time-ms (* seconds 1000) 9 | limit-atom (atom limit) 10 | inter-atom (atom nil) 11 | action #(do (when msg (println (if (fn? msg) 12 | (msg) 13 | msg))) 14 | (swap! atm identity) 15 | (when limit 16 | (if (< 1 @limit-atom) 17 | (swap! limit-atom dec) 18 | (js/clearInterval @inter-atom)))) 19 | inter-id (-> action (js/setInterval time-ms))] 20 | (swap! inter-atom (constantly inter-id)))) 21 | 22 | (def initial-conf (merge {} (:conf e/data))) 23 | 24 | (def conf (atom initial-conf)) 25 | 26 | (defn ^:export update-conf! [conf-map] 27 | (swap! conf merge conf-map)) 28 | 29 | (def peers (atom {})) 30 | 31 | (shake peers :seconds 1 :limit 30) 32 | 33 | (shake peers :seconds 30) 34 | 35 | (def local-val (atom nil)) 36 | 37 | (when-not (e/in-screen?) 38 | (swap! peers assoc :parent {:id :parent :w js/self :port js/self}) 39 | (if (e/in-root?) 40 | (swap! peers assoc :screen {:id :screen :w js/self :port js/self}) 41 | (swap! peers assoc :root {:id :root :w js/self :port js/self}))) 42 | 43 | (def responses 44 | (atom {})) 45 | 46 | (def requests 47 | (atom {})) 48 | 49 | (def future-pool (atom {:available #{} :in-use #{}})) 50 | 51 | (def idb (atom nil)) 52 | -------------------------------------------------------------------------------- /src/cljs_thread/sw.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.sw 2 | (:require 3 | [clojure.edn :as edn] 4 | [cljs-thread.env :as e] 5 | [cljs-thread.on-when :refer [on-watch]] 6 | [cljs-thread.state :as s] 7 | [cljs-thread.util :as u])) 8 | 9 | (defn ^:export respond [id res] 10 | (swap! s/responses assoc id res)) 11 | 12 | (defn sleep-time [t] 13 | (js/Promise. 14 | (fn [r] (js/setTimeout r t)))) 15 | 16 | (defn response [result] 17 | (js/Response. result 18 | #js {"headers" 19 | #js {"content-type" "text/html; charset=utf-8" 20 | "Cache-Control" "max-age=31556952,immutable" 21 | "Cross-Origin-Opener-Policy" "same-origin" 22 | "Cross-Origin-Embedder-Policy" "credentialless"}})) 23 | 24 | (defn wake [] 25 | (response "Wake up!")) 26 | 27 | (defn sleep [e] 28 | (let [u (js/URL. (.-url e.request)) 29 | t (pr-str (->> u .-search str rest (apply str) edn/read-string))] 30 | (.respondWith e (-> (sleep-time t) 31 | (.then wake))))) 32 | 33 | (defn handle-request [e] 34 | (let [u (js/URL. (.-url e.request)) 35 | r (->> u .-search u/decode-qp) 36 | {:keys [request-id requester duration max-time no-park]} r] 37 | (if-not no-park 38 | (if-let [res (get @s/responses request-id)] 39 | (do (swap! s/responses dissoc request-id) 40 | (swap! s/requests dissoc request-id) 41 | (.respondWith e (js/Promise.resolve (response res)))) 42 | (let [p (js/Promise. (fn [resolve _reject] 43 | (swap! s/requests assoc request-id resolve)))] 44 | (.respondWith e p))) 45 | (.respondWith e (-> (on-watch s/responses 46 | #(contains? % request-id) 47 | {:max-time max-time} 48 | (let [result (get @s/responses request-id)] 49 | (swap! s/responses dissoc request-id) 50 | (response result))) 51 | (.then #(js/Promise.resolve %)) 52 | (.catch #(do (println :error %) 53 | (println :request-id request-id) 54 | (println :requester requester) 55 | (println :stack (.-stack %)) 56 | (println :event (pr-str e)) 57 | (println :url (pr-str u)) 58 | (println :handle-request (pr-str r)) 59 | (js/Promise.resolve (response (clj->js {:error %})))))))))) 60 | 61 | (defn handle-respond [e] 62 | (-> (-> e .-request .clone .text) 63 | (.then (fn [data] 64 | (let [{:keys [request-id] res :response} (edn/read-string data)] 65 | (if-let [resolve (get @s/requests request-id)] 66 | (do (swap! s/requests dissoc request-id) 67 | (resolve (response res))) 68 | (swap! s/responses assoc request-id res)))))) 69 | (.respondWith e (js/Promise.resolve (response "done")))) 70 | 71 | (defn fetch-response [e] 72 | (let [url (js/URL. (.-url e.request)) 73 | path (.-pathname url)] 74 | (cond (= path "/intercept/sleep/t.js") 75 | (sleep e) 76 | (= path "/intercept/request/key.js") 77 | (handle-request e) 78 | (= path "/intercept/response/key.js") 79 | (handle-respond e)))) 80 | 81 | (when (e/in-sw?) 82 | 83 | (s/shake s/responses :seconds 1 :limit 120) 84 | (s/shake s/responses :seconds 30) 85 | (.addEventListener js/self "install" #(js/self.skipWaiting)) 86 | ;; (.addEventListener js/self "activate" #(.waitUntil % (js/self.clients.claim))) 87 | (.addEventListener js/self "activate" #(js/self.clients.claim)) 88 | (.addEventListener js/self "fetch" fetch-response) 89 | 90 | :end) 91 | 92 | (defn init! [] 93 | #_ 94 | (println :starting :sw)) 95 | -------------------------------------------------------------------------------- /src/cljs_thread/sync.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.sync 2 | (:require 3 | [cljs.reader :refer [register-tag-parser!]] 4 | [cljs-thread.util :as u] 5 | [cljs-thread.env :as e] 6 | [cljs-thread.state :as s] 7 | [cljs-thread.id :refer [IDable get-id]] 8 | [clojure.edn :as edn])) 9 | 10 | (defn no-blocking? [] 11 | (not (contains? @s/conf :sw-connect-string))) 12 | 13 | (defn throw-if-non-blocking [] 14 | (when (no-blocking?) 15 | (throw (ex-info (str "Can't deref without a service worker.\n" 16 | "Try adding a `:sw-connect-string \"sw.js\"` to your `init! config\n" 17 | "Something like:\n" 18 | " `(cljs-thread.core/init! {:sw-connect-string \"sw.js\"\n" 19 | " :connect-string \"/core.js\"})") 20 | {:conf @s/conf 21 | :env e/data})))) 22 | 23 | (defn request [getter & {:as opts :keys [resolve reject no-park max-time duration]}] 24 | (throw-if-non-blocking) 25 | (let [req {:request-id getter :requester (:id e/data) :no-park no-park :max-time max-time :duration duration} 26 | do-request (fn [] 27 | (try 28 | (let [xhr (js/XMLHttpRequest.)] 29 | (.open xhr "GET" 30 | (str "/intercept/request/key.js" (u/encode-qp req)) 31 | (if (or (e/in-screen?) resolve) true false)) 32 | (.setRequestHeader xhr "cache-control" "no-cache, no-store, max-age=0") 33 | (when resolve 34 | (set! (.-onload xhr) #(resolve (edn/read-string (.-response xhr))))) 35 | (when reject 36 | (set! (.-onerror xhr) #(reject (.-status xhr)))) 37 | (.send xhr) 38 | (if resolve 39 | xhr 40 | (let [res (edn/read-string (.-responseText xhr))] 41 | res))) 42 | (catch :default e 43 | (when-not (= :repl-sync (:id e/data)) 44 | (println :error :requesting-response :e e)))))] 45 | (when (or (not (= getter :sw)) (not (e/in-screen?)) (.-controller js/navigator.serviceWorker)) 46 | (do-request)))) 47 | 48 | (defn send-response [payload & [db?]] 49 | (throw-if-non-blocking) 50 | (try 51 | (let [req (if db? 52 | {:responder (:id e/data) :db? db?} 53 | {:responder (:id e/data)}) 54 | xhr (js/XMLHttpRequest.)] 55 | (.open xhr "POST" (str "/intercept/response/key.js" (u/encode-qp req))) 56 | (.setRequestHeader xhr "Content-Type" "text/plain;charset=UTF-8") 57 | (.setRequestHeader xhr "cache-control" "no-cache, no-store, max-age=0") 58 | (.send xhr (pr-str payload)) 59 | (.-responseText xhr) 60 | nil) 61 | (catch :default e 62 | (println :error :sending-response :e e)))) 63 | 64 | (extend-type js/Promise 65 | ICloneable 66 | (-clone [p] (.then p))) 67 | 68 | (extend-type string 69 | ICloneable 70 | (-clone [s] (js/String. s))) 71 | 72 | (extend-type cljs.core/Keyword 73 | ICloneable 74 | (-clone [k] (keyword k))) 75 | 76 | (defn wrap-derefable [{:keys [promise? id] :as data}] 77 | (let [id (if (instance? IDable id) (get-id id) id) 78 | resolved? (atom false) 79 | resolved-value (atom nil) 80 | promise? (if (or (e/in-root?) (e/in-screen?)) true promise?) 81 | do-promise (fn [no-delay?] 82 | (-> (js/Promise. (if no-delay? 83 | #(request id {:resolve %1 :reject %2}) 84 | #(do id))) 85 | (.then (fn [result] 86 | (reset! resolved? true) 87 | (reset! resolved-value result) 88 | (if (:error result) 89 | (throw (ex-info "Error in remote call" {:result (pr-str result)})) 90 | result))))) 91 | p (if-not promise? 92 | id 93 | (do-promise false))] 94 | (specify p 95 | IDable 96 | (get-id [_] id) 97 | IPending 98 | (-realized? [_] @resolved?) 99 | IPrintWithWriter 100 | (-pr-writer [x writer opts] 101 | (-write writer 102 | (str "#cljs-thread {:id " 103 | (if (keyword? id) 104 | id 105 | (pr-str id)) 106 | "}"))) 107 | IDeref 108 | (-deref [_] 109 | (if-let [res @resolved-value] 110 | res 111 | (if promise? 112 | (do-promise true) 113 | (let [_ (throw-if-non-blocking) 114 | res (request id)] 115 | (reset! resolved? true) 116 | (reset! resolved-value res) 117 | (if (:error res) 118 | (throw (ex-info "Error in remote call" {:results (pr-str res)})) 119 | res)))))))) 120 | 121 | (register-tag-parser! 122 | 'cljs-thread (fn [x] 123 | (wrap-derefable x))) 124 | 125 | (defn sleep [n] 126 | (throw-if-non-blocking) 127 | (let [xhr (js/XMLHttpRequest.)] 128 | (.open xhr "GET" (str "/intercept/sleep/t.js?" n) false) 129 | (.setRequestHeader xhr "cache-control" "no-cache, no-store, max-age=0") 130 | (.send xhr "request") 131 | (.-responseText xhr)) 132 | nil) 133 | -------------------------------------------------------------------------------- /src/cljs_thread/util.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-thread.util 2 | (:require 3 | [clojure.edn :as edn] 4 | [goog.string :as string] 5 | [cljs.reader :as reader])) 6 | 7 | (set! reader/*default-data-reader-fn* (atom tagged-literal)) 8 | 9 | (defn encode-qp [m] 10 | (let [qp-s (str "?" (string/urlEncode (pr-str m)))] 11 | qp-s)) 12 | 13 | (defn decode-qp [s] 14 | (some->> s 15 | str 16 | (#(if (.startsWith % "?") (rest %) (seq %))) 17 | (apply str) 18 | string/urlDecode 19 | edn/read-string)) 20 | 21 | (defn gen-id [& [data]] 22 | (or (:id data) 23 | (str (random-uuid)))) 24 | 25 | (defn num-cores [] 26 | (.-hardwareConcurrency js/self.navigator)) 27 | 28 | (defn in-browser? [browser-string] 29 | (-> js/navigator.userAgent 30 | (.indexOf browser-string) 31 | (> -1))) 32 | 33 | (defn in-chrome? [] 34 | (in-browser? "Chrome")) 35 | 36 | (defn in-ie? [] 37 | (or (in-browser? "MSIE") 38 | (in-browser? "rv:"))) 39 | 40 | (defn in-firefox? [] 41 | (in-browser? "Firefox")) 42 | 43 | (defn in-safari? [] 44 | (and (in-browser? "Safari") 45 | (not (in-chrome?)))) 46 | 47 | (defn in-opera? [] 48 | (and (in-browser? "OP") 49 | (not (in-chrome?)))) 50 | 51 | (defn browser-type [] 52 | (cond (in-chrome?) :chrome 53 | (in-ie?) :ie 54 | (in-firefox?) :firefox 55 | (in-safari?) :safari 56 | (in-opera?) :opera)) 57 | 58 | (defn typed-array? 59 | "Tests whether a given `value` is a typed array." 60 | [value] 61 | (let [value-type (type value)] 62 | (or (when (exists? js/SharedArrayBuffer) 63 | (= value-type js/SharedArrayBuffer)) ;; <- not yet tested 64 | (= value-type js/Int8Array) 65 | (= value-type js/Uint8Array) 66 | (= value-type js/Uint8ClampedArray) 67 | (= value-type js/Int16Array) 68 | (= value-type js/Uint16Array) 69 | (= value-type js/Int32Array) 70 | (= value-type js/Uint32Array) 71 | (= value-type js/Float32Array) 72 | (= value-type js/Float64Array)))) 73 | 74 | ;; TODO: add transfer semantics for these types: 75 | ;;:Blob.readAsArrayBuffer 76 | ;;:File.readAsArrayBuffer 77 | ;;:Base64 78 | ;;:DataView 79 | ;;:ArrayBuffer 80 | ;;:MessagePort 81 | ;;:ReadableStream 82 | ;;:WritableStream 83 | ;;:TransformStream 84 | ;;:AudioData 85 | ;;:ImageBitmap 86 | ;;:VideoFrame 87 | ;;:OffscreenCanvas 88 | ;;:RTCDataChannel 89 | --------------------------------------------------------------------------------