Remove Origins

js.Origin was added to allow frames on the same origin to share our zig<->js
maps / identity. It assumes that scripts on different origins will never be
allowed (by v8) to access the same zig instances.

If two different origins DID access the same zig instance, we'd have a few
different problems. First, while the mapping would exist in Origin1's
identity_map, when the zig instance was returned to a script in Origin2, it
would not be found in Origin2's identity_map, and thus create a new v8::Object.
Thus we'd end up with 2 v8::Objects for the same Zig instance. This is
potentially not the end of the world, but not great either as any zig-native
data _would_ be shared (it's the same instance after all), but js-native data
wouldn't.

The real problem this introduces though is with Finalizers. A weak reference
that falls out of scope in Origin1 will get cleaned up, even though it's still
referenced from Origin2.

Now, under normal circumstances, this isn't an issue; v8 _does_ ensure that
cross-origin access isn't allowed (because we set a SecurityToken on the
v8::Context). But it seems like the v8::Inspector isn't bound by these
restrictions and can happily access and share objects across origin.

The simplest solution I can come up with is to move the mapping from the Origin
to the Session. This does mean that objects might live longer than they have to.
When all references to an origin go out of scope, we can do some cleanup. Not
so when the Session owns this data. But really, how often are iframes on
different origins being created and deleted within the lifetime of a page?

When Origins were first introduces, the Session got burdened with having to
manage multiple lifecycles:
1 - The page-surviving data (e.g. history)
2 - The root page lifecycle (e.g. page_arena, queuedNavigation)
3 - The origin lookup

