Compare commits

...

50 Commits

Author SHA1 Message Date
Pierre Tachoire
47afdc003a Merge pull request #2044 from lightpanda-io/proxy-auth-challenge
Some checks are pending
e2e-test / zig build release (push) Waiting to run
e2e-test / demo-scripts (push) Blocked by required conditions
e2e-test / wba-demo-scripts (push) Blocked by required conditions
e2e-test / wba-test (push) Blocked by required conditions
e2e-test / cdp-and-hyperfine-bench (push) Blocked by required conditions
e2e-test / perf-fmt (push) Blocked by required conditions
e2e-test / browser fetch (push) Blocked by required conditions
zig-test / zig fmt (push) Waiting to run
zig-test / zig test using v8 in debug mode (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
http: add connect code into auth challenge detection
2026-03-30 17:18:56 +02:00
Pierre Tachoire
7606528b37 ci: fix request interception proxy challenge 2026-03-30 15:32:38 +02:00
Pierre Tachoire
9ca6bf42ae http: add connect headers to auth challenge detection 2026-03-30 15:17:12 +02:00
Pierre Tachoire
a272a2c314 http: add connect code into auth challenge detection 2026-03-30 15:08:36 +02:00
katie-lpd
cafa16d190 Update README.md
Updating benchmark text to fit the image
2026-03-30 13:38:46 +02:00
Pierre Tachoire
a5ed3cdaee Merge pull request #2042 from lightpanda-io/security.md
add SECURITY.md
2026-03-30 11:30:42 +02:00
Pierre Tachoire
b473f0e681 add SECURITY.md 2026-03-30 11:28:46 +02:00
Karl Seguin
be7226fc7b Merge pull request #2024 from lightpanda-io/finalizers
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Rework finalizers
2026-03-30 17:03:08 +08:00
Karl Seguin
5cd356631c unregister before releasingRef to avoid potential use-after-free 2026-03-30 15:42:08 +08:00
Pierre Tachoire
8723ecdd2d Merge pull request #2028 from lightpanda-io/http_client_safe_kill
Protect transfer.kill() the way transfer.abort() is protected
2026-03-30 09:22:40 +02:00
Pierre Tachoire
451178558a Merge pull request #2026 from lightpanda-io/invalid_access_dom_exception
Add missing InvalidAccessError DOMException mapping
2026-03-30 09:21:51 +02:00
Karl Seguin
70dc0f6b95 Merge pull request #2027 from lightpanda-io/mcp-protocol-version
mcp: allow configuring protocol version
2026-03-30 13:38:06 +08:00
Adrià Arrufat
d99599fa21 zig fmt 2026-03-30 07:24:08 +02:00
Adrià Arrufat
20e62a5551 mcp: inline mcpVersion helper from Config 2026-03-30 07:13:45 +02:00
Adrià Arrufat
e083d4a3d1 Config: remove LIGHTPANDA_MCP_VERSION env var 2026-03-30 07:07:23 +02:00
Karl Seguin
7a23686cbd Merge pull request #2033 from lightpanda-io/canvas_context_cache
Canvas context cache
2026-03-30 12:27:04 +08:00
Karl Seguin
25889ff918 Improve canvas context caching
Improve https://github.com/lightpanda-io/browser/pull/2022 to also cache webgl
context and add tests.
2026-03-30 12:14:32 +08:00
Karl Seguin
b4e3f246ca Merge remote-tracking branch 'evan108108/fix/canvas-getcontext-caching' into canvas_context_cache 2026-03-30 11:58:45 +08:00
Karl Seguin
8eeeeda8c1 Merge pull request #2021 from evan108108/fix/navigator-spec-compliance
fix: navigator.languages should include base language per spec
2026-03-30 11:40:49 +08:00
Karl Seguin
75dc4d5b0e Merge pull request #2031 from lightpanda-io/cdp-add-script-to-evaluate-on-new-document
Cdp add script to evaluate on new document
2026-03-30 11:16:39 +08:00
Karl Seguin
0d40aed1b7 zig fmt 2026-03-30 09:32:22 +08:00
Karl Seguin
78cb766298 Log for unimplemented parameter
Wrap script_on_new_document execution in try/catch for better error reporting.

Improve test for script_on_new_document
2026-03-30 09:31:13 +08:00
Karl Seguin
f60e5cce6d Protect transfer.kill() the way transfer.abort() is protected
Transfer.abort() is protected from aborting the transfer while inside of a
libcurl callback (since libcurl doesn't support mutating the easy while inside
of a callback AND it causes issues in the zig code).

This applies similar logic to Transfer.kill() which is less likely to be called
but worse if it is called in a callback, as transfer.kill() deinit's the
transfer - something the callback caller is not expecting. Since killing isn't
safe to do, we flag the transfer as aborted AND null/noop all the callbacks.

Fixes WPT crash /content-security-policy/frame-src/frame-src-blocked-path-matching.sub.html
2026-03-29 19:48:47 +08:00
Adrià Arrufat
81d4bdb157 mcp: change default protocol version to 2024-11-05 2026-03-29 08:34:24 +02:00
Adrià Arrufat
cf5e4d7d1e mcp: allow configuring protocol version
Closes #2023
2026-03-29 08:29:04 +02:00
Karl Seguin
9f81d7d3ff Add missing InvalidAccessError DOMException mapping
Fixes WPT crash /WebCryptoAPI/sign_verify/eddsa_curve25519.https.any.html
2026-03-29 11:46:44 +08:00
Karl Seguin
269924090a fix double free 2026-03-29 07:12:18 +08:00
Karl Seguin
ad54437ca3 zig fmt 2026-03-28 21:43:46 +08:00
Karl Seguin
01ecb296e5 Rework finalizers
This commit involves a number of changes to finalizers, all aimed towards
better consistency and reliability.

A big part of this has to do with v8::Inspector's ability to move objects
across IsolatedWorlds. There has been a few previous efforts on this, the most
significant being https://github.com/lightpanda-io/browser/pull/1901. To recap,
a Zig instance can map to 0-N v8::Objects. Where N is the total number of
IsolatedWorlds. Generally, IsolatedWorlds between origins are...isolated...but
the v8::Inspector isn't bound by this. So a Zig instance cannot be tied to a
Context/Identity/IsolatedWorld...it has to live until all references, possibly
from different IsolatedWorlds, are released (or the page is reset).

Finalizers could previously be managed via reference counting or explicitly
toggling the instance as weak/strong. Now, only reference counting is supported.
weak/strong can essentially be seen as an acquireRef (rc += 1) and
releaseRef (rc -= 1). Explicit setting did make some things easier, like not
having to worry so much about double-releasing (e.g. XHR abort being called
multiple times), but it was only used in a few places AND it simply doesn't work
with objects shared between IsolatedWorlds. It is never a boolean now, as 3
different IsolatedWorlds can each hold a reference.

Temps and Globals are tracked on the Session. Previously, they were tracked on
the Identity, but that makes no sense. If a Zig instance can outlive an Identity,
then any of its Temp references can too. This hasn't been a problem because we've
only seen MutationObserver and IntersectionObserver be used cross-origin,
but the right CDP script can make this crash with a use-after-free (e.g.
`MessageEvent.data` is released when the Identity is done, but `MessageEvent` is
still referenced by a different IsolateWorld).

Rather than deinit with a `comptime shutdown: bool`, there is now an explicit
`releaseRef` and `deinit`.

Bridge registration has been streamlined. Previously, types had to register
their finalizer AND acquireRef/releaseRef/deinit had to be declared on the entire
prototype chain, even if these methods just delegated to their proto. Finalizers
are now automatically enabled if a type has a `acquireRef` function. If a type
has an `acquireRef`, then it must have a `releaseRef` and a `deinit`. So if
there's custom cleanup to do in `deinit`, then you also have to define
`acquireRef` and `releaseRef` which will just delegate to the _proto.

Furthermore these finalizer methods can be defined anywhere on the chain.

Previously:

```zig
const KeywboardEvent = struct {
  _proto: *Event,
  ...

  pub fn deinit(self: *KeyboardEvent, session: *Session) void {
    self._proto.deinit(session);
  }

  pub fn releaseRef(self: *KeyboardEvent, session: *Session) void {
    self._proto.releaseRef(session);
  }
}
```

```zig
const KeyboardEvent = struct {
  _proto: *Event,
  ...
  // no deinit, releaseRef, acquireref
}
```

Since the `KeyboardEvent` doesn't participate in finalization directly, it
doesn't have to define anything. The bridge will detect the most specific place
they are defined and call them there.
2026-03-28 21:11:23 +08:00
evan108108
1f22462f13 fix: cache canvas 2D context and lock context type per spec
Per the HTML spec, HTMLCanvasElement.getContext() should:
1. Return the same object on repeated calls with the same type
2. Return null if a different context type was already requested

Previously, every getContext("2d") call created a new
CanvasRenderingContext2D object. This caused issues with code
that relies on identity checks (ctx === canvas.getContext("2d"))
and wasted memory by allocating duplicate contexts.

The fix caches the 2D context and tracks which context type was
first requested, returning null for incompatible subsequent calls.
2026-03-27 21:06:09 -04:00
evan108108
273ea91378 fix: navigator.languages should include base language per spec
Per the HTML spec, navigator.languages should return the user's
preferred languages. Most browsers return at least ["en-US", "en"]
to include the base language tag alongside the regional variant.

This matches Chrome, Firefox, and Safari behavior and improves
compatibility with sites that check for language negotiation.
2026-03-27 21:04:55 -04:00
Pierre Tachoire
03ed45637a Merge pull request #1889 from lightpanda-io/wp/mrdimidium/refactor-redirects
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / zig build release (push) Has been cancelled
wpt / build wpt runner (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
Rework header/data callbacks in HttpClient
2026-03-27 14:22:58 +01:00
Nikolay Govorov
9068fe718e Fix SameSite cookies 2026-03-27 11:16:46 +00:00
Nikolay Govorov
5369d25213 fix recv e2e test 2026-03-27 09:49:16 +00:00
Nikolay Govorov
649d8d1024 Remove duplication in cookies instalation 2026-03-27 09:49:13 +00:00
Nikolay Govorov
15d60d845a Fixup error handling in HttpClient process messages 2026-03-27 09:49:11 +00:00
Nikolay Govorov
c4b837b598 Revert log reimport 2026-03-27 09:49:09 +00:00
Nikolay Govorov
54391238c9 Move cdp callbacks from dataCallback to processMessages 2026-03-27 09:49:07 +00:00
Nikolay Govorov
d33edc5697 Fixup cookies management 2026-03-27 09:49:05 +00:00
Nikolay Govorov
16ca8d4b14 Fix cleanup connections in HttpClient 2026-03-27 09:49:03 +00:00
Nikolay Govorov
707ffb4893 Move redirects handling from curl callbacks 2026-03-27 09:48:59 +00:00
Pierre Tachoire
4782b37216 Merge pull request #2016 from lightpanda-io/readme-mention-cors
mention CORS is missing in the README's status
2026-03-27 08:34:09 +01:00
Pierre Tachoire
ce197256dd Merge pull request #2010 from lightpanda-io/build-pre-nightly
build: simplify nightly versioning
2026-03-27 08:33:45 +01:00
Pierre Tachoire
e6d644998a mention CORS is missing in the README's status 2026-03-27 08:26:56 +01:00
Karl Seguin
67bd555e75 Merge pull request #2013 from lightpanda-io/cleanup_dead_code_removal
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Remove unused imports
2026-03-27 13:52:49 +08:00
Adrià Arrufat
a10e533701 Remove more unused imports 2026-03-27 14:24:17 +09:00
Karl Seguin
226d9bfc6f zig fmt 2026-03-27 12:47:24 +08:00
Karl Seguin
ea422075c7 Remove unused imports
And some smaller cleanups.
2026-03-27 12:45:26 +08:00
Adrià Arrufat
7f2139f612 build: simplify nightly versioning 2026-03-27 10:47:43 +09:00
Navid EMAD
886aa3abba CDP: implement Page.addScriptToEvaluateOnNewDocument
Replace the hardcoded stub with a working implementation that stores
registered scripts and evaluates them in each new document.

Changes:
- Add ScriptOnNewDocument struct and storage list on BrowserContext
- Store scripts with unique identifiers when addScript is called
- Evaluate all registered scripts in pageNavigated, after the execution
  context is created but before frameNavigated/loadEventFired events
  are sent to the CDP client
- Add removeScriptToEvaluateOnNewDocument for cleanup
- Return unique identifiers per the CDP spec (was hardcoded to "1")

Scripts are evaluated with error suppression (warns on failure) to
avoid breaking navigation if a script has issues.

This unblocks CDP clients that rely on auto-injected scripts (polyfills,
monitoring, test helpers) persisting across navigations. Previously
clients had to manually re-inject after every Page.navigate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:48:07 +01:00
107 changed files with 1506 additions and 1345 deletions

View File

@@ -100,14 +100,14 @@ jobs:
./proxy/proxy & echo $! > PROXY.id ./proxy/proxy & echo $! > PROXY.id
./lightpanda serve --http-proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid ./lightpanda serve --http-proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
go run runner/main.go go run runner/main.go
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
kill `cat LPD.pid` `cat PROXY.id` kill `cat LPD.pid` `cat PROXY.id`
- name: run request interception through proxy - name: run request interception through proxy and playwright
run: | run: |
export PROXY_USERNAME=username PROXY_PASSWORD=password export PROXY_USERNAME=username PROXY_PASSWORD=password
./proxy/proxy & echo $! > PROXY.id ./proxy/proxy & echo $! > PROXY.id
./lightpanda serve & echo $! > LPD.pid ./lightpanda serve & echo $! > LPD.pid
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
kill `cat LPD.pid` `cat PROXY.id` kill `cat LPD.pid` `cat PROXY.id`
@@ -161,14 +161,18 @@ jobs:
--http-proxy 'http://127.0.0.1:3000' \ --http-proxy 'http://127.0.0.1:3000' \
& echo $! > LPD.pid & echo $! > LPD.pid
go run runner/main.go go run runner/main.go
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
kill `cat LPD.pid` `cat PROXY.id` kill `cat LPD.pid` `cat PROXY.id`
- name: run request interception through proxy - name: run request interception through proxy and playwright
run: | run: |
export PROXY_USERNAME=username PROXY_PASSWORD=password export PROXY_USERNAME=username PROXY_PASSWORD=password
./proxy/proxy & echo $! > PROXY.id ./proxy/proxy & echo $! > PROXY.id
./lightpanda serve & echo $! > LPD.pid ./lightpanda serve \
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js --web-bot-auth-key-file private_key.pem \
--web-bot-auth-keyid ${{ vars.WBA_KEY_ID }} \
--web-bot-auth-domain ${{ vars.WBA_DOMAIN }} \
& echo $! > LPD.pid
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
kill `cat LPD.pid` `cat PROXY.id` kill `cat LPD.pid` `cat PROXY.id`

View File

@@ -7,7 +7,7 @@ env:
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }} AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }} RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dversion_string={0}', github.ref_name) || format('-Dpre_version={0}', 'nightly') }} VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dversion={0}', github.ref_name) || '-Dversion=nightly' }}
on: on:
push: push:

View File

