hoplon

The :hoplon: ClojureScript Web Framework - http://hoplon.io/
2018-01-31T04:32:08.000145Z

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

2018-01-31T04:32:15.000032Z

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

2018-01-31T04:34:00.000011Z

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

2018-01-31T04:35:07.000220Z

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

2018-01-31T04:36:49.000080Z

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

flyboarder 2018-01-31T05:21:26.000065Z

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

flyboarder 2018-01-31T05:21:51.000105Z

@flyboarder uploaded a file: https://clojurians.slack.com/files/U0ALQHJRF/F918CEA9Z/-.clj

flyboarder 2018-01-31T05:22:06.000249Z

^ setup a useful cell

flyboarder 2018-01-31T05:22:20.000152Z

@flyboarder uploaded a file: https://clojurians.slack.com/files/U0ALQHJRF/F90QKHS8Z/-.clj

flyboarder 2018-01-31T05:22:36.000137Z

^ implicit state scope and -tpl

2018-01-31T05:23:43.000088Z

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

flyboarder 2018-01-31T05:24:31.000064Z

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

2018-01-31T05:24:54.000081Z

kk i'll read it

2018-01-31T05:24:56.000158Z

2018-01-31T05:25:51.000191Z

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

flyboarder 2018-01-31T05:26:21.000235Z

idea is that you can go like this…

flyboarder 2018-01-31T05:28:04.000070Z

@flyboarder uploaded a file: https://clojurians.slack.com/files/U0ALQHJRF/F91BBP57C/-.clj

2018-01-31T05:28:23.000159Z

yup

2018-01-31T05:28:40.000115Z

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

2018-01-31T05:28:52.000134Z

but that logic is tied into castra

flyboarder 2018-01-31T05:29:11.000180Z

yes very similar

2018-01-31T05:29:13.000009Z

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

2018-01-31T05:29:35.000034Z

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

2018-01-31T05:29:40.000067Z

i'm still in the multiple cell paradigm

2018-01-31T05:30:13.000106Z

so i have something like this for sente

2018-01-31T05:30:16.000161Z

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

flyboarder 2018-01-31T05:30:46.000054Z

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

2018-01-31T05:31:04.000021Z

processing? is like *loading*

2018-01-31T05:31:42.000183Z

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

2018-01-31T05:32:36.000007Z

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

flyboarder 2018-01-31T05:33:39.000081Z

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

2018-01-31T05:33:50.000168Z

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

2018-01-31T05:35:19.000013Z

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

flyboarder 2018-01-31T05:35:35.000207Z

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

2018-01-31T05:36:05.000055Z

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

2018-01-31T05:36:38.000010Z

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

2018-01-31T05:37:05.000101Z

e.g.

flyboarder 2018-01-31T05:37:07.000216Z

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

2018-01-31T05:37:09.000008Z

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

2018-01-31T05:37:42.000003Z

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

2018-01-31T05:37:59.000009Z

and also it gets tracked in metrics

flyboarder 2018-01-31T05:38:39.000055Z

very cool!

2018-01-31T05:39:20.000260Z

but then a barebones setup looks like

2018-01-31T05:39:23.000142Z

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

2018-01-31T05:40:41.000025Z

internally sente.wire/send! builds the missing callbacks

2018-01-31T05:42:51.000144Z

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

2018-01-31T05:43:55.000037Z

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

2018-01-31T05:45:31.000120Z

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

flyboarder 2018-01-31T05:45:33.000047Z

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

flyboarder 2018-01-31T05:46:40.000245Z

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

flyboarder 2018-01-31T05:47:18.000074Z

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

2018-01-31T05:47:35.000028Z

ok cool

2018-01-31T05:48:00.000121Z

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

flyboarder 2018-01-31T05:48:35.000055Z

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

2018-01-31T05:48:57.000150Z

yeah, and you can pass that around

2018-01-31T05:49:45.000017Z

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

flyboarder 2018-01-31T05:50:39.000033Z

yep exactly what im going for :thumbsup:

flyboarder 2018-01-31T05:51:10.000090Z

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

2018-01-31T05:51:33.000265Z

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

2018-01-31T05:51:37.000018Z

dunno if that fits

flyboarder 2018-01-31T05:53:19.000039Z

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

flyboarder 2018-01-31T05:53:50.000089Z

I also want to figure out the concept of expected state

flyboarder 2018-01-31T05:55:39.000048Z

for search results for example

2018-01-31T05:56:32.000179Z

http://g.recordit.co/nnlFVdKf0i.gif

2018-01-31T05:56:41.000284Z

*loading*

flyboarder 2018-01-31T05:56:47.000179Z

very cool!

2018-01-31T05:58:43.000012Z

http://g.recordit.co/E6TblxE2f6.gif

2018-01-31T05:58:47.000074Z

*status* i guess?

2018-01-31T06:01:24.000139Z

keyed-for-tpl is pretty important for these FYI

2018-01-31T06:01:54.000196Z

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

2018-01-31T06:02:14.000179Z

easy to delete the wrong thing, etc.

2018-01-31T06:02:41.000147Z

@flyboarder what would expected look like?

2018-01-31T06:04:30.000023Z

is it something that could leverage spec?

flyboarder 2018-01-31T07:29:44.000080Z

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

2018-01-31T08:52:59.000468Z

would you ever have both expected and empty?

2018-01-31T08:53:42.000198Z

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

kennytilton 2018-01-31T09:55:29.000422Z

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.

2018-01-31T09:55:58.000531Z

@hiskennyness "shut up and propagate"

2018-01-31T09:56:50.000616Z

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

2018-01-31T09:57:24.000380Z

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

2018-01-31T09:57:51.000305Z

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

kennytilton 2018-01-31T09:59:25.000134Z

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

2018-01-31T10:01:21.000406Z

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"

2018-01-31T10:02:03.000194Z

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

kennytilton 2018-01-31T10:02:48.000280Z

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

2018-01-31T10:03:03.000191Z

well i prefer it that way

2018-01-31T10:03:09.000380Z

i need some tools 😉

2018-01-31T10:03:31.000363Z

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

kennytilton 2018-01-31T10:04:08.000639Z

I like to think of myself as a bionic programmer.

2018-01-31T10:04:18.000191Z

hah

2018-01-31T10:05:34.000631Z

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

2018-01-31T10:05:48.000239Z

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

2018-01-31T10:06:58.000594Z

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

2018-01-31T10:07:31.000751Z

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

2018-01-31T10:08:11.000408Z

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

kennytilton 2018-01-31T10:15:30.000397Z

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

2018-01-31T10:22:36.000216Z

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

flyboarder 2018-01-31T19:19:17.000518Z

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

flyboarder 2018-01-31T19:21:44.000676Z

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