Skip to content

Instantly share code, notes, and snippets.

@joshcho
Last active April 2, 2024 08:56
Show Gist options
  • Save joshcho/9549e8b35acaec2815ae6436adb02a79 to your computer and use it in GitHub Desktop.
Save joshcho/9549e8b35acaec2815ae6436adb02a79 to your computer and use it in GitHub Desktop.
Implement optimistic update for atoms in Electric
;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; In jurisdictions that recognize copyright laws, the author or authors
;; of this software dedicate any and all copyright interest in the
;; software to the public domain. We make this dedication for the benefit
;; of the public at large and to the detriment of our heirs and
;; successors. We intend this dedication to be an overt act of
;; relinquishment in perpetuity of all present and future rights to this
;; software under copyright law.
;;
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
;; IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
;; OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
;; ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
;; OTHER DEALINGS IN THE SOFTWARE.
;;
;; For more information, please refer to <https://unlicense.org>
;; First we introduce a few transducers. Then take a look at `relay-atom`.
(defn ignore-first
"Transducer that ignores the first value in a sequence."
[]
(fn [rf]
(let [first? (atom true)]
(fn
([] (rf))
([result] (rf result))
([result input]
(if @first?
(do (reset! first? false) result)
(rf result input)))))))
#?(:clj
(do
(defmacro relay-atom
"Returns a relay atom that 1) propagates changes from user via
`server-effect` and 2) receives changes from the `server-query`.
Note that `server-effect` is a Clojure function, not Electric.
Specify `skip-effect-list` to prevent effects when they change."
[server-query server-effect]
`(let [!relay# (atom (e/server
(e/snapshot ~server-query)))
!send-count# (atom 0)
!stop-propagation# (atom false)]
(e/for-event
[v# (->> (e/fn [] (e/watch !relay#))
(m/eduction (remove e/failure?)
(ignore-first)))]
( ;; js/console.log
identity
(new
(e/fn [!send-count# !stop-propagation#]
(if @!stop-propagation#
(reset! !stop-propagation# false)
(do
;; (js/console.log "sending" v#)
(swap! !send-count# inc)
(e/server (new ~server-effect v#)))))
!send-count# !stop-propagation#)))
(e/for-event
[v# (->> (e/fn []
(e/server ~server-query))
(m/eduction (remove e/failure?)))]
(new
(e/fn [!send-count#]
(if (<= @!send-count# 0)
(do
;; (js/console.log "received change" v#)
(reset! !stop-propagation# true)
(reset! !relay# v#))
(do
;; (js/console.log "received what was sent" v#)
(swap! !send-count# dec))))
!send-count#))
!relay#))
(defmacro e-relay-atom [e-id attr]
`(relay-atom
(~attr
(d/entity ~'db ~e-id))
(e/fn [v#]
(tx/transact! ~'!conn
[{:db/id (e/snapshot ~e-id)
~attr v#}]))))
(defmacro selected-task-relay-atom [attr]
`(e-relay-atom ~'selected-task-id ~attr))))
(comment
;; usage, do this in electric scope
(let [!value (relay-atom
(:some/value (d/entity db 42)) ; server query
(fn [v] ; server effect
(d/transact! !conn [{:db/id 42
:some/value v}])))
value (e/watch !value) ; if query is changed in another session, !value is also updated
]
(ui/button
(e/fn []
(if (= value "A")
(reset! !value "B") ; triggers server change
(reset! !value "A") ; triggers server change
))
(dom/text value))))
(comment
(let [!value (e-relay-atom 42 :some/value)
value (e/watch !value)]
;; do things
))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment