Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v2
with:
node-version: "19.3.0"
node-version: "22.19.0"
- uses: erlef/setup-beam@v1
with:
otp-version: false
Expand Down
112 changes: 109 additions & 3 deletions src/gleam/fetch.gleam
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import gleam/dynamic.{type Dynamic}
import gleam/fetch/fetch_options.{type FetchOptions}
import gleam/fetch/form_data.{type FormData}
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
Expand All @@ -13,7 +14,7 @@ import gleam/javascript/promise.{type Promise}
pub type FetchError {
/// A network error occured, maybe because user lost network connection,
/// because the network took to long to answer, or because the
/// server timed out.
/// server timed out. Also happens when fetch has been aborted.
NetworkError(String)
/// Fetch is unable to read body, for example when body as already been read
/// once.
Expand Down Expand Up @@ -41,7 +42,27 @@ pub type FetchResponse
/// |> fetch.raw_send
/// ```
@external(javascript, "../gleam_fetch_ffi.mjs", "raw_send")
pub fn raw_send(a: FetchRequest) -> Promise(Result(FetchResponse, FetchError))
pub fn raw_send(
request: FetchRequest,
) -> Promise(Result(FetchResponse, FetchError))

/// Call directly `fetch` with a `Request` and `FetchOptions`,
/// then convert the result back to Gleam.
/// Let you get back a `FetchResponse` instead of the Gleam
/// `gleam/http/response.Response` data.
///
/// ```gleam
/// request.new()
/// |> request.set_host("example.com")
/// |> request.set_path("/example")
/// |> fetch.to_fetch_request
/// |> fetch.raw_send_with(fetch_options.new())
/// ```
@external(javascript, "../gleam_fetch_ffi.mjs", "raw_send")
pub fn raw_send_with(
request: FetchRequest,
options: FetchOptions,
) -> Promise(Result(FetchResponse, FetchError))

/// Call `fetch` with a Gleam `Request(String)`, and convert the result back
/// to Gleam. Use it to send strings or JSON stringified.
Expand Down Expand Up @@ -69,6 +90,34 @@ pub fn send(
})
}

/// Call `fetch` with a Gleam `Request(String)` and `FetchOptions`,
/// then convert the result back /// to Gleam.
/// Use it to send strings or JSON stringified.
///
/// If you're looking for something more low-level, take a look at
/// [`raw_send_with`](#raw_send_with).
///
/// ```gleam
/// let my_data = json.object([#("field", "value")])
/// request.new()
/// |> request.set_host("example.com")
/// |> request.set_path("/example")
/// |> request.set_body(json.to_string(my_data))
/// |> request.set_header("content-type", "application/json")
/// |> fetch.send_with(fetch_options.new())
/// ```
pub fn send_with(
request: Request(String),
options: FetchOptions,
) -> Promise(Result(Response(FetchBody), FetchError)) {
request
|> to_fetch_request
|> raw_send_with(options)
|> promise.try_await(fn(resp) {
promise.resolve(Ok(from_fetch_response(resp)))
})
}

/// Call `fetch` with a Gleam `Request(FormData)`, and convert the result back
/// to Gleam. Request will be sent as a `multipart/form-data`, and should be
/// decoded as-is on servers.
Expand Down Expand Up @@ -97,6 +146,36 @@ pub fn send_form_data(
})
}

/// Call `fetch` with a Gleam `Request(FormData)` and `FetchOptions`,
/// then convert the result back to Gleam.
/// Request will be sent as a `multipart/form-data`, and should be
/// decoded as-is on servers.
///
/// If you're looking for something more low-level, take a look at
/// [`raw_send_with`](#raw_send_with).
///
/// ```gleam
/// request.new()
/// |> request.set_host("example.com")
/// |> request.set_path("/example")
/// |> request.set_body({
/// form_data.new()
/// |> form_data.append("key", "value")
/// })
/// |> fetch.send_form_data_with(fetch_options.new())
/// ```
pub fn send_form_data_with(
request: Request(FormData),
options: FetchOptions,
) -> Promise(Result(Response(FetchBody), FetchError)) {
request
|> form_data_to_fetch_request
|> raw_send_with(options)
|> promise.try_await(fn(resp) {
promise.resolve(Ok(from_fetch_response(resp)))
})
}

/// Call `fetch` with a Gleam `Request(FormData)`, and convert the result back
/// to Gleam. Binary will be sent as-is, and you probably want a proper
/// content-type added.
Expand All @@ -110,7 +189,7 @@ pub fn send_form_data(
/// |> request.set_path("/example")
/// |> request.set_body(<<"data">>)
/// |> request.set_header("content-type", "application/octet-stream")
/// |> fetch.send_form_data
/// |> fetch.send_bits
/// ```
pub fn send_bits(
request: Request(BitArray),
Expand All @@ -123,6 +202,33 @@ pub fn send_bits(
})
}

/// Call `fetch` with a Gleam `Request(FormData)` and `FetchOptions`,
/// then convert the result back to Gleam. Binary will be sent as-is,
/// and you probably want a proper content-type added.
///
/// If you're looking for something more low-level, take a look at
/// [`raw_send_with`](#raw_send_with).
///
/// ```gleam
/// request.new()
/// |> request.set_host("example.com")
/// |> request.set_path("/example")
/// |> request.set_body(<<"data">>)
/// |> request.set_header("content-type", "application/octet-stream")
/// |> fetch.send_bits_with(fetch_options.new())
/// ```
pub fn send_bits_with(
request: Request(BitArray),
options: FetchOptions,
) -> Promise(Result(Response(FetchBody), FetchError)) {
request
|> bitarray_request_to_fetch_request
|> raw_send_with(options)
|> promise.try_await(fn(resp) {
promise.resolve(Ok(from_fetch_response(resp)))
})
}

