mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 07:33:16 +00:00
Merge branch 'main' into semantic-versioning
This commit is contained in:
@@ -17,12 +17,15 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
const ArenaPool = @This();
|
const ArenaPool = @This();
|
||||||
|
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
retain_bytes: usize,
|
retain_bytes: usize,
|
||||||
free_list_len: u16 = 0,
|
free_list_len: u16 = 0,
|
||||||
@@ -30,10 +33,17 @@ free_list: ?*Entry = null,
|
|||||||
free_list_max: u16,
|
free_list_max: u16,
|
||||||
entry_pool: std.heap.MemoryPool(Entry),
|
entry_pool: std.heap.MemoryPool(Entry),
|
||||||
mutex: std.Thread.Mutex = .{},
|
mutex: std.Thread.Mutex = .{},
|
||||||
|
// Debug mode: track acquire/release counts per debug name to detect leaks and double-frees
|
||||||
|
_leak_track: if (IS_DEBUG) std.StringHashMapUnmanaged(isize) else void = if (IS_DEBUG) .empty else {},
|
||||||
|
|
||||||
const Entry = struct {
|
const Entry = struct {
|
||||||
next: ?*Entry,
|
next: ?*Entry,
|
||||||
arena: ArenaAllocator,
|
arena: ArenaAllocator,
|
||||||
|
debug: if (IS_DEBUG) []const u8 else void = if (IS_DEBUG) "" else {},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DebugInfo = struct {
|
||||||
|
debug: []const u8 = "",
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) ArenaPool {
|
pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) ArenaPool {
|
||||||
@@ -42,10 +52,26 @@ pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) Arena
|
|||||||
.free_list_max = free_list_max,
|
.free_list_max = free_list_max,
|
||||||
.retain_bytes = retain_bytes,
|
.retain_bytes = retain_bytes,
|
||||||
.entry_pool = .init(allocator),
|
.entry_pool = .init(allocator),
|
||||||
|
._leak_track = if (IS_DEBUG) .empty else {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *ArenaPool) void {
|
pub fn deinit(self: *ArenaPool) void {
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
var has_leaks = false;
|
||||||
|
var it = self._leak_track.iterator();
|
||||||
|
while (it.next()) |kv| {
|
||||||
|
if (kv.value_ptr.* != 0) {
|
||||||
|
std.debug.print("ArenaPool leak detected: '{s}' count={d}\n", .{ kv.key_ptr.*, kv.value_ptr.* });
|
||||||
|
has_leaks = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (has_leaks) {
|
||||||
|
@panic("ArenaPool: leaked arenas detected");
|
||||||
|
}
|
||||||
|
self._leak_track.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
var entry = self.free_list;
|
var entry = self.free_list;
|
||||||
while (entry) |e| {
|
while (entry) |e| {
|
||||||
entry = e.next;
|
entry = e.next;
|
||||||
@@ -54,13 +80,21 @@ pub fn deinit(self: *ArenaPool) void {
|
|||||||
self.entry_pool.deinit();
|
self.entry_pool.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn acquire(self: *ArenaPool) !Allocator {
|
pub fn acquire(self: *ArenaPool, dbg: DebugInfo) !Allocator {
|
||||||
self.mutex.lock();
|
self.mutex.lock();
|
||||||
defer self.mutex.unlock();
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
if (self.free_list) |entry| {
|
if (self.free_list) |entry| {
|
||||||
self.free_list = entry.next;
|
self.free_list = entry.next;
|
||||||
self.free_list_len -= 1;
|
self.free_list_len -= 1;
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
entry.debug = dbg.debug;
|
||||||
|
const gop = try self._leak_track.getOrPut(self.allocator, dbg.debug);
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
gop.value_ptr.* = 0;
|
||||||
|
}
|
||||||
|
gop.value_ptr.* += 1;
|
||||||
|
}
|
||||||
return entry.arena.allocator();
|
return entry.arena.allocator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +102,16 @@ pub fn acquire(self: *ArenaPool) !Allocator {
|
|||||||
entry.* = .{
|
entry.* = .{
|
||||||
.next = null,
|
.next = null,
|
||||||
.arena = ArenaAllocator.init(self.allocator),
|
.arena = ArenaAllocator.init(self.allocator),
|
||||||
|
.debug = if (IS_DEBUG) dbg.debug else {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
const gop = try self._leak_track.getOrPut(self.allocator, dbg.debug);
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
gop.value_ptr.* = 0;
|
||||||
|
}
|
||||||
|
gop.value_ptr.* += 1;
|
||||||
|
}
|
||||||
return entry.arena.allocator();
|
return entry.arena.allocator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +125,19 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
|||||||
self.mutex.lock();
|
self.mutex.lock();
|
||||||
defer self.mutex.unlock();
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
if (self._leak_track.getPtr(entry.debug)) |count| {
|
||||||
|
count.* -= 1;
|
||||||
|
if (count.* < 0) {
|
||||||
|
std.debug.print("ArenaPool double-free detected: '{s}'\n", .{entry.debug});
|
||||||
|
@panic("ArenaPool: double-free detected");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std.debug.print("ArenaPool release of untracked arena: '{s}'\n", .{entry.debug});
|
||||||
|
@panic("ArenaPool: release of untracked arena");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const free_list_len = self.free_list_len;
|
const free_list_len = self.free_list_len;
|
||||||
if (free_list_len == self.free_list_max) {
|
if (free_list_len == self.free_list_max) {
|
||||||
arena.deinit();
|
arena.deinit();
|
||||||
@@ -106,7 +161,7 @@ test "arena pool - basic acquire and use" {
|
|||||||
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
defer pool.deinit();
|
defer pool.deinit();
|
||||||
|
|
||||||
const alloc = try pool.acquire();
|
const alloc = try pool.acquire(.{ .debug = "test" });
|
||||||
const buf = try alloc.alloc(u8, 64);
|
const buf = try alloc.alloc(u8, 64);
|
||||||
@memset(buf, 0xAB);
|
@memset(buf, 0xAB);
|
||||||
try testing.expectEqual(@as(u8, 0xAB), buf[0]);
|
try testing.expectEqual(@as(u8, 0xAB), buf[0]);
|
||||||
@@ -118,14 +173,14 @@ test "arena pool - reuse entry after release" {
|
|||||||
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
defer pool.deinit();
|
defer pool.deinit();
|
||||||
|
|
||||||
const alloc1 = try pool.acquire();
|
const alloc1 = try pool.acquire(.{ .debug = "test" });
|
||||||
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
||||||
|
|
||||||
pool.release(alloc1);
|
pool.release(alloc1);
|
||||||
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
||||||
|
|
||||||
// The same entry should be returned from the free list.
|
// The same entry should be returned from the free list.
|
||||||
const alloc2 = try pool.acquire();
|
const alloc2 = try pool.acquire(.{ .debug = "test" });
|
||||||
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
||||||
try testing.expectEqual(alloc1.ptr, alloc2.ptr);
|
try testing.expectEqual(alloc1.ptr, alloc2.ptr);
|
||||||
|
|
||||||
@@ -136,9 +191,9 @@ test "arena pool - multiple concurrent arenas" {
|
|||||||
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
defer pool.deinit();
|
defer pool.deinit();
|
||||||
|
|
||||||
const a1 = try pool.acquire();
|
const a1 = try pool.acquire(.{ .debug = "test1" });
|
||||||
const a2 = try pool.acquire();
|
const a2 = try pool.acquire(.{ .debug = "test2" });
|
||||||
const a3 = try pool.acquire();
|
const a3 = try pool.acquire(.{ .debug = "test3" });
|
||||||
|
|
||||||
// All three must be distinct arenas.
|
// All three must be distinct arenas.
|
||||||
try testing.expect(a1.ptr != a2.ptr);
|
try testing.expect(a1.ptr != a2.ptr);
|
||||||
@@ -161,8 +216,8 @@ test "arena pool - free list respects max limit" {
|
|||||||
var pool = ArenaPool.init(testing.allocator, 1, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 1, 1024 * 16);
|
||||||
defer pool.deinit();
|
defer pool.deinit();
|
||||||
|
|
||||||
const a1 = try pool.acquire();
|
const a1 = try pool.acquire(.{ .debug = "test1" });
|
||||||
const a2 = try pool.acquire();
|
const a2 = try pool.acquire(.{ .debug = "test2" });
|
||||||
|
|
||||||
pool.release(a1);
|
pool.release(a1);
|
||||||
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
||||||
@@ -176,7 +231,7 @@ test "arena pool - reset clears memory without releasing" {
|
|||||||
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
defer pool.deinit();
|
defer pool.deinit();
|
||||||
|
|
||||||
const alloc = try pool.acquire();
|
const alloc = try pool.acquire(.{ .debug = "test" });
|
||||||
|
|
||||||
const buf = try alloc.alloc(u8, 128);
|
const buf = try alloc.alloc(u8, 128);
|
||||||
@memset(buf, 0xFF);
|
@memset(buf, 0xFF);
|
||||||
@@ -200,8 +255,8 @@ test "arena pool - deinit with entries in free list" {
|
|||||||
// detected by the test allocator).
|
// detected by the test allocator).
|
||||||
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
|
||||||
const a1 = try pool.acquire();
|
const a1 = try pool.acquire(.{ .debug = "test1" });
|
||||||
const a2 = try pool.acquire();
|
const a2 = try pool.acquire(.{ .debug = "test2" });
|
||||||
_ = try a1.alloc(u8, 256);
|
_ = try a1.alloc(u8, 256);
|
||||||
_ = try a2.alloc(u8, 512);
|
_ = try a2.alloc(u8, 512);
|
||||||
pool.release(a1);
|
pool.release(a1);
|
||||||
|
|||||||
@@ -217,6 +217,13 @@ pub const DumpFormat = enum {
|
|||||||
semantic_tree_text,
|
semantic_tree_text,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const WaitUntil = enum {
|
||||||
|
load,
|
||||||
|
domcontentloaded,
|
||||||
|
networkidle,
|
||||||
|
fixed,
|
||||||
|
};
|
||||||
|
|
||||||
pub const Fetch = struct {
|
pub const Fetch = struct {
|
||||||
url: [:0]const u8,
|
url: [:0]const u8,
|
||||||
dump_mode: ?DumpFormat = null,
|
dump_mode: ?DumpFormat = null,
|
||||||
@@ -224,6 +231,8 @@ pub const Fetch = struct {
|
|||||||
with_base: bool = false,
|
with_base: bool = false,
|
||||||
with_frames: bool = false,
|
with_frames: bool = false,
|
||||||
strip: dump.Opts.Strip = .{},
|
strip: dump.Opts.Strip = .{},
|
||||||
|
wait_ms: u32 = 5000,
|
||||||
|
wait_until: WaitUntil = .load,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Common = struct {
|
pub const Common = struct {
|
||||||
@@ -387,6 +396,13 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
|||||||
\\
|
\\
|
||||||
\\--with_frames Includes the contents of iframes. Defaults to false.
|
\\--with_frames Includes the contents of iframes. Defaults to false.
|
||||||
\\
|
\\
|
||||||
|
\\--wait_ms Wait time in milliseconds.
|
||||||
|
\\ Defaults to 5000.
|
||||||
|
\\
|
||||||
|
\\--wait_until Wait until the specified event.
|
||||||
|
\\ Supported events: load, domcontentloaded, networkidle, fixed.
|
||||||
|
\\ Defaults to 'load'.
|
||||||
|
\\
|
||||||
++ common_options ++
|
++ common_options ++
|
||||||
\\
|
\\
|
||||||
\\serve command
|
\\serve command
|
||||||
@@ -619,8 +635,34 @@ fn parseFetchArgs(
|
|||||||
var url: ?[:0]const u8 = null;
|
var url: ?[:0]const u8 = null;
|
||||||
var common: Common = .{};
|
var common: Common = .{};
|
||||||
var strip: dump.Opts.Strip = .{};
|
var strip: dump.Opts.Strip = .{};
|
||||||
|
var wait_ms: u32 = 5000;
|
||||||
|
var wait_until: WaitUntil = .load;
|
||||||
|
|
||||||
while (args.next()) |opt| {
|
while (args.next()) |opt| {
|
||||||
|
if (std.mem.eql(u8, "--wait_ms", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--wait_ms" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
wait_ms = std.fmt.parseInt(u32, str, 10) catch |err| {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = "--wait_ms", .err = err });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--wait_until", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.app, "missing argument value", .{ .arg = "--wait_until" });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
wait_until = std.meta.stringToEnum(WaitUntil, str) orelse {
|
||||||
|
log.fatal(.app, "invalid argument value", .{ .arg = "--wait_until", .val = str });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--dump", opt)) {
|
if (std.mem.eql(u8, "--dump", opt)) {
|
||||||
var peek_args = args.*;
|
var peek_args = args.*;
|
||||||
if (peek_args.next()) |next_arg| {
|
if (peek_args.next()) |next_arg| {
|
||||||
@@ -709,6 +751,8 @@ fn parseFetchArgs(
|
|||||||
.common = common,
|
.common = common,
|
||||||
.with_base = with_base,
|
.with_base = with_base,
|
||||||
.with_frames = with_frames,
|
.with_frames = with_frames,
|
||||||
|
.wait_ms = wait_ms,
|
||||||
|
.wait_until = wait_until,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,7 +302,11 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
|
|||||||
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
|
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
|
||||||
errdefer self._script_manager.deinit();
|
errdefer self._script_manager.deinit();
|
||||||
|
|
||||||
self.js = try browser.env.createContext(self);
|
self.js = try browser.env.createContext(self, .{
|
||||||
|
.identity = &session.identity,
|
||||||
|
.identity_arena = session.page_arena,
|
||||||
|
.call_arena = self.call_arena,
|
||||||
|
});
|
||||||
errdefer self.js.deinit();
|
errdefer self.js.deinit();
|
||||||
|
|
||||||
document._page = self;
|
document._page = self;
|
||||||
|
|||||||
@@ -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,17 +66,14 @@ 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,
|
||||||
|
|
||||||
|
// Identity tracking for the main world. All main world contexts share this,
|
||||||
|
// ensuring object identity works across same-origin frames.
|
||||||
|
identity: js.Identity = .{},
|
||||||
|
|
||||||
// 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,
|
||||||
|
|
||||||
// In Debug, we use this to see if anything fails to release an arena back to
|
|
||||||
// the pool.
|
|
||||||
_arena_pool_leak_track: if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {
|
|
||||||
owner: []const u8,
|
|
||||||
count: usize,
|
|
||||||
}) else void = if (IS_DEBUG) .empty else {},
|
|
||||||
|
|
||||||
page: ?Page,
|
page: ?Page,
|
||||||
|
|
||||||
queued_navigation: std.ArrayList(*Page),
|
queued_navigation: std.ArrayList(*Page),
|
||||||
@@ -84,17 +82,17 @@ 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;
|
||||||
const arena_pool = browser.arena_pool;
|
const arena_pool = browser.arena_pool;
|
||||||
|
|
||||||
const arena = try arena_pool.acquire();
|
const arena = try arena_pool.acquire(.{ .debug = "Session" });
|
||||||
errdefer arena_pool.release(arena);
|
errdefer arena_pool.release(arena);
|
||||||
|
|
||||||
const page_arena = try arena_pool.acquire();
|
const page_arena = try arena_pool.acquire(.{ .debug = "Session.page_arena" });
|
||||||
errdefer arena_pool.release(page_arena);
|
errdefer arena_pool.release(page_arena);
|
||||||
|
|
||||||
self.* = .{
|
self.* = .{
|
||||||
@@ -104,8 +102,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 = .{},
|
||||||
@@ -171,32 +167,11 @@ pub const GetArenaOpts = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator {
|
pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator {
|
||||||
const allocator = try self.arena_pool.acquire();
|
return self.arena_pool.acquire(.{ .debug = opts.debug });
|
||||||
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));
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 };
|
|
||||||
}
|
|
||||||
return allocator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn releaseArena(self: *Session, allocator: Allocator) void {
|
pub fn releaseArena(self: *Session, allocator: Allocator) void {
|
||||||
if (comptime IS_DEBUG) {
|
self.arena_pool.release(allocator);
|
||||||
const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
|
|
||||||
if (found.count != 1) {
|
|
||||||
log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count });
|
|
||||||
if (comptime builtin.is_test) {
|
|
||||||
@panic("ArenaPool Double Free");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
found.count = 0;
|
|
||||||
}
|
|
||||||
return self.arena_pool.release(allocator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin {
|
pub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin {
|
||||||
@@ -237,18 +212,9 @@ 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
|
self.identity.deinit();
|
||||||
if (comptime IS_DEBUG) {
|
self.identity = .{};
|
||||||
var it = self._arena_pool_leak_track.valueIterator();
|
|
||||||
while (it.next()) |value_ptr| {
|
|
||||||
if (value_ptr.count > 0) {
|
|
||||||
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self._arena_pool_leak_track.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 +225,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);
|
||||||
@@ -319,10 +284,15 @@ fn findPageBy(page: *Page, comptime field: []const u8, id: u32) ?*Page {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
const WaitOpts = struct {
|
||||||
|
timeout_ms: u32 = 5000,
|
||||||
|
until: lp.Config.WaitUntil = .load,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn wait(self: *Session, opts: WaitOpts) WaitResult {
|
||||||
var page = &(self.page orelse return .no_page);
|
var page = &(self.page orelse return .no_page);
|
||||||
while (true) {
|
while (true) {
|
||||||
const wait_result = self._wait(page, wait_ms) catch |err| {
|
const wait_result = self._wait(page, opts) catch |err| {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
error.JsError => {}, // already logged (with hopefully more context)
|
error.JsError => {}, // already logged (with hopefully more context)
|
||||||
else => log.err(.browser, "session wait", .{
|
else => log.err(.browser, "session wait", .{
|
||||||
@@ -346,9 +316,11 @@ pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
fn _wait(self: *Session, page: *Page, opts: WaitOpts) !WaitResult {
|
||||||
|
const wait_until = opts.until;
|
||||||
|
|
||||||
var timer = try std.time.Timer.start();
|
var timer = try std.time.Timer.start();
|
||||||
var ms_remaining = wait_ms;
|
var ms_remaining = opts.timeout_ms;
|
||||||
|
|
||||||
const browser = self.browser;
|
const browser = self.browser;
|
||||||
var http_client = browser.http_client;
|
var http_client = browser.http_client;
|
||||||
@@ -372,8 +344,10 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
// There's no JS to run, and no reason to run the scheduler.
|
// There's no JS to run, and no reason to run the scheduler.
|
||||||
if (http_client.active == 0 and exit_when_done) {
|
if (http_client.active == 0 and exit_when_done) {
|
||||||
// haven't started navigating, I guess.
|
// haven't started navigating, I guess.
|
||||||
|
if (wait_until != .fixed) {
|
||||||
return .done;
|
return .done;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Either we have active http connections, or we're in CDP
|
// Either we have active http connections, or we're in CDP
|
||||||
// mode with an extra socket. Either way, we're waiting
|
// mode with an extra socket. Either way, we're waiting
|
||||||
// for http traffic
|
// for http traffic
|
||||||
@@ -423,17 +397,14 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
std.debug.assert(http_client.intercepted == 0);
|
std.debug.assert(http_client.intercepted == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var ms = blk: {
|
const is_event_done = switch (wait_until) {
|
||||||
// if (wait_ms - ms_remaining < 100) {
|
.fixed => false,
|
||||||
// if (comptime builtin.is_test) {
|
.domcontentloaded => (page._load_state == .load or page._load_state == .complete),
|
||||||
// return .done;
|
.load => (page._load_state == .complete),
|
||||||
// }
|
.networkidle => (page._notified_network_idle == .done),
|
||||||
// // Look, we want to exit ASAP, but we don't want
|
};
|
||||||
// // to exit so fast that we've run none of the
|
|
||||||
// // background jobs.
|
|
||||||
// break :blk 50;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
var ms = blk: {
|
||||||
if (browser.hasBackgroundTasks()) {
|
if (browser.hasBackgroundTasks()) {
|
||||||
// _we_ have nothing to run, but v8 is working on
|
// _we_ have nothing to run, but v8 is working on
|
||||||
// background tasks. We'll wait for them.
|
// background tasks. We'll wait for them.
|
||||||
@@ -441,19 +412,27 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
break :blk 20;
|
break :blk 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
break :blk browser.msToNextMacrotask() orelse return .done;
|
const next_task = browser.msToNextMacrotask();
|
||||||
|
if (next_task == null and is_event_done) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
break :blk next_task orelse 20;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ms > ms_remaining) {
|
if (ms > ms_remaining) {
|
||||||
|
if (is_event_done) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
// Same as above, except we have a scheduled task,
|
// Same as above, except we have a scheduled task,
|
||||||
// it just happens to be too far into the future
|
// it just happens to be too far into the future
|
||||||
// compared to how long we were told to wait.
|
// compared to how long we were told to wait.
|
||||||
if (!browser.hasBackgroundTasks()) {
|
if (browser.hasBackgroundTasks()) {
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
// _we_ have nothing to run, but v8 is working on
|
// _we_ have nothing to run, but v8 is working on
|
||||||
// background tasks. We'll wait for them.
|
// background tasks. We'll wait for them.
|
||||||
browser.waitForBackgroundTasks();
|
browser.waitForBackgroundTasks();
|
||||||
|
}
|
||||||
|
// We're still wait for our wait_until. Not sure for what
|
||||||
|
// but let's keep waiting. Worst case, we'll timeout.
|
||||||
ms = 20;
|
ms = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,16 +611,6 @@ fn processRootQueuedNavigation(self: *Session) !void {
|
|||||||
|
|
||||||
defer self.arena_pool.release(qn.arena);
|
defer self.arena_pool.release(qn.arena);
|
||||||
|
|
||||||
// HACK
|
|
||||||
// Mark as released in tracking BEFORE removePage clears the map.
|
|
||||||
// We can't call releaseArena() because that would also return the arena
|
|
||||||
// to the pool, making the memory invalid before we use qn.url/qn.opts.
|
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
if (self._arena_pool_leak_track.getPtr(@intFromPtr(qn.arena.ptr))) |found| {
|
|
||||||
found.count = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.removePage();
|
self.removePage();
|
||||||
|
|
||||||
self.page = @as(Page, undefined);
|
self.page = @as(Page, undefined);
|
||||||
@@ -672,3 +641,36 @@ pub fn nextPageId(self: *Session) u32 {
|
|||||||
self.page_id_gen = id;
|
self.page_id_gen = id;
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
identity: *js.Identity,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release this item from the identity tracking maps (called after finalizer runs from V8)
|
||||||
|
pub fn releaseIdentity(self: *FinalizerCallback) void {
|
||||||
|
const session = self.session;
|
||||||
|
const id = @intFromPtr(self.ptr);
|
||||||
|
|
||||||
|
if (self.identity.identity_map.fetchRemove(id)) |kv| {
|
||||||
|
var global = kv.value;
|
||||||
|
v8.v8__Global__Reset(&global);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = self.identity.finalizer_callbacks.remove(id);
|
||||||
|
|
||||||
|
session.releaseArena(self.arena);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ templates: []*const v8.FunctionTemplate,
|
|||||||
// Arena for the lifetime of the context
|
// Arena for the lifetime of the context
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
|
|
||||||
// The page.call_arena
|
// The call_arena for this context. For main world contexts this is
|
||||||
|
// page.call_arena. For isolated world contexts this is a separate arena
|
||||||
|
// owned by the IsolatedWorld.
|
||||||
call_arena: Allocator,
|
call_arena: Allocator,
|
||||||
|
|
||||||
// Because calls can be nested (i.e.a function calling a callback),
|
// Because calls can be nested (i.e.a function calling a callback),
|
||||||
@@ -79,6 +81,16 @@ local: ?*const js.Local = null,
|
|||||||
|
|
||||||
origin: *Origin,
|
origin: *Origin,
|
||||||
|
|
||||||
|
// Identity tracking for this context. For main world contexts, this points to
|
||||||
|
// Session's Identity. For isolated world contexts (CDP inspector), this points
|
||||||
|
// to IsolatedWorld's Identity. This ensures same-origin frames share object
|
||||||
|
// identity while isolated worlds have separate identity tracking.
|
||||||
|
identity: *js.Identity,
|
||||||
|
|
||||||
|
// Allocator to use for identity map operations. For main world contexts this is
|
||||||
|
// session.page_arena, for isolated worlds it's the isolated world's arena.
|
||||||
|
identity_arena: Allocator,
|
||||||
|
|
||||||
// Unlike other v8 types, like functions or objects, modules are not shared
|
// Unlike other v8 types, like functions or objects, modules are not shared
|
||||||
// across origins.
|
// across origins.
|
||||||
global_modules: std.ArrayList(v8.Global) = .empty,
|
global_modules: std.ArrayList(v8.Global) = .empty,
|
||||||
@@ -185,9 +197,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 +214,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.identity.globals.append(self.identity_arena, 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.identity.temps.put(self.identity_arena, global.data_ptr, 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.identity.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 +235,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.identity.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 +248,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.identity.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);
|
||||||
@@ -247,6 +258,48 @@ pub fn strongRef(self: *Context, obj: anytype) void {
|
|||||||
v8.v8__Global__ClearWeak(&fc.global);
|
v8.v8__Global__ClearWeak(&fc.global);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const IdentityResult = struct {
|
||||||
|
value_ptr: *v8.Global,
|
||||||
|
found_existing: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn addIdentity(self: *Context, ptr: usize) !IdentityResult {
|
||||||
|
const gop = try self.identity.identity_map.getOrPut(self.identity_arena, ptr);
|
||||||
|
return .{
|
||||||
|
.value_ptr = gop.value_ptr,
|
||||||
|
.found_existing = gop.found_existing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn releaseTemp(self: *Context, global: v8.Global) void {
|
||||||
|
if (self.identity.temps.fetchRemove(global.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createFinalizerCallback(
|
||||||
|
self: *Context,
|
||||||
|
global: v8.Global,
|
||||||
|
ptr: *anyopaque,
|
||||||
|
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
|
||||||
|
) !*Session.FinalizerCallback {
|
||||||
|
const session = self.session;
|
||||||
|
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
|
||||||
|
errdefer session.releaseArena(arena);
|
||||||
|
const fc = try arena.create(Session.FinalizerCallback);
|
||||||
|
fc.* = .{
|
||||||
|
.arena = arena,
|
||||||
|
.session = session,
|
||||||
|
.ptr = ptr,
|
||||||
|
.global = global,
|
||||||
|
.zig_finalizer = zig_finalizer,
|
||||||
|
// Store identity pointer for cleanup when V8 GCs the object
|
||||||
|
.identity = self.identity,
|
||||||
|
};
|
||||||
|
return fc;
|
||||||
|
}
|
||||||
|
|
||||||
// Any operation on the context have to be made from a local.
|
// Any operation on the context have to be made from a local.
|
||||||
pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
|
pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
|
||||||
const isolate = self.isolate;
|
const isolate = self.isolate;
|
||||||
@@ -545,13 +598,13 @@ pub fn dynamicModuleCallback(
|
|||||||
|
|
||||||
break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
|
break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
||||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
|
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
|
||||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalized_specifier = self.script_manager.?.resolveSpecifier(
|
const normalized_specifier = self.script_manager.?.resolveSpecifier(
|
||||||
@@ -560,14 +613,14 @@ pub fn dynamicModuleCallback(
|
|||||||
specifier,
|
specifier,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
|
||||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const promise = self._dynamicModuleCallback(normalized_specifier, resource, &local) catch |err| blk: {
|
const promise = self._dynamicModuleCallback(normalized_specifier, resource, &local) catch |err| blk: {
|
||||||
log.err(.js, "dynamic module callback", .{
|
log.err(.js, "dynamic module callback", .{
|
||||||
.err = err,
|
.err = err,
|
||||||
});
|
});
|
||||||
break :blk local.rejectPromise("Failed to load module") catch return null;
|
break :blk local.rejectPromise(.{ .generic_error = "Out of memory" });
|
||||||
};
|
};
|
||||||
return @constCast(promise.handle);
|
return @constCast(promise.handle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const Snapshot = @import("Snapshot.zig");
|
|||||||
const Inspector = @import("Inspector.zig");
|
const Inspector = @import("Inspector.zig");
|
||||||
|
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
const Window = @import("../webapi/Window.zig");
|
const Window = @import("../webapi/Window.zig");
|
||||||
|
|
||||||
const JsApis = bridge.JsApis;
|
const JsApis = bridge.JsApis;
|
||||||
@@ -254,8 +255,15 @@ pub fn deinit(self: *Env) void {
|
|||||||
allocator.destroy(self.isolate_params);
|
allocator.destroy(self.isolate_params);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createContext(self: *Env, page: *Page) !*Context {
|
pub const ContextParams = struct {
|
||||||
const context_arena = try self.app.arena_pool.acquire();
|
identity: *js.Identity,
|
||||||
|
identity_arena: Allocator,
|
||||||
|
call_arena: Allocator,
|
||||||
|
debug_name: []const u8 = "Context",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
|
||||||
|
const context_arena = try self.app.arena_pool.acquire(.{ .debug = params.debug_name });
|
||||||
errdefer self.app.arena_pool.release(context_arena);
|
errdefer self.app.arena_pool.release(context_arena);
|
||||||
|
|
||||||
const isolate = self.isolate;
|
const isolate = self.isolate;
|
||||||
@@ -300,33 +308,43 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
|
|||||||
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
|
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);
|
|
||||||
|
|
||||||
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,
|
||||||
.templates = self.templates,
|
.templates = self.templates,
|
||||||
.call_arena = page.call_arena,
|
.call_arena = params.call_arena,
|
||||||
.microtask_queue = microtask_queue,
|
.microtask_queue = microtask_queue,
|
||||||
.script_manager = &page._script_manager,
|
.script_manager = &page._script_manager,
|
||||||
.scheduler = .init(context_arena),
|
.scheduler = .init(context_arena),
|
||||||
|
.identity = params.identity,
|
||||||
|
.identity_arena = params.identity_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 params.identity.identity_map.getOrPut(params.identity_arena, @intFromPtr(page.window));
|
||||||
|
if (gop.found_existing == false) {
|
||||||
|
// our window wrapped in a v8::Global
|
||||||
|
var global_global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
|
||||||
|
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
|
||||||
@@ -495,6 +513,11 @@ pub fn terminate(self: *const Env) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
||||||
|
const promise_event = v8.v8__PromiseRejectMessage__GetEvent(&message_handle);
|
||||||
|
if (promise_event != v8.kPromiseRejectWithNoHandler and promise_event != v8.kPromiseHandlerAddedAfterReject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
||||||
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
||||||
const isolate = js.Isolate{ .handle = v8_isolate };
|
const isolate = js.Isolate{ .handle = v8_isolate };
|
||||||
@@ -508,7 +531,7 @@ fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) v
|
|||||||
};
|
};
|
||||||
|
|
||||||
const page = ctx.page;
|
const page = ctx.page;
|
||||||
page.window.unhandledPromiseRejection(.{
|
page.window.unhandledPromiseRejection(promise_event == v8.kPromiseRejectWithNoHandler, .{
|
||||||
.local = &local,
|
.local = &local,
|
||||||
.handle = &message_handle,
|
.handle = &message_handle,
|
||||||
}, page) catch |err| {
|
}, page) catch |err| {
|
||||||
|
|||||||
@@ -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, .temps = {} };
|
||||||
}
|
}
|
||||||
try ctx.trackTemp(global);
|
try ctx.trackTemp(global);
|
||||||
return .{ .handle = global, .origin = ctx.origin };
|
return .{ .handle = global, .temps = &ctx.identity.temps };
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@@ -257,7 +258,10 @@ 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);
|
if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/browser/js/Identity.zig
Normal file
76
src/browser/js/Identity.zig
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// Identity manages the mapping between Zig instances and their v8::Object wrappers.
|
||||||
|
// This provides object identity semantics - the same Zig instance always maps to
|
||||||
|
// the same JS object within a given Identity scope.
|
||||||
|
//
|
||||||
|
// Main world contexts share a single Identity (on Session), ensuring that
|
||||||
|
// `window.top.document === top's document` works across same-origin frames.
|
||||||
|
//
|
||||||
|
// Isolated worlds (CDP inspector) have their own Identity, ensuring their
|
||||||
|
// v8::Global wrappers don't leak into the main world.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
|
const v8 = js.v8;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const Identity = @This();
|
||||||
|
|
||||||
|
// 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 on cleanup.
|
||||||
|
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, *Session.FinalizerCallback) = .empty,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Identity) void {
|
||||||
|
{
|
||||||
|
var it = self.finalizer_callbacks.valueIterator();
|
||||||
|
while (it.next()) |finalizer| {
|
||||||
|
finalizer.*.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,21 @@ pub fn createError(self: Isolate, msg: []const u8) *const v8.Value {
|
|||||||
return v8.v8__Exception__Error(message).?;
|
return v8.v8__Exception__Error(message).?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn createRangeError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__RangeError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createReferenceError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__ReferenceError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createSyntaxError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__SyntaxError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn createTypeError(self: Isolate, msg: []const u8) *const v8.Value {
|
pub fn createTypeError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
const message = self.initStringHandle(msg);
|
const message = self.initStringHandle(msg);
|
||||||
return v8.v8__Exception__TypeError(message).?;
|
return v8.v8__Exception__TypeError(message).?;
|
||||||
|
|||||||
@@ -202,20 +202,20 @@ 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 context_arena = ctx.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 context_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 ctx.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 +244,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 context_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 +276,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 ctx.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 ctx.identity.finalizer_callbacks.put(ctx.identity_arena, @intFromPtr(resolved.ptr), fc);
|
||||||
}
|
}
|
||||||
|
|
||||||
conditionallyReference(value);
|
conditionallyReference(value);
|
||||||
@@ -1206,9 +1206,9 @@ pub fn stackTrace(self: *const Local) !?[]const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// == Promise Helpers ==
|
// == Promise Helpers ==
|
||||||
pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise {
|
pub fn rejectPromise(self: *const Local, err: js.PromiseResolver.RejectError) js.Promise {
|
||||||
var resolver = js.PromiseResolver.init(self);
|
var resolver = js.PromiseResolver.init(self);
|
||||||
resolver.reject("Local.rejectPromise", value);
|
resolver.rejectError("Local.rejectPromise", err);
|
||||||
return resolver.promise();
|
return resolver.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,19 +16,21 @@
|
|||||||
// 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 managed
|
||||||
|
// separately via js.Identity - Session has the main world Identity, and
|
||||||
|
// IsolatedWorlds have their own Identity instances.
|
||||||
|
|
||||||
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,40 +40,12 @@ 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(.{ .debug = "Origin" });
|
||||||
errdefer app.arena_pool.release(arena);
|
errdefer app.arena_pool.release(arena);
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
@@ -88,175 +62,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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -16,9 +16,12 @@
|
|||||||
// 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/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
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 +66,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, .temps = {} };
|
||||||
}
|
}
|
||||||
try ctx.trackTemp(global);
|
try ctx.trackTemp(global);
|
||||||
return .{ .handle = global, .origin = ctx.origin };
|
return .{ .handle = global, .temps = &ctx.identity.temps };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Temp = G(.temp);
|
pub const Temp = G(.temp);
|
||||||
@@ -80,7 +83,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,
|
temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@@ -96,7 +99,10 @@ 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);
|
if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,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 DOMException = @import("../webapi/DOMException.zig");
|
const DOMException = @import("../webapi/DOMException.zig");
|
||||||
|
|
||||||
const PromiseResolver = @This();
|
const PromiseResolver = @This();
|
||||||
@@ -66,19 +67,37 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const RejectError = union(enum) {
|
pub const RejectError = union(enum) {
|
||||||
generic: []const u8,
|
/// Not to be confused with `DOMException`; this is bare `Error`.
|
||||||
|
generic_error: []const u8,
|
||||||
|
range_error: []const u8,
|
||||||
|
reference_error: []const u8,
|
||||||
|
syntax_error: []const u8,
|
||||||
type_error: []const u8,
|
type_error: []const u8,
|
||||||
dom_exception: anyerror,
|
/// DOM exceptions are unknown to V8, belongs to web standards.
|
||||||
|
dom_exception: struct { err: anyerror },
|
||||||
};
|
};
|
||||||
pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void {
|
|
||||||
|
/// Rejects the promise w/ an error object.
|
||||||
|
pub fn rejectError(
|
||||||
|
self: PromiseResolver,
|
||||||
|
comptime source: []const u8,
|
||||||
|
err: RejectError,
|
||||||
|
) void {
|
||||||
const handle = switch (err) {
|
const handle = switch (err) {
|
||||||
.type_error => |str| self.local.isolate.createTypeError(str),
|
.generic_error => |msg| self.local.isolate.createError(msg),
|
||||||
.generic => |str| self.local.isolate.createError(str),
|
.range_error => |msg| self.local.isolate.createRangeError(msg),
|
||||||
|
.reference_error => |msg| self.local.isolate.createReferenceError(msg),
|
||||||
|
.syntax_error => |msg| self.local.isolate.createSyntaxError(msg),
|
||||||
|
.type_error => |msg| self.local.isolate.createTypeError(msg),
|
||||||
|
// "Exceptional".
|
||||||
.dom_exception => |exception| {
|
.dom_exception => |exception| {
|
||||||
self.reject(source, DOMException.fromError(exception));
|
self._reject(DOMException.fromError(exception.err) orelse unreachable) catch |reject_err| {
|
||||||
|
log.err(.bug, "rejectDomException", .{ .source = source, .err = reject_err, .persistent = false });
|
||||||
|
};
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
|
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
|
||||||
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
|
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, .temps = {} };
|
||||||
}
|
}
|
||||||
try ctx.trackTemp(global);
|
try ctx.trackTemp(global);
|
||||||
return .{ .handle = global, .origin = ctx.origin };
|
return .{ .handle = global, .temps = &ctx.identity.temps };
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@@ -381,7 +382,10 @@ 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);
|
if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 @@ 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 value_ptr = fc.ptr;
|
const value_ptr = fc.ptr;
|
||||||
if (origin.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
if (fc.identity.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
||||||
func(@ptrCast(@alignCast(value_ptr)), false, fc.session);
|
func(@ptrCast(@alignCast(value_ptr)), false, fc.session);
|
||||||
origin.release(value_ptr);
|
fc.releaseIdentity();
|
||||||
} 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.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub const Env = @import("Env.zig");
|
|||||||
pub const bridge = @import("bridge.zig");
|
pub const bridge = @import("bridge.zig");
|
||||||
pub const Caller = @import("Caller.zig");
|
pub const Caller = @import("Caller.zig");
|
||||||
pub const Origin = @import("Origin.zig");
|
pub const Origin = @import("Origin.zig");
|
||||||
|
pub const Identity = @import("Identity.zig");
|
||||||
pub const Context = @import("Context.zig");
|
pub const Context = @import("Context.zig");
|
||||||
pub const Local = @import("Local.zig");
|
pub const Local = @import("Local.zig");
|
||||||
pub const Inspector = @import("Inspector.zig");
|
pub const Inspector = @import("Inspector.zig");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div id="host2"></div>
|
<div id="host2"></div>
|
||||||
<div id="host3"></div>
|
<div id="host3"></div>
|
||||||
|
|
||||||
<!-- <script id="attachShadow_open">
|
<script id="attachShadow_open">
|
||||||
{
|
{
|
||||||
const host = $('#host1');
|
const host = $('#host1');
|
||||||
const shadow = host.attachShadow({ mode: 'open' });
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
shadow.replaceChildren('New content');
|
shadow.replaceChildren('New content');
|
||||||
testing.expectEqual('New content', shadow.innerHTML);
|
testing.expectEqual('New content', shadow.innerHTML);
|
||||||
}
|
}
|
||||||
</script> -->
|
</script>
|
||||||
|
|
||||||
<script id="getElementById">
|
<script id="getElementById">
|
||||||
{
|
{
|
||||||
@@ -154,3 +154,16 @@
|
|||||||
testing.expectEqual(null, shadow.getElementById('nonexistent'));
|
testing.expectEqual(null, shadow.getElementById('nonexistent'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script id=adoptedStyleSheets>
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const acss = shadow.adoptedStyleSheets;
|
||||||
|
testing.expectEqual(0, acss.length);
|
||||||
|
acss.push(new CSSStyleSheet());
|
||||||
|
testing.expectEqual(1, acss.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page)
|
|||||||
return local.resolvePromise(definition.constructor);
|
return local.resolvePromise(definition.constructor);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateName(name) catch |err| {
|
validateName(name) catch |err| switch (err) {
|
||||||
return local.rejectPromise(DOMException.fromError(err) orelse unreachable);
|
error.SyntaxError => return local.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const gop = try self._when_defined.getOrPut(page.arena, name);
|
const gop = try self._when_defined.getOrPut(page.arena, name);
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ pub fn getStorage(self: *Navigator) *StorageManager {
|
|||||||
|
|
||||||
pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise {
|
pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise {
|
||||||
log.info(.not_implemented, "navigator.getBattery", .{});
|
log.info(.not_implemented, "navigator.getBattery", .{});
|
||||||
return page.js.local.?.rejectErrorPromise(.{ .dom_exception = error.NotSupported });
|
return page.js.local.?.rejectErrorPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
|
pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ _mode: Mode,
|
|||||||
_host: *Element,
|
_host: *Element,
|
||||||
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
|
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
|
||||||
_removed_ids: std.StringHashMapUnmanaged(void) = .{},
|
_removed_ids: std.StringHashMapUnmanaged(void) = .{},
|
||||||
|
_adopted_style_sheets: ?js.Object.Global = null,
|
||||||
|
|
||||||
pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {
|
pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {
|
||||||
return page._factory.documentFragment(ShadowRoot{
|
return page._factory.documentFragment(ShadowRoot{
|
||||||
@@ -99,6 +100,20 @@ pub fn getElementById(self: *ShadowRoot, id: []const u8, page: *Page) ?*Element
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getAdoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object.Global {
|
||||||
|
if (self._adopted_style_sheets) |ass| {
|
||||||
|
return ass;
|
||||||
|
}
|
||||||
|
const js_arr = page.js.local.?.newArray(0);
|
||||||
|
const js_obj = js_arr.toObject();
|
||||||
|
self._adopted_style_sheets = try js_obj.persist();
|
||||||
|
return self._adopted_style_sheets.?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setAdoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void {
|
||||||
|
self._adopted_style_sheets = try sheets.persist();
|
||||||
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(ShadowRoot);
|
pub const bridge = js.Bridge(ShadowRoot);
|
||||||
|
|
||||||
@@ -121,6 +136,7 @@ pub const JsApi = struct {
|
|||||||
}
|
}
|
||||||
return self.getElementById(try value.toZig([]const u8), page);
|
return self.getElementById(try value.toZig([]const u8), page);
|
||||||
}
|
}
|
||||||
|
pub const adoptedStyleSheets = bridge.accessor(ShadowRoot.getAdoptedStyleSheets, ShadowRoot.setAdoptedStyleSheets, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ pub fn generateKey(
|
|||||||
key_usages: []const []const u8,
|
key_usages: []const []const u8,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch |err| {
|
const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch {
|
||||||
return page.js.local.?.rejectPromise(@errorName(err));
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } });
|
||||||
};
|
};
|
||||||
|
|
||||||
return page.js.local.?.resolvePromise(key_or_pair);
|
return page.js.local.?.resolvePromise(key_or_pair);
|
||||||
@@ -112,7 +112,7 @@ pub fn exportKey(
|
|||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
if (!key.canExportKey()) {
|
if (!key.canExportKey()) {
|
||||||
return error.InvalidAccessError;
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, format, "raw")) {
|
if (std.mem.eql(u8, format, "raw")) {
|
||||||
@@ -124,9 +124,10 @@ pub fn exportKey(
|
|||||||
|
|
||||||
if (is_unsupported) {
|
if (is_unsupported) {
|
||||||
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format });
|
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format });
|
||||||
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return page.js.local.?.rejectPromise(@errorName(error.NotSupported));
|
return page.js.local.?.rejectPromise(.{ .type_error = "invalid format" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive a secret key from a master key.
|
/// Derive a secret key from a master key.
|
||||||
@@ -148,7 +149,7 @@ pub fn deriveBits(
|
|||||||
log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name });
|
log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name });
|
||||||
}
|
}
|
||||||
|
|
||||||
return page.js.local.?.rejectPromise(@errorName(error.NotSupported));
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -185,19 +186,19 @@ pub fn sign(
|
|||||||
.hmac => {
|
.hmac => {
|
||||||
// Verify algorithm.
|
// Verify algorithm.
|
||||||
if (!algorithm.isHMAC()) {
|
if (!algorithm.isHMAC()) {
|
||||||
return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError));
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call sign for HMAC.
|
// Call sign for HMAC.
|
||||||
const result = key.signHMAC(data, page) catch |err| {
|
const result = key.signHMAC(data, page) catch {
|
||||||
return page.js.local.?.rejectPromise(@errorName(err));
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
};
|
};
|
||||||
|
|
||||||
return page.js.local.?.resolvePromise(result);
|
return page.js.local.?.resolvePromise(result);
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
|
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
|
||||||
return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError));
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -211,18 +212,20 @@ pub fn verify(
|
|||||||
data: []const u8, // ArrayBuffer.
|
data: []const u8, // ArrayBuffer.
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
if (!algorithm.isHMAC()) return error.InvalidAccessError;
|
if (!algorithm.isHMAC()) {
|
||||||
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
|
}
|
||||||
|
|
||||||
return switch (key._type) {
|
return switch (key._type) {
|
||||||
.hmac => key.verifyHMAC(signature, data, page),
|
.hmac => key.verifyHMAC(signature, data, page),
|
||||||
else => return error.InvalidAccessError,
|
else => page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise {
|
pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise {
|
||||||
const local = page.js.local.?;
|
const local = page.js.local.?;
|
||||||
if (algorithm.len > 10) {
|
if (algorithm.len > 10) {
|
||||||
return local.rejectPromise(DOMException.fromError(error.NotSupported));
|
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
}
|
}
|
||||||
const normalized = std.ascii.lowerString(&page.buf, algorithm);
|
const normalized = std.ascii.lowerString(&page.buf, algorithm);
|
||||||
if (std.mem.eql(u8, normalized, "sha-1")) {
|
if (std.mem.eql(u8, normalized, "sha-1")) {
|
||||||
@@ -245,7 +248,7 @@ pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray
|
|||||||
Sha512.hash(data.values, page.buf[0..Sha512.digest_length], .{});
|
Sha512.hash(data.values, page.buf[0..Sha512.digest_length], .{});
|
||||||
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha512.digest_length] });
|
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha512.digest_length] });
|
||||||
}
|
}
|
||||||
return local.rejectPromise(DOMException.fromError(error.NotSupported));
|
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the desired digest by its name.
|
/// Returns the desired digest by its name.
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ _on_pageshow: ?js.Function.Global = null,
|
|||||||
_on_popstate: ?js.Function.Global = null,
|
_on_popstate: ?js.Function.Global = null,
|
||||||
_on_error: ?js.Function.Global = null,
|
_on_error: ?js.Function.Global = null,
|
||||||
_on_message: ?js.Function.Global = null,
|
_on_message: ?js.Function.Global = null,
|
||||||
_on_unhandled_rejection: ?js.Function.Global = null, // TODO: invoke on error
|
_on_rejection_handled: ?js.Function.Global = null,
|
||||||
|
_on_unhandled_rejection: ?js.Function.Global = null,
|
||||||
_current_event: ?*Event = null,
|
_current_event: ?*Event = null,
|
||||||
_location: *Location,
|
_location: *Location,
|
||||||
_timer_id: u30 = 0,
|
_timer_id: u30 = 0,
|
||||||
@@ -222,6 +223,14 @@ pub fn setOnMessage(self: *Window, setter: ?FunctionSetter) void {
|
|||||||
self._on_message = getFunctionFromSetter(setter);
|
self._on_message = getFunctionFromSetter(setter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getOnRejectionHandled(self: *const Window) ?js.Function.Global {
|
||||||
|
return self._on_rejection_handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnRejectionHandled(self: *Window, setter: ?FunctionSetter) void {
|
||||||
|
self._on_rejection_handled = getFunctionFromSetter(setter);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getOnUnhandledRejection(self: *const Window) ?js.Function.Global {
|
pub fn getOnUnhandledRejection(self: *const Window) ?js.Function.Global {
|
||||||
return self._on_unhandled_rejection;
|
return self._on_unhandled_rejection;
|
||||||
}
|
}
|
||||||
@@ -572,7 +581,7 @@ pub fn scrollBy(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
|||||||
return self.scrollTo(.{ .x = absx }, absy, page);
|
return self.scrollTo(.{ .x = absx }, absy, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection, page: *Page) !void {
|
pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.PromiseRejection, page: *Page) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.js, "unhandled rejection", .{
|
log.debug(.js, "unhandled rejection", .{
|
||||||
.value = rejection.reason(),
|
.value = rejection.reason(),
|
||||||
@@ -580,13 +589,20 @@ pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const event_name, const attribute_callback = blk: {
|
||||||
|
if (no_handler) {
|
||||||
|
break :blk .{ "unhandledrejection", self._on_unhandled_rejection };
|
||||||
|
}
|
||||||
|
break :blk .{ "rejectionhandled", self._on_rejection_handled };
|
||||||
|
};
|
||||||
|
|
||||||
const target = self.asEventTarget();
|
const target = self.asEventTarget();
|
||||||
if (page._event_manager.hasDirectListeners(target, "unhandledrejection", self._on_unhandled_rejection)) {
|
if (page._event_manager.hasDirectListeners(target, event_name, attribute_callback)) {
|
||||||
const event = (try @import("event/PromiseRejectionEvent.zig").init("unhandledrejection", .{
|
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
|
||||||
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
||||||
.promise = try rejection.promise().temp(),
|
.promise = try rejection.promise().temp(),
|
||||||
}, page)).asEvent();
|
}, page)).asEvent();
|
||||||
try page._event_manager.dispatchDirect(target, event, self._on_unhandled_rejection, .{ .context = "window.unhandledrejection" });
|
try page._event_manager.dispatchDirect(target, event, attribute_callback, .{ .context = "window.unhandledrejection" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,6 +829,7 @@ pub const JsApi = struct {
|
|||||||
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
|
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
|
||||||
pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{});
|
pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{});
|
||||||
pub const onmessage = bridge.accessor(Window.getOnMessage, Window.setOnMessage, .{});
|
pub const onmessage = bridge.accessor(Window.getOnMessage, Window.setOnMessage, .{});
|
||||||
|
pub const onrejectionhandled = bridge.accessor(Window.getOnRejectionHandled, Window.setOnRejectionHandled, .{});
|
||||||
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
|
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
|
||||||
pub const event = bridge.accessor(Window.getEvent, null, .{ .null_as_undefined = true });
|
pub const event = bridge.accessor(Window.getEvent, null, .{ .null_as_undefined = true });
|
||||||
pub const fetch = bridge.function(Window.fetch, .{});
|
pub const fetch = bridge.function(Window.fetch, .{});
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
|||||||
defer ls.deinit();
|
defer ls.deinit();
|
||||||
|
|
||||||
// fetch() must reject with a TypeError on network errors per spec
|
// fetch() must reject with a TypeError on network errors per spec
|
||||||
ls.toLocal(self._resolver).rejectError("fetch error", .{ .type_error = @errorName(err) });
|
ls.toLocal(self._resolver).rejectError("fetch error", .{ .type_error = "fetch error" });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn httpShutdownCallback(ctx: *anyopaque) void {
|
fn httpShutdownCallback(ctx: *anyopaque) void {
|
||||||
|
|||||||
@@ -192,8 +192,8 @@ pub fn text(self: *const Request, page: *Page) !js.Promise {
|
|||||||
pub fn json(self: *const Request, page: *Page) !js.Promise {
|
pub fn json(self: *const Request, page: *Page) !js.Promise {
|
||||||
const body = self._body orelse "";
|
const body = self._body orelse "";
|
||||||
const local = page.js.local.?;
|
const local = page.js.local.?;
|
||||||
const value = local.parseJSON(body) catch |err| {
|
const value = local.parseJSON(body) catch {
|
||||||
return local.rejectPromise(.{@errorName(err)});
|
return local.rejectPromise(.{ .syntax_error = "failed to parse" });
|
||||||
};
|
};
|
||||||
return local.resolvePromise(try value.persist());
|
return local.resolvePromise(try value.persist());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ pub fn getText(self: *const Response, page: *Page) !js.Promise {
|
|||||||
pub fn getJson(self: *Response, page: *Page) !js.Promise {
|
pub fn getJson(self: *Response, page: *Page) !js.Promise {
|
||||||
const body = self._body orelse "";
|
const body = self._body orelse "";
|
||||||
const local = page.js.local.?;
|
const local = page.js.local.?;
|
||||||
const value = local.parseJSON(body) catch |err| {
|
const value = local.parseJSON(body) catch {
|
||||||
return local.rejectPromise(.{@errorName(err)});
|
return local.rejectPromise(.{ .syntax_error = "failed to parse" });
|
||||||
};
|
};
|
||||||
return local.resolvePromise(try value.persist());
|
return local.resolvePromise(try value.persist());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ pub fn pipeThrough(self: *ReadableStream, transform: PipeTransform, page: *Page)
|
|||||||
/// Returns a promise that resolves when piping is complete.
|
/// Returns a promise that resolves when piping is complete.
|
||||||
pub fn pipeTo(self: *ReadableStream, destination: *WritableStream, page: *Page) !js.Promise {
|
pub fn pipeTo(self: *ReadableStream, destination: *WritableStream, page: *Page) !js.Promise {
|
||||||
if (self.getLocked()) {
|
if (self.getLocked()) {
|
||||||
return page.js.local.?.rejectPromise("ReadableStream is locked");
|
return page.js.local.?.rejectPromise(.{ .type_error = "ReadableStream is locked" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const local = page.js.local.?;
|
const local = page.js.local.?;
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ pub const ReadResult = struct {
|
|||||||
|
|
||||||
pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
|
pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
|
||||||
const stream = self._stream orelse {
|
const stream = self._stream orelse {
|
||||||
return page.js.local.?.rejectPromise("Reader has been released");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Reader has been released" });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (stream._state == .errored) {
|
if (stream._state == .errored) {
|
||||||
const err = stream._stored_error orelse "Stream errored";
|
//const err = stream._stored_error orelse "Stream errored";
|
||||||
return page.js.local.?.rejectPromise(err);
|
return page.js.local.?.rejectPromise(.{ .type_error = "Stream errored" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stream._controller.dequeue()) |chunk| {
|
if (stream._controller.dequeue()) |chunk| {
|
||||||
@@ -95,7 +95,7 @@ pub fn releaseLock(self: *ReadableStreamDefaultReader) void {
|
|||||||
|
|
||||||
pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise {
|
pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise {
|
||||||
const stream = self._stream orelse {
|
const stream = self._stream orelse {
|
||||||
return page.js.local.?.rejectPromise("Reader has been released");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Reader has been released" });
|
||||||
};
|
};
|
||||||
|
|
||||||
self.releaseLock();
|
self.releaseLock();
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ pub fn init(stream: *WritableStream, page: *Page) !*WritableStreamDefaultWriter
|
|||||||
|
|
||||||
pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, page: *Page) !js.Promise {
|
pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, page: *Page) !js.Promise {
|
||||||
const stream = self._stream orelse {
|
const stream = self._stream orelse {
|
||||||
return page.js.local.?.rejectPromise("Writer has been released");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Writer has been released" });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (stream._state != .writable) {
|
if (stream._state != .writable) {
|
||||||
return page.js.local.?.rejectPromise("Stream is not writable");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Stream is not writable" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try stream.writeChunk(chunk, page);
|
try stream.writeChunk(chunk, page);
|
||||||
@@ -46,11 +46,11 @@ pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, page: *Page) !
|
|||||||
|
|
||||||
pub fn close(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
|
pub fn close(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
|
||||||
const stream = self._stream orelse {
|
const stream = self._stream orelse {
|
||||||
return page.js.local.?.rejectPromise("Writer has been released");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Writer has been released" });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (stream._state != .writable) {
|
if (stream._state != .writable) {
|
||||||
return page.js.local.?.rejectPromise("Stream is not writable");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Stream is not writable" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try stream.closeStream(page);
|
try stream.closeStream(page);
|
||||||
@@ -67,7 +67,7 @@ pub fn releaseLock(self: *WritableStreamDefaultWriter) void {
|
|||||||
|
|
||||||
pub fn getClosed(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
|
pub fn getClosed(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
|
||||||
const stream = self._stream orelse {
|
const stream = self._stream orelse {
|
||||||
return page.js.local.?.rejectPromise("Writer has been released");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Writer has been released" });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (stream._state == .closed) {
|
if (stream._state == .closed) {
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
|||||||
// timeouts (or http events) which are ready to be processed.
|
// timeouts (or http events) which are ready to be processed.
|
||||||
pub fn pageWait(self: *Self, ms: u32) Session.WaitResult {
|
pub fn pageWait(self: *Self, ms: u32) Session.WaitResult {
|
||||||
const session = &(self.browser.session orelse return .no_page);
|
const session = &(self.browser.session orelse return .no_page);
|
||||||
return session.wait(ms);
|
return session.wait(.{ .timeout_ms = ms });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called from above, in processMessage which handles client messages
|
// Called from above, in processMessage which handles client messages
|
||||||
@@ -489,12 +489,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
|
|
||||||
pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld {
|
pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld {
|
||||||
const browser = &self.cdp.browser;
|
const browser = &self.cdp.browser;
|
||||||
const arena = try browser.arena_pool.acquire();
|
const arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld" });
|
||||||
errdefer browser.arena_pool.release(arena);
|
errdefer browser.arena_pool.release(arena);
|
||||||
|
|
||||||
|
const call_arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld.call_arena" });
|
||||||
|
errdefer browser.arena_pool.release(call_arena);
|
||||||
|
|
||||||
const world = try arena.create(IsolatedWorld);
|
const world = try arena.create(IsolatedWorld);
|
||||||
world.* = .{
|
world.* = .{
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
|
.call_arena = call_arena,
|
||||||
.context = null,
|
.context = null,
|
||||||
.browser = browser,
|
.browser = browser,
|
||||||
.name = try arena.dupe(u8, world_name),
|
.name = try arena.dupe(u8, world_name),
|
||||||
@@ -745,13 +749,20 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
/// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
|
/// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
|
||||||
const IsolatedWorld = struct {
|
const IsolatedWorld = struct {
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
|
call_arena: Allocator,
|
||||||
browser: *Browser,
|
browser: *Browser,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
context: ?*js.Context = null,
|
context: ?*js.Context = null,
|
||||||
grant_universal_access: bool,
|
grant_universal_access: bool,
|
||||||
|
|
||||||
|
// Identity tracking for this isolated world (separate from main world).
|
||||||
|
// This ensures CDP inspector contexts don't share v8::Globals with main world.
|
||||||
|
identity: js.Identity = .{},
|
||||||
|
|
||||||
pub fn deinit(self: *IsolatedWorld) void {
|
pub fn deinit(self: *IsolatedWorld) void {
|
||||||
self.removeContext() catch {};
|
self.removeContext() catch {};
|
||||||
|
self.identity.deinit();
|
||||||
|
self.browser.arena_pool.release(self.call_arena);
|
||||||
self.browser.arena_pool.release(self.arena);
|
self.browser.arena_pool.release(self.arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,6 +770,8 @@ const IsolatedWorld = struct {
|
|||||||
const ctx = self.context orelse return error.NoIsolatedContextToRemove;
|
const ctx = self.context orelse return error.NoIsolatedContextToRemove;
|
||||||
self.browser.env.destroyContext(ctx);
|
self.browser.env.destroyContext(ctx);
|
||||||
self.context = null;
|
self.context = null;
|
||||||
|
self.identity.deinit();
|
||||||
|
self.identity = .{};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
|
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
|
||||||
@@ -768,7 +781,13 @@ const IsolatedWorld = struct {
|
|||||||
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
|
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
|
||||||
pub fn createContext(self: *IsolatedWorld, page: *Page) !*js.Context {
|
pub fn createContext(self: *IsolatedWorld, page: *Page) !*js.Context {
|
||||||
if (self.context == null) {
|
if (self.context == null) {
|
||||||
self.context = try self.browser.env.createContext(page);
|
const ctx = try self.browser.env.createContext(page, .{
|
||||||
|
.identity = &self.identity,
|
||||||
|
.identity_arena = self.arena,
|
||||||
|
.call_arena = self.call_arena,
|
||||||
|
.debug_name = "IsolatedContext",
|
||||||
|
});
|
||||||
|
self.context = ctx;
|
||||||
} else {
|
} else {
|
||||||
log.warn(.cdp, "not implemented", .{
|
log.warn(.cdp, "not implemented", .{
|
||||||
.feature = "createContext: Not implemented second isolated context creation",
|
.feature = "createContext: Not implemented second isolated context creation",
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ test "cdp.lp: action tools" {
|
|||||||
const page = try bc.session.createPage();
|
const page = try bc.session.createPage();
|
||||||
const url = "http://localhost:9582/src/browser/tests/mcp_actions.html";
|
const url = "http://localhost:9582/src/browser/tests/mcp_actions.html";
|
||||||
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
|
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
|
||||||
_ = bc.session.wait(5000);
|
_ = bc.session.wait(.{});
|
||||||
|
|
||||||
// Test Click
|
// Test Click
|
||||||
const btn = page.document.getElementById("btn", page).?.asNode();
|
const btn = page.document.getElementById("btn", page).?.asNode();
|
||||||
|
|||||||
@@ -394,15 +394,13 @@ fn sendMessageToTarget(cmd: anytype) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn detachFromTarget(cmd: anytype) !void {
|
fn detachFromTarget(cmd: anytype) !void {
|
||||||
// TODO check if sessionId/targetId match.
|
|
||||||
// const params = (try cmd.params(struct {
|
|
||||||
// sessionId: ?[]const u8,
|
|
||||||
// targetId: ?[]const u8,
|
|
||||||
// })) orelse return error.InvalidParams;
|
|
||||||
|
|
||||||
if (cmd.browser_context) |bc| {
|
if (cmd.browser_context) |bc| {
|
||||||
|
if (bc.session_id) |session_id| {
|
||||||
|
try cmd.sendEvent("Target.detachedFromTarget", .{
|
||||||
|
.sessionId = session_id,
|
||||||
|
}, .{});
|
||||||
|
}
|
||||||
bc.session_id = null;
|
bc.session_id = null;
|
||||||
// TODO should we send a Target.detachedFromTarget event?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd.sendResult(null, .{});
|
return cmd.sendResult(null, .{});
|
||||||
@@ -427,8 +425,12 @@ fn setAutoAttach(cmd: anytype) !void {
|
|||||||
if (cmd.cdp.target_auto_attach == false) {
|
if (cmd.cdp.target_auto_attach == false) {
|
||||||
// detach from all currently attached targets.
|
// detach from all currently attached targets.
|
||||||
if (cmd.browser_context) |bc| {
|
if (cmd.browser_context) |bc| {
|
||||||
|
if (bc.session_id) |session_id| {
|
||||||
|
try cmd.sendEvent("Target.detachedFromTarget", .{
|
||||||
|
.sessionId = session_id,
|
||||||
|
}, .{});
|
||||||
|
}
|
||||||
bc.session_id = null;
|
bc.session_id = null;
|
||||||
// TODO should we send a Target.detachedFromTarget event?
|
|
||||||
}
|
}
|
||||||
try cmd.sendResult(null, .{});
|
try cmd.sendResult(null, .{});
|
||||||
return;
|
return;
|
||||||
@@ -759,9 +761,11 @@ test "cdp.target: detachFromTarget" {
|
|||||||
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
||||||
|
|
||||||
try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
|
try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
|
||||||
try ctx.expectSentResult(.{ .sessionId = bc.session_id.? }, .{ .id = 11 });
|
const session_id = bc.session_id.?;
|
||||||
|
try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });
|
||||||
|
|
||||||
try ctx.processMessage(.{ .id = 12, .method = "Target.detachFromTarget", .params = .{ .targetId = bc.target_id.? } });
|
try ctx.processMessage(.{ .id = 12, .method = "Target.detachFromTarget", .params = .{ .targetId = bc.target_id.? } });
|
||||||
|
try ctx.expectSentEvent("Target.detachedFromTarget", .{ .sessionId = session_id }, .{});
|
||||||
try testing.expectEqual(null, bc.session_id);
|
try testing.expectEqual(null, bc.session_id);
|
||||||
try ctx.expectSentResult(null, .{ .id = 12 });
|
try ctx.expectSentResult(null, .{ .id = 12 });
|
||||||
|
|
||||||
@@ -769,3 +773,36 @@ test "cdp.target: detachFromTarget" {
|
|||||||
try ctx.expectSentResult(.{ .sessionId = bc.session_id.? }, .{ .id = 13 });
|
try ctx.expectSentResult(.{ .sessionId = bc.session_id.? }, .{ .id = 13 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "cdp.target: detachFromTarget without session" {
|
||||||
|
var ctx = testing.context();
|
||||||
|
defer ctx.deinit();
|
||||||
|
_ = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||||
|
{
|
||||||
|
// detach when no session is attached should not send event
|
||||||
|
try ctx.processMessage(.{ .id = 10, .method = "Target.detachFromTarget" });
|
||||||
|
try ctx.expectSentResult(null, .{ .id = 10 });
|
||||||
|
try ctx.expectSentCount(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "cdp.target: setAutoAttach false sends detachedFromTarget" {
|
||||||
|
var ctx = testing.context();
|
||||||
|
defer ctx.deinit();
|
||||||
|
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||||
|
{
|
||||||
|
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||||
|
try testing.expectEqual(true, bc.target_id != null);
|
||||||
|
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
||||||
|
|
||||||
|
try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
|
||||||
|
const session_id = bc.session_id.?;
|
||||||
|
try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });
|
||||||
|
|
||||||
|
// setAutoAttach false should fire detachedFromTarget event
|
||||||
|
try ctx.processMessage(.{ .id = 12, .method = "Target.setAutoAttach", .params = .{ .autoAttach = false, .waitForDebuggerOnStart = false } });
|
||||||
|
try ctx.expectSentEvent("Target.detachedFromTarget", .{ .sessionId = session_id }, .{});
|
||||||
|
try testing.expectEqual(null, bc.session_id);
|
||||||
|
try ctx.expectSentResult(null, .{ .id = 12 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ const TestContext = struct {
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
try page.navigate(full_url, .{});
|
try page.navigate(full_url, .{});
|
||||||
_ = bc.session.wait(2000);
|
_ = bc.session.wait(.{});
|
||||||
}
|
}
|
||||||
return bc;
|
return bc;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
|||||||
|
|
||||||
pub const FetchOpts = struct {
|
pub const FetchOpts = struct {
|
||||||
wait_ms: u32 = 5000,
|
wait_ms: u32 = 5000,
|
||||||
|
wait_until: Config.WaitUntil = .load,
|
||||||
dump: dump.Opts,
|
dump: dump.Opts,
|
||||||
dump_mode: ?Config.DumpFormat = null,
|
dump_mode: ?Config.DumpFormat = null,
|
||||||
writer: ?*std.Io.Writer = null,
|
writer: ?*std.Io.Writer = null,
|
||||||
@@ -107,7 +108,7 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
|
|||||||
.reason = .address_bar,
|
.reason = .address_bar,
|
||||||
.kind = .{ .push = null },
|
.kind = .{ .push = null },
|
||||||
});
|
});
|
||||||
_ = session.wait(opts.wait_ms);
|
_ = session.wait(.{ .timeout_ms = opts.wait_ms, .until = opts.wait_until });
|
||||||
|
|
||||||
const writer = opts.writer orelse return;
|
const writer = opts.writer orelse return;
|
||||||
if (opts.dump_mode) |mode| {
|
if (opts.dump_mode) |mode| {
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
|||||||
log.debug(.app, "startup", .{ .mode = "fetch", .dump_mode = opts.dump_mode, .url = url, .snapshot = app.snapshot.fromEmbedded() });
|
log.debug(.app, "startup", .{ .mode = "fetch", .dump_mode = opts.dump_mode, .url = url, .snapshot = app.snapshot.fromEmbedded() });
|
||||||
|
|
||||||
var fetch_opts = lp.FetchOpts{
|
var fetch_opts = lp.FetchOpts{
|
||||||
.wait_ms = 5000,
|
.wait_ms = opts.wait_ms,
|
||||||
|
.wait_until = opts.wait_until,
|
||||||
.dump_mode = opts.dump_mode,
|
.dump_mode = opts.dump_mode,
|
||||||
.dump = .{
|
.dump = .{
|
||||||
.strip = opts.strip,
|
.strip = opts.strip,
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ pub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void {
|
|||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
try page.navigate(url, .{});
|
try page.navigate(url, .{});
|
||||||
_ = session.wait(2000);
|
_ = session.wait(.{});
|
||||||
|
|
||||||
ls.local.eval("testing.assertOk()", "testing.assertOk()") catch |err| {
|
ls.local.eval("testing.assertOk()", "testing.assertOk()") catch |err| {
|
||||||
const caught = try_catch.caughtOrError(allocator, err);
|
const caught = try_catch.caughtOrError(allocator, err);
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ pub fn sendResult(self: *Self, id: std.json.Value, result: anytype) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn sendError(self: *Self, id: std.json.Value, code: protocol.ErrorCode, message: []const u8) !void {
|
pub fn sendError(self: *Self, id: std.json.Value, code: protocol.ErrorCode, message: []const u8) !void {
|
||||||
try self.sendResponse(.{
|
try self.sendResponse(protocol.Response{
|
||||||
.id = id,
|
.id = id,
|
||||||
.@"error" = protocol.Error{
|
.@"error" = protocol.Error{
|
||||||
.code = @intFromEnum(code),
|
.code = @intFromEnum(code),
|
||||||
@@ -114,7 +114,7 @@ test "MCP.Server - Integration: synchronous smoke test" {
|
|||||||
|
|
||||||
try router.processRequests(server, &in_reader);
|
try router.processRequests(server, &in_reader);
|
||||||
|
|
||||||
try testing.expectJson(.{ .id = 1 }, out_alloc.writer.buffered());
|
try testing.expectJson(.{ .jsonrpc = "2.0", .id = 1 }, out_alloc.writer.buffered());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "MCP.Server - Integration: ping request returns an empty result" {
|
test "MCP.Server - Integration: ping request returns an empty result" {
|
||||||
@@ -135,5 +135,5 @@ test "MCP.Server - Integration: ping request returns an empty result" {
|
|||||||
|
|
||||||
try router.processRequests(server, &in_reader);
|
try router.processRequests(server, &in_reader);
|
||||||
|
|
||||||
try testing.expectJson(.{ .id = "ping-1", .result = .{} }, out_alloc.writer.buffered());
|
try testing.expectJson(.{ .jsonrpc = "2.0", .id = "ping-1", .result = .{} }, out_alloc.writer.buffered());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ test "MCP.router - handleMessage - synchronous unit tests" {
|
|||||||
\\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}}
|
\\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}}
|
||||||
);
|
);
|
||||||
try testing.expectJson(
|
try testing.expectJson(
|
||||||
\\{ "id": 1, "result": { "capabilities": { "tools": {} } } }
|
\\{ "jsonrpc": "2.0", "id": 1, "result": { "capabilities": { "tools": {} } } }
|
||||||
, out_alloc.writer.buffered());
|
, out_alloc.writer.buffered());
|
||||||
out_alloc.writer.end = 0;
|
out_alloc.writer.end = 0;
|
||||||
|
|
||||||
@@ -128,14 +128,14 @@ test "MCP.router - handleMessage - synchronous unit tests" {
|
|||||||
try handleMessage(server, aa,
|
try handleMessage(server, aa,
|
||||||
\\{"jsonrpc":"2.0","id":2,"method":"ping"}
|
\\{"jsonrpc":"2.0","id":2,"method":"ping"}
|
||||||
);
|
);
|
||||||
try testing.expectJson(.{ .id = 2, .result = .{} }, out_alloc.writer.buffered());
|
try testing.expectJson(.{ .jsonrpc = "2.0", .id = 2, .result = .{} }, out_alloc.writer.buffered());
|
||||||
out_alloc.writer.end = 0;
|
out_alloc.writer.end = 0;
|
||||||
|
|
||||||
// 3. Tools list
|
// 3. Tools list
|
||||||
try handleMessage(server, aa,
|
try handleMessage(server, aa,
|
||||||
\\{"jsonrpc":"2.0","id":3,"method":"tools/list"}
|
\\{"jsonrpc":"2.0","id":3,"method":"tools/list"}
|
||||||
);
|
);
|
||||||
try testing.expectJson(.{ .id = 3 }, out_alloc.writer.buffered());
|
try testing.expectJson(.{ .jsonrpc = "2.0", .id = 3 }, out_alloc.writer.buffered());
|
||||||
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"name\":\"goto\"") != null);
|
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"name\":\"goto\"") != null);
|
||||||
out_alloc.writer.end = 0;
|
out_alloc.writer.end = 0;
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ test "MCP.router - handleMessage - synchronous unit tests" {
|
|||||||
try handleMessage(server, aa,
|
try handleMessage(server, aa,
|
||||||
\\{"jsonrpc":"2.0","id":4,"method":"unknown_method"}
|
\\{"jsonrpc":"2.0","id":4,"method":"unknown_method"}
|
||||||
);
|
);
|
||||||
try testing.expectJson(.{ .id = 4, .@"error" = .{ .code = -32601 } }, out_alloc.writer.buffered());
|
try testing.expectJson(.{ .jsonrpc = "2.0", .id = 4, .@"error" = .{ .code = -32601 } }, out_alloc.writer.buffered());
|
||||||
out_alloc.writer.end = 0;
|
out_alloc.writer.end = 0;
|
||||||
|
|
||||||
// 5. Parse error
|
// 5. Parse error
|
||||||
@@ -152,6 +152,6 @@ test "MCP.router - handleMessage - synchronous unit tests" {
|
|||||||
defer filter.deinit();
|
defer filter.deinit();
|
||||||
|
|
||||||
try handleMessage(server, aa, "invalid json");
|
try handleMessage(server, aa, "invalid json");
|
||||||
try testing.expectJson("{\"id\": null, \"error\": {\"code\": -32700}}", out_alloc.writer.buffered());
|
try testing.expectJson("{\"jsonrpc\": \"2.0\", \"id\": null, \"error\": {\"code\": -32700}}", out_alloc.writer.buffered());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -558,7 +558,7 @@ fn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value) !void {
|
|||||||
return error.NavigationFailed;
|
return error.NavigationFailed;
|
||||||
};
|
};
|
||||||
|
|
||||||
_ = server.session.wait(5000);
|
_ = server.session.wait(.{});
|
||||||
}
|
}
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
@@ -623,7 +623,7 @@ test "MCP - Actions: click, fill, scroll" {
|
|||||||
const page = try server.session.createPage();
|
const page = try server.session.createPage();
|
||||||
const url = "http://localhost:9582/src/browser/tests/mcp_actions.html";
|
const url = "http://localhost:9582/src/browser/tests/mcp_actions.html";
|
||||||
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
|
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
|
||||||
_ = server.session.wait(5000);
|
_ = server.session.wait(.{});
|
||||||
|
|
||||||
// Test Click
|
// Test Click
|
||||||
const btn = page.document.getElementById("btn", page).?.asNode();
|
const btn = page.document.getElementById("btn", page).?.asNode();
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
|
|||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
try page.navigate(url, .{});
|
try page.navigate(url, .{});
|
||||||
_ = test_session.wait(2000);
|
_ = test_session.wait(.{});
|
||||||
|
|
||||||
test_browser.runMicrotasks();
|
test_browser.runMicrotasks();
|
||||||
|
|
||||||
@@ -439,7 +439,7 @@ pub fn pageTest(comptime test_file: []const u8) !*Page {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try page.navigate(url, .{});
|
try page.navigate(url, .{});
|
||||||
_ = test_session.wait(2000);
|
_ = test_session.wait(.{});
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user