diff --git a/src/bun.js/api/html_rewriter.zig b/src/bun.js/api/html_rewriter.zig index b928bb7e8d..f29c3508aa 100644 --- a/src/bun.js/api/html_rewriter.zig +++ b/src/bun.js/api/html_rewriter.zig @@ -197,7 +197,7 @@ pub const HTMLRewriter = struct { if (kind != .other) { { - const body_value = try jsc.WebCore.Body.extract(global, response_value); + const body_value = try jsc.WebCore.Body.extract(global, response_value, null); const resp = bun.new(Response, Response{ .init = .{ .status_code = 200, diff --git a/src/bun.js/webcore/BakeResponse.zig b/src/bun.js/webcore/BakeResponse.zig index 4c03c755c0..bc88feee79 100644 --- a/src/bun.js/webcore/BakeResponse.zig +++ b/src/bun.js/webcore/BakeResponse.zig @@ -44,7 +44,7 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, b } } - return Response.constructor(globalThis, callframe); + return Response.constructor(globalThis, callframe, .zero); } pub export fn BakeResponseClass__constructRedirect(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue { diff --git a/src/bun.js/webcore/Body.zig b/src/bun.js/webcore/Body.zig index ac9c0b00e8..a8cf436090 100644 --- a/src/bun.js/webcore/Body.zig +++ b/src/bun.js/webcore/Body.zig @@ -16,9 +16,9 @@ pub fn use(this: *Body) Blob { return this.value.use(); } -pub fn clone(this: *Body, globalThis: *JSGlobalObject) bun.JSError!Body { +pub fn clone(this: *Body, owner: jsc.WebCore.ReadableStream.Ref.Owner, globalThis: *JSGlobalObject, readable_stream_tee: ?*[2]jsc.JSValue) bun.JSError!Body { return Body{ - .value = try this.value.clone(globalThis), + .value = try this.value.clone(owner, globalThis, readable_stream_tee), }; } @@ -504,6 +504,10 @@ pub const Value = union(Tag) { } pub fn fromJS(globalThis: *JSGlobalObject, value: JSValue) bun.JSError!Value { + return fromJSWithReadableStreamValue(globalThis, value, null); + } + + pub fn fromJSWithReadableStreamValue(globalThis: *JSGlobalObject, value: JSValue, readable_stream_value: ?*JSValue) bun.JSError!Value { value.ensureStillAlive(); if (value.isEmptyOrUndefinedOrNull()) { @@ -598,6 +602,11 @@ pub const Value = union(Tag) { else => {}, } + if (readable_stream_value) |readable_stream_ptr| { + readable_stream_ptr.* = readable.value; + return .{ .Locked = .{ .global = globalThis, .readable = .empty } }; + } + return Body.Value.fromReadableStreamWithoutLockCheck(readable, globalThis); } @@ -941,23 +950,32 @@ pub const Value = union(Tag) { } } - pub fn tee(this: *Value, globalThis: *jsc.JSGlobalObject) bun.JSError!Value { + pub fn tee(this: *Value, owner: jsc.WebCore.ReadableStream.Ref.Owner, globalThis: *jsc.JSGlobalObject, readable_stream_tee: ?*[2]jsc.JSValue) bun.JSError!Value { var locked = &this.Locked; - if (locked.readable.isDisturbed(.strong, globalThis)) { - return Value{ .Used = {} }; + if (locked.readable.isDisturbed(owner, globalThis)) { + return .Used; } - if (try locked.readable.tee(.{ .strong = {} }, globalThis)) |readable| { - return Value{ + if (try locked.readable.tee(owner, globalThis, readable_stream_tee)) |result| { + if (readable_stream_tee != null) { + return .{ + .Locked = .{ + .global = globalThis, + }, + }; + } + + return .{ .Locked = .{ - .readable = .{ .strong = jsc.WebCore.ReadableStream.Strong.init(readable.@"0", globalThis) }, + .readable = .{ .strong = .init(result.@"1", globalThis) }, .global = globalThis, }, }; } - if (locked.promise != null or locked.action != .none or locked.readable.has(.strong, globalThis)) { - return Value{ .Used = {} }; + + if (locked.promise != null or locked.action != .none or locked.readable.has(owner, globalThis)) { + return .Used; } var drain_result: jsc.WebCore.DrainResult = .{ @@ -970,8 +988,8 @@ pub const Value = union(Tag) { } if (drain_result == .empty or drain_result == .aborted) { - this.* = .{ .Null = {} }; - return Value{ .Null = {} }; + this.* = .Null; + return .Null; } var reader = jsc.WebCore.ByteStream.Source.new(.{ @@ -994,28 +1012,35 @@ pub const Value = union(Tag) { .ptr = .{ .Bytes = &reader.context }, .value = stream_value, }; - locked.readable = .{ .strong = jsc.WebCore.ReadableStream.Strong.init(stream, globalThis) }; + locked.readable.set(.strong, stream, globalThis); if (locked.onReadableStreamAvailable) |onReadableStreamAvailable| { - onReadableStreamAvailable(locked.task.?, globalThis, locked.readable.get(.{ .strong = {} }, globalThis).?); + onReadableStreamAvailable(locked.task.?, globalThis, locked.readable.get(owner, globalThis).?); } - const first, const second = (try locked.readable.tee(.strong, globalThis)) orelse return Value{ .Used = {} }; - locked.readable = .{ .strong = .init(first, globalThis) }; + const tee_result = (try locked.readable.tee(owner, globalThis, readable_stream_tee)) orelse return Value{ .Used = {} }; + + if (readable_stream_tee != null) { + return .{ + .Locked = .{ + .global = globalThis, + }, + }; + } return Value{ .Locked = .{ - .readable = .{ .strong = .init(second, globalThis) }, + .readable = .{ .strong = .init(tee_result.@"1", globalThis) }, .global = globalThis, }, }; } - pub fn clone(this: *Value, globalThis: *jsc.JSGlobalObject) bun.JSError!Value { - this.toBlobIfPossible(.empty); + pub fn clone(this: *Value, owner: jsc.WebCore.ReadableStream.Ref.Owner, globalThis: *jsc.JSGlobalObject, readable_stream_tee: ?*[2]jsc.JSValue) bun.JSError!Value { + this.toBlobIfPossible(owner); if (this.* == .Locked) { - return this.tee(globalThis); + return try this.tee(owner, globalThis, readable_stream_tee); } if (this.* == .InternalBlob) { @@ -1054,10 +1079,11 @@ pub const Value = union(Tag) { pub fn extract( globalThis: *JSGlobalObject, value: JSValue, + readable_stream_value: ?*JSValue, ) bun.JSError!Body { var body = Body{ .value = Value{ .Null = {} } }; - body.value = try Value.fromJS(globalThis, value); + body.value = try Value.fromJSWithReadableStreamValue(globalThis, value, readable_stream_value); if (body.value == .Blob) { assert(!body.value.Blob.isHeapAllocated()); // owned by Body } @@ -1328,7 +1354,7 @@ pub const ValueBufferer = struct { js_sink: ?*ArrayBufferSink.JSSink = null, byte_stream: ?*jsc.WebCore.ByteStream = null, // readable stream strong ref to keep byte stream alive - readable_stream_ref: jsc.WebCore.ReadableStream.Ref = .{ .empty = {} }, + readable_stream_ref: jsc.WebCore.ReadableStream.Strong = .{}, stream_buffer: bun.MutableString, allocator: std.mem.Allocator, global: *JSGlobalObject, @@ -1564,14 +1590,15 @@ pub const ValueBufferer = struct { assert(value.* == .Locked); const locked = &value.Locked; if (locked.readable.get(.{ .empty = {} }, sink.global)) |stream| { - // keep the stream alive until we're done with it - sink.readable_stream_ref = locked.readable; - value.* = .{ .Used = {} }; - if (stream.isLocked(sink.global)) { return error.StreamAlreadyUsed; } + // keep the stream alive until we're done with it + sink.readable_stream_ref = .init(stream, sink.global); + value.deinit(); + value.* = .{ .Used = {} }; + switch (stream.ptr) { .Invalid => { return error.InvalidStream; diff --git a/src/bun.js/webcore/ReadableStream.zig b/src/bun.js/webcore/ReadableStream.zig index 94cb4c5720..0a4086b662 100644 --- a/src/bun.js/webcore/ReadableStream.zig +++ b/src/bun.js/webcore/ReadableStream.zig @@ -29,7 +29,7 @@ pub const Ref = union(Type) { .Response => { if (owner == .Response) { if (owner.Response.as(jsc.WebCore.Response)) |_| { - if (jsc.WebCore.Response.js.gc.body.get(owner.Response)) |body_value| { + if (Response.js.gc.body.get(owner.Response)) |body_value| { return ReadableStream.fromJS(body_value, global) catch null; } } @@ -38,7 +38,7 @@ pub const Ref = union(Type) { .Request => { if (owner == .Request) { if (owner.Request.as(jsc.WebCore.Request)) |_| { - if (jsc.WebCore.Request.js.gc.body.get(owner.Request)) |body_value| { + if (Request.js.gc.body.get(owner.Request)) |body_value| { return ReadableStream.fromJS(body_value, global) catch null; } } @@ -55,9 +55,50 @@ pub const Ref = union(Type) { return stream.isDisturbed(global); } - pub fn tee(this: *const Ref, owner: Owner, global: *jsc.JSGlobalObject) bun.JSError!?struct { ReadableStream, ReadableStream } { + pub fn setValue(this: *Ref, owner: Owner, stream_jsvalue: jsc.JSValue, global: *jsc.JSGlobalObject) void { + switch (owner) { + .Response => |jsvalue| { + if (jsvalue != .zero) { + Response.js.gc.body.set(jsvalue, global, stream_jsvalue); + this.deinit(); + this.* = .Response; + } else { + this.deinit(); + this.* = .empty; + } + }, + .Request => |jsvalue| { + if (jsvalue != .zero) { + Request.js.gc.body.set(jsvalue, global, stream_jsvalue); + this.deinit(); + this.* = .Request; + } else { + this.deinit(); + this.* = .empty; + } + }, + .strong => { + this.deinit(); + this.* = .{ .strong = .init(stream_jsvalue, global) }; + }, + .empty => {}, + } + } + + pub fn set(this: *Ref, owner: Owner, stream: ReadableStream, global: *jsc.JSGlobalObject) void { + this.setValue(owner, stream.value, global); + } + + pub fn tee(this: *Ref, owner: Owner, global: *jsc.JSGlobalObject, readable_stream_value: ?*[2]jsc.JSValue) bun.JSError!?struct { ReadableStream, ReadableStream } { const stream = get(this, owner, global) orelse return null; - return stream.tee(global) catch null; + + const result = try stream.tee(global) orelse return null; + if (readable_stream_value) |value| { + value.* = .{ result.@"0".value, result.@"1".value }; + } else { + this.set(owner, result.@"0", global); + } + return result; } pub fn has(this: *const Ref, owner: Owner, global: *jsc.JSGlobalObject) bool { @@ -77,16 +118,16 @@ pub const Ref = union(Type) { pub fn init(owner: Owner, global: *jsc.JSGlobalObject) Ref { switch (owner) { .Response => { - return .{ .Response = {} }; + return .Response; }, .Request => { - return .{ .Request = {} }; + return .Request; }, .strong => { return .{ .strong = .init(owner.strong, global) }; }, .empty => { - return .{ .empty = {} }; + return .empty; }, } } @@ -100,7 +141,7 @@ pub const Ref = union(Type) { .Request => {}, .empty => {}, } - this.* = .{ .empty = {} }; + this.* = .empty; } pub fn abort(this: *Ref, owner: Owner, global: *jsc.JSGlobalObject) bool { @@ -974,3 +1015,5 @@ const Blob = webcore.Blob; const streams = webcore.streams; const std = @import("std"); +const Request = jsc.WebCore.Request; +const Response = jsc.WebCore.Response; diff --git a/src/bun.js/webcore/Request.zig b/src/bun.js/webcore/Request.zig index 01b36116c2..8e6279b6d5 100644 --- a/src/bun.js/webcore/Request.zig +++ b/src/bun.js/webcore/Request.zig @@ -175,7 +175,8 @@ pub export fn Bun__JSRequest__calculateEstimatedByteSize(this: *Request) void { pub fn toJS(this: *Request, globalObject: *JSGlobalObject) JSValue { this.calculateEstimatedByteSize(); - return js.toJSUnchecked(globalObject, this); + const value = js.toJSUnchecked(globalObject, this); + return value; } extern "C" fn Bun__JSRequest__createForBake(globalObject: *jsc.JSGlobalObject, requestPtr: *Request) callconv(jsc.conv) jsc.JSValue; @@ -522,7 +523,7 @@ const Fields = enum { url, }; -pub fn constructInto(globalThis: *jsc.JSGlobalObject, arguments: []const jsc.JSValue) bun.JSError!Request { +pub fn constructInto(globalThis: *jsc.JSGlobalObject, arguments: []const jsc.JSValue, readable_stream_tee: ?*[2]jsc.JSValue) bun.JSError!Request { var success = false; const vm = globalThis.bunVM(); const body = try vm.initRequestBodyValue(.{ .Null = {} }); @@ -582,7 +583,7 @@ pub fn constructInto(globalThis: *jsc.JSGlobalObject, arguments: []const jsc.JSV if (value_type == .DOMWrapper) { if (value.asDirect(Request)) |request| { if (values_to_try.len == 1) { - try request.cloneInto(&req, bun.default_allocator, globalThis, fields.contains(.url)); + try request.cloneInto(&req, bun.default_allocator, globalThis, fields.contains(.url), value, readable_stream_tee); success = true; return req; } @@ -610,7 +611,7 @@ pub fn constructInto(globalThis: *jsc.JSGlobalObject, arguments: []const jsc.JSV switch (request.body.value) { .Null, .Empty, .Used => {}, else => { - req.body.value = try request.body.value.clone(globalThis); + req.body.value = try request.body.value.clone(.{ .Request = value }, globalThis, readable_stream_tee); fields.insert(.body); }, } @@ -641,7 +642,11 @@ pub fn constructInto(globalThis: *jsc.JSGlobalObject, arguments: []const jsc.JSV switch (response.body.value) { .Null, .Empty, .Used => {}, else => { - req.body.value = try response.body.value.clone(globalThis); + req.body.value = try response.body.value.clone(.empty, globalThis, readable_stream_tee); + if (readable_stream_tee) |tee_value| { + Response.js.gc.body.set(value, globalThis, tee_value[1]); + } + fields.insert(.body); }, } @@ -654,7 +659,7 @@ pub fn constructInto(globalThis: *jsc.JSGlobalObject, arguments: []const jsc.JSV if (!fields.contains(.body)) { if (try value.fastGet(globalThis, .body)) |body_| { fields.insert(.body); - req.body.value = try Body.Value.fromJS(globalThis, body_); + req.body.value = try Body.Value.fromJSWithReadableStreamValue(globalThis, body_, if (readable_stream_tee) |tee| &tee[1] else null); } if (globalThis.hasException()) return error.JSError; @@ -775,12 +780,23 @@ pub fn constructInto(globalThis: *jsc.JSGlobalObject, arguments: []const jsc.JSV return req; } -pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!*Request { +pub fn constructor( + globalThis: *jsc.JSGlobalObject, + callframe: *jsc.CallFrame, + thisValue: JSValue, +) bun.JSError!*Request { const arguments_ = callframe.arguments_old(2); const arguments = arguments_.ptr[0..arguments_.len]; + var readable_stream_tee: [2]JSValue = .{ .zero, .zero }; - const request = try constructInto(globalThis, arguments); - return Request.new(request); + const request = try constructInto(globalThis, arguments, &readable_stream_tee); + const result = Request.new(request); + + if (readable_stream_tee[1] != .zero and result.body.value == .Locked) { + js.gc.body.set(thisValue, globalThis, readable_stream_tee[1]); + } + + return result; } pub fn getBodyValue( @@ -795,22 +811,16 @@ pub fn doClone( callframe: *jsc.CallFrame, ) bun.JSError!jsc.JSValue { const this_value = callframe.this(); - const cloned = try this.clone(bun.default_allocator, globalThis); - + var readable_stream_tee: [2]JSValue = .{ .zero, .zero }; + const cloned = try this.clone(bun.default_allocator, globalThis, this_value, &readable_stream_tee); const js_wrapper = cloned.toJS(globalThis); if (js_wrapper != .zero) { - if (cloned.body.value == .Locked) { - if (cloned.body.value.Locked.readable.get(.{ .Request = js_wrapper }, globalThis)) |readable| { - // If we are teed, then we need to update the cached .body - // value to point to the new readable stream - // We must do this on both the original and cloned request - // but especially the original request since it will have a stale .body value now. - js.bodySetCached(js_wrapper, globalThis, readable.value); - const this_js = this.toJS(globalThis); - if (this.body.value.Locked.readable.get(.{ .Request = this_js }, globalThis)) |other_readable| { - js.bodySetCached(this_value, globalThis, other_readable.value); - } - } + if (this.body.value == .Locked and readable_stream_tee[0] != .zero) { + js.gc.body.set(js_wrapper, globalThis, readable_stream_tee[0]); + } + + if (cloned.body.value == .Locked and readable_stream_tee[1] != .zero) { + js.gc.body.set(js_wrapper, globalThis, readable_stream_tee[1]); } } @@ -928,11 +938,13 @@ pub fn cloneInto( allocator: std.mem.Allocator, globalThis: *JSGlobalObject, preserve_url: bool, + this_value: jsc.JSValue, + readable_stream_tee: ?*[2]jsc.JSValue, ) bun.JSError!void { _ = allocator; this.ensureURL() catch {}; const vm = globalThis.bunVM(); - var body_ = try this.body.value.clone(globalThis); + var body_ = try this.body.value.clone(.{ .Request = this_value }, globalThis, readable_stream_tee); errdefer body_.deinit(); const body = try vm.initRequestBodyValue(body_); const url = if (preserve_url) req.url else this.url.dupeRef(); @@ -953,10 +965,10 @@ pub fn cloneInto( } } -pub fn clone(this: *Request, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) bun.JSError!*Request { +pub fn clone(this: *Request, allocator: std.mem.Allocator, globalThis: *JSGlobalObject, this_value: jsc.JSValue, readable_stream_tee: ?*[2]jsc.JSValue) bun.JSError!*Request { const req = Request.new(undefined); errdefer bun.destroy(req); - try this.cloneInto(req, allocator, globalThis, false); + try this.cloneInto(req, allocator, globalThis, false, this_value, readable_stream_tee); return req; } diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index 56db374d97..ecef3ef846 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -298,8 +298,10 @@ pub fn makeMaybePooled(globalObject: *jsc.JSGlobalObject, ptr: *Response) JSValu pub fn cloneValue( this: *Response, globalThis: *JSGlobalObject, + this_value: jsc.JSValue, + readable_stream_tee: ?*[2]jsc.JSValue, ) bun.JSError!Response { - var body = try this.body.clone(globalThis); + var body = try this.body.clone(.{ .Response = this_value }, globalThis, readable_stream_tee); errdefer body.deinit(bun.default_allocator); var init = try this.init.clone(globalThis); errdefer init.deinit(bun.default_allocator); @@ -311,8 +313,8 @@ pub fn cloneValue( }; } -pub fn clone(this: *Response, globalThis: *JSGlobalObject) bun.JSError!*Response { - return bun.new(Response, try this.cloneValue(globalThis)); +pub fn clone(this: *Response, globalThis: *JSGlobalObject, this_value: jsc.JSValue, readable_stream_tee: ?*[2]jsc.JSValue) bun.JSError!*Response { + return bun.new(Response, try this.cloneValue(globalThis, this_value, readable_stream_tee)); } pub fn getStatus( @@ -538,7 +540,7 @@ pub fn constructError( return response.toJS(globalThis); } -pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!*Response { +pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, thisValue: JSValue) bun.JSError!*Response { var arguments = callframe.argumentsAsArray(2); if (!arguments[0].isUndefinedOrNull() and arguments[0].isObject()) { @@ -566,10 +568,11 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b return s3.throwSignError(sign_err, globalThis); }; defer result.deinit(); - response.init.headers = try response.getOrCreateHeaders(globalThis); + const headers = try response.getOrCreateHeaders(globalThis); + errdefer headers.deref(); + try headers.put(.Location, result.url, globalThis); response.redirected = true; - var headers_ref = response.init.headers.?; - try headers_ref.put(.Location, result.url, globalThis); + response.init.headers = headers; return bun.new(Response, response); } } @@ -595,13 +598,15 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b return error.JSError; } + var readable_stream_value: JSValue = .zero; + var body: Body = brk: { if (arguments[0].isUndefinedOrNull()) { break :brk Body{ .value = Body.Value{ .Null = {} }, }; } - break :brk try Body.extract(globalThis, arguments[0]); + break :brk try Body.extract(globalThis, arguments[0], &readable_stream_value); }; errdefer body.deinit(bun.default_allocator); @@ -609,21 +614,23 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b return error.JSError; } + if (body.value == .Blob and + init.headers != null and + body.value.Blob.content_type.len > 0 and + !init.headers.?.fastHas(.ContentType)) + { + try init.headers.?.put(.ContentType, body.value.Blob.content_type, globalThis); + } + var response = bun.new(Response, Response{ .body = body, .init = init, }); - if (response.body.value == .Blob and - response.init.headers != null and - response.body.value.Blob.content_type.len > 0 and - !response.init.headers.?.fastHas(.ContentType)) - { - try response.init.headers.?.put(.ContentType, response.body.value.Blob.content_type, globalThis); - } - response.calculateEstimatedByteSize(); - + if (thisValue != .zero and readable_stream_value != .zero) { + response.body.value.Locked.readable.set(.{ .Response = thisValue }, response.body.value.Locked.global, readable_stream_value); + } return response; } diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index e1900cd059..31fb3c4d18 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -4,6 +4,7 @@ export default [ define({ name: "Request", construct: true, + constructNeedsThis: true, finalize: true, final: false, klass: {}, @@ -68,6 +69,7 @@ export default [ define({ name: "Response", construct: true, + constructNeedsThis: true, finalize: true, final: false, JSType: "0b11101110", diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index 1215f77d54..45a889ba7e 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -1710,7 +1710,7 @@ size_t ${name}::memoryCost(void* ptr) { size_t ${name}::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) { auto* thisObject = jsCast<${name}*>(cell); auto* wrapped = thisObject->wrapped(); - return Base::estimatedSize(cell, vm) + ${name}::memoryCost(wrapped); + return Base::estimatedSize(cell, vm) + (wrapped ? ${name}::memoryCost(wrapped) : 0); } void ${name}::destroy(JSCell* cell) diff --git a/src/codegen/generate-jssink.ts b/src/codegen/generate-jssink.ts index 13d71b6d18..f235591c7e 100644 --- a/src/codegen/generate-jssink.ts +++ b/src/codegen/generate-jssink.ts @@ -424,7 +424,8 @@ size_t ${controller}::memoryCost(void* sinkPtr) { } size_t ${controller}::estimatedSize(JSCell* cell, JSC::VM& vm) { - return Base::estimatedSize(cell, vm) + ${controller}::memoryCost(jsCast<${controller}*>(cell)->wrapped()); + auto* wrapped = jsCast<${controller}*>(cell)->wrapped(); + return Base::estimatedSize(cell, vm) + (wrapped ? ${controller}::memoryCost(wrapped) : 0); } JSC_DECLARE_HOST_FUNCTION(${controller}__close);