Merge branch 'main' into agent

This commit is contained in:
Adrià Arrufat
2026-04-14 10:31:16 +02:00
44 changed files with 1992 additions and 548 deletions

View File

@@ -13,7 +13,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.3.8'
default: 'v0.3.9'
v8:
description: 'v8 version to install'
required: false

View File

@@ -3,7 +3,7 @@ FROM debian:stable-slim
ARG MINISIG=0.12
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4
ARG ZIG_V8=v0.3.8
ARG ZIG_V8=v0.3.9
ARG TARGETPLATFORM
RUN apt-get update -yq && \

View File

@@ -24,25 +24,24 @@ const String = @import("../string.zig").String;
const js = @import("js/js.zig");
const Page = @import("Page.zig");
const EventManagerBase = @import("EventManagerBase.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
const EventTarget = @import("webapi/EventTarget.zig");
const Element = @import("webapi/Element.zig");
const EventManagerBase = @import("EventManagerBase.zig");
const Allocator = std.mem.Allocator;
const IS_DEBUG = builtin.mode == .Debug;
pub const EventManager = @This();
// Re-export types from EventManagerBase for API compatibility
pub const RegisterOptions = EventManagerBase.RegisterOptions;
pub const Callback = EventManagerBase.Callback;
pub const Listener = EventManagerBase.Listener;
const IS_DEBUG = builtin.mode == .Debug;
pub const EventManager = @This();
page: *Page,
base: EventManagerBase,

View File

@@ -57,6 +57,7 @@ const PerformanceObserver = @import("webapi/PerformanceObserver.zig");
const AbstractRange = @import("webapi/AbstractRange.zig");
const MutationObserver = @import("webapi/MutationObserver.zig");
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
const Worker = @import("webapi/Worker.zig");
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
const SubmitEvent = @import("webapi/event/SubmitEvent.zig");
@@ -231,6 +232,9 @@ iframe: ?*IFrame = null,
frames: std.ArrayList(*Page) = .{},
frames_sorted: bool = true,
// Workers created by this page. Cleaned up when page is destroyed.
workers: std.ArrayList(*Worker) = .{},
// DOM version used to invalidate cached state of "live" collections
version: usize = 0,
@@ -340,6 +344,10 @@ pub fn deinit(self: *Page, abort_http: bool) void {
frame.deinit(abort_http);
}
for (self.workers.items) |worker| {
worker.deinit();
}
if (comptime IS_DEBUG) {
log.debug(.page, "page.deinit", .{ .url = self.url, .type = self._type });
@@ -400,6 +408,19 @@ pub fn deinit(self: *Page, abort_http: bool) void {
session.releaseArena(self.call_arena);
}
pub fn trackWorker(self: *Page, worker: *Worker) !void {
try self.workers.append(self.arena, worker);
}
pub fn removeWorker(self: *Page, worker: *Worker) void {
for (self.workers.items, 0..) |w, i| {
if (w == worker) {
_ = self.workers.swapRemove(i);
break;
}
}
}
pub fn base(self: *const Page) [:0]const u8 {
return self.base_url orelse self.url;
}
@@ -1545,7 +1566,7 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
continue;
};
const target = slot.asNode().asEventTarget();
_ = target.dispatchEvent(event, self) catch |err| {
self._event_manager.dispatch(target, event) catch |err| {
log.err(.page, "deliverSlotchange.dispatch", .{ .err = err, .type = self._type, .url = self.url });
};
}

View File

@@ -21,6 +21,8 @@ const log = @import("../../log.zig");
const string = @import("../../string.zig");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
const js = @import("js.zig");
const Local = @import("Local.zig");
@@ -57,7 +59,7 @@ fn throwDetachedError(isolate: *v8.Isolate) void {
_ = v8.v8__Isolate__ThrowException(isolate, js_exception);
}
fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) void {
pub fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) void {
ctx.call_depth += 1;
self.* = Caller{
.local = .{
@@ -67,9 +69,9 @@ fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context)
.isolate = ctx.isolate,
},
.prev_local = ctx.local,
.prev_context = ctx.page.js,
.prev_context = ctx.global.getJs(),
};
ctx.page.js = ctx;
ctx.global.setJs(ctx);
ctx.local = &self.local;
}
@@ -100,7 +102,7 @@ pub fn deinit(self: *Caller) void {
ctx.call_depth = call_depth;
ctx.local = self.prev_local;
ctx.page.js = self.prev_context;
ctx.global.setJs(self.prev_context);
}
pub const CallOpts = struct {
@@ -182,7 +184,7 @@ fn _getIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, inf
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = idx;
if (@typeInfo(F).@"fn".params.len == 3) {
@field(args, "2") = local.ctx.page;
@field(args, "2") = getGlobalArg(@TypeOf(args.@"2"), local.ctx);
}
const ret = @call(.auto, func, args);
return handleIndexedReturn(T, F, true, local, ret, info, opts);
@@ -209,7 +211,7 @@ fn _getNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *c
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
if (@typeInfo(F).@"fn".params.len == 3) {
@field(args, "2") = local.ctx.page;
@field(args, "2") = getGlobalArg(@TypeOf(args.@"2"), local.ctx);
}
const ret = @call(.auto, func, args);
return handleIndexedReturn(T, F, true, local, ret, info, opts);
@@ -237,7 +239,7 @@ fn _setNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *c
@field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
@field(args, "2") = try local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
if (@typeInfo(F).@"fn".params.len == 4) {
@field(args, "3") = local.ctx.page;
@field(args, "3") = getGlobalArg(@TypeOf(args.@"3"), local.ctx);
}
const ret = @call(.auto, func, args);
return handleIndexedReturn(T, F, false, local, ret, info, opts);
@@ -263,7 +265,7 @@ fn _deleteNamedIndex(comptime T: type, local: *const Local, func: anytype, name:
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
if (@typeInfo(F).@"fn".params.len == 3) {
@field(args, "2") = local.ctx.page;
@field(args, "2") = getGlobalArg(@TypeOf(args.@"2"), local.ctx);
}
const ret = @call(.auto, func, args);
return handleIndexedReturn(T, F, false, local, ret, info, opts);
@@ -289,7 +291,7 @@ fn _getEnumerator(comptime T: type, local: *const Local, func: anytype, info: Pr
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
if (@typeInfo(F).@"fn".params.len == 2) {
@field(args, "1") = local.ctx.page;
@field(args, "1") = getGlobalArg(@TypeOf(args.@"1"), local.ctx);
}
const ret = @call(.auto, func, args);
return handleIndexedReturn(T, F, true, local, ret, info, opts);
@@ -448,6 +450,33 @@ fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page;
}
fn isSession(comptime T: type) bool {
return T == *Session or T == *const Session;
}
fn isExecution(comptime T: type) bool {
return T == *js.Execution or T == *const js.Execution;
}
fn getGlobalArg(comptime T: type, ctx: *Context) T {
if (comptime isPage(T)) {
return switch (ctx.global) {
.page => |page| page,
.worker => unreachable,
};
}
if (comptime isExecution(T)) {
return &ctx.execution;
}
if (comptime isSession(T)) {
return ctx.session;
}
@compileError("Unsupported global arg type: " ++ @typeName(T));
}
// These wrap the raw v8 C API to provide a cleaner interface.
pub const FunctionCallbackInfo = struct {
handle: *const v8.FunctionCallbackInfo,
@@ -719,15 +748,16 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info:
return args;
}
// If the last parameter is the Page, set it, and exclude it
// If the last parameter is the Page or Worker, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isPage(params[params.len - 1].type.?)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = local.ctx.page;
const LastParamType = params[params.len - 1].type.?;
if (comptime isPage(LastParamType) or isExecution(LastParamType) or isSession(LastParamType)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = getGlobalArg(LastParamType, local.ctx);
break :blk params[0 .. params.len - 1];
}
// we have neither a Page nor a JsObject. All params must be
// we have neither a Page, Execution, nor a JsObject. All params must be
// bound to a JavaScript value.
break :blk params;
};
@@ -776,7 +806,11 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info:
}
if (comptime isPage(param.type.?)) {
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
@compileError("Page must be the last parameter: " ++ @typeName(F));
} else if (comptime isExecution(param.type.?)) {
@compileError("Execution must be the last parameter: " ++ @typeName(F));
} else if (comptime isSession(param.type.?)) {
@compileError("Session must be the last parameter: " ++ @typeName(F));
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument;

View File

@@ -25,10 +25,12 @@ const bridge = @import("bridge.zig");
const Env = @import("Env.zig");
const Origin = @import("Origin.zig");
const Scheduler = @import("Scheduler.zig");
const Execution = @import("Execution.zig");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const ScriptManager = @import("../ScriptManager.zig");
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
const v8 = js.v8;
const Caller = js.Caller;
@@ -37,12 +39,38 @@ const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
// Loosely maps to a Browser Page.
// Loosely maps to a Browser Page or Worker.
const Context = @This();
pub const GlobalScope = union(enum) {
page: *Page,
worker: *WorkerGlobalScope,
pub fn base(self: GlobalScope) [:0]const u8 {
return switch (self) {
.page => |page| page.base(),
.worker => |worker| worker.base(),
};
}
pub fn getJs(self: GlobalScope) *Context {
return switch (self) {
.page => |page| page.js,
.worker => |worker| worker.js,
};
}
pub fn setJs(self: GlobalScope, ctx: *Context) void {
switch (self) {
.page => |page| page.js = ctx,
.worker => |worker| worker.js = ctx,
}
}
};
id: usize,
env: *Env,
page: *Page,
global: GlobalScope,
session: *Session,
isolate: js.Isolate,
@@ -111,6 +139,10 @@ script_manager: ?*ScriptManager,
// Our macrotasks
scheduler: Scheduler,
// Execution context for worker-compatible APIs. This provides a common
// interface that works in both Page and Worker contexts.
execution: Execution,
unknown_properties: (if (IS_DEBUG) std.StringHashMapUnmanaged(UnknownPropertyStat) else void) = if (IS_DEBUG) .{} else {},
const ModuleEntry = struct {
@@ -259,7 +291,11 @@ pub fn toLocal(self: *Context, global: anytype) js.Local.ToLocalReturnType(@Type
}
pub fn getIncumbent(self: *Context) *Page {
return fromC(v8.v8__Isolate__GetIncumbentContext(self.env.isolate.handle).?).?.page;
const ctx = fromC(v8.v8__Isolate__GetIncumbentContext(self.env.isolate.handle).?).?;
return switch (ctx.global) {
.page => |page| page,
.worker => unreachable,
};
}
pub fn stringToPersistedFunction(
@@ -529,7 +565,7 @@ pub fn dynamicModuleCallback(
if (resource_value.isNullOrUndefined()) {
// will only be null / undefined in extreme cases (e.g. WPT tests)
// where you're
break :blk self.page.base();
break :blk self.global.base();
}
break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
@@ -871,17 +907,16 @@ pub fn enter(self: *Context, hs: *js.HandleScope) Entered {
const isolate = self.isolate;
js.HandleScope.init(hs, isolate);
const page = self.page;
const original = page.js;
page.js = self;
const original = self.global.getJs();
self.global.setJs(self);
const handle: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle));
v8.v8__Context__Enter(handle);
return .{ .original = original, .handle = handle, .handle_scope = hs };
return .{ .original = original, .handle = handle, .handle_scope = hs, .global = self.global };
}
const Entered = struct {
// the context we should restore on the page
// the context we should restore on the page/worker
original: *Context,
// the handle of the entered context
@@ -889,8 +924,10 @@ const Entered = struct {
handle_scope: *js.HandleScope,
global: GlobalScope,
pub fn exit(self: Entered) void {
self.original.page.js = self.original;
self.global.setJs(self.original);
v8.v8__Context__Exit(self.handle);
self.handle_scope.deinit();
}
@@ -899,7 +936,10 @@ const Entered = struct {
pub fn queueMutationDelivery(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
ctx.page.deliverMutations();
switch (ctx.global) {
.page => |page| page.deliverMutations(),
.worker => unreachable,
}
}
}.run);
}
@@ -907,7 +947,10 @@ pub fn queueMutationDelivery(self: *Context) !void {
pub fn queueIntersectionChecks(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
ctx.page.performScheduledIntersectionChecks();
switch (ctx.global) {
.page => |page| page.performScheduledIntersectionChecks(),
.worker => unreachable,
}
}
}.run);
}
@@ -915,7 +958,10 @@ pub fn queueIntersectionChecks(self: *Context) !void {
pub fn queueIntersectionDelivery(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
ctx.page.deliverIntersections();
switch (ctx.global) {
.page => |page| page.deliverIntersections(),
.worker => unreachable,
}
}
}.run);
}
@@ -923,7 +969,10 @@ pub fn queueIntersectionDelivery(self: *Context) !void {
pub fn queueSlotchangeDelivery(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
ctx.page.deliverSlotchangeEvents();
switch (ctx.global) {
.page => |page| page.deliverSlotchangeEvents(),
.worker => unreachable,
}
}
}.run);
}

View File

