Merge pull request #1817 from lightpanda-io/frames_postMessage

window.postMessage across frames
This commit is contained in:
Karl Seguin
2026-03-17 06:42:32 +08:00
committed by GitHub
17 changed files with 147 additions and 68 deletions

View File

@@ -13,7 +13,7 @@ inputs:
zig-v8: zig-v8:
description: 'zig v8 version to install' description: 'zig v8 version to install'
required: false required: false
default: 'v0.3.3' default: 'v0.3.4'
v8: v8:
description: 'v8 version to install' description: 'v8 version to install'
required: false required: false

View File

@@ -3,7 +3,7 @@ FROM debian:stable-slim
ARG MINISIG=0.12 ARG MINISIG=0.12
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4 ARG V8=14.0.365.4
ARG ZIG_V8=v0.3.3 ARG ZIG_V8=v0.3.4
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apt-get update -yq && \ RUN apt-get update -yq && \

View File

@@ -5,8 +5,8 @@
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.15.2",
.dependencies = .{ .dependencies = .{
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.3.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.4.tar.gz",
.hash = "v8-0.0.0-xddH6yx3BAAGD9jSoq_ttt_bk9MectTU44s_HZxxE5LD", .hash = "v8-0.0.0-xddH6_F3BAAiFvKY6R1H-gkuQlk19BkDQ0--uZuTrSup",
}, },
// .v8 = .{ .path = "../zig-v8-fork" }, // .v8 = .{ .path = "../zig-v8-fork" },
.brotli = .{ .brotli = .{

View File

@@ -91,25 +91,32 @@ pub fn runMicrotasks(self: *Browser) void {
self.env.runMicrotasks(); self.env.runMicrotasks();
} }
pub fn runMacrotasks(self: *Browser) !?u64 { pub fn runMacrotasks(self: *Browser) !void {
const env = &self.env; const env = &self.env;
const time_to_next = try self.env.runMacrotasks(); try self.env.runMacrotasks();
env.pumpMessageLoop(); env.pumpMessageLoop();
// either of the above could have queued more microtasks // either of the above could have queued more microtasks
env.runMicrotasks(); env.runMicrotasks();
return time_to_next;
} }
pub fn hasBackgroundTasks(self: *Browser) bool { pub fn hasBackgroundTasks(self: *Browser) bool {
return self.env.hasBackgroundTasks(); return self.env.hasBackgroundTasks();
} }
pub fn waitForBackgroundTasks(self: *Browser) void { pub fn waitForBackgroundTasks(self: *Browser) void {
self.env.waitForBackgroundTasks(); 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 { pub fn runIdleTasks(self: *const Browser) void {
self.env.runIdleTasks(); self.env.runIdleTasks();
} }

View File

@@ -709,11 +709,14 @@ pub fn scriptsCompletedLoading(self: *Page) void {
} }
pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void { pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void {
blk: { var ls: JS.Local.Scope = undefined;
var ls: JS.Local.Scope = undefined; self.js.localScope(&ls);
self.js.localScope(&ls); defer ls.deinit();
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| { const event = Event.initTrusted(comptime .wrap("load"), .{}, self) catch |err| {
log.err(.page, "iframe event init", .{ .err = err, .url = iframe._src }); log.err(.page, "iframe event init", .{ .err = err, .url = iframe._src });
break :blk; break :blk;
@@ -722,6 +725,7 @@ pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void {
log.warn(.js, "iframe onload", .{ .err = err, .url = iframe._src }); log.warn(.js, "iframe onload", .{ .err = err, .url = iframe._src });
}; };
} }
self.pendingLoadCompleted(); self.pendingLoadCompleted();
} }

View File

@@ -401,7 +401,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
// scheduler.run could trigger new http transfers, so do not // scheduler.run could trigger new http transfers, so do not
// store http_client.active BEFORE this call and then use // store http_client.active BEFORE this call and then use
// it AFTER. // it AFTER.
const ms_to_next_task = try browser.runMacrotasks(); try browser.runMacrotasks();
// Each call to this runs scheduled load events. // Each call to this runs scheduled load events.
try page.dispatchLoad(); try page.dispatchLoad();
@@ -423,16 +423,16 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
std.debug.assert(http_client.intercepted == 0); std.debug.assert(http_client.intercepted == 0);
} }
var ms: u64 = ms_to_next_task orelse blk: { var ms = blk: {
if (wait_ms - ms_remaining < 100) { // if (wait_ms - ms_remaining < 100) {
if (comptime builtin.is_test) { // if (comptime builtin.is_test) {
return .done; // return .done;
} // }
// Look, we want to exit ASAP, but we don't want // // Look, we want to exit ASAP, but we don't want
// to exit so fast that we've run none of the // // to exit so fast that we've run none of the
// background jobs. // // background jobs.
break :blk 50; // break :blk 50;
} // }
if (browser.hasBackgroundTasks()) { if (browser.hasBackgroundTasks()) {
// _we_ have nothing to run, but v8 is working on // _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; break :blk 20;
} }
// No http transfers, no cdp extra socket, no break :blk browser.msToNextMacrotask() orelse return .done;
// scheduled tasks, we're done.
return .done;
}; };
if (ms > ms_remaining) { 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 // We're here because we either have active HTTP
// connections, or exit_when_done == false (aka, there's // connections, or exit_when_done == false (aka, there's
// an cdp_socket registered with the http client). // an cdp_socket registered with the http client).
// We should continue to run lowPriority tasks, so we // We should continue to run tasks, so we minimize how long
// minimize how long we'll poll for network I/O. // we'll poll for network I/O.
var ms_to_wait = @min(200, ms_to_next_task orelse 200); var ms_to_wait = @min(200, browser.msToNextMacrotask() orelse 200);
if (ms_to_wait > 10 and browser.hasBackgroundTasks()) { if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {
// if we have background tasks, we don't want to wait too // if we have background tasks, we don't want to wait too
// long for a message from the client. We want to go back // long for a message from the client. We want to go back

View File

@@ -255,6 +255,10 @@ pub fn toLocal(self: *Context, global: anytype) js.Local.ToLocalReturnType(@Type
return l.toLocal(global); return l.toLocal(global);
} }
pub fn getIncumbent(self: *Context) *Page {
return fromC(v8.v8__Isolate__GetIncumbentContext(self.env.isolate.handle).?).page;
}
pub fn stringToPersistedFunction( pub fn stringToPersistedFunction(
self: *Context, self: *Context,
function_body: []const u8, function_body: []const u8,

View File

@@ -382,8 +382,7 @@ pub fn runMicrotasks(self: *Env) void {
} }
} }
pub fn runMacrotasks(self: *Env) !?u64 { pub fn runMacrotasks(self: *Env) !void {
var ms_to_next_task: ?u64 = null;
for (self.contexts[0..self.context_count]) |ctx| { for (self.contexts[0..self.context_count]) |ctx| {
if (comptime builtin.is_test == false) { if (comptime builtin.is_test == false) {
// I hate this comptime check as much as you do. But we have tests // 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; var hs: js.HandleScope = undefined;
const entered = ctx.enter(&hs); const entered = ctx.enter(&hs);
defer entered.exit(); defer entered.exit();
try ctx.scheduler.run();
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;
}
} }
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 { pub fn pumpMessageLoop(self: *const Env) void {

View File

@@ -74,9 +74,10 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts
}); });
} }
pub fn run(self: *Scheduler) !?u64 { pub fn run(self: *Scheduler) !void {
_ = try self.runQueue(&self.low_priority); const now = milliTimestamp(.monotonic);
return self.runQueue(&self.high_priority); try self.runQueue(&self.low_priority, now);
try self.runQueue(&self.high_priority, now);
} }
pub fn hasReadyTasks(self: *Scheduler) bool { 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); return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now);
} }
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 { pub fn msToNextHigh(self: *Scheduler) ?u64 {
if (queue.count() == 0) { const task = self.high_priority.peek() orelse return null;
return null;
}
const now = milliTimestamp(.monotonic); 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_| { while (queue.peek()) |*task_| {
if (task_.run_at > now) { if (task_.run_at > now) {
return @intCast(task_.run_at - now); return;
} }
var task = queue.remove(); var task = queue.remove();
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
@@ -114,7 +122,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
try self.low_priority.add(task); try self.low_priority.add(task);
} }
} }
return null; return;
} }
fn queueuHasReadyTask(queue: *Queue, now: u64) bool { fn queueuHasReadyTask(queue: *Queue, now: u64) bool {

View File

@@ -12,7 +12,7 @@
testing.expectEqual('', $('#a0').href); testing.expectEqual('', $('#a0').href);
testing.expectEqual(testing.BASE_URL + 'element/anchor1.html', $('#a1').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('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href);
testing.expectEqual(testing.BASE_URL + 'element/html/foo', $('#link').href); testing.expectEqual(testing.BASE_URL + 'element/html/foo', $('#link').href);

View File

@@ -32,7 +32,7 @@
testing.expectEqual(testing.BASE_URL + 'element/html/hello', form.action) testing.expectEqual(testing.BASE_URL + 'element/html/hello', form.action)
form.action = '/hello'; form.action = '/hello';
testing.expectEqual(testing.ORIGIN + 'hello', form.action) testing.expectEqual(testing.ORIGIN + '/hello', form.action)
form.action = 'https://lightpanda.io/hello'; form.action = 'https://lightpanda.io/hello';
testing.expectEqual('https://lightpanda.io/hello', form.action) testing.expectEqual('https://lightpanda.io/hello', form.action)

View File

@@ -37,7 +37,7 @@
testing.expectEqual('test.png', img.getAttribute('src')); testing.expectEqual('test.png', img.getAttribute('src'));
img.src = '/absolute/path.png'; 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')); testing.expectEqual('/absolute/path.png', img.getAttribute('src'));
img.src = 'https://example.com/image.png'; img.src = 'https://example.com/image.png';

View File

@@ -8,7 +8,7 @@
testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href); testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);
l2.href = '/over/9000'; l2.href = '/over/9000';
testing.expectEqual(testing.ORIGIN + 'over/9000', l2.href); testing.expectEqual(testing.ORIGIN + '/over/9000', l2.href);
l2.crossOrigin = 'nope'; l2.crossOrigin = 'nope';
testing.expectEqual('anonymous', l2.crossOrigin); testing.expectEqual('anonymous', l2.crossOrigin);

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<iframe id="receiver"></iframe>
<script id="messages">
{
let reply = null;
window.addEventListener('message', (e) => {
console.warn('reply')
reply = e.data;
});
const iframe = $('#receiver');
iframe.src = 'support/message_receiver.html';
iframe.addEventListener('load', () => {
iframe.contentWindow.postMessage('ping', '*');
});
testing.eventually(() => {
testing.expectEqual('pong', reply.data);
testing.expectEqual(testing.ORIGIN, reply.origin);
});
}
</script>

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<script>
window.addEventListener('message', (e) => {
console.warn('Frame Message', e.data);
if (e.data === 'ping') {
window.top.postMessage({data: 'pong', origin: e.origin}, '*');
}
});
</script>

View File

@@ -114,7 +114,7 @@
eventually: eventually, eventually: eventually,
IS_TEST_RUNNER: IS_TEST_RUNNER, IS_TEST_RUNNER: IS_TEST_RUNNER,
HOST: '127.0.0.1', 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/', BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/',
}; };
@@ -124,7 +124,7 @@
// seemless, namely around adapting paths/urls. // seemless, namely around adapting paths/urls.
console.warn(`The page is not being executed in the test runner, certain behavior has been adjusted`); console.warn(`The page is not being executed in the test runner, certain behavior has been adjusted`);
window.testing.HOST = location.hostname; 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.testing.BASE_URL = location.origin + '/src/browser/tests/';
window.addEventListener('load', testing.assertOk); window.addEventListener('load', testing.assertOk);
} }

