Compare commits

...

4 Commits

Author SHA1 Message Date
Karl Seguin
3864aa4a6b Create Worker context in snapshot
Turns out you can embed multiple contexts within a snapshot. So our snapshot
now contains the Page context (as before) but also the Worker context. This
gives us the performance benefit of snapshots and makes context creation for
pages and workers much more similar.
2026-04-03 20:10:49 +08:00
Karl Seguin
4dd014de41 Make context work with Page of WGS
A context can be created for either a Page or a Worker. This removes the
Context.page field and replaces it with a Context.global union.
2026-04-03 15:56:19 +08:00
Karl Seguin
224a7ca0fe Tweak snapshot for workers
We'll have two types of contexts: one for pages and one for workers. They'll
[probably] both be js.Context, but they'll have distinct FunctionTemplates
attached to their global. The Worker template will only contain a small subset
of the main Page's types, along with 1 or 2 of its own specific ones.

The Snapshot now creates the templates for both, so that the Env contains the
function templates for both contexts. Furthermore, having a "merged" view like
this ensures that the env.template[N] indices are consistent between the two.

However, the snapshot only attaches the Page-specific types to the snapshot
context. This allows the Page-context to be created as-is (e.g. efficiently).
The worker context will be created lazily, on demand, but from the templates
loaded into the env (since, again, the env contains templates for both).
2026-04-03 14:50:45 +08:00
Karl Seguin
226d1ff183 Introduce Execution
A Worker has no page. So any API that is accessible to a worker cannot take
a *Page parameter. Such APIs will now take a js.Execution which the context
will own and create from the Page (or from the WorkerGlobalScope when that's
created).

To test this, in addition to introducing the Execution, this change also updates
URLSearchParams which is accessible to Worker (and the Page obviously). This
change is obviously viral..if URLSearchParams no longer has a *Page but instead
has an *Execution, then any function it calls must also be updated.

So some APIs will take a *Page (those only accessible from a Page) and some will
take an *Execution (those accessible from a Page or Worker). I'm ok with that.

A lot of private/internal functions take a *Page, because it's simple, but all
they want is a call_arena or something. We'll try to update those as much as
possible. The Page/Execution being injected from the bridge is convenient, but
we should be more specific for internal calls and pass only what's needed.
2026-04-03 09:36:40 +08:00
24 changed files with 892 additions and 423 deletions

View File

