├── .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 |
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 |