Skip to content

Commit fb9cce7

Browse files
committed
Scripts now properly block rendering
Re-enabled CDP tests Fixed more tests
1 parent 1a04ebc commit fb9cce7

File tree

14 files changed

+171
-201
lines changed

14 files changed

+171
-201
lines changed

src/browser/EventManager.zig

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,6 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
130130
}
131131

132132
fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
133-
if (event._bubbles == false) {
134-
event._event_phase = .at_target;
135-
const target_et = target.asEventTarget();
136-
if (self.lookup.getPtr(@intFromPtr(target_et))) |list| {
137-
try self.dispatchPhase(list, target_et, event, null);
138-
}
139-
event._event_phase = .none;
140-
return;
141-
}
142-
143133
var path_len: usize = 0;
144134
var path_buffer: [128]*EventTarget = undefined;
145135

@@ -150,7 +140,8 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
150140
path_len += 1;
151141
}
152142

153-
// Even though the window isn't part of the DOM, events bubble to it
143+
// Even though the window isn't part of the DOM, events always propagate
144+
// through it in the capture phase
154145
if (path_len < path_buffer.len) {
155146
path_buffer[path_len] = self.page.window.asEventTarget();
156147
path_len += 1;
@@ -159,6 +150,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
159150
const path = path_buffer[0..path_len];
160151

161152
// Phase 1: Capturing phase (root → target, excluding target)
153+
// This happens for all events, regardless of bubbling
162154
event._event_phase = .capturing_phase;
163155
var i: usize = path_len;
164156
while (i > 1) {
@@ -173,6 +165,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
173165
}
174166
}
175167

168+
// Phase 2: At target
176169
event._event_phase = .at_target;
177170
const target_et = target.asEventTarget();
178171
if (self.lookup.getPtr(@intFromPtr(target_et))) |list| {
@@ -183,12 +176,16 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
183176
}
184177
}
185178

