diff --git a/build.zig.zon b/build.zig.zon
index eb3812a8..b7f9cf3b 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -5,10 +5,10 @@
.minimum_zig_version = "0.15.2",
.dependencies = .{
.v8 = .{
- .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.1.tar.gz",
- .hash = "v8-0.0.0-xddH64J7BAC81mkf6G9RbEJxS-W3TIRl5iFnShwbqCqy",
+ .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.2.tar.gz",
+ .hash = "v8-0.0.0-xddH6wx-BABNgL7YIDgbnFgKZuXZ68yZNngNSrV6OjrY",
},
- //.v8 = .{ .path = "../zig-v8-fork" },
+ // .v8 = .{ .path = "../zig-v8-fork" },
.brotli = .{
// v1.2.0
.url = "https://github.com/google/brotli/archive/028fb5a23661f123017c060daa546b55cf4bde29.tar.gz",
diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index 47c63904..9b05c39b 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -190,6 +190,8 @@ _queued_navigation: ?*QueuedNavigation = null,
// The URL of the current page
url: [:0]const u8 = "about:blank",
+origin: ?[]const u8 = null,
+
// The base url specifies the base URL used to resolve the relative urls.
// It is set by a tag.
// If null the url must be used.
@@ -388,10 +390,6 @@ pub fn getTitle(self: *Page) !?[]const u8 {
return null;
}
-pub fn getOrigin(self: *Page, allocator: Allocator) !?[]const u8 {
- return try URL.getOrigin(allocator, self.url);
-}
-
// Add comon headers for a request:
// * cookies
// * referer
@@ -449,7 +447,7 @@ pub fn releaseArena(self: *Page, allocator: Allocator) void {
}
pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
- const current_origin = (try URL.getOrigin(self.call_arena, self.url)) orelse return false;
+ const current_origin = self.origin orelse return false;
return std.mem.startsWith(u8, url, current_origin);
}
@@ -472,6 +470,14 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
// page and dispatch the events.
if (std.mem.eql(u8, "about:blank", request_url)) {
self.url = "about:blank";
+
+ if (self.parent) |parent| {
+ self.origin = parent.origin;
+ } else {
+ self.origin = null;
+ }
+ try self.js.setOrigin(self.origin);
+
// Assume we parsed the document.
// It's important to force a reset during the following navigation.
self._parse_state = .complete;
@@ -518,6 +524,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
var http_client = session.browser.http_client;
self.url = try self.arena.dupeZ(u8, request_url);
+ self.origin = try URL.getOrigin(self.arena, self.url);
self._req_id = req_id;
self._navigated_options = .{
@@ -825,9 +832,15 @@ fn notifyParentLoadComplete(self: *Page) void {
fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
var self: *Page = @ptrCast(@alignCast(transfer.ctx));
- // would be different than self.url in the case of a redirect
const header = &transfer.response_header.?;
- self.url = try self.arena.dupeZ(u8, std.mem.span(header.url));
+
+ const response_url = std.mem.span(header.url);
+ if (std.mem.eql(u8, response_url, self.url) == false) {
+ // would be different than self.url in the case of a redirect
+ self.url = try self.arena.dupeZ(u8, response_url);
+ self.origin = try URL.getOrigin(self.arena, self.url);
+ }
+ try self.js.setOrigin(self.origin);
self.window._location = try Location.init(self.url, self);
self.document._location = self.window._location;
diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig
index 9180223c..065b5e28 100644
--- a/src/browser/js/Context.zig
+++ b/src/browser/js/Context.zig
@@ -23,6 +23,7 @@ const log = @import("../../log.zig");
const js = @import("js.zig");
const Env = @import("Env.zig");
const bridge = @import("bridge.zig");
+const Origin = @import("Origin.zig");
const Scheduler = @import("Scheduler.zig");
const Page = @import("../Page.zig");
@@ -74,12 +75,7 @@ call_depth: usize = 0,
// context.localScope
local: ?*const js.Local = null,
-// Serves two purposes. Like `global_objects`, this is used to free
-// every Global(Object) we've created during the lifetime of the context.
-// More importantly, it serves as an identity map - for a given Zig
-// instance, we map it to the same Global(Object).
-// The key is the @intFromPtr of the Zig value
-identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
+origin: *Origin,
// Any type that is stored in the identity_map which has a finalizer declared
// will have its finalizer stored here. This is only used when shutting down
@@ -87,26 +83,9 @@ identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
finalizer_callback_pool: std.heap.MemoryPool(FinalizerCallback),
-// Some web APIs have to manage opaque values. Ideally, they use an
-// js.Object, but the js.Object has no lifetime guarantee beyond the
-// current call. They can call .persist() on their js.Object to get
-// a `Global(Object)`. We need to track these to free them.
-// This used to be a map and acted like identity_map; the key was
-// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without
-// a reliable way to know if an object has already been persisted,
-// we now simply persist every time persist() is called.
-global_values: std.ArrayList(v8.Global) = .empty,
-global_objects: std.ArrayList(v8.Global) = .empty,
+// Unlike other v8 types, like functions or objects, modules are not shared
+// across origins.
global_modules: std.ArrayList(v8.Global) = .empty,
-global_promises: std.ArrayList(v8.Global) = .empty,
-global_functions: std.ArrayList(v8.Global) = .empty,
-global_promise_resolvers: std.ArrayList(v8.Global) = .empty,
-
-// Temp variants stored in HashMaps for O(1) early cleanup.
-// Key is global.data_ptr.
-global_values_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
-global_promises_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
-global_functions_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Our module cache: normalized module specifier => module.
module_cache: std.StringHashMapUnmanaged(ModuleEntry) = .empty,
@@ -174,12 +153,6 @@ pub fn deinit(self: *Context) void {
// this can release objects
self.scheduler.deinit();
- {
- var it = self.identity_map.valueIterator();
- while (it.next()) |global| {
- v8.v8__Global__Reset(global);
- }
- }
{
var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |finalizer| {
@@ -188,50 +161,11 @@ pub fn deinit(self: *Context) void {
self.finalizer_callback_pool.deinit();
}
- for (self.global_values.items) |*global| {
- v8.v8__Global__Reset(global);
- }
-
- for (self.global_objects.items) |*global| {
- v8.v8__Global__Reset(global);
- }
-
for (self.global_modules.items) |*global| {
v8.v8__Global__Reset(global);
}
- for (self.global_functions.items) |*global| {
- v8.v8__Global__Reset(global);
- }
-
- for (self.global_promises.items) |*global| {
- v8.v8__Global__Reset(global);
- }
-
- for (self.global_promise_resolvers.items) |*global| {
- v8.v8__Global__Reset(global);
- }
-
- {
- var it = self.global_values_temp.valueIterator();
- while (it.next()) |global| {
- v8.v8__Global__Reset(global);
- }
- }
-
- {
- var it = self.global_promises_temp.valueIterator();
- while (it.next()) |global| {
- v8.v8__Global__Reset(global);
- }
- }
-
- {
- var it = self.global_functions_temp.valueIterator();
- while (it.next()) |global| {
- v8.v8__Global__Reset(global);
- }
- }
+ env.releaseOrigin(self.origin);
v8.v8__Global__Reset(&self.handle);
env.isolate.notifyContextDisposed();
@@ -241,6 +175,38 @@ pub fn deinit(self: *Context) void {
v8.v8__MicrotaskQueue__DELETE(self.microtask_queue);
}
+pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
+ const env = self.env;
+ const isolate = env.isolate;
+
+ const origin = try env.getOrCreateOrigin(key);
+ errdefer env.releaseOrigin(origin);
+
+ try self.origin.transferTo(origin);
+ self.origin.deinit(env.app);
+
+ self.origin = origin;
+
+ {
+ var ls: js.Local.Scope = undefined;
+ self.localScope(&ls);
+ defer ls.deinit();
+
+ // Set the V8::Context SecurityToken, which is a big part of what allows
+ // one context to access another.
+ const token_local = v8.v8__Global__Get(&origin.security_token, isolate.handle);
+ v8.v8__Context__SetSecurityToken(ls.local.handle, token_local);
+ }
+}
+
+pub fn trackGlobal(self: *Context, global: v8.Global) !void {
+ return self.origin.trackGlobal(global);
+}
+
+pub fn trackTemp(self: *Context, global: v8.Global) !void {
+ return self.origin.trackTemp(global);
+}
+
pub fn weakRef(self: *Context, obj: anytype) void {
const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
if (comptime IS_DEBUG) {
@@ -279,7 +245,7 @@ pub fn release(self: *Context, item: anytype) void {
if (@TypeOf(item) == *anyopaque) {
// Existing *anyopaque path for identity_map. Called internally from
// finalizers
- var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse {
+ var global = self.origin.identity_map.fetchRemove(@intFromPtr(item)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
@@ -301,14 +267,14 @@ pub fn release(self: *Context, item: anytype) void {
return;
}
- var map = switch (@TypeOf(item)) {
- js.Value.Temp => &self.global_values_temp,
- js.Promise.Temp => &self.global_promises_temp,
- js.Function.Temp => &self.global_functions_temp,
- else => |T| @compileError("Context.release cannot be called with a " ++ @typeName(T)),
- };
+ if (comptime IS_DEBUG) {
+ switch (@TypeOf(item)) {
+ js.Value.Temp, js.Promise.Temp, js.Function.Temp => {},
+ else => |T| @compileError("Context.release cannot be called with a " ++ @typeName(T)),
+ }
+ }
- if (map.fetchRemove(item.handle.data_ptr)) |kv| {
+ if (self.origin.temps.fetchRemove(item.handle.data_ptr)) |kv| {
var global = kv.value;
v8.v8__Global__Reset(&global);
}
diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig
index 1a86ffd5..9bacc088 100644
--- a/src/browser/js/Env.zig
+++ b/src/browser/js/Env.zig
@@ -26,6 +26,7 @@ const App = @import("../../App.zig");
const log = @import("../../log.zig");
const bridge = @import("bridge.zig");
+const Origin = @import("Origin.zig");
const Context = @import("Context.zig");
const Isolate = @import("Isolate.zig");
const Platform = @import("Platform.zig");
@@ -57,6 +58,8 @@ const Env = @This();
app: *App,
+allocator: Allocator,
+
platform: *const Platform,
// the global isolate
@@ -70,6 +73,9 @@ isolate_params: *v8.CreateParams,
context_id: usize,
+// Maps origin -> shared Origin contains, for v8 values shared across same-origin Contexts
+origins: std.StringHashMapUnmanaged(*Origin) = .empty,
+
// Global handles that need to be freed on deinit
eternal_function_templates: []v8.Eternal,
@@ -206,6 +212,7 @@ pub fn init(app: *App, opts: InitOpts) !Env {
return .{
.app = app,
.context_id = 0,
+ .allocator = allocator,
.contexts = undefined,
.context_count = 0,
.isolate = isolate,
@@ -228,7 +235,17 @@ pub fn deinit(self: *Env) void {
ctx.deinit();
}
- const allocator = self.app.allocator;
+ const app = self.app;
+ const allocator = app.allocator;
+
+ {
+ var it = self.origins.valueIterator();
+ while (it.next()) |value| {
+ value.*.deinit(app);
+ }
+ self.origins.deinit(allocator);
+ }
+
if (self.inspector) |i| {
i.deinit(allocator);
}
@@ -272,6 +289,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
// get the global object for the context, this maps to our Window
const global_obj = v8.v8__Context__Global(v8_context).?;
+
{
// Store our TAO inside the internal field of the global object. This
// maps the v8::Object -> Zig instance. Almost all objects have this, and
@@ -287,6 +305,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
};
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
}
+
// our window wrapped in a v8::Global
var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
@@ -294,10 +313,14 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
const context_id = self.context_id;
self.context_id = context_id + 1;
+ const origin = try self.getOrCreateOrigin(null);
+ errdefer self.releaseOrigin(origin);
+
const context = try context_arena.create(Context);
context.* = .{
.env = self,
.page = page,
+ .origin = origin,
.id = context_id,
.isolate = isolate,
.arena = context_arena,
@@ -309,7 +332,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
.scheduler = .init(context_arena),
.finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator),
};
- try context.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global);
+ try context.origin.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global);
// Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out
@@ -350,6 +373,41 @@ pub fn destroyContext(self: *Env, context: *Context) void {
context.deinit();
}
+pub fn getOrCreateOrigin(self: *Env, key_: ?[]const u8) !*Origin {
+ const key = key_ orelse {
+ var opaque_origin: [36]u8 = undefined;
+ @import("../../id.zig").uuidv4(&opaque_origin);
+ // Origin.init will dupe opaque_origin. It's fine that this doesn't
+ // get added to self.origins. In fact, it further isolates it. When the
+ // context is freed, it'll call env.releaseOrigin which will free it.
+ return Origin.init(self.app, self.isolate, &opaque_origin);
+ };
+
+ const gop = try self.origins.getOrPut(self.allocator, key);
+ if (gop.found_existing) {
+ const origin = gop.value_ptr.*;
+ origin.rc += 1;
+ return origin;
+ }
+
+ errdefer _ = self.origins.remove(key);
+
+ const origin = try Origin.init(self.app, self.isolate, key);
+ gop.key_ptr.* = origin.key;
+ gop.value_ptr.* = origin;
+ return origin;
+}
+
+pub fn releaseOrigin(self: *Env, origin: *Origin) void {
+ const rc = origin.rc;
+ if (rc == 1) {
+ _ = self.origins.remove(origin.key);
+ origin.deinit(self.app);
+ } else {
+ origin.rc = rc - 1;
+ }
+}
+
pub fn runMicrotasks(self: *Env) void {
if (self.microtask_queues_are_running == false) {
const v8_isolate = self.isolate.handle;
diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig
index 01243d35..203fd9ab 100644
--- a/src/browser/js/Function.zig
+++ b/src/browser/js/Function.zig
@@ -209,9 +209,9 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) {
- try ctx.global_functions.append(ctx.arena, global);
+ try ctx.trackGlobal(global);
} else {
- try ctx.global_functions_temp.put(ctx.arena, global.data_ptr, global);
+ try ctx.trackTemp(global);
}
return .{ .handle = global };
}
diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig
index 6a68b332..305391f5 100644
--- a/src/browser/js/Local.zig
+++ b/src/browser/js/Local.zig
@@ -171,7 +171,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
.pointer => |ptr| {
const resolved = resolveValue(value);
- const gop = try ctx.identity_map.getOrPut(arena, @intFromPtr(resolved.ptr));
+ const gop = try ctx.origin.identity_map.getOrPut(arena, @intFromPtr(resolved.ptr));
if (gop.found_existing) {
// we've seen this instance before, return the same object
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);
diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig
index 981f4a2b..fbf036e4 100644
--- a/src/browser/js/Object.zig
+++ b/src/browser/js/Object.zig
@@ -97,7 +97,7 @@ pub fn persist(self: Object) !Global {
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
- try ctx.global_objects.append(ctx.arena, global);
+ try ctx.trackGlobal(global);
return .{ .handle = global };
}
diff --git a/src/browser/js/Origin.zig b/src/browser/js/Origin.zig
new file mode 100644
index 00000000..6cb0b356
--- /dev/null
+++ b/src/browser/js/Origin.zig
@@ -0,0 +1,148 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+// Origin represents the shared Zig<->JS bridge state for all contexts within
+// the same origin. Multiple contexts (frames) from the same origin share a
+// single Origin, ensuring that JS objects maintain their identity across frames.
+
+const std = @import("std");
+const js = @import("js.zig");
+
+const App = @import("../../App.zig");
+
+const v8 = js.v8;
+const Allocator = std.mem.Allocator;
+const IS_DEBUG = @import("build").mode == .Debug;
+
+const Origin = @This();
+
+rc: usize = 1,
+arena: Allocator,
+
+// The key, e.g. lightpanda.io:443
+key: []const u8,
+
+// Security token - all contexts in this realm must use the same v8::Value instance
+// as their security token for V8 to allow cross-context access
+security_token: v8.Global,
+
+// Serves two purposes. Like `global_objects`, this is used to free
+// every Global(Object) we've created during the lifetime of the realm.
+// More importantly, it serves as an identity map - for a given Zig
+// instance, we map it to the same Global(Object).
+// The key is the @intFromPtr of the Zig value
+identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
+
+// Some web APIs have to manage opaque values. Ideally, they use an
+// js.Object, but the js.Object has no lifetime guarantee beyond the
+// current call. They can call .persist() on their js.Object to get
+// a `Global(Object)`. We need to track these to free them.
+// This used to be a map and acted like identity_map; the key was
+// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without
+// a reliable way to know if an object has already been persisted,
+// we now simply persist every time persist() is called.
+globals: std.ArrayList(v8.Global) = .empty,
+
+// Temp variants stored in HashMaps for O(1) early cleanup.
+// Key is global.data_ptr.
+temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
+
+pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
+ const arena = try app.arena_pool.acquire();
+ errdefer app.arena_pool.release(arena);
+
+ var hs: js.HandleScope = undefined;
+ hs.init(isolate);
+ defer hs.deinit();
+
+ const owned_key = try arena.dupe(u8, key);
+ const token_local = isolate.initStringHandle(owned_key);
+ var token_global: v8.Global = undefined;
+ v8.v8__Global__New(isolate.handle, token_local, &token_global);
+
+ const self = try arena.create(Origin);
+ self.* = .{
+ .rc = 1,
+ .arena = arena,
+ .key = owned_key,
+ .globals = .empty,
+ .temps = .empty,
+ .security_token = token_global,
+ };
+ return self;
+}
+
+pub fn deinit(self: *Origin, app: *App) void {
+ v8.v8__Global__Reset(&self.security_token);
+
+ {
+ var it = self.identity_map.valueIterator();
+ while (it.next()) |global| {
+ v8.v8__Global__Reset(global);
+ }
+ }
+
+ for (self.globals.items) |*global| {
+ v8.v8__Global__Reset(global);
+ }
+
+ {
+ var it = self.temps.valueIterator();
+ while (it.next()) |global| {
+ v8.v8__Global__Reset(global);
+ }
+ }
+
+ app.arena_pool.release(self.arena);
+}
+
+pub fn trackGlobal(self: *Origin, global: v8.Global) !void {
+ return self.globals.append(self.arena, global);
+}
+
+pub fn trackTemp(self: *Origin, global: v8.Global) !void {
+ return self.temps.put(self.arena, global.data_ptr, global);
+}
+
+pub fn transferTo(self: *Origin, dest: *Origin) !void {
+ const arena = dest.arena;
+
+ try dest.globals.ensureUnusedCapacity(arena, self.globals.items.len);
+ for (self.globals.items) |obj| {
+ dest.globals.appendAssumeCapacity(obj);
+ }
+ self.globals.clearRetainingCapacity();
+
+ {
+ try dest.temps.ensureUnusedCapacity(arena, self.temps.count());
+ var it = self.temps.iterator();
+ while (it.next()) |kv| {
+ try dest.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*);
+ }
+ self.temps.clearRetainingCapacity();
+ }
+
+ {
+ try dest.identity_map.ensureUnusedCapacity(arena, self.identity_map.count());
+ var it = self.identity_map.iterator();
+ while (it.next()) |kv| {
+ try dest.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*);
+ }
+ self.identity_map.clearRetainingCapacity();
+ }
+}
diff --git a/src/browser/js/Promise.zig b/src/browser/js/Promise.zig
index afadbe82..98520d4b 100644
--- a/src/browser/js/Promise.zig
+++ b/src/browser/js/Promise.zig
@@ -62,9 +62,9 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) {
- try ctx.global_promises.append(ctx.arena, global);
+ try ctx.trackGlobal(global);
} else {
- try ctx.global_promises_temp.put(ctx.arena, global.data_ptr, global);
+ try ctx.trackTemp(global);
}
return .{ .handle = global };
}
diff --git a/src/browser/js/PromiseResolver.zig b/src/browser/js/PromiseResolver.zig
index 183effee..f2aac0e0 100644
--- a/src/browser/js/PromiseResolver.zig
+++ b/src/browser/js/PromiseResolver.zig
@@ -79,7 +79,7 @@ pub fn persist(self: PromiseResolver) !Global {
var ctx = self.local.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
- try ctx.global_promise_resolvers.append(ctx.arena, global);
+ try ctx.trackGlobal(global);
return .{ .handle = global };
}
diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig
index 7963ae7c..fbc961ed 100644
--- a/src/browser/js/Value.zig
+++ b/src/browser/js/Value.zig
@@ -259,9 +259,9 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) {
- try ctx.global_values.append(ctx.arena, global);
+ try ctx.trackGlobal(global);
} else {
- try ctx.global_values_temp.put(ctx.arena, global.data_ptr, global);
+ try ctx.trackTemp(global);
}
return .{ .handle = global };
}
diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig
index 9415b717..22651c39 100644
--- a/src/browser/js/js.zig
+++ b/src/browser/js/js.zig
@@ -161,7 +161,7 @@ pub fn ArrayBufferRef(comptime kind: ArrayType) type {
var ctx = self.local.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
- try ctx.global_values.append(ctx.arena, global);
+ try ctx.trackGlobal(global);
return .{ .handle = global };
}
diff --git a/src/browser/tests/frames/frames.html b/src/browser/tests/frames/frames.html
index 4e614de9..97bed281 100644
--- a/src/browser/tests/frames/frames.html
+++ b/src/browser/tests/frames/frames.html
@@ -64,11 +64,12 @@
// child frame's top.parent is itself (root has no parent)
testing.expectEqual(window, window[0].top.parent);
- // Todo: Context security tokens
- // testing.expectEqual(true, window.sub1_loaded);
- // testing.expectEqual(true, window.sub2_loaded);
- // testing.expectEqual(1, window.sub1_count);
- // testing.expectEqual(2, window.sub2_count);
+ // Cross-frame property access
+ testing.expectEqual(true, window.sub1_loaded);
+ testing.expectEqual(true, window.sub2_loaded);
+ testing.expectEqual(1, window.sub1_count);
+ // depends on how far the initial load got before it was cancelled.
+ testing.expectEqual(true, window.sub2_count == 1 || window.sub2_count == 2);
});
diff --git a/src/browser/tests/testing.js b/src/browser/tests/testing.js
index 90434f0f..987ba042 100644
--- a/src/browser/tests/testing.js
+++ b/src/browser/tests/testing.js
@@ -118,7 +118,7 @@
BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/',
};
- if (!IS_TEST_RUNNER) {
+ if (window.navigator.userAgent.startsWith("Lightpanda/") == false) {
// The page is running in a different browser. Probably a developer making sure
// a test is correct. There are a few tweaks we need to do to make this a
// seemless, namely around adapting paths/urls.
diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig
index 3bc6f586..fda7d2a5 100644
--- a/src/browser/webapi/URL.zig
+++ b/src/browser/webapi/URL.zig
@@ -243,11 +243,10 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {
var uuid_buf: [36]u8 = undefined;
@import("../../id.zig").uuidv4(&uuid_buf);
- const origin = (try page.getOrigin(page.call_arena)) orelse "null";
const blob_url = try std.fmt.allocPrint(
page.arena,
"blob:{s}/{s}",
- .{ origin, uuid_buf },
+ .{ page.origin orelse "null", uuid_buf },
);
try page._blob_urls.put(page.arena, blob_url, blob);
return blob_url;
diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig
index a96ac2b6..6e406c05 100644
--- a/src/cdp/domains/page.zig
+++ b/src/cdp/domains/page.zig
@@ -414,7 +414,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
bc.inspector_session.inspector.contextCreated(
&ls.local,
"",
- try page.getOrigin(arena) orelse "",
+ page.origin orelse "",
aux_data,
true,
);
diff --git a/src/testing.zig b/src/testing.zig
index a398f824..774f76e4 100644
--- a/src/testing.zig
+++ b/src/testing.zig
@@ -414,15 +414,6 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
try_catch.init(&ls.local);
defer try_catch.deinit();
- // by default, on load, testing.js will call testing.assertOk(). This makes our
- // tests work well in a browser. But, for our test runner, we disable that
- // and call it explicitly. This gives us better error messages.
- ls.local.eval("window._lightpanda_skip_auto_assert = true;", "auto_assert") catch |err| {
- const caught = try_catch.caughtOrError(arena_allocator, err);
- std.debug.print("disable auto assert failure\nError: {f}\n", .{caught});
- return err;
- };
-
try page.navigate(url, .{});
_ = test_session.wait(2000);