Commit Graph

38 Commits

Author SHA1 Message Date
Karl Seguin
ea422075c7 Remove unused imports
And some smaller cleanups.
2026-03-27 12:45:26 +08:00
Karl Seguin
f70865e174 Take 2.
History: We started with 1 context and thus only had 1 identity map. Frames
were added, and we tried to stick with 1 identity map per context. That didn't
work - it breaks cross-frame scripting. We introduced "Origin" so that all
frames on the same origin share the same objects. That almost worked, by
the v8::Inspector isn't bound by a Context's SecurityToken. So we tried 1 global
identity map. But that doesn't work. CDP IsolateWorlds do, in fact, need some
isolation. They need new v8::Objects created in their context, even if the
object already exists in the main context.

In the end, you end up with something like this: A page (and all its frames)
needs 1 view of the data. And each IsolateWorld needs it own view. This commit
introduces a js.Identity which is referenced by the context. The Session has a
js.Identity (used by all pages), and each IsolateWorld has its own js.Identity.

As a bonus, the arena pool memory-leak detection has been moved out of the
session and into the ArenaPool. This means _all_ arena pool access is audited
(in debug mode). This seems superfluous, but it's actually necessary since
IsolateWorlds (which now own their own identity) can outlive the Page so there's
no clear place to "check" for leaks - except on ArenaPool deinit.
2026-03-19 18:46:35 +08:00
Karl Seguin
d9c5f56500 Remove Origins
js.Origin was added to allow frames on the same origin to share our zig<->js
maps / identity. It assumes that scripts on different origins will never be
allowed (by v8) to access the same zig instances.

If two different origins DID access the same zig instance, we'd have a few
different problems. First, while the mapping would exist in Origin1's
identity_map, when the zig instance was returned to a script in Origin2, it
would not be found in Origin2's identity_map, and thus create a new v8::Object.
Thus we'd end up with 2 v8::Objects for the same Zig instance. This is
potentially not the end of the world, but not great either as any zig-native
data _would_ be shared (it's the same instance after all), but js-native data
wouldn't.

The real problem this introduces though is with Finalizers. A weak reference
that falls out of scope in Origin1 will get cleaned up, even though it's still
referenced from Origin2.

Now, under normal circumstances, this isn't an issue; v8 _does_ ensure that
cross-origin access isn't allowed (because we set a SecurityToken on the
v8::Context). But it seems like the v8::Inspector isn't bound by these
restrictions and can happily access and share objects across origin.

The simplest solution I can come up with is to move the mapping from the Origin
to the Session. This does mean that objects might live longer than they have to.
When all references to an origin go out of scope, we can do some cleanup. Not
so when the Session owns this data. But really, how often are iframes on
different origins being created and deleted within the lifetime of a page?

When Origins were first introduces, the Session got burdened with having to
manage multiple lifecycles:
1 - The page-surviving data (e.g. history)
2 - The root page lifecycle (e.g. page_arena, queuedNavigation)
3 - The origin lookup

