diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml
index ac054c8f..0c4069fb 100644
--- a/.github/actions/install/action.yml
+++ b/.github/actions/install/action.yml
@@ -13,7 +13,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
- default: 'v0.3.3'
+ default: 'v0.3.4'
v8:
description: 'v8 version to install'
required: false
diff --git a/Dockerfile b/Dockerfile
index f106905a..f5cd202d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,7 +3,7 @@ FROM debian:stable-slim
ARG MINISIG=0.12
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4
-ARG ZIG_V8=v0.3.3
+ARG ZIG_V8=v0.3.4
ARG TARGETPLATFORM
RUN apt-get update -yq && \
diff --git a/build.zig.zon b/build.zig.zon
index 9a28408b..cee52057 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/refs/tags/v0.3.4.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 9f7a22a1..db47d800 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 404a8bc4..73b6b26e 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 70af9d24..46fca6a9 100644
--- a/src/browser/js/Context.zig
+++ b/src/browser/js/Context.zig
@@ -255,6 +255,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, .{});