mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
2012 lines
74 KiB
Zig
2012 lines
74 KiB
Zig
const std = @import("std");
|
|
const Api = @import("../../../api/schema.zig").Api;
|
|
const RequestContext = @import("../../../http.zig").RequestContext;
|
|
const MimeType = @import("../../../http.zig").MimeType;
|
|
const ZigURL = @import("../../../query_string_map.zig").URL;
|
|
const HTTPClient = @import("http");
|
|
const NetworkThread = HTTPClient.NetworkThread;
|
|
|
|
const JSC = @import("../../../jsc.zig");
|
|
const js = JSC.C;
|
|
|
|
const Method = @import("../../../http/method.zig").Method;
|
|
|
|
const ObjectPool = @import("../../../pool.zig").ObjectPool;
|
|
|
|
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 ZigString = JSC.ZigString;
|
|
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 = @import("../javascript.zig").Task;
|
|
|
|
const picohttp = @import("picohttp");
|
|
|
|
pub const Response = struct {
|
|
pub const Class = NewClass(
|
|
Response,
|
|
.{ .name = "Response" },
|
|
.{
|
|
.@"constructor" = constructor,
|
|
.@"text" = .{
|
|
.rfn = getText,
|
|
.ts = d.ts{},
|
|
},
|
|
.@"json" = .{
|
|
.rfn = getJson,
|
|
.ts = d.ts{},
|
|
},
|
|
.@"arrayBuffer" = .{
|
|
.rfn = getArrayBuffer,
|
|
.ts = d.ts{},
|
|
},
|
|
},
|
|
.{
|
|
// .@"url" = .{
|
|
// .@"get" = getURL,
|
|
// .ro = true,
|
|
// },
|
|
|
|
.@"ok" = .{
|
|
.@"get" = getOK,
|
|
.ro = true,
|
|
},
|
|
.@"status" = .{
|
|
.@"get" = getStatus,
|
|
.ro = true,
|
|
},
|
|
},
|
|
);
|
|
|
|
allocator: std.mem.Allocator,
|
|
body: Body,
|
|
status_text: string = "",
|
|
|
|
pub const Props = struct {};
|
|
|
|
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.body.init.status_code == 304 or (this.body.init.status_code >= 200 and this.body.init.status_code <= 299));
|
|
}
|
|
|
|
pub fn getText(
|
|
this: *Response,
|
|
_: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSObjectRef,
|
|
_: []const js.JSValueRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Response/text
|
|
defer this.body.value = .Empty;
|
|
return JSPromise.resolvedPromiseValue(
|
|
VirtualMachine.vm.global,
|
|
(brk: {
|
|
switch (this.body.value) {
|
|
.Unconsumed => {
|
|
if (this.body.len > 0) {
|
|
if (this.body.ptr) |_ptr| {
|
|
var offset: usize = 0;
|
|
while (offset < this.body.len and (_ptr[offset] > 127 or strings.utf8ByteSequenceLength(_ptr[offset]) == 0)) : (offset += 1) {}
|
|
if (offset < this.body.len) {
|
|
break :brk ZigString.init(_ptr[offset..this.body.len]).toValue(VirtualMachine.vm.global);
|
|
}
|
|
}
|
|
}
|
|
|
|
break :brk ZigString.init("").toValue(VirtualMachine.vm.global);
|
|
},
|
|
.Empty => {
|
|
break :brk ZigString.init("").toValue(VirtualMachine.vm.global);
|
|
},
|
|
.String => |str| {
|
|
break :brk ZigString.init(str).toValue(VirtualMachine.vm.global);
|
|
},
|
|
.ArrayBuffer => |buffer| {
|
|
break :brk ZigString.init(buffer.ptr[buffer.offset..buffer.byte_len]).toValue(VirtualMachine.vm.global);
|
|
},
|
|
}
|
|
}),
|
|
).asRef();
|
|
}
|
|
|
|
var temp_error_buffer: [4096]u8 = undefined;
|
|
var error_arg_list: [1]js.JSObjectRef = undefined;
|
|
pub fn getJson(
|
|
this: *Response,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSObjectRef,
|
|
_: []const js.JSValueRef,
|
|
exception: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
var zig_string = ZigString.init("");
|
|
var deallocate = false;
|
|
defer {
|
|
if (deallocate) {
|
|
if (this.body.value == .Unconsumed) {
|
|
this.body.ptr_allocator.?.free(this.body.ptr.?[0..this.body.len]);
|
|
this.body.ptr_allocator = null;
|
|
this.body.ptr = null;
|
|
this.body.len = 0;
|
|
}
|
|
}
|
|
|
|
this.body.value = .Empty;
|
|
}
|
|
|
|
var json_value = (js.JSValueMakeFromJSONString(
|
|
ctx,
|
|
brk: {
|
|
switch (this.body.value) {
|
|
.Unconsumed => {
|
|
if (this.body.ptr) |_ptr| {
|
|
zig_string = ZigString.init(_ptr[0..this.body.len]);
|
|
deallocate = true;
|
|
|
|
break :brk zig_string.toJSStringRef();
|
|
}
|
|
|
|
break :brk zig_string.toJSStringRef();
|
|
},
|
|
.Empty => {
|
|
break :brk zig_string.toJSStringRef();
|
|
},
|
|
.String => |str| {
|
|
zig_string = ZigString.init(str);
|
|
break :brk zig_string.toJSStringRef();
|
|
},
|
|
.ArrayBuffer => |buffer| {
|
|
zig_string = ZigString.init(buffer.ptr[buffer.offset..buffer.byte_len]);
|
|
break :brk zig_string.toJSStringRef();
|
|
},
|
|
}
|
|
},
|
|
) orelse {
|
|
var out = std.fmt.bufPrint(&temp_error_buffer, "Invalid JSON\n\n \"{s}\"", .{zig_string.slice()[0..std.math.min(zig_string.len, 4000)]}) catch unreachable;
|
|
error_arg_list[0] = ZigString.init(out).toValueGC(VirtualMachine.vm.global).asRef();
|
|
return JSPromise.rejectedPromiseValue(
|
|
VirtualMachine.vm.global,
|
|
JSValue.fromRef(
|
|
js.JSObjectMakeError(
|
|
ctx,
|
|
1,
|
|
&error_arg_list,
|
|
exception,
|
|
),
|
|
),
|
|
).asRef();
|
|
});
|
|
|
|
return JSPromise.resolvedPromiseValue(
|
|
VirtualMachine.vm.global,
|
|
JSValue.fromRef(json_value),
|
|
).asRef();
|
|
}
|
|
pub fn getArrayBuffer(
|
|
this: *Response,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSObjectRef,
|
|
_: []const js.JSValueRef,
|
|
exception: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
defer this.body.value = .Empty;
|
|
return JSPromise.resolvedPromiseValue(
|
|
VirtualMachine.vm.global,
|
|
JSValue.fromRef(
|
|
(brk: {
|
|
switch (this.body.value) {
|
|
.Unconsumed => {
|
|
if (this.body.ptr) |_ptr| {
|
|
break :brk js.JSObjectMakeTypedArrayWithBytesNoCopy(
|
|
ctx,
|
|
js.JSTypedArrayType.kJSTypedArrayTypeUint8Array,
|
|
_ptr,
|
|
this.body.len,
|
|
null,
|
|
null,
|
|
exception,
|
|
);
|
|
}
|
|
|
|
break :brk js.JSObjectMakeTypedArray(
|
|
ctx,
|
|
js.JSTypedArrayType.kJSTypedArrayTypeUint8Array,
|
|
0,
|
|
exception,
|
|
);
|
|
},
|
|
.Empty => {
|
|
break :brk js.JSObjectMakeTypedArray(ctx, js.JSTypedArrayType.kJSTypedArrayTypeUint8Array, 0, exception);
|
|
},
|
|
.String => |str| {
|
|
break :brk js.JSObjectMakeTypedArrayWithBytesNoCopy(
|
|
ctx,
|
|
js.JSTypedArrayType.kJSTypedArrayTypeUint8Array,
|
|
@intToPtr([*]u8, @ptrToInt(str.ptr)),
|
|
str.len,
|
|
null,
|
|
null,
|
|
exception,
|
|
);
|
|
},
|
|
.ArrayBuffer => |buffer| {
|
|
break :brk js.JSObjectMakeTypedArrayWithBytesNoCopy(
|
|
ctx,
|
|
buffer.typed_array_type,
|
|
buffer.ptr,
|
|
buffer.byte_len,
|
|
null,
|
|
null,
|
|
exception,
|
|
);
|
|
},
|
|
}
|
|
}),
|
|
),
|
|
).asRef();
|
|
}
|
|
|
|
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);
|
|
this.allocator.destroy(this);
|
|
}
|
|
|
|
pub fn mimeType(response: *const Response, request_ctx: *const RequestContext) string {
|
|
if (response.body.init.headers) |headers| {
|
|
// Remember, we always lowercase it
|
|
// hopefully doesn't matter here tho
|
|
if (headers.getHeaderIndex("content-type")) |content_type| {
|
|
return headers.asStr(headers.entries.items(.value)[content_type]);
|
|
}
|
|
}
|
|
|
|
if (request_ctx.url.extname.len > 0) {
|
|
return MimeType.byExtension(request_ctx.url.extname).value;
|
|
}
|
|
|
|
switch (response.body.value) {
|
|
.Empty => {
|
|
return "text/plain";
|
|
},
|
|
.String => |body| {
|
|
// poor man's mimetype sniffing
|
|
if (body.len > 0 and (body[0] == '{' or body[0] == '[')) {
|
|
return MimeType.json.value;
|
|
}
|
|
|
|
return MimeType.html.value;
|
|
},
|
|
.Unconsumed, .ArrayBuffer => {
|
|
return "application/octet-stream";
|
|
},
|
|
}
|
|
}
|
|
|
|
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.@"404"(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;
|
|
};
|
|
|
|
// if (exception != null) {
|
|
// return null;
|
|
// }
|
|
|
|
var response = getAllocator(ctx).create(Response) catch unreachable;
|
|
response.* = Response{
|
|
.body = body,
|
|
.allocator = getAllocator(ctx),
|
|
};
|
|
return Response.Class.make(
|
|
ctx,
|
|
response,
|
|
);
|
|
}
|
|
};
|
|
|
|
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{},
|
|
},
|
|
},
|
|
.{},
|
|
);
|
|
|
|
const fetch_error_cant_fetch_same_origin = "fetch to same-origin on the server is not supported yet - sorry! (it would just hang forever)";
|
|
|
|
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,
|
|
|
|
const Pool = ObjectPool(FetchTasklet, init, true);
|
|
const BodyPool = ObjectPool(MutableString, MutableString.init2048, true);
|
|
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 {
|
|
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 {
|
|
const fetch_error = std.fmt.allocPrint(
|
|
default_allocator,
|
|
"Fetch error: {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_headers = Headers.fromPicoHeaders(allocator, http_response.headers) catch unreachable;
|
|
response_headers.guard = .immutable;
|
|
var response = allocator.create(Response) catch unreachable;
|
|
var duped = allocator.dupe(u8, this.http.response_buffer.toOwnedSlice()) catch unreachable;
|
|
|
|
response.* = Response{
|
|
.allocator = allocator,
|
|
.status_text = allocator.dupe(u8, http_response.status) catch unreachable,
|
|
.body = .{
|
|
.init = .{
|
|
.headers = response_headers,
|
|
.status_code = @truncate(u16, http_response.status_code),
|
|
},
|
|
.value = .{
|
|
.Unconsumed = 0,
|
|
},
|
|
.ptr = duped.ptr,
|
|
.len = duped.len,
|
|
.ptr_allocator = allocator,
|
|
},
|
|
};
|
|
return JSValue.fromRef(Response.Class.make(@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,
|
|
) !*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.http = try HTTPClient.AsyncHTTP.init(
|
|
allocator,
|
|
method,
|
|
url,
|
|
headers,
|
|
headers_buf,
|
|
&linked_list.data.pooled_body.data,
|
|
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,
|
|
) !*FetchTasklet.Pool.Node {
|
|
var node = try get(allocator, method, url, headers, headers_buf, request_body, timeout);
|
|
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);
|
|
|
|
try VirtualMachine.vm.enqueueTask(Task.init(&node.data));
|
|
return node;
|
|
}
|
|
|
|
pub fn callback(http_: *HTTPClient.AsyncHTTP, sender: *HTTPClient.AsyncHTTP.HTTPSender) void {
|
|
var task: *FetchTasklet = @fieldParentPtr(FetchTasklet, "http", http_);
|
|
@atomicStore(Status, &task.status, Status.done, .Monotonic);
|
|
_ = task.javascript_vm.eventLoop().ready_tasks_count.fetchAdd(1, .Monotonic);
|
|
sender.release();
|
|
}
|
|
};
|
|
|
|
pub fn call(
|
|
_: void,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSObjectRef,
|
|
arguments: []const js.JSValueRef,
|
|
exception: js.ExceptionRef,
|
|
) js.JSObjectRef {
|
|
if (arguments.len == 0) {
|
|
const fetch_error = fetch_error_no_args;
|
|
return JSPromise.rejectedPromiseValue(VirtualMachine.vm.global, ZigString.init(fetch_error).toErrorInstance(VirtualMachine.vm.global)).asRef();
|
|
}
|
|
|
|
if (!js.JSValueIsString(ctx, arguments[0])) {
|
|
const fetch_error = fetch_type_error_strings.get(js.JSValueGetType(ctx, arguments[0]));
|
|
return JSPromise.rejectedPromiseValue(VirtualMachine.vm.global, ZigString.init(fetch_error).toErrorInstance(VirtualMachine.vm.global)).asRef();
|
|
}
|
|
|
|
var url_zig_str = ZigString.init("");
|
|
JSValue.fromRef(arguments[0]).toZigString(&url_zig_str, VirtualMachine.vm.global);
|
|
var url_str = url_zig_str.slice();
|
|
|
|
if (url_str.len == 0) {
|
|
const fetch_error = fetch_error_blank_url;
|
|
return JSPromise.rejectedPromiseValue(VirtualMachine.vm.global, ZigString.init(fetch_error).toErrorInstance(VirtualMachine.vm.global)).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");
|
|
const url = ZigURL.parse(url_str);
|
|
|
|
if (url.origin.len > 0 and strings.eql(url.origin, VirtualMachine.vm.bundler.options.origin.origin)) {
|
|
const fetch_error = fetch_error_cant_fetch_same_origin;
|
|
return JSPromise.rejectedPromiseValue(VirtualMachine.vm.global, ZigString.init(fetch_error).toErrorInstance(VirtualMachine.vm.global)).asRef();
|
|
}
|
|
|
|
var headers: ?Headers = null;
|
|
var body: string = "";
|
|
var method = Method.GET;
|
|
|
|
if (arguments.len >= 2 and js.JSValueIsObject(ctx, arguments[1])) {
|
|
var array = js.JSObjectCopyPropertyNames(ctx, arguments[1]);
|
|
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")) {
|
|
if (js.JSObjectGetProperty(ctx, arguments[1], property_name_ref, null)) |value| {
|
|
if (GetJSPrivateData(Headers, value)) |headers_ptr| {
|
|
headers = headers_ptr.*;
|
|
} else if (Headers.JS.headersInit(ctx, value) catch null) |headers_| {
|
|
headers = headers_;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"body".len => {
|
|
if (js.JSStringIsEqualToUTF8CString(property_name_ref, "body")) {
|
|
if (js.JSObjectGetProperty(ctx, arguments[1], property_name_ref, null)) |value| {
|
|
var body_ = Body.extractBody(ctx, value, false, null, exception);
|
|
if (exception.* != null) return js.JSValueMakeNull(ctx);
|
|
switch (body_.value) {
|
|
.ArrayBuffer => |arraybuffer| {
|
|
body = arraybuffer.ptr[0..arraybuffer.byte_len];
|
|
},
|
|
.String => |str| {
|
|
body = str;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"method".len => {
|
|
if (js.JSStringIsEqualToUTF8CString(property_name_ref, "method")) {
|
|
if (js.JSObjectGetProperty(ctx, arguments[1], property_name_ref, null)) |value| {
|
|
var string_ref = js.JSValueToStringCopy(ctx, value, exception);
|
|
|
|
if (exception.* != null) return js.JSValueMakeNull(ctx);
|
|
defer js.JSStringRelease(string_ref);
|
|
var method_name_buf: [16]u8 = undefined;
|
|
var method_name = method_name_buf[0..js.JSStringGetUTF8CString(string_ref, &method_name_buf, method_name_buf.len)];
|
|
method = Method.which(method_name) orelse method;
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
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 resolve = FetchTasklet.FetchResolver.Class.make(ctx: js.JSContextRef, ptr: *ZigType)
|
|
var queued = FetchTasklet.queue(
|
|
default_allocator,
|
|
VirtualMachine.vm.global,
|
|
method,
|
|
url,
|
|
header_entries,
|
|
header_buf,
|
|
null,
|
|
std.time.ns_per_hour,
|
|
) catch unreachable;
|
|
queued.data.this_object = js.JSObjectMake(ctx, null, JSPrivateDataPtr.init(&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,
|
|
used: u32 = 0,
|
|
guard: Guard = Guard.none,
|
|
|
|
pub fn deinit(
|
|
headers: *Headers,
|
|
) void {
|
|
headers.buf.deinit(headers.allocator);
|
|
headers.entries.deinit(headers.allocator);
|
|
}
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Headers#methods
|
|
pub const JS = struct {
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Headers/get
|
|
pub fn get(
|
|
this: *Headers,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSObjectRef,
|
|
arguments: []const js.JSValueRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
if (arguments.len == 0 or !js.JSValueIsString(ctx, arguments[0]) or js.JSStringIsEqual(arguments[0], Properties.Refs.empty_string)) {
|
|
return js.JSValueMakeNull(ctx);
|
|
}
|
|
|
|
const key_len = js.JSStringGetUTF8CString(arguments[0], &header_kv_buf, header_kv_buf.len);
|
|
const key = header_kv_buf[0 .. key_len - 1];
|
|
if (this.getHeaderIndex(key)) |index| {
|
|
var str = this.asStr(this.entries.items(.value)[index]);
|
|
var ref = js.JSStringCreateWithUTF8CString(str.ptr);
|
|
defer js.JSStringRelease(ref);
|
|
return js.JSValueMakeString(ctx, ref);
|
|
} else {
|
|
return js.JSValueMakeNull(ctx);
|
|
}
|
|
}
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Headers/set
|
|
// > The difference between set() and Headers.append is that if the specified header already exists and accepts multiple values
|
|
// > set() overwrites the existing value with the new one, whereas Headers.append appends the new value to the end of the set of values.
|
|
pub fn set(
|
|
this: *Headers,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSObjectRef,
|
|
arguments: []const js.JSValueRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
if (this.guard == .request or arguments.len < 2 or !js.JSValueIsString(ctx, arguments[0]) or js.JSStringIsEqual(arguments[0], Properties.Refs.empty_string) or !js.JSValueIsString(ctx, arguments[1])) {
|
|
return js.JSValueMakeUndefined(ctx);
|
|
}
|
|
|
|
this.putHeader(arguments[0], arguments[1], false);
|
|
return js.JSValueMakeUndefined(ctx);
|
|
}
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Headers/append
|
|
pub fn append(
|
|
this: *Headers,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSObjectRef,
|
|
arguments: []const js.JSValueRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
if (this.guard == .request or arguments.len < 2 or !js.JSValueIsString(ctx, arguments[0]) or js.JSStringIsEqual(arguments[0], Properties.Refs.empty_string) or !js.JSValueIsString(ctx, arguments[1])) {
|
|
return js.JSValueMakeUndefined(ctx);
|
|
}
|
|
|
|
this.putHeader(arguments[0], arguments[1], true);
|
|
return js.JSValueMakeUndefined(ctx);
|
|
}
|
|
pub fn delete(
|
|
this: *Headers,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSObjectRef,
|
|
arguments: []const js.JSValueRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
if (this.guard == .request or arguments.len < 1 or !js.JSValueIsString(ctx, arguments[0]) or js.JSStringIsEqual(arguments[0], Properties.Refs.empty_string)) {
|
|
return js.JSValueMakeUndefined(ctx);
|
|
}
|
|
|
|
const key_len = js.JSStringGetUTF8CString(arguments[0], &header_kv_buf, header_kv_buf.len) - 1;
|
|
const key = header_kv_buf[0..key_len];
|
|
|
|
if (this.getHeaderIndex(key)) |header_i| {
|
|
this.entries.orderedRemove(header_i);
|
|
}
|
|
|
|
return js.JSValueMakeUndefined(ctx);
|
|
}
|
|
pub fn entries(
|
|
_: *Headers,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSObjectRef,
|
|
_: []const js.JSValueRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
Output.prettyErrorln("<r><b>Headers.entries()<r> is not implemented yet - sorry!!", .{});
|
|
return js.JSValueMakeNull(ctx);
|
|
}
|
|
pub fn keys(
|
|
_: *Headers,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSObjectRef,
|
|
_: []const js.JSValueRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
Output.prettyErrorln("H<r><b>Headers.keys()<r> is not implemented yet- sorry!!", .{});
|
|
return js.JSValueMakeNull(ctx);
|
|
}
|
|
pub fn values(
|
|
_: *Headers,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSObjectRef,
|
|
_: []const js.JSValueRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
Output.prettyErrorln("<r><b>Headers.values()<r> is not implemented yet - sorry!!", .{});
|
|
return js.JSValueMakeNull(ctx);
|
|
}
|
|
|
|
pub fn headersInit(ctx: js.JSContextRef, header_prop: js.JSObjectRef) !?Headers {
|
|
const header_keys = js.JSObjectCopyPropertyNames(ctx, header_prop);
|
|
defer js.JSPropertyNameArrayRelease(header_keys);
|
|
const total_header_count = js.JSPropertyNameArrayGetCount(header_keys);
|
|
if (total_header_count == 0) return null;
|
|
|
|
// 2 passes through the headers
|
|
|
|
// Pass #1: find the "real" count.
|
|
// The number of things which are strings or numbers.
|
|
// Anything else should be ignored.
|
|
// We could throw a TypeError, but ignoring silently is more JavaScript-like imo
|
|
var real_header_count: usize = 0;
|
|
var estimated_buffer_len: usize = 0;
|
|
var j: usize = 0;
|
|
while (j < total_header_count) : (j += 1) {
|
|
var key_ref = js.JSPropertyNameArrayGetNameAtIndex(header_keys, j);
|
|
var value_ref = js.JSObjectGetProperty(ctx, header_prop, key_ref, null);
|
|
|
|
switch (js.JSValueGetType(ctx, value_ref)) {
|
|
js.JSType.kJSTypeNumber => {
|
|
const key_len = js.JSStringGetLength(key_ref);
|
|
if (key_len > 0) {
|
|
real_header_count += 1;
|
|
estimated_buffer_len += key_len;
|
|
estimated_buffer_len += std.fmt.count("{d}", .{js.JSValueToNumber(ctx, value_ref, null)});
|
|
}
|
|
},
|
|
js.JSType.kJSTypeString => {
|
|
const key_len = js.JSStringGetLength(key_ref);
|
|
const value_len = js.JSStringGetLength(value_ref);
|
|
if (key_len > 0 and value_len > 0) {
|
|
real_header_count += 1;
|
|
estimated_buffer_len += key_len + value_len;
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
if (real_header_count == 0 or estimated_buffer_len == 0) return null;
|
|
|
|
j = 0;
|
|
var allocator = getAllocator(ctx);
|
|
var headers = Headers{
|
|
.allocator = allocator,
|
|
.buf = try std.ArrayListUnmanaged(u8).initCapacity(allocator, estimated_buffer_len),
|
|
.entries = Headers.Entries{},
|
|
};
|
|
errdefer headers.deinit();
|
|
try headers.entries.ensureTotalCapacity(allocator, real_header_count);
|
|
headers.buf.expandToCapacity();
|
|
while (j < total_header_count) : (j += 1) {
|
|
var key_ref = js.JSPropertyNameArrayGetNameAtIndex(header_keys, j);
|
|
var value_ref = js.JSObjectGetProperty(ctx, header_prop, key_ref, null);
|
|
|
|
switch (js.JSValueGetType(ctx, value_ref)) {
|
|
js.JSType.kJSTypeNumber => {
|
|
if (js.JSStringGetLength(key_ref) == 0) continue;
|
|
try headers.appendInit(ctx, key_ref, .kJSTypeNumber, value_ref);
|
|
},
|
|
js.JSType.kJSTypeString => {
|
|
if (js.JSStringGetLength(value_ref) == 0 or js.JSStringGetLength(key_ref) == 0) continue;
|
|
try headers.appendInit(ctx, key_ref, .kJSTypeString, value_ref);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Headers/Headers
|
|
pub fn constructor(
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
arguments: []const js.JSValueRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSObjectRef {
|
|
var headers = getAllocator(ctx).create(Headers) catch unreachable;
|
|
if (arguments.len > 0 and js.JSValueIsObjectOfClass(ctx, arguments[0], Headers.Class.get().*)) {
|
|
var other = castObj(arguments[0], Headers);
|
|
other.clone(headers) catch unreachable;
|
|
} else if (arguments.len == 1 and js.JSValueIsObject(ctx, arguments[0])) {
|
|
headers.* = (JS.headersInit(ctx, arguments[0]) catch unreachable) orelse Headers{
|
|
.entries = @TypeOf(headers.entries){},
|
|
.buf = @TypeOf(headers.buf){},
|
|
.used = 0,
|
|
.allocator = getAllocator(ctx),
|
|
.guard = Guard.none,
|
|
};
|
|
} else {
|
|
headers.* = Headers{
|
|
.entries = @TypeOf(headers.entries){},
|
|
.buf = @TypeOf(headers.buf){},
|
|
.used = 0,
|
|
.allocator = getAllocator(ctx),
|
|
.guard = Guard.none,
|
|
};
|
|
}
|
|
|
|
return Headers.Class.make(ctx, headers);
|
|
}
|
|
|
|
pub fn finalize(
|
|
this: *Headers,
|
|
) void {
|
|
this.deinit();
|
|
}
|
|
};
|
|
pub const Class = NewClass(
|
|
Headers,
|
|
.{
|
|
.name = "Headers",
|
|
.read_only = true,
|
|
},
|
|
.{
|
|
.@"get" = .{
|
|
.rfn = JS.get,
|
|
},
|
|
.@"set" = .{
|
|
.rfn = JS.set,
|
|
.ts = d.ts{},
|
|
},
|
|
.@"append" = .{
|
|
.rfn = JS.append,
|
|
.ts = d.ts{},
|
|
},
|
|
.@"delete" = .{
|
|
.rfn = JS.delete,
|
|
.ts = d.ts{},
|
|
},
|
|
.@"entries" = .{
|
|
.rfn = JS.entries,
|
|
.ts = d.ts{},
|
|
},
|
|
.@"keys" = .{
|
|
.rfn = JS.keys,
|
|
.ts = d.ts{},
|
|
},
|
|
.@"values" = .{
|
|
.rfn = JS.values,
|
|
.ts = d.ts{},
|
|
},
|
|
.@"constructor" = .{
|
|
.rfn = JS.constructor,
|
|
.ts = d.ts{},
|
|
},
|
|
.@"finalize" = .{
|
|
.rfn = JS.finalize,
|
|
},
|
|
},
|
|
.{},
|
|
);
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Glossary/Guard
|
|
pub const Guard = enum {
|
|
immutable,
|
|
request,
|
|
@"request-no-cors",
|
|
response,
|
|
none,
|
|
};
|
|
|
|
pub fn fromPicoHeaders(allocator: std.mem.Allocator, picohttp_headers: []const picohttp.Header) !Headers {
|
|
var total_len: usize = 0;
|
|
for (picohttp_headers) |header| {
|
|
total_len += header.name.len;
|
|
total_len += header.value.len;
|
|
}
|
|
// for the null bytes
|
|
total_len += picohttp_headers.len * 2;
|
|
var headers = Headers{
|
|
.allocator = allocator,
|
|
.entries = Headers.Entries{},
|
|
.buf = std.ArrayListUnmanaged(u8){},
|
|
};
|
|
try headers.entries.ensureTotalCapacity(allocator, picohttp_headers.len);
|
|
try headers.buf.ensureTotalCapacity(allocator, total_len);
|
|
headers.buf.expandToCapacity();
|
|
headers.guard = Guard.request;
|
|
|
|
for (picohttp_headers) |header| {
|
|
headers.entries.appendAssumeCapacity(.{
|
|
.name = headers.appendString(
|
|
string,
|
|
header.name,
|
|
true,
|
|
true,
|
|
true,
|
|
),
|
|
.value = headers.appendString(
|
|
string,
|
|
header.value,
|
|
true,
|
|
true,
|
|
true,
|
|
),
|
|
});
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
// TODO: is it worth making this lazy? instead of copying all the request headers, should we just do it on get/put/iterator?
|
|
pub fn fromRequestCtx(allocator: std.mem.Allocator, request: *RequestContext) !Headers {
|
|
return fromPicoHeaders(allocator, request.request.headers);
|
|
}
|
|
|
|
pub fn asStr(headers: *const Headers, ptr: Api.StringPointer) []u8 {
|
|
return headers.buf.items[ptr.offset..][0..ptr.length];
|
|
}
|
|
|
|
threadlocal var header_kv_buf: [4096]u8 = undefined;
|
|
|
|
pub fn putHeader(headers: *Headers, key_: js.JSStringRef, value_: js.JSStringRef, comptime append: bool) void {
|
|
const key_len = js.JSStringGetUTF8CString(key_, &header_kv_buf, header_kv_buf.len) - 1;
|
|
// TODO: make this one pass instead of two
|
|
var key = strings.trim(header_kv_buf[0..key_len], " \n\r");
|
|
key = std.ascii.lowerString(key[0..key.len], key);
|
|
|
|
var remainder = header_kv_buf[key.len..];
|
|
const value_len = js.JSStringGetUTF8CString(value_, remainder.ptr, remainder.len) - 1;
|
|
var value = strings.trim(remainder[0..value_len], " \n\r");
|
|
|
|
if (headers.getHeaderIndex(key)) |header_i| {
|
|
const existing_value = headers.entries.items(.value)[header_i];
|
|
|
|
if (append) {
|
|
const end = @truncate(u32, value.len + existing_value.length + 2);
|
|
headers.buf.ensureUnusedCapacity(headers.allocator, end) catch unreachable;
|
|
headers.buf.expandToCapacity();
|
|
var new_end = headers.buf.items[headers.used..][0 .. end - 1];
|
|
const existing_buf = headers.asStr(existing_value);
|
|
std.mem.copy(u8, existing_buf, new_end);
|
|
new_end[existing_buf.len] = ',';
|
|
std.mem.copy(u8, new_end[existing_buf.len + 1 ..], value);
|
|
new_end.ptr[end - 1] = 0;
|
|
headers.entries.items(.value)[header_i] = Api.StringPointer{ .offset = headers.used, .length = end - 1 };
|
|
headers.used += end;
|
|
// Can we get away with just overwriting in-place?
|
|
} else if (existing_value.length < value.len) {
|
|
std.mem.copy(u8, headers.asStr(existing_value), value);
|
|
headers.entries.items(.value)[header_i].length = @truncate(u32, value.len);
|
|
headers.asStr(headers.entries.items(.value)[header_i]).ptr[value.len] = 0;
|
|
// Otherwise, append to the buffer, and just don't bother dealing with the existing header value
|
|
// We assume that these header objects are going to be kind of short-lived.
|
|
} else {
|
|
headers.buf.ensureUnusedCapacity(headers.allocator, value.len + 1) catch unreachable;
|
|
headers.buf.expandToCapacity();
|
|
headers.entries.items(.value)[header_i] = headers.appendString(string, value, false, true, true);
|
|
}
|
|
} else {
|
|
headers.appendHeader(key, value, false, false, true);
|
|
}
|
|
}
|
|
|
|
pub fn getHeaderIndex(headers: *const Headers, key: string) ?u32 {
|
|
for (headers.entries.items(.name)) |name, i| {
|
|
if (name.length == key.len and strings.eqlInsensitive(key, headers.asStr(name))) {
|
|
return @truncate(u32, i);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn appendHeader(
|
|
headers: *Headers,
|
|
key: string,
|
|
value: string,
|
|
comptime needs_lowercase: bool,
|
|
comptime needs_normalize: bool,
|
|
comptime append_null: bool,
|
|
) void {
|
|
headers.buf.ensureUnusedCapacity(headers.allocator, key.len + value.len + 2) catch unreachable;
|
|
headers.buf.expandToCapacity();
|
|
headers.entries.append(
|
|
headers.allocator,
|
|
.{
|
|
.name = headers.appendString(
|
|
string,
|
|
key,
|
|
needs_lowercase,
|
|
needs_normalize,
|
|
append_null,
|
|
),
|
|
.value = headers.appendString(
|
|
string,
|
|
value,
|
|
needs_lowercase,
|
|
needs_normalize,
|
|
append_null,
|
|
),
|
|
},
|
|
) catch unreachable;
|
|
}
|
|
|
|
fn appendString(
|
|
this: *Headers,
|
|
comptime StringType: type,
|
|
str: StringType,
|
|
comptime needs_lowercase: bool,
|
|
comptime needs_normalize: bool,
|
|
comptime append_null: bool,
|
|
) Api.StringPointer {
|
|
var ptr = Api.StringPointer{ .offset = this.used, .length = 0 };
|
|
ptr.length = @truncate(
|
|
u32,
|
|
switch (comptime StringType) {
|
|
js.JSStringRef => js.JSStringGetLength(str),
|
|
else => str.len,
|
|
},
|
|
);
|
|
std.debug.assert(ptr.length > 0);
|
|
|
|
std.debug.assert(this.buf.items.len >= ptr.offset + ptr.length);
|
|
var slice = this.buf.items[ptr.offset..][0..ptr.length];
|
|
switch (comptime StringType) {
|
|
js.JSStringRef => {
|
|
ptr.length = @truncate(u32, js.JSStringGetUTF8CString(str, slice.ptr, slice.len) - 1);
|
|
},
|
|
else => {
|
|
std.mem.copy(u8, slice, str);
|
|
},
|
|
}
|
|
|
|
if (comptime needs_normalize) {
|
|
slice = strings.trim(slice, " \r\n");
|
|
}
|
|
|
|
if (comptime needs_lowercase) {
|
|
for (slice) |c, i| {
|
|
slice[i] = std.ascii.toLower(c);
|
|
}
|
|
}
|
|
if (comptime append_null) {
|
|
slice.ptr[slice.len] = 0;
|
|
this.used += 1;
|
|
}
|
|
|
|
ptr.length = @truncate(u32, slice.len);
|
|
this.used += @truncate(u32, ptr.length);
|
|
return ptr;
|
|
}
|
|
|
|
fn appendNumber(this: *Headers, num: f64) Api.StringPointer {
|
|
var ptr = Api.StringPointer{ .offset = this.used, .length = @truncate(
|
|
u32,
|
|
std.fmt.count("{d}", .{num}),
|
|
) };
|
|
std.debug.assert(this.buf.items.len >= ptr.offset + ptr.length);
|
|
var slice = this.buf.items[ptr.offset..][0..ptr.length];
|
|
var buf = std.fmt.bufPrint(slice, "{d}", .{num}) catch &[_]u8{};
|
|
ptr.length = @truncate(u32, buf.len);
|
|
this.used += ptr.length;
|
|
return ptr;
|
|
}
|
|
|
|
pub fn appendInit(this: *Headers, ctx: js.JSContextRef, key: js.JSStringRef, comptime value_type: js.JSType, value: js.JSValueRef) !void {
|
|
this.entries.append(this.allocator, .{
|
|
.name = this.appendString(js.JSStringRef, key, true, true, false),
|
|
.value = switch (comptime value_type) {
|
|
js.JSType.kJSTypeNumber => this.appendNumber(js.JSValueToNumber(ctx, value, null)),
|
|
js.JSType.kJSTypeString => this.appendString(js.JSStringRef, value, true, true, false),
|
|
else => unreachable,
|
|
},
|
|
}) catch unreachable;
|
|
}
|
|
|
|
pub fn clone(this: *Headers, to: *Headers) !void {
|
|
to.* = Headers{
|
|
.entries = try this.entries.clone(this.allocator),
|
|
.buf = try @TypeOf(this.buf).initCapacity(this.allocator, this.buf.items.len),
|
|
.used = this.used,
|
|
.allocator = this.allocator,
|
|
.guard = Guard.none,
|
|
};
|
|
to.buf.expandToCapacity();
|
|
std.mem.copy(u8, to.buf.items, this.buf.items);
|
|
}
|
|
};
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Body
|
|
pub const Body = struct {
|
|
init: Init,
|
|
value: Value,
|
|
ptr: ?[*]u8 = null,
|
|
len: usize = 0,
|
|
ptr_allocator: ?std.mem.Allocator = null,
|
|
|
|
pub fn deinit(this: *Body, allocator: std.mem.Allocator) void {
|
|
if (this.init.headers) |headers| {
|
|
headers.deinit();
|
|
}
|
|
|
|
switch (this.value) {
|
|
.ArrayBuffer => {},
|
|
.String => |str| {
|
|
allocator.free(str);
|
|
},
|
|
.Empty => {},
|
|
}
|
|
}
|
|
|
|
pub const Init = struct {
|
|
headers: ?Headers,
|
|
status_code: u16,
|
|
|
|
pub fn init(_: std.mem.Allocator, ctx: js.JSContextRef, init_ref: js.JSValueRef) !?Init {
|
|
var result = Init{ .headers = null, .status_code = 0 };
|
|
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| {
|
|
switch (js.JSValueGetType(ctx, header_prop)) {
|
|
js.JSType.kJSTypeObject => {
|
|
result.headers = try Headers.JS.headersInit(ctx, header_prop);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"statusCode".len => {
|
|
if (js.JSStringIsEqualToUTF8CString(property_name_ref, "statusCode")) {
|
|
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 (result.headers == null and result.status_code < 200) return null;
|
|
return result;
|
|
}
|
|
};
|
|
pub const Value = union(Tag) {
|
|
ArrayBuffer: ArrayBuffer,
|
|
String: string,
|
|
Empty: u0,
|
|
Unconsumed: u0,
|
|
pub const Tag = enum {
|
|
ArrayBuffer,
|
|
String,
|
|
Empty,
|
|
Unconsumed,
|
|
};
|
|
|
|
pub fn length(value: *const Value) usize {
|
|
switch (value.*) {
|
|
.ArrayBuffer => |buf| {
|
|
return buf.ptr[buf.offset..buf.byte_len].len;
|
|
},
|
|
.String => |str| {
|
|
return str.len;
|
|
},
|
|
else => {
|
|
return 0;
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
pub fn @"404"(_: js.JSContextRef) Body {
|
|
return Body{ .init = Init{
|
|
.headers = null,
|
|
.status_code = 404,
|
|
}, .value = .{ .Empty = 0 } };
|
|
}
|
|
|
|
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 }, .value = .{ .Empty = 0 } };
|
|
|
|
switch (js.JSValueGetType(ctx, body_ref)) {
|
|
.kJSTypeString => {
|
|
var allocator = getAllocator(ctx);
|
|
|
|
if (comptime has_init) {
|
|
if (Init.init(allocator, ctx, init_ref.?)) |maybeInit| {
|
|
if (maybeInit) |init_| {
|
|
body.init = init_;
|
|
}
|
|
} else |_| {}
|
|
}
|
|
|
|
var wtf_string = JSValue.fromRef(body_ref).toWTFString(VirtualMachine.vm.global);
|
|
|
|
if (wtf_string.isEmpty()) {
|
|
body.value = .{ .String = "" };
|
|
return body;
|
|
}
|
|
|
|
if (!wtf_string.is8Bit()) {
|
|
var js_string = js.JSValueToStringCopy(ctx, body_ref, exception);
|
|
defer js.JSStringRelease(js_string);
|
|
body.ptr_allocator = default_allocator;
|
|
const len = js.JSStringGetLength(js_string);
|
|
var body_string = default_allocator.alloc(u8, len + 1) catch unreachable;
|
|
body.ptr = body_string.ptr;
|
|
body.len = body_string.len;
|
|
body.value = .{ .String = body_string.ptr[0..js.JSStringGetUTF8CString(js_string, body_string.ptr, body_string.len)] };
|
|
return body;
|
|
}
|
|
|
|
var slice = wtf_string.characters8()[0..wtf_string.length()];
|
|
|
|
if (slice.len == 0) {
|
|
body.value = .{ .String = "" };
|
|
return body;
|
|
}
|
|
|
|
body.value = Value{
|
|
.String = slice,
|
|
};
|
|
// body.ptr = body.
|
|
// body.len = body.value.String.len;str.characters8()[0..len] };
|
|
|
|
return body;
|
|
},
|
|
.kJSTypeObject => {
|
|
const typed_array = js.JSValueGetTypedArrayType(ctx, body_ref, exception);
|
|
switch (typed_array) {
|
|
js.JSTypedArrayType.kJSTypedArrayTypeNone => {},
|
|
else => {
|
|
const buffer = ArrayBuffer{
|
|
.ptr = @ptrCast([*]u8, js.JSObjectGetTypedArrayBytesPtr(ctx, body_ref.?, exception).?),
|
|
.offset = @truncate(u32, js.JSObjectGetTypedArrayByteOffset(ctx, body_ref.?, exception)),
|
|
.len = @truncate(u32, js.JSObjectGetTypedArrayLength(ctx, body_ref.?, exception)),
|
|
.byte_len = @truncate(u32, js.JSObjectGetTypedArrayLength(ctx, body_ref.?, exception)),
|
|
.typed_array_type = typed_array,
|
|
};
|
|
var allocator = getAllocator(ctx);
|
|
|
|
if (comptime has_init) {
|
|
if (Init.init(allocator, ctx, init_ref.?)) |maybeInit| {
|
|
if (maybeInit) |init_| {
|
|
body.init = init_;
|
|
}
|
|
} else |_| {}
|
|
}
|
|
body.value = Value{ .ArrayBuffer = buffer };
|
|
body.ptr = buffer.ptr[buffer.offset..buffer.byte_len].ptr;
|
|
body.len = buffer.ptr[buffer.offset..buffer.byte_len].len;
|
|
return body;
|
|
},
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
if (exception == null) {
|
|
JSError(getAllocator(ctx), "Body must be a string or a TypedArray (for now)", .{}, ctx, exception);
|
|
}
|
|
|
|
return body;
|
|
}
|
|
};
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Request
|
|
pub const Request = struct {
|
|
request_context: *RequestContext,
|
|
url_string_ref: js.JSStringRef = null,
|
|
headers: ?Headers = null,
|
|
|
|
pub const Class = NewClass(
|
|
Request,
|
|
.{
|
|
.name = "Request",
|
|
.read_only = true,
|
|
},
|
|
.{},
|
|
.{
|
|
.@"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,
|
|
},
|
|
},
|
|
);
|
|
|
|
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(VirtualMachine.vm.global).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(VirtualMachine.vm.global).asRef());
|
|
}
|
|
pub fn getDestination(
|
|
_: *Request,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSStringRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
return js.JSValueMakeString(ctx, ZigString.init("").toValueGC(VirtualMachine.vm.global).asRef());
|
|
}
|
|
pub fn getHeaders(
|
|
this: *Request,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSStringRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
if (this.headers == null) {
|
|
this.headers = Headers.fromRequestCtx(getAllocator(ctx), this.request_context) catch unreachable;
|
|
}
|
|
|
|
return Headers.Class.make(ctx, &this.headers.?);
|
|
}
|
|
pub fn getIntegrity(
|
|
_: *Request,
|
|
_: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSStringRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
return ZigString.Empty.toValueGC(VirtualMachine.vm.global).asRef();
|
|
}
|
|
pub fn getMethod(
|
|
this: *Request,
|
|
_: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSStringRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
const string_contents: string = switch (this.request_context.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).toValueGC(VirtualMachine.vm.global).asRef();
|
|
}
|
|
|
|
pub fn getMode(
|
|
_: *Request,
|
|
_: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSStringRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
return ZigString.init(Properties.UTF8.navigate).toValueGC(VirtualMachine.vm.global).asRef();
|
|
}
|
|
pub fn getRedirect(
|
|
_: *Request,
|
|
_: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSStringRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
return ZigString.init(Properties.UTF8.follow).toValueGC(VirtualMachine.vm.global).asRef();
|
|
}
|
|
pub fn getReferrer(
|
|
this: *Request,
|
|
_: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSStringRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
if (this.request_context.header("Referrer")) |referrer| {
|
|
return ZigString.init(referrer).toValueGC(VirtualMachine.vm.global).asRef();
|
|
} else {
|
|
return ZigString.init("").toValueGC(VirtualMachine.vm.global).asRef();
|
|
}
|
|
}
|
|
pub fn getReferrerPolicy(
|
|
_: *Request,
|
|
_: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSStringRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
return ZigString.init("").toValueGC(VirtualMachine.vm.global).asRef();
|
|
}
|
|
pub fn getUrl(
|
|
this: *Request,
|
|
ctx: js.JSContextRef,
|
|
_: js.JSObjectRef,
|
|
_: js.JSStringRef,
|
|
_: js.ExceptionRef,
|
|
) js.JSValueRef {
|
|
if (this.url_string_ref == null) {
|
|
this.url_string_ref = js.JSStringCreateWithUTF8CString(this.request_context.getFullURL());
|
|
}
|
|
|
|
return js.JSValueMakeString(ctx, this.url_string_ref);
|
|
}
|
|
};
|
|
|
|
// 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,
|
|
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,
|
|
},
|
|
.{
|
|
.@"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 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 {
|
|
return Request.Class.make(ctx, &this.request);
|
|
}
|
|
|
|
// 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 {
|
|
if (this.request_context.has_called_done) return js.JSValueMakeUndefined(ctx);
|
|
|
|
// 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.loaded or !js.JSValueIsObject(ctx, arguments[0])) {
|
|
JSError(getAllocator(ctx), "event.respondWith() must be a Response or a Promise<Response>.", .{}, ctx, exception);
|
|
this.request_context.sendInternalError(error.respondWithWasEmpty) catch {};
|
|
return js.JSValueMakeUndefined(ctx);
|
|
}
|
|
|
|
var arg = arguments[0];
|
|
|
|
if (!js.JSValueIsObjectOfClass(ctx, arg, Response.Class.ref)) {
|
|
this.pending_promise = this.pending_promise orelse JSInternalPromise.resolvedPromise(VirtualMachine.vm.global, JSValue.fromRef(arguments[0]));
|
|
}
|
|
|
|
if (this.pending_promise) |promise| {
|
|
var status = promise.status(VirtualMachine.vm.global.vm());
|
|
|
|
if (status == .Pending) {
|
|
VirtualMachine.vm.tick();
|
|
status = promise.status(VirtualMachine.vm.global.vm());
|
|
}
|
|
|
|
switch (status) {
|
|
.Fulfilled => {},
|
|
else => {
|
|
this.rejected = true;
|
|
this.pending_promise = null;
|
|
this.onPromiseRejectionHandler.?(
|
|
this.onPromiseRejectionCtx,
|
|
error.PromiseRejection,
|
|
this,
|
|
promise.result(VirtualMachine.vm.global.vm()),
|
|
);
|
|
return js.JSValueMakeUndefined(ctx);
|
|
},
|
|
}
|
|
|
|
arg = promise.result(VirtualMachine.vm.global.vm()).asRef();
|
|
}
|
|
|
|
if (!js.JSValueIsObjectOfClass(ctx, arg, Response.Class.ref)) {
|
|
this.rejected = true;
|
|
this.pending_promise = null;
|
|
JSError(getAllocator(ctx), "event.respondWith() must be a Response or a Promise<Response>.", .{}, ctx, exception);
|
|
this.onPromiseRejectionHandler.?(this.onPromiseRejectionCtx, error.RespondWithInvalidType, this, JSValue.fromRef(exception.*));
|
|
|
|
return js.JSValueMakeUndefined(ctx);
|
|
}
|
|
|
|
var response: *Response = GetJSPrivateData(Response, arg) orelse {
|
|
this.rejected = true;
|
|
this.pending_promise = null;
|
|
JSError(getAllocator(ctx), "event.respondWith()'s Response object was invalid. This may be an internal error.", .{}, 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, (this.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",
|
|
.{
|
|
this.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| {
|
|
this.request_context.clearHeaders() catch {};
|
|
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")) {
|
|
needs_mime_type = false;
|
|
}
|
|
|
|
if (strings.eqlComptime(name, "content-length")) {
|
|
content_length = std.fmt.parseInt(usize, headers.asStr(header.value), 10) catch null;
|
|
continue;
|
|
}
|
|
|
|
this.request_context.appendHeaderSlow(
|
|
name,
|
|
headers.asStr(header.value),
|
|
) catch unreachable;
|
|
}
|
|
}
|
|
|
|
if (needs_mime_type) {
|
|
this.request_context.appendHeader("Content-Type", response.mimeType(this.request_context));
|
|
}
|
|
|
|
const content_length_ = content_length orelse response.body.value.length();
|
|
|
|
if (content_length_ == 0) {
|
|
this.request_context.sendNoContent() catch return js.JSValueMakeUndefined(ctx);
|
|
return js.JSValueMakeUndefined(ctx);
|
|
}
|
|
|
|
if (FeatureFlags.strong_etags_for_built_files) {
|
|
switch (response.body.value) {
|
|
.ArrayBuffer => |buf| {
|
|
const did_send = this.request_context.writeETag(buf.ptr[buf.offset..buf.byte_len]) catch false;
|
|
if (did_send) return js.JSValueMakeUndefined(ctx);
|
|
},
|
|
.String => |str| {
|
|
const did_send = this.request_context.writeETag(str) catch false;
|
|
if (did_send) {
|
|
// defer getAllocator(ctx).destroy(str.ptr);
|
|
return js.JSValueMakeUndefined(ctx);
|
|
}
|
|
},
|
|
else => unreachable,
|
|
}
|
|
}
|
|
|
|
defer this.request_context.done();
|
|
defer {
|
|
if (response.body.ptr_allocator) |alloc| {
|
|
if (response.body.ptr) |ptr| {
|
|
alloc.free(ptr[0..response.body.len]);
|
|
}
|
|
|
|
response.body.ptr_allocator = null;
|
|
}
|
|
}
|
|
|
|
this.request_context.writeStatusSlow(response.body.init.status_code) catch return js.JSValueMakeUndefined(ctx);
|
|
this.request_context.prepareToSendBody(content_length_, false) catch return js.JSValueMakeUndefined(ctx);
|
|
|
|
switch (response.body.value) {
|
|
.ArrayBuffer => |buf| {
|
|
this.request_context.writeBodyBuf(buf.ptr[buf.offset..buf.byte_len]) catch return js.JSValueMakeUndefined(ctx);
|
|
},
|
|
.String => |str| {
|
|
// defer getAllocator(ctx).destroy(str.ptr);
|
|
this.request_context.writeBodyBuf(str) catch return js.JSValueMakeUndefined(ctx);
|
|
},
|
|
else => unreachable,
|
|
}
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
// pub const ReadableStream = struct {
|
|
// pub const Class = NewClass(
|
|
// ReadableStream,
|
|
// .{
|
|
// .name = "ReadableStream",
|
|
// },
|
|
// .{},
|
|
// .{
|
|
|
|
// },
|
|
// );
|
|
// };
|
|
|
|
// pub const TextEncoder = struct {
|
|
// pub const Class = NewClass(
|
|
// TextEncoder,
|
|
// .{
|
|
// .name = "TextEncoder",
|
|
// },
|
|
// .{
|
|
// .encoding = .{
|
|
// .@"get" = getEncoding,
|
|
// .ro = true,
|
|
// },
|
|
// },
|
|
// .{
|
|
// .encode = .{
|
|
// .rfn = encode,
|
|
// },
|
|
// .constructor = .{
|
|
// .rfn = constructor,
|
|
// },
|
|
// .encodeInto = .{
|
|
// .rfn = encodeInto,
|
|
// },
|
|
// },
|
|
// );
|
|
|
|
// const encoding_str = "utf-8";
|
|
// pub fn getEncoding(
|
|
// this: *TextEncoder,
|
|
// ctx: js.JSContextRef,
|
|
// thisObject: js.JSObjectRef,
|
|
// prop: js.JSStringRef,
|
|
// exception: js.ExceptionRef,
|
|
// ) js.JSValueRef {
|
|
// return ZigString.init(encoding_str).toValue(ctx).asRef()
|
|
// }
|
|
// };
|
|
|
|
// pub const TextDecoder = struct {
|
|
// pub const Class = NewClass(
|
|
// TextDecoder,
|
|
// .{
|
|
// .name = "TextDecoder",
|
|
// },
|
|
// .{},
|
|
// .{
|
|
// .decode = .{},
|
|
// .constructor = .{},
|
|
// },
|
|
// );
|
|
// };
|
|
|
|
test "" {
|
|
std.testing.refAllDecls(Api);
|
|
}
|