Call for examples: show us your async/promisey code


#1

In order to come up with a good pattern for dealing with promises (e.g. async/await), it would be super useful to have a bunch of code examples of gnarly async code in reason. In my experience writing javascript, there’s maybe 10% of the time when async/await is relevant, but when I need it, it really helps a lot.
I don’t have any reason projects where I do a ton of async stuff at the moment – if you do, please share!
(doesn’t have to be a full example, could just be a gist of the interesting bits, etc.)

Thanks!


#2

Here’s some async code reading and writing from Firestore

let handler = (store, req, res, user) => {
  let data = Message.findMatchOfJs(Request.body(req));
  store
  |> Firestore.collection("matchQueue")
  |> Firestore.Collection.get()
  |> Js.Promise.then_(query => {
       let queue = Array.map(doc => doc##id, query##docs);
       if (Array.length(queue) === 0) {
         addToQueue(store, user##uid, data);
       } else if (queue[0] != user##uid) {
         let opponentUid = queue[0];
         let opponentRef =
           store
           |> Firestore.collection("matchQueue")
           |> Firestore.Collection.doc(opponentUid);
         opponentRef
         |> Firestore.DocumentRef.get()
         |> Js.Promise.then_(snapshot => {
              let opponent =
                Firestore.DocumentSnapshot.data(snapshot)
                |> Message.findMatchOfJs;
              opponentRef
              |> Firestore.DocumentRef.delete()
              |> Js.Promise.then_(() =>
                   createGame(
                     store,
                     (user##uid, data##displayName),
                     (opponentUid, opponent##displayName),
                   )
                 );
            });
       } else {
         let userRef =
           store
           |> Firestore.collection("matchQueue")
           |> Firestore.Collection.doc(user##uid);
         Js.Promise.resolve(userRef);
       };
     })
  |> Js.Promise.then_(_ref => {
       Response.status(res, 200);
       Response.send(res, Js.Json.string("OK"));
       Js.Promise.resolve();
     });
};

#3

Here is code making a POST request to a Node API using bs-axios.

        <input
          className="mr-2 font-sans ls3 bg-sm-theme w-32 h-12 cursor-pointer"
          value="SAVE"
          _type="submit"
          onClick=(
            (_) => {
              Js.Promise.(
                Axios.Instance.postData(
                  Utils.instance,
                  "/furnishing-item",
                  {
                    "company_id": self.state.company_id,
                    "description": self.state.description,
                    "link": self.state.link,
                    "sku": self.state.sku,
                    "quantity": self.state.quantity,
                    "wholesale_price": self.state.wholesale_price,
                    "shipping_amount": self.state.shipping_amount,
                    "s_h": self.state.s_h,
                    "notes": self.state.notes,
                    "furnishings_id": self.state.furnishings_id,
                    "project_id": self.state.project_id,
                    "inherited_multiplier": self.state.wholesale_multiplier
                  },
                )
                |> then_(response => {
                  let f_id = string_of_int(self.state.furnishings_id);
                  let p_id = string_of_int(self.state.project_id);
                  if(self.state.showAddress == false) {
                    resolve(Bindings.Js.replace(Utils.url ++ "/project/" ++ p_id ++ "/furnishings/" ++ f_id));
                  } else {
                    resolve(
                      Js.Promise.(
                        Axios.Instance.postData(
                          Utils.instance,
                          "/shipping-address",
                          {
                            "name": self.state.name,
                            "line1": self.state.address,
                            "line2": self.state.line2,
                            "city": self.state.city,
                            "state": self.state.state,
                            "zip": self.state.zip,
                            "project_id": self.state.project_id,
                            "item_id": response##data##currval
                          },
                        )
                        |> then_(_response => {
                          resolve(Bindings.Js.replace(Utils.url ++ "/project/" ++ p_id ++ "/furnishings/" ++ f_id));
                        })
                        |> ignore
                      )
                    );
                  }
                })
                |> ignore
              );
            }
          )
        />

#4

I’m using this functor most of the time to make Post / Get Requests via fetch, then using a callback once the data has been returned.

module type ResponseType = {
  type responseData;
  let responseDecoder: Js.Json.t => responseData;
};
module MakeResponse = (Type: ResponseType) => {
  type t =
    | Success(Type.responseData)
    | Error(string);
  let decode = (json: Js.Json.t) =>
    Json.Decode.(
      switch (json |> field("requestState", string)) {
      | "success" => Success(json |> field("data", Type.responseDecoder))
      | "error" => Error(json |> field("data", string))
      | _ => raise(Not_found)
      }
    );
};
module type RequestType = {let url: string; let method_: Fetch.requestMethod;};
module MakeRequest = (Request: RequestType, ResponseT: ResponseType) => {
  module Response = MakeResponse(ResponseT);
  let send = (~body: option('a)=?, callback: Response.t => unit) => {
    let requestInit =
      switch (Request.method_) {
      | Get =>
        Fetch.RequestInit.make(
          ~method_=Request.method_,
          ~mode=Fetch.SameOrigin,
          ~credentials=Fetch.SameOrigin,
          ~cache=Fetch.NoCache,
          (),
        )
      | Post =>
        Fetch.RequestInit.make(
          ~method_=Request.method_,
          ~headers=
            Fetch.HeadersInit.make({"Content-Type": "application/json"}),
          ~body=
            switch (body) {
            | Some(body) => Fetch.BodyInit.make(body)
            | None => Fetch.BodyInit.make("")
            },
          ~mode=Fetch.SameOrigin,
          ~credentials=Fetch.SameOrigin,
          ~cache=Fetch.NoCache,
          (),
        )
      | _ => raise(Not_found)
      };
    let _unit =
      Js.Promise.(
        Fetch.fetchWithInit(Request.url, requestInit)
        |> then_(res =>
             Fetch.Response.ok(res) ?
               Fetch.Response.json(res) :
               Js.Exn.raiseError(Fetch.Response.statusText(res))
           )
        |> then_(result => callback(Response.decode(result)) |> resolve)
        |> catch(e => {
             callback(Response.Error([%bs.raw "e.message"]));
             Js.log(e);
             resolve();
           })
      );
    ();
  };
};

#5

I think an async / await syntax would help this code greatly:


#6

I have to define parsed type for each response data. And the way of building params takes too much efforts.


#7

I concocted some small functions for myself to work with Belt.Result.t and Js.Promise.t.

Signature of functions

type promise('a) = Js.Promise.t('a);

/* Classic promise */  
let then_: ('a => promise('b), promise('a)) => promise('b);
let reject: exn => promise('a);
let resolve: 'a => promise('a);
let flatMap: (promise('a), 'a => promise('b)) => promise('b);
let map: (promise('a), 'a => 'b) => promise('b);
let tap: (promise('a), 'a => unit) => promise('a);
let rejectIfNone: (promise(option('a)), exn) => promise('a);

let flatCatch: (promise('a), exn => promise('a)) => promise('a);
let catch: (promise('a), exn => 'a) => promise('a);
let finally: (promise('a), unit => unit) => promise('a);

/* Result */
type res('a, 'e) = Belt.Result.t('a, 'e);
type resPromise('a, 'e) = promise(res('a, 'e));

let resolveOk: 'a => resPromise('a, 'e);
let flatMapOk: (resPromise('a, 'e), 'a => resPromise('b, 'e)) => resPromise('b, 'e);
let mapOk: (resPromise('a, 'e), 'a => res('b, 'e)) => resPromise('b, 'e);
let flatMapError: (resPromise('a, 'e), 'e => resPromise('a, 'ee)) => resPromise('a, 'ee);
let mapError: (resPromise('a, 'e), 'e => res('a, 'ee)) => resPromise('a, 'ee);
let errorIfNone: (resPromise(option('a), 'e), 'e) => resPromise('a, 'e);
let convertResult: promise('a) => resPromise('a, [> | `exn(exn)]);
let fromResult: resPromise('a, exn) => promise('a);

Example of use:

let getJsonResponse = req =>
  resolveOk(Request.bodyJSON(req))
  |. errorIfNone(`InvalidRequest)
  |. mapOk(body => Js.Json.decodeObject(body) |. Ok)
  |. errorIfNone(`InvalidRequest)
  |. mapOk(body =>
       (
         Utils.Json.getString("_username", body) |> Utils.def(""),
         Utils.Json.getString("_password", body) |> Utils.def(""),
       )
       |. Ok
     )
  |. flatMapOk(((username, password)) => LoginClient.login(~username, ~password))
  |. mapOk(token => Ok(makeJsonRes(res, Ok, Js.Dict.fromList([("token", Js.Json.string(token))]))))
  |. mapError(
       fun
       | `InvalidRequest => Ok(makeJsonError(res, BadRequest, "Bad request"))
       | `BadCredentials => Ok(makeJsonError(res, BadRequest, "Bad credentials"))
       | `exn(exn) => Ok(makeJsonError(res, InternalServerError, Utils.Error.exnToString(exn))),
     );

Here is another more complete example :

let submitFacebookLogin = () => {
  dispatch(Security_reducer.Submit);
  Facebook.WithScriptjs.loadScript(~appId=Container.facebookAppId)
  |. flatMapOk(fb => Facebook.FB.login(fb, ~scope="public_profile,email") |. mapOk(res => Ok((fb, res))))
  |. mapOk(((fb, res)) =>
        switch (res) {
        | Connected({accessToken}) => Ok((fb, accessToken))
        | NotAuthorized => Error(`NotAuthorized)
        | Unknown => Error(`Unknown)
        }
      )
  |. flatMapOk(((fb, accessToken)) =>
        LoginClient.facebookLogin(~accessToken)
        |. flatMap(
            fun
            | Error(_) => {
                dispatch(Security_reducer.InitialNotLogged);
                Facebook.FB.Api.get(
                  fb,
                  "me?fields=id,name,email,last_name,first_name",
                  {"access_token": accessToken},
                )
                |. convertResult
                |. mapOk(response =>
                      Ok(
                        dispatch(
                          Reductive_router_reducer.Push(
                            Url.signUp(
                              ~defaultForm={
                                email: Utils.Json.getString("email", response),
                                lastname: Utils.Json.getString("last_name", response),
                                firstname: Utils.Json.getString("first_name", response),
                              },
                              (),
                            ),
                          ),
                        ),
                      )
                    );
              }
            | Ok(_) =>
              Apollo_client.resetStore(Container.apolloClient)
              |. convertResult
              |. flatMapOk(_ => getProfileId() |. convertResult)
              |. mapOk(profileId => Ok(dispatch(Security_reducer.SubmitSuccess({profileId: profileId}))))
              |. mapOk(_ => Ok(dispatch(Reductive_router_reducer.Redirect(Url.home, Redirect302)))),
          )
      )
  |. mapError(error => {
        let message =
          switch (error) {
          | `ErrorLoadFb => "Impossible de charger le script de connection facebook. Verifiez votre connexion internet."
          | `NotAuthorized => "Facebook ne nous a pas autorisé a récuperer les informations nécessaires. Veuillez réessayer."
          | `Unknown => "Erreur inconnue en provenance de facebook"
          | `exn(exn) => Utils.Error.exnToString(exn)
          };
        Ok(dispatch(Security_reducer.SubmitFailed(message)));
      });
};

#8

Using Fetch API. Full repo: https://github.com/gladimdim/reason-admin/blob/master/src/SourceDetails.re

Quote:

| LoadSources =>
      ReasonReact.UpdateWithSideEffects(
        {...state, loading: true},
        (
          self => {
            let auth =
              Array.make(
                1,
                (
                  "Authorization",
                  Config.AppliedConfig.bearerAuthString(
                    self.state.access_token,
                  ),
                ),
              )
              |> Fetch.HeadersInit.makeWithArray;
            Fetch.fetchWithInit(
              Config.AppliedConfig.url,
              Fetch.RequestInit.make(~headers=auth, ()),
            )
            |> Js.Promise.then_(Fetch.Response.json)
            |> Js.Promise.then_(json => {
                 let result = Decode.Sources.sourceModel(json);
                 self.send(SourcesLoaded(result)) |> ignore;
                 Js.Promise.resolve();
               })
            |> ignore;
          }
        ),
      )

#9

Full repo: https://github.com/cnguy/onetricks.net/blob/master/client/src/api/OneTricksService.re

Context: ReasonReact
Libs: bs-fetch, bs-json

Most of my logic for grabbing API data is like this:

  1. Fetch JSON from API
  2. Decode and then parse into friendly data
  3. Callback from ReasonReact component to update state
let fallbackGet = (cb: oneTricks => unit) : unit => {
  Js.Promise.(
    Fetch.fetch("http://media.onetricks.net/api/fallback-3-26-2018.json")
    |> then_(Fetch.Response.json)
    |> then_(payload =>
         payload |> Decoder.players |> parseIntoOneTricks |> cb |> resolve
       )
    |> catch(error => Js.log(error) |> resolve)
    |> resolve
  )
  |> ignore;
  ();
};