Skip to content
Merged
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 CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ FetchContent_Declare(llhttp
EXCLUDE_FROM_ALL)
FetchContent_Declare(UrlLib
GIT_REPOSITORY https://github.com/BabylonJS/UrlLib.git
GIT_TAG 74985214bd4f83a4906b2c62134ac2f9ab89e1ae
GIT_TAG e86ffb34e77092266145497681efc74e0a920ffe
EXCLUDE_FROM_ALL)
# --------------------------------------------------

Expand Down
15 changes: 13 additions & 2 deletions Polyfills/AbortController/Readme.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
# AbortController
Implements parts of [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/) and [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). Provides a way to trigger the abort signal. *Work In Progress*

Supported on `AbortSignal`:
* `aborted` (read-only) and `reason`
* [`throwIfAborted()`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/throwIfAborted)
* static [`AbortSignal.abort(reason?)`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/abort)
* `onabort`, `addEventListener("abort", ...)`, `removeEventListener`

`AbortController.abort(reason?)` forwards the reason to the signal; when no reason is given the
signal's `reason` defaults to an `AbortError` (an `Error` whose `name` is `"AbortError"`, since
there is no `DOMException` polyfill). `fetch()` honors an `AbortSignal` passed via `init.signal`:
an already-aborted signal rejects the promise synchronously, and an in-flight abort cancels the
transport and rejects with the signal's `reason`. (Transport cancellation is effective on backends
where `UrlLib::UrlRequest::Abort()` is implemented.)

Currently not implemented:
* [`ThrowIfAborted`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/throwIfAborted)
* [`Timeout`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout)
* [`Abort`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/abort)

