From 5041450fcfb121b8b85760895bf32d0abe6c46ac Mon Sep 17 00:00:00 2001 From: William Chan Date: Sat, 18 Apr 2026 07:26:12 +0800 Subject: [PATCH 1/2] feat(window): accept DOMString handler in setTimeout/setInterval The HTML spec defines `TimerHandler` as `Function or DOMString`, and the string form is still widely used by legacy sites and server-rendered pages (e.g. clocks that do `setInterval("updateTime()", 1000)`). Previously the binding signature required `js.Function.Temp`, so passing a string threw `TypeError: invalid argument` and aborted page scripts. Accept `js.Value.Temp` instead, then resolve the handler per spec: * Function -> persist as-is (unchanged behavior). * String -> compile as the body of an anonymous function via `Local.compileFunction`, mirroring what the existing `Context.stringToPersistedFunction` helper does for HTML attribute event handlers. * otherwise -> `InvalidArgument` (TypeError, same as before). Tests added to window/timers.html: * setTimeout(string) runs the compiled code. * setInterval(string) fires at least once and is cancellable. * non-function / non-string handler still throws TypeError. Spec: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers --- src/browser/tests/window/timers.html | 30 ++++++++++++++++++++++++++++ src/browser/webapi/Window.zig | 29 +++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/browser/tests/window/timers.html b/src/browser/tests/window/timers.html index 9e22d4d2..27235738 100644 --- a/src/browser/tests/window/timers.html +++ b/src/browser/tests/window/timers.html @@ -39,3 +39,33 @@ clearImmediate(-3); testing.expectEqual(true, true); + + + + + + + diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 9e716e91..9be3ae1f 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -249,7 +249,8 @@ pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, pag return Fetch.init(input, options, page); } -pub fn setTimeout(self: *Window, cb: js.Function.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 { +pub fn setTimeout(self: *Window, handler: js.Value.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 { + const cb = try resolveTimerHandler(handler, page); return self.scheduleCallback(cb, delay_ms orelse 0, .{ .repeat = false, .params = params, @@ -258,7 +259,8 @@ pub fn setTimeout(self: *Window, cb: js.Function.Temp, delay_ms: ?u32, params: [ }, page); } -pub fn setInterval(self: *Window, cb: js.Function.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 { +pub fn setInterval(self: *Window, handler: js.Value.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 { + const cb = try resolveTimerHandler(handler, page); return self.scheduleCallback(cb, delay_ms orelse 0, .{ .repeat = true, .params = params, @@ -267,6 +269,29 @@ pub fn setInterval(self: *Window, cb: js.Function.Temp, delay_ms: ?u32, params: }, page); } +// 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: js.Value.Temp, page: *Page) !js.Function.Temp { + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + + const value = handler.local(&ls.local); + if (value.isFunction()) { + const js_func = js.Function{ .local = &ls.local, .handle = @ptrCast(value.handle) }; + return try js_func.temp(); + } + if (value.isString()) |js_str| { + const body = try js_str.toSliceWithAlloc(page.call_arena); + const fun = try ls.local.compileFunction(body, &.{}, &.{}); + return try fun.temp(); + } + return error.InvalidArgument; +} + pub fn setImmediate(self: *Window, cb: js.Function.Temp, params: []js.Value.Temp, page: *Page) !u32 { return self.scheduleCallback(cb, 0, .{ .repeat = false, From 9a6875f1b58e24608f9a7252b9541d8d91e2d284 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 20 Apr 2026 09:51:42 +0800 Subject: [PATCH 2/2] Prefer union over js.Value type checking Allow mapping js input to a js.String (previously, had to be either a []const u8, an SSO, or a js.Value). Allow compileFunction to be called directly with a js.String. --- src/browser/js/Local.zig | 5 +++-- src/browser/webapi/Window.zig | 31 ++++++++++++++----------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index e77c60ac..d45a5e69 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -120,14 +120,14 @@ pub fn exec(self: *const Local, src: []const u8, name: ?[]const u8) !js.Value { /// https://v8.github.io/api/head/classv8_1_1ScriptCompiler.html#a3a15bb5a7dfc3f998e6ac789e6b4646a pub fn compileFunction( self: *const Local, - function_body: []const u8, + src: anytype, /// We tend to know how many params we'll pass; can remove the comptime if necessary. comptime parameter_names: []const []const u8, extensions: []const v8.Object, ) !js.Function { // TODO: Make configurable. const script_name = self.isolate.initStringHandle("anonymous"); - const script_source = self.isolate.initStringHandle(function_body); + const script_source = if (@TypeOf(src) == js.String) src.handle else self.isolate.initStringHandle(src); var parameter_list: [parameter_names.len]*const v8.String = undefined; inline for (0..parameter_names.len) |i| { @@ -742,6 +742,7 @@ fn jsValueToStruct(self: *const Local, comptime T: type, js_val: js.Value) !?T { else => unreachable, }; }, + js.String => return js_val.isString(), string.String => { const js_str = js_val.isString() orelse return null; return try js_str.toSSO(false); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 9be3ae1f..0a36a7d0 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -249,7 +249,12 @@ pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, pag return Fetch.init(input, options, page); } -pub fn setTimeout(self: *Window, handler: js.Value.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 { +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, page: *Page) !u32 { const cb = try resolveTimerHandler(handler, page); return self.scheduleCallback(cb, delay_ms orelse 0, .{ .repeat = false, @@ -259,7 +264,7 @@ pub fn setTimeout(self: *Window, handler: js.Value.Temp, delay_ms: ?u32, params: }, page); } -pub fn setInterval(self: *Window, handler: js.Value.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 { +pub fn setInterval(self: *Window, handler: LegacyHandler, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 { const cb = try resolveTimerHandler(handler, page); return self.scheduleCallback(cb, delay_ms orelse 0, .{ .repeat = true, @@ -274,22 +279,14 @@ pub fn setInterval(self: *Window, handler: js.Value.Temp, delay_ms: ?u32, params // 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: js.Value.Temp, page: *Page) !js.Function.Temp { - var ls: js.Local.Scope = undefined; - page.js.localScope(&ls); - defer ls.deinit(); - - const value = handler.local(&ls.local); - if (value.isFunction()) { - const js_func = js.Function{ .local = &ls.local, .handle = @ptrCast(value.handle) }; - return try js_func.temp(); +fn resolveTimerHandler(handler: LegacyHandler, page: *Page) !js.Function.Temp { + switch (handler) { + .function => |fun| return fun, + .string => |str| { + const fun = try page.js.local.?.compileFunction(str, &.{}, &.{}); + return fun.temp(); + }, } - if (value.isString()) |js_str| { - const body = try js_str.toSliceWithAlloc(page.call_arena); - const fun = try ls.local.compileFunction(body, &.{}, &.{}); - return try fun.temp(); - } - return error.InvalidArgument; } pub fn setImmediate(self: *Window, cb: js.Function.Temp, params: []js.Value.Temp, page: *Page) !u32 {