186-
event._event_phase = .bubbling_phase;
187-
for (path[1..]) |current_target| {
188-
if (self.lookup.getPtr(@intFromPtr(current_target))) |list| {
189-
try self.dispatchPhase(list, current_target, event, false);
190-
if (event._stop_propagation) {
191-
break;
179+
// Phase 3: Bubbling phase (target → root, excluding target)
180+
// This only happens if the event bubbles
181+
if (event._bubbles) {
182+
event._event_phase = .bubbling_phase;
183+
for (path[1..]) |current_target| {
184+
if (self.lookup.getPtr(@intFromPtr(current_target))) |list| {
185+
try self.dispatchPhase(list, current_target, event, false);
186+
if (event._stop_propagation) {
187+
break;
188+
}
192189
}
193190
}
194191
}

src/browser/ScriptManager.zig

Lines changed: 73 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

1919
const std = @import("std");
20+
const builtin = @import("builtin");
2021

2122
const js = @import("js/js.zig");
2223
const log = @import("../log.zig");
@@ -31,11 +32,13 @@ const Element = @import("webapi/Element.zig");
3132
const Allocator = std.mem.Allocator;
3233
const ArrayListUnmanaged = std.ArrayListUnmanaged;
3334

35+
const IS_DEBUG = builtin.mode == .Debug;
36+
3437
const ScriptManager = @This();
3538

3639
page: *Page,
3740

38-
// used to prevent recursive evalutaion
41+
// used to prevent recursive evaluation
3942
is_evaluating: bool,
4043

4144
// Only once this is true can deferred scripts be run
@@ -45,9 +48,6 @@ static_scripts_done: bool,
4548
// on shutdown/abort, we need to cleanup any pending ones.
4649
asyncs: OrderList,
4750

48-
// Normal scripts (non-deferred & non-async). These must be executed in order
49-
scripts: OrderList,
50-
5151
// List of deferred scripts. These must be executed in order, but only once
5252
// dom_loaded == true,
5353
deferreds: OrderList,
@@ -85,7 +85,6 @@ pub fn init(page: *Page) ScriptManager {
8585
return .{
8686
.page = page,
8787
.asyncs = .{},
88-
.scripts = .{},
8988
.deferreds = .{},
9089
.importmap = .empty,
9190
.sync_modules = .empty,
@@ -130,7 +129,6 @@ pub fn reset(self: *ScriptManager) void {
130129
self.importmap = .empty;
131130

132131
self.clearList(&self.asyncs);
133-
self.clearList(&self.scripts);
134132
self.clearList(&self.deferreds);
135133
self.static_scripts_done = false;
136134
}
@@ -212,57 +210,78 @@ pub fn add(self: *ScriptManager, script_element: *Element.Html.Script, comptime
212210
.is_async = if (remote_url == null) false else element.getAttributeSafe("async") != null,
213211
};
214212

215-
if (source == .@"inline" and self.scripts.first == null) {
216-
// inline script with no pending scripts, execute it immediately.
217-
// (if there is a pending script, then we cannot execute this immediately
218-
// as it needs to be executed in order)
213+
if (source == .@"inline") {
214+
// inline script gets executed immediately
219215
return script.eval(page);
220216
}
221217

222-
const pending_script = try self.script_pool.create();
223-
errdefer self.script_pool.destroy(pending_script);
224-
pending_script.* = .{
225-
.script = script,
226-
.complete = false,
227-
.manager = self,
228-
.node = .{},
229-
};
218+
const pending_script = blk: {
219+
// Done in a block this way so that, if something fails in this block
220+
// it's cleaned up with these errdefers
221+
// BUT, if we need to load/execute the script immediately, cleanup/lifetimes
222+
// become the responsibility of the outer block.
223+
const pending_script = try self.script_pool.create();
224+
errdefer self.script_pool.destroy(pending_script);
225+
226+
pending_script.* = .{
227+
.script = script,
228+
.complete = false,
229+
.manager = self,
230+
.node = .{},
231+
};
232+
errdefer pending_script.deinit();
230233

231-
if (source == .@"inline") {
232-
// if we're here, it means that we have pending scripts (i.e. self.scripts
233-
// is not empty). Because the script is inline, it's complete/ready, but
234-
// we need to process them in order
235-
pending_script.complete = true;
236-
self.scripts.append(&pending_script.node);
237-
return;
238-
} else {
239-
log.debug(.http, "script queue", .{
240-
.ctx = ctx,
234+
if (comptime IS_DEBUG) {
235+
log.debug(.http, "script queue", .{
236+
.ctx = ctx,
237+
.url = remote_url.?,
238+
.stack = page.js.stackTrace() catch "???",
239+
});
240+
}
241+
242+
var headers = try self.client.newHeaders();
243+
try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers);
244+
245+
try self.client.request(.{
241246
.url = remote_url.?,
242-
.stack = page.js.stackTrace() catch "???",
247+
.ctx = pending_script,
248+
.method = .GET,
249+
.headers = headers,
250+
.resource_type = .script,
251+
.cookie_jar = &page._session.cookie_jar,
252+
.start_callback = if (log.enabled(.http, .debug)) startCallback else null,
253+
.header_callback = headerCallback,
254+
.data_callback = dataCallback,
255+
.done_callback = doneCallback,
256+
.error_callback = errorCallback,
243257
});
244-
}
245258

246-
pending_script.getList().append(&pending_script.node);
259+
if (script.is_defer) {
260+
// non-blocking loading, track the list this belongs to, and return
261+
pending_script.list = &self.deferreds;
262+
return;
263+
}
247264

248-
errdefer pending_script.deinit();
265+
if (script.is_async) {
266+
// non-blocking loading, track the list this belongs to, and return
267+
pending_script.list = &self.asyncs;
268+
return;
269+
}
249270

250-
var headers = try self.client.newHeaders();
251-
try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers);
271+
break :blk pending_script;
272+
};
252273

253-
try self.client.request(.{
254-
.url = remote_url.?,
255-
.ctx = pending_script,
256-
.method = .GET,
257-
.headers = headers,
258-
.resource_type = .script,
259-
.cookie_jar = &page._session.cookie_jar,
260-
.start_callback = if (log.enabled(.http, .debug)) startCallback else null,
261-
.header_callback = headerCallback,
262-
.data_callback = dataCallback,
263-
.done_callback = doneCallback,
264-
.error_callback = errorCallback,
265-
});
274+
defer pending_script.deinit();
275+
276+
// this is <script src="..."></script>, it needs to block the caller
277+
// until it's evaluated
278+
var client = self.client;
279+
while (true) {
280+
if (pending_script.complete) {
281+
return pending_script.script.eval(page);
282+
}
283+
_ = try client.tick(200);
284+
}
266285
}
267286

268287
// Resolve a module specifier to an valid URL.
@@ -394,6 +413,7 @@ pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.C
394413
.error_callback = AsyncModule.errorCallback,
395414
});
396415
}
416+
397417
pub fn pageIsLoaded(self: *ScriptManager) void {
398418
std.debug.assert(self.static_scripts_done == false);
399419
self.static_scripts_done = true;
@@ -415,15 +435,6 @@ fn evaluate(self: *ScriptManager) void {
415435
self.is_evaluating = true;
416436
defer self.is_evaluating = false;
417437

418-
while (self.scripts.first) |n| {
419-
var pending_script: *PendingScript = @fieldParentPtr("node", n);
420-
if (pending_script.complete == false) {
421-
return;
422-
}
423-
defer pending_script.deinit();
424-
pending_script.script.eval(page);
425-
}
426-
427438
if (self.static_scripts_done == false) {
428439
// We can only execute deferred scripts if
429440
// 1 - all the normal scripts are done
@@ -460,7 +471,6 @@ fn evaluate(self: *ScriptManager) void {
460471
pub fn isDone(self: *const ScriptManager) bool {
461472
return self.asyncs.first == null and // there are no more async scripts
462473
self.static_scripts_done and // and we've finished parsing the HTML to queue all <scripts>
463-
self.scripts.first == null and // and there are no more <script src=> to wait for
464474
self.deferreds.first == null; // and there are no more <script defer src=> to wait for
465475
}
466476

@@ -536,12 +546,13 @@ fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
536546
// A script which is pending execution.
537547
// It could be pending because:
538548
// (a) we're still downloading its content or
539-
// (b) this is a non-async script that has to be executed in order
549+
// (b) it's a deferred script which has to be executed in order
540550
pub const PendingScript = struct {
541551
script: Script,
542552
complete: bool,
543553
node: OrderList.Node,
544554
manager: *ScriptManager,
555+
list: ?*std.DoublyLinkedList = null,
545556

546557
fn deinit(self: *PendingScript) void {
547558
const script = &self.script;
@@ -550,14 +561,11 @@ pub const PendingScript = struct {
550561
if (script.source == .remote) {
551562
manager.buffer_pool.release(script.source.remote);
552563
}
553-
self.getList().remove(&self.node);
554-
}
555564

556-
fn remove(self: *PendingScript) void {
557-
if (self.node) |*node| {
558-
self.getList().remove(node);
559-
self.node = null;
565+
if (self.list) |list| {
566+
list.remove(&self.node);
560567
}
568+
manager.script_pool.destroy(self);
561569
}
562570

563571
fn startCallback(self: *PendingScript, transfer: *Http.Transfer) !void {
@@ -614,6 +622,7 @@ pub const PendingScript = struct {
614622
manager.evaluate();
615623
return;
616624
}
625+
617626
// async script can be evaluated immediately
618627
self.script.eval(manager.page);
619628
self.deinit();
@@ -634,23 +643,6 @@ pub const PendingScript = struct {
634643

635644
manager.evaluate();
636645
}
637-
638-
fn getList(self: *const PendingScript) *OrderList {
639-
// When a script has both the async and defer flag set, it should be
640-
// treated as async. Async is newer, so some websites use both so that
641-
// if async isn't known, it'll fallback to defer.
642-
643-
const script = &self.script;
644-
if (script.is_async) {
645-
return &self.manager.asyncs;
646-
}
647-
648-
if (script.is_defer) {
649-
return &self.manager.deferreds;
650-
}
651-
652-
return &self.manager.scripts;
653-
}
654646
};
655647

656648
const Script = struct {

src/browser/js/Context.zig

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -571,16 +571,16 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_: ?v8.Object, value: anytype) !
571571
return self.mapZigInstanceToJs(js_obj_, heap);
572572
},
573573
.pointer => |ptr| {
574-
const gop = try self.identity_map.getOrPut(arena, @intFromPtr(value));
574+
const resolved = resolveValue(value);
575+
576+
const gop = try self.identity_map.getOrPut(arena, @intFromPtr(resolved.ptr));
575577
if (gop.found_existing) {
576578
// we've seen this instance before, return the same
577579
// PersistentObject.
578580
return gop.value_ptr.*;
579581
}
580582

581583
const isolate = self.isolate;
582-
const resolved = resolveValue(value);
583-
584584
// Sometimes we're creating a new v8.Object, like when
585585
// we're returning a value from a function. In those cases
586586
// we have to get the object template, and we can get an object
@@ -1874,11 +1874,6 @@ fn zigJsonToJs(isolate: v8.Isolate, v8_context: v8.Context, value: std.json.Valu
18741874
}
18751875
}
18761876

1877-
pub fn getGlobalThis(self: *Context) js.This {
1878-
const js_global = self.v8_context.getGlobal();
1879-
return .{ .obj = .{ .js_obj = js_global, .context = self } };
1880-
}
1881-
18821877
// == Misc ==
18831878
// An interface for types that want to have their jsDeinit function to be
18841879
// called when the call context ends

src/browser/js/Env.zig

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,11 @@ pub fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.Funct
241241
bridge.Function => {
242242
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
243243
const js_name: v8.Name = v8.String.initUtf8(isolate, name).toName();
244-
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
244+
if (value.static) {
245+
template.set(js_name, function_template, v8.PropertyAttribute.None);
246+
} else {
247+
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
248+
}
245249
},
246250
bridge.Indexed => {
247251
const configuration = v8.IndexedPropertyHandlerConfiguration{

0 commit comments

Comments
 (0)