Skip to content

Commit e292e85

Browse files
committed
Implement AbortSignal, AbortController, RequestInit
Does not fully implement RequestInit, only what seemed useful. Closes: #4 Closes: #10
1 parent a18d7ff commit e292e85

File tree

7 files changed

+636
-6
lines changed

7 files changed

+636
-6
lines changed

src/gleam/fetch.gleam

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import gleam/dynamic.{type Dynamic}
2+
import gleam/fetch/fetch_options.{type FetchOptions}
23
import gleam/fetch/form_data.{type FormData}
34
import gleam/http/request.{type Request}
45
import gleam/http/response.{type Response}
@@ -13,7 +14,7 @@ import gleam/javascript/promise.{type Promise}
1314
pub type FetchError {
1415
/// A network error occured, maybe because user lost network connection,
1516
/// because the network took to long to answer, or because the
16-
/// server timed out.
17+
/// server timed out. Also happens when fetch has been aborted.
1718
NetworkError(String)
1819
/// Fetch is unable to read body, for example when body as already been read
1920
/// once.
@@ -41,7 +42,27 @@ pub type FetchResponse
4142
/// |> fetch.raw_send
4243
/// ```
4344
@external(javascript, "../gleam_fetch_ffi.mjs", "raw_send")
44-
pub fn raw_send(a: FetchRequest) -> Promise(Result(FetchResponse, FetchError))
45+
pub fn raw_send(
46+
request: FetchRequest,
47+
) -> Promise(Result(FetchResponse, FetchError))
48+
49+
/// Call directly `fetch` with a `Request` and `FetchOptions`,
50+
/// then convert the result back to Gleam.
51+
/// Let you get back a `FetchResponse` instead of the Gleam
52+
/// `gleam/http/response.Response` data.
53+
///
54+
/// ```gleam
55+
/// request.new()
56+
/// |> request.set_host("example.com")
57+
/// |> request.set_path("/example")
58+
/// |> fetch.to_fetch_request
59+
/// |> fetch.raw_send_with(fetch_options.new())
60+
/// ```
61+
@external(javascript, "../gleam_fetch_ffi.mjs", "raw_send")
62+
pub fn raw_send_with(
63+
request: FetchRequest,
64+
options: FetchOptions,
65+
) -> Promise(Result(FetchResponse, FetchError))
4566

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

93+
/// Call `fetch` with a Gleam `Request(String)` and `FetchOptions`,
94+
/// then convert the result back /// to Gleam.
95+
/// Use it to send strings or JSON stringified.
96+
///
97+
/// If you're looking for something more low-level, take a look at
98+
/// [`raw_send_with`](#raw_send_with).
99+
///
100+
/// ```gleam
101+
/// let my_data = json.object([#("field", "value")])
102+
/// request.new()
103+
/// |> request.set_host("example.com")
104+
/// |> request.set_path("/example")
105+
/// |> request.set_body(json.to_string(my_data))
106+
/// |> request.set_header("content-type", "application/json")
107+
/// |> fetch.send_with(fetch_options.new())
108+
/// ```
109+
pub fn send_with(
110+
request: Request(String),
111+
options: FetchOptions,
112+
) -> Promise(Result(Response(FetchBody), FetchError)) {
113+
request
114+
|> to_fetch_request
115+
|> raw_send_with(options)
116+
|> promise.try_await(fn(resp) {
117+
promise.resolve(Ok(from_fetch_response(resp)))
118+
})
119+
}
120+
72121
/// Call `fetch` with a Gleam `Request(FormData)`, and convert the result back
73122
/// to Gleam. Request will be sent as a `multipart/form-data`, and should be
74123
/// decoded as-is on servers.
@@ -97,6 +146,36 @@ pub fn send_form_data(
97146
})
98147
}
99148