View File

@@ -66,6 +66,7 @@ _on_load: ?js.Function.Global = null,
_on_pageshow: ?js.Function.Global = null, _on_pageshow: ?js.Function.Global = null,
_on_popstate: ?js.Function.Global = null, _on_popstate: ?js.Function.Global = null,
_on_error: ?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 _on_unhandled_rejection: ?js.Function.Global = null, // TODO: invoke on error
_location: *Location, _location: *Location,
_timer_id: u30 = 0, _timer_id: u30 = 0,
@@ -208,6 +209,14 @@ pub fn setOnError(self: *Window, setter: ?FunctionSetter) void {
self._on_error = getFunctionFromSetter(setter); 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 { pub fn getOnUnhandledRejection(self: *const Window) ?js.Function.Global {
return self._on_unhandled_rejection; 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 // In a full implementation, we would validate the origin
_ = target_origin; _ = target_origin;
// postMessage queues a task (not a microtask), so use the scheduler // self = the window that will get the message
const arena = try page.getArena(.{ .debug = "Window.schedule" }); // page = the context calling postMessage
errdefer page.releaseArena(arena); 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); const callback = try arena.create(PostMessageCallback);
callback.* = .{ callback.* = .{
.page = page,
.arena = arena, .arena = arena,
.message = message, .message = message,
.page = target_page,
.source = source_window,
.origin = try arena.dupe(u8, origin), .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", .name = "postMessage",
.low_priority = false, .low_priority = false,
.finalizer = PostMessageCallback.cancelled, .finalizer = PostMessageCallback.cancelled,
@@ -702,6 +718,7 @@ const ScheduleCallback = struct {
const PostMessageCallback = struct { const PostMessageCallback = struct {
page: *Page, page: *Page,
source: *Window,
arena: Allocator, arena: Allocator,
origin: []const u8, origin: []const u8,
message: js.Value.Temp, message: js.Value.Temp,
@@ -712,7 +729,7 @@ const PostMessageCallback = struct {
fn cancelled(ctx: *anyopaque) void { fn cancelled(ctx: *anyopaque) void {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx)); const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
self.page.releaseArena(self.arena); self.deinit();
} }
fn run(ctx: *anyopaque) !?u32 { fn run(ctx: *anyopaque) !?u32 {
@@ -722,14 +739,17 @@ const PostMessageCallback = struct {
const page = self.page; const page = self.page;
const window = page.window; const window = page.window;
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{ const event_target = window.asEventTarget();
.data = self.message, if (page._event_manager.hasDirectListeners(event_target, "message", window._on_message)) {
.origin = self.origin, const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
.source = window, .data = self.message,
.bubbles = false, .origin = self.origin,
.cancelable = false, .source = self.source,
}, page)).asEvent(); .bubbles = false,
try page._event_manager.dispatch(window.asEventTarget(), event); .cancelable = false,
}, page)).asEvent();
try page._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" });
}
return null; return null;
} }
@@ -783,6 +803,7 @@ pub const JsApi = struct {
pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{}); pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{}); pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{}); 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 onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
pub const fetch = bridge.function(Window.fetch, .{}); pub const fetch = bridge.function(Window.fetch, .{});
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{}); pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});