This commit doesn't change that, but it makes the session responsible for
_a lot_ more of the root page lifecycle (#2 above).

I lied. js.Origin still exists, but it's a shell of its former self. It only
exists to store the SecurityToken name that is re-used for every context with
the same origin.

The v8 namespace leaks into Session.

MutationObserver and IntersectionObserver are now back to using weak/strong refs
which was one of the failing cases before this change.
This commit is contained in:
Karl Seguin
2026-03-18 18:59:09 +08:00
parent edd0c5c83f
commit d9c5f56500
12 changed files with 215 additions and 259 deletions

View File

@@ -24,6 +24,7 @@ const log = @import("../log.zig");
const App = @import("../App.zig"); const App = @import("../App.zig");
const js = @import("js/js.zig"); const js = @import("js/js.zig");
const v8 = js.v8;
const storage = @import("webapi/storage/storage.zig"); const storage = @import("webapi/storage/storage.zig");
const Navigation = @import("webapi/navigation/Navigation.zig"); const Navigation = @import("webapi/navigation/Navigation.zig");
const History = @import("webapi/History.zig"); const History = @import("webapi/History.zig");
@@ -65,6 +66,19 @@ page_arena: Allocator,
// Origin map for same-origin context sharing. Scoped to the root page lifetime. // Origin map for same-origin context sharing. Scoped to the root page lifetime.
origins: std.StringHashMapUnmanaged(*js.Origin) = .empty, origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
// Session-scoped identity tracking (for the lifetime of the root page).
// Maps Zig instance pointers to their v8::Global(Object) wrappers.
identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Tracked global v8 objects that need to be released when the page is reset.
globals: std.ArrayList(v8.Global) = .empty,
// Temporary v8 globals that can be released early. Key is global.data_ptr.
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Finalizer callbacks for weak references. Key is @intFromPtr of the Zig instance.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
// Shared resources for all pages in this session. // Shared resources for all pages in this session.
// These live for the duration of the page tree (root + frames). // These live for the duration of the page tree (root + frames).
arena_pool: *ArenaPool, arena_pool: *ArenaPool,
@@ -84,8 +98,8 @@ queued_navigation: std.ArrayList(*Page),
// about:blank navigations (which may add to queued_navigation). // about:blank navigations (which may add to queued_navigation).
queued_queued_navigation: std.ArrayList(*Page), queued_queued_navigation: std.ArrayList(*Page),
page_id_gen: u32, page_id_gen: u32 = 0,
frame_id_gen: u32, frame_id_gen: u32 = 0,
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void { pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
const allocator = browser.app.allocator; const allocator = browser.app.allocator;
@@ -104,8 +118,6 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
.page_arena = page_arena, .page_arena = page_arena,
.factory = Factory.init(page_arena), .factory = Factory.init(page_arena),
.history = .{}, .history = .{},
.page_id_gen = 0,
.frame_id_gen = 0,
// The prototype (EventTarget) for Navigation is created when a Page is created. // The prototype (EventTarget) for Navigation is created when a Page is created.
.navigation = .{ ._proto = undefined }, .navigation = .{ ._proto = undefined },
.storage_shed = .{}, .storage_shed = .{},
@@ -174,7 +186,7 @@ pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator {
const allocator = try self.arena_pool.acquire(); const allocator = try self.arena_pool.acquire();
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
// Use session's arena (not page_arena) since page_arena gets reset between pages // Use session's arena (not page_arena) since page_arena gets reset between pages
const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr)); const gop = try self._arena_pool_leak_track.getOrPut(self.page_arena, @intFromPtr(allocator.ptr));
if (gop.found_existing and gop.value_ptr.count != 0) { if (gop.found_existing and gop.value_ptr.count != 0) {
log.err(.bug, "ArenaPool Double Use", .{ .owner = gop.value_ptr.*.owner }); log.err(.bug, "ArenaPool Double Use", .{ .owner = gop.value_ptr.*.owner });
@panic("ArenaPool Double Use"); @panic("ArenaPool Double Use");
@@ -237,7 +249,35 @@ pub fn releaseOrigin(self: *Session, origin: *js.Origin) void {
/// Reset page_arena and factory for a clean slate. /// Reset page_arena and factory for a clean slate.
/// Called when root page is removed. /// Called when root page is removed.
fn resetPageResources(self: *Session) void { fn resetPageResources(self: *Session) void {
// Check for arena leaks before releasing {
var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |finalizer| {
finalizer.*.deinit();
}
self.finalizer_callbacks = .empty;
}
{
var it = self.identity_map.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
self.identity_map = .empty;
}
for (self.globals.items) |*global| {
v8.v8__Global__Reset(global);
}
self.globals = .empty;
{
var it = self.temps.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
self.temps = .empty;
}
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
var it = self._arena_pool_leak_track.valueIterator(); var it = self._arena_pool_leak_track.valueIterator();
while (it.next()) |value_ptr| { while (it.next()) |value_ptr| {
@@ -245,10 +285,9 @@ fn resetPageResources(self: *Session) void {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner }); log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner });
} }
} }
self._arena_pool_leak_track.clearRetainingCapacity(); self._arena_pool_leak_track = .empty;
} }
// All origins should have been released when contexts were destroyed
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
std.debug.assert(self.origins.count() == 0); std.debug.assert(self.origins.count() == 0);
} }
@@ -259,10 +298,9 @@ fn resetPageResources(self: *Session) void {
while (it.next()) |value| { while (it.next()) |value| {
value.*.deinit(app); value.*.deinit(app);
} }
self.origins.clearRetainingCapacity(); self.origins = .empty;
} }
// Release old page_arena and acquire fresh one
self.frame_id_gen = 0; self.frame_id_gen = 0;
self.arena_pool.reset(self.page_arena, 64 * 1024); self.arena_pool.reset(self.page_arena, 64 * 1024);
self.factory = Factory.init(self.page_arena); self.factory = Factory.init(self.page_arena);
@@ -672,3 +710,91 @@ pub fn nextPageId(self: *Session) u32 {
self.page_id_gen = id; self.page_id_gen = id;
return id; return id;
} }
// These methods manage the mapping between Zig instances and v8 objects,
// scoped to the lifetime of the root page.
pub fn trackGlobal(self: *Session, global: v8.Global) !void {
return self.globals.append(self.page_arena, global);
}
pub const IdentityResult = struct {
value_ptr: *v8.Global,
found_existing: bool,
};
pub fn addIdentity(self: *Session, ptr: usize) !IdentityResult {
const gop = try self.identity_map.getOrPut(self.page_arena, ptr);
return .{
.value_ptr = gop.value_ptr,
.found_existing = gop.found_existing,
};
}
pub fn trackTemp(self: *Session, global: v8.Global) !void {
return self.temps.put(self.page_arena, global.data_ptr, global);
}
pub fn releaseTemp(self: *Session, global: v8.Global) void {
if (self.temps.fetchRemove(global.data_ptr)) |kv| {
var g = kv.value;
v8.v8__Global__Reset(&g);
}
}
/// Release an item from the identity_map (called after finalizer runs from V8)
pub fn releaseIdentity(self: *Session, item: *anyopaque) void {
var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse {
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
return;
};
v8.v8__Global__Reset(&global.value);
// The item has been finalized, remove it from the finalizer callback so that
// we don't try to call it again on shutdown.
const kv = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse {
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
return;
};
const fc = kv.value;
self.releaseArena(fc.arena);
}
pub fn createFinalizerCallback(
self: *Session,
global: v8.Global,
ptr: *anyopaque,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
) !*FinalizerCallback {
const arena = try self.getArena(.{ .debug = "FinalizerCallback" });
errdefer self.releaseArena(arena);
const fc = try arena.create(FinalizerCallback);
fc.* = .{
.arena = arena,
.session = self,
.ptr = ptr,
.global = global,
.zig_finalizer = zig_finalizer,
};
return fc;
}
// A type that has a finalizer can have its finalizer called one of two ways.
// The first is from V8 via the WeakCallback we give to weakRef. But that isn't
// guaranteed to fire, so we track this in finalizer_callbacks and call them on
// page reset.
pub const FinalizerCallback = struct {
arena: Allocator,
session: *Session,
ptr: *anyopaque,
global: v8.Global,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
pub fn deinit(self: *FinalizerCallback) void {
self.zig_finalizer(self.ptr, self.session);
self.session.releaseArena(self.arena);
}
};