@@ -36,8 +36,8 @@ Lightpanda is the open-source browser made for headless usage:
Fast web automation for AI agents, LLM training, scraping and testing: Fast web automation for AI agents, LLM training, scraping and testing:
- Ultra-low memory footprint (9x less than Chrome) - Ultra-low memory footprint (16x less than Chrome)
- Exceptionally fast execution (11x faster than Chrome) - Exceptionally fast execution (9x faster than Chrome)
- Instant startup - Instant startup
[^1]: **Playwright support disclaimer:** [^1]: **Playwright support disclaimer:**
@@ -170,6 +170,7 @@ You may still encounter errors or crashes. Please open an issue with specifics i
Here are the key features we have implemented: Here are the key features we have implemented:
- [ ] CORS [#2015](https://github.com/lightpanda-io/browser/issues/2015)
- [x] HTTP loader ([Libcurl](https://curl.se/libcurl/)) - [x] HTTP loader ([Libcurl](https://curl.se/libcurl/))
- [x] HTML parser ([html5ever](https://github.com/servo/html5ever)) - [x] HTML parser ([html5ever](https://github.com/servo/html5ever))
- [x] DOM tree - [x] DOM tree

11
SECURITY.md Normal file
View File

@@ -0,0 +1,11 @@
# Reporting security issues
## Supported Versions
Security fixes are applied to the latest `main` branch.
## Reporting a Vulnerability
Please **DO NOT** file a public issue, instead send your report privately to security@lightpanda.io.
Security reports are greatly appreciated and we will publicly thank you for it, although we keep your name confidential if you request it.

View File

@@ -719,39 +719,45 @@ fn buildCurl(
return lib; return lib;
} }
/// Returns `MAJOR.MINOR.PATCH-dev` when `git describe` fails. /// Resolves the semantic version of the build.
///
/// The base version is read from `build.zig.zon`. This can be overridden
/// using the `-Dversion` command-line flag:
/// - If the flag contains a full semantic version (e.g., `1.2.3`), it replaces
/// the base version entirely.
/// - If the flag contains a simple string (e.g., `nightly`), it replaces only
/// the pre-release tag of the base version (e.g., `1.0.0-dev` -> `1.0.0-nightly`).
///
/// For versions that have a pre-release tag and no explicit build metadata,
/// this function automatically enriches the version with the git commit count
/// and short hash (e.g., `1.0.0-dev.5243+dbe45229`).
fn resolveVersion(b: *std.Build) std.SemanticVersion { fn resolveVersion(b: *std.Build) std.SemanticVersion {
const version_string = b.option([]const u8, "version_string", "Override the version of this build"); const opt_version = b.option([]const u8, "version", "Override the version of this build");
if (version_string) |semver_string| {
return std.SemanticVersion.parse(semver_string) catch |err| {
std.debug.panic("Expected -Dversion-string={s} to be a semantic version: {}", .{ semver_string, err });
};
}
const pre_version = b.option([]const u8, "pre_version", "Override the pre version of this build"); const version = if (opt_version) |v|
const pre = blk: { std.SemanticVersion.parse(v) catch blk: {
if (pre_version) |pre| { var fallback = lightpanda_version;
break :blk pre; fallback.pre = v;
break :blk fallback;
} }
else
lightpanda_version;
break :blk lightpanda_version.pre; // Only enrich versions that have a pre-release field and no explicit build metadata.
}; if (version.pre == null or version.build != null) return version;
// If it's a stable release (no pre or build metadata in build.zig.zon), use it as is
if (pre == null and lightpanda_version.build == null) return lightpanda_version;
// For dev/nightly versions, calculate the commit count and hash // For dev/nightly versions, calculate the commit count and hash
const git_hash_raw = runGit(b, &.{ "rev-parse", "--short", "HEAD" }) catch return lightpanda_version; const git_hash_raw = runGit(b, &.{ "rev-parse", "--short", "HEAD" }) catch return version;
const commit_hash = std.mem.trim(u8, git_hash_raw, " \n\r"); const commit_hash = std.mem.trim(u8, git_hash_raw, " \n\r");
const git_count_raw = runGit(b, &.{ "rev-list", "--count", "HEAD" }) catch return lightpanda_version; const git_count_raw = runGit(b, &.{ "rev-list", "--count", "HEAD" }) catch return version;
const commit_count = std.mem.trim(u8, git_count_raw, " \n\r"); const commit_count = std.mem.trim(u8, git_count_raw, " \n\r");
return .{ return .{
.major = lightpanda_version.major, .major = version.major,
.minor = lightpanda_version.minor, .minor = version.minor,
.patch = lightpanda_version.patch, .patch = version.patch,
.pre = b.fmt("{s}.{s}", .{ pre.?, commit_count }), .pre = b.fmt("{s}.{s}", .{ version.pre.?, commit_count }),
.build = commit_hash, .build = commit_hash,
}; };
} }

View File

@@ -18,6 +18,7 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const log = @import("log.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
@@ -62,7 +63,7 @@ pub fn deinit(self: *ArenaPool) void {
var it = self._leak_track.iterator(); var it = self._leak_track.iterator();
while (it.next()) |kv| { while (it.next()) |kv| {
if (kv.value_ptr.* != 0) { if (kv.value_ptr.* != 0) {
std.debug.print("ArenaPool leak detected: '{s}' count={d}\n", .{ kv.key_ptr.*, kv.value_ptr.* }); log.err(.bug, "ArenaPool leak", .{ .name = kv.key_ptr.*, .count = kv.value_ptr.* });
has_leaks = true; has_leaks = true;
} }
} }
@@ -129,11 +130,11 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
if (self._leak_track.getPtr(entry.debug)) |count| { if (self._leak_track.getPtr(entry.debug)) |count| {
count.* -= 1; count.* -= 1;
if (count.* < 0) { if (count.* < 0) {
std.debug.print("ArenaPool double-free detected: '{s}'\n", .{entry.debug}); log.err(.bug, "ArenaPool double-free", .{ .name = entry.debug });
@panic("ArenaPool: double-free detected"); @panic("ArenaPool: double-free detected");
} }
} else { } else {
std.debug.print("ArenaPool release of untracked arena: '{s}'\n", .{entry.debug}); log.err(.bug, "ArenaPool release unknown", .{ .name = entry.debug });
@panic("ArenaPool: release of untracked arena"); @panic("ArenaPool: release of untracked arena");
} }
} }

View File

@@ -24,6 +24,7 @@ const log = @import("log.zig");
const dump = @import("browser/dump.zig"); const dump = @import("browser/dump.zig");
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config; const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;
const mcp = @import("mcp.zig");
pub const RunMode = enum { pub const RunMode = enum {
help, help,
@@ -222,6 +223,7 @@ pub const Serve = struct {
pub const Mcp = struct { pub const Mcp = struct {
common: Common = .{}, common: Common = .{},
version: mcp.Version = .default,
}; };
pub const DumpFormat = enum { pub const DumpFormat = enum {
@@ -453,6 +455,12 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
\\Starts an MCP (Model Context Protocol) server over stdio \\Starts an MCP (Model Context Protocol) server over stdio
\\Example: {s} mcp \\Example: {s} mcp
\\ \\
\\Options:
\\--version
\\ Override the reported MCP version.
\\ Valid: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25.
\\ Defaults to "2024-11-05".
\\
++ common_options ++ ++ common_options ++
\\ \\
\\version command \\version command
@@ -640,10 +648,22 @@ fn parseMcpArgs(
allocator: Allocator, allocator: Allocator,
args: *std.process.ArgIterator, args: *std.process.ArgIterator,
) !Mcp { ) !Mcp {
var mcp: Mcp = .{}; var result: Mcp = .{};
while (args.next()) |opt| { while (args.next()) |opt| {
if (try parseCommonArg(allocator, opt, args, &mcp.common)) { if (std.mem.eql(u8, "--version", opt)) {
const str = args.next() orelse {
log.fatal(.mcp, "missing argument value", .{ .arg = opt });
return error.InvalidArgument;
};
result.version = std.meta.stringToEnum(mcp.Version, str) orelse {
log.fatal(.mcp, "invalid protocol version", .{ .value = str });
return error.InvalidArgument;
};
continue;
}
if (try parseCommonArg(allocator, opt, args, &result.common)) {
continue; continue;
} }
@@ -651,7 +671,7 @@ fn parseMcpArgs(
return error.UnkownOption; return error.UnkownOption;
} }
return mcp; return result;
} }
fn parseFetchArgs( fn parseFetchArgs(

View File

@@ -22,7 +22,6 @@ const net = std.net;
const posix = std.posix; const posix = std.posix;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const log = @import("log.zig"); const log = @import("log.zig");
const App = @import("App.zig"); const App = @import("App.zig");

View File

@@ -19,17 +19,13 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const js = @import("js/js.zig"); const js = @import("js/js.zig");
const log = @import("../log.zig");
const App = @import("../App.zig"); const App = @import("../App.zig");
const HttpClient = @import("HttpClient.zig"); const HttpClient = @import("HttpClient.zig");
const ArenaPool = App.ArenaPool; const ArenaPool = App.ArenaPool;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Session = @import("Session.zig"); const Session = @import("Session.zig");
const Notification = @import("../Notification.zig"); const Notification = @import("../Notification.zig");

View File

@@ -205,7 +205,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void { pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {
event.acquireRef(); event.acquireRef();
defer event.deinit(false, self.page._session); defer _ = event.releaseRef(self.page._session);
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles }); log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
@@ -240,7 +240,7 @@ pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event,
defer window._current_event = prev_event; defer window._current_event = prev_event;
event.acquireRef(); event.acquireRef();
defer event.deinit(false, page._session); defer _ = event.releaseRef(page._session);
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context }); log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context });
@@ -425,7 +425,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
ls.deinit(); ls.deinit();
} }
const activation_state = ActivationState.create(event, target, page); const activation_state = try ActivationState.create(event, target, page);
// Defer runs even on early return - ensures event phase is reset // Defer runs even on early return - ensures event phase is reset
// and default actions execute (unless prevented) // and default actions execute (unless prevented)
@@ -820,7 +820,7 @@ const ActivationState = struct {
const Input = Element.Html.Input; const Input = Element.Html.Input;
fn create(event: *const Event, target: *Node, page: *Page) ?ActivationState { fn create(event: *const Event, target: *Node, page: *Page) !?ActivationState {
if (event._type_string.eql(comptime .wrap("click")) == false) { if (event._type_string.eql(comptime .wrap("click")) == false) {
return null; return null;
} }

View File

@@ -239,7 +239,7 @@ fn eventInit(arena: Allocator, typ: String, value: anytype) !Event {
const time_stamp = (raw_timestamp / 2) * 2; const time_stamp = (raw_timestamp / 2) * 2;
return .{ return .{
._rc = 0, ._rc = .{},
._arena = arena, ._arena = arena,
._type = unionInit(Event.Type, value), ._type = unionInit(Event.Type, value),
._type_string = typ, ._type_string = typ,
@@ -255,6 +255,7 @@ pub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child
const blob_ptr = chain.get(0); const blob_ptr = chain.get(0);
blob_ptr.* = .{ blob_ptr.* = .{
._rc = .{},
._arena = arena, ._arena = arena,
._type = unionInit(Blob.Type, chain.get(1)), ._type = unionInit(Blob.Type, chain.get(1)),
._slice = "", ._slice = "",
@@ -271,7 +272,7 @@ pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page:
const doc = page.document.asNode(); const doc = page.document.asNode();
const abstract_range = chain.get(0); const abstract_range = chain.get(0);
abstract_range.* = AbstractRange{ abstract_range.* = AbstractRange{
._rc = 0, ._rc = .{},
._arena = arena, ._arena = arena,
._page_id = page.id, ._page_id = page.id,
._type = unionInit(AbstractRange.Type, chain.get(1)), ._type = unionInit(AbstractRange.Type, chain.get(1)),

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,6 @@ const IS_DEBUG = builtin.mode == .Debug;
const log = @import("../log.zig"); const log = @import("../log.zig");
const App = @import("../App.zig");
const String = @import("../string.zig").String; const String = @import("../string.zig").String;
const Mime = @import("Mime.zig"); const Mime = @import("Mime.zig");
@@ -43,7 +42,6 @@ const URL = @import("URL.zig");
const Blob = @import("webapi/Blob.zig"); const Blob = @import("webapi/Blob.zig");
const Node = @import("webapi/Node.zig"); const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig"); const Event = @import("webapi/Event.zig");
const EventTarget = @import("webapi/EventTarget.zig");
const CData = @import("webapi/CData.zig"); const CData = @import("webapi/CData.zig");
const Element = @import("webapi/Element.zig"); const Element = @import("webapi/Element.zig");
const HtmlElement = @import("webapi/element/Html.zig"); const HtmlElement = @import("webapi/element/Html.zig");
@@ -59,7 +57,6 @@ const AbstractRange = @import("webapi/AbstractRange.zig");
const MutationObserver = @import("webapi/MutationObserver.zig"); const MutationObserver = @import("webapi/MutationObserver.zig");
const IntersectionObserver = @import("webapi/IntersectionObserver.zig"); const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig"); const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
const storage = @import("webapi/storage/storage.zig");
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig"); const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
const SubmitEvent = @import("webapi/event/SubmitEvent.zig"); const SubmitEvent = @import("webapi/event/SubmitEvent.zig");
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind; const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
@@ -67,7 +64,6 @@ const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
const MouseEvent = @import("webapi/event/MouseEvent.zig"); const MouseEvent = @import("webapi/event/MouseEvent.zig");
const HttpClient = @import("HttpClient.zig"); const HttpClient = @import("HttpClient.zig");
const ArenaPool = App.ArenaPool;
const timestamp = @import("../datetime.zig").timestamp; const timestamp = @import("../datetime.zig").timestamp;
const milliTimestamp = @import("../datetime.zig").milliTimestamp; const milliTimestamp = @import("../datetime.zig").milliTimestamp;
@@ -385,12 +381,9 @@ pub fn getTitle(self: *Page) !?[]const u8 {
return null; return null;
} }
// Add comon headers for a request: // Add common headers for a request:
// * cookies
// * referer // * referer
pub fn headersForRequest(self: *Page, temp: Allocator, url: [:0]const u8, headers: *HttpClient.Headers) !void { pub fn headersForRequest(self: *Page, headers: *HttpClient.Headers) !void {
try self.requestCookie(.{}).headersForRequest(temp, url, headers);
// Build the referer // Build the referer
const referer = blk: { const referer = blk: {
if (self.referer_header == null) { if (self.referer_header == null) {
@@ -545,8 +538,6 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
if (opts.header) |hdr| { if (opts.header) |hdr| {
try headers.add(hdr); try headers.add(hdr);
} }
try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, self.url, &headers);
// We dispatch page_navigate event before sending the request. // We dispatch page_navigate event before sending the request.
// It ensures the event page_navigated is not dispatched before this one. // It ensures the event page_navigated is not dispatched before this one.
session.notification.dispatch(.page_navigate, &.{ session.notification.dispatch(.page_navigate, &.{
@@ -573,6 +564,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
.headers = headers, .headers = headers,
.body = opts.body, .body = opts.body,
.cookie_jar = &session.cookie_jar, .cookie_jar = &session.cookie_jar,
.cookie_origin = self.url,
.resource_type = .document, .resource_type = .document,
.notification = self._session.notification, .notification = self._session.notification,
.header_callback = pageHeaderDoneCallback, .header_callback = pageHeaderDoneCallback,
@@ -1036,6 +1028,7 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
}); });
parser.parse(html); parser.parse(html);
self._parse_state = .complete;
self.documentIsComplete(); self.documentIsComplete();
}, },
else => unreachable, else => unreachable,
@@ -3399,7 +3392,7 @@ pub fn handleClick(self: *Page, target: *Node) !void {
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void { pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
const event = keyboard_event.asEvent(); const event = keyboard_event.asEvent();
const element = self.window._document._active_element orelse { const element = self.window._document._active_element orelse {
keyboard_event.deinit(false, self._session); _ = event.releaseRef(self._session);
return; return;
}; };
@@ -3495,7 +3488,7 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
// so submit_event is still valid when we check _prevent_default // so submit_event is still valid when we check _prevent_default
submit_event.acquireRef(); submit_event.acquireRef();
defer submit_event.deinit(false, self._session); defer _ = submit_event.releaseRef(self._session);
try self._event_manager.dispatch(form_element.asEventTarget(), submit_event); try self._event_manager.dispatch(form_element.asEventTarget(), submit_event);
// If the submit event was prevented, don't submit the form // If the submit event was prevented, don't submit the form
@@ -3554,19 +3547,6 @@ pub fn insertText(self: *Page, v: []const u8) !void {
} }
} }
const RequestCookieOpts = struct {
is_http: bool = true,
is_navigation: bool = false,
};
pub fn requestCookie(self: *const Page, opts: RequestCookieOpts) HttpClient.RequestCookie {
return .{
.jar = &self._session.cookie_jar,
.origin = self.url,
.is_http = opts.is_http,
.is_navigation = opts.is_navigation,
};
}
fn asUint(comptime string: anytype) std.meta.Int( fn asUint(comptime string: anytype) std.meta.Int(
.unsigned, .unsigned,
@bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0 @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0

View File

@@ -21,12 +21,9 @@ const lp = @import("lightpanda");
const builtin = @import("builtin"); const builtin = @import("builtin");
const log = @import("../log.zig"); const log = @import("../log.zig");
const App = @import("../App.zig");
const Page = @import("Page.zig"); const Page = @import("Page.zig");
const Session = @import("Session.zig"); const Session = @import("Session.zig");
const Browser = @import("Browser.zig");
const Factory = @import("Factory.zig");
const HttpClient = @import("HttpClient.zig"); const HttpClient = @import("HttpClient.zig");
const IS_DEBUG = builtin.mode == .Debug; const IS_DEBUG = builtin.mode == .Debug;

View File

@@ -28,12 +28,10 @@ const String = @import("../string.zig").String;
const js = @import("js/js.zig"); const js = @import("js/js.zig");
const URL = @import("URL.zig"); const URL = @import("URL.zig");
const Page = @import("Page.zig"); const Page = @import("Page.zig");
const Browser = @import("Browser.zig");
const Element = @import("webapi/Element.zig"); const Element = @import("webapi/Element.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const IS_DEBUG = builtin.mode == .Debug; const IS_DEBUG = builtin.mode == .Debug;
@@ -138,9 +136,9 @@ fn clearList(list: *std.DoublyLinkedList) void {
} }
} }
fn getHeaders(self: *ScriptManager, arena: Allocator, url: [:0]const u8) !net_http.Headers { fn getHeaders(self: *ScriptManager) !net_http.Headers {
var headers = try self.client.newHeaders(); var headers = try self.client.newHeaders();
try self.page.headersForRequest(arena, url, &headers); try self.page.headersForRequest(&headers);
return headers; return headers;
} }
@@ -280,9 +278,10 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
.ctx = script, .ctx = script,
.method = .GET, .method = .GET,
.frame_id = page._frame_id, .frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url), .headers = try self.getHeaders(),
.blocking = is_blocking, .blocking = is_blocking,
.cookie_jar = &page._session.cookie_jar, .cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.resource_type = .script, .resource_type = .script,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null, .start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
@@ -405,8 +404,9 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
.ctx = script, .ctx = script,
.method = .GET, .method = .GET,
.frame_id = page._frame_id, .frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url), .headers = try self.getHeaders(),
.cookie_jar = &page._session.cookie_jar, .cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.resource_type = .script, .resource_type = .script,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null, .start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
@@ -508,10 +508,11 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
.url = url, .url = url,
.method = .GET, .method = .GET,
.frame_id = page._frame_id, .frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url), .headers = try self.getHeaders(),
.ctx = script, .ctx = script,
.resource_type = .script, .resource_type = .script,
.cookie_jar = &page._session.cookie_jar, .cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null, .start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
.header_callback = Script.headerCallback, .header_callback = Script.headerCallback,
@@ -654,7 +655,6 @@ pub const Script = struct {
debug_transfer_aborted: bool = false, debug_transfer_aborted: bool = false,
debug_transfer_bytes_received: usize = 0, debug_transfer_bytes_received: usize = 0,
debug_transfer_notified_fail: bool = false, debug_transfer_notified_fail: bool = false,
debug_transfer_redirecting: bool = false,
debug_transfer_intercept_state: u8 = 0, debug_transfer_intercept_state: u8 = 0,
debug_transfer_auth_challenge: bool = false, debug_transfer_auth_challenge: bool = false,
debug_transfer_easy_id: usize = 0, debug_transfer_easy_id: usize = 0,
@@ -730,7 +730,6 @@ pub const Script = struct {
.a3 = self.debug_transfer_aborted, .a3 = self.debug_transfer_aborted,
.a4 = self.debug_transfer_bytes_received, .a4 = self.debug_transfer_bytes_received,
.a5 = self.debug_transfer_notified_fail, .a5 = self.debug_transfer_notified_fail,
.a6 = self.debug_transfer_redirecting,
.a7 = self.debug_transfer_intercept_state, .a7 = self.debug_transfer_intercept_state,
.a8 = self.debug_transfer_auth_challenge, .a8 = self.debug_transfer_auth_challenge,
.a9 = self.debug_transfer_easy_id, .a9 = self.debug_transfer_easy_id,
@@ -739,10 +738,9 @@ pub const Script = struct {
.b3 = transfer.aborted, .b3 = transfer.aborted,
.b4 = transfer.bytes_received, .b4 = transfer.bytes_received,
.b5 = transfer._notified_fail, .b5 = transfer._notified_fail,
.b6 = transfer._redirecting,
.b7 = @intFromEnum(transfer._intercept_state), .b7 = @intFromEnum(transfer._intercept_state),
.b8 = transfer._auth_challenge != null, .b8 = transfer._auth_challenge != null,
.b9 = if (transfer._conn) |c| @intFromPtr(c.easy) else 0, .b9 = if (transfer._conn) |c| @intFromPtr(c._easy) else 0,
}); });
self.header_callback_called = true; self.header_callback_called = true;
self.debug_transfer_id = transfer.id; self.debug_transfer_id = transfer.id;
@@ -750,10 +748,9 @@ pub const Script = struct {
self.debug_transfer_aborted = transfer.aborted; self.debug_transfer_aborted = transfer.aborted;
self.debug_transfer_bytes_received = transfer.bytes_received; self.debug_transfer_bytes_received = transfer.bytes_received;
self.debug_transfer_notified_fail = transfer._notified_fail; self.debug_transfer_notified_fail = transfer._notified_fail;
self.debug_transfer_redirecting = transfer._redirecting;
self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state); self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state);
self.debug_transfer_auth_challenge = transfer._auth_challenge != null; self.debug_transfer_auth_challenge = transfer._auth_challenge != null;
self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c.easy) else 0; self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c._easy) else 0;
} }
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity }); lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });

View File

@@ -71,6 +71,18 @@ origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
// ensuring object identity works across same-origin frames. // ensuring object identity works across same-origin frames.
identity: js.Identity = .{}, identity: js.Identity = .{},
// Shared finalizer callbacks across all Identities. Keyed by Zig instance ptr.
// This ensures objects are only freed when ALL v8 wrappers are gone.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
// Tracked global v8 objects that need to be released on cleanup.
// Lives at Session level so objects can outlive individual Identities.
globals: std.ArrayList(v8.Global) = .empty,
// Temporary v8 globals that can be released early. Key is global.data_ptr.
// Lives at Session level so objects holding Temps can outlive individual Identities.
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Shared resources for all pages in this session. // Shared resources for all pages in this session.
// These live for the duration of the page tree (root + frames). // These live for the duration of the page tree (root + frames).
arena_pool: *ArenaPool, arena_pool: *ArenaPool,
@@ -224,6 +236,30 @@ pub fn releaseOrigin(self: *Session, origin: *js.Origin) void {
/// Reset page_arena and factory for a clean slate. /// Reset page_arena and factory for a clean slate.
/// Called when root page is removed. /// Called when root page is removed.
fn resetPageResources(self: *Session) void { fn resetPageResources(self: *Session) void {
// Force cleanup all remaining finalized objects
{
var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |fc| {
fc.*.deinit(self);
}
self.finalizer_callbacks = .empty;
}
{
for (self.globals.items) |*global| {
v8.v8__Global__Reset(global);
}
self.globals = .empty;
}
{
var it = self.temps.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
self.temps = .empty;
}
self.identity.deinit(); self.identity.deinit();
self.identity = .{}; self.identity = .{};
@@ -457,35 +493,25 @@ pub fn nextPageId(self: *Session) u32 {
return id; return id;
} }
// A type that has a finalizer can have its finalizer called one of two ways. // Every finalizable instance of Zig gets 1 FinalizerCallback registered in the
// The first is from V8 via the WeakCallback we give to weakRef. But that isn't // session. This is to ensure that, if v8 doesn't finalize the value, we can
// guaranteed to fire, so we track this in finalizer_callbacks and call them on // release on page reset.
// page reset.
pub const FinalizerCallback = struct { pub const FinalizerCallback = struct {
arena: Allocator, arena: Allocator,
session: *Session, session: *Session,
ptr: *anyopaque, resolved_ptr_id: usize,
global: v8.Global, finalizer_ptr_id: usize,
identity: *js.Identity, _deinit: *const fn (ptr_id: usize, session: *Session) void,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
pub fn deinit(self: *FinalizerCallback) void { // For every FinalizerCallback we'll have 1+ FinalizerCallback.Identity: one
self.zig_finalizer(self.ptr, self.session); // for every identity that gets the instance. In most cases, that'l be 1.
self.session.releaseArena(self.arena); pub const Identity = struct {
} identity: *js.Identity,
fc: *Session.FinalizerCallback,
/// Release this item from the identity tracking maps (called after finalizer runs from V8) };
pub fn releaseIdentity(self: *FinalizerCallback) void {
const session = self.session;
const id = @intFromPtr(self.ptr);
if (self.identity.identity_map.fetchRemove(id)) |kv| {
var global = kv.value;
v8.v8__Global__Reset(&global);
}
_ = self.identity.finalizer_callbacks.remove(id);
fn deinit(self: *FinalizerCallback, session: *Session) void {
self._deinit(self.finalizer_ptr_id, session);
session.releaseArena(self.arena); session.releaseArena(self.arena);
} }
}; };

View File

@@ -128,7 +128,7 @@ fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void
const new_this_handle = info.getThis(); const new_this_handle = info.getThis();
var this = js.Object{ .local = local, .handle = new_this_handle }; var this = js.Object{ .local = local, .handle = new_this_handle };
if (@typeInfo(ReturnType) == .error_union) { if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err; const non_error_res = try res;
this = try local.mapZigInstanceToJs(new_this_handle, non_error_res); this = try local.mapZigInstanceToJs(new_this_handle, non_error_res);
} else { } else {
this = try local.mapZigInstanceToJs(new_this_handle, res); this = try local.mapZigInstanceToJs(new_this_handle, res);

View File

@@ -21,8 +21,8 @@ const lp = @import("lightpanda");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("js.zig"); const js = @import("js.zig");
const Env = @import("Env.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
const Env = @import("Env.zig");
const Origin = @import("Origin.zig"); const Origin = @import("Origin.zig");
const Scheduler = @import("Scheduler.zig"); const Scheduler = @import("Scheduler.zig");
@@ -214,48 +214,11 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
} }
pub fn trackGlobal(self: *Context, global: v8.Global) !void { pub fn trackGlobal(self: *Context, global: v8.Global) !void {
return self.identity.globals.append(self.identity_arena, global); return self.session.globals.append(self.session.page_arena, global);
} }
pub fn trackTemp(self: *Context, global: v8.Global) !void { pub fn trackTemp(self: *Context, global: v8.Global) !void {
return self.identity.temps.put(self.identity_arena, global.data_ptr, global); return self.session.temps.put(self.session.page_arena, global.data_ptr, global);
}
pub fn weakRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj);
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
}
pub fn safeWeakRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj);
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__ClearWeak(&fc.global);
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
}
pub fn strongRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj);
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__ClearWeak(&fc.global);
} }
pub const IdentityResult = struct { pub const IdentityResult = struct {
@@ -271,35 +234,6 @@ pub fn addIdentity(self: *Context, ptr: usize) !IdentityResult {
}; };
} }
pub fn releaseTemp(self: *Context, global: v8.Global) void {
if (self.identity.temps.fetchRemove(global.data_ptr)) |kv| {
var g = kv.value;
v8.v8__Global__Reset(&g);
}
}
pub fn createFinalizerCallback(
self: *Context,
global: v8.Global,
ptr: *anyopaque,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
) !*Session.FinalizerCallback {
const session = self.session;
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
errdefer session.releaseArena(arena);
const fc = try arena.create(Session.FinalizerCallback);
fc.* = .{
.arena = arena,
.session = session,
.ptr = ptr,
.global = global,
.zig_finalizer = zig_finalizer,
// Store identity pointer for cleanup when V8 GCs the object
.identity = self.identity,
};
return fc;
}
// Any operation on the context have to be made from a local. // Any operation on the context have to be made from a local.
pub fn localScope(self: *Context, ls: *js.Local.Scope) void { pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
const isolate = self.isolate; const isolate = self.isolate;

View File

@@ -26,7 +26,6 @@ const App = @import("../../App.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
const Origin = @import("Origin.zig");
const Context = @import("Context.zig"); const Context = @import("Context.zig");
const Isolate = @import("Isolate.zig"); const Isolate = @import("Isolate.zig");
const Platform = @import("Platform.zig"); const Platform = @import("Platform.zig");
@@ -34,7 +33,6 @@ const Snapshot = @import("Snapshot.zig");
const Inspector = @import("Inspector.zig"); const Inspector = @import("Inspector.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const Window = @import("../webapi/Window.zig"); const Window = @import("../webapi/Window.zig");
const JsApis = bridge.JsApis; const JsApis = bridge.JsApis;

View File

@@ -21,7 +21,6 @@ const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Session = @import("../Session.zig");
const Function = @This(); const Function = @This();
@@ -214,7 +213,7 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl
return .{ .handle = global, .temps = {} }; return .{ .handle = global, .temps = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .temps = &ctx.identity.temps }; return .{ .handle = global, .temps = &ctx.session.temps };
} }
pub fn tempWithThis(self: *const Function, value: anytype) !Temp { pub fn tempWithThis(self: *const Function, value: anytype) !Temp {

View File

@@ -32,45 +32,15 @@ const js = @import("js.zig");
const Session = @import("../Session.zig"); const Session = @import("../Session.zig");
const v8 = js.v8; const v8 = js.v8;
const Allocator = std.mem.Allocator;
const Identity = @This(); const Identity = @This();
// Maps Zig instance pointers to their v8::Global(Object) wrappers. // Maps Zig instance pointers to their v8::Global(Object) wrappers.
identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Tracked global v8 objects that need to be released on cleanup.
globals: std.ArrayList(v8.Global) = .empty,
// Temporary v8 globals that can be released early. Key is global.data_ptr.
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Finalizer callbacks for weak references. Key is @intFromPtr of the Zig instance.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *Session.FinalizerCallback) = .empty,
pub fn deinit(self: *Identity) void { pub fn deinit(self: *Identity) void {
{ var it = self.identity_map.valueIterator();
var it = self.finalizer_callbacks.valueIterator(); while (it.next()) |global| {
while (it.next()) |finalizer| {
finalizer.*.deinit();
}
}
{
var it = self.identity_map.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
for (self.globals.items) |*global| {
v8.v8__Global__Reset(global); v8.v8__Global__Reset(global);
} }
{
var it = self.temps.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
} }

View File

@@ -17,11 +17,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const string = @import("../../string.zig"); const string = @import("../../string.zig");
const Session = @import("../Session.zig");
const js = @import("js.zig"); const js = @import("js.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
const Caller = @import("Caller.zig"); const Caller = @import("Caller.zig");
@@ -33,7 +33,6 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8; const v8 = js.v8;
const CallOpts = Caller.CallOpts; const CallOpts = Caller.CallOpts;
const Allocator = std.mem.Allocator;
// Where js.Context has a lifetime tied to the page, and holds the // Where js.Context has a lifetime tied to the page, and holds the
// v8::Global<v8::Context>, this has a much shorter lifetime and holds a // v8::Global<v8::Context>, this has a much shorter lifetime and holds a
@@ -215,7 +214,8 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
.pointer => |ptr| { .pointer => |ptr| {
const resolved = resolveValue(value); const resolved = resolveValue(value);
const gop = try ctx.addIdentity(@intFromPtr(resolved.ptr)); const resolved_ptr_id = @intFromPtr(resolved.ptr);
const gop = try ctx.addIdentity(resolved_ptr_id);
if (gop.found_existing) { if (gop.found_existing) {
// we've seen this instance before, return the same object // we've seen this instance before, return the same object
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self); return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);
@@ -264,31 +264,27 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
// dont' use js_obj.persist(), because we don't want to track this in // dont' use js_obj.persist(), because we don't want to track this in
// context.global_objects, we want to track it in context.identity_map. // context.global_objects, we want to track it in context.identity_map.
v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr); v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr);
if (@hasDecl(JsApi.Meta, "finalizer")) { if (resolved.finalizer) |finalizer| {
// It would be great if resolved knew the resolved type, but I const finalizer_ptr_id = finalizer.ptr_id;
// can't figure out how to make that work, since it depends on finalizer.acquireRef(finalizer_ptr_id);
// the [runtime] `value`.
// We need the resolved finalizer, which we have in resolved.
//
// The above if statement would be more clear as:
// if (resolved.finalizer_from_v8) |finalizer| {
// But that's a runtime check.
// Instead, we check if the base has finalizer. The assumption
// here is that if a resolve type has a finalizer, then the base
// should have a finalizer too.
const fc = try ctx.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
{
errdefer fc.deinit();
try ctx.identity.finalizer_callbacks.put(ctx.identity_arena, @intFromPtr(resolved.ptr), fc);
}
conditionallyReference(value); const session = ctx.session;
if (@hasDecl(JsApi.Meta, "weak")) { const finalizer_gop = try session.finalizer_callbacks.getOrPut(session.page_arena, finalizer_ptr_id);
if (comptime IS_DEBUG) { if (finalizer_gop.found_existing == false) {
std.debug.assert(JsApi.Meta.weak == true); // This is the first context (and very likely only one) to
} // see this Zig instance. We need to create the FinalizerCallback
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, fc, resolved.finalizer_from_v8, v8.kParameter); // so that we can cleanup on page reset if v8 doesn't finalize.
errdefer _ = session.finalizer_callbacks.remove(finalizer_ptr_id);
finalizer_gop.value_ptr.* = try self.createFinalizerCallback(resolved_ptr_id, finalizer_ptr_id, finalizer.deinit);
} }
const fc = finalizer_gop.value_ptr.*;
const identity_finalizer = try fc.arena.create(Session.FinalizerCallback.Identity);
identity_finalizer.* = .{
.fc = fc,
.identity = ctx.identity,
};
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, identity_finalizer, finalizer.release, v8.kParameter);
} }
return js_obj; return js_obj;
}, },
@@ -1123,12 +1119,19 @@ fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T {
// This function recursively walks the _type union field (if there is one) to // This function recursively walks the _type union field (if there is one) to
// get the most specific class_id possible. // get the most specific class_id possible.
const Resolved = struct { const Resolved = struct {
weak: bool,
ptr: *anyopaque, ptr: *anyopaque,
class_id: u16, class_id: u16,
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry, prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null, finalizer: ?Finalizer,
finalizer_from_zig: ?*const fn (ptr: *anyopaque, session: *Session) void = null,
const Finalizer = struct {
// Resolved.ptr is the most specific value in a chain (e.g. IFrame, not EventTarget, Node, ...)
// Finalizer.ptr_id is the most specific value in a chain that defines an acquireRef
ptr_id: usize,
deinit: *const fn (ptr_id: usize, session: *Session) void,
acquireRef: *const fn (ptr_id: usize) void,
release: *const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void,
};
}; };
pub fn resolveValue(value: anytype) Resolved { pub fn resolveValue(value: anytype) Resolved {
const T = bridge.Struct(@TypeOf(value)); const T = bridge.Struct(@TypeOf(value));
@@ -1155,27 +1158,85 @@ pub fn resolveValue(value: anytype) Resolved {
unreachable; unreachable;
} }
fn resolveT(comptime T: type, value: *anyopaque) Resolved { fn resolveT(comptime T: type, value: *T) Resolved {
const Meta = T.JsApi.Meta; const Meta = T.JsApi.Meta;
return .{ return .{
.ptr = value, .ptr = value,
.class_id = Meta.class_id, .class_id = Meta.class_id,
.prototype_chain = &Meta.prototype_chain, .prototype_chain = &Meta.prototype_chain,
.weak = if (@hasDecl(Meta, "weak")) Meta.weak else false, .finalizer = blk: {
.finalizer_from_v8 = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_v8 else null, const FT = (comptime findFinalizerType(T)) orelse break :blk null;
.finalizer_from_zig = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_zig else null, const getFinalizerPtr = comptime finalizerPtrGetter(T, FT);
const finalizer_ptr = getFinalizerPtr(value);
const Wrap = struct {
fn deinit(ptr_id: usize, session: *Session) void {
FT.deinit(@ptrFromInt(ptr_id), session);
}
fn acquireRef(ptr_id: usize) void {
FT.acquireRef(@ptrFromInt(ptr_id));
}
fn release(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const identity_finalizer: *Session.FinalizerCallback.Identity = @ptrCast(@alignCast(ptr));
const fc = identity_finalizer.fc;
if (identity_finalizer.identity.identity_map.fetchRemove(fc.resolved_ptr_id)) |kv| {
var global = kv.value;
v8.v8__Global__Reset(&global);
}
FT.releaseRef(@ptrFromInt(fc.finalizer_ptr_id), fc.session);
}
};
break :blk .{
.ptr_id = @intFromPtr(finalizer_ptr),
.deinit = Wrap.deinit,
.acquireRef = Wrap.acquireRef,
.release = Wrap.release,
};
},
}; };
} }
fn conditionallyReference(value: anytype) void { // Start at the "resolved" type (the most specific) and work our way up the
const T = bridge.Struct(@TypeOf(value)); // prototype chain looking for the type that defines acquireRef
if (@hasDecl(T, "acquireRef")) { fn findFinalizerType(comptime T: type) ?type {
value.acquireRef(); const S = bridge.Struct(T);
return; if (@hasDecl(S, "acquireRef")) {
return S;
} }
if (@hasField(T, "_proto")) { if (@hasField(S, "_proto")) {
conditionallyReference(value._proto); const ProtoPtr = std.meta.fieldInfo(S, ._proto).type;
const ProtoChild = @typeInfo(ProtoPtr).pointer.child;
return findFinalizerType(ProtoChild);
} }
return null;
}
// Generate a function that follows the _proto pointer chain to get to the finalizer type
fn finalizerPtrGetter(comptime T: type, comptime FT: type) *const fn (*T) *FT {
const S = bridge.Struct(T);
if (S == FT) {
return struct {
fn get(v: *T) *FT {
return v;
}
}.get;
}
if (@hasField(S, "_proto")) {
const ProtoPtr = std.meta.fieldInfo(S, ._proto).type;
const ProtoChild = @typeInfo(ProtoPtr).pointer.child;
const childGetter = comptime finalizerPtrGetter(ProtoChild, FT);
return struct {
fn get(v: *T) *FT {
return childGetter(v._proto);
}
}.get;
}
@compileError("Cannot find path from " ++ @typeName(T) ++ " to " ++ @typeName(FT));
} }
pub fn stackTrace(self: *const Local) !?[]const u8 { pub fn stackTrace(self: *const Local) !?[]const u8 {
@@ -1383,6 +1444,34 @@ pub fn debugContextId(self: *const Local) i32 {
return v8.v8__Context__DebugContextId(self.handle); return v8.v8__Context__DebugContextId(self.handle);
} }
fn createFinalizerCallback(
self: *const Local,
// Key in identity map
// The most specific value (KeyboardEvent, not Event)
resolved_ptr_id: usize,
// The most specific value where finalizers are defined
// What actually gets acquired / released / deinit
finalizer_ptr_id: usize,
deinit: *const fn (ptr_id: usize, session: *Session) void,
) !*Session.FinalizerCallback {
const session = self.ctx.session;
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
errdefer session.releaseArena(arena);
const fc = try arena.create(Session.FinalizerCallback);
fc.* = .{
.arena = arena,
.session = session,
._deinit = deinit,
.resolved_ptr_id = resolved_ptr_id,
.finalizer_ptr_id = finalizer_ptr_id,
};
return fc;
}
// Encapsulates a Local and a HandleScope. When we're going from V8->Zig // Encapsulates a Local and a HandleScope. When we're going from V8->Zig
// we easily get both a Local and a HandleScope via Caller.init. // we easily get both a Local and a HandleScope via Caller.init.
// But when we're going from Zig -> V8, things are more complicated. // But when we're going from Zig -> V8, things are more complicated.

View File

@@ -20,8 +20,6 @@ const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const Session = @import("../Session.zig");
const Promise = @This(); const Promise = @This();
local: *const js.Local, local: *const js.Local,
@@ -69,7 +67,7 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo
return .{ .handle = global, .temps = {} }; return .{ .handle = global, .temps = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .temps = &ctx.identity.temps }; return .{ .handle = global, .temps = &ctx.session.temps };
} }
pub const Temp = G(.temp); pub const Temp = G(.temp);

View File

@@ -25,7 +25,6 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8; const v8 = js.v8;
const JsApis = bridge.JsApis; const JsApis = bridge.JsApis;
const Allocator = std.mem.Allocator;
const Snapshot = @This(); const Snapshot = @This();

View File

@@ -25,7 +25,6 @@ const v8 = js.v8;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Session = @import("../Session.zig");
const Value = @This(); const Value = @This();
@@ -304,7 +303,7 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa
return .{ .handle = global, .temps = {} }; return .{ .handle = global, .temps = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .temps = &ctx.identity.temps }; return .{ .handle = global, .temps = &ctx.session.temps };
} }
pub fn toZig(self: Value, comptime T: type) !T { pub fn toZig(self: Value, comptime T: type) !T {

View File

@@ -18,15 +18,12 @@
const std = @import("std"); const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const lp = @import("lightpanda");
const log = @import("../../log.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Session = @import("../Session.zig"); const Session = @import("../Session.zig");
const v8 = js.v8; const v8 = js.v8;
const Caller = @import("Caller.zig"); const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
@@ -104,36 +101,21 @@ pub fn Builder(comptime T: type) type {
} }
return entries; return entries;
} }
pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, session: *Session) void) Finalizer {
return .{
.from_zig = struct {
fn wrap(ptr: *anyopaque, session: *Session) void {
func(@ptrCast(@alignCast(ptr)), true, session);
}
}.wrap,
.from_v8 = struct {
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const fc: *Session.FinalizerCallback = @ptrCast(@alignCast(ptr));
const value_ptr = fc.ptr;
if (fc.identity.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
func(@ptrCast(@alignCast(value_ptr)), false, fc.session);
fc.releaseIdentity();
} else {
// A bit weird, but v8 _requires_ that we release it
// If we don't. We'll 100% crash.
v8.v8__Global__Reset(&fc.global);
}
}
}.wrap,
};
}
}; };
} }
fn releaseRef(comptime T: type, ptr_id: usize, session: *Session) void {
if (@hasDecl(T, "releaseRef")) {
T.releaseRef(@ptrFromInt(ptr_id), session);
return;
}
if (@hasField(T, "_proto")) {
releaseRef(Struct(std.meta.fieldInfo(T, ._proto).type), ptr_id, session);
return;
}
@compileError(@typeName(T) ++ " marked with finalizer without an acquireRef in its prototype chain");
}
pub const Constructor = struct { pub const Constructor = struct {
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void, func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
@@ -414,17 +396,6 @@ pub const Property = struct {
} }
}; };
const Finalizer = struct {
// The finalizer wrapper when called from Zig. This is only called on
// Origin.deinit
from_zig: *const fn (ctx: *anyopaque, session: *Session) void,
// The finalizer wrapper when called from V8. This may never be called
// (hence why we fallback to calling in Origin.deinit). If it is called,
// it is only ever called after we SetWeak on the Global.
from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void,
};
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller: Caller = undefined; var caller: Caller = undefined;