@@ -34,6 +34,7 @@ const Inspector = @import("Inspector.zig");
const Page = @import("../Page.zig");
const Window = @import("../webapi/Window.zig");
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
const JsApis = bridge.JsApis;
const Allocator = std.mem.Allocator;
@@ -83,9 +84,6 @@ eternal_function_templates: []v8.Eternal,
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
templates: []*const v8.FunctionTemplate,
// Global template created once per isolate and reused across all contexts
global_template: v8.Eternal,
// Inspector associated with the Isolate. Exists when CDP is being used.
inspector: ?*Inspector,
@@ -146,7 +144,6 @@ pub fn init(app: *App, opts: InitOpts) !Env {
const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);
errdefer allocator.free(templates);
var global_eternal: v8.Eternal = undefined;
var private_symbols: PrivateSymbols = undefined;
{
var temp_scope: js.HandleScope = undefined;
@@ -164,44 +161,6 @@ pub fn init(app: *App, opts: InitOpts) !Env {
templates[i] = @ptrCast(@alignCast(eternal_ptr.?));
}
// Create global template once per isolate
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate_handle);
const window_name = v8.v8__String__NewFromUtf8(isolate_handle, "Window", v8.kNormal, 6);
v8.v8__FunctionTemplate__SetClassName(js_global, window_name);
// Find Window in JsApis by name (avoids circular import)
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
v8.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);
const global_template_local = v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
v8.v8__ObjectTemplate__SetNamedHandler(global_template_local, &.{
.getter = bridge.unknownWindowPropertyCallback,
.setter = null,
.query = null,
.deleter = null,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
});
// I don't 100% understand this. We actually set this up in the snapshot,
// but for the global instance, it doesn't work. SetIndexedHandler and
// SetNamedHandler are set on the Instance template, and that's the key
// difference. The context has its own global instance, so we need to set
// these back up directly on it. There might be a better way to do this.
v8.v8__ObjectTemplate__SetIndexedHandler(global_template_local, &.{
.getter = Window.JsApi.index.getter,
.setter = null,
.query = null,
.deleter = null,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = 0,
});
v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal);
private_symbols = PrivateSymbols.init(isolate_handle);
}
@@ -221,7 +180,6 @@ pub fn init(app: *App, opts: InitOpts) !Env {
.templates = templates,
.isolate_params = params,
.inspector = inspector,
.global_template = global_eternal,
.private_symbols = private_symbols,
.microtask_queues_are_running = false,
.eternal_function_templates = eternal_function_templates,
@@ -261,7 +219,18 @@ pub const ContextParams = struct {
};
pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
const context_arena = try self.app.arena_pool.acquire(.large, params.debug_name);
return self._createContext(page, params);
}
pub fn createWorkerContext(self: *Env, worker: *WorkerGlobalScope, params: ContextParams) !*Context {
return self._createContext(worker, params);
}
fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context {
const T = @TypeOf(global);
const is_page = T == *Page;
const context_arena = try self.app.arena_pool.acquire(.medium, params.debug_name);
errdefer self.app.arena_pool.release(context_arena);
const isolate = self.isolate;
@@ -273,12 +242,10 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
const microtask_queue = v8.v8__MicrotaskQueue__New(isolate.handle, v8.kExplicit).?;
errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue);
// Get the global template that was created once per isolate
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?));
v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime Snapshot.countInternalFields(Window.JsApi));
const v8_context = v8.v8__Context__New__Config(isolate.handle, &.{
.global_template = global_template,
// Restore the context from the snapshot (0 = Page, 1 = Worker)
const snapshot_index: u32 = if (comptime is_page) 0 else 1;
const v8_context = v8.v8__Context__FromSnapshot__Config(isolate.handle, snapshot_index, &.{
.global_template = null,
.global_object = null,
.microtask_queue = microtask_queue,
}).?;
@@ -287,36 +254,36 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
var context_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
// get the global object for the context, this maps to our Window
// Get the global object for the context
const global_obj = v8.v8__Context__Global(v8_context).?;
{
// Store our TAO inside the internal field of the global object. This
// maps the v8::Object -> Zig instance. Almost all objects have this, and
// it gets setup automatically as objects are created, but the Window
// object already exists in v8 (it's the global) so we manually create
// the mapping here.
const tao = try params.identity_arena.create(@import("TaggedOpaque.zig"));
tao.* = .{
.value = @ptrCast(page.window),
.prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr,
.prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len),
.subtype = .node, // this probably isn't right, but it's what we've been doing all along
};
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
}
// Store our TAO inside the internal field of the global object. This
// maps the v8::Object -> Zig instance.
const tao = try params.identity_arena.create(@import("TaggedOpaque.zig"));
tao.* = if (comptime is_page) .{
.value = @ptrCast(global.window),
.prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr,
.prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len),
.subtype = .node,
} else .{
.value = @ptrCast(global),
.prototype_chain = (&WorkerGlobalScope.JsApi.Meta.prototype_chain).ptr,
.prototype_len = @intCast(WorkerGlobalScope.JsApi.Meta.prototype_chain.len),
.subtype = null,
};
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
const context_id = self.context_id;
self.context_id = context_id + 1;
const session = page._session;
const session = global._session;
const origin = try session.getOrCreateOrigin(null);
errdefer session.releaseOrigin(origin);
const context = try context_arena.create(Context);
context.* = .{
.env = self,
.page = page,
.global = if (comptime is_page) .{ .page = global } else .{ .worker = global },
.origin = origin,
.id = context_id,
.session = session,
@@ -326,22 +293,31 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
.templates = self.templates,
.call_arena = params.call_arena,
.microtask_queue = microtask_queue,
.script_manager = &page._script_manager,
.script_manager = if (comptime is_page) &global._script_manager else null,
.scheduler = .init(context_arena),
.identity = params.identity,
.identity_arena = params.identity_arena,
.execution = undefined,
};
{
// Multiple contexts can be created for the same Window (via CDP). We only
// need to register the first one.
const gop = try params.identity.identity_map.getOrPut(params.identity_arena, @intFromPtr(page.window));
if (gop.found_existing == false) {
// our window wrapped in a v8::Global
var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
gop.value_ptr.* = global_global;
}
context.execution = .{
.url = &global.url,
.buf = &global.buf,
.context = context,
.arena = global.arena,
.call_arena = params.call_arena,
._factory = global._factory,
._scheduler = &context.scheduler,
};
// Register in the identity map. Multiple contexts can be created for the
// same global (via CDP), so we only register the first one.
const identity_ptr = if (comptime is_page) @intFromPtr(global.window) else @intFromPtr(global);
const gop = try params.identity.identity_map.getOrPut(params.identity_arena, identity_ptr);
if (gop.found_existing == false) {
var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
gop.value_ptr.* = global_global;
}
// Store a pointer to our context inside the v8 context so that, given
@@ -528,13 +504,25 @@ fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) v
.call_arena = ctx.call_arena,
};
const page = ctx.page;
page.window.unhandledPromiseRejection(promise_event == v8.kPromiseRejectWithNoHandler, .{
.local = &local,
.handle = &message_handle,
}, page) catch |err| {
log.warn(.browser, "unhandled rejection handler", .{ .err = err });
};
const no_handler = promise_event == v8.kPromiseRejectWithNoHandler;
switch (ctx.global) {
.page => |page| {
page.window.unhandledPromiseRejection(no_handler, .{
.local = &local,
.handle = &message_handle,
}, page) catch |err| {
log.warn(.browser, "unhandled rejection handler", .{ .err = err, .target = "window" });
};
},
.worker => |wsg| {
wsg.unhandledPromiseRejection(no_handler, .{
.local = &local,
.handle = &message_handle,
}) catch |err| {
log.warn(.browser, "unhandled rejection handler", .{ .err = err, .target = "worker" });
};
},
}
}
fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {
@@ -566,3 +554,35 @@ const PrivateSymbols = struct {
self.child_nodes.deinit();
}
};
const testing = @import("../../testing.zig");
test "Env: Worker context " {
const session = testing.test_session;
const page = try session.createPage();
defer session.removePage();
const worker = try @import("../webapi/Worker.zig").init("about:blank", &page.js.execution);
var ls: js.Local.Scope = undefined;
worker._worker_scope.js.localScope(&ls);
defer ls.deinit();
try testing.expectEqual(true, (try ls.local.exec("typeof Node === 'undefined'", null)).isTrue());
try testing.expectEqual(true, (try ls.local.exec("typeof WorkerGlobalScope !== 'undefined'", null)).isTrue());
}
test "Env: Page context" {
const session = testing.test_session;
const page = try session.createPage();
defer session.removePage();
// Page already has a context created, use it directly
const ctx = page.js;
var ls: js.Local.Scope = undefined;
ctx.localScope(&ls);
defer ls.deinit();
try testing.expectEqual(true, (try ls.local.exec("typeof Node !== 'undefined'", null)).isTrue());
try testing.expectEqual(true, (try ls.local.exec("typeof WorkerGlobalScope === 'undefined'", null)).isTrue());
}

View File

@@ -0,0 +1,47 @@
// 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/>.
//! Execution context for worker-compatible APIs.
//!
//! This provides a common interface for APIs that work in both Window and Worker
//! contexts. Instead of taking `*Page` (which is DOM-specific), these APIs take
//! `*Execution` which abstracts the common infrastructure.
//!
//! The bridge constructs an Execution on-the-fly from the current context,
//! whether it's a Page context or a Worker context.
const std = @import("std");
const Context = @import("Context.zig");
const Scheduler = @import("Scheduler.zig");
const Factory = @import("../Factory.zig");
const Allocator = std.mem.Allocator;
const Execution = @This();
context: *Context,
// Fields named to match Page for generic code (executor._factory works for both)
buf: []u8,
arena: Allocator,
call_arena: Allocator,
_factory: *Factory,
_scheduler: *Scheduler,
// Pointer to the url field (Page or WorkerGlobalScope) - allows access to current url even after navigation
url: *[:0]const u8,

View File

@@ -332,7 +332,15 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
}
if (@typeInfo(ptr.child) == .@"struct" and @hasDecl(ptr.child, "runtimeGenericWrap")) {
const wrap = try value.runtimeGenericWrap(self.ctx.page);
const page = switch (self.ctx.global) {
.page => |p| p,
.worker => {
// No Worker-related API currently uses this, so haven't
// added support for it
unreachable;
},
};
const wrap = try value.runtimeGenericWrap(page);
return self.zigValueToJs(wrap, opts);
}
@@ -409,7 +417,15 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
// zig fmt: on
if (@hasDecl(T, "runtimeGenericWrap")) {
const wrap = try value.runtimeGenericWrap(self.ctx.page);
const page = switch (self.ctx.global) {
.page => |p| p,
.worker => {
// No Worker-related API currently uses this, so haven't
// added support for it
unreachable;
},
};
const wrap = try value.runtimeGenericWrap(page);
return self.zigValueToJs(wrap, opts);
}

View File

@@ -52,6 +52,11 @@ pub fn deinit(self: *Scheduler) void {
finalizeTasks(&self.high_priority);
}
pub fn reset(self: *Scheduler) void {
self.low_priority.clearRetainingCapacity();
self.high_priority.clearRetainingCapacity();
}
const AddOpts = struct {
name: []const u8 = "",
low_priority: bool = false,

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("js.zig");
const bridge = @import("bridge.zig");
const log = @import("../../log.zig");
@@ -25,6 +26,8 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8;
const JsApis = bridge.JsApis;
const PageJsApis = bridge.PageJsApis;
const WorkerJsApis = bridge.WorkerJsApis;
const Snapshot = @This();
@@ -136,7 +139,7 @@ pub fn create() !Snapshot {
v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
// Create templates (constructors only) FIRST
// Create templates for ALL types (JsApis)
var templates: [JsApis.len]*const v8.FunctionTemplate = undefined;
inline for (JsApis, 0..) |JsApi, i| {
@setEvalBranchQuota(10_000);
@@ -145,114 +148,51 @@ pub fn create() !Snapshot {
}
// Set up prototype chains BEFORE attaching properties
// This must come before attachClass so inheritance is set up first
inline for (JsApis, 0..) |JsApi, i| {
if (comptime protoIndexLookup(JsApi)) |proto_index| {
v8.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]);
}
}
// Set up the global template to inherit from Window's template
// This way the global object gets all Window properties through inheritance
const context = v8.v8__Context__New(isolate, null, null);
v8.v8__Context__Enter(context);
defer v8.v8__Context__Exit(context);
// Add ALL templates to snapshot (done once, in any context)
// We need a context to call AddData, so create a temporary one
{
const temp_context = v8.v8__Context__New(isolate, null, null);
v8.v8__Context__Enter(temp_context);
defer v8.v8__Context__Exit(temp_context);
// Add templates to context snapshot
var last_data_index: usize = 0;
inline for (JsApis, 0..) |_, i| {
@setEvalBranchQuota(10_000);
const data_index = v8.v8__SnapshotCreator__AddData(snapshot_creator, @ptrCast(templates[i]));
if (i == 0) {
data_start = data_index;
last_data_index = data_index;
} else {
// This isn't strictly required, but it means we only need to keep
// the first data_index. This is based on the assumption that
// addDataWithContext always increases by 1. If we ever hit this
// error, then that assumption is wrong and we should capture
// all the indexes explicitly in an array.
if (data_index != last_data_index + 1) {
return error.InvalidDataIndex;
}
last_data_index = data_index;
}
}
// Realize all templates by getting their functions and attaching to global
const global_obj = v8.v8__Context__Global(context);
inline for (JsApis, 0..) |JsApi, i| {
const func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
// Attach to global if it has a name
if (@hasDecl(JsApi.Meta, "name")) {
if (@hasDecl(JsApi.Meta, "constructor_alias")) {
const alias = JsApi.Meta.constructor_alias;
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len));
var maybe_result: v8.MaybeBool = undefined;
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
// @TODO: This is wrong. This name should be registered with the
// illegalConstructorCallback. I.e. new Image() is OK, but
// new HTMLImageElement() isn't.
// But we _have_ to register the name, i.e. HTMLImageElement
// has to be registered so, for now, instead of creating another
// template, we just hook it into the constructor.
const name = JsApi.Meta.name;
const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
var maybe_result2: v8.MaybeBool = undefined;
v8.v8__Object__DefineOwnProperty(global_obj, context, illegal_class_name, func, 0, &maybe_result2);
var last_data_index: usize = 0;
inline for (JsApis, 0..) |_, i| {
@setEvalBranchQuota(10_000);
const data_index = v8.v8__SnapshotCreator__AddData(snapshot_creator, @ptrCast(templates[i]));
if (i == 0) {
data_start = data_index;
last_data_index = data_index;
} else {
const name = JsApi.Meta.name;
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
var maybe_result: v8.MaybeBool = undefined;
var properties: v8.PropertyAttribute = v8.None;
if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == false) {
properties |= v8.DontEnum;
if (data_index != last_data_index + 1) {
return error.InvalidDataIndex;
}
v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, properties, &maybe_result);
last_data_index = data_index;
}
}
// V8 requires a default context. We could probably make this our
// Page context, but having both the Page and Worker context be
// added via addContext makes things a little more consistent.
v8.v8__SnapshotCreator__setDefaultContext(snapshot_creator, temp_context);
}
{
// If we want to overwrite the built-in console, we have to
// delete the built-in one.
const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7);
var maybe_deleted: v8.MaybeBool = undefined;
v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted);
if (maybe_deleted.value == false) {
return error.ConsoleDeleteError;
}
}
// This shouldn't be necessary, but it is:
// https://groups.google.com/g/v8-users/c/qAQQBmbi--8
// TODO: see if newer V8 engines have a way around this.
inline for (JsApis, 0..) |JsApi, i| {
if (comptime protoIndexLookup(JsApi)) |proto_index| {
const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context);
const proto_obj: *const v8.Object = @ptrCast(proto_func);
const self_func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
const self_obj: *const v8.Object = @ptrCast(self_func);
var maybe_result: v8.MaybeBool = undefined;
v8.v8__Object__SetPrototype(self_obj, context, proto_obj, &maybe_result);
}
const Window = @import("../webapi/Window.zig");
const index = try createSnapshotContext(&PageJsApis, Window.JsApi, isolate, snapshot_creator.?, &templates);
std.debug.assert(index == 0);
}
{
// Custom exception
// TODO: this is an horrible hack, I can't figure out how to do this cleanly.
const code_str = "DOMException.prototype.__proto__ = Error.prototype";
const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len));
const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed;
_ = v8.v8__Script__Run(script, context) orelse return error.ScriptRunFailed;
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
const index = try createSnapshotContext(&WorkerJsApis, WorkerGlobalScope.JsApi, isolate, snapshot_creator.?, &templates);
std.debug.assert(index == 1);
}
v8.v8__SnapshotCreator__setDefaultContext(snapshot_creator, context);
}
const blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep);
@@ -260,25 +200,127 @@ pub fn create() !Snapshot {
return .{
.owns_data = true,
.data_start = data_start,
.external_references = external_references,
.startup_data = blob,
.external_references = external_references,
};
}
// Helper to check if a JsApi has a NamedIndexed handler
fn hasNamedIndexedGetter(comptime JsApi: type) bool {
const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| {
const value = @field(JsApi, d.name);
const T = @TypeOf(value);
if (T == bridge.NamedIndexed) {
return true;
fn createSnapshotContext(
comptime ContextApis: []const type,
comptime GlobalScopeApi: type,
isolate: *v8.Isolate,
snapshot_creator: *v8.SnapshotCreator,
templates: []*const v8.FunctionTemplate,
) !usize {
// Create a global template that inherits from the GlobalScopeApi (Window or WorkerGlobalScope)
const global_scope_index = comptime bridge.JsApiLookup.getId(GlobalScopeApi);
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate);
const class_name = v8.v8__String__NewFromUtf8(isolate, GlobalScopeApi.Meta.name.ptr, v8.kNormal, @intCast(GlobalScopeApi.Meta.name.len));
v8.v8__FunctionTemplate__SetClassName(js_global, class_name);
v8.v8__FunctionTemplate__Inherit(js_global, templates[global_scope_index]);
const global_template = v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime countInternalFields(GlobalScopeApi));
// Set up named/indexed handlers for Window's global object (for named element access like window.myDiv)
if (comptime std.mem.eql(u8, GlobalScopeApi.Meta.name, "Window")) {
v8.v8__ObjectTemplate__SetNamedHandler(global_template, &.{
.getter = bridge.unknownWindowPropertyCallback,
.setter = null,
.query = null,
.deleter = null,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
});
v8.v8__ObjectTemplate__SetIndexedHandler(global_template, &.{
.getter = @import("../webapi/Window.zig").JsApi.index.getter,
.setter = null,
.query = null,
.deleter = null,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = 0,
});
}
const context = v8.v8__Context__New(isolate, global_template, null);
v8.v8__Context__Enter(context);
defer v8.v8__Context__Exit(context);
// Initialize embedder data to null so callbacks can detect snapshot creation
v8.v8__Context__SetAlignedPointerInEmbedderData(context, 1, null);
const global_obj = v8.v8__Context__Global(context);
// Attach constructors for this context's APIs to the global
inline for (ContextApis) |JsApi| {
const template_index = comptime bridge.JsApiLookup.getId(JsApi);
const func = v8.v8__FunctionTemplate__GetFunction(templates[template_index], context);
if (@hasDecl(JsApi.Meta, "name")) {
if (@hasDecl(JsApi.Meta, "constructor_alias")) {
const alias = JsApi.Meta.constructor_alias;
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len));
var maybe_result: v8.MaybeBool = undefined;
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
const name = JsApi.Meta.name;
const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
var maybe_result2: v8.MaybeBool = undefined;
v8.v8__Object__DefineOwnProperty(global_obj, context, illegal_class_name, func, 0, &maybe_result2);
} else {
const name = JsApi.Meta.name;
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
var maybe_result: v8.MaybeBool = undefined;
var properties: v8.PropertyAttribute = v8.None;
if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == false) {
properties |= v8.DontEnum;
}
v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, properties, &maybe_result);
}
}
}
return false;
{
// Delete built-in console so we can inject our own
const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7);
var maybe_deleted: v8.MaybeBool = undefined;
v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted);
if (maybe_deleted.value == false) {
return error.ConsoleDeleteError;
}
}
// Set prototype chains on function objects
// https://groups.google.com/g/v8-users/c/qAQQBmbi--8
inline for (JsApis, 0..) |JsApi, i| {
if (comptime protoIndexLookup(JsApi)) |proto_index| {
const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context);
const proto_obj: *const v8.Object = @ptrCast(proto_func);
const self_func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
const self_obj: *const v8.Object = @ptrCast(self_func);
var maybe_result: v8.MaybeBool = undefined;
v8.v8__Object__SetPrototype(self_obj, context, proto_obj, &maybe_result);
}
}
{
// DOMException prototype setup
const code_str = "DOMException.prototype.__proto__ = Error.prototype";
const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len));
const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed;
_ = v8.v8__Script__Run(script, context) orelse return error.ScriptRunFailed;
}
return v8.v8__SnapshotCreator__AddContext(snapshot_creator, context);
}
// Count total callbacks needed for external_references array
fn countExternalReferences() comptime_int {
@setEvalBranchQuota(100_000);
@@ -290,24 +332,24 @@ fn countExternalReferences() comptime_int {
// +1 for the noop function shared by various types
count += 1;
// +1 for unknownWindowPropertyCallback used on Window's global template
count += 1;
inline for (JsApis) |JsApi| {
// Constructor (only if explicit)
if (@hasDecl(JsApi, "constructor")) {
count += 1;
}
// Callable (htmldda)
if (@hasDecl(JsApi, "callable")) {
count += 1;
}
// All other callbacks
const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| {
const value = @field(JsApi, d.name);
const T = @TypeOf(value);
if (T == bridge.Accessor) {
count += 1; // getter
count += 1;
if (value.setter != null) {
count += 1;
}
@@ -321,14 +363,13 @@ fn countExternalReferences() comptime_int {
count += 1;
}
} else if (T == bridge.NamedIndexed) {
count += 1; // getter
count += 1;
if (value.setter != null) count += 1;
if (value.deleter != null) count += 1;
}
}
}
// In debug mode, add unknown property callbacks for types without NamedIndexed
if (comptime IS_DEBUG) {
inline for (JsApis) |JsApi| {
if (!hasNamedIndexedGetter(JsApi)) {
@@ -350,6 +391,9 @@ fn collectExternalReferences() [countExternalReferences()]isize {
references[idx] = @bitCast(@intFromPtr(&bridge.Function.noopFunction));
idx += 1;
references[idx] = @bitCast(@intFromPtr(&bridge.unknownWindowPropertyCallback));
idx += 1;
inline for (JsApis) |JsApi| {
if (@hasDecl(JsApi, "constructor")) {
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
@@ -400,7 +444,6 @@ fn collectExternalReferences() [countExternalReferences()]isize {
}
}
// In debug mode, collect unknown property callbacks for types without NamedIndexed
if (comptime IS_DEBUG) {
inline for (JsApis) |JsApi| {
if (!hasNamedIndexedGetter(JsApi)) {
@@ -413,37 +456,7 @@ fn collectExternalReferences() [countExternalReferences()]isize {
return references;
}
// Even if a struct doesn't have a `constructor` function, we still
// `generateConstructor`, because this is how we create our
// FunctionTemplate. Such classes exist, but they can't be instantiated
// via `new ClassName()` - but they could, for example, be created in
// Zig and returned from a function call, which is why we need the
// FunctionTemplate.
fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate {
const callback = blk: {
if (@hasDecl(JsApi, "constructor")) {
break :blk JsApi.constructor.func;
}
// Use shared illegal constructor callback
break :blk illegalConstructorCallback;
};
const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?;
{
const internal_field_count = comptime countInternalFields(JsApi);
if (internal_field_count > 0) {
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);
}
}
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
v8.v8__FunctionTemplate__SetClassName(template, class_name);
return template;
}
pub fn countInternalFields(comptime JsApi: type) u8 {
fn countInternalFields(comptime JsApi: type) u8 {
var last_used_id = 0;
var cache_count: u8 = 0;
@@ -481,14 +494,80 @@ pub fn countInternalFields(comptime JsApi: type) u8 {
return cache_count + 1;
}
// Attaches JsApi members to the prototype template (normal case)
// Shared illegal constructor callback for types without explicit constructors
fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);
log.warn(.js, "Illegal constructor call", .{});
const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19);
const js_exception = v8.v8__Exception__TypeError(message);
_ = v8.v8__Isolate__ThrowException(isolate, js_exception);
var return_value: v8.ReturnValue = undefined;
v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);
v8.v8__ReturnValue__Set(return_value, js_exception);
}
// Helper to check if a JsApi has a NamedIndexed handler (public for reuse)
fn hasNamedIndexedGetter(comptime JsApi: type) bool {
const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| {
const value = @field(JsApi, d.name);
const T = @TypeOf(value);
if (T == bridge.NamedIndexed) {
return true;
}
}
return false;
}
// Generic prototype index lookup for a given API list
fn protoIndexLookup(comptime JsApi: type) ?u16 {
@setEvalBranchQuota(100_000);
comptime {
const T = JsApi.bridge.type;
if (!@hasField(T, "_proto")) {
return null;
}
const Ptr = std.meta.fieldInfo(T, ._proto).type;
const F = @typeInfo(Ptr).pointer.child;
// Look up in the provided API list
for (JsApis, 0..) |Api, i| {
if (Api == F.JsApi) {
return i;
}
}
@compileError("Prototype " ++ @typeName(F.JsApi) ++ " not found in API list");
}
}
// Generate a constructor template for a JsApi type (public for reuse)
pub fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate {
const callback = blk: {
if (@hasDecl(JsApi, "constructor")) {
break :blk JsApi.constructor.func;
}
break :blk illegalConstructorCallback;
};
const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?;
{
const internal_field_count = comptime countInternalFields(JsApi);
if (internal_field_count > 0) {
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);
}
}
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
v8.v8__FunctionTemplate__SetClassName(template, class_name);
return template;
}
// Attach JsApi members to a template (public for reuse)
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.FunctionTemplate) void {
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
// Create a signature that validates the receiver is an instance of this template.
// This prevents crashes when JavaScript extracts a getter/method and calls it
// with the wrong `this` (e.g., documentGetter.call(null)).
const signature = v8.v8__Signature__New(isolate, template);
const declarations = @typeInfo(JsApi).@"struct".decls;
@@ -524,7 +603,6 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
}
if (value.static) {
// Static accessors: use Template's SetAccessorProperty
v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute);
} else {
v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{
@@ -600,11 +678,10 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
}
if (value.template) {
// apply it both to the type itself (e.g. Node.Elem)
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
}
},
bridge.Constructor => {}, // already handled in generateConstructor
bridge.Constructor => {},
else => {},
}
}
@@ -637,30 +714,3 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
}
}
}
fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
@setEvalBranchQuota(2000);
comptime {
const T = JsApi.bridge.type;
if (!@hasField(T, "_proto")) {
return null;
}
const Ptr = std.meta.fieldInfo(T, ._proto).type;
const F = @typeInfo(Ptr).pointer.child;
return bridge.JsApiLookup.getId(F.JsApi);
}
}
// Shared illegal constructor callback for types without explicit constructors
fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);
log.warn(.js, "Illegal constructor call", .{});
const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19);
const js_exception = v8.v8__Exception__TypeError(message);
_ = v8.v8__Isolate__ThrowException(isolate, js_exception);
var return_value: v8.ReturnValue = undefined;
v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);
v8.v8__ReturnValue__Set(return_value, js_exception);
}

