diff --git a/src/app.zig b/src/app.zig
index a5ceab82c..719dd9b72 100644
--- a/src/app.zig
+++ b/src/app.zig
@@ -4,7 +4,7 @@ const Allocator = std.mem.Allocator;
const log = @import("log.zig");
const Http = @import("http/Http.zig");
-const Platform = @import("browser/js/js.zig").Platform;
+const Platform = @import("browser/js/Platform.zig");
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
const Notification = @import("notification.zig").Notification;
diff --git a/src/browser/State.zig b/src/browser/State.zig
index 77165e5b8..022c0310c 100644
--- a/src/browser/State.zig
+++ b/src/browser/State.zig
@@ -53,7 +53,7 @@ style_sheet: ?*StyleSheet = null,
// for dom/document
active_element: ?*parser.Element = null,
-adopted_style_sheets: ?js.JsObject = null,
+adopted_style_sheets: ?js.Object = null,
// for HTMLSelectElement
// By default, if no option is explicitly selected, the first option should
diff --git a/src/browser/console/console.zig b/src/browser/console/console.zig
index 16d3731aa..78f2433a7 100644
--- a/src/browser/console/console.zig
+++ b/src/browser/console/console.zig
@@ -28,39 +28,39 @@ pub const Console = struct {
timers: std.StringHashMapUnmanaged(u32) = .{},
counts: std.StringHashMapUnmanaged(u32) = .{},
- pub fn _lp(values: []js.JsObject, page: *Page) !void {
+ pub fn _lp(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
}
- pub fn _log(values: []js.JsObject, page: *Page) !void {
+ pub fn _log(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
}
- pub fn _info(values: []js.JsObject, page: *Page) !void {
+ pub fn _info(values: []js.Object, page: *Page) !void {
return _log(values, page);
}
- pub fn _debug(values: []js.JsObject, page: *Page) !void {
+ pub fn _debug(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
}
- pub fn _warn(values: []js.JsObject, page: *Page) !void {
+ pub fn _warn(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
}
- pub fn _error(values: []js.JsObject, page: *Page) !void {
+ pub fn _error(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
@@ -132,7 +132,7 @@ pub const Console = struct {
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
}
- pub fn _assert(assertion: js.JsObject, values: []js.JsObject, page: *Page) !void {
+ pub fn _assert(assertion: js.Object, values: []js.Object, page: *Page) !void {
if (assertion.isTruthy()) {
return;
}
@@ -143,7 +143,7 @@ pub const Console = struct {
log.info(.console, "assertion failed", .{ .values = serialized_values });
}
- fn serializeValues(values: []js.JsObject, page: *Page) ![]const u8 {
+ fn serializeValues(values: []js.Object, page: *Page) ![]const u8 {
if (values.len == 0) {
return "";
}
diff --git a/src/browser/crypto/crypto.zig b/src/browser/crypto/crypto.zig
index 8d69176aa..7cb38f31d 100644
--- a/src/browser/crypto/crypto.zig
+++ b/src/browser/crypto/crypto.zig
@@ -24,7 +24,7 @@ const uuidv4 = @import("../../id.zig").uuidv4;
pub const Crypto = struct {
_not_empty: bool = true,
- pub fn _getRandomValues(_: *const Crypto, js_obj: js.JsObject) !js.JsObject {
+ pub fn _getRandomValues(_: *const Crypto, js_obj: js.Object) !js.Object {
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
const buf = into.asBuffer();
if (buf.len > 65_536) {
diff --git a/src/browser/dom/Animation.zig b/src/browser/dom/Animation.zig
index 8f25af6e0..cd5d1702a 100644
--- a/src/browser/dom/Animation.zig
+++ b/src/browser/dom/Animation.zig
@@ -23,12 +23,12 @@ const Page = @import("../page.zig").Page;
const Animation = @This();
-effect: ?js.JsObject,
-timeline: ?js.JsObject,
+effect: ?js.Object,
+timeline: ?js.Object,
ready_resolver: ?js.PromiseResolver,
finished_resolver: ?js.PromiseResolver,
-pub fn constructor(effect: ?js.JsObject, timeline: ?js.JsObject) !Animation {
+pub fn constructor(effect: ?js.Object, timeline: ?js.Object) !Animation {
return .{
.effect = if (effect) |eo| try eo.persist() else null,
.timeline = if (timeline) |to| try to.persist() else null,
@@ -65,19 +65,19 @@ pub fn get_ready(self: *Animation, page: *Page) !js.Promise {
return self.ready_resolver.?.promise();
}
-pub fn get_effect(self: *const Animation) ?js.JsObject {
+pub fn get_effect(self: *const Animation) ?js.Object {
return self.effect;
}
-pub fn set_effect(self: *Animation, effect: js.JsObject) !void {
+pub fn set_effect(self: *Animation, effect: js.Object) !void {
self.effect = try effect.persist();
}
-pub fn get_timeline(self: *const Animation) ?js.JsObject {
+pub fn get_timeline(self: *const Animation) ?js.Object {
return self.timeline;
}
-pub fn set_timeline(self: *Animation, timeline: js.JsObject) !void {
+pub fn set_timeline(self: *Animation, timeline: js.Object) !void {
self.timeline = try timeline.persist();
}
diff --git a/src/browser/dom/MessageChannel.zig b/src/browser/dom/MessageChannel.zig
index 354c990b0..027ac1256 100644
--- a/src/browser/dom/MessageChannel.zig
+++ b/src/browser/dom/MessageChannel.zig
@@ -74,18 +74,18 @@ pub const MessagePort = struct {
onmessageerror_cbk: ?js.Function = null,
// This is the queue of messages to dispatch to THIS MessagePort when the
// MessagePort is started.
- queue: std.ArrayListUnmanaged(js.JsObject) = .empty,
+ queue: std.ArrayListUnmanaged(js.Object) = .empty,
pub const PostMessageOption = union(enum) {
- transfer: js.JsObject,
+ transfer: js.Object,
options: Opts,
pub const Opts = struct {
- transfer: js.JsObject,
+ transfer: js.Object,
};
};
- pub fn _postMessage(self: *MessagePort, obj: js.JsObject, opts_: ?PostMessageOption, page: *Page) !void {
+ pub fn _postMessage(self: *MessagePort, obj: js.Object, opts_: ?PostMessageOption, page: *Page) !void {
if (self.closed) {
return;
}
@@ -150,7 +150,7 @@ pub const MessagePort = struct {
// called from our pair. If port1.postMessage("x") is called, then this
// will be called on port2.
- fn dispatchOrQueue(self: *MessagePort, obj: js.JsObject, arena: Allocator) !void {
+ fn dispatchOrQueue(self: *MessagePort, obj: js.Object, arena: Allocator) !void {
// our pair should have checked this already
std.debug.assert(self.closed == false);
@@ -165,7 +165,7 @@ pub const MessagePort = struct {
return self.queue.append(arena, try obj.persist());
}
- fn dispatch(self: *MessagePort, obj: js.JsObject) !void {
+ fn dispatch(self: *MessagePort, obj: js.Object) !void {
// obj is already persisted, don't use `MessageEvent.constructor`, but
// go directly to `init`, which assumes persisted objects.
var evt = try MessageEvent.init(.{ .data = obj });
@@ -205,12 +205,12 @@ pub const MessageEvent = struct {
pub const union_make_copy = true;
proto: parser.Event,
- data: ?js.JsObject,
+ data: ?js.Object,
// You would think if port1 sends to port2, the source would be port2
// (which is how I read the documentation), but it appears to always be
// null. It can always be set explicitly via the constructor;
- source: ?js.JsObject,
+ source: ?js.Object,
origin: []const u8,
@@ -224,8 +224,8 @@ pub const MessageEvent = struct {
ports: []*MessagePort,
const Options = struct {
- data: ?js.JsObject = null,
- source: ?js.JsObject = null,
+ data: ?js.Object = null,
+ source: ?js.Object = null,
origin: []const u8 = "",
lastEventId: []const u8 = "",
ports: []*MessagePort = &.{},
@@ -241,7 +241,7 @@ pub const MessageEvent = struct {
});
}
- // This is like "constructor", but it assumes js.JsObjects have already been
+ // This is like "constructor", but it assumes js.Objects have already been
// persisted. Necessary because this `new MessageEvent()` can be called
// directly from JS OR from a port.postMessage. In the latter case, data
// may have already been persisted (as it might need to be queued);
@@ -261,7 +261,7 @@ pub const MessageEvent = struct {
};
}
- pub fn get_data(self: *const MessageEvent) !?js.JsObject {
+ pub fn get_data(self: *const MessageEvent) !?js.Object {
return self.data;
}
@@ -269,7 +269,7 @@ pub const MessageEvent = struct {
return self.origin;
}
- pub fn get_source(self: *const MessageEvent) ?js.JsObject {
+ pub fn get_source(self: *const MessageEvent) ?js.Object {
return self.source;
}
diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig
index 002333f67..81ce57e4c 100644
--- a/src/browser/dom/document.zig
+++ b/src/browser/dom/document.zig
@@ -298,7 +298,7 @@ pub const Document = struct {
return &.{};
}
- pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !js.JsObject {
+ pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !js.Object {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
if (state.adopted_style_sheets) |obj| {
return obj;
@@ -309,7 +309,7 @@ pub const Document = struct {
return obj;
}
- pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: js.JsObject, page: *Page) !void {
+ pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: js.Object, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.adopted_style_sheets = try sheets.persist();
}
diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig
index 65b34dd43..906b85694 100644
--- a/src/browser/dom/element.zig
+++ b/src/browser/dom/element.zig
@@ -660,7 +660,7 @@ pub const Element = struct {
return sr;
}
- pub fn _animate(self: *parser.Element, effect: js.JsObject, opts: js.JsObject) !Animation {
+ pub fn _animate(self: *parser.Element, effect: js.Object, opts: js.Object) !Animation {
_ = self;
_ = opts;
return Animation.constructor(effect, null);
diff --git a/src/browser/dom/node_iterator.zig b/src/browser/dom/node_iterator.zig
index ebeee7a22..bf059340b 100644
--- a/src/browser/dom/node_iterator.zig
+++ b/src/browser/dom/node_iterator.zig
@@ -45,7 +45,7 @@ pub const NodeIterator = struct {
// One of the few cases where null and undefined resolve to different default.
// We need the raw JsObject so that we can probe the tri state:
// null, undefined or i32.
- pub const WhatToShow = js.JsObject;
+ pub const WhatToShow = js.Object;
pub const NodeIteratorOpts = union(enum) {
function: js.Function,
diff --git a/src/browser/dom/nodelist.zig b/src/browser/dom/nodelist.zig
index 7a32f7bb3..d2496b6a6 100644
--- a/src/browser/dom/nodelist.zig
+++ b/src/browser/dom/nodelist.zig
@@ -23,7 +23,6 @@ const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
-
const NodeUnion = @import("node.zig").Union;
const Node = @import("node.zig").Node;
@@ -174,7 +173,7 @@ pub const NodeList = struct {
}
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
- pub fn postAttach(self: *NodeList, js_this: js.JsThis) !void {
+ pub fn postAttach(self: *NodeList, js_this: js.This) !void {
const len = self.get_length();
for (0..len) |i| {
const node = try self._item(@intCast(i)) orelse unreachable;
diff --git a/src/browser/dom/performance.zig b/src/browser/dom/performance.zig
index d5de884dd..951c8f239 100644
--- a/src/browser/dom/performance.zig
+++ b/src/browser/dom/performance.zig
@@ -148,10 +148,10 @@ pub const PerformanceMark = struct {
pub const prototype = *PerformanceEntry;
proto: PerformanceEntry,
- detail: ?js.JsObject,
+ detail: ?js.Object,
const Options = struct {
- detail: ?js.JsObject = null,
+ detail: ?js.Object = null,
startTime: ?f64 = null,
};
@@ -171,7 +171,7 @@ pub const PerformanceMark = struct {
return .{ .proto = proto, .detail = detail };
}
- pub fn get_detail(self: *const PerformanceMark) ?js.JsObject {
+ pub fn get_detail(self: *const PerformanceMark) ?js.Object {
return self.detail;
}
};
diff --git a/src/browser/dom/shadow_root.zig b/src/browser/dom/shadow_root.zig
index f7d6d1da4..946fd793d 100644
--- a/src/browser/dom/shadow_root.zig
+++ b/src/browser/dom/shadow_root.zig
@@ -34,7 +34,7 @@ pub const ShadowRoot = struct {
mode: Mode,
host: *parser.Element,
proto: *parser.DocumentFragment,
- adopted_style_sheets: ?js.JsObject = null,
+ adopted_style_sheets: ?js.Object = null,
pub const Mode = enum {
open,
@@ -45,7 +45,7 @@ pub const ShadowRoot = struct {
return Element.toInterface(self.host);
}
- pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.JsObject {
+ pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object {
if (self.adopted_style_sheets) |obj| {
return obj;
}
@@ -55,7 +55,7 @@ pub const ShadowRoot = struct {
return obj;
}
- pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: js.JsObject) !void {
+ pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void {
self.adopted_style_sheets = try sheets.persist();
}
diff --git a/src/browser/dom/token_list.zig b/src/browser/dom/token_list.zig
index 4d989a90a..b1d036772 100644
--- a/src/browser/dom/token_list.zig
+++ b/src/browser/dom/token_list.zig
@@ -136,7 +136,7 @@ pub const DOMTokenList = struct {
}
// TODO handle thisArg
- pub fn _forEach(self: *parser.TokenList, cbk: js.Function, this_arg: js.JsObject) !void {
+ pub fn _forEach(self: *parser.TokenList, cbk: js.Function, this_arg: js.Object) !void {
var entries = _entries(self);
while (try entries._next()) |entry| {
var result: js.Function.Result = undefined;
diff --git a/src/browser/dom/tree_walker.zig b/src/browser/dom/tree_walker.zig
index 06a6552a5..7da44d548 100644
--- a/src/browser/dom/tree_walker.zig
+++ b/src/browser/dom/tree_walker.zig
@@ -35,7 +35,7 @@ pub const TreeWalker = struct {
// One of the few cases where null and undefined resolve to different default.
// We need the raw JsObject so that we can probe the tri state:
// null, undefined or i32.
- pub const WhatToShow = js.JsObject;
+ pub const WhatToShow = js.Object;
pub const TreeWalkerOpts = union(enum) {
function: js.Function,
diff --git a/src/browser/events/custom_event.zig b/src/browser/events/custom_event.zig
index bf54d2b2e..d4ed47e89 100644
--- a/src/browser/events/custom_event.zig
+++ b/src/browser/events/custom_event.zig
@@ -28,13 +28,13 @@ pub const CustomEvent = struct {
pub const union_make_copy = true;
proto: parser.Event,
- detail: ?js.JsObject,
+ detail: ?js.Object,
const CustomEventInit = struct {
bubbles: bool = false,
cancelable: bool = false,
composed: bool = false,
- detail: ?js.JsObject = null,
+ detail: ?js.Object = null,
};
pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent {
@@ -54,7 +54,7 @@ pub const CustomEvent = struct {
};
}
- pub fn get_detail(self: *CustomEvent) ?js.JsObject {
+ pub fn get_detail(self: *CustomEvent) ?js.Object {
return self.detail;
}
@@ -65,7 +65,7 @@ pub const CustomEvent = struct {
event_type: []const u8,
can_bubble: bool,
cancelable: bool,
- maybe_detail: ?js.JsObject,
+ maybe_detail: ?js.Object,
) !void {
// This function can only be called after the constructor has called.
// So we assume proto is initialized already by constructor.
diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig
index 7e399849a..17d192ff5 100644
--- a/src/browser/events/event.zig
+++ b/src/browser/events/event.zig
@@ -227,7 +227,7 @@ pub const EventHandler = struct {
pub const Listener = union(enum) {
function: js.Function,
- object: js.JsObject,
+ object: js.Object,
pub fn callback(self: Listener, target: *parser.EventTarget) !?js.Function {
return switch (self) {
diff --git a/src/browser/fetch/Headers.zig b/src/browser/fetch/Headers.zig
index 9c78046e2..82ddba5dc 100644
--- a/src/browser/fetch/Headers.zig
+++ b/src/browser/fetch/Headers.zig
@@ -68,7 +68,7 @@ pub const HeadersInit = union(enum) {
// Headers
headers: *Headers,
// Mappings
- object: js.JsObject,
+ object: js.Object,
};
pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
@@ -158,7 +158,7 @@ pub fn _entries(self: *const Headers) HeadersEntryIterable {
};
}
-pub fn _forEach(self: *Headers, callback_fn: js.Function, this_arg: ?js.JsObject) !void {
+pub fn _forEach(self: *Headers, callback_fn: js.Function, this_arg: ?js.Object) !void {
var iter = self.headers.iterator();
const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;
diff --git a/src/browser/fetch/Response.zig b/src/browser/fetch/Response.zig
index 6320c5577..ccda4c068 100644
--- a/src/browser/fetch/Response.zig
+++ b/src/browser/fetch/Response.zig
@@ -171,7 +171,7 @@ pub fn _bytes(self: *Response, page: *Page) !js.Promise {
}
const resolver = js.PromiseResolver{
- .js_context = page.main_context,
+ .context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
diff --git a/src/browser/html/History.zig b/src/browser/html/History.zig
index 8111ba44a..a11962021 100644
--- a/src/browser/html/History.zig
+++ b/src/browser/html/History.zig
@@ -113,7 +113,7 @@ fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void {
);
}
-pub fn _pushState(self: *History, state: js.JsObject, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
+pub fn _pushState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
const json = try state.toJson(arena);
@@ -123,7 +123,7 @@ pub fn _pushState(self: *History, state: js.JsObject, _: ?[]const u8, _url: ?[]c
self.current = self.stack.items.len - 1;
}
-pub fn _replaceState(self: *History, state: js.JsObject, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
+pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
if (self.current) |curr| {
diff --git a/src/browser/html/error_event.zig b/src/browser/html/error_event.zig
index 315584181..3fc14de7f 100644
--- a/src/browser/html/error_event.zig
+++ b/src/browser/html/error_event.zig
@@ -28,14 +28,14 @@ pub const ErrorEvent = struct {
filename: []const u8,
lineno: i32,
colno: i32,
- @"error": ?js.JsObject,
+ @"error": ?js.Object,
const ErrorEventInit = struct {
message: []const u8 = "",
filename: []const u8 = "",
lineno: i32 = 0,
colno: i32 = 0,
- @"error": ?js.JsObject = null,
+ @"error": ?js.Object = null,
};
pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent {
@@ -72,7 +72,7 @@ pub const ErrorEvent = struct {
return self.colno;
}
- pub fn get_error(self: *const ErrorEvent) js.UndefinedOr(js.JsObject) {
+ pub fn get_error(self: *const ErrorEvent) js.UndefinedOr(js.Object) {
if (self.@"error") |e| {
return .{ .value = e };
}
diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig
index 1ca681b3e..407e83dd8 100644
--- a/src/browser/html/window.zig
+++ b/src/browser/html/window.zig
@@ -268,11 +268,11 @@ pub const Window = struct {
_ = self.timers.remove(id);
}
- pub fn _setTimeout(self: *Window, cbk: js.Function, delay: ?u32, params: []js.JsObject, page: *Page) !u32 {
+ pub fn _setTimeout(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
}
- pub fn _setInterval(self: *Window, cbk: js.Function, delay: ?u32, params: []js.JsObject, page: *Page) !u32 {
+ pub fn _setInterval(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
}
@@ -320,7 +320,7 @@ pub const Window = struct {
const CreateTimeoutOpts = struct {
name: []const u8,
- args: []js.JsObject = &.{},
+ args: []js.Object = &.{},
repeat: bool = false,
animation_frame: bool = false,
low_priority: bool = false,
@@ -345,9 +345,9 @@ pub const Window = struct {
errdefer _ = self.timers.remove(timer_id);
const args = opts.args;
- var persisted_args: []js.JsObject = &.{};
+ var persisted_args: []js.Object = &.{};
if (args.len > 0) {
- persisted_args = try page.arena.alloc(js.JsObject, args.len);
+ persisted_args = try page.arena.alloc(js.Object, args.len);
for (args, persisted_args) |a, *ca| {
ca.* = try a.persist();
}
@@ -480,7 +480,7 @@ const TimerCallback = struct {
window: *Window,
- args: []js.JsObject = &.{},
+ args: []js.Object = &.{},
fn run(ctx: *anyopaque) ?u32 {
const self: *TimerCallback = @ptrCast(@alignCast(ctx));
diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig
new file mode 100644
index 000000000..635066e1d
--- /dev/null
+++ b/src/browser/js/Caller.zig
@@ -0,0 +1,560 @@
+const std = @import("std");
+const js = @import("js.zig");
+const v8 = js.v8;
+
+const log = @import("../../log.zig");
+const Page = @import("../page.zig").Page;
+
+const types = @import("types.zig");
+const Context = @import("Context.zig");
+
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+
+const CALL_ARENA_RETAIN = 1024 * 16;
+
+// Responsible for calling Zig functions from JS invocations. This could
+// probably just contained in ExecutionWorld, but having this specific logic, which
+// is somewhat repetitive between constructors, functions, getters, etc contained
+// here does feel like it makes it cleaner.
+const Caller = @This();
+context: *Context,
+v8_context: v8.Context,
+isolate: v8.Isolate,
+call_arena: Allocator,
+
+// info is a v8.PropertyCallbackInfo or a v8.FunctionCallback
+// All we really want from it is the isolate.
+// executor = Isolate -> getCurrentContext -> getEmbedderData()
+pub fn init(info: anytype) Caller {
+ const isolate = info.getIsolate();
+ const v8_context = isolate.getCurrentContext();
+ const context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
+
+ context.call_depth += 1;
+ return .{
+ .context = context,
+ .isolate = isolate,
+ .v8_context = v8_context,
+ .call_arena = context.call_arena,
+ };
+}
+
+pub fn deinit(self: *Caller) void {
+ const context = self.context;
+ const call_depth = context.call_depth - 1;
+
+ // Because of callbacks, calls can be nested. Because of this, we
+ // can't clear the call_arena after _every_ call. Imagine we have
+ // arr.forEach((i) => { console.log(i); }
+ //
+ // First we call forEach. Inside of our forEach call,
+ // we call console.log. If we reset the call_arena after this call,
+ // it'll reset it for the `forEach` call after, which might still
+ // need the data.
+ //
+ // Therefore, we keep a call_depth, and only reset the call_arena
+ // when a top-level (call_depth == 0) function ends.
+ if (call_depth == 0) {
+ const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
+ _ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
+ }
+
+ // Set this _after_ we've executed the above code, so that if the
+ // above code executes any callbacks, they aren't being executed
+ // at scope 0, which would be wrong.
+ context.call_depth = call_depth;
+}
+
+pub fn constructor(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
+ const args = try self.getArgs(Struct, named_function, 0, info);
+ const res = @call(.auto, Struct.constructor, args);
+
+ const ReturnType = @typeInfo(@TypeOf(Struct.constructor)).@"fn".return_type orelse {
+ @compileError(@typeName(Struct) ++ " has a constructor without a return type");
+ };
+
+ const this = info.getThis();
+ if (@typeInfo(ReturnType) == .error_union) {
+ const non_error_res = res catch |err| return err;
+ _ = try Context.mapZigInstanceToJs(self.v8_context, this, non_error_res);
+ } else {
+ _ = try Context.mapZigInstanceToJs(self.v8_context, this, res);
+ }
+ info.getReturnValue().set(this);
+}
+
+pub fn method(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
+ if (comptime isSelfReceiver(Struct, named_function) == false) {
+ return self.function(Struct, named_function, info);
+ }
+
+ const context = self.context;
+ const func = @field(Struct, named_function.name);
+ var args = try self.getArgs(Struct, named_function, 1, info);
+ const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
+
+ // inject 'self' as the first parameter
+ @field(args, "0") = zig_instance;
+
+ const res = @call(.auto, func, args);
+ info.getReturnValue().set(try context.zigValueToJs(res));
+}
+
+pub fn function(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
+ const context = self.context;
+ const func = @field(Struct, named_function.name);
+ const args = try self.getArgs(Struct, named_function, 0, info);
+ const res = @call(.auto, func, args);
+ info.getReturnValue().set(try context.zigValueToJs(res));
+}
+
+pub fn getIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, idx: u32, info: v8.PropertyCallbackInfo) !u8 {
+ const context = self.context;
+ const func = @field(Struct, named_function.name);
+ const IndexedGet = @TypeOf(func);
+ if (@typeInfo(IndexedGet).@"fn".return_type == null) {
+ @compileError(named_function.full_name ++ " must have a return type");
+ }
+
+ var has_value = true;
+
+ var args: ParamterTypes(IndexedGet) = undefined;
+ const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
+ switch (arg_fields.len) {
+ 0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
+ 3, 4 => {
+ const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
+ comptime assertSelfReceiver(Struct, named_function);
+ @field(args, "0") = zig_instance;
+ @field(args, "1") = idx;
+ @field(args, "2") = &has_value;
+ if (comptime arg_fields.len == 4) {
+ comptime assertIsPageArg(Struct, named_function, 3);
+ @field(args, "3") = context.page;
+ }
+ },
+ else => @compileError(named_function.full_name ++ " has too many parmaters"),
+ }
+
+ const res = @call(.auto, func, args);
+ if (has_value == false) {
+ return v8.Intercepted.No;
+ }
+ info.getReturnValue().set(try context.zigValueToJs(res));
+ return v8.Intercepted.Yes;
+}
+
+pub fn getNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
+ const context = self.context;
+ const func = @field(Struct, named_function.name);
+ comptime assertSelfReceiver(Struct, named_function);
+
+ var has_value = true;
+ var args = try self.getArgs(Struct, named_function, 3, info);
+ const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
+ @field(args, "0") = zig_instance;
+ @field(args, "1") = try self.nameToString(name);
+ @field(args, "2") = &has_value;
+
+ const res = @call(.auto, func, args);
+ if (has_value == false) {
+ return v8.Intercepted.No;
+ }
+ info.getReturnValue().set(try self.context.zigValueToJs(res));
+ return v8.Intercepted.Yes;
+}
+
+pub fn setNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo) !u8 {
+ const context = self.context;
+ const func = @field(Struct, named_function.name);
+ comptime assertSelfReceiver(Struct, named_function);
+
+ var has_value = true;
+ var args = try self.getArgs(Struct, named_function, 4, info);
+ const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
+ @field(args, "0") = zig_instance;
+ @field(args, "1") = try self.nameToString(name);
+ @field(args, "2") = try context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value);
+ @field(args, "3") = &has_value;
+
+ const res = @call(.auto, func, args);
+ return namedSetOrDeleteCall(res, has_value);
+}
+
+pub fn deleteNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
+ const context = self.context;
+ const func = @field(Struct, named_function.name);
+ comptime assertSelfReceiver(Struct, named_function);
+
+ var has_value = true;
+ var args = try self.getArgs(Struct, named_function, 3, info);
+ const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
+ @field(args, "0") = zig_instance;
+ @field(args, "1") = try self.nameToString(name);
+ @field(args, "2") = &has_value;
+
+ const res = @call(.auto, func, args);
+ return namedSetOrDeleteCall(res, has_value);
+}
+
+fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
+ if (@typeInfo(@TypeOf(res)) == .error_union) {
+ _ = try res;
+ }
+ if (has_value == false) {
+ return v8.Intercepted.No;
+ }
+ return v8.Intercepted.Yes;
+}
+
+fn nameToString(self: *Caller, name: v8.Name) ![]const u8 {
+ return js.valueToString(self.call_arena, .{ .handle = name.handle }, self.isolate, self.v8_context);
+}
+
+fn isSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) bool {
+ return checkSelfReceiver(Struct, named_function, false);
+}
+fn assertSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) void {
+ _ = checkSelfReceiver(Struct, named_function, true);
+}
+fn checkSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction, comptime fail: bool) bool {
+ const func = @field(Struct, named_function.name);
+ const params = @typeInfo(@TypeOf(func)).@"fn".params;
+ if (params.len == 0) {
+ if (fail) {
+ @compileError(named_function.full_name ++ " must have a self parameter");
+ }
+ return false;
+ }
+
+ const R = types.Receiver(Struct);
+ const first_param = params[0].type.?;
+ if (first_param != *R and first_param != *const R) {
+ if (fail) {
+ @compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{
+ named_function.full_name,
+ @typeName(R),
+ @typeName(R),
+ @typeName(first_param),
+ }));
+ }
+ return false;
+ }
+ return true;
+}
+
+fn assertIsPageArg(comptime Struct: type, comptime named_function: NamedFunction, index: comptime_int) void {
+ const F = @TypeOf(@field(Struct, named_function.name));
+ const param = @typeInfo(F).@"fn".params[index].type.?;
+ if (isPage(param)) {
+ return;
+ }
+ @compileError(std.fmt.comptimePrint("The {d} parameter to {s} must be a *Page or *const Page. Got: {s}", .{ index, named_function.full_name, @typeName(param) }));
+}
+
+pub fn handleError(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, err: anyerror, info: anytype) void {
+ const isolate = self.isolate;
+
+ if (comptime @import("builtin").mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
+ if (log.enabled(.js, .warn)) {
+ logFunctionCallError(self.call_arena, self.isolate, self.v8_context, err, named_function.full_name, info);
+ }
+ }
+
+ var js_err: ?v8.Value = switch (err) {
+ error.InvalidArgument => createTypeException(isolate, "invalid argument"),
+ error.OutOfMemory => js._createException(isolate, "out of memory"),
+ error.IllegalConstructor => js._createException(isolate, "Illegal Contructor"),
+ else => blk: {
+ const func = @field(Struct, named_function.name);
+ const return_type = @typeInfo(@TypeOf(func)).@"fn".return_type orelse {
+ // void return type;
+ break :blk null;
+ };
+
+ if (@typeInfo(return_type) != .error_union) {
+ // type defines a custom exception, but this function should
+ // not fail. We failed somewhere inside of js.zig and
+ // should return the error as-is, since it isn't related
+ // to our Struct
+ break :blk null;
+ }
+
+ const function_error_set = @typeInfo(return_type).error_union.error_set;
+
+ const E = comptime getCustomException(Struct) orelse break :blk null;
+ if (function_error_set == E or isErrorSetException(E, err)) {
+ const custom_exception = E.init(self.call_arena, err, named_function.js_name) catch |init_err| {
+ switch (init_err) {
+ // if a custom exceptions' init wants to return a
+ // different error, we need to think about how to
+ // handle that failure.
+ error.OutOfMemory => break :blk js._createException(isolate, "out of memory"),
+ }
+ };
+ // ughh..how to handle an error here?
+ break :blk self.context.zigValueToJs(custom_exception) catch js._createException(isolate, "internal error");
+ }
+ // this error isn't part of a custom exception
+ break :blk null;
+ },
+ };
+
+ if (js_err == null) {
+ js_err = js._createException(isolate, @errorName(err));
+ }
+ const js_exception = isolate.throwException(js_err.?);
+ info.getReturnValue().setValueHandle(js_exception.handle);
+}
+
+// walk the prototype chain to see if a type declares a custom Exception
+fn getCustomException(comptime Struct: type) ?type {
+ var S = Struct;
+ while (true) {
+ if (@hasDecl(S, "Exception")) {
+ return S.Exception;
+ }
+ if (@hasDecl(S, "prototype") == false) {
+ return null;
+ }
+ // long ago, we validated that every prototype declaration
+ // is a pointer.
+ S = @typeInfo(S.prototype).pointer.child;
+ }
+}
+
+// Does the error we want to return belong to the custom exeception's ErrorSet
+fn isErrorSetException(comptime E: type, err: anytype) bool {
+ const Entry = std.meta.Tuple(&.{ []const u8, void });
+
+ const error_set = @typeInfo(E.ErrorSet).error_set.?;
+ const entries = comptime blk: {
+ var kv: [error_set.len]Entry = undefined;
+ for (error_set, 0..) |e, i| {
+ kv[i] = .{ e.name, {} };
+ }
+ break :blk kv;
+ };
+ const lookup = std.StaticStringMap(void).initComptime(entries);
+ return lookup.has(@errorName(err));
+}
+
+// If we call a method in javascript: cat.lives('nine');
+//
+// Then we'd expect a Zig function with 2 parameters: a self and the string.
+// In this case, offset == 1. Offset is always 1 for setters or methods.
+//
+// Offset is always 0 for constructors.
+//
+// For constructors, setters and methods, we can further increase offset + 1
+// if the first parameter is an instance of Page.
+//
+// Finally, if the JS function is called with _more_ parameters and
+// the last parameter in Zig is an array, we'll try to slurp the additional
+// parameters into the array.
+fn getArgs(self: *const Caller, comptime Struct: type, comptime named_function: NamedFunction, comptime offset: usize, info: anytype) !ParamterTypes(@TypeOf(@field(Struct, named_function.name))) {
+ const context = self.context;
+ const F = @TypeOf(@field(Struct, named_function.name));
+ var args: ParamterTypes(F) = undefined;
+
+ const params = @typeInfo(F).@"fn".params[offset..];
+ // Except for the constructor, the first parameter is always `self`
+ // This isn't something we'll bind from JS, so skip it.
+ const params_to_map = blk: {
+ if (params.len == 0) {
+ return args;
+ }
+
+ // If the last parameter is the Page, set it, and exclude it
+ // from our params slice, because we don't want to bind it to
+ // a JS argument
+ if (comptime isPage(params[params.len - 1].type.?)) {
+ @field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
+ break :blk params[0 .. params.len - 1];
+ }
+
+ // If the last parameter is a special JsThis, set it, and exclude it
+ // from our params slice, because we don't want to bind it to
+ // a JS argument
+ if (comptime params[params.len - 1].type.? == js.This) {
+ @field(args, tupleFieldName(params.len - 1 + offset)) = .{ .obj = .{
+ .context = context,
+ .js_obj = info.getThis(),
+ } };
+
+ // AND the 2nd last parameter is state
+ if (params.len > 1 and comptime isPage(params[params.len - 2].type.?)) {
+ @field(args, tupleFieldName(params.len - 2 + offset)) = self.context.page;
+ break :blk params[0 .. params.len - 2];
+ }
+
+ break :blk params[0 .. params.len - 1];
+ }
+
+ // we have neither a Page nor a JsObject. All params must be
+ // bound to a JavaScript value.
+ break :blk params;
+ };
+
+ if (params_to_map.len == 0) {
+ return args;
+ }
+
+ const js_parameter_count = info.length();
+ const last_js_parameter = params_to_map.len - 1;
+ var is_variadic = false;
+
+ {
+ // This is going to get complicated. If the last Zig parameter
+ // is a slice AND the corresponding javascript parameter is
+ // NOT an an array, then we'll treat it as a variadic.
+
+ const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
+ const last_parameter_type_info = @typeInfo(last_parameter_type);
+ if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
+ const slice_type = last_parameter_type_info.pointer.child;
+ const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
+ if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
+ is_variadic = true;
+ if (js_parameter_count == 0) {
+ @field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
+ } else if (js_parameter_count >= params_to_map.len) {
+ const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
+ for (arr, last_js_parameter..) |*a, i| {
+ const js_value = info.getArg(@as(u32, @intCast(i)));
+ a.* = try context.jsValueToZig(named_function, slice_type, js_value);
+ }
+ @field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
+ } else {
+ @field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
+ }
+ }
+ }
+ }
+
+ inline for (params_to_map, 0..) |param, i| {
+ const field_index = comptime i + offset;
+ if (comptime i == params_to_map.len - 1) {
+ if (is_variadic) {
+ break;
+ }
+ }
+
+ if (comptime isPage(param.type.?)) {
+ @compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ named_function.full_name);
+ } else if (comptime param.type.? == js.This) {
+ @compileError("JsThis must be the last parameter: " ++ named_function.full_name);
+ } else if (i >= js_parameter_count) {
+ if (@typeInfo(param.type.?) != .optional) {
+ return error.InvalidArgument;
+ }
+ @field(args, tupleFieldName(field_index)) = null;
+ } else {
+ const js_value = info.getArg(@as(u32, @intCast(i)));
+ @field(args, tupleFieldName(field_index)) = context.jsValueToZig(named_function, param.type.?, js_value) catch {
+ return error.InvalidArgument;
+ };
+ }
+ }
+
+ return args;
+}
+
+// We want the function name, or more precisely, the "Struct.function" for
+// displaying helpful @compileError.
+// However, there's no way to get the name from a std.Builtin.Fn, so we create
+// a NamedFunction as part of our binding, and pass it around incase we need
+// to display an error
+pub const NamedFunction = struct {
+ name: []const u8,
+ js_name: []const u8,
+ full_name: []const u8,
+
+ pub fn init(comptime Struct: type, comptime name: []const u8) NamedFunction {
+ return .{
+ .name = name,
+ .js_name = if (name[0] == '_') name[1..] else name,
+ .full_name = @typeName(Struct) ++ "." ++ name,
+ };
+ }
+};
+
+// Takes a function, and returns a tuple for its argument. Used when we
+// @call a function
+fn ParamterTypes(comptime F: type) type {
+ const params = @typeInfo(F).@"fn".params;
+ var fields: [params.len]std.builtin.Type.StructField = undefined;
+
+ inline for (params, 0..) |param, i| {
+ fields[i] = .{
+ .name = tupleFieldName(i),
+ .type = param.type.?,
+ .default_value_ptr = null,
+ .is_comptime = false,
+ .alignment = @alignOf(param.type.?),
+ };
+ }
+
+ return @Type(.{ .@"struct" = .{
+ .layout = .auto,
+ .decls = &.{},
+ .fields = &fields,
+ .is_tuple = true,
+ } });
+}
+
+fn tupleFieldName(comptime i: usize) [:0]const u8 {
+ return switch (i) {
+ 0 => "0",
+ 1 => "1",
+ 2 => "2",
+ 3 => "3",
+ 4 => "4",
+ 5 => "5",
+ 6 => "6",
+ 7 => "7",
+ 8 => "8",
+ 9 => "9",
+ else => std.fmt.comptimePrint("{d}", .{i}),
+ };
+}
+
+fn isPage(comptime T: type) bool {
+ return T == *Page or T == *const Page;
+}
+
+// This is extracted to speed up compilation. When left inlined in handleError,
+// this can add as much as 10 seconds of compilation time.
+fn logFunctionCallError(arena: Allocator, isolate: v8.Isolate, context: v8.Context, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void {
+ const args_dump = serializeFunctionArgs(arena, isolate, context, info) catch "failed to serialize args";
+ log.info(.js, "function call error", .{
+ .name = function_name,
+ .err = err,
+ .args = args_dump,
+ .stack = Context.stackForLogs(arena, isolate) catch |err1| @errorName(err1),
+ });
+}
+
+fn serializeFunctionArgs(arena: Allocator, isolate: v8.Isolate, context: v8.Context, info: v8.FunctionCallbackInfo) ![]const u8 {
+ const separator = log.separator();
+ const js_parameter_count = info.length();
+
+ var arr: std.ArrayListUnmanaged(u8) = .{};
+ for (0..js_parameter_count) |i| {
+ const js_value = info.getArg(@intCast(i));
+ const value_string = try js.valueToDetailString(arena, js_value, isolate, context);
+ const value_type = try js.stringToZig(arena, try js_value.typeOf(isolate), isolate);
+ try std.fmt.format(arr.writer(arena), "{s}{d}: {s} ({s})", .{
+ separator,
+ i + 1,
+ value_string,
+ value_type,
+ });
+ }
+ return arr.items;
+}
+
+fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value {
+ return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg));
+}
diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig
new file mode 100644
index 000000000..5ce9da48f
--- /dev/null
+++ b/src/browser/js/Context.zig
@@ -0,0 +1,1731 @@
+const std = @import("std");
+const js = @import("js.zig");
+const v8 = js.v8;
+
+const log = @import("../../log.zig");
+const Page = @import("../page.zig").Page;
+const ScriptManager = @import("../ScriptManager.zig");
+
+const Allocator = std.mem.Allocator;
+
+const types = @import("types.zig");
+const Caller = @import("Caller.zig");
+const NamedFunction = Caller.NamedFunction;
+const PersistentObject = v8.Persistent(v8.Object);
+const PersistentModule = v8.Persistent(v8.Module);
+const PersistentPromise = v8.Persistent(v8.Promise);
+const PersistentFunction = v8.Persistent(v8.Function);
+const TaggedAnyOpaque = js.TaggedAnyOpaque;
+
+// Loosely maps to a Browser Page.
+const Context = @This();
+
+id: usize,
+page: *Page,
+isolate: v8.Isolate,
+// This context is a persistent object. The persistent needs to be recovered and reset.
+v8_context: v8.Context,
+handle_scope: ?v8.HandleScope,
+
+// references Env.templates
+templates: []v8.FunctionTemplate,
+
+// references the Env.meta_lookup
+meta_lookup: []types.Meta,
+
+// An arena for the lifetime of a call-group. Gets reset whenever
+// call_depth reaches 0.
+call_arena: Allocator,
+
+// An arena for the lifetime of the context
+context_arena: Allocator,
+
+// Because calls can be nested (i.e.a function calling a callback),
+// we can only reset the call_arena when call_depth == 0. If we were
+// to reset it within a callback, it would invalidate the data of
+// the call which is calling the callback.
+call_depth: usize = 0,
+
+// Callbacks are PesistendObjects. When the context ends, we need
+// to free every callback we created.
+callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .empty,
+
+// Serves two purposes. Like `callbacks` above, this is used to free
+// every PeristentObjet we've created during the lifetime of the context.
+// More importantly, it serves as an identity map - for a given Zig
+// instance, we map it to the same PersistentObject.
+// The key is the @intFromPtr of the Zig value
+identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty,
+
+// Some web APIs have to manage opaque values. Ideally, they use an
+// js.Object, but the js.Object has no lifetime guarantee beyond the
+// current call. They can call .persist() on their js.Object to get
+// a `*PersistentObject()`. We need to track these to free them.
+// This used to be a map and acted like identity_map; the key was
+// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without
+// a reliable way to know if an object has already been persisted,
+// we now simply persist every time persist() is called.
+js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
+
+// Various web APIs depend on having a persistent promise resolver. They
+// require for this PromiseResolver to be valid for a lifetime longer than
+// the function that resolves/rejects them.
+persisted_promise_resolvers: std.ArrayListUnmanaged(v8.Persistent(v8.PromiseResolver)) = .empty,
+
+// Some Zig types have code to execute to cleanup
+destructor_callbacks: std.ArrayListUnmanaged(DestructorCallback) = .empty,
+
+// Our module cache: normalized module specifier => module.
+module_cache: std.StringHashMapUnmanaged(ModuleEntry) = .empty,
+
+// Module => Path. The key is the module hashcode (module.getIdentityHash)
+// and the value is the full path to the module. We need to capture this
+// so that when we're asked to resolve a dependent module, and all we're
+// given is the specifier, we can form the full path. The full path is
+// necessary to lookup/store the dependent module in the module_cache.
+module_identifier: std.AutoHashMapUnmanaged(u32, []const u8) = .empty,
+
+// the page's script manager
+script_manager: ?*ScriptManager,
+
+// Global callback is called on missing property.
+global_callback: ?js.GlobalMissingCallback = null,
+
+const ModuleEntry = struct {
+ // Can be null if we're asynchrously loading the module, in
+ // which case resolver_promise cannot be null.
+ module: ?PersistentModule = null,
+
+ // The promise of the evaluating module. The resolved value is
+ // meaningless to us, but the resolver promise needs to chain
+ // to this, since we need to know when it's complete.
+ module_promise: ?PersistentPromise = null,
+
+ // The promise for the resolver which is loading the module.
+ // (AKA, the first time we try to load it). This resolver will
+ // chain to the module_promise and, when it's done evaluating
+ // will resolve its namespace. Any other attempt to load the
+ // module willchain to this.
+ resolver_promise: ?PersistentPromise = null,
+};
+
+// no init, started with executor.createContext()
+
+pub fn deinit(self: *Context) void {
+ {
+ // reverse order, as this has more chance of respecting any
+ // dependencies objects might have with each other.
+ const items = self.destructor_callbacks.items;
+ var i = items.len;
+ while (i > 0) {
+ i -= 1;
+ items[i].destructor();
+ }
+ }
+
+ {
+ var it = self.identity_map.valueIterator();
+ while (it.next()) |p| {
+ p.deinit();
+ }
+ }
+
+ for (self.js_object_list.items) |*p| {
+ p.deinit();
+ }
+
+ for (self.persisted_promise_resolvers.items) |*p| {
+ p.deinit();
+ }
+
+ {
+ var it = self.module_cache.valueIterator();
+ while (it.next()) |entry| {
+ if (entry.module) |*mod| {
+ mod.deinit();
+ }
+ if (entry.module_promise) |*p| {
+ p.deinit();
+ }
+ if (entry.resolver_promise) |*p| {
+ p.deinit();
+ }
+ }
+ }
+
+ for (self.callbacks.items) |*cb| {
+ cb.deinit();
+ }
+ if (self.handle_scope) |*scope| {
+ scope.deinit();
+ self.v8_context.exit();
+ }
+ var presistent_context = v8.Persistent(v8.Context).recoverCast(self.v8_context);
+ presistent_context.deinit();
+}
+
+fn trackCallback(self: *Context, pf: PersistentFunction) !void {
+ return self.callbacks.append(self.context_arena, pf);
+}
+
+// Given an anytype, turns it into a v8.Object. The anytype could be:
+// 1 - A V8.object already
+// 2 - Our js.Object wrapper around a V8.Object
+// 3 - A zig instance that has previously been given to V8
+// (i.e., the value has to be known to the executor)
+pub fn valueToExistingObject(self: *const Context, value: anytype) !v8.Object {
+ if (@TypeOf(value) == v8.Object) {
+ return value;
+ }
+
+ if (@TypeOf(value) == js.Object) {
+ return value.js_obj;
+ }
+
+ const persistent_object = self.identity_map.get(@intFromPtr(value)) orelse {
+ return error.InvalidThisForCallback;
+ };
+
+ return persistent_object.castToObject();
+}
+
+pub fn stackTrace(self: *const Context) !?[]const u8 {
+ return stackForLogs(self.call_arena, self.isolate);
+}
+
+// Executes the src
+pub fn eval(self: *Context, src: []const u8, name: ?[]const u8) !void {
+ _ = try self.exec(src, name);
+}
+
+pub fn exec(self: *Context, src: []const u8, name: ?[]const u8) !js.Value {
+ const v8_context = self.v8_context;
+
+ const scr = try compileScript(self.isolate, v8_context, src, name);
+
+ const value = scr.run(v8_context) catch {
+ return error.ExecutionError;
+ };
+
+ return self.createValue(value);
+}
+
+pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) {
+ if (cacheable) {
+ if (self.module_cache.get(url)) |entry| {
+ // The dynamic import will create an entry without the
+ // module to prevent multiple calls from asynchronously
+ // loading the same module. If we're here, without the
+ // module, then it's time to load it.
+ if (entry.module != null) {
+ return if (comptime want_result) entry else {};
+ }
+ }
+ }
+ errdefer _ = self.module_cache.remove(url);
+
+ const m = try compileModule(self.isolate, src, url);
+
+ const arena = self.context_arena;
+ const owned_url = try arena.dupe(u8, url);
+
+ try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
+ errdefer _ = self.module_identifier.remove(m.getIdentityHash());
+
+ const v8_context = self.v8_context;
+ {
+ // Non-async modules are blocking. We can download them in
+ // parallel, but they need to be processed serially. So we
+ // want to get the list of dependent modules this module has
+ // and start downloading them asap.
+ const requests = m.getModuleRequests();
+ const isolate = self.isolate;
+ for (0..requests.length()) |i| {
+ const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
+ const specifier = try js.stringToZig(self.call_arena, req.getSpecifier(), isolate);
+ const normalized_specifier = try @import("../../url.zig").stitch(
+ self.call_arena,
+ specifier,
+ owned_url,
+ .{ .alloc = .if_needed, .null_terminated = true },
+ );
+ const gop = try self.module_cache.getOrPut(self.context_arena, normalized_specifier);
+ if (!gop.found_existing) {
+ const owned_specifier = try self.context_arena.dupeZ(u8, normalized_specifier);
+ gop.key_ptr.* = owned_specifier;
+ gop.value_ptr.* = .{};
+ try self.script_manager.?.getModule(owned_specifier);
+ }
+ }
+ }
+
+ if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
+ return error.ModuleInstantiationError;
+ }
+
+ const evaluated = try m.evaluate(v8_context);
+ // https://v8.github.io/api/head/classv8_1_1Module.html#a1f1758265a4082595757c3251bb40e0f
+ // Must be a promise that gets returned here.
+ std.debug.assert(evaluated.isPromise());
+
+ if (comptime !want_result) {
+ // avoid creating a bunch of persisted objects if it isn't
+ // cacheable and the caller doesn't care about results.
+ // This is pretty common, i.e. every
-
+ -->