mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Extract Window Scheduling and re-use it in Worker
Add worker.setInterval, clearInterval, setTimeout, clearTimeout by extracting the scheduling logic from Window and making it use Execution rather than Frame.
This commit is contained in:
85
src/browser/tests/worker/timers-worker.js
Normal file
85
src/browser/tests/worker/timers-worker.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// Exercises setTimeout / setInterval inside a WorkerGlobalScope.
|
||||
// Mirrors src/browser/tests/window/timers.html.
|
||||
(async function() {
|
||||
try {
|
||||
const results = {};
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// setTimeout: returns a number; passes extra args through; `this` is self.
|
||||
{
|
||||
let timeout_this = null;
|
||||
const sum = await new Promise((resolve) => {
|
||||
const id = setTimeout(function (a, b) {
|
||||
timeout_this = this;
|
||||
resolve(a + b);
|
||||
}, 1, 2, 3);
|
||||
results.setTimeout_id_is_number = (typeof id === 'number');
|
||||
});
|
||||
results.setTimeout_args = sum;
|
||||
results.setTimeout_this_is_self = (timeout_this === self);
|
||||
results.setTimeout_length = setTimeout.length;
|
||||
}
|
||||
|
||||
// setInterval fires repeatedly; clearInterval stops it.
|
||||
// A second timer cleared before its first tick must never fire.
|
||||
{
|
||||
let count1 = 0;
|
||||
const id1 = setInterval(() => { count1 += 1; }, 1);
|
||||
|
||||
let fired2 = false;
|
||||
const id2 = setInterval(() => { fired2 = true; }, 1);
|
||||
clearInterval(id2);
|
||||
|
||||
results.setInterval_ids_distinct = (id1 !== id2);
|
||||
|
||||
await sleep(10);
|
||||
clearInterval(id1);
|
||||
const after_clear = count1;
|
||||
await sleep(5);
|
||||
|
||||
results.setInterval_fired_multiple = (after_clear >= 1);
|
||||
results.setInterval_clear_stops = (count1 === after_clear);
|
||||
results.setInterval_pre_clear_silent = !fired2;
|
||||
}
|
||||
|
||||
// clearTimeout / clearInterval with bogus ids must be silent.
|
||||
{
|
||||
let threw = false;
|
||||
try {
|
||||
clearTimeout(-1);
|
||||
clearInterval(-2);
|
||||
} catch (_) { threw = true; }
|
||||
results.clear_invalid_silent = !threw;
|
||||
}
|
||||
|
||||
// Legacy: setTimeout("...", n) compiles the string into a function body.
|
||||
{
|
||||
self.__st_string_ran = 0;
|
||||
const id = setTimeout("self.__st_string_ran = 42;", 1);
|
||||
results.setTimeout_string_id_is_number = (typeof id === 'number');
|
||||
await sleep(5);
|
||||
results.setTimeout_string_ran = self.__st_string_ran;
|
||||
}
|
||||
|
||||
// Legacy: setInterval("...", n) compiles the string into a function body.
|
||||
{
|
||||
self.__si_string_ran = 0;
|
||||
const id = setInterval("self.__si_string_ran += 1;", 1);
|
||||
await sleep(5);
|
||||
clearInterval(id);
|
||||
results.setInterval_string_ran = (self.__si_string_ran >= 1);
|
||||
}
|
||||
|
||||
// Non-function, non-string handlers must throw.
|
||||
{
|
||||
let threw = false;
|
||||
try { setTimeout(123, 1); } catch (_) { threw = true; }
|
||||
results.setTimeout_invalid_throws = threw;
|
||||
}
|
||||
|
||||
postMessage({ ok: true, results });
|
||||
} catch (e) {
|
||||
postMessage({ ok: false, err: String(e), stack: e.stack });
|
||||
}
|
||||
})();
|
||||
@@ -276,6 +276,40 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="worker_timers" type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
const worker = new Worker('./timers-worker.js');
|
||||
|
||||
worker.onmessage = function(event) {
|
||||
state.resolve(event.data);
|
||||
};
|
||||
|
||||
await state.done((data) => {
|
||||
testing.expectTrue(data.ok, 'worker timers error: ' + data.err);
|
||||
const r = data.results;
|
||||
|
||||
testing.expectEqual(true, r.setTimeout_id_is_number);
|
||||
testing.expectEqual(5, r.setTimeout_args);
|
||||
testing.expectEqual(true, r.setTimeout_this_is_self);
|
||||
testing.expectEqual(1, r.setTimeout_length);
|
||||
|
||||
testing.expectEqual(true, r.setInterval_ids_distinct);
|
||||
testing.expectEqual(true, r.setInterval_fired_multiple);
|
||||
testing.expectEqual(true, r.setInterval_clear_stops);
|
||||
testing.expectEqual(true, r.setInterval_pre_clear_silent);
|
||||
|
||||
testing.expectEqual(true, r.clear_invalid_silent);
|
||||
|
||||
testing.expectEqual(true, r.setTimeout_string_id_is_number);
|
||||
testing.expectEqual(42, r.setTimeout_string_ran);
|
||||
testing.expectEqual(true, r.setInterval_string_ran);
|
||||
|
||||
testing.expectEqual(true, r.setTimeout_invalid_throws);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="importScripts" type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
|
||||
205
src/browser/webapi/Timers.zig
Normal file
205
src/browser/webapi/Timers.zig
Normal file
@@ -0,0 +1,205 @@
|
||||
// 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/>.
|
||||
|
||||
// Shared bookkeeping for setTimeout / setInterval (and Window-only
|
||||
// setImmediate / requestAnimationFrame / requestIdleCallback). Both Window
|
||||
// and WorkerGlobalScope embed a Timers and forward their JS-bridged
|
||||
// methods through `schedule` / `clear`.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Timers = @This();
|
||||
|
||||
_timer_id: u30 = 0,
|
||||
_callbacks: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
|
||||
|
||||
pub const Mode = enum {
|
||||
idle,
|
||||
normal,
|
||||
animation_frame,
|
||||
};
|
||||
|
||||
pub const ScheduleOpts = struct {
|
||||
repeat: bool,
|
||||
params: []js.Value.Temp,
|
||||
name: []const u8,
|
||||
low_priority: bool = false,
|
||||
mode: Mode = .normal,
|
||||
};
|
||||
|
||||
pub fn schedule(
|
||||
self: *Timers,
|
||||
exec: *js.Execution,
|
||||
cb: js.Function.Temp,
|
||||
delay_ms: u32,
|
||||
opts: ScheduleOpts,
|
||||
) !u32 {
|
||||
if (self._callbacks.count() > 512) {
|
||||
// these are active
|
||||
return error.TooManyTimeout;
|
||||
}
|
||||
|
||||
const arena = try exec.getArena(.tiny, "Timers.schedule");
|
||||
errdefer exec.releaseArena(arena);
|
||||
|
||||
const timer_id = self._timer_id +% 1;
|
||||
self._timer_id = timer_id;
|
||||
|
||||
var persisted_params: []js.Value.Temp = &.{};
|
||||
if (opts.params.len > 0) {
|
||||
persisted_params = try arena.dupe(js.Value.Temp, opts.params);
|
||||
}
|
||||
|
||||
const gop = try self._callbacks.getOrPut(exec.arena, timer_id);
|
||||
if (gop.found_existing) {
|
||||
// 2^31 would have to wrap for this to happen.
|
||||
return error.TooManyTimeout;
|
||||
}
|
||||
errdefer _ = self._callbacks.remove(timer_id);
|
||||
|
||||
const callback = try arena.create(ScheduleCallback);
|
||||
callback.* = .{
|
||||
.cb = cb,
|
||||
.exec = exec,
|
||||
.timers = self,
|
||||
.arena = arena,
|
||||
.mode = opts.mode,
|
||||
.name = opts.name,
|
||||
.timer_id = timer_id,
|
||||
.params = persisted_params,
|
||||
.repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null,
|
||||
};
|
||||
gop.value_ptr.* = callback;
|
||||
|
||||
try exec.context.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{
|
||||
.name = opts.name,
|
||||
.low_priority = opts.low_priority,
|
||||
.finalizer = ScheduleCallback.cancelled,
|
||||
});
|
||||
|
||||
return timer_id;
|
||||
}
|
||||
|
||||
pub fn clear(self: *Timers, id: u32) void {
|
||||
var sc = self._callbacks.fetchRemove(id) orelse return;
|
||||
sc.value.removed = true;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout
|
||||
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timerhandler
|
||||
// TimerHandler = Function or DOMString. When a string is passed, it is
|
||||
// compiled into an anonymous function body, matching how legacy browsers
|
||||
// (and all current UAs) interpret `setTimeout("foo()", 100)`.
|
||||
pub const LegacyHandler = union(enum) {
|
||||
function: js.Function.Temp,
|
||||
string: js.String,
|
||||
|
||||
pub fn resolve(handler: LegacyHandler, exec: *js.Execution) !js.Function.Temp {
|
||||
switch (handler) {
|
||||
.function => |fun| return fun,
|
||||
.string => |str| {
|
||||
const fun = try exec.context.local.?.compileFunction(str, &.{}, &.{});
|
||||
return fun.temp();
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ScheduleCallback = struct {
|
||||
// for debugging
|
||||
name: []const u8,
|
||||
|
||||
// Timers._callbacks key
|
||||
timer_id: u31,
|
||||
|
||||
// delay, in ms, to repeat. When null, removed after first invocation.
|
||||
repeat_ms: ?u32,
|
||||
|
||||
cb: js.Function.Temp,
|
||||
|
||||
mode: Mode,
|
||||
exec: *js.Execution,
|
||||
timers: *Timers,
|
||||
arena: Allocator,
|
||||
removed: bool = false,
|
||||
params: []const js.Value.Temp,
|
||||
|
||||
fn cancelled(ptr: *anyopaque) void {
|
||||
var self: *ScheduleCallback = @ptrCast(@alignCast(ptr));
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
fn deinit(self: *ScheduleCallback) void {
|
||||
self.cb.release();
|
||||
for (self.params) |param| {
|
||||
param.release();
|
||||
}
|
||||
self.exec.releaseArena(self.arena);
|
||||
}
|
||||
|
||||
fn run(ptr: *anyopaque) !?u32 {
|
||||
const self: *ScheduleCallback = @ptrCast(@alignCast(ptr));
|
||||
if (self.removed) {
|
||||
self.deinit();
|
||||
return null;
|
||||
}
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self.exec.context.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
switch (self.mode) {
|
||||
.idle => {
|
||||
const IdleDeadline = @import("IdleDeadline.zig");
|
||||
ls.toLocal(self.cb).call(void, .{IdleDeadline{}}) catch |err| {
|
||||
log.warn(.js, "idleCallback", .{ .name = self.name, .err = err });
|
||||
};
|
||||
},
|
||||
.animation_frame => {
|
||||
// requestAnimationFrame is window-only; if a worker ever
|
||||
// schedules with this mode it's a programming error.
|
||||
const window = switch (self.exec.context.global) {
|
||||
.frame => |frame| frame.window,
|
||||
.worker => unreachable,
|
||||
};
|
||||
ls.toLocal(self.cb).call(void, .{window._performance.now()}) catch |err| {
|
||||
log.warn(.js, "RAF", .{ .name = self.name, .err = err });
|
||||
};
|
||||
},
|
||||
.normal => {
|
||||
ls.toLocal(self.cb).call(void, self.params) catch |err| {
|
||||
log.warn(.js, "timer", .{ .name = self.name, .err = err });
|
||||
};
|
||||
},
|
||||
}
|
||||
ls.local.runMicrotasks();
|
||||
|
||||
if (self.repeat_ms) |ms| {
|
||||
return ms;
|
||||
}
|
||||
defer self.deinit();
|
||||
_ = self.timers._callbacks.remove(self.timer_id);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -44,6 +44,7 @@ const Element = @import("Element.zig");
|
||||
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
|
||||
const CustomElementRegistry = @import("CustomElementRegistry.zig");
|
||||
const Selection = @import("Selection.zig");
|
||||
const Timers = @import("Timers.zig");
|
||||
const Notification = @import("../../Notification.zig");
|
||||
|
||||
const log = lp.log;
|
||||
@@ -77,8 +78,7 @@ _on_rejection_handled: ?js.Function.Global = null,
|
||||
_on_unhandled_rejection: ?js.Function.Global = null,
|
||||
_current_event: ?*Event = null,
|
||||
_location: *Location,
|
||||
_timer_id: u30 = 0,
|
||||
_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
|
||||
_timers: Timers = .{},
|
||||
_custom_elements: CustomElementRegistry = .{},
|
||||
_scroll_pos: struct {
|
||||
x: u32,
|
||||
@@ -286,63 +286,39 @@ pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, exe
|
||||
return Fetch.init(input, options, exec);
|
||||
}
|
||||
|
||||
const LegacyHandler = union(enum) {
|
||||
function: js.Function.Temp,
|
||||
string: js.String,
|
||||
};
|
||||
|
||||
pub fn setTimeout(self: *Window, handler: LegacyHandler, delay_ms: ?u32, params: []js.Value.Temp, frame: *Frame) !u32 {
|
||||
const cb = try resolveTimerHandler(handler, frame);
|
||||
return self.scheduleCallback(cb, delay_ms orelse 0, .{
|
||||
pub fn setTimeout(self: *Window, handler: Timers.LegacyHandler, delay_ms: ?u32, params: []js.Value.Temp, exec: *js.Execution) !u32 {
|
||||
const cb = try handler.resolve(exec);
|
||||
return self._timers.schedule(exec, cb, delay_ms orelse 0, .{
|
||||
.repeat = false,
|
||||
.params = params,
|
||||
.low_priority = false,
|
||||
.name = "window.setTimeout",
|
||||
}, frame);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn setInterval(self: *Window, handler: LegacyHandler, delay_ms: ?u32, params: []js.Value.Temp, frame: *Frame) !u32 {
|
||||
const cb = try resolveTimerHandler(handler, frame);
|
||||
return self.scheduleCallback(cb, delay_ms orelse 0, .{
|
||||
pub fn setInterval(self: *Window, handler: Timers.LegacyHandler, delay_ms: ?u32, params: []js.Value.Temp, exec: *js.Execution) !u32 {
|
||||
const cb = try handler.resolve(exec);
|
||||
return self._timers.schedule(exec, cb, delay_ms orelse 0, .{
|
||||
.repeat = true,
|
||||
.params = params,
|
||||
.low_priority = false,
|
||||
.name = "window.setInterval",
|
||||
}, frame);
|
||||
});
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout
|
||||
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timerhandler
|
||||
// TimerHandler = Function or DOMString. When a string is passed, it is
|
||||
// compiled into an anonymous function body, matching how legacy browsers
|
||||
// (and all current UAs) interpret `setTimeout("foo()", 100)`.
|
||||
fn resolveTimerHandler(handler: LegacyHandler, frame: *Frame) !js.Function.Temp {
|
||||
switch (handler) {
|
||||
.function => |fun| return fun,
|
||||
.string => |str| {
|
||||
const fun = try frame.js.local.?.compileFunction(str, &.{}, &.{});
|
||||
return fun.temp();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setImmediate(self: *Window, cb: js.Function.Temp, params: []js.Value.Temp, frame: *Frame) !u32 {
|
||||
return self.scheduleCallback(cb, 0, .{
|
||||
pub fn setImmediate(self: *Window, cb: js.Function.Temp, params: []js.Value.Temp, exec: *js.Execution) !u32 {
|
||||
return self._timers.schedule(exec, cb, 0, .{
|
||||
.repeat = false,
|
||||
.params = params,
|
||||
.low_priority = false,
|
||||
.name = "window.setImmediate",
|
||||
}, frame);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn requestAnimationFrame(self: *Window, cb: js.Function.Temp, frame: *Frame) !u32 {
|
||||
return self.scheduleCallback(cb, 5, .{
|
||||
pub fn requestAnimationFrame(self: *Window, cb: js.Function.Temp, exec: *js.Execution) !u32 {
|
||||
return self._timers.schedule(exec, cb, 5, .{
|
||||
.repeat = false,
|
||||
.params = &.{},
|
||||
.low_priority = false,
|
||||
.mode = .animation_frame,
|
||||
.name = "window.requestAnimationFrame",
|
||||
}, frame);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn queueMicrotask(_: *Window, cb: js.Function, frame: *Frame) void {
|
||||
@@ -350,42 +326,37 @@ pub fn queueMicrotask(_: *Window, cb: js.Function, frame: *Frame) void {
|
||||
}
|
||||
|
||||
pub fn clearTimeout(self: *Window, id: u32) void {
|
||||
var sc = self._timers.fetchRemove(id) orelse return;
|
||||
sc.value.removed = true;
|
||||
self._timers.clear(id);
|
||||
}
|
||||
|
||||
pub fn clearInterval(self: *Window, id: u32) void {
|
||||
var sc = self._timers.fetchRemove(id) orelse return;
|
||||
sc.value.removed = true;
|
||||
self._timers.clear(id);
|
||||
}
|
||||
|
||||
pub fn clearImmediate(self: *Window, id: u32) void {
|
||||
var sc = self._timers.fetchRemove(id) orelse return;
|
||||
sc.value.removed = true;
|
||||
self._timers.clear(id);
|
||||
}
|
||||
|
||||
pub fn cancelAnimationFrame(self: *Window, id: u32) void {
|
||||
var sc = self._timers.fetchRemove(id) orelse return;
|
||||
sc.value.removed = true;
|
||||
self._timers.clear(id);
|
||||
}
|
||||
|
||||
const RequestIdleCallbackOpts = struct {
|
||||
timeout: ?u32 = null,
|
||||
};
|
||||
pub fn requestIdleCallback(self: *Window, cb: js.Function.Temp, opts_: ?RequestIdleCallbackOpts, frame: *Frame) !u32 {
|
||||
pub fn requestIdleCallback(self: *Window, cb: js.Function.Temp, opts_: ?RequestIdleCallbackOpts, exec: *js.Execution) !u32 {
|
||||
const opts = opts_ orelse RequestIdleCallbackOpts{};
|
||||
return self.scheduleCallback(cb, opts.timeout orelse 50, .{
|
||||
return self._timers.schedule(exec, cb, opts.timeout orelse 50, .{
|
||||
.mode = .idle,
|
||||
.repeat = false,
|
||||
.params = &.{},
|
||||
.low_priority = true,
|
||||
.name = "window.requestIdleCallback",
|
||||
}, frame);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn cancelIdleCallback(self: *Window, id: u32) void {
|
||||
var sc = self._timers.fetchRemove(id) orelse return;
|
||||
sc.value.removed = true;
|
||||
self._timers.clear(id);
|
||||
}
|
||||
|
||||
pub fn reportError(self: *Window, err: js.Value, frame: *Frame) !void {
|
||||
@@ -801,140 +772,6 @@ pub const Access = union(enum) {
|
||||
}
|
||||
};
|
||||
|
||||
const ScheduleOpts = struct {
|
||||
repeat: bool,
|
||||
params: []js.Value.Temp,
|
||||
name: []const u8,
|
||||
low_priority: bool = false,
|
||||
animation_frame: bool = false,
|
||||
mode: ScheduleCallback.Mode = .normal,
|
||||
};
|
||||
fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: ScheduleOpts, frame: *Frame) !u32 {
|
||||
if (self._timers.count() > 512) {
|
||||
// these are active
|
||||
return error.TooManyTimeout;
|
||||
}
|
||||
|
||||
const arena = try frame.getArena(.tiny, "Window.schedule");
|
||||
errdefer frame.releaseArena(arena);
|
||||
|
||||
const timer_id = self._timer_id +% 1;
|
||||
self._timer_id = timer_id;
|
||||
|
||||
const params = opts.params;
|
||||
var persisted_params: []js.Value.Temp = &.{};
|
||||
if (params.len > 0) {
|
||||
persisted_params = try arena.dupe(js.Value.Temp, params);
|
||||
}
|
||||
|
||||
const gop = try self._timers.getOrPut(frame.arena, timer_id);
|
||||
if (gop.found_existing) {
|
||||
// 2^31 would have to wrap for this to happen.
|
||||
return error.TooManyTimeout;
|
||||
}
|
||||
errdefer _ = self._timers.remove(timer_id);
|
||||
|
||||
const callback = try arena.create(ScheduleCallback);
|
||||
callback.* = .{
|
||||
.cb = cb,
|
||||
.frame = frame,
|
||||
.arena = arena,
|
||||
.mode = opts.mode,
|
||||
.name = opts.name,
|
||||
.timer_id = timer_id,
|
||||
.params = persisted_params,
|
||||
.repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null,
|
||||
};
|
||||
gop.value_ptr.* = callback;
|
||||
|
||||
try frame.js.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{
|
||||
.name = opts.name,
|
||||
.low_priority = opts.low_priority,
|
||||
.finalizer = ScheduleCallback.cancelled,
|
||||
});
|
||||
|
||||
return timer_id;
|
||||
}
|
||||
|
||||
const ScheduleCallback = struct {
|
||||
// for debugging
|
||||
name: []const u8,
|
||||
|
||||
// window._timers key
|
||||
timer_id: u31,
|
||||
|
||||
// delay, in ms, to repeat. When null, will be removed after the first time
|
||||
repeat_ms: ?u32,
|
||||
|
||||
cb: js.Function.Temp,
|
||||
|
||||
mode: Mode,
|
||||
frame: *Frame,
|
||||
arena: Allocator,
|
||||
removed: bool = false,
|
||||
params: []const js.Value.Temp,
|
||||
|
||||
const Mode = enum {
|
||||
idle,
|
||||
normal,
|
||||
animation_frame,
|
||||
};
|
||||
|
||||
fn cancelled(ctx: *anyopaque) void {
|
||||
var self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
fn deinit(self: *ScheduleCallback) void {
|
||||
self.cb.release();
|
||||
for (self.params) |param| {
|
||||
param.release();
|
||||
}
|
||||
self.frame.releaseArena(self.arena);
|
||||
}
|
||||
|
||||
fn run(ctx: *anyopaque) !?u32 {
|
||||
const self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
|
||||
const frame = self.frame;
|
||||
const window = frame.window;
|
||||
|
||||
if (self.removed) {
|
||||
self.deinit();
|
||||
return null;
|
||||
}
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
switch (self.mode) {
|
||||
.idle => {
|
||||
const IdleDeadline = @import("IdleDeadline.zig");
|
||||
ls.toLocal(self.cb).call(void, .{IdleDeadline{}}) catch |err| {
|
||||
log.warn(.js, "window.idleCallback", .{ .name = self.name, .err = err });
|
||||
};
|
||||
},
|
||||
.animation_frame => {
|
||||
ls.toLocal(self.cb).call(void, .{window._performance.now()}) catch |err| {
|
||||
log.warn(.js, "window.RAF", .{ .name = self.name, .err = err });
|
||||
};
|
||||
},
|
||||
.normal => {
|
||||
ls.toLocal(self.cb).call(void, self.params) catch |err| {
|
||||
log.warn(.js, "window.timer", .{ .name = self.name, .err = err });
|
||||
};
|
||||
},
|
||||
}
|
||||
ls.local.runMicrotasks();
|
||||
if (self.repeat_ms) |ms| {
|
||||
return ms;
|
||||
}
|
||||
defer self.deinit();
|
||||
_ = window._timers.remove(self.timer_id);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const PostMessageCallback = struct {
|
||||
frame: *Frame,
|
||||
source: *Window,
|
||||
|
||||
@@ -37,6 +37,7 @@ const Event = @import("Event.zig");
|
||||
const Worker = @import("Worker.zig");
|
||||
const Crypto = @import("Crypto.zig");
|
||||
const Console = @import("Console.zig");
|
||||
const Timers = @import("Timers.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
const MessageEvent = @import("event/MessageEvent.zig");
|
||||
const ErrorEvent = @import("event/ErrorEvent.zig");
|
||||
@@ -97,6 +98,8 @@ _on_unhandled_rejection: ?JS.Function.Global = null,
|
||||
_on_message: ?JS.Function.Global = null,
|
||||
_on_messageerror: ?JS.Function.Global = null,
|
||||
|
||||
_timers: Timers = .{},
|
||||
|
||||
pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope {
|
||||
const arena = worker._arena;
|
||||
const parent = worker._frame;
|
||||
@@ -454,6 +457,36 @@ pub fn fetch(_: *const WorkerGlobalScope, input: Fetch.Input, options: ?Fetch.In
|
||||
return Fetch.init(input, options, exec);
|
||||
}
|
||||
|
||||
pub fn queueMicrotask(self: *WorkerGlobalScope, cb: JS.Function) void {
|
||||
self.js.queueMicrotaskFunc(cb);
|
||||
}
|
||||
|
||||
pub fn setTimeout(self: *WorkerGlobalScope, handler: Timers.LegacyHandler, delay_ms: ?u32, params: []JS.Value.Temp, exec: *JS.Execution) !u32 {
|
||||
const cb = try handler.resolve(exec);
|
||||
return self._timers.schedule(exec, cb, delay_ms orelse 0, .{
|
||||
.repeat = false,
|
||||
.params = params,
|
||||
.name = "worker.setTimeout",
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clearTimeout(self: *WorkerGlobalScope, id: u32) void {
|
||||
self._timers.clear(id);
|
||||
}
|
||||
|
||||
pub fn setInterval(self: *WorkerGlobalScope, handler: Timers.LegacyHandler, delay_ms: ?u32, params: []JS.Value.Temp, exec: *JS.Execution) !u32 {
|
||||
const cb = try handler.resolve(exec);
|
||||
return self._timers.schedule(exec, cb, delay_ms orelse 0, .{
|
||||
.repeat = true,
|
||||
.params = params,
|
||||
.name = "worker.setInterval",
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clearInterval(self: *WorkerGlobalScope, id: u32) void {
|
||||
self._timers.clear(id);
|
||||
}
|
||||
|
||||
const FunctionSetter = union(enum) {
|
||||
func: JS.Function.Global,
|
||||
anything: JS.Value,
|
||||
@@ -546,6 +579,11 @@ pub const JsApi = struct {
|
||||
pub const close = bridge.function(WorkerGlobalScope.close, .{});
|
||||
pub const fetch = bridge.function(WorkerGlobalScope.fetch, .{});
|
||||
pub const importScripts = bridge.function(WorkerGlobalScope.importScripts, .{ .dom_exception = true });
|
||||
pub const queueMicrotask = bridge.function(WorkerGlobalScope.queueMicrotask, .{});
|
||||
pub const setTimeout = bridge.function(WorkerGlobalScope.setTimeout, .{});
|
||||
pub const clearTimeout = bridge.function(WorkerGlobalScope.clearTimeout, .{});
|
||||
pub const setInterval = bridge.function(WorkerGlobalScope.setInterval, .{});
|
||||
pub const clearInterval = bridge.function(WorkerGlobalScope.clearInterval, .{});
|
||||
|
||||
pub const onmessage = bridge.accessor(WorkerGlobalScope.getOnMessage, WorkerGlobalScope.setOnMessage, .{});
|
||||
pub const onmessageerror = bridge.accessor(WorkerGlobalScope.getOnMessageError, WorkerGlobalScope.setOnMessageError, .{});
|
||||
|
||||
Reference in New Issue
Block a user