149+
/// Call `fetch` with a Gleam `Request(FormData)` and `FetchOptions`,
150+
/// then convert the result back to Gleam.
151+
/// Request will be sent as a `multipart/form-data`, and should be
152+
/// decoded as-is on servers.
153+
///
154+
/// If you're looking for something more low-level, take a look at
155+
/// [`raw_send_with`](#raw_send_with).
156+
///
157+
/// ```gleam
158+
/// request.new()
159+
/// |> request.set_host("example.com")
160+
/// |> request.set_path("/example")
161+
/// |> request.set_body({
162+
/// form_data.new()
163+
/// |> form_data.append("key", "value")
164+
/// })
165+
/// |> fetch.send_form_data_with(fetch_options.new())
166+
/// ```
167+
pub fn send_form_data_with(
168+
request: Request(FormData),
169+
options: FetchOptions,
170+
) -> Promise(Result(Response(FetchBody), FetchError)) {
171+
request
172+
|> form_data_to_fetch_request
173+
|> raw_send_with(options)
174+
|> promise.try_await(fn(resp) {
175+
promise.resolve(Ok(from_fetch_response(resp)))
176+
})
177+
}
178+
100179
/// Call `fetch` with a Gleam `Request(FormData)`, and convert the result back
101180
/// to Gleam. Binary will be sent as-is, and you probably want a proper
102181
/// content-type added.
@@ -110,7 +189,7 @@ pub fn send_form_data(
110189
/// |> request.set_path("/example")
111190
/// |> request.set_body(<<"data">>)
112191
/// |> request.set_header("content-type", "application/octet-stream")
113-
/// |> fetch.send_form_data
192+
/// |> fetch.send_bits
114193
/// ```
115194
pub fn send_bits(
116195
request: Request(BitArray),
@@ -123,6 +202,33 @@ pub fn send_bits(
123202
})
124203
}
125204

205+
/// Call `fetch` with a Gleam `Request(FormData)` and `FetchOptions`,
206+
/// then convert the result back to Gleam. Binary will be sent as-is,
207+
/// and you probably want a proper content-type added.
208+
///
209+
/// If you're looking for something more low-level, take a look at
210+
/// [`raw_send_with`](#raw_send_with).
211+
///
212+
/// ```gleam
213+
/// request.new()
214+
/// |> request.set_host("example.com")
215+
/// |> request.set_path("/example")
216+
/// |> request.set_body(<<"data">>)
217+
/// |> request.set_header("content-type", "application/octet-stream")
218+
/// |> fetch.send_bits_with(fetch_options.new())
219+
/// ```
220+
pub fn send_bits_with(
221+
request: Request(BitArray),
222+
options: FetchOptions,
223+
) -> Promise(Result(Response(FetchBody), FetchError)) {
224+
request
225+
|> bitarray_request_to_fetch_request
226+
|> raw_send_with(options)
227+
|> promise.try_await(fn(resp) {
228+
promise.resolve(Ok(from_fetch_response(resp)))
229+
})
230+
}
231+
126232
/// Convert a Gleam `Request(String)` to a JavaScript
127233
/// [`Request`](https://developer.mozilla.org/docs/Web/API/Request), where
128234
/// `body` is a string.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import gleam/fetch/abort_signal.{type AbortSignal}
2+
3+
/// Gleam equivalent of JavaScript [`AbortController`](https://developer.mozilla.org/docs/Web/API/AbortController).
4+
pub type AbortController
5+
6+
/// `AbortController` allows aborting fetch request using `AbortSignal` with
7+
/// specified reason.
8+
///
9+
/// The signal has to obtained using `get_controller_signal`, then the fetch can
10+
/// be aborted commonly, either by user action or by timeout.
11+
///
12+
/// Equivalent to JavaScript [`AbortController`](https://developer.mozilla.org/docs/Web/API/AbortController).
13+
@external(javascript, "../../gleam_fetch_ffi.mjs", "newAbortController")
14+
pub fn new() -> AbortController
15+
16+
/// Aborts the signal bound to the controller.
17+
///
18+
/// The default abort reason is "AbortError".
19+
///
20+
/// ```gleam
21+
/// let controller = abort_controller.new()
22+
/// controller
23+
/// |> abort_controller.abort()
24+
/// ```
25+
///
26+
/// Similar to JavaScript [`abort`](https://developer.mozilla.org/docs/Web/API/AbortController/abort).
27+
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortControllerAbort")
28+
pub fn abort(abort_controller: AbortController) -> Nil
29+
30+
/// Aborts the signal bound to the controller with specified reason.
31+
///
32+
/// ```gleam
33+
/// let controller = abort_controller.new()
34+
/// controller
35+
/// |> abort_controller.abort_with("Cancelled operation")
36+
/// ```
37+
///
38+
/// Similar to JavaScript [`abort`](https://developer.mozilla.org/docs/Web/API/AbortController/abort).
39+
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortControllerAbort")
40+
pub fn abort_with(abort_controller: AbortController, reason: String) -> Nil
41+
42+
/// Returns the associated ['AbortSignal'](https://developer.mozilla.org/docs/Web/API/AbortSignal).
43+
///
44+
/// The signal is commonly then passed to the `FetchOptions`.
45+
///
46+
/// ```gleam
47+
/// let signal = abort_controller.new().get_controller_signal()
48+
/// let options = fetch_options.new()
49+
/// |> fetch_options.set_signal(signal)
50+
/// ```
51+
///
52+
/// Equivalent to JavaScript [`signal`](https://developer.mozilla.org/docs/Web/API/AbortController/signal).
53+
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortControllerGetSignal")
54+
pub fn get_controller_signal(abort_controller: AbortController) -> AbortSignal

