OvermindDL1 / bucklescript-tea

TEA for Bucklescript

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[FEATURE] Add a Cmd to process Promises

OvermindDL1 opened this issue · comments

Add a Cmd to process Javascript native Promises. They are basically just a Task so it is pretty trivial to add in a Cmd handler to it.

Maybe starting with something kind of like:

let run promise =
  let open Vdom in
  Tea_cmd.call
    (fun callbacks ->
      Js.Promise.then_ promise (function ->
        None -> ()
        Some msg -> !callbacks.enqueue msg
      )
      |> Js.Promise.catch (function ->
        None -> ()
        Some msg -> !callbacks.enqueue msg
      )
    )

Maybe even a method to convert them to a Task, then all the normal machinery would work...

@OvermindDL1 thanks again for your kind help!
So, currently I'm trying to understand Tea_cmdand Tea_task modules, I used your code above, here for reference (this compiles and works for me).

module CmdPromise = struct

  let run promise =
    Tea_cmd.call
      (fun callbacks ->
         let _ =
           promise 
           |> Js.Promise.then_ (function
               | None -> 
                 Js.Promise.resolve ()
               | Some msg -> 
                 let () = !callbacks.enqueue msg in 
                 Js.Promise.resolve ()
             ) 
         in
         ()
      )
end

I'm calling it like this in my update method:

let update model = function
  | RegisterServiceWorker -> 
    let promise = 
      let p = register model.sw "/serviceworker.js" in 
      let open Js.Promise in 
      p
      |> then_ (fun reg -> resolve (Some (ServiceWorkerIsRegistered reg)))
      |> catch (fun err -> resolve (Some (ServiceWorkerError err)))
    in 
    (model, CmdPromise.run promise)

My next "steps" to figure this out are

  • write a wrapper / run method that returns a Result.t
  • write a wrapper that converts it into a Tea_task

A result wouldn't work as you have to return things async (and returning a result.t is sync, not async), so the proper way would be to enqueue a message, as your first chunk of code is doing, so that's good. :-)

A task is useful for more composing, though it's not really required to do as you can compose in a promise anyway, however if integrating with other tasks it would be useful to have a lifting function, that can be done later though as it is entirely fluff. :-)

So this is what my Cmd_promise.ml currently looks like:

open Js.Promise

let run promise =
  Tea_cmd.call
    (fun callbacks ->
       let _ =
         promise 
         |> then_ (function
             | None -> 
               resolve ()
             | Some msg -> 
               let () = !callbacks.enqueue msg in 
               resolve ()
           ) 
       in
       ()
    )

let msg resolve_msg rejected_msg promise =
  promise
  |> then_ (fun r -> resolve (Some (resolve_msg r)))
  |> catch (fun e -> resolve (Some (rejected_msg e)))
  |> run

The msg function gets two msg constructors, each one for the resolved / rejected msg. Thats why the msg type needs to derive accessors:

type msg = 
  | RunPromise
  | PromiseResolved of what_the_promise_returns
  | PromiseRejected of Js.Promise.error
[@@bs.deriving {accessors}]

And in the update:

let update model = function
  | RunPromise ->
    let promise = some_external in
    let cmd = Cmd_promise.msg promiseResolved promiseRejected promise in
    model,cmd
  | PromiseResolved res ->
    let some = work_with res in
    ...
  | PromiseRejected err ->
    (* Js.Promise.error is pretty vague, cast it into a string *)
    let str = {j| err |j} in
    ... (* log error somewhere *)

That looks quite good yeah!

Might be worth PR'ing it in now if you want? :-)

Might be worth PR'ing it in now if you want? :-)

Sure go ahead, I'm not yure where to but it, tbh, new module (e.g. Cmd_promise) or append it to Tea_cmd...

But I feel, it's still missing something, might be a Sub registration or something like that as mentioned in your comment on #80

though perhaps combining the Cmd and promise handling into a single call would be easier for the user

Will look into that.

A subscription is better for things that can push multiple events and can be 'stopped', but if you don't need to worry about being able to stop it then Cmd's, even with multiple events, is probably fine.

Hmm...

How about this:

(* tea_promise.ml *)

let cmd promise tagger =
  let open Vdom in
  Tea_cmd.call (function callbacks -> 
      let _ = promise
              |> Js.Promise.then_ (function res ->
                match tagger res with
                | Some msg -> 
                  let () = !callbacks.enqueue msg in
                  Js.Promise.resolve ()
                | None -> Js.Promise.resolve ()
                )
      in
      ()
    )


let result promise msg =
  let open Vdom in
  Tea_cmd.call (function callbacks ->
      let enq result =
        !callbacks.enqueue (msg result)
      in
      let _ = promise
              |> Js.Promise.then_ (function res ->
                  let resolve = enq (Tea_result.Ok res) in
                  Js.Promise.resolve resolve
                )
              |> Js.Promise.catch (function err ->
                  let err_to_string err =
                    {j|$err|j} in
                  let reject = enq (Tea_result.Error (err_to_string err)) in
                  Js.Promise.resolve reject
                )
      in
      ()
    )

I wrote a simple TEA app to test:

open Tea.App
open Tea.Html

type msg =
  | Resolve
  | Reject
  | Result of (string, string) Tea.Result.t
[@@bs.deriving {accessors}]

type model = {
  result : string;
}

let init () = {
  result = "";
}, Tea.Cmd.none

let subscriptions _ = Tea.Sub.none

let makePromise fn =
  let open Js.Promise in
  resolve "Hello from Promise."
  |> then_(fn)

let makeResolve () =
  makePromise (fun (r : string) ->
      Js.Promise.resolve (r ^ " |> Chained")
    )

let makeReject () =
  makePromise (fun _ ->
      Js.Promise.reject(Js.Exn.raiseError "Sorry, but this was rejected.")
    )

let update model = function 
  | Resolve -> 
    {model with result = ""}, Tea_promise.result (makeResolve ()) result
  | Reject -> 
    {model with result = ""}, Tea_promise.result (makeReject ()) result

  | Result (Tea.Result.Ok res) -> 
    {model with result = res}, Tea.Cmd.none
  | Result (Tea.Result.Error err) -> 
    {model with result = err}, Tea.Cmd.none


let view_button title msg =
  button
    [ onClick msg ]
    [ text title ]

let view model =
  div
    []
    [ view_button "Resolve" Resolve
    ; br []
    ; view_button "Reject" Reject
    ; br []
    ; span [] [ text @@ "Result: " ^ model.result ]
    ]

let main =
  standardProgram {
    init;
    update;
    view;
    subscriptions;
  }

Hmm, that does look good from an API and useability standpoint. :-)

Just confirming, javascript promises don't have any way to be canceled once they are started, correct? If so then that should be good to take verbatim. :-)

Just confirming, javascript promises don't have any way to be canceled once they are started, correct?

Short answer: As far as I understand the standard, you are correct a promise can not be canceled.
Long answer: There was once a / serveral proposals for cancellable promises, but it did not land in any standard afaik: see this discussion here

Cool, so that makes it easy then, Cmd's are pefect for them! Do you want to PR it (add yourself to the contributor list too) or shall I grab it up later? :-)

Awesome, will make a PR

Thanks :-) With #90 merged, you can probably close this

Ah that wasn't marked in the commit log! Heh, closing. ^.^