diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig
index 09d187053..1c976427e 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");
@@ -513,6 +514,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 3fdc44f5f..e8fd20d38 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;
}
@@ -585,6 +591,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, .{});
+};