View File

@@ -248,9 +248,15 @@ pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
// Throws a DataCloneError for host objects (Blob, File, etc.) that cannot be serialized.
// Does not support transferables which require additional delegate callbacks.
pub fn structuredClone(self: Value) !Value {
const local = self.local;
const v8_context = local.handle;
const v8_isolate = local.isolate.handle;
return self.structuredCloneTo(self.local);
}
// Clone a value to a different context (within the same isolate).
// Used for cross-context messaging (e.g., Worker <-> Page).
pub fn structuredCloneTo(self: Value, target: *const js.Local) !Value {
const source_context = self.local.handle;
const target_context = target.handle;
const v8_isolate = target.isolate.handle;
const SerializerDelegate = struct {
// Called when V8 encounters a host object it doesn't know how to serialize.
@@ -280,7 +286,7 @@ pub fn structuredClone(self: Value) !Value {
var write_result: v8.MaybeBool = undefined;
v8.v8__ValueSerializer__WriteHeader(serializer);
v8.v8__ValueSerializer__WriteValue(serializer, v8_context, self.handle, &write_result);
v8.v8__ValueSerializer__WriteValue(serializer, source_context, self.handle, &write_result);
if (!write_result.has_value or !write_result.value) {
return error.JsException;
}
@@ -297,14 +303,14 @@ pub fn structuredClone(self: Value) !Value {
defer v8.v8__ValueDeserializer__DELETE(deserializer);
var read_header_result: v8.MaybeBool = undefined;
v8.v8__ValueDeserializer__ReadHeader(deserializer, v8_context, &read_header_result);
v8.v8__ValueDeserializer__ReadHeader(deserializer, target_context, &read_header_result);
if (!read_header_result.has_value or !read_header_result.value) {
return error.JsException;
}
break :blk v8.v8__ValueDeserializer__ReadValue(deserializer, v8_context) orelse return error.JsException;
break :blk v8.v8__ValueDeserializer__ReadValue(deserializer, target_context) orelse return error.JsException;
};
return .{ .local = local, .handle = cloned_handle };
return .{ .local = target, .handle = cloned_handle };
}
pub fn persist(self: Value) !Global {

View File

@@ -24,6 +24,7 @@ const Session = @import("../Session.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
@@ -398,10 +399,15 @@ pub const Property = struct {
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
// During snapshot creation, there's no Context in embedder data yet.
// I hate this check, but there doesn't seem to be a way to add this method
// to the global, without triggering it during snapshot creation.
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate) orelse return 0;
const ctx: *Context = @ptrCast(@alignCast(v8.v8__Context__GetAlignedPointerFromEmbedderData(v8_context, 1) orelse return 0));
var caller: Caller = undefined;
if (!caller.init(v8_isolate)) {
return 0;
}
caller.initWithContext(ctx, v8_context);
defer caller.deinit();
const local = &caller.local;
@@ -414,14 +420,18 @@ pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8
return 0;
};
const page = local.ctx.page;
const document = page.document;
if (document.getElementById(property, page)) |el| {
const js_val = local.zigValueToJs(el, .{}) catch return 0;
var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };
pc.getReturnValue().set(js_val);
return 1;
// Only Page contexts have document.getElementById lookup
switch (local.ctx.global) {
.page => |page| {
const document = page.document;
if (document.getElementById(property, page)) |el| {
const js_val = local.zigValueToJs(el, .{}) catch return 0;
var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };
pc.getReturnValue().set(js_val);
return 1;
}
},
.worker => {}, // no global lookup in a worker
}
if (comptime IS_DEBUG) {
@@ -459,7 +469,8 @@ pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8
.{ "ApplePaySession", {} },
});
if (!ignored.has(property)) {
const key = std.fmt.bufPrint(&local.ctx.page.buf, "Window:{s}", .{property}) catch return 0;
var buf: [2048]u8 = undefined;
const key = std.fmt.bufPrint(&buf, "Window:{s}", .{property}) catch return 0;
logUnknownProperty(local, key) catch return 0;
}
}
@@ -524,7 +535,8 @@ pub fn unknownObjectPropertyCallback(comptime JsApi: type) *const fn (?*const v8
const ignored = std.StaticStringMap(void).initComptime(.{});
if (!ignored.has(property)) {
const key = std.fmt.bufPrint(&local.ctx.page.buf, "{s}:{s}", .{ if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi), property }) catch return 0;
var buf: [2048]u8 = undefined;
const key = std.fmt.bufPrint(&buf, "{s}:{s}", .{ if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi), property }) catch return 0;
logUnknownProperty(local, key) catch return 0;
}
// not intercepted
@@ -689,7 +701,8 @@ pub const SubType = enum {
webassemblymemory,
};
pub const JsApis = flattenTypes(&.{
// APIs for Page/Window contexts. Used by Snapshot.zig for Page snapshot creation.
pub const PageJsApis = flattenTypes(&.{
@import("../webapi/AbortController.zig"),
@import("../webapi/AbortSignal.zig"),
@import("../webapi/CData.zig"),
@@ -831,6 +844,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/event/FormDataEvent.zig"),
@import("../webapi/MessageChannel.zig"),
@import("../webapi/MessagePort.zig"),
@import("../webapi/Worker.zig"),
@import("../webapi/media/MediaError.zig"),
@import("../webapi/media/TextTrackCue.zig"),
@import("../webapi/media/VTTCue.zig"),
@@ -885,3 +899,33 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/Selection.zig"),
@import("../webapi/ImageData.zig"),
});
// APIs available on Worker context globals (constructors like URL, Headers, etc.)
// This is a subset of PageJsApis plus WorkerGlobalScope.
// TODO: Expand this list to include all worker-appropriate APIs.
pub const WorkerJsApis = flattenTypes(&.{
@import("../webapi/WorkerGlobalScope.zig"),
@import("../webapi/EventTarget.zig"),
@import("../webapi/DOMException.zig"),
@import("../webapi/net/URLSearchParams.zig"),
@import("../webapi/encoding/TextEncoder.zig"),
@import("../webapi/encoding/TextDecoder.zig"),
@import("../webapi/File.zig"),
@import("../webapi/Console.zig"),
@import("../webapi/Crypto.zig"),
// @import("../webapi/URL.zig"),
// @import("../webapi/Blob.zig"),
// @import("../webapi/net/FormData.zig"),
// @import("../webapi/Performance.zig"),
// @import("../webapi/net/Response.zig"),
// @import("../webapi/net/Request.zig"),
// @import("../webapi/net/Headers.zig"),
// @import("../webapi/AbortSignal.zig"),
// @import("../webapi/AbortController.zig"),
});
// Master list of ALL JS APIs across all contexts.
// Used by Env (class IDs, templates), JsApiLookup, and anywhere that needs
// to know about all possible types. Individual snapshots use their own
// subsets (PageJsApis, WorkerSnapshot.JsApis).
pub const JsApis = PageJsApis ++ [_]type{@import("../webapi/WorkerGlobalScope.zig").JsApi};

View File

@@ -27,6 +27,7 @@ pub const Caller = @import("Caller.zig");
pub const Origin = @import("Origin.zig");
pub const Identity = @import("Identity.zig");
pub const Context = @import("Context.zig");
pub const Execution = @import("Execution.zig");
pub const Local = @import("Local.zig");
pub const Inspector = @import("Inspector.zig");
pub const Snapshot = @import("Snapshot.zig");

View File

@@ -0,0 +1,4 @@
// Simple worker that echoes messages back with a prefix
onmessage = function(event) {
postMessage({ echo: event.data, from: 'worker' });
};

View File

@@ -0,0 +1,206 @@
<!DOCTYPE html>
<body></body>
<script src="../testing.js"></script>
<script id="worker_creation">
{
// Just test that Worker constructor works - don't terminate immediately
// as that would destroy the context before the script loads
const worker = new Worker('./echo-worker.js');
testing.expectTrue(worker instanceof Worker);
}
</script>
<script id="worker_message">
testing.async(async (capture) => {
const worker = new Worker('./echo-worker.js');
const response = await new Promise((resolve) => {
worker.onmessage = function(event) {
resolve(event.data);
};
// Give the script time to load before posting
setTimeout(() => {
worker.postMessage({ greeting: 'hello' });
}, 100);
});
capture();
testing.expectEqual('hello', response.echo.greeting);
testing.expectEqual('worker', response.from);
});
</script>
<script id="worker_structured_clone_date">
testing.async(async (capture) => {
const worker = new Worker('./echo-worker.js');
const testDate = new Date('2024-06-15T12:30:00Z');
const response = await new Promise((resolve) => {
worker.onmessage = function(event) {
resolve(event.data);
};
setTimeout(() => {
worker.postMessage({ date: testDate });
}, 100);
});
capture();
testing.expectTrue(response.echo.date instanceof Date);
testing.expectEqual(testDate.getTime(), response.echo.date.getTime());
});
</script>
<script id="worker_structured_clone_arraybuffer">
testing.async(async (capture) => {
const worker = new Worker('./echo-worker.js');
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
view[0] = 1; view[1] = 2; view[2] = 3; view[3] = 4;
view[4] = 5; view[5] = 6; view[6] = 7; view[7] = 8;
const response = await new Promise((resolve) => {
worker.onmessage = function(event) {
resolve(event.data);
};
setTimeout(() => {
worker.postMessage({ buffer: buffer });
}, 100);
});
capture();
testing.expectTrue(response.echo.buffer instanceof ArrayBuffer);
testing.expectEqual(8, response.echo.buffer.byteLength);
const resultView = new Uint8Array(response.echo.buffer);
testing.expectEqual(1, resultView[0]);
testing.expectEqual(8, resultView[7]);
});
</script>
<script id="worker_structured_clone_typedarray">
testing.async(async (capture) => {
const worker = new Worker('./echo-worker.js');
const arr = new Float64Array([1.5, 2.5, 3.5, 4.5]);
const response = await new Promise((resolve) => {
worker.onmessage = function(event) {
resolve(event.data);
};
setTimeout(() => {
worker.postMessage({ arr: arr });
}, 100);
});
capture();
testing.expectTrue(response.echo.arr instanceof Float64Array);
testing.expectEqual(4, response.echo.arr.length);
testing.expectEqual(1.5, response.echo.arr[0]);
testing.expectEqual(4.5, response.echo.arr[3]);
});
</script>
<script id="worker_structured_clone_map">
testing.async(async (capture) => {
const worker = new Worker('./echo-worker.js');
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
const response = await new Promise((resolve) => {
worker.onmessage = function(event) {
resolve(event.data);
};
setTimeout(() => {
worker.postMessage({ map: map });
}, 100);
});
capture();
testing.expectTrue(response.echo.map instanceof Map);
testing.expectEqual(3, response.echo.map.size);
testing.expectEqual(1, response.echo.map.get('a'));
testing.expectEqual(3, response.echo.map.get('c'));
});
</script>
<script id="worker_structured_clone_set">
testing.async(async (capture) => {
const worker = new Worker('./echo-worker.js');
const set = new Set([1, 2, 3, 'four', 'five']);
const response = await new Promise((resolve) => {
worker.onmessage = function(event) {
resolve(event.data);
};
setTimeout(() => {
worker.postMessage({ set: set });
}, 100);
});
capture();
testing.expectTrue(response.echo.set instanceof Set);
testing.expectEqual(5, response.echo.set.size);
testing.expectTrue(response.echo.set.has(1));
testing.expectTrue(response.echo.set.has('four'));
});
</script>
<script id="worker_structured_clone_regexp">
testing.async(async (capture) => {
const worker = new Worker('./echo-worker.js');
const regex = /hello.*world/gi;
const response = await new Promise((resolve) => {
worker.onmessage = function(event) {
resolve(event.data);
};
setTimeout(() => {
worker.postMessage({ regex: regex });
}, 100);
});
capture();
testing.expectTrue(response.echo.regex instanceof RegExp);
testing.expectEqual('hello.*world', response.echo.regex.source);
testing.expectTrue(response.echo.regex.global);
testing.expectTrue(response.echo.regex.ignoreCase);
});
</script>
<script id="worker_structured_clone_nested">
testing.async(async (capture) => {
const worker = new Worker('./echo-worker.js');
const complex = {
string: 'hello',
number: 42,
boolean: true,
null: null,
array: [1, 2, { nested: 'value' }],
date: new Date('2024-01-01'),
buffer: new Uint8Array([10, 20, 30]).buffer
};
const response = await new Promise((resolve) => {
worker.onmessage = function(event) {
resolve(event.data);
};
setTimeout(() => {
worker.postMessage(complex);
}, 100);
});
capture();
testing.expectEqual('hello', response.echo.string);
testing.expectEqual(42, response.echo.number);
testing.expectEqual(true, response.echo.boolean);
testing.expectEqual(null, response.echo.null);
testing.expectEqual(3, response.echo.array.length);
testing.expectEqual('value', response.echo.array[2].nested);
testing.expectTrue(response.echo.date instanceof Date);
testing.expectTrue(response.echo.buffer instanceof ArrayBuffer);
});
</script>

View File

@@ -29,61 +29,61 @@ _counts: std.StringHashMapUnmanaged(u64) = .{},
pub const init: Console = .{};
pub fn trace(_: *const Console, values: []js.Value, page: *Page) !void {
pub fn trace(_: *const Console, values: []js.Value, exec: *js.Execution) !void {
logger.debug(.js, "console.trace", .{
.stack = page.js.local.?.stackTrace() catch "???",
.args = ValueWriter{ .page = page, .values = values },
.stack = exec.context.local.?.stackTrace() catch "???",
.args = ValueWriter{ .values = values },
});
}
pub fn debug(_: *const Console, values: []js.Value, page: *Page) void {
logger.debug(.js, "console.debug", .{ValueWriter{ .page = page, .values = values }});
page.appendConsoleMessage(.debug, values);
pub fn debug(_: *const Console, values: []js.Value, exec: *js.Execution) void {
logger.debug(.js, "console.debug", .{ValueWriter{ .values = values }});
appendMessage(exec, .debug, values);
}
pub fn info(_: *const Console, values: []js.Value, page: *Page) void {
logger.info(.js, "console.info", .{ValueWriter{ .page = page, .values = values }});
page.appendConsoleMessage(.info, values);
pub fn info(_: *const Console, values: []js.Value, exec: *js.Execution) void {
logger.info(.js, "console.info", .{ValueWriter{ .values = values }});
appendMessage(exec, .info, values);
}
pub fn log(_: *const Console, values: []js.Value, page: *Page) void {
logger.info(.js, "console.log", .{ValueWriter{ .page = page, .values = values }});
page.appendConsoleMessage(.log, values);
pub fn log(_: *const Console, values: []js.Value, exec: *js.Execution) void {
logger.info(.js, "console.log", .{ValueWriter{ .values = values }});
appendMessage(exec, .log, values);
}
pub fn warn(_: *const Console, values: []js.Value, page: *Page) void {
logger.warn(.js, "console.warn", .{ValueWriter{ .page = page, .values = values }});
page.appendConsoleMessage(.warn, values);
pub fn warn(_: *const Console, values: []js.Value, exec: *js.Execution) void {
logger.warn(.js, "console.warn", .{ValueWriter{ .values = values }});
appendMessage(exec, .warn, values);
}
pub fn clear(_: *const Console) void {}
pub fn assert(_: *const Console, assertion: js.Value, values: []js.Value, page: *Page) void {
pub fn assert(_: *const Console, assertion: js.Value, values: []js.Value, exec: *js.Execution) void {
if (assertion.toBool()) {
return;
}
logger.warn(.js, "console.assert", .{ValueWriter{ .page = page, .values = values }});
page.appendConsoleMessage(.warn, values);
logger.warn(.js, "console.assert", .{ValueWriter{ .values = values }});
appendMessage(exec, .warn, values);
}
pub fn @"error"(_: *const Console, values: []js.Value, page: *Page) void {
logger.warn(.js, "console.error", .{ValueWriter{ .page = page, .values = values, .include_stack = true }});
page.appendConsoleMessage(.@"error", values);
pub fn @"error"(_: *const Console, values: []js.Value, exec: *js.Execution) void {
logger.warn(.js, "console.error", .{ValueWriter{ .values = values, .stack = exec.context.local.?.stackTrace() catch |err| @errorName(err) orelse "???" }});
appendMessage(exec, .@"error", values);
}
pub fn table(_: *const Console, data: js.Value, columns: ?js.Value) void {
logger.info(.js, "console.table", .{ .data = data, .columns = columns });
}
pub fn count(self: *Console, label_: ?[]const u8, page: *Page) !void {
pub fn count(self: *Console, label_: ?[]const u8, exec: *js.Execution) !void {
const label = label_ orelse "default";
const gop = try self._counts.getOrPut(page.arena, label);
const gop = try self._counts.getOrPut(exec.arena, label);
var current: u64 = 0;
if (gop.found_existing) {
current = gop.value_ptr.*;
} else {
gop.key_ptr.* = try page.dupeString(label);
gop.key_ptr.* = try exec.arena.dupe(u8, label);
}
const c = current + 1;
@@ -101,15 +101,15 @@ pub fn countReset(self: *Console, label_: ?[]const u8) !void {
logger.info(.js, "console.countReset", .{ .label = label, .count = kv.value });
}
pub fn time(self: *Console, label_: ?[]const u8, page: *Page) !void {
pub fn time(self: *Console, label_: ?[]const u8, exec: *js.Execution) !void {
const label = label_ orelse "default";
const gop = try self._timers.getOrPut(page.arena, label);
const gop = try self._timers.getOrPut(exec.arena, label);
if (gop.found_existing) {
logger.info(.js, "console.time", .{ .label = label, .err = "duplicate timer" });
return;
}
gop.key_ptr.* = try page.dupeString(label);
gop.key_ptr.* = try exec.arena.dupe(u8, label);
gop.value_ptr.* = timestamp();
}
@@ -134,12 +134,12 @@ pub fn timeEnd(self: *Console, label_: ?[]const u8) void {
logger.info(.js, "console.timeEnd", .{ .label = label, .elapsed = elapsed - kv.value });
}
pub fn group(_: *const Console, values: []js.Value, page: *Page) void {
logger.info(.js, "console.group", .{ValueWriter{ .page = page, .values = values }});
pub fn group(_: *const Console, values: []js.Value) void {
logger.info(.js, "console.group", .{ValueWriter{ .values = values }});
}
pub fn groupCollapsed(_: *const Console, values: []js.Value, page: *Page) void {
logger.info(.js, "console.groupCollapsed", .{ValueWriter{ .page = page, .values = values }});
pub fn groupCollapsed(_: *const Console, values: []js.Value) void {
logger.info(.js, "console.groupCollapsed", .{ValueWriter{ .values = values }});
}
pub fn groupEnd(_: *const Console) void {}
@@ -148,17 +148,26 @@ fn timestamp() u64 {
return @import("../../datetime.zig").timestamp(.monotonic);
}
// Forwards page-context console output to the Page's message buffer (read by
// the `consoleLogs` tool / CDP Runtime.consoleAPICalled). Worker contexts are
// dropped — no buffer is attached there.
fn appendMessage(exec: *js.Execution, level: Page.ConsoleMessage.Level, values: []js.Value) void {
switch (exec.context.global) {
.page => |p| p.appendConsoleMessage(level, values),
.worker => {},
}
}
const ValueWriter = struct {
page: *Page,
values: []js.Value,
include_stack: bool = false,
stack: ?[]const u8 = null,
pub fn format(self: ValueWriter, writer: *std.io.Writer) !void {
for (self.values, 1..) |value, i| {
try writer.print("\n arg({d}): {f}", .{ i, value });
}
if (self.include_stack) {
try writer.print("\n stack: {s}", .{self.page.js.local.?.stackTrace() catch |err| @errorName(err) orelse "???"});
if (self.stack) |s| {
try writer.print("\n stack: {s}", .{s});
}
}

View File

@@ -18,7 +18,6 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const DOMException = @This();
@@ -129,7 +128,7 @@ pub fn getMessage(self: *const DOMException) []const u8 {
};
}
pub fn toString(self: *const DOMException, page: *Page) ![]const u8 {
pub fn toString(self: *const DOMException, exec: *js.Execution) ![]const u8 {
const msg = blk: {
if (self._custom_message) |msg| {
break :blk msg;
@@ -139,7 +138,7 @@ pub fn toString(self: *const DOMException, page: *Page) ![]const u8 {
else => break :blk self.getMessage(),
}
};
return std.fmt.bufPrint(&page.buf, "{s}: {s}", .{ self.getName(), msg }) catch return msg;
return std.fmt.bufPrint(exec.buf, "{s}: {s}", .{ self.getName(), msg }) catch return msg;
}
const Code = enum(u8) {

View File

@@ -19,11 +19,13 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const EventManager = @import("../EventManager.zig");
const RegisterOptions = EventManager.RegisterOptions;
const Event = @import("Event.zig");
const WorkerGlobalScope = @import("WorkerGlobalScope.zig");
const RegisterOptions = EventManager.RegisterOptions;
const EventTarget = @This();
@@ -34,6 +36,8 @@ pub const Type = union(enum) {
generic: void,
node: *@import("Node.zig"),
window: *@import("Window.zig"),
worker: *@import("Worker.zig"),
worker_global_scope: *@import("WorkerGlobalScope.zig"),
xhr: *@import("net/XMLHttpRequestEventTarget.zig"),
abort_signal: *@import("AbortSignal.zig"),
media_query_list: *@import("css/MediaQueryList.zig"),
@@ -48,21 +52,26 @@ pub const Type = union(enum) {
websocket: *@import("net/WebSocket.zig"),
};
pub fn init(page: *Page) !*EventTarget {
return page._factory.create(EventTarget{
pub fn init(session: *Session) !*EventTarget {
return session.factory.create(EventTarget{
._type = .generic,
});
}
pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool {
pub fn dispatchEvent(self: *EventTarget, event: *Event, exec: *js.Execution) !bool {
if (event._event_phase != .none) {
return error.InvalidStateError;
}
event._is_trusted = false;
event.acquireRef();
defer _ = event.releaseRef(page._session);
try page._event_manager.dispatch(self, event);
switch (exec.context.global) {
.page => |page| {
event.acquireRef();
defer _ = event.releaseRef(page._session);
try page._event_manager.dispatch(self, event);
},
.worker => |wgs| try wgs.dispatch(self, event, null),
}
return !event._cancelable or !event._prevent_default;
}
@@ -75,12 +84,12 @@ pub const EventListenerCallback = union(enum) {
function: js.Function,
object: js.Object,
};
pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?AddEventListenerOptions, page: *Page) !void {
pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?AddEventListenerOptions, exec: *js.Execution) !void {
const callback = callback_ orelse return;
const em_callback = switch (callback) {
.object => |obj| EventManager.Callback{ .object = obj },
.function => |func| EventManager.Callback{ .function = func },
const em_callback: EventManager.Callback = switch (callback) {
.object => |obj| .{ .object = obj },
.function => |func| .{ .function = func },
};
const options = blk: {
@@ -90,7 +99,11 @@ pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventLi
.capture => |capture| RegisterOptions{ .capture = capture },
};
};
return page._event_manager.register(self, typ, em_callback, options);
switch (exec.context.global) {
.page => |page| _ = try page._event_manager.register(self, typ, em_callback, options),
.worker => |wgs| _ = try wgs._event_manager.register(self, typ, em_callback, options),
}
}
const RemoveEventListenerOptions = union(enum) {
@@ -101,7 +114,7 @@ const RemoveEventListenerOptions = union(enum) {
capture: bool = false,
};
};
pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?RemoveEventListenerOptions, page: *Page) !void {
pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?RemoveEventListenerOptions, exec: *js.Execution) !void {
const callback = callback_ orelse return;
// For object callbacks, check if handleEvent exists
@@ -111,9 +124,9 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?Even
}
}
const em_callback = switch (callback) {
.function => |func| EventManager.Callback{ .function = func },
.object => |obj| EventManager.Callback{ .object = obj },
const em_callback: EventManager.Callback = switch (callback) {
.function => |func| .{ .function = func },
.object => |obj| .{ .object = obj },
};
const use_capture = blk: {
@@ -123,7 +136,11 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?Even
.options => |opts| opts.capture,
};
};
return page._event_manager.remove(self, typ, em_callback, use_capture);
switch (exec.context.global) {
.page => |page| page._event_manager.remove(self, typ, em_callback, use_capture),
.worker => |wgs| wgs._event_manager.remove(self, typ, em_callback, use_capture),
}
}
pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
@@ -131,6 +148,8 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
.node => |n| n.format(writer),
.generic => writer.writeAll("<EventTarget>"),
.window => writer.writeAll("<Window>"),
.worker => writer.writeAll("<Worker>"),
.worker_global_scope => writer.writeAll("<WorkerGlobalScope>"),
.xhr => writer.writeAll("<XMLHttpRequestEventTarget>"),
.abort_signal => writer.writeAll("<AbortSignal>"),
.media_query_list => writer.writeAll("<MediaQueryList>"),
@@ -151,6 +170,8 @@ pub fn toString(self: *EventTarget) []const u8 {
.node => return "[object Node]",
.generic => return "[object EventTarget]",
.window => return "[object Window]",
.worker => return "[object Worker]",
.worker_global_scope => return "[object WorkerGlobalScope]",
.xhr => return "[object XMLHttpRequestEventTarget]",
.abort_signal => return "[object AbortSignal]",
.media_query_list => return "[object MediaQueryList]",

View File

@@ -20,7 +20,6 @@ const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const Blob = @import("Blob.zig");
@@ -30,10 +29,10 @@ const File = @This();
_proto: *Blob,
// TODO: Implement File API.
pub fn init(page: *Page) !*File {
const arena = try page.getArena(.tiny, "File");
errdefer page.releaseArena(arena);
return page._factory.blob(arena, File{ ._proto = undefined });
pub fn init(session: *Session) !*File {
const arena = try session.getArena(.tiny, "File");
errdefer session.releaseArena(arena);
return session.factory.blob(arena, File{ ._proto = undefined });
}
pub const JsApi = struct {

View File

@@ -62,7 +62,7 @@ pub fn pushState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8
_ = try page._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
page.url = url;
page.window._location._url = try URL.init(url, null, page);
page.window._location._url = try URL.init(url, null, &page.js.execution);
}
pub fn replaceState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {

View File

@@ -24,6 +24,7 @@ const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const h5e = @import("../parser/html5ever.zig");
const Execution = js.Execution;
const Allocator = std.mem.Allocator;
pub fn registerTypes() []const type {
@@ -34,7 +35,7 @@ pub fn registerTypes() []const type {
};
}
const Normalizer = *const fn ([]const u8, *Page) []const u8;
const Normalizer = *const fn ([]const u8, []u8) []const u8;
pub const Entry = struct {
name: String,
@@ -62,14 +63,14 @@ pub fn copy(arena: Allocator, original: KeyValueList) !KeyValueList {
return list;
}
pub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?Normalizer, page: *Page) !KeyValueList {
pub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?Normalizer, buf: []u8) !KeyValueList {
var it = try js_obj.nameIterator();
var list = KeyValueList.init();
try list.ensureTotalCapacity(arena, it.count);
while (try it.next()) |name| {
const js_value = try js_obj.get(name);
const normalized = if (comptime normalizer) |n| n(name, page) else name;
const normalized = if (comptime normalizer) |n| n(name, buf) else name;
list._entries.appendAssumeCapacity(.{
.name = try String.init(arena, normalized, .{}),
@@ -80,12 +81,12 @@ pub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?N
return list;
}
pub fn fromArray(arena: Allocator, kvs: []const [2][]const u8, comptime normalizer: ?Normalizer, page: *Page) !KeyValueList {
pub fn fromArray(arena: Allocator, kvs: []const [2][]const u8, comptime normalizer: ?Normalizer, buf: []u8) !KeyValueList {
var list = KeyValueList.init();
try list.ensureTotalCapacity(arena, kvs.len);
for (kvs) |pair| {
const normalized = if (comptime normalizer) |n| n(pair[0], page) else pair[0];
const normalized = if (comptime normalizer) |n| n(pair[0], buf) else pair[0];
list._entries.appendAssumeCapacity(.{
.name = try String.init(arena, normalized, .{}),
@@ -112,12 +113,11 @@ pub fn get(self: *const KeyValueList, name: []const u8) ?[]const u8 {
return null;
}
pub fn getAll(self: *const KeyValueList, name: []const u8, page: *Page) ![]const []const u8 {
const arena = page.call_arena;
pub fn getAll(self: *const KeyValueList, allocator: Allocator, name: []const u8) ![]const []const u8 {
var arr: std.ArrayList([]const u8) = .empty;
for (self._entries.items) |*entry| {
if (entry.name.eqlSlice(name)) {
try arr.append(arena, entry.value.str());
try arr.append(allocator, entry.value.str());
}
}
return arr.items;
@@ -321,7 +321,7 @@ pub const Iterator = struct {
pub const Entry = struct { []const u8, []const u8 };
pub fn next(self: *Iterator, _: *const Page) ?Iterator.Entry {
pub fn next(self: *Iterator, _: *const Execution) ?Iterator.Entry {
const index = self.index;
const entries = self.kv._entries.items;
if (index >= entries.len) {

View File

@@ -27,7 +27,7 @@ const Location = @This();
_url: *URL,
pub fn init(raw_url: [:0]const u8, page: *Page) !*Location {
const url = try URL.init(raw_url, null, page);
const url = try URL.init(raw_url, null, &page.js.execution);
return page._factory.create(Location{
._url = url,
});
@@ -53,12 +53,12 @@ pub fn getPort(self: *const Location) []const u8 {
return self._url.getPort();
}
pub fn getOrigin(self: *const Location, page: *const Page) ![]const u8 {
return self._url.getOrigin(page);
pub fn getOrigin(self: *const Location, exec: *const js.Execution) ![]const u8 {
return self._url.getOrigin(exec);
}
pub fn getSearch(self: *const Location, page: *const Page) ![]const u8 {
return self._url.getSearch(page);
pub fn getSearch(self: *const Location, exec: *const js.Execution) ![]const u8 {
return self._url.getSearch(exec);
}
pub fn getHash(self: *const Location) []const u8 {
@@ -98,8 +98,8 @@ pub fn reload(_: *const Location, page: *Page) !void {
return page.scheduleNavigation(page.url, .{ .reason = .script, .kind = .reload }, .{ .script = page });
}
pub fn toString(self: *const Location, page: *const Page) ![:0]const u8 {
return self._url.toString(page);
pub fn toString(self: *const Location, exec: *const js.Execution) ![:0]const u8 {
return self._url.toString(exec);
}
pub const JsApi = struct {

View File

@@ -128,7 +128,7 @@ const PostMessageCallback = struct {
.data = .{ .value = self.message },
.origin = "",
.source = null,
}, page) catch |err| {
}, page._session) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
return null;
}).asEvent();

View File

@@ -23,6 +23,7 @@ const U = @import("../URL.zig");
const Page = @import("../Page.zig");
const URLSearchParams = @import("net/URLSearchParams.zig");
const Blob = @import("Blob.zig");
const Execution = js.Execution;
const Allocator = std.mem.Allocator;
@@ -36,11 +37,12 @@ _search_params: ?*URLSearchParams = null,
pub const resolve = @import("../URL.zig").resolve;
pub const eqlDocument = @import("../URL.zig").eqlDocument;
pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL {
const arena = page.arena;
pub fn init(url: [:0]const u8, base_: ?[:0]const u8, exec: *const Execution) !*URL {
const arena = exec.arena;
const context_url = exec.url.*;
if (std.mem.eql(u8, url, "about:blank")) {
return page._factory.create(URL{
return exec._factory.create(URL{
._raw = "about:blank",
._arena = arena,
});
@@ -48,9 +50,9 @@ pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL {
const url_is_absolute = @import("../URL.zig").isCompleteHTTPUrl(url);
const base = if (base_) |b| blk: {
// If URL is absolute, base is ignored (but we still use page.url internally)
// If URL is absolute, base is ignored (but we still use context url internally)
if (url_is_absolute) {
break :blk page.url;
break :blk context_url;
}
// For relative URLs, base must be a valid absolute URL
if (!@import("../URL.zig").isCompleteHTTPUrl(b)) {
@@ -59,11 +61,11 @@ pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL {
break :blk b;
} else if (!url_is_absolute) {
return error.TypeError;
} else page.url;
} else context_url;
const raw = try resolve(arena, base, url, .{ .always_dupe = true });
return page._factory.create(URL{
return exec._factory.create(URL{
._raw = raw,
._arena = arena,
});
@@ -107,20 +109,20 @@ pub fn getPort(self: *const URL) []const u8 {
return U.getPort(self._raw);
}
pub fn getOrigin(self: *const URL, page: *const Page) ![]const u8 {
return (try U.getOrigin(page.call_arena, self._raw)) orelse {
pub fn getOrigin(self: *const URL, exec: *const Execution) ![]const u8 {
return (try U.getOrigin(exec.call_arena, self._raw)) orelse {
// yes, a null string, that's what the spec wants
return "null";
};
}
pub fn getSearch(self: *const URL, page: *const Page) ![]const u8 {
pub fn getSearch(self: *const URL, exec: *const Execution) ![]const u8 {
// If searchParams has been accessed, generate search from it
if (self._search_params) |sp| {
if (sp.getSize() == 0) {
return "";
}
var buf = std.Io.Writer.Allocating.init(page.call_arena);
var buf = std.Io.Writer.Allocating.init(exec.call_arena);
try buf.writer.writeByte('?');
try sp.toString(&buf.writer);
return buf.written();
@@ -132,30 +134,30 @@ pub fn getHash(self: *const URL) []const u8 {
return U.getHash(self._raw);
}
pub fn getSearchParams(self: *URL, page: *Page) !*URLSearchParams {
pub fn getSearchParams(self: *URL, exec: *const Execution) !*URLSearchParams {
if (self._search_params) |sp| {
return sp;
}
// Get current search string (without the '?')
const search = try self.getSearch(page);
const search = try self.getSearch(exec);
const search_value = if (search.len > 0) search[1..] else "";
const params = try URLSearchParams.init(.{ .query_string = search_value }, page);
const params = try URLSearchParams.init(.{ .query_string = search_value }, exec);
self._search_params = params;
return params;
}
pub fn setHref(self: *URL, value: []const u8, page: *Page) !void {
const base = if (U.isCompleteHTTPUrl(value)) page.url else self._raw;
const raw = try U.resolve(self._arena orelse page.arena, base, value, .{ .always_dupe = true });
pub fn setHref(self: *URL, value: []const u8, exec: *const Execution) !void {
const base = if (U.isCompleteHTTPUrl(value)) exec.url.* else self._raw;
const raw = try U.resolve(self._arena orelse exec.arena, base, value, .{ .always_dupe = true });
self._raw = raw;
// Update existing searchParams if it exists
if (self._search_params) |sp| {
const search = U.getSearch(raw);
const search_value = if (search.len > 0) search[1..] else "";
try sp.updateFromString(search_value, page);
try sp.updateFromString(search_value, exec);
}
}
@@ -184,7 +186,7 @@ pub fn setPathname(self: *URL, value: []const u8) !void {
self._raw = try U.setPathname(self._raw, value, allocator);
}
pub fn setSearch(self: *URL, value: []const u8, page: *Page) !void {
pub fn setSearch(self: *URL, value: []const u8, exec: *const Execution) !void {
const allocator = self._arena orelse return error.NoAllocator;
self._raw = try U.setSearch(self._raw, value, allocator);
@@ -192,7 +194,7 @@ pub fn setSearch(self: *URL, value: []const u8, page: *Page) !void {
if (self._search_params) |sp| {
const search = U.getSearch(self._raw);
const search_value = if (search.len > 0) search[1..] else "";
try sp.updateFromString(search_value, page);
try sp.updateFromString(search_value, exec);
}
}
@@ -201,7 +203,7 @@ pub fn setHash(self: *URL, value: []const u8) !void {
self._raw = try U.setHash(self._raw, value, allocator);
}
pub fn toString(self: *const URL, page: *const Page) ![:0]const u8 {
pub fn toString(self: *const URL, exec: *const Execution) ![:0]const u8 {
const sp = self._search_params orelse {
return self._raw;
};
@@ -217,7 +219,7 @@ pub fn toString(self: *const URL, page: *const Page) ![:0]const u8 {
const hash = self.getHash();
// Build the new URL string
var buf = std.Io.Writer.Allocating.init(page.call_arena);
var buf = std.Io.Writer.Allocating.init(exec.call_arena);
try buf.writer.writeAll(base);
// Add / if missing (e.g., "https://example.com" -> "https://example.com/")

View File

@@ -335,7 +335,7 @@ pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
.message = err.toStringSlice() catch "Unknown error",
.bubbles = false,
.cancelable = true,
}, page);
}, page._session);
// Invoke window.onerror callback if set (per WHATWG spec, this is called
// with 5 arguments: message, source, lineno, colno, error)
@@ -411,7 +411,7 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons
errdefer target_page.releaseArena(arena);
// Origin should be the source window's origin (where the message came from)
const origin = try source_window._location.getOrigin(page);
const origin = try source_window._location.getOrigin(&page.js.execution);
const callback = try arena.create(PostMessageCallback);
callback.* = .{
.arena = arena,
@@ -429,27 +429,11 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons
}
pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
const encoded_len = std.base64.standard.Encoder.calcSize(input.len);
const encoded = try page.call_arena.alloc(u8, encoded_len);
return std.base64.standard.Encoder.encode(encoded, input);
return @import("encoding/base64.zig").encode(page.call_arena, input);
}
pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace);
// Forgiving base64 decode per WHATWG spec:
// https://infra.spec.whatwg.org/#forgiving-base64-decode
// Remove trailing padding to use standard_no_pad decoder
const unpadded = std.mem.trimRight(u8, trimmed, "=");
// Length % 4 == 1 is invalid (can't represent valid base64)
if (unpadded.len % 4 == 1) {
return error.InvalidCharacterError;
}
const decoded_len = std.base64.standard_no_pad.Decoder.calcSizeForSlice(unpadded) catch return error.InvalidCharacterError;
const decoded = try page.call_arena.alloc(u8, decoded_len);
std.base64.standard_no_pad.Decoder.decode(decoded, unpadded) catch return error.InvalidCharacterError;
return decoded;
return @import("encoding/base64.zig").decode(page.call_arena, input);
}
pub fn structuredClone(_: *const Window, value: js.Value) !js.Value {
@@ -590,6 +574,7 @@ pub fn scrollBy(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.PromiseRejection, page: *Page) !void {
if (comptime IS_DEBUG) {
log.debug(.js, "unhandled rejection", .{
.target = "window",
.value = rejection.reason(),
.stack = rejection.local.stackTrace() catch |err| @errorName(err) orelse "???",
});
@@ -607,7 +592,7 @@ pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
.reason = if (rejection.reason()) |r| try r.temp() else null,
.promise = try rejection.promise().temp(),
}, page)).asEvent();
}, page._session)).asEvent();
try page._event_manager.dispatchDirect(target, event, attribute_callback, .{ .context = "window.unhandledrejection" });
}
}
@@ -796,7 +781,7 @@ const PostMessageCallback = struct {
.source = self.source,
.bubbles = false,
.cancelable = false,
}, page)).asEvent();
}, page._session)).asEvent();
try page._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" });
}

View File

@@ -0,0 +1,405 @@
// 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 std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const http = @import("../../network/http.zig");
const URL = @import("../URL.zig");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const HttpClient = @import("../HttpClient.zig");
const Blob = @import("Blob.zig");
const EventTarget = @import("EventTarget.zig");
const MessageEvent = @import("event/MessageEvent.zig");
const ErrorEvent = @import("event/ErrorEvent.zig");
const WorkerGlobalScope = @import("WorkerGlobalScope.zig");
const Execution = js.Execution;
const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Worker = @This();
// used by HttpClient when generating notification
// Ultimately used by CDP to generate request/loader ids.
id: u32,
_pseudo_frame_id: u32,
_proto: *EventTarget,
_page: *Page,
_arena: Allocator,
_worker_scope: *WorkerGlobalScope,
_url: [:0]const u8,
_script_loaded: bool = false,
_script_buffer: std.ArrayList(u8) = .empty,
_http_response: ?HttpClient.Response = null,
// Event handlers
_on_error: ?js.Function.Global = null,
_on_message: ?js.Function.Global = null,
_on_messageerror: ?js.Function.Global = null,
pub fn init(url: []const u8, exec: *Execution) !*Worker {
const page = switch (exec.context.global) {
.page => |p| p,
.worker => return error.WorkerCannotCreateWorker,
};
const session = page._session;
const arena = try session.getArena(.large, "Worker");
errdefer session.releaseArena(arena);
const resolved_url = try URL.resolve(arena, exec.url.*, url, .{});
const self = try session.factory.eventTargetWithAllocator(arena, Worker{
.id = session.nextPageId(),
._pseudo_frame_id = session.nextFrameId(),
._arena = arena,
._proto = undefined,
._page = page,
._url = resolved_url,
._worker_scope = undefined,
});
self._worker_scope = try WorkerGlobalScope.init(self, resolved_url);
errdefer self._worker_scope.deinit();
try page.trackWorker(self);
if (std.mem.startsWith(u8, url, "blob:")) {
errdefer page.removeWorker(self);
const blob: *Blob = page.lookupBlobUrl(url) orelse {
log.warn(.js, "invalid blob", .{ .target = "worker" });
return error.BlobNotFound;
};
try self.loadInitialScript(blob._slice);
return self;
}
const http_client = session.browser.http_client;
http_client.request(.{
.ctx = self,
.url = resolved_url,
.method = .GET,
.headers = try http_client.newHeaders(),
.page_id = self.id,
.frame_id = self._pseudo_frame_id,
.resource_type = .script,
.cookie_jar = &session.cookie_jar,
.cookie_origin = resolved_url,
.notification = session.notification,
.header_callback = httpHeaderCallback,
.data_callback = httpDataCallback,
.done_callback = httpDoneCallback,
.error_callback = httpErrorCallback,
}) catch |err| {
log.err(.browser, "Worker request", .{ .url = resolved_url, .err = err });
page.removeWorker(self);
return err;
};
return self;
}
// Called from Page.deinit when the page is destroyed, so we don't need to
// remove from the page's worker list.
pub fn deinit(self: *Worker) void {
if (self._http_response) |res| {
res.abort(error.Abort);
self._http_response = null;
}
self._worker_scope.deinit();
self._page._session.releaseArena(self._arena);
}
pub fn asEventTarget(self: *Worker) *EventTarget {
return self._proto;
}
fn httpHeaderCallback(response: HttpClient.Response) !bool {
const self: *Worker = @ptrCast(@alignCast(response.ctx));
const status = response.status() orelse return false;
if (status < 200 or status >= 300) {
log.warn(.browser, "Worker status", .{
.url = self._url,
.status = status,
});
return false;
}
self._http_response = response;
if (response.contentLength()) |cl| {
try self._script_buffer.ensureTotalCapacity(self._arena, cl);
}
return true;
}
fn httpDataCallback(response: HttpClient.Response, data: []const u8) !void {
const self: *Worker = @ptrCast(@alignCast(response.ctx));
try self._script_buffer.appendSlice(self._arena, data);
}
fn httpDoneCallback(ctx: *anyopaque) !void {
const self: *Worker = @ptrCast(@alignCast(ctx));
self._http_response = null;
self._script_loaded = true;
const url = self._url;
const script = self._script_buffer.items;
if (comptime IS_DEBUG) {
log.info(.browser, "worker fetch done", .{
.url = url,
.len = script.len,
});
}
try self.loadInitialScript(script);
}
fn loadInitialScript(self: *Worker, script: []const u8) !void {
var ls: js.Local.Scope = undefined;
self._worker_scope.js.localScope(&ls);
defer ls.deinit();
var try_catch: js.TryCatch = undefined;
try_catch.init(&ls.local);
defer try_catch.deinit();
_ = ls.local.eval(script, self._url) catch |err| {
const caught = try_catch.caughtOrError(self._arena, err);
log.err(.browser, "worker script error", .{ .url = self._url, .caught = caught });
self.fireErrorEvent(caught.exception orelse @errorName(err), null);
return;
};
ls.local.runMacrotasks();
}
fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *Worker = @ptrCast(@alignCast(ctx));
self._http_response = null;
log.err(.browser, "worker fetch error", .{
.url = self._worker_scope.url,
.err = err,
});
self.fireErrorEvent(@errorName(err), null);
}
// Fire an error event on the Worker object (parent context)
fn fireErrorEvent(self: *Worker, message: []const u8, error_value: ?js.Value.Temp) void {
self._fireErrorEvent(message, error_value) catch |err| {
log.warn(.browser, "worker fire error", .{ .err = err, .message = message });
};
}
fn _fireErrorEvent(self: *Worker, message: []const u8, error_value: ?js.Value.Temp) !void {
const page = self._page;
const session = page._session;
const target = self.asEventTarget();
const on_error = self._on_error;
// Check if there are any listeners
if (!page._event_manager.hasDirectListeners(target, "error", on_error)) {
if (error_value) |ev| ev.release();
return;
}
const error_event = try ErrorEvent.initTrusted(comptime .wrap("error"), .{
.@"error" = error_value,
.message = message,
.filename = self._url,
.bubbles = false,
.cancelable = true,
}, session);
try page._event_manager.dispatchDirect(target, error_event.asEvent(), on_error, .{
.context = "Worker.onerror",
});
}
pub fn terminate(self: *Worker) void {
// Abort any pending script fetch
if (self._http_response) |resp| {
resp.abort(error.Abort);
self._http_response = null;
}
self._page.removeWorker(self);
}
// Posts a message from the page to the worker.
pub fn postMessage(self: *Worker, data: js.Value) !void {
try self._worker_scope.receiveMessage(data);
}
// Called internally by WorkerGlobalScope when it wants to post a message to us
pub fn receiveMessage(self: *Worker, data: js.Value) !void {
const page = self._page;
const cloned_data = blk: {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
// clones from where it currently is (the Worker context) to our Page's context
const cloned = data.structuredCloneTo(&ls.local) catch |err| break :blk err;
break :blk cloned.temp();
};
const message_arena = try page.getArena(.tiny, "Worker.receiveMessage");
errdefer page.releaseArena(message_arena);
const callback = try message_arena.create(ReceiveMessageCallback);
callback.* = .{
.worker = self,
.data = cloned_data,
.arena = message_arena,
};
try page.js.scheduler.add(callback, ReceiveMessageCallback.run, 0, .{
.name = "Worker.receiveMessage",
.low_priority = false,
.finalizer = ReceiveMessageCallback.cancelled,
});
}
pub fn getOnMessage(self: *const Worker) ?js.Function.Global {
return self._on_message;
}
pub fn setOnMessage(self: *Worker, setter: ?FunctionSetter) void {
self._on_message = getFunctionFromSetter(setter);
}
pub fn getOnMessageError(self: *const Worker) ?js.Function.Global {
return self._on_messageerror;
}
pub fn setOnMessageError(self: *Worker, setter: ?FunctionSetter) void {
self._on_messageerror = getFunctionFromSetter(setter);
}
pub fn getOnError(self: *const Worker) ?js.Function.Global {
return self._on_error;
}
pub fn setOnError(self: *Worker, setter: ?FunctionSetter) void {
self._on_error = getFunctionFromSetter(setter);
}
const FunctionSetter = union(enum) {
func: js.Function.Global,
anything: js.Value,
};
fn getFunctionFromSetter(setter_: ?FunctionSetter) ?js.Function.Global {
const setter = setter_ orelse return null;
return switch (setter) {
.func => |func| func,
.anything => null,
};
}
const ReceiveMessageCallback = struct {
data: anyerror!js.Value.Temp,
arena: Allocator,
worker: *Worker,
fn cancelled(ctx: *anyopaque) void {
const self: *ReceiveMessageCallback = @ptrCast(@alignCast(ctx));
if (self.data) |d| {
d.release();
} else |_| {}
self.deinit();
}
fn deinit(self: *ReceiveMessageCallback) void {
self.worker._page._session.releaseArena(self.arena);
}
fn run(ctx: *anyopaque) !?u32 {
const self: *ReceiveMessageCallback = @ptrCast(@alignCast(ctx));
defer self.deinit();
const worker = self.worker;
const page = worker._page;
const target = worker.asEventTarget();
// If data is null, structured clone failed - fire messageerror
const data = self.data catch |err| {
const on_messageerror = worker._on_messageerror;
if (!page._event_manager.hasDirectListeners(target, "messageerror", on_messageerror)) {
return null;
}
const event = (try MessageEvent.initTrusted(comptime .wrap("messageerror"), .{
.data = .{ .string = @errorName(err) },
.bubbles = false,
.cancelable = false,
}, page._session)).asEvent();
try page._event_manager.dispatchDirect(target, event, on_messageerror, .{ .context = "Worker.messageerror" });
return null;
};
const on_message = worker._on_message;
// Check if there are any listeners before creating the event
if (!page._event_manager.hasDirectListeners(target, "message", on_message)) {
data.release();
return null;
}
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = .{ .value = data },
.bubbles = false,
.cancelable = false,
}, page._session)).asEvent();
try page._event_manager.dispatchDirect(target, event, on_message, .{ .context = "Worker.receiveMessage" });
return null;
}
};
pub const JsApi = struct {
pub const bridge = js.Bridge(Worker);
pub const Meta = struct {
pub const name = "Worker";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(Worker.init, .{});
pub const terminate = bridge.function(Worker.terminate, .{});
pub const postMessage = bridge.function(Worker.postMessage, .{});
pub const onmessage = bridge.accessor(Worker.getOnMessage, Worker.setOnMessage, .{});
pub const onmessageerror = bridge.accessor(Worker.getOnMessageError, Worker.setOnMessageError, .{});
pub const onerror = bridge.accessor(Worker.getOnError, Worker.setOnError, .{});
};
const testing = @import("../../testing.zig");
test "WebApi: Worker" {
try testing.htmlRunner("worker", .{});
}

View File

@@ -0,0 +1,427 @@
// 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/>.
// The struct is like a mix of Page and Window, but a very limited Page and
// a very limited Window. This dual-purpose does make it a bit harder to know
// what's what...e.g what is a WebAPI call and what it called internally.
const std = @import("std");
const log = @import("../../log.zig");
const JS = @import("../js/js.zig");
const Factory = @import("../Factory.zig");
const Session = @import("../Session.zig");
const EventManagerBase = @import("../EventManagerBase.zig");
const Worker = @import("Worker.zig");
const Crypto = @import("Crypto.zig");
const Console = @import("Console.zig");
const EventTarget = @import("EventTarget.zig");
const MessageEvent = @import("event/MessageEvent.zig");
const ErrorEvent = @import("event/ErrorEvent.zig");
const builtin = @import("builtin");
const IS_DEBUG = builtin.mode == .Debug;
const Allocator = std.mem.Allocator;
const WorkerGlobalScope = @This();
// Meant to follow the same field naming as Page so that an anytype of generic
// can access these the same for a Page of a WGS.
// These fields represent the "Page"-like component of the WGS
_session: *Session,
_factory: *Factory,
_identity: JS.Identity = .{},
arena: Allocator,
call_arena: Allocator,
url: [:0]const u8,
buf: [1024]u8 = undefined, // same size as page.buf
js: *JS.Context,
// Reference back to the Worker object (for postMessage to page)
_worker: *Worker,
// Event management for non-DOM targets in worker context
_event_manager: EventManagerBase,
// These fields represent the "Window"-like component of the WGS
_closed: bool = false,
_proto: *EventTarget,
_console: Console = .init,
_crypto: Crypto = .init,
_on_error: ?JS.Function.Global = null,
_on_rejection_handled: ?JS.Function.Global = null,
_on_unhandled_rejection: ?JS.Function.Global = null,
_on_message: ?JS.Function.Global = null,
_on_messageerror: ?JS.Function.Global = null,
pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope {
const arena = worker._arena;
const session = worker._page._session;
const factory = &session.factory;
const call_arena = try session.getArena(.small, "WorkerGlobalScope.call_arena");
errdefer session.releaseArena(call_arena);
const self = try factory.eventTargetWithAllocator(arena, WorkerGlobalScope{
.url = url,
.arena = arena,
.js = undefined,
.call_arena = call_arena,
._session = session,
._identity = .{},
._proto = undefined,
._factory = factory,
._worker = worker,
._event_manager = .init(arena),
});
errdefer factory.destroy(self);
self.js = try session.browser.env.createWorkerContext(self, .{
.call_arena = call_arena,
.identity_arena = arena,
.identity = &self._identity,
});
return self;
}
pub fn deinit(self: *WorkerGlobalScope) void {
self._identity.deinit();
const session = self._session;
session.browser.env.destroyContext(self.js);
session.releaseArena(self.call_arena);
}
pub fn base(self: *const WorkerGlobalScope) [:0]const u8 {
return self.url;
}
pub fn asEventTarget(self: *WorkerGlobalScope) *EventTarget {
return self._proto;
}
const Event = @import("Event.zig");
// Dispatch an event to listeners on the given target within this worker context.
pub fn dispatch(self: *WorkerGlobalScope, target: *EventTarget, event: *Event, handler: anytype) !void {
try self._event_manager.dispatchDirect(
self.call_arena,
self.js,
target,
event,
handler,
self._session,
.{},
);
}
pub fn getSelf(self: *WorkerGlobalScope) *WorkerGlobalScope {
return self;
}
pub fn getConsole(self: *WorkerGlobalScope) *Console {
return &self._console;
}
pub fn getCrypto(self: *WorkerGlobalScope) *Crypto {
return &self._crypto;
}
pub fn getOnError(self: *const WorkerGlobalScope) ?JS.Function.Global {
return self._on_error;
}
pub fn setOnError(self: *WorkerGlobalScope, setter: ?FunctionSetter) void {
self._on_error = getFunctionFromSetter(setter);
}
pub fn getOnRejectionHandled(self: *const WorkerGlobalScope) ?JS.Function.Global {
return self._on_rejection_handled;
}
pub fn setOnRejectionHandled(self: *WorkerGlobalScope, setter: ?FunctionSetter) void {
self._on_rejection_handled = getFunctionFromSetter(setter);
}
pub fn getOnUnhandledRejection(self: *const WorkerGlobalScope) ?JS.Function.Global {
return self._on_unhandled_rejection;
}
pub fn setOnUnhandledRejection(self: *WorkerGlobalScope, setter: ?FunctionSetter) void {
self._on_unhandled_rejection = getFunctionFromSetter(setter);
}
pub fn getOnMessage(self: *const WorkerGlobalScope) ?JS.Function.Global {
return self._on_message;
}
pub fn setOnMessage(self: *WorkerGlobalScope, setter: ?FunctionSetter) void {
self._on_message = getFunctionFromSetter(setter);
}
pub fn getOnMessageError(self: *const WorkerGlobalScope) ?JS.Function.Global {
return self._on_messageerror;
}
pub fn setOnMessageError(self: *WorkerGlobalScope, setter: ?FunctionSetter) void {
self._on_messageerror = getFunctionFromSetter(setter);
}
// Posts a message from the worker back to the page.
// The message is cloned via structured clone and dispatched on the Worker object.
pub fn postMessage(self: *WorkerGlobalScope, data: JS.Value) !void {
try self._worker.receiveMessage(data);
}
// Called internally by Worker when it wants to post a message to us
pub fn receiveMessage(self: *WorkerGlobalScope, data: JS.Value) !void {
if (self._closed) {
return;
}
const cloned_data: ?JS.Value.Temp = blk: {
// Enter our context to clone the message
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
// clones from where it currently is (the Worker's Page context) to our Context
const cloned = data.structuredCloneTo(&ls.local) catch break :blk null;
break :blk cloned.temp() catch break :blk null;
};
const session = self._session;
const message_arena = try session.getArena(.tiny, "WorkerGlobalScope.receiveMessage");
errdefer session.releaseArena(message_arena);
const callback = try message_arena.create(ReceiveMessageCallback);
callback.* = .{
.data = cloned_data,
.worker_scope = self,
.arena = message_arena,
};
try self.js.scheduler.add(callback, ReceiveMessageCallback.run, 0, .{
.name = "WorkerGlobalScope.receiveMessage",
.low_priority = false,
.finalizer = ReceiveMessageCallback.cancelled,
});
}
pub fn btoa(_: *const WorkerGlobalScope, input: []const u8, exec: *JS.Execution) ![]const u8 {
const base64 = @import("encoding/base64.zig");
return base64.encode(exec.call_arena, input);
}
pub fn atob(_: *const WorkerGlobalScope, input: []const u8, exec: *JS.Execution) ![]const u8 {
const base64 = @import("encoding/base64.zig");
return base64.decode(exec.call_arena, input);
}
pub fn structuredClone(_: *const WorkerGlobalScope, value: JS.Value) !JS.Value {
return value.structuredClone();
}
pub fn unhandledPromiseRejection(self: *WorkerGlobalScope, no_handler: bool, rejection: JS.PromiseRejection) !void {
if (comptime IS_DEBUG) {
log.debug(.js, "unhandled rejection", .{
.target = "worker",
.value = rejection.reason(),
.stack = rejection.local.stackTrace() catch |err| @errorName(err) orelse "???",
});
}
const event_name, const attribute_callback = blk: {
if (no_handler) {
break :blk .{ "unhandledrejection", self._on_unhandled_rejection };
}
break :blk .{ "rejectionhandled", self._on_rejection_handled };
};
const target = self.asEventTarget();
if (self._event_manager.hasDirectListeners(target, event_name, attribute_callback)) {
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
.reason = if (rejection.reason()) |r| try r.temp() else null,
.promise = try rejection.promise().temp(),
}, self._session)).asEvent();
try self.dispatch(target, event, attribute_callback);
}
}
pub fn close(self: *WorkerGlobalScope) void {
// TOOD: we should also stop new tasks from being scheduled
self.js.scheduler.reset();
self._closed = true;
}
pub fn reportError(self: *WorkerGlobalScope, err: JS.Value) !void {
const error_event = try ErrorEvent.initTrusted(comptime .wrap("error"), .{
.@"error" = try err.temp(),
.message = err.toStringSlice() catch "Unknown error",
.bubbles = false,
.cancelable = true,
}, self._session);
// Invoke onerror callback if set (per WHATWG spec, this is called
// with 5 arguments: message, source, lineno, colno, error)
// If it returns true, the event is cancelled.
var prevent_default = false;
if (self._on_error) |on_error| {
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
const local_func = ls.toLocal(on_error);
const result = local_func.call(JS.Value, .{
error_event._message,
error_event._filename,
error_event._line_number,
error_event._column_number,
err,
}) catch null;
// Per spec: returning true from onerror cancels the event
if (result) |r| {
prevent_default = r.isTrue();
}
}
const event = error_event.asEvent();
event._prevent_default = prevent_default;
// Pass null as handler: onerror was already called above with 5 args.
// We still dispatch so that addEventListener('error', ...) listeners fire.
try self.dispatch(self.asEventTarget(), event, null);
if (comptime builtin.is_test == false) {
if (!event._prevent_default) {
log.warn(.js, "worker.reportError", .{
.message = error_event._message,
.filename = error_event._filename,
.line_number = error_event._line_number,
.column_number = error_event._column_number,
});
}
}
}
// TODO: importScripts - needs script loading infrastructure
// TODO: location - needs WorkerLocation
// TODO: navigator - needs WorkerNavigator
// TODO: Timer functions - need scheduler integration
const FunctionSetter = union(enum) {
func: JS.Function.Global,
anything: JS.Value,
};
fn getFunctionFromSetter(setter_: ?FunctionSetter) ?JS.Function.Global {
const setter = setter_ orelse return null;
return switch (setter) {
.func => |func| func,
.anything => null,
};
}
const ReceiveMessageCallback = struct {
data: ?JS.Value.Temp,
arena: Allocator,
worker_scope: *WorkerGlobalScope,
fn cancelled(ctx: *anyopaque) void {
const self: *ReceiveMessageCallback = @ptrCast(@alignCast(ctx));
if (self.data) |d| d.release();
self.deinit();
}
fn deinit(self: *ReceiveMessageCallback) void {
self.worker_scope._session.releaseArena(self.arena);
}
fn run(ctx: *anyopaque) !?u32 {
const self: *ReceiveMessageCallback = @ptrCast(@alignCast(ctx));
defer self.deinit();
const worker_scope = self.worker_scope;
const target = worker_scope.asEventTarget();
// If data is null, structured clone failed - fire messageerror
if (self.data == null) {
const on_messageerror = worker_scope._on_messageerror;
if (!worker_scope._event_manager.hasDirectListeners(target, "messageerror", on_messageerror)) {
return null;
}
const event = (try MessageEvent.initTrusted(comptime .wrap("messageerror"), .{
.bubbles = false,
.cancelable = false,
}, worker_scope._session)).asEvent();
try worker_scope.dispatch(target, event, on_messageerror);
return null;
}
const on_message = worker_scope._on_message;
// Check if there are any listeners before creating the event
if (!worker_scope._event_manager.hasDirectListeners(target, "message", on_message)) {
self.data.?.release();
return null;
}
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = .{ .value = self.data.? },
.bubbles = false,
.cancelable = false,
}, worker_scope._session)).asEvent();
try worker_scope.dispatch(target, event, on_message);
return null;
}
};
pub const JsApi = struct {
pub const bridge = JS.Bridge(WorkerGlobalScope);
pub const Meta = struct {
pub const name = "WorkerGlobalScope";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const self = bridge.accessor(WorkerGlobalScope.getSelf, null, .{});
pub const console = bridge.accessor(WorkerGlobalScope.getConsole, null, .{});
pub const crypto = bridge.accessor(WorkerGlobalScope.getCrypto, null, .{});
pub const onerror = bridge.accessor(WorkerGlobalScope.getOnError, WorkerGlobalScope.setOnError, .{});
pub const onrejectionhandled = bridge.accessor(WorkerGlobalScope.getOnRejectionHandled, WorkerGlobalScope.setOnRejectionHandled, .{});
pub const onunhandledrejection = bridge.accessor(WorkerGlobalScope.getOnUnhandledRejection, WorkerGlobalScope.setOnUnhandledRejection, .{});
pub const btoa = bridge.function(WorkerGlobalScope.btoa, .{});
pub const atob = bridge.function(WorkerGlobalScope.atob, .{ .dom_exception = true });
pub const structuredClone = bridge.function(WorkerGlobalScope.structuredClone, .{});
pub const postMessage = bridge.function(WorkerGlobalScope.postMessage, .{});
pub const reportError = bridge.function(WorkerGlobalScope.reportError, .{});
pub const close = bridge.function(WorkerGlobalScope.close, .{});
pub const onmessage = bridge.accessor(WorkerGlobalScope.getOnMessage, WorkerGlobalScope.setOnMessage, .{});
pub const onmessageerror = bridge.accessor(WorkerGlobalScope.getOnMessageError, WorkerGlobalScope.setOnMessageError, .{});
// Return false since workers don't have secure-context-only APIs
pub const isSecureContext = bridge.property(false, .{ .template = false });
};

View File

@@ -18,6 +18,7 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const Node = @import("../Node.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
@@ -140,7 +141,7 @@ const Iterator = struct {
const Entry = struct { u32, *Node };
pub fn next(self: *Iterator, page: *Page) !?Entry {
pub fn next(self: *Iterator, page: *const Page) !?Entry {
const index = self.index;
const node = try self.list.getAtIndex(index, page) orelse return null;
self.index = index + 1;

View File

@@ -43,7 +43,7 @@ const Lookup = std.StringArrayHashMapUnmanaged(void);
const WHITESPACE = " \t\n\r\x0C";
pub fn length(self: *const DOMTokenList, page: *Page) !u32 {
const tokens = try self.getTokens(page);
const tokens = try self.getTokens(page.call_arena);
return @intCast(tokens.count());
}
@@ -82,8 +82,8 @@ pub fn add(self: *DOMTokenList, tokens: []const []const u8, page: *Page) !void {
try validateToken(token);
}
var lookup = try self.getTokens(page);
const allocator = page.call_arena;
var lookup = try self.getTokens(allocator);
try lookup.ensureUnusedCapacity(allocator, tokens.len);
for (tokens) |token| {
@@ -98,7 +98,7 @@ pub fn remove(self: *DOMTokenList, tokens: []const []const u8, page: *Page) !voi
try validateToken(token);
}
var lookup = try self.getTokens(page);
var lookup = try self.getTokens(page.call_arena);
for (tokens) |token| {
_ = lookup.orderedRemove(token);
}
@@ -149,7 +149,8 @@ pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8
return error.InvalidCharacterError;
}
var lookup = try self.getTokens(page);
const allocator = page.call_arena;
var lookup = try self.getTokens(page.call_arena);
// Check if old_token exists
if (!lookup.contains(old_token)) {
@@ -162,7 +163,6 @@ pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8
return true;
}
const allocator = page.call_arena;
// Build new token list preserving order but replacing old with new
var new_tokens = try std.ArrayList([]const u8).initCapacity(allocator, lookup.count());
var replaced_old = false;
@@ -237,14 +237,13 @@ pub fn forEach(self: *DOMTokenList, cb_: js.Function, js_this_: ?js.Object, page
}
}
fn getTokens(self: *const DOMTokenList, page: *Page) !Lookup {
fn getTokens(self: *const DOMTokenList, allocator: std.mem.Allocator) !Lookup {
const value = self.getValue();
if (value.len == 0) {
return .empty;
}
var list: Lookup = .empty;
const allocator = page.call_arena;
try list.ensureTotalCapacity(allocator, 4);
var it = std.mem.tokenizeAny(u8, value, WHITESPACE);

View File

@@ -24,6 +24,7 @@ const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
const Execution = js.Execution;
const HTMLAllCollection = @This();
@@ -133,11 +134,11 @@ pub fn callable(self: *HTMLAllCollection, arg: CAllAsFunctionArg, page: *Page) ?
};
}
pub fn iterator(self: *HTMLAllCollection, page: *Page) !*Iterator {
pub fn iterator(self: *HTMLAllCollection, exec: *const Execution) !*Iterator {
return Iterator.init(.{
.list = self,
.tw = self._tw.clone(),
}, page);
}, exec);
}
const GenericIterator = @import("iterator.zig").Entry;
@@ -145,7 +146,7 @@ pub const Iterator = GenericIterator(struct {
list: *HTMLAllCollection,
tw: TreeWalker.FullExcludeSelf,
pub fn next(self: *@This(), _: *Page) ?*Element {
pub fn next(self: *@This(), _: *const Execution) ?*Element {
while (self.tw.next()) |node| {
if (node.is(Element)) |el| {
return el;

View File

@@ -23,6 +23,7 @@ const Page = @import("../../Page.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
const NodeLive = @import("node_live.zig").NodeLive;
const Execution = js.Execution;
const Mode = enum {
tag,
@@ -77,7 +78,7 @@ pub fn getByName(self: *HTMLCollection, name: []const u8, page: *Page) ?*Element
};
}
pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
pub fn iterator(self: *HTMLCollection, exec: *const Execution) !*Iterator {
return Iterator.init(.{
.list = self,
.tw = switch (self._data) {
@@ -94,7 +95,7 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
.form => |*impl| .{ .form = impl._tw.clone() },
.empty => .empty,
},
}, page);
}, exec);
}
const GenericIterator = @import("iterator.zig").Entry;
@@ -115,7 +116,7 @@ pub const Iterator = GenericIterator(struct {
empty: void,
},
pub fn next(self: *@This(), _: *Page) ?*Element {
pub fn next(self: *@This(), _: *const Execution) ?*Element {
return switch (self.list._data) {
.tag => |*impl| impl.nextTw(&self.tw.tag),
.tag_name => |*impl| impl.nextTw(&self.tw.tag_name),

View File

@@ -21,6 +21,7 @@ const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const Execution = js.Execution;
pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
const R = reflect(Inner, field);
@@ -38,8 +39,8 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
pub const js_as_object = true;
};
pub fn init(inner: Inner, page: *Page) !*Self {
const self = try page._factory.create(Self{ ._inner = inner });
pub fn init(inner: Inner, executor: R.Executor) !*Self {
const self = try executor._factory.create(Self{ ._inner = inner });
if (@hasDecl(Inner, "acquireRef")) {
self._inner.acquireRef();
@@ -62,8 +63,8 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
self._rc.acquire();
}
pub fn next(self: *Self, page: *Page) if (R.has_error_return) anyerror!Result else Result {
const entry = (if (comptime R.has_error_return) try self._inner.next(page) else self._inner.next(page)) orelse {
pub fn next(self: *Self, executor: R.Executor) if (R.has_error_return) anyerror!Result else Result {
const entry = (if (comptime R.has_error_return) try self._inner.next(executor) else self._inner.next(executor)) orelse {
return .{ .done = true, .value = null };
};
@@ -92,17 +93,22 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
}
fn reflect(comptime Inner: type, comptime field: ?[]const u8) Reflect {
const R = @typeInfo(@TypeOf(Inner.next)).@"fn".return_type.?;
const fn_info = @typeInfo(@TypeOf(Inner.next)).@"fn";
const R = fn_info.return_type.?;
const has_error_return = @typeInfo(R) == .error_union;
// The executor type is the last parameter of inner.next (after self)
const Executor = fn_info.params[1].type.?;
return .{
.has_error_return = has_error_return,
.ValueType = ValueType(unwrapOptional(unwrapError(R)), field),
.Executor = Executor,
};
}
const Reflect = struct {
has_error_return: bool,
ValueType: type,
Executor: type,
};
fn unwrapError(comptime T: type) type {

View File

@@ -21,7 +21,6 @@ const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const html5ever = @import("../../parser/html5ever.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const Allocator = std.mem.Allocator;
@@ -42,7 +41,7 @@ const InitOpts = struct {
ignoreBOM: bool = false,
};
pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder {
pub fn init(label_: ?[]const u8, opts_: ?InitOpts, session: *Session) !*TextDecoder {
const label = label_ orelse "utf-8";
const info = html5ever.encoding_for_label(label.ptr, label.len);
@@ -56,8 +55,8 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder {
return error.RangeError;
}
const arena = try page.getArena(.large, "TextDecoder");
errdefer page.releaseArena(arena);
const arena = try session.getArena(.large, "TextDecoder");
errdefer session.releaseArena(arena);
const opts = opts_ orelse InitOpts{};
const self = try arena.create(TextDecoder);

View File

@@ -0,0 +1,50 @@
// 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/>.
//! Base64 encoding/decoding helpers for btoa/atob.
//! Used by both Window and WorkerGlobalScope.
const std = @import("std");
const Allocator = std.mem.Allocator;
/// Encodes input to base64 (btoa).
pub fn encode(alloc: Allocator, input: []const u8) ![]const u8 {
const encoded_len = std.base64.standard.Encoder.calcSize(input.len);
const encoded = try alloc.alloc(u8, encoded_len);
return std.base64.standard.Encoder.encode(encoded, input);
}
/// Decodes base64 input (atob).
/// Implements forgiving base64 decode per WHATWG spec.
pub fn decode(alloc: Allocator, input: []const u8) ![]const u8 {
const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace);
// Forgiving base64 decode per WHATWG spec:
// https://infra.spec.whatwg.org/#forgiving-base64-decode
// Remove trailing padding to use standard_no_pad decoder
const unpadded = std.mem.trimRight(u8, trimmed, "=");
// Length % 4 == 1 is invalid (can't represent valid base64)
if (unpadded.len % 4 == 1) {
return error.InvalidCharacterError;
}
const decoded_len = std.base64.standard_no_pad.Decoder.calcSizeForSlice(unpadded) catch return error.InvalidCharacterError;
const decoded = try alloc.alloc(u8, decoded_len);
std.base64.standard_no_pad.Decoder.decode(decoded, unpadded) catch return error.InvalidCharacterError;
return decoded;
}

View File

@@ -20,7 +20,6 @@ const std = @import("std");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const Event = @import("../Event.zig");
@@ -46,23 +45,23 @@ pub const ErrorEventOptions = struct {
const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent {
const arena = try page.getArena(.small, "ErrorEvent");
errdefer page.releaseArena(arena);
pub fn init(typ: []const u8, opts_: ?Options, session: *Session) !*ErrorEvent {
const arena = try session.getArena(.small, "ErrorEvent");
errdefer session.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, opts_, false, page);
return initWithTrusted(arena, type_string, opts_, false, session);
}
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*ErrorEvent {
const arena = try page.getArena(.small, "ErrorEvent.trusted");
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, opts_, true, page);
pub fn initTrusted(typ: String, opts_: ?Options, session: *Session) !*ErrorEvent {
const arena = try session.getArena(.small, "ErrorEvent.trusted");
errdefer session.releaseArena(arena);
return initWithTrusted(arena, typ, opts_, true, session);
}
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*ErrorEvent {
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, session: *Session) !*ErrorEvent {
const opts = opts_ orelse Options{};
const event = try page._factory.event(
const event = try session.factory.event(
arena,
typ,
ErrorEvent{

View File

@@ -20,11 +20,13 @@ const std = @import("std");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Factory = @import("../../Factory.zig");
const Session = @import("../../Session.zig");
const Event = @import("../Event.zig");
const Window = @import("../Window.zig");
const Allocator = std.mem.Allocator;
const MessageEvent = @This();
@@ -53,19 +55,19 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent {
const arena = try page.getArena(.small, "MessageEvent");
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, opts_, false, page);
return initWithTrusted(arena, type_string, opts_, false, page._factory);
}
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*MessageEvent {
const arena = try page.getArena(.small, "MessageEvent.trusted");
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, opts_, true, page);
pub fn initTrusted(typ: String, opts_: ?Options, session: *Session) !*MessageEvent {
const arena = try session.getArena(.small, "MessageEvent.trusted");
errdefer session.releaseArena(arena);
return initWithTrusted(arena, typ, opts_, true, &session.factory);
}
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*MessageEvent {
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, factory: *Factory) !*MessageEvent {
const opts = opts_ orelse Options{};
const event = try page._factory.event(
const event = try factory.event(
arena,
typ,
MessageEvent{

View File

@@ -19,8 +19,8 @@ const std = @import("std");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const Event = @import("../Event.zig");
const PromiseRejectionEvent = @This();
@@ -36,13 +36,13 @@ const PromiseRejectionEventOptions = struct {
const Options = Event.inheritOptions(PromiseRejectionEvent, PromiseRejectionEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*PromiseRejectionEvent {
const arena = try page.getArena(.tiny, "PromiseRejectionEvent");
errdefer page.releaseArena(arena);
pub fn init(typ: []const u8, opts_: ?Options, session: *Session) !*PromiseRejectionEvent {
const arena = try session.getArena(.tiny, "PromiseRejectionEvent");
errdefer session.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = opts_ orelse Options{};
const event = try page._factory.event(
const event = try session.factory.event(
arena,
type_string,
PromiseRejectionEvent{

View File

@@ -57,7 +57,7 @@ pub fn get(self: *const FormData, name: []const u8) ?[]const u8 {
}
pub fn getAll(self: *const FormData, name: []const u8, page: *Page) ![]const []const u8 {
return self._list.getAll(name, page);
return self._list.getAll(page.call_arena, name);
}
pub fn has(self: *const FormData, name: []const u8) bool {
@@ -76,16 +76,16 @@ pub fn delete(self: *FormData, name: []const u8) void {
self._list.delete(name, null);
}
pub fn keys(self: *FormData, page: *Page) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, page);
pub fn keys(self: *FormData, exec: *const js.Execution) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, exec);
}
pub fn values(self: *FormData, page: *Page) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, page);
pub fn values(self: *FormData, exec: *const js.Execution) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, exec);
}
pub fn entries(self: *FormData, page: *Page) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, page);
pub fn entries(self: *FormData, exec: *const js.Execution) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, exec);
}
pub fn forEach(self: *FormData, cb_: js.Function, js_this_: ?js.Object) !void {

View File

@@ -20,8 +20,8 @@ pub const InitOpts = union(enum) {
pub fn init(opts_: ?InitOpts, page: *Page) !*Headers {
const list = if (opts_) |opts| switch (opts) {
.obj => |obj| try KeyValueList.copy(page.arena, obj._list),
.js_obj => |js_obj| try KeyValueList.fromJsObject(page.arena, js_obj, normalizeHeaderName, page),
.strings => |kvs| try KeyValueList.fromArray(page.arena, kvs, normalizeHeaderName, page),
.js_obj => |js_obj| try KeyValueList.fromJsObject(page.arena, js_obj, normalizeHeaderName, &page.buf),
.strings => |kvs| try KeyValueList.fromArray(page.arena, kvs, normalizeHeaderName, &page.buf),
} else KeyValueList.init();
return page._factory.create(Headers{
@@ -30,18 +30,18 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*Headers {
}
pub fn append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const normalized_name = normalizeHeaderName(name, page);
const normalized_name = normalizeHeaderName(name, &page.buf);
try self._list.append(page.arena, normalized_name, value);
}
pub fn delete(self: *Headers, name: []const u8, page: *Page) void {
const normalized_name = normalizeHeaderName(name, page);
const normalized_name = normalizeHeaderName(name, &page.buf);
self._list.delete(normalized_name, null);
}
pub fn get(self: *const Headers, name: []const u8, page: *Page) !?[]const u8 {
const normalized_name = normalizeHeaderName(name, page);
const all_values = try self._list.getAll(normalized_name, page);
const normalized_name = normalizeHeaderName(name, &page.buf);
const all_values = try self._list.getAll(page.call_arena, normalized_name);
if (all_values.len == 0) {
return null;
@@ -53,25 +53,25 @@ pub fn get(self: *const Headers, name: []const u8, page: *Page) !?[]const u8 {
}
pub fn has(self: *const Headers, name: []const u8, page: *Page) bool {
const normalized_name = normalizeHeaderName(name, page);
const normalized_name = normalizeHeaderName(name, &page.buf);
return self._list.has(normalized_name);
}
pub fn set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const normalized_name = normalizeHeaderName(name, page);
const normalized_name = normalizeHeaderName(name, &page.buf);
try self._list.set(page.arena, normalized_name, value);
}
pub fn keys(self: *Headers, page: *Page) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, page);
pub fn keys(self: *Headers, exec: *const js.Execution) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, exec);
}
pub fn values(self: *Headers, page: *Page) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, page);
pub fn values(self: *Headers, exec: *const js.Execution) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, exec);
}
pub fn entries(self: *Headers, page: *Page) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, page);
pub fn entries(self: *Headers, exec: *const js.Execution) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, exec);
}
pub fn forEach(self: *Headers, cb_: js.Function, js_this_: ?js.Object) !void {
@@ -94,11 +94,11 @@ pub fn populateHttpHeader(self: *Headers, allocator: Allocator, http_headers: *h
}
}
fn normalizeHeaderName(name: []const u8, page: *Page) []const u8 {
if (name.len > page.buf.len) {
fn normalizeHeaderName(name: []const u8, buf: []u8) []const u8 {
if (name.len > buf.len) {
return name;
}
return std.ascii.lowerString(&page.buf, name);
return std.ascii.lowerString(buf, name);
}
pub const JsApi = struct {

View File

@@ -23,9 +23,9 @@ const log = @import("../../../log.zig");
const String = @import("../../../string.zig").String;
const Allocator = std.mem.Allocator;
const Page = @import("../../Page.zig");
const FormData = @import("FormData.zig");
const KeyValueList = @import("../KeyValueList.zig");
const Execution = js.Execution;
const URLSearchParams = @This();
@@ -38,12 +38,12 @@ const InitOpts = union(enum) {
query_string: []const u8,
};
pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
const arena = page.arena;
pub fn init(opts_: ?InitOpts, exec: *const Execution) !*URLSearchParams {
const arena = exec.arena;
const params: KeyValueList = blk: {
const opts = opts_ orelse break :blk .empty;
switch (opts) {
.query_string => |qs| break :blk try paramsFromString(arena, qs, &page.buf),
.query_string => |qs| break :blk try paramsFromString(arena, qs, exec.buf),
.form_data => |fd| break :blk try KeyValueList.copy(arena, fd._list),
.value => |js_val| {
// Order matters here; Array is also an Object.
@@ -51,24 +51,25 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
break :blk try paramsFromArray(arena, js_val.toArray());
}
if (js_val.isObject()) {
break :blk try KeyValueList.fromJsObject(arena, js_val.toObject(), null, page);
// normalizer is null, so page won't be used
break :blk try KeyValueList.fromJsObject(arena, js_val.toObject(), null, exec.buf);
}
if (js_val.isString()) |js_str| {
break :blk try paramsFromString(arena, try js_str.toSliceWithAlloc(arena), &page.buf);
break :blk try paramsFromString(arena, try js_str.toSliceWithAlloc(arena), exec.buf);
}
return error.InvalidArgument;
},
}
};
return page._factory.create(URLSearchParams{
return exec._factory.create(URLSearchParams{
._arena = arena,
._params = params,
});
}
pub fn updateFromString(self: *URLSearchParams, query_string: []const u8, page: *Page) !void {
self._params = try paramsFromString(self._arena, query_string, &page.buf);
pub fn updateFromString(self: *URLSearchParams, query_string: []const u8, exec: *const Execution) !void {
self._params = try paramsFromString(self._arena, query_string, exec.buf);
}
pub fn getSize(self: *const URLSearchParams) usize {
@@ -79,8 +80,8 @@ pub fn get(self: *const URLSearchParams, name: []const u8) ?[]const u8 {
return self._params.get(name);
}
pub fn getAll(self: *const URLSearchParams, name: []const u8, page: *Page) ![]const []const u8 {
return self._params.getAll(name, page);
pub fn getAll(self: *const URLSearchParams, name: []const u8, exec: *const Execution) ![]const []const u8 {
return self._params.getAll(exec.call_arena, name);
}
pub fn has(self: *const URLSearchParams, name: []const u8) bool {
@@ -99,16 +100,16 @@ pub fn delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) void
self._params.delete(name, value);
}
pub fn keys(self: *URLSearchParams, page: *Page) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._params }, page);
pub fn keys(self: *URLSearchParams, exec: *const Execution) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._params }, exec);
}
pub fn values(self: *URLSearchParams, page: *Page) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._params }, page);
pub fn values(self: *URLSearchParams, exec: *const Execution) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._params }, exec);
}
pub fn entries(self: *URLSearchParams, page: *Page) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._params }, page);
pub fn entries(self: *URLSearchParams, exec: *const Execution) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._params }, exec);
}
pub fn toString(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
@@ -287,7 +288,7 @@ pub const Iterator = struct {
const Entry = struct { []const u8, []const u8 };
pub fn next(self: *Iterator, _: *Page) !?Iterator.Entry {
pub fn next(self: *Iterator, _: *const Execution) !?Iterator.Entry {
const index = self.index;
const items = self.list._params.items;
if (index >= items.len) {
@@ -325,8 +326,8 @@ pub const JsApi = struct {
pub const sort = bridge.function(URLSearchParams.sort, .{});
pub const toString = bridge.function(_toString, .{});
fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
fn _toString(self: *const URLSearchParams, exec: *const Execution) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(exec.call_arena);
try self.toString(&buf.writer);
return buf.written();
}

View File

@@ -475,7 +475,7 @@ fn dispatchMessageEvent(self: *WebSocket, data: []const u8, frame_type: http.WsF
const event = try MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = msg_data,
.origin = "",
}, page);
}, page._session);
try page._event_manager.dispatchDirect(target, event.asEvent(), self._on_message, .{ .context = "WebSocket message" });
}
}

View File

@@ -206,7 +206,7 @@ fn getCookies(cmd: *CDP.Command) !void {
fn getResponseBody(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
requestId: []const u8, // "REQ-{d}"
requestId: []const u8, // "REQ-{d}" or "LID-{d}"
})) orelse return error.InvalidParams;
const request_id = try idFromRequestId(params.requestId);
@@ -439,7 +439,8 @@ const TransferAsResponseWriter = struct {
};
fn idFromRequestId(request_id: []const u8) !u64 {
if (!std.mem.startsWith(u8, request_id, "REQ-")) {
// The requesIid for the original document is its loaderId.
if (!std.mem.startsWith(u8, request_id, "REQ-") and !std.mem.startsWith(u8, request_id, "LID-")) {
return error.InvalidParams;
}
return std.fmt.parseInt(u64, request_id[4..], 10) catch return error.InvalidParams;

View File

@@ -55,25 +55,33 @@ pub const CacheControl = struct {
var max_age_set = false;
var max_s_age_set = false;
var is_public = false;
var iter = std.mem.splitScalar(u8, value, ',');
while (iter.next()) |part| {
const directive = std.mem.trim(u8, part, &std.ascii.whitespace);
if (std.ascii.eqlIgnoreCase(directive, "no-store")) {
const stripped = std.mem.trim(u8, part, &std.ascii.whitespace);
var buf: [16]u8 = undefined;
const len = @min(buf.len, stripped.len);
const directive = std.ascii.lowerString(buf[0..len], stripped[0..len]);
if (std.mem.eql(u8, directive, "no-store")) {
return null;
} else if (std.ascii.eqlIgnoreCase(directive, "no-cache")) {
}
if (std.mem.eql(u8, directive, "no-cache")) {
return null;
} else if (std.ascii.eqlIgnoreCase(directive, "public")) {
is_public = true;
} else if (std.ascii.startsWithIgnoreCase(directive, "max-age=")) {
}
if (std.mem.eql(u8, directive, "private")) {
return null;
}
if (std.mem.startsWith(u8, directive, "max-age=")) {
if (!max_s_age_set) {
if (std.fmt.parseInt(u64, directive[8..], 10) catch null) |max_age| {
cc.max_age = max_age;
max_age_set = true;
}
}
} else if (std.ascii.startsWithIgnoreCase(directive, "s-maxage=")) {
} else if (std.mem.startsWith(u8, directive, "s-maxage=")) {
if (std.fmt.parseInt(u64, directive[9..], 10) catch null) |max_age| {
cc.max_age = max_age;
max_age_set = true;
@@ -83,7 +91,6 @@ pub const CacheControl = struct {
}
if (!max_age_set) return null;
if (!is_public) return null;
if (cc.max_age == 0) return null;
return cc;
@@ -211,3 +218,31 @@ pub fn tryCache(
.vary_headers = &.{},
};
}
const testing = @import("../../testing.zig");
test "Cache: CacheControl.parse" {
try testing.expectEqual(300, CacheControl.parse("max-age=300").?.max_age);
try testing.expectEqual(300, CacheControl.parse("Max-Age=300").?.max_age);
try testing.expectEqual(300, CacheControl.parse("MAX-AGE=300").?.max_age);
try testing.expectEqual(300, CacheControl.parse("public, max-age=300").?.max_age);
try testing.expectEqual(300, CacheControl.parse(" max-age=300 ").?.max_age);
try testing.expectEqual(600, CacheControl.parse("max-age=300, s-maxage=600").?.max_age);
try testing.expectEqual(600, CacheControl.parse("s-maxage=600, max-age=300").?.max_age);
try testing.expectEqual(null, CacheControl.parse("no-store"));
try testing.expectEqual(null, CacheControl.parse("no-cache"));
try testing.expectEqual(null, CacheControl.parse("private"));
try testing.expectEqual(null, CacheControl.parse("max-age=300, no-store"));
try testing.expectEqual(null, CacheControl.parse("no-cache, max-age=300"));
try testing.expectEqual(null, CacheControl.parse("Private, max-age=300"));
try testing.expectEqual(null, CacheControl.parse("max-age=0"));
try testing.expectEqual(null, CacheControl.parse("public"));
try testing.expectEqual(null, CacheControl.parse(""));
try testing.expectEqual(null, CacheControl.parse("max-age=abc"));
try testing.expectEqual(null, CacheControl.parse("max-age="));
}