This commit doesn't change that, but it makes the session responsible for
_a lot_ more of the root page lifecycle (#2 above).

I lied. js.Origin still exists, but it's a shell of its former self. It only
exists to store the SecurityToken name that is re-used for every context with
the same origin.

The v8 namespace leaks into Session.

MutationObserver and IntersectionObserver are now back to using weak/strong refs
which was one of the failing cases before this change.
2026-03-19 14:54:10 +08:00
Karl Seguin
2a103fc94a Use Session as a container for cross-frame resources
The introduction of frames means that data is no longer tied to a specific Page
or Context. 255b9a91cc introduced Origins for
v8 values shared across frames of the same origin. The commit highlighted the
lifetime mismatched that we now have with data that can outlive 1 frame. A
specific issue with that commit was the finalizers were still Context-owned.
But like any other piece of data, that isn't right; aside from modules, nothing
should be context-owned.

This commit continues where the last left off and moves finalizers from Context
to Origin. This is done in a separate commit because it introduces significant
changes. Currently, finalizers take a *Page, but that's no longer correct. A
value created in one Page, can outlive the Page. We need another container. I
original thought to use Origin, but that isn't know to CDP/MCP. Instead, I
decide to enhance the Session.

Session is now the owner of the page.arena, the page.factory and the
page.arena_pool. Finalizers are given a *Session which they can use to release
their arena.
2026-03-11 08:44:49 +08:00
Karl Seguin
94ce5edd20 Frames on the same origin share v8 data
Depends on: https://github.com/lightpanda-io/zig-v8-fork/pull/153

In some ways this is an extension of
https://github.com/lightpanda-io/browser/pull/1635 but it has more implications
with respect to correctness.

A js.Context wraps a v8::Context. One of the important thing it adds is the
identity_map so that, given a Zig instance we always return the same v8::Object.

But imagine code running in a frame. This frame has its own Context, and thus
its own identity_map. What happens when that frame does:

```js
window.top.frame_loaded = true;
```

From Zig's point of view, `Window.getTop` will return the correct Zig instance.
It will return the *Window references by the "root" page. When that instance is
passed to the bridge, we'll look for the v8::Object in the Context's
`identity_map` but wont' find it. The mapping exists in the root context
`identity_map`, but not within this frame. So we create a new v8::Object and now
our 1 zig instance has N v8::Objects for every page/frame that tries to access
it.

This breaks cross-frame scripting which should work, at least to some degree,
even when frames are on the same origin.

This commit adds a `js.Origin` which contains the `identity_map`, along with our
other `v8::Global` storage. The `Env` now contains a `*js.Origin` lookup,
mapping an origin string (e.g. lightpanda.io:443) to an *Origin. When a Page's
URL is changed, we call `self.js.setOrigin(new_url)` which will then either get
or create an origin from the Env's origin lookup map.

js.Origin is reference counted so that it remains valid so long as at least 1
frame references them.

There's some special handling for null-origins (i.e. about:blank). At the root,
null origins get a distinct/isolated Origin. For a frame, the parent's origin
is used.

Above, we talked about `identity_map`, but a `js.Context` has 8 other fields
to track v8 values, e.g. `global_objects`, `global_functions`,
`global_values_temp`, etc. These all must be shared by frames on the same
origin. So all of these have also been moved to js.Origin. They've also been
merged so that we now have 3 fields: `identity_map`, `globals` and `temps`.

Finally, when the origin of a context is changed, we set the v8::Context's
SecurityToken (to that origin). This is a key part of how v8 allows cross-
context access.
2026-03-11 08:43:40 +08:00
Karl Seguin
9c7ecf221e Improve frame sub-navigation
This makes frame sub-navigation "work" for all page navigations (click, form
submit, location.top...) as well as setting the iframe.src.

Fixes at least 2 WPT crashes.

BUT, the implementation still isn't 100% correct, with two known issues:

1. Navigation currently happens in the context where it's called, not the
   context of the frame. So if Page1 accesses Frame1 and causes it to navigate,
   e.g. f1.contentDocument.querySelector('#link').click(), it's Page1 that will
   be navigated, since the JS is being executed in the Page1 context.
   This should be relatively easy to fix.

2. There are particularly complicated cases in WPT where a frame is navigated
   inside of its own load, creating an endless loop. There's some partial
   support for this as-is, but it doesn't work correctly and it currently is
   defensive and likely will not continue to navigate. This is particularly true
   when sub-navigation is done to about:blank within the frame's on load event.
   (Which is probably not a real concern, but an issue for some WPT tests)

Although it shares a lot with the original navigation code, there are many more
edge cases here, possibly due to being developed along side WPT tests. The
source of most of the complexity is the synchronous handling of "about:blank"
in page.navigate, which can result in a scheduled navigation synchronously
causing more scheduled navigation. (Specifically because
`self.documentIsComplete();` is called from page.navigate in that case). It
might be worth seeing if something can be done about that, to simplify this new
code (removing the double queue, removing the flag, simplifying pre-existing
schedule checks ,...)
2026-03-05 15:09:39 +08:00
Karl Seguin
e2645e4126 Fix 3 WPT cases for NodeIterator
- Prevent recursive filters
- Rethrow on filter exception
- Add no-op (as per spec) `detach`
2026-02-09 18:19:36 +08:00
Karl Seguin
a19a125aec Remove unused import
And a few unused functions
2026-01-29 19:44:10 +08:00
Pierre Tachoire
ae298fc2e6 use caught formatter and init caught into _tryCallWithThis 2026-01-28 11:27:05 +01:00
Pierre Tachoire
fc5496e570 always log try/catch error on call function
We force log of detailled error caught during function call.
2026-01-27 18:41:12 +01:00
Karl Seguin
8f5f6212d2 Always use TryCatch when calling a function
And always ensure caught is initialized.
2026-01-26 20:06:47 +08:00
Karl Seguin
c63c85071a Start to eagerly reset globals.
Currently, when you create a Global (Value, Object, Function, ...) it exists
until the context is destroyed.

This PR adds the ability to eagerly free them when they fall out of scope, which
is only possible because of the new finalizer hooks.

Previously, we had js.Value, js.Value.Global; js.Function, js.Function.Global,
etc. This PR introduces a .Temp variant: js.Value.Temp and js.Function.Temp.
This is purely a discriminatory type and it behaves (and IS) a Global. The
difference is that it can be released:

  page.js.release(self._on_ready_state_change.?)

Why a new type? There's no guarantee that a global (the existing .Global or the
new .Temp) will get released before the context ends. For this reason, we always
track them in order to free the on context deninit:

