Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Firebase transaction #36

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ checkouts/
pom.xml

.firebaserc

# MacOS system file
.DS_Store
60 changes: 56 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,15 +261,18 @@ See [Firebase docs][phone-auth] for details.

The firebase database is a tree. You can write values to nodes in a tree, or push them
to auto-generated unique sub-nodes of a node. In re-frame-firebase, these are exposed
through the `:firebase/write` `:firebase/push` and `:firebase/update` effect handlers.
through the `:firebase/write` and `:firebase/push` effect handlers.

Each takes parameters:
- `:path` - A vector representing a node in the firebase tree, e.g. `[:my :node]`
- `:value` - The value to write or push.
- `:on-success` - Event vector or function to call when write succeeds.
- `:on-failure` - Event vector or function to call with the error.

Example:
There are also the atomic `:firebase/update`, `:firebase/transaction`, and
`:firebase/swap` effect handlers, discussed below.

Write example:

```clojure
(re-frame/reg-event-fx
Expand Down Expand Up @@ -318,8 +321,57 @@ Example (diff in bold):

[multi-location-update-blogpost]: https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html

Re-frame-firebase also supplies `:firebase/multi` to allow multiple write and/or
pushes from a single event:
The `:firebase/transaction` effect handler and its more Clojure-y variant
`:firebase/swap` perform atomic modifications to the tree.

In `:firebase/transaction`, the `:transaction-update` parameter is a function that takes
one parameter that is the old value at the `:path` location and returns the new
value. Note that the function may be called multiple times, so should be free of side
effects.

The function must also tolerate a `nil` input gracefully. To abort a transaction, say to
avoid overwriting an existing value, the function returns `js/undefined`.

Finally, note that the `:apply-locally` boolean indicates whether the local
firebase-system cached value should be applied optimistically, which may result in more
than one update event to be emitted if the function needs to be run more than once. The
default value is `true`.

```clojure
{:firebase/transaction {:path [:my :data]
:transaction-update (fn [old-val] (if old-val (inc old-val)))
:apply-locally false ;; default is true = multiple update events may be received if transaction-update needs to be run more than once.
;; The on-* handlers can also take a re-frame event
:on-success (fn [snapshot committed] (if committed (prn "Transaction committed: " snapshot)))
:on-failure (fn [err snapshot committed] (prn "Error: " err))}}
```

`:firebase/swap` is similar to `:firebase/transaction` but takes an `:argv` argument,
typically a vector. The argument `:f` is the renamed `:transaction-update`, the
update function. The old value at the `:path` is prepended to `:argv` and then `:f`
is applied much like `clojure.core\swap!` does for atoms.

Both atomic effect handlers are provided to appeal to users coming from Firebase-first
or Clojure-first backgrounds, respectively. For those coming from Firebase, note that
the `snapshot` and `committed` parameters are reversed in on-*. This is to facilitate
re-frame event handlers as they receive only the first passed parameter, ignoring the
rest. Passing snapshot rather than committed makes for more useful possibilities.

Example (diff in bold):

<pre>
{:firebase/<b>swap</b> {:path [:my :data]
<b>:f + </b>
<b>:argv [2 3] ;; So the swap will perform (+ old-value 2 3) </b>
:apply-locally false ;; default is true = multiple update events may be received if transaction-update needs to be run more than once.
;; The on-* handlers can also take a re-frame event
:on-success (fn [snapshot committed] (if committed (prn "Transaction committed: " snapshot)))
<b>:on-failure [:handle-failure]</b>}}
</pre>


Re-frame-firebase also supplies `:firebase/multi` to allow multiple writes and other
effects from a single event:

```clojure
(re-frame/reg-event-fx
Expand Down
24 changes: 24 additions & 0 deletions src/com/degel/re_frame_firebase.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@
;;;
(re-frame/reg-fx :firebase/update database/update-effect)

;;; Transactionally reads and writes a value to Firebase. NB: :transaction-update function
;;; may run more than once so must be free of side effects. Importantly, it must be able
;;; to handle null data. To abort a transaction, return js/undefined.
;;; See https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction
;;;
;;; Examples FX:
;;; {:firebase/transaction {:path [:my :data]
;;; :transaction-update (fn [old-val] (if old-val (inc old-val)))
;;; :apply-locally false ;; default is true = multiple update events may be received if transaction-update needs to be run more than once.
;;; ;; The on-* handlers can also take a re-frame event
;;; :on-success (fn [snapshot committed] (if committed (prn "Transaction committed: " snapshot)))
;;; :on-failure (fn [err snapshot committed] (prn "Error: " err))}}
;;;
;;; {:firebase/swap {:path [:my :data]
;;; :f +
;;; :argv [2 3]
;;; :apply-locally false ;; default is true = multiple update events may be received if transaction-update needs to be run more than once.
;;; ;; The on-* handlers can also take a re-frame event
;;; :on-success (fn [snapshot committed] (if committed (prn "Transaction committed: " snapshot)))
;;; :on-failure [:firebase-error]}}
(re-frame/reg-fx :firebase/transaction database/transaction-effect)
(re-frame/reg-fx :firebase/swap database/swap-effect) ; A synonym with :argv for update function :f

;;; Write a value to a Firebase list.
;;; See https://firebase.google.com/docs/reference/js/firebase.database.Reference#push
Expand Down Expand Up @@ -74,6 +96,8 @@
:firebase/write (database/write-effect args)
:firebase/update (database/update-effect args)
:firebase/push (database/push-effect args)
:firebase/transaction (database/transaction-effect args)
:firebase/swap (database/swap-effect args)
:firebase/read-once (database/once-effect args)
:firestore/delete (firestore/delete-effect args)
:firestore/set (firestore/set-effect args)
Expand Down
38 changes: 38 additions & 0 deletions src/com/degel/re_frame_firebase/database.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,44 @@

(def ^:private update-effect updater)

(defn- transaction->js
[retval]
;; Preserve js/undefined as it signals to abort the transaction.
;; https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction
(if (= js/undefined retval)
retval
(clj->js retval)))

(defn- transaction-update-wrapper [transaction-update]
(fn [old-value]
(-> old-value
js->clj
clojure.walk/keywordize-keys
transaction-update
transaction->js)))

(defn- transactioner [{:keys [path transaction-update on-success on-failure apply-locally]}]
(.transaction (fb-ref path)
(transaction-update-wrapper transaction-update)
(success-failure-wrapper on-success on-failure)
;; Force apply-locally to be a boolean, as required by .transaction.
(if (or (false? apply-locally)
(nil? apply-locally))
false
true)))

(def transaction-effect transactioner)

(defn- swapper [{:keys [path f argv on-success on-failure apply-locally]}]
(transactioner
{:path path
:transaction-update (fn [old-val] (apply f old-val argv))
:on-success on-success
:on-failure on-failure
:apply-locally apply-locally}))

(def swap-effect swapper)

(defn push-effect [{:keys [path value on-success on-failure] :as all}]
(let [key (.-key (.push (fb-ref path)))]
(setter (assoc all
Expand Down
48 changes: 42 additions & 6 deletions src/com/degel/re_frame_firebase/helpers.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,50 @@
(.catch promise (re-utils/event->fn on-failure))
(.catch promise (core/default-error-handler))))


(defn success-failure-wrapper [on-success on-failure]
{:pre [(utils/validate (s/nilable :re-frame/vec-or-fn) on-success)
(utils/validate (s/nilable :re-frame/vec-or-fn) on-failure)]
:post (fn? %)}
(let [on-success (and on-success (re-utils/event->fn on-success))
on-failure (and on-failure (re-utils/event->fn on-failure))]
(fn [err]
(cond (nil? err) (when on-success (on-success))
on-failure (on-failure err)
:else ((core/default-error-handler) err)))))
on-failure (and on-failure (re-utils/event->fn on-failure))
wrapped-handler (fn
([err] (cond (nil? err) (when on-success (on-success))
on-failure (on-failure err)
:else ((core/default-error-handler) err)))

;; I am unable to find in the Google Firebase documentation* a 2-arity
;; callback for .set .update or .transaction that uses this wrapper. Yet, I've
;; observed that such a callback exists specifically on .update. With
;; trepidation arising from minimal ad hoc testing, I am forwarding the second
;; parameter, assuming that this behavior was undetected and inconsequential before
;; I wrote wrapped-handler to be multi-arity.
;;
;; [TODO] Find the reason for this 2-arity version and properly dispatch it.
;;
;; * https://firebase.google.com/docs/reference/js/firebase.database.Reference
([err other]
(cond (nil? err) (when on-success (on-success other))
on-failure (on-failure err other)
:else ((core/default-error-handler) err)))

;; onComplete invoked in :firebase/transaction and :firebase/swap accepts an
;; error code, a boolean indicating committed status, and a snapshot of the
;; data at that path.
;;
;; This is useful for exposing state changes upon completion of the
;; transaction, as the transaction-update or f functions must be side-effect
;; free. Notably here, we reverse the order of committed and snapshot in the
;; cljs versions on-success and on-failure. So, if the on-success handler is
;; a re-frame event vector (in iron.re-utils/re-utils they only take the first
;; parameter), it gets the snapshotted data. An on-failure event handler
;; would get the error code; it has snapshot and committed reversed for
;; continuity.
([err committed snapshot]
(cond (nil? err) (when on-success (on-success (js->clj-tree snapshot) committed))
on-failure (on-failure err (js->clj-tree snapshot) committed)
:else ((core/default-error-handler) err))))]
wrapped-handler))