From 4ff55a8cff45b67f8f2ad681cf968fdf8160996a Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 22 Apr 2026 12:47:44 +0800 Subject: [PATCH] More Worker APIs Enable URL, AbortController and AbortSignal for Workers. Had a large impact because Event.initTrusted was changed from taking a *Frame to taking a *Session. Also, make WGS and Frame have a matching `dispatch` method so that it's easier to dispatch events against an executor (inline else =>). Similarly, added dupeString directly to executor to make migrations easier. --- src/browser/EventManager.zig | 4 +- src/browser/Frame.zig | 24 +++++++++--- src/browser/ScriptManager.zig | 2 +- src/browser/actions.zig | 6 +-- src/browser/js/Execution.zig | 10 +++++ src/browser/js/bridge.zig | 6 +-- src/browser/tests/worker/api-worker.js | 28 ++++++++++++++ src/browser/tests/worker/worker.html | 11 ++++++ src/browser/webapi/AbortController.zig | 13 ++++--- src/browser/webapi/AbortSignal.zig | 46 ++++++++++++----------- src/browser/webapi/Event.zig | 6 +-- src/browser/webapi/EventTarget.zig | 8 ++-- src/browser/webapi/URL.zig | 33 +++++++++------- src/browser/webapi/Window.zig | 4 +- src/browser/webapi/WorkerGlobalScope.zig | 32 ++++++++++++---- src/browser/webapi/css/FontFaceSet.zig | 4 +- src/browser/webapi/net/WebSocket.zig | 4 +- src/browser/webapi/net/XMLHttpRequest.zig | 2 +- 18 files changed, 167 insertions(+), 76 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 1b4231fb..994550e9 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -124,7 +124,7 @@ pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, co // property is just a shortcut for calling addEventListener, but they are distinct. // An event set via property cannot be removed by removeEventListener. If you // set both the property and add a listener, they both execute. -const DispatchDirectOptions = EventManagerBase.DispatchDirectOptions; +pub const DispatchDirectOptions = EventManagerBase.DispatchDirectOptions; // Direct dispatch for non-DOM targets (Window, XHR, AbortSignal) or DOM nodes with // property handlers. No propagation - just calls the handler and registered listeners. @@ -617,7 +617,7 @@ const ActivationState = struct { const event = try Event.initTrusted(comptime .wrap(typ), .{ .bubbles = true, .cancelable = false, - }, frame); + }, frame._session); const target = input.asElement().asEventTarget(); try frame._event_manager.dispatch(target, event); diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index b0f9e07a..8f1b120b 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -35,6 +35,7 @@ const URL = @import("URL.zig"); const Blob = @import("webapi/Blob.zig"); const Node = @import("webapi/Node.zig"); const Event = @import("webapi/Event.zig"); +const EventTarget = @import("webapi/EventTarget.zig"); const CData = @import("webapi/CData.zig"); const Element = @import("webapi/Element.zig"); const HtmlElement = @import("webapi/element/Html.zig"); @@ -806,7 +807,7 @@ pub fn documentIsLoaded(self: *Frame) void { } pub fn _documentIsLoaded(self: *Frame) !void { - const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self); + const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self._session); try self._event_manager.dispatch( self.document.asEventTarget(), event, @@ -833,7 +834,7 @@ pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame) void { defer entered.exit(); blk: { - const event = Event.initTrusted(comptime .wrap("load"), .{}, self) catch |err| { + const event = Event.initTrusted(comptime .wrap("load"), .{}, self._session) catch |err| { log.err(.frame, "iframe event init", .{ .err = err, .url = iframe._src }); break :blk; }; @@ -887,7 +888,7 @@ fn _documentIsComplete(self: *Frame) !void { // Dispatch window.load event. const window_target = self.window.asEventTarget(); if (self._event_manager.hasDirectListeners(window_target, "load", self.window._on_load)) { - const event = try Event.initTrusted(comptime .wrap("load"), .{}, self); + const event = try Event.initTrusted(comptime .wrap("load"), .{}, self._session); // This event is weird, it's dispatched directly on the window, but // with the document as the target. event._target = self.document.asEventTarget(); @@ -1467,7 +1468,7 @@ pub fn dispatchLoad(self: *Frame) !void { for (to_process.items) |html_element| { if (has_dom_load_listener or html_element.hasAttributeFunction(.onload, self)) { - const event = try Event.initTrusted(comptime .wrap("load"), .{}, self); + const event = try Event.initTrusted(comptime .wrap("load"), .{}, self._session); try self._event_manager.dispatch(html_element.asEventTarget(), event); } } @@ -1578,7 +1579,7 @@ pub fn deliverSlotchangeEvents(self: *Frame) void { self._slots_pending_slotchange.clearRetainingCapacity(); for (slots) |slot| { - const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self) catch |err| { + const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self._session) catch |err| { log.err(.frame, "deliverSlotchange.init", .{ .err = err, .type = self._type, .url = self.url }); continue; }; @@ -2604,6 +2605,19 @@ pub fn dupeString(self: *Frame, value: []const u8) ![]const u8 { return self.arena.dupe(u8, value); } +// Direct (non-propagating) dispatch of an event. Mirrors WorkerGlobalScope.dispatch +// so worker-compatible APIs can uniformly call `global.dispatch(...)` across both +// Frame and Worker contexts. +pub fn dispatch( + self: *Frame, + target: *EventTarget, + event: *Event, + handler: anytype, + comptime opts: EventManager.DispatchDirectOptions, +) !void { + return self._event_manager.dispatchDirect(target, event, handler, opts); +} + pub fn dupeSSO(self: *Frame, value: []const u8) !String { return String.init(self.arena, value, .{ .dupe = true }); } diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 24fd7642..327ce69b 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -949,7 +949,7 @@ pub const Script = struct { fn executeCallback(self: *const Script, typ: String, frame: *Frame) void { const Event = @import("webapi/Event.zig"); - const event = Event.initTrusted(typ, .{}, frame) catch |err| { + const event = Event.initTrusted(typ, .{}, frame._session) catch |err| { log.warn(.js, "script internal callback", .{ .url = self.url, .type = typ, diff --git a/src/browser/actions.zig b/src/browser/actions.zig index cca8b9a0..d09cf57b 100644 --- a/src/browser/actions.zig +++ b/src/browser/actions.zig @@ -28,12 +28,12 @@ const Session = @import("Session.zig"); const Selector = @import("webapi/selector/Selector.zig"); fn dispatchInputAndChangeEvents(el: *Element, frame: *Frame) !void { - const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, frame); + const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, frame._session); frame._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| { lp.log.err(.app, "dispatch input event failed", .{ .err = err }); }; - const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, frame); + const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, frame._session); frame._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| { lp.log.err(.app, "dispatch change event failed", .{ .err = err }); }; @@ -196,7 +196,7 @@ pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, frame: *Frame) !void { }; } - const scroll_evt: *Event = try .initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, frame); + const scroll_evt: *Event = try .initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, frame._session); frame._event_manager.dispatch(el.asEventTarget(), scroll_evt) catch |err| { lp.log.err(.app, "dispatch scroll event failed", .{ .err = err }); }; diff --git a/src/browser/js/Execution.zig b/src/browser/js/Execution.zig index 6acfe455..e752e904 100644 --- a/src/browser/js/Execution.zig +++ b/src/browser/js/Execution.zig @@ -26,10 +26,13 @@ //! whether it's a Page context or a Worker context. const std = @import("std"); +const lp = @import("lightpanda"); + const Context = @import("Context.zig"); const Scheduler = @import("Scheduler.zig"); const Factory = @import("../Factory.zig"); +const String = lp.String; const Allocator = std.mem.Allocator; const Execution = @This(); @@ -53,3 +56,10 @@ charset: *[]const u8, pub fn base(self: *const Execution) [:0]const u8 { return self.context.global.base(); } + +pub fn dupeString(self: *const Execution, value: []const u8) ![]const u8 { + if (String.intern(value)) |v| { + return v; + } + return self.arena.dupe(u8, value); +} diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 32ac0de2..e61818c7 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -933,10 +933,10 @@ pub const WorkerJsApis = flattenTypes(&.{ @import("../webapi/streams/WritableStreamDefaultController.zig"), @import("../webapi/encoding/TextEncoderStream.zig"), @import("../webapi/encoding/TextDecoderStream.zig"), - // @import("../webapi/URL.zig"), + @import("../webapi/AbortSignal.zig"), + @import("../webapi/AbortController.zig"), + @import("../webapi/URL.zig"), // @import("../webapi/Performance.zig"), - // @import("../webapi/AbortSignal.zig"), - // @import("../webapi/AbortController.zig"), }); // Master list of ALL JS APIs across all contexts. diff --git a/src/browser/tests/worker/api-worker.js b/src/browser/tests/worker/api-worker.js index 2c01d8fd..edfea446 100644 --- a/src/browser/tests/worker/api-worker.js +++ b/src/browser/tests/worker/api-worker.js @@ -29,6 +29,27 @@ const response_body_text = await response.text(); const response_clone_text = await response.clone().text(); + // AbortController + AbortSignal dispatch (exercises the inline-else dispatch path) + const controller = new AbortController(); + const ac_aborted_before = controller.signal.aborted; + let ac_listener_fired = false; + controller.signal.addEventListener('abort', () => { ac_listener_fired = true; }); + controller.abort('cancelled'); + const ac_aborted_after = controller.signal.aborted; + const ac_reason = String(controller.signal.reason); + + // Pre-aborted static constructor + throwIfAborted + const pre = AbortSignal.abort('already-gone'); + const pre_aborted = pre.aborted; + let pre_threw = false; + try { pre.throwIfAborted(); } catch (_) { pre_threw = true; } + + // URL.createObjectURL / revokeObjectURL from a worker + const blob = new Blob(['hello worker'], { type: 'text/plain' }); + const blob_url = URL.createObjectURL(blob); + const blob_url_is_blob = blob_url.startsWith('blob:'); + URL.revokeObjectURL(blob_url); + postMessage({ ok: true, results: { @@ -47,6 +68,13 @@ response_headers_content_type: response.headers.get('content-type'), response_body_text, response_clone_text, + ac_aborted_before, + ac_aborted_after, + ac_listener_fired, + ac_reason, + pre_aborted, + pre_threw, + blob_url_is_blob, }, }); } catch (e) { diff --git a/src/browser/tests/worker/worker.html b/src/browser/tests/worker/worker.html index c0960231..1d912060 100644 --- a/src/browser/tests/worker/worker.html +++ b/src/browser/tests/worker/worker.html @@ -209,6 +209,17 @@ testing.expectEqual('text/plain', r.response_headers_content_type); testing.expectEqual('response body', r.response_body_text); testing.expectEqual('response body', r.response_clone_text); + + // AbortController / AbortSignal + testing.expectEqual(false, r.ac_aborted_before); + testing.expectEqual(true, r.ac_aborted_after); + testing.expectEqual(true, r.ac_listener_fired); + testing.expectEqual('cancelled', r.ac_reason); + testing.expectEqual(true, r.pre_aborted); + testing.expectEqual(true, r.pre_threw); + + // URL.createObjectURL / revokeObjectURL + testing.expectEqual(true, r.blob_url_is_blob); }); } diff --git a/src/browser/webapi/AbortController.zig b/src/browser/webapi/AbortController.zig index d4d0d07f..0c751497 100644 --- a/src/browser/webapi/AbortController.zig +++ b/src/browser/webapi/AbortController.zig @@ -19,16 +19,17 @@ const std = @import("std"); const js = @import("../js/js.zig"); -const Frame = @import("../Frame.zig"); const AbortSignal = @import("AbortSignal.zig"); +const Execution = js.Execution; + const AbortController = @This(); _signal: *AbortSignal, -pub fn init(frame: *Frame) !*AbortController { - const signal = try AbortSignal.init(frame); - return frame._factory.create(AbortController{ +pub fn init(exec: *const Execution) !*AbortController { + const signal = try AbortSignal.init(exec); + return exec._factory.create(AbortController{ ._signal = signal, }); } @@ -37,8 +38,8 @@ pub fn getSignal(self: *const AbortController) *AbortSignal { return self._signal; } -pub fn abort(self: *AbortController, reason_: ?js.Value.Global, frame: *Frame) !void { - try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, frame); +pub fn abort(self: *AbortController, reason_: ?js.Value.Global, exec: *const Execution) !void { + try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, exec); } pub const JsApi = struct { diff --git a/src/browser/webapi/AbortSignal.zig b/src/browser/webapi/AbortSignal.zig index f4e554fc..8e1241e0 100644 --- a/src/browser/webapi/AbortSignal.zig +++ b/src/browser/webapi/AbortSignal.zig @@ -20,12 +20,12 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../js/js.zig"); -const Frame = @import("../Frame.zig"); const Event = @import("Event.zig"); const EventTarget = @import("EventTarget.zig"); const log = lp.log; +const Execution = js.Execution; const AbortSignal = @This(); @@ -34,8 +34,8 @@ _aborted: bool = false, _reason: Reason = .undefined, _on_abort: ?js.Function.Global = null, -pub fn init(frame: *Frame) !*AbortSignal { - return frame._factory.eventTarget(AbortSignal{ +pub fn init(exec: *const Execution) !*AbortSignal { + return exec._factory.eventTarget(AbortSignal{ ._proto = undefined, }); } @@ -60,7 +60,7 @@ pub fn asEventTarget(self: *AbortSignal) *EventTarget { return self._proto; } -pub fn abort(self: *AbortSignal, reason_: ?Reason, frame: *Frame) !void { +pub fn abort(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !void { if (self._aborted) { return; } @@ -71,36 +71,40 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, frame: *Frame) !void { if (reason_) |reason| { switch (reason) { .js_val => |js_val| self._reason = .{ .js_val = js_val }, - .string => |str| self._reason = .{ .string = try frame.dupeString(str) }, + .string => |str| self._reason = .{ .string = try exec.dupeString(str) }, .undefined => self._reason = reason, } } else { self._reason = .{ .string = "AbortError" }; } - // Dispatch abort event const target = self.asEventTarget(); - if (frame._event_manager.hasDirectListeners(target, "abort", self._on_abort)) { - const event = try Event.initTrusted(comptime .wrap("abort"), .{}, frame); - try frame._event_manager.dispatchDirect(target, event, self._on_abort, .{ .context = "abort signal" }); + const on_abort = self._on_abort; + switch (exec.context.global) { + inline else => |g| { + if (g._event_manager.hasDirectListeners(target, "abort", on_abort)) { + const event = try Event.initTrusted(comptime .wrap("abort"), .{}, g._session); + try g.dispatch(target, event, on_abort, .{ .context = "abort signal" }); + } + }, } } // Static method to create an already-aborted signal -pub fn createAborted(reason_: ?js.Value.Global, frame: *Frame) !*AbortSignal { - const signal = try init(frame); - try signal.abort(if (reason_) |r| .{ .js_val = r } else null, frame); +pub fn createAborted(reason_: ?js.Value.Global, exec: *const Execution) !*AbortSignal { + const signal = try init(exec); + try signal.abort(if (reason_) |r| .{ .js_val = r } else null, exec); return signal; } -pub fn createTimeout(delay: u32, frame: *Frame) !*AbortSignal { - const callback = try frame.arena.create(TimeoutCallback); +pub fn createTimeout(delay: u32, exec: *const Execution) !*AbortSignal { + const callback = try exec.arena.create(TimeoutCallback); callback.* = .{ - .frame = frame, - .signal = try init(frame), + .exec = exec, + .signal = try init(exec), }; - try frame.js.scheduler.add(callback, TimeoutCallback.run, delay, .{ + try exec._scheduler.add(callback, TimeoutCallback.run, delay, .{ .name = "AbortSignal.timeout", }); @@ -111,8 +115,8 @@ const ThrowIfAborted = union(enum) { exception: js.Exception, undefined: void, }; -pub fn throwIfAborted(self: *const AbortSignal, frame: *Frame) !ThrowIfAborted { - const local = frame.js.local.?; +pub fn throwIfAborted(self: *const AbortSignal, exec: *const Execution) !ThrowIfAborted { + const local = exec.context.local.?; if (self._aborted) { const exception = switch (self._reason) { @@ -132,12 +136,12 @@ const Reason = union(enum) { }; const TimeoutCallback = struct { - frame: *Frame, + exec: *const Execution, signal: *AbortSignal, fn run(ctx: *anyopaque) !?u32 { const self: *TimeoutCallback = @ptrCast(@alignCast(ctx)); - self.signal.abort(.{ .string = "TimeoutError" }, self.frame) catch |err| { + self.signal.abort(.{ .string = "TimeoutError" }, self.exec) catch |err| { log.warn(.app, "abort signal timeout", .{ .err = err }); }; return null; diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index f8117e39..b3053b8e 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -96,9 +96,9 @@ pub fn init(typ: []const u8, opts_: ?Options, frame: *Frame) !*Event { return initWithTrusted(arena, str, opts_, false); } -pub fn initTrusted(typ: String, opts_: ?Options, frame: *Frame) !*Event { - const arena = try frame.getArena(.tiny, "Event.trusted"); - errdefer frame.releaseArena(arena); +pub fn initTrusted(typ: String, opts_: ?Options, session: *Session) !*Event { + const arena = try session.getArena(.tiny, "Event.trusted"); + errdefer session.releaseArena(arena); return initWithTrusted(arena, typ, opts_, true); } diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index f7effdd6..2b52c3ec 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -70,7 +70,7 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, exec: *js.Execution) !bo defer _ = event.releaseRef(frame._session); try frame._event_manager.dispatch(self, event); }, - .worker => |wgs| try wgs.dispatch(self, event, null), + .worker => |wgs| try wgs.dispatch(self, event, null, .{}), } return !event._cancelable or !event._prevent_default; } @@ -101,8 +101,7 @@ pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventLi }; switch (exec.context.global) { - .frame => |frame| _ = try frame._event_manager.register(self, typ, em_callback, options), - .worker => |wgs| _ = try wgs._event_manager.register(self, typ, em_callback, options), + inline else => |g| _ = try g._event_manager.register(self, typ, em_callback, options), } } @@ -138,8 +137,7 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?Even }; switch (exec.context.global) { - .frame => |frame| frame._event_manager.remove(self, typ, em_callback, use_capture), - .worker => |wgs| wgs._event_manager.remove(self, typ, em_callback, use_capture), + inline else => |g| g._event_manager.remove(self, typ, em_callback, use_capture), } } diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index bb1f712e..76f12142 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -20,7 +20,6 @@ const std = @import("std"); const js = @import("../js/js.zig"); const U = @import("../URL.zig"); -const Frame = @import("../Frame.zig"); const URLSearchParams = @import("net/URLSearchParams.zig"); const Blob = @import("Blob.zig"); const Execution = js.Execution; @@ -248,28 +247,36 @@ pub fn canParse(url: []const u8, base_: ?[]const u8) bool { return U.isCompleteHTTPUrl(url); } -pub fn createObjectURL(blob: *Blob, frame: *Frame) ![]const u8 { +pub fn createObjectURL(blob: *Blob, exec: *const Execution) ![]const u8 { var uuid_buf: [36]u8 = undefined; @import("../../id.zig").uuidv4(&uuid_buf); - const blob_url = try std.fmt.allocPrint( - frame.arena, - "blob:{s}/{s}", - .{ frame.origin orelse "null", uuid_buf }, - ); - try frame._blob_urls.put(frame.arena, blob_url, blob); - blob.acquireRef(); - return blob_url; + switch (exec.context.global) { + inline else => |g| { + const blob_url = try std.fmt.allocPrint( + g.arena, + "blob:{s}/{s}", + .{ g.origin orelse "null", uuid_buf }, + ); + try g._blob_urls.put(g.arena, blob_url, blob); + blob.acquireRef(); + return blob_url; + }, + } } -pub fn revokeObjectURL(url: []const u8, frame: *Frame) void { +pub fn revokeObjectURL(url: []const u8, exec: *const Execution) void { // Per spec: silently ignore non-blob URLs if (!std.mem.startsWith(u8, url, "blob:")) { return; } - if (frame._blob_urls.fetchRemove(url)) |entry| { - entry.value.releaseRef(frame._session); + switch (exec.context.global) { + inline else => |g| { + if (g._blob_urls.fetchRemove(url)) |entry| { + entry.value.releaseRef(g._session); + } + }, } } diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index ea53a242..96df3ea7 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -538,7 +538,7 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, frame: *Frame) !void return null; } - const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, p); + const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, p._session); try p._event_manager.dispatch(p.document.asEventTarget(), event); pos.state = .end; @@ -565,7 +565,7 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, frame: *Frame) !void .end => {}, .done => return null, } - const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, p); + const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, p._session); try p._event_manager.dispatch(p.document.asEventTarget(), event); pos.state = .done; diff --git a/src/browser/webapi/WorkerGlobalScope.zig b/src/browser/webapi/WorkerGlobalScope.zig index 9aa0f3a3..e6c35b05 100644 --- a/src/browser/webapi/WorkerGlobalScope.zig +++ b/src/browser/webapi/WorkerGlobalScope.zig @@ -28,6 +28,7 @@ const Factory = @import("../Factory.zig"); const Session = @import("../Session.zig"); const EventManagerBase = @import("../EventManagerBase.zig"); +const Blob = @import("Blob.zig"); const Worker = @import("Worker.zig"); const Crypto = @import("Crypto.zig"); const Console = @import("Console.zig"); @@ -52,11 +53,16 @@ _identity: JS.Identity = .{}, arena: Allocator, call_arena: Allocator, url: [:0]const u8, +// Same-origin constraint: a worker's origin is inherited from its parent frame. +origin: ?[]const u8 = null, buf: [1024]u8 = undefined, // same size as frame.buf // Document charset (matches Page.charset). Workers default to UTF-8. charset: []const u8 = "UTF-8", js: *JS.Context, +// Blob URL registry for URL.createObjectURL/revokeObjectURL. +_blob_urls: std.StringHashMapUnmanaged(*Blob) = .{}, + // Reference back to the Worker object (for postMessage to frame) _worker: *Worker, @@ -76,7 +82,8 @@ _on_messageerror: ?JS.Function.Global = null, pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope { const arena = worker._arena; - const session = worker._frame._session; + const parent = worker._frame; + const session = parent._session; const factory = &session.factory; const call_arena = try session.getArena(.small, "WorkerGlobalScope.call_arena"); @@ -85,6 +92,7 @@ pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope { const self = try factory.eventTargetWithAllocator(arena, WorkerGlobalScope{ .url = url, .arena = arena, + .origin = parent.origin, .js = undefined, .call_arena = call_arena, ._session = session, @@ -108,6 +116,10 @@ pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope { pub fn deinit(self: *WorkerGlobalScope) void { self._identity.deinit(); const session = self._session; + var it = self._blob_urls.valueIterator(); + while (it.next()) |blob| { + blob.*.releaseRef(session); + } session.browser.env.destroyContext(self.js); session.releaseArena(self.call_arena); } @@ -123,7 +135,13 @@ pub fn asEventTarget(self: *WorkerGlobalScope) *EventTarget { const Event = @import("Event.zig"); // Dispatch an event to listeners on the given target within this worker context. -pub fn dispatch(self: *WorkerGlobalScope, target: *EventTarget, event: *Event, handler: anytype) !void { +pub fn dispatch( + self: *WorkerGlobalScope, + target: *EventTarget, + event: *Event, + handler: anytype, + comptime opts: EventManagerBase.DispatchDirectOptions, +) !void { try self._event_manager.dispatchDirect( self.call_arena, self.js, @@ -131,7 +149,7 @@ pub fn dispatch(self: *WorkerGlobalScope, target: *EventTarget, event: *Event, h event, handler, self._session, - .{}, + opts, ); } @@ -265,7 +283,7 @@ pub fn unhandledPromiseRejection(self: *WorkerGlobalScope, no_handler: bool, rej .reason = if (rejection.reason()) |r| try r.temp() else null, .promise = try rejection.promise().temp(), }, self._session)).asEvent(); - try self.dispatch(target, event, attribute_callback); + try self.dispatch(target, event, attribute_callback, .{}); } } @@ -311,7 +329,7 @@ pub fn reportError(self: *WorkerGlobalScope, err: JS.Value) !void { event._prevent_default = prevent_default; // Pass null as handler: onerror was already called above with 5 args. // We still dispatch so that addEventListener('error', ...) listeners fire. - try self.dispatch(self.asEventTarget(), event, null); + try self.dispatch(self.asEventTarget(), event, null, .{}); if (comptime builtin.is_test == false) { if (!event._prevent_default) { @@ -375,7 +393,7 @@ const ReceiveMessageCallback = struct { .bubbles = false, .cancelable = false, }, worker_scope._session)).asEvent(); - try worker_scope.dispatch(target, event, on_messageerror); + try worker_scope.dispatch(target, event, on_messageerror, .{}); return null; } @@ -392,7 +410,7 @@ const ReceiveMessageCallback = struct { .bubbles = false, .cancelable = false, }, worker_scope._session)).asEvent(); - try worker_scope.dispatch(target, event, on_message); + try worker_scope.dispatch(target, event, on_message, .{}); return null; } }; diff --git a/src/browser/webapi/css/FontFaceSet.zig b/src/browser/webapi/css/FontFaceSet.zig index e5009c85..6d4695da 100644 --- a/src/browser/webapi/css/FontFaceSet.zig +++ b/src/browser/webapi/css/FontFaceSet.zig @@ -80,13 +80,13 @@ pub fn load(self: *FontFaceSet, font: []const u8, frame: *Frame) !js.Promise { // Dispatch loading event const target = self.asEventTarget(); if (frame._event_manager.hasDirectListeners(target, "loading", null)) { - const event = try Event.initTrusted(comptime .wrap("loading"), .{}, frame); + const event = try Event.initTrusted(comptime .wrap("loading"), .{}, frame._session); try frame._event_manager.dispatchDirect(target, event, null, .{ .context = "load font face set" }); } // Dispatch loadingdone event if (frame._event_manager.hasDirectListeners(target, "loadingdone", null)) { - const event = try Event.initTrusted(comptime .wrap("loadingdone"), .{}, frame); + const event = try Event.initTrusted(comptime .wrap("loadingdone"), .{}, frame._session); try frame._event_manager.dispatchDirect(target, event, null, .{ .context = "load font face set" }); } diff --git a/src/browser/webapi/net/WebSocket.zig b/src/browser/webapi/net/WebSocket.zig index 74bd668b..7338234f 100644 --- a/src/browser/webapi/net/WebSocket.zig +++ b/src/browser/webapi/net/WebSocket.zig @@ -452,7 +452,7 @@ fn dispatchOpenEvent(self: *WebSocket) !void { const target = self.asEventTarget(); if (frame._event_manager.hasDirectListeners(target, "open", self._on_open)) { - const event = try Event.initTrusted(comptime .wrap("open"), .{}, frame); + const event = try Event.initTrusted(comptime .wrap("open"), .{}, frame._session); try frame._event_manager.dispatchDirect(target, event, self._on_open, .{ .context = "WebSocket open" }); } } @@ -487,7 +487,7 @@ fn dispatchErrorEvent(self: *WebSocket) !void { const target = self.asEventTarget(); if (frame._event_manager.hasDirectListeners(target, "error", self._on_error)) { - const event = try Event.initTrusted(comptime .wrap("error"), .{}, frame); + const event = try Event.initTrusted(comptime .wrap("error"), .{}, frame._session); try frame._event_manager.dispatchDirect(target, event, self._on_error, .{ .context = "WebSocket error" }); } } diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 660be153..10dc62f4 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -588,7 +588,7 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, frame: *Frame) !void { const target = self.asEventTarget(); if (frame._event_manager.hasDirectListeners(target, "readystatechange", self._on_ready_state_change)) { - const event = try Event.initTrusted(.wrap("readystatechange"), .{}, frame); + const event = try Event.initTrusted(.wrap("readystatechange"), .{}, frame._session); try frame._event_manager.dispatchDirect(target, event, self._on_ready_state_change, .{ .context = "XHR state change" }); } }