From 0672fad797da2428ce1f126d13f6651396c847f4 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 31 Jul 2026 17:23:33 +0800 Subject: [PATCH] webapi: Add Scheduler (window and worker) This is a new API, but it's in all browser (nightly in Firefox). It's essentially a more powerful api than setTimeout, it has priority and cancellation. --- src/browser/Factory.zig | 7 + src/browser/js/Scheduler.zig | 3 +- src/browser/js/bridge.zig | 8 + src/browser/tests/scheduler/scheduler.html | 259 ++++++++++++++++ src/browser/webapi/AbortSignal.zig | 44 ++- src/browser/webapi/Event.zig | 2 + src/browser/webapi/Scheduler.zig | 284 ++++++++++++++++++ src/browser/webapi/TaskController.zig | 63 ++++ src/browser/webapi/TaskSignal.zig | 99 ++++++ src/browser/webapi/Window.zig | 7 + src/browser/webapi/WorkerGlobalScope.zig | 7 + .../webapi/event/TaskPriorityChangeEvent.zig | 92 ++++++ 12 files changed, 865 insertions(+), 10 deletions(-) create mode 100644 src/browser/tests/scheduler/scheduler.html create mode 100644 src/browser/webapi/Scheduler.zig create mode 100644 src/browser/webapi/TaskController.zig create mode 100644 src/browser/webapi/TaskSignal.zig create mode 100644 src/browser/webapi/event/TaskPriorityChangeEvent.zig diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 4c0a5a4a0..36092f97c 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -32,6 +32,7 @@ const MouseEvent = @import("webapi/event/MouseEvent.zig"); const Element = @import("webapi/Element.zig"); const Document = @import("webapi/Document.zig"); const EventTarget = @import("webapi/EventTarget.zig"); +const AbortSignal = @import("webapi/AbortSignal.zig"); const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig"); const Blob = @import("webapi/Blob.zig"); const AbstractRange = @import("webapi/AbstractRange.zig"); @@ -512,6 +513,12 @@ pub fn xhrEventTarget(_: *const Factory, allocator: Allocator, child: anytype) ! ).create(allocator, child); } +pub fn taskSignal(self: *Factory, child: anytype) !*@TypeOf(child) { + return try AutoPrototypeChain( + &.{ EventTarget, AbortSignal, @TypeOf(child) }, + ).create(self._slab.allocator(), child); +} + pub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) { const allocator = self._slab.allocator(); const TextTrackCue = @import("webapi/media/TextTrackCue.zig"); diff --git a/src/browser/js/Scheduler.zig b/src/browser/js/Scheduler.zig index 6a6170917..b564991f3 100644 --- a/src/browser/js/Scheduler.zig +++ b/src/browser/js/Scheduler.zig @@ -68,6 +68,7 @@ pub fn reset(self: *Scheduler) void { const AddOpts = struct { name: []const u8 = "", + front: bool = false, // run before any timed tasks, multi-fronts are FIFO amongst themselves low_priority: bool = false, finalizer: ?Finalizer = null, }; @@ -84,7 +85,7 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts .sequence = seq, .name = opts.name, .finalizer = opts.finalizer, - .run_at = lp.datetime.milliTimestamp(.boot) + run_in_ms, + .run_at = if (opts.front) 0 else lp.datetime.milliTimestamp(.boot) + run_in_ms, }); } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index bd2088395..e9c6a2d9e 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -932,6 +932,10 @@ pub const SubType = enum { pub const PageJsApis = flattenTypes(&.{ @import("../webapi/AbortController.zig"), @import("../webapi/AbortSignal.zig"), + @import("../webapi/Scheduler.zig"), + @import("../webapi/TaskController.zig"), + @import("../webapi/TaskSignal.zig"), + @import("../webapi/event/TaskPriorityChangeEvent.zig"), @import("../webapi/CData.zig"), @import("../webapi/cdata/Comment.zig"), @import("../webapi/cdata/Text.zig"), @@ -1259,6 +1263,10 @@ const worker_common_apis = [_]type{ @import("../webapi/encoding/TextDecoderStream.zig"), @import("../webapi/AbortSignal.zig"), @import("../webapi/AbortController.zig"), + @import("../webapi/Scheduler.zig"), + @import("../webapi/TaskController.zig"), + @import("../webapi/TaskSignal.zig"), + @import("../webapi/event/TaskPriorityChangeEvent.zig"), @import("../webapi/URL.zig"), @import("../webapi/canvas/OffscreenCanvas.zig"), @import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"), diff --git a/src/browser/tests/scheduler/scheduler.html b/src/browser/tests/scheduler/scheduler.html new file mode 100644 index 000000000..200785bdd --- /dev/null +++ b/src/browser/tests/scheduler/scheduler.html @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/AbortSignal.zig b/src/browser/webapi/AbortSignal.zig index 5e3fcf95d..c3664baa7 100644 --- a/src/browser/webapi/AbortSignal.zig +++ b/src/browser/webapi/AbortSignal.zig @@ -22,6 +22,7 @@ const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const Event = @import("Event.zig"); +const Scheduler = @import("Scheduler.zig"); const EventTarget = @import("EventTarget.zig"); const DOMException = @import("DOMException.zig"); const ModelContextTool = @import("ModelContext.zig").Tool; @@ -36,6 +37,9 @@ pub const Proto = EventTarget; const Dependend = union(enum) { signal: *AbortSignal, model_context_tool: *ModelContextTool, + // Handled by the owning signal's markAborted (which runs for dependent + // signals too, unlike this union's markAborted). + scheduler_task: *Scheduler.Task, // Returns false if the dependent was already aborted, in which case no // abort event must be dispatched for it. @@ -50,18 +54,20 @@ const Dependend = union(enum) { try dep.markAborted(exec); return true; }, + .scheduler_task => return false, } } fn dispatchAbortEvent(self: Dependend, exec: *const Execution) !void { switch (self) { .signal => |dep| try dep.dispatchAbortEvent(exec), - .model_context_tool => {}, + .model_context_tool, .scheduler_task => {}, } } }; _proto: *EventTarget, +_type: Type = .generic, _aborted: bool = false, _is_dependent: bool = false, _reason: Reason = .undefined, @@ -69,6 +75,11 @@ _on_abort: ?js.Function.Global = null, _dependents: std.ArrayList(Dependend) = .empty, _source_signals: std.ArrayList(*AbortSignal) = .empty, +pub const Type = union(enum) { + generic: void, + task_signal: *@import("TaskSignal.zig"), +}; + pub fn init(exec: *const Execution) !*AbortSignal { return exec._factory.eventTarget(AbortSignal{ ._proto = undefined, @@ -136,18 +147,23 @@ fn markAborted(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !vo dom.* = DOMException.fromError(error.AbortError).?; self._reason = .{ .dom = dom }; } + + // Unlike the loop in abort(), this runs for dependent signals too, so a + // task registered on an any() signal still gets rejected. + for (self._dependents.items) |dep| { + switch (dep) { + .scheduler_task => |task| task.onAbort(self._reason, exec), + else => {}, + } + } } fn dispatchAbortEvent(self: *AbortSignal, exec: *const Execution) !void { const target = self.asEventTarget(); const on_abort = self._on_abort; - switch (exec.js.global) { - inline else => |g| { - if (g._event_manager.hasDirectListeners(target, "abort", on_abort)) { - const event = try Event.initTrusted(comptime .wrap("abort"), .{}, g._page); - try g.dispatch(target, event, on_abort, .{ .context = "abort signal" }); - } - }, + if (exec.hasDirectListeners(target, "abort", on_abort)) { + const event = try Event.initTrusted(comptime .wrap("abort"), .{}, exec.page); + try exec.dispatch(target, event, on_abort, .{ .context = "abort signal" }); } } @@ -232,13 +248,23 @@ pub fn throwIfAborted(self: *const AbortSignal, exec: *const Execution) !ThrowIf return .undefined; } -const Reason = union(enum) { +pub const Reason = union(enum) { js_val: js.Value.Global, dom: *DOMException, string: []const u8, undefined: void, }; +// The reason as a JS value, e.g. to reject a promise with it. +pub fn reasonJsValue(reason: Reason, local: *const js.Local) !js.Value { + return switch (reason) { + .dom => |dom| local.zigValueToJs(dom, .{}), + .string => |str| local.zigValueToJs(str, .{}), + .js_val => |global| local.toLocal(global), + .undefined => local.zigValueToJs(DOMException.fromError(error.AbortError).?, .{}), + }; +} + const TimeoutCallback = struct { exec: *const Execution, signal: *AbortSignal, diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 25a6ae733..13aa08661 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -96,6 +96,7 @@ pub const Type = union(enum) { cookie_change_event: *@import("event/CookieChangeEvent.zig"), idb_version_change_event: *@import("storage/idb/IDBVersionChangeEvent.zig"), toggle_event: *@import("event/ToggleEvent.zig"), + task_priority_change_event: *@import("event/TaskPriorityChangeEvent.zig"), }; pub const Options = struct { @@ -209,6 +210,7 @@ pub fn is(self: *Event, comptime T: type) ?*T { .cookie_change_event => |e| return if (T == @import("event/CookieChangeEvent.zig")) e else null, .idb_version_change_event => |e| return if (T == @import("storage/idb/IDBVersionChangeEvent.zig")) e else null, .toggle_event => |e| return if (T == @import("event/ToggleEvent.zig")) e else null, + .task_priority_change_event => |e| return if (T == @import("event/TaskPriorityChangeEvent.zig")) e else null, .ui_event => |e| { if (T == @import("event/UIEvent.zig")) { return e; diff --git a/src/browser/webapi/Scheduler.zig b/src/browser/webapi/Scheduler.zig new file mode 100644 index 000000000..9dc4c035f --- /dev/null +++ b/src/browser/webapi/Scheduler.zig @@ -0,0 +1,284 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +const lp = @import("lightpanda"); + +const js = @import("../js/js.zig"); + +const AbortSignal = @import("AbortSignal.zig"); + +const log = lp.log; +const Execution = js.Execution; + +const Scheduler = @This(); + +// State of the postTask callback (or yield continuation) currently running, +// so scheduler.yield() can inherit its priority and abort signal. +_current: ?TaskState = null, + +pub const Priority = enum { + @"user-blocking", + @"user-visible", + background, + + pub const js_enum_from_string = true; + + pub fn toString(self: Priority) []const u8 { + return @tagName(self); + } +}; + +const TaskState = struct { + priority: Priority, + signal: ?*AbortSignal, +}; + +const PostTaskOptions = struct { + priority: ?Priority = null, + delay: ?u32 = null, + signal: ?*AbortSignal = null, +}; + +pub fn postTask(self: *Scheduler, cb: js.Function.Global, options_: ?js.Value, exec: *js.Execution) !js.Promise { + const local = exec.js.local.?; + const resolver = local.createPromiseResolver(); + + const opts: PostTaskOptions = blk: { + const options_val = options_ orelse break :blk .{}; + if (options_val.isNullOrUndefined()) { + break :blk .{}; + } + break :blk options_val.toZig(PostTaskOptions) catch { + cb.release(); + resolver.rejectError("scheduler.postTask", .{ .type_error = "invalid postTask options" }); + return resolver.promise(); + }; + }; + + if (opts.signal) |signal| { + if (signal._aborted) { + cb.release(); + rejectWithReason(resolver, signal._reason, local); + return resolver.promise(); + } + } + + // An explicit priority wins over the signal's + const priority: Priority = opts.priority orelse blk: { + const signal = opts.signal orelse break :blk .@"user-visible"; + break :blk switch (signal._type) { + .task_signal => |ts| ts._priority, + .generic => .@"user-visible", + }; + }; + + const task = try Task.create(opts.signal, exec); + task.cb = cb; + task.scheduler = self; + task.resolver = try resolver.persist(); + task.state = .{ .priority = priority, .signal = opts.signal }; + if (opts.signal) |signal| { + try signal._dependents.append(exec.arena, .{ .scheduler_task = task }); + } + try exec.js.scheduler.add(task, Task.run, opts.delay orelse 0, .{ + .name = "scheduler.postTask", + .finalizer = Task.finalize, + }); + return resolver.promise(); +} + +pub fn yield(self: *Scheduler, exec: *js.Execution) !js.Promise { + const local = exec.js.local.?; + const resolver = local.createPromiseResolver(); + + const state = self._current orelse TaskState{ .priority = .@"user-visible", .signal = null }; + if (state.signal) |signal| { + if (signal._aborted) { + rejectWithReason(resolver, signal._reason, local); + return resolver.promise(); + } + } + + const task = try Task.create(state.signal, exec); + task.cb = null; + task.scheduler = self; + task.resolver = try resolver.persist(); + task.state = state; + if (state.signal) |signal| { + try signal._dependents.append(exec.arena, .{ .scheduler_task = task }); + } + try exec.js.scheduler.add(task, Task.run, 0, .{ + .name = "scheduler.yield", + .front = true, + .finalizer = Task.finalize, + }); + return resolver.promise(); +} + +fn rejectWithReason(resolver: js.PromiseResolver, reason: AbortSignal.Reason, local: *const js.Local) void { + const value = AbortSignal.reasonJsValue(reason, local) catch { + resolver.rejectError("scheduler task abort", .{ .dom_exception = .{ .err = error.AbortError } }); + return; + }; + resolver.reject("scheduler task abort", value); +} + +pub const Task = struct { + // null for a yield continuation + cb: ?js.Function.Global, + exec: *js.Execution, + scheduler: *Scheduler, + // null once settled (ran, aborted, or finalized) + resolver: ?js.PromiseResolver.Global, + state: TaskState, + // Pooled backing arena, released when the queued task is consumed. null + // when a signal is attached, the Task is pinned by the signal. + arena: ?*lp.Arena, + + fn create(signal: ?*AbortSignal, exec: *js.Execution) !*Task { + if (signal != null) { + const task = try exec.arena.create(Task); + task.exec = exec; + task.arena = null; + return task; + } + const arena = try exec.getArena(.tiny, "scheduler.task"); + const task = try arena.create(Task); + task.exec = exec; + task.arena = arena; + return task; + } + + fn deinit(self: *Task) void { + if (self.resolver) |resolver| { + resolver.release(); + self.resolver = null; + } + if (self.cb) |cb| { + cb.release(); + self.cb = null; + } + if (self.arena) |arena| { + self.arena = null; + arena.release(); + } + } + + fn finalize(ctx: *anyopaque) void { + const self: *Task = @ptrCast(@alignCast(ctx)); + self.deinit(); + } + + fn run(ctx: *anyopaque) !?u32 { + const self: *Task = @ptrCast(@alignCast(ctx)); + defer self.deinit(); + + if (self.resolver == null) { + return null; // onAbort fired + } + + const prev = self.scheduler._current; + self.scheduler._current = self.state; + defer self.scheduler._current = prev; + + var ls: js.Local.Scope = undefined; + self.exec.js.localScope(&ls); + defer ls.deinit(); + + const cb_global = self.cb orelse { + // yield continuation: the awaiting code runs in the microtasks + // the resolve triggers, with _current set so it keeps inheriting. + const resolver_global = self.resolver.?; + self.resolver = null; + defer resolver_global.release(); + ls.toLocal(resolver_global).resolve("scheduler.yield", {}); + return null; + }; + + self.cb = null; + defer cb_global.release(); + + var try_catch: js.TryCatch = undefined; + try_catch.init(&ls.local); + defer try_catch.deinit(); + + // callback could still abort this... + const call_result = ls.toLocal(cb_global).callRethrow(js.Value, .{}); + + if (call_result) |result| { + const resolver_global = self.resolver orelse return null; + self.resolver = null; + defer resolver_global.release(); + ls.toLocal(resolver_global).resolve("scheduler.postTask", result); + } else |err| { + if (err == error.ExecutionTerminated) { + return err; + } + self.exec.page.recordJsError(err); + + const resolver_global = self.resolver orelse return null; + self.resolver = null; + defer resolver_global.release(); + + const resolver = ls.toLocal(resolver_global); + if (try_catch.exceptionValue()) |exception| { + resolver.reject("scheduler.postTask", exception); + } else { + resolver.rejectError("scheduler.postTask", .{ .generic_error = "postTask callback failed" }); + } + } + return null; + } + + // Called by AbortSignal when our signal aborts before the task ran. The task + // will still run, but resolver will be null and it'll exit. + pub fn onAbort(self: *Task, reason: AbortSignal.Reason, exec: *const Execution) void { + const resolver_global = self.resolver orelse return; + defer resolver_global.release(); + + self.resolver = null; + + if (self.cb) |cb| { + cb.release(); + self.cb = null; + } + + var ls: js.Local.Scope = undefined; + exec.js.localScope(&ls); + defer ls.deinit(); + rejectWithReason(resolver_global.local(&ls.local), reason, &ls.local); + } +}; + +pub const JsApi = struct { + pub const bridge = js.Bridge(Scheduler); + + pub const Meta = struct { + pub const name = "Scheduler"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const postTask = bridge.function(Scheduler.postTask, .{}); + pub const yield = bridge.function(Scheduler.yield, .{}); +}; + +const testing = @import("../../testing.zig"); +test "WebApi: Scheduler" { + try testing.htmlRunner("scheduler/scheduler.html", .{}); +} diff --git a/src/browser/webapi/TaskController.zig b/src/browser/webapi/TaskController.zig new file mode 100644 index 000000000..6edc647c4 --- /dev/null +++ b/src/browser/webapi/TaskController.zig @@ -0,0 +1,63 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +const js = @import("../js/js.zig"); + +const Scheduler = @import("Scheduler.zig"); +const TaskSignal = @import("TaskSignal.zig"); +const AbortController = @import("AbortController.zig"); + +const Execution = js.Execution; + +// https://wicg.github.io/scheduling-apis/#sec-task-controller +const TaskController = @This(); + +pub const Proto = AbortController; + +_proto: *AbortController, + +const Options = struct { + priority: Scheduler.Priority = .@"user-visible", +}; + +pub fn init(options_: ?Options, exec: *const Execution) !*TaskController { + const opts = options_ orelse Options{}; + const signal = try TaskSignal.init(opts.priority, exec); + return exec._factory.chained(.{ + AbortController{ ._signal = signal.asAbortSignal() }, + TaskController{ ._proto = undefined }, + }); +} + +pub fn setPriority(self: *TaskController, priority: Scheduler.Priority, exec: *const Execution) !void { + return self._proto._signal._type.task_signal.setPriority(priority, exec); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(TaskController); + + pub const Meta = struct { + pub const name = "TaskController"; + + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(TaskController.init, .{}); + pub const setPriority = bridge.function(TaskController.setPriority, .{}); +}; diff --git a/src/browser/webapi/TaskSignal.zig b/src/browser/webapi/TaskSignal.zig new file mode 100644 index 000000000..2a83a0e50 --- /dev/null +++ b/src/browser/webapi/TaskSignal.zig @@ -0,0 +1,99 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +const js = @import("../js/js.zig"); + +const Scheduler = @import("Scheduler.zig"); +const AbortSignal = @import("AbortSignal.zig"); +const EventTarget = @import("EventTarget.zig"); +const TaskPriorityChangeEvent = @import("event/TaskPriorityChangeEvent.zig"); + +const Execution = js.Execution; + +// https://wicg.github.io/scheduling-apis/#sec-task-signal +const TaskSignal = @This(); + +pub const Proto = AbortSignal; + +_proto: *AbortSignal, +_priority: Scheduler.Priority, +_priority_changing: bool = false, +_on_prioritychange: ?js.Function.Global = null, + +pub fn init(priority: Scheduler.Priority, exec: *const Execution) !*TaskSignal { + return exec._factory.taskSignal(TaskSignal{ + ._proto = undefined, + ._priority = priority, + }); +} + +pub fn asAbortSignal(self: *TaskSignal) *AbortSignal { + return self._proto; +} + +pub fn asEventTarget(self: *TaskSignal) *EventTarget { + return self._proto.asEventTarget(); +} + +pub fn getPriority(self: *const TaskSignal) Scheduler.Priority { + return self._priority; +} + +pub fn getOnPriorityChange(self: *const TaskSignal) ?js.Function.Global { + return self._on_prioritychange; +} + +pub fn setOnPriorityChange(self: *TaskSignal, cb: ?js.Function.Global) !void { + self._on_prioritychange = cb; +} + +pub fn setPriority(self: *TaskSignal, priority: Scheduler.Priority, exec: *const Execution) !void { + if (self._priority_changing) { + // re-entrant priority changes (from a prioritychange listener) throw. + return error.NotSupportedError; + } + if (priority == self._priority) { + return; + } + self._priority_changing = true; + defer self._priority_changing = false; + + const previous = self._priority; + self._priority = priority; + + const target = self.asEventTarget(); + const on_prioritychange = self._on_prioritychange; + if (exec.hasDirectListeners(target, "prioritychange", on_prioritychange)) { + const event = try TaskPriorityChangeEvent.initTrusted(.{ .previousPriority = previous }, exec.page); + try exec.dispatch(target, event.asEvent(), on_prioritychange, .{ .context = "task signal" }); + } +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(TaskSignal); + + pub const Meta = struct { + pub const name = "TaskSignal"; + + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const priority = bridge.accessor(TaskSignal.getPriority, null, .{}); + pub const onprioritychange = bridge.accessor(TaskSignal.getOnPriorityChange, TaskSignal.setOnPriorityChange, .{}); +}; diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index ac6e4a850..4c56bc239 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -50,6 +50,7 @@ const CSSStyleProperties = @import("css/CSSStyleProperties.zig"); const CustomElementRegistry = @import("CustomElementRegistry.zig"); const Selection = @import("Selection.zig"); const Timers = @import("Timers.zig"); +const Scheduler = @import("Scheduler.zig"); const Notification = @import("../../Notification.zig"); const log = lp.log; @@ -94,6 +95,7 @@ _reporting_error: bool = false, _current_event: ?*Event = null, _location: *Location, _timers: Timers = .{}, +_scheduler: Scheduler = .{}, _custom_elements: CustomElementRegistry = .{}, _scroll_pos: struct { x: u32, @@ -246,6 +248,10 @@ pub fn getNavigator(self: *Window) *Navigator { return &self._navigator; } +pub fn getScheduler(self: *Window) *Scheduler { + return &self._scheduler; +} + pub fn getModelContext(self: *Window) *ModelContext { return &self._model_context; } @@ -1172,6 +1178,7 @@ pub const JsApi = struct { pub const window = bridge.accessor(Window.getWindow, null, .{}); pub const parent = bridge.accessor(Window.getParent, Window.setParent, .{}); pub const navigator = bridge.accessor(Window.getNavigator, null, .{}); + pub const scheduler = bridge.accessor(Window.getScheduler, null, .{}); pub const screen = bridge.accessor(Window.getScreen, Window.setScreen, .{}); pub const visualViewport = bridge.accessor(Window.getVisualViewport, Window.setVisualViewport, .{}); pub const performance = bridge.accessor(Window.getPerformance, Window.setPerformance, .{}); diff --git a/src/browser/webapi/WorkerGlobalScope.zig b/src/browser/webapi/WorkerGlobalScope.zig index 84ae1ba34..779ba12a9 100644 --- a/src/browser/webapi/WorkerGlobalScope.zig +++ b/src/browser/webapi/WorkerGlobalScope.zig @@ -38,6 +38,7 @@ const Crypto = @import("Crypto.zig"); const Console = @import("Console.zig"); const Navigator = @import("Navigator.zig"); const Timers = @import("Timers.zig"); +const Scheduler = @import("Scheduler.zig"); const EventTarget = @import("EventTarget.zig"); const Performance = @import("Performance.zig"); const WorkerLocation = @import("WorkerLocation.zig"); @@ -118,6 +119,7 @@ _cookie_store: ?*CookieStore = null, _location: WorkerLocation, _timers: Timers = .{}, +_scheduler: Scheduler = .{}, pub const Type = union(enum) { shared: *SharedWorkerGlobalScope, @@ -305,6 +307,10 @@ pub fn getNavigator(self: *WorkerGlobalScope) *Navigator { return &self._navigator; } +pub fn getScheduler(self: *WorkerGlobalScope) *Scheduler { + return &self._scheduler; +} + pub fn performance(self: *WorkerGlobalScope) *Performance { return &self._performance; } @@ -583,6 +589,7 @@ pub const JsApi = struct { pub const console = bridge.accessor(WorkerGlobalScope.getConsole, WorkerGlobalScope.setConsole, .{}); pub const crypto = bridge.accessor(WorkerGlobalScope.getCrypto, null, .{}); pub const navigator = bridge.accessor(WorkerGlobalScope.getNavigator, null, .{}); + pub const scheduler = bridge.accessor(WorkerGlobalScope.getScheduler, null, .{}); pub const performance = bridge.accessor(struct { // Unnecessary, But, our WebAPI getters are ALWAYS `fn getPerformance()...`. // But for performance, we _need_ to have fn performance() *Performance to diff --git a/src/browser/webapi/event/TaskPriorityChangeEvent.zig b/src/browser/webapi/event/TaskPriorityChangeEvent.zig new file mode 100644 index 000000000..1a7d372ed --- /dev/null +++ b/src/browser/webapi/event/TaskPriorityChangeEvent.zig @@ -0,0 +1,92 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +const lp = @import("lightpanda"); + +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); + +const Event = @import("../Event.zig"); +const Scheduler = @import("../Scheduler.zig"); + +const String = lp.String; + +/// https://wicg.github.io/scheduling-apis/#sec-task-priority-change-event +const TaskPriorityChangeEvent = @This(); + +pub const Proto = Event; + +_proto: *Event, +_previous_priority: Scheduler.Priority, + +const TaskPriorityChangeEventOptions = struct { + previousPriority: Scheduler.Priority, +}; + +const Options = Event.inheritOptions(TaskPriorityChangeEvent, TaskPriorityChangeEventOptions); + +pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*TaskPriorityChangeEvent { + // previousPriority is a required member + const opts = opts_ orelse return error.TypeError; + const arena = try page.getArena(.tiny, "TaskPriorityChangeEvent"); + errdefer arena.release(); + const type_string = try String.init(arena.allocator(), typ, .{}); + return initWithTrusted(arena, type_string, opts, false, page); +} + +pub fn initTrusted(opts: Options, page: *Page) !*TaskPriorityChangeEvent { + const arena = try page.getArena(.tiny, "TaskPriorityChangeEvent.trusted"); + errdefer arena.release(); + const type_string = try String.init(arena.allocator(), "prioritychange", .{}); + return initWithTrusted(arena, type_string, opts, true, page); +} + +fn initWithTrusted(arena: *lp.Arena, typ: String, opts: Options, trusted: bool, page: *Page) !*TaskPriorityChangeEvent { + const event = try page.factory.event( + arena, + typ, + TaskPriorityChangeEvent{ + ._proto = undefined, + ._previous_priority = opts.previousPriority, + }, + ); + + Event.populatePrototypes(event, opts, trusted); + return event; +} + +pub fn asEvent(self: *TaskPriorityChangeEvent) *Event { + return self._proto; +} + +pub fn getPreviousPriority(self: *const TaskPriorityChangeEvent) Scheduler.Priority { + return self._previous_priority; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(TaskPriorityChangeEvent); + + pub const Meta = struct { + pub const name = "TaskPriorityChangeEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(TaskPriorityChangeEvent.init, .{}); + pub const previousPriority = bridge.accessor(TaskPriorityChangeEvent.getPreviousPriority, null, .{}); +};