@@ -5,8 +5,8 @@
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.15.2",
.dependencies = .{ .dependencies = .{
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.7.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/715ccbae21d7528eba951f78af4dfd48835fc172.tar.gz",
.hash = "v8-0.0.0-xddH67uBBAD95hWsPQz3Ni1PlZjdywtPXrGUAp8rSKco", .hash = "v8-0.0.0-xddH65-HBADXFCII9ucZE3NgbkWmwsbTbsx8qevYVki5",
}, },
// .v8 = .{ .path = "../zig-v8-fork" }, // .v8 = .{ .path = "../zig-v8-fork" },
.brotli = .{ .brotli = .{

View File

@@ -21,6 +21,7 @@ const log = @import("../../log.zig");
const string = @import("../../string.zig"); const string = @import("../../string.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
const js = @import("js.zig"); const js = @import("js.zig");
const Local = @import("Local.zig"); const Local = @import("Local.zig");
@@ -54,9 +55,9 @@ fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context)
.isolate = ctx.isolate, .isolate = ctx.isolate,
}, },
.prev_local = ctx.local, .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; ctx.local = &self.local;
} }
@@ -87,7 +88,7 @@ pub fn deinit(self: *Caller) void {
ctx.call_depth = call_depth; ctx.call_depth = call_depth;
ctx.local = self.prev_local; ctx.local = self.prev_local;
ctx.page.js = self.prev_context; ctx.global.setJs(self.prev_context);
} }
pub const CallOpts = struct { pub const CallOpts = struct {
@@ -169,7 +170,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, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = idx; @field(args, "1") = idx;
if (@typeInfo(F).@"fn".params.len == 3) { 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); const ret = @call(.auto, func, args);
return handleIndexedReturn(T, F, true, local, ret, info, opts); return handleIndexedReturn(T, F, true, local, ret, info, opts);
@@ -196,7 +197,7 @@ fn _getNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *c
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis()); @field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name); @field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
if (@typeInfo(F).@"fn".params.len == 3) { 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); const ret = @call(.auto, func, args);
return handleIndexedReturn(T, F, true, local, ret, info, opts); return handleIndexedReturn(T, F, true, local, ret, info, opts);
@@ -224,7 +225,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, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
@field(args, "2") = try local.jsValueToZig(@TypeOf(@field(args, "2")), js_value); @field(args, "2") = try local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
if (@typeInfo(F).@"fn".params.len == 4) { 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); const ret = @call(.auto, func, args);
return handleIndexedReturn(T, F, false, local, ret, info, opts); return handleIndexedReturn(T, F, false, local, ret, info, opts);
@@ -250,7 +251,7 @@ fn _deleteNamedIndex(comptime T: type, local: *const Local, func: anytype, name:
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis()); @field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name); @field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
if (@typeInfo(F).@"fn".params.len == 3) { 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); const ret = @call(.auto, func, args);
return handleIndexedReturn(T, F, false, local, ret, info, opts); return handleIndexedReturn(T, F, false, local, ret, info, opts);
@@ -276,7 +277,7 @@ fn _getEnumerator(comptime T: type, local: *const Local, func: anytype, info: Pr
var args: ParameterTypes(F) = undefined; var args: ParameterTypes(F) = undefined;
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis()); @field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
if (@typeInfo(F).@"fn".params.len == 2) { 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); const ret = @call(.auto, func, args);
return handleIndexedReturn(T, F, true, local, ret, info, opts); return handleIndexedReturn(T, F, true, local, ret, info, opts);
@@ -434,6 +435,25 @@ fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page; return T == *Page or T == *const Page;
} }
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;
}
@compileError("Unsupported global arg type: " ++ @typeName(T));
}
// These wrap the raw v8 C API to provide a cleaner interface. // These wrap the raw v8 C API to provide a cleaner interface.
pub const FunctionCallbackInfo = struct { pub const FunctionCallbackInfo = struct {
handle: *const v8.FunctionCallbackInfo, handle: *const v8.FunctionCallbackInfo,
@@ -702,15 +722,16 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info:
return args; 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 // from our params slice, because we don't want to bind it to
// a JS argument // a JS argument
if (comptime isPage(params[params.len - 1].type.?)) { const LastParamType = params[params.len - 1].type.?;
@field(args, tupleFieldName(params.len - 1 + offset)) = local.ctx.page; if (comptime isPage(LastParamType) or isExecution(LastParamType)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = getGlobalArg(LastParamType, local.ctx);
break :blk params[0 .. params.len - 1]; 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. // bound to a JavaScript value.
break :blk params; break :blk params;
}; };
@@ -759,7 +780,9 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info:
} }
if (comptime isPage(param.type.?)) { 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 (i >= js_parameter_count) { } else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) { if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument; return error.InvalidArgument;

View File

@@ -25,10 +25,12 @@ const bridge = @import("bridge.zig");
const Env = @import("Env.zig"); const Env = @import("Env.zig");
const Origin = @import("Origin.zig"); const Origin = @import("Origin.zig");
const Scheduler = @import("Scheduler.zig"); const Scheduler = @import("Scheduler.zig");
const Execution = @import("Execution.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Session = @import("../Session.zig"); const Session = @import("../Session.zig");
const ScriptManager = @import("../ScriptManager.zig"); const ScriptManager = @import("../ScriptManager.zig");
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
const v8 = js.v8; const v8 = js.v8;
const Caller = js.Caller; const Caller = js.Caller;
@@ -37,12 +39,38 @@ const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
// Loosely maps to a Browser Page. // Loosely maps to a Browser Page or Worker.
const Context = @This(); 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, id: usize,
env: *Env, env: *Env,
page: *Page, global: GlobalScope,
session: *Session, session: *Session,
isolate: js.Isolate, isolate: js.Isolate,
@@ -111,6 +139,10 @@ script_manager: ?*ScriptManager,
// Our macrotasks // Our macrotasks
scheduler: Scheduler, 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 {}, unknown_properties: (if (IS_DEBUG) std.StringHashMapUnmanaged(UnknownPropertyStat) else void) = if (IS_DEBUG) .{} else {},
const ModuleEntry = struct { const ModuleEntry = struct {
@@ -257,7 +289,16 @@ pub fn toLocal(self: *Context, global: anytype) js.Local.ToLocalReturnType(@Type
} }
pub fn getIncumbent(self: *Context) *Page { 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 => {
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
unreachable;
},
};
} }
pub fn stringToPersistedFunction( pub fn stringToPersistedFunction(
@@ -527,7 +568,7 @@ pub fn dynamicModuleCallback(
if (resource_value.isNullOrUndefined()) { if (resource_value.isNullOrUndefined()) {
// will only be null / undefined in extreme cases (e.g. WPT tests) // will only be null / undefined in extreme cases (e.g. WPT tests)
// where you're // 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| { break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
@@ -867,17 +908,16 @@ pub fn enter(self: *Context, hs: *js.HandleScope) Entered {
const isolate = self.isolate; const isolate = self.isolate;
js.HandleScope.init(hs, isolate); js.HandleScope.init(hs, isolate);
const page = self.page; const original = self.global.getJs();
const original = page.js; self.global.setJs(self);
page.js = self;
const handle: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle)); const handle: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle));
v8.v8__Context__Enter(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 { const Entered = struct {
// the context we should restore on the page // the context we should restore on the page/worker
original: *Context, original: *Context,
// the handle of the entered context // the handle of the entered context
@@ -885,8 +925,10 @@ const Entered = struct {
handle_scope: *js.HandleScope, handle_scope: *js.HandleScope,
global: GlobalScope,
pub fn exit(self: Entered) void { pub fn exit(self: Entered) void {
self.original.page.js = self.original; self.global.setJs(self.original);
v8.v8__Context__Exit(self.handle); v8.v8__Context__Exit(self.handle);
self.handle_scope.deinit(); self.handle_scope.deinit();
} }
@@ -895,7 +937,10 @@ const Entered = struct {
pub fn queueMutationDelivery(self: *Context) !void { pub fn queueMutationDelivery(self: *Context) !void {
self.enqueueMicrotask(struct { self.enqueueMicrotask(struct {
fn run(ctx: *Context) void { fn run(ctx: *Context) void {
ctx.page.deliverMutations(); switch (ctx.global) {
.page => |page| page.deliverMutations(),
.worker => unreachable,
}
} }
}.run); }.run);
} }
@@ -903,7 +948,10 @@ pub fn queueMutationDelivery(self: *Context) !void {
pub fn queueIntersectionChecks(self: *Context) !void { pub fn queueIntersectionChecks(self: *Context) !void {
self.enqueueMicrotask(struct { self.enqueueMicrotask(struct {
fn run(ctx: *Context) void { fn run(ctx: *Context) void {
ctx.page.performScheduledIntersectionChecks(); switch (ctx.global) {
.page => |page| page.performScheduledIntersectionChecks(),
.worker => unreachable,
}
} }
}.run); }.run);
} }
@@ -911,7 +959,10 @@ pub fn queueIntersectionChecks(self: *Context) !void {
pub fn queueIntersectionDelivery(self: *Context) !void { pub fn queueIntersectionDelivery(self: *Context) !void {
self.enqueueMicrotask(struct { self.enqueueMicrotask(struct {
fn run(ctx: *Context) void { fn run(ctx: *Context) void {
ctx.page.deliverIntersections(); switch (ctx.global) {
.page => |page| page.deliverIntersections(),
.worker => unreachable,
}
} }
}.run); }.run);
} }
@@ -919,7 +970,10 @@ pub fn queueIntersectionDelivery(self: *Context) !void {
pub fn queueSlotchangeDelivery(self: *Context) !void { pub fn queueSlotchangeDelivery(self: *Context) !void {
self.enqueueMicrotask(struct { self.enqueueMicrotask(struct {
fn run(ctx: *Context) void { fn run(ctx: *Context) void {
ctx.page.deliverSlotchangeEvents(); switch (ctx.global) {
.page => |page| page.deliverSlotchangeEvents(),
.worker => unreachable,
}
} }
}.run); }.run);
} }

View File

@@ -34,6 +34,7 @@ const Inspector = @import("Inspector.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Window = @import("../webapi/Window.zig"); const Window = @import("../webapi/Window.zig");
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
const JsApis = bridge.JsApis; const JsApis = bridge.JsApis;
const Allocator = std.mem.Allocator; 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 // Dynamic slice to avoid circular dependency on JsApis.len at comptime
templates: []*const v8.FunctionTemplate, 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 associated with the Isolate. Exists when CDP is being used.
inspector: ?*Inspector, inspector: ?*Inspector,
@@ -146,7 +144,6 @@ pub fn init(app: *App, opts: InitOpts) !Env {
const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len); const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);
errdefer allocator.free(templates); errdefer allocator.free(templates);
var global_eternal: v8.Eternal = undefined;
var private_symbols: PrivateSymbols = undefined; var private_symbols: PrivateSymbols = undefined;
{ {
var temp_scope: js.HandleScope = 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.?)); 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); private_symbols = PrivateSymbols.init(isolate_handle);
} }
@@ -221,7 +180,6 @@ pub fn init(app: *App, opts: InitOpts) !Env {
.templates = templates, .templates = templates,
.isolate_params = params, .isolate_params = params,
.inspector = inspector, .inspector = inspector,
.global_template = global_eternal,
.private_symbols = private_symbols, .private_symbols = private_symbols,
.microtask_queues_are_running = false, .microtask_queues_are_running = false,
.eternal_function_templates = eternal_function_templates, .eternal_function_templates = eternal_function_templates,
@@ -261,6 +219,17 @@ pub const ContextParams = struct {
}; };
pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context { pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
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(.{ .debug = params.debug_name }); const context_arena = try self.app.arena_pool.acquire(.{ .debug = params.debug_name });
errdefer self.app.arena_pool.release(context_arena); errdefer self.app.arena_pool.release(context_arena);
@@ -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).?; const microtask_queue = v8.v8__MicrotaskQueue__New(isolate.handle, v8.kExplicit).?;
errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue); errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue);
// Get the global template that was created once per isolate // Restore the context from the snapshot (0 = Page, 1 = Worker)
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?)); const snapshot_index: u32 = if (comptime is_page) 0 else 1;
v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime Snapshot.countInternalFields(Window.JsApi)); const v8_context = v8.v8__Context__FromSnapshot__Config(isolate.handle, snapshot_index, &.{
.global_template = null,
const v8_context = v8.v8__Context__New__Config(isolate.handle, &.{
.global_template = global_template,
.global_object = null, .global_object = null,
.microtask_queue = microtask_queue, .microtask_queue = microtask_queue,
}).?; }).?;
@@ -287,36 +254,36 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
var context_global: v8.Global = undefined; var context_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, v8_context, &context_global); 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).?; const global_obj = v8.v8__Context__Global(v8_context).?;
{ // Store our TAO inside the internal field of the global object. This
// Store our TAO inside the internal field of the global object. This // maps the v8::Object -> Zig instance.
// maps the v8::Object -> Zig instance. Almost all objects have this, and const tao = try params.identity_arena.create(@import("TaggedOpaque.zig"));
// it gets setup automatically as objects are created, but the Window tao.* = if (comptime is_page) .{
// object already exists in v8 (it's the global) so we manually create .value = @ptrCast(global.window),
// the mapping here. .prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr,
const tao = try params.identity_arena.create(@import("TaggedOpaque.zig")); .prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len),
tao.* = .{ .subtype = .node,
.value = @ptrCast(page.window), } else .{
.prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr, .value = @ptrCast(global),
.prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len), .prototype_chain = (&WorkerGlobalScope.JsApi.Meta.prototype_chain).ptr,
.subtype = .node, // this probably isn't right, but it's what we've been doing all along .prototype_len = @intCast(WorkerGlobalScope.JsApi.Meta.prototype_chain.len),
}; .subtype = null,
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao); };
} v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
const context_id = self.context_id; const context_id = self.context_id;
self.context_id = context_id + 1; self.context_id = context_id + 1;
const session = page._session; const session = global._session;
const origin = try session.getOrCreateOrigin(null); const origin = try session.getOrCreateOrigin(null);
errdefer session.releaseOrigin(origin); errdefer session.releaseOrigin(origin);
const context = try context_arena.create(Context); const context = try context_arena.create(Context);
context.* = .{ context.* = .{
.env = self, .env = self,
.page = page, .global = if (comptime is_page) .{ .page = global } else .{ .worker = global },
.origin = origin, .origin = origin,
.id = context_id, .id = context_id,
.session = session, .session = session,
@@ -326,22 +293,31 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
.templates = self.templates, .templates = self.templates,
.call_arena = params.call_arena, .call_arena = params.call_arena,
.microtask_queue = microtask_queue, .microtask_queue = microtask_queue,
.script_manager = &page._script_manager, .script_manager = if (comptime is_page) &global._script_manager else null,
.scheduler = .init(context_arena), .scheduler = .init(context_arena),
.identity = params.identity, .identity = params.identity,
.identity_arena = params.identity_arena, .identity_arena = params.identity_arena,
.execution = undefined,
}; };
{ context.execution = .{
// Multiple contexts can be created for the same Window (via CDP). We only .url = &global.url,
// need to register the first one. .buf = &global.buf,
const gop = try params.identity.identity_map.getOrPut(params.identity_arena, @intFromPtr(page.window)); .context = context,
if (gop.found_existing == false) { .arena = global.arena,
// our window wrapped in a v8::Global .call_arena = params.call_arena,
var global_global: v8.Global = undefined; ._factory = global._factory,
v8.v8__Global__New(isolate.handle, global_obj, &global_global); ._scheduler = &context.scheduler,
gop.value_ptr.* = global_global; };
}
// 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 // Store a pointer to our context inside the v8 context so that, given
@@ -528,13 +504,19 @@ fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) v
.call_arena = ctx.call_arena, .call_arena = ctx.call_arena,
}; };
const page = ctx.page; switch (ctx.global) {
page.window.unhandledPromiseRejection(promise_event == v8.kPromiseRejectWithNoHandler, .{ .page => |page| {
.local = &local, page.window.unhandledPromiseRejection(promise_event == v8.kPromiseRejectWithNoHandler, .{
.handle = &message_handle, .local = &local,
}, page) catch |err| { .handle = &message_handle,
log.warn(.browser, "unhandled rejection handler", .{ .err = err }); }, page) catch |err| {
}; log.warn(.browser, "unhandled rejection handler", .{ .err = err });
};
},
.worker => {
// TODO: Worker promise rejection handling
},
}
} }
fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void { fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {
@@ -566,3 +548,50 @@ const PrivateSymbols = struct {
self.child_nodes.deinit(); self.child_nodes.deinit();
} }
}; };
const testing = @import("../../testing.zig");
const EventTarget = @import("../webapi/EventTarget.zig");
test "Env: Worker context " {
const session = testing.test_session;
// Create a dummy WorkerGlobalScope using page's resources (hackish until session.createWorker exists)
const worker = try session.factory.eventTarget(WorkerGlobalScope{
._session = session,
._factory = &session.factory,
.arena = session.arena,
.url = "about:blank",
._proto = undefined,
._performance = .init(),
});
const ctx = try testing.test_browser.env.createWorkerContext(worker, .{
.identity = &session.identity,
.identity_arena = session.arena,
.call_arena = session.arena,
});
defer testing.test_browser.env.destroyContext(ctx);
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());
}
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")) { 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); return self.zigValueToJs(wrap, opts);
} }
@@ -409,7 +417,15 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
// zig fmt: on // zig fmt: on
if (@hasDecl(T, "runtimeGenericWrap")) { 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); return self.zigValueToJs(wrap, opts);
} }

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> // Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io> // Pierre Tachoire <pierre@lightpanda.io>
@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("js.zig"); const js = @import("js.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
@@ -25,6 +26,8 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8; const v8 = js.v8;
const JsApis = bridge.JsApis; const JsApis = bridge.JsApis;
const PageJsApis = bridge.PageJsApis;
const WorkerJsApis = bridge.WorkerJsApis;
const Snapshot = @This(); const Snapshot = @This();
@@ -135,7 +138,7 @@ pub fn create() !Snapshot {
v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate); v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);
defer v8.v8__HandleScope__DESTRUCT(&handle_scope); 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; var templates: [JsApis.len]*const v8.FunctionTemplate = undefined;
inline for (JsApis, 0..) |JsApi, i| { inline for (JsApis, 0..) |JsApi, i| {
@setEvalBranchQuota(10_000); @setEvalBranchQuota(10_000);
@@ -144,114 +147,51 @@ pub fn create() !Snapshot {
} }
// Set up prototype chains BEFORE attaching properties // Set up prototype chains BEFORE attaching properties
// This must come before attachClass so inheritance is set up first
inline for (JsApis, 0..) |JsApi, i| { inline for (JsApis, 0..) |JsApi, i| {
if (comptime protoIndexLookup(JsApi)) |proto_index| { if (comptime protoIndexLookup(JsApi)) |proto_index| {
v8.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]); v8.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]);
} }
} }
// Set up the global template to inherit from Window's template // Add ALL templates to snapshot (done once, in any context)
// This way the global object gets all Window properties through inheritance // We need a context to call AddData, so create a temporary one
const context = v8.v8__Context__New(isolate, null, null); {
v8.v8__Context__Enter(context); const temp_context = v8.v8__Context__New(isolate, null, null);
defer v8.v8__Context__Exit(context); v8.v8__Context__Enter(temp_context);
defer v8.v8__Context__Exit(temp_context);
// Add templates to context snapshot var last_data_index: usize = 0;
var last_data_index: usize = 0; inline for (JsApis, 0..) |_, i| {
inline for (JsApis, 0..) |_, i| { @setEvalBranchQuota(10_000);
@setEvalBranchQuota(10_000); const data_index = v8.v8__SnapshotCreator__AddData(snapshot_creator, @ptrCast(templates[i]));
const data_index = v8.v8__SnapshotCreator__AddData(snapshot_creator, @ptrCast(templates[i])); if (i == 0) {
if (i == 0) { data_start = data_index;
data_start = data_index; last_data_index = 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);
} else { } else {
const name = JsApi.Meta.name; if (data_index != last_data_index + 1) {
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); return error.InvalidDataIndex;
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); 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
// indexed 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 const Window = @import("../webapi/Window.zig");
// delete the built-in one. const index = try createSnapshotContext(&PageJsApis, Window.JsApi, isolate, snapshot_creator.?, &templates);
const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7); std.debug.assert(index == 0);
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);
}
} }
{ {
// Custom exception const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
// TODO: this is an horrible hack, I can't figure out how to do this cleanly. const index = try createSnapshotContext(&WorkerJsApis, WorkerGlobalScope.JsApi, isolate, snapshot_creator.?, &templates);
const code_str = "DOMException.prototype.__proto__ = Error.prototype"; std.debug.assert(index == 1);
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;
} }
v8.v8__SnapshotCreator__setDefaultContext(snapshot_creator, context);
} }
const blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep); const blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep);
@@ -259,25 +199,127 @@ pub fn create() !Snapshot {
return .{ return .{
.owns_data = true, .owns_data = true,
.data_start = data_start, .data_start = data_start,
.external_references = external_references,
.startup_data = blob, .startup_data = blob,
.external_references = external_references,
}; };
} }
// Helper to check if a JsApi has a NamedIndexed handler fn createSnapshotContext(
fn hasNamedIndexedGetter(comptime JsApi: type) bool { comptime ContextApis: []const type,
const declarations = @typeInfo(JsApi).@"struct".decls; comptime GlobalScopeApi: type,
inline for (declarations) |d| { isolate: *v8.Isolate,
const value = @field(JsApi, d.name); snapshot_creator: *v8.SnapshotCreator,
const T = @TypeOf(value); templates: []*const v8.FunctionTemplate,
if (T == bridge.NamedIndexed) { ) !usize {
return true; // 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 { fn countExternalReferences() comptime_int {
@setEvalBranchQuota(100_000); @setEvalBranchQuota(100_000);
@@ -289,24 +331,24 @@ fn countExternalReferences() comptime_int {
// +1 for the noop function shared by various types // +1 for the noop function shared by various types
count += 1; count += 1;
// +1 for unknownWindowPropertyCallback used on Window's global template
count += 1;
inline for (JsApis) |JsApi| { inline for (JsApis) |JsApi| {
// Constructor (only if explicit)
if (@hasDecl(JsApi, "constructor")) { if (@hasDecl(JsApi, "constructor")) {
count += 1; count += 1;
} }
// Callable (htmldda)
if (@hasDecl(JsApi, "callable")) { if (@hasDecl(JsApi, "callable")) {
count += 1; count += 1;
} }
// All other callbacks
const declarations = @typeInfo(JsApi).@"struct".decls; const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| { inline for (declarations) |d| {
const value = @field(JsApi, d.name); const value = @field(JsApi, d.name);
const T = @TypeOf(value); const T = @TypeOf(value);
if (T == bridge.Accessor) { if (T == bridge.Accessor) {
count += 1; // getter count += 1;
if (value.setter != null) { if (value.setter != null) {
count += 1; count += 1;
} }
@@ -320,14 +362,13 @@ fn countExternalReferences() comptime_int {
count += 1; count += 1;
} }
} else if (T == bridge.NamedIndexed) { } else if (T == bridge.NamedIndexed) {
count += 1; // getter count += 1;
if (value.setter != null) count += 1; if (value.setter != null) count += 1;
if (value.deleter != null) count += 1; if (value.deleter != null) count += 1;
} }
} }
} }
// In debug mode, add unknown property callbacks for types without NamedIndexed
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
inline for (JsApis) |JsApi| { inline for (JsApis) |JsApi| {
if (!hasNamedIndexedGetter(JsApi)) { if (!hasNamedIndexedGetter(JsApi)) {
@@ -349,6 +390,9 @@ fn collectExternalReferences() [countExternalReferences()]isize {
references[idx] = @bitCast(@intFromPtr(&bridge.Function.noopFunction)); references[idx] = @bitCast(@intFromPtr(&bridge.Function.noopFunction));
idx += 1; idx += 1;
references[idx] = @bitCast(@intFromPtr(&bridge.unknownWindowPropertyCallback));
idx += 1;
inline for (JsApis) |JsApi| { inline for (JsApis) |JsApi| {
if (@hasDecl(JsApi, "constructor")) { if (@hasDecl(JsApi, "constructor")) {
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func)); references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
@@ -399,7 +443,6 @@ fn collectExternalReferences() [countExternalReferences()]isize {
} }
} }
// In debug mode, collect unknown property callbacks for types without NamedIndexed
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
inline for (JsApis) |JsApi| { inline for (JsApis) |JsApi| {
if (!hasNamedIndexedGetter(JsApi)) { if (!hasNamedIndexedGetter(JsApi)) {
@@ -412,37 +455,11 @@ fn collectExternalReferences() [countExternalReferences()]isize {
return references; return references;
} }
// Even if a struct doesn't have a `constructor` function, we still fn protoIndexLookup(comptime JsApi: type) ?u16 {
// `generateConstructor`, because this is how we create our return protoIndexLookupFor(&JsApis, JsApi);
// 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 last_used_id = 0;
var cache_count: u8 = 0; var cache_count: u8 = 0;
@@ -480,14 +497,80 @@ pub fn countInternalFields(comptime JsApi: type) u8 {
return cache_count + 1; 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 protoIndexLookupFor(comptime ApiList: []const type, 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 (ApiList, 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 { fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.FunctionTemplate) void {
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template); const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(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 signature = v8.v8__Signature__New(isolate, template);
const declarations = @typeInfo(JsApi).@"struct".decls; const declarations = @typeInfo(JsApi).@"struct".decls;
@@ -523,7 +606,6 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
} }
if (value.static) { if (value.static) {
// Static accessors: use Template's SetAccessorProperty
v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute); v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute);
} else { } else {
v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{ v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{
@@ -535,7 +617,6 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
} }
}, },
bridge.Function => { bridge.Function => {
// For non-static functions, use the signature to validate the receiver
const func_signature = if (value.static) null else signature; const func_signature = if (value.static) null else signature;
const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{ const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{
.callback = value.func, .callback = value.func,
@@ -589,7 +670,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
bridge.Property => { bridge.Property => {
const js_value = switch (value.value) { const js_value = switch (value.value) {
.null => js.simpleZigValueToJs(.{ .handle = isolate }, null, true, false), .null => js.simpleZigValueToJs(.{ .handle = isolate }, null, true, false),
inline .bool, .int, .float, .string => |v| js.simpleZigValueToJs(.{ .handle = isolate }, v, true, false), inline .bool, .int, .float, .string => |pv| js.simpleZigValueToJs(.{ .handle = isolate }, pv, true, false),
}; };
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
@@ -599,11 +680,10 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
} }
if (value.template) { 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); v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
} }
}, },
bridge.Constructor => {}, // already handled in generateConstructor bridge.Constructor => {},
else => {}, else => {},
} }
} }
@@ -636,30 +716,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

@@ -24,6 +24,7 @@ const Session = @import("../Session.zig");
const v8 = js.v8; const v8 = js.v8;
const Caller = @import("Caller.zig"); const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
@@ -386,6 +387,11 @@ pub const Property = struct {
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
// During snapshot creation, there's no Context in embedder data yet
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate) orelse return 0;
if (v8.v8__Context__GetAlignedPointerFromEmbedderData(v8_context, 1) == null) return 0;
var caller: Caller = undefined; var caller: Caller = undefined;
caller.init(v8_isolate); caller.init(v8_isolate);
defer caller.deinit(); defer caller.deinit();
@@ -400,14 +406,18 @@ pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8
return 0; return 0;
}; };
const page = local.ctx.page; // Only Page contexts have document.getElementById lookup
const document = page.document; switch (local.ctx.global) {
.page => |page| {
if (document.getElementById(property, page)) |el| { const document = page.document;
const js_val = local.zigValueToJs(el, .{}) catch return 0; if (document.getElementById(property, page)) |el| {
var pc = Caller.PropertyCallbackInfo{ .handle = handle.? }; const js_val = local.zigValueToJs(el, .{}) catch return 0;
pc.getReturnValue().set(js_val); var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };
return 1; pc.getReturnValue().set(js_val);
return 1;
}
},
.worker => {}, // no global lookup in a worker
} }
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
@@ -445,7 +455,8 @@ pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8
.{ "ApplePaySession", {} }, .{ "ApplePaySession", {} },
}); });
if (!ignored.has(property)) { 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; logUnknownProperty(local, key) catch return 0;
} }
} }
@@ -508,7 +519,8 @@ pub fn unknownObjectPropertyCallback(comptime JsApi: type) *const fn (?*const v8
const ignored = std.StaticStringMap(void).initComptime(.{}); const ignored = std.StaticStringMap(void).initComptime(.{});
if (!ignored.has(property)) { 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; logUnknownProperty(local, key) catch return 0;
} }
// not intercepted // not intercepted
@@ -550,7 +562,7 @@ fn PrototypeType(comptime T: type) ?type {
return Struct(std.meta.fieldInfo(T, ._proto).type); return Struct(std.meta.fieldInfo(T, ._proto).type);
} }
fn flattenTypes(comptime Types: []const type) [countFlattenedTypes(Types)]type { pub fn flattenTypes(comptime Types: []const type) [countFlattenedTypes(Types)]type {
var index: usize = 0; var index: usize = 0;
var flat: [countFlattenedTypes(Types)]type = undefined; var flat: [countFlattenedTypes(Types)]type = undefined;
for (Types) |T| { for (Types) |T| {
@@ -673,7 +685,8 @@ pub const SubType = enum {
webassemblymemory, 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/AbortController.zig"),
@import("../webapi/AbortSignal.zig"), @import("../webapi/AbortSignal.zig"),
@import("../webapi/CData.zig"), @import("../webapi/CData.zig"),
@@ -866,3 +879,33 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/Selection.zig"), @import("../webapi/Selection.zig"),
@import("../webapi/ImageData.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/AbortController.zig"),
@import("../webapi/AbortSignal.zig"),
@import("../webapi/URL.zig"),
@import("../webapi/net/URLSearchParams.zig"),
@import("../webapi/net/Headers.zig"),
@import("../webapi/net/Request.zig"),
@import("../webapi/net/Response.zig"),
@import("../webapi/encoding/TextEncoder.zig"),
@import("../webapi/encoding/TextDecoder.zig"),
@import("../webapi/Blob.zig"),
@import("../webapi/File.zig"),
@import("../webapi/net/FormData.zig"),
@import("../webapi/Console.zig"),
@import("../webapi/Crypto.zig"),
@import("../webapi/Performance.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 Origin = @import("Origin.zig");
pub const Identity = @import("Identity.zig"); pub const Identity = @import("Identity.zig");
pub const Context = @import("Context.zig"); pub const Context = @import("Context.zig");
pub const Execution = @import("Execution.zig");
pub const Local = @import("Local.zig"); pub const Local = @import("Local.zig");
pub const Inspector = @import("Inspector.zig"); pub const Inspector = @import("Inspector.zig");
pub const Snapshot = @import("Snapshot.zig"); pub const Snapshot = @import("Snapshot.zig");

View File

@@ -34,6 +34,7 @@ pub const Type = union(enum) {
generic: void, generic: void,
node: *@import("Node.zig"), node: *@import("Node.zig"),
window: *@import("Window.zig"), window: *@import("Window.zig"),
worker_global_scope: *@import("WorkerGlobalScope.zig"),
xhr: *@import("net/XMLHttpRequestEventTarget.zig"), xhr: *@import("net/XMLHttpRequestEventTarget.zig"),
abort_signal: *@import("AbortSignal.zig"), abort_signal: *@import("AbortSignal.zig"),
media_query_list: *@import("css/MediaQueryList.zig"), media_query_list: *@import("css/MediaQueryList.zig"),
@@ -130,6 +131,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
.node => |n| n.format(writer), .node => |n| n.format(writer),
.generic => writer.writeAll("<EventTarget>"), .generic => writer.writeAll("<EventTarget>"),
.window => writer.writeAll("<Window>"), .window => writer.writeAll("<Window>"),
.worker_global_scope => writer.writeAll("<WorkerGlobalScope>"),
.xhr => writer.writeAll("<XMLHttpRequestEventTarget>"), .xhr => writer.writeAll("<XMLHttpRequestEventTarget>"),
.abort_signal => writer.writeAll("<AbortSignal>"), .abort_signal => writer.writeAll("<AbortSignal>"),
.media_query_list => writer.writeAll("<MediaQueryList>"), .media_query_list => writer.writeAll("<MediaQueryList>"),
@@ -149,6 +151,7 @@ pub fn toString(self: *EventTarget) []const u8 {
.node => return "[object Node]", .node => return "[object Node]",
.generic => return "[object EventTarget]", .generic => return "[object EventTarget]",
.window => return "[object Window]", .window => return "[object Window]",
.worker_global_scope => return "[object WorkerGlobalScope]",
.xhr => return "[object XMLHttpRequestEventTarget]", .xhr => return "[object XMLHttpRequestEventTarget]",
.abort_signal => return "[object AbortSignal]", .abort_signal => return "[object AbortSignal]",
.media_query_list => return "[object MediaQueryList]", .media_query_list => return "[object MediaQueryList]",

View File

@@ -22,6 +22,7 @@ const String = @import("../../string.zig").String;
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Execution = js.Execution;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -33,7 +34,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 { pub const Entry = struct {
name: String, name: String,
@@ -61,14 +62,14 @@ pub fn copy(arena: Allocator, original: KeyValueList) !KeyValueList {
return list; 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 it = try js_obj.nameIterator();
var list = KeyValueList.init(); var list = KeyValueList.init();
try list.ensureTotalCapacity(arena, it.count); try list.ensureTotalCapacity(arena, it.count);
while (try it.next()) |name| { while (try it.next()) |name| {
const js_value = try js_obj.get(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(.{ list._entries.appendAssumeCapacity(.{
.name = try String.init(arena, normalized, .{}), .name = try String.init(arena, normalized, .{}),
@@ -79,12 +80,12 @@ pub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?N
return list; 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(); var list = KeyValueList.init();
try list.ensureTotalCapacity(arena, kvs.len); try list.ensureTotalCapacity(arena, kvs.len);
for (kvs) |pair| { 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(.{ list._entries.appendAssumeCapacity(.{
.name = try String.init(arena, normalized, .{}), .name = try String.init(arena, normalized, .{}),
@@ -111,12 +112,11 @@ pub fn get(self: *const KeyValueList, name: []const u8) ?[]const u8 {
return null; return null;
} }
pub fn getAll(self: *const KeyValueList, name: []const u8, page: *Page) ![]const []const u8 { pub fn getAll(self: *const KeyValueList, allocator: Allocator, name: []const u8) ![]const []const u8 {
const arena = page.call_arena;
var arr: std.ArrayList([]const u8) = .empty; var arr: std.ArrayList([]const u8) = .empty;
for (self._entries.items) |*entry| { for (self._entries.items) |*entry| {
if (entry.name.eqlSlice(name)) { if (entry.name.eqlSlice(name)) {
try arr.append(arena, entry.value.str()); try arr.append(allocator, entry.value.str());
} }
} }
return arr.items; return arr.items;
@@ -260,7 +260,7 @@ pub const Iterator = struct {
pub const Entry = struct { []const u8, []const u8 }; 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 index = self.index;
const entries = self.kv._entries.items; const entries = self.kv._entries.items;
if (index >= entries.len) { if (index >= entries.len) {

View File

@@ -27,7 +27,7 @@ const Location = @This();
_url: *URL, _url: *URL,
pub fn init(raw_url: [:0]const u8, page: *Page) !*Location { 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{ return page._factory.create(Location{
._url = url, ._url = url,
}); });
@@ -53,12 +53,12 @@ pub fn getPort(self: *const Location) []const u8 {
return self._url.getPort(); return self._url.getPort();
} }
pub fn getOrigin(self: *const Location, page: *const Page) ![]const u8 { pub fn getOrigin(self: *const Location, exec: *const js.Execution) ![]const u8 {
return self._url.getOrigin(page); return self._url.getOrigin(exec);
} }
pub fn getSearch(self: *const Location, page: *const Page) ![]const u8 { pub fn getSearch(self: *const Location, exec: *const js.Execution) ![]const u8 {
return self._url.getSearch(page); return self._url.getSearch(exec);
} }
pub fn getHash(self: *const Location) []const u8 { 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 }); return page.scheduleNavigation(page.url, .{ .reason = .script, .kind = .reload }, .{ .script = page });
} }
pub fn toString(self: *const Location, page: *const Page) ![:0]const u8 { pub fn toString(self: *const Location, exec: *const js.Execution) ![:0]const u8 {
return self._url.toString(page); return self._url.toString(exec);
} }
pub const JsApi = struct { pub const JsApi = struct {

View File

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

View File

@@ -411,7 +411,7 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons
errdefer target_page.releaseArena(arena); errdefer target_page.releaseArena(arena);
// Origin should be the source window's origin (where the message came from) // 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); const callback = try arena.create(PostMessageCallback);
callback.* = .{ callback.* = .{
.arena = arena, .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 { pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
const encoded_len = std.base64.standard.Encoder.calcSize(input.len); return @import("encoding/base64.zig").encode(page.call_arena, input);
const encoded = try page.call_arena.alloc(u8, encoded_len);
return std.base64.standard.Encoder.encode(encoded, input);
} }
pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 { pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace); return @import("encoding/base64.zig").decode(page.call_arena, input);
// 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;
} }
pub fn structuredClone(_: *const Window, value: js.Value) !js.Value { pub fn structuredClone(_: *const Window, value: js.Value) !js.Value {

View File

@@ -0,0 +1,154 @@
// 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 Console = @import("Console.zig");
const Crypto = @import("Crypto.zig");
const EventTarget = @import("EventTarget.zig");
const Factory = @import("../Factory.zig");
const Performance = @import("Performance.zig");
const Session = @import("../Session.zig");
const Allocator = std.mem.Allocator;
const WorkerGlobalScope = @This();
// Infrastructure fields (similar to Page)
_session: *Session,
_factory: *Factory,
arena: Allocator,
url: [:0]const u8,
buf: [1024]u8 = undefined, // same size as page.buf
js: *JS.Context = undefined,
// WebAPI fields
_proto: *EventTarget,
_console: Console = .init,
_crypto: Crypto = .init,
_performance: Performance,
_on_error: ?JS.Function.Global = null,
_on_rejection_handled: ?JS.Function.Global = null,
_on_unhandled_rejection: ?JS.Function.Global = null,
pub fn base(self: *const WorkerGlobalScope) [:0]const u8 {
return self.url;
}
pub fn asEventTarget(self: *WorkerGlobalScope) *EventTarget {
return self._proto;
}
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 getPerformance(self: *WorkerGlobalScope) *Performance {
return &self._performance;
}
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 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();
}
// 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,
};
}
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 performance = bridge.accessor(WorkerGlobalScope.getPerformance, 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, .{});
// 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 std = @import("std");
const js = @import("../../js/js.zig");
const Node = @import("../Node.zig"); const Node = @import("../Node.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
@@ -136,7 +137,7 @@ const Iterator = struct {
const Entry = struct { u32, *Node }; 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 index = self.index;
const node = try self.list.getAtIndex(index, page) orelse return null; const node = try self.list.getAtIndex(index, page) orelse return null;
self.index = index + 1; self.index = index + 1;

View File

@@ -43,7 +43,7 @@ const Lookup = std.StringArrayHashMapUnmanaged(void);
const WHITESPACE = " \t\n\r\x0C"; const WHITESPACE = " \t\n\r\x0C";
pub fn length(self: *const DOMTokenList, page: *Page) !u32 { 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()); return @intCast(tokens.count());
} }
@@ -82,8 +82,8 @@ pub fn add(self: *DOMTokenList, tokens: []const []const u8, page: *Page) !void {
try validateToken(token); try validateToken(token);
} }
var lookup = try self.getTokens(page);
const allocator = page.call_arena; const allocator = page.call_arena;
var lookup = try self.getTokens(allocator);
try lookup.ensureUnusedCapacity(allocator, tokens.len); try lookup.ensureUnusedCapacity(allocator, tokens.len);
for (tokens) |token| { for (tokens) |token| {
@@ -98,7 +98,7 @@ pub fn remove(self: *DOMTokenList, tokens: []const []const u8, page: *Page) !voi
try validateToken(token); try validateToken(token);
} }
var lookup = try self.getTokens(page); var lookup = try self.getTokens(page.call_arena);
for (tokens) |token| { for (tokens) |token| {
_ = lookup.orderedRemove(token); _ = lookup.orderedRemove(token);
} }
@@ -149,7 +149,8 @@ pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8
return error.InvalidCharacterError; 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 // Check if old_token exists
if (!lookup.contains(old_token)) { if (!lookup.contains(old_token)) {
@@ -162,7 +163,6 @@ pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8
return true; return true;
} }
const allocator = page.call_arena;
// Build new token list preserving order but replacing old with new // Build new token list preserving order but replacing old with new
var new_tokens = try std.ArrayList([]const u8).initCapacity(allocator, lookup.count()); var new_tokens = try std.ArrayList([]const u8).initCapacity(allocator, lookup.count());
var replaced_old = false; 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(); const value = self.getValue();
if (value.len == 0) { if (value.len == 0) {
return .empty; return .empty;
} }
var list: Lookup = .empty; var list: Lookup = .empty;
const allocator = page.call_arena;
try list.ensureTotalCapacity(allocator, 4); try list.ensureTotalCapacity(allocator, 4);
var it = std.mem.tokenizeAny(u8, value, WHITESPACE); 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 Node = @import("../Node.zig");
const Element = @import("../Element.zig"); const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig"); const TreeWalker = @import("../TreeWalker.zig");
const Execution = js.Execution;
const HTMLAllCollection = @This(); 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(.{ return Iterator.init(.{
.list = self, .list = self,
.tw = self._tw.clone(), .tw = self._tw.clone(),
}, page); }, exec);
} }
const GenericIterator = @import("iterator.zig").Entry; const GenericIterator = @import("iterator.zig").Entry;
@@ -145,7 +146,7 @@ pub const Iterator = GenericIterator(struct {
list: *HTMLAllCollection, list: *HTMLAllCollection,
tw: TreeWalker.FullExcludeSelf, tw: TreeWalker.FullExcludeSelf,
pub fn next(self: *@This(), _: *Page) ?*Element { pub fn next(self: *@This(), _: *const Execution) ?*Element {
while (self.tw.next()) |node| { while (self.tw.next()) |node| {
if (node.is(Element)) |el| { if (node.is(Element)) |el| {
return el; return el;

View File

@@ -23,6 +23,7 @@ const Page = @import("../../Page.zig");
const Element = @import("../Element.zig"); const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig"); const TreeWalker = @import("../TreeWalker.zig");
const NodeLive = @import("node_live.zig").NodeLive; const NodeLive = @import("node_live.zig").NodeLive;
const Execution = js.Execution;
const Mode = enum { const Mode = enum {
tag, 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(.{ return Iterator.init(.{
.list = self, .list = self,
.tw = switch (self._data) { .tw = switch (self._data) {
@@ -94,7 +95,7 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
.form => |*impl| .{ .form = impl._tw.clone() }, .form => |*impl| .{ .form = impl._tw.clone() },
.empty => .empty, .empty => .empty,
}, },
}, page); }, exec);
} }
const GenericIterator = @import("iterator.zig").Entry; const GenericIterator = @import("iterator.zig").Entry;
@@ -115,7 +116,7 @@ pub const Iterator = GenericIterator(struct {
empty: void, empty: void,
}, },
pub fn next(self: *@This(), _: *Page) ?*Element { pub fn next(self: *@This(), _: *const Execution) ?*Element {
return switch (self.list._data) { return switch (self.list._data) {
.tag => |*impl| impl.nextTw(&self.tw.tag), .tag => |*impl| impl.nextTw(&self.tw.tag),
.tag_name => |*impl| impl.nextTw(&self.tw.tag_name), .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 js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
const Execution = js.Execution;
pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type { pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
const R = reflect(Inner, field); 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 const js_as_object = true;
}; };
pub fn init(inner: Inner, page: *Page) !*Self { pub fn init(inner: Inner, executor: R.Executor) !*Self {
const self = try page._factory.create(Self{ ._inner = inner }); const self = try executor._factory.create(Self{ ._inner = inner });
if (@hasDecl(Inner, "acquireRef")) { if (@hasDecl(Inner, "acquireRef")) {
self._inner.acquireRef(); self._inner.acquireRef();
@@ -62,8 +63,8 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
self._rc.acquire(); self._rc.acquire();
} }
pub fn next(self: *Self, page: *Page) if (R.has_error_return) anyerror!Result else Result { 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(page) else self._inner.next(page)) orelse { const entry = (if (comptime R.has_error_return) try self._inner.next(executor) else self._inner.next(executor)) orelse {
return .{ .done = true, .value = null }; 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 { 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; 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 .{ return .{
.has_error_return = has_error_return, .has_error_return = has_error_return,
.ValueType = ValueType(unwrapOptional(unwrapError(R)), field), .ValueType = ValueType(unwrapOptional(unwrapError(R)), field),
.Executor = Executor,
}; };
} }
const Reflect = struct { const Reflect = struct {
has_error_return: bool, has_error_return: bool,
ValueType: type, ValueType: type,
Executor: type,
}; };
fn unwrapError(comptime T: type) type { fn unwrapError(comptime T: type) type {

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

@@ -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 { 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 { 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); self._list.delete(name, null);
} }
pub fn keys(self: *FormData, page: *Page) !*KeyValueList.KeyIterator { pub fn keys(self: *FormData, exec: *const js.Execution) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, page); return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, exec);
} }
pub fn values(self: *FormData, page: *Page) !*KeyValueList.ValueIterator { pub fn values(self: *FormData, exec: *const js.Execution) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, page); return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, exec);
} }
pub fn entries(self: *FormData, page: *Page) !*KeyValueList.EntryIterator { pub fn entries(self: *FormData, exec: *const js.Execution) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, page); return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, exec);
} }
pub fn forEach(self: *FormData, cb_: js.Function, js_this_: ?js.Object) !void { 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 { pub fn init(opts_: ?InitOpts, page: *Page) !*Headers {
const list = if (opts_) |opts| switch (opts) { const list = if (opts_) |opts| switch (opts) {
.obj => |obj| try KeyValueList.copy(page.arena, obj._list), .obj => |obj| try KeyValueList.copy(page.arena, obj._list),
.js_obj => |js_obj| try KeyValueList.fromJsObject(page.arena, js_obj, 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), .strings => |kvs| try KeyValueList.fromArray(page.arena, kvs, normalizeHeaderName, &page.buf),
} else KeyValueList.init(); } else KeyValueList.init();
return page._factory.create(Headers{ 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 { 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); try self._list.append(page.arena, normalized_name, value);
} }
pub fn delete(self: *Headers, name: []const u8, page: *Page) void { 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); self._list.delete(normalized_name, null);
} }
pub fn get(self: *const Headers, name: []const u8, page: *Page) !?[]const u8 { pub fn get(self: *const Headers, name: []const u8, page: *Page) !?[]const u8 {
const normalized_name = normalizeHeaderName(name, page); const normalized_name = normalizeHeaderName(name, &page.buf);
const all_values = try self._list.getAll(normalized_name, page); const all_values = try self._list.getAll(page.call_arena, normalized_name);
if (all_values.len == 0) { if (all_values.len == 0) {
return null; 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 { 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); return self._list.has(normalized_name);
} }
pub fn set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void { 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); try self._list.set(page.arena, normalized_name, value);
} }
pub fn keys(self: *Headers, page: *Page) !*KeyValueList.KeyIterator { pub fn keys(self: *Headers, exec: *const js.Execution) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, page); return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, exec);
} }
pub fn values(self: *Headers, page: *Page) !*KeyValueList.ValueIterator { pub fn values(self: *Headers, exec: *const js.Execution) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, page); return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, exec);
} }
pub fn entries(self: *Headers, page: *Page) !*KeyValueList.EntryIterator { pub fn entries(self: *Headers, exec: *const js.Execution) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, page); return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, exec);
} }
pub fn forEach(self: *Headers, cb_: js.Function, js_this_: ?js.Object) !void { 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 { fn normalizeHeaderName(name: []const u8, buf: []u8) []const u8 {
if (name.len > page.buf.len) { if (name.len > buf.len) {
return name; return name;
} }
return std.ascii.lowerString(&page.buf, name); return std.ascii.lowerString(buf, name);
} }
pub const JsApi = struct { pub const JsApi = struct {

View File

@@ -26,6 +26,7 @@ const Allocator = std.mem.Allocator;
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const FormData = @import("FormData.zig"); const FormData = @import("FormData.zig");
const KeyValueList = @import("../KeyValueList.zig"); const KeyValueList = @import("../KeyValueList.zig");
const Execution = js.Execution;
const URLSearchParams = @This(); const URLSearchParams = @This();
@@ -38,12 +39,12 @@ const InitOpts = union(enum) {
query_string: []const u8, query_string: []const u8,
}; };
pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams { pub fn init(opts_: ?InitOpts, exec: *const Execution) !*URLSearchParams {
const arena = page.arena; const arena = exec.arena;
const params: KeyValueList = blk: { const params: KeyValueList = blk: {
const opts = opts_ orelse break :blk .empty; const opts = opts_ orelse break :blk .empty;
switch (opts) { 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), .form_data => |fd| break :blk try KeyValueList.copy(arena, fd._list),
.value => |js_val| { .value => |js_val| {
// Order matters here; Array is also an Object. // Order matters here; Array is also an Object.
@@ -51,24 +52,25 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
break :blk try paramsFromArray(arena, js_val.toArray()); break :blk try paramsFromArray(arena, js_val.toArray());
} }
if (js_val.isObject()) { 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| { 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 error.InvalidArgument;
}, },
} }
}; };
return page._factory.create(URLSearchParams{ return exec._factory.create(URLSearchParams{
._arena = arena, ._arena = arena,
._params = params, ._params = params,
}); });
} }
pub fn updateFromString(self: *URLSearchParams, query_string: []const u8, page: *Page) !void { pub fn updateFromString(self: *URLSearchParams, query_string: []const u8, exec: *const Execution) !void {
self._params = try paramsFromString(self._arena, query_string, &page.buf); self._params = try paramsFromString(self._arena, query_string, exec.buf);
} }
pub fn getSize(self: *const URLSearchParams) usize { pub fn getSize(self: *const URLSearchParams) usize {
@@ -79,8 +81,8 @@ pub fn get(self: *const URLSearchParams, name: []const u8) ?[]const u8 {
return self._params.get(name); return self._params.get(name);
} }
pub fn getAll(self: *const URLSearchParams, name: []const u8, page: *Page) ![]const []const u8 { pub fn getAll(self: *const URLSearchParams, name: []const u8, exec: *const Execution) ![]const []const u8 {
return self._params.getAll(name, page); return self._params.getAll(exec.call_arena, name);
} }
pub fn has(self: *const URLSearchParams, name: []const u8) bool { pub fn has(self: *const URLSearchParams, name: []const u8) bool {
@@ -99,16 +101,16 @@ pub fn delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) void
self._params.delete(name, value); self._params.delete(name, value);
} }
pub fn keys(self: *URLSearchParams, page: *Page) !*KeyValueList.KeyIterator { pub fn keys(self: *URLSearchParams, exec: *const Execution) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._params }, page); return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._params }, exec);
} }
pub fn values(self: *URLSearchParams, page: *Page) !*KeyValueList.ValueIterator { pub fn values(self: *URLSearchParams, exec: *const Execution) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._params }, page); return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._params }, exec);
} }
pub fn entries(self: *URLSearchParams, page: *Page) !*KeyValueList.EntryIterator { pub fn entries(self: *URLSearchParams, exec: *const Execution) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._params }, page); return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._params }, exec);
} }
pub fn toString(self: *const URLSearchParams, writer: *std.Io.Writer) !void { pub fn toString(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
@@ -314,7 +316,7 @@ pub const Iterator = struct {
const Entry = struct { []const u8, []const u8 }; 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 index = self.index;
const items = self.list._params.items; const items = self.list._params.items;
if (index >= items.len) { if (index >= items.len) {
@@ -352,8 +354,8 @@ pub const JsApi = struct {
pub const sort = bridge.function(URLSearchParams.sort, .{}); pub const sort = bridge.function(URLSearchParams.sort, .{});
pub const toString = bridge.function(_toString, .{}); pub const toString = bridge.function(_toString, .{});
fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 { fn _toString(self: *const URLSearchParams, exec: *const Execution) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena); var buf = std.Io.Writer.Allocating.init(exec.call_arena);
try self.toString(&buf.writer); try self.toString(&buf.writer);
return buf.written(); return buf.written();
} }