/// Convert a Gleam `Request(String)` to a JavaScript
/// [`Request`](https://developer.mozilla.org/docs/Web/API/Request), where
/// `body` is a string.
Expand Down
54 changes: 54 additions & 0 deletions src/gleam/fetch/abort_controller.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import gleam/fetch/abort_signal.{type AbortSignal}

/// Gleam equivalent of JavaScript [`AbortController`](https://developer.mozilla.org/docs/Web/API/AbortController).
pub type AbortController

/// `AbortController` allows aborting fetch request using `AbortSignal` with
/// specified reason.
///
/// The signal has to obtained using `get_controller_signal`, then the fetch can
/// be aborted commonly, either by user action or by timeout.
///
/// Equivalent to JavaScript [`AbortController`](https://developer.mozilla.org/docs/Web/API/AbortController).
@external(javascript, "../../gleam_fetch_ffi.mjs", "newAbortController")
pub fn new() -> AbortController

/// Aborts the signal bound to the controller.
///
/// The default abort reason is "AbortError".
///
/// ```gleam
/// let controller = abort_controller.new()
/// controller
/// |> abort_controller.abort()
/// ```
///
/// Similar to JavaScript [`abort`](https://developer.mozilla.org/docs/Web/API/AbortController/abort).
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortControllerAbort")
pub fn abort(abort_controller: AbortController) -> Nil

/// Aborts the signal bound to the controller with specified reason.
///
/// ```gleam
/// let controller = abort_controller.new()
/// controller
/// |> abort_controller.abort_with("Cancelled operation")
/// ```
///
/// Similar to JavaScript [`abort`](https://developer.mozilla.org/docs/Web/API/AbortController/abort).
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortControllerAbort")
pub fn abort_with(abort_controller: AbortController, reason: String) -> Nil

/// Returns the associated ['AbortSignal'](https://developer.mozilla.org/docs/Web/API/AbortSignal).
///
/// The signal is commonly then passed to the `FetchOptions`.
///
/// ```gleam
/// let signal = abort_controller.new().get_controller_signal()
/// let options = fetch_options.new()
/// |> fetch_options.set_signal(signal)
/// ```
///
/// Equivalent to JavaScript [`signal`](https://developer.mozilla.org/docs/Web/API/AbortController/signal).
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortControllerGetSignal")
pub fn get_controller_signal(abort_controller: AbortController) -> AbortSignal
99 changes: 99 additions & 0 deletions src/gleam/fetch/abort_signal.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/// Gleam equivalent of JavaScript [`AbortSignal`](https://developer.mozilla.org/docs/Web/API/AbortSignal).
pub type AbortSignal

/// Returns whether signal has already been aborted.
///
/// ```gleam
/// let aborted = abort_controller.new()
/// |> abort_controller.get_controller_signal
/// |> abort_signal.get_aborted
/// ```
///
/// Equivalent to JavaScript [`aborted`](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted).
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalAborted")
pub fn get_aborted(signal: AbortSignal) -> Bool

/// Returns the specified reason for abortion.
///
/// Default reason is "AbortError".
///
/// ```gleam
/// let reason = abort_controller.new()
/// |> abort_controller.get_controller_signal
/// |> abort_signal.get_reason
/// ```
///
/// Equivalent to JavaScript [`reason`](https://developer.mozilla.org/docs/Web/API/AbortSignal/reason).
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalReason")
pub fn get_reason(signal: AbortSignal) -> String

/// Creates new signal that is already aborted.
///
/// The default abort reason is "AbortError".
///
/// ```gleam
/// let signal = abort_signal.abort()
///
/// let req = request.new()
/// |> request.set_host("example.com")
/// |> request.set_path("/example")
///
/// let options =
/// fetch_options.new()
/// |> fetch_options.set_signal(signal)
///
/// fetch.send_with(req, options)
/// ```
///
/// Similar to JavaScript [`abort`](https://developer.mozilla.org/docs/Web/API/AbortController/abort).
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalAbort")
pub fn abort() -> AbortSignal

/// Creates new signal that is already aborted with specified reason.
///
/// ```gleam
/// let signal = abort_signal.abort_with("Cancelled")
///
/// let req = request.new()
/// |> request.set_host("example.com")
/// |> request.set_path("/example")
///
/// let options =
/// fetch_options.new()
/// |> fetch_options.set_signal(signal)
///
/// fetch.send_with(req, options)
/// ```
///
/// Similar to JavaScript [`abort`](https://developer.mozilla.org/docs/Web/API/AbortController/abort).
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalAbort")
pub fn abort_with(reason: String) -> AbortSignal

/// Creates new signal composed of multiple other signals.
///
/// This is useful, if you want to have a request that can either
/// timeout or be cancelled by the user.
///
/// ```gleam
/// let signal =
/// abort_controller.new()
/// |> abort_controller.get_controller_signal
///
/// let multi_signal = abort_signal.from([signal, abort_signal.timeout(500)])
/// ```
///
/// Equivalent to JavaScript ['any'](https://developer.mozilla.org/docs/Web/API/AbortSignal/any_static).
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalFrom")
pub fn from(signals: List(AbortSignal)) -> AbortSignal

/// Creates new signal that will error on timeout after specified time.
///
/// The reason message is "TimeoutError", on unsupported browsers "TypeError".
///
/// ```gleam
/// let signal = abort_signal.timeout(500)
/// ```
///
/// Equivalent to JavaScript ['timeout'](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static).
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalTimeout")
pub fn timeout(time: Int) -> AbortSignal
Loading