that's how i think of "callback hell", the issue isn't the nesting per se (that's a symptom) but the explicit time dependencies and implicit value dependencies, which is why i don't think promises add much value (no pun intended) - .then()
is obviously a very explicit statement about time
cells are implicit on time and explicit on value dependencies/relationships/transformations
which is very similar to the arguments against imperative programming, where you "step through" time with values being incidental at every step, rather than specifying an outcome or data shapes/relationships with time being implicit
and OO is just imperative programming re-organised, so the native browser DOM model always leads us back down the road to callback hell
if you want to really stretch the idea you could start invoking metaphors from quantum theory where you can never measure certain properties together perfectly (e.g. velocity and position) - i haven't seen an example of explicitly defining value and time relationships together, in a way that is guaranteed to be consistent at least
ok so I have a working “simple” state machine for async data flow
@flyboarder uploaded a file: https://clojurians.slack.com/files/U0ALQHJRF/F918CEA9Z/-.clj
^ setup a useful cell
@flyboarder uploaded a file: https://clojurians.slack.com/files/U0ALQHJRF/F90QKHS8Z/-.clj
^ implicit state scope and -tpl
wah, that second snippet is a bit more complex than the first 😛
its actually so simple! create multiple implicit variables and 4 basic formulas 😉
kk i'll read it
☕
so is the idea here to make something like castra but more generalised?
idea is that you can go like this…
@flyboarder uploaded a file: https://clojurians.slack.com/files/U0ALQHJRF/F91BBP57C/-.clj
yup
yeah i meant how when you use castra there's the loading/error/response cells
but that logic is tied into castra
yes very similar
rather than a standalone standard that you could plug into anything that needs to do async fetching/work
mmm, i see how i could find something like this useful
i'm still in the multiple cell paradigm
so i have something like this for sente
(defn send!
[{:keys [event data success error spinny can-drop? processing? result]}]
{:pre [(keyword? event) (or (keyword? spinny) (nil? spinny))]}
(let [send-vec (if data [event data] [event])
cb? (or result success processing?)
spinny (or spinny :background-task)
processing? (or processing? (j/cell true))
result (or result (j/cell nil))
success (or
success
#(reset! result %))
error (or
error
(fn [r]
(when (and @connectivity.internet/connected?= @connectivity.sente/connected?=)
(wheel.system-message.state/error!
(wheel.system-message.state/messages)
"Something is wrong. Try refreshing the page. If this error persists please contact us."))
(throw (js/Error. r))))]
(if @connectivity.sente/connected?=
(if cb?
; We only want a spinny wheel if there's a success callback waiting on a
; round trip.
(let [s (spinny.state/+! spinny.state/state spinny)
cb (fn [r]
(j/dosync
(if (taoensso.sente/cb-success? r)
(success r)
(error r))
(reset! processing? false)
(spinny.state/timeout! spinny.state/state s)))]
(reset! processing? true)
(@sente.state/chsk-send! send-vec sente.data/timeout cb))
(@sente.state/chsk-send! send-vec))
(let [m (str "Attempted to send data without an open websocket: " send-vec)]
; Websockets don't work in CI so avoid spamming logs or erroring out.
(when-not env.data/testing?
(if can-drop?
(taoensso.timbre/debug m)
(throw (js/Error. m))))))))
Im all for multiple cells, the above just creates am implicit state scope mostly for custom elements to implement
processing?
is like *loading*
then there's result
which i think is like your *data*
i don't have an explicit *empty*
state though, i'd be checking that ad-hoc in context based on what's in result
very cool! I’m using *status*
for things like validation/completeness
a standardised way to represent this stuff outside castra would pave the way for me to spin out a sente lib
the main thing stopping me is that i'm making my own conventions up as i go 😛
yeah I think the thing to note here is the common occurrence of contextual concepts like success
error
result
yes, also note that my implementation is a combination of cells and callbacks
the default callbacks simply insert data into a cell, but sometimes you need to do a little more co-ordination/preprocessing than that
e.g.
yeah the whole (or cell (fn []))
thing is interesting
(defn +!
[conn]
{:pre [(d/conn? conn)]}
(sente.wire/send!
{:event ::+!
:spinny :blocking
:data {:id (wheel.test.util/fake :project/id)}
:success (fn [r]
(j/dosync
(let [project-datom (datascript.rethinkdb/ratom->datom (:ratom r))]
(route.state/navigate! :project-scope {:project-id (:v project-datom)})
(swap! conn #(d/db-with % [project-datom]))
(metrics.activation/new-project!))))}))
once you create a new project, you get navigated to the project
and also it gets tracked in metrics
very cool!
but then a barebones setup looks like
(defn fetch-projects-auth-meta!
[result]
{:pre [(j/cell? result)]}
(sente.wire/send!
{:event :project/projects-auth-meta
:result result}))
internally sente.wire/send!
builds the missing callbacks
but the only way to know what callbacks to build is with a well defined set of states like what you're working on...
mine have just grown organically based on needs of my UI, i can't really say i "designed" them in a formal/general sense 😛
@flyboarder one nitpick is the not
in loading doesn't account for processes that aggregate several async processes that all need to complete for a single "load"
I like your callbacks tho, with my approach I am leaving it up to the data
cell to be a lense which can persist state back to whatever
@thedavidmeister right thats why loading!
is an optional state method, nothing prevents (reset! *loading* :step/1)
it’s still a true
value which delegates to the :loading
option in the -tpl
ok cool
so it's just intended as fallback behaviour, to do a basic toggle
yep which is all most dom elements need to be able to do
yeah, and you can pass that around
i've got standard buttons that disable themselves and pick up a spinny wheel based on processing?
cells
yep exactly what im going for :thumbsup:
i think *loading*
makes sense within a custom element, same with *error*
but then i have other things that are a countdown instead of a boolean, and can be canceled
dunno if that fits
for sure! I would probably implement that over the *status*
state
I also want to figure out the concept of expected
state
for search results for example
*loading*
very cool!
*status*
i guess?
keyed-for-tpl
is pretty important for these FYI
very easy for these async processes to lose track of what they're supposed to be tracking without a key
easy to delete the wrong thing, etc.
@flyboarder what would expected
look like?
is it something that could leverage spec?
the idea was for things like a search where you expect a result but this is different from an empty state
would you ever have both expected
and empty
?
i don't think i totally understand, can you get a screenshot of something that does it?
I happen to be playing with my cells-based XHR handler and it has occurred to me that dataflow (DF) is so effective against callback hell because DF is indifferent to time. An XHR response arriving who-knows-when is no different than a user deciding to click their mouse or press a key: DF engines Just Propagate(tm) values.
@hiskennyness "shut up and propagate"
yeah totally, it's all the same from the DF perspective
we don't need a dozen ways to observe a dozen different types of events...
but we do have a dozen ways to handle a dozen types of data 😛
As long as the CBH developer can specify the value dependency (“I need the value from XHR-1 to do XHR-2”) declaratively, the composite XHR group will converge on the desired result (driven by haphazardly returning responses).
yeah, i mean ^^ those "resend invite" buttons are themselves dependent on websocket states before they even render, and then they actually wait for the new invite data to come in from a different websocket before they are "finished"
they push to one server and receive a response from a different one, but it's no biggie
“but we do have a dozen ways to handle a dozen types of data” Sure, but that code (each formula we write) is our application and easy to write in isolation. The killer is the propagation of change, and DF solves that.
well i prefer it that way
i need some tools 😉
we're not quite at the stage where AI can write everything for me
I like to think of myself as a bionic programmer.
hah
but yeah, this whole page i'm working on that the moment is really handy to have cells for 🙂
it's not just "send a request and wait for response"
it's "send an invite and spin until that email address appears in one of multiple different possible places, based on broadcasts"
because the user could exist in the system already and be added straight away, or just be given an "invite" to redeem when they finally do sign up
"the composite XHR group will converge on the desired result" - this composite XHR group would be a PITA otherwise
I am coding up a solution to an XHR use case I found while exploring ReactiveX and it is funny how quickly the wheels came off when something got into me and I tried being just a little reactive. Time reared its head. Back to “all in”….
you can mix and match approaches, but make sure to quarantine it in a scope somewhere
@thedavidmeister so *expected*
could be true
or an integer then *empty*
would be more like *missing*
, or better (defc= *missing* (and *empty* *expected*))
something is not missing if you aren’t expecting a result, or if an empty result is still a valid result