aboutsummaryrefslogtreecommitdiff
path: root/src/bun.js/webcore/response.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/bun.js/webcore/response.zig')
-rw-r--r--src/bun.js/webcore/response.zig4844
1 files changed, 4844 insertions, 0 deletions
diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig
new file mode 100644
index 000000000..017edb805
--- /dev/null
+++ b/src/bun.js/webcore/response.zig
@@ -0,0 +1,4844 @@
+const std = @import("std");
+const Api = @import("../../api/schema.zig").Api;
+const bun = @import("../../global.zig");
+const RequestContext = @import("../../http.zig").RequestContext;
+const MimeType = @import("../../http.zig").MimeType;
+const ZigURL = @import("../../url.zig").URL;
+const HTTPClient = @import("http");
+const NetworkThread = HTTPClient.NetworkThread;
+const AsyncIO = NetworkThread.AsyncIO;
+const JSC = @import("javascript_core");
+const js = JSC.C;
+
+const Method = @import("../../http/method.zig").Method;
+const FetchHeaders = JSC.FetchHeaders;
+const ObjectPool = @import("../../pool.zig").ObjectPool;
+const SystemError = JSC.SystemError;
+const Output = @import("../../global.zig").Output;
+const MutableString = @import("../../global.zig").MutableString;
+const strings = @import("../../global.zig").strings;
+const string = @import("../../global.zig").string;
+const default_allocator = @import("../../global.zig").default_allocator;
+const FeatureFlags = @import("../../global.zig").FeatureFlags;
+const ArrayBuffer = @import("../base.zig").ArrayBuffer;
+const Properties = @import("../base.zig").Properties;
+const NewClass = @import("../base.zig").NewClass;
+const d = @import("../base.zig").d;
+const castObj = @import("../base.zig").castObj;
+const getAllocator = @import("../base.zig").getAllocator;
+const JSPrivateDataPtr = @import("../base.zig").JSPrivateDataPtr;
+const GetJSPrivateData = @import("../base.zig").GetJSPrivateData;
+const Environment = @import("../../env.zig");
+const ZigString = JSC.ZigString;
+const IdentityContext = @import("../../identity_context.zig").IdentityContext;
+const JSInternalPromise = JSC.JSInternalPromise;
+const JSPromise = JSC.JSPromise;
+const JSValue = JSC.JSValue;
+const JSError = JSC.JSError;
+const JSGlobalObject = JSC.JSGlobalObject;
+
+const VirtualMachine = @import("../javascript.zig").VirtualMachine;
+const Task = JSC.Task;
+const JSPrinter = @import("../../js_printer.zig");
+const picohttp = @import("picohttp");
+const StringJoiner = @import("../../string_joiner.zig");
+const uws = @import("uws");
+
+pub const Response = struct {
+ pub const Pool = struct {
+ response_objects_pool: [127]JSC.C.JSObjectRef = undefined,
+ response_objects_used: u8 = 0,
+
+ pub fn get(this: *Pool, ptr: *Response) ?JSC.C.JSObjectRef {
+ if (comptime JSC.is_bindgen)
+ unreachable;
+ if (this.response_objects_used > 0) {
+ var result = this.response_objects_pool[this.response_objects_used - 1];
+ this.response_objects_used -= 1;
+ if (JSC.C.JSObjectSetPrivate(result, JSPrivateDataPtr.init(ptr).ptr())) {
+ return result;
+ } else {
+ JSC.C.JSValueUnprotect(VirtualMachine.vm.global.ref(), result);
+ }
+ }
+
+ return null;
+ }
+
+ pub fn push(this: *Pool, globalThis: *JSC.JSGlobalObject, object: JSC.JSValue) void {
+ var remaining = this.response_objects_pool[@minimum(this.response_objects_used, this.response_objects_pool.len)..];
+ if (remaining.len == 0) {
+ JSC.C.JSValueUnprotect(globalThis.ref(), object.asObjectRef());
+ return;
+ }
+
+ if (object.as(Response)) |resp| {
+ _ = JSC.C.JSObjectSetPrivate(object.asObjectRef(), null);
+
+ _ = resp.body.use();
+ resp.finalize();
+ remaining[0] = object.asObjectRef();
+ this.response_objects_used += 1;
+ }
+ }
+ };
+
+ pub const Constructor = JSC.NewConstructor(
+ Response,
+ .{
+ .@"constructor" = constructor,
+ .@"json" = .{ .rfn = constructJSON },
+ .@"redirect" = .{ .rfn = constructRedirect },
+ .@"error" = .{ .rfn = constructError },
+ },
+ .{},
+ );
+
+ pub const Class = NewClass(
+ Response,
+ .{ .name = "Response" },
+ .{
+ .@"finalize" = finalize,
+ .@"text" = .{
+ .rfn = Response.getText,
+ .ts = d.ts{},
+ },
+ .@"json" = .{
+ .rfn = Response.getJSON,
+ .ts = d.ts{},
+ },
+ .@"arrayBuffer" = .{
+ .rfn = Response.getArrayBuffer,
+ .ts = d.ts{},
+ },
+ .@"blob" = .{
+ .rfn = Response.getBlob,
+ .ts = d.ts{},
+ },
+
+ .@"clone" = .{
+ .rfn = doClone,
+ .ts = d.ts{},
+ },
+ },
+ .{
+ .@"url" = .{
+ .@"get" = getURL,
+ .ro = true,
+ },
+
+ .@"ok" = .{
+ .@"get" = getOK,
+ .ro = true,
+ },
+ .@"status" = .{
+ .@"get" = getStatus,
+ .ro = true,
+ },
+ .@"statusText" = .{
+ .@"get" = getStatusText,
+ .ro = true,
+ },
+ .@"headers" = .{
+ .@"get" = getHeaders,
+ .ro = true,
+ },
+ .@"bodyUsed" = .{
+ .@"get" = getBodyUsed,
+ .ro = true,
+ },
+ .@"type" = .{
+ .@"get" = getResponseType,
+ .ro = true,
+ },
+ },
+ );
+
+ allocator: std.mem.Allocator,
+ body: Body,
+ url: string = "",
+ status_text: string = "",
+ redirected: bool = false,
+
+ pub fn getBodyValue(
+ this: *Response,
+ ) *Body.Value {
+ return &this.body.value;
+ }
+
+ pub inline fn statusCode(this: *const Response) u16 {
+ return this.body.init.status_code;
+ }
+
+ pub fn redirectLocation(this: *const Response) ?[]const u8 {
+ return this.header("location");
+ }
+
+ pub fn header(this: *const Response, comptime name: []const u8) ?[]const u8 {
+ return (this.body.init.headers orelse return null).get(name);
+ }
+
+ pub const Props = struct {};
+
+ pub fn writeFormat(this: *const Response, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
+ const Writer = @TypeOf(writer);
+ try formatter.writeIndent(Writer, writer);
+ try writer.print("Response ({}) {{\n", .{bun.fmt.size(this.body.len())});
+ {
+ formatter.indent += 1;
+ defer formatter.indent -|= 1;
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("ok: ");
+ formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.isOK()), .BooleanObject, enable_ansi_colors);
+ formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
+ try writer.writeAll("\n");
+
+ try this.body.writeFormat(formatter, writer, enable_ansi_colors);
+
+ formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
+ try writer.writeAll("\n");
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("url: \"");
+ try writer.print(comptime Output.prettyFmt("<r><b>{s}<r>", enable_ansi_colors), .{this.url});
+ try writer.writeAll("\"");
+ formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
+ try writer.writeAll("\n");
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("statusText: ");
+ try JSPrinter.writeJSONString(this.status_text, Writer, writer, false);
+ formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
+ try writer.writeAll("\n");
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("redirected: ");
+ formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.redirected), .BooleanObject, enable_ansi_colors);
+ }
+ try writer.writeAll("\n");
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("}");
+ }
+
+ pub fn isOK(this: *const Response) bool {
+ return this.body.init.status_code == 304 or (this.body.init.status_code >= 200 and this.body.init.status_code <= 299);
+ }
+
+ pub fn getURL(
+ this: *Response,
+ ctx: js.JSContextRef,
+ _: js.JSValueRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ // https://developer.mozilla.org/en-US/docs/Web/API/Response/url
+ return ZigString.init(this.url).toValueGC(ctx.ptr()).asObjectRef();
+ }
+
+ pub fn getResponseType(
+ this: *Response,
+ ctx: js.JSContextRef,
+ _: js.JSValueRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ if (this.body.init.status_code < 200) {
+ return ZigString.init("error").toValue(ctx.ptr()).asObjectRef();
+ }
+
+ return ZigString.init("basic").toValue(ctx.ptr()).asObjectRef();
+ }
+
+ pub fn getBodyUsed(
+ this: *Response,
+ _: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return JSC.JSValue.jsBoolean(this.body.value == .Used).asRef();
+ }
+
+ pub fn getStatusText(
+ this: *Response,
+ ctx: js.JSContextRef,
+ _: js.JSValueRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ // https://developer.mozilla.org/en-US/docs/Web/API/Response/url
+ return ZigString.init(this.status_text).withEncoding().toValueGC(ctx.ptr()).asObjectRef();
+ }
+
+ pub fn getOK(
+ this: *Response,
+ ctx: js.JSContextRef,
+ _: js.JSValueRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ // https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
+ return js.JSValueMakeBoolean(ctx, this.isOK());
+ }
+
+ fn getOrCreateHeaders(this: *Response) *FetchHeaders {
+ if (this.body.init.headers == null) {
+ this.body.init.headers = FetchHeaders.createEmpty();
+ }
+ return this.body.init.headers.?;
+ }
+
+ pub fn getHeaders(
+ this: *Response,
+ ctx: js.JSContextRef,
+ _: js.JSValueRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return this.getOrCreateHeaders().toJS(ctx.ptr()).asObjectRef();
+ }
+
+ pub fn doClone(
+ this: *Response,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ var cloned = this.clone(getAllocator(ctx), ctx.ptr());
+ var val = Response.makeMaybePooled(ctx, cloned);
+ if (this.body.init.headers) |headers| {
+ cloned.body.init.headers = headers.cloneThis();
+ }
+
+ return val;
+ }
+
+ pub fn makeMaybePooled(ctx: js.JSContextRef, ptr: *Response) JSC.C.JSObjectRef {
+ if (comptime JSC.is_bindgen)
+ unreachable;
+ var vm = ctx.bunVM();
+ if (vm.response_objects_pool) |pool| {
+ if (pool.get(ptr)) |object| {
+ JSC.C.JSValueUnprotect(ctx, object);
+ return object;
+ }
+ }
+
+ return Response.Class.make(ctx, ptr);
+ }
+
+ pub fn cloneInto(
+ this: *const Response,
+ new_response: *Response,
+ allocator: std.mem.Allocator,
+ globalThis: *JSGlobalObject,
+ ) void {
+ new_response.* = Response{
+ .allocator = allocator,
+ .body = this.body.clone(allocator, globalThis),
+ .url = allocator.dupe(u8, this.url) catch unreachable,
+ .status_text = allocator.dupe(u8, this.status_text) catch unreachable,
+ .redirected = this.redirected,
+ };
+ }
+
+ pub fn clone(this: *const Response, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) *Response {
+ var new_response = allocator.create(Response) catch unreachable;
+ this.cloneInto(new_response, allocator, globalThis);
+ return new_response;
+ }
+
+ pub usingnamespace BlobInterface(@This());
+
+ pub fn getStatus(
+ this: *Response,
+ ctx: js.JSContextRef,
+ _: js.JSValueRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ // https://developer.mozilla.org/en-US/docs/Web/API/Response/status
+ return js.JSValueMakeNumber(ctx, @intToFloat(f64, this.body.init.status_code));
+ }
+
+ pub fn finalize(
+ this: *Response,
+ ) void {
+ this.body.deinit(this.allocator);
+
+ var allocator = this.allocator;
+
+ if (this.status_text.len > 0) {
+ allocator.free(this.status_text);
+ }
+
+ if (this.url.len > 0) {
+ allocator.free(this.url);
+ }
+
+ allocator.destroy(this);
+ }
+
+ pub fn mimeType(response: *const Response, request_ctx_: ?*const RequestContext) string {
+ return mimeTypeWithDefault(response, MimeType.other, request_ctx_);
+ }
+
+ pub fn mimeTypeWithDefault(response: *const Response, default: MimeType, request_ctx_: ?*const RequestContext) string {
+ if (response.header("content-type")) |content_type| {
+ // Remember, we always lowercase it
+ // hopefully doesn't matter here tho
+ return content_type;
+ }
+
+ if (request_ctx_) |request_ctx| {
+ if (request_ctx.url.extname.len > 0) {
+ return MimeType.byExtension(request_ctx.url.extname).value;
+ }
+ }
+
+ switch (response.body.value) {
+ .Blob => |blob| {
+ if (blob.content_type.len > 0) {
+ return blob.content_type;
+ }
+
+ return default.value;
+ },
+ .Used, .Locked, .Empty, .Error => return default.value,
+ }
+ }
+
+ pub fn constructJSON(
+ _: void,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) js.JSObjectRef {
+ // https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4
+ var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments);
+ // var response = getAllocator(ctx).create(Response) catch unreachable;
+
+ var response = Response{
+ .body = Body{
+ .init = Body.Init{
+ .status_code = 200,
+ },
+ .value = Body.Value.empty,
+ },
+ .allocator = getAllocator(ctx),
+ .url = "",
+ };
+
+ const json_value = args.nextEat() orelse JSC.JSValue.zero;
+
+ if (@enumToInt(json_value) != 0) {
+ var zig_str = JSC.ZigString.init("");
+ // calling JSON.stringify on an empty string adds extra quotes
+ // so this is correct
+ json_value.jsonStringify(ctx.ptr(), 0, &zig_str);
+
+ if (zig_str.len > 0) {
+ var zig_str_slice = zig_str.toSlice(getAllocator(ctx));
+
+ if (zig_str_slice.allocated) {
+ response.body.value = .{
+ .Blob = Blob.initWithAllASCII(zig_str_slice.mut(), zig_str_slice.allocator, ctx.ptr(), false),
+ };
+ } else {
+ response.body.value = .{
+ .Blob = Blob.initWithAllASCII(getAllocator(ctx).dupe(u8, zig_str_slice.slice()) catch unreachable, zig_str_slice.allocator, ctx.ptr(), true),
+ };
+ }
+ }
+ }
+
+ if (args.nextEat()) |init| {
+ if (init.isUndefinedOrNull()) {} else if (init.isNumber()) {
+ response.body.init.status_code = @intCast(u16, @minimum(@maximum(0, init.toInt32()), std.math.maxInt(u16)));
+ } else {
+ if (Body.Init.init(getAllocator(ctx), ctx, init.asObjectRef()) catch null) |_init| {
+ response.body.init = _init;
+ }
+ }
+ }
+
+ var headers_ref = response.getOrCreateHeaders();
+ headers_ref.putDefault("content-type", MimeType.json.value);
+ var ptr = response.allocator.create(Response) catch unreachable;
+ ptr.* = response;
+
+ return Response.makeMaybePooled(ctx, ptr);
+ }
+ pub fn constructRedirect(
+ _: void,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) js.JSObjectRef {
+ // https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4
+ var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments);
+ // var response = getAllocator(ctx).create(Response) catch unreachable;
+
+ var response = Response{
+ .body = Body{
+ .init = Body.Init{
+ .status_code = 302,
+ },
+ .value = Body.Value.empty,
+ },
+ .allocator = getAllocator(ctx),
+ .url = "",
+ };
+
+ const url_string_value = args.nextEat() orelse JSC.JSValue.zero;
+ var url_string = ZigString.init("");
+
+ if (@enumToInt(url_string_value) != 0) {
+ url_string = url_string_value.getZigString(ctx.ptr());
+ }
+ var url_string_slice = url_string.toSlice(getAllocator(ctx));
+ defer url_string_slice.deinit();
+
+ if (args.nextEat()) |init| {
+ if (init.isUndefinedOrNull()) {} else if (init.isNumber()) {
+ response.body.init.status_code = @intCast(u16, @minimum(@maximum(0, init.toInt32()), std.math.maxInt(u16)));
+ } else {
+ if (Body.Init.init(getAllocator(ctx), ctx, init.asObjectRef()) catch null) |_init| {
+ response.body.init = _init;
+ }
+ }
+ }
+
+ response.body.init.headers = response.getOrCreateHeaders();
+ response.body.init.status_code = 302;
+ var headers_ref = response.body.init.headers.?;
+ headers_ref.put("location", url_string_slice.slice());
+ var ptr = response.allocator.create(Response) catch unreachable;
+ ptr.* = response;
+
+ return Response.makeMaybePooled(ctx, ptr);
+ }
+ pub fn constructError(
+ _: void,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) js.JSObjectRef {
+ var response = getAllocator(ctx).create(Response) catch unreachable;
+ response.* = Response{
+ .body = Body{
+ .init = Body.Init{
+ .status_code = 0,
+ },
+ .value = Body.Value.empty,
+ },
+ .allocator = getAllocator(ctx),
+ .url = "",
+ };
+
+ return Response.makeMaybePooled(
+ ctx,
+ response,
+ );
+ }
+
+ pub fn constructor(
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) js.JSObjectRef {
+ const body: Body = brk: {
+ switch (arguments.len) {
+ 0 => {
+ break :brk Body.@"200"(ctx);
+ },
+ 1 => {
+ break :brk Body.extract(ctx, arguments[0], exception);
+ },
+ else => {
+ if (js.JSValueGetType(ctx, arguments[1]) == js.JSType.kJSTypeObject) {
+ break :brk Body.extractWithInit(ctx, arguments[0], arguments[1], exception);
+ } else {
+ break :brk Body.extract(ctx, arguments[0], exception);
+ }
+ },
+ }
+ unreachable;
+ };
+
+ var response = getAllocator(ctx).create(Response) catch unreachable;
+ response.* = Response{
+ .body = body,
+ .allocator = getAllocator(ctx),
+ .url = "",
+ };
+ return Response.makeMaybePooled(
+ ctx,
+ response,
+ );
+ }
+};
+
+const null_fd = std.math.maxInt(JSC.Node.FileDescriptor);
+
+pub const Fetch = struct {
+ const headers_string = "headers";
+ const method_string = "method";
+
+ var fetch_body_string: MutableString = undefined;
+ var fetch_body_string_loaded = false;
+
+ const JSType = js.JSType;
+
+ const fetch_error_no_args = "fetch() expects a string but received no arguments.";
+ const fetch_error_blank_url = "fetch() URL must not be a blank string.";
+ const JSTypeErrorEnum = std.enums.EnumArray(JSType, string);
+ const fetch_type_error_names: JSTypeErrorEnum = brk: {
+ var errors = JSTypeErrorEnum.initUndefined();
+ errors.set(JSType.kJSTypeUndefined, "Undefined");
+ errors.set(JSType.kJSTypeNull, "Null");
+ errors.set(JSType.kJSTypeBoolean, "Boolean");
+ errors.set(JSType.kJSTypeNumber, "Number");
+ errors.set(JSType.kJSTypeString, "String");
+ errors.set(JSType.kJSTypeObject, "Object");
+ errors.set(JSType.kJSTypeSymbol, "Symbol");
+ break :brk errors;
+ };
+
+ const fetch_type_error_string_values = .{
+ std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeUndefined)}),
+ std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeNull)}),
+ std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeBoolean)}),
+ std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeNumber)}),
+ std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeString)}),
+ std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeObject)}),
+ std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeSymbol)}),
+ };
+
+ const fetch_type_error_strings: JSTypeErrorEnum = brk: {
+ var errors = JSTypeErrorEnum.initUndefined();
+ errors.set(
+ JSType.kJSTypeUndefined,
+ std.mem.span(fetch_type_error_string_values[0]),
+ );
+ errors.set(
+ JSType.kJSTypeNull,
+ std.mem.span(fetch_type_error_string_values[1]),
+ );
+ errors.set(
+ JSType.kJSTypeBoolean,
+ std.mem.span(fetch_type_error_string_values[2]),
+ );
+ errors.set(
+ JSType.kJSTypeNumber,
+ std.mem.span(fetch_type_error_string_values[3]),
+ );
+ errors.set(
+ JSType.kJSTypeString,
+ std.mem.span(fetch_type_error_string_values[4]),
+ );
+ errors.set(
+ JSType.kJSTypeObject,
+ std.mem.span(fetch_type_error_string_values[5]),
+ );
+ errors.set(
+ JSType.kJSTypeSymbol,
+ std.mem.span(fetch_type_error_string_values[6]),
+ );
+ break :brk errors;
+ };
+
+ pub const Class = NewClass(
+ void,
+ .{ .name = "fetch" },
+ .{
+ .@"call" = .{
+ .rfn = Fetch.call,
+ .ts = d.ts{},
+ },
+ },
+ .{},
+ );
+
+ pub const FetchTasklet = struct {
+ promise: *JSInternalPromise = undefined,
+ http: HTTPClient.AsyncHTTP = undefined,
+ status: Status = Status.pending,
+ javascript_vm: *VirtualMachine = undefined,
+ global_this: *JSGlobalObject = undefined,
+
+ empty_request_body: MutableString = undefined,
+ // pooled_body: *BodyPool.Node = undefined,
+ this_object: js.JSObjectRef = null,
+ resolve: js.JSObjectRef = null,
+ reject: js.JSObjectRef = null,
+ context: FetchTaskletContext = undefined,
+ response_buffer: MutableString = undefined,
+
+ blob_store: ?*Blob.Store = null,
+
+ const Pool = ObjectPool(FetchTasklet, init, true, 32);
+ const BodyPool = ObjectPool(MutableString, MutableString.init2048, true, 8);
+ pub const FetchTaskletContext = struct {
+ tasklet: *FetchTasklet,
+ };
+
+ pub fn init(_: std.mem.Allocator) anyerror!FetchTasklet {
+ return FetchTasklet{};
+ }
+
+ pub const Status = enum(u8) {
+ pending,
+ running,
+ done,
+ };
+
+ pub fn onDone(this: *FetchTasklet) void {
+ if (comptime JSC.is_bindgen)
+ unreachable;
+ var args = [1]js.JSValueRef{undefined};
+
+ var callback_object = switch (this.http.state.load(.Monotonic)) {
+ .success => this.resolve,
+ .fail => this.reject,
+ else => unreachable,
+ };
+
+ args[0] = switch (this.http.state.load(.Monotonic)) {
+ .success => this.onResolve().asObjectRef(),
+ .fail => this.onReject().asObjectRef(),
+ else => unreachable,
+ };
+
+ _ = js.JSObjectCallAsFunction(this.global_this.ref(), callback_object, null, 1, &args, null);
+
+ this.release();
+ }
+
+ pub fn reset(_: *FetchTasklet) void {}
+
+ pub fn release(this: *FetchTasklet) void {
+ js.JSValueUnprotect(this.global_this.ref(), this.resolve);
+ js.JSValueUnprotect(this.global_this.ref(), this.reject);
+ js.JSValueUnprotect(this.global_this.ref(), this.this_object);
+
+ this.global_this = undefined;
+ this.javascript_vm = undefined;
+ this.promise = undefined;
+ this.status = Status.pending;
+ // var pooled = this.pooled_body;
+ // BodyPool.release(pooled);
+ // this.pooled_body = undefined;
+ this.http = undefined;
+ this.this_object = null;
+ this.resolve = null;
+ this.reject = null;
+ Pool.release(@fieldParentPtr(Pool.Node, "data", this));
+ }
+
+ pub const FetchResolver = struct {
+ pub fn call(
+ _: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: usize,
+ arguments: [*c]const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) callconv(.C) js.JSObjectRef {
+ return JSPrivateDataPtr.from(js.JSObjectGetPrivate(arguments[0]))
+ .get(FetchTaskletContext).?.tasklet.onResolve().asObjectRef();
+ // return js.JSObjectGetPrivate(arguments[0]).? .tasklet.onResolve().asObjectRef();
+ }
+ };
+
+ pub const FetchRejecter = struct {
+ pub fn call(
+ _: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: usize,
+ arguments: [*c]const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) callconv(.C) js.JSObjectRef {
+ return JSPrivateDataPtr.from(js.JSObjectGetPrivate(arguments[0]))
+ .get(FetchTaskletContext).?.tasklet.onReject().asObjectRef();
+ }
+ };
+
+ pub fn onReject(this: *FetchTasklet) JSValue {
+ if (this.blob_store) |store| {
+ store.deref();
+ }
+ const fetch_error = std.fmt.allocPrint(
+ default_allocator,
+ "fetch() failed – {s}\nurl: \"{s}\"",
+ .{
+ @errorName(this.http.err orelse error.HTTPFail),
+ this.http.url.href,
+ },
+ ) catch unreachable;
+ return ZigString.init(fetch_error).toErrorInstance(this.global_this);
+ }
+
+ pub fn onResolve(this: *FetchTasklet) JSValue {
+ var allocator = default_allocator;
+ var http_response = this.http.response.?;
+ var response = allocator.create(Response) catch unreachable;
+ if (this.blob_store) |store| {
+ store.deref();
+ }
+ response.* = Response{
+ .allocator = allocator,
+ .url = allocator.dupe(u8, this.http.url.href) catch unreachable,
+ .status_text = allocator.dupe(u8, http_response.status) catch unreachable,
+ .redirected = this.http.redirect_count > 0,
+ .body = .{
+ .init = .{
+ .headers = FetchHeaders.createFromPicoHeaders(this.global_this, http_response.headers),
+ .status_code = @truncate(u16, http_response.status_code),
+ },
+ .value = .{
+ .Blob = Blob.init(this.http.response_buffer.toOwnedSliceLeaky(), allocator, this.global_this),
+ },
+ },
+ };
+ return JSValue.fromRef(Response.makeMaybePooled(@ptrCast(js.JSContextRef, this.global_this), response));
+ }
+
+ pub fn get(
+ allocator: std.mem.Allocator,
+ method: Method,
+ url: ZigURL,
+ headers: Headers.Entries,
+ headers_buf: string,
+ request_body: ?*MutableString,
+ timeout: usize,
+ request_body_store: ?*Blob.Store,
+ ) !*FetchTasklet.Pool.Node {
+ var linked_list = FetchTasklet.Pool.get(allocator);
+ linked_list.data.javascript_vm = VirtualMachine.vm;
+ linked_list.data.empty_request_body = MutableString.init(allocator, 0) catch unreachable;
+ // linked_list.data.pooled_body = BodyPool.get(allocator);
+ linked_list.data.blob_store = request_body_store;
+ linked_list.data.response_buffer = MutableString.initEmpty(allocator);
+ linked_list.data.http = try HTTPClient.AsyncHTTP.init(
+ allocator,
+ method,
+ url,
+ headers,
+ headers_buf,
+ &linked_list.data.response_buffer,
+ request_body orelse &linked_list.data.empty_request_body,
+
+ timeout,
+ );
+ linked_list.data.context = .{ .tasklet = &linked_list.data };
+
+ return linked_list;
+ }
+
+ pub fn queue(
+ allocator: std.mem.Allocator,
+ global: *JSGlobalObject,
+ method: Method,
+ url: ZigURL,
+ headers: Headers.Entries,
+ headers_buf: string,
+ request_body: ?*MutableString,
+ timeout: usize,
+ request_body_store: ?*Blob.Store,
+ ) !*FetchTasklet.Pool.Node {
+ var node = try get(allocator, method, url, headers, headers_buf, request_body, timeout, request_body_store);
+ node.data.promise = JSInternalPromise.create(global);
+
+ node.data.global_this = global;
+ node.data.http.callback = callback;
+ var batch = NetworkThread.Batch{};
+ node.data.http.schedule(allocator, &batch);
+ NetworkThread.global.pool.schedule(batch);
+ VirtualMachine.vm.active_tasks +|= 1;
+ return node;
+ }
+
+ pub fn callback(http_: *HTTPClient.AsyncHTTP) void {
+ var task: *FetchTasklet = @fieldParentPtr(FetchTasklet, "http", http_);
+ @atomicStore(Status, &task.status, Status.done, .Monotonic);
+ task.javascript_vm.eventLoop().enqueueTaskConcurrent(Task.init(task));
+ }
+ };
+
+ pub fn call(
+ _: void,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) js.JSObjectRef {
+ var globalThis = ctx.ptr();
+
+ if (arguments.len == 0) {
+ const fetch_error = fetch_error_no_args;
+ return JSPromise.rejectedPromiseValue(globalThis, ZigString.init(fetch_error).toErrorInstance(globalThis)).asRef();
+ }
+
+ var headers: ?Headers = null;
+ var body: MutableString = MutableString.initEmpty(bun.default_allocator);
+ var method = Method.GET;
+ var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments);
+ var url: ZigURL = undefined;
+ var first_arg = args.nextEat().?;
+ var blob_store: ?*Blob.Store = null;
+ if (first_arg.isString()) {
+ var url_zig_str = ZigString.init("");
+ JSValue.fromRef(arguments[0]).toZigString(&url_zig_str, globalThis);
+ var url_str = url_zig_str.slice();
+
+ if (url_str.len == 0) {
+ const fetch_error = fetch_error_blank_url;
+ return JSPromise.rejectedPromiseValue(globalThis, ZigString.init(fetch_error).toErrorInstance(globalThis)).asRef();
+ }
+
+ if (url_str[0] == '/') {
+ url_str = strings.append(getAllocator(ctx), VirtualMachine.vm.bundler.options.origin.origin, url_str) catch unreachable;
+ } else {
+ url_str = getAllocator(ctx).dupe(u8, url_str) catch unreachable;
+ }
+
+ NetworkThread.init() catch @panic("Failed to start network thread");
+ url = ZigURL.parse(url_str);
+
+ if (arguments.len >= 2 and js.JSValueIsObject(ctx, arguments[1])) {
+ var options = JSValue.fromRef(arguments[1]);
+ if (options.get(ctx.ptr(), "method")) |method_| {
+ var slice_ = method_.toSlice(ctx.ptr(), getAllocator(ctx));
+ defer slice_.deinit();
+ method = Method.which(slice_.slice()) orelse .GET;
+ }
+
+ if (options.get(ctx.ptr(), "headers")) |headers_| {
+ if (headers_.as(FetchHeaders)) |headers__| {
+ headers = Headers.from(headers__, bun.default_allocator) catch unreachable;
+ // TODO: make this one pass
+ } else if (FetchHeaders.createFromJS(ctx.ptr(), headers_)) |headers__| {
+ headers = Headers.from(headers__, bun.default_allocator) catch unreachable;
+ headers__.deref();
+ }
+ }
+
+ if (options.get(ctx.ptr(), "body")) |body__| {
+ if (Blob.fromJS(ctx.ptr(), body__, true, false)) |new_blob| {
+ if (new_blob.size > 0) {
+ body = MutableString{
+ .list = std.ArrayListUnmanaged(u8){
+ .items = bun.constStrToU8(new_blob.sharedView()),
+ .capacity = new_blob.size,
+ },
+ .allocator = bun.default_allocator,
+ };
+ blob_store = new_blob.store;
+ }
+ // transfer is unnecessary here because this is a new slice
+ //new_blob.transfer();
+ } else |_| {
+ return JSPromise.rejectedPromiseValue(globalThis, ZigString.init("fetch() received invalid body").toErrorInstance(globalThis)).asRef();
+ }
+ }
+ }
+ } else if (first_arg.asCheckLoaded(Request)) |request| {
+ url = ZigURL.parse(request.url.dupe(getAllocator(ctx)) catch unreachable);
+ method = request.method;
+ if (request.headers) |head| {
+ headers = Headers.from(head, bun.default_allocator) catch unreachable;
+ }
+ var blob = request.body.use();
+ // TODO: make RequestBody _NOT_ a MutableString
+ body = MutableString{
+ .list = std.ArrayListUnmanaged(u8){
+ .items = bun.constStrToU8(blob.sharedView()),
+ .capacity = bun.constStrToU8(blob.sharedView()).len,
+ },
+ .allocator = blob.allocator orelse bun.default_allocator,
+ };
+ blob_store = blob.store;
+ } else {
+ const fetch_error = fetch_type_error_strings.get(js.JSValueGetType(ctx, arguments[0]));
+ return JSPromise.rejectedPromiseValue(globalThis, ZigString.init(fetch_error).toErrorInstance(globalThis)).asRef();
+ }
+
+ var header_entries: Headers.Entries = .{};
+ var header_buf: string = "";
+
+ if (headers) |head| {
+ header_entries = head.entries;
+ header_buf = head.buf.items;
+ }
+ var resolve = js.JSObjectMakeFunctionWithCallback(ctx, null, Fetch.FetchTasklet.FetchResolver.call);
+ var reject = js.JSObjectMakeFunctionWithCallback(ctx, null, Fetch.FetchTasklet.FetchRejecter.call);
+
+ js.JSValueProtect(ctx, resolve);
+ js.JSValueProtect(ctx, reject);
+
+ var request_body: ?*MutableString = null;
+ if (body.list.items.len > 0) {
+ var mutable = bun.default_allocator.create(MutableString) catch unreachable;
+ mutable.* = body;
+ request_body = mutable;
+ }
+
+ // var resolve = FetchTasklet.FetchResolver.Class.make(ctx: js.JSContextRef, ptr: *ZigType)
+ var queued = FetchTasklet.queue(
+ default_allocator,
+ globalThis,
+ method,
+ url,
+ header_entries,
+ header_buf,
+ request_body,
+ std.time.ns_per_hour,
+ blob_store,
+ ) catch unreachable;
+ queued.data.this_object = js.JSObjectMake(ctx, null, JSPrivateDataPtr.from(&queued.data.context).ptr());
+ js.JSValueProtect(ctx, queued.data.this_object);
+
+ var promise = js.JSObjectMakeDeferredPromise(ctx, &resolve, &reject, exception);
+ queued.data.reject = reject;
+ queued.data.resolve = resolve;
+
+ return promise;
+ // queued.data.promise.create(globalThis: *JSGlobalObject)
+ }
+};
+
+// https://developer.mozilla.org/en-US/docs/Web/API/Headers
+pub const Headers = struct {
+ pub usingnamespace HTTPClient.Headers;
+ entries: Headers.Entries = .{},
+ buf: std.ArrayListUnmanaged(u8) = .{},
+ allocator: std.mem.Allocator,
+
+ pub fn asStr(this: *const Headers, ptr: Api.StringPointer) []const u8 {
+ return if (ptr.offset + ptr.length <= this.buf.items.len)
+ this.buf.items[ptr.offset..][0..ptr.length]
+ else
+ "";
+ }
+
+ pub fn from(headers_ref: *FetchHeaders, allocator: std.mem.Allocator) !Headers {
+ var header_count: u32 = 0;
+ var buf_len: u32 = 0;
+ headers_ref.count(&header_count, &buf_len);
+ var headers = Headers{
+ .entries = .{},
+ .buf = .{},
+ .allocator = allocator,
+ };
+ headers.entries.ensureTotalCapacity(allocator, header_count) catch unreachable;
+ headers.entries.len = header_count;
+ headers.buf.ensureTotalCapacityPrecise(allocator, buf_len) catch unreachable;
+ headers.buf.items.len = buf_len;
+ var sliced = headers.entries.slice();
+ var names = sliced.items(.name);
+ var values = sliced.items(.value);
+ headers_ref.copyTo(names.ptr, values.ptr, headers.buf.items.ptr);
+ return headers;
+ }
+};
+
+const PathOrBlob = union(enum) {
+ path: JSC.Node.PathOrFileDescriptor,
+ blob: Blob,
+
+ pub fn fromJS(ctx: js.JSContextRef, args: *JSC.Node.ArgumentsSlice, exception: js.ExceptionRef) ?PathOrBlob {
+ if (JSC.Node.PathOrFileDescriptor.fromJS(ctx, args, exception)) |path| {
+ return PathOrBlob{ .path = .{
+ .path = .{
+ .string = bun.PathString.init((bun.default_allocator.dupeZ(u8, path.path.slice()) catch unreachable)[0..path.path.slice().len]),
+ },
+ } };
+ }
+
+ const arg = args.nextEat() orelse return null;
+
+ if (arg.as(Blob)) |blob| {
+ return PathOrBlob{
+ .blob = blob.dupe(),
+ };
+ }
+
+ return null;
+ }
+};
+
+pub const Blob = struct {
+ size: SizeType = 0,
+ offset: SizeType = 0,
+ /// When set, the blob will be freed on finalization callbacks
+ /// If the blob is contained in Response or Request, this must be null
+ allocator: ?std.mem.Allocator = null,
+ store: ?*Store = null,
+ content_type: string = "",
+ content_type_allocated: bool = false,
+
+ /// JavaScriptCore strings are either latin1 or UTF-16
+ /// When UTF-16, they're nearly always due to non-ascii characters
+ is_all_ascii: ?bool = null,
+
+ globalThis: *JSGlobalObject = undefined,
+
+ /// Max int of double precision
+ /// 9 petabytes is probably enough for awhile
+ /// We want to avoid coercing to a BigInt because that's a heap allocation
+ /// and it's generally just harder to use
+ pub const SizeType = u52;
+ pub const max_size = std.math.maxInt(SizeType);
+
+ const CopyFilePromiseHandler = struct {
+ promise: *JSPromise,
+ globalThis: *JSGlobalObject,
+ pub fn run(handler: *@This(), blob_: Store.CopyFile.ResultType) void {
+ var promise = handler.promise;
+ var globalThis = handler.globalThis;
+ bun.default_allocator.destroy(handler);
+ var blob = blob_ catch |err| {
+ var error_string = ZigString.init(
+ std.fmt.allocPrint(bun.default_allocator, "Failed to write file \"{s}\"", .{std.mem.span(@errorName(err))}) catch unreachable,
+ );
+ error_string.mark();
+
+ promise.reject(globalThis, error_string.toErrorInstance(globalThis));
+ return;
+ };
+ var _blob = bun.default_allocator.create(Blob) catch unreachable;
+ _blob.* = blob;
+ _blob.allocator = bun.default_allocator;
+ promise.resolve(
+ globalThis,
+ );
+ }
+ };
+
+ const WriteFileWaitFromLockedValueTask = struct {
+ file_blob: Blob,
+ globalThis: *JSGlobalObject,
+ promise: *JSPromise,
+
+ pub fn thenWrap(this: *anyopaque, value: *Body.Value) void {
+ then(bun.cast(*WriteFileWaitFromLockedValueTask, this), value);
+ }
+
+ pub fn then(this: *WriteFileWaitFromLockedValueTask, value: *Body.Value) void {
+ var promise = this.promise;
+ var globalThis = this.globalThis;
+ var file_blob = this.file_blob;
+ switch (value.*) {
+ .Error => |err| {
+ file_blob.detach();
+ _ = value.use();
+ bun.default_allocator.destroy(this);
+ promise.reject(globalThis, err);
+ },
+ .Used => {
+ file_blob.detach();
+ _ = value.use();
+ bun.default_allocator.destroy(this);
+ promise.reject(globalThis, ZigString.init("Body was used after it was consumed").toErrorInstance(globalThis));
+ },
+ .Empty, .Blob => {
+ var blob = value.use();
+ // TODO: this should be one promise not two!
+ const new_promise = writeFileWithSourceDestination(globalThis.ref(), &blob, &file_blob);
+ if (JSC.JSValue.fromRef(new_promise.?).asPromise()) |_promise| {
+ switch (_promise.status(globalThis.vm())) {
+ .Pending => {
+ promise.resolve(
+ globalThis,
+ JSC.JSValue.fromRef(new_promise.?),
+ );
+ },
+ .Rejected => {
+ promise.reject(globalThis, _promise.result(globalThis.vm()));
+ },
+ else => {
+ promise.resolve(globalThis, _promise.result(globalThis.vm()));
+ },
+ }
+ } else if (JSC.JSValue.fromRef(new_promise.?).asInternalPromise()) |_promise| {
+ switch (_promise.status(globalThis.vm())) {
+ .Pending => {
+ promise.resolve(
+ globalThis,
+ JSC.JSValue.fromRef(new_promise.?),
+ );
+ },
+ .Rejected => {
+ promise.reject(globalThis, _promise.result(globalThis.vm()));
+ },
+ else => {
+ promise.resolve(globalThis, _promise.result(globalThis.vm()));
+ },
+ }
+ }
+
+ file_blob.detach();
+ bun.default_allocator.destroy(this);
+ },
+ .Locked => {
+ value.Locked.callback = thenWrap;
+ value.Locked.task = this;
+ },
+ }
+ }
+ };
+
+ pub fn writeFileWithSourceDestination(
+ ctx: JSC.C.JSContextRef,
+ source_blob: *Blob,
+ destination_blob: *Blob,
+ ) js.JSObjectRef {
+ const destination_type = std.meta.activeTag(destination_blob.store.?.data);
+
+ // Writing an empty string to a file is a no-op
+ if (source_blob.store == null) {
+ destination_blob.detach();
+ return JSC.JSPromise.resolvedPromiseValue(ctx.ptr(), JSC.JSValue.jsNumber(0)).asObjectRef();
+ }
+
+ const source_type = std.meta.activeTag(source_blob.store.?.data);
+
+ if (destination_type == .file and source_type == .bytes) {
+ var write_file_promise = bun.default_allocator.create(WriteFilePromise) catch unreachable;
+ write_file_promise.* = .{
+ .promise = JSC.JSPromise.create(ctx.ptr()),
+ .globalThis = ctx.ptr(),
+ };
+ JSC.C.JSValueProtect(ctx, write_file_promise.promise.asValue(ctx.ptr()).asObjectRef());
+
+ var file_copier = Store.WriteFile.create(
+ bun.default_allocator,
+ destination_blob.*,
+ source_blob.*,
+ *WriteFilePromise,
+ write_file_promise,
+ WriteFilePromise.run,
+ ) catch unreachable;
+ var task = Store.WriteFile.WriteFileTask.createOnJSThread(bun.default_allocator, ctx.ptr(), file_copier) catch unreachable;
+ task.schedule();
+ return write_file_promise.promise.asValue(ctx.ptr()).asObjectRef();
+ }
+ // If this is file <> file, we can just copy the file
+ else if (destination_type == .file and source_type == .file) {
+ var file_copier = Store.CopyFile.create(
+ bun.default_allocator,
+ destination_blob.store.?,
+ source_blob.store.?,
+
+ destination_blob.offset,
+ destination_blob.size,
+ ctx.ptr(),
+ ) catch unreachable;
+ file_copier.schedule();
+ return file_copier.promise.asObjectRef();
+ } else if (destination_type == .bytes and source_type == .bytes) {
+ // If this is bytes <> bytes, we can just duplicate it
+ // this is an edgecase
+ // it will happen if someone did Bun.write(new Blob([123]), new Blob([456]))
+ // eventually, this could be like Buffer.concat
+ var clone = source_blob.dupe();
+ clone.allocator = bun.default_allocator;
+ var cloned = bun.default_allocator.create(Blob) catch unreachable;
+ cloned.* = clone;
+ return JSPromise.resolvedPromiseValue(ctx.ptr(), JSC.JSValue.fromRef(Blob.Class.make(ctx, cloned))).asObjectRef();
+ } else if (destination_type == .bytes and source_type == .file) {
+ return JSPromise.resolvedPromiseValue(
+ ctx.ptr(),
+ JSC.JSValue.fromRef(
+ source_blob.getSlice(ctx, undefined, undefined, &.{}, null),
+ ),
+ ).asObjectRef();
+ }
+
+ unreachable;
+ }
+ pub fn writeFile(
+ _: void,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) js.JSObjectRef {
+ var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments);
+ // accept a path or a blob
+ var path_or_blob = PathOrBlob.fromJS(ctx, &args, exception) orelse {
+ exception.* = JSC.toInvalidArguments("Bun.write expects a path, file descriptor or a blob", .{}, ctx).asObjectRef();
+ return null;
+ };
+
+ // if path_or_blob is a path, convert it into a file blob
+ var destination_blob: Blob = if (path_or_blob == .path)
+ Blob.findOrCreateFileFromPath(path_or_blob.path, ctx.ptr())
+ else
+ path_or_blob.blob.dupe();
+
+ if (destination_blob.store == null) {
+ exception.* = JSC.toInvalidArguments("Writing to an empty blob is not implemented yet", .{}, ctx).asObjectRef();
+ return null;
+ }
+
+ var data = args.nextEat() orelse {
+ exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef();
+ return null;
+ };
+
+ if (data.isUndefinedOrNull() or data.isEmpty()) {
+ exception.* = JSC.toInvalidArguments("Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write", .{}, ctx).asObjectRef();
+ return null;
+ }
+
+ // TODO: implement a writeev() fast path
+ var source_blob: Blob = brk: {
+ if (data.as(Response)) |response| {
+ switch (response.body.value) {
+ .Used, .Empty, .Blob => {
+ break :brk response.body.use();
+ },
+ .Error => {
+ destination_blob.detach();
+ const err = response.body.value.Error;
+ JSC.C.JSValueUnprotect(ctx, err.asObjectRef());
+ _ = response.body.value.use();
+ return JSC.JSPromise.rejectedPromiseValue(ctx.ptr(), err).asObjectRef();
+ },
+ .Locked => {
+ var task = bun.default_allocator.create(WriteFileWaitFromLockedValueTask) catch unreachable;
+ var promise = JSC.JSPromise.create(ctx.ptr());
+ task.* = WriteFileWaitFromLockedValueTask{
+ .globalThis = ctx.ptr(),
+ .file_blob = destination_blob,
+ .promise = promise,
+ };
+
+ response.body.value.Locked.task = task;
+ response.body.value.Locked.callback = WriteFileWaitFromLockedValueTask.thenWrap;
+
+ return promise.asValue(ctx.ptr()).asObjectRef();
+ },
+ }
+ }
+
+ if (data.as(Request)) |request| {
+ switch (request.body) {
+ .Used, .Empty, .Blob => {
+ break :brk request.body.use();
+ },
+ .Error => {
+ destination_blob.detach();
+ const err = request.body.Error;
+ JSC.C.JSValueUnprotect(ctx, err.asObjectRef());
+ _ = request.body.use();
+ return JSC.JSPromise.rejectedPromiseValue(ctx.ptr(), err).asObjectRef();
+ },
+ .Locked => {
+ var task = bun.default_allocator.create(WriteFileWaitFromLockedValueTask) catch unreachable;
+ var promise = JSC.JSPromise.create(ctx.ptr());
+ task.* = WriteFileWaitFromLockedValueTask{
+ .globalThis = ctx.ptr(),
+ .file_blob = destination_blob,
+ .promise = promise,
+ };
+
+ request.body.Locked.task = task;
+ request.body.Locked.callback = WriteFileWaitFromLockedValueTask.thenWrap;
+
+ return promise.asValue(ctx.ptr()).asObjectRef();
+ },
+ }
+ }
+
+ break :brk Blob.fromJS(
+ ctx.ptr(),
+ data,
+ false,
+ false,
+ ) catch |err| {
+ if (err == error.InvalidArguments) {
+ exception.* = JSC.toInvalidArguments(
+ "Expected an Array",
+ .{},
+ ctx,
+ ).asObjectRef();
+ return null;
+ }
+
+ exception.* = JSC.toInvalidArguments(
+ "Out of memory",
+ .{},
+ ctx,
+ ).asObjectRef();
+ return null;
+ };
+ };
+
+ return writeFileWithSourceDestination(ctx, &source_blob, &destination_blob);
+ }
+
+ pub fn constructFile(
+ _: void,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) js.JSObjectRef {
+ var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments);
+ defer args.deinit();
+
+ var path = JSC.Node.PathOrFileDescriptor.fromJS(ctx, &args, exception) orelse {
+ exception.* = JSC.toInvalidArguments("Expected file path string or file descriptor", .{}, ctx).asObjectRef();
+ return js.JSValueMakeUndefined(ctx);
+ };
+
+ const blob = Blob.findOrCreateFileFromPath(path, ctx.ptr());
+
+ var ptr = bun.default_allocator.create(Blob) catch unreachable;
+ ptr.* = blob;
+ ptr.allocator = bun.default_allocator;
+ return Blob.Class.make(ctx, ptr);
+ }
+
+ pub fn findOrCreateFileFromPath(path_: JSC.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject) Blob {
+ var path = path_;
+ var vm = globalThis.bunVM();
+ if (vm.getFileBlob(path)) |blob| {
+ blob.ref();
+ return Blob.initWithStore(blob, globalThis);
+ }
+
+ switch (path) {
+ .path => {
+ path.path = .{
+ .string = bun.PathString.init(
+ (bun.default_allocator.dupeZ(u8, path.path.slice()) catch unreachable)[0..path.path.slice().len],
+ ),
+ };
+ },
+ .fd => {
+ switch (path.fd) {
+ std.os.STDIN_FILENO => return Blob.initWithStore(
+ vm.rareData().stdin(),
+ globalThis,
+ ),
+ std.os.STDERR_FILENO => return Blob.initWithStore(
+ vm.rareData().stderr(),
+ globalThis,
+ ),
+ std.os.STDOUT_FILENO => return Blob.initWithStore(
+ vm.rareData().stdout(),
+ globalThis,
+ ),
+ else => {},
+ }
+ },
+ }
+
+ const result = Blob.initWithStore(Blob.Store.initFile(path, null, bun.default_allocator) catch unreachable, globalThis);
+ vm.putFileBlob(path, result.store.?) catch unreachable;
+ return result;
+ }
+
+ pub const Store = struct {
+ data: Data,
+
+ mime_type: MimeType = MimeType.other,
+ ref_count: u32 = 0,
+ is_all_ascii: ?bool = null,
+ allocator: std.mem.Allocator,
+
+ pub fn size(this: *const Store) SizeType {
+ return switch (this.data) {
+ .bytes => this.data.bytes.len,
+ .file => Blob.max_size,
+ };
+ }
+
+ pub const Map = std.HashMap(u64, *JSC.WebCore.Blob.Store, IdentityContext(u64), 80);
+
+ pub const Data = union(enum) {
+ bytes: ByteStore,
+ file: FileStore,
+ };
+
+ pub fn ref(this: *Store) void {
+ this.ref_count += 1;
+ }
+
+ pub fn external(ptr: ?*anyopaque, _: ?*anyopaque, _: usize) callconv(.C) void {
+ if (ptr == null) return;
+ var this = bun.cast(*Store, ptr);
+ this.deref();
+ }
+
+ pub fn initFile(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType, allocator: std.mem.Allocator) !*Store {
+ var store = try allocator.create(Blob.Store);
+ store.* = .{
+ .data = .{ .file = FileStore.init(
+ pathlike,
+ mime_type orelse brk: {
+ if (pathlike == .path) {
+ const sliced = pathlike.path.slice();
+ if (sliced.len > 0) {
+ var extname = std.fs.path.extension(sliced);
+ extname = std.mem.trim(u8, extname, ".");
+ if (HTTPClient.MimeType.byExtensionNoDefault(extname)) |mime| {
+ break :brk mime;
+ }
+ }
+ }
+
+ break :brk null;
+ },
+ ) },
+ .allocator = allocator,
+ .ref_count = 1,
+ };
+ return store;
+ }
+
+ pub fn init(bytes: []u8, allocator: std.mem.Allocator) !*Store {
+ var store = try allocator.create(Blob.Store);
+ store.* = .{
+ .data = .{ .bytes = ByteStore.init(bytes, allocator) },
+ .allocator = allocator,
+ .ref_count = 1,
+ };
+ return store;
+ }
+
+ pub fn sharedView(this: Store) []u8 {
+ if (this.data == .bytes)
+ return this.data.bytes.slice();
+
+ return &[_]u8{};
+ }
+
+ pub fn deref(this: *Blob.Store) void {
+ this.ref_count -= 1;
+ if (this.ref_count == 0) {
+ this.deinit();
+ }
+ }
+
+ pub fn deinit(this: *Blob.Store) void {
+ switch (this.data) {
+ .bytes => |*bytes| {
+ bytes.deinit();
+ },
+ .file => |file| {
+ VirtualMachine.vm.removeFileBlob(file.pathlike);
+ },
+ }
+
+ this.allocator.destroy(this);
+ }
+
+ pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*Blob.Store {
+ return try Blob.Store.init(list.items, allocator);
+ }
+
+ pub fn FileOpenerMixin(comptime This: type) type {
+ return struct {
+ const __opener_flags = std.os.O.NONBLOCK | std.os.O.CLOEXEC;
+ const open_flags_ = if (@hasDecl(This, "open_flags"))
+ This.open_flags | __opener_flags
+ else
+ std.os.O.RDONLY | __opener_flags;
+
+ pub fn getFdMac(this: *This) AsyncIO.OpenError!JSC.Node.FileDescriptor {
+ var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var path_string = if (@hasField(This, "file_store"))
+ this.file_store.pathlike.path
+ else
+ this.file_blob.store.?.data.file.pathlike.path;
+
+ var path = path_string.sliceZ(&buf);
+
+ this.opened_fd = switch (JSC.Node.Syscall.open(path, open_flags_, JSC.Node.default_permission)) {
+ .result => |fd| fd,
+ .err => |err| {
+ this.errno = AsyncIO.asError(err.errno);
+ this.system_error = err.withPath(path_string.slice()).toSystemError();
+
+ return @errSetCast(AsyncIO.OpenError, this.errno.?);
+ },
+ };
+
+ return this.opened_fd;
+ }
+
+ pub fn getFd(this: *This) AsyncIO.OpenError!JSC.Node.FileDescriptor {
+ if (this.opened_fd != null_fd) {
+ return this.opened_fd;
+ }
+
+ if (comptime Environment.isMac) {
+ return try this.getFdMac();
+ } else {
+ return try this.getFdLinux();
+ }
+ }
+
+ pub fn getFdLinux(this: *This) AsyncIO.OpenError!JSC.Node.FileDescriptor {
+ var aio = &AsyncIO.global;
+
+ var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var path_string = if (@hasField(This, "file_store"))
+ this.file_store.pathlike.path
+ else
+ this.file_blob.store.?.data.file.pathlike.path;
+
+ var path = path_string.sliceZ(&buf);
+
+ aio.open(
+ *This,
+ this,
+ onOpen,
+ &this.open_completion,
+ path,
+ open_flags_,
+ JSC.Node.default_permission,
+ );
+
+ suspend {
+ this.open_frame = @frame().*;
+ }
+
+ if (this.errno) |errno| {
+ this.system_error = .{
+ .syscall = ZigString.init("open"),
+ .code = ZigString.init(std.mem.span(@errorName(errno))),
+ .path = ZigString.init(path_string.slice()),
+ };
+
+ return @errSetCast(AsyncIO.OpenError, errno);
+ }
+
+ return this.opened_fd;
+ }
+
+ pub fn onOpen(this: *This, completion: *HTTPClient.NetworkThread.Completion, result: AsyncIO.OpenError!JSC.Node.FileDescriptor) void {
+ this.opened_fd = result catch {
+ this.errno = AsyncIO.asError(-completion.result);
+
+ if (comptime Environment.isLinux) resume this.open_frame;
+ return;
+ };
+
+ if (comptime Environment.isLinux) resume this.open_frame;
+ }
+ };
+ }
+
+ pub fn FileCloserMixin(comptime This: type) type {
+ return struct {
+ pub fn doClose(this: *This) AsyncIO.CloseError!void {
+ var aio = &AsyncIO.global;
+
+ aio.close(
+ *This,
+ this,
+ onClose,
+ &this.close_completion,
+ this.opened_fd,
+ );
+ this.opened_fd = null_fd;
+
+ suspend {
+ this.close_frame = @frame().*;
+ }
+
+ if (@hasField(This, "errno")) {
+ if (this.errno) |errno| {
+ return @errSetCast(AsyncIO.CloseError, errno);
+ }
+ }
+ }
+
+ pub fn onClose(this: *This, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.CloseError!void) void {
+ result catch |err| {
+ if (@hasField(This, "errno")) {
+ this.errno = err;
+ }
+ resume this.close_frame;
+ return;
+ };
+
+ resume this.close_frame;
+ }
+ };
+ }
+
+ pub const ReadFile = struct {
+ const OpenFrameType = if (Environment.isMac)
+ void
+ else
+ @Frame(ReadFile.getFdLinux);
+ file_store: FileStore,
+ byte_store: ByteStore = ByteStore{ .allocator = bun.default_allocator },
+ store: ?*Store = null,
+ offset: SizeType = 0,
+ max_length: SizeType = Blob.max_size,
+ open_frame: OpenFrameType = undefined,
+ read_frame: @Frame(ReadFile.doRead) = undefined,
+ close_frame: @Frame(ReadFile.doClose) = undefined,
+ open_completion: HTTPClient.NetworkThread.Completion = undefined,
+ opened_fd: JSC.Node.FileDescriptor = null_fd,
+ read_completion: HTTPClient.NetworkThread.Completion = undefined,
+ read_len: SizeType = 0,
+ read_off: SizeType = 0,
+ size: SizeType = 0,
+ buffer: []u8 = undefined,
+ runAsyncFrame: @Frame(ReadFile.runAsync) = undefined,
+ close_completion: HTTPClient.NetworkThread.Completion = undefined,
+ task: HTTPClient.NetworkThread.Task = undefined,
+ system_error: ?JSC.SystemError = null,
+ errno: ?anyerror = null,
+ onCompleteCtx: *anyopaque = undefined,
+ onCompleteCallback: OnReadFileCallback = undefined,
+
+ convert_to_byte_blob: bool = false,
+
+ pub const Read = struct {
+ buf: []u8,
+ is_temporary: bool = false,
+ };
+ pub const ResultType = SystemError.Maybe(Read);
+
+ pub const OnReadFileCallback = fn (ctx: *anyopaque, bytes: ResultType) void;
+
+ pub usingnamespace FileOpenerMixin(ReadFile);
+ pub usingnamespace FileCloserMixin(ReadFile);
+
+ pub fn createWithCtx(
+ allocator: std.mem.Allocator,
+ store: *Store,
+ onReadFileContext: *anyopaque,
+ onCompleteCallback: OnReadFileCallback,
+ off: SizeType,
+ max_len: SizeType,
+ ) !*ReadFile {
+ var read_file = try allocator.create(ReadFile);
+ read_file.* = ReadFile{
+ .file_store = store.data.file,
+ .offset = off,
+ .max_length = max_len,
+ .store = store,
+ .onCompleteCtx = onReadFileContext,
+ .onCompleteCallback = onCompleteCallback,
+ };
+ store.ref();
+ return read_file;
+ }
+
+ pub fn create(
+ allocator: std.mem.Allocator,
+ store: *Store,
+ off: SizeType,
+ max_len: SizeType,
+ comptime Context: type,
+ context: Context,
+ comptime callback: fn (ctx: Context, bytes: ResultType) void,
+ ) !*ReadFile {
+ const Handler = struct {
+ pub fn run(ptr: *anyopaque, bytes: ResultType) void {
+ callback(bun.cast(Context, ptr), bytes);
+ }
+ };
+
+ return try ReadFile.createWithCtx(allocator, store, @ptrCast(*anyopaque, context), Handler.run, off, max_len);
+ }
+
+ pub fn doRead(this: *ReadFile) AsyncIO.ReadError!SizeType {
+ var aio = &AsyncIO.global;
+
+ var remaining = this.buffer[this.read_off..];
+ this.read_len = 0;
+ aio.read(
+ *ReadFile,
+ this,
+ onRead,
+ &this.read_completion,
+ this.opened_fd,
+ remaining[0..@minimum(remaining.len, this.max_length - this.read_off)],
+ this.offset + this.read_off,
+ );
+
+ suspend {
+ this.read_frame = @frame().*;
+ }
+
+ if (this.errno) |errno| {
+ this.system_error = JSC.SystemError{
+ .code = ZigString.init(std.mem.span(@errorName(errno))),
+ .path = if (this.file_store.pathlike == .path)
+ ZigString.init(this.file_store.pathlike.path.slice())
+ else
+ ZigString.Empty,
+ .syscall = ZigString.init("read"),
+ };
+
+ return @errSetCast(AsyncIO.ReadError, errno);
+ }
+
+ return this.read_len;
+ }
+
+ pub const ReadFileTask = JSC.IOTask(@This());
+
+ pub fn then(this: *ReadFile, _: *JSC.JSGlobalObject) void {
+ var cb = this.onCompleteCallback;
+ var cb_ctx = this.onCompleteCtx;
+
+ if (this.store == null and this.system_error != null) {
+ var system_error = this.system_error.?;
+ bun.default_allocator.destroy(this);
+ cb(cb_ctx, ResultType{ .err = system_error });
+ return;
+ } else if (this.store == null) {
+ bun.default_allocator.destroy(this);
+ cb(cb_ctx, ResultType{ .err = SystemError{
+ .code = ZigString.init("INTERNAL_ERROR"),
+ .path = ZigString.Empty,
+ .message = ZigString.init("assertion failure - store should not be null"),
+ .syscall = ZigString.init("read"),
+ } });
+ return;
+ }
+ var store = this.store.?;
+
+ if (this.convert_to_byte_blob and this.file_store.pathlike == .path) {
+ VirtualMachine.vm.removeFileBlob(this.file_store.pathlike);
+ }
+
+ if (this.system_error) |err| {
+ bun.default_allocator.destroy(this);
+ store.deref();
+ cb(cb_ctx, ResultType{ .err = err });
+ return;
+ }
+
+ var buf = this.buffer;
+ const is_temporary = !this.convert_to_byte_blob;
+ if (this.convert_to_byte_blob) {
+ if (store.data == .bytes) {
+ bun.default_allocator.free(this.buffer);
+ buf = store.data.bytes.slice();
+ } else if (store.data == .file) {
+ if (this.file_store.pathlike == .path) {
+ if (this.file_store.pathlike.path == .string) {
+ bun.default_allocator.free(this.file_store.pathlike.path.slice());
+ }
+ }
+ store.data = .{ .bytes = ByteStore.init(buf, bun.default_allocator) };
+ }
+ }
+
+ bun.default_allocator.destroy(this);
+
+ // Attempt to free it as soon as possible
+ if (store.ref_count > 1) {
+ store.deref();
+ cb(cb_ctx, .{ .result = .{ .buf = buf, .is_temporary = is_temporary } });
+ } else {
+ cb(cb_ctx, .{ .result = .{ .buf = buf, .is_temporary = is_temporary } });
+ store.deref();
+ }
+ }
+ pub fn run(this: *ReadFile, task: *ReadFileTask) void {
+ var frame = HTTPClient.getAllocator().create(@Frame(runAsync)) catch unreachable;
+ _ = @asyncCall(std.mem.asBytes(frame), undefined, runAsync, .{ this, task });
+ }
+
+ pub fn onRead(this: *ReadFile, completion: *HTTPClient.NetworkThread.Completion, result: AsyncIO.ReadError!usize) void {
+ this.read_len = @truncate(SizeType, result catch |err| {
+ if (@hasField(HTTPClient.NetworkThread.Completion, "result")) {
+ this.errno = AsyncIO.asError(-completion.result);
+ this.system_error = (JSC.Node.Syscall.Error{
+ .errno = @intCast(JSC.Node.Syscall.Error.Int, -completion.result),
+ .syscall = .read,
+ }).toSystemError();
+ } else {
+ this.errno = err;
+ this.system_error = .{ .code = ZigString.init(std.mem.span(@errorName(err))), .syscall = ZigString.init("read") };
+ }
+ this.read_len = 0;
+ resume this.read_frame;
+ return;
+ });
+
+ resume this.read_frame;
+ }
+
+ fn runAsync(this: *ReadFile, task: *ReadFileTask) void {
+ this.runAsync_();
+ task.onFinish();
+
+ suspend {
+ HTTPClient.getAllocator().destroy(@frame());
+ }
+ }
+
+ fn runAsync_(this: *ReadFile) void {
+ if (this.file_store.pathlike == .fd) {
+ this.opened_fd = this.file_store.pathlike.fd;
+ }
+
+ const fd = this.getFd() catch return;
+ const needs_close = this.file_store.pathlike == .path and fd != null_fd and fd > 2;
+ const stat: std.os.Stat = switch (JSC.Node.Syscall.fstat(fd)) {
+ .result => |result| result,
+ .err => |err| {
+ this.errno = AsyncIO.asError(err.errno);
+ this.system_error = err.toSystemError();
+ return;
+ },
+ };
+ if (std.os.S.ISDIR(stat.mode)) {
+ this.errno = error.EISDIR;
+ this.system_error = JSC.SystemError{
+ .code = ZigString.init("EISDIR"),
+ .path = if (this.file_store.pathlike == .path)
+ ZigString.init(this.file_store.pathlike.path.slice())
+ else
+ ZigString.Empty,
+ .message = ZigString.init("Directories cannot be read like files"),
+ .syscall = ZigString.init("read"),
+ };
+ return;
+ }
+
+ if (stat.size > 0 and std.os.S.ISREG(stat.mode)) {
+ this.size = @minimum(
+ @truncate(SizeType, @intCast(SizeType, @maximum(@intCast(i64, stat.size), 0))),
+ this.max_length,
+ );
+ // read up to 4k at a time if
+ // they didn't explicitly set a size and we're reading from something that's not a regular file
+ } else if (stat.size == 0 and !std.os.S.ISREG(stat.mode)) {
+ this.size = if (this.max_length == Blob.max_size)
+ 4096
+ else
+ this.max_length;
+ }
+
+ if (this.size == 0) {
+ this.buffer = &[_]u8{};
+ this.byte_store = ByteStore.init(this.buffer, bun.default_allocator);
+
+ if (needs_close) {
+ this.doClose() catch {};
+ }
+ return;
+ }
+
+ var bytes = bun.default_allocator.alloc(u8, this.size) catch |err| {
+ this.errno = err;
+ if (needs_close) {
+ this.doClose() catch {};
+ }
+ return;
+ };
+ this.buffer = bytes;
+ this.convert_to_byte_blob = std.os.S.ISREG(stat.mode) and this.file_store.pathlike == .path;
+
+ var remain = bytes;
+ while (remain.len > 0) {
+ var read_len = this.doRead() catch {
+ if (needs_close) {
+ this.doClose() catch {};
+ }
+ return;
+ };
+ this.read_off += read_len;
+ if (read_len == 0) break;
+ remain = remain[read_len..];
+ }
+
+ _ = bun.default_allocator.resize(bytes, this.read_off);
+ this.buffer = bytes[0..this.read_off];
+ this.byte_store = ByteStore.init(this.buffer, bun.default_allocator);
+ }
+ };
+
+ pub const WriteFile = struct {
+ const OpenFrameType = if (Environment.isMac)
+ void
+ else
+ @Frame(WriteFile.getFdLinux);
+
+ file_blob: Blob,
+ bytes_blob: Blob,
+
+ opened_fd: JSC.Node.FileDescriptor = null_fd,
+ open_frame: OpenFrameType = undefined,
+ write_frame: @Frame(WriteFile.doWrite) = undefined,
+ close_frame: @Frame(WriteFile.doClose) = undefined,
+ system_error: ?JSC.SystemError = null,
+ errno: ?anyerror = null,
+ open_completion: HTTPClient.NetworkThread.Completion = undefined,
+
+ write_completion: HTTPClient.NetworkThread.Completion = undefined,
+ close_completion: HTTPClient.NetworkThread.Completion = undefined,
+ task: HTTPClient.NetworkThread.Task = undefined,
+
+ onCompleteCtx: *anyopaque = undefined,
+ onCompleteCallback: OnWriteFileCallback = undefined,
+ wrote: usize = 0,
+
+ pub const ResultType = SystemError.Maybe(SizeType);
+ pub const OnWriteFileCallback = fn (ctx: *anyopaque, count: ResultType) void;
+
+ pub usingnamespace FileOpenerMixin(WriteFile);
+ pub usingnamespace FileCloserMixin(WriteFile);
+
+ // Do not open with APPEND because we may use pwrite()
+ pub const open_flags = std.os.O.WRONLY | std.os.O.CREAT | std.os.O.TRUNC;
+
+ pub fn createWithCtx(
+ allocator: std.mem.Allocator,
+ file_blob: Blob,
+ bytes_blob: Blob,
+ onWriteFileContext: *anyopaque,
+ onCompleteCallback: OnWriteFileCallback,
+ ) !*WriteFile {
+ var read_file = try allocator.create(WriteFile);
+ read_file.* = WriteFile{
+ .file_blob = file_blob,
+ .bytes_blob = bytes_blob,
+ .onCompleteCtx = onWriteFileContext,
+ .onCompleteCallback = onCompleteCallback,
+ };
+ file_blob.store.?.ref();
+ bytes_blob.store.?.ref();
+ return read_file;
+ }
+
+ pub fn create(
+ allocator: std.mem.Allocator,
+ file_blob: Blob,
+ bytes_blob: Blob,
+ comptime Context: type,
+ context: Context,
+ comptime callback: fn (ctx: Context, bytes: ResultType) void,
+ ) !*WriteFile {
+ const Handler = struct {
+ pub fn run(ptr: *anyopaque, bytes: ResultType) void {
+ callback(bun.cast(Context, ptr), bytes);
+ }
+ };
+
+ return try WriteFile.createWithCtx(
+ allocator,
+ file_blob,
+ bytes_blob,
+ @ptrCast(*anyopaque, context),
+ Handler.run,
+ );
+ }
+
+ pub fn doWrite(
+ this: *WriteFile,
+ buffer: []const u8,
+ file_offset: u64,
+ ) AsyncIO.WriteError!SizeType {
+ var aio = &AsyncIO.global;
+ this.wrote = 0;
+ const fd = this.opened_fd;
+ aio.write(
+ *WriteFile,
+ this,
+ onWrite,
+ &this.write_completion,
+ fd,
+ buffer,
+ if (fd > 2) file_offset else 0,
+ );
+
+ suspend {
+ this.write_frame = @frame().*;
+ }
+
+ if (this.errno) |errno| {
+ this.system_error = this.system_error orelse JSC.SystemError{
+ .code = ZigString.init(std.mem.span(@errorName(errno))),
+ .syscall = ZigString.init("write"),
+ };
+ return @errSetCast(AsyncIO.WriteError, errno);
+ }
+
+ return @truncate(SizeType, this.wrote);
+ }
+
+ pub const WriteFileTask = JSC.IOTask(@This());
+
+ pub fn then(this: *WriteFile, _: *JSC.JSGlobalObject) void {
+ var cb = this.onCompleteCallback;
+ var cb_ctx = this.onCompleteCtx;
+
+ this.bytes_blob.store.?.deref();
+ this.file_blob.store.?.deref();
+
+ if (this.system_error) |err| {
+ bun.default_allocator.destroy(this);
+ cb(cb_ctx, .{
+ .err = err,
+ });
+ return;
+ }
+
+ const wrote = this.wrote;
+ bun.default_allocator.destroy(this);
+ cb(cb_ctx, .{ .result = @truncate(SizeType, wrote) });
+ }
+ pub fn run(this: *WriteFile, task: *WriteFileTask) void {
+ var frame = HTTPClient.getAllocator().create(@Frame(runAsync)) catch unreachable;
+ _ = @asyncCall(std.mem.asBytes(frame), undefined, runAsync, .{ this, task });
+ }
+
+ fn runAsync(this: *WriteFile, task: *WriteFileTask) void {
+ this._runAsync();
+ task.onFinish();
+ suspend {
+ HTTPClient.getAllocator().destroy(@frame());
+ }
+ }
+
+ pub fn onWrite(this: *WriteFile, _: *HTTPClient.NetworkThread.Completion, result: AsyncIO.WriteError!usize) void {
+ this.wrote += @truncate(SizeType, result catch |err| {
+ this.errno = err;
+ this.wrote = 0;
+ resume this.write_frame;
+ return;
+ });
+
+ resume this.write_frame;
+ }
+
+ fn _runAsync(this: *WriteFile) void {
+ const file = this.file_blob.store.?.data.file;
+ if (file.pathlike == .fd) {
+ this.opened_fd = file.pathlike.fd;
+ }
+
+ const fd = this.getFd() catch return;
+ const needs_close = file.pathlike == .path and fd > 2;
+
+ var remain = this.bytes_blob.sharedView();
+
+ var total_written: usize = 0;
+ var file_offset = this.file_blob.offset;
+
+ const end =
+ @minimum(this.file_blob.size, remain.len);
+
+ while (remain.len > 0 and total_written < end) {
+ const wrote_len = this.doWrite(remain, file_offset) catch {
+ if (needs_close) {
+ this.doClose() catch {};
+ }
+ this.wrote = @truncate(SizeType, total_written);
+ return;
+ };
+ remain = remain[wrote_len..];
+ total_written += wrote_len;
+ file_offset += wrote_len;
+ if (wrote_len == 0) break;
+ }
+
+ this.wrote = @truncate(SizeType, total_written);
+
+ if (needs_close) {
+ this.doClose() catch {};
+ }
+ }
+ };
+
+ pub const IOWhich = enum {
+ source,
+ destination,
+ both,
+ };
+
+ const unsupported_directory_error = SystemError{
+ .errno = @intCast(c_int, @enumToInt(bun.C.SystemErrno.EISDIR)),
+ .message = ZigString.init("That doesn't work on folders"),
+ .syscall = ZigString.init("fstat"),
+ };
+ const unsupported_non_regular_file_error = SystemError{
+ .errno = @intCast(c_int, @enumToInt(bun.C.SystemErrno.ENOTSUP)),
+ .message = ZigString.init("Non-regular files aren't supported yet"),
+ .syscall = ZigString.init("fstat"),
+ };
+
+ // blocking, but off the main thread
+ pub const CopyFile = struct {
+ destination_file_store: FileStore,
+ source_file_store: FileStore,
+ store: ?*Store = null,
+ source_store: ?*Store = null,
+ offset: SizeType = 0,
+ size: SizeType = 0,
+ max_length: SizeType = Blob.max_size,
+ destination_fd: JSC.Node.FileDescriptor = null_fd,
+ source_fd: JSC.Node.FileDescriptor = null_fd,
+
+ system_error: ?SystemError = null,
+
+ read_len: SizeType = 0,
+ read_off: SizeType = 0,
+
+ globalThis: *JSGlobalObject,
+
+ pub const ResultType = anyerror!SizeType;
+
+ pub const Callback = fn (ctx: *anyopaque, len: ResultType) void;
+ pub const CopyFilePromiseTask = JSC.ConcurrentPromiseTask(CopyFile);
+ pub const CopyFilePromiseTaskEventLoopTask = CopyFilePromiseTask.EventLoopTask;
+
+ pub fn create(
+ allocator: std.mem.Allocator,
+ store: *Store,
+ source_store: *Store,
+ off: SizeType,
+ max_len: SizeType,
+ globalThis: *JSC.JSGlobalObject,
+ ) !*CopyFilePromiseTask {
+ var read_file = try allocator.create(CopyFile);
+ read_file.* = CopyFile{
+ .store = store,
+ .source_store = source_store,
+ .offset = off,
+ .max_length = max_len,
+ .globalThis = globalThis,
+ .destination_file_store = store.data.file,
+ .source_file_store = source_store.data.file,
+ };
+ store.ref();
+ source_store.ref();
+ return try CopyFilePromiseTask.createOnJSThread(allocator, globalThis, read_file);
+ }
+
+ const linux = std.os.linux;
+ const darwin = std.os.darwin;
+
+ pub fn deinit(this: *CopyFile) void {
+ if (this.source_file_store.pathlike == .path) {
+ if (this.source_file_store.pathlike.path == .string and this.system_error == null) {
+ bun.default_allocator.free(bun.constStrToU8(this.source_file_store.pathlike.path.slice()));
+ }
+ }
+ this.store.?.deref();
+
+ bun.default_allocator.destroy(this);
+ }
+
+ pub fn reject(this: *CopyFile, promise: *JSC.JSInternalPromise) void {
+ var globalThis = this.globalThis;
+ var system_error: SystemError = this.system_error orelse SystemError{};
+ if (this.source_file_store.pathlike == .path and system_error.path.len == 0) {
+ system_error.path = ZigString.init(this.source_file_store.pathlike.path.slice());
+ system_error.path.mark();
+ }
+
+ if (system_error.message.len == 0) {
+ system_error.message = ZigString.init("Failed to copy file");
+ }
+
+ var instance = system_error.toErrorInstance(this.globalThis);
+ if (this.store) |store| {
+ store.deref();
+ }
+ promise.reject(globalThis, instance);
+ }
+
+ pub fn then(this: *CopyFile, promise: *JSC.JSInternalPromise) void {
+ this.source_store.?.deref();
+
+ if (this.system_error != null) {
+ this.reject(promise);
+ return;
+ }
+
+ promise.resolve(this.globalThis, JSC.JSValue.jsNumberFromUint64(this.read_len));
+ }
+
+ pub fn run(this: *CopyFile) void {
+ this.runAsync();
+ }
+
+ pub fn doClose(this: *CopyFile) void {
+ const close_input = this.destination_file_store.pathlike != .fd and this.destination_fd != null_fd;
+ const close_output = this.source_file_store.pathlike != .fd and this.source_fd != null_fd;
+
+ if (close_input and close_output) {
+ this.doCloseFile(.both);
+ } else if (close_input) {
+ this.doCloseFile(.destination);
+ } else if (close_output) {
+ this.doCloseFile(.source);
+ }
+ }
+
+ const os = std.os;
+
+ pub fn doCloseFile(this: *CopyFile, comptime which: IOWhich) void {
+ switch (which) {
+ .both => {
+ _ = JSC.Node.Syscall.close(this.destination_fd);
+ _ = JSC.Node.Syscall.close(this.source_fd);
+ },
+ .destination => {
+ _ = JSC.Node.Syscall.close(this.destination_fd);
+ },
+ .source => {
+ _ = JSC.Node.Syscall.close(this.source_fd);
+ },
+ }
+ }
+
+ const O = if (Environment.isLinux) linux.O else std.os.O;
+ const open_destination_flags = O.CLOEXEC | O.CREAT | O.WRONLY | O.TRUNC;
+ const open_source_flags = O.CLOEXEC | O.RDONLY;
+
+ pub fn doOpenFile(this: *CopyFile, comptime which: IOWhich) !void {
+ // open source file first
+ // if it fails, we don't want the extra destination file hanging out
+ if (which == .both or which == .source) {
+ this.source_fd = switch (JSC.Node.Syscall.open(
+ this.source_file_store.pathlike.path.sliceZAssume(),
+ open_source_flags,
+ 0,
+ )) {
+ .result => |result| result,
+ .err => |errno| {
+ this.system_error = errno.toSystemError();
+ return AsyncIO.asError(errno.errno);
+ },
+ };
+ }
+
+ if (which == .both or which == .destination) {
+ this.destination_fd = switch (JSC.Node.Syscall.open(
+ this.destination_file_store.pathlike.path.sliceZAssume(),
+ open_destination_flags,
+ JSC.Node.default_permission,
+ )) {
+ .result => |result| result,
+ .err => |errno| {
+ if (which == .both) {
+ _ = JSC.Node.Syscall.close(this.source_fd);
+ this.source_fd = 0;
+ }
+
+ this.system_error = errno.toSystemError();
+ return AsyncIO.asError(errno.errno);
+ },
+ };
+ }
+ }
+
+ const TryWith = enum {
+ sendfile,
+ copy_file_range,
+ splice,
+
+ pub const tag = std.EnumMap(TryWith, JSC.Node.Syscall.Tag).init(.{
+ .sendfile = .sendfile,
+ .copy_file_range = .copy_file_range,
+ .splice = .splice,
+ });
+ };
+
+ pub fn doCopyFileRange(
+ this: *CopyFile,
+ comptime use: TryWith,
+ comptime clear_append_if_invalid: bool,
+ ) anyerror!void {
+ this.read_off += this.offset;
+
+ var remain = @as(usize, this.max_length);
+ if (remain == max_size or remain == 0) {
+ // sometimes stat lies
+ // let's give it 4096 and see how it goes
+ remain = 4096;
+ }
+
+ var total_written: usize = 0;
+ const src_fd = this.source_fd;
+ const dest_fd = this.destination_fd;
+
+ defer {
+ this.read_len = @truncate(SizeType, total_written);
+ }
+
+ var has_unset_append = false;
+
+ while (true) {
+ const written = switch (comptime use) {
+ .copy_file_range => linux.copy_file_range(src_fd, null, dest_fd, null, remain, 0),
+ .sendfile => linux.sendfile(dest_fd, src_fd, null, remain),
+ .splice => bun.C.splice(src_fd, null, dest_fd, null, remain, 0),
+ };
+
+ switch (linux.getErrno(written)) {
+ .SUCCESS => {},
+
+ .INVAL => {
+ if (comptime clear_append_if_invalid) {
+ if (!has_unset_append) {
+ // https://kylelaker.com/2018/08/31/stdout-oappend.html
+ // make() can set STDOUT / STDERR to O_APPEND
+ // this messes up sendfile()
+ has_unset_append = true;
+ const flags = linux.fcntl(dest_fd, linux.F.GETFL, 0);
+ if ((flags & O.APPEND) != 0) {
+ _ = linux.fcntl(dest_fd, linux.F.SETFL, flags ^ O.APPEND);
+ continue;
+ }
+ }
+ }
+
+ this.system_error = (JSC.Node.Syscall.Error{
+ .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(linux.E.INVAL)),
+ .syscall = TryWith.tag.get(use).?,
+ }).toSystemError();
+ return AsyncIO.asError(linux.E.INVAL);
+ },
+ else => |errno| {
+ this.system_error = (JSC.Node.Syscall.Error{
+ .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(errno)),
+ .syscall = TryWith.tag.get(use).?,
+ }).toSystemError();
+ return AsyncIO.asError(errno);
+ },
+ }
+
+ // wrote zero bytes means EOF
+ remain -|= written;
+ total_written += written;
+ if (written == 0 or remain == 0) break;
+ }
+ }
+
+ pub fn doFCopyFile(this: *CopyFile) anyerror!void {
+ switch (JSC.Node.Syscall.fcopyfile(this.source_fd, this.destination_fd, os.system.COPYFILE_DATA)) {
+ .err => |errno| {
+ this.system_error = errno.toSystemError();
+
+ return AsyncIO.asError(errno.errno);
+ },
+ .result => {},
+ }
+ }
+
+ pub fn doClonefile(this: *CopyFile) anyerror!void {
+ var source_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+
+ switch (JSC.Node.Syscall.clonefile(
+ this.source_file_store.pathlike.path.sliceZ(&source_buf),
+ this.destination_file_store.pathlike.path.sliceZ(
+ &dest_buf,
+ ),
+ )) {
+ .err => |errno| {
+ this.system_error = errno.toSystemError();
+ return AsyncIO.asError(errno.errno);
+ },
+ .result => {},
+ }
+ }
+
+ pub fn runAsync(this: *CopyFile) void {
+ // defer task.onFinish();
+
+ var stat_: ?std.os.Stat = null;
+
+ if (this.destination_file_store.pathlike == .fd) {
+ this.destination_fd = this.destination_file_store.pathlike.fd;
+ }
+
+ if (this.source_file_store.pathlike == .fd) {
+ this.source_fd = this.source_file_store.pathlike.fd;
+ }
+
+ // Do we need to open both files?
+ if (this.destination_fd == null_fd and this.source_fd == null_fd) {
+
+ // First, we attempt to clonefile() on macOS
+ // This is the fastest way to copy a file.
+ if (comptime Environment.isMac) {
+ if (this.offset == 0 and this.source_file_store.pathlike == .path and this.destination_file_store.pathlike == .path) {
+ do_clonefile: {
+
+ // stat the output file, make sure it:
+ // 1. Exists
+ switch (JSC.Node.Syscall.stat(this.source_file_store.pathlike.path.sliceZAssume())) {
+ .result => |result| {
+ stat_ = result;
+
+ if (os.S.ISDIR(result.mode)) {
+ this.system_error = unsupported_directory_error;
+ return;
+ }
+
+ if (!os.S.ISREG(result.mode))
+ break :do_clonefile;
+ },
+ .err => |err| {
+ // If we can't stat it, we also can't copy it.
+ this.system_error = err.toSystemError();
+ return;
+ },
+ }
+
+ if (this.doClonefile()) {
+ if (this.max_length != Blob.max_size and this.max_length < @intCast(SizeType, stat_.?.size)) {
+ // If this fails...well, there's not much we can do about it.
+ _ = bun.C.truncate(
+ this.destination_file_store.pathlike.path.sliceZAssume(),
+ @intCast(std.os.off_t, this.max_length),
+ );
+ this.read_len = @intCast(SizeType, this.max_length);
+ } else {
+ this.read_len = @intCast(SizeType, stat_.?.size);
+ }
+ return;
+ } else |_| {
+
+ // this may still fail, in which case we just continue trying with fcopyfile
+ // it can fail when the input file already exists
+ // or if the output is not a directory
+ // or if it's a network volume
+ this.system_error = null;
+ }
+ }
+ }
+ }
+
+ this.doOpenFile(.both) catch return;
+ // Do we need to open only one file?
+ } else if (this.destination_fd == null_fd) {
+ this.source_fd = this.source_file_store.pathlike.fd;
+
+ this.doOpenFile(.destination) catch return;
+ // Do we need to open only one file?
+ } else if (this.source_fd == null_fd) {
+ this.destination_fd = this.destination_file_store.pathlike.fd;
+
+ this.doOpenFile(.source) catch return;
+ }
+
+ if (this.system_error != null) {
+ return;
+ }
+
+ std.debug.assert(this.destination_fd != null_fd);
+ std.debug.assert(this.source_fd != null_fd);
+
+ if (this.destination_file_store.pathlike == .fd) {}
+
+ const stat: std.os.Stat = stat_ orelse switch (JSC.Node.Syscall.fstat(this.source_fd)) {
+ .result => |result| result,
+ .err => |err| {
+ this.doClose();
+ this.system_error = err.toSystemError();
+ return;
+ },
+ };
+
+ if (os.S.ISDIR(stat.mode)) {
+ this.system_error = unsupported_directory_error;
+ this.doClose();
+ return;
+ }
+
+ if (stat.size != 0) {
+ this.max_length = @maximum(@minimum(@intCast(SizeType, stat.size), this.max_length), this.offset) - this.offset;
+ if (this.max_length == 0) {
+ this.doClose();
+ return;
+ }
+
+ if (os.S.ISREG(stat.mode) and
+ this.max_length > std.mem.page_size and
+ this.max_length != Blob.max_size)
+ {
+ bun.C.preallocate_file(this.destination_fd, 0, this.max_length) catch {};
+ }
+ }
+
+ if (comptime Environment.isLinux) {
+
+ // Bun.write(Bun.file("a"), Bun.file("b"))
+ if (os.S.ISREG(stat.mode) and (os.S.ISREG(this.destination_file_store.mode) or this.destination_file_store.mode == 0)) {
+ if (this.destination_file_store.is_atty orelse false) {
+ this.doCopyFileRange(.copy_file_range, true) catch {};
+ } else {
+ this.doCopyFileRange(.copy_file_range, false) catch {};
+ }
+
+ this.doClose();
+ return;
+ }
+
+ // $ bun run foo.js | bun run bar.js
+ if (os.S.ISFIFO(stat.mode) and os.S.ISFIFO(this.destination_file_store.mode)) {
+ if (this.destination_file_store.is_atty orelse false) {
+ this.doCopyFileRange(.splice, true) catch {};
+ } else {
+ this.doCopyFileRange(.splice, false) catch {};
+ }
+
+ this.doClose();
+ return;
+ }
+
+ if (os.S.ISREG(stat.mode) or os.S.ISCHR(stat.mode) or os.S.ISSOCK(stat.mode)) {
+ if (this.destination_file_store.is_atty orelse false) {
+ this.doCopyFileRange(.sendfile, true) catch {};
+ } else {
+ this.doCopyFileRange(.sendfile, false) catch {};
+ }
+
+ this.doClose();
+ return;
+ }
+
+ this.system_error = unsupported_non_regular_file_error;
+ this.doClose();
+ return;
+ }
+
+ if (comptime Environment.isMac) {
+ this.doFCopyFile() catch {
+ this.doClose();
+
+ return;
+ };
+ if (stat.size != 0 and @intCast(SizeType, stat.size) > this.max_length) {
+ _ = darwin.ftruncate(this.destination_fd, @intCast(std.os.off_t, this.max_length));
+ }
+
+ this.doClose();
+ } else {
+ @compileError("TODO: implement copyfile");
+ }
+ }
+ };
+ };
+
+ pub const FileStore = struct {
+ pathlike: JSC.Node.PathOrFileDescriptor,
+ mime_type: HTTPClient.MimeType = HTTPClient.MimeType.other,
+ is_atty: ?bool = null,
+ mode: JSC.Node.Mode = 0,
+ seekable: ?bool = null,
+ max_size: SizeType = 0,
+
+ pub fn init(pathlike: JSC.Node.PathOrFileDescriptor, mime_type: ?HTTPClient.MimeType) FileStore {
+ return .{ .pathlike = pathlike, .mime_type = mime_type orelse HTTPClient.MimeType.other };
+ }
+ };
+
+ pub const ByteStore = struct {
+ ptr: [*]u8 = undefined,
+ len: SizeType = 0,
+ cap: SizeType = 0,
+ allocator: std.mem.Allocator,
+
+ pub fn init(bytes: []u8, allocator: std.mem.Allocator) ByteStore {
+ return .{
+ .ptr = bytes.ptr,
+ .len = @truncate(SizeType, bytes.len),
+ .cap = @truncate(SizeType, bytes.len),
+ .allocator = allocator,
+ };
+ }
+
+ pub fn fromArrayList(list: std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !*ByteStore {
+ return ByteStore.init(list.items, allocator);
+ }
+
+ pub fn slice(this: ByteStore) []u8 {
+ return this.ptr[0..this.len];
+ }
+
+ pub fn deinit(this: *ByteStore) void {
+ this.allocator.free(this.ptr[0..this.cap]);
+ }
+
+ pub fn asArrayList(this: ByteStore) std.ArrayListUnmanaged(u8) {
+ return this.asArrayListLeak();
+ }
+
+ pub fn asArrayListLeak(this: ByteStore) std.ArrayListUnmanaged(u8) {
+ return .{
+ .items = this.ptr[0..this.len],
+ .capacity = this.cap,
+ };
+ }
+ };
+
+ pub const Constructor = JSC.NewConstructor(
+ Blob,
+ .{
+ .constructor = .{ .rfn = constructor },
+ },
+ .{},
+ );
+
+ pub const Class = NewClass(
+ Blob,
+ .{ .name = "Blob" },
+ .{ .finalize = finalize, .text = .{
+ .rfn = getText,
+ }, .json = .{
+ .rfn = getJSON,
+ }, .arrayBuffer = .{
+ .rfn = getArrayBuffer,
+ }, .slice = .{
+ .rfn = getSlice,
+ }, .stream = .{
+ .rfn = getStream,
+ } },
+ .{
+ .@"type" = .{
+ .get = getType,
+ .set = setType,
+ },
+ .@"size" = .{
+ .get = getSize,
+ .ro = true,
+ },
+ },
+ );
+
+ pub fn getStream(
+ this: *Blob,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) JSC.C.JSValueRef {
+ var recommended_chunk_size: SizeType = 0;
+ if (arguments.len > 0) {
+ if (!JSValue.c(arguments[0]).isNumber() and !JSValue.c(arguments[0]).isUndefinedOrNull()) {
+ JSC.throwInvalidArguments("chunkSize must be a number", .{}, ctx, exception);
+ return null;
+ }
+
+ recommended_chunk_size = @intCast(SizeType, @maximum(0, @truncate(i52, JSValue.c(arguments[0]).toInt64())));
+ }
+ return JSC.WebCore.ReadableStream.fromBlob(
+ ctx.ptr(),
+ this,
+ recommended_chunk_size,
+ ).asObjectRef();
+ }
+
+ fn promisified(
+ value: JSC.JSValue,
+ global: *JSGlobalObject,
+ ) JSC.JSValue {
+ if (value.isError()) {
+ return JSC.JSPromise.rejectedPromiseValue(global, value);
+ }
+
+ if (value.jsType() == .JSPromise)
+ return value;
+
+ return JSC.JSPromise.resolvedPromiseValue(global, value);
+ }
+
+ pub fn getText(
+ this: *Blob,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) JSC.C.JSObjectRef {
+ return promisified(this.toString(ctx.ptr(), .clone), ctx.ptr()).asObjectRef();
+ }
+
+ pub fn getTextTransfer(
+ this: *Blob,
+ ctx: js.JSContextRef,
+ ) JSC.C.JSObjectRef {
+ return promisified(this.toString(ctx.ptr(), .transfer), ctx.ptr()).asObjectRef();
+ }
+
+ pub fn getJSON(
+ this: *Blob,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) JSC.C.JSObjectRef {
+ return promisified(this.toJSON(ctx.ptr(), .share), ctx.ptr()).asObjectRef();
+ }
+
+ pub fn getArrayBufferTransfer(
+ this: *Blob,
+ ctx: js.JSContextRef,
+ ) JSC.C.JSObjectRef {
+ return promisified(this.toArrayBuffer(ctx.ptr(), .transfer), ctx.ptr()).asObjectRef();
+ }
+
+ pub fn getArrayBuffer(
+ this: *Blob,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) JSC.C.JSObjectRef {
+ return promisified(this.toArrayBuffer(ctx.ptr(), .clone), ctx.ptr()).asObjectRef();
+ }
+
+ /// https://w3c.github.io/FileAPI/#slice-method-algo
+ /// The slice() method returns a new Blob object with bytes ranging from the
+ /// optional start parameter up to but not including the optional end
+ /// parameter, and with a type attribute that is the value of the optional
+ /// contentType parameter. It must act as follows:
+ pub fn getSlice(
+ this: *Blob,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ args: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) JSC.C.JSObjectRef {
+ if (this.size == 0) {
+ return constructor(ctx, null, &[_]js.JSValueRef{}, exception);
+ }
+ // If the optional start parameter is not used as a parameter when making this call, let relativeStart be 0.
+ var relativeStart: i64 = 0;
+
+ // If the optional end parameter is not used as a parameter when making this call, let relativeEnd be size.
+ var relativeEnd: i64 = @intCast(i64, this.size);
+
+ var args_iter = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), args);
+ if (args_iter.nextEat()) |start_| {
+ const start = start_.toInt64();
+ if (start < 0) {
+ // If the optional start parameter is negative, let relativeStart be start + size.
+ relativeStart = @intCast(i64, @maximum(start + @intCast(i64, this.size), 0));
+ } else {
+ // Otherwise, let relativeStart be start.
+ relativeStart = @minimum(@intCast(i64, start), @intCast(i64, this.size));
+ }
+ }
+
+ if (args_iter.nextEat()) |end_| {
+ const end = end_.toInt64();
+ // If end is negative, let relativeEnd be max((size + end), 0).
+ if (end < 0) {
+ // If the optional start parameter is negative, let relativeStart be start + size.
+ relativeEnd = @intCast(i64, @maximum(end + @intCast(i64, this.size), 0));
+ } else {
+ // Otherwise, let relativeStart be start.
+ relativeEnd = @minimum(@intCast(i64, end), @intCast(i64, this.size));
+ }
+ }
+
+ var content_type: string = "";
+ if (args_iter.nextEat()) |content_type_| {
+ if (content_type_.isString()) {
+ var zig_str = content_type_.getZigString(ctx.ptr());
+ var slicer = zig_str.toSlice(bun.default_allocator);
+ defer slicer.deinit();
+ var slice = slicer.slice();
+ var content_type_buf = getAllocator(ctx).alloc(u8, slice.len) catch unreachable;
+ content_type = strings.copyLowercase(slice, content_type_buf);
+ }
+ }
+
+ const len = @intCast(SizeType, @maximum(relativeEnd - relativeStart, 0));
+
+ // This copies over the is_all_ascii flag
+ // which is okay because this will only be a <= slice
+ var blob = this.dupe();
+ blob.offset = @intCast(SizeType, relativeStart);
+ blob.size = len;
+ blob.content_type = content_type;
+ blob.content_type_allocated = content_type.len > 0;
+
+ var blob_ = getAllocator(ctx).create(Blob) catch unreachable;
+ blob_.* = blob;
+ blob_.allocator = getAllocator(ctx);
+ return Blob.Class.make(ctx, blob_);
+ }
+
+ pub fn getType(
+ this: *Blob,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return ZigString.init(this.content_type).toValue(ctx.ptr()).asObjectRef();
+ }
+
+ pub fn setType(
+ this: *Blob,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ value: js.JSValueRef,
+ _: js.ExceptionRef,
+ ) bool {
+ var zig_str = JSValue.fromRef(value).getZigString(ctx.ptr());
+ if (zig_str.is16Bit())
+ return false;
+
+ var slice = zig_str.trimmedSlice();
+ if (strings.eql(slice, this.content_type))
+ return true;
+
+ const prev_content_type = this.content_type;
+ {
+ defer if (this.content_type_allocated) bun.default_allocator.free(prev_content_type);
+ var content_type_buf = getAllocator(ctx).alloc(u8, slice.len) catch unreachable;
+ this.content_type = strings.copyLowercase(slice, content_type_buf);
+ }
+
+ this.content_type_allocated = true;
+ return true;
+ }
+
+ pub fn getSize(
+ this: *Blob,
+ _: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ if (this.size == Blob.max_size) {
+ this.resolveSize();
+ if (this.size == Blob.max_size and this.store != null) {
+ return JSValue.jsNumberFromChar(0).asRef();
+ }
+ }
+
+ if (this.size < std.math.maxInt(i32)) {
+ return JSValue.jsNumber(this.size).asRef();
+ }
+
+ return JSC.JSValue.jsNumberFromUint64(this.size).asRef();
+ }
+
+ pub fn resolveSize(this: *Blob) void {
+ if (this.store) |store| {
+ if (store.data == .bytes) {
+ const offset = this.offset;
+ const store_size = store.size();
+ if (store_size != Blob.max_size) {
+ this.offset = @minimum(store_size, offset);
+ this.size = store_size - offset;
+ }
+ }
+ } else {
+ this.size = 0;
+ }
+ }
+
+ pub fn constructor(
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ args: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) js.JSObjectRef {
+ var blob: Blob = undefined;
+ switch (args.len) {
+ 0 => {
+ var empty: []u8 = &[_]u8{};
+ blob = Blob.init(empty, getAllocator(ctx), ctx.ptr());
+ },
+ else => {
+ blob = fromJS(ctx.ptr(), JSValue.fromRef(args[0]), false, true) catch |err| {
+ if (err == error.InvalidArguments) {
+ JSC.JSError(getAllocator(ctx), "new Blob() expects an Array", .{}, ctx, exception);
+ return null;
+ }
+ JSC.JSError(getAllocator(ctx), "out of memory :(", .{}, ctx, exception);
+ return null;
+ };
+
+ if (args.len > 1) {
+ var options = JSValue.fromRef(args[1]);
+ if (options.isCell()) {
+ // type, the ASCII-encoded string in lower case
+ // representing the media type of the Blob.
+ // Normative conditions for this member are provided
+ // in the § 3.1 Constructors.
+ if (options.get(ctx.ptr(), "type")) |content_type| {
+ if (content_type.isString()) {
+ var content_type_str = content_type.getZigString(ctx.ptr());
+ if (!content_type_str.is16Bit()) {
+ var slice = content_type_str.trimmedSlice();
+ var content_type_buf = getAllocator(ctx).alloc(u8, slice.len) catch unreachable;
+ blob.content_type = strings.copyLowercase(slice, content_type_buf);
+ blob.content_type_allocated = true;
+ }
+ }
+ }
+ }
+ }
+
+ if (blob.content_type.len == 0) {
+ blob.content_type = "";
+ }
+ },
+ }
+
+ var blob_ = getAllocator(ctx).create(Blob) catch unreachable;
+ blob_.* = blob;
+ blob_.allocator = getAllocator(ctx);
+ return Blob.Class.make(ctx, blob_);
+ }
+
+ pub fn finalize(this: *Blob) void {
+ this.deinit();
+ }
+
+ pub fn initWithAllASCII(bytes: []u8, allocator: std.mem.Allocator, globalThis: *JSGlobalObject, is_all_ascii: bool) Blob {
+ // avoid allocating a Blob.Store if the buffer is actually empty
+ var store: ?*Blob.Store = null;
+ if (bytes.len > 0) {
+ store = Blob.Store.init(bytes, allocator) catch unreachable;
+ store.?.is_all_ascii = is_all_ascii;
+ }
+ return Blob{
+ .size = @truncate(SizeType, bytes.len),
+ .store = store,
+ .allocator = null,
+ .content_type = "",
+ .globalThis = globalThis,
+ .is_all_ascii = is_all_ascii,
+ };
+ }
+
+ pub fn init(bytes: []u8, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) Blob {
+ return Blob{
+ .size = @truncate(SizeType, bytes.len),
+ .store = if (bytes.len > 0)
+ Blob.Store.init(bytes, allocator) catch unreachable
+ else
+ null,
+ .allocator = null,
+ .content_type = "",
+ .globalThis = globalThis,
+ };
+ }
+
+ pub fn initWithStore(store: *Blob.Store, globalThis: *JSGlobalObject) Blob {
+ return Blob{
+ .size = store.size(),
+ .store = store,
+ .allocator = null,
+ .content_type = if (store.data == .file)
+ store.data.file.mime_type.value
+ else
+ "",
+ .globalThis = globalThis,
+ };
+ }
+
+ pub fn initEmpty(globalThis: *JSGlobalObject) Blob {
+ return Blob{
+ .size = 0,
+ .store = null,
+ .allocator = null,
+ .content_type = "",
+ .globalThis = globalThis,
+ };
+ }
+
+ // Transferring doesn't change the reference count
+ // It is a move
+ inline fn transfer(this: *Blob) void {
+ this.store = null;
+ }
+
+ pub fn detach(this: *Blob) void {
+ if (this.store != null) this.store.?.deref();
+ this.store = null;
+ }
+
+ /// This does not duplicate
+ /// This creates a new view
+ /// and increment the reference count
+ pub fn dupe(this: *const Blob) Blob {
+ if (this.store != null) this.store.?.ref();
+ var duped = this.*;
+ duped.allocator = null;
+ return duped;
+ }
+
+ pub fn deinit(this: *Blob) void {
+ this.detach();
+
+ if (this.allocator) |alloc| {
+ this.allocator = null;
+ alloc.destroy(this);
+ }
+ }
+
+ pub fn sharedView(this: *const Blob) []const u8 {
+ if (this.size == 0 or this.store == null) return "";
+ var slice_ = this.store.?.sharedView();
+ if (slice_.len == 0) return "";
+ slice_ = slice_[this.offset..];
+
+ return slice_[0..@minimum(slice_.len, @as(usize, this.size))];
+ }
+
+ pub fn view(this: *const Blob) []const u8 {
+ if (this.size == 0 or this.store == null) return "";
+ return this.store.?.sharedView()[this.offset..][0..this.size];
+ }
+
+ pub const Lifetime = JSC.WebCore.Lifetime;
+ pub fn setIsASCIIFlag(this: *Blob, is_all_ascii: bool) void {
+ this.is_all_ascii = is_all_ascii;
+ // if this Blob represents the entire binary data
+ // which will be pretty common
+ // we can update the store's is_all_ascii flag
+ // and any other Blob that points to the same store
+ // can skip checking the encoding
+ if (this.size > 0 and this.offset == 0 and this.store.?.data == .bytes) {
+ this.store.?.is_all_ascii = is_all_ascii;
+ }
+ }
+
+ pub fn NewReadFileHandler(comptime Function: anytype, comptime lifetime: Lifetime) type {
+ return struct {
+ context: Blob,
+ promise: *JSPromise,
+ globalThis: *JSGlobalObject,
+ pub fn run(handler: *@This(), bytes_: Blob.Store.ReadFile.ResultType) void {
+ var promise = handler.promise;
+ var blob = handler.context;
+ blob.allocator = null;
+ var globalThis = handler.globalThis;
+ bun.default_allocator.destroy(handler);
+ switch (bytes_) {
+ .result => |result| {
+ const bytes = result.buf;
+ const is_temporary = result.is_temporary;
+ if (blob.size > 0)
+ blob.size = @minimum(@truncate(u32, bytes.len), blob.size);
+ if (!is_temporary) {
+ promise.resolve(globalThis, Function(&blob, globalThis, bytes, comptime lifetime));
+ } else {
+ promise.resolve(globalThis, Function(&blob, globalThis, bytes, .temporary));
+ }
+ },
+ .err => |err| {
+ promise.reject(globalThis, err.toErrorInstance(globalThis));
+ },
+ }
+ }
+ };
+ }
+
+ pub const WriteFilePromise = struct {
+ promise: *JSPromise,
+ globalThis: *JSGlobalObject,
+ pub fn run(handler: *@This(), count: Blob.Store.WriteFile.ResultType) void {
+ var promise = handler.promise;
+ var globalThis = handler.globalThis;
+ bun.default_allocator.destroy(handler);
+ switch (count) {
+ .err => |err| {
+ promise.reject(globalThis, err.toErrorInstance(globalThis));
+ },
+ .result => |wrote| {
+ promise.resolve(globalThis, JSC.JSValue.jsNumberFromUint64(wrote));
+ },
+ }
+ }
+ };
+
+ pub fn NewInternalReadFileHandler(comptime Context: type, comptime Function: anytype) type {
+ return struct {
+ pub fn run(handler: *anyopaque, bytes_: Store.ReadFile.ResultType) void {
+ Function(bun.cast(Context, handler), bytes_);
+ }
+ };
+ }
+
+ pub fn doReadFileInternal(this: *Blob, comptime Handler: type, ctx: Handler, comptime Function: anytype, global: *JSGlobalObject) void {
+ var file_read = Store.ReadFile.createWithCtx(
+ bun.default_allocator,
+ this.store.?,
+ ctx,
+ NewInternalReadFileHandler(Handler, Function).run,
+ this.offset,
+ this.size,
+ ) catch unreachable;
+ var read_file_task = Store.ReadFile.ReadFileTask.createOnJSThread(bun.default_allocator, global, file_read) catch unreachable;
+ read_file_task.schedule();
+ }
+
+ pub fn doReadFile(this: *Blob, comptime Function: anytype, comptime lifetime: Lifetime, global: *JSGlobalObject) JSValue {
+ const Handler = NewReadFileHandler(Function, lifetime);
+ var promise = JSPromise.create(global);
+
+ var handler = Handler{
+ .context = this.*,
+ .promise = promise,
+ .globalThis = global,
+ };
+
+ var ptr = bun.default_allocator.create(Handler) catch unreachable;
+ ptr.* = handler;
+ var file_read = Store.ReadFile.create(
+ bun.default_allocator,
+ this.store.?,
+ this.offset,
+ this.size,
+ *Handler,
+ ptr,
+ Handler.run,
+ ) catch unreachable;
+ var read_file_task = Store.ReadFile.ReadFileTask.createOnJSThread(bun.default_allocator, global, file_read) catch unreachable;
+ read_file_task.schedule();
+ return promise.asValue(global);
+ }
+
+ pub fn needsToReadFile(this: *const Blob) bool {
+ return this.store != null and this.store.?.data == .file;
+ }
+
+ pub fn toStringWithBytes(this: *Blob, global: *JSGlobalObject, buf: []const u8, comptime lifetime: Lifetime) JSValue {
+ // null == unknown
+ // false == can't be
+ const could_be_all_ascii = this.is_all_ascii orelse this.store.?.is_all_ascii;
+
+ if (could_be_all_ascii == null or !could_be_all_ascii.?) {
+ // if toUTF16Alloc returns null, it means there are no non-ASCII characters
+ // instead of erroring, invalid characters will become a U+FFFD replacement character
+ if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch unreachable) |external| {
+ if (lifetime != .temporary)
+ this.setIsASCIIFlag(false);
+
+ if (lifetime == .transfer) {
+ this.detach();
+ }
+
+ if (lifetime == .temporary) {
+ bun.default_allocator.free(bun.constStrToU8(buf));
+ }
+
+ return ZigString.toExternalU16(external.ptr, external.len, global);
+ }
+
+ if (lifetime != .temporary) this.setIsASCIIFlag(true);
+ }
+
+ switch (comptime lifetime) {
+ // strings are immutable
+ // we don't need to clone
+ .clone => {
+ this.store.?.ref();
+ return ZigString.init(buf).external(global, this.store.?, Store.external);
+ },
+ .transfer => {
+ var store = this.store.?;
+ this.transfer();
+ return ZigString.init(buf).external(global, store, Store.external);
+ },
+ // strings are immutable
+ // sharing isn't really a thing
+ .share => {
+ this.store.?.ref();
+ return ZigString.init(buf).external(global, this.store.?, Store.external);
+ },
+ .temporary => {
+ return ZigString.init(buf).toExternalValue(global);
+ },
+ }
+ }
+
+ pub fn toString(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
+ if (this.needsToReadFile()) {
+ return this.doReadFile(toStringWithBytes, lifetime, global);
+ }
+
+ const view_: []u8 =
+ bun.constStrToU8(this.sharedView());
+
+ if (view_.len == 0)
+ return ZigString.Empty.toValue(global);
+
+ return toStringWithBytes(this, global, view_, lifetime);
+ }
+
+ pub fn toJSON(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
+ if (this.needsToReadFile()) {
+ return this.doReadFile(toJSONWithBytes, lifetime, global);
+ }
+
+ var view_ = this.sharedView();
+
+ if (view_.len == 0)
+ return ZigString.Empty.toValue(global);
+
+ return toJSONWithBytes(this, global, view_, lifetime);
+ }
+
+ pub fn toJSONWithBytes(this: *Blob, global: *JSGlobalObject, buf: []const u8, comptime lifetime: Lifetime) JSValue {
+ // null == unknown
+ // false == can't be
+ const could_be_all_ascii = this.is_all_ascii orelse this.store.?.is_all_ascii;
+
+ if (could_be_all_ascii == null or !could_be_all_ascii.?) {
+ // if toUTF16Alloc returns null, it means there are no non-ASCII characters
+ if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch null) |external| {
+ if (comptime lifetime != .temporary) this.setIsASCIIFlag(false);
+ return ZigString.toExternalU16(external.ptr, external.len, global).parseJSON(global);
+ }
+
+ if (comptime lifetime != .temporary) this.setIsASCIIFlag(true);
+ }
+
+ if (comptime lifetime == .temporary) {
+ return ZigString.init(buf).toExternalValue(
+ global,
+ ).parseJSON(global);
+ } else {
+ return ZigString.init(buf).toValue(
+ global,
+ ).parseJSON(global);
+ }
+ }
+
+ pub fn toArrayBufferWithBytes(this: *Blob, global: *JSGlobalObject, buf: []u8, comptime lifetime: Lifetime) JSValue {
+ switch (comptime lifetime) {
+ .clone => {
+ var clone = bun.default_allocator.alloc(u8, buf.len) catch unreachable;
+ @memcpy(clone.ptr, buf.ptr, buf.len);
+
+ return JSC.ArrayBuffer.fromBytes(clone, .ArrayBuffer).toJS(global.ref(), null);
+ },
+ .share => {
+ this.store.?.ref();
+ return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJSWithContext(
+ global.ref(),
+ this.store.?,
+ JSC.BlobArrayBuffer_deallocator,
+ null,
+ );
+ },
+ .transfer => {
+ var store = this.store.?;
+ this.transfer();
+ return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJSWithContext(
+ global.ref(),
+ store,
+ JSC.BlobArrayBuffer_deallocator,
+ null,
+ );
+ },
+ .temporary => {
+ return JSC.ArrayBuffer.fromBytes(buf, .ArrayBuffer).toJS(
+ global.ref(),
+ null,
+ );
+ },
+ }
+ }
+
+ pub fn toArrayBuffer(this: *Blob, global: *JSGlobalObject, comptime lifetime: Lifetime) JSValue {
+ if (this.needsToReadFile()) {
+ return this.doReadFile(toArrayBufferWithBytes, lifetime, global);
+ }
+
+ var view_ = this.sharedView();
+
+ if (view_.len == 0)
+ return JSC.ArrayBuffer.fromBytes(&[_]u8{}, .ArrayBuffer).toJS(global.ref(), null);
+
+ return toArrayBufferWithBytes(this, global, bun.constStrToU8(view_), lifetime);
+ }
+
+ pub inline fn fromJS(
+ global: *JSGlobalObject,
+ arg: JSValue,
+ comptime move: bool,
+ comptime require_array: bool,
+ ) anyerror!Blob {
+ return fromJSMovable(global, arg, move, require_array);
+ }
+
+ pub inline fn fromJSMove(global: *JSGlobalObject, arg: JSValue) anyerror!Blob {
+ return fromJSWithoutDeferGC(global, arg, true, false);
+ }
+
+ pub inline fn fromJSClone(global: *JSGlobalObject, arg: JSValue) anyerror!Blob {
+ return fromJSWithoutDeferGC(global, arg, false, true);
+ }
+
+ pub inline fn fromJSCloneOptionalArray(global: *JSGlobalObject, arg: JSValue) anyerror!Blob {
+ return fromJSWithoutDeferGC(global, arg, false, false);
+ }
+
+ fn fromJSMovable(
+ global: *JSGlobalObject,
+ arg: JSValue,
+ comptime move: bool,
+ comptime require_array: bool,
+ ) anyerror!Blob {
+ const FromJSFunction = if (comptime move and !require_array)
+ fromJSMove
+ else if (!require_array)
+ fromJSCloneOptionalArray
+ else
+ fromJSClone;
+ const DeferCtx = struct {
+ args: std.meta.ArgsTuple(@TypeOf(FromJSFunction)),
+ ret: anyerror!Blob = undefined,
+
+ pub fn run(ctx: ?*anyopaque) callconv(.C) void {
+ var that = bun.cast(*@This(), ctx.?);
+ that.ret = @call(.{}, FromJSFunction, that.args);
+ }
+ };
+ var ctx = DeferCtx{
+ .args = .{
+ global,
+ arg,
+ },
+ .ret = undefined,
+ };
+ global.vm().deferGC(&ctx, DeferCtx.run);
+ return ctx.ret;
+ }
+
+ fn fromJSWithoutDeferGC(
+ global: *JSGlobalObject,
+ arg: JSValue,
+ comptime move: bool,
+ comptime require_array: bool,
+ ) anyerror!Blob {
+ var current = arg;
+ if (current.isUndefinedOrNull()) {
+ return Blob{ .globalThis = global };
+ }
+
+ var top_value = current;
+ var might_only_be_one_thing = false;
+ switch (current.jsTypeLoose()) {
+ .Array, .DerivedArray => {
+ var top_iter = JSC.JSArrayIterator.init(current, global);
+ might_only_be_one_thing = top_iter.len == 1;
+ if (top_iter.len == 0) {
+ return Blob{ .globalThis = global };
+ }
+ if (might_only_be_one_thing) {
+ top_value = top_iter.next().?;
+ }
+ },
+ else => {
+ might_only_be_one_thing = true;
+ if (require_array) {
+ return error.InvalidArguments;
+ }
+ },
+ }
+
+ if (might_only_be_one_thing or !move) {
+
+ // Fast path: one item, we don't need to join
+ switch (top_value.jsTypeLoose()) {
+ .Cell,
+ .NumberObject,
+ JSC.JSValue.JSType.String,
+ JSC.JSValue.JSType.StringObject,
+ JSC.JSValue.JSType.DerivedStringObject,
+ => {
+ var sliced = top_value.toSlice(global, bun.default_allocator);
+ const is_all_ascii = !sliced.allocated;
+ if (!sliced.allocated and sliced.len > 0) {
+ sliced.ptr = @ptrCast([*]const u8, (try bun.default_allocator.dupe(u8, sliced.slice())).ptr);
+ sliced.allocated = true;
+ }
+
+ return Blob.initWithAllASCII(bun.constStrToU8(sliced.slice()), bun.default_allocator, global, is_all_ascii);
+ },
+
+ JSC.JSValue.JSType.ArrayBuffer,
+ JSC.JSValue.JSType.Int8Array,
+ JSC.JSValue.JSType.Uint8Array,
+ JSC.JSValue.JSType.Uint8ClampedArray,
+ JSC.JSValue.JSType.Int16Array,
+ JSC.JSValue.JSType.Uint16Array,
+ JSC.JSValue.JSType.Int32Array,
+ JSC.JSValue.JSType.Uint32Array,
+ JSC.JSValue.JSType.Float32Array,
+ JSC.JSValue.JSType.Float64Array,
+ JSC.JSValue.JSType.BigInt64Array,
+ JSC.JSValue.JSType.BigUint64Array,
+ JSC.JSValue.JSType.DataView,
+ => {
+ var buf = try bun.default_allocator.dupe(u8, top_value.asArrayBuffer(global).?.byteSlice());
+
+ return Blob.init(buf, bun.default_allocator, global);
+ },
+
+ else => {
+ if (JSC.C.JSObjectGetPrivate(top_value.asObjectRef())) |priv| {
+ var data = JSC.JSPrivateDataPtr.from(priv);
+ switch (data.tag()) {
+ .Blob => {
+ var blob: *Blob = data.as(Blob);
+ if (comptime move) {
+ var _blob = blob.*;
+ _blob.allocator = null;
+ blob.transfer();
+ return _blob;
+ } else {
+ return blob.dupe();
+ }
+ },
+
+ else => return Blob.initEmpty(global),
+ }
+ }
+ },
+ }
+ }
+
+ var stack_allocator = std.heap.stackFallback(1024, bun.default_allocator);
+ var stack_mem_all = stack_allocator.get();
+ var stack: std.ArrayList(JSValue) = std.ArrayList(JSValue).init(stack_mem_all);
+ var joiner = StringJoiner{ .use_pool = false, .node_allocator = stack_mem_all };
+ var could_have_non_ascii = false;
+
+ defer if (stack_allocator.fixed_buffer_allocator.end_index >= 1024) stack.deinit();
+
+ while (true) {
+ switch (current.jsTypeLoose()) {
+ .NumberObject,
+ JSC.JSValue.JSType.String,
+ JSC.JSValue.JSType.StringObject,
+ JSC.JSValue.JSType.DerivedStringObject,
+ => {
+ var sliced = current.toSlice(global, bun.default_allocator);
+ could_have_non_ascii = could_have_non_ascii or sliced.allocated;
+ joiner.append(
+ sliced.slice(),
+ 0,
+ if (sliced.allocated) sliced.allocator else null,
+ );
+ },
+
+ .Array, .DerivedArray => {
+ var iter = JSC.JSArrayIterator.init(current, global);
+ try stack.ensureUnusedCapacity(iter.len);
+ var any_arrays = false;
+ while (iter.next()) |item| {
+ if (item.isUndefinedOrNull()) continue;
+
+ // When it's a string or ArrayBuffer inside an array, we can avoid the extra push/pop
+ // we only really want this for nested arrays
+ // However, we must preserve the order
+ // That means if there are any arrays
+ // we have to restart the loop
+ if (!any_arrays) {
+ switch (item.jsTypeLoose()) {
+ .NumberObject,
+ .Cell,
+ JSC.JSValue.JSType.String,
+ JSC.JSValue.JSType.StringObject,
+ JSC.JSValue.JSType.DerivedStringObject,
+ => {
+ var sliced = item.toSlice(global, bun.default_allocator);
+ could_have_non_ascii = could_have_non_ascii or sliced.allocated;
+ joiner.append(
+ sliced.slice(),
+ 0,
+ if (sliced.allocated) sliced.allocator else null,
+ );
+ continue;
+ },
+ JSC.JSValue.JSType.ArrayBuffer,
+ JSC.JSValue.JSType.Int8Array,
+ JSC.JSValue.JSType.Uint8Array,
+ JSC.JSValue.JSType.Uint8ClampedArray,
+ JSC.JSValue.JSType.Int16Array,
+ JSC.JSValue.JSType.Uint16Array,
+ JSC.JSValue.JSType.Int32Array,
+ JSC.JSValue.JSType.Uint32Array,
+ JSC.JSValue.JSType.Float32Array,
+ JSC.JSValue.JSType.Float64Array,
+ JSC.JSValue.JSType.BigInt64Array,
+ JSC.JSValue.JSType.BigUint64Array,
+ JSC.JSValue.JSType.DataView,
+ => {
+ could_have_non_ascii = true;
+ var buf = item.asArrayBuffer(global).?;
+ joiner.append(buf.byteSlice(), 0, null);
+ continue;
+ },
+ .Array, .DerivedArray => {
+ any_arrays = true;
+ could_have_non_ascii = true;
+ break;
+ },
+ else => {
+ if (JSC.C.JSObjectGetPrivate(item.asObjectRef())) |priv| {
+ var data = JSC.JSPrivateDataPtr.from(priv);
+ switch (data.tag()) {
+ .Blob => {
+ var blob: *Blob = data.as(Blob);
+ could_have_non_ascii = could_have_non_ascii or !(blob.is_all_ascii orelse false);
+ joiner.append(blob.sharedView(), 0, null);
+ continue;
+ },
+ else => {},
+ }
+ }
+ },
+ }
+ }
+
+ stack.appendAssumeCapacity(item);
+ }
+ },
+
+ JSC.JSValue.JSType.ArrayBuffer,
+ JSC.JSValue.JSType.Int8Array,
+ JSC.JSValue.JSType.Uint8Array,
+ JSC.JSValue.JSType.Uint8ClampedArray,
+ JSC.JSValue.JSType.Int16Array,
+ JSC.JSValue.JSType.Uint16Array,
+ JSC.JSValue.JSType.Int32Array,
+ JSC.JSValue.JSType.Uint32Array,
+ JSC.JSValue.JSType.Float32Array,
+ JSC.JSValue.JSType.Float64Array,
+ JSC.JSValue.JSType.BigInt64Array,
+ JSC.JSValue.JSType.BigUint64Array,
+ JSC.JSValue.JSType.DataView,
+ => {
+ var buf = current.asArrayBuffer(global).?;
+ joiner.append(buf.slice(), 0, null);
+ could_have_non_ascii = true;
+ },
+
+ else => {
+ outer: {
+ if (JSC.C.JSObjectGetPrivate(current.asObjectRef())) |priv| {
+ var data = JSC.JSPrivateDataPtr.from(priv);
+ switch (data.tag()) {
+ .Blob => {
+ var blob: *Blob = data.as(Blob);
+ could_have_non_ascii = could_have_non_ascii or !(blob.is_all_ascii orelse false);
+ joiner.append(blob.sharedView(), 0, null);
+ break :outer;
+ },
+ else => {},
+ }
+ }
+
+ var sliced = current.toSlice(global, bun.default_allocator);
+ could_have_non_ascii = could_have_non_ascii or sliced.allocated;
+ joiner.append(
+ sliced.slice(),
+ 0,
+ if (sliced.allocated) sliced.allocator else null,
+ );
+ }
+ },
+ }
+ current = stack.popOrNull() orelse break;
+ }
+
+ var joined = try joiner.done(bun.default_allocator);
+
+ if (!could_have_non_ascii) {
+ return Blob.initWithAllASCII(joined, bun.default_allocator, global, true);
+ }
+ return Blob.init(joined, bun.default_allocator, global);
+ }
+};
+
+// https://developer.mozilla.org/en-US/docs/Web/API/Body
+pub const Body = struct {
+ init: Init = Init{ .headers = null, .status_code = 200 },
+ value: Value = Value.empty,
+
+ pub inline fn len(this: *const Body) usize {
+ return this.slice().len;
+ }
+
+ pub fn slice(this: *const Body) []const u8 {
+ return this.value.slice();
+ }
+
+ pub fn use(this: *Body) Blob {
+ return this.value.use();
+ }
+
+ pub fn clone(this: Body, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) Body {
+ return Body{
+ .init = this.init.clone(globalThis),
+ .value = this.value.clone(allocator),
+ };
+ }
+
+ pub fn writeFormat(this: *const Body, formatter: *JSC.Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
+ const Writer = @TypeOf(writer);
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("bodyUsed: ");
+ formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.value == .Used), .BooleanObject, enable_ansi_colors);
+ try formatter.printComma(Writer, writer, enable_ansi_colors);
+ try writer.writeAll("\n");
+
+ // if (this.init.headers) |headers| {
+ // try formatter.writeIndent(Writer, writer);
+ // try writer.writeAll("headers: ");
+ // try headers.leak().writeFormat(formatter, writer, comptime enable_ansi_colors);
+ // try writer.writeAll("\n");
+ // }
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll("status: ");
+ formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(this.init.status_code), .NumberObject, enable_ansi_colors);
+ }
+
+ pub fn deinit(this: *Body, _: std.mem.Allocator) void {
+ if (this.init.headers) |headers| {
+ headers.deref();
+ this.init.headers = null;
+ }
+ this.value.deinit();
+ }
+
+ pub const Init = struct {
+ headers: ?*FetchHeaders = null,
+ status_code: u16,
+ method: Method = Method.GET,
+
+ pub fn clone(this: Init, _: *JSGlobalObject) Init {
+ var that = this;
+ var headers = this.headers;
+ if (headers) |head| {
+ that.headers = head.cloneThis();
+ }
+
+ return that;
+ }
+
+ pub fn init(_: std.mem.Allocator, ctx: js.JSContextRef, init_ref: js.JSValueRef) !?Init {
+ var result = Init{ .status_code = 200 };
+ var array = js.JSObjectCopyPropertyNames(ctx, init_ref);
+ defer js.JSPropertyNameArrayRelease(array);
+ const count = js.JSPropertyNameArrayGetCount(array);
+
+ var i: usize = 0;
+ while (i < count) : (i += 1) {
+ var property_name_ref = js.JSPropertyNameArrayGetNameAtIndex(array, i);
+ switch (js.JSStringGetLength(property_name_ref)) {
+ "headers".len => {
+ if (js.JSStringIsEqualToUTF8CString(property_name_ref, "headers")) {
+ // only support headers as an object for now.
+ if (js.JSObjectGetProperty(ctx, init_ref, property_name_ref, null)) |header_prop| {
+ const header_val = JSValue.fromRef(header_prop);
+ if (header_val.as(FetchHeaders)) |orig| {
+ result.headers = orig.cloneThis();
+ } else {
+ result.headers = FetchHeaders.createFromJS(ctx.ptr(), header_val);
+ }
+ }
+ }
+ },
+
+ "method".len => {
+ if (js.JSStringIsEqualToUTF8CString(property_name_ref, "status")) {
+ var value_ref = js.JSObjectGetProperty(ctx, init_ref, property_name_ref, null);
+ var exception: js.JSValueRef = null;
+ const number = js.JSValueToNumber(ctx, value_ref, &exception);
+ if (exception != null or !std.math.isFinite(number)) continue;
+ result.status_code = @truncate(u16, @floatToInt(u64, number));
+ } else if (js.JSStringIsEqualToUTF8CString(property_name_ref, "method")) {
+ result.method = Method.which(
+ JSC.JSValue.fromRef(init_ref).get(ctx.ptr(), "method").?.getZigString(ctx.ptr()).slice(),
+ ) orelse Method.GET;
+ }
+ },
+ else => {},
+ }
+ }
+
+ if (result.headers == null and result.status_code < 200) return null;
+ return result;
+ }
+ };
+
+ pub const PendingValue = struct {
+ promise: ?JSValue = null,
+ readable: ?JSC.WebCore.ReadableStream = null,
+ // writable: JSC.WebCore.Sink
+
+ global: *JSGlobalObject,
+ task: ?*anyopaque = null,
+ /// runs after the data is available.
+ callback: ?fn (ctx: *anyopaque, value: *Value) void = null,
+ /// conditionally runs when requesting data
+ /// used in HTTP server to ignore request bodies unless asked for it
+ onPull: ?fn (ctx: *anyopaque) void = null,
+ deinit: bool = false,
+ action: Action = Action.none,
+
+ pub fn setPromise(value: *PendingValue, globalThis: *JSC.JSGlobalObject, action: Action) JSValue {
+ value.action = action;
+
+ if (value.readable) |*readable| {
+ // switch (readable.ptr) {
+ // .JavaScript
+ // }
+ switch (action) {
+ .getText, .getJSON, .getBlob, .getArrayBuffer => {
+ switch (readable.ptr) {
+ .Blob => unreachable,
+ else => {},
+ }
+ value.promise = switch (action) {
+ .getJSON => globalThis.readableStreamToJSON(readable.value),
+ .getArrayBuffer => globalThis.readableStreamToArrayBuffer(readable.value),
+ .getText => globalThis.readableStreamToText(readable.value),
+ .getBlob => globalThis.readableStreamToBlob(readable.value),
+ else => unreachable,
+ };
+ value.promise.?.ensureStillAlive();
+ readable.value.unprotect();
+
+ // js now owns the memory
+ value.readable = null;
+
+ return value.promise.?;
+ },
+ .none => {},
+ }
+ }
+
+ {
+ var promise = JSC.JSPromise.create(globalThis);
+ const promise_value = promise.asValue(globalThis);
+ value.promise = promise_value;
+
+ if (value.onPull) |onPull| {
+ value.onPull = null;
+ onPull(value.task.?);
+ }
+ return promise_value;
+ }
+ }
+
+ pub const Action = enum {
+ none,
+ getText,
+ getJSON,
+ getArrayBuffer,
+ getBlob,
+ };
+ };
+
+ pub const Value = union(Tag) {
+ Blob: Blob,
+ Locked: PendingValue,
+ Used: void,
+ Empty: void,
+ Error: JSValue,
+
+ pub const Tag = enum {
+ Blob,
+ Locked,
+ Used,
+ Empty,
+ Error,
+ };
+
+ pub const empty = Value{ .Empty = .{} };
+
+ pub fn fromReadableStream(readable: JSC.WebCore.ReadableStream, globalThis: *JSGlobalObject) Value {
+ if (readable.isLocked(globalThis)) {
+ return .{ .Error = ZigString.init("Cannot use a locked ReadableStream").toErrorInstance(globalThis) };
+ }
+
+ readable.value.protect();
+ return .{
+ .Locked = .{
+ .readable = readable,
+ .global = globalThis,
+ },
+ };
+ }
+
+ pub fn resolve(this: *Value, new: *Value, global: *JSGlobalObject) void {
+ if (this.* == .Locked) {
+ var locked = &this.Locked;
+ if (locked.readable) |readable| {
+ readable.done();
+ locked.readable = null;
+ }
+
+ if (locked.callback) |callback| {
+ locked.callback = null;
+ callback(locked.task.?, new);
+ }
+
+ if (locked.promise) |promise| {
+ locked.promise = null;
+ var blob = new.use();
+
+ switch (locked.action) {
+ .getText => {
+ promise.asPromise().?.resolve(global, JSValue.fromRef(blob.getTextTransfer(global.ref())));
+ },
+ .getJSON => {
+ promise.asPromise().?.resolve(global, blob.toJSON(global, .share));
+ blob.detach();
+ },
+ .getArrayBuffer => {
+ promise.asPromise().?.resolve(global, JSValue.fromRef(blob.getArrayBufferTransfer(global.ref())));
+ },
+ .getBlob => {
+ var ptr = bun.default_allocator.create(Blob) catch unreachable;
+ ptr.* = blob;
+ ptr.allocator = bun.default_allocator;
+ promise.asPromise().?.resolve(global, JSC.JSValue.fromRef(Blob.Class.make(global.ref(), ptr)));
+ },
+ else => {
+ var ptr = bun.default_allocator.create(Blob) catch unreachable;
+ ptr.* = blob;
+ ptr.allocator = bun.default_allocator;
+ promise.asInternalPromise().?.resolve(global, JSC.JSValue.fromRef(Blob.Class.make(global.ref(), ptr)));
+ },
+ }
+ JSC.C.JSValueUnprotect(global.ref(), promise.asObjectRef());
+ }
+ }
+ }
+ pub fn slice(this: Value) []const u8 {
+ return switch (this) {
+ .Blob => this.Blob.sharedView(),
+ else => "",
+ };
+ }
+
+ pub fn use(this: *Value) Blob {
+ switch (this.*) {
+ .Blob => {
+ var new_blob = this.Blob;
+ std.debug.assert(new_blob.allocator == null); // owned by Body
+ this.* = .{ .Used = .{} };
+ return new_blob;
+ },
+ else => {
+ return Blob.initEmpty(undefined);
+ },
+ }
+ }
+
+ pub fn toErrorInstance(this: *Value, error_instance: JSC.JSValue, global: *JSGlobalObject) void {
+ if (this.* == .Locked) {
+ var locked = this.Locked;
+ locked.deinit = true;
+ if (locked.promise) |promise| {
+ if (promise.asInternalPromise()) |internal| {
+ internal.reject(global, error_instance);
+ } else if (promise.asPromise()) |internal| {
+ internal.reject(global, error_instance);
+ }
+ JSC.C.JSValueUnprotect(global.ref(), promise.asObjectRef());
+ locked.promise = null;
+ }
+
+ if (locked.readable) |readable| {
+ readable.done();
+ locked.readable = null;
+ }
+
+ this.* = .{ .Error = error_instance };
+ if (locked.callback) |callback| {
+ locked.callback = null;
+ callback(locked.task.?, this);
+ }
+ return;
+ }
+
+ this.* = .{ .Error = error_instance };
+ }
+
+ pub fn toErrorString(this: *Value, comptime err: string, global: *JSGlobalObject) void {
+ var error_str = ZigString.init(err);
+ var error_instance = error_str.toErrorInstance(global);
+ return this.toErrorInstance(error_instance, global);
+ }
+
+ pub fn toError(this: *Value, err: anyerror, global: *JSGlobalObject) void {
+ var error_str = ZigString.init(std.fmt.allocPrint(
+ bun.default_allocator,
+ "Error reading file {s}",
+ .{@errorName(err)},
+ ) catch unreachable);
+ error_str.mark();
+ var error_instance = error_str.toErrorInstance(global);
+ return this.toErrorInstance(error_instance, global);
+ }
+
+ pub fn deinit(this: *Value) void {
+ const tag = @as(Tag, this.*);
+ if (tag == .Locked) {
+ if (this.Locked.readable) |*readable| {
+ readable.done();
+ }
+
+ this.Locked.deinit = true;
+ return;
+ }
+
+ if (tag == .Blob) {
+ this.Blob.deinit();
+ this.* = Value.empty;
+ }
+
+ if (tag == .Error) {
+ JSC.C.JSValueUnprotect(VirtualMachine.vm.global.ref(), this.Error.asObjectRef());
+ }
+ }
+
+ pub fn clone(this: Value, _: std.mem.Allocator) Value {
+ if (this == .Blob) {
+ return Value{ .Blob = this.Blob.dupe() };
+ }
+
+ return Value{ .Empty = .{} };
+ }
+ };
+
+ pub fn @"404"(_: js.JSContextRef) Body {
+ return Body{
+ .init = Init{
+ .headers = null,
+ .status_code = 404,
+ },
+ .value = Value.empty,
+ };
+ }
+
+ pub fn @"200"(_: js.JSContextRef) Body {
+ return Body{
+ .init = Init{
+ .status_code = 200,
+ },
+ .value = Value.empty,
+ };
+ }
+
+ pub fn extract(ctx: js.JSContextRef, body_ref: js.JSObjectRef, exception: js.ExceptionRef) Body {
+ return extractBody(
+ ctx,
+ body_ref,
+ false,
+ null,
+ exception,
+ );
+ }
+
+ pub fn extractWithInit(ctx: js.JSContextRef, body_ref: js.JSObjectRef, init_ref: js.JSValueRef, exception: js.ExceptionRef) Body {
+ return extractBody(
+ ctx,
+ body_ref,
+ true,
+ init_ref,
+ exception,
+ );
+ }
+
+ // https://github.com/WebKit/webkit/blob/main/Source/WebCore/Modules/fetch/FetchBody.cpp#L45
+ inline fn extractBody(
+ ctx: js.JSContextRef,
+ body_ref: js.JSObjectRef,
+ comptime has_init: bool,
+ init_ref: js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) Body {
+ var body = Body{
+ .init = Init{ .headers = null, .status_code = 200 },
+ };
+ const value = JSC.JSValue.fromRef(body_ref);
+ var allocator = getAllocator(ctx);
+
+ if (comptime has_init) {
+ if (Init.init(allocator, ctx, init_ref.?)) |maybeInit| {
+ if (maybeInit) |init_| {
+ body.init = init_;
+ }
+ } else |_| {}
+ }
+
+ if (JSC.WebCore.ReadableStream.fromJS(value, ctx)) |readable| {
+ switch (readable.ptr) {
+ .Blob => |blob| {
+ body.value = .{
+ .Blob = Blob.initWithStore(blob.store, ctx),
+ };
+ blob.store.ref();
+
+ readable.done();
+
+ if (!blob.done) {
+ blob.done = true;
+ blob.deinit();
+ }
+ return body;
+ },
+ else => {},
+ }
+
+ body.value = Body.Value.fromReadableStream(readable, ctx);
+ return body;
+ }
+
+ body.value = .{
+ .Blob = Blob.fromJS(ctx.ptr(), value, true, false) catch |err| {
+ if (err == error.InvalidArguments) {
+ JSC.JSError(allocator, "Expected an Array", .{}, ctx, exception);
+ return body;
+ }
+
+ JSC.JSError(allocator, "Out of memory", .{}, ctx, exception);
+ return body;
+ },
+ };
+
+ std.debug.assert(body.value.Blob.allocator == null); // owned by Body
+
+ return body;
+ }
+};
+
+// https://developer.mozilla.org/en-US/docs/Web/API/Request
+pub const Request = struct {
+ url: ZigString = ZigString.Empty,
+ headers: ?*FetchHeaders = null,
+ body: Body.Value = Body.Value{ .Empty = .{} },
+ method: Method = Method.GET,
+ uws_request: ?*uws.Request = null,
+
+ pub fn fromRequestContext(ctx: *RequestContext, global: *JSGlobalObject) !Request {
+ var req = Request{
+ .url = ZigString.init(std.mem.span(ctx.getFullURL())),
+ .body = Body.Value.empty,
+ .method = ctx.method,
+ .headers = FetchHeaders.createFromPicoHeaders(global, ctx.request.headers),
+ };
+ req.url.mark();
+ return req;
+ }
+
+ pub fn mimeType(this: *const Request) string {
+ if (this.headers) |headers| {
+ // Remember, we always lowercase it
+ // hopefully doesn't matter here tho
+ if (headers.get("content-type")) |content_type| {
+ return content_type;
+ }
+ }
+
+ switch (this.body) {
+ .Blob => |blob| {
+ if (blob.content_type.len > 0) {
+ return blob.content_type;
+ }
+
+ return MimeType.other.value;
+ },
+ .Error, .Used, .Locked, .Empty => return MimeType.other.value,
+ }
+ }
+
+ pub const Constructor = JSC.NewConstructor(
+ Request,
+ .{
+ .constructor = .{ .rfn = constructor },
+ },
+ .{},
+ );
+
+ pub const Class = NewClass(
+ Request,
+ .{
+ .name = "Request",
+ .read_only = true,
+ },
+ .{
+ .finalize = finalize,
+ .text = .{
+ .rfn = Request.getText,
+ },
+ .json = .{
+ .rfn = Request.getJSON,
+ },
+ .arrayBuffer = .{
+ .rfn = Request.getArrayBuffer,
+ },
+ .blob = .{
+ .rfn = Request.getBlob,
+ },
+ .clone = .{
+ .rfn = Request.doClone,
+ },
+ },
+ .{
+ .@"cache" = .{
+ .@"get" = getCache,
+ .@"ro" = true,
+ },
+ .@"credentials" = .{
+ .@"get" = getCredentials,
+ .@"ro" = true,
+ },
+ .@"destination" = .{
+ .@"get" = getDestination,
+ .@"ro" = true,
+ },
+ .@"headers" = .{
+ .@"get" = getHeaders,
+ .@"ro" = true,
+ },
+ .@"integrity" = .{
+ .@"get" = getIntegrity,
+ .@"ro" = true,
+ },
+ .@"method" = .{
+ .@"get" = getMethod,
+ .@"ro" = true,
+ },
+ .@"mode" = .{
+ .@"get" = getMode,
+ .@"ro" = true,
+ },
+ .@"redirect" = .{
+ .@"get" = getRedirect,
+ .@"ro" = true,
+ },
+ .@"referrer" = .{
+ .@"get" = getReferrer,
+ .@"ro" = true,
+ },
+ .@"referrerPolicy" = .{
+ .@"get" = getReferrerPolicy,
+ .@"ro" = true,
+ },
+ .@"url" = .{
+ .@"get" = getUrl,
+ .@"ro" = true,
+ },
+ .@"bodyUsed" = .{
+ .@"get" = getBodyUsed,
+ .@"ro" = true,
+ },
+ },
+ );
+
+ pub fn getCache(
+ _: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return js.JSValueMakeString(ctx, ZigString.init(Properties.UTF8.default).toValueGC(ctx.ptr()).asRef());
+ }
+ pub fn getCredentials(
+ _: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return js.JSValueMakeString(ctx, ZigString.init(Properties.UTF8.include).toValueGC(ctx.ptr()).asRef());
+ }
+ pub fn getDestination(
+ _: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return js.JSValueMakeString(ctx, ZigString.init("").toValueGC(ctx.ptr()).asRef());
+ }
+
+ pub fn getIntegrity(
+ _: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return ZigString.Empty.toValueGC(ctx.ptr()).asRef();
+ }
+ pub fn getMethod(
+ this: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ const string_contents: string = switch (this.method) {
+ .GET => Properties.UTF8.GET,
+ .HEAD => Properties.UTF8.HEAD,
+ .PATCH => Properties.UTF8.PATCH,
+ .PUT => Properties.UTF8.PUT,
+ .POST => Properties.UTF8.POST,
+ .OPTIONS => Properties.UTF8.OPTIONS,
+ else => "",
+ };
+
+ return ZigString.init(string_contents).toValue(ctx.ptr()).asRef();
+ }
+
+ pub fn getMode(
+ _: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return ZigString.init(Properties.UTF8.navigate).toValue(ctx.ptr()).asRef();
+ }
+
+ pub fn finalize(this: *Request) void {
+ if (this.headers) |headers| {
+ headers.deref();
+ this.headers = null;
+ }
+
+ if (this.url.isGloballyAllocated()) {
+ bun.default_allocator.free(bun.constStrToU8(this.url.slice()));
+ }
+
+ bun.default_allocator.destroy(this);
+ }
+
+ pub fn getRedirect(
+ _: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return ZigString.init(Properties.UTF8.follow).toValueGC(ctx.ptr()).asRef();
+ }
+ pub fn getReferrer(
+ this: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ if (this.headers) |headers_ref| {
+ if (headers_ref.get("referrer")) |referrer| {
+ return ZigString.init(referrer).toValueGC(ctx.ptr()).asRef();
+ }
+ }
+
+ return ZigString.init("").toValueGC(ctx.ptr()).asRef();
+ }
+ pub fn getReferrerPolicy(
+ _: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return ZigString.init("").toValueGC(ctx.ptr()).asRef();
+ }
+ pub fn getUrl(
+ this: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return this.url.toValueGC(ctx.ptr()).asObjectRef();
+ }
+
+ pub fn constructor(
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) js.JSObjectRef {
+ var request = Request{};
+
+ switch (arguments.len) {
+ 0 => {},
+ 1 => {
+ request.url = JSC.JSValue.fromRef(arguments[0]).getZigString(ctx.ptr());
+ },
+ else => {
+ request.url = JSC.JSValue.fromRef(arguments[0]).getZigString(ctx.ptr());
+
+ if (Body.Init.init(getAllocator(ctx), ctx, arguments[1]) catch null) |req_init| {
+ request.headers = req_init.headers;
+ request.method = req_init.method;
+ }
+
+ if (JSC.JSValue.fromRef(arguments[1]).get(ctx.ptr(), "body")) |body_| {
+ if (Blob.fromJS(ctx.ptr(), body_, true, false)) |blob| {
+ if (blob.size > 0) {
+ request.body = Body.Value{ .Blob = blob };
+ }
+ } else |err| {
+ if (err == error.InvalidArguments) {
+ JSC.JSError(getAllocator(ctx), "Expected an Array", .{}, ctx, exception);
+ return null;
+ }
+
+ JSC.JSError(getAllocator(ctx), "Invalid Body", .{}, ctx, exception);
+ return null;
+ }
+ }
+ },
+ }
+
+ var request_ = getAllocator(ctx).create(Request) catch unreachable;
+ request_.* = request;
+ return Request.Class.make(
+ ctx,
+ request_,
+ );
+ }
+
+ pub fn getBodyValue(
+ this: *Request,
+ ) *Body.Value {
+ return &this.body;
+ }
+
+ pub fn getBodyUsed(
+ this: *Request,
+ _: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return JSC.JSValue.jsBoolean(this.body == .Used).asRef();
+ }
+
+ pub usingnamespace BlobInterface(@This());
+
+ pub fn doClone(
+ this: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ var cloned = this.clone(getAllocator(ctx), ctx.ptr());
+ return Request.Class.make(ctx, cloned);
+ }
+
+ pub fn getHeaders(
+ this: *Request,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ if (this.headers == null) {
+ if (this.uws_request) |req| {
+ this.headers = FetchHeaders.createFromUWS(ctx.ptr(), req);
+ } else {
+ this.headers = FetchHeaders.createEmpty();
+ }
+ }
+
+ return this.headers.?.toJS(ctx.ptr()).asObjectRef();
+ }
+
+ pub fn cloneInto(
+ this: *const Request,
+ req: *Request,
+ allocator: std.mem.Allocator,
+ globalThis: *JSGlobalObject,
+ ) void {
+ req.* = Request{
+ .body = this.body.clone(allocator),
+ .url = ZigString.init(allocator.dupe(u8, this.url.slice()) catch unreachable),
+ .method = this.method,
+ };
+ if (this.headers) |head| {
+ req.headers = head.cloneThis();
+ } else if (this.uws_request) |uws_req| {
+ req.headers = FetchHeaders.createFromUWS(globalThis, uws_req);
+ }
+ }
+
+ pub fn clone(this: *const Request, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) *Request {
+ var req = allocator.create(Request) catch unreachable;
+ this.cloneInto(req, allocator, globalThis);
+ return req;
+ }
+};
+
+fn BlobInterface(comptime Type: type) type {
+ return struct {
+ pub fn getText(
+ this: *Type,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ var value: *Body.Value = this.getBodyValue();
+ if (value.* == .Locked) {
+ return value.Locked.setPromise(ctx.ptr(), .getText).asObjectRef();
+ }
+
+ var blob = this.body.use();
+ return blob.getTextTransfer(ctx);
+ }
+
+ pub fn getJSON(
+ this: *Type,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) js.JSValueRef {
+ var value: *Body.Value = this.getBodyValue();
+ if (value.* == .Locked) {
+ return value.Locked.setPromise(ctx.ptr(), .getJSON).asObjectRef();
+ }
+
+ var blob = this.body.use();
+ return blob.getJSON(ctx, null, null, &.{}, exception);
+ }
+ pub fn getArrayBuffer(
+ this: *Type,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ var value: *Body.Value = this.getBodyValue();
+
+ if (value.* == .Locked) {
+ return value.Locked.setPromise(ctx.ptr(), .getArrayBuffer).asObjectRef();
+ }
+
+ var blob = this.body.use();
+ return blob.getArrayBufferTransfer(ctx);
+ }
+
+ pub fn getBlob(
+ this: *Type,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ var value: *Body.Value = this.getBodyValue();
+
+ if (value.* == .Locked) {
+ return value.Locked.setPromise(ctx.ptr(), .getBlob).asObjectRef();
+ }
+
+ var blob = this.body.use();
+ var ptr = getAllocator(ctx).create(Blob) catch unreachable;
+ ptr.* = blob;
+ blob.allocator = getAllocator(ctx);
+ return JSC.JSPromise.resolvedPromiseValue(ctx.ptr(), JSValue.fromRef(Blob.Class.make(ctx, ptr))).asObjectRef();
+ }
+
+ // pub fn getBody(
+ // this: *Type,
+ // ctx: js.JSContextRef,
+ // _: js.JSObjectRef,
+ // _: js.JSObjectRef,
+ // _: []const js.JSValueRef,
+ // _: js.ExceptionRef,
+ // ) js.JSValueRef {
+ // var value: *Body.Value = this.getBodyValue();
+
+ // switch (value.*) {
+ // .Empty => {},
+ // }
+ // }
+ };
+}
+
+// https://github.com/WebKit/WebKit/blob/main/Source/WebCore/workers/service/FetchEvent.h
+pub const FetchEvent = struct {
+ started_waiting_at: u64 = 0,
+ response: ?*Response = null,
+ request_context: ?*RequestContext = null,
+ request: Request,
+ pending_promise: ?*JSInternalPromise = null,
+
+ onPromiseRejectionCtx: *anyopaque = undefined,
+ onPromiseRejectionHandler: ?fn (ctx: *anyopaque, err: anyerror, fetch_event: *FetchEvent, value: JSValue) void = null,
+ rejected: bool = false,
+
+ pub const Class = NewClass(
+ FetchEvent,
+ .{
+ .name = "FetchEvent",
+ .read_only = true,
+ .ts = .{ .class = d.ts.class{ .interface = true } },
+ },
+ .{
+ .@"respondWith" = .{
+ .rfn = respondWith,
+ .ts = d.ts{
+ .tsdoc = "Render the response in the active HTTP request",
+ .@"return" = "void",
+ .args = &[_]d.ts.arg{
+ .{ .name = "response", .@"return" = "Response" },
+ },
+ },
+ },
+ .@"waitUntil" = waitUntil,
+ .finalize = finalize,
+ },
+ .{
+ .@"client" = .{
+ .@"get" = getClient,
+ .ro = true,
+ .ts = d.ts{
+ .tsdoc = "HTTP client metadata. This is not implemented yet, do not use.",
+ .@"return" = "undefined",
+ },
+ },
+ .@"request" = .{
+ .@"get" = getRequest,
+ .ro = true,
+ .ts = d.ts{
+ .tsdoc = "HTTP request",
+ .@"return" = "InstanceType<Request>",
+ },
+ },
+ },
+ );
+
+ pub fn finalize(
+ this: *FetchEvent,
+ ) void {
+ VirtualMachine.vm.allocator.destroy(this);
+ }
+
+ pub fn getClient(
+ _: *FetchEvent,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ Output.prettyErrorln("FetchEvent.client is not implemented yet - sorry!!", .{});
+ Output.flush();
+ return js.JSValueMakeUndefined(ctx);
+ }
+ pub fn getRequest(
+ this: *FetchEvent,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSStringRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ var req = bun.default_allocator.create(Request) catch unreachable;
+ req.* = this.request;
+
+ return Request.Class.make(
+ ctx,
+ req,
+ );
+ }
+
+ // https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith
+ pub fn respondWith(
+ this: *FetchEvent,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ arguments: []const js.JSValueRef,
+ exception: js.ExceptionRef,
+ ) js.JSValueRef {
+ var request_context = this.request_context orelse return js.JSValueMakeUndefined(ctx);
+ if (request_context.has_called_done) return js.JSValueMakeUndefined(ctx);
+ var globalThis = ctx.ptr();
+
+ // A Response or a Promise that resolves to a Response. Otherwise, a network error is returned to Fetch.
+ if (arguments.len == 0 or !Response.Class.isLoaded() or !js.JSValueIsObject(ctx, arguments[0])) {
+ JSError(getAllocator(ctx), "event.respondWith() must be a Response or a Promise<Response>.", .{}, ctx, exception);
+ request_context.sendInternalError(error.respondWithWasEmpty) catch {};
+ return js.JSValueMakeUndefined(ctx);
+ }
+
+ var arg = arguments[0];
+
+ if (JSValue.fromRef(arg).as(Response) == null) {
+ this.pending_promise = this.pending_promise orelse JSInternalPromise.resolvedPromise(globalThis, JSValue.fromRef(arguments[0]));
+ }
+
+ if (this.pending_promise) |promise| {
+ VirtualMachine.vm.event_loop.waitForPromise(promise);
+
+ switch (promise.status(ctx.ptr().vm())) {
+ .Fulfilled => {},
+ else => {
+ this.rejected = true;
+ this.pending_promise = null;
+ this.onPromiseRejectionHandler.?(
+ this.onPromiseRejectionCtx,
+ error.PromiseRejection,
+ this,
+ promise.result(globalThis.vm()),
+ );
+ return js.JSValueMakeUndefined(ctx);
+ },
+ }
+
+ arg = promise.result(ctx.ptr().vm()).asRef();
+ }
+
+ var response: *Response = GetJSPrivateData(Response, arg) orelse {
+ this.rejected = true;
+ this.pending_promise = null;
+ JSError(getAllocator(ctx), "event.respondWith() expects Response or Promise<Response>", .{}, ctx, exception);
+ this.onPromiseRejectionHandler.?(this.onPromiseRejectionCtx, error.RespondWithInvalidTypeInternal, this, JSValue.fromRef(exception.*));
+ return js.JSValueMakeUndefined(ctx);
+ };
+
+ defer {
+ if (!VirtualMachine.vm.had_errors) {
+ Output.printElapsed(@intToFloat(f64, (request_context.timer.lap())) / std.time.ns_per_ms);
+
+ Output.prettyError(
+ " <b>{s}<r><d> - <b>{d}<r> <d>transpiled, <d><b>{d}<r> <d>imports<r>\n",
+ .{
+ request_context.matched_route.?.name,
+ VirtualMachine.vm.transpiled_count,
+ VirtualMachine.vm.resolved_count,
+ },
+ );
+ }
+ }
+
+ defer this.pending_promise = null;
+ var needs_mime_type = true;
+ var content_length: ?usize = null;
+
+ if (response.body.init.headers) |headers_ref| {
+ var headers = Headers.from(headers_ref, request_context.allocator) catch unreachable;
+
+ var i: usize = 0;
+ while (i < headers.entries.len) : (i += 1) {
+ var header = headers.entries.get(i);
+ const name = headers.asStr(header.name);
+ if (strings.eqlComptime(name, "content-type") and headers.asStr(header.value).len > 0) {
+ needs_mime_type = false;
+ }
+
+ if (strings.eqlComptime(name, "content-length")) {
+ content_length = std.fmt.parseInt(usize, headers.asStr(header.value), 10) catch null;
+ continue;
+ }
+
+ // Some headers need to be managed by bun
+ if (strings.eqlComptime(name, "transfer-encoding") or
+ strings.eqlComptime(name, "content-encoding") or
+ strings.eqlComptime(name, "strict-transport-security") or
+ strings.eqlComptime(name, "content-security-policy"))
+ {
+ continue;
+ }
+
+ request_context.appendHeaderSlow(
+ name,
+ headers.asStr(header.value),
+ ) catch unreachable;
+ }
+ }
+
+ if (needs_mime_type) {
+ request_context.appendHeader("Content-Type", response.mimeTypeWithDefault(MimeType.html, request_context));
+ }
+
+ var blob = response.body.value.use();
+ defer blob.deinit();
+
+ const content_length_ = content_length orelse blob.size;
+
+ if (content_length_ == 0) {
+ request_context.sendNoContent() catch return js.JSValueMakeUndefined(ctx);
+ return js.JSValueMakeUndefined(ctx);
+ }
+
+ if (FeatureFlags.strong_etags_for_built_files) {
+ const did_send = request_context.writeETag(blob.sharedView()) catch false;
+ if (did_send) {
+ // defer getAllocator(ctx).destroy(str.ptr);
+ return js.JSValueMakeUndefined(ctx);
+ }
+ }
+
+ defer request_context.done();
+
+ request_context.writeStatusSlow(response.body.init.status_code) catch return js.JSValueMakeUndefined(ctx);
+ request_context.prepareToSendBody(content_length_, false) catch return js.JSValueMakeUndefined(ctx);
+
+ request_context.writeBodyBuf(blob.sharedView()) catch return js.JSValueMakeUndefined(ctx);
+
+ return js.JSValueMakeUndefined(ctx);
+ }
+
+ // our implementation of the event listener already does this
+ // so this is a no-op for us
+ pub fn waitUntil(
+ _: *FetchEvent,
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) js.JSValueRef {
+ return js.JSValueMakeUndefined(ctx);
+ }
+};