src/gleam/fetch/abort_signal.gleam

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/// Gleam equivalent of JavaScript [`AbortSignal`](https://developer.mozilla.org/docs/Web/API/AbortSignal).
2+
pub type AbortSignal
3+
4+
/// Returns whether signal has already been aborted.
5+
///
6+
/// ```gleam
7+
/// let aborted = abort_controller.new()
8+
/// |> abort_controller.get_controller_signal
9+
/// |> abort_signal.get_aborted
10+
/// ```
11+
///
12+
/// Equivalent to JavaScript [`aborted`](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted).
13+
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalAborted")
14+
pub fn get_aborted(signal: AbortSignal) -> Bool
15+
16+
/// Returns the specified reason for abortion.
17+
///
18+
/// Default reason is "AbortError".
19+
///
20+
/// ```gleam
21+
/// let reason = abort_controller.new()
22+
/// |> abort_controller.get_controller_signal
23+
/// |> abort_signal.get_reason
24+
/// ```
25+
///
26+
/// Equivalent to JavaScript [`reason`](https://developer.mozilla.org/docs/Web/API/AbortSignal/reason).
27+
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalReason")
28+
pub fn get_reason(signal: AbortSignal) -> String
29+
30+
/// Creates new signal that is already aborted.
31+
///
32+
/// The default abort reason is "AbortError".
33+
///
34+
/// ```gleam
35+
/// let signal = abort_signal.abort()
36+
///
37+
/// let req = request.new()
38+
/// |> request.set_host("example.com")
39+
/// |> request.set_path("/example")
40+
///
41+
/// let options =
42+
/// fetch_options.new()
43+
/// |> fetch_options.set_signal(signal)
44+
///
45+
/// fetch.send_with(req, options)
46+
/// ```
47+
///
48+
/// Similar to JavaScript [`abort`](https://developer.mozilla.org/docs/Web/API/AbortController/abort).
49+
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalAbort")
50+
pub fn abort() -> AbortSignal
51+
52+
/// Creates new signal that is already aborted with specified reason.
53+
///
54+
/// ```gleam
55+
/// let signal = abort_signal.abort_with("Cancelled")
56+
///
57+
/// let req = request.new()
58+
/// |> request.set_host("example.com")
59+
/// |> request.set_path("/example")
60+
///
61+
/// let options =
62+
/// fetch_options.new()
63+
/// |> fetch_options.set_signal(signal)
64+
///
65+
/// fetch.send_with(req, options)
66+
/// ```
67+
///
68+
/// Similar to JavaScript [`abort`](https://developer.mozilla.org/docs/Web/API/AbortController/abort).
69+
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalAbort")
70+
pub fn abort_with(reason: String) -> AbortSignal
71+
72+
/// Creates new signal composed of multiple other signals.
73+
///
74+
/// This is useful, if you want to have a request that can either
75+
/// timeout or be cancelled by the user.
76+
///
77+
/// ```gleam
78+
/// let signal =
79+
/// abort_controller.new()
80+
/// |> abort_controller.get_controller_signal
81+
///
82+
/// let multi_signal = abort_signal.from([signal, abort_signal.timeout(500)])
83+
/// ```
84+
///
85+
/// Equivalent to JavaScript ['any'](https://developer.mozilla.org/docs/Web/API/AbortSignal/any_static).
86+
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalFrom")
87+
pub fn from(signals: List(AbortSignal)) -> AbortSignal
88+
89+
/// Creates new signal that will error on timeout after specified time.
90+
///
91+
/// The reason message is "TimeoutError", on unsupported browsers "TypeError".
92+
///
93+
/// ```gleam
94+
/// let signal = abort_signal.timeout(500)
95+
/// ```
96+
///
97+
/// Equivalent to JavaScript ['timeout'](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static).
98+
@external(javascript, "../../gleam_fetch_ffi.mjs", "abortSignalTimeout")
99+
pub fn timeout(time: Int) -> AbortSignal

0 commit comments

Comments
 (0)