diff --git a/src/bun.js/bindings/SnapshotSerializers.cpp b/src/bun.js/bindings/SnapshotSerializers.cpp new file mode 100644 index 0000000000..84445bb760 --- /dev/null +++ b/src/bun.js/bindings/SnapshotSerializers.cpp @@ -0,0 +1,227 @@ +#include "root.h" +#include "SnapshotSerializers.h" + +#include +#include +#include +#include +#include +#include "ErrorCode.h" + +namespace Bun { + +using namespace JSC; + +const ClassInfo SnapshotSerializers::s_info = { "SnapshotSerializers"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(SnapshotSerializers) }; + +SnapshotSerializers::SnapshotSerializers(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void SnapshotSerializers::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + + // Initialize empty arrays + m_testCallbacks.set(vm, this, JSC::constructEmptyArray(this->globalObject(), nullptr, 0)); + m_serializeCallbacks.set(vm, this, JSC::constructEmptyArray(this->globalObject(), nullptr, 0)); +} + +template +void SnapshotSerializers::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + SnapshotSerializers* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + + visitor.append(thisObject->m_testCallbacks); + visitor.append(thisObject->m_serializeCallbacks); +} + +DEFINE_VISIT_CHILDREN(SnapshotSerializers); + +SnapshotSerializers* SnapshotSerializers::create(VM& vm, Structure* structure) +{ + SnapshotSerializers* serializers = new (NotNull, allocateCell(vm)) SnapshotSerializers(vm, structure); + serializers->finishCreation(vm); + return serializers; +} + +bool SnapshotSerializers::addSerializer(JSGlobalObject* globalObject, JSValue testCallback, JSValue serializeCallback) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Check for re-entrancy + if (m_isExecuting) { + throwTypeError(globalObject, scope, "Cannot add snapshot serializer from within a test or serialize callback"_s); + return false; + } + + // Validate that both callbacks are callable + if (!testCallback.isCallable()) { + throwTypeError(globalObject, scope, "Snapshot serializer test callback must be a function"_s); + return false; + } + + if (!serializeCallback.isCallable()) { + throwTypeError(globalObject, scope, "Snapshot serializer serialize callback must be a function"_s); + return false; + } + + // Get the arrays + JSArray* testCallbacks = m_testCallbacks.get(); + JSArray* serializeCallbacks = m_serializeCallbacks.get(); + + if (!testCallbacks || !serializeCallbacks) { + throwOutOfMemoryError(globalObject, scope); + return false; + } + + // Add to the end of the arrays (most recent last, we'll iterate in reverse) + testCallbacks->push(globalObject, testCallback); + RETURN_IF_EXCEPTION(scope, false); + + serializeCallbacks->push(globalObject, serializeCallback); + RETURN_IF_EXCEPTION(scope, false); + + return true; +} + +JSValue SnapshotSerializers::serialize(JSGlobalObject* globalObject, JSValue value) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Check for re-entrancy + if (m_isExecuting) { + throwTypeError(globalObject, scope, "Cannot serialize from within a test or serialize callback"_s); + return jsNull(); + } + + // RAII guard to manage m_isExecuting flag + class ExecutionGuard { + public: + ExecutionGuard(bool& flag) : m_flag(flag) { m_flag = true; } + ~ExecutionGuard() { m_flag = false; } + private: + bool& m_flag; + }; + ExecutionGuard guard(m_isExecuting); + + JSArray* testCallbacks = m_testCallbacks.get(); + JSArray* serializeCallbacks = m_serializeCallbacks.get(); + + if (!testCallbacks || !serializeCallbacks) { + return jsNull(); + } + + unsigned length = testCallbacks->length(); + + // Iterate through serializers in reverse order (most recent to least recent) + for (int i = static_cast(length) - 1; i >= 0; i--) { + JSValue testCallback = testCallbacks->getIndex(globalObject, static_cast(i)); + RETURN_IF_EXCEPTION(scope, {}); + + if (!testCallback.isCallable()) { + continue; + } + + // Call the test function with the value + auto callData = JSC::getCallData(testCallback); + MarkedArgumentBuffer args; + args.append(value); + ASSERT(!args.hasOverflowed()); + + JSValue testResult = call(globalObject, testCallback, callData, jsUndefined(), args); + RETURN_IF_EXCEPTION(scope, {}); + + // If the test returns truthy, use this serializer + if (testResult.toBoolean(globalObject)) { + RETURN_IF_EXCEPTION(scope, {}); + + JSValue serializeCallback = serializeCallbacks->getIndex(globalObject, static_cast(i)); + RETURN_IF_EXCEPTION(scope, {}); + + if (!serializeCallback.isCallable()) { + continue; + } + + // Call the serialize function with the value + auto serializeCallData = JSC::getCallData(serializeCallback); + MarkedArgumentBuffer serializeArgs; + serializeArgs.append(value); + ASSERT(!serializeArgs.hasOverflowed()); + + JSValue result = call(globalObject, serializeCallback, serializeCallData, jsUndefined(), serializeArgs); + RETURN_IF_EXCEPTION(scope, {}); + + // Return the serialized result (should be a string or null) + RELEASE_AND_RETURN(scope, result); + } + } + + // No matching serializer found + return jsNull(); +} + +} // namespace Bun + +using namespace Bun; +using namespace JSC; + +// Zig-exported functions + +extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue SnapshotSerializers__add( + Zig::GlobalObject* globalObject, + JSC::EncodedJSValue encodedSerializers, + JSC::EncodedJSValue encodedTestCallback, + JSC::EncodedJSValue encodedSerializeCallback) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue serializersValue = JSValue::decode(encodedSerializers); + SnapshotSerializers* serializers = jsDynamicCast(serializersValue); + + if (!serializers) { + throwTypeError(globalObject, scope, "Invalid SnapshotSerializers object"_s); + return JSValue::encode(jsUndefined()); + } + + JSValue testCallback = JSValue::decode(encodedTestCallback); + JSValue serializeCallback = JSValue::decode(encodedSerializeCallback); + + bool success = serializers->addSerializer(globalObject, testCallback, serializeCallback); + RETURN_IF_EXCEPTION(scope, {}); + + if (success) { + RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined())); + } + + return JSValue::encode(jsUndefined()); +} + +extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue SnapshotSerializers__serialize( + Zig::GlobalObject* globalObject, + JSC::EncodedJSValue encodedSerializers, + JSC::EncodedJSValue encodedValue) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue serializersValue = JSValue::decode(encodedSerializers); + SnapshotSerializers* serializers = jsDynamicCast(serializersValue); + + if (!serializers) { + throwTypeError(globalObject, scope, "Invalid SnapshotSerializers object"_s); + return JSValue::encode(jsNull()); + } + + JSValue value = JSValue::decode(encodedValue); + JSValue result = serializers->serialize(globalObject, value); + RETURN_IF_EXCEPTION(scope, {}); + + RELEASE_AND_RETURN(scope, JSValue::encode(result)); +} diff --git a/src/bun.js/bindings/SnapshotSerializers.h b/src/bun.js/bindings/SnapshotSerializers.h new file mode 100644 index 0000000000..9a0fd0246f --- /dev/null +++ b/src/bun.js/bindings/SnapshotSerializers.h @@ -0,0 +1,76 @@ +#pragma once + +#include "root.h" +#include +#include +#include +#include "ZigGlobalObject.h" + +namespace Bun { + +class SnapshotSerializers final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + static SnapshotSerializers* create(JSC::VM& vm, JSC::Structure* structure); + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForSnapshotSerializers.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForSnapshotSerializers = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForSnapshotSerializers.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForSnapshotSerializers = std::forward(space); }); + } + + DECLARE_INFO; + DECLARE_VISIT_CHILDREN; + + // Add a new snapshot serializer + // Returns true on success, false if in re-entrant call (and throws) + bool addSerializer(JSC::JSGlobalObject* globalObject, JSC::JSValue testCallback, JSC::JSValue serializeCallback); + + // Test a value and serialize if a matching serializer is found + // Returns the serialized string or null + JSC::JSValue serialize(JSC::JSGlobalObject* globalObject, JSC::JSValue value); + +private: + SnapshotSerializers(JSC::VM& vm, JSC::Structure* structure); + + void finishCreation(JSC::VM& vm); + + // Arrays stored in reverse order (most recent first for iteration) + JSC::WriteBarrier m_testCallbacks; + JSC::WriteBarrier m_serializeCallbacks; + + // Re-entrancy guard + bool m_isExecuting { false }; +}; + +} // namespace Bun + +// Exposed to Zig +extern "C" { + +[[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue SnapshotSerializers__add( + Zig::GlobalObject* globalObject, + JSC::EncodedJSValue encodedSerializers, + JSC::EncodedJSValue encodedTestCallback, + JSC::EncodedJSValue encodedSerializeCallback); + +[[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue SnapshotSerializers__serialize( + Zig::GlobalObject* globalObject, + JSC::EncodedJSValue encodedSerializers, + JSC::EncodedJSValue encodedValue); + +} // extern "C" diff --git a/src/bun.js/bindings/SnapshotSerializersBindings.zig b/src/bun.js/bindings/SnapshotSerializersBindings.zig new file mode 100644 index 0000000000..9304cc2922 --- /dev/null +++ b/src/bun.js/bindings/SnapshotSerializersBindings.zig @@ -0,0 +1,62 @@ +// Example Zig bindings for SnapshotSerializers +// This shows how to use the exported C++ functions from Zig + +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; + +// Import the exported C++ functions +extern "c" fn SnapshotSerializers__add( + globalObject: *JSC.JSGlobalObject, + serializers: JSC.JSValue, + testCallback: JSC.JSValue, + serializeCallback: JSC.JSValue, +) JSC.JSValue; + +extern "c" fn SnapshotSerializers__serialize( + globalObject: *JSC.JSGlobalObject, + serializers: JSC.JSValue, + value: JSC.JSValue, +) JSC.JSValue; + +/// Add a snapshot serializer +pub fn addSerializer( + global: *JSC.JSGlobalObject, + serializers: JSC.JSValue, + test_callback: JSC.JSValue, + serialize_callback: JSC.JSValue, +) JSC.JSValue { + return SnapshotSerializers__add( + global, + serializers, + test_callback, + serialize_callback, + ); +} + +/// Serialize a value using the registered serializers +pub fn serialize( + global: *JSC.JSGlobalObject, + serializers: JSC.JSValue, + value: JSC.JSValue, +) JSC.JSValue { + return SnapshotSerializers__serialize( + global, + serializers, + value, + ); +} + +// Example usage: +// +// const serializers = SnapshotSerializers.create(vm, structure); +// +// // Add a serializer for custom objects +// const test_fn = JSFunction.create(...); // function that returns true for custom objects +// const serialize_fn = JSFunction.create(...); // function that serializes the object +// +// _ = addSerializer(global, serializers, test_fn, serialize_fn); +// +// // Use the serializer +// const custom_object = ...; +// const result = serialize(global, serializers, custom_object); diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index ce662148a5..eab601e826 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -47,6 +47,7 @@ public: std::unique_ptr m_clientSubspaceForJSMockFunction; std::unique_ptr m_clientSubspaceForAsyncContextFrame; std::unique_ptr m_clientSubspaceForMockWithImplementationCleanupData; + std::unique_ptr m_clientSubspaceForSnapshotSerializers; std::unique_ptr m_clientSubspaceForProcessObject; std::unique_ptr m_clientSubspaceForInternalModuleRegistry; std::unique_ptr m_clientSubspaceForErrorCodeCache; diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index cce908c751..99e3b9a017 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -47,6 +47,7 @@ public: std::unique_ptr m_subspaceForJSMockFunction; std::unique_ptr m_subspaceForAsyncContextFrame; std::unique_ptr m_subspaceForMockWithImplementationCleanupData; + std::unique_ptr m_subspaceForSnapshotSerializers; std::unique_ptr m_subspaceForProcessObject; std::unique_ptr m_subspaceForInternalModuleRegistry; std::unique_ptr m_subspaceForErrorCodeCache; diff --git a/src/bun.js/test/snapshot.zig b/src/bun.js/test/snapshot.zig index 11277e710e..5b2cf24d7e 100644 --- a/src/bun.js/test/snapshot.zig +++ b/src/bun.js/test/snapshot.zig @@ -17,6 +17,7 @@ pub const Snapshots = struct { snapshot_dir_path: ?string = null, inline_snapshots_to_write: *std.AutoArrayHashMap(TestRunner.File.ID, std.array_list.Managed(InlineSnapshotToWrite)), last_error_snapshot_name: ?[]const u8 = null, + serializers: jsc.Strong.Optional = .empty, pub const InlineSnapshotToWrite = struct { line: c_ulong,