View File

@@ -185,9 +185,8 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc }); lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
const origin = try self.session.getOrCreateOrigin(key); const origin = try self.session.getOrCreateOrigin(key);
errdefer self.session.releaseOrigin(origin);
try origin.takeover(self.origin);
self.session.releaseOrigin(self.origin);
self.origin = origin; self.origin = origin;
{ {
@@ -203,16 +202,16 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
} }
pub fn trackGlobal(self: *Context, global: v8.Global) !void { pub fn trackGlobal(self: *Context, global: v8.Global) !void {
return self.origin.trackGlobal(global); return self.session.trackGlobal(global);
} }
pub fn trackTemp(self: *Context, global: v8.Global) !void { pub fn trackTemp(self: *Context, global: v8.Global) !void {
return self.origin.trackTemp(global); return self.session.trackTemp(global);
} }
pub fn weakRef(self: *Context, obj: anytype) void { pub fn weakRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj); const resolved = js.Local.resolveValue(obj);
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { const fc = self.session.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
// should not be possible // should not be possible
std.debug.assert(false); std.debug.assert(false);
@@ -224,7 +223,7 @@ pub fn weakRef(self: *Context, obj: anytype) void {
pub fn safeWeakRef(self: *Context, obj: anytype) void { pub fn safeWeakRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj); const resolved = js.Local.resolveValue(obj);
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { const fc = self.session.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
// should not be possible // should not be possible
std.debug.assert(false); std.debug.assert(false);
@@ -237,7 +236,7 @@ pub fn safeWeakRef(self: *Context, obj: anytype) void {
pub fn strongRef(self: *Context, obj: anytype) void { pub fn strongRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj); const resolved = js.Local.resolveValue(obj);
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { const fc = self.session.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
// should not be possible // should not be possible
std.debug.assert(false); std.debug.assert(false);

View File

@@ -307,16 +307,17 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
const context_id = self.context_id; const context_id = self.context_id;
self.context_id = context_id + 1; self.context_id = context_id + 1;
const origin = try page._session.getOrCreateOrigin(null); const session = page._session;
errdefer page._session.releaseOrigin(origin); const origin = try session.getOrCreateOrigin(null);
errdefer session.releaseOrigin(origin);
const context = try context_arena.create(Context); const context = try context_arena.create(Context);
context.* = .{ context.* = .{
.env = self, .env = self,
.page = page, .page = page,
.session = page._session,
.origin = origin, .origin = origin,
.id = context_id, .id = context_id,
.session = session,
.isolate = isolate, .isolate = isolate,
.arena = context_arena, .arena = context_arena,
.handle = context_global, .handle = context_global,
@@ -326,7 +327,15 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
.script_manager = &page._script_manager, .script_manager = &page._script_manager,
.scheduler = .init(context_arena), .scheduler = .init(context_arena),
}; };
try context.origin.identity_map.putNoClobber(origin.arena, @intFromPtr(page.window), global_global);
{
// Multiple contexts can be created for the same Window (via CDP). We only
// need to register the first one.
const gop = try session.identity_map.getOrPut(session.page_arena, @intFromPtr(page.window));
if (gop.found_existing == false) {
gop.value_ptr.* = global_global;
}
}
// Store a pointer to our context inside the v8 context so that, given // Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out // a v8 context, we can get our context out

View File

@@ -21,6 +21,7 @@ const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Session = @import("../Session.zig");
const Function = @This(); const Function = @This();
@@ -210,10 +211,10 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) { if (comptime is_global) {
try ctx.trackGlobal(global); try ctx.trackGlobal(global);
return .{ .handle = global, .origin = {} }; return .{ .handle = global, .session = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .origin = ctx.origin }; return .{ .handle = global, .session = ctx.session };
} }
pub fn tempWithThis(self: *const Function, value: anytype) !Temp { pub fn tempWithThis(self: *const Function, value: anytype) !Temp {
@@ -237,7 +238,7 @@ const GlobalType = enum(u8) {
fn G(comptime global_type: GlobalType) type { fn G(comptime global_type: GlobalType) type {
return struct { return struct {
handle: v8.Global, handle: v8.Global,
origin: if (global_type == .temp) *js.Origin else void, session: if (global_type == .temp) *Session else void,
const Self = @This(); const Self = @This();
@@ -257,7 +258,7 @@ fn G(comptime global_type: GlobalType) type {
} }
pub fn release(self: *const Self) void { pub fn release(self: *const Self) void {
self.origin.releaseTemp(self.handle); self.session.releaseTemp(self.handle);
} }
}; };
} }

View File

@@ -202,20 +202,21 @@ pub fn compileAndRun(self: *const Local, src: []const u8, name: ?[]const u8) !js
// we can just grab it from the identity_map) // we can just grab it from the identity_map)
pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object { pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object {
const ctx = self.ctx; const ctx = self.ctx;
const origin_arena = ctx.origin.arena; const session = ctx.session;
const page_arena = session.page_arena;
const T = @TypeOf(value); const T = @TypeOf(value);
switch (@typeInfo(T)) { switch (@typeInfo(T)) {
.@"struct" => { .@"struct" => {
// Struct, has to be placed on the heap // Struct, has to be placed on the heap
const heap = try origin_arena.create(T); const heap = try page_arena.create(T);
heap.* = value; heap.* = value;
return self.mapZigInstanceToJs(js_obj_handle, heap); return self.mapZigInstanceToJs(js_obj_handle, heap);
}, },
.pointer => |ptr| { .pointer => |ptr| {
const resolved = resolveValue(value); const resolved = resolveValue(value);
const gop = try ctx.origin.addIdentity(@intFromPtr(resolved.ptr)); const gop = try session.addIdentity(@intFromPtr(resolved.ptr));
if (gop.found_existing) { if (gop.found_existing) {
// we've seen this instance before, return the same object // we've seen this instance before, return the same object
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self); return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);
@@ -244,7 +245,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
// The TAO contains the pointer to our Zig instance as // The TAO contains the pointer to our Zig instance as
// well as any meta data we'll need to use it later. // well as any meta data we'll need to use it later.
// See the TaggedOpaque struct for more details. // See the TaggedOpaque struct for more details.
const tao = try origin_arena.create(TaggedOpaque); const tao = try page_arena.create(TaggedOpaque);
tao.* = .{ tao.* = .{
.value = resolved.ptr, .value = resolved.ptr,
.prototype_chain = resolved.prototype_chain.ptr, .prototype_chain = resolved.prototype_chain.ptr,
@@ -276,10 +277,10 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
// Instead, we check if the base has finalizer. The assumption // Instead, we check if the base has finalizer. The assumption
// here is that if a resolve type has a finalizer, then the base // here is that if a resolve type has a finalizer, then the base
// should have a finalizer too. // should have a finalizer too.
const fc = try ctx.origin.createFinalizerCallback(ctx.session, gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?); const fc = try session.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
{ {
errdefer fc.deinit(); errdefer fc.deinit();
try ctx.origin.finalizer_callbacks.put(ctx.origin.arena, @intFromPtr(resolved.ptr), fc); try session.finalizer_callbacks.put(page_arena, @intFromPtr(resolved.ptr), fc);
} }
conditionallyReference(value); conditionallyReference(value);

View File

@@ -16,19 +16,20 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// Origin represents the shared Zig<->JS bridge state for all contexts within // Origin represents the security token for contexts within the same origin.
// the same origin. Multiple contexts (frames) from the same origin share a // Multiple contexts (frames) from the same origin share a single Origin,
// single Origin, ensuring that JS objects maintain their identity across frames. // which provides the V8 SecurityToken that allows cross-context access.
//
// Note: Identity tracking (mapping Zig instances to v8::Objects) is now
// handled at the Session level, scoped to the root page lifetime.
const std = @import("std"); const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const App = @import("../../App.zig"); const App = @import("../../App.zig");
const Session = @import("../Session.zig");
const v8 = js.v8; const v8 = js.v8;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Origin = @This(); const Origin = @This();
@@ -38,38 +39,10 @@ arena: Allocator,
// The key, e.g. lightpanda.io:443 // The key, e.g. lightpanda.io:443
key: []const u8, key: []const u8,
// Security token - all contexts in this realm must use the same v8::Value instance // Security token - all contexts in this origin must use the same v8::Value instance
// as their security token for V8 to allow cross-context access // as their security token for V8 to allow cross-context access
security_token: v8.Global, 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,
// 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
// if v8 hasn't called the finalizer directly itself.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
taken_over: std.ArrayList(*Origin),
pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin { pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
const arena = try app.arena_pool.acquire(); const arena = try app.arena_pool.acquire();
errdefer app.arena_pool.release(arena); errdefer app.arena_pool.release(arena);
@@ -88,175 +61,12 @@ pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
.rc = 1, .rc = 1,
.arena = arena, .arena = arena,
.key = owned_key, .key = owned_key,
.temps = .empty,
.globals = .empty,
.taken_over = .empty,
.security_token = token_global, .security_token = token_global,
}; };
return self; return self;
} }
pub fn deinit(self: *Origin, app: *App) void { pub fn deinit(self: *Origin, app: *App) void {
for (self.taken_over.items) |o| {
o.deinit(app);
}
// Call finalizers before releasing anything
{
var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |finalizer| {
finalizer.*.deinit();
}
}
v8.v8__Global__Reset(&self.security_token); 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); app.arena_pool.release(self.arena);
} }
pub fn trackGlobal(self: *Origin, global: v8.Global) !void {
return self.globals.append(self.arena, global);
}
pub const IdentityResult = struct {
value_ptr: *v8.Global,
found_existing: bool,
};
pub fn addIdentity(self: *Origin, ptr: usize) !IdentityResult {
const gop = try self.identity_map.getOrPut(self.arena, ptr);
return .{
.value_ptr = gop.value_ptr,
.found_existing = gop.found_existing,
};
}
pub fn trackTemp(self: *Origin, global: v8.Global) !void {
return self.temps.put(self.arena, global.data_ptr, global);
}
pub fn releaseTemp(self: *Origin, global: v8.Global) void {
if (self.temps.fetchRemove(global.data_ptr)) |kv| {
var g = kv.value;
v8.v8__Global__Reset(&g);
}
}
/// Release an item from the identity_map (called after finalizer runs from V8)
pub fn release(self: *Origin, item: *anyopaque) void {
var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse {
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
return;
};
v8.v8__Global__Reset(&global.value);
// The item has been finalized, remove it from the finalizer callback so that
// we don't try to call it again on shutdown.
const kv = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse {
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
return;
};
const fc = kv.value;
fc.session.releaseArena(fc.arena);
}
pub fn createFinalizerCallback(
self: *Origin,
session: *Session,
global: v8.Global,
ptr: *anyopaque,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
) !*FinalizerCallback {
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
errdefer session.releaseArena(arena);
const fc = try arena.create(FinalizerCallback);
fc.* = .{
.arena = arena,
.origin = self,
.session = session,
.ptr = ptr,
.global = global,
.zig_finalizer = zig_finalizer,
};
return fc;
}
pub fn takeover(self: *Origin, original: *Origin) !void {
const arena = self.arena;
try self.globals.ensureUnusedCapacity(arena, original.globals.items.len);
for (original.globals.items) |obj| {
self.globals.appendAssumeCapacity(obj);
}
original.globals.clearRetainingCapacity();
{
try self.temps.ensureUnusedCapacity(arena, original.temps.count());
var it = original.temps.iterator();
while (it.next()) |kv| {
try self.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*);
}
original.temps.clearRetainingCapacity();
}
{
try self.finalizer_callbacks.ensureUnusedCapacity(arena, original.finalizer_callbacks.count());
var it = original.finalizer_callbacks.iterator();
while (it.next()) |kv| {
kv.value_ptr.*.origin = self;
try self.finalizer_callbacks.put(arena, kv.key_ptr.*, kv.value_ptr.*);
}
original.finalizer_callbacks.clearRetainingCapacity();
}
{
try self.identity_map.ensureUnusedCapacity(arena, original.identity_map.count());
var it = original.identity_map.iterator();
while (it.next()) |kv| {
try self.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*);
}
original.identity_map.clearRetainingCapacity();
}
try self.taken_over.append(self.arena, original);
}
// A type that has a finalizer can have its finalizer called one of two ways.
// The first is from V8 via the WeakCallback we give to weakRef. But that isn't
// guaranteed to fire, so we track this in finalizer_callbacks and call them on
// origin shutdown.
pub const FinalizerCallback = struct {
arena: Allocator,
origin: *Origin,
session: *Session,
ptr: *anyopaque,
global: v8.Global,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
pub fn deinit(self: *FinalizerCallback) void {
self.zig_finalizer(self.ptr, self.session);
self.session.releaseArena(self.arena);
}
};

View File

@@ -19,6 +19,8 @@
const js = @import("js.zig"); const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const Session = @import("../Session.zig");
const Promise = @This(); const Promise = @This();
local: *const js.Local, local: *const js.Local,
@@ -63,10 +65,10 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) { if (comptime is_global) {
try ctx.trackGlobal(global); try ctx.trackGlobal(global);
return .{ .handle = global, .origin = {} }; return .{ .handle = global, .session = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .origin = ctx.origin }; return .{ .handle = global, .session = ctx.session };
} }
pub const Temp = G(.temp); pub const Temp = G(.temp);
@@ -80,7 +82,7 @@ const GlobalType = enum(u8) {
fn G(comptime global_type: GlobalType) type { fn G(comptime global_type: GlobalType) type {
return struct { return struct {
handle: v8.Global, handle: v8.Global,
origin: if (global_type == .temp) *js.Origin else void, session: if (global_type == .temp) *Session else void,
const Self = @This(); const Self = @This();
@@ -96,7 +98,7 @@ fn G(comptime global_type: GlobalType) type {
} }
pub fn release(self: *const Self) void { pub fn release(self: *const Self) void {
self.origin.releaseTemp(self.handle); self.session.releaseTemp(self.handle);
} }
}; };
} }

View File

@@ -56,7 +56,7 @@ fn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) !
pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) { pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) {
if (comptime global) { if (comptime global) {
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.origin.arena) }; return .{ .str = try self.toSSOWithAlloc(self.local.ctx.session.page_arena) };
} }
return self.toSSOWithAlloc(self.local.call_arena); return self.toSSOWithAlloc(self.local.call_arena);
} }

View File

@@ -25,6 +25,7 @@ const v8 = js.v8;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Session = @import("../Session.zig");
const Value = @This(); const Value = @This();
@@ -300,10 +301,10 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) { if (comptime is_global) {
try ctx.trackGlobal(global); try ctx.trackGlobal(global);
return .{ .handle = global, .origin = {} }; return .{ .handle = global, .session = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .origin = ctx.origin }; return .{ .handle = global, .session = ctx.session };
} }
pub fn toZig(self: Value, comptime T: type) !T { pub fn toZig(self: Value, comptime T: type) !T {
@@ -361,7 +362,7 @@ const GlobalType = enum(u8) {
fn G(comptime global_type: GlobalType) type { fn G(comptime global_type: GlobalType) type {
return struct { return struct {
handle: v8.Global, handle: v8.Global,
origin: if (global_type == .temp) *js.Origin else void, session: if (global_type == .temp) *Session else void,
const Self = @This(); const Self = @This();
@@ -381,7 +382,7 @@ fn G(comptime global_type: GlobalType) type {
} }
pub fn release(self: *const Self) void { pub fn release(self: *const Self) void {
self.origin.releaseTemp(self.handle); self.session.releaseTemp(self.handle);
} }
}; };
} }

View File

@@ -27,7 +27,6 @@ const v8 = js.v8;
const Caller = @import("Caller.zig"); const Caller = @import("Caller.zig");
const Context = @import("Context.zig"); const Context = @import("Context.zig");
const Origin = @import("Origin.zig");
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
@@ -117,13 +116,13 @@ pub fn Builder(comptime T: type) type {
.from_v8 = struct { .from_v8 = struct {
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?; const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const fc: *Origin.FinalizerCallback = @ptrCast(@alignCast(ptr)); const fc: *Session.FinalizerCallback = @ptrCast(@alignCast(ptr));
const origin = fc.origin; const session = fc.session;
const value_ptr = fc.ptr; const value_ptr = fc.ptr;
if (origin.finalizer_callbacks.contains(@intFromPtr(value_ptr))) { if (session.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
func(@ptrCast(@alignCast(value_ptr)), false, fc.session); func(@ptrCast(@alignCast(value_ptr)), false, session);
origin.release(value_ptr); session.releaseIdentity(value_ptr);
} else { } else {
// A bit weird, but v8 _requires_ that we release it // A bit weird, but v8 _requires_ that we release it
// If we don't. We'll 100% crash. // If we don't. We'll 100% crash.

View File

@@ -93,12 +93,12 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I
} }
pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void { pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void {
if (shutdown) {
self._callback.release(); self._callback.release();
session.releaseArena(self._arena); if ((comptime IS_DEBUG) and !shutdown) {
} else if (comptime IS_DEBUG) { std.debug.assert(self._observing.items.len == 0);
std.debug.assert(false);
} }
session.releaseArena(self._arena);
} }
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void { pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
@@ -111,6 +111,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
// Register with page if this is our first observation // Register with page if this is our first observation
if (self._observing.items.len == 0) { if (self._observing.items.len == 0) {
page.js.strongRef(self);
try page.registerIntersectionObserver(self); try page.registerIntersectionObserver(self);
} }
@@ -145,18 +146,22 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi
break; break;
} }
} }
if (self._observing.items.len == 0) {
page.js.safeWeakRef(self);
}
} }
pub fn disconnect(self: *IntersectionObserver, page: *Page) void { pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
page.unregisterIntersectionObserver(self);
self._observing.clearRetainingCapacity();
self._previous_states.clearRetainingCapacity(); self._previous_states.clearRetainingCapacity();
for (self._pending_entries.items) |entry| { for (self._pending_entries.items) |entry| {
entry.deinit(false, page._session); entry.deinit(false, page._session);
} }
self._pending_entries.clearRetainingCapacity(); self._pending_entries.clearRetainingCapacity();
page.js.safeWeakRef(self);
self._observing.clearRetainingCapacity();
page.unregisterIntersectionObserver(self);
} }
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry { pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
@@ -358,6 +363,7 @@ pub const JsApi = struct {
pub const name = "IntersectionObserver"; pub const name = "IntersectionObserver";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit); pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
}; };

