Converting from Js.Promise to `reason-promise` in ReasonML

269 views Asked by At

I have a situation where a library is using reason-promise as a default one branch and not another. I am finding it difficult to switch from one branch to another because I can't, for the life of me, figure out how to use reason-promise. I am not much better at Js.Promise but that is besides the point.

The library in question is reason-apollo-client. The branch has a bunch of other improvements, includes reason-promise as the default promise implementation. That branch is the next branch.

By way of example, found at reason-promise-question, I have a Js.Promise function that gets me the current token value. It has the following type:

unit => Js.Promise.t(
  Js.Nullable.t(string)
)

The function can be found in Tokens and is reproduced below. This is a dummy function, there is no binding. The point is to see how to get it to compile.

[@bs.val] [@bs.scope "localStorage"]
external setItem: (string, string) => unit = "setItem";
let setUserToken: string => Js.Promise.t(unit) =
  token => Js.Promise.resolve(setItem("auth", token));

[@bs.val] [@bs.scope "localStorage"]
external getItem: string => Js.Nullable.t(string) = "getItem";
let getUserToken: unit => Js.Promise.t(Js.Nullable.t(string)) =
  () => Js.Promise.resolve(getItem("auth"));

let setTempUserToken: string => Js.Promise.t(unit) =
  _ => Js.Promise.resolve();

let getTempUserToken: unit => Js.Promise.t(Js.Nullable.t(string)) =
  () => Js.Promise.resolve(Js.Nullable.undefined);

When I try use this with reason-promise when creating an apollo/client authlink, I get the following error:

unit => Js.Promise.t(
  Js.Nullable.t(string)
)
Error: This expression has type
         Js.Promise.t(Js.Json.t) = Js.Promise.t(Js.Json.t)
       but an expression was expected of type
         Promise.t(Js.Json.t) = Promise.rejectable(Js.Json.t, Promise.never)

Here is the authlink function:

let authLink =
  ApolloClient.Link.ContextLink.makeAsync((~operation as _, ~prevContext as ctx) => {
    Tokens.getUserToken()
    ->Js.Promise.then_(
        token => {
          switch (token->Js.Nullable.toOption) {
          | None =>
            Tokens.getTempUserToken()
            ->Js.Promise.then_(
                token => Js.Promise.resolve(Js.Nullable.toOption(token)),
                _,
              )
          | Some(token) => Js.Promise.resolve(Some(token))
          }
        },
        _,
      )
    ->Js.Promise.then_(
        fun
        | None => Js.Promise.resolve(Js.Json.null)
        | Some(token) => {
            Js.Promise.resolve(
              [%raw
                {| (context, token) => ({
                headers: {
                  ...ctx.headers,
                  authorization: `Bearer ${token}`
                }
              }) |}
              ](
                ctx,
                token,
              ),
            );
          },
        _,
      )
  });

How do we convert this to a reason-promise? Please feel free to be as diadactic as you want to be.

Thank you, in advance.

1

There are 1 answers

0
namesis On

You should be able to convert your vanilla Js.Promise.t into a Promise.t.

First, you will have to convert your Js.Promise.t into a Promise.Js.t. This can be done by

let myIntermediatePromise = myJsPromise |>  Promise.Js.fromBsPromise

The reason that the extra Promise.Js.t type exists is because of the difference in the philosophy of error handling between Js.Promise.t and Promise.t. The latter, handles errors using the Belt.Result module (as seen on https://github.com/aantron/promise#handling-errors-with-result) whereas the former uses rejection (which is typically handled with catch (in JavaScript) or catch_ (in Reason)). The intermediate product of Promise.Js.t encapsulates the same philosophy as the JavaScript promise.

In order to convert the intermediate product to the final Promise.t, you will therefore need to explicitly indicate what should happen when/if the intermediate promise gets rejected. This can be done with a few different ways but as an example you can do:

let myFinalPromise = myIntermediatePromise |> Promise.Js.toResult

This will convert your original Js.Promise.t('a) into a Promise.t(Result('a, Js.Promise.error)).

If you'd like to further convert to Promise.t('a), you have to explicitly handle the possible error.

See https://github.com/aantron/promise#advanced-rejection for more details.

NB that it looks like apollo has since moved to using vanilla promises (https://github.com/reasonml-community/reason-apollo-client/pull/57).