Improve MutationObserver

- Fix get_removedNodes (it was returning addedNodes)
- get_removedNodes and get addedNodes now return references
- used enum for dispatching and clean up dispatching in general
- Remove MutationRecords and simply return an array
  - this allows the returned records to be iterable (as they should be)
  - jsruntime ZigToJs will now map a Zig array to a JS array
-Rely on default initialize of NodeList
-Batch observed records
 - Callback only executed when call_depth == 0
 - Fixes crashes when a MutationObserver callback mutated the nodes being
   observes.
 - Fixes some WPT issues, but Netsurf's mutationEventRelatedNode does not
   appear to be to spec, so most tests fail.
 - Allow zig methods to execute arbitrary code when call_depth == 0
   - This is a preview of how I hope to make XHR not crash if the CDP session
     ends while there's still network activity
This commit is contained in:
Karl Seguin
2025-04-21 08:53:46 +08:00
parent 332508f563
commit 4c89bb0e0a
7 changed files with 269 additions and 467 deletions

View File

@@ -661,11 +661,22 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.void, .bool, .int, .comptime_int, .float, .comptime_float, .array => {
.void, .bool, .int, .comptime_int, .float, .comptime_float => {
// Need to do this to keep the compiler happy
// simpleZigValueToJs handles all of these cases.
unreachable;
},
.array => {
var js_arr = v8.Array.init(isolate, value.len);
var js_obj = js_arr.castTo(v8.Object);
for (value, 0..) |v, i| {
const js_val = try zigValueToJs(templates, isolate, context, v);
if (js_obj.setValueAtIndex(context, @intCast(i), js_val) == false) {
return error.FailedToCreateArray;
}
}
return js_obj.toValue();
},
.pointer => |ptr| switch (ptr.size) {
.one => {
const type_name = @typeName(ptr.child);
@@ -1018,6 +1029,10 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
return gop.value_ptr.*;
}
if (comptime @hasDecl(ptr.child, "jsScopeEnd")) {
try scope.scope_end_callbacks.append(scope_arena, ScopeEndCallback.init(value));
}
// Sometimes we're creating a new v8.Object, like when
// we're returning a value from a function. In those cases
// we have the FunctionTemplate, and we can get an object
@@ -1131,6 +1146,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
pub const Scope = struct {
arena: Allocator,
handle_scope: v8.HandleScope,
scope_end_callbacks: std.ArrayListUnmanaged(ScopeEndCallback) = .{},
callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .{},
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{},
@@ -1150,6 +1166,34 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
}
};
// An interface for types that want to their jsScopeEnd function to be
// called when the scope ends
const ScopeEndCallback = struct {
ptr: *anyopaque,
scopeEndFn: *const fn (ptr: *anyopaque, executor: *Executor) void,
fn init(ptr: anytype) ScopeEndCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn scopeEnd(pointer: *anyopaque, executor: *Executor) void {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.jsScopeEnd(self, executor);
}
};
return .{
.ptr = ptr,
.scopeEndFn = gen.scopeEnd,
};
}
pub fn scopeEnd(self: ScopeEndCallback, executor: *Executor) void {
self.scopeEndFn(self.ptr, executor);
}
};
pub const Callback = struct {
id: usize,
executor: *Executor,
@@ -1572,7 +1616,6 @@ fn Caller(comptime E: type) type {
fn deinit(self: *Self) void {
const executor = self.executor;
const call_depth = executor.call_depth - 1;
executor.call_depth = call_depth;
// Because of callbacks, calls can be nested. Because of this, we
// can't clear the call_arena after _every_ call. Imagine we have
@@ -1585,9 +1628,20 @@ fn Caller(comptime E: type) type {
//
// Therefore, we keep a call_depth, and only reset the call_arena
// when a top-level (call_depth == 0) function ends.
if (call_depth == 0) {
const scope = &self.executor.scope.?;
for (scope.scope_end_callbacks.items) |cb| {
cb.scopeEnd(executor);
}
_ = executor._call_arena_instance.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
// Set this _after_ we've executed the above code, so that if the
// above code executes any callbacks, they aren't being executed
// at scope 0, which would be wrong.
executor.call_depth = call_depth;
}
fn constructor(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void {