View File

@@ -21,7 +21,6 @@ const std = @import("std");
const Page = @import("Page.zig"); const Page = @import("Page.zig");
const URL = @import("URL.zig"); const URL = @import("URL.zig");
const TreeWalker = @import("webapi/TreeWalker.zig"); const TreeWalker = @import("webapi/TreeWalker.zig");
const CData = @import("webapi/CData.zig");
const Element = @import("webapi/Element.zig"); const Element = @import("webapi/Element.zig");
const Node = @import("webapi/Node.zig"); const Node = @import("webapi/Node.zig");
const isAllWhitespace = @import("../string.zig").isAllWhitespace; const isAllWhitespace = @import("../string.zig").isAllWhitespace;

View File

@@ -148,3 +148,13 @@
} }
</script> </script>
<script id=identity>
{
const element = document.createElement('canvas');
const ctx = element.getContext('2d');
testing.expectTrue(ctx === element.getContext('2d'));
testing.expectEqual(null, element.getContext('webgl'));
}
</script>

View File

@@ -85,3 +85,13 @@
loseContext.restoreContext(); loseContext.restoreContext();
} }
</script> </script>
<script id=identity>
{
const element = document.createElement('canvas');
const ctx = element.getContext('webgl');
testing.expectTrue(ctx === element.getContext('webgl'));
testing.expectEqual(null, element.getContext('2d'));
}
</script>

