diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index c08eada914e5..94cbe6b261d0 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -7,7 +7,7 @@ // -lto variants built with ThinLTO (per-module summaries for cross-language // importing), and the Windows ICU data table filtered + per-item zstd // compressed (lazily decompressed via bun_icu_decompress.cpp). -export const WEBKIT_VERSION = "639550acdcb2a5fba8a5812b03ff4184522e73fa"; +export const WEBKIT_VERSION = "a0e65bf298499d828f0c60d4557899b94a69d7ae"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/jsc/bindings/BunClientData.cpp b/src/jsc/bindings/BunClientData.cpp index 031428729e72..61756b46c8d7 100644 --- a/src/jsc/bindings/BunClientData.cpp +++ b/src/jsc/bindings/BunClientData.cpp @@ -107,13 +107,13 @@ void JSVMClientData::create(VM* vm, void* bunVM) auto provider = WebCore::createBuiltinsSourceProvider(); JSVMClientData* clientData = new JSVMClientData(*vm, provider); clientData->bunVM = bunVM; - vm->deferredWorkTimer->onAddPendingWork = [clientData](Ref&& ticket, JSC::DeferredWorkTimer::WorkType kind) -> void { + vm->deferredWorkTimer->onAddPendingWork = [clientData](Ref&& ticket, JSC::DeferredWorkTimer::WorkType kind) -> void { Bun::JSCTaskScheduler::onAddPendingWork(clientData, WTF::move(ticket), kind); }; - vm->deferredWorkTimer->onScheduleWorkSoon = [clientData](JSC::DeferredWorkTimer::Ticket ticket, JSC::DeferredWorkTimer::Task&& task) -> void { - Bun::JSCTaskScheduler::onScheduleWorkSoon(clientData, ticket, WTF::move(task)); + vm->deferredWorkTimer->onScheduleWorkSoon = [clientData](Ref&& ticket, JSC::DeferredWorkTimer::Task&& task) -> void { + Bun::JSCTaskScheduler::onScheduleWorkSoon(clientData, WTF::move(ticket), WTF::move(task)); }; - vm->deferredWorkTimer->onCancelPendingWork = [clientData](JSC::DeferredWorkTimer::Ticket ticket) -> void { + vm->deferredWorkTimer->onCancelPendingWork = [clientData](JSC::DeferredWorkTimer::Ticket& ticket) -> void { Bun::JSCTaskScheduler::onCancelPendingWork(clientData, ticket); }; diff --git a/src/jsc/bindings/JSCTaskScheduler.cpp b/src/jsc/bindings/JSCTaskScheduler.cpp index f87ee0d1eb04..828d54d1461d 100644 --- a/src/jsc/bindings/JSCTaskScheduler.cpp +++ b/src/jsc/bindings/JSCTaskScheduler.cpp @@ -5,7 +5,6 @@ using Ticket = JSC::DeferredWorkTimer::Ticket; using Task = JSC::DeferredWorkTimer::Task; -using TicketData = JSC::DeferredWorkTimer::TicketData; namespace Bun { using namespace JSC; @@ -15,13 +14,13 @@ extern "C" void Bun__eventLoop__incrementRefConcurrently(void* bunVM, int delta) class JSCDeferredWorkTask { public: - JSCDeferredWorkTask(Ref ticket, Task&& task) + JSCDeferredWorkTask(Ref ticket, Task&& task) : ticket(WTF::move(ticket)) , task(WTF::move(task)) { } - Ref ticket; + Ref ticket; Task task; ~JSCDeferredWorkTask() { @@ -32,14 +31,9 @@ class JSCDeferredWorkTask { WTF_MAKE_TZONE_ALLOCATED(JSCDeferredWorkTask); }; -static JSC::VM& getVM(Ticket& ticket) -{ - return ticket->scriptExecutionOwner()->vm(); -} - // Drop `ticket` from whichever pending set holds it. Caller holds m_lock; the // event-loop ref is balanced after the caller releases the lock. -static bool dropPendingTicketLocked(Bun::JSCTaskScheduler& scheduler, Ticket ticket) WTF_REQUIRES_LOCK(scheduler.m_lock) +static bool dropPendingTicketLocked(Bun::JSCTaskScheduler& scheduler, Ticket* ticket) WTF_REQUIRES_LOCK(scheduler.m_lock) { bool isKeepingEventLoopAlive = scheduler.m_pendingTicketsKeepingEventLoopAlive.removeIf([ticket](auto pendingTicket) { return pendingTicket.ptr() == ticket; @@ -53,7 +47,7 @@ static bool dropPendingTicketLocked(Bun::JSCTaskScheduler& scheduler, Ticket tic return isKeepingEventLoopAlive; } -void JSCTaskScheduler::onAddPendingWork(WebCore::JSVMClientData* clientData, Ref&& ticket, JSC::DeferredWorkTimer::WorkType kind) +void JSCTaskScheduler::onAddPendingWork(WebCore::JSVMClientData* clientData, Ref&& ticket, JSC::DeferredWorkTimer::WorkType kind) { auto& scheduler = clientData->deferredWorkTimer; Locker holder { scheduler.m_lock }; @@ -66,7 +60,7 @@ void JSCTaskScheduler::onAddPendingWork(WebCore::JSVMClientData* clientData, Ref scheduler.m_pendingTicketsOther.add(WTF::move(ticket)); } } -void JSCTaskScheduler::onScheduleWorkSoon(WebCore::JSVMClientData* clientData, Ticket ticket, Task&& task) +void JSCTaskScheduler::onScheduleWorkSoon(WebCore::JSVMClientData* clientData, Ref&& ticket, Task&& task) { auto& scheduler = clientData->deferredWorkTimer; Locker holder { scheduler.m_lock }; @@ -80,23 +74,23 @@ void JSCTaskScheduler::onScheduleWorkSoon(WebCore::JSVMClientData* clientData, T // across the check and the enqueue so the transition in markShuttingDown // cannot race a cross-thread Atomics.notify. if (scheduler.m_isShuttingDown) [[unlikely]] { - bool wasKeepingAlive = dropPendingTicketLocked(scheduler, ticket); + bool wasKeepingAlive = dropPendingTicketLocked(scheduler, ticket.ptr()); holder.unlockEarly(); if (wasKeepingAlive) Bun__eventLoop__incrementRefConcurrently(clientData->bunVM, -1); return; } - auto* job = new JSCDeferredWorkTask(*ticket, WTF::move(task)); + auto* job = new JSCDeferredWorkTask(WTF::move(ticket), WTF::move(task)); Bun__queueJSCDeferredWorkTaskConcurrently(clientData->bunVM, job); } -void JSCTaskScheduler::onCancelPendingWork(WebCore::JSVMClientData* clientData, Ticket ticket) +void JSCTaskScheduler::onCancelPendingWork(WebCore::JSVMClientData* clientData, Ticket& ticket) { auto* bunVM = clientData->bunVM; auto& scheduler = clientData->deferredWorkTimer; Locker holder { scheduler.m_lock }; - bool wasKeepingAlive = dropPendingTicketLocked(scheduler, ticket); + bool wasKeepingAlive = dropPendingTicketLocked(scheduler, &ticket); holder.unlockEarly(); if (wasKeepingAlive) Bun__eventLoop__incrementRefConcurrently(bunVM, -1); @@ -114,7 +108,7 @@ static void runPendingWork(void* bunVM, Bun::JSCTaskScheduler& scheduler, JSCDef holder.unlockEarly(); if (pendingTicket && !pendingTicket->isCancelled()) { - job->task(job->ticket.ptr()); + job->task(job->ticket.get()); } delete job; @@ -138,7 +132,7 @@ extern "C" void Bun__JSCTaskScheduler__markShuttingDown(JSC::JSGlobalObject* glo } // Reclaim a queued-but-never-dispatched job during shutdown. Called while the -// JSC VM is still alive, so ~Ref and the captured Task lambda may +// JSC VM is still alive, so ~Ref and the captured Task lambda may // safely touch TZone-allocated / JSC-owned state. Mirrors runPendingWork's // ticket take() so the pending set and event-loop ref stay balanced. extern "C" void Bun__deleteDeferredWorkTask(Bun::JSCDeferredWorkTask* job) diff --git a/src/jsc/bindings/JSCTaskScheduler.h b/src/jsc/bindings/JSCTaskScheduler.h index 488c7bec16b1..5d7cf518fec5 100644 --- a/src/jsc/bindings/JSCTaskScheduler.h +++ b/src/jsc/bindings/JSCTaskScheduler.h @@ -16,9 +16,9 @@ class JSCTaskScheduler { { } - static void onAddPendingWork(WebCore::JSVMClientData* clientData, Ref&& ticket, JSC::DeferredWorkTimer::WorkType kind); - static void onScheduleWorkSoon(WebCore::JSVMClientData* clientData, JSC::DeferredWorkTimer::Ticket ticket, JSC::DeferredWorkTimer::Task&& task); - static void onCancelPendingWork(WebCore::JSVMClientData* clientData, JSC::DeferredWorkTimer::Ticket ticket); + static void onAddPendingWork(WebCore::JSVMClientData* clientData, Ref&& ticket, JSC::DeferredWorkTimer::WorkType kind); + static void onScheduleWorkSoon(WebCore::JSVMClientData* clientData, Ref&& ticket, JSC::DeferredWorkTimer::Task&& task); + static void onCancelPendingWork(WebCore::JSVMClientData* clientData, JSC::DeferredWorkTimer::Ticket& ticket); // Set once the owning VM's event loop has taken its last tick. After this, // onScheduleWorkSoon drops the task instead of enqueueing a ConcurrentTask @@ -35,8 +35,8 @@ class JSCTaskScheduler { public: Lock m_lock; bool m_isShuttingDown WTF_GUARDED_BY_LOCK(m_lock) { false }; - UncheckedKeyHashSet> m_pendingTicketsKeepingEventLoopAlive; - UncheckedKeyHashSet> m_pendingTicketsOther; + UncheckedKeyHashSet> m_pendingTicketsKeepingEventLoopAlive; + UncheckedKeyHashSet> m_pendingTicketsOther; }; } diff --git a/src/jsc/bindings/JSEnvironmentVariableMap.cpp b/src/jsc/bindings/JSEnvironmentVariableMap.cpp index 4f4f217679c6..fc29c4c824fa 100644 --- a/src/jsc/bindings/JSEnvironmentVariableMap.cpp +++ b/src/jsc/bindings/JSEnvironmentVariableMap.cpp @@ -487,8 +487,10 @@ static constexpr ASCIILiteral kProxyEnvVarNames[] = { // side-effecting var need only be added in one place. static void applyTZFromString(JSGlobalObject* globalObject, const String& value) { - if (value.length() < 32 && WTF::setTimeZoneOverride(value)) - JSC::getVM(globalObject).dateCache.resetIfNecessarySlow(); + if (value.length() < 32 && WTF::setTimeZoneOverride(value)) { + WTF::timeZoneDidChange(); + JSC::getVM(globalObject).dateCache.clearForTimeZoneChange(); + } } static void applyTLSRejectFromString(JSGlobalObject*, const String& value) { diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 42dd3fa08aac..b2b5535f071b 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -315,6 +315,10 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c JSC::Options::useAsyncStackTrace() = true; JSC::Options::useExplicitResourceManagement() = true; JSC::Options::useImportDefer() = true; + // Upstream enabled Temporal by default; keep it off in Bun until + // the remaining integration work lands. BUN_JSC_useTemporal=1 + // re-enables it for opt-in testing. + JSC::Options::useTemporal() = false; JSC::dangerouslyOverrideJSCBytecodeCacheVersion(getWebKitBytecodeCacheVersion()); #ifdef BUN_DEBUG @@ -3324,7 +3328,8 @@ extern "C" bool JSGlobalObject__setTimeZone(JSC::JSGlobalObject* globalObject, c auto& vm = JSC::getVM(globalObject); if (WTF::setTimeZoneOverride(Zig::toString(*timeZone))) { - vm.dateCache.resetIfNecessarySlow(); + WTF::timeZoneDidChange(); + vm.dateCache.clearForTimeZoneChange(); return true; } diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index d82aafa920be..c82c4a503b98 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -6606,8 +6606,8 @@ extern "C" uint64_t Bun__JSArray__nextPresentIndex( uint64_t result = notFound; if (JSC::SparseArrayValueMap* map = storage->m_sparseMap.get()) { for (const auto& entry : *map) { - if (entry.key >= start && entry.key < result) - result = entry.key; + if (entry.index() >= start && entry.index() < result) + result = entry.index(); } } return result; diff --git a/src/jsc/bindings/webcore/SerializedScriptValue.cpp b/src/jsc/bindings/webcore/SerializedScriptValue.cpp index 97b92658c7ad..4c3d2d90deb0 100644 --- a/src/jsc/bindings/webcore/SerializedScriptValue.cpp +++ b/src/jsc/bindings/webcore/SerializedScriptValue.cpp @@ -6403,11 +6403,16 @@ ExceptionOr> SerializedScriptValue::create(JSGlobalOb if (auto arrayBuffer = toPossiblySharedArrayBuffer(vm, transferable.get())) { if (arrayBuffer->isDetached() || arrayBuffer->isShared()) return Exception { DataCloneError }; - if (arrayBuffer->isLocked()) { - auto scope = DECLARE_THROW_SCOPE(vm); + if (arrayBuffer->isWasmMemory()) { throwVMTypeError(&lexicalGlobalObject, scope, errorMessageForTransfer(arrayBuffer)); RELEASE_AND_RETURN(scope, Exception { ExistingExceptionError }); } + // No generic isDetachable() gate: Bun's native borrows call + // ArrayBuffer::pin(), which clears isDetachable() without setting + // the lock flag. A pinned buffer falls through so transferTo() + // takes its copyTo() fallback (see bindings.cpp + // JSC__JSValue__pinArrayBuffer). WebAssembly.Memory stays rejected + // above per the spec's [[ArrayBufferDetachKey]] requirement. arrayBuffers.append(WTF::move(arrayBuffer)); continue; } diff --git a/src/jsc/modules/BunJSCModule.h b/src/jsc/modules/BunJSCModule.h index deb683bb80d7..816f6efe2a3e 100644 --- a/src/jsc/modules/BunJSCModule.h +++ b/src/jsc/modules/BunJSCModule.h @@ -651,7 +651,8 @@ JSC_DEFINE_HOST_FUNCTION(functionSetTimeZone, (JSGlobalObject * globalObject, Ca makeString("Invalid timezone: \""_s, timeZoneName, "\""_s)); return {}; } - vm.dateCache.resetIfNecessarySlow(); + WTF::timeZoneDidChange(); + vm.dateCache.clearForTimeZoneChange(); WTF::Vector buffer; WTF::getTimeZoneOverride(buffer); WTF::String timeZoneString(buffer.span()); diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index 649928a78729..685c1006bcc1 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -1202,7 +1202,7 @@ pub(crate) fn __bun_release_task_at_shutdown(task: bun_event_loop::Task) -> bool // completion) enqueued this after the event loop's last tick. The // dispatch arm above would have `delete`d it; mirror that here so the // re-queue path doesn't keep it alive past worker VM dealloc. Runs - // before JSC teardown, so ~Ref is safe. + // before JSC teardown, so ~Ref is safe. task_tag::JSCDeferredWorkTask => { unsafe extern "C" { fn Bun__deleteDeferredWorkTask(task: *mut JSCDeferredWorkTask); diff --git a/test/js/bun/jsc/temporal-global.test.ts b/test/js/bun/jsc/temporal-global.test.ts new file mode 100644 index 000000000000..d429300f7b78 --- /dev/null +++ b/test/js/bun/jsc/temporal-global.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +// Upstream WebKit enabled Temporal by default; Bun overrides useTemporal to +// false in ZigGlobalObject.cpp until the remaining integration work lands. +// BUN_JSC_useTemporal=1 re-enables it for opt-in testing. +test("Temporal is not exposed by default", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `process.stdout.write(typeof Temporal)`], + env: { ...bunEnv, BUN_JSC_useTemporal: undefined }, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "undefined", stderr: expect.any(String), exitCode: 0 }); +}); + +test("Temporal is exposed when BUN_JSC_useTemporal=1", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `process.stdout.write(typeof Temporal + " " + typeof Temporal.Now.instant)`], + env: { ...bunEnv, BUN_JSC_useTemporal: "1" }, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "object function", stderr: expect.any(String), exitCode: 0 }); +}); diff --git a/test/js/node/async_hooks/AsyncLocalStorage.test.ts b/test/js/node/async_hooks/AsyncLocalStorage.test.ts index 142f3303a89e..48e49dbdb44a 100644 --- a/test/js/node/async_hooks/AsyncLocalStorage.test.ts +++ b/test/js/node/async_hooks/AsyncLocalStorage.test.ts @@ -1152,3 +1152,72 @@ describe("async context passes through", () => { expect(a).toBe("value"); }); }); + +describe("async generators", () => { + // WebKit c8b6308aaa69 introduced a cooperative async-generator driver + // (InternalMicrotask::AsyncGeneratorDriverResume) as the fast path for + // `for await` over a pristine async generator. It must capture and + // restore Bun's async context like every other resume-body microtask. + test("for await over an async generator preserves the store", async () => { + const als = new AsyncLocalStorage(); + async function* gen() { + yield 1; + yield 2; + yield 3; + } + const seen: unknown[] = []; + await als.run("STORE_A", async () => { + for await (const x of gen()) { + seen.push([x, als.getStore()]); + } + seen.push(["done", als.getStore()]); + }); + expect(seen).toEqual([ + [1, "STORE_A"], + [2, "STORE_A"], + [3, "STORE_A"], + ["done", "STORE_A"], + ]); + }); + + test("for await body sees its own store, not an interleaved one", async () => { + const als = new AsyncLocalStorage(); + async function* gen() { + yield 1; + yield 2; + } + const seen: unknown[] = []; + await Promise.all([ + als.run("A", async () => { + for await (const x of gen()) seen.push(["A-loop", x, als.getStore()]); + }), + als.run("B", async () => { + for await (const x of gen()) seen.push(["B-loop", x, als.getStore()]); + }), + ]); + for (const [tag, , store] of seen) { + expect(store).toBe(tag === "A-loop" ? "A" : "B"); + } + expect(seen.length).toBe(4); + }); + + test("thrown from async generator preserves the store in the catch", async () => { + const als = new AsyncLocalStorage(); + async function* gen() { + yield 1; + throw new Error("boom"); + } + let caughtStore: unknown; + await als.run("STORE_X", async () => { + try { + for await (const _ of gen()) { + expect(als.getStore()).toBe("STORE_X"); + } + } catch (e) { + expect((e as Error).message).toBe("boom"); + caughtStore = als.getStore(); + } + }); + expect(caughtStore).toBe("STORE_X"); + }); +}); diff --git a/test/js/web/workers/structured-clone.test.ts b/test/js/web/workers/structured-clone.test.ts index 3af2be1c6868..e6a305671110 100644 --- a/test/js/web/workers/structured-clone.test.ts +++ b/test/js/web/workers/structured-clone.test.ts @@ -3,6 +3,7 @@ import { openSync } from "fs"; import { bunEnv, bunExe, tls } from "harness"; import { createPrivateKey, createPublicKey, createSecretKey, KeyObject, X509Certificate } from "node:crypto"; import { BlockList } from "node:net"; +import { deflate } from "node:zlib"; import { join } from "path"; // Terminal object types that were never entered into the structured clone object @@ -449,6 +450,40 @@ for (const structuredCloneFn of [structuredClone, jscSerializeRoundtrip, jscSeri structuredCloneFn(buffer, { transfer: [buffer] }); }).toThrow(DOMException); }); + // Bun's native borrows call ArrayBuffer::pin(), which makes the buffer + // non-detachable without setting the C-API lock flag. Transferring a + // pinned buffer must copy via transferTo()'s copyTo() fallback, not + // throw (see bindings.cpp JSC__JSValue__pinArrayBuffer). Locks this in + // so a future WebKit sync that re-adds upstream's !isDetachable() gate + // in SerializedScriptValue::create fails CI. + test("A Bun-pinned ArrayBuffer copies on transfer instead of detaching", async () => { + const ab = new ArrayBuffer(64); + new Uint8Array(ab).fill(42); + const { promise, resolve, reject } = Promise.withResolvers(); + // Starting the async deflate pins ab for the duration of the call. + deflate(new Uint8Array(ab), e => (e ? reject(e) : resolve())); + try { + const clone = structuredCloneFn(ab, { transfer: [ab] }); + expect({ + cloneLength: clone.byteLength, + origLength: ab.byteLength, + sameObject: clone === ab, + cloneFirst: new Uint8Array(clone)[0], + }).toEqual({ cloneLength: 64, origLength: 64, sameObject: false, cloneFirst: 42 }); + } finally { + await promise; + } + expect(ab.byteLength).toBe(64); + }); + // WebAssembly.Memory buffers carry a non-undefined [[ArrayBufferDetachKey]] + // and must be rejected from a transfer list (per HTML's + // StructuredSerializeWithTransfer), unlike a Bun-pinned buffer above. + test("A WebAssembly.Memory buffer is rejected from the transfer list", () => { + const mem = new WebAssembly.Memory({ initial: 1 }); + const buf = mem.buffer; + expect(() => structuredCloneFn(buf, { transfer: [buf] })).toThrow(TypeError); + expect(buf.byteLength).toBe(65536); + }); // https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal // Serializing (not transferring) a detached ArrayBuffer must throw a // "DataCloneError" DOMException, not a TypeError.