Both the AbortController and AbortSignal polyfills are initialized inside AbortController's initialize method:
```c++
Expand Down
6 changes: 3 additions & 3 deletions Polyfills/AbortController/Source/AbortController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ namespace Babylon::Polyfills::Internal
return m_signal.Value();
}

void AbortController::Abort(const Napi::CallbackInfo&)
void AbortController::Abort(const Napi::CallbackInfo& info)
{
AbortSignal* sig = AbortSignal::Unwrap(m_signal.Value());

assert(sig != nullptr);
sig->Abort();
sig->Abort(info.Length() > 0 ? info[0] : info.Env().Undefined());
}

AbortController::AbortController(const Napi::CallbackInfo& info)
Expand Down
53 changes: 49 additions & 4 deletions Polyfills/AbortController/Source/AbortSignal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ namespace Babylon::Polyfills::Internal
env,
JS_ABORT_SIGNAL_CONSTRUCTOR_NAME,
{
InstanceAccessor("aborted", &AbortSignal::GetAborted, &AbortSignal::SetAborted),
InstanceAccessor("aborted", &AbortSignal::GetAborted, nullptr),
InstanceAccessor("reason", &AbortSignal::GetReason, nullptr),
InstanceAccessor("onabort", &AbortSignal::GetOnAbort, &AbortSignal::SetOnAbort),
InstanceMethod("throwIfAborted", &AbortSignal::ThrowIfAborted),
InstanceMethod("addEventListener", &AbortSignal::AddEventListener),
InstanceMethod("removeEventListener", &AbortSignal::RemoveEventListener),
StaticMethod("abort", &AbortSignal::AbortStatic),
});

env.Global().Set(JS_ABORT_SIGNAL_CONSTRUCTOR_NAME, func);
Expand All @@ -26,10 +29,30 @@ namespace Babylon::Polyfills::Internal
{
}

void AbortSignal::Abort()
Napi::Value AbortSignal::CreateAbortError(Napi::Env env, const char* message)
{
// There is no DOMException polyfill, so represent the abort reason as an Error whose `name`
// is "AbortError" -- the value web code checks (`err.name === "AbortError"`).
Napi::Error error = Napi::Error::New(env, message);
error.Set("name", Napi::String::New(env, "AbortError"));
return error.Value();
}

void AbortSignal::Abort(const Napi::Value& reason)
{
if (m_aborted)
{
return;
}

m_aborted = true;

Napi::Env env = Env();
const Napi::Value resolvedReason = (reason.IsUndefined() || reason.IsEmpty())
? CreateAbortError(env, "The operation was aborted.")
: reason;
m_reason = Napi::Persistent(resolvedReason);

auto onabort = m_onabort.Value();
if (!onabort.IsNull() && !onabort.IsUndefined())
{
Expand All @@ -39,14 +62,36 @@ namespace Babylon::Polyfills::Internal
RaiseEvent("abort");
}

Napi::Value AbortSignal::AbortStatic(const Napi::CallbackInfo& info)
{
Napi::Env env = info.Env();
Napi::Object signalObject = env.Global().Get(JS_ABORT_SIGNAL_CONSTRUCTOR_NAME).As<Napi::Function>().New({});
AbortSignal* signal = AbortSignal::Unwrap(signalObject);
signal->Abort(info.Length() > 0 ? info[0] : env.Undefined());
return signalObject;
}

Napi::Value AbortSignal::GetAborted(const Napi::CallbackInfo&)
{
return Napi::Value::From(Env(), m_aborted);
}

void AbortSignal::SetAborted(const Napi::CallbackInfo&, const Napi::Value& value)
Napi::Value AbortSignal::GetReason(const Napi::CallbackInfo&)
{
m_aborted = value.As<Napi::Boolean>();
if (m_reason.IsEmpty())
{
return Env().Undefined();
}

return m_reason.Value();
}

void AbortSignal::ThrowIfAborted(const Napi::CallbackInfo& info)
{
if (m_aborted)
{
throw Napi::Error{info.Env(), GetReason(info)};
}
}

Napi::Value AbortSignal::GetOnAbort(const Napi::CallbackInfo&)
Expand Down
17 changes: 15 additions & 2 deletions Polyfills/AbortController/Source/AbortSignal.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,23 @@ namespace Babylon::Polyfills::Internal
static void Initialize(Napi::Env env);
explicit AbortSignal(const Napi::CallbackInfo& info);

void Abort();
// Transition the signal to the aborted state with the given reason (undefined -> a default
// "AbortError"), firing onabort and any "abort" listeners. No-op if already aborted.
void Abort(const Napi::Value& reason);

// Build the default abort reason: an Error whose name is "AbortError" (there is no
// DOMException polyfill), matching what the platform uses when abort() is called with no
// reason and what fetch() rejects with on abort.
static Napi::Value CreateAbortError(Napi::Env env, const char* message);

private:
Napi::Value GetAborted(const Napi::CallbackInfo& info);
void SetAborted(const Napi::CallbackInfo&, const Napi::Value& value);

Napi::Value GetReason(const Napi::CallbackInfo& info);
void ThrowIfAborted(const Napi::CallbackInfo& info);

// AbortSignal.abort(reason?) -- returns an AbortSignal already in the aborted state.
static Napi::Value AbortStatic(const Napi::CallbackInfo& info);

Napi::Value GetOnAbort(const Napi::CallbackInfo& info);
void SetOnAbort(const Napi::CallbackInfo&, const Napi::Value& value);
Expand All @@ -33,6 +45,7 @@ namespace Babylon::Polyfills::Internal
std::unordered_map<std::string, std::vector<Napi::FunctionReference>> m_eventHandlerRefs;

Napi::FunctionReference m_onabort;
Napi::Reference<Napi::Value> m_reason;
bool m_aborted = false;
};
}
33 changes: 33 additions & 0 deletions Polyfills/Fetch/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,36 @@ Like `XMLHttpRequest`, `fetch()` supports loading local resources:
* Only `GET` and `POST` methods are supported (a `UrlLib` limitation shared with `XMLHttpRequest`).
* Only string request bodies are supported.
* Consistent with the fetch spec, the promise rejects only on transport-level failures. A completed request with a non-`2xx` status (e.g. `404`) still resolves, with `response.ok === false`.

## Transport-failure rejections
On a transport-level failure (DNS failure, connection refused, TLS failure, missing local
asset, ...) the promise rejects with a **`TypeError`** whose `message` is the stable string
`"fetch failed"`. The message is intentionally constant so crash-report grouping stays intact;
the variable detail is carried on `error.cause` (the Node/undici shape), never spread across the
message:

```js
try {
await fetch("https://does-not-resolve.invalid/");
} catch (error) {
error.message; // "fetch failed" (stable)
error.cause.code; // stable token, e.g. "CURLE_COULDNT_RESOLVE_HOST" (where available)
error.cause.detail; // full normalized UrlLib string (where available)
error.cause.url; // the requested URL
error.cause.status; // 0 for a transport failure
}
```

`error.cause.code` / `error.cause.detail` come from `UrlLib`'s normalized transport-error
accessors and are present on the backends that populate them (Apple, Linux); on backends that do
not yet (Windows, Android) they are omitted while the standard observable shape (a `TypeError`
with the stable message, plus `cause.url` / `cause.status`) is preserved. This is a deliberate,
strictly-additive superset of the spec: spec-conformant code only sees a `TypeError`, exactly as
in a browser, while BN-aware diagnostic code can read `cause` to distinguish a DNS failure from a
refused connection or a missing local asset.

The rejection's `stack` is captured synchronously at the `fetch()` call site (before the request
is handed to a worker thread), so crash reports can attribute the failing call rather than an
empty scheduler tick. (Engines that only materialize `.stack` when an error is thrown may omit
the frames.)

Loading
Loading