View File

@@ -15,8 +15,9 @@
testing.expectEqual(true, validPlatforms.includes(navigator.platform)); testing.expectEqual(true, validPlatforms.includes(navigator.platform));
testing.expectEqual('en-US', navigator.language); testing.expectEqual('en-US', navigator.language);
testing.expectEqual(true, Array.isArray(navigator.languages)); testing.expectEqual(true, Array.isArray(navigator.languages));
testing.expectEqual(1, navigator.languages.length); testing.expectEqual(2, navigator.languages.length);
testing.expectEqual('en-US', navigator.languages[0]); testing.expectEqual('en-US', navigator.languages[0]);
testing.expectEqual('en', navigator.languages[1]);
testing.expectEqual(true, navigator.onLine); testing.expectEqual(true, navigator.onLine);
testing.expectEqual(true, navigator.cookieEnabled); testing.expectEqual(true, navigator.cookieEnabled);
testing.expectEqual(true, navigator.hardwareConcurrency > 0); testing.expectEqual(true, navigator.hardwareConcurrency > 0);

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Session = @import("../Session.zig"); const Session = @import("../Session.zig");
@@ -31,7 +33,7 @@ const AbstractRange = @This();
pub const _prototype_root = true; pub const _prototype_root = true;
_rc: u8, _rc: lp.RC(u8) = .{},
_type: Type, _type: Type,
_page_id: u32, _page_id: u32,
_arena: Allocator, _arena: Allocator,
@@ -44,24 +46,18 @@ _start_container: *Node,
_range_link: std.DoublyLinkedList.Node = .{}, _range_link: std.DoublyLinkedList.Node = .{},
pub fn acquireRef(self: *AbstractRange) void { pub fn acquireRef(self: *AbstractRange) void {
self._rc += 1; self._rc.acquire();
} }
pub fn deinit(self: *AbstractRange, shutdown: bool, session: *Session) void { pub fn deinit(self: *AbstractRange, session: *Session) void {
_ = shutdown; if (session.findPageById(self._page_id)) |page| {
const rc = self._rc; page._live_ranges.remove(&self._range_link);
if (comptime IS_DEBUG) {
std.debug.assert(rc != 0);
} }
session.releaseArena(self._arena);
}
if (rc == 1) { pub fn releaseRef(self: *AbstractRange, session: *Session) void {
if (session.findPageById(self._page_id)) |page| { self._rc.release(self, session);
page._live_ranges.remove(&self._range_link);
}
session.releaseArena(self._arena);
return;
}
self._rc = rc - 1;
} }
pub const Type = union(enum) { pub const Type = union(enum) {
@@ -338,8 +334,6 @@ pub const JsApi = struct {
pub const name = "AbstractRange"; pub const name = "AbstractRange";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(AbstractRange.deinit);
}; };
pub const startContainer = bridge.accessor(AbstractRange.getStartContainer, null, .{}); pub const startContainer = bridge.accessor(AbstractRange.getStartContainer, null, .{});

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Writer = std.Io.Writer; const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -25,6 +25,7 @@ const Session = @import("../Session.zig");
const Mime = @import("../Mime.zig"); const Mime = @import("../Mime.zig");
const Writer = std.Io.Writer;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
/// https://w3c.github.io/FileAPI/#blob-section /// https://w3c.github.io/FileAPI/#blob-section
@@ -34,6 +35,7 @@ const Blob = @This();
pub const _prototype_root = true; pub const _prototype_root = true;
_type: Type, _type: Type,
_rc: lp.RC(u32),
_arena: Allocator, _arena: Allocator,
@@ -120,6 +122,7 @@ pub fn initWithMimeValidation(
const self = try arena.create(Blob); const self = try arena.create(Blob);
self.* = .{ self.* = .{
._rc = .{},
._arena = arena, ._arena = arena,
._type = .generic, ._type = .generic,
._slice = data, ._slice = data,
@@ -128,11 +131,18 @@ pub fn initWithMimeValidation(
return self; return self;
} }
pub fn deinit(self: *Blob, shutdown: bool, session: *Session) void { pub fn deinit(self: *Blob, session: *Session) void {
_ = shutdown;
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *Blob, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Blob) void {
self._rc.acquire();
}
const largest_vector = @max(std.simd.suggestVectorLength(u8) orelse 1, 8); const largest_vector = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
/// Array of possible vector sizes for the current arch in decrementing order. /// Array of possible vector sizes for the current arch in decrementing order.
/// We may move this to some file for SIMD helpers in the future. /// We may move this to some file for SIMD helpers in the future.
@@ -325,8 +335,6 @@ pub const JsApi = struct {
pub const name = "Blob"; pub const name = "Blob";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Blob.deinit);
}; };
pub const constructor = bridge.constructor(Blob.init, .{}); pub const constructor = bridge.constructor(Blob.init, .{});

View File

@@ -24,7 +24,6 @@ const Page = @import("../Page.zig");
const Node = @import("Node.zig"); const Node = @import("Node.zig");
const Element = @import("Element.zig"); const Element = @import("Element.zig");
const DOMException = @import("DOMException.zig");
const Custom = @import("element/html/Custom.zig"); const Custom = @import("element/html/Custom.zig");
const CustomElementDefinition = @import("CustomElementDefinition.zig"); const CustomElementDefinition = @import("CustomElementDefinition.zig");

View File

@@ -59,6 +59,7 @@ pub fn fromError(err: anyerror) ?DOMException {
error.TimeoutError => .{ ._code = .timeout_error }, error.TimeoutError => .{ ._code = .timeout_error },
error.InvalidNodeType => .{ ._code = .invalid_node_type_error }, error.InvalidNodeType => .{ ._code = .invalid_node_type_error },
error.DataClone => .{ ._code = .data_clone_error }, error.DataClone => .{ ._code = .data_clone_error },
error.InvalidAccessError => .{ ._code = .invalid_access_error },
else => null, else => null,
}; };
} }

View File

@@ -61,7 +61,7 @@ _fonts: ?*FontFaceSet = null,
_write_insertion_point: ?*Node = null, _write_insertion_point: ?*Node = null,
_script_created_parser: ?Parser.Streaming = null, _script_created_parser: ?Parser.Streaming = null,
_adopted_style_sheets: ?js.Object.Global = null, _adopted_style_sheets: ?js.Object.Global = null,
_selection: Selection = .init, _selection: Selection = .{ ._rc = .init(1) },
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter
// Incremented during custom element reactions when parsing. When > 0, // Incremented during custom element reactions when parsing. When > 0,

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -55,7 +57,7 @@ _is_trusted: bool = false,
// - 0: no reference, always a transient state going to either 1 or about to be deinit'd // - 0: no reference, always a transient state going to either 1 or about to be deinit'd
// - 1: either zig or v8 have a reference // - 1: either zig or v8 have a reference
// - 2: both zig and v8 have a reference // - 2: both zig and v8 have a reference
_rc: u8 = 0, _rc: lp.RC(u8) = .{},
pub const EventPhase = enum(u8) { pub const EventPhase = enum(u8) {
none = 0, none = 0,
@@ -139,25 +141,16 @@ pub fn initEvent(
} }
pub fn acquireRef(self: *Event) void { pub fn acquireRef(self: *Event) void {
self._rc += 1; self._rc.acquire();
} }
pub fn deinit(self: *Event, shutdown: bool, session: *Session) void { /// Force cleanup on Session shutdown.
if (shutdown) { pub fn deinit(self: *Event, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
return; }
}
const rc = self._rc; pub fn releaseRef(self: *Event, session: *Session) void {
if (comptime IS_DEBUG) { self._rc.release(self, session);
std.debug.assert(rc != 0);
}
if (rc == 1) {
session.releaseArena(self._arena);
} else {
self._rc = rc - 1;
}
} }
pub fn as(self: *Event, comptime T: type) *T { pub fn as(self: *Event, comptime T: type) *T {
@@ -440,8 +433,6 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Event.deinit);
pub const enumerable = false; pub const enumerable = false;
}; };

