Merge pull request #1293 from lightpanda-io/v8-json-parse
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled

Use V8 to parse JSON with fetch/xhr
This commit is contained in:
Karl Seguin
2025-12-25 20:47:55 +08:00
committed by GitHub
8 changed files with 48 additions and 32 deletions

View File

@@ -254,17 +254,13 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
self.body_used = true; self.body_used = true;
if (self.body) |body| { if (self.body) |body| {
const p = std.json.parseFromSliceLeaky( const value = js.Value.fromJson(page.js, body) catch |e| {
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" }); log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
return error.SyntaxError; 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); return page.js.resolvePromise(null);
} }

View File

@@ -179,17 +179,13 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
if (self.body) |body| { if (self.body) |body| {
self.body_used = true; self.body_used = true;
const p = std.json.parseFromSliceLeaky( const value = js.Value.fromJson(page.js, body) catch |e| {
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" }); log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
return error.SyntaxError; 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); return page.js.resolvePromise(null);
} }

View File

@@ -14,6 +14,7 @@ const types = @import("types.zig");
const Caller = @import("Caller.zig"); const Caller = @import("Caller.zig");
const NamedFunction = Caller.NamedFunction; const NamedFunction = Caller.NamedFunction;
const PersistentObject = v8.Persistent(v8.Object); const PersistentObject = v8.Persistent(v8.Object);
const PersistentValue = v8.Persistent(v8.Value);
const PersistentModule = v8.Persistent(v8.Module); const PersistentModule = v8.Persistent(v8.Module);
const PersistentPromise = v8.Persistent(v8.Promise); const PersistentPromise = v8.Persistent(v8.Promise);
const PersistentFunction = v8.Persistent(v8.Function); 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. // we now simply persist every time persist() is called.
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty, 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 // Various web APIs depend on having a persistent promise resolver. They
// require for this PromiseResolver to be valid for a lifetime longer than // require for this PromiseResolver to be valid for a lifetime longer than
// the function that resolves/rejects them. // the function that resolves/rejects them.
@@ -149,6 +153,10 @@ pub fn deinit(self: *Context) void {
p.deinit(); p.deinit();
} }
for (self.js_value_list.items) |*p| {
p.deinit();
}
for (self.persisted_promise_resolvers.items) |*p| { for (self.persisted_promise_resolvers.items) |*p| {
p.deinit(); p.deinit();
} }

View File

@@ -148,6 +148,8 @@ pub const Exception = struct {
}; };
pub const Value = struct { pub const Value = struct {
const PersistentValue = v8.Persistent(v8.Value);
value: v8.Value, value: v8.Value,
context: *const Context, context: *const Context,
@@ -161,6 +163,15 @@ pub const Value = struct {
const value = try v8.Json.parse(ctx.v8_context, json_string); const value = try v8.Json.parse(ctx.v8_context, json_string);
return Value{ .context = ctx, .value = value }; 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 { pub const ValueIterator = struct {

View File

@@ -31,6 +31,7 @@ const Mime = @import("../mime.zig").Mime;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Http = @import("../../http/Http.zig"); const Http = @import("../../http/Http.zig");
const js = @import("../js/js.zig");
// XHR interfaces // XHR interfaces
// https://xhr.spec.whatwg.org/#interface-xmlhttprequest // https://xhr.spec.whatwg.org/#interface-xmlhttprequest
@@ -128,21 +129,19 @@ pub const XMLHttpRequest = struct {
JSON, JSON,
}; };
const JSONValue = std.json.Value;
const Response = union(ResponseType) { const Response = union(ResponseType) {
Empty: void, Empty: void,
Text: []const u8, Text: []const u8,
ArrayBuffer: void, ArrayBuffer: void,
Blob: void, Blob: void,
Document: *parser.Document, Document: *parser.Document,
JSON: JSONValue, JSON: js.Value,
}; };
const ResponseObj = union(enum) { const ResponseObj = union(enum) {
Document: *parser.Document, Document: *parser.Document,
Failure: void, Failure: void,
JSON: JSONValue, JSON: js.Value,
fn deinit(self: ResponseObj) void { fn deinit(self: ResponseObj) void {
switch (self) { switch (self) {
@@ -605,7 +604,7 @@ pub const XMLHttpRequest = struct {
} }
// https://xhr.spec.whatwg.org/#the-response-attribute // 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.response_type == .Empty or self.response_type == .Text) {
if (self.state == .loading or self.state == .done) { if (self.state == .loading or self.state == .done) {
return .{ .Text = try self.get_responseText() }; 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 // TODO Let jsonObject be the result of running parse JSON from bytes
// on thiss received bytes. If that threw an exception, then return // on thiss received bytes. If that threw an exception, then return
// null. // null.
self.setResponseObjJSON(); self.setResponseObjJSON(page);
} }
if (self.response_obj) |obj| { if (self.response_obj) |obj| {
@@ -691,22 +690,24 @@ pub const XMLHttpRequest = struct {
}; };
} }
// setResponseObjJSON parses the received bytes as a std.json.Value. // setResponseObjJSON parses the received bytes as a js.Value.
fn setResponseObjJSON(self: *XMLHttpRequest) void { fn setResponseObjJSON(self: *XMLHttpRequest, page: *Page) void {
// TODO should we use parseFromSliceLeaky if we expect the allocator is const value = js.Value.fromJson(
// already an arena? page.js,
const p = std.json.parseFromSliceLeaky(
JSONValue,
self.arena,
self.response_bytes.items, self.response_bytes.items,
.{},
) catch |e| { ) catch |e| {
log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" }); log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" });
self.response_obj = .{ .Failure = {} }; self.response_obj = .{ .Failure = {} };
return; 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 { pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {

View File

@@ -862,7 +862,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
} }
if (std.mem.eql(u8, path, "/xhr/json")) { if (std.mem.eql(u8, path, "/xhr/json")) {
return req.respond("{\"over\":\"9000!!!\"}", .{ return req.respond("{\"over\":\"9000!!!\",\"updated_at\":1765867200000}", .{
.extra_headers = &.{ .extra_headers = &.{
.{ .name = "Content-Type", .value = "application/json" }, .{ .name = "Content-Type", .value = "application/json" },
}, },

View File

@@ -12,7 +12,8 @@
}); });
testing.async(promise1, (json) => { testing.async(promise1, (json) => {
testing.expectEqual({over: '9000!!!'}, json); testing.expectEqual("number", typeof json.updated_at);
testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);
}); });
</script> </script>
@@ -29,6 +30,7 @@
}); });
testing.async(promise1, (json) => { testing.async(promise1, (json) => {
testing.expectEqual({over: '9000!!!'}, json); testing.expectEqual("number", typeof json.updated_at);
testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);
}); });
</script> </script>

View File

@@ -65,6 +65,8 @@
testing.expectEqual(200, req3.status); testing.expectEqual(200, req3.status);
testing.expectEqual('OK', req3.statusText); testing.expectEqual('OK', req3.statusText);
testing.expectEqual('9000!!!', req3.response.over); testing.expectEqual('9000!!!', req3.response.over);
testing.expectEqual("number", typeof req3.response.updated_at);
testing.expectEqual(1765867200000, req3.response.updated_at);
}); });
</script> </script>