View File

@@ -86,12 +86,12 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
} }
pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void { pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void {
if (shutdown) {
self._callback.release(); self._callback.release();
session.releaseArena(self._arena); if ((comptime IS_DEBUG) and !shutdown) {
} else if (comptime IS_DEBUG) { std.debug.assert(self._observing.items.len == 0);
std.debug.assert(false);
} }
session.releaseArena(self._arena);
} }
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void { pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
@@ -158,6 +158,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
// Register with page if this is our first observation // Register with page if this is our first observation
if (self._observing.items.len == 0) { if (self._observing.items.len == 0) {
page.js.strongRef(self);
try page.registerMutationObserver(self); try page.registerMutationObserver(self);
} }
@@ -168,13 +169,13 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
} }
pub fn disconnect(self: *MutationObserver, page: *Page) void { pub fn disconnect(self: *MutationObserver, page: *Page) void {
page.unregisterMutationObserver(self);
self._observing.clearRetainingCapacity();
for (self._pending_records.items) |record| { for (self._pending_records.items) |record| {
record.deinit(false, page._session); record.deinit(false, page._session);
} }
self._pending_records.clearRetainingCapacity(); self._pending_records.clearRetainingCapacity();
page.js.safeWeakRef(self);
self._observing.clearRetainingCapacity();
page.unregisterMutationObserver(self);
} }
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord { pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
@@ -440,6 +441,7 @@ pub const JsApi = struct {
pub const name = "MutationObserver"; pub const name = "MutationObserver";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MutationObserver.deinit); pub const finalizer = bridge.finalizer(MutationObserver.deinit);
}; };