View File

@@ -60,7 +60,7 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool {
event._is_trusted = false; event._is_trusted = false;
event.acquireRef(); event.acquireRef();
defer event.deinit(false, page._session); defer _ = event.releaseRef(page._session);
try page._event_manager.dispatch(self, event); try page._event_manager.dispatch(self, event);
return !event._cancelable or !event._prevent_default; return !event._cancelable or !event._prevent_default;
} }

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -26,7 +27,6 @@ const Blob = @import("Blob.zig");
const File = @This(); const File = @This();
/// `File` inherits `Blob`.
_proto: *Blob, _proto: *Blob,
// TODO: Implement File API. // TODO: Implement File API.
@@ -36,10 +36,6 @@ pub fn init(page: *Page) !*File {
return page._factory.blob(arena, File{ ._proto = undefined }); return page._factory.blob(arena, File{ ._proto = undefined });
} }
pub fn deinit(self: *File, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub const JsApi = struct { pub const JsApi = struct {
pub const bridge = js.Bridge(File); pub const bridge = js.Bridge(File);
@@ -47,8 +43,6 @@ pub const JsApi = struct {
pub const name = "File"; pub const name = "File";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(File.deinit);
}; };
pub const constructor = bridge.constructor(File.init, .{}); pub const constructor = bridge.constructor(File.init, .{});

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -31,6 +33,7 @@ const Allocator = std.mem.Allocator;
/// https://developer.mozilla.org/en-US/docs/Web/API/FileReader /// https://developer.mozilla.org/en-US/docs/Web/API/FileReader
const FileReader = @This(); const FileReader = @This();
_rc: lp.RC(u8) = .{},
_page: *Page, _page: *Page,
_proto: *EventTarget, _proto: *EventTarget,
_arena: Allocator, _arena: Allocator,
@@ -70,7 +73,7 @@ pub fn init(page: *Page) !*FileReader {
}); });
} }
pub fn deinit(self: *FileReader, _: bool, session: *Session) void { pub fn deinit(self: *FileReader, session: *Session) void {
if (self._on_abort) |func| func.release(); if (self._on_abort) |func| func.release();
if (self._on_error) |func| func.release(); if (self._on_error) |func| func.release();
if (self._on_load) |func| func.release(); if (self._on_load) |func| func.release();
@@ -81,6 +84,14 @@ pub fn deinit(self: *FileReader, _: bool, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *FileReader, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *FileReader) void {
self._rc.acquire();
}
fn asEventTarget(self: *FileReader) *EventTarget { fn asEventTarget(self: *FileReader) *EventTarget {
return self._proto; return self._proto;
} }
@@ -309,8 +320,6 @@ pub const JsApi = struct {
pub const name = "FileReader"; pub const name = "FileReader";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FileReader.deinit);
}; };
pub const constructor = bridge.constructor(FileReader.init, .{}); pub const constructor = bridge.constructor(FileReader.init, .{});

View File

@@ -18,7 +18,6 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const String = @import("../../string.zig").String;
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Node = @import("Node.zig"); const Node = @import("Node.zig");

View File

@@ -19,10 +19,8 @@
const std = @import("std"); const std = @import("std");
const String = @import("../../string.zig").String; const String = @import("../../string.zig").String;
const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const color = @import("../color.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
/// https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData /// https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData

View File

@@ -16,6 +16,8 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
@@ -39,6 +41,7 @@ pub fn registerTypes() []const type {
const IntersectionObserver = @This(); const IntersectionObserver = @This();
_rc: lp.RC(u8) = .{},
_arena: Allocator, _arena: Allocator,
_callback: js.Function.Temp, _callback: js.Function.Temp,
_observing: std.ArrayList(*Element) = .{}, _observing: std.ArrayList(*Element) = .{},
@@ -108,15 +111,22 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I
return self; return self;
} }
pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void { pub fn deinit(self: *IntersectionObserver, session: *Session) void {
self._callback.release(); self._callback.release();
if ((comptime IS_DEBUG) and !shutdown) { for (self._pending_entries.items) |entry| {
std.debug.assert(self._observing.items.len == 0); entry.deinitIfUnused(session);
} }
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *IntersectionObserver, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *IntersectionObserver) void {
self._rc.acquire();
}
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void { pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
// Check if already observing this target // Check if already observing this target
for (self._observing.items) |elem| { for (self._observing.items) |elem| {
@@ -127,7 +137,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
// Register with page if this is our first observation // Register with page if this is our first observation
if (self._observing.items.len == 0) { if (self._observing.items.len == 0) {
page.js.strongRef(self); self._rc._refs += 1;
try page.registerIntersectionObserver(self); try page.registerIntersectionObserver(self);
} }
@@ -144,17 +154,19 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
} }
pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) void { pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) void {
const original_length = self._observing.items.len;
for (self._observing.items, 0..) |elem, i| { for (self._observing.items, 0..) |elem, i| {
if (elem == target) { if (elem == target) {
_ = self._observing.swapRemove(i); _ = self._observing.swapRemove(i);
_ = self._previous_states.remove(target); _ = self._previous_states.remove(target);
// Remove any pending entries for this target // Remove any pending entries for this target.
// Entries will be cleaned up by V8 GC via the finalizer.
var j: usize = 0; var j: usize = 0;
while (j < self._pending_entries.items.len) { while (j < self._pending_entries.items.len) {
if (self._pending_entries.items[j]._target == target) { if (self._pending_entries.items[j]._target == target) {
const entry = self._pending_entries.swapRemove(j); const entry = self._pending_entries.swapRemove(j);
entry.deinit(false, page._session); entry.deinitIfUnused(page._session);
} else { } else {
j += 1; j += 1;
} }
@@ -163,21 +175,26 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi
} }
} }
if (self._observing.items.len == 0) { if (original_length > 0 and self._observing.items.len == 0) {
page.js.safeWeakRef(self); self._rc._refs -= 1;
} }
} }
pub fn disconnect(self: *IntersectionObserver, page: *Page) void { pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
page.unregisterIntersectionObserver(self);
self._observing.clearRetainingCapacity();
self._previous_states.clearRetainingCapacity();
for (self._pending_entries.items) |entry| { for (self._pending_entries.items) |entry| {
entry.deinit(false, page._session); entry.deinitIfUnused(page._session);
} }
self._pending_entries.clearRetainingCapacity(); self._pending_entries.clearRetainingCapacity();
page.js.safeWeakRef(self); self._previous_states.clearRetainingCapacity();
const observing_count = self._observing.items.len;
self._observing.clearRetainingCapacity();
page.unregisterIntersectionObserver(self);
if (observing_count > 0) {
_ = self.releaseRef(page._session);
}
} }
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry { pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
@@ -268,7 +285,6 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
._bounding_client_rect = try page._factory.create(data.bounding_client_rect), ._bounding_client_rect = try page._factory.create(data.bounding_client_rect),
._intersection_ratio = data.intersection_ratio, ._intersection_ratio = data.intersection_ratio,
}; };
try self._pending_entries.append(self._arena, entry); try self._pending_entries.append(self._arena, entry);
} }
@@ -310,6 +326,7 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
} }
pub const IntersectionObserverEntry = struct { pub const IntersectionObserverEntry = struct {
_rc: lp.RC(u8) = .{},
_arena: Allocator, _arena: Allocator,
_time: f64, _time: f64,
_target: *Element, _target: *Element,
@@ -319,10 +336,25 @@ pub const IntersectionObserverEntry = struct {
_intersection_ratio: f64, _intersection_ratio: f64,
_is_intersecting: bool, _is_intersecting: bool,
pub fn deinit(self: *IntersectionObserverEntry, _: bool, session: *Session) void { pub fn deinit(self: *IntersectionObserverEntry, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
fn deinitIfUnused(self: *IntersectionObserverEntry, session: *Session) void {
if (self._rc._refs == 0) {
// hasn't been handed to JS yet.
self.deinit(session);
}
}
pub fn releaseRef(self: *IntersectionObserverEntry, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *IntersectionObserverEntry) void {
self._rc.acquire();
}
pub fn getTarget(self: *const IntersectionObserverEntry) *Element { pub fn getTarget(self: *const IntersectionObserverEntry) *Element {
return self._target; return self._target;
} }
@@ -358,8 +390,6 @@ pub const IntersectionObserverEntry = struct {
pub const name = "IntersectionObserverEntry"; pub const name = "IntersectionObserverEntry";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(IntersectionObserverEntry.deinit);
}; };
pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{}); pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{});
@@ -379,8 +409,6 @@ pub const JsApi = struct {
pub const name = "IntersectionObserver"; pub const name = "IntersectionObserver";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
}; };
pub const constructor = bridge.constructor(init, .{}); pub const constructor = bridge.constructor(init, .{});

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const String = @import("../../string.zig").String; const String = @import("../../string.zig").String;
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
@@ -39,6 +40,7 @@ pub fn registerTypes() []const type {
const MutationObserver = @This(); const MutationObserver = @This();
_rc: lp.RC(u8) = .{},
_arena: Allocator, _arena: Allocator,
_callback: js.Function.Temp, _callback: js.Function.Temp,
_observing: std.ArrayList(Observing) = .{}, _observing: std.ArrayList(Observing) = .{},
@@ -85,15 +87,20 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
return self; return self;
} }
pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void { /// Force cleanup on Session shutdown.
pub fn deinit(self: *MutationObserver, session: *Session) void {
self._callback.release(); self._callback.release();
if ((comptime IS_DEBUG) and !shutdown) {
std.debug.assert(self._observing.items.len == 0);
}
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *MutationObserver, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *MutationObserver) void {
self._rc.acquire();
}
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void { pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
const arena = self._arena; const arena = self._arena;
@@ -158,7 +165,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
// Register with page if this is our first observation // Register with page if this is our first observation
if (self._observing.items.len == 0) { if (self._observing.items.len == 0) {
page.js.strongRef(self); self._rc._refs += 1;
try page.registerMutationObserver(self); try page.registerMutationObserver(self);
} }
@@ -169,13 +176,17 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
} }
pub fn disconnect(self: *MutationObserver, page: *Page) void { pub fn disconnect(self: *MutationObserver, page: *Page) void {
page.unregisterMutationObserver(self);
self._observing.clearRetainingCapacity();
for (self._pending_records.items) |record| { for (self._pending_records.items) |record| {
record.deinit(false, page._session); _ = record.releaseRef(page._session);
} }
self._pending_records.clearRetainingCapacity(); self._pending_records.clearRetainingCapacity();
page.js.safeWeakRef(self); const observing_count = self._observing.items.len;
self._observing.clearRetainingCapacity();
if (observing_count > 0) {
_ = self.releaseRef(page._session);
}
page.unregisterMutationObserver(self);
} }
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord { pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
@@ -348,6 +359,7 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
} }
pub const MutationRecord = struct { pub const MutationRecord = struct {
_rc: lp.RC(u8) = .{},
_type: Type, _type: Type,
_target: *Node, _target: *Node,
_arena: Allocator, _arena: Allocator,
@@ -364,10 +376,18 @@ pub const MutationRecord = struct {
characterData, characterData,
}; };
pub fn deinit(self: *MutationRecord, _: bool, session: *Session) void { pub fn deinit(self: *MutationRecord, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *MutationRecord, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *MutationRecord) void {
self._rc.acquire();
}
pub fn getType(self: *const MutationRecord) []const u8 { pub fn getType(self: *const MutationRecord) []const u8 {
return switch (self._type) { return switch (self._type) {
.attributes => "attributes", .attributes => "attributes",
@@ -418,8 +438,6 @@ pub const MutationRecord = struct {
pub const name = "MutationRecord"; pub const name = "MutationRecord";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MutationRecord.deinit);
}; };
pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{}); pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{});
@@ -441,8 +459,6 @@ pub const JsApi = struct {
pub const name = "MutationObserver"; pub const name = "MutationObserver";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MutationObserver.deinit);
}; };
pub const constructor = bridge.constructor(MutationObserver.init, .{}); pub const constructor = bridge.constructor(MutationObserver.init, .{});

View File

@@ -40,8 +40,8 @@ pub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 {
return page._session.browser.app.config.http_headers.user_agent; return page._session.browser.app.config.http_headers.user_agent;
} }
pub fn getLanguages(_: *const Navigator) [1][]const u8 { pub fn getLanguages(_: *const Navigator) [2][]const u8 {
return .{"en-US"}; return .{ "en-US", "en" };
} }
pub fn getPlatform(_: *const Navigator) []const u8 { pub fn getPlatform(_: *const Navigator) []const u8 {

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Session = @import("../Session.zig"); const Session = @import("../Session.zig");
@@ -50,14 +51,23 @@ pub fn query(_: *const Permissions, qd: QueryDescriptor, page: *Page) !js.Promis
} }
const PermissionStatus = struct { const PermissionStatus = struct {
_rc: lp.RC(u8) = .{},
_arena: Allocator, _arena: Allocator,
_name: []const u8, _name: []const u8,
_state: []const u8, _state: []const u8,
pub fn deinit(self: *PermissionStatus, _: bool, session: *Session) void { pub fn deinit(self: *PermissionStatus, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *PermissionStatus, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *PermissionStatus) void {
self._rc.acquire();
}
fn getName(self: *const PermissionStatus) []const u8 { fn getName(self: *const PermissionStatus) []const u8 {
return self._name; return self._name;
} }
@@ -72,8 +82,6 @@ const PermissionStatus = struct {
pub const name = "PermissionStatus"; pub const name = "PermissionStatus";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PermissionStatus.deinit);
}; };
pub const name = bridge.accessor(getName, null, .{}); pub const name = bridge.accessor(getName, null, .{});
pub const state = bridge.accessor(getState, null, .{}); pub const state = bridge.accessor(getState, null, .{});

View File

@@ -28,8 +28,6 @@ const DocumentFragment = @import("DocumentFragment.zig");
const AbstractRange = @import("AbstractRange.zig"); const AbstractRange = @import("AbstractRange.zig");
const DOMRect = @import("DOMRect.zig"); const DOMRect = @import("DOMRect.zig");
const Allocator = std.mem.Allocator;
const Range = @This(); const Range = @This();
_proto: *AbstractRange, _proto: *AbstractRange,
@@ -40,10 +38,6 @@ pub fn init(page: *Page) !*Range {
return page._factory.abstractRange(arena, Range{ ._proto = undefined }, page); return page._factory.abstractRange(arena, Range{ ._proto = undefined }, page);
} }
pub fn deinit(self: *Range, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asAbstractRange(self: *Range) *AbstractRange { pub fn asAbstractRange(self: *Range) *AbstractRange {
return self._proto; return self._proto;
} }
@@ -699,8 +693,6 @@ pub const JsApi = struct {
pub const name = "Range"; pub const name = "Range";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Range.deinit);
}; };
// Constants for compareBoundaryPoints // Constants for compareBoundaryPoints

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig"); const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -27,25 +27,33 @@ const Range = @import("Range.zig");
const AbstractRange = @import("AbstractRange.zig"); const AbstractRange = @import("AbstractRange.zig");
const Node = @import("Node.zig"); const Node = @import("Node.zig");
const Event = @import("Event.zig"); const Event = @import("Event.zig");
const Document = @import("Document.zig");
/// https://w3c.github.io/selection-api/ /// https://w3c.github.io/selection-api/
const Selection = @This(); const Selection = @This();
pub const SelectionDirection = enum { backward, forward, none }; pub const SelectionDirection = enum { backward, forward, none };
_rc: lp.RC(u8) = .{},
_range: ?*Range = null, _range: ?*Range = null,
_direction: SelectionDirection = .none, _direction: SelectionDirection = .none,
pub const init: Selection = .{}; pub const init: Selection = .{};
pub fn deinit(self: *Selection, shutdown: bool, session: *Session) void { pub fn deinit(self: *Selection, session: *Session) void {
if (self._range) |r| { if (self._range) |r| {
r.deinit(shutdown, session); r.asAbstractRange().releaseRef(session);
self._range = null; self._range = null;
} }
} }
pub fn releaseRef(self: *Selection, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Selection) void {
self._rc.acquire();
}
fn dispatchSelectionChangeEvent(page: *Page) !void { fn dispatchSelectionChangeEvent(page: *Page) !void {
const event = try Event.init("selectionchange", .{}, page); const event = try Event.init("selectionchange", .{}, page);
try page._event_manager.dispatch(page.document.asEventTarget(), event); try page._event_manager.dispatch(page.document.asEventTarget(), event);
@@ -695,7 +703,7 @@ pub fn toString(self: *const Selection, page: *Page) ![]const u8 {
fn setRange(self: *Selection, new_range: ?*Range, page: *Page) void { fn setRange(self: *Selection, new_range: ?*Range, page: *Page) void {
if (self._range) |existing| { if (self._range) |existing| {
existing.deinit(false, page._session); _ = existing.asAbstractRange().releaseRef(page._session);
} }
if (new_range) |nr| { if (new_range) |nr| {
nr.asAbstractRange().acquireRef(); nr.asAbstractRange().acquireRef();
@@ -710,7 +718,6 @@ pub const JsApi = struct {
pub const name = "Selection"; pub const name = "Selection";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const finalizer = bridge.finalizer(Selection.deinit);
}; };
pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{}); pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{});

View File

@@ -256,8 +256,7 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {
.{ page.origin orelse "null", uuid_buf }, .{ page.origin orelse "null", uuid_buf },
); );
try page._blob_urls.put(page.arena, blob_url, blob); try page._blob_urls.put(page.arena, blob_url, blob);
// prevent GC from cleaning up the blob while it's in the registry blob.acquireRef();
page.js.strongRef(blob);
return blob_url; return blob_url;
} }
@@ -267,9 +266,8 @@ pub fn revokeObjectURL(url: []const u8, page: *Page) void {
return; return;
} }
// Remove from registry and release strong ref (no-op if not found)
if (page._blob_urls.fetchRemove(url)) |entry| { if (page._blob_urls.fetchRemove(url)) |entry| {
page.js.weakRef(entry.value); entry.value.releaseRef(page._session);
} }
} }

