Fork me on GitHub
#hoplon
<
2018-01-31
>
thedavidmeister04:01:08

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

thedavidmeister04:01:15

cells are implicit on time and explicit on value dependencies/relationships/transformations

thedavidmeister04:01:00

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

thedavidmeister04:01:07

and OO is just imperative programming re-organised, so the native browser DOM model always leads us back down the road to callback hell

thedavidmeister04:01:49

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

flyboarder05:01:26

ok so I have a working “simple” state machine for async data flow

flyboarder05:01:06

^ setup a useful cell

flyboarder05:01:36

^ implicit state scope and -tpl

thedavidmeister05:01:43

wah, that second snippet is a bit more complex than the first 😛

flyboarder05:01:31

its actually so simple! create multiple implicit variables and 4 basic formulas 😉

thedavidmeister05:01:51

so is the idea here to make something like castra but more generalised?

flyboarder05:01:21

idea is that you can go like this…

thedavidmeister05:01:40

yeah i meant how when you use castra there's the loading/error/response cells

thedavidmeister05:01:52

but that logic is tied into castra

flyboarder05:01:11

yes very similar

thedavidmeister05:01:13

rather than a standalone standard that you could plug into anything that needs to do async fetching/work

thedavidmeister05:01:35

mmm, i see how i could find something like this useful

thedavidmeister05:01:40

i'm still in the multiple cell paradigm

thedavidmeister05:01:13

so i have something like this for sente

thedavidmeister05:01:16

(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))))))))

flyboarder05:01:46

Im all for multiple cells, the above just creates am implicit state scope mostly for custom elements to implement

thedavidmeister05:01:04

processing? is like *loading*

thedavidmeister05:01:42

then there's result which i think is like your *data*

thedavidmeister05:01:36

i don't have an explicit *empty* state though, i'd be checking that ad-hoc in context based on what's in result

flyboarder05:01:39

very cool! I’m using *status* for things like validation/completeness

thedavidmeister05:01:50

a standardised way to represent this stuff outside castra would pave the way for me to spin out a sente lib

thedavidmeister05:01:19

the main thing stopping me is that i'm making my own conventions up as i go 😛

flyboarder05:01:35

yeah I think the thing to note here is the common occurrence of contextual concepts like success error result

thedavidmeister05:01:05

yes, also note that my implementation is a combination of cells and callbacks

thedavidmeister05:01:38

the default callbacks simply insert data into a cell, but sometimes you need to do a little more co-ordination/preprocessing than that

flyboarder05:01:07

yeah the whole (or cell (fn [])) thing is interesting

thedavidmeister05:01:09

(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!))))}))

thedavidmeister05:01:42

once you create a new project, you get navigated to the project

thedavidmeister05:01:59

and also it gets tracked in metrics

thedavidmeister05:01:20

but then a barebones setup looks like

thedavidmeister05:01:23

(defn fetch-projects-auth-meta!
 [result]
 {:pre [(j/cell? result)]}
 (sente.wire/send!
  {:event :project/projects-auth-meta
   :result result}))

thedavidmeister05:01:41

internally sente.wire/send! builds the missing callbacks

thedavidmeister05:01:51

but the only way to know what callbacks to build is with a well defined set of states like what you're working on...

thedavidmeister05:01:55

mine have just grown organically based on needs of my UI, i can't really say i "designed" them in a formal/general sense 😛

thedavidmeister05:01:31

@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"

flyboarder05:01:33

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

flyboarder05:01:40

@thedavidmeister right thats why loading! is an optional state method, nothing prevents (reset! *loading* :step/1)

flyboarder05:01:18

it’s still a true value which delegates to the :loading option in the -tpl

thedavidmeister05:01:00

so it's just intended as fallback behaviour, to do a basic toggle

flyboarder05:01:35

yep which is all most dom elements need to be able to do

thedavidmeister05:01:57

yeah, and you can pass that around

thedavidmeister05:01:45

i've got standard buttons that disable themselves and pick up a spinny wheel based on processing? cells

flyboarder05:01:39

yep exactly what im going for :thumbsup:

flyboarder05:01:10

i think *loading* makes sense within a custom element, same with *error*

thedavidmeister05:01:33

but then i have other things that are a countdown instead of a boolean, and can be canceled

thedavidmeister05:01:37

dunno if that fits

flyboarder05:01:19

for sure! I would probably implement that over the *status* state

flyboarder05:01:50

I also want to figure out the concept of expected state

flyboarder05:01:39

for search results for example

thedavidmeister05:01:47

*status* i guess?

thedavidmeister06:01:24

keyed-for-tpl is pretty important for these FYI

thedavidmeister06:01:54

very easy for these async processes to lose track of what they're supposed to be tracking without a key

thedavidmeister06:01:14

easy to delete the wrong thing, etc.

thedavidmeister06:01:41

@flyboarder what would expected look like?

thedavidmeister06:01:30

is it something that could leverage spec?

flyboarder07:01:44

the idea was for things like a search where you expect a result but this is different from an empty state

thedavidmeister08:01:59

would you ever have both expected and empty?

thedavidmeister08:01:42

i don't think i totally understand, can you get a screenshot of something that does it?

kennytilton09:01:29

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.

thedavidmeister09:01:58

@hiskennyness "shut up and propagate"

thedavidmeister09:01:50

yeah totally, it's all the same from the DF perspective

thedavidmeister09:01:24

we don't need a dozen ways to observe a dozen different types of events...

thedavidmeister09:01:51

but we do have a dozen ways to handle a dozen types of data 😛

kennytilton09:01:25

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).

thedavidmeister10:01:21

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"

thedavidmeister10:01:03

they push to one server and receive a response from a different one, but it's no biggie

kennytilton10:01:48

“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.

thedavidmeister10:01:03

well i prefer it that way

thedavidmeister10:01:09

i need some tools 😉

thedavidmeister10:01:31

we're not quite at the stage where AI can write everything for me

kennytilton10:01:08

I like to think of myself as a bionic programmer.

thedavidmeister10:01:34

but yeah, this whole page i'm working on that the moment is really handy to have cells for 🙂

thedavidmeister10:01:48

it's not just "send a request and wait for response"

thedavidmeister10:01:58

it's "send an invite and spin until that email address appears in one of multiple different possible places, based on broadcasts"

thedavidmeister10:01:31

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

thedavidmeister10:01:11

"the composite XHR group will converge on the desired result" - this composite XHR group would be a PITA otherwise

kennytilton10:01:30

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”….

thedavidmeister10:01:36

you can mix and match approaches, but make sure to quarantine it in a scope somewhere

flyboarder19:01:17

@thedavidmeister so *expected* could be true or an integer then *empty* would be more like *missing*, or better (defc= *missing* (and *empty* *expected*))

flyboarder19:01:44

something is not missing if you aren’t expecting a result, or if an empty result is still a valid result