diff --git a/build.zig.zon b/build.zig.zon index 9a28408b..33fc0a09 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.3.tar.gz", - .hash = "v8-0.0.0-xddH6yx3BAAGD9jSoq_ttt_bk9MectTU44s_HZxxE5LD", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/34cb5f50227047c6cc1b2af73dad958c267f0a83.tar.gz", + .hash = "v8-0.0.0-xddH6_F3BAAiFvKY6R1H-gkuQlk19BkDQ0--uZuTrSup", }, // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{ diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig index 8f8c4aa2..50a7c037 100644 --- a/src/browser/Browser.zig +++ b/src/browser/Browser.zig @@ -91,25 +91,32 @@ pub fn runMicrotasks(self: *Browser) void { self.env.runMicrotasks(); } -pub fn runMacrotasks(self: *Browser) !?u64 { +pub fn runMacrotasks(self: *Browser) !void { const env = &self.env; - const time_to_next = try self.env.runMacrotasks(); + try self.env.runMacrotasks(); env.pumpMessageLoop(); // either of the above could have queued more microtasks env.runMicrotasks(); - - return time_to_next; } pub fn hasBackgroundTasks(self: *Browser) bool { return self.env.hasBackgroundTasks(); } + pub fn waitForBackgroundTasks(self: *Browser) void { self.env.waitForBackgroundTasks(); } +pub fn msToNextMacrotask(self: *Browser) ?u64 { + return self.env.msToNextMacrotask(); +} + +pub fn msTo(self: *Browser) bool { + return self.env.hasBackgroundTasks(); +} + pub fn runIdleTasks(self: *const Browser) void { self.env.runIdleTasks(); } diff --git a/src/browser/Page.zig b/src/browser/Page.zig index cb62cb31..b290b862 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -709,11 +709,14 @@ pub fn scriptsCompletedLoading(self: *Page) void { } pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void { - blk: { - var ls: JS.Local.Scope = undefined; - self.js.localScope(&ls); - defer ls.deinit(); + var ls: JS.Local.Scope = undefined; + self.js.localScope(&ls); + defer ls.deinit(); + const entered = self.js.enter(&ls.handle_scope); + defer entered.exit(); + + blk: { const event = Event.initTrusted(comptime .wrap("load"), .{}, self) catch |err| { log.err(.page, "iframe event init", .{ .err = err, .url = iframe._src }); break :blk; @@ -722,6 +725,7 @@ pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void { log.warn(.js, "iframe onload", .{ .err = err, .url = iframe._src }); }; } + self.pendingLoadCompleted(); } diff --git a/src/browser/Session.zig b/src/browser/Session.zig index fea56a87..4f605ec0 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -401,7 +401,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult { // scheduler.run could trigger new http transfers, so do not // store http_client.active BEFORE this call and then use // it AFTER. - const ms_to_next_task = try browser.runMacrotasks(); + try browser.runMacrotasks(); // Each call to this runs scheduled load events. try page.dispatchLoad(); @@ -423,16 +423,16 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult { std.debug.assert(http_client.intercepted == 0); } - var ms: u64 = ms_to_next_task orelse blk: { - if (wait_ms - ms_remaining < 100) { - if (comptime builtin.is_test) { - return .done; - } - // Look, we want to exit ASAP, but we don't want - // to exit so fast that we've run none of the - // background jobs. - break :blk 50; - } + var ms = blk: { + // if (wait_ms - ms_remaining < 100) { + // if (comptime builtin.is_test) { + // return .done; + // } + // // Look, we want to exit ASAP, but we don't want + // // to exit so fast that we've run none of the + // // background jobs. + // break :blk 50; + // } if (browser.hasBackgroundTasks()) { // _we_ have nothing to run, but v8 is working on @@ -441,9 +441,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult { break :blk 20; } - // No http transfers, no cdp extra socket, no - // scheduled tasks, we're done. - return .done; + break :blk browser.msToNextMacrotask() orelse return .done; }; if (ms > ms_remaining) { @@ -470,9 +468,9 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult { // We're here because we either have active HTTP // connections, or exit_when_done == false (aka, there's // an cdp_socket registered with the http client). - // We should continue to run lowPriority tasks, so we - // minimize how long we'll poll for network I/O. - var ms_to_wait = @min(200, ms_to_next_task orelse 200); + // We should continue to run tasks, so we minimize how long + // we'll poll for network I/O. + var ms_to_wait = @min(200, browser.msToNextMacrotask() orelse 200); if (ms_to_wait > 10 and browser.hasBackgroundTasks()) { // if we have background tasks, we don't want to wait too // long for a message from the client. We want to go back diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 5c58c5cb..a972b6c7 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -252,6 +252,10 @@ pub fn toLocal(self: *Context, global: anytype) js.Local.ToLocalReturnType(@Type return l.toLocal(global); } +pub fn getIncumbent(self: *Context) *Page { + return fromC(v8.v8__Isolate__GetIncumbentContext(self.env.isolate.handle).?).page; +} + pub fn stringToPersistedFunction( self: *Context, function_body: []const u8, diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index ba2e3e5a..1ac9e6b3 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -382,8 +382,7 @@ pub fn runMicrotasks(self: *Env) void { } } -pub fn runMacrotasks(self: *Env) !?u64 { - var ms_to_next_task: ?u64 = null; +pub fn runMacrotasks(self: *Env) !void { for (self.contexts[0..self.context_count]) |ctx| { if (comptime builtin.is_test == false) { // I hate this comptime check as much as you do. But we have tests @@ -398,13 +397,17 @@ pub fn runMacrotasks(self: *Env) !?u64 { var hs: js.HandleScope = undefined; const entered = ctx.enter(&hs); defer entered.exit(); - - const ms = (try ctx.scheduler.run()) orelse continue; - if (ms_to_next_task == null or ms < ms_to_next_task.?) { - ms_to_next_task = ms; - } + try ctx.scheduler.run(); } - return ms_to_next_task; +} + +pub fn msToNextMacrotask(self: *Env) ?u64 { + var next_task: u64 = std.math.maxInt(u64); + for (self.contexts[0..self.context_count]) |ctx| { + const candidate = ctx.scheduler.msToNextHigh() orelse continue; + next_task = @min(candidate, next_task); + } + return if (next_task == std.math.maxInt(u64)) null else next_task; } pub fn pumpMessageLoop(self: *const Env) void { diff --git a/src/browser/js/Scheduler.zig b/src/browser/js/Scheduler.zig index e667a872..322351f3 100644 --- a/src/browser/js/Scheduler.zig +++ b/src/browser/js/Scheduler.zig @@ -74,9 +74,10 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts }); } -pub fn run(self: *Scheduler) !?u64 { - _ = try self.runQueue(&self.low_priority); - return self.runQueue(&self.high_priority); +pub fn run(self: *Scheduler) !void { + const now = milliTimestamp(.monotonic); + try self.runQueue(&self.low_priority, now); + try self.runQueue(&self.high_priority, now); } pub fn hasReadyTasks(self: *Scheduler) bool { @@ -84,16 +85,23 @@ pub fn hasReadyTasks(self: *Scheduler) bool { return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now); } -fn runQueue(self: *Scheduler, queue: *Queue) !?u64 { - if (queue.count() == 0) { - return null; - } - +pub fn msToNextHigh(self: *Scheduler) ?u64 { + const task = self.high_priority.peek() orelse return null; const now = milliTimestamp(.monotonic); + if (task.run_at <= now) { + return 0; + } + return @intCast(task.run_at - now); +} + +fn runQueue(self: *Scheduler, queue: *Queue, now: u64) !void { + if (queue.count() == 0) { + return; + } while (queue.peek()) |*task_| { if (task_.run_at > now) { - return @intCast(task_.run_at - now); + return; } var task = queue.remove(); if (comptime IS_DEBUG) { @@ -114,7 +122,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 { try self.low_priority.add(task); } } - return null; + return; } fn queueuHasReadyTask(queue: *Queue, now: u64) bool { diff --git a/src/browser/tests/element/html/anchor.html b/src/browser/tests/element/html/anchor.html index 0522163f..74bf486c 100644 --- a/src/browser/tests/element/html/anchor.html +++ b/src/browser/tests/element/html/anchor.html @@ -12,7 +12,7 @@ testing.expectEqual('', $('#a0').href); testing.expectEqual(testing.BASE_URL + 'element/anchor1.html', $('#a1').href); - testing.expectEqual(testing.ORIGIN + 'hello/world/anchor2.html', $('#a2').href); + testing.expectEqual(testing.ORIGIN + '/hello/world/anchor2.html', $('#a2').href); testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href); testing.expectEqual(testing.BASE_URL + 'element/html/foo', $('#link').href); diff --git a/src/browser/tests/element/html/form.html b/src/browser/tests/element/html/form.html index f62cb221..17743135 100644 --- a/src/browser/tests/element/html/form.html +++ b/src/browser/tests/element/html/form.html @@ -32,7 +32,7 @@ testing.expectEqual(testing.BASE_URL + 'element/html/hello', form.action) form.action = '/hello'; - testing.expectEqual(testing.ORIGIN + 'hello', form.action) + testing.expectEqual(testing.ORIGIN + '/hello', form.action) form.action = 'https://lightpanda.io/hello'; testing.expectEqual('https://lightpanda.io/hello', form.action) diff --git a/src/browser/tests/element/html/image.html b/src/browser/tests/element/html/image.html index 92cd947d..baa09918 100644 --- a/src/browser/tests/element/html/image.html +++ b/src/browser/tests/element/html/image.html @@ -37,7 +37,7 @@ testing.expectEqual('test.png', img.getAttribute('src')); img.src = '/absolute/path.png'; - testing.expectEqual(testing.ORIGIN + 'absolute/path.png', img.src); + testing.expectEqual(testing.ORIGIN + '/absolute/path.png', img.src); testing.expectEqual('/absolute/path.png', img.getAttribute('src')); img.src = 'https://example.com/image.png'; diff --git a/src/browser/tests/element/html/link.html b/src/browser/tests/element/html/link.html index bed5e6ab..4d967e37 100644 --- a/src/browser/tests/element/html/link.html +++ b/src/browser/tests/element/html/link.html @@ -8,7 +8,7 @@ testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href); l2.href = '/over/9000'; - testing.expectEqual(testing.ORIGIN + 'over/9000', l2.href); + testing.expectEqual(testing.ORIGIN + '/over/9000', l2.href); l2.crossOrigin = 'nope'; testing.expectEqual('anonymous', l2.crossOrigin); diff --git a/src/browser/tests/frames/post_message.html b/src/browser/tests/frames/post_message.html new file mode 100644 index 00000000..6d206b74 --- /dev/null +++ b/src/browser/tests/frames/post_message.html @@ -0,0 +1,25 @@ + + + + + + diff --git a/src/browser/tests/frames/support/message_receiver.html b/src/browser/tests/frames/support/message_receiver.html new file mode 100644 index 00000000..55612a7c --- /dev/null +++ b/src/browser/tests/frames/support/message_receiver.html @@ -0,0 +1,9 @@ + + diff --git a/src/browser/tests/testing.js b/src/browser/tests/testing.js index 987ba042..01bb19db 100644 --- a/src/browser/tests/testing.js +++ b/src/browser/tests/testing.js @@ -114,7 +114,7 @@ eventually: eventually, IS_TEST_RUNNER: IS_TEST_RUNNER, HOST: '127.0.0.1', - ORIGIN: 'http://127.0.0.1:9582/', + ORIGIN: 'http://127.0.0.1:9582', BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/', }; @@ -124,7 +124,7 @@ // seemless, namely around adapting paths/urls. console.warn(`The page is not being executed in the test runner, certain behavior has been adjusted`); window.testing.HOST = location.hostname; - window.testing.ORIGIN = location.origin + '/'; + window.testing.ORIGIN = location.origin; window.testing.BASE_URL = location.origin + '/src/browser/tests/'; window.addEventListener('load', testing.assertOk); } diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 0f288398..099cad65 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -66,6 +66,7 @@ _on_load: ?js.Function.Global = null, _on_pageshow: ?js.Function.Global = null, _on_popstate: ?js.Function.Global = null, _on_error: ?js.Function.Global = null, +_on_message: ?js.Function.Global = null, _on_unhandled_rejection: ?js.Function.Global = null, // TODO: invoke on error _location: *Location, _timer_id: u30 = 0, @@ -208,6 +209,14 @@ pub fn setOnError(self: *Window, setter: ?FunctionSetter) void { self._on_error = getFunctionFromSetter(setter); } +pub fn getOnMessage(self: *const Window) ?js.Function.Global { + return self._on_message; +} + +pub fn setOnMessage(self: *Window, setter: ?FunctionSetter) void { + self._on_message = getFunctionFromSetter(setter); +} + pub fn getOnUnhandledRejection(self: *const Window) ?js.Function.Global { return self._on_unhandled_rejection; } @@ -369,19 +378,26 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons // In a full implementation, we would validate the origin _ = target_origin; - // postMessage queues a task (not a microtask), so use the scheduler - const arena = try page.getArena(.{ .debug = "Window.schedule" }); - errdefer page.releaseArena(arena); + // self = the window that will get the message + // page = the context calling postMessage + const target_page = self._page; + const source_window = target_page.js.getIncumbent().window; - const origin = try self._location.getOrigin(page); + const arena = try target_page.getArena(.{ .debug = "Window.postMessage" }); + errdefer target_page.releaseArena(arena); + + // Origin should be the source window's origin (where the message came from) + const origin = try source_window._location.getOrigin(page); const callback = try arena.create(PostMessageCallback); callback.* = .{ - .page = page, .arena = arena, .message = message, + .page = target_page, + .source = source_window, .origin = try arena.dupe(u8, origin), }; - try page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{ + + try target_page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{ .name = "postMessage", .low_priority = false, .finalizer = PostMessageCallback.cancelled, @@ -702,6 +718,7 @@ const ScheduleCallback = struct { const PostMessageCallback = struct { page: *Page, + source: *Window, arena: Allocator, origin: []const u8, message: js.Value.Temp, @@ -712,7 +729,7 @@ const PostMessageCallback = struct { fn cancelled(ctx: *anyopaque) void { const self: *PostMessageCallback = @ptrCast(@alignCast(ctx)); - self.page.releaseArena(self.arena); + self.deinit(); } fn run(ctx: *anyopaque) !?u32 { @@ -722,14 +739,17 @@ const PostMessageCallback = struct { const page = self.page; const window = page.window; - const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{ - .data = self.message, - .origin = self.origin, - .source = window, - .bubbles = false, - .cancelable = false, - }, page)).asEvent(); - try page._event_manager.dispatch(window.asEventTarget(), event); + const event_target = window.asEventTarget(); + if (page._event_manager.hasDirectListeners(event_target, "message", window._on_message)) { + const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{ + .data = self.message, + .origin = self.origin, + .source = self.source, + .bubbles = false, + .cancelable = false, + }, page)).asEvent(); + try page._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" }); + } return null; } @@ -783,6 +803,7 @@ pub const JsApi = struct { pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{}); pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{}); pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{}); + pub const onmessage = bridge.accessor(Window.getOnMessage, Window.setOnMessage, .{}); pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{}); pub const fetch = bridge.function(Window.fetch, .{}); pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});