diff --git a/src/browser/fetch/Request.zig b/src/browser/fetch/Request.zig index f13a8cb8..6e09a5e3 100644 --- a/src/browser/fetch/Request.zig +++ b/src/browser/fetch/Request.zig @@ -254,17 +254,13 @@ pub fn _json(self: *Response, page: *Page) !js.Promise { self.body_used = true; if (self.body) |body| { - const p = std.json.parseFromSliceLeaky( - std.json.Value, - page.call_arena, - body, - .{}, - ) catch |e| { + const value = js.Value.fromJson(page.js, body) catch |e| { log.info(.browser, "invalid json", .{ .err = e, .source = "Request" }); return error.SyntaxError; }; + const pvalue = try value.persist(page.js); - return page.js.resolvePromise(p); + return page.js.resolvePromise(pvalue); } return page.js.resolvePromise(null); } diff --git a/src/browser/fetch/Response.zig b/src/browser/fetch/Response.zig index 69f1c39e..4932b45e 100644 --- a/src/browser/fetch/Response.zig +++ b/src/browser/fetch/Response.zig @@ -179,17 +179,13 @@ pub fn _json(self: *Response, page: *Page) !js.Promise { if (self.body) |body| { self.body_used = true; - const p = std.json.parseFromSliceLeaky( - std.json.Value, - page.call_arena, - body, - .{}, - ) catch |e| { + const value = js.Value.fromJson(page.js, body) catch |e| { log.info(.browser, "invalid json", .{ .err = e, .source = "Response" }); return error.SyntaxError; }; + const pvalue = try value.persist(page.js); - return page.js.resolvePromise(p); + return page.js.resolvePromise(pvalue); } return page.js.resolvePromise(null); } diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 6ac0dae0..2e577265 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -14,6 +14,7 @@ const types = @import("types.zig"); const Caller = @import("Caller.zig"); const NamedFunction = Caller.NamedFunction; const PersistentObject = v8.Persistent(v8.Object); +const PersistentValue = v8.Persistent(v8.Value); const PersistentModule = v8.Persistent(v8.Module); const PersistentPromise = v8.Persistent(v8.Promise); const PersistentFunction = v8.Persistent(v8.Function); @@ -70,6 +71,9 @@ identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty, // we now simply persist every time persist() is called. js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty, +// js_value_list tracks persisted js values. +js_value_list: std.ArrayListUnmanaged(PersistentValue) = .empty, + // Various web APIs depend on having a persistent promise resolver. They // require for this PromiseResolver to be valid for a lifetime longer than // the function that resolves/rejects them. @@ -149,6 +153,10 @@ pub fn deinit(self: *Context) void { p.deinit(); } + for (self.js_value_list.items) |*p| { + p.deinit(); + } + for (self.persisted_promise_resolvers.items) |*p| { p.deinit(); } diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 1504e846..5a35b100 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -148,6 +148,8 @@ pub const Exception = struct { }; pub const Value = struct { + const PersistentValue = v8.Persistent(v8.Value); + value: v8.Value, context: *const Context, @@ -161,6 +163,15 @@ pub const Value = struct { const value = try v8.Json.parse(ctx.v8_context, json_string); return Value{ .context = ctx, .value = value }; } + + pub fn persist(self: Value, context: *Context) !Value { + const js_value = self.value; + + const persisted = PersistentValue.init(context.isolate, js_value); + try context.js_value_list.append(context.arena, persisted); + + return Value{ .context = context, .value = persisted.toValue() }; + } }; pub const ValueIterator = struct { diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index dfc9ca99..2207a897 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -31,6 +31,7 @@ const Mime = @import("../mime.zig").Mime; const parser = @import("../netsurf.zig"); const Page = @import("../page.zig").Page; const Http = @import("../../http/Http.zig"); +const js = @import("../js/js.zig"); // XHR interfaces // https://xhr.spec.whatwg.org/#interface-xmlhttprequest @@ -128,21 +129,19 @@ pub const XMLHttpRequest = struct { JSON, }; - const JSONValue = std.json.Value; - const Response = union(ResponseType) { Empty: void, Text: []const u8, ArrayBuffer: void, Blob: void, Document: *parser.Document, - JSON: JSONValue, + JSON: js.Value, }; const ResponseObj = union(enum) { Document: *parser.Document, Failure: void, - JSON: JSONValue, + JSON: js.Value, fn deinit(self: ResponseObj) void { switch (self) { @@ -605,7 +604,7 @@ pub const XMLHttpRequest = struct { } // https://xhr.spec.whatwg.org/#the-response-attribute - pub fn get_response(self: *XMLHttpRequest) !?Response { + pub fn get_response(self: *XMLHttpRequest, page: *Page) !?Response { if (self.response_type == .Empty or self.response_type == .Text) { if (self.state == .loading or self.state == .done) { return .{ .Text = try self.get_responseText() }; @@ -652,7 +651,7 @@ pub const XMLHttpRequest = struct { // TODO Let jsonObject be the result of running parse JSON from bytes // on this’s received bytes. If that threw an exception, then return // null. - self.setResponseObjJSON(); + self.setResponseObjJSON(page); } if (self.response_obj) |obj| { @@ -691,22 +690,24 @@ pub const XMLHttpRequest = struct { }; } - // setResponseObjJSON parses the received bytes as a std.json.Value. - fn setResponseObjJSON(self: *XMLHttpRequest) void { - // TODO should we use parseFromSliceLeaky if we expect the allocator is - // already an arena? - const p = std.json.parseFromSliceLeaky( - JSONValue, - self.arena, + // setResponseObjJSON parses the received bytes as a js.Value. + fn setResponseObjJSON(self: *XMLHttpRequest, page: *Page) void { + const value = js.Value.fromJson( + page.js, self.response_bytes.items, - .{}, ) catch |e| { log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" }); self.response_obj = .{ .Failure = {} }; return; }; - self.response_obj = .{ .JSON = p }; + const pvalue = value.persist(page.js) catch |e| { + log.warn(.http, "persist v8 json value", .{ .err = e, .url = self.url, .source = "xhr" }); + self.response_obj = .{ .Failure = {} }; + return; + }; + + self.response_obj = .{ .JSON = pvalue }; } pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { diff --git a/src/main.zig b/src/main.zig index f3f9ec65..cd3be8f3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -862,7 +862,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { } if (std.mem.eql(u8, path, "/xhr/json")) { - return req.respond("{\"over\":\"9000!!!\"}", .{ + return req.respond("{\"over\":\"9000!!!\",\"updated_at\":1765867200000}", .{ .extra_headers = &.{ .{ .name = "Content-Type", .value = "application/json" }, }, diff --git a/src/tests/fetch/fetch.html b/src/tests/fetch/fetch.html index 877f887b..5d25ca54 100644 --- a/src/tests/fetch/fetch.html +++ b/src/tests/fetch/fetch.html @@ -12,7 +12,8 @@ }); testing.async(promise1, (json) => { - testing.expectEqual({over: '9000!!!'}, json); + testing.expectEqual("number", typeof json.updated_at); + testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json); }); @@ -29,6 +30,7 @@ }); testing.async(promise1, (json) => { - testing.expectEqual({over: '9000!!!'}, json); + testing.expectEqual("number", typeof json.updated_at); + testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json); }); diff --git a/src/tests/xhr/xhr.html b/src/tests/xhr/xhr.html index 13ab6216..7afd28c2 100644 --- a/src/tests/xhr/xhr.html +++ b/src/tests/xhr/xhr.html @@ -65,6 +65,8 @@ testing.expectEqual(200, req3.status); testing.expectEqual('OK', req3.statusText); testing.expectEqual('9000!!!', req3.response.over); + testing.expectEqual("number", typeof req3.response.updated_at); + testing.expectEqual(1765867200000, req3.response.updated_at); });