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).
This commit is contained in:
Karl Seguin
2026-04-03 10:26:51 +08:00
parent 226d1ff183
commit 224a7ca0fe
6 changed files with 322 additions and 133 deletions

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -25,6 +25,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8;
const JsApis = bridge.JsApis;
const PageJsApis = bridge.PageJsApis;
const Snapshot = @This();
@@ -113,6 +114,8 @@ fn isValid(self: Snapshot) bool {
}
pub fn create() !Snapshot {
comptime validatePrototypeChains(&JsApis);
var external_references = collectExternalReferences();
var params: v8.CreateParams = undefined;
@@ -135,7 +138,7 @@ pub fn create() !Snapshot {
v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
// Create templates (constructors only) FIRST
// Create templates for ALL types (JsApis)
var templates: [JsApis.len]*const v8.FunctionTemplate = undefined;
inline for (JsApis, 0..) |JsApi, i| {
@setEvalBranchQuota(10_000);
@@ -144,20 +147,17 @@ pub fn create() !Snapshot {
}
// Set up prototype chains BEFORE attaching properties
// This must come before attachClass so inheritance is set up first
inline for (JsApis, 0..) |JsApi, i| {
if (comptime protoIndexLookup(JsApi)) |proto_index| {
v8.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]);
}
}
// Set up the global template to inherit from Window's template
// This way the global object gets all Window properties through inheritance
const context = v8.v8__Context__New(isolate, null, null);
v8.v8__Context__Enter(context);
defer v8.v8__Context__Exit(context);
// Add templates to context snapshot
// Add ALL templates to context snapshot
var last_data_index: usize = 0;
inline for (JsApis, 0..) |_, i| {
@setEvalBranchQuota(10_000);
@@ -166,11 +166,6 @@ pub fn create() !Snapshot {
data_start = data_index;
last_data_index = data_index;
} else {
// This isn't strictly required, but it means we only need to keep
// the first data_index. This is based on the assumption that
// addDataWithContext always increases by 1. If we ever hit this
// error, then that assumption is wrong and we should capture
// all the indexes explicitly in an array.
if (data_index != last_data_index + 1) {
return error.InvalidDataIndex;
}
@@ -178,13 +173,12 @@ pub fn create() !Snapshot {
}
}
// 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| {
// Attach only PAGE types to the default context's global
inline for (PageJsApis, 0..) |JsApi, i| {
// PageJsApis[i] == JsApis[i] because the PageJsApis are position at the start of the list
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;
@@ -192,12 +186,6 @@ pub fn create() !Snapshot {
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;
@@ -216,8 +204,7 @@ pub fn create() !Snapshot {
}
{
// If we want to overwrite the built-in console, we have to
// delete the built-in one.
// 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);
@@ -226,9 +213,8 @@ pub fn create() !Snapshot {
}
}
// This shouldn't be necessary, but it is:
// Set prototype chains on function objects
// 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);
@@ -243,8 +229,7 @@ pub fn create() !Snapshot {
}
{
// Custom exception
// TODO: this is an horrible hack, I can't figure out how to do this cleanly.
// 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;
@@ -264,20 +249,6 @@ pub fn create() !Snapshot {
};
}
// Helper to check if a JsApi has a NamedIndexed handler
fn hasNamedIndexedGetter(comptime JsApi: type) bool {
const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| {
const value = @field(JsApi, d.name);
const T = @TypeOf(value);
if (T == bridge.NamedIndexed) {
return true;
}
}
return false;
}
// Count total callbacks needed for external_references array
fn countExternalReferences() comptime_int {
@setEvalBranchQuota(100_000);
@@ -290,23 +261,20 @@ fn countExternalReferences() comptime_int {
count += 1;
inline for (JsApis) |JsApi| {
// Constructor (only if explicit)
if (@hasDecl(JsApi, "constructor")) {
count += 1;
}
// Callable (htmldda)
if (@hasDecl(JsApi, "callable")) {
count += 1;
}
// All other callbacks
const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| {
const value = @field(JsApi, d.name);
const T = @TypeOf(value);
if (T == bridge.Accessor) {
count += 1; // getter
count += 1;
if (value.setter != null) {
count += 1;
}
@@ -320,14 +288,13 @@ fn countExternalReferences() comptime_int {
count += 1;
}
} else if (T == bridge.NamedIndexed) {
count += 1; // getter
count += 1;
if (value.setter != null) count += 1;
if (value.deleter != null) count += 1;
}
}
}
// In debug mode, add unknown property callbacks for types without NamedIndexed
if (comptime IS_DEBUG) {
inline for (JsApis) |JsApi| {
if (!hasNamedIndexedGetter(JsApi)) {
@@ -399,7 +366,6 @@ fn collectExternalReferences() [countExternalReferences()]isize {
}
}
// In debug mode, collect unknown property callbacks for types without NamedIndexed
if (comptime IS_DEBUG) {
inline for (JsApis) |JsApi| {
if (!hasNamedIndexedGetter(JsApi)) {
@@ -412,34 +378,8 @@ fn collectExternalReferences() [countExternalReferences()]isize {
return references;
}
// Even if a struct doesn't have a `constructor` function, we still
// `generateConstructor`, because this is how we create our
// FunctionTemplate. Such classes exist, but they can't be instantiated
// via `new ClassName()` - but they could, for example, be created in
// Zig and returned from a function call, which is why we need the
// FunctionTemplate.
fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate {
const callback = blk: {
if (@hasDecl(JsApi, "constructor")) {
break :blk JsApi.constructor.func;
}
// Use shared illegal constructor callback
break :blk illegalConstructorCallback;
};
const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?;
{
const internal_field_count = comptime countInternalFields(JsApi);
if (internal_field_count > 0) {
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);
}
}
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
v8.v8__FunctionTemplate__SetClassName(template, class_name);
return template;
fn protoIndexLookup(comptime JsApi: type) ?u16 {
return protoIndexLookupFor(&JsApis, JsApi);
}
pub fn countInternalFields(comptime JsApi: type) u8 {
@@ -480,14 +420,109 @@ pub fn countInternalFields(comptime JsApi: type) u8 {
return cache_count + 1;
}
// Attaches JsApi members to the prototype template (normal case)
// Shared illegal constructor callback for types without explicit constructors
fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);
log.warn(.js, "Illegal constructor call", .{});
const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19);
const js_exception = v8.v8__Exception__TypeError(message);
_ = v8.v8__Isolate__ThrowException(isolate, js_exception);
var return_value: v8.ReturnValue = undefined;
v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);
v8.v8__ReturnValue__Set(return_value, js_exception);
}
// Helper to check if a JsApi has a NamedIndexed handler (public for reuse)
fn hasNamedIndexedGetter(comptime JsApi: type) bool {
const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| {
const value = @field(JsApi, d.name);
const T = @TypeOf(value);
if (T == bridge.NamedIndexed) {
return true;
}
}
return false;
}
// Generic prototype index lookup for a given API list
fn 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");
}
}
// Validates that every type in the API list has its full prototype chain
// contained within that same list. This catches errors where a type is added
// to a snapshot but its prototype dependencies are missing.
// See bridge.AllJsApis for more information.
fn validatePrototypeChains(comptime ApiList: []const type) void {
@setEvalBranchQuota(100_000);
inline for (ApiList) |JsApi| {
const T = JsApi.bridge.type;
if (@hasField(T, "_proto")) {
const Ptr = std.meta.fieldInfo(T, ._proto).type;
const ProtoType = @typeInfo(Ptr).pointer.child;
// Verify the prototype's JsApi is in our list
var found = false;
inline for (ApiList) |Api| {
if (Api == ProtoType.JsApi) {
found = true;
break;
}
}
if (!found) {
@compileError(
@typeName(JsApi) ++ " has prototype " ++
@typeName(ProtoType.JsApi) ++ " which is not in the API list",
);
}
}
}
}
// Generate a constructor template for a JsApi type (public for reuse)
pub fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate {
const callback = blk: {
if (@hasDecl(JsApi, "constructor")) {
break :blk JsApi.constructor.func;
}
break :blk illegalConstructorCallback;
};
const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?;
{
const internal_field_count = comptime countInternalFields(JsApi);
if (internal_field_count > 0) {
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);
}
}
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
v8.v8__FunctionTemplate__SetClassName(template, class_name);
return template;
}
// Attach JsApi members to a template (public for reuse)
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.FunctionTemplate) void {
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
// Create a signature that validates the receiver is an instance of this template.
// This prevents crashes when JavaScript extracts a getter/method and calls it
// with the wrong `this` (e.g., documentGetter.call(null)).
const signature = v8.v8__Signature__New(isolate, template);
const declarations = @typeInfo(JsApi).@"struct".decls;
@@ -523,7 +558,6 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
}
if (value.static) {
// Static accessors: use Template's SetAccessorProperty
v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute);
} else {
v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{
@@ -535,7 +569,6 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
}
},
bridge.Function => {
// For non-static functions, use the signature to validate the receiver
const func_signature = if (value.static) null else signature;
const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{
.callback = value.func,
@@ -589,7 +622,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
bridge.Property => {
const js_value = switch (value.value) {
.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));
@@ -599,11 +632,10 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
}
if (value.template) {
// apply it both to the type itself (e.g. Node.Elem)
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
}
},
bridge.Constructor => {}, // already handled in generateConstructor
bridge.Constructor => {},
else => {},
}
}
@@ -636,30 +668,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

@@ -550,7 +550,7 @@ fn PrototypeType(comptime T: type) ?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 flat: [countFlattenedTypes(Types)]type = undefined;
for (Types) |T| {
@@ -673,7 +673,8 @@ pub const SubType = enum {
webassemblymemory,
};
pub const JsApis = flattenTypes(&.{
/// APIs for Page/Window contexts. Used by Snapshot.zig for Page snapshot creation.
pub const PageJsApis = flattenTypes(&.{
@import("../webapi/AbortController.zig"),
@import("../webapi/AbortSignal.zig"),
@import("../webapi/CData.zig"),
@@ -866,3 +867,14 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/Selection.zig"),
@import("../webapi/ImageData.zig"),
});
/// APIs that exist only in Worker contexts (not in Page/Window).
const WorkerOnlyApis = flattenTypes(&.{
@import("../webapi/WorkerGlobalScope.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 ++ WorkerOnlyApis;

View File

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

View File

@@ -429,27 +429,11 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons
}
pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
const encoded_len = std.base64.standard.Encoder.calcSize(input.len);
const encoded = try page.call_arena.alloc(u8, encoded_len);
return std.base64.standard.Encoder.encode(encoded, input);
return @import("encoding/base64.zig").encode(page.call_arena, input);
}
pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace);
// Forgiving base64 decode per WHATWG spec:
// https://infra.spec.whatwg.org/#forgiving-base64-decode
// Remove trailing padding to use standard_no_pad decoder
const unpadded = std.mem.trimRight(u8, trimmed, "=");
// Length % 4 == 1 is invalid (can't represent valid base64)
if (unpadded.len % 4 == 1) {
return error.InvalidCharacterError;
}
const decoded_len = std.base64.standard_no_pad.Decoder.calcSizeForSlice(unpadded) catch return error.InvalidCharacterError;
const decoded = try page.call_arena.alloc(u8, decoded_len);
std.base64.standard_no_pad.Decoder.decode(decoded, unpadded) catch return error.InvalidCharacterError;
return decoded;
return @import("encoding/base64.zig").decode(page.call_arena, input);
}
pub fn structuredClone(_: *const Window, value: js.Value) !js.Value {

View File

@@ -0,0 +1,135 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const base64 = @import("encoding/base64.zig");
const Console = @import("Console.zig");
const Crypto = @import("Crypto.zig");
const EventTarget = @import("EventTarget.zig");
const Performance = @import("Performance.zig");
const WorkerGlobalScope = @This();
_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 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 {
return base64.encode(exec.call_arena, input);
}
pub fn atob(_: *const WorkerGlobalScope, input: []const u8, exec: *js.Execution) ![]const u8 {
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

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