diff options
Diffstat (limited to '')
-rw-r--r-- | examples/http-stop.ts | 12 | ||||
-rw-r--r-- | examples/http.ts | 1 | ||||
-rw-r--r-- | packages/bun-types/types.d.ts | 147 | ||||
-rw-r--r-- | src/deps/uws.zig | 3 | ||||
-rw-r--r-- | src/http.zig | 2 | ||||
-rw-r--r-- | src/http_client_async.zig | 1 | ||||
-rw-r--r-- | src/javascript/jsc/api/bun.zig | 44 | ||||
-rw-r--r-- | src/javascript/jsc/api/server.zig | 256 | ||||
-rw-r--r-- | src/javascript/jsc/base.zig | 16 | ||||
-rw-r--r-- | src/javascript/jsc/javascript.zig | 38 | ||||
-rw-r--r-- | types/bun/bun.d.ts | 22 |
11 files changed, 478 insertions, 64 deletions
diff --git a/examples/http-stop.ts b/examples/http-stop.ts new file mode 100644 index 000000000..64d3abfa1 --- /dev/null +++ b/examples/http-stop.ts @@ -0,0 +1,12 @@ +const server = Bun.serve({ + fetch(req: Request) { + return new Response(`Pending requests: ${this?.pendingRequests ?? 0}`); + }, +}); + +setTimeout(() => { + // stop the server after the first request + // when the server is stopped, this becomes undefined + server?.stop(); + console.log("Stopping the server..."); +}, 1000); diff --git a/examples/http.ts b/examples/http.ts index 97caf6bfc..8b17c320c 100644 --- a/examples/http.ts +++ b/examples/http.ts @@ -21,7 +21,6 @@ Bun.serve({ port: 3000, // number or string }); - // Start a fast HTTP server from the main file's export // export default { // fetch(req) { diff --git a/packages/bun-types/types.d.ts b/packages/bun-types/types.d.ts index 792ec33a3..731454297 100644 --- a/packages/bun-types/types.d.ts +++ b/packages/bun-types/types.d.ts @@ -66,7 +66,7 @@ declare module "bun" { * }); * ``` */ - export function serve(options: Serve): void; + export function serve(options: Serve): Server; /** * Synchronously resolve a `moduleId` as though it were imported from `parent` @@ -521,9 +521,10 @@ declare module "bun" { * Respond to {@link Request} objects with a {@link Response} object. * */ - fetch(request: Request): Response | Promise<Response>; + fetch(this: Server, request: Request): Response | Promise<Response>; error?: ( + this: Server, request: Errorlike ) => Response | Promise<Response> | undefined | Promise<undefined>; } @@ -571,6 +572,23 @@ declare module "bun" { serverNames: Record<string, SSLOptions & SSLAdvancedOptions>; }; + interface Server { + /** + * Stop listening to prevent new connections from being accepted. + * + * It does not close existing connections. + */ + stop(): void; + + /** + * How many requests are in-flight right now? + */ + readonly pendingRequests: number; + readonly port: number; + readonly hostname: string; + readonly development: boolean; + } + export type Serve = SSLServeOptions | ServeOptions; /** @@ -4724,7 +4742,7 @@ interface Process { setuid(id: number | string): void; } -declare let process: Process; +declare var process: Process; interface BlobInterface { text(): Promise<string>; @@ -4765,7 +4783,7 @@ interface Headers { ): void; } -declare let Headers: { +declare var Headers: { prototype: Headers; new (init?: HeadersInit): Headers; }; @@ -5223,7 +5241,7 @@ interface Crypto { randomUUID(): string; } -declare let crypto: Crypto; +declare var crypto: Crypto; /** * [`atob`](https://developer.mozilla.org/en-US/docs/Web/API/atob) converts ascii text into base64. @@ -5603,7 +5621,7 @@ interface EventTarget { ): void; } -declare let EventTarget: { +declare var EventTarget: { prototype: EventTarget; new (): EventTarget; }; @@ -5707,7 +5725,7 @@ interface Event { readonly NONE: number; } -declare let Event: { +declare var Event: { prototype: Event; new (type: string, eventInitDict?: EventInit): Event; readonly AT_TARGET: number; @@ -5727,7 +5745,7 @@ interface ErrorEvent extends Event { readonly message: string; } -declare let ErrorEvent: { +declare var ErrorEvent: { prototype: ErrorEvent; new (type: string, eventInitDict?: ErrorEventInit): ErrorEvent; }; @@ -5775,7 +5793,7 @@ interface URLSearchParams { ): void; } -declare let URLSearchParams: { +declare var URLSearchParams: { prototype: URLSearchParams; new ( init?: string[][] | Record<string, string> | string | URLSearchParams @@ -5783,7 +5801,7 @@ declare let URLSearchParams: { toString(): string; }; -declare let URL: { +declare var URL: { prototype: URL; new (url: string | URL, base?: string | URL): URL; /** Not implemented yet */ @@ -5802,7 +5820,7 @@ interface EventListenerObject { handleEvent(object: Event): void; } -declare let AbortController: { +declare var AbortController: { prototype: AbortController; new (): AbortController; }; @@ -5859,7 +5877,7 @@ interface AbortSignal extends EventTarget { ): void; } -declare let AbortSignal: { +declare var AbortSignal: { prototype: AbortSignal; new (): AbortSignal; }; @@ -5878,6 +5896,111 @@ type DOMHighResTimeStamp = number; // type EpochTimeStamp = number; type EventListenerOrEventListenerObject = EventListener | EventListenerObject; +/** + * Low-level JavaScriptCore API for accessing the native ES Module loader (not a Bun API) + * + * Before using this, be aware of a few things: + * + * **Using this incorrectly will crash your application**. + * + * This API may change any time JavaScriptCore is updated. + * + * Bun may rewrite ESM import specifiers to point to bundled code. This will + * be confusing when using this API, as it will return a string like + * "/node_modules.server.bun". + * + * Bun may inject additional imports into your code. This usually has a `bun:` prefix. + * + */ +declare var Loader: { + /** + * ESM module registry + * + * This lets you implement live reload in Bun. If you + * delete a module specifier from this map, the next time it's imported, it + * will be re-transpiled and loaded again. + * + * The keys are the module specifiers and the + * values are metadata about the module. + * + * The keys are an implementation detail for Bun that will change between + * versions. + * + * - Userland modules are an absolute file path + * - Virtual modules have a `bun:` prefix or `node:` prefix + * - JS polyfills start with `"/bun-vfs/"`. `"buffer"` is an example of a JS polyfill + * - If you have a `node_modules.bun` file, many modules will point to that file + * + * Virtual modules and JS polyfills are embedded in bun's binary. They don't + * point to anywhere in your local filesystem. + * + * + */ + registry: Map< + string, + { + /** + * This refers to the state the ESM module is in + * + * TODO: make an enum for this number + * + * + */ + state: number; + dependencies: string[]; + /** + * Your application will probably crash if you mess with this. + */ + module: any; + } + >; + /** + * For an already-evaluated module, return the dependencies as module specifiers + * + * This list is already sorted and uniqued. + * + * @example + * + * For this code: + * ```js + * // /foo.js + * import classNames from 'classnames'; + * import React from 'react'; + * import {createElement} from 'react'; + * ``` + * + * This would return: + * ```js + * Loader.dependencyKeysIfEvaluated("/foo.js") + * ["bun:wrap", "/path/to/node_modules/classnames/index.js", "/path/to/node_modules/react/index.js"] + * ``` + * + * @param specifier - module specifier as it appears in transpiled source code + * + */ + dependencyKeysIfEvaluated: (specifier: string) => string[]; + /** + * The function JavaScriptCore internally calls when you use an import statement. + * + * This may return a path to `node_modules.server.bun`, which will be confusing. + * + * Consider {@link Bun.resolve} or {@link ImportMeta.resolve} + * instead. + * + * @param specifier - module specifier as it appears in transpiled source code + */ + resolve: (specifier: string) => Promise<string>; + /** + * Synchronously resolve a module specifier + * + * This may return a path to `node_modules.server.bun`, which will be confusing. + * + * Consider {@link Bun.resolveSync} + * instead. + */ + resolveSync: (specifier: string, from: string) => string; +}; + // ./path.d.ts diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 6c0ae02c0..900cb4fae 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -20,7 +20,8 @@ pub const Loop = opaque { pub fn nextTick(this: *Loop, comptime UserType: type, user_data: UserType, comptime deferCallback: fn (ctx: UserType) void) void { const Handler = struct { pub fn callback(data: *anyopaque) callconv(.C) void { - deferCallback(@ptrCast(UserType, @alignCast(@alignOf(UserType), data))); + const std = @import("std"); + deferCallback(@ptrCast(UserType, @alignCast(@alignOf(std.meta.Child(UserType)), data))); } }; uws_loop_defer(this, user_data, Handler.callback); diff --git a/src/http.zig b/src/http.zig index 068bfa356..816ee655a 100644 --- a/src/http.zig +++ b/src/http.zig @@ -675,8 +675,6 @@ pub const RequestContext = struct { const AsyncIO = @import("io"); pub fn writeSocket(ctx: *RequestContext, buf: anytype, _: anytype) !usize { - // ctx.conn.client.setWriteBufferSize(@intCast(u32, buf.len)) catch {}; - switch (Syscall.send(ctx.conn.client.socket.fd, buf, SOCKET_FLAGS)) { .err => |err| { const erro = AsyncIO.asError(err.getErrno()); diff --git a/src/http_client_async.zig b/src/http_client_async.zig index 5210fe39f..43f710838 100644 --- a/src/http_client_async.zig +++ b/src/http_client_async.zig @@ -456,6 +456,7 @@ pub const AsyncHTTP = struct { outer: { this.err = null; this.state.store(.sending, .Monotonic); + var timer = std.time.Timer.start() catch @panic("Timer failure"); defer this.elapsed = timer.read(); diff --git a/src/javascript/jsc/api/bun.zig b/src/javascript/jsc/api/bun.zig index 8826acc2e..f0d483e1f 100644 --- a/src/javascript/jsc/api/bun.zig +++ b/src/javascript/jsc/api/bun.zig @@ -1191,25 +1191,67 @@ pub fn serve( return null; } + // Listen happens on the next tick! + // This is so we can return a Server object if (config.ssl_config != null) { if (config.development) { var server = JSC.API.DebugSSLServer.init(config, ctx.ptr()); server.listen(); + if (!server.thisObject.isEmpty()) { + exception.* = server.thisObject.asObjectRef(); + server.thisObject = JSC.JSValue.zero; + server.deinit(); + return null; + } + var obj = JSC.API.DebugSSLServer.Class.make(ctx, server); + JSC.C.JSValueProtect(ctx, obj); + server.thisObject = JSValue.c(obj); + return obj; } else { var server = JSC.API.SSLServer.init(config, ctx.ptr()); server.listen(); + if (!server.thisObject.isEmpty()) { + exception.* = server.thisObject.asObjectRef(); + server.thisObject = JSC.JSValue.zero; + server.deinit(); + return null; + } + var obj = JSC.API.SSLServer.Class.make(ctx, server); + JSC.C.JSValueProtect(ctx, obj); + server.thisObject = JSValue.c(obj); + return obj; } } else { if (config.development) { var server = JSC.API.DebugServer.init(config, ctx.ptr()); server.listen(); + if (!server.thisObject.isEmpty()) { + exception.* = server.thisObject.asObjectRef(); + server.thisObject = JSC.JSValue.zero; + server.deinit(); + return null; + } + var obj = JSC.API.DebugServer.Class.make(ctx, server); + JSC.C.JSValueProtect(ctx, obj); + server.thisObject = JSValue.c(obj); + return obj; } else { var server = JSC.API.Server.init(config, ctx.ptr()); server.listen(); + if (!server.thisObject.isEmpty()) { + exception.* = server.thisObject.asObjectRef(); + server.thisObject = JSC.JSValue.zero; + server.deinit(); + return null; + } + var obj = JSC.API.Server.Class.make(ctx, server); + JSC.C.JSValueProtect(ctx, obj); + server.thisObject = JSValue.c(obj); + return obj; } } - return JSC.JSValue.jsUndefined().asObjectRef(); + unreachable; } pub fn allocUnsafe( diff --git a/src/javascript/jsc/api/server.zig b/src/javascript/jsc/api/server.zig index 2d5462c77..dc990b9eb 100644 --- a/src/javascript/jsc/api/server.zig +++ b/src/javascript/jsc/api/server.zig @@ -477,12 +477,21 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp return; } - if (arguments.len == 0 or arguments[0].isEmptyOrUndefinedOrNull()) { + if (arguments.len == 0) { ctx.renderMissing(); return; } - var response = arguments[0].as(JSC.WebCore.Response) orelse { + handleResolve(ctx, arguments[0]); + } + + fn handleResolve(ctx: *RequestContext, value: JSC.JSValue) void { + if (value.isEmptyOrUndefinedOrNull()) { + ctx.renderMissing(); + return; + } + + var response = value.as(JSC.WebCore.Response) orelse { Output.prettyErrorln("Expected a Response object", .{}); Output.flush(); ctx.renderMissing(); @@ -501,8 +510,12 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp return; } + handleReject(ctx, if (arguments.len > 0) arguments[0] else JSC.JSValue.jsUndefined()); + } + + fn handleReject(ctx: *RequestContext, value: JSC.JSValue) void { ctx.runErrorHandler( - if (arguments.len > 0) arguments[0] else JSC.JSValue.jsUndefined(), + value, ); if (ctx.aborted) { @@ -516,7 +529,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } pub fn renderMissing(ctx: *RequestContext) void { - if (debug_mode) { + if (comptime !debug_mode) { ctx.resp.writeStatus("204 No Content"); ctx.resp.endWithoutBody(); ctx.finalize(); @@ -646,9 +659,11 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp this.response_buf_owned.clearAndFree(bun.default_allocator); } pub fn finalize(this: *RequestContext) void { + var server = this.server; this.finalizeWithoutDeinit(); - - this.server.request_pool_allocator.destroy(this); + std.debug.assert(server.pending_requests > 0); + server.request_pool_allocator.destroy(this); + server.onRequestComplete(); } fn writeHeaders( @@ -671,7 +686,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } } - fn cleanupAfterSendfile(this: *RequestContext) void { + fn cleanupAndFinalizeAfterSendfile(this: *RequestContext) void { this.resp.setWriteOffset(this.sendfile.offset); this.resp.endWithoutBody(); // use node syscall so that we don't segfault on BADF @@ -686,6 +701,11 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp }}; pub fn onSendfile(this: *RequestContext) bool { + if (this.aborted) { + this.cleanupAndFinalizeAfterSendfile(); + return false; + } + const adjusted_count_temporary = @minimum(@as(u64, this.sendfile.remain), @as(u63, std.math.maxInt(u63))); // TODO we should not need this int cast; improve the return type of `@minimum` const adjusted_count = @intCast(u63, adjusted_count_temporary); @@ -707,7 +727,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp Output.prettyErrorln("Error: {s}", .{@tagName(errcode)}); Output.flush(); } - this.cleanupAfterSendfile(); + this.cleanupAndFinalizeAfterSendfile(); return errcode != .SUCCESS; } } else { @@ -731,7 +751,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp Output.prettyErrorln("Error: {s}", .{@tagName(errcode)}); Output.flush(); } - this.cleanupAfterSendfile(); + this.cleanupAndFinalizeAfterSendfile(); return errcode == .SUCCESS; } } @@ -774,7 +794,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } fn onPrepareSendfile(this: *RequestContext, fd: i32, size: Blob.SizeType, err: ?JSC.SystemError, globalThis: *JSGlobalObject) void { - this.setAbortHandler(); + if (this.aborted) { + this.finalize(); + return; + } if (err) |system_error| { if (system_error.errno == @enumToInt(std.os.E.NOENT)) { @@ -796,7 +819,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp }; if (this.aborted) { - _ = JSC.Node.Syscall.close(this.sendfile.fd); this.finalize(); return; } @@ -804,12 +826,12 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp this.resp.runCorked(*RequestContext, renderMetadata, this); if (size == 0) { - this.cleanupAfterSendfile(); - this.finalize(); - + this.cleanupAndFinalizeAfterSendfile(); return; } + this.setAbortHandler(); + // TODO: fix this to be MSGHDR _ = std.os.write(this.sendfile.socket_fd, "\r\n") catch 0; @@ -828,6 +850,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp pub fn doSendfile(this: *RequestContext, blob: Blob) void { if (this.has_sendfile_ctx) return; + if (this.aborted) { + this.finalize(); + return; + } this.has_sendfile_ctx = true; this.setAbortHandler(); @@ -947,7 +973,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp if (!this.server.config.onError.isEmpty() and !this.has_called_error_handler) { this.has_called_error_handler = true; var args = [_]JSC.C.JSValueRef{value.asObjectRef()}; - const result = JSC.C.JSObjectCallAsFunctionReturnValue(this.server.globalThis.ref(), this.server.config.onError.asObjectRef(), null, 1, &args); + const result = JSC.C.JSObjectCallAsFunctionReturnValue(this.server.globalThis.ref(), this.server.config.onError.asObjectRef(), this.server.thisObject.asObjectRef(), 1, &args); if (!result.isEmptyOrUndefinedOrNull()) { if (result.isError() or result.isAggregateError(this.server.globalThis)) { @@ -1152,13 +1178,124 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { pub const App = uws.NewApp(ssl_enabled); listener: ?*App.ListenSocket = null, - + thisObject: JSC.JSValue = JSC.JSValue.zero, app: *App = undefined, + vm: *JSC.VirtualMachine = undefined, globalThis: *JSGlobalObject, base_url_string_for_joining: string = "", response_objects_pool: JSC.WebCore.Response.Pool = JSC.WebCore.Response.Pool{}, config: ServerConfig = ServerConfig{}, + next_tick_pending: bool = false, + pending_requests: usize = 0, request_pool_allocator: std.mem.Allocator = undefined, + has_js_deinited: bool = false, + listen_callback: JSC.AnyTask = undefined, + + pub const Class = JSC.NewClass( + ThisServer, + .{ .name = "Server" }, + .{ + .stop = .{ + .rfn = JSC.wrapSync(ThisServer, "stopFromJS"), + }, + .finalize = .{ + .rfn = finalize, + }, + }, + .{ + .port = .{ + .get = JSC.getterWrap(ThisServer, "getPort"), + }, + .hostname = .{ + .get = JSC.getterWrap(ThisServer, "getHostname"), + }, + .development = .{ + .get = JSC.getterWrap(ThisServer, "getDevelopment"), + }, + .pendingRequests = .{ + .get = JSC.getterWrap(ThisServer, "getPendingRequests"), + }, + }, + ); + + pub fn stopFromJS(this: *ThisServer) JSC.JSValue { + if (this.listener != null) { + JSC.C.JSValueUnprotect(this.globalThis.ref(), this.thisObject.asObjectRef()); + this.thisObject = JSC.JSValue.jsUndefined(); + this.stop(); + } + + return JSC.JSValue.jsUndefined(); + } + + pub fn getPort(this: *ThisServer) JSC.JSValue { + return JSC.JSValue.jsNumber(this.config.port); + } + + pub fn getPendingRequests(this: *ThisServer) JSC.JSValue { + return JSC.JSValue.jsNumber(@intCast(i32, @truncate(u31, this.pending_requests))); + } + + pub fn getHostname(this: *ThisServer, globalThis: *JSGlobalObject) JSC.JSValue { + return ZigString.init(this.config.base_uri).toValue(globalThis); + } + + pub fn getDevelopment( + _: *ThisServer, + ) JSC.JSValue { + return JSC.JSValue.jsBoolean(debug_mode); + } + + pub fn onRequestComplete(this: *ThisServer) void { + this.pending_requests -= 1; + this.deinitIfWeCan(); + } + + pub fn finalize(this: *ThisServer) void { + this.has_js_deinited = true; + this.deinitIfWeCan(); + } + + pub fn deinitIfWeCan(this: *ThisServer) void { + if (this.pending_requests == 0 and this.listener == null and this.has_js_deinited) + this.deinit(); + } + + pub fn stop(this: *ThisServer) void { + this.next_tick_pending = true; + + if (this.listener) |listener| { + listener.close(); + this.listener = null; + } + + this.deinitIfWeCan(); + } + + pub fn deinit(this: *ThisServer) void { + if (this.vm.response_objects_pool) |pool| { + if (pool == &this.response_objects_pool) { + this.vm.response_objects_pool = null; + } + } + + this.app.destroy(); + bun.default_allocator.destroy(this); + } + + pub fn nextTick(this: *ThisServer) void { + std.debug.assert(this.next_tick_pending); + + this.next_tick_pending = false; + this.vm.tick(); + } + + pub fn queueNextTick(this: *ThisServer) void { + std.debug.assert(!this.next_tick_pending); + + this.next_tick_pending = true; + uws.Loop.get().?.nextTick(*ThisServer, this, nextTick); + } pub fn init(config: ServerConfig, globalThis: *JSGlobalObject) *ThisServer { var server = bun.default_allocator.create(ThisServer) catch @panic("Out of memory!"); @@ -1166,6 +1303,7 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { .globalThis = globalThis, .config = config, .base_url_string_for_joining = strings.trim(config.base_url.href, "/"), + .vm = JSC.VirtualMachine.vm, }; RequestContext.pool = bun.default_allocator.create(RequestContext.RequestContextStackAllocator) catch @panic("Out of memory!"); server.request_pool_allocator = RequestContext.pool.get(); @@ -1227,7 +1365,8 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { zig_str.withEncoding().mark(); } } - JSC.VirtualMachine.vm.defaultErrorHandler(zig_str.toErrorInstance(this.globalThis), null); + // store the exception in here + this.thisObject = zig_str.toErrorInstance(this.globalThis); return; } @@ -1237,14 +1376,20 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { } this.listener = socket; - VirtualMachine.vm.uws_event_loop = uws.Loop.get(); - VirtualMachine.vm.response_objects_pool = &this.response_objects_pool; + this.vm.uws_event_loop = uws.Loop.get(); + this.vm.response_objects_pool = &this.response_objects_pool; + this.listen_callback = JSC.AnyTask.New(ThisServer, run).init(this); + this.vm.eventLoop().enqueueTask(JSC.Task.init(&this.listen_callback)); + } + pub fn run(this: *ThisServer) void { this.app.run(); } - pub fn onBunInfoRequest(_: *ThisServer, req: *uws.Request, resp: *App.Response) void { + pub fn onBunInfoRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void { if (comptime JSC.is_bindgen) return undefined; + this.pending_requests += 1; + defer this.pending_requests -= 1; req.setYield(false); var stack_fallback = std.heap.stackFallback(8096, bun.default_allocator); var allocator = stack_fallback.get(); @@ -1268,8 +1413,10 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { resp.end(buffer, false); } - pub fn onSrcRequest(_: *ThisServer, req: *uws.Request, resp: *App.Response) void { + pub fn onSrcRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void { if (comptime JSC.is_bindgen) return undefined; + this.pending_requests += 1; + defer this.pending_requests -= 1; req.setYield(false); if (req.header("open-in-editor") == null) { resp.writeStatus("501 Not Implemented"); @@ -1298,7 +1445,8 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { pub fn onRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void { if (comptime JSC.is_bindgen) return undefined; - + this.pending_requests += 1; + var vm = this.vm; req.setYield(false); var ctx = this.request_pool_allocator.create(RequestContext) catch @panic("ran out of memory"); ctx.create(this, req, resp); @@ -1321,11 +1469,16 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { var args = [_]JSC.C.JSValueRef{JSC.WebCore.Request.Class.make(this.globalThis.ref(), request_object)}; ctx.request_js_object = args[0]; JSC.C.JSValueProtect(this.globalThis.ref(), args[0]); - ctx.response_jsvalue = JSC.C.JSObjectCallAsFunctionReturnValue(this.globalThis.ref(), this.config.onRequest.asObjectRef(), null, 1, &args); - defer JSC.VirtualMachine.vm.tick(); + ctx.response_jsvalue = JSC.C.JSObjectCallAsFunctionReturnValue(this.globalThis.ref(), this.config.onRequest.asObjectRef(), this.thisObject.asObjectRef(), 1, &args); + var needs_tick = false; + + defer if (!this.next_tick_pending and (needs_tick or + // this is evaluated _after_ this function call + vm.eventLoop().pending_tasks_count.value > 0)) + this.queueNextTick(); + if (ctx.aborted) { ctx.finalize(); - return; } @@ -1340,17 +1493,48 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { } JSC.C.JSValueProtect(this.globalThis.ref(), ctx.response_jsvalue.asObjectRef()); - if (ctx.response_jsvalue.as(JSC.WebCore.Response)) |response| { ctx.render(response); - return; } - if (ctx.response_jsvalue.jsTypeLoose() == .JSPromise) { - ctx.setAbortHandler(); - JSC.VirtualMachine.vm.tick(); + var wait_for_promise = false; + + if (ctx.response_jsvalue.asPromise()) |promise| { + // If we immediately have the value available, we can skip the extra event loop tick + switch (promise.status(vm.global.vm())) { + .Pending => {}, + .Fulfilled => { + ctx.handleResolve(promise.result(vm.global.vm())); + return; + }, + .Rejected => { + ctx.handleReject(promise.result(vm.global.vm())); + return; + }, + } + wait_for_promise = true; + needs_tick = true; + // I don't think this case should happen + // But I'm uncertain + } else if (ctx.response_jsvalue.asInternalPromise()) |promise| { + switch (promise.status(vm.global.vm())) { + .Pending => {}, + .Fulfilled => { + ctx.handleResolve(promise.result(vm.global.vm())); + return; + }, + .Rejected => { + ctx.handleReject(promise.result(vm.global.vm())); + return; + }, + } + wait_for_promise = true; + needs_tick = true; + } + if (wait_for_promise) { + ctx.setAbortHandler(); ctx.response_jsvalue.then( this.globalThis, RequestContext, @@ -1361,11 +1545,8 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { return; } - // switch (ctx.response_jsvalue.jsTypeLoose()) { - // .JSPromise => { - // JSPromise. - // }, - // } + // The user returned something that wasn't a promise or a promise with a response + if (!ctx.resp.hasResponded()) ctx.renderMissing(); } pub fn listen(this: *ThisServer) void { @@ -1404,8 +1585,7 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { }; } -pub const Server = NewServer(false, true); -pub const SSLServer = NewServer(true, true); - +pub const Server = NewServer(false, false); +pub const SSLServer = NewServer(true, false); pub const DebugServer = NewServer(false, true); pub const DebugSSLServer = NewServer(true, true); diff --git a/src/javascript/jsc/base.zig b/src/javascript/jsc/base.zig index 115315566..70feecb76 100644 --- a/src/javascript/jsc/base.zig +++ b/src/javascript/jsc/base.zig @@ -2548,6 +2548,10 @@ const EndTag = JSC.Cloudflare.EndTag; const DocEnd = JSC.Cloudflare.DocEnd; const AttributeIterator = JSC.Cloudflare.AttributeIterator; const Blob = JSC.WebCore.Blob; +const Server = JSC.API.Server; +const SSLServer = JSC.API.SSLServer; +const DebugServer = JSC.API.DebugServer; +const DebugSSLServer = JSC.API.DebugSSLServer; pub const JSPrivateDataPtr = TaggedPointerUnion(.{ AttributeIterator, @@ -2556,6 +2560,8 @@ pub const JSPrivateDataPtr = TaggedPointerUnion(.{ Body, BuildError, Comment, + DebugServer, + DebugSSLServer, DescribeScope, DirEnt, DocEnd, @@ -2575,6 +2581,8 @@ pub const JSPrivateDataPtr = TaggedPointerUnion(.{ ResolveError, Response, Router, + Server, + SSLServer, Stats, TextChunk, TextDecoder, @@ -2605,6 +2613,7 @@ pub fn getterWrap(comptime Container: type, comptime name: string) GetterType(Co return struct { const FunctionType = @TypeOf(@field(Container, name)); const FunctionTypeInfo: std.builtin.TypeInfo.Fn = @typeInfo(FunctionType).Fn; + const ArgsTuple = std.meta.ArgsTuple(FunctionType); pub fn callback( this: *Container, @@ -2613,7 +2622,12 @@ pub fn getterWrap(comptime Container: type, comptime name: string) GetterType(Co _: js.JSStringRef, exception: js.ExceptionRef, ) js.JSObjectRef { - const result: JSValue = @call(.{}, @field(Container, name), .{ this, ctx.ptr() }); + const result: JSValue = if (comptime std.meta.fields(ArgsTuple).len == 1) + @call(.{}, @field(Container, name), .{ + this, + }) + else + @call(.{}, @field(Container, name), .{ this, ctx.ptr() }); if (!result.isUndefinedOrNull() and result.isError()) { exception.* = result.asObjectRef(); return null; diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index e63c94b3b..978349d62 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -312,6 +312,7 @@ pub const Task = TaggedPointerUnion(.{ OpenAndStatFileTask, CopyFilePromiseTask, WriteFileTask, + AnyTask, // PromiseTask, // TimeoutTasklet, }); @@ -433,6 +434,30 @@ pub const SavedSourceMap = struct { }; const uws = @import("uws"); +pub const AnyTask = struct { + ctx: *anyopaque, + callback: fn (*anyopaque) void, + + pub fn run(this: *AnyTask) void { + this.callback(this.ctx); + } + + pub fn New(comptime Type: type, comptime Callback: anytype) type { + return struct { + pub fn init(ctx: *Type) AnyTask { + return AnyTask{ + .callback = wrap, + .ctx = ctx, + }; + } + + pub fn wrap(this: *anyopaque) void { + Callback(@ptrCast(*Type, @alignCast(@alignOf(Type), this))); + } + }; + } +}; + // If you read JavascriptCore/API/JSVirtualMachine.mm - https://github.com/WebKit/WebKit/blob/acff93fb303baa670c055cb24c2bad08691a01a0/Source/JavaScriptCore/API/JSVirtualMachine.mm#L101 // We can see that it's sort of like std.mem.Allocator but for JSGlobalContextRef, to support Automatic Reference Counting // Its unavailable on Linux @@ -583,6 +608,12 @@ pub const VirtualMachine = struct { finished += 1; vm_.active_tasks -|= 1; }, + @field(Task.Tag, @typeName(AnyTask)) => { + var any: *AnyTask = task.get(AnyTask).?; + any.run(); + finished += 1; + vm_.active_tasks -|= 1; + }, else => unreachable, } } @@ -1677,12 +1708,7 @@ pub const VirtualMachine = struct { var promise: *JSInternalPromise = undefined; promise = JSModuleLoader.loadAndEvaluateModule(this.global, &ZigString.init(entry_path)); - - this.tick(); - - while (promise.status(this.global.vm()) == JSPromise.Status.Pending) { - this.tick(); - } + this.waitForPromise(promise); return promise; } diff --git a/types/bun/bun.d.ts b/types/bun/bun.d.ts index 43b08deb8..c9f3ebb5b 100644 --- a/types/bun/bun.d.ts +++ b/types/bun/bun.d.ts @@ -56,7 +56,7 @@ declare module "bun" { * }); * ``` */ - export function serve(options: Serve): void; + export function serve(options: Serve): Server; /** * Synchronously resolve a `moduleId` as though it were imported from `parent` @@ -511,9 +511,10 @@ declare module "bun" { * Respond to {@link Request} objects with a {@link Response} object. * */ - fetch(request: Request): Response | Promise<Response>; + fetch(this: Server, request: Request): Response | Promise<Response>; error?: ( + this: Server, request: Errorlike ) => Response | Promise<Response> | undefined | Promise<undefined>; } @@ -561,6 +562,23 @@ declare module "bun" { serverNames: Record<string, SSLOptions & SSLAdvancedOptions>; }; + interface Server { + /** + * Stop listening to prevent new connections from being accepted. + * + * It does not close existing connections. + */ + stop(): void; + + /** + * How many requests are in-flight right now? + */ + readonly pendingRequests: number; + readonly port: number; + readonly hostname: string; + readonly development: boolean; + } + export type Serve = SSLServeOptions | ServeOptions; /** |