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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Preserve Error cause graphs in structured cloning and worker messages, while retaining v14 bytes for cause-free values and negotiating cause support for advanced IPC with older Bun processes; thanks fuller-stack-dev.

- Sync oven-sh/bun through `ad4089bf49d9f4c3ab7c8d95a58f3e0f0f186054`, including lint, format, binding, WebSocket and SQL updates, while retaining the fork runtime and CI contracts.

- Share identical `node:vm` script and compiled-function source buffers and source-map URI strings through a bounded per-VM weak index, including after compilation-cache eviction.
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/process/ipc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ process.on("message", message => {

By default, Bun serializes messages with the JSC `serialize` API. This API supports everything [`structuredClone` supports](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm), including strings, typed arrays, and objects. The API does not support transferring ownership of objects.

Advanced IPC preserves `Error.cause` once the peer has advertised support on that channel. Messages sent before that advertisement, or to an older Bun process, omit causes and retain the v14 format used by older builds. Cause-free messages always retain v14 bytes. This allows an already-running process to communicate with a child started from an updated Bun executable. In-process `structuredClone`, Worker, MessagePort, and BroadcastChannel messages preserve causes without IPC negotiation.

```ts child.ts icon="/icons/typescript.svg"
// send a string
process.send("Hello from child as string");
Expand Down
2 changes: 1 addition & 1 deletion docs/runtime/nodejs-compat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ Destroying an HTTP/2 session releases its TLS transport without waiting for the

Map and Set serialization copies entries and ignores custom own properties, like `structuredClone`. Existing Bun records with own-property sections remain readable.

🟡 `writeHeapSnapshot`, `getHeapSnapshot`, `getHeapStatistics`, `getHeapSpaceStatistics`, `queryObjects`, `GCProfiler` and `startupSnapshot` are implemented. `queryObjects()` collects garbage and returns a count or shallow summaries of objects matching the constructor's prototype chain in the current context. The heap statistics describe JavaScriptCore's single heap, and `setFlagsFromString` ignores the flags it is given. `serialize` and `deserialize` use JavaScriptCore's wire format instead of V8's. Missing `startCpuProfile`, `startHeapProfile`, `Serializer`/`Deserializer`, `takeCoverage`/`stopCoverage` and `promiseHooks`. For profiling, use [`bun:jsc`](/project/benchmarking#javascript-heap-stats) instead.
🟡 `writeHeapSnapshot`, `getHeapSnapshot`, `getHeapStatistics`, `getHeapSpaceStatistics`, `queryObjects`, `GCProfiler` and `startupSnapshot` are implemented. `queryObjects()` collects garbage and returns a count or shallow summaries of objects matching the constructor's prototype chain in the current context. The heap statistics describe JavaScriptCore's single heap, and `setFlagsFromString` ignores the flags it is given. `serialize` and `deserialize` use JavaScriptCore's wire format instead of V8's. Cause-free values retain the v14 format; Error cause graphs use v15, which older Bun builds cannot deserialize. These bytes are not interchangeable with Node.js serialization. Missing `startCpuProfile`, `startHeapProfile`, `Serializer`/`Deserializer`, `takeCoverage`/`stopCoverage` and `promiseHooks`. For profiling, use [`bun:jsc`](/project/benchmarking#javascript-heap-stats) instead.

### [`node:vm`](https://nodejs.org/api/vm.html)

Expand Down
5 changes: 2 additions & 3 deletions src/js/node/v8.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ function getCppHeapStatistics(type = "detailed") {
}
// Buffer-bearing payloads are framed as MAGIC + version + SSV([value, buffers]) so deserialize
// can restore Buffer prototypes (see internal/serialization_buffers). Leading 0xFF cannot collide
// with bare SSV output; Buffer-free payloads stay bare SSV so older readers keep working.
// with bare SSV output; Buffer-free payloads retain the current bare SSV framing.

function hasBufferEnvelopeMagic(view) {
// In-bounds integer-indexed reads on a typed array never consult the
Expand Down Expand Up @@ -345,8 +345,7 @@ function stopCoverage() {
notimpl("stopCoverage");
}
function serialize(arg1) {
const tagged = require("internal/serialization_buffers").tagBuffers(arg1);
return serializeNative(tagged === null ? arg1 : tagged, tagged !== null);
return serializeNative(arg1);
}

function getDefaultHeapSnapshotPath() {
Expand Down
15 changes: 15 additions & 0 deletions src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2192,6 +2192,8 @@ pub enum TemporalType {
pub struct SerializedFlags {
pub for_cross_process_transfer: bool,
pub for_storage: bool,
pub preserve_node_buffers: bool,
pub omit_error_cause: bool,
}

/// `JSValue.SerializedScriptValue` — owned view over a
Expand All @@ -2200,8 +2202,13 @@ pub struct SerializedScriptValue {
bytes: *const u8,
size: usize,
handle: *mut c_void,
has_buffer_envelope: bool,
}
impl SerializedScriptValue {
pub fn has_buffer_envelope(&self) -> bool {
self.has_buffer_envelope
}

/// Borrow the serialized bytes. Valid only while `self` is alive (the
/// backing buffer is freed on drop); the lifetime is tied to `&self`.
#[inline]
Expand All @@ -2224,6 +2231,7 @@ struct SerializedScriptValueExternal {
bytes: *const u8,
size: usize,
handle: *mut c_void,
has_buffer_envelope: bool,
}

/// Callback signature for [`JSValue::for_each`] / [`JSValue::for_each_with_context`].
Expand Down Expand Up @@ -2692,6 +2700,12 @@ impl JSValue {
if flags.for_storage {
bits |= 1 << 1;
}
if flags.preserve_node_buffers {
bits |= 1 << 2;
}
if flags.omit_error_cause {
bits |= 1 << 3;
}
let ext = host_fn::from_js_host_call_generic(global, || {
Bun__serializeJSValue(global, self, bits)
})?;
Expand All @@ -2702,6 +2716,7 @@ impl JSValue {
bytes: ext.bytes,
size: ext.size,
handle: ext.handle,
has_buffer_envelope: ext.has_buffer_envelope,
})
}
}
Expand Down
15 changes: 0 additions & 15 deletions src/jsc/bindings/IPC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,6 @@ extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue IPCParse(Zig::Globa
return JSC::JSValue::encode(result);
}

extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue IPCTagAdvancedBuffers(Zig::GlobalObject* global, JSC::EncodedJSValue message)
{
auto& vm = JSC::getVM(global);
auto scope = DECLARE_THROW_SCOPE(vm);
JSC::JSFunction* tagFunction = global->m_ipcTagAdvancedBuffersFunction.getInitializedOnMainThread(global);
JSC::CallData callData = JSC::getCallData(tagFunction);

JSC::MarkedArgumentBuffer args;
args.append(JSC::JSValue::decode(message));

auto result = JSC::call(global, tagFunction, callData, JSC::jsUndefined(), args);
RETURN_IF_EXCEPTION(scope, {});
return JSC::JSValue::encode(result);
}

extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue IPCRestoreAdvancedBuffers(Zig::GlobalObject* global, JSC::EncodedJSValue envelope)
{
auto& vm = JSC::getVM(global);
Expand Down
10 changes: 4 additions & 6 deletions src/jsc/bindings/NodeV8.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
#include "ErrorCode.h"
#include "BunClientData.h"
#include "NodeV8.h"
#include "MessagePort.h"
#include "SerializedScriptValue.h"
#include "ZigGlobalObject.h"

Expand Down Expand Up @@ -46,16 +45,15 @@ JSC_DEFINE_HOST_FUNCTION(functionSerializeForNode, (JSGlobalObject * globalObjec
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* domGlobal = uncheckedDowncast<WebCore::JSDOMGlobalObject>(globalObject);
Vector<Strong<JSObject>> transfers;
Vector<RefPtr<WebCore::MessagePort>> ports;
auto result = WebCore::SerializedScriptValue::create(*domGlobal, callFrame->argument(0), WTF::move(transfers), ports, WebCore::SerializationForStorage::Yes);
bool hasBufferEnvelope = false;
auto result = WebCore::SerializedScriptValue::createForNode(*domGlobal, callFrame->argument(0), WebCore::SerializationForStorage::Yes, WebCore::SerializationForCrossProcessTransfer::No, hasBufferEnvelope);
EXCEPTION_ASSERT(result.hasException() == !!scope.exception());
if (result.hasException()) {
WebCore::propagateException(*domGlobal, scope, result.releaseException());
RELEASE_AND_RETURN(scope, {});
}
auto serialized = result.releaseReturnValue();
auto prefix = callFrame->argument(1).asBoolean() ? std::span<const uint8_t>(bufferEnvelopeMagic) : std::span<const uint8_t>();
auto prefix = hasBufferEnvelope ? std::span<const uint8_t>(bufferEnvelopeMagic) : std::span<const uint8_t>();
auto buffer = serialized->toArrayBuffer(ArrayBufferSharingMode::Default, prefix);
size_t length = buffer->byteLength();
RELEASE_AND_RETURN(scope, JSValue::encode(JSUint8Array::create(globalObject, domGlobal->JSBufferSubclassStructure(), WTF::move(buffer), 0, length)));
Expand Down Expand Up @@ -204,7 +202,7 @@ JSC::JSObject* createNodeV8Binding(JSC::JSGlobalObject* globalObject)
RETURN_IF_EXCEPTION(scope, nullptr);
}
object->putDirect(vm, Identifier::fromString(vm, "bufferEnvelopeMagic"_s), magic);
object->putDirectNativeFunction(vm, globalObject, Identifier::fromString(vm, "serialize"_s), 2, functionSerializeForNode, ImplementationVisibility::Public, NoIntrinsic, 0);
object->putDirectNativeFunction(vm, globalObject, Identifier::fromString(vm, "serialize"_s), 1, functionSerializeForNode, ImplementationVisibility::Public, NoIntrinsic, 0);
object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "queryObjects"_s), 1, functionQueryObjects, ImplementationVisibility::Public, JSC::NoIntrinsic, 0);
object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "getHeapUsage"_s), 0, functionGetHeapUsage, ImplementationVisibility::Public, JSC::NoIntrinsic, 0);
object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "isStringOneByteRepresentation"_s), 1, functionIsStringOneByteRepresentation, ImplementationVisibility::Public, JSC::NoIntrinsic, 0);
Expand Down
12 changes: 10 additions & 2 deletions src/jsc/bindings/Serialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ struct SerializedValueSlice {
const uint8_t* bytes;
size_t size;
WebCore::SerializedScriptValue* value; // NOLINT
bool hasBufferEnvelope;
};

enum class SerializedFlags : uint8_t {
None = 0,
ForCrossProcessTransfer = 1 << 0,
ForStorage = 1 << 1,
PreserveNodeBuffers = 1 << 2,
OmitErrorCause = 1 << 3,
};

/// Returns a "slice" that also contains a pointer to the SerializedScriptValue. Must be freed by the caller
Expand All @@ -31,9 +34,13 @@ extern "C" SerializedValueSlice Bun__serializeJSValue(JSGlobalObject* globalObje
Vector<JSC::Strong<JSC::JSObject>> transferList;
Vector<RefPtr<MessagePort>> dummyPorts;
auto forStorage = (static_cast<uint8_t>(flags) & static_cast<uint8_t>(SerializedFlags::ForStorage)) ? SerializationForStorage::Yes : SerializationForStorage::No;
auto context = SerializationContext::Default;
auto context = (static_cast<uint8_t>(flags) & static_cast<uint8_t>(SerializedFlags::OmitErrorCause)) ? SerializationContext::LegacyIPC : SerializationContext::Default;
auto forTransferEnum = (static_cast<uint8_t>(flags) & static_cast<uint8_t>(SerializedFlags::ForCrossProcessTransfer)) ? SerializationForCrossProcessTransfer::Yes : SerializationForCrossProcessTransfer::No;
ExceptionOr<Ref<SerializedScriptValue>> serialized = SerializedScriptValue::create(*globalObject, value, WTF::move(transferList), dummyPorts, forStorage, context, forTransferEnum);
bool hasBufferEnvelope = false;
bool preserveNodeBuffers = static_cast<uint8_t>(flags) & static_cast<uint8_t>(SerializedFlags::PreserveNodeBuffers);
ExceptionOr<Ref<SerializedScriptValue>> serialized = preserveNodeBuffers
? SerializedScriptValue::createForNode(*globalObject, value, forStorage, forTransferEnum, hasBufferEnvelope, context)
: SerializedScriptValue::create(*globalObject, value, WTF::move(transferList), dummyPorts, forStorage, context, forTransferEnum);

EXCEPTION_ASSERT(!!scope.exception() == serialized.hasException());
if (serialized.hasException()) {
Expand All @@ -49,6 +56,7 @@ extern "C" SerializedValueSlice Bun__serializeJSValue(JSGlobalObject* globalObje
bytes.begin(),
bytes.size(),
&serializedValue.leakRef(),
hasBufferEnvelope,
};
}

Expand Down
Loading
Loading