From dab8012b6a093582fc5333234632ad9056e26b13 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 2 Oct 2025 12:46:49 +0800 Subject: [PATCH] Start extract JS structs into their own files Renames JsContext -> js.Context, JsObject -> js.Object and JsThis -> js.This which is more consistent with the other types. The JsObject -> js.Object is the reason so many files were touched. This is still a [messy] transition, with more refactoring planned to clean it up. --- src/app.zig | 2 +- src/browser/State.zig | 2 +- src/browser/console/console.zig | 16 +- src/browser/crypto/crypto.zig | 2 +- src/browser/dom/Animation.zig | 14 +- src/browser/dom/MessageChannel.zig | 26 +- src/browser/dom/document.zig | 4 +- src/browser/dom/element.zig | 2 +- src/browser/dom/node_iterator.zig | 2 +- src/browser/dom/nodelist.zig | 3 +- src/browser/dom/performance.zig | 6 +- src/browser/dom/shadow_root.zig | 6 +- src/browser/dom/token_list.zig | 2 +- src/browser/dom/tree_walker.zig | 2 +- src/browser/events/custom_event.zig | 8 +- src/browser/events/event.zig | 2 +- src/browser/fetch/Headers.zig | 4 +- src/browser/fetch/Response.zig | 2 +- src/browser/html/History.zig | 4 +- src/browser/html/error_event.zig | 6 +- src/browser/html/window.zig | 12 +- src/browser/js/Caller.zig | 560 +++ src/browser/js/Context.zig | 1731 ++++++++ src/browser/js/Env.zig | 549 +++ src/browser/js/ExecutionWorld.zig | 259 ++ src/browser/js/Function.zig | 147 + src/browser/js/Inspector.zig | 153 + src/browser/js/Object.zig | 156 + src/browser/js/Platform.zig | 21 + src/browser/js/This.zig | 29 + src/browser/js/TryCatch.zig | 65 + src/browser/js/js.zig | 3949 +---------------- src/browser/js/subtype.zig | 20 - src/browser/js/types.zig | 183 + src/browser/page.zig | 4 +- src/browser/polyfill/polyfill.zig | 6 +- src/browser/session.zig | 2 +- src/browser/streams/ReadableStream.zig | 2 +- .../ReadableStreamDefaultController.zig | 2 +- src/browser/url/url.zig | 2 +- src/cdp/cdp.zig | 12 +- src/cdp/domains/dom.zig | 2 +- src/cdp/domains/page.zig | 4 +- src/tests/streams/readable_stream.html | 4 +- 44 files changed, 4027 insertions(+), 3962 deletions(-) create mode 100644 src/browser/js/Caller.zig create mode 100644 src/browser/js/Context.zig create mode 100644 src/browser/js/Env.zig create mode 100644 src/browser/js/ExecutionWorld.zig create mode 100644 src/browser/js/Function.zig create mode 100644 src/browser/js/Inspector.zig create mode 100644 src/browser/js/Object.zig create mode 100644 src/browser/js/Platform.zig create mode 100644 src/browser/js/This.zig create mode 100644 src/browser/js/TryCatch.zig delete mode 100644 src/browser/js/subtype.zig create mode 100644 src/browser/js/types.zig 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 - -->