Js.Promise.all polyfill

I don’t want to use Promise.all, but I don’t know how to do it.

This is origin code.

 let func = () => {
    Js.Promise.all(
      values->Array.map(value:string => value->f1),
        ) |> Js.Promise.then_((value2: array<array<Js.String.t>>) => {
      let results:array<Js.String.t> = value2->Belt.Array.concatMany
      f2->Belt.Option.map(f => results->f)->ignore
      Js.Promise.resolve()
    })
  }

I try to change but failed :frowning:

  let func = () => {
      values
      ->Array.map(value => value->f1)
      ->Array.map(promise =>
        promise |> Js.Promise.then_(value2 => {
          value2->Js.log
          Js.Promise.resolve()
        })
      )
      ->Array.concat
      // how can i get [value2, value2, value2...]
  }

Hi. Not tested well, but this should work:

let all = (promises: array<Js.Promise.t<'a>>): Js.Promise.t<array<'a>> =>
  Js.Array2.reduce(
    promises,
    (acc, x) =>
      Js.Promise.then_(
        arr =>
          Js.Promise.then_(
            val => Js.Promise.resolve(arr->Js.Array2.concat([val])),
            x,
          ),
        acc,
      ),
    Js.Promise.resolve([]),
  )

Why not use Promise.all though?

Note: this polyfill will handle errors slightly differently from Promise.all

2 Likes

Thank u for answer.
This is because even if one promise fails, the rest must be used.

Ah, then the above solution won’t work either. I would try something like this in this case:

let allWithErrors = (promises: array<Js.Promise.t<'a>>): Js.Promise.t<
  array<result<'a, Js.Promise.error>>,
> =>
  promises
  ->Js.Array2.map(p =>
    Js.Promise.catch(
      e => Js.Promise.resolve(Error(e)),
      Js.Promise.then_(x => Js.Promise.resolve(Ok(x)), p),
    )
  )
  ->Js.Promise.all

This way you’ll get an array of the results of all promises: either an error or a value for each.

2 Likes

Thank you very much indeed.

1 Like

JS has a built-in function for this: Promise.allSettled(). It’s not included in the ReScript bindings, but it should be easy to bind manually.

4 Likes

Here’s what I did for Promise all and allSettled. My use case is just manually prefetching my own images in the load event so I just call blob on the successful ones. Lemme know what you think!

//Promise.res
type t<+'a> = Js.Promise.t<'a>
exception JsError(Js.Exn.t)
external unsafeToJsExn: exn => Js.Exn.t = "%identity"
@val @scope("Promise")
external resolve: 'a => t<'a> = "resolve"
@send external then: (t<'a>, @uncurry ('a => t<'b>)) => t<'b> = "then"
type resp = {
  ok: bool,
  redirected: bool,
  status: int,
  statusText: string,
  @as("type") _type: string,
  url: string,
}
type outcome = {
  status: string,
  value: Js.Nullable.t<resp>,
  reason: Js.Nullable.t<string>,
}
@val @scope("Promise")
external allSettled3: ((t<'a>, t<'b>, t<'c>)) => t<(outcome, outcome, outcome)> = "allSettled"
@val @scope("Promise")
external all3: ((t<'a>, t<'b>, t<'c>)) => t<('a, 'b, 'c)> = "all"
@send external _catch: (t<'a>, @uncurry (exn => t<'a>)) => t<'a> = "catch"
let catch = (promise, callback) => {
  _catch(promise, err => {
    let v = if Js.Exn.isCamlExceptionOrOpenVariant(err) {
      err
    } else {
      JsError(unsafeToJsExn(err))
    }
    callback(. v)
  })
}
//Prefetch.res
open Promise
type blob
@send external blob: resp => Promise.t<blob> = "blob"
@val external fetch: string => Promise.t<resp> = "fetch"
@val external fetchAllSettled: string => Promise.t<outcome> = "fetch"
let getPicsAllSettled = assets =>
  {
    let (asset1, asset2, asset3) = assets
    allSettled3((fetchAllSettled(asset1), fetchAllSettled(asset2), fetchAllSettled(asset3)))
    ->then(((res1, res2, res3)) => {
      [res1, res2, res3]->Js.Array2.forEach(r =>
        switch r.status {
        | "fulfilled" =>
          switch Js.Nullable.toOption(r.value) {
          | Some(resp) => Js.log2("Asset " ++ resp.url ++ " fetched ok: ", resp.ok)
          | None => ()
          }
        | "rejected" =>
          switch Js.Nullable.toOption(r.reason) {
          | Some(msg) => Js.log("Asset fetch failed: " ++ msg)
          | None => ()
          }
        | _ => ()
        }
      )
      Ok(
        [res1, res2, res3]
        ->Js.Array2.filter(r => r.status == "fulfilled")
        ->Js.Array2.map(r =>
          switch Js.Nullable.toOption(r.value) {
          | Some(resp) => resp->blob
          | None =>
            {
              ok: false,
              redirected: false,
              status: 0,
              statusText: "_",
              _type: "_",
              url: "_",
            }->blob
          }
        ),
      )->resolve
    })
    ->catch((. e) => {
      let msg = switch e {
      | JsError(err) =>
        switch Js.Exn.message(err) {
        | Some(msg) => msg
        | None => ""
        }
      | _ => "Unexpected error occurred"
      }
      Js.log2("Fetch error: ", msg)
      Error(msg)->resolve
    })
  }->ignore
let getPics = assets =>
  {
    let (asset1, asset2, asset3) = assets
    all3((fetch(asset1), fetch(asset2), fetch(asset3)))
    ->then(((res1, res2, res3)) =>
      {
        let resps = [res1, res2, res3]
        resps->Js.Array2.forEach(r => Js.log2("Asset " ++ r.url ++ " fetched ok: ", r.ok))
        switch resps->Js.Array2.every(r => r.ok) {
        | true => Ok(resps->Js.Array2.map(r => r->blob))
        | false => {
            let {status, statusText, url} = switch resps->Js.Array2.find(r => !r.ok) {
            | Some(r) => r
            | None => {
                ok: false,
                redirected: false,
                status: 0,
                statusText: "_",
                _type: "_",
                url: "_",
              }
            }
            let msg = j`Fetch error for asset ${url}: $status - ${statusText}`
            Js.log(msg)
            Error(msg)
          }
        }
      }->resolve
    )
    ->catch((. e) => {
      let msg = switch e {
      | JsError(err) =>
        switch Js.Exn.message(err) {
        | Some(msg) => msg
        | None => ""
        }
      | _ => "Unexpected error occurred"
      }
      Js.log2("Fetch error: ", msg)
      Error(msg)->resolve
    })
  }->ignore
let handlerAllSettled = (assets, _e) => getPicsAllSettled(assets)
let handler = (assets, _e) => getPics(assets)
//call site
@scope("window") @val
external addWindowEventListener: (string, unit => unit) => unit = "addEventListener"
let bghand = Prefetch.handler((asset1, asset2, asset3))
//or....
let bghand = Prefetch.handlerAllSettled((asset1, asset2, asset3))

addWindowEventListener("load", bghand)
1 Like

What I was going to do was upload the image. amazing.
I’m still a beginner, so I’ll take my time to read your code!