```zig
    for (self.global_functions.items) |*global| {
        v8.v8__Global__Reset(global);
    }
```

If a .Temp is eagerly released, we need to remove it from this list. The simple
solution would be to switch `global_functions` from an ArrayList to a HashMap.
But that adds overhead for values that we know we'll never be able to eagerly
release. For this reason, .Temp are stored in a hashmap (and can be released)
and .Globla are stored in an ArrayList (and cannot be released). It's a micro-
optimization...eagerly releasing doesn't have to O(N) scan the list, and we only
pay the memory overhead of the hashmap for values that have a change to be
eagerly freed.

Eager-freeing is now applied to both the callbacn and the values for window
timers (setTimeout, setInterval, RAF). And to the XHR ready_state_change
callback. (we'll do more as we go).
2026-01-26 07:39:05 +08:00
Karl Seguin
62aa564df1 Remove Global v8::Local<V8::Context>
When we create a js.Context, we create the underlying v8.Context and store it
for the duration of the page lifetime. This works because we have a global
HandleScope - the v8.Context (which is really a v8::Local<v8::Context>) is that
to the global HandleScope, effectively making it a global.

If we want to remove our global HandleScope, then we can no longer pin the
v8.Context in our js.Context. Our js.Context now only holds a v8.Global of the
v8.Context (v8::Global<v8::Context).

This PR introduces a new type, js.Local, which takes over a lot of the
functionality previously found in either js.Caller or js.Context. The simplest
way to think about it is:

1 - For v8 -> zig calls, we create a js.Caller (as always)
2 - For zig -> v8 calls, we go through the js.Context (as always)
3 - The shared functionality, which works on a v8.Context, now belongs to js.Local

For #1 (v8 -> zig), creating a js.Local for a js.Caller is really simple and
centralized. v8 largely gives us everything we need from the
FunctionCallbackInfo or PropertyCallbackInfo.  For #2, it's messier, because we
can only create a local v8::Context if we have a HandleScope, which we may or
may not.

Unfortunately, in many cases, what to do becomes the responsibility of the caller
and much of the code has to become aware of this local-ness. What does it means
for our code? The impact is on WebAPIs that store .Global. Because the global
can't do anything. You always need to convert that .Global to a local
(e.g. js.Function.Global -> js.Function).

If you're 100% sure the WebAPI is only being invoked by a v8 callback, you can
use `page.js.local.?.toLocal(some_global).call(...)` to get the local value.

If you're 100% sure the WebAPI is only being invoked by Zig, you need to create
 `js.Local.Scope` to get access to a local:

```zig
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
ls.toLocal(some_global).call(...)
// can also access `&ls.local` for APIs that require a *const js.Local
```
For functions that can be invoked by either V8 or Zig, you should generally push
the responsibility to the caller by accepting a `local: *const js.Local`. If the
caller is a v8 callback, it can pass `page.js.local.?`. If the caller is a Zig
callback, it can create a `Local.Scope`.

As an alternative, it is possible to simply pass the *Page, and check
`if page.js.local == null` and, if so, create a Local.Scope. But this should only
be done for performance reasons. We currently only do this in 1 place, and it's
because the Zig caller doesn't know whether a Local will actually be needed and
it's potentially called on every element creating from the parser.
2026-01-19 07:28:33 +08:00
Karl Seguin
5def997bed Make Global Function explicit.
This is the first in a series of changes to make globals explicit. The ultimate
goal of having explicit Globals is to move away from the global HandleScope and
to explicit HandleScopes.

Currently, we treat globals and locals interchangeably. In fact, for Global ->
Local, we just ptrCast. This works because we have 1 global HandleScope, which
effectively disables V8's GC and thus nothing ever gets moved.

If we're going to introduce explicit HandleScopes, then we need to first have
correct Globals. Specifically, when we want to act on the global, we need to
get the local value, and that will eventually mean making sure there's a
HandleScope.

While adding explicit globals, we're keeping the global HandleScope so that we
can minimize the change. So, given that we still have the global HandleScope
the change is largely two things:
1 - js.Function.persit() returns a js.Function.Global. Types that persist global
   functions must be updated to js.Function.Global.
2 - To turn js.Function.Global -> js.Function, we need to call .local() on it.

The bridge has been updated to support js.Function.Global for both input and
output parameters. Thus, window.setOnLoad can now directly take a
js.Function.Global, and window.getOnLoad can directly return that
js.Function.Global.
2026-01-19 07:15:48 +08:00
Karl Seguin
8e14dacc32 Improve ergonomics of try catch (and Function's tryCall)
It now returns a Caught struct which contains all information. The Caught struct
can be logged directly, providing more consistent logs for caught errors.
2026-01-14 17:34:02 +08:00
Karl Seguin
1b0ea44519 merge main 2026-01-13 12:58:31 +08:00
Karl Seguin
f2a9125b99 js.v8 is not equal to js.v8.c
This means the C funtions/types now sit in the root of v8.
2026-01-13 12:58:30 +08:00
Karl Seguin
18c846757b migrate almost all types 2026-01-13 12:58:28 +08:00
Karl Seguin
bc11a48e6b migrate most cases, merge Caller into bridge 2026-01-13 12:57:06 +08:00
Karl Seguin
701de08e8a have our js.Context directly hold a js handle 2026-01-13 12:57:06 +08:00
Karl Seguin
363b95bdef Isolate and HandleScope 2026-01-13 12:57:06 +08:00
Karl Seguin
ca5a385b51 Port js.Object
Use js.Value in apis that should take values (not objects), like console.log
and setTimeout and reportError.
2026-01-13 12:57:03 +08:00
Karl Seguin
6ecf52cc03 port Platform and Inspector to use v8's C handles/functions directly 2026-01-13 12:56:07 +08:00
Karl Seguin
8aaef674fe Migrate Function and String 2026-01-13 12:56:07 +08:00
Karl Seguin
dab6345885 dispatch events with proper this 2026-01-07 17:57:34 +08:00
Karl Seguin
645ec79fce access page from context, document call_depth usage 2026-01-06 18:04:17 +08:00
Muki Kiboigo
05da040ce1 increment call_depth on callWithThis 2026-01-05 09:27:17 -08:00
Karl Seguin
087086c308 remove some unused imports 2025-12-26 12:40:20 +08:00
Karl Seguin
9f3cb4349d more legacy test fixes 2025-12-25 09:08:01 +08:00
Karl Seguin
e807c9b6be Add XmlSerializer, add Response.type, tweak HTMLTemplate to redirect some calls to its Content (DocumentFragment) 2025-12-02 00:08:57 +08:00
Karl Seguin
f25b8fc7b0 Event.composedPath and adjusted target when crossing shadowroot boundary 2025-11-27 16:57:33 +08:00
Karl Seguin
67f63a6bb3 improve parsed (i.e. static) custom element callbacks 2025-11-26 19:43:22 +08:00
Karl Seguin
e336c67857 various small api fixes/tweaks 2025-11-24 20:12:43 +08:00
Karl Seguin
bd3da38fc8 add native custom elements 2025-11-19 22:50:52 +08:00
Karl Seguin
b047cb6dc1 remove libdom 2025-10-27 22:14:59 +08:00
Karl Seguin
75e0637d2d Ensure page background tasks are re-registered on reset 2025-10-09 16:29:09 +08:00
Karl Seguin
2e734fae57 This is the last of the big changes to the js code
This Pr largely tightens up a lot of the code. 'v8' is no longer imported
outside of js. A number of helper functions have been moved to the js.Context.
For example, js.Function.getName used to call:

```zig
return js.valueToString(allocator, name, self.context.isolate, self.context.v8_context);
```

It now calls:

```zig
return self.context.valueToString(name, .{ .allocator = allocator });
```

Page.main_context has been renamed to `Page.js`. This, in combination with new
promise helpers, turns:

```zig
const resolver = page.main_context.createPromiseResolver();
try resolver.resolve({});
return resolver.promise();
```

into:

```zig
return page.js.resolvePromise({});
```
2025-10-03 15:06:16 +08:00
Karl Seguin
dab8012b6a 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.
2025-10-02 12:48:50 +08:00