diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 78cc588ad3..b0fef6e230 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -1098,10 +1098,10 @@ const ReqOrSaved = union(enum) { }; } - pub fn url(this: *const @This(), alloc: Allocator) []const u8 { + pub fn url(this: *const @This(), alloc: Allocator) bun.ZigString.Slice { return switch (this.*) { - .req => |req| req.url(), - .saved => |saved| saved.request.url.toUTF8(alloc).slice(), + .req => |req| bun.ZigString.Slice.fromUTF8NeverFree(req.url()), + .saved => |saved| saved.request.url.toUTF8(alloc), }; } }; @@ -1120,7 +1120,6 @@ fn deferRequest( deferred.data = .{ .route_bundle_index = route_bundle_index, .dev = dev, - .ref_count = .init(), .handler = switch (kind) { .bundled_html_page => brk: { resp.onAborted(*DeferredRequest, DeferredRequest.onAbort, &deferred.data); @@ -1134,14 +1133,28 @@ fn deferRequest( }, .saved => |saved| saved, }; - server_handler.ctx.setAdditionalOnAbortCallback(.{ .cb = DeferredRequest.onAbortWrapper, .data = &deferred.data }); + server_handler.ctx.ref(); + server_handler.ctx.setAdditionalOnAbortCallback(.{ + .cb = DeferredRequest.onAbortWrapper, + .data = &deferred.data, + .deref_fn = struct { + fn deref_fn(ptr: *anyopaque) void { + var self: *DeferredRequest = @ptrCast(@alignCast(ptr)); + self.weakDeref(); + } + }.deref_fn, + }); break :brk .{ .server_handler = server_handler, }; }, }, }; - deferred.data.ref(); + + if (deferred.data.handler == .server_handler) { + deferred.data.weakRef(); + } + requests_array.prepend(deferred); } @@ -1589,24 +1602,45 @@ pub const DeferredRequest = struct { pub const List = std.SinglyLinkedList(DeferredRequest); pub const Node = List.Node; - const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinitImpl, .{ - .debug_name = "DeferredRequest", - }); - const debugLog = bun.Output.Scoped("DlogeferredRequest", .hidden).log; route_bundle_index: RouteBundle.Index, handler: Handler, dev: *DevServer, - /// This struct can have at most 2 references it: - /// - The dev server (`dev.current_bundle.requests`) - /// - uws.Response as a user data pointer - ref_count: RefCount, + /// This struct can be referenced by the dev server (`dev.current_bundle.requests`) + /// + /// Simultaneously, RequestContext may refer to it for AdditionalOnAbortCallback, + /// but we treat this as a weak reference, otherwise we will have reference + /// count cycles as this struct itself may store a reference to + /// RequestContext in the `server_handler` variant of `Handler` + referenced_by_devserver: bool = true, + weakly_referenced_by_requestcontext: bool = false, - // expose `ref` and `deref` as public methods - pub const ref = RefCount.ref; - pub const deref = RefCount.deref; + pub fn isAlive(this: *DeferredRequest) bool { + return this.referenced_by_devserver; + } + + pub fn deref(this: *DeferredRequest) void { + this.referenced_by_devserver = false; + const should_free = !this.weakly_referenced_by_requestcontext; + this.__deinit(); + if (should_free) { + this.__free(); + } + } + + pub fn weakRef(this: *DeferredRequest) void { + bun.assert(!this.weakly_referenced_by_requestcontext); + this.weakly_referenced_by_requestcontext = true; + } + + pub fn weakDeref(this: *DeferredRequest) void { + this.weakly_referenced_by_requestcontext = false; + if (!this.referenced_by_devserver) { + this.__free(); + } + } const Handler = union(enum) { /// For a .framework route. This says to call and render the page. @@ -1628,6 +1662,7 @@ pub const DeferredRequest = struct { fn onAbortWrapper(this: *anyopaque) void { const self: *DeferredRequest = @alignCast(@ptrCast(this)); + if (!self.isAlive()) return; self.onAbortImpl(); } @@ -1642,16 +1677,19 @@ pub const DeferredRequest = struct { assert(this.handler == .aborted); } + /// Actually free the underlying allocation for the node, does not deinitialize children + fn __free(this: *DeferredRequest) void { + this.dev.deferred_request_pool.put(@fieldParentPtr("data", this)); + } + /// *WARNING*: Do not call this directly, instead call `.deref()` /// /// Calling this is only required if the desired handler is going to be avoided, /// such as for bundling failures or aborting the server. /// Does not free the underlying `DeferredRequest.Node` - fn deinitImpl(this: *DeferredRequest) void { + fn __deinit(this: *DeferredRequest) void { debugLog("DeferredRequest(0x{x}) deinitImpl", .{@intFromPtr(this)}); - this.ref_count.assertNoRefs(); - defer this.dev.deferred_request_pool.put(@fieldParentPtr("data", this)); switch (this.handler) { .server_handler => |*saved| saved.deinit(), .bundled_html_page, .aborted => {}, diff --git a/src/bun.js.zig b/src/bun.js.zig index 437c653c38..13ca71282d 100644 --- a/src/bun.js.zig +++ b/src/bun.js.zig @@ -465,6 +465,7 @@ pub const Run = struct { } bun.api.napi.fixDeadCodeElimination(); + bun.webcore.BakeResponse.fixDeadCodeElimination(); bun.crash_handler.fixDeadCodeElimination(); @import("./bun.js/bindings/JSSecrets.zig").fixDeadCodeElimination(); vm.globalExit(); diff --git a/src/bun.js/api/server/AnyRequestContext.zig b/src/bun.js/api/server/AnyRequestContext.zig index 4818bf30d6..506b8af0db 100644 --- a/src/bun.js/api/server/AnyRequestContext.zig +++ b/src/bun.js/api/server/AnyRequestContext.zig @@ -243,6 +243,28 @@ pub fn onAbort(self: AnyRequestContext, response: uws.AnyResponse) void { } } +pub fn ref(self: AnyRequestContext) void { + if (self.tagged_pointer.isNull()) { + return; + } + + switch (self.tagged_pointer.tag()) { + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => { + self.tagged_pointer.as(HTTPServer.RequestContext).ref(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => { + self.tagged_pointer.as(HTTPSServer.RequestContext).ref(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => { + self.tagged_pointer.as(DebugHTTPServer.RequestContext).ref(); + }, + @field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => { + self.tagged_pointer.as(DebugHTTPSServer.RequestContext).ref(); + }, + else => @panic("Unexpected AnyRequestContext tag"), + } +} + pub fn deref(self: AnyRequestContext) void { if (self.tagged_pointer.isNull()) { return; diff --git a/src/bun.js/api/server/RequestContext.zig b/src/bun.js/api/server/RequestContext.zig index 8929fbbaca..ab06fd80a8 100644 --- a/src/bun.js/api/server/RequestContext.zig +++ b/src/bun.js/api/server/RequestContext.zig @@ -11,6 +11,11 @@ pub const AdditionalOnAbortCallback = struct { cb: *const fn (this: *anyopaque) void, data: *anyopaque, + deref_fn: *const fn (this: *anyopaque) void, + + pub fn deref(this: AdditionalOnAbortCallback) void { + this.deref_fn(this.data); + } }; pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comptime ThisServer: type) type { @@ -258,6 +263,11 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, this.request_body = null; } + if (this.additional_on_abort) |cb| { + cb.deref(); + this.additional_on_abort = null; + } + if (this.server) |server| { this.server = null; server.request_pool_allocator.put(this); @@ -266,7 +276,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, } pub fn deref(this: *RequestContext) void { - streamLog("deref", .{}); + streamLog("deref {d} -> {d}", .{ this.ref_count, this.ref_count - 1 }); assert(this.ref_count > 0); const ref_count = this.ref_count; this.ref_count -= 1; @@ -277,7 +287,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, } pub fn ref(this: *RequestContext) void { - streamLog("ref", .{}); + streamLog("ref {d} -> {d}", .{ this.ref_count, this.ref_count + 1 }); this.ref_count += 1; } @@ -599,6 +609,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, // mark request as aborted this.flags.aborted = true; if (this.additional_on_abort) |abort| { + defer abort.deref(); this.additional_on_abort = null; abort.cb(abort.data); } diff --git a/src/bun.js/bindings/JSBakeResponse.cpp b/src/bun.js/bindings/JSBakeResponse.cpp index 50c5737744..51248d9d77 100644 --- a/src/bun.js/bindings/JSBakeResponse.cpp +++ b/src/bun.js/bindings/JSBakeResponse.cpp @@ -33,10 +33,10 @@ static JSC_DECLARE_CUSTOM_GETTER(jsBakeResponsePrototypeGetDebugInfo); static JSC_DECLARE_CUSTOM_GETTER(jsBakeResponsePrototypeGetDebugStack); static JSC_DECLARE_CUSTOM_GETTER(jsBakeResponsePrototypeGetDebugTask); -extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES ResponseClass__constructForSSR(JSC::JSGlobalObject*, JSC::CallFrame*, int*); +extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructForSSR(JSC::JSGlobalObject*, JSC::CallFrame*, int*); extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ResponseClass__constructError(JSC::JSGlobalObject*, JSC::CallFrame*); extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ResponseClass__constructJSON(JSC::JSGlobalObject*, JSC::CallFrame*); -extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ResponseClass__constructRender(JSC::JSGlobalObject*, JSC::CallFrame*); +extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructRender(JSC::JSGlobalObject*, JSC::CallFrame*); extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ResponseClass__constructRedirect(JSC::JSGlobalObject*, JSC::CallFrame*); extern JSC_CALLCONV size_t Response__estimatedSize(void* ptr); @@ -76,7 +76,7 @@ extern "C" bool JSC__JSValue__isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JS return isJSXElement(JSValue0, globalObject); } -extern JSC_CALLCONV JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES Response__createForSSR(Zig::GlobalObject* globalObject, void* ptr, uint8_t kind) +extern JSC_CALLCONV JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES BakeResponse__createForSSR(Zig::GlobalObject* globalObject, void* ptr, uint8_t kind) { Structure* structure = globalObject->bakeAdditions().JSBakeResponseStructure(globalObject); @@ -102,7 +102,7 @@ static const HashTableValue JSBakeResponseConstructorTableValues[] = { { "json"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ResponseClass__constructJSON, 0 } }, { "redirect"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ResponseClass__constructRedirect, 0 } }, - { "render"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ResponseClass__constructRender, 0 } } + { "render"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, BakeResponseClass__constructRender, 0 } } }; @@ -213,7 +213,7 @@ public: JSBakeResponse* instance = JSBakeResponse::create(vm, globalObject, structure, nullptr); int arg_was_jsx = 0; - void* ptr = ResponseClass__constructForSSR(globalObject, callFrame, &arg_was_jsx); + void* ptr = BakeResponseClass__constructForSSR(globalObject, callFrame, &arg_was_jsx); if (scope.exception()) [[unlikely]] { ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new Response() allocated memory without checking for exceptions."); return JSValue::encode(JSC::jsUndefined()); @@ -243,7 +243,7 @@ public: Structure* structure = globalObject->bakeAdditions().JSBakeResponseStructure(globalObject); JSBakeResponse* instance = JSBakeResponse::create(vm, globalObject, structure, nullptr); - void* ptr = ResponseClass__constructForSSR(globalObject, callFrame, nullptr); + void* ptr = BakeResponseClass__constructForSSR(globalObject, callFrame, nullptr); if (scope.exception()) [[unlikely]] { ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new Response() allocated memory without checking for exceptions."); return JSValue::encode(JSC::jsUndefined()); diff --git a/src/bun.js/webcore.zig b/src/bun.js/webcore.zig index 9fcfebfc29..130f7a784d 100644 --- a/src/bun.js/webcore.zig +++ b/src/bun.js/webcore.zig @@ -19,6 +19,7 @@ pub const AutoFlusher = @import("./webcore/AutoFlusher.zig"); pub const EncodingLabel = @import("./webcore/EncodingLabel.zig").EncodingLabel; pub const Fetch = @import("./webcore/fetch.zig"); pub const Response = @import("./webcore/Response.zig"); +pub const BakeResponse = @import("./webcore/BakeResponse.zig"); pub const TextDecoder = @import("./webcore/TextDecoder.zig"); pub const TextEncoder = @import("./webcore/TextEncoder.zig"); pub const TextEncoderStreamEncoder = @import("./webcore/TextEncoderStreamEncoder.zig"); diff --git a/src/bun.js/webcore/BakeResponse.zig b/src/bun.js/webcore/BakeResponse.zig new file mode 100644 index 0000000000..1ffacce445 --- /dev/null +++ b/src/bun.js/webcore/BakeResponse.zig @@ -0,0 +1,143 @@ +pub fn fixDeadCodeElimination() void { + std.mem.doNotOptimizeAway(&BakeResponseClass__constructForSSR); + std.mem.doNotOptimizeAway(&BakeResponseClass__constructRender); +} + +extern "C" fn BakeResponse__createForSSR(globalObject: *JSGlobalObject, this: *Response, kind: u8) callconv(jsc.conv) jsc.JSValue; + +/// Corresponds to `JSBakeResponseKind` in +/// `src/bun.js/bindings/JSBakeResponse.h` +const SSRKind = enum(u8) { + regular = 0, + redirect = 1, + render = 2, +}; + +pub fn toJSForSSR(this: *Response, globalObject: *JSGlobalObject, kind: SSRKind) JSValue { + this.calculateEstimatedByteSize(); + return BakeResponse__createForSSR(globalObject, this, @intFromEnum(kind)); +} + +pub export fn BakeResponseClass__constructForSSR(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame, bake_ssr_has_jsx: ?*c_int) callconv(jsc.conv) ?*anyopaque { + return @as(*Response, constructor(globalObject, callFrame, bake_ssr_has_jsx) catch |err| switch (err) { + error.JSError => return null, + error.OutOfMemory => { + globalObject.throwOutOfMemory() catch {}; + return null; + }, + }); +} + +pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, bake_ssr_has_jsx: ?*c_int) bun.JSError!*Response { + var arguments = callframe.argumentsAsArray(2); + + // Allow `return new Response( ... , { ... }` + // inside of a react component + if (!arguments[0].isUndefinedOrNull() and arguments[0].isObject()) { + bake_ssr_has_jsx.?.* = 0; + if (try arguments[0].isJSXElement(globalThis)) { + const vm = globalThis.bunVM(); + if (vm.getDevServerAsyncLocalStorage()) |async_local_storage| { + try assertStreamingDisabled(globalThis, async_local_storage, "new Response(, { ... })"); + } + bake_ssr_has_jsx.?.* = 1; + } + } + + return Response.constructor(globalThis, callframe); +} + +pub fn constructRedirect( + globalThis: *jsc.JSGlobalObject, + callframe: *jsc.CallFrame, +) bun.JSError!JSValue { + const response = try Response.constructRedirectImpl(globalThis, callframe); + const ptr = bun.new(Response, response); + + const vm = globalThis.bunVM(); + // Check if dev_server_async_local_storage is set (indicating we're in Bun dev server) + if (vm.getDevServerAsyncLocalStorage()) |async_local_storage| { + try assertStreamingDisabled(globalThis, async_local_storage, "Response.redirect"); + return toJSForSSR(ptr, globalThis, .redirect); + } + + return ptr.toJS(globalThis); +} + +pub export fn BakeResponseClass__constructRender(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue { + return @call(.always_inline, jsc.toJSHostFn(constructRender), .{ globalObject, callFrame }); +} + +/// This function is only available on JSBakeResponse +pub fn constructRender( + globalThis: *jsc.JSGlobalObject, + callframe: *jsc.CallFrame, +) bun.JSError!JSValue { + const arguments = callframe.argumentsAsArray(2); + const vm = globalThis.bunVM(); + + // Check if dev server async local_storage is set + const async_local_storage = vm.getDevServerAsyncLocalStorage() orelse { + return globalThis.throwInvalidArguments("Response.render() is only available in the Bun dev server", .{}); + }; + + try assertStreamingDisabled(globalThis, async_local_storage, "Response.render"); + + // Validate arguments + if (arguments.len < 1) { + return globalThis.throwInvalidArguments("Response.render() requires at least a path argument", .{}); + } + + const path_arg = arguments[0]; + if (!path_arg.isString()) { + return globalThis.throwInvalidArguments("Response.render() path must be a string", .{}); + } + + // Get the path string + const path_str = try path_arg.toSlice(globalThis, bun.default_allocator); + + // Duplicate the path string so it persists + const path_copy = bun.default_allocator.dupe(u8, path_str.slice()) catch { + path_str.deinit(); + return globalThis.throwOutOfMemory(); + }; + path_str.deinit(); + + // Create a Response with Render body + const response = bun.new(Response, Response{ + .body = Body{ + .value = .{ + .Render = .{ + .path = bun.ptr.Owned([]const u8).fromRaw(path_copy), + }, + }, + }, + .init = Response.Init{ + .status_code = 200, + }, + }); + + const response_js = toJSForSSR(response, globalThis, .render); + response_js.ensureStillAlive(); + + return response_js; +} + +fn assertStreamingDisabled(globalThis: *jsc.JSGlobalObject, async_local_storage: JSValue, display_function: []const u8) bun.JSError!void { + if (async_local_storage.isEmptyOrUndefinedOrNull() or !async_local_storage.isObject()) return globalThis.throwInvalidArguments("store value must be an object", .{}); + const getStoreFn = (try async_local_storage.getPropertyValue(globalThis, "getStore")) orelse return globalThis.throwInvalidArguments("store value must have a \"getStore\" field", .{}); + const store_value = try getStoreFn.call(globalThis, async_local_storage, &.{}); + const streaming_val = (try store_value.getPropertyValue(globalThis, "streaming")) orelse return globalThis.throwInvalidArguments("store value must have a \"streaming\" field", .{}); + if (!streaming_val.isBoolean()) return globalThis.throwInvalidArguments("\"streaming\" fied must be a boolean", .{}); + if (streaming_val.asBoolean()) return globalThis.throwInvalidArguments("\"{s}\" is not available when `export const streaming = true`", .{display_function}); +} + +const bun = @import("bun"); +const std = @import("std"); + +const jsc = bun.jsc; +const JSGlobalObject = jsc.JSGlobalObject; +const JSValue = jsc.JSValue; + +const Body = bun.webcore.Body; +const Response = bun.webcore.Response; diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index c28ce904fa..53bd35d647 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -66,21 +66,6 @@ pub fn toJS(this: *Response, globalObject: *JSGlobalObject) JSValue { return js.toJSUnchecked(globalObject, this); } -/// Corresponds to `JSBakeResponseKind` in -/// `src/bun.js/bindings/JSBakeResponse.h` -const SSRKind = enum(u8) { - regular = 0, - redirect = 1, - render = 2, -}; - -extern "C" fn Response__createForSSR(globalObject: *JSGlobalObject, this: *Response, kind: u8) callconv(jsc.conv) jsc.JSValue; - -pub fn toJSForSSR(this: *Response, globalObject: *JSGlobalObject, kind: SSRKind) JSValue { - this.calculateEstimatedByteSize(); - return Response__createForSSR(globalObject, this, @intFromEnum(kind)); -} - pub fn getBodyValue( this: *Response, ) *Body.Value { @@ -334,7 +319,6 @@ pub fn cloneValue( } pub fn clone(this: *Response, globalThis: *JSGlobalObject) bun.JSError!*Response { - // TODO: handle clone for jsxElement for bake? return bun.new(Response, try this.cloneValue(globalThis)); } @@ -473,6 +457,16 @@ pub fn constructRedirect( globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, ) bun.JSError!JSValue { + const response = try constructRedirectImpl(globalThis, callframe); + const ptr = bun.new(Response, response); + const response_js = ptr.toJS(globalThis); + return response_js; +} + +pub fn constructRedirectImpl( + globalThis: *jsc.JSGlobalObject, + callframe: *jsc.CallFrame, +) bun.JSError!Response { var args_list = callframe.arguments_old(4); // https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4 var args = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), args_list.ptr[0..args_list.len]); @@ -519,7 +513,7 @@ pub fn constructRedirect( } if (globalThis.hasException()) { - return .zero; + return error.JSError; } did_succeed = true; break :brk response; @@ -528,77 +522,7 @@ pub fn constructRedirect( response.init.headers = try response.getOrCreateHeaders(globalThis); var headers_ref = response.init.headers.?; try headers_ref.put(.Location, url_string_slice.slice(), globalThis); - const ptr = bun.new(Response, response); - - const vm = globalThis.bunVM(); - // Check if dev_server_async_local_storage is set (indicating we're in Bun dev server) - if (vm.getDevServerAsyncLocalStorage()) |async_local_storage| { - try assertStreamingDisabled(globalThis, async_local_storage, "Response.redirect"); - return ptr.toJSForSSR(globalThis, .redirect); - } - - const response_js = ptr.toJS(globalThis); - - return response_js; -} - -pub export fn ResponseClass__constructRender(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue { - return @call(.always_inline, jsc.toJSHostFn(constructRender), .{ globalObject, callFrame }); -} - -/// This function is only available on JSBakeResponse -pub fn constructRender( - globalThis: *jsc.JSGlobalObject, - callframe: *jsc.CallFrame, -) bun.JSError!JSValue { - const arguments = callframe.argumentsAsArray(2); - const vm = globalThis.bunVM(); - - // Check if dev server async local_storage is set - const async_local_storage = vm.getDevServerAsyncLocalStorage() orelse { - return globalThis.throwInvalidArguments("Response.render() is only available in the Bun dev server", .{}); - }; - - try assertStreamingDisabled(globalThis, async_local_storage, "Response.render"); - - // Validate arguments - if (arguments.len < 1) { - return globalThis.throwInvalidArguments("Response.render() requires at least a path argument", .{}); - } - - const path_arg = arguments[0]; - if (!path_arg.isString()) { - return globalThis.throwInvalidArguments("Response.render() path must be a string", .{}); - } - - // Get the path string - const path_str = try path_arg.toSlice(globalThis, bun.default_allocator); - - // Duplicate the path string so it persists - const path_copy = bun.default_allocator.dupe(u8, path_str.slice()) catch { - path_str.deinit(); - return globalThis.throwOutOfMemory(); - }; - path_str.deinit(); - - // Create a Response with Render body - var response = bun.new(Response, Response{ - .body = Body{ - .value = .{ - .Render = .{ - .path = bun.ptr.Owned([]const u8).fromRaw(path_copy), - }, - }, - }, - .init = Response.Init{ - .status_code = 200, - }, - }); - - const response_js = response.toJSForSSR(globalThis, .render); - response_js.ensureStillAlive(); - - return response_js; + return response; } pub fn constructError( @@ -621,28 +545,6 @@ pub fn constructError( } pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!*Response { - return constructorImpl(globalThis, callframe, null); -} - -pub fn ResponseClass__constructForSSR(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame, bake_ssr_has_jsx: ?*c_int) callconv(jsc.conv) ?*anyopaque { - return @as(*Response, Response.constructorForSSR(globalObject, callFrame, bake_ssr_has_jsx) catch |err| switch (err) { - error.JSError => return null, - error.OutOfMemory => { - globalObject.throwOutOfMemory() catch {}; - return null; - }, - }); -} - -comptime { - @export(&ResponseClass__constructForSSR, .{ .name = "ResponseClass__constructForSSR" }); -} - -pub fn constructorForSSR(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, bake_ssr_has_jsx: ?*c_int) bun.JSError!*Response { - return constructorImpl(globalThis, callframe, bake_ssr_has_jsx); -} - -pub fn constructorImpl(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, bake_ssr_has_jsx: ?*c_int) bun.JSError!*Response { var arguments = callframe.argumentsAsArray(2); if (!arguments[0].isUndefinedOrNull() and arguments[0].isObject()) { @@ -677,19 +579,6 @@ pub fn constructorImpl(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFram return bun.new(Response, response); } } - - // Special case for bake: allow `return new Response( ... , { ... }` - // inside of a react component - if (bake_ssr_has_jsx != null) { - bake_ssr_has_jsx.?.* = 0; - if (try arguments[0].isJSXElement(globalThis)) { - const vm = globalThis.bunVM(); - if (vm.getDevServerAsyncLocalStorage()) |async_local_storage| { - try assertStreamingDisabled(globalThis, async_local_storage, "new Response(, { ... })"); - } - bake_ssr_has_jsx.?.* = 1; - } - } } var init: Init = (brk: { if (arguments[1].isUndefinedOrNull()) { @@ -872,15 +761,6 @@ inline fn emptyWithStatus(_: *jsc.JSGlobalObject, status: u16) Response { }); } -fn assertStreamingDisabled(globalThis: *jsc.JSGlobalObject, async_local_storage: JSValue, display_function: []const u8) bun.JSError!void { - if (async_local_storage.isEmptyOrUndefinedOrNull() or !async_local_storage.isObject()) return globalThis.throwInvalidArguments("store value must be an object", .{}); - const getStoreFn = (try async_local_storage.getPropertyValue(globalThis, "getStore")) orelse return globalThis.throwInvalidArguments("store value must have a \"getStore\" field", .{}); - const store_value = try getStoreFn.call(globalThis, async_local_storage, &.{}); - const streaming_val = (try store_value.getPropertyValue(globalThis, "streaming")) orelse return globalThis.throwInvalidArguments("store value must have a \"streaming\" field", .{}); - if (!streaming_val.isBoolean()) return globalThis.throwInvalidArguments("\"streaming\" fied must be a boolean", .{}); - if (streaming_val.asBoolean()) return globalThis.throwInvalidArguments("\"{s}\" is not available when `export const streaming = true`", .{display_function}); -} - /// https://developer.mozilla.org/en-US/docs/Web/API/Headers // TODO: move to http.zig. this has nothing to do with jsc or WebCore