mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
* very very wip
* almost ready to fix the errors
* Update identity_context.zig
* Update base.zig
* [bun test] It runs successfully
* Remove unnecessary call
* [Bun.js] Improve JS <> Zig unicode string interop
This fixes longstanding unicode bugs with `console.log` & `fetch`.
I believe @evanwashere reported this first awhile ago
* [Bun.js] Implement `Object.is()` binding and a way to set a timeout for script execution
* Update PLCrashReport.zig
* [Bun.js] Make `console.log` more closely match Node.js and Deno
* [Bun.js] Implement formatting specifier for console.*
* Implement `console.clear()`
* bug fix
* Support console.clear()
* Buffer stderr
* [bun test] Begin implementing Node.js `fs`
* Update darwin_c.zig
* Implement more of `fs`
* `mkdir`, `mkdir` recursive, `mkdtemp`
* `open`, `read` (and pread)
* Move some things into more files
* Implement readdir
* `readFile`, `readLink`, and `realpath`
* `writeFile`, `symlink`, `chown`, `rename`, `stat`, `unlink`, `truncate`
* `lutimes`
* Implement `SystemError` and begin wiring up the `fs` module
* `"fs"` - Most of the arguments / validation
* `fs` - Rest of the arguments / validations
* Begin wiring up the `fs` module
* Fix all the build errors
* support printing typed arrays in console.log
* It...works?
* Support `require("fs")`, `import fs from 'fs';`, `import * as fs from 'fs'`
* Fix a couple bugs
* get rid of the crash reporter for now
* Update fs.exports.js
* [bun.js] slight improvement to startup time
* [bun.js] Improve error message printing
* [Bun.js] Add `Bun.gc()` to run the garbage collector manually and report heap size
* [Bun.js] Add Bun.generateHeapSnapshot to return what JS types are using memory
* [Bun.js] Add `Bun.shrink()` to tell JSC to shrink the VM size
* Improve encoding reader
* [bun.js] Improve callback & microtask performance
* Update node_fs.zig
* Implement `console.assert`
* simple test
* [Bun.js] Prepare for multiple globals/realms to support testing
* Create callbacks-overhead.mjs
* Update http.zig
* [Bun.js] Implement `queueMicrotask`
* Add test for queueMicrotask
* 😪
* [Bun.js] Implement `process.versions`, `process.pid`, `process.ppid`, `process.nextTick`, `process.versions`,
* Implement `process.env.toJSON()`
* [Bun.js] Improve performance of `fs.existsSync`
* 💅
* [Bun.js] Implement `process.chdir(str)` and `process.cwd()`, support up to 4 args in `process.nextTick`
* Make creating Zig::Process lazy
* Split processi nto separte file
* [Bun.js] Node.js Streams - Part 1/?
* [Bun.js] Node.js streams 2/?
* WIP streams
* fix crash
* Reduce allocations in many places
* swap
* Make `bun` start 2ms faster
* Always use an apiLock()
* libBacktrace doesn't really work yet
* Fix crash in the upgrade checker
* Clean up code for importing the runtime when not bundling
* 📷
* Update linker.zig
* 68!
* backtrace
* no, really backtrace
* Fix
* Linux fixes
* Fixes on Linux
* Update mimalloc
* [bun test] Automatically scan for {.test,_test,.spec,_spec}.{jsx,tsx,js,cts,mts,ts,cjs}
2031 lines
74 KiB
Zig
2031 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,
|
||
ctx: 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(
|
||
ctx.ptr(),
|
||
(brk: {
|
||
switch (this.body.value) {
|
||
.Unconsumed => {
|
||
if (this.body.len > 0) {
|
||
if (this.body.ptr) |_ptr| {
|
||
var zig_string = ZigString.init(_ptr[0..this.body.len]);
|
||
zig_string.detectEncoding();
|
||
if (zig_string.is16Bit()) {
|
||
var value = zig_string.to16BitValue(ctx.ptr());
|
||
this.body.ptr_allocator.?.free(_ptr[0..this.body.len]);
|
||
this.body.ptr_allocator = null;
|
||
this.body.ptr = null;
|
||
break :brk value;
|
||
}
|
||
|
||
break :brk zig_string.toValue(ctx.ptr());
|
||
}
|
||
}
|
||
|
||
break :brk ZigString.init("").toValue(ctx.ptr());
|
||
},
|
||
.Empty => {
|
||
break :brk ZigString.init("").toValue(ctx.ptr());
|
||
},
|
||
.String => |str| {
|
||
var zig_string = ZigString.init(str);
|
||
|
||
zig_string.detectEncoding();
|
||
if (zig_string.is16Bit()) {
|
||
var value = zig_string.to16BitValue(ctx.ptr());
|
||
if (this.body.ptr_allocator) |allocator| this.body.deinit(allocator);
|
||
break :brk value;
|
||
}
|
||
|
||
break :brk zig_string.toValue(ctx.ptr());
|
||
},
|
||
.ArrayBuffer => |buffer| {
|
||
break :brk ZigString.init(buffer.ptr[buffer.offset..buffer.byte_len]).toValue(ctx.ptr());
|
||
},
|
||
}
|
||
}),
|
||
).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(ctx.ptr()).asRef();
|
||
return JSPromise.rejectedPromiseValue(
|
||
ctx.ptr(),
|
||
JSValue.fromRef(
|
||
js.JSObjectMakeError(
|
||
ctx,
|
||
1,
|
||
&error_arg_list,
|
||
exception,
|
||
),
|
||
),
|
||
).asRef();
|
||
});
|
||
|
||
return JSPromise.resolvedPromiseValue(
|
||
ctx.ptr(),
|
||
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(
|
||
ctx.ptr(),
|
||
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() 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_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);
|
||
|
||
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().enqueueTaskConcurrent(Task.init(task));
|
||
sender.release();
|
||
}
|
||
};
|
||
|
||
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();
|
||
}
|
||
|
||
if (!js.JSValueIsString(ctx, arguments[0])) {
|
||
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 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");
|
||
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(globalThis, ZigString.init(fetch_error).toErrorInstance(globalThis)).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,
|
||
globalThis,
|
||
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 {
|
||
this.ptr_allocator = null;
|
||
if (this.init.headers) |*headers| {
|
||
headers.deinit();
|
||
}
|
||
|
||
switch (this.value) {
|
||
.ArrayBuffer => {},
|
||
.String => |str| {
|
||
allocator.free(str);
|
||
},
|
||
.Empty => {},
|
||
else => {},
|
||
}
|
||
}
|
||
|
||
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(ctx.ptr());
|
||
|
||
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(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 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,
|
||
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.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(ctx.ptr()).asRef();
|
||
}
|
||
|
||
pub fn getMode(
|
||
_: *Request,
|
||
ctx: js.JSContextRef,
|
||
_: js.JSObjectRef,
|
||
_: js.JSStringRef,
|
||
_: js.ExceptionRef,
|
||
) js.JSValueRef {
|
||
return ZigString.init(Properties.UTF8.navigate).toValueGC(ctx.ptr()).asRef();
|
||
}
|
||
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.request_context.header("Referrer")) |referrer| {
|
||
return ZigString.init(referrer).toValueGC(ctx.ptr()).asRef();
|
||
} else {
|
||
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 {
|
||
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);
|
||
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.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(globalThis, JSValue.fromRef(arguments[0]));
|
||
}
|
||
|
||
if (this.pending_promise) |promise| {
|
||
var status = promise.status(globalThis.vm());
|
||
|
||
if (status == .Pending) {
|
||
VirtualMachine.vm.tick();
|
||
status = promise.status(globalThis.vm());
|
||
}
|
||
|
||
switch (status) {
|
||
.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();
|
||
}
|
||
|
||
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);
|
||
}
|