Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
29e0357
Add optional WHATWG Streams polyfill
matthargett Jul 22, 2026
8e8ff8f
Complete Blob streaming and slicing support
matthargett Jul 22, 2026
f16e5a7
Add browser-compatible Headers and Response
matthargett Jul 22, 2026
4b2961d
Handle empty Response byte bodies
matthargett Jul 22, 2026
476cc2b
Support data URLs in the fetch polyfill
matthargett Jul 22, 2026
e04a95f
Avoid intermediate data URL decode buffers
matthargett Jul 22, 2026
a823b2f
Keep Streams implementations internally consistent
matthargett Jul 22, 2026
e29e752
Write Blob BYOB reads into caller buffers
matthargett Jul 22, 2026
006a18d
Track Response body disturbance through stream operations
matthargett Jul 22, 2026
517b298
Preserve Streams bundle bytes across source splits
matthargett Jul 26, 2026
242dccf
Streams: emit the embedded bundle as <=16 KB literals; link Streams i…
matthargett Sep 13, 2026
df025b3
Blob: cast the BYOB respond() length to double (MSVC C4244 as error)
matthargett Sep 13, 2026
160557a
Fetch: emit the embedded polyfill as adjacent <=16 KB literals (MSVC …
matthargett Sep 13, 2026
ec9bacb
Blob: read DataView parts through JS properties (JSI's Napi::DataView…
matthargett Sep 13, 2026
8163ab8
CI: suppress WebKitGTK-internal LeakSanitizer reports on the Linux sa…
matthargett Sep 13, 2026
d667e48
Tests: give the heavy Blob/Response stream tests a 60 s budget (timed…
matthargett Sep 13, 2026
30eebd2
QuickJS: bound the JS stack to the thread's real stack; Chakra: defin…
matthargett Sep 13, 2026
f30a41c
Resolve data: URLs for every UrlLib consumer via a registered scheme …
matthargett Sep 13, 2026
e5a9663
Address review: define _GNU_SOURCE before system headers, bound the f…
matthargett Sep 13, 2026
f5b43de
QuickJS: keep <windows.h> from defining min/max (Win32 QuickJS build)
matthargett Sep 13, 2026
6e662d7
QuickJS: measure the JS stack budget from the current stack pointer t…
matthargett Sep 13, 2026
5d7db94
QuickJS: nested 8 MiB Android thread + 6 MiB JS stack limit (supersed…
matthargett Sep 13, 2026
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
7 changes: 7 additions & 0 deletions .github/lsan_suppressions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# LeakSanitizer suppressions for the Linux sanitizer job.
#
# WebKitGTK's JavaScriptCore grows global bookkeeping (e.g. WTF::BitVector under
# JSObjectMake -> JSObject::setPrototypeDirect on a Structure transition) that it never
# frees at exit, and LSan reports it as a direct leak attributed to whichever caller
# happened to trigger the growth -- here napi reference creation. Not our allocation.
leak:libjavascriptcoregtk
1 change: 1 addition & 0 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ jobs:
run: ./UnitTests
env:
TSAN_OPTIONS: ${{ inputs.enable-thread-sanitizer && format('suppressions={0}/.github/tsan_suppressions.txt', github.workspace) || '' }}
LSAN_OPTIONS: ${{ inputs.enable-sanitizers && format('suppressions={0}/.github/lsan_suppressions.txt', github.workspace) || '' }}
# JSC's concurrent GC on Linux uses SIGUSR1 + sem_wait to suspend mutator
# threads at safepoints. TSan's signal interception delays SIGUSR1 delivery
# indefinitely, deadlocking the Collector Thread's sem_wait. Disabling the
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ option(JSRUNTIMEHOST_POLYFILL_FILE "Include JsRuntimeHost Polyfill File and File
option(JSRUNTIMEHOST_POLYFILL_PERFORMANCE "Include JsRuntimeHost Polyfill Performance." ON)
option(JSRUNTIMEHOST_POLYFILL_TEXTDECODER "Include JsRuntimeHost Polyfill TextDecoder." ON)
option(JSRUNTIMEHOST_POLYFILL_TEXTENCODER "Include JsRuntimeHost Polyfill TextEncoder." ON)
option(JSRUNTIMEHOST_POLYFILL_STREAMS "Include JsRuntimeHost Polyfill Web Streams." ON)

# Sanitizers
option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF)
Expand Down
115 changes: 94 additions & 21 deletions Core/AppRuntime/Source/AppRuntime_QuickJS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,110 @@
#pragma warning(pop)
#endif

#include <stdexcept>
#include <cstddef>

#if defined(__ANDROID__)
#include <pthread.h>
#include <exception>
#include <functional>
#include <utility>
#endif

namespace Babylon
{
void AppRuntime::RunEnvironmentTier(const char* /*executablePath*/)
namespace
{
// Create the runtime.
JSRuntime* runtime = JS_NewRuntime();
if (!runtime)
// Runs the QuickJS environment on the calling thread. QuickJS's interpreter recurses in C
// (one JS_CallInternal frame per JS call), so deep JS call stacks need a comparably deep C
// stack; QuickJS's own limit (JS_DEFAULT_STACK_SIZE, 1 MiB) guards against overrun.
// jsStackLimit: value for JS_SetMaxStackSize, or 0 to keep QuickJS's default (1 MiB). A
// non-default limit is only safe when the caller guarantees a stack large enough to hold it
// below the guard page -- see the Android nested-thread path below.
void RunQuickJSEnvironment(AppRuntime& appRuntime, void (AppRuntime::*run)(Napi::Env), size_t jsStackLimit)
{
throw std::runtime_error{"Failed to create QuickJS runtime"};
}
JSRuntime* runtime = JS_NewRuntime();
if (!runtime)
{
throw std::runtime_error{"Failed to create QuickJS runtime"};
}
if (jsStackLimit != 0)
{
JS_SetMaxStackSize(runtime, jsStackLimit);
}

// Create the context.
JSContext* context = JS_NewContext(runtime);
if (!context)
{
JSContext* context = JS_NewContext(runtime);
if (!context)
{
JS_FreeRuntime(runtime);
throw std::runtime_error{"Failed to create QuickJS context"};
}

{
Napi::Env env = Napi::Attach(context);
(appRuntime.*run)(env);
Napi::Detach(env);
}

JS_FreeContext(context);
JS_FreeRuntime(runtime);
throw std::runtime_error{"Failed to create QuickJS context"};
}
}

// Use the context within a scope.
{
Napi::Env env = Napi::Attach(context);

Run(env);
void AppRuntime::RunEnvironmentTier(const char* /*executablePath*/)
{
#if defined(__ANDROID__)
// bionic gives this worker thread ~1 MiB of stack, at or below QuickJS's default 1 MiB
// recursion limit -- so deep-but-legal JS recursion faults the guard page (SIGSEGV) before
// QuickJS can raise a catchable "stack overflow", while clamping the limit below 1 MiB
// instead rejects call depths that every other engine (and desktop QuickJS on its ~8 MiB
// stack) accepts. Run the environment on a nested thread with a desktop-sized stack so
// QuickJS's raised limit sits safely below the guard page and deep recursion fits. The
// stack must be generous because connectedAndroidTest is an unoptimized Debug build, whose
// JS_CallInternal frames are several times larger than a release build's -- depth-128
// recursion (which release QuickJS clears within the 1 MiB default) needs well over 1 MiB
// here, so QuickJS's default limit would still reject it on any thread size.
constexpr size_t NestedStackSize{8 * 1024 * 1024};
constexpr size_t JsStackLimit{6 * 1024 * 1024}; // < NestedStackSize guard; > debug depth-128 need
std::function<void()> body{[this] { RunQuickJSEnvironment(*this, &AppRuntime::Run, JsStackLimit); }};
std::exception_ptr thrown{};
auto payload = std::make_pair(&body, &thrown);
auto trampoline = [](void* arg) -> void* {
auto* p = static_cast<std::pair<std::function<void()>*, std::exception_ptr*>*>(arg);
try
{
(*p->first)();
}
catch (...)
{
*p->second = std::current_exception();
}
return nullptr;
};

Napi::Detach(env);
pthread_attr_t attr;
if (pthread_attr_init(&attr) == 0)
{
pthread_attr_setstacksize(&attr, NestedStackSize);
pthread_t tid{};
const int created = pthread_create(&tid, &attr, trampoline, &payload);
pthread_attr_destroy(&attr);
if (created == 0)
{
pthread_join(tid, nullptr);
if (thrown)
{
std::rethrow_exception(thrown);
}
return;
}
}

// Destroy the context and runtime.
JS_FreeContext(context);
JS_FreeRuntime(runtime);
// Thread creation failed: fall back to the current (small) worker thread, where only
// QuickJS's default limit is safe.
RunQuickJSEnvironment(*this, &AppRuntime::Run, 0);
#else
RunQuickJSEnvironment(*this, &AppRuntime::Run, 0);
#endif
}

void AppRuntime::ShutdownEnvironment(Napi::Env)
Expand Down
31 changes: 31 additions & 0 deletions Core/Node-API/Source/env_chakra.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,37 @@ namespace Napi
JsValueRef global;
ThrowIfFailed(JsGetGlobalObject(&global));
JsPropertyIdRef propertyId;

// The Windows 10 Chakra predates ES2020 and has no `globalThis`; scripts written against
// browsers (and the polyfills in this repo) reference it. Define it as a plain, writable,
// configurable property of the global object, exactly as the spec describes.
ThrowIfFailed(JsGetPropertyIdFromName(L"globalThis", &propertyId));
JsValueRef existingGlobalThis;
ThrowIfFailed(JsGetProperty(global, propertyId, &existingGlobalThis));
JsValueType existingType;
ThrowIfFailed(JsGetValueType(existingGlobalThis, &existingType));
if (existingType == JsUndefined)
{
// { value: globalThis, writable: true, enumerable: false, configurable: true } -- the
// spec's own data property; plain assignment would make it enumerable.
JsValueRef descriptor;
ThrowIfFailed(JsCreateObject(&descriptor));
JsValueRef trueValue;
ThrowIfFailed(JsGetTrueValue(&trueValue));
JsValueRef falseValue;
ThrowIfFailed(JsGetFalseValue(&falseValue));
JsPropertyIdRef descriptorPropertyId;
ThrowIfFailed(JsGetPropertyIdFromName(L"value", &descriptorPropertyId));
ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, global, true));
ThrowIfFailed(JsGetPropertyIdFromName(L"writable", &descriptorPropertyId));
ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, trueValue, true));
ThrowIfFailed(JsGetPropertyIdFromName(L"enumerable", &descriptorPropertyId));
ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, falseValue, true));
ThrowIfFailed(JsGetPropertyIdFromName(L"configurable", &descriptorPropertyId));
ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, trueValue, true));
bool defined;
ThrowIfFailed(JsDefineProperty(global, propertyId, descriptor, &defined));
}
ThrowIfFailed(JsGetPropertyIdFromName(L"Object", &propertyId));
JsValueRef object;
ThrowIfFailed(JsGetProperty(global, propertyId, &object));
Expand Down
1 change: 1 addition & 0 deletions Polyfills/Blob/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
set(SOURCES
"Include/Babylon/Polyfills/Blob.h"
"InternalInclude/Babylon/Polyfills/BlobInternal.h"
"README.md"
"Source/Blob.cpp"
"Source/Blob.h")

Expand Down
12 changes: 12 additions & 0 deletions Polyfills/Blob/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Blob

Provides the browser `Blob` constructor, byte/text readers, zero-copy Blob
composition and slicing, and a lazily pulled byte `ReadableStream`.

Call `Babylon::Polyfills::Streams::Initialize` before using `Blob.stream()` on
engines that do not provide Web Streams. Stream reads copy only the requested
chunk into JavaScript-owned memory; composing Blobs and slicing share immutable
native byte segments.

The focused tests are adapted from WPT `FileAPI/blob`, WebKit's Blob stream
chunk/crash regressions, and Firefox's large Blob `pipeTo` regression.
Loading