From 2b79a65d6de9979d78deba591807b36dd552f0b7 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 31 Jan 2024 09:46:12 +0100 Subject: [PATCH] xhr: implement async http client --- src/apiweb.zig | 2 + src/run_tests.zig | 2 + src/xhr/xhr.zig | 121 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 117 insertions(+), 8 deletions(-) diff --git a/src/apiweb.zig b/src/apiweb.zig index 40cc8779..a08f9f41 100644 --- a/src/apiweb.zig +++ b/src/apiweb.zig @@ -5,6 +5,7 @@ const Console = @import("jsruntime").Console; const DOM = @import("dom/dom.zig"); const HTML = @import("html/html.zig"); const Events = @import("events/event.zig"); +const XHR = @import("xhr/xhr.zig"); pub const HTMLDocument = @import("html/document.zig").HTMLDocument; @@ -14,4 +15,5 @@ pub const Interfaces = generate.Tuple(.{ DOM.Interfaces, Events.Interfaces, HTML.Interfaces, + XHR.Interfaces, }); diff --git a/src/run_tests.zig b/src/run_tests.zig index 592bc08e..86e09a8f 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -24,6 +24,7 @@ const AttrTestExecFn = @import("dom/attribute.zig").testExecFn; const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn; const EventTestExecFn = @import("events/event.zig").testExecFn; const xhr = @import("xhr/xhr.zig"); +const XHRTestExecFn = xhr.testExecFn; pub const Types = jsruntime.reflect(apiweb.Interfaces); @@ -79,6 +80,7 @@ fn testsAllExecFn( AttrTestExecFn, EventTargetTestExecFn, EventTestExecFn, + XHRTestExecFn, }; inline for (testFns) |testFn| { diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 5abcec12..71db0823 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -1,20 +1,55 @@ const std = @import("std"); +const jsruntime = @import("jsruntime"); +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; const generate = @import("../generate.zig"); -const EventTarget = @import("../dom/event_target.zig").EventTarget; +const EventTarget = @import("../dom/event_target.zig").EventTarget; +const Callback = jsruntime.Callback; const DOMError = @import("../netsurf.zig").DOMError; +const Loop = jsruntime.Loop; +const YieldImpl = Loop.Yield(XMLHttpRequest); +const Client = @import("../async/Client.zig"); + // XHR interfaces // https://xhr.spec.whatwg.org/#interface-xmlhttprequest pub const Interfaces = generate.Tuple(.{ XMLHttpRequestEventTarget, XMLHttpRequestUpload, + XMLHttpRequest, }); pub const XMLHttpRequestEventTarget = struct { pub const prototype = *EventTarget; pub const mem_guarantied = true; + + onloadstart_cbk: ?Callback = null, + onprogress_cbk: ?Callback = null, + onabort_cbk: ?Callback = null, + onload_cbk: ?Callback = null, + ontimeout_cbk: ?Callback = null, + onloadend_cbk: ?Callback = null, + + pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.onloadstart_cbk = handler; + } + pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.onprogress_cbk = handler; + } + pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.onabort = handler; + } + pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.onload = handler; + } + pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.ontimeout = handler; + } + pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.onloadend = handler; + } }; pub const XMLHttpRequestUpload = struct { @@ -26,17 +61,43 @@ pub const XMLHttpRequest = struct { pub const prototype = *XMLHttpRequestEventTarget; pub const mem_guarantied = true; - pub fn constructor() XMLHttpRequest { - return XMLHttpRequest{}; - } - pub const UNSENT: u16 = 0; pub const OPENED: u16 = 1; pub const HEADERS_RECEIVED: u16 = 2; pub const LOADING: u16 = 3; pub const DONE: u16 = 4; + cli: Client, + impl: YieldImpl, + readyState: u16 = UNSENT, + uri: std.Uri, + headers: std.http.Headers, + asyn: bool = true, + err: ?anyerror = null, + + pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !*XMLHttpRequest { + var req = try alloc.create(XMLHttpRequest); + req.* = XMLHttpRequest{ + .headers = .{ .allocator = alloc, .owned = false }, + .impl = undefined, + .uri = undefined, + // TODO retrieve the HTTP client globally to reuse existing connections. + .cli = .{ + .allocator = alloc, + .loop = loop, + }, + }; + req.impl = YieldImpl.init(loop, req); + return req; + } + + pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { + self.headers.deinit(); + // TODO the client must be shared between requests. + self.cli.deinit(); + alloc.destroy(self); + } pub fn get_readyState(self: *XMLHttpRequest) u16 { return self.readyState; @@ -50,9 +111,6 @@ pub const XMLHttpRequest = struct { username: ?[]const u8, password: ?[]const u8, ) !void { - _ = self; - _ = url; - _ = asyn; _ = username; _ = password; @@ -61,6 +119,9 @@ pub const XMLHttpRequest = struct { // "InvalidStateError" DOMException. try validMethod(method); + + self.uri = try std.Uri.parse(url); + self.asyn = if (asyn) |b| b else true; } const methods = [_][]const u8{ "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT" }; @@ -82,4 +143,48 @@ pub const XMLHttpRequest = struct { // If method is not a method, then throw a "SyntaxError" DOMException. return DOMError.Syntax; } + + pub fn _send(self: *XMLHttpRequest) void { + self.impl.yield(); + } + + fn onerr(self: *XMLHttpRequest, err: anyerror) void { + self.err = err; + self.readyState = DONE; + } + + pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void { + if (err) |e| return self.onerr(e); + var req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e); + defer req.deinit(); + + self.readyState = OPENED; + + req.send(.{}) catch |e| return self.onerr(e); + req.finish() catch |e| return self.onerr(e); + req.wait() catch |e| return self.onerr(e); + self.readyState = HEADERS_RECEIVED; + self.readyState = LOADING; + self.readyState = DONE; + } }; + +pub fn testExecFn( + _: std.mem.Allocator, + js_env: *jsruntime.Env, +) anyerror!void { + var send = [_]Case{ + .{ .src = + \\var nb = 0; var evt; + \\function cbk(event) { + \\ evt = event; + \\ nb ++; + \\} + , .ex = "undefined" }, + .{ .src = "const req = new XMLHttpRequest();", .ex = "undefined" }, + .{ .src = "req.onload = cbk; true;", .ex = "true" }, + .{ .src = "req.open('GET', 'https://w3.org');", .ex = "undefined" }, + .{ .src = "req.send();", .ex = "undefined" }, + }; + try checkCases(js_env, &send); +}