diff options
Diffstat (limited to '')
-rw-r--r-- | packages/bun-types/globals.d.ts | 2 | ||||
-rw-r--r-- | src/bun.js/api/server.zig | 18 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGeneratedClasses.cpp | 37 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGeneratedClasses.h | 1 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.cpp | 23 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 18 | ||||
-rw-r--r-- | src/bun.js/bindings/generated_classes.zig | 26 | ||||
-rw-r--r-- | src/bun.js/bindings/headers-cpp.h | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/headers.h | 5 | ||||
-rw-r--r-- | src/bun.js/bindings/headers.zig | 3 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/AbortSignal.cpp | 9 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/AbortSignal.h | 1 | ||||
-rw-r--r-- | src/bun.js/webcore/request.zig | 42 | ||||
-rw-r--r-- | src/bun.js/webcore/response.classes.ts | 4 | ||||
-rw-r--r-- | src/bun.js/webcore/response.zig | 6 | ||||
-rw-r--r-- | src/http_client_async.zig | 140 | ||||
-rw-r--r-- | test/bun.js/bun-server.test.ts | 24 | ||||
-rw-r--r-- | test/bun.js/fetch.test.js | 397 |
18 files changed, 624 insertions, 134 deletions
diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index 69c100c05..3e677b458 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -1056,8 +1056,6 @@ declare class Request implements BlobInterface { readonly referrerPolicy: ReferrerPolicy; /** * Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. - * - * Note: this is **not implemented yet**. The cake is a lie. */ readonly signal: AbortSignal; diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 62d8f602a..546350679 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -444,7 +444,6 @@ pub const ServerConfig = struct { args.base_url = URL.parse(args.base_uri); } } else { - const hostname: string = if (has_hostname and std.mem.span(args.hostname).len > 0) std.mem.span(args.hostname) else "0.0.0.0"; const protocol: string = if (args.ssl_config != null) "https" else "http"; @@ -1011,6 +1010,15 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object // but we received nothing or the connection was aborted if (request_js.as(Request)) |req| { + if (req.signal) |signal| { + // if signal is not aborted, abort the signal + if (!signal.aborted()) { + const reason = JSC.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, this.server.globalThis); + reason.ensureStillAlive(); + _ = signal.signal(reason); + } + } + // the promise is pending if (req.body == .Locked and (req.body.Locked.action != .none or req.body.Locked.promise != null)) { this.pending_promises_for_abort += 1; @@ -1079,6 +1087,14 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object // but we received nothing or the connection was aborted if (request_js.as(Request)) |req| { + if (req.signal) |signal| { + // if signal is not aborted, abort the signal + if (!signal.aborted()) { + const reason = JSC.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, this.server.globalThis); + reason.ensureStillAlive(); + _ = signal.signal(reason); + } + } // the promise is pending if (req.body == .Locked and req.body.Locked.action != .none and req.body.Locked.promise != null) { req.body.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index 805c4c929..716f683e5 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -6201,6 +6201,9 @@ JSC_DECLARE_CUSTOM_GETTER(RequestPrototype__referrerGetterWrap); extern "C" JSC::EncodedJSValue RequestPrototype__getReferrerPolicy(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_CUSTOM_GETTER(RequestPrototype__referrerPolicyGetterWrap); +extern "C" JSC::EncodedJSValue RequestPrototype__getSignal(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); +JSC_DECLARE_CUSTOM_GETTER(RequestPrototype__signalGetterWrap); + extern "C" EncodedJSValue RequestPrototype__getText(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(RequestPrototype__textCallback); @@ -6227,6 +6230,7 @@ static const HashTableValue JSRequestPrototypeTableValues[] = { { "redirect"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, RequestPrototype__redirectGetterWrap, 0 } }, { "referrer"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, RequestPrototype__referrerGetterWrap, 0 } }, { "referrerPolicy"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, RequestPrototype__referrerPolicyGetterWrap, 0 } }, + { "signal"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, RequestPrototype__signalGetterWrap, 0 } }, { "text"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, RequestPrototype__textCallback, 0 } }, { "url"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, RequestPrototype__urlGetterWrap, 0 } } }; @@ -6507,6 +6511,37 @@ JSC_DEFINE_CUSTOM_GETTER(RequestPrototype__referrerPolicyGetterWrap, (JSGlobalOb RELEASE_AND_RETURN(throwScope, result); } +JSC_DEFINE_CUSTOM_GETTER(RequestPrototype__signalGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + auto& vm = lexicalGlobalObject->vm(); + Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSRequest* thisObject = jsCast<JSRequest*>(JSValue::decode(thisValue)); + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + + if (JSValue cachedValue = thisObject->m_signal.get()) + return JSValue::encode(cachedValue); + + JSC::JSValue result = JSC::JSValue::decode( + RequestPrototype__getSignal(thisObject->wrapped(), globalObject)); + RETURN_IF_EXCEPTION(throwScope, {}); + thisObject->m_signal.set(vm, thisObject, result); + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); +} + +extern "C" void RequestPrototype__signalSetCachedValue(JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast<JSRequest*>(JSValue::decode(thisValue)); + thisObject->m_signal.set(vm, thisObject, JSValue::decode(value)); +} + +extern "C" EncodedJSValue RequestPrototype__signalGetCachedValue(JSC::EncodedJSValue thisValue) +{ + auto* thisObject = jsCast<JSRequest*>(JSValue::decode(thisValue)); + return JSValue::encode(thisObject->m_signal.get()); +} + JSC_DEFINE_HOST_FUNCTION(RequestPrototype__textCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -6720,6 +6755,7 @@ void JSRequest::visitChildrenImpl(JSCell* cell, Visitor& visitor) } visitor.append(thisObject->m_body); visitor.append(thisObject->m_headers); + visitor.append(thisObject->m_signal); visitor.append(thisObject->m_url); } @@ -6733,6 +6769,7 @@ void JSRequest::visitAdditionalChildren(Visitor& visitor) visitor.append(thisObject->m_body); visitor.append(thisObject->m_headers); + visitor.append(thisObject->m_signal); visitor.append(thisObject->m_url); ; } diff --git a/src/bun.js/bindings/ZigGeneratedClasses.h b/src/bun.js/bindings/ZigGeneratedClasses.h index 3cd3e8a1a..acf1dc140 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.h +++ b/src/bun.js/bindings/ZigGeneratedClasses.h @@ -618,6 +618,7 @@ public: mutable JSC::WriteBarrier<JSC::Unknown> m_body; mutable JSC::WriteBarrier<JSC::Unknown> m_headers; + mutable JSC::WriteBarrier<JSC::Unknown> m_signal; mutable JSC::WriteBarrier<JSC::Unknown> m_url; }; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index c093fad59..8e401b4cd 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3819,8 +3819,22 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskJob(JSC__JSGlobalObject* arg0 JSC::JSValue::decode(JSValue4)); } -extern "C" JSC__AbortSignal* JSC__AbortSignal__signal(JSC__AbortSignal* arg0, JSC__JSValue JSValue1) -{ +extern "C" JSC__JSValue JSC__AbortSignal__create(JSC__JSGlobalObject* globalObject) { + Zig::GlobalObject* thisObject = JSC::jsCast<Zig::GlobalObject*>(globalObject); + auto* context = thisObject->scriptExecutionContext(); + auto abortSignal = WebCore::AbortSignal::create(context); + + return JSValue::encode(toJSNewlyCreated<IDLInterface<JSC__AbortSignal>>(*globalObject, *jsCast<JSDOMGlobalObject*>(globalObject), WTFMove(abortSignal))); +} +extern "C" JSC__JSValue JSC__AbortSignal__toJS(JSC__AbortSignal* arg0, JSC__JSGlobalObject* globalObject) { + WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0); + + return JSValue::encode(toJS<IDLInterface<JSC__AbortSignal>>(*globalObject, *jsCast<JSDOMGlobalObject*>(globalObject), *abortSignal)); +} + + +extern "C" JSC__AbortSignal* JSC__AbortSignal__signal(JSC__AbortSignal* arg0, JSC__JSValue JSValue1) { + WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0); abortSignal->signalAbort(JSC::JSValue::decode(JSValue1)); return arg0; @@ -3851,6 +3865,10 @@ extern "C" JSC__AbortSignal* JSC__AbortSignal__unref(JSC__AbortSignal* arg0) abortSignal->deref(); return arg0; } +extern "C" void JSC__AbortSignal__cleanNativeBindings(JSC__AbortSignal* arg0, void* arg1) { + WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0); + abortSignal->cleanNativeBindings(arg1); +} extern "C" JSC__AbortSignal* JSC__AbortSignal__addListener(JSC__AbortSignal* arg0, void* ctx, void (*callback)(void* ctx, JSC__JSValue reason)) { @@ -3919,6 +3937,7 @@ extern "C" JSC__JSValue JSC__AbortSignal__createTimeoutError(const ZigString* me return JSC::JSValue::encode(error); } + #pragma mark - WebCore::DOMFormData CPP_DECL void WebCore__DOMFormData__append(WebCore__DOMFormData* arg0, ZigString* arg1, ZigString* arg2) diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index a98d350c2..a43ff8265 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -1697,6 +1697,11 @@ pub const AbortSignal = extern opaque { ) *AbortSignal { return cppFn("addListener", .{ this, ctx, callback }); } + + pub fn cleanNativeBindings(this: *AbortSignal, ctx: ?*anyopaque) void { + return cppFn("cleanNativeBindings", .{ this, ctx }); + } + pub fn signal( this: *AbortSignal, reason: JSValue, @@ -1728,6 +1733,14 @@ pub const AbortSignal = extern opaque { return cppFn("fromJS", .{value}); } + pub fn toJS(this: *AbortSignal, global: *JSGlobalObject) JSValue { + return cppFn("toJS", .{this, global}); + } + + pub fn create(global: *JSGlobalObject) JSValue { + return cppFn("create", .{ global }); + } + pub fn createAbortError(message: *const ZigString, code: *const ZigString, global: *JSGlobalObject) JSValue { return cppFn("createAbortError", .{ message, code, global }); } @@ -1739,6 +1752,7 @@ pub const AbortSignal = extern opaque { pub const Extern = [_][]const u8{ "createAbortError", "createTimeoutError", + "create", "ref", "unref", "signal", @@ -1746,6 +1760,8 @@ pub const AbortSignal = extern opaque { "aborted", "addListener", "fromJS", + "toJS", + "cleanNativeBindings" }; }; @@ -3557,7 +3573,7 @@ pub const JSValue = enum(JSValueReprInt) { status, url, body, - data, + data }; // intended to be more lightweight than ZigString diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index dec2d6543..015c740ec 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -1680,6 +1680,28 @@ pub const JSRequest = struct { return result; } + extern fn RequestPrototype__signalSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void; + + extern fn RequestPrototype__signalGetCachedValue(JSC.JSValue) JSC.JSValue; + + /// `Request.signal` setter + /// This value will be visited by the garbage collector. + pub fn signalSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { + JSC.markBinding(@src()); + RequestPrototype__signalSetCachedValue(thisValue, globalObject, value); + } + + /// `Request.signal` getter + /// This value will be visited by the garbage collector. + pub fn signalGetCached(thisValue: JSC.JSValue) ?JSC.JSValue { + JSC.markBinding(@src()); + const result = RequestPrototype__signalGetCachedValue(thisValue); + if (result == .zero) + return null; + + return result; + } + extern fn RequestPrototype__urlSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void; extern fn RequestPrototype__urlGetCachedValue(JSC.JSValue) JSC.JSValue; @@ -1799,6 +1821,9 @@ pub const JSRequest = struct { if (@TypeOf(Request.getReferrerPolicy) != GetterType) @compileLog("Expected Request.getReferrerPolicy to be a getter"); + if (@TypeOf(Request.getSignal) != GetterType) + @compileLog("Expected Request.getSignal to be a getter"); + if (@TypeOf(Request.getText) != CallbackType) @compileLog("Expected Request.getText to be a callback but received " ++ @typeName(@TypeOf(Request.getText))); if (@TypeOf(Request.getUrl) != GetterType) @@ -1825,6 +1850,7 @@ pub const JSRequest = struct { @export(Request.getRedirect, .{ .name = "RequestPrototype__getRedirect" }); @export(Request.getReferrer, .{ .name = "RequestPrototype__getReferrer" }); @export(Request.getReferrerPolicy, .{ .name = "RequestPrototype__getReferrerPolicy" }); + @export(Request.getSignal, .{ .name = "RequestPrototype__getSignal" }); @export(Request.getText, .{ .name = "RequestPrototype__getText" }); @export(Request.getUrl, .{ .name = "RequestPrototype__getUrl" }); } diff --git a/src/bun.js/bindings/headers-cpp.h b/src/bun.js/bindings/headers-cpp.h index 45ef8dbb7..3c53d5f99 100644 --- a/src/bun.js/bindings/headers-cpp.h +++ b/src/bun.js/bindings/headers-cpp.h @@ -1,4 +1,4 @@ -//-- AUTOGENERATED FILE -- 1676701449 +//-- AUTOGENERATED FILE -- 1676922916 // clang-format off #pragma once diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 50c37dde6..6b005216a 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -1,5 +1,5 @@ // clang-format off -//-- AUTOGENERATED FILE -- 1676945760 +//-- AUTOGENERATED FILE -- 1676922916 #pragma once #include <stddef.h> @@ -210,11 +210,14 @@ CPP_DECL JSC__JSInternalPromise* JSC__JSModuleLoader__loadAndEvaluateModule(JSC_ CPP_DECL bool JSC__AbortSignal__aborted(JSC__AbortSignal* arg0); CPP_DECL JSC__JSValue JSC__AbortSignal__abortReason(JSC__AbortSignal* arg0); CPP_DECL JSC__AbortSignal* JSC__AbortSignal__addListener(JSC__AbortSignal* arg0, void* arg1, void(* ArgFn2)(void* arg0, JSC__JSValue JSValue1)) __attribute__((nonnull (2))); +CPP_DECL void JSC__AbortSignal__cleanNativeBindings(JSC__AbortSignal* arg0, void* arg1); +CPP_DECL JSC__JSValue JSC__AbortSignal__create(JSC__JSGlobalObject* arg0); CPP_DECL JSC__JSValue JSC__AbortSignal__createAbortError(const ZigString* arg0, const ZigString* arg1, JSC__JSGlobalObject* arg2); CPP_DECL JSC__JSValue JSC__AbortSignal__createTimeoutError(const ZigString* arg0, const ZigString* arg1, JSC__JSGlobalObject* arg2); CPP_DECL JSC__AbortSignal* JSC__AbortSignal__fromJS(JSC__JSValue JSValue0); CPP_DECL JSC__AbortSignal* JSC__AbortSignal__ref(JSC__AbortSignal* arg0); CPP_DECL JSC__AbortSignal* JSC__AbortSignal__signal(JSC__AbortSignal* arg0, JSC__JSValue JSValue1); +CPP_DECL JSC__JSValue JSC__AbortSignal__toJS(JSC__AbortSignal* arg0, JSC__JSGlobalObject* arg1); CPP_DECL JSC__AbortSignal* JSC__AbortSignal__unref(JSC__AbortSignal* arg0); #pragma mark - JSC::JSPromise diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index 12c72a717..1e0e56dcb 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -138,11 +138,14 @@ pub extern fn JSC__JSModuleLoader__loadAndEvaluateModule(arg0: *bindings.JSGloba pub extern fn JSC__AbortSignal__aborted(arg0: ?*bindings.AbortSignal) bool; pub extern fn JSC__AbortSignal__abortReason(arg0: ?*bindings.AbortSignal) JSC__JSValue; pub extern fn JSC__AbortSignal__addListener(arg0: ?*bindings.AbortSignal, arg1: ?*anyopaque, ArgFn2: ?*const fn (?*anyopaque, JSC__JSValue) callconv(.C) void) ?*bindings.AbortSignal; +pub extern fn JSC__AbortSignal__cleanNativeBindings(arg0: ?*bindings.AbortSignal, arg1: ?*anyopaque) void; +pub extern fn JSC__AbortSignal__create(arg0: *bindings.JSGlobalObject) JSC__JSValue; pub extern fn JSC__AbortSignal__createAbortError(arg0: [*c]const ZigString, arg1: [*c]const ZigString, arg2: *bindings.JSGlobalObject) JSC__JSValue; pub extern fn JSC__AbortSignal__createTimeoutError(arg0: [*c]const ZigString, arg1: [*c]const ZigString, arg2: *bindings.JSGlobalObject) JSC__JSValue; pub extern fn JSC__AbortSignal__fromJS(JSValue0: JSC__JSValue) ?*bindings.AbortSignal; pub extern fn JSC__AbortSignal__ref(arg0: ?*bindings.AbortSignal) ?*bindings.AbortSignal; pub extern fn JSC__AbortSignal__signal(arg0: ?*bindings.AbortSignal, JSValue1: JSC__JSValue) ?*bindings.AbortSignal; +pub extern fn JSC__AbortSignal__toJS(arg0: ?*bindings.AbortSignal, arg1: *bindings.JSGlobalObject) JSC__JSValue; pub extern fn JSC__AbortSignal__unref(arg0: ?*bindings.AbortSignal) ?*bindings.AbortSignal; pub extern fn JSC__JSPromise__asValue(arg0: ?*bindings.JSPromise, arg1: *bindings.JSGlobalObject) JSC__JSValue; pub extern fn JSC__JSPromise__create(arg0: *bindings.JSGlobalObject) ?*bindings.JSPromise; diff --git a/src/bun.js/bindings/webcore/AbortSignal.cpp b/src/bun.js/bindings/webcore/AbortSignal.cpp index 132ecefca..d20af5c81 100644 --- a/src/bun.js/bindings/webcore/AbortSignal.cpp +++ b/src/bun.js/bindings/webcore/AbortSignal.cpp @@ -115,6 +115,15 @@ void AbortSignal::signalAbort(JSC::JSValue reason) dispatchEvent(Event::create(eventNames().abortEvent, Event::CanBubble::No, Event::IsCancelable::No)); } +void AbortSignal::cleanNativeBindings(void* ref) { + auto callbacks = std::exchange(m_native_callbacks, {}); + + callbacks.removeAllMatching([=](auto callback){ + const auto [ ctx, func ] = callback; + return ctx == ref; + }); +} + // https://dom.spec.whatwg.org/#abortsignal-follow void AbortSignal::signalFollow(AbortSignal& signal) { diff --git a/src/bun.js/bindings/webcore/AbortSignal.h b/src/bun.js/bindings/webcore/AbortSignal.h index b0c59daae..03374b248 100644 --- a/src/bun.js/bindings/webcore/AbortSignal.h +++ b/src/bun.js/bindings/webcore/AbortSignal.h @@ -65,6 +65,7 @@ public: using Algorithm = Function<void(JSValue)>; void addAlgorithm(Algorithm&& algorithm) { m_algorithms.append(WTFMove(algorithm)); } + void cleanNativeBindings(void* ref); void addNativeCallback(std::tuple<void*, void (*)(void*, JSC::EncodedJSValue)> callback) { m_native_callbacks.append(callback); } bool isFollowingSignal() const { return !!m_followingSignal; } diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig index 04fb3f7e2..b2dfe829e 100644 --- a/src/bun.js/webcore/request.zig +++ b/src/bun.js/webcore/request.zig @@ -12,6 +12,7 @@ const js = JSC.C; const Method = @import("../../http/method.zig").Method; const FetchHeaders = JSC.FetchHeaders; +const AbortSignal = JSC.AbortSignal; const ObjectPool = @import("../../pool.zig").ObjectPool; const SystemError = JSC.SystemError; const Output = @import("bun").Output; @@ -57,6 +58,7 @@ pub const Request = struct { url_was_allocated: bool = false, headers: ?*FetchHeaders = null, + signal: ?*AbortSignal = null, body: Body.Value = Body.Value{ .Empty = {} }, method: Method = Method.GET, uws_request: ?*uws.Request = null, @@ -217,6 +219,21 @@ pub const Request = struct { return ZigString.Empty.toValueGC(globalThis); } + pub fn getSignal(this: *Request, globalThis: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + // Already have an C++ instance + if(this.signal) |signal| { + return signal.toJS(globalThis); + } else { + //Lazy create default signal + const js_signal = AbortSignal.create(globalThis); + js_signal.ensureStillAlive(); + if (AbortSignal.fromJS(js_signal)) |signal| { + this.signal = signal; + } + return js_signal; + } + } + pub fn getMethod( this: *Request, globalThis: *JSC.JSGlobalObject, @@ -254,6 +271,10 @@ pub const Request = struct { } this.body.deinit(); + if(this.signal) |signal| { + _ = signal.unref(); + this.signal = null; + } bun.default_allocator.destroy(this); } @@ -415,6 +436,22 @@ pub const Request = struct { } } + if (arguments[1].get(globalThis, "signal")) |signal_| { + if (AbortSignal.fromJS(signal_)) |signal| { + //Keep it alive + signal_.ensureStillAlive(); + request.signal = signal; + _ = signal.ref(); + + } else { + if (request.headers) |head| { + head.deref(); + } + + return null; + } + } + request.url = (arguments[0].toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch { return null; }).slice(); @@ -506,6 +543,11 @@ pub const Request = struct { req.headers = FetchHeaders.createFromUWS(globalThis, uws_req); this.headers = req.headers.?.cloneThis(globalThis).?; } + + if(this.signal) |signal| { + _ = signal.ref(); + req.signal = signal; + } } pub fn clone(this: *Request, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) *Request { diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index 7ebe4774b..67ac4716b 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -55,6 +55,10 @@ export default [ bodyUsed: { getter: "getBodyUsed", }, + signal: { + getter: "getSignal", + cache: true, + }, }, }), define({ diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 5c0cd9c8e..39ee205c2 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -672,7 +672,7 @@ pub const Fetch = struct { true => this.onResolve(), false => this.onReject(), }; - + result.ensureStillAlive(); this.clearData(); promise_value.ensureStillAlive(); @@ -1015,6 +1015,10 @@ pub const Fetch = struct { // no proxy only url url = ZigURL.parse(getAllocator(ctx).dupe(u8, request.url) catch unreachable); url_proxy_buffer = url.href; + if (request.signal) |signal_| { + _ = signal_.ref(); + signal = signal_; + } } } else if (first_arg.toStringOrNull(globalThis)) |jsstring| { if (arguments.len >= 2) { diff --git a/src/http_client_async.zig b/src/http_client_async.zig index b6dd0e828..0c7395319 100644 --- a/src/http_client_async.zig +++ b/src/http_client_async.zig @@ -235,6 +235,8 @@ fn NewHTTPContext(comptime ssl: bool) type { /// Attempt to keep the socket alive by reusing it for another request. /// If no space is available, close the socket. pub fn releaseSocket(this: *@This(), socket: HTTPSocket, hostname: []const u8, port: u16) void { + log("releaseSocket", .{}); + if (comptime Environment.allow_assert) { std.debug.assert(!socket.isClosed()); std.debug.assert(!socket.isShutdown()); @@ -1015,7 +1017,9 @@ pub fn ClientSocketAbortHandler(comptime is_ssl: bool) type { log("onAborted", .{}); if (this) |this_| { const self = bun.cast(*@This(), this_); - self.client.closeAndAbort(reason, is_ssl, self.socket); + if (self.client.state.response_stage != .done and self.client.state.response_stage != .fail) { + self.client.closeAndAbort(reason, is_ssl, self.socket); + } } } @@ -1031,6 +1035,14 @@ pub fn ClientSocketAbortHandler(comptime is_ssl: bool) type { pub fn addAbortSignalEventListenner(this: *HTTPClient, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void { if (this.signal) |signal| { + const aborted = signal.aborted(); + if (aborted) { + log("addAbortSignalEventListenner already aborted!", .{}); + const reason = signal.abortReason(); + this.closeAndAbort(reason, is_ssl, socket); + return; + } + const handler = ClientSocketAbortHandler(is_ssl).init(this, socket) catch unreachable; this.abort_handler = bun.cast(*anyopaque, handler); this.abort_handler_deinit = ClientSocketAbortHandler(is_ssl).deinit; @@ -1054,6 +1066,15 @@ pub fn hasSignalAborted(this: *HTTPClient) ?JSC.JSValue { return null; } +pub fn deinitSignal(this: *HTTPClient) void { + if (this.signal != null) { + var signal = this.signal.?; + const ctx = bun.cast(*anyopaque, this); + signal.cleanNativeBindings(ctx); + _ = signal.unref(); + this.signal = null; + } +} pub fn deinit(this: *HTTPClient) void { if (this.redirect) |redirect| { redirect.release(); @@ -1068,11 +1089,8 @@ pub fn deinit(this: *HTTPClient) void { this.proxy_tunnel = null; } - if (this.signal != null) { - var signal = this.signal.?; - _ = signal.unref(); - this.signal = null; - } + this.deinitSignal(); + if (this.abort_handler != null and this.abort_handler_deinit != null) { this.abort_handler_deinit.?(this.abort_handler.?); this.abort_handler = null; @@ -1538,6 +1556,12 @@ pub fn start(this: *HTTPClient, body: []const u8, body_out_str: *MutableString) } fn start_(this: *HTTPClient, comptime is_ssl: bool) void { + // Aborted before connecting + if (this.hasSignalAborted()) |reason| { + this.fail(error.Aborted, reason); + return; + } + var socket = http_thread.connect(this, is_ssl) catch |err| { this.fail(err, null); return; @@ -1815,12 +1839,17 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s } pub fn closeAndFail(this: *HTTPClient, err: anyerror, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void { - socket.ext(**anyopaque).?.* = bun.cast( - **anyopaque, - NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr(), - ); - socket.close(0, null); - this.fail(err, null); + if (this.state.stage != .fail and this.state.stage != .done) { + log("closeAndFail", .{}); + if (!socket.isClosed()) { + socket.ext(**anyopaque).?.* = bun.cast( + **anyopaque, + NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr(), + ); + socket.close(0, null); + } + this.fail(err, null); + } } fn startProxySendHeaders(this: *HTTPClient, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void { @@ -2112,12 +2141,17 @@ pub fn onData(this: *HTTPClient, comptime is_ssl: bool, incoming_data: []const u } pub fn closeAndAbort(this: *HTTPClient, reason: JSC.JSValue, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void { - socket.ext(**anyopaque).?.* = bun.cast( - **anyopaque, - NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr(), - ); - socket.close(0, null); - this.fail(error.Aborted, reason); + if (this.state.stage != .fail and this.state.stage != .done) { + log("closeAndAbort", .{}); + if (!socket.isClosed()) { + socket.ext(**anyopaque).?.* = bun.cast( + **anyopaque, + NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr(), + ); + socket.close(0, null); + } + this.fail(error.Aborted, reason); + } } fn fail(this: *HTTPClient, err: anyerror, reason: ?JSC.JSValue) void { @@ -2132,6 +2166,8 @@ fn fail(this: *HTTPClient, err: anyerror, reason: ?JSC.JSValue) void { this.proxy_tunneling = false; callback.run(result); + + this.deinitSignal(); } // We have to clone metadata immediately after use @@ -2164,45 +2200,47 @@ pub fn setTimeout(this: *HTTPClient, socket: anytype, amount: c_uint) void { } pub fn done(this: *HTTPClient, comptime is_ssl: bool, ctx: *NewHTTPContext(is_ssl), socket: NewHTTPContext(is_ssl).HTTPSocket) void { - var out_str = this.state.body_out_str.?; - var body = out_str.*; - this.cloned_metadata.response = this.state.pending_response; - const result = this.toResult(this.cloned_metadata, null); - const callback = this.completion_callback; + if (this.state.stage != .done and this.state.stage != .fail) { + log("done", .{}); - this.state.response_stage = .done; - this.state.request_stage = .done; - this.state.stage = .done; + this.deinitSignal(); - socket.ext(**anyopaque).?.* = bun.cast(**anyopaque, NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr()); + var out_str = this.state.body_out_str.?; + var body = out_str.*; + this.cloned_metadata.response = this.state.pending_response; + const result = this.toResult(this.cloned_metadata, null); + const callback = this.completion_callback; - if (this.state.allow_keepalive and !this.disable_keepalive and !socket.isClosed() and FeatureFlags.enable_keepalive) { - ctx.releaseSocket( - socket, - this.connected_url.hostname, - this.connected_url.getPortAuto(), - ); - } else if (!socket.isClosed()) { - socket.close(0, null); - } + socket.ext(**anyopaque).?.* = bun.cast(**anyopaque, NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr()); - this.state.reset(); - result.body.?.* = body; - std.debug.assert(this.state.stage != .done); - this.state.response_stage = .done; - this.state.request_stage = .done; - this.state.stage = .done; - this.proxy_tunneling = false; - if (comptime print_every > 0) { - print_every_i += 1; - if (print_every_i % print_every == 0) { - Output.prettyln("Heap stats for HTTP thread\n", .{}); - Output.flush(); - default_arena.dumpThreadStats(); - print_every_i = 0; + if (this.state.allow_keepalive and !this.disable_keepalive and !socket.isClosed() and FeatureFlags.enable_keepalive) { + ctx.releaseSocket( + socket, + this.connected_url.hostname, + this.connected_url.getPortAuto(), + ); + } else if (!socket.isClosed()) { + socket.close(0, null); + } + + this.state.reset(); + result.body.?.* = body; + std.debug.assert(this.state.stage != .done); + this.state.response_stage = .done; + this.state.request_stage = .done; + this.state.stage = .done; + this.proxy_tunneling = false; + if (comptime print_every > 0) { + print_every_i += 1; + if (print_every_i % print_every == 0) { + Output.prettyln("Heap stats for HTTP thread\n", .{}); + Output.flush(); + default_arena.dumpThreadStats(); + print_every_i = 0; + } } + callback.run(result); } - callback.run(result); } pub const HTTPClientResult = struct { diff --git a/test/bun.js/bun-server.test.ts b/test/bun.js/bun-server.test.ts index 667d7bdca..98554dfbb 100644 --- a/test/bun.js/bun-server.test.ts +++ b/test/bun.js/bun-server.test.ts @@ -26,4 +26,28 @@ describe("Server", () => { expect(await response.text()).toBe("Hello"); server.stop(); }); + + test('abort signal on server', async ()=> { + { + let signalOnServer = false; + const server = Bun.serve({ + async fetch(req) { + req.signal.addEventListener("abort", () => { + signalOnServer = true; + }); + await Bun.sleep(3000); + return new Response("Hello"); + }, + port: 54321, + }); + + try { + await fetch("http://localhost:54321", { signal: AbortSignal.timeout(100) }); + } catch {} + await Bun.sleep(300); + expect(signalOnServer).toBe(true); + server.stop(); + } + + }) }); diff --git a/test/bun.js/fetch.test.js b/test/bun.js/fetch.test.js index 46409633c..0e222b984 100644 --- a/test/bun.js/fetch.test.js +++ b/test/bun.js/fetch.test.js @@ -8,64 +8,289 @@ const exampleFixture = fs.readFileSync( "utf8", ); -let server ; +let server; beforeAll(() => { server = Bun.serve({ - async fetch(){ + async fetch(request) { + + if (request.url.endsWith("/nodelay")) { + return new Response("Hello") + } + if (request.url.endsWith("/stream")) { + const reader = request.body.getReader(); + const body = new ReadableStream({ + async pull(controller) { + if (!reader) controller.close(); + const { done, value } = await reader.read(); + // When no more data needs to be consumed, close the stream + if (done) { + controller.close(); + return; + } + // Enqueue the next data chunk into our target stream + controller.enqueue(value); + } + }); + return new Response(body); + } + if ((request.method).toUpperCase() === "POST") { + const body = await request.text(); + return new Response(body); + } await Bun.sleep(2000); return new Response("Hello") }, port: 64321 }); - + }); afterAll(() => { server.stop(); }); +const payload = new Uint8Array(1024 * 1024 * 2); +crypto.getRandomValues(payload); + +describe("AbortSignalStreamTest",async () => { + + const port = 84322; + async function abortOnStage(body, stage, port) { + let error = undefined; + var abortController = new AbortController(); + { + const server = Bun.serve({ + async fetch(request) { + let chunk_count = 0; + const reader = request.body.getReader(); + return Response( + new ReadableStream({ + async pull(controller) { + while (true) { + chunk_count++; + + const { done, value } = await reader.read(); + if (chunk_count == stage) { + abortController.abort(); + } + + if (done) { + controller.close(); + return; + } + controller.enqueue(value); + } + }, + }), + ); + }, + port, + }); -describe("AbortSignal", ()=> { - it("AbortError", async ()=> { + try { + const signal = abortController.signal; + + await fetch(`http://127.0.0.1:${port}`, { method: "POST", body, signal: signal }).then((res) => res.arrayBuffer()); + + } catch (ex) { + error = ex; + + } + server.stop(); + expect(error instanceof DOMException).toBeTruthy(); + expect(error.name).toBe("AbortError"); + expect(error.message).toBe("The operation was aborted."); + } + } + + for (let i = 1; i < 7; i++) { + await it(`Abort after ${i} chunks`, async () => { + await abortOnStage(payload, i, port + i); + })(); + } +}) + +describe("AbortSignalDirectStreamTest", () => { + const port = 74322; + async function abortOnStage(body, stage, port) { + let error = undefined; + var abortController = new AbortController(); + { + const server = Bun.serve({ + async fetch(request) { + let chunk_count = 0; + const reader = request.body.getReader(); + return Response( + new ReadableStream({ + type: "direct", + async pull(controller) { + while (true) { + chunk_count++; + + const { done, value } = await reader.read(); + if (chunk_count == stage) { + abortController.abort(); + } + + if (done) { + controller.end(); + return; + } + controller.write(value); + } + }, + }), + ); + }, + port, + }); + + try { + const signal = abortController.signal; + + await fetch(`http://127.0.0.1:${port}`, { method: "POST", body, signal: signal }).then((res) => res.arrayBuffer()); + + } catch (ex) { + error = ex; + } + server.stop(); + expect(error instanceof DOMException).toBeTruthy(); + expect(error.name).toBe("AbortError"); + expect(error.message).toBe("The operation was aborted."); + } + } + + for (let i = 1; i < 7; i++) { + await it(`Abort after ${i} chunks`, async () => { + await abortOnStage(payload, i, port + i); + })(); + } +}) + +describe("AbortSignal", () => { + it("AbortError", async () => { let name; try { var controller = new AbortController(); const signal = controller.signal; - async function manualAbort(){ + async function manualAbort() { await Bun.sleep(10); controller.abort(); } - await Promise.all([fetch("http://127.0.0.1:64321", { signal: signal }).then((res)=> res.text()), manualAbort()]); - } catch (error){ + await Promise.all([fetch("http://127.0.0.1:64321", { signal: signal }).then((res) => res.text()), manualAbort()]); + } catch (error) { name = error.name; } expect(name).toBe("AbortError"); }) - it("AbortErrorWithReason", async ()=> { + + it("AbortAfterFinish", async () => { + let error = undefined; + try { + var controller = new AbortController(); + const signal = controller.signal; + + await fetch("http://127.0.0.1:64321/nodelay", { signal: signal }).then((res) => res.text()) + controller.abort(); + } catch (ex) { + error = ex; + } + expect(error).toBeUndefined(); + }) + + it("AbortErrorWithReason", async () => { let reason; try { var controller = new AbortController(); const signal = controller.signal; - async function manualAbort(){ + async function manualAbort() { await Bun.sleep(10); controller.abort("My Reason"); } - await Promise.all([fetch("http://127.0.0.1:64321", { signal: signal }).then((res)=> res.text()), manualAbort()]); - } catch (error){ - reason = error + await Promise.all([fetch("http://127.0.0.1:64321", { signal: signal }).then((res) => res.text()), manualAbort()]); + } catch (error) { + reason = error } expect(reason).toBe("My Reason"); }) - it("TimeoutError", async ()=> { + + it("AbortErrorEventListener", async () => { + let name; + try { + var controller = new AbortController(); + const signal = controller.signal; + var eventSignal = undefined; + signal.addEventListener("abort", (ev) => { + eventSignal = ev.currentTarget; + }); + + async function manualAbort() { + await Bun.sleep(10); + controller.abort(); + } + await Promise.all([fetch("http://127.0.0.1:64321", { signal: signal }).then((res) => res.text()), manualAbort()]); + } catch (error) { + name = error.name; + } + expect(eventSignal).toBeDefined(); + expect(eventSignal.reason.name).toBe(name); + expect(eventSignal.aborted).toBe(true); + }) + + it("AbortErrorWhileUploading", async () => { + const abortController = new AbortController(); + let error; + try { + await fetch( + "http://localhost:64321", + { + method: "POST", + body: new ReadableStream({ + pull(controller) { + controller.enqueue(new Uint8Array([1, 2, 3, 4])); + //this will abort immediately should abort before connected + abortController.abort(); + }, + }), + signal: abortController.signal, + }, + ); + } catch (ex) { + error = ex + } + + expect(error instanceof DOMException).toBeTruthy(); + expect(error.name).toBe("AbortError"); + expect(error.message).toBe("The operation was aborted."); + }); + + it("TimeoutError", async () => { let name; try { const signal = AbortSignal.timeout(10); - await fetch("http://127.0.0.1:64321", { signal: signal }).then((res)=> res.text()); - } catch (error){ + await fetch("http://127.0.0.1:64321", { signal: signal }).then((res) => res.text()); + } catch (error) { name = error.name; } expect(name).toBe("TimeoutError"); }) + it("Request", async () => { + let name; + try { + var controller = new AbortController(); + const signal = controller.signal; + const request = new Request("http://127.0.0.1:64321", { signal }); + async function manualAbort() { + await Bun.sleep(10); + controller.abort(); + } + await Promise.all([fetch(request).then((res) => res.text()), manualAbort()]); + } catch (error) { + name = error.name + } + expect(name).toBe("AbortError"); + }) + }) describe("Headers", () => { @@ -296,33 +521,31 @@ function testBlobInterface(blobbyConstructor, hasBlobFn) { if (withGC) gc(); }); - it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> json${ - withGC ? " (with gc) " : "" - }`, async () => { - if (withGC) gc(); - var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject))); - if (withGC) gc(); - expect(JSON.stringify(await response.json())).toBe(JSON.stringify(jsonObject)); - if (withGC) gc(); - }); + it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> json${withGC ? " (with gc) " : "" + }`, async () => { + if (withGC) gc(); + var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject))); + if (withGC) gc(); + expect(JSON.stringify(await response.json())).toBe(JSON.stringify(jsonObject)); + if (withGC) gc(); + }); - it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> invalid json${ - withGC ? " (with gc) " : "" - }`, async () => { - if (withGC) gc(); - var response = blobbyConstructor( - new TextEncoder().encode(JSON.stringify(jsonObject) + " NOW WE ARE INVALID JSON"), - ); - if (withGC) gc(); - var failed = false; - try { - await response.json(); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); - if (withGC) gc(); - }); + it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> invalid json${withGC ? " (with gc) " : "" + }`, async () => { + if (withGC) gc(); + var response = blobbyConstructor( + new TextEncoder().encode(JSON.stringify(jsonObject) + " NOW WE ARE INVALID JSON"), + ); + if (withGC) gc(); + var failed = false; + try { + await response.json(); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + if (withGC) gc(); + }); it(`${jsonObject.hello === true ? "latin1" : "utf16"} text${withGC ? " (with gc) " : ""}`, async () => { if (withGC) gc(); @@ -332,15 +555,14 @@ function testBlobInterface(blobbyConstructor, hasBlobFn) { if (withGC) gc(); }); - it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> text${ - withGC ? " (with gc) " : "" - }`, async () => { - if (withGC) gc(); - var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject))); - if (withGC) gc(); - expect(await response.text()).toBe(JSON.stringify(jsonObject)); - if (withGC) gc(); - }); + it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> text${withGC ? " (with gc) " : "" + }`, async () => { + if (withGC) gc(); + var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject))); + if (withGC) gc(); + expect(await response.text()).toBe(JSON.stringify(jsonObject)); + if (withGC) gc(); + }); it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer${withGC ? " (with gc) " : ""}`, async () => { if (withGC) gc(); @@ -365,30 +587,29 @@ function testBlobInterface(blobbyConstructor, hasBlobFn) { if (withGC) gc(); }); - it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> arrayBuffer${ - withGC ? " (with gc) " : "" - }`, async () => { - if (withGC) gc(); + it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> arrayBuffer${withGC ? " (with gc) " : "" + }`, async () => { + if (withGC) gc(); - var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject))); - if (withGC) gc(); + var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject))); + if (withGC) gc(); - const bytes = new TextEncoder().encode(JSON.stringify(jsonObject)); - if (withGC) gc(); + const bytes = new TextEncoder().encode(JSON.stringify(jsonObject)); + if (withGC) gc(); - const compare = new Uint8Array(await response.arrayBuffer()); - if (withGC) gc(); + const compare = new Uint8Array(await response.arrayBuffer()); + if (withGC) gc(); - withoutAggressiveGC(() => { - for (let i = 0; i < compare.length; i++) { - if (withGC) gc(); + withoutAggressiveGC(() => { + for (let i = 0; i < compare.length; i++) { + if (withGC) gc(); - expect(compare[i]).toBe(bytes[i]); - if (withGC) gc(); - } + expect(compare[i]).toBe(bytes[i]); + if (withGC) gc(); + } + }); + if (withGC) gc(); }); - if (withGC) gc(); - }); hasBlobFn && it(`${jsonObject.hello === true ? "latin1" : "utf16"} blob${withGC ? " (with gc) " : ""}`, async () => { @@ -445,7 +666,7 @@ describe("Bun.file", () => { it("size is Infinity on a fifo", () => { try { unlinkSync("/tmp/test-fifo"); - } catch (e) {} + } catch (e) { } mkfifo("/tmp/test-fifo"); const { size } = Bun.file("/tmp/test-fifo"); @@ -463,14 +684,14 @@ describe("Bun.file", () => { beforeAll(async () => { try { unlinkSync("/tmp/my-new-file"); - } catch {} + } catch { } await Bun.write("/tmp/my-new-file", "hey"); chmodSync("/tmp/my-new-file", 0o000); }); afterAll(() => { try { unlinkSync("/tmp/my-new-file"); - } catch {} + } catch { } }); forEachMethod(m => () => { @@ -483,7 +704,7 @@ describe("Bun.file", () => { beforeAll(() => { try { unlinkSync("/tmp/does-not-exist"); - } catch {} + } catch { } }); forEachMethod(m => async () => { @@ -732,10 +953,14 @@ describe("Request", () => { body: "<div>hello</div>", }); gc(); + expect(body.signal).toBeDefined(); + gc(); expect(body.headers.get("content-type")).toBe("text/html; charset=utf-8"); gc(); var clone = body.clone(); gc(); + expect(clone.signal).toBeDefined(); + gc(); body.headers.set("content-type", "text/plain"); gc(); expect(clone.headers.get("content-type")).toBe("text/html; charset=utf-8"); @@ -743,9 +968,33 @@ describe("Request", () => { expect(body.headers.get("content-type")).toBe("text/plain"); gc(); expect(await clone.text()).toBe("<div>hello</div>"); - gc(); }); + it("signal", async () => { + gc(); + const controller = new AbortController(); + const req = new Request("https://hello.com", { signal: controller.signal }) + expect(req.signal.aborted).toBe(false); + gc(); + controller.abort(); + gc(); + expect(req.signal.aborted).toBe(true); + }) + + it("cloned signal", async () => { + gc(); + const controller = new AbortController(); + const req = new Request("https://hello.com", { signal: controller.signal }) + expect(req.signal.aborted).toBe(false); + gc(); + controller.abort(); + gc(); + expect(req.signal.aborted).toBe(true); + gc(); + const cloned = req.clone(); + expect(cloned.signal.aborted).toBe(true); + }) + testBlobInterface(data => new Request("https://hello.com", { body: data }), true); }); |