├── .gitignore
├── README.md
├── deps.edn
├── doc
├── arborist-protocols.md
├── arch
│ ├── arborist-protocols.md
│ ├── big-picture.md
│ ├── collections.md
│ ├── composability.md
│ ├── forms.md
│ └── scheduler.md
├── async.md
├── components.md
├── events.md
├── fragments.md
├── reusable-components.md
├── what-the-heck-just-happened.md
└── worker.md
├── examples
├── app
│ ├── .gitignore
│ └── public
│ │ ├── css
│ │ ├── codemirror.css
│ │ └── tailwind.min.css
│ │ └── index.html
├── bench-fragment
│ ├── .gitignore
│ └── index.html
├── conduit
│ ├── .gitignore
│ ├── index.html
│ └── manifest.json
├── fulcro
│ ├── .gitignore
│ └── index.html
├── reusable-components
│ ├── .gitignore
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── css
│ │ │ ├── material-design-iconic-font.min.css
│ │ │ ├── re-com.css
│ │ │ └── tailwind.min.css
│ │ └── index.html
│ ├── shadow-cljs.edn
│ └── src
│ │ └── main
│ │ └── test
│ │ └── samples.cljs
├── suspense
│ ├── .gitignore
│ ├── css
│ │ └── tailwind.min.css
│ └── index.html
├── todomvc-split
│ ├── .gitignore
│ ├── index.html
│ ├── learn.json
│ ├── loading.svg
│ └── vendor
│ │ ├── todomvc-app-css
│ │ ├── index.css
│ │ ├── license
│ │ ├── package.json
│ │ └── readme.md
│ │ └── todomvc-common
│ │ ├── base.css
│ │ ├── base.js
│ │ ├── license
│ │ ├── package.json
│ │ └── readme.md
├── todomvc
│ ├── .gitignore
│ ├── index.html
│ ├── learn.json
│ ├── loading.svg
│ └── vendor
│ │ ├── todomvc-app-css
│ │ ├── index.css
│ │ ├── license
│ │ ├── package.json
│ │ └── readme.md
│ │ └── todomvc-common
│ │ ├── base.css
│ │ ├── base.js
│ │ ├── license
│ │ ├── package.json
│ │ └── readme.md
├── ui
│ ├── .gitignore
│ └── index.html
└── website
│ └── dev.edn
├── package-lock.json
├── package.json
├── packages
└── examples
│ ├── package-lock.json
│ └── package.json
├── project.clj
├── public
└── index.html
├── shadow-cljs.edn
└── src
├── dev
├── dummy
│ ├── services
│ │ └── foo.clj
│ ├── suspense.cljs
│ ├── system.clj
│ ├── ui.cljs
│ └── website.clj
├── shadow
│ └── experiments
│ │ └── grove
│ │ └── test_app
│ │ └── dom.cljs
└── todomvc
│ ├── model.cljs
│ ├── simple.cljs
│ └── split
│ ├── db.cljs
│ ├── env.cljs
│ ├── main.cljs
│ ├── single.cljs
│ ├── views.cljs
│ └── worker.cljs
├── main
└── shadow
│ └── experiments
│ ├── arborist.clj
│ ├── arborist.cljs
│ ├── arborist
│ ├── attributes.cljs
│ ├── collections.clj
│ ├── collections.cljs
│ ├── common.cljs
│ ├── dom_scheduler.clj
│ ├── dom_scheduler.cljs
│ ├── fragments.cljc
│ ├── fragments.cljs
│ ├── interpreted.cljs
│ └── protocols.cljs
│ ├── archetype
│ └── website.clj
│ ├── grove.clj
│ ├── grove.cljs
│ ├── grove
│ ├── cards
│ │ ├── env.cljs
│ │ └── runner.cljs
│ ├── components.clj
│ ├── components.cljs
│ ├── css_transition.cljs
│ ├── db.cljc
│ ├── dev_support.cljs
│ ├── edn.cljs
│ ├── effects.cljs
│ ├── eql_query.cljc
│ ├── event_fsm.cljs
│ ├── events.clj
│ ├── events.cljs
│ ├── examples
│ │ ├── app.cljs
│ │ ├── cljs_editor.cljs
│ │ ├── env.cljs
│ │ ├── js_editor.cljs
│ │ └── model.cljs
│ ├── history.cljs
│ ├── http_fx.cljs
│ ├── keyboard.cljs
│ ├── local.cljs
│ ├── preload.cljs
│ ├── protocols.cljs
│ ├── runtime.clj
│ ├── runtime.cljs
│ ├── server.clj
│ ├── timeouts.cljs
│ ├── transit.cljs
│ ├── ui
│ │ ├── atoms.cljs
│ │ ├── data.cljs
│ │ ├── dnd_sortable.cljs
│ │ ├── forms.cljs
│ │ ├── grid.cljs
│ │ ├── loadable.clj
│ │ ├── loadable.cljs
│ │ ├── portal.cljs
│ │ ├── suspense.cljs
│ │ ├── testing.cljs
│ │ ├── util.cljs
│ │ └── vlist.cljs
│ └── websocket_engine.cljs
│ ├── system.clj
│ ├── system
│ └── runtime.clj
│ └── system_dev.clj
└── test
└── shadow
└── experiments
├── arborist
├── components.js
├── keyed_seq_test.cljs
└── wc_test.cljs
├── arborist_test.clj
└── grove
├── bench_db.cljs
├── bench_fragment.cljs
├── builder_test.clj
├── collections_test.clj
├── db_test.clj
├── forms_test.cljs
├── html_test.clj
├── others
├── builder.clj
├── builder.cljs
├── html.clj
├── html.cljs
├── protocols.cljc
├── react.clj
└── react.cljs
└── react
├── bench_dom.cljs
└── dump.cljs
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | public/js
3 |
4 | /target
5 | /tmp
6 | /checkouts
7 | /src/gen
8 | /out
9 |
10 | /.classpath
11 | /.project
12 | /.settings
13 | /.cpcache
14 |
15 | pom.xml
16 | pom.xml.asc
17 | *.iml
18 | *.jar
19 | *.log
20 | .shadow-cljs
21 | .idea
22 | .lein-*
23 | .nrepl-*
24 | .DS_Store
25 |
26 | .hgignore
27 | .hg/
28 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths
2 | ["src/dev"
3 | "src/main"
4 | "src/test"]
5 |
6 | :deps
7 | {}
8 |
9 | :aliases
10 | {:server
11 | {:extra-deps {thheller/shadow-cljs {:mvn/version "2.17.8"}}
12 | :jvm-opts ["-Dfile.encoding=UTF-8"]
13 | :main-opts ["-m" "shadow.cljs.devtools.cli" "clj-repl"]}}}
14 |
--------------------------------------------------------------------------------
/doc/arch/collections.md:
--------------------------------------------------------------------------------
1 | # Collections
2 |
3 | As previously described the core Arborist protocol only covers how elements are created, updated and destroyed. There was no mention how collections of elements are handled. The core implementation indeed doesn't cover collections of elements at all. Instead this is left to specialized implementations of the core Arborist protocol. All other implementations as such only handle a single node in the tree and they look just like any other node.
4 |
5 | Other react-like implementations usually only have one algorithm for dealing with collections. So end users create an array of elements and the library handles dealing with them in a uniform way. As far as I can tell there is no way to teach these libraries about new algos.
6 |
7 | In Arborist there are 2 core implementations and a couple more specialized ones for certain situations.
8 |
9 |
10 | ## simple-seq
11 |
12 | `simple-seq` is a very basic implementation rendering elements in order. It makes no attempts to minimize the DOM operations in any way. This is by far the fastest method of rendering a collection of elements that very rarely change.
13 |
14 | Say you want to get this piece of DOM from a simple `["a" "b" "c"]` vector.
15 |
16 | ```html
17 |
18 |
a
19 |
b
20 |
c
21 |
22 | ```
23 | With `simple-seq` that becomes
24 | ```clojure
25 | (<< [:ul
26 | (sg/simple-seq ["a" "b" "c"]
27 | (fn [item]
28 | (<< [:li item])))])
29 | ```
30 |
31 | Append only collections can also be very efficient using this but anything that changes the order or deletes items at the start or middle will end up doing a lot more DOM operation than its more optimized variant the `keyed-seq`.
32 |
33 | ## keyed-seq
34 |
35 | This is basically the more commonly key-based algorithm many other implemenations such as react use. Each element in the collection needs to supply a key and that key is used to re-order DOM elements instead of rendering over them. When Element #5 was moved to #2 the actual DOM element is just moved, instead of rendering the contents of #5 into what was previously #2.
36 |
37 | However instead of the developer providing elements with a key, the `keyed-seq` instead will just take the regular collection and a key-fn to it can construct the necessary keys itself. It also happens to safe one iteration over all the elements, which saves a bit of time.
38 |
39 | Collections such as `["a" "b" "c"]` that don't have a natural key are probably better handled by `simple-seq`. However many collections you may end up working with might have a natural key already, so we just use them.
40 |
41 | ```clojure
42 | (def data
43 | [{:id 1 :text "a"}
44 | {:id 2 :text "b"}
45 | {:id 3 :text "c"}])
46 |
47 | (<< [:ul
48 | (sg/keyed-seq data :id
49 | (fn [item]
50 | (<< [:li (:text item)])))])
51 | ```
52 |
53 | `keyed-seq` here takes the additional `:id` arguments which in this case is the `key-fn`. Since keywords implement the core clojure IFn protocol they can just be called as a function, so essentially this will extract the `:id` from each item in the collection and use that for `keyed-seq` purposes. And function taking one argument (the collection item) is valid here. `identity` is fine too.
54 |
55 | Note that the above example is misleading. `data` is fixed and cannot change, so even though the collection has a natural key using `simple-seq` here would still be more efficient. Just imagine for a sec that `data` isn't actually fixed and may be changing while visible on screen.
56 |
57 | ## simple-seq vs keyed-seq
58 |
59 | It is absolutely fine to only use `simple-seq` and only used `keyeq-seq` to optimize certain places.
60 |
61 | Whether the overhead of `keyed-seq` is actually worth it largely depends on how often the collection is actually modified. The more items are added, removed or re-ordered the more relevant it becomes.
62 |
63 | ## reagent/react comparison
64 |
65 | In react-based libraries such as reagent very commonly `for` or `map` are used to render collections.
66 |
67 | ```clojure
68 | ;; grove
69 | (<< [:ul
70 | (sg/simple-seq ["a" "b" "c"]
71 | (fn [item]
72 | (<< [:li item])))])
73 |
74 | ;; reagent
75 | [:ul
76 | (for [item ["a" "b" "c"]]
77 | [:li item])]
78 | ```
79 |
80 | While this looks a little shorter syntax wise this has a couple of problems. First we really can't have laziness here at all. All collections must be forced in the render phase. If react for example would delay forcing this collection, maybe due to concurrent mode, things will get hairy very quickly.
81 |
82 | Secondly react will yell at you since no key is provided. So often you need to invent a key here. I have seen apps that either just use the `item` itself, using the index together with `map-indexed` or something worse such as `(random-uuid)`. These defeat the purpose of keys completely and nullify all they are meant to optimize. I have also seen the alternative of using `into`, such as
83 |
84 | ```clojure
85 | (into [:ul]
86 | (for [item ["a" "b" "c"]]
87 | [:li item]))
88 | ```
89 |
90 | This is reasonable since it makes react happy. As far as it is concerned it is no longer seeing a collection, just some elements. However, given how reagent works this is much less efficient and leads to many more iterations of the collection than necessary.
91 |
92 | # Conclusion
93 |
94 | I think `simple-seq` and `keyed-seq` provide a reasonable alternative that still look friendly enough as to not miss `for` too much. An alternative macro might be useful.
95 |
96 | The burden is on the developer to pick the best variant but using either is fine for most cases. This is fine and also leaves the door open for more specialized implementations such as virtual lists. Or maybe collections that can be sorted via drag&drop. They can also do what is best for them and can re-use the existing implementations or just to something entirely custom.
--------------------------------------------------------------------------------
/doc/arch/composability.md:
--------------------------------------------------------------------------------
1 | # Composability
2 |
3 | One major goal the arborist protocols are designed for is that everything composes effortlessly together. Let me use an example to show what I mean by that.
4 |
5 | Suppose there is a button in your UI that you want to use in several places.
6 | ```clojure
7 | (defn ui-fancy-button [ev label]
8 | (<< [:button {:class "fancy-button" :on-click ev} label]))
9 | ```
10 |
11 | `:on-click` it should trigger an event and it should use a text `label`. I'm trying to keep this example as simple as possible. You can make buttons much more complicated if you want to.
12 |
13 | One might use it like
14 |
15 | ```clojure
16 | (defn ui-example []
17 | (<< [:div "My UI"]
18 | [:div "look at my fancy button: "
19 | (ui-fancy-button ::click! "Click me!")]))
20 | ```
21 |
22 | Simple enough. Events are allowed to be keywords or maps with an `:e` keyword.
23 |
24 |
25 | ```clojure
26 | (ui-fancy-button {:e ::click! :some "data"}
27 | "Click me!")
28 | ```
29 |
30 | Since events are just data they compose just fine.
31 |
32 | The label however is a different story. Naively you might restrict this to be a `string?` only, which on the surface certainly looks reasonable. However, since everything is meant to compose this is also valid.
33 |
34 | ```clojure
35 | (ui-fancy-button {:e ::click! :some "data"}
36 | (<< "You should " [:b "Click me!"]))
37 | ```
38 |
39 | So, now this is no longer just a string but "actual" HTML, yielding the expected DOM structure. (`:on-click` omitted)
40 |
41 | ```html
42 |
43 | ```
44 |
45 | So, the fragments compose nicely with each other and can be passed as arguments just fine. Of course this isn't limited to fragments. You could pass in a component, `sg/keyed-seq` and all the other arborist protocol implementations as well.
46 |
47 | Since fragments also cover passing multiple children it is never necessary to fall back to CLJS varargs and splicing children in somehow. Each function/component can just take one "child" argument and that covers zero or more actual children being passed.
--------------------------------------------------------------------------------
/doc/arch/forms.md:
--------------------------------------------------------------------------------
1 | # Forms
2 |
3 | Forms are one of the most essential things in frontend UIs. Just displaying data is quite boring. At some point you'll want to let your users modify it in some way.
4 |
5 | Although all the mechanisms for working with forms are present in `shadow-grove` currently I consider it a substantial missing piece to have no first class form support. Forms can get complex rather quickly and unreasonably complex if you factor in accessibility concerns (eg. `aria-*` attributes).
6 |
7 | ## Form Problem: Rendering
8 |
9 | There are certain aspects in the rendering mode used by `shadow-grove` (as well as `react` and others) where you end up in a render cycle that can be problematic.
10 |
11 | ```clojure
12 | (defc ui-form []
13 | (bind data-ref (atom {:hello "world"}))
14 | (bind {:keys [hello] :as data} (sg/watch data-ref))
15 |
16 | (render
17 | (<< [:form {:on-submit ::submit!}
18 | [:label {:for "hello"} "Hello: "]
19 | [:input {:id "hello" :type "text" :value hello :on-input ::input!}]
20 | [:button {:type "submit"} "Go!"]]))
21 |
22 | (event ::submit! [env ev e]
23 | (js/console.log "submit" data))
24 |
25 | (event ::input! [env ev e]
26 | (swap! data-ref assoc :hello (.. e -target -value))))
27 | ```
28 |
29 | I'm using a local `atom` here, which I consider a total anti-pattern, but it is useful for keeping this example short and concise. The same problem arises when using the properly normalized db and EQL queries. The steps that occur are:
30 |
31 | - On first mount the `data-ref` atom is created with the initial state of `{:hello "world"}`. This will only run once.
32 | - The second `bind` will watch the first binding `data-ref`. It will trigger whenever the value in `data-ref` is modified.
33 | - It gets the `:hello` value out of the data and the `render` uses it to set the `:value` of the input.
34 | - The `input` event of the text input will trigger with any input made. So suppose we add a `!`. It'll trigger the `::input!` event with `(.. e -target -value)` being `hello!`.
35 | - We then update the `data-ref` accordingly, which will in turn trigger the second hook to update.
36 | - Since `hello` changed we will also and up in `render` again to set `:value` to `"hello!"`.
37 |
38 | As you may have guessed this render was entirely unnecessary. It was already at that value since it originated from there.
39 |
40 | This can get way out of control if you do something async (eg. talk to a server) which may take some time. If the user continues typing you now may need to abort that work. At the very least you need to make sure you don't end up resetting the value back to something outdated. Just debouncing the event is not a solution.
41 |
42 | One way for dealing with this in `react` is via "controlled" and "uncontrolled" components. You could do the same in `shadow-grove` but I consider this lacking in several regards.
43 |
44 | ## Form Goals
45 |
46 | - Avoid unnecessary renders.
47 | - Deal with `id` and `aria-*` related attributes in some declarative manner.
48 | - Neatly integrated into the core library
49 | - Extensible. Probably protocol based so custom input components can be created.
50 | - Composable. Nesting form inputs is common, one form may become an attribute of another form.
51 | - Validation. HTML standard form validation is not enough, full current form state needs to be considered for some validations.
52 | - Error Messages. Ideally should also cover with error messages in some way, although that may be best dealt with directly in render.
53 |
54 | **I have not yet settled on something for this.** For now everything is in the exact same place that `react` has been in since release. It is definitely workable but I'd like something better integrated and less manual.
--------------------------------------------------------------------------------
/doc/events.md:
--------------------------------------------------------------------------------
1 | ## Thoughts on DOM Events
2 |
3 | In React all events are handled by functions. Creating these functions in "render" is problematic because the engine can only see that they are different and accordingly must update the DOM to apply a new handler.
4 |
5 |
6 | ```
7 | [:div {:onClick (fn [e] ...)} ...]
8 | ```
9 |
10 | To make things a bit cleaner in React you'd move them to a hook or class-bound fn so the engine can "see" that fns didn't change and skip the DOM update. But that makes handling these functions a bit harder since without a class we can't have the bound fn and hooks need to manage captured state properly (easy to miss).
11 |
12 | ```
13 |
...
14 | ```
15 |
16 | This makes it impossible to pass arguments to the event handler. It must manage that via state/props or component attributes. Thats not necessarily a bad thing but may require creating multiple event fns that then dispatch to some common implementation.
17 |
18 | ```
19 |
...
20 |
...
21 | ```
22 |
23 |
24 | In `shadow.arborist` I want to discourage the inline-fn as much as possible since its the worst case and breaks the "declarative" nature of hiccup completely.
25 |
26 | One approach I currently prefer is using a vector and a keyword with additional optional args.
27 |
28 | ```
29 | [:div {:on-click [::foo! 1 2 3]} ...]
30 | ```
31 |
32 | If the arguments are completely static the fragment macro detects that and never needs to update the event listener on the actual DOM node.
33 |
34 | Args however would still cause an update in each render though.
35 |
36 | ```
37 | [:div {:on-click [::foo! some-arg]} ...]
38 | ```
39 |
40 | Not worth worrying about since its cheap enough BUT there may be cases where render can be skipped completely if only `some-arg` changed.
41 |
42 | ```
43 | (defc ui-example [{:keys [some-arg] :as props}]
44 | [::foo!
45 | (fn [env e] (js/console.log e some-arg))]
46 |
47 | (<< [:div {:on-click [::foo!]} ...]))
48 | ```
49 |
50 | In this the hooks would trigger the update but since the body is not using the changed `some-arg` the render and DOM update is skipped completely. Not sure how common this would be. Probably unlikely.
51 |
52 | ```
53 | (defc ui-example [{:keys [some-arg] :as props}]
54 | [foo! (some-hook-creating-event-fn)]
55 |
56 | (<< [:div {:on-click [foo!]} ...]))
57 | ```
58 |
59 | This is currently also allowed. Not a fan but makes it easier to pass around event handlers to child components and so on.
60 |
61 | Overall this should have all the power of inline functions without the "mess". Keyword events also make it a bit easier to move events out of the component into some kind of multi-method or general "transaction" system. The only reason to process events in the components is to extract the required data out of the DOM event `e` (eg. `e.target.value`).
--------------------------------------------------------------------------------
/doc/fragments.md:
--------------------------------------------------------------------------------
1 | ## Thoughts on Fragments
2 |
3 | Fragments are "optimized" Hiccup so that it doesn't have to allocate the entire structure each render and can also skip over the diffing as much as possible.
4 |
5 |
6 | ```
7 | (defn ui-fragment [v]
8 | (<< [:div.card
9 | [:div.card-title "title"]
10 | [:div.card-body v]
11 | (when v
12 | (<< [:div.card-footer
13 | [:div.card-actions
14 | [:button "ok" v]
15 | [:button "cancel"]]]))]))
16 | ```
17 |
18 | I kinda like `<<` but `<>` or `html` would also work. Naming is hard.
19 |
20 |
21 | ```
22 | (defn ui-hiccup [v]
23 | [:div.card
24 | [:div.card-title "title"]
25 | [:div.card-body v]
26 | (when v
27 | [:div.card-footer
28 | [:div.card-actions
29 | [:button "ok" v]
30 | [:button "cancel"]]])])
31 | ```
32 |
33 | "Interpreted" hiccup won't be enabled by default but it is easy to support. Just 2-10x slower for simple example and lots more for complex examples using actual maps as props. It just has to allocate too much and diff too much in each render. Same as React. With the fragment macro it can detect most of it at compile time and emit optimized create functions and update functions that skip over the "static" things. Similar to Svelte.
34 |
35 | The question is how smart should be macro be. I prefer it to be kinda dumb in that it only analyzes vectors and as soon as it encounters anything else it backs off and treats everything as normal code. In the fragment example however that means then `(when v ...)` stops the fragment processing so the body needs to use `<<` again. For me this is natural already but may confuse people used to Hiccup/Reagent. With a good runtime error this should be easy enough to adapt to.
36 |
37 | There could be a "strict" variant of the fragment macro that only allows a basic subset of clojure code inside the fragment itself. So that it doesn't allow most forms but knows how to process `if` and `when` and maybe `for` or so. Similar to other directives in vue/svelte/etc. So that fragments don't allow ALL clojure code since that is too hard to actually analyze at compile time. We also need `<<` anyways when passing fragments as args and so on. Easy to detect `[:div {:attr (some-code) ...]` not so easy to detect
38 |
39 | ```
40 | [:div
41 | (some-code-that-has-a-nested-fragment
42 | [:div ...])]
43 | ```
44 |
45 |
46 | ## Components in Fragments
47 |
48 | Hiccup and JSX have special syntax for Components too look like regular DOM elements. That is not a pattern I want to repeat. For one there should be no difference between a regular function call (returning anything renderable) and a component (stateful, with lifecycle).
49 |
50 | ```
51 | (<< [:div "before" [some-component foo] "after"])
52 | ```
53 |
54 | This is ambigious to parse since we can't know if `foo` is a map of props or something renderable like a string.
55 |
56 | ```
57 | (<< [:div "before" (some-component foo) "after"])
58 | ```
59 |
60 | works just as well and makes it clearer that hiccup notation is for DOM elements only and everything else is actual "code".
61 |
62 | The only area where allowing dynamic "tags" is interesting is for passing nested elements.
63 |
64 | ```
65 | [:div
66 | [some-component {:static "props}
67 | [:div ...]]]
68 | ```
69 |
70 | React doesn't support this at all and requires passing functions. I think that is fine but makes diffing problematic again. I actually prefer the web components "slot" method that could be easily adapted.
71 |
72 | ```
73 | [:div
74 | [:> (some-component {:static "props"})
75 | [:div ...]]]
76 | ```
77 |
78 | `:>` for components and nested elements for slot content where the component itself just leaves a marker where that content should go similar to web components.
79 |
80 | ```
81 | (defc some-components [props]
82 | []
83 | (<< [:div "component-content" (sc/slot)]))
84 | ```
85 |
86 | Slots could also be named to allow multiple. Without shadow/light DOM separation however I'm not sure how useful this actually is. Not sure how useful this is but I do like the idea. Maybe it just feels weird because React doesn't have it. I can't think of a situation where I'd use it because of using React too much? It just feels better to pass data to the actual component and let it render stuff however it needs?
87 |
--------------------------------------------------------------------------------
/doc/worker.md:
--------------------------------------------------------------------------------
1 | ## Write framework using a web-worker?
2 |
3 | In a discussion with @jacekschae about web frameworks in general and why Workers aren't used more I just dismissed it outright claiming that serializing data back and forth adds more overall cost than what you'd gain by moving code into a worker. Shortly after I however realized that I didn't test this in a long time and the last time I tested this was using EDN and naively just shipping the entire app-state map in each frame. That of course blew the 16ms budget with even very small app-state maps.
4 |
5 | I decided to revisit my assumptions about all of this and starting adapting the "grove" framework to allow moving all "application" code to a worker and only keeping UI "views" in the main thread. That actually turned out to be real easy given that the component code already is designed to abstract away async code as much as possible.
6 |
7 | There are now two variants for "todomvc". One using the worker split and one with no split.
8 |
9 | - https://code.thheller.com/demos/todomvc/
10 | - https://code.thheller.com/demos/todomvc-split/
11 |
12 | Code in `src/dev/todomvc`.
13 |
14 | The "views" and "tx" (state ops) code is almost identical. It isn't currently since I need to sort out some framework namespaces and probably introduce a protocol or something. They can be 100% identical.
15 |
16 | So whats left is a bit of glue code either starting a worker or not and ensuring that code can be split properly so the worker doesn't end up loading view code (which may contain DOM interop code it can't run).
17 |
18 | ## API Design
19 |
20 | `defc` components currently use `sg/query` to "query" the application state. In the default variant that just accesses the data directly. In the worker variant it instead sends the query (already data) to the worker using transit. The worker then processes the actual query and sends back the data. The query hook "suspended" when it sent the query and can "resume" processing when the data arrives.
21 |
22 | ```clojure
23 | (defc ui-sample [todo-ident]
24 | [data
25 | (sg/query todo-ident
26 | [::m/todo-text
27 | ::m/editing?
28 | ::m/completed?])]
29 |
30 | (<< [:div ... data]))
31 | ```
32 |
33 | The worker variant obviously makes everything async but from the User API perspective this looks identical. The implementation takes care of async and the `defc` macro makes it look like regular sync code.
34 |
35 | It does make scheduling/coordinating all that work a lot harder but that work needs to be done anyways since queries are supposed to be async in non-worker code too (to support fetching remote data). So its really just an implementation detail, the component author shouldn't see it.
36 |
37 | The assumption is that everything can go async anyways, so actually doing it to talk to a worker might not be that bad.
38 |
39 | ### Moving to Worker
40 |
41 | Since queries allow us to only ship small chunks of data at a time (and updating that incrementally) the "main" thread never needs the full database. Turns out `transit-cljs` is already quite fast and probably fast enough to not worry about it. So serialization performance is actually not the limiting factor anymore and could be optimized further if needed.
42 |
43 | Overall however for a simple app like todomvc using the worker split actually still makes the app slower. I do however believe that larger actual apps could benefit a lot from moving more code to the worker. Enough so that I'll will try to explore this as the default for the framework. Its about time we moved off the main thread anyways.
44 |
45 | todomvc is "slower" since startup basically happens twice. Since we need `cljs.core` available in both the main thread and the worker we'll have a `shared.js` containing all shared code. `cljs.core` and `transit-cljs` at the very least. The non-worker code didn't use `transit-cljs` at all so was obviously smaller but real apps likely have that dependency anyways. The `shared.js` however needs to be loaded and eval'd in the main thread and the worker. I think the Browser will be smart enough to actually only send out the request once but it'll still load the code twice. This isn't noticeable on my desktop but running with 6x slowdown (emulating a slow mobile) this is still noticeable. I also can't seem to get the Worker to actually start faster/sooner. It always seems to take a bit of time longer thus causing a bit of an additional delay.
46 |
47 | On the desktop none of this is noticeable (at most 100ms difference on start). Making everything async actually performs better when using 6x slowdown since the "blocking" periods are smaller but the startup delay is also longer.
48 |
49 | ### Work TBD
50 |
51 | - The UI code will need a "Suspense" style component so the UI becomes less "jumpy" but even without just looks like a regular UI talking to the network. This will need to exist for other async IO purposes anyways so it isn't worker related.
52 |
53 | - Framework re-org so that using a worker becomes opt-in/out and doesn't require any changes to UI code. Currently this requires changing a `ns` `:require` but shouldn't.
54 |
55 | - The DB should be separated anyways but there should be ways to "force" that no view code can access DB code directly and has to go through `sg/query`.
56 |
57 | - Can't use the `fulcro` patterns either since the Worker code can't access the View Components either. Also creating one big root query is a problem for serialization so instead of passing "data" down we only pass down idents that child components then query for data. Maybe there should be a "shared" schema of sorts.
58 |
59 | - Limiting the "event" system. The mutable JS `event` object can never cross over to the worker but the main thread should still be able to do everything it needs. This works already but is a bit ugly.
60 |
61 |
--------------------------------------------------------------------------------
/examples/app/.gitignore:
--------------------------------------------------------------------------------
1 | public/js
2 | public/bootstrap
3 |
--------------------------------------------------------------------------------
/examples/app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | shadow-grove
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/bench-fragment/.gitignore:
--------------------------------------------------------------------------------
1 | /js
2 |
--------------------------------------------------------------------------------
/examples/bench-fragment/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/conduit/.gitignore:
--------------------------------------------------------------------------------
1 | /js
2 |
--------------------------------------------------------------------------------
/examples/conduit/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 | Conduit
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/examples/conduit/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Conduit",
3 | "name": "Conduit with ClojureScript and re-frame",
4 | "start_url": "/index.html",
5 | "icons": [
6 | {
7 | "src": "favicon.ico",
8 | "sizes": "64x64 32x32 24x24 16x16",
9 | "type": "image/x-icon"
10 | },
11 | {
12 | "src": "/img/icon@1.png",
13 | "sizes": "128x128",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "/img/icon@1.5x.png",
18 | "sizes": "192x192",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "/img/icon@2x.png",
23 | "sizes": "256x256",
24 | "type": "image/png"
25 | },
26 | {
27 | "src": "/img/icon@3x.png",
28 | "sizes": "384x384",
29 | "type": "image/png"
30 | },
31 | {
32 | "src": "/img/icon@4x.png",
33 | "sizes": "512x512",
34 | "type": "image/png"
35 | }
36 | ],
37 | "display": "standalone",
38 | "theme_color": "#000000",
39 | "background_color": "#ffffff"
40 | }
41 |
--------------------------------------------------------------------------------
/examples/fulcro/.gitignore:
--------------------------------------------------------------------------------
1 | js/
2 |
--------------------------------------------------------------------------------
/examples/fulcro/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | fulcro test
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/reusable-components/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | public/js
3 |
4 | /target
5 | /checkouts
6 | /src/gen
7 |
8 | pom.xml
9 | pom.xml.asc
10 | *.iml
11 | *.jar
12 | *.log
13 | .shadow-cljs
14 | .idea
15 | .lein-*
16 | .nrepl-*
17 | .DS_Store
18 |
19 | .hgignore
20 | .hg/
21 |
--------------------------------------------------------------------------------
/examples/reusable-components/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "instruction-counter",
3 | "version": "0.0.1",
4 | "private": true,
5 | "devDependencies": {
6 | "shadow-cljs": "2.11.18"
7 | },
8 | "dependencies": {
9 | "react": "17.0.1",
10 | "react-dom": "17.0.1"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/reusable-components/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |