Skip to content

Lib: add Promise + Fetch bindings (#2031, #596)#2239

Open
hhugo wants to merge 7 commits into
masterfrom
promise-2031
Open

Lib: add Promise + Fetch bindings (#2031, #596)#2239
hhugo wants to merge 7 commits into
masterfrom
promise-2031

Conversation

@hhugo
Copy link
Copy Markdown
Member

@hhugo hhugo commented May 12, 2026

This is following #2221

Summary

Closes #2031 and addresses the long-standing Fetch request (#596).

  • Promise module in lib/js_of_ocaml/: bindings to JS Promise with an indirection wrapper that prevents auto-flattening of thenables, so 'a Promise.t Promise.t stays sound at the OCaml type level. Wrap helpers live in runtime/{js,wasm}/promise.{js,wat} and are exposed via Jsoo_runtime.Promise. Wrapping is conditional on the value being thenable, so non-thenable resolves pay no allocation and foreign promises handed in via of_any flow through unchanged. then_ takes an optional ?on_error for the two-callback .then(f, g) form.
  • Js_of_ocaml_lwt.Promise.{to_lwt,of_lwt} for Lwt interop, with a Rejected of error exception for non-OCaml rejection reasons.
  • Abort module: general-purpose AbortController / AbortSignal primitive, separate from Fetch so it can be reused by future bindings (Streams, custom cancellation, …).
  • Fetch module: Headers, Request, RequestInit, Response, ResponseInit class types and the fetch entry point. Body-reader methods (text, json, blob, arrayBuffer, formData) typed with Promise.t. Uses Abort.signal for the signal field.
  • More Promise-typed Dom_html bindings: requestFullscreen / exitFullscreen / requestPointerLock now return unit Promise.t Js.meth (legacy fire-and-forget kept under _-suffixed names); mediaElement.play returns unit Promise.t Js.meth (legacy play_ kept); Animation.finished and Animation.ready exposed as animation t Promise.t readonly_prop.
  • CONTRIBUTING.md: documents the existing leading-_ (mangling) vs trailing-_ (alternate form / legacy stub) split for new bindings.
  • API-STATUS.md: updated to mark Promise, Fetch, AbortController, Fullscreen, Pointer Lock as bound; collapsed Tier 1.

Out of scope (left for follow-ups): ReadableStream / response body streaming, Request clone-from-Request constructor, richer FormData integration, the wider modern surface on navigator (mediaDevices, clipboard, serviceWorker, permissions, credentials, …).

Test plan

  • dune build (jsoo)
  • WASM_OF_OCAML=true dune build (wasm)
  • dune runtest lib/tests — sync sanity tests for Promise (constructor / instanceof / make sync body)
  • dune runtest compiler/tests-jsoo — end-to-end async test_promise covering resolve / then_ / catch / finally / all / race / nested promise / of_any of a foreign promise / reject-with-promise (verifies that JS doesn't auto-follow on rejection)
  • dune runtest compiler/tests-jsootest_lwt_promise round-trips Promise ↔ Lwt for both fulfilled and rejected paths

🤖 Generated with Claude Code

Comment thread lib/js_of_ocaml/fetch.ml Outdated
Comment thread lib/lwt/lwt_promise.mli Outdated
@hhugo hhugo requested a review from vouillon May 21, 2026 11:46
Bind JavaScript promises with an indirection wrapper that prevents the
runtime from auto-flattening thenables, so 'a Promise.t Promise.t stays
sound at the OCaml type level.

The wrap/unwrap indirection lives in runtime/{js,wasm}/promise.{js,wat}
and is exposed via Jsoo_runtime.Promise. Wrap is conditional on the value
being thenable, so non-thenable resolves pay no allocation, and foreign
promises handed in via [of_any] flow through [then_] unchanged.

[then_] accepts an [?on_error] for the two-callback .then(f, g) form,
distinct from [then_ |> catch] in that [on_error] only fires for
rejections of the input promise.
hhugo and others added 6 commits May 21, 2026 14:13
Js_of_ocaml_lwt.Promise extends Js_of_ocaml.Promise with [to_lwt] and
[of_lwt] for converting between Lwt promises and JavaScript promises.
Rejections of foreign promises are surfaced to Lwt as a Failure carrying
[Js_error.to_string].
- requestFullscreen / requestPointerLock / exitFullscreen: add
  unit Promise.t Js.meth variants, keeping the existing fire-and-forget
  forms under the _-suffixed names.
- mediaElement: rename existing fire-and-forget play to play_; add
  play : unit Promise.t meth so callers can handle autoplay rejections
  (NotAllowedError).
- animation: add finished and ready Promise<Animation> properties.
Adds:
- Abort module: general-purpose AbortController/AbortSignal primitive
  (also usable by event listeners, streams, custom cancellation).
- Fetch module: Headers, Request, RequestInit, Response, ResponseInit
  class types, plus the [fetch] entry point. The body-reader methods
  (text, json, blob, arrayBuffer, formData) are typed with Promise.t.
  Uses Abort.signal for the [signal] field.

Out of scope (left for follow-ups): ReadableStream / response body
streaming, Request.clone-from-Request constructor variant, FormData
constructor improvements.
Codify the leading-underscore-for-mangling vs trailing-underscore-for-
alternate-form split that the codebase has informally followed. Helps
future contributors avoid adding new keyword escapes via trailing _
(which is reserved for argument overloads and legacy fire-and-forget
variants of Promise-typed methods).
Three method bindings used multi-underscore names that ppx_js
unescape mangles to a JS identifier different from the intended one
(unescape drops everything from the last `_`):

- Typed_array._BYTES_PER_ELEMENT -> BYTES_PER (intended BYTES_PER_ELEMENT)
- WebGL._MAX_RENDERBUFFER_SIZE -> MAX_RENDERBUFFER (intended MAX_RENDERBUFFER_SIZE)
- canvasElement.toDataURL_type_compression -> toDataURL_type
  (intended toDataURL; toDataURL_type is the existing 1-arg overload)

Renamed to _BYTES_PER_ELEMENT_, _MAX_RENDERBUFFER_SIZE_, and
toDataURL_compression respectively so the JS calls resolve correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mangler drops the last `_` and everything after it, so names with
more than one non-leading underscore resolve to something shorter than
intended (this is what motivated the preceding mangling-bug fixes for
`_BYTES_PER_ELEMENT`, `_MAX_RENDERBUFFER_SIZE`, and
`toDataURL_type_compression`). Document the rule with a small examples
table and the two escape patterns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE REQUEST] Integrate a Promise API

2 participants