View File

@@ -19,7 +19,6 @@
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const EventTarget = @import("EventTarget.zig"); const EventTarget = @import("EventTarget.zig");
const Window = @import("Window.zig");
const VisualViewport = @This(); const VisualViewport = @This();

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../../../log.zig"); const log = @import("../../../log.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
@@ -33,6 +34,7 @@ const PlayState = enum {
finished, finished,
}; };
_rc: lp.RC(u32) = .{},
_page: *Page, _page: *Page,
_arena: Allocator, _arena: Allocator,
@@ -62,10 +64,18 @@ pub fn init(page: *Page) !*Animation {
return self; return self;
} }
pub fn deinit(self: *Animation, _: bool, session: *Session) void { pub fn deinit(self: *Animation, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *Animation, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Animation) void {
self._rc.acquire();
}
pub fn play(self: *Animation, page: *Page) !void { pub fn play(self: *Animation, page: *Page) !void {
if (self._playState == .running) { if (self._playState == .running) {
return; return;
@@ -75,7 +85,7 @@ pub fn play(self: *Animation, page: *Page) !void {
self._playState = .running; self._playState = .running;
// Schedule the transition from .running => .finished in 10ms. // Schedule the transition from .running => .finished in 10ms.
page.js.strongRef(self); self.acquireRef();
try page.js.scheduler.add( try page.js.scheduler.add(
self, self,
Animation.update, Animation.update,
@@ -201,7 +211,7 @@ fn update(ctx: *anyopaque) !?u32 {
} }
// No future change scheduled, set the object weak for garbage collection. // No future change scheduled, set the object weak for garbage collection.
self._page.js.weakRef(self); self.releaseRef(self._page._session);
return null; return null;
} }
@@ -220,8 +230,6 @@ pub const JsApi = struct {
pub const name = "Animation"; pub const name = "Animation";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Animation.deinit);
}; };
pub const play = bridge.function(Animation.play, .{}); pub const play = bridge.function(Animation.play, .{});

View File

@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const CData = @import("../CData.zig"); const CData = @import("../CData.zig");

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../../../log.zig"); const log = @import("../../../log.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
@@ -37,15 +38,9 @@ _data: union(enum) {
radio_node_list: *RadioNodeList, radio_node_list: *RadioNodeList,
name: NodeLive(.name), name: NodeLive(.name),
}, },
_rc: usize = 0, _rc: lp.RC(u32) = .{},
pub fn deinit(self: *NodeList, _: bool, session: *Session) void {
const rc = self._rc;
if (rc > 1) {
self._rc = rc - 1;
return;
}
pub fn deinit(self: *NodeList, session: *Session) void {
switch (self._data) { switch (self._data) {
.selector_list => |list| list.deinit(session), .selector_list => |list| list.deinit(session),
.child_nodes => |cn| cn.deinit(session), .child_nodes => |cn| cn.deinit(session),
@@ -53,8 +48,12 @@ pub fn deinit(self: *NodeList, _: bool, session: *Session) void {
} }
} }
pub fn releaseRef(self: *NodeList, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *NodeList) void { pub fn acquireRef(self: *NodeList) void {
self._rc += 1; self._rc.acquire();
} }
pub fn length(self: *NodeList, page: *Page) !u32 { pub fn length(self: *NodeList, page: *Page) !u32 {
@@ -119,8 +118,12 @@ const Iterator = struct {
const Entry = struct { u32, *Node }; const Entry = struct { u32, *Node };
pub fn deinit(self: *Iterator, shutdown: bool, session: *Session) void { pub fn deinit(self: *Iterator, session: *Session) void {
self.list.deinit(shutdown, session); self.list.deinit(session);
}
pub fn releaseRef(self: *Iterator, session: *Session) void {
self.list.releaseRef(session);
} }
pub fn acquireRef(self: *Iterator) void { pub fn acquireRef(self: *Iterator) void {
@@ -143,8 +146,6 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false; pub const enumerable = false;
pub const weak = true;
pub const finalizer = bridge.finalizer(NodeList.deinit);
}; };
pub const length = bridge.accessor(NodeList.length, null, .{}); pub const length = bridge.accessor(NodeList.length, null, .{});

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
@@ -40,9 +41,15 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
return page._factory.create(Self{ .inner = inner }); return page._factory.create(Self{ .inner = inner });
} }
pub fn deinit(self: *Self, shutdown: bool, session: *Session) void { pub fn deinit(self: *Self, session: *Session) void {
if (@hasDecl(Inner, "deinit")) { _ = self;
self.inner.deinit(shutdown, session); _ = session;
}
pub fn releaseRef(self: *Self, session: *Session) void {
// Release the reference to the inner type that we acquired
if (@hasDecl(Inner, "releaseRef")) {
self.inner.releaseRef(session);
} }
} }
@@ -73,8 +80,6 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
pub const Meta = struct { pub const Meta = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Self.deinit);
}; };
pub const next = bridge.function(Self.next, .{ .null_as_undefined = true }); pub const next = bridge.function(Self.next, .{ .null_as_undefined = true });

View File

@@ -20,7 +20,6 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const log = @import("../../../log.zig");
const crypto = @import("../../../sys/libcrypto.zig"); const crypto = @import("../../../sys/libcrypto.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");

View File

@@ -20,12 +20,10 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const log = @import("../../../log.zig");
const crypto = @import("../../../sys/libcrypto.zig"); const crypto = @import("../../../sys/libcrypto.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Algorithm = @import("algorithm.zig").Algorithm;
const CryptoKey = @import("../CryptoKey.zig"); const CryptoKey = @import("../CryptoKey.zig");

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
@@ -25,6 +26,7 @@ const Allocator = std.mem.Allocator;
const FontFace = @This(); const FontFace = @This();
_rc: lp.RC(u8) = .{},
_arena: Allocator, _arena: Allocator,
_family: []const u8, _family: []const u8,
@@ -42,10 +44,18 @@ pub fn init(family: []const u8, source: []const u8, page: *Page) !*FontFace {
return self; return self;
} }
pub fn deinit(self: *FontFace, _: bool, session: *Session) void { pub fn deinit(self: *FontFace, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *FontFace, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *FontFace) void {
self._rc.acquire();
}
pub fn getFamily(self: *const FontFace) []const u8 { pub fn getFamily(self: *const FontFace) []const u8 {
return self._family; return self._family;
} }
@@ -67,8 +77,6 @@ pub const JsApi = struct {
pub const name = "FontFace"; pub const name = "FontFace";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FontFace.deinit);
}; };
pub const constructor = bridge.constructor(FontFace.init, .{}); pub const constructor = bridge.constructor(FontFace.init, .{});

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
@@ -28,6 +29,7 @@ const Allocator = std.mem.Allocator;
const FontFaceSet = @This(); const FontFaceSet = @This();
_rc: lp.RC(u8) = .{},
_proto: *EventTarget, _proto: *EventTarget,
_arena: Allocator, _arena: Allocator,
@@ -41,10 +43,18 @@ pub fn init(page: *Page) !*FontFaceSet {
}); });
} }
pub fn deinit(self: *FontFaceSet, _: bool, session: *Session) void { pub fn deinit(self: *FontFaceSet, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *FontFaceSet, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *FontFaceSet) void {
self._rc.acquire();
}
pub fn asEventTarget(self: *FontFaceSet) *EventTarget { pub fn asEventTarget(self: *FontFaceSet) *EventTarget {
return self._proto; return self._proto;
} }
@@ -95,8 +105,6 @@ pub const JsApi = struct {
pub const name = "FontFaceSet"; pub const name = "FontFaceSet";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FontFaceSet.deinit);
}; };
pub const size = bridge.property(0, .{ .template = false, .readonly = true }); pub const size = bridge.property(0, .{ .template = false, .readonly = true });

View File

@@ -22,7 +22,6 @@ const reflect = @import("../../reflect.zig");
const log = @import("../../../log.zig"); const log = @import("../../../log.zig");
const global_event_handlers = @import("../global_event_handlers.zig"); const global_event_handlers = @import("../global_event_handlers.zig");
const GlobalEventHandlersLookup = global_event_handlers.Lookup;
const GlobalEventHandler = global_event_handlers.Handler; const GlobalEventHandler = global_event_handlers.Handler;
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");

View File

@@ -29,6 +29,9 @@ const OffscreenCanvas = @import("../../canvas/OffscreenCanvas.zig");
const Canvas = @This(); const Canvas = @This();
_proto: *HtmlElement, _proto: *HtmlElement,
_cached: ?DrawingContext = null,
const ContextType = enum { none, @"2d", webgl };
pub fn asElement(self: *Canvas) *Element { pub fn asElement(self: *Canvas) *Element {
return self._proto._proto; return self._proto._proto;
@@ -68,17 +71,28 @@ const DrawingContext = union(enum) {
}; };
pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext { pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext {
if (std.mem.eql(u8, context_type, "2d")) { if (self._cached) |cached| {
const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self }); const matches = switch (cached) {
return .{ .@"2d" = ctx }; .@"2d" => std.mem.eql(u8, context_type, "2d"),
.webgl => std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl"),
};
return if (matches) cached else null;
} }
if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) { const drawing_context: DrawingContext = blk: {
const ctx = try page._factory.create(WebGLRenderingContext{}); if (std.mem.eql(u8, context_type, "2d")) {
return .{ .webgl = ctx }; const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self });
} break :blk .{ .@"2d" = ctx };
}
return null; if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) {
const ctx = try page._factory.create(WebGLRenderingContext{});
break :blk .{ .webgl = ctx };
}
return null;
};
self._cached = drawing_context;
return drawing_context;
} }
/// Transfers control of the canvas to an OffscreenCanvas. /// Transfers control of the canvas to an OffscreenCanvas.

View File

@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const log = @import("../../../../log.zig");
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig"); const Page = @import("../../../Page.zig");
const Window = @import("../../Window.zig"); const Window = @import("../../Window.zig");

View File

@@ -5,10 +5,6 @@ const URL = @import("../../../URL.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
const Event = @import("../../Event.zig");
const log = @import("../../../../log.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const Image = @This(); const Image = @This();
_proto: *HtmlElement, _proto: *HtmlElement,

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const log = @import("../../../../log.zig");
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig"); const Page = @import("../../../Page.zig");

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
@@ -25,6 +26,7 @@ const Allocator = std.mem.Allocator;
const TextDecoder = @This(); const TextDecoder = @This();
_rc: lp.RC(u8) = .{},
_fatal: bool, _fatal: bool,
_arena: Allocator, _arena: Allocator,
_ignore_bom: bool, _ignore_bom: bool,
@@ -60,10 +62,18 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder {
return self; return self;
} }
pub fn deinit(self: *TextDecoder, _: bool, session: *Session) void { pub fn deinit(self: *TextDecoder, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *TextDecoder, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *TextDecoder) void {
self._rc.acquire();
}
pub fn getIgnoreBOM(self: *const TextDecoder) bool { pub fn getIgnoreBOM(self: *const TextDecoder) bool {
return self._ignore_bom; return self._ignore_bom;
} }
@@ -109,8 +119,6 @@ pub const JsApi = struct {
pub const name = "TextDecoder"; pub const name = "TextDecoder";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(TextDecoder.deinit);
}; };
pub const constructor = bridge.constructor(TextDecoder.init, .{}); pub const constructor = bridge.constructor(TextDecoder.init, .{});

View File

@@ -22,7 +22,6 @@ const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const CompositionEvent = @This(); const CompositionEvent = @This();
@@ -54,10 +53,6 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CompositionEvent {
return event; return event;
} }
pub fn deinit(self: *CompositionEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *CompositionEvent) *Event { pub fn asEvent(self: *CompositionEvent) *Event {
return self._proto; return self._proto;
} }
@@ -73,8 +68,6 @@ pub const JsApi = struct {
pub const name = "CompositionEvent"; pub const name = "CompositionEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(CompositionEvent.deinit);
}; };
pub const constructor = bridge.constructor(CompositionEvent.init, .{}); pub const constructor = bridge.constructor(CompositionEvent.init, .{});

View File

@@ -73,11 +73,19 @@ pub fn initCustomEvent(
self._detail = detail_; self._detail = detail_;
} }
pub fn deinit(self: *CustomEvent, shutdown: bool, session: *Session) void { pub fn deinit(self: *CustomEvent, session: *Session) void {
if (self._detail) |d| { if (self._detail) |d| {
d.release(); d.release();
} }
self._proto.deinit(shutdown, session); self._proto.deinit(session);
}
pub fn acquireRef(self: *CustomEvent) void {
self._proto.acquireRef();
}
pub fn releaseRef(self: *CustomEvent, session: *Session) void {
self._proto._rc.release(self, session);
} }
pub fn asEvent(self: *CustomEvent) *Event { pub fn asEvent(self: *CustomEvent) *Event {
@@ -95,8 +103,6 @@ pub const JsApi = struct {
pub const name = "CustomEvent"; pub const name = "CustomEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(CustomEvent.deinit);
pub const enumerable = false; pub const enumerable = false;
}; };

View File

@@ -80,11 +80,19 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *ErrorEvent, shutdown: bool, session: *Session) void { pub fn deinit(self: *ErrorEvent, session: *Session) void {
if (self._error) |e| { if (self._error) |e| {
e.release(); e.release();
} }
self._proto.deinit(shutdown, session); self._proto.deinit(session);
}
pub fn acquireRef(self: *ErrorEvent) void {
self._proto.acquireRef();
}
pub fn releaseRef(self: *ErrorEvent, session: *Session) void {
self._proto._rc.release(self, session);
} }
pub fn asEvent(self: *ErrorEvent) *Event { pub fn asEvent(self: *ErrorEvent) *Event {

View File

@@ -70,10 +70,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *FocusEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *FocusEvent) *Event { pub fn asEvent(self: *FocusEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -89,8 +85,6 @@ pub const JsApi = struct {
pub const name = "FocusEvent"; pub const name = "FocusEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FocusEvent.deinit);
}; };
pub const constructor = bridge.constructor(FocusEvent.init, .{}); pub const constructor = bridge.constructor(FocusEvent.init, .{});

View File

@@ -24,7 +24,6 @@ const Session = @import("../../Session.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const UIEvent = @import("UIEvent.zig");
const FormData = @import("../net/FormData.zig"); const FormData = @import("../net/FormData.zig");
@@ -67,10 +66,6 @@ fn initWithTrusted(arena: Allocator, typ: String, maybe_options: ?Options, trust
return event; return event;
} }
pub fn deinit(self: *FormDataEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *FormDataEvent) *Event { pub fn asEvent(self: *FormDataEvent) *Event {
return self._proto; return self._proto;
} }
@@ -86,8 +81,6 @@ pub const JsApi = struct {
pub const name = "FormDataEvent"; pub const name = "FormDataEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FormDataEvent.deinit);
}; };
pub const constructor = bridge.constructor(FormDataEvent.init, .{}); pub const constructor = bridge.constructor(FormDataEvent.init, .{});

View File

@@ -83,10 +83,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *InputEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *InputEvent) *Event { pub fn asEvent(self: *InputEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -110,8 +106,6 @@ pub const JsApi = struct {
pub const name = "InputEvent"; pub const name = "InputEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(InputEvent.deinit);
}; };
pub const constructor = bridge.constructor(InputEvent.init, .{}); pub const constructor = bridge.constructor(InputEvent.init, .{});

View File

@@ -229,10 +229,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *KeyboardEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *KeyboardEvent) *Event { pub fn asEvent(self: *KeyboardEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -296,8 +292,6 @@ pub const JsApi = struct {
pub const name = "KeyboardEvent"; pub const name = "KeyboardEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(KeyboardEvent.deinit);
}; };
pub const constructor = bridge.constructor(KeyboardEvent.init, .{}); pub const constructor = bridge.constructor(KeyboardEvent.init, .{});

View File

@@ -73,11 +73,19 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *MessageEvent, shutdown: bool, session: *Session) void { pub fn deinit(self: *MessageEvent, session: *Session) void {
if (self._data) |d| { if (self._data) |d| {
d.release(); d.release();
} }
self._proto.deinit(shutdown, session); self._proto.deinit(session);
}
pub fn acquireRef(self: *MessageEvent) void {
self._proto.acquireRef();
}
pub fn releaseRef(self: *MessageEvent, session: *Session) void {
self._proto._rc.release(self, session);
} }
pub fn asEvent(self: *MessageEvent) *Event { pub fn asEvent(self: *MessageEvent) *Event {
@@ -103,8 +111,6 @@ pub const JsApi = struct {
pub const name = "MessageEvent"; pub const name = "MessageEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MessageEvent.deinit);
}; };
pub const constructor = bridge.constructor(MessageEvent.init, .{}); pub const constructor = bridge.constructor(MessageEvent.init, .{});

View File

@@ -121,10 +121,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *MouseEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *MouseEvent) *Event { pub fn asEvent(self: *MouseEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -203,8 +199,6 @@ pub const JsApi = struct {
pub const name = "MouseEvent"; pub const name = "MouseEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MouseEvent.deinit);
}; };
pub const constructor = bridge.constructor(MouseEvent.init, .{}); pub const constructor = bridge.constructor(MouseEvent.init, .{});

View File

@@ -83,10 +83,6 @@ fn initWithTrusted(
return event; return event;
} }
pub fn deinit(self: *NavigationCurrentEntryChangeEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event { pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event {
return self._proto; return self._proto;
} }
@@ -106,8 +102,6 @@ pub const JsApi = struct {
pub const name = "NavigationCurrentEntryChangeEvent"; pub const name = "NavigationCurrentEntryChangeEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(NavigationCurrentEntryChangeEvent.deinit);
}; };
pub const constructor = bridge.constructor(NavigationCurrentEntryChangeEvent.init, .{}); pub const constructor = bridge.constructor(NavigationCurrentEntryChangeEvent.init, .{});

