Merge pull request #3094 from lightpanda-io/webapi-scheduler

webapi: Add Scheduler (window and worker)
This commit is contained in:
Karl Seguin
2026-08-01 08:04:26 +08:00
committed by GitHub
12 changed files with 865 additions and 10 deletions

View File

@@ -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");

View File

@@ -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,
});
}

View File

@@ -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"),

View File

@@ -0,0 +1,259 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=schedulerSurface>
{
testing.expectEqual('function', typeof scheduler.postTask);
testing.expectEqual('function', typeof scheduler.yield);
testing.expectEqual(true, scheduler instanceof Scheduler);
testing.expectEqual('function', typeof TaskController);
testing.expectEqual('function', typeof TaskSignal);
testing.expectEqual('function', typeof TaskPriorityChangeEvent);
}
</script>
<script id=taskControllerSignal>
{
const controller = new TaskController();
const signal = controller.signal;
testing.expectEqual(true, signal instanceof TaskSignal);
testing.expectEqual(true, signal instanceof AbortSignal);
testing.expectEqual('user-visible', signal.priority);
testing.expectEqual(false, signal.aborted);
const bg = new TaskController({ priority: 'background' });
testing.expectEqual('background', bg.signal.priority);
}
</script>
<script id=setPriorityFiresPriorityChange>
{
const controller = new TaskController({ priority: 'user-blocking' });
const signal = controller.signal;
let fired = 0;
signal.addEventListener('prioritychange', (event) => {
fired += 1;
testing.expectEqual('prioritychange', event.type);
testing.expectEqual('user-blocking', event.previousPriority);
testing.expectEqual('background', signal.priority);
});
controller.setPriority('background');
testing.expectEqual(1, fired);
// same priority: no event
controller.setPriority('background');
testing.expectEqual(1, fired);
}
</script>
<script id=taskPriorityChangeEventConstructor>
{
const event = new TaskPriorityChangeEvent('prioritychange', { previousPriority: 'background' });
testing.expectEqual('background', event.previousPriority);
testing.expectEqual(false, event.isTrusted);
let threw = false;
try {
new TaskPriorityChangeEvent('prioritychange');
} catch (e) {
threw = true;
}
testing.expectEqual(true, threw);
}
</script>
<script id=postTaskResolves type=module>
{
const state = await testing.async();
const value = await scheduler.postTask(() => 42);
state.resolve(value);
await state.done((v) => {
testing.expectEqual(42, v);
});
}
</script>
<script id=postTaskAdoptsAsyncCallback type=module>
{
const state = await testing.async();
const value = await scheduler.postTask(async () => 'async-result');
state.resolve(value);
await state.done((v) => {
testing.expectEqual('async-result', v);
});
}
</script>
<script id=postTaskRejectsOnThrow type=module>
{
const state = await testing.async();
try {
await scheduler.postTask(() => { throw new Error('boom'); });
state.resolve('resolved');
} catch (e) {
state.resolve(e.message);
}
await state.done((v) => {
testing.expectEqual('boom', v);
});
}
</script>
<script id=postTaskOrderingAndDelay type=module>
{
const state = await testing.async();
const order = [];
const delayed = scheduler.postTask(() => order.push('delayed'), { delay: 5 });
const first = scheduler.postTask(() => order.push('first'));
const second = scheduler.postTask(() => order.push('second'));
await Promise.all([delayed, first, second]);
state.resolve(order.join(','));
await state.done((v) => {
testing.expectEqual('first,second,delayed', v);
});
}
</script>
<script id=postTaskPreAbortedSignal type=module>
{
const state = await testing.async();
const controller = new TaskController();
controller.abort('nope');
let ran = false;
try {
await scheduler.postTask(() => { ran = true; }, { signal: controller.signal });
state.resolve('resolved');
} catch (e) {
state.resolve(e);
}
await state.done((v) => {
testing.expectEqual('nope', v);
testing.expectEqual(false, ran);
});
}
</script>
<script id=postTaskAbortBeforeRun type=module>
{
const state = await testing.async();
const controller = new TaskController();
let ran = false;
const task = scheduler.postTask(() => { ran = true; }, { signal: controller.signal, delay: 50 });
controller.abort('cancelled');
try {
await task;
state.resolve('resolved');
} catch (e) {
state.resolve(e);
}
await state.done((v) => {
testing.expectEqual('cancelled', v);
testing.expectEqual(false, ran);
});
}
</script>
<script id=postTaskPlainAbortControllerWorks type=module>
{
const state = await testing.async();
const controller = new AbortController();
const task = scheduler.postTask(() => 'ok', { signal: controller.signal, delay: 50 });
controller.abort();
try {
await task;
state.resolve('resolved');
} catch (e) {
state.resolve(e.name);
}
await state.done((v) => {
testing.expectEqual('AbortError', v);
});
}
</script>
<script id=postTaskInvalidPriority type=module>
{
const state = await testing.async();
try {
await scheduler.postTask(() => {}, { priority: 'bogus' });
state.resolve('resolved');
} catch (e) {
state.resolve(e.name);
}
await state.done((v) => {
testing.expectEqual('TypeError', v);
});
}
</script>
<script id=postTaskAbortDuringRun type=module>
{
const state = await testing.async();
const controller = new TaskController();
try {
await scheduler.postTask(() => {
controller.abort('mid');
return 'value';
}, { signal: controller.signal });
state.resolve('resolved');
} catch (e) {
state.resolve('rejected:' + e);
}
await state.done((v) => {
testing.expectEqual('rejected:mid', v);
});
}
</script>
<script id=yieldInterleavesTasks type=module>
{
const state = await testing.async();
const order = [];
const yielding = scheduler.postTask(async () => {
order.push('a');
await scheduler.yield();
order.push('c');
});
const other = scheduler.postTask(() => order.push('b'));
await Promise.all([yielding, other]);
state.resolve(order.join(','));
await state.done((v) => {
// the yield continuation jumps ahead of the already-queued task
testing.expectEqual('a,c,b', v);
});
}
</script>
<script id=yieldInheritsAbortedSignal type=module>
{
const state = await testing.async();
const controller = new TaskController();
const task = scheduler.postTask(async () => {
controller.abort('stop');
try {
await scheduler.yield();
state.resolve('not-interrupted');
} catch (e) {
state.resolve('interrupted:' + e);
}
}, { signal: controller.signal });
// the abort also rejects the task promise itself (see postTaskAbortDuringRun)
task.catch(() => {});
await state.done((v) => {
testing.expectEqual('interrupted:stop', v);
});
}
</script>
<script id=yieldOutsideTask type=module>
{
const state = await testing.async();
await scheduler.yield();
state.resolve('done');
await state.done((v) => {
testing.expectEqual('done', v);
});
}
</script>

View File

@@ -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,

View File

@@ -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;

View File

@@ -0,0 +1,284 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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", .{});
}

View File

@@ -0,0 +1,63 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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, .{});
};

View File

@@ -0,0 +1,99 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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, .{});
};

View File

@@ -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, .{});

View File

@@ -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

View File

@@ -0,0 +1,92 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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, .{});
};