diff --git a/src/browser/Session.zig b/src/browser/Session.zig index 73b6b26e..6376ff03 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -24,6 +24,7 @@ const log = @import("../log.zig"); const App = @import("../App.zig"); const js = @import("js/js.zig"); +const v8 = js.v8; const storage = @import("webapi/storage/storage.zig"); const Navigation = @import("webapi/navigation/Navigation.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. 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. // These live for the duration of the page tree (root + frames). arena_pool: *ArenaPool, @@ -84,8 +98,8 @@ queued_navigation: std.ArrayList(*Page), // about:blank navigations (which may add to queued_navigation). queued_queued_navigation: std.ArrayList(*Page), -page_id_gen: u32, -frame_id_gen: u32, +page_id_gen: u32 = 0, +frame_id_gen: u32 = 0, pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void { const allocator = browser.app.allocator; @@ -104,8 +118,6 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi .page_arena = page_arena, .factory = Factory.init(page_arena), .history = .{}, - .page_id_gen = 0, - .frame_id_gen = 0, // The prototype (EventTarget) for Navigation is created when a Page is created. .navigation = .{ ._proto = undefined }, .storage_shed = .{}, @@ -174,7 +186,7 @@ pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator { const allocator = try self.arena_pool.acquire(); if (comptime IS_DEBUG) { // 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) { log.err(.bug, "ArenaPool Double Use", .{ .owner = gop.value_ptr.*.owner }); @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. /// Called when root page is removed. 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) { var it = self._arena_pool_leak_track.valueIterator(); while (it.next()) |value_ptr| { @@ -245,10 +285,9 @@ fn resetPageResources(self: *Session) void { 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) { std.debug.assert(self.origins.count() == 0); } @@ -259,10 +298,9 @@ fn resetPageResources(self: *Session) void { while (it.next()) |value| { value.*.deinit(app); } - self.origins.clearRetainingCapacity(); + self.origins = .empty; } - // Release old page_arena and acquire fresh one self.frame_id_gen = 0; self.arena_pool.reset(self.page_arena, 64 * 1024); self.factory = Factory.init(self.page_arena); @@ -672,3 +710,91 @@ pub fn nextPageId(self: *Session) u32 { self.page_id_gen = 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); + } +}; diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index da7362aa..5ea9454f 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -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 }); 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; { @@ -203,16 +202,16 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !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 { - return self.origin.trackTemp(global); + return self.session.trackTemp(global); } pub fn weakRef(self: *Context, obj: anytype) void { 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) { // should not be possible std.debug.assert(false); @@ -224,7 +223,7 @@ pub fn weakRef(self: *Context, obj: anytype) void { pub fn safeWeakRef(self: *Context, obj: anytype) void { 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) { // should not be possible std.debug.assert(false); @@ -237,7 +236,7 @@ pub fn safeWeakRef(self: *Context, obj: anytype) void { pub fn strongRef(self: *Context, obj: anytype) void { 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) { // should not be possible std.debug.assert(false); diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 09117eb0..c417a6e5 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -307,16 +307,17 @@ pub fn createContext(self: *Env, page: *Page) !*Context { const context_id = self.context_id; self.context_id = context_id + 1; - const origin = try page._session.getOrCreateOrigin(null); - errdefer page._session.releaseOrigin(origin); + const session = page._session; + const origin = try session.getOrCreateOrigin(null); + errdefer session.releaseOrigin(origin); const context = try context_arena.create(Context); context.* = .{ .env = self, .page = page, - .session = page._session, .origin = origin, .id = context_id, + .session = session, .isolate = isolate, .arena = context_arena, .handle = context_global, @@ -326,7 +327,15 @@ pub fn createContext(self: *Env, page: *Page) !*Context { .script_manager = &page._script_manager, .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 // a v8 context, we can get our context out diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index bfb5e53d..e2c10e9f 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -21,6 +21,7 @@ const js = @import("js.zig"); const v8 = js.v8; const log = @import("../../log.zig"); +const Session = @import("../Session.zig"); 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); if (comptime is_global) { try ctx.trackGlobal(global); - return .{ .handle = global, .origin = {} }; + return .{ .handle = global, .session = {} }; } 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 { @@ -237,7 +238,7 @@ const GlobalType = enum(u8) { fn G(comptime global_type: GlobalType) type { return struct { handle: v8.Global, - origin: if (global_type == .temp) *js.Origin else void, + session: if (global_type == .temp) *Session else void, const Self = @This(); @@ -257,7 +258,7 @@ fn G(comptime global_type: GlobalType) type { } pub fn release(self: *const Self) void { - self.origin.releaseTemp(self.handle); + self.session.releaseTemp(self.handle); } }; } diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index a45b35df..ca09fc91 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -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) pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object { const ctx = self.ctx; - const origin_arena = ctx.origin.arena; + const session = ctx.session; + const page_arena = session.page_arena; const T = @TypeOf(value); switch (@typeInfo(T)) { .@"struct" => { // Struct, has to be placed on the heap - const heap = try origin_arena.create(T); + const heap = try page_arena.create(T); heap.* = value; return self.mapZigInstanceToJs(js_obj_handle, heap); }, .pointer => |ptr| { 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) { // we've seen this instance before, return the same object 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 // well as any meta data we'll need to use it later. // See the TaggedOpaque struct for more details. - const tao = try origin_arena.create(TaggedOpaque); + const tao = try page_arena.create(TaggedOpaque); tao.* = .{ .value = resolved.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 // here is that if a resolve type has a finalizer, then the base // 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(); - 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); diff --git a/src/browser/js/Origin.zig b/src/browser/js/Origin.zig index 9dc5857b..6f79b005 100644 --- a/src/browser/js/Origin.zig +++ b/src/browser/js/Origin.zig @@ -16,19 +16,20 @@ // 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. +// Origin represents the security token for contexts within the same origin. +// Multiple contexts (frames) from the same origin share a single Origin, +// 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 js = @import("js.zig"); const App = @import("../../App.zig"); -const Session = @import("../Session.zig"); const v8 = js.v8; const Allocator = std.mem.Allocator; -const IS_DEBUG = @import("builtin").mode == .Debug; const Origin = @This(); @@ -38,38 +39,10 @@ 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 +// 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 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 { const arena = try app.arena_pool.acquire(); errdefer app.arena_pool.release(arena); @@ -88,175 +61,12 @@ pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin { .rc = 1, .arena = arena, .key = owned_key, - .temps = .empty, - .globals = .empty, - .taken_over = .empty, .security_token = token_global, }; return self; } 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); - - { - 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 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); - } -}; diff --git a/src/browser/js/Promise.zig b/src/browser/js/Promise.zig index 372d2578..0a08f424 100644 --- a/src/browser/js/Promise.zig +++ b/src/browser/js/Promise.zig @@ -19,6 +19,8 @@ const js = @import("js.zig"); const v8 = js.v8; +const Session = @import("../Session.zig"); + const Promise = @This(); 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); if (comptime is_global) { try ctx.trackGlobal(global); - return .{ .handle = global, .origin = {} }; + return .{ .handle = global, .session = {} }; } try ctx.trackTemp(global); - return .{ .handle = global, .origin = ctx.origin }; + return .{ .handle = global, .session = ctx.session }; } pub const Temp = G(.temp); @@ -80,7 +82,7 @@ const GlobalType = enum(u8) { fn G(comptime global_type: GlobalType) type { return struct { handle: v8.Global, - origin: if (global_type == .temp) *js.Origin else void, + session: if (global_type == .temp) *Session else void, const Self = @This(); @@ -96,7 +98,7 @@ fn G(comptime global_type: GlobalType) type { } pub fn release(self: *const Self) void { - self.origin.releaseTemp(self.handle); + self.session.releaseTemp(self.handle); } }; } diff --git a/src/browser/js/String.zig b/src/browser/js/String.zig index 47be227a..2cbe6a17 100644 --- a/src/browser/js/String.zig +++ b/src/browser/js/String.zig @@ -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) { 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); } diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig index 8e05690b..edbbe4e3 100644 --- a/src/browser/js/Value.zig +++ b/src/browser/js/Value.zig @@ -25,6 +25,7 @@ const v8 = js.v8; const IS_DEBUG = @import("builtin").mode == .Debug; const Allocator = std.mem.Allocator; +const Session = @import("../Session.zig"); 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); if (comptime is_global) { try ctx.trackGlobal(global); - return .{ .handle = global, .origin = {} }; + return .{ .handle = global, .session = {} }; } 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 { @@ -361,7 +362,7 @@ const GlobalType = enum(u8) { fn G(comptime global_type: GlobalType) type { return struct { handle: v8.Global, - origin: if (global_type == .temp) *js.Origin else void, + session: if (global_type == .temp) *Session else void, const Self = @This(); @@ -381,7 +382,7 @@ fn G(comptime global_type: GlobalType) type { } pub fn release(self: *const Self) void { - self.origin.releaseTemp(self.handle); + self.session.releaseTemp(self.handle); } }; } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 4fc96b7e..57cff1fa 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -27,7 +27,6 @@ const v8 = js.v8; const Caller = @import("Caller.zig"); const Context = @import("Context.zig"); -const Origin = @import("Origin.zig"); const IS_DEBUG = @import("builtin").mode == .Debug; @@ -117,13 +116,13 @@ pub fn Builder(comptime T: type) type { .from_v8 = struct { fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void { 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; - if (origin.finalizer_callbacks.contains(@intFromPtr(value_ptr))) { - func(@ptrCast(@alignCast(value_ptr)), false, fc.session); - origin.release(value_ptr); + if (session.finalizer_callbacks.contains(@intFromPtr(value_ptr))) { + func(@ptrCast(@alignCast(value_ptr)), false, session); + session.releaseIdentity(value_ptr); } else { // A bit weird, but v8 _requires_ that we release it // If we don't. We'll 100% crash. diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index 8586a11d..74a5d79e 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -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 { - if (shutdown) { - self._callback.release(); - session.releaseArena(self._arena); - } else if (comptime IS_DEBUG) { - std.debug.assert(false); + self._callback.release(); + if ((comptime IS_DEBUG) and !shutdown) { + std.debug.assert(self._observing.items.len == 0); } + + session.releaseArena(self._arena); } 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 if (self._observing.items.len == 0) { + page.js.strongRef(self); try page.registerIntersectionObserver(self); } @@ -145,18 +146,22 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi break; } } + + if (self._observing.items.len == 0) { + page.js.safeWeakRef(self); + } } pub fn disconnect(self: *IntersectionObserver, page: *Page) void { + page.unregisterIntersectionObserver(self); + self._observing.clearRetainingCapacity(); self._previous_states.clearRetainingCapacity(); for (self._pending_entries.items) |entry| { entry.deinit(false, page._session); } self._pending_entries.clearRetainingCapacity(); - - self._observing.clearRetainingCapacity(); - page.unregisterIntersectionObserver(self); + page.js.safeWeakRef(self); } pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry { @@ -358,6 +363,7 @@ pub const JsApi = struct { pub const name = "IntersectionObserver"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const weak = true; pub const finalizer = bridge.finalizer(IntersectionObserver.deinit); }; diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig index df86d1e1..b8608381 100644 --- a/src/browser/webapi/MutationObserver.zig +++ b/src/browser/webapi/MutationObserver.zig @@ -86,12 +86,12 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver { } pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void { - if (shutdown) { - self._callback.release(); - session.releaseArena(self._arena); - } else if (comptime IS_DEBUG) { - std.debug.assert(false); + self._callback.release(); + if ((comptime IS_DEBUG) and !shutdown) { + std.debug.assert(self._observing.items.len == 0); } + + session.releaseArena(self._arena); } 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 if (self._observing.items.len == 0) { + page.js.strongRef(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 { + page.unregisterMutationObserver(self); + self._observing.clearRetainingCapacity(); for (self._pending_records.items) |record| { record.deinit(false, page._session); } self._pending_records.clearRetainingCapacity(); - - self._observing.clearRetainingCapacity(); - page.unregisterMutationObserver(self); + page.js.safeWeakRef(self); } pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord { @@ -440,6 +441,7 @@ pub const JsApi = struct { pub const name = "MutationObserver"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const weak = true; pub const finalizer = bridge.finalizer(MutationObserver.deinit); };