View File

@@ -66,10 +66,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *PageTransitionEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *PageTransitionEvent) *Event { pub fn asEvent(self: *PageTransitionEvent) *Event {
return self._proto; return self._proto;
} }
@@ -85,8 +81,6 @@ pub const JsApi = struct {
pub const name = "PageTransitionEvent"; pub const name = "PageTransitionEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PageTransitionEvent.deinit);
}; };
pub const constructor = bridge.constructor(PageTransitionEvent.init, .{}); pub const constructor = bridge.constructor(PageTransitionEvent.init, .{});

View File

@@ -128,10 +128,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PointerEvent {
return event; return event;
} }
pub fn deinit(self: *PointerEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *PointerEvent) *Event { pub fn asEvent(self: *PointerEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -191,8 +187,6 @@ pub const JsApi = struct {
pub const name = "PointerEvent"; pub const name = "PointerEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PointerEvent.deinit);
}; };
pub const constructor = bridge.constructor(PointerEvent.init, .{}); pub const constructor = bridge.constructor(PointerEvent.init, .{});

View File

@@ -67,10 +67,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *PopStateEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *PopStateEvent) *Event { pub fn asEvent(self: *PopStateEvent) *Event {
return self._proto; return self._proto;
} }
@@ -92,8 +88,6 @@ pub const JsApi = struct {
pub const name = "PopStateEvent"; pub const name = "PopStateEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PopStateEvent.deinit);
}; };
pub const constructor = bridge.constructor(PopStateEvent.init, .{}); pub const constructor = bridge.constructor(PopStateEvent.init, .{});

View File

@@ -68,10 +68,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *ProgressEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *ProgressEvent) *Event { pub fn asEvent(self: *ProgressEvent) *Event {
return self._proto; return self._proto;
} }
@@ -96,8 +92,6 @@ pub const JsApi = struct {
pub const name = "ProgressEvent"; pub const name = "ProgressEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(ProgressEvent.deinit);
}; };
pub const constructor = bridge.constructor(ProgressEvent.init, .{}); pub const constructor = bridge.constructor(ProgressEvent.init, .{});

View File

@@ -22,7 +22,6 @@ const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const PromiseRejectionEvent = @This(); const PromiseRejectionEvent = @This();
@@ -57,14 +56,22 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*PromiseRejectionEve
return event; return event;
} }
pub fn deinit(self: *PromiseRejectionEvent, shutdown: bool, session: *Session) void { pub fn deinit(self: *PromiseRejectionEvent, session: *Session) void {
if (self._reason) |r| { if (self._reason) |r| {
r.release(); r.release();
} }
if (self._promise) |p| { if (self._promise) |p| {
p.release(); p.release();
} }
self._proto.deinit(shutdown, session); self._proto.deinit(session);
}
pub fn acquireRef(self: *PromiseRejectionEvent) void {
self._proto.acquireRef();
}
pub fn releaseRef(self: *PromiseRejectionEvent, session: *Session) void {
self._proto._rc.release(self, session);
} }
pub fn asEvent(self: *PromiseRejectionEvent) *Event { pub fn asEvent(self: *PromiseRejectionEvent) *Event {
@@ -86,8 +93,6 @@ pub const JsApi = struct {
pub const name = "PromiseRejectionEvent"; pub const name = "PromiseRejectionEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PromiseRejectionEvent.deinit);
}; };
pub const constructor = bridge.constructor(PromiseRejectionEvent.init, .{}); pub const constructor = bridge.constructor(PromiseRejectionEvent.init, .{});

View File

@@ -67,10 +67,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *SubmitEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *SubmitEvent) *Event { pub fn asEvent(self: *SubmitEvent) *Event {
return self._proto; return self._proto;
} }
@@ -86,8 +82,6 @@ pub const JsApi = struct {
pub const name = "SubmitEvent"; pub const name = "SubmitEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(SubmitEvent.deinit);
}; };
pub const constructor = bridge.constructor(SubmitEvent.init, .{}); pub const constructor = bridge.constructor(SubmitEvent.init, .{});

View File

@@ -59,10 +59,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*TextEvent {
return event; return event;
} }
pub fn deinit(self: *TextEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *TextEvent) *Event { pub fn asEvent(self: *TextEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -101,8 +97,6 @@ pub const JsApi = struct {
pub const name = "TextEvent"; pub const name = "TextEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(TextEvent.deinit);
}; };
// No constructor - TextEvent is created via document.createEvent('TextEvent') // No constructor - TextEvent is created via document.createEvent('TextEvent')

View File

@@ -71,10 +71,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {
return event; return event;
} }
pub fn deinit(self: *UIEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn as(self: *UIEvent, comptime T: type) *T { pub fn as(self: *UIEvent, comptime T: type) *T {
return self.is(T).?; return self.is(T).?;
} }
@@ -122,8 +118,6 @@ pub const JsApi = struct {
pub const name = "UIEvent"; pub const name = "UIEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(UIEvent.deinit);
}; };
pub const constructor = bridge.constructor(UIEvent.init, .{}); pub const constructor = bridge.constructor(UIEvent.init, .{});

View File

@@ -87,10 +87,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*WheelEvent {
return event; return event;
} }
pub fn deinit(self: *WheelEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *WheelEvent) *Event { pub fn asEvent(self: *WheelEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -118,8 +114,6 @@ pub const JsApi = struct {
pub const name = "WheelEvent"; pub const name = "WheelEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(WheelEvent.deinit);
}; };
pub const constructor = bridge.constructor(WheelEvent.init, .{}); pub const constructor = bridge.constructor(WheelEvent.init, .{});

View File

@@ -27,8 +27,6 @@ const Page = @import("../../Page.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const EventTarget = @import("../EventTarget.zig"); const EventTarget = @import("../EventTarget.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation // https://developer.mozilla.org/en-US/docs/Web/API/Navigation
const Navigation = @This(); const Navigation = @This();

View File

@@ -18,7 +18,6 @@
const std = @import("std"); const std = @import("std");
const URL = @import("../URL.zig"); const URL = @import("../URL.zig");
const EventTarget = @import("../EventTarget.zig");
const NavigationState = @import("root.zig").NavigationState; const NavigationState = @import("root.zig").NavigationState;
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");

View File

@@ -62,7 +62,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
} }
const response = try Response.init(null, .{ .status = 0 }, page); const response = try Response.init(null, .{ .status = 0 }, page);
errdefer response.deinit(true, page._session); errdefer response.deinit(page._session);
const fetch = try response._arena.create(Fetch); const fetch = try response._arena.create(Fetch);
fetch.* = .{ fetch.* = .{
@@ -80,7 +80,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
if (request._headers) |h| { if (request._headers) |h| {
try h.populateHttpHeader(page.call_arena, &headers); try h.populateHttpHeader(page.call_arena, &headers);
} }
try page.headersForRequest(page.arena, request._url, &headers); try page.headersForRequest(&headers);
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.http, "fetch", .{ .url = request._url }); log.debug(.http, "fetch", .{ .url = request._url });
@@ -95,6 +95,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
.headers = headers, .headers = headers,
.resource_type = .fetch, .resource_type = .fetch,
.cookie_jar = &page._session.cookie_jar, .cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = httpStartCallback, .start_callback = httpStartCallback,
.header_callback = httpHeaderDoneCallback, .header_callback = httpHeaderDoneCallback,
@@ -225,7 +226,7 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
return ls.toLocal(self._resolver).resolve("fetch done", js_val); return ls.toLocal(self._resolver).resolve("fetch done", js_val);
} }
fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { fn httpErrorCallback(ctx: *anyopaque, _: anyerror) void {
const self: *Fetch = @ptrCast(@alignCast(ctx)); const self: *Fetch = @ptrCast(@alignCast(ctx));
var response = self._response; var response = self._response;
@@ -234,7 +235,7 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
// clear this. (defer since `self is in the response's arena). // clear this. (defer since `self is in the response's arena).
defer if (self._owns_response) { defer if (self._owns_response) {
response.deinit(err == error.Abort, self._page._session); response.deinit(self._page._session);
self._owns_response = false; self._owns_response = false;
}; };
@@ -256,7 +257,7 @@ fn httpShutdownCallback(ctx: *anyopaque) void {
if (self._owns_response) { if (self._owns_response) {
var response = self._response; var response = self._response;
response._transfer = null; response._transfer = null;
response.deinit(true, self._page._session); response.deinit(self._page._session);
// Do not access `self` after this point: the Fetch struct was // Do not access `self` after this point: the Fetch struct was
// allocated from response._arena which has been released. // allocated from response._arena which has been released.
} }

View File

@@ -22,7 +22,6 @@ const log = @import("../../../log.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Form = @import("../element/html/Form.zig"); const Form = @import("../element/html/Form.zig");
const Element = @import("../Element.zig"); const Element = @import("../Element.zig");
const KeyValueList = @import("../KeyValueList.zig"); const KeyValueList = @import("../KeyValueList.zig");

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const HttpClient = @import("../../HttpClient.zig"); const HttpClient = @import("../../HttpClient.zig");
@@ -38,6 +39,7 @@ pub const Type = enum {
opaqueredirect, opaqueredirect,
}; };
_rc: lp.RC(u8) = .{},
_status: u16, _status: u16,
_arena: Allocator, _arena: Allocator,
_headers: *Headers, _headers: *Headers,
@@ -78,18 +80,22 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response {
return self; return self;
} }
pub fn deinit(self: *Response, shutdown: bool, session: *Session) void { pub fn deinit(self: *Response, session: *Session) void {
if (self._transfer) |transfer| { if (self._transfer) |transfer| {
if (shutdown) { transfer.abort(error.Abort);
transfer.terminate();
} else {
transfer.abort(error.Abort);
}
self._transfer = null; self._transfer = null;
} }
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *Response, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Response) void {
self._rc.acquire();
}
pub fn getStatus(self: *const Response) u16 { pub fn getStatus(self: *const Response) u16 {
return self._status; return self._status;
} }
@@ -197,8 +203,6 @@ pub const JsApi = struct {
pub const name = "Response"; pub const name = "Response";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Response.deinit);
}; };
pub const constructor = bridge.constructor(Response.init, .{}); pub const constructor = bridge.constructor(Response.init, .{});

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const log = @import("../../../log.zig"); const log = @import("../../../log.zig");
@@ -29,7 +30,6 @@ const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
const Node = @import("../Node.zig"); const Node = @import("../Node.zig");
const Blob = @import("../Blob.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const Headers = @import("Headers.zig"); const Headers = @import("Headers.zig");
const EventTarget = @import("../EventTarget.zig"); const EventTarget = @import("../EventTarget.zig");
@@ -39,6 +39,7 @@ const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
const XMLHttpRequest = @This(); const XMLHttpRequest = @This();
_rc: lp.RC(u8) = .{},
_page: *Page, _page: *Page,
_proto: *XMLHttpRequestEventTarget, _proto: *XMLHttpRequestEventTarget,
_arena: Allocator, _arena: Allocator,
@@ -88,21 +89,18 @@ const ResponseType = enum {
pub fn init(page: *Page) !*XMLHttpRequest { pub fn init(page: *Page) !*XMLHttpRequest {
const arena = try page.getArena(.{ .debug = "XMLHttpRequest" }); const arena = try page.getArena(.{ .debug = "XMLHttpRequest" });
errdefer page.releaseArena(arena); errdefer page.releaseArena(arena);
return page._factory.xhrEventTarget(arena, XMLHttpRequest{ const xhr = try page._factory.xhrEventTarget(arena, XMLHttpRequest{
._page = page, ._page = page,
._arena = arena, ._arena = arena,
._proto = undefined, ._proto = undefined,
._request_headers = try Headers.init(null, page), ._request_headers = try Headers.init(null, page),
}); });
return xhr;
} }
pub fn deinit(self: *XMLHttpRequest, shutdown: bool, session: *Session) void { pub fn deinit(self: *XMLHttpRequest, session: *Session) void {
if (self._transfer) |transfer| { if (self._transfer) |transfer| {
if (shutdown) { transfer.abort(error.Abort);
transfer.terminate();
} else {
transfer.abort(error.Abort);
}
self._transfer = null; self._transfer = null;
} }
@@ -138,6 +136,14 @@ pub fn deinit(self: *XMLHttpRequest, shutdown: bool, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *XMLHttpRequest, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *XMLHttpRequest) void {
self._rc.acquire();
}
fn asEventTarget(self: *XMLHttpRequest) *EventTarget { fn asEventTarget(self: *XMLHttpRequest) *EventTarget {
return self._proto._proto; return self._proto._proto;
} }
@@ -225,7 +231,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
try self._request_headers.populateHttpHeader(page.call_arena, &headers); try self._request_headers.populateHttpHeader(page.call_arena, &headers);
if (cookie_support) { if (cookie_support) {
try page.headersForRequest(self._arena, self._url, &headers); try page.headersForRequest(&headers);
} }
try http_client.request(.{ try http_client.request(.{
@@ -236,6 +242,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
.frame_id = page._frame_id, .frame_id = page._frame_id,
.body = self._request_body, .body = self._request_body,
.cookie_jar = if (cookie_support) &page._session.cookie_jar else null, .cookie_jar = if (cookie_support) &page._session.cookie_jar else null,
.cookie_origin = page.url,
.resource_type = .xhr, .resource_type = .xhr,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = httpStartCallback, .start_callback = httpStartCallback,
@@ -245,8 +252,6 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
.error_callback = httpErrorCallback, .error_callback = httpErrorCallback,
.shutdown_callback = httpShutdownCallback, .shutdown_callback = httpShutdownCallback,
}); });
page.js.strongRef(self);
} }
fn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void { fn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void {
@@ -388,6 +393,7 @@ fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
log.debug(.http, "request start", .{ .method = self._method, .url = self._url, .source = "xhr" }); log.debug(.http, "request start", .{ .method = self._method, .url = self._url, .source = "xhr" });
} }
self._transfer = transfer; self._transfer = transfer;
self.acquireRef();
} }
fn httpHeaderCallback(transfer: *HttpClient.Transfer, header: net_http.Header) !void { fn httpHeaderCallback(transfer: *HttpClient.Transfer, header: net_http.Header) !void {
@@ -486,15 +492,17 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
.loaded = loaded, .loaded = loaded,
}, page); }, page);
page.js.weakRef(self); self.releaseRef(page._session);
} }
fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *XMLHttpRequest = @ptrCast(@alignCast(ctx)); const self: *XMLHttpRequest = @ptrCast(@alignCast(ctx));
// http client will close it after an error, it isn't safe to keep around // http client will close it after an error, it isn't safe to keep around
self._transfer = null;
self.handleError(err); self.handleError(err);
self._page.js.weakRef(self); if (self._transfer != null) {
self._transfer = null;
self.releaseRef(self._page._session);
}
} }
fn httpShutdownCallback(ctx: *anyopaque) void { fn httpShutdownCallback(ctx: *anyopaque) void {
@@ -505,10 +513,10 @@ fn httpShutdownCallback(ctx: *anyopaque) void {
pub fn abort(self: *XMLHttpRequest) void { pub fn abort(self: *XMLHttpRequest) void {
self.handleError(error.Abort); self.handleError(error.Abort);
if (self._transfer) |transfer| { if (self._transfer) |transfer| {
transfer.abort(error.Abort);
self._transfer = null; self._transfer = null;
transfer.abort(error.Abort);
self.releaseRef(self._page._session);
} }
self._page.js.weakRef(self);
} }
fn handleError(self: *XMLHttpRequest, err: anyerror) void { fn handleError(self: *XMLHttpRequest, err: anyerror) void {
@@ -582,8 +590,6 @@ pub const JsApi = struct {
pub const name = "XMLHttpRequest"; pub const name = "XMLHttpRequest";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(XMLHttpRequest.deinit);
}; };
pub const constructor = bridge.constructor(XMLHttpRequest.init, .{}); pub const constructor = bridge.constructor(XMLHttpRequest.init, .{});

View File

@@ -25,7 +25,6 @@ const json = std.json;
const Incrementing = @import("id.zig").Incrementing; const Incrementing = @import("id.zig").Incrementing;
const log = @import("../log.zig"); const log = @import("../log.zig");
const App = @import("../App.zig");
const Notification = @import("../Notification.zig"); const Notification = @import("../Notification.zig");
const Client = @import("../Server.zig").Client; const Client = @import("../Server.zig").Client;
@@ -35,7 +34,6 @@ const Browser = @import("../browser/Browser.zig");
const Session = @import("../browser/Session.zig"); const Session = @import("../browser/Session.zig");
const Page = @import("../browser/Page.zig"); const Page = @import("../browser/Page.zig");
const Mime = @import("../browser/Mime.zig"); const Mime = @import("../browser/Mime.zig");
const HttpClient = @import("../browser/HttpClient.zig");
const InterceptState = @import("domains/fetch.zig").InterceptState; const InterceptState = @import("domains/fetch.zig").InterceptState;
@@ -365,6 +363,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
inspector_session: *js.Inspector.Session, inspector_session: *js.Inspector.Session,
isolated_worlds: std.ArrayList(*IsolatedWorld), isolated_worlds: std.ArrayList(*IsolatedWorld),
// Scripts registered via Page.addScriptToEvaluateOnNewDocument.
// Evaluated in each new document after navigation completes.
scripts_on_new_document: std.ArrayList(ScriptOnNewDocument) = .empty,
next_script_id: u32 = 1,
http_proxy_changed: bool = false, http_proxy_changed: bool = false,
// Extra headers to add to all requests. // Extra headers to add to all requests.
@@ -470,8 +473,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
if (self.http_proxy_changed) { if (self.http_proxy_changed) {
// has to be called after browser.closeSession, since it won't // has to be called after browser.closeSession, since it won't
// work if there are active connections. // work if there are active connections.
browser.http_client.restoreOriginalProxy() catch |err| { browser.http_client.changeProxy(null) catch |err| {
log.warn(.http, "restoreOriginalProxy", .{ .err = err }); log.warn(.http, "changeProxy", .{ .err = err });
}; };
} }
self.intercept_state.deinit(); self.intercept_state.deinit();
@@ -764,6 +767,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
/// Clients create this to be able to create variables and run code without interfering with the /// Clients create this to be able to create variables and run code without interfering with the
/// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after /// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after
/// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed /// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed
const ScriptOnNewDocument = struct {
identifier: u32,
source: []const u8,
};
/// in the isolated world by using its Context ID or the worldName. /// in the isolated world by using its Context ID or the worldName.
/// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects. /// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects.
/// An isolated world has it's own instance of globals like Window. /// An isolated world has it's own instance of globals like Window.

View File

@@ -18,7 +18,6 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const log = @import("../../log.zig");
const markdown = lp.markdown; const markdown = lp.markdown;
const SemanticTree = lp.SemanticTree; const SemanticTree = lp.SemanticTree;
const interactive = lp.interactive; const interactive = lp.interactive;

View File

@@ -351,6 +351,10 @@ pub const TransferAsRequestWriter = struct {
try jws.objectField(hdr.name); try jws.objectField(hdr.name);
try jws.write(hdr.value); try jws.write(hdr.value);
} }
if (try transfer.getCookieString()) |cookies| {
try jws.objectField("Cookie");
try jws.write(cookies[0 .. cookies.len - 1]);
}
try jws.endObject(); try jws.endObject();
} }
try jws.endObject(); try jws.endObject();

View File

@@ -37,6 +37,7 @@ pub fn processMessage(cmd: anytype) !void {
getFrameTree, getFrameTree,
setLifecycleEventsEnabled, setLifecycleEventsEnabled,
addScriptToEvaluateOnNewDocument, addScriptToEvaluateOnNewDocument,
removeScriptToEvaluateOnNewDocument,
createIsolatedWorld, createIsolatedWorld,
navigate, navigate,
reload, reload,
@@ -51,6 +52,7 @@ pub fn processMessage(cmd: anytype) !void {
.getFrameTree => return getFrameTree(cmd), .getFrameTree => return getFrameTree(cmd),
.setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd), .setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd),
.addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd), .addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),
.removeScriptToEvaluateOnNewDocument => return removeScriptToEvaluateOnNewDocument(cmd),
.createIsolatedWorld => return createIsolatedWorld(cmd), .createIsolatedWorld => return createIsolatedWorld(cmd),
.navigate => return navigate(cmd), .navigate => return navigate(cmd),
.reload => return doReload(cmd), .reload => return doReload(cmd),
@@ -147,22 +149,55 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
// TODO: hard coded method
// With the command we receive a script we need to store and run for each new document.
// Note that the worldName refers to the name given to the isolated world.
fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void { fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
// const params = (try cmd.params(struct { const params = (try cmd.params(struct {
// source: []const u8, source: []const u8,
// worldName: ?[]const u8 = null, worldName: ?[]const u8 = null,
// includeCommandLineAPI: bool = false, includeCommandLineAPI: bool = false,
// runImmediately: bool = false, runImmediately: bool = false,
// })) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
if (params.runImmediately) {
log.warn(.not_implemented, "addScriptOnNewDocument", .{ .param = "runImmediately" });
}
const script_id = bc.next_script_id;
bc.next_script_id += 1;
const source_dupe = try bc.arena.dupe(u8, params.source);
try bc.scripts_on_new_document.append(bc.arena, .{
.identifier = script_id,
.source = source_dupe,
});
var id_buf: [16]u8 = undefined;
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{script_id}) catch "1";
return cmd.sendResult(.{ return cmd.sendResult(.{
.identifier = "1", .identifier = id_str,
}, .{}); }, .{});
} }
fn removeScriptToEvaluateOnNewDocument(cmd: anytype) !void {
const params = (try cmd.params(struct {
identifier: []const u8,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const target_id = std.fmt.parseInt(u32, params.identifier, 10) catch
return cmd.sendResult(null, .{});
for (bc.scripts_on_new_document.items, 0..) |script, i| {
if (script.identifier == target_id) {
_ = bc.scripts_on_new_document.orderedRemove(i);
break;
}
}
return cmd.sendResult(null, .{});
}
fn close(cmd: anytype) !void { fn close(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
@@ -482,6 +517,27 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
); );
} }
// Evaluate scripts registered via Page.addScriptToEvaluateOnNewDocument.
// Must run after the execution context is created but before the client
// receives frameNavigated/loadEventFired so polyfills are available for
// subsequent CDP commands.
if (bc.scripts_on_new_document.items.len > 0) {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
for (bc.scripts_on_new_document.items) |script| {
var try_catch: lp.js.TryCatch = undefined;
try_catch.init(&ls.local);
defer try_catch.deinit();
ls.local.eval(script.source, null) catch |err| {
const caught = try_catch.caughtOrError(arena, err);
log.warn(.cdp, "script on new doc", .{ .caught = caught });
};
}
}
// frameNavigated event // frameNavigated event
try cdp.sendEvent("Page.frameNavigated", .{ try cdp.sendEvent("Page.frameNavigated", .{
.type = "Navigation", .type = "Navigation",
@@ -840,3 +896,55 @@ test "cdp.page: reload" {
try ctx.processMessage(.{ .id = 32, .method = "Page.reload", .params = .{ .ignoreCache = true } }); try ctx.processMessage(.{ .id = 32, .method = "Page.reload", .params = .{ .ignoreCache = true } });
} }
} }
test "cdp.page: addScriptToEvaluateOnNewDocument" {
var ctx = try testing.context();
defer ctx.deinit();
var bc = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* });
{
// Register a script — should return unique identifier "1"
try ctx.processMessage(.{ .id = 20, .method = "Page.addScriptToEvaluateOnNewDocument", .params = .{ .source = "window.__test = 1" } });
try ctx.expectSentResult(.{
.identifier = "1",
}, .{ .id = 20 });
}
{
// Register another script — should return identifier "2"
try ctx.processMessage(.{ .id = 21, .method = "Page.addScriptToEvaluateOnNewDocument", .params = .{ .source = "window.__test2 = 2" } });
try ctx.expectSentResult(.{
.identifier = "2",
}, .{ .id = 21 });
}
{
// Remove the first script — should succeed
try ctx.processMessage(.{ .id = 22, .method = "Page.removeScriptToEvaluateOnNewDocument", .params = .{ .identifier = "1" } });
try ctx.expectSentResult(null, .{ .id = 22 });
}
{
// Remove a non-existent identifier — should succeed silently
try ctx.processMessage(.{ .id = 23, .method = "Page.removeScriptToEvaluateOnNewDocument", .params = .{ .identifier = "999" } });
try ctx.expectSentResult(null, .{ .id = 23 });
}
{
try ctx.processMessage(.{ .id = 34, .method = "Page.reload" });
// wait for this event, which is sent after we've run the registered scripts
try ctx.expectSentEvent("Page.frameNavigated", .{
.frame = .{ .loaderId = "LID-0000000002" },
}, .{});
const page = bc.session.currentPage() orelse unreachable;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const test_val = try ls.local.exec("window.__test2", null);
try testing.expectEqual(2, try test_val.toI32());
}
}

View File

@@ -24,9 +24,6 @@ const log = @import("../../log.zig");
const URL = @import("../../browser/URL.zig"); const URL = @import("../../browser/URL.zig");
const js = @import("../../browser/js/js.zig"); const js = @import("../../browser/js/js.zig");
// TODO: hard coded IDs
const LOADER_ID = "LOADERID42AA389647D702B4D805F49A";
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
getTargets, getTargets,

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const IS_DEBUG = @import("builtin").mode == .Debug;
pub fn toPageId(comptime id_type: enum { frame_id, loader_id }, input: []const u8) !u32 { pub fn toPageId(comptime id_type: enum { frame_id, loader_id }, input: []const u8) !u32 {
const err = switch (comptime id_type) { const err = switch (comptime id_type) {

View File

@@ -19,10 +19,6 @@
const std = @import("std"); const std = @import("std");
const json = std.json; const json = std.json;
const posix = std.posix; const posix = std.posix;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const Testing = @This();
const CDP = @import("CDP.zig"); const CDP = @import("CDP.zig");
const Server = @import("../Server.zig"); const Server = @import("../Server.zig");
@@ -172,13 +168,26 @@ const TestContext = struct {
index: ?usize = null, index: ?usize = null,
}; };
pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void { pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void {
const serialized = try json.Stringify.valueAlloc(base.arena_allocator, expected, .{ const expected_json = blk: {
.whitespace = .indent_2, // Zig makes this hard. When sendJSON is called, we're sending an anytype.
.emit_null_optional_fields = false, // We can't record that in an ArrayList(???), so we serialize it to JSON.
}); // Now, ideally, we could just take our expected structure, serialize it to
// json and check if the two are equal.
// Except serializing to JSON isn't deterministic.
// So we serialize the JSON then we deserialize to json.Value. And then we can
// compare our anytype expectation with the json.Value that we captured
const serialized = try json.Stringify.valueAlloc(base.arena_allocator, expected, .{
.whitespace = .indent_2,
.emit_null_optional_fields = false,
});
break :blk try std.json.parseFromSliceLeaky(json.Value, base.arena_allocator, serialized, .{});
};
for (0..5) |_| { for (0..5) |_| {
for (self.received.items, 0..) |received, i| { for (self.received.items, 0..) |received, i| {
if (try compareExpectedToSent(serialized, received) == false) { if (try base.isEqualJson(expected_json, received) == false) {
continue; continue;
} }
@@ -191,6 +200,15 @@ const TestContext = struct {
} }
return; return;
} }
if (self.cdp_) |*cdp__| {
if (cdp__.browser_context) |*bc| {
if (bc.session.page != null) {
var runner = try bc.session.runner(.{});
_ = try runner.tick(.{ .ms = 1000 });
}
}
}
std.Thread.sleep(5 * std.time.ns_per_ms); std.Thread.sleep(5 * std.time.ns_per_ms);
try self.read(); try self.read();
} }
@@ -303,17 +321,3 @@ pub fn context() !TestContext {
.socket = pair[0], .socket = pair[0],
}; };
} }
// Zig makes this hard. When sendJSON is called, we're sending an anytype.
// We can't record that in an ArrayList(???), so we serialize it to JSON.
// Now, ideally, we could just take our expected structure, serialize it to
// json and check if the two are equal.
// Except serializing to JSON isn't deterministic.
// So we serialize the JSON then we deserialize to json.Value. And then we can
// compare our anytype expectation with the json.Value that we captured
fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool {
const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{});
defer expected_value.deinit();
return base.isEqualJson(expected_value.value, actual);
}

View File

@@ -209,6 +209,37 @@ noinline fn assertionFailure(comptime ctx: []const u8, args: anytype) noreturn {
@import("crash_handler.zig").crash(ctx, args, @returnAddress()); @import("crash_handler.zig").crash(ctx, args, @returnAddress());
} }
// Reference counting helper
pub fn RC(comptime T: type) type {
return struct {
_refs: T = 0,
pub fn init(refs: T) @This() {
return .{ ._refs = refs };
}
pub fn acquire(self: *@This()) void {
self._refs += 1;
}
pub fn release(self: *@This(), value: anytype, session: *Session) void {
if (comptime IS_DEBUG) {
std.debug.assert(self._refs > 0);
}
const refs = self._refs - 1;
self._refs = refs;
if (refs > 0) {
return;
}
value.deinit(session);
if (session.finalizer_callbacks.fetchRemove(@intFromPtr(value))) |kv| {
session.releaseArena(kv.value.arena);
}
}
};
}
test { test {
std.testing.refAllDecls(@This()); std.testing.refAllDecls(@This());
} }

View File

@@ -49,9 +49,6 @@ const Opts = struct {
pub var opts = Opts{}; pub var opts = Opts{};
// synchronizes writes to the output
var out_lock: Thread.Mutex = .{};
// synchronizes access to last_log // synchronizes access to last_log
var last_log_lock: Thread.Mutex = .{}; var last_log_lock: Thread.Mutex = .{};

View File

@@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
pub const protocol = @import("mcp/protocol.zig"); pub const protocol = @import("mcp/protocol.zig");
pub const Version = protocol.Version;
pub const router = @import("mcp/router.zig"); pub const router = @import("mcp/router.zig");
pub const Server = @import("mcp/Server.zig"); pub const Server = @import("mcp/Server.zig");

View File

@@ -114,7 +114,7 @@ test "MCP.Server - Integration: synchronous smoke test" {
try router.processRequests(server, &in_reader); try router.processRequests(server, &in_reader);
try testing.expectJson(.{ .jsonrpc = "2.0", .id = 1 }, out_alloc.writer.buffered()); try testing.expectJson(.{ .jsonrpc = "2.0", .id = 1, .result = .{ .protocolVersion = "2024-11-05" } }, out_alloc.writer.buffered());
} }
test "MCP.Server - Integration: ping request returns an empty result" { test "MCP.Server - Integration: ping request returns an empty result" {

View File

@@ -1,5 +1,14 @@
const std = @import("std"); const std = @import("std");
pub const Version = enum {
@"2024-11-05",
@"2025-03-26",
@"2025-06-18",
@"2025-11-25",
pub const default: Version = .@"2024-11-05";
};
pub const Request = struct { pub const Request = struct {
jsonrpc: []const u8 = "2.0", jsonrpc: []const u8 = "2.0",
id: ?std.json.Value = null, id: ?std.json.Value = null,

Some files were not shown because too many files have changed in this diff Show More