cleanup resolvers

This commit is contained in:
Karl Seguin
2026-01-01 09:57:41 +08:00
parent e6af7d1bd0
commit 01ecd725b8
14 changed files with 83 additions and 124 deletions

View File

@@ -34,8 +34,11 @@ const Allocator = std.mem.Allocator;
const PersistentObject = v8.Persistent(v8.Object); const PersistentObject = v8.Persistent(v8.Object);
const PersistentModule = v8.Persistent(v8.Module); const PersistentModule = v8.Persistent(v8.Module);
const PersistentPromise = v8.Persistent(v8.Promise); const PersistentPromise = v8.Persistent(v8.Promise);
const PersistentPromiseResolver = v8.Persistent(v8.PromiseResolver);
const TaggedAnyOpaque = js.TaggedAnyOpaque; const TaggedAnyOpaque = js.TaggedAnyOpaque;
const IS_DEBUG = builtin.mode == .Debug;
// Loosely maps to a Browser Page. // Loosely maps to a Browser Page.
const Context = @This(); const Context = @This();
@@ -70,6 +73,8 @@ call_depth: usize = 0,
// The key is the @intFromPtr of the Zig value // The key is the @intFromPtr of the Zig value
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty, identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty,
persisted_promise_resolvers: std.ArrayList(PersistentPromiseResolver) = .empty,
// Some web APIs have to manage opaque values. Ideally, they use an // Some web APIs have to manage opaque values. Ideally, they use an
// js.Object, but the js.Object has no lifetime guarantee beyond the // js.Object, but the js.Object has no lifetime guarantee beyond the
// current call. They can call .persist() on their js.Object to get // current call. They can call .persist() on their js.Object to get
@@ -81,11 +86,7 @@ identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty,
global_values: std.ArrayList(js.Global(js.Value)) = .empty, global_values: std.ArrayList(js.Global(js.Value)) = .empty,
global_objects: std.ArrayList(js.Global(js.Object)) = .empty, global_objects: std.ArrayList(js.Global(js.Object)) = .empty,
global_functions: std.ArrayList(js.Global(js.Function)) = .empty, global_functions: std.ArrayList(js.Global(js.Function)) = .empty,
global_promise_resolvers: std.ArrayList(js.Global(js.PromiseResolver)) = .empty,
// Various web APIs depend on having a persistent promise resolver. They
// require for this PromiseResolver to be valid for a lifetime longer than
// the function that resolves/rejects them.
persisted_promise_resolvers: std.ArrayListUnmanaged(v8.Persistent(v8.PromiseResolver)) = .empty,
// Some Zig types have code to execute to cleanup // Some Zig types have code to execute to cleanup
destructor_callbacks: std.ArrayListUnmanaged(DestructorCallback) = .empty, destructor_callbacks: std.ArrayListUnmanaged(DestructorCallback) = .empty,
@@ -171,6 +172,10 @@ pub fn deinit(self: *Context) void {
global.deinit(); global.deinit();
} }
for (self.global_promise_resolvers.items) |*global| {
global.deinit();
}
for (self.persisted_promise_resolvers.items) |*p| { for (self.persisted_promise_resolvers.items) |*p| {
p.deinit(); p.deinit();
} }
@@ -205,16 +210,7 @@ pub fn eval(self: *Context, src: []const u8, name: ?[]const u8) !void {
} }
pub fn exec(self: *Context, src: []const u8, name: ?[]const u8) !js.Value { pub fn exec(self: *Context, src: []const u8, name: ?[]const u8) !js.Value {
const v8_context = v8.Context{ .handle = self.handle }; return self.compileAndRun(src, name);
const v8_isolate = v8.Isolate{ .handle = self.isolate.handle };
const scr = try compileScript(v8_isolate, v8_context, src, name);
const value = scr.run(v8_context.handle) catch {
return error.ExecutionError;
};
return self.createValue(value);
} }
pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) { pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) {
@@ -315,16 +311,11 @@ pub fn stringToFunction(self: *Context, str: []const u8) !js.Function {
} }
const full = try std.fmt.allocPrintSentinel(self.call_arena, "(function(e) {{ {s}{s} }})", .{ normalized, extra }, 0); const full = try std.fmt.allocPrintSentinel(self.call_arena, "(function(e) {{ {s}{s} }})", .{ normalized, extra }, 0);
const v8_context = v8.Context{ .handle = self.handle }; const js_value = try self.compileAndRun(full, null);
const v8_isolate = v8.Isolate{ .handle = self.isolate.handle };
const script = try compileScript(v8_isolate, v8_context, full, null);
const js_value = script.run(v8_context.handle) catch {
return error.ExecutionError;
};
if (!js_value.isFunction()) { if (!js_value.isFunction()) {
return error.StringFunctionError; return error.StringFunctionError;
} }
return self.createFunction(js_value); return self.createFunction(.{ .handle = js_value.handle });
} }
// After we compile a module, whether it's a top-level one, or a nested one, // After we compile a module, whether it's a top-level one, or a nested one,
@@ -390,7 +381,9 @@ pub fn createObject(self: *Context, js_value: v8.Value) js.Object {
pub fn createFunction(self: *Context, js_value: v8.Value) !js.Function { pub fn createFunction(self: *Context, js_value: v8.Value) !js.Function {
// caller should have made sure this was a function // caller should have made sure this was a function
if (comptime IS_DEBUG) {
std.debug.assert(js_value.isFunction()); std.debug.assert(js_value.isFunction());
}
return .{ return .{
.ctx = self, .ctx = self,
@@ -398,6 +391,13 @@ pub fn createFunction(self: *Context, js_value: v8.Value) !js.Function {
}; };
} }
pub fn newString(self: *Context, str: []const u8) js.String {
return .{
.ctx = self,
.handle = self.isolate.newStringHandle(str),
};
}
pub fn throw(self: *Context, err: []const u8) js.Exception { pub fn throw(self: *Context, err: []const u8) js.Exception {
const v8_isolate = v8.Isolate{ .handle = self.isolate.handle }; const v8_isolate = v8.Isolate{ .handle = self.isolate.handle };
const js_value = js._createException(v8_isolate, err); const js_value = js._createException(v8_isolate, err);
@@ -1203,38 +1203,8 @@ pub fn runMicrotasks(self: *Context) void {
self.isolate.performMicrotasksCheckpoint(); self.isolate.performMicrotasksCheckpoint();
} }
// creates a PersistentPromiseResolver, taking in a lifetime parameter. pub fn createPromiseResolver(self: *Context) js.PromiseResolver {
// If the lifetime is page, the page will clean up the PersistentPromiseResolver.
// If the lifetime is self, you will be expected to deinitalize the PersistentPromiseResolver.
const PromiseResolverLifetime = enum {
none,
self, // it's a persisted promise, but it'll be managed by the caller
page, // it's a persisted promise, tied to the page lifetime
};
fn PromiseResolverType(comptime lifetime: PromiseResolverLifetime) type {
if (lifetime == .none) {
return js.PromiseResolver;
}
return error{OutOfMemory}!js.PersistentPromiseResolver;
}
pub fn createPromiseResolver(self: *Context, comptime lifetime: PromiseResolverLifetime) PromiseResolverType(lifetime) {
if (comptime lifetime == .none) {
return js.PromiseResolver.init(self); return js.PromiseResolver.init(self);
}
const v8_context = v8.Context{ .handle = self.handle };
const resolver = v8.PromiseResolver.init(v8_context);
const v8_isolate = v8.Isolate{ .handle = self.isolate.handle };
const persisted = v8.Persistent(v8.PromiseResolver).init(v8_isolate, resolver);
if (comptime lifetime == .page) {
try self.persisted_promise_resolvers.append(self.arena, persisted);
}
return .{
.context = self,
.resolver = persisted,
};
} }
// == Callbacks == // == Callbacks ==
@@ -1928,24 +1898,30 @@ fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T {
return error.InvalidArgument; return error.InvalidArgument;
} }
fn compileScript(isolate: v8.Isolate, ctx: v8.Context, src: []const u8, name: ?[]const u8) !js.Script { fn compileAndRun(self: *Context, src: []const u8, name: ?[]const u8) !js.Value {
// compile const script_name = self.isolate.newStringHandle(name orelse "anonymous");
const script_name = v8.String.initUtf8(isolate, name orelse "anonymous"); const script_source = self.isolate.newStringHandle(src);
const script_source = v8.String.initUtf8(isolate, src);
const origin = v8.ScriptOrigin.initDefault(script_name.toValue()); // Create ScriptOrigin
var origin: v8.c.ScriptOrigin = undefined;
v8.c.v8__ScriptOrigin__CONSTRUCT(&origin, @ptrCast(script_name));
var script_comp_source: v8.ScriptCompilerSource = undefined; // Create ScriptCompilerSource
v8.ScriptCompilerSource.init(&script_comp_source, script_source, origin, null); var script_comp_source: v8.c.ScriptCompilerSource = undefined;
defer script_comp_source.deinit(); v8.c.v8__ScriptCompiler__Source__CONSTRUCT2(script_source, &origin, null, &script_comp_source);
defer v8.c.v8__ScriptCompiler__Source__DESTRUCT(&script_comp_source);
const v8_script = v8.ScriptCompiler.compile( // Compile the script
ctx, const v8_script = v8.c.v8__ScriptCompiler__Compile(
self.handle,
&script_comp_source, &script_comp_source,
.kNoCompileOptions, v8.c.kNoCompileOptions,
.kNoCacheNoReason, v8.c.kNoCacheNoReason,
) catch return error.CompilationError; ) orelse return error.CompilationError;
return .{ .handle = v8_script.handle };
// Run the script
const result = v8.c.v8__Script__Run(v8_script, self.handle) orelse return error.ExecutionError;
return .{ .ctx = self, .handle = result };
} }
fn compileModule(isolate: v8.Isolate, src: []const u8, name: []const u8) !js.Module { fn compileModule(isolate: v8.Isolate, src: []const u8, name: []const u8) !js.Module {

View File

@@ -63,3 +63,7 @@ pub fn throwException(self: Isolate, value: anytype) v8.Value {
.handle = v8.c.v8__Isolate__ThrowException(self.handle, handle).?, .handle = v8.c.v8__Isolate__ThrowException(self.handle, handle).?,
}; };
} }
pub fn newStringHandle(self: Isolate, str: []const u8) *const v8.c.String {
return v8.c.v8__String__NewFromUtf8(self.handle, str.ptr, v8.c.kNormal, @as(c_int, @intCast(str.len))).?;
}

View File

@@ -68,7 +68,7 @@ pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{
pub fn get(self: Object, key: []const u8) !js.Value { pub fn get(self: Object, key: []const u8) !js.Value {
const ctx = self.ctx; const ctx = self.ctx;
const js_key = v8.c.v8__String__NewFromUtf8(ctx.isolate.handle, key.ptr, v8.c.kNormal, @intCast(key.len)).?; const js_key = ctx.isolate.newStringHandle(key);
const js_val_handle = v8.c.v8__Object__Get(self.handle, ctx.handle, js_key) orelse return error.JsException; const js_val_handle = v8.c.v8__Object__Get(self.handle, ctx.handle, js_key) orelse return error.JsException;
const js_val = v8.Value{ .handle = js_val_handle }; const js_val = v8.Value{ .handle = js_val_handle };
return ctx.createValue(js_val); return ctx.createValue(js_val);
@@ -119,7 +119,7 @@ pub fn getFunction(self: Object, name: []const u8) !?js.Function {
} }
const ctx = self.ctx; const ctx = self.ctx;
const js_name = v8.c.v8__String__NewFromUtf8(ctx.isolate.handle, name.ptr, v8.c.kNormal, @intCast(name.len)).?; const js_name = ctx.isolate.newStringHandle(name);
const js_val_handle = v8.c.v8__Object__Get(self.handle, ctx.handle, js_name) orelse return error.JsException; const js_val_handle = v8.c.v8__Object__Get(self.handle, ctx.handle, js_name) orelse return error.JsException;
const js_value = v8.Value{ .handle = js_val_handle }; const js_value = v8.Value{ .handle = js_val_handle };

View File

@@ -22,10 +22,10 @@ const log = @import("../../log.zig");
const PromiseResolver = @This(); const PromiseResolver = @This();
ctx: *const js.Context, ctx: *js.Context,
handle: *const v8.c.PromiseResolver, handle: *const v8.c.PromiseResolver,
pub fn init(ctx: *const js.Context) PromiseResolver { pub fn init(ctx: *js.Context) PromiseResolver {
return .{ return .{
.ctx = ctx, .ctx = ctx,
.handle = v8.c.v8__Promise__Resolver__New(ctx.handle).?, .handle = v8.c.v8__Promise__Resolver__New(ctx.handle).?,
@@ -73,3 +73,15 @@ fn _reject(self: PromiseResolver, value: anytype) !void {
} }
ctx.runMicrotasks(); ctx.runMicrotasks();
} }
pub fn persist(self: PromiseResolver) !PromiseResolver {
var ctx = self.ctx;
const global = js.Global(PromiseResolver).init(ctx.isolate.handle, self.handle);
try ctx.global_promise_resolvers.append(ctx.arena, global);
return .{
.ctx = ctx,
.handle = global.local(),
};
}

View File

@@ -1,38 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("js.zig");
const v8 = js.v8;
const Script = @This();
handle: *const v8.c.Script,
pub fn compile(ctx_handle: *const v8.c.Context, src_handle: *const v8.c.String, origin: ?*const v8.c.ScriptOrigin) !Script {
if (v8.c.v8__Script__Compile(ctx_handle, src_handle, origin)) |handle| {
return .{ .handle = handle };
}
return error.JsException;
}
pub fn run(self: Script, ctx_handle: *const v8.c.Context) !v8.Value {
if (v8.c.v8__Script__Run(self.handle, ctx_handle)) |value| {
return .{ .handle = value };
}
return error.JsException;
}

View File

@@ -526,8 +526,10 @@ fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
fn illegalConstructorCallback(raw_info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void { fn illegalConstructorCallback(raw_info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void {
const isolate = v8.c.v8__FunctionCallbackInfo__GetIsolate(raw_info); const isolate = v8.c.v8__FunctionCallbackInfo__GetIsolate(raw_info);
log.warn(.js, "Illegal constructor call", .{}); log.warn(.js, "Illegal constructor call", .{});
const message = v8.c.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.c.kNormal, 19); const message = v8.c.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.c.kNormal, 19);
const js_exception = v8.c.v8__Exception__TypeError(message); const js_exception = v8.c.v8__Exception__TypeError(message);
_ = v8.c.v8__Isolate__ThrowException(isolate, js_exception); _ = v8.c.v8__Isolate__ThrowException(isolate, js_exception);
var return_value: v8.c.ReturnValue = undefined; var return_value: v8.c.ReturnValue = undefined;
v8.c.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value); v8.c.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);

View File

@@ -54,6 +54,10 @@ pub fn isSymbol(self: Value) bool {
return v8.c.v8__Value__IsSymbol(self.handle); return v8.c.v8__Value__IsSymbol(self.handle);
} }
pub fn isFunction(self: Value) bool {
return v8.c.v8__Value__IsFunction(self.handle);
}
pub fn toString(self: Value, opts: js.String.ToZigOpts) ![]u8 { pub fn toString(self: Value, opts: js.String.ToZigOpts) ![]u8 {
return self._toString(false, opts); return self._toString(false, opts);
} }

View File

@@ -42,7 +42,6 @@ pub const PromiseResolver = @import("PromiseResolver.zig");
pub const Module = @import("Module.zig"); pub const Module = @import("Module.zig");
pub const BigInt = @import("BigInt.zig"); pub const BigInt = @import("BigInt.zig");
pub const Name = @import("Name.zig"); pub const Name = @import("Name.zig");
pub const Script = @import("Script.zig");
pub const Integer = @import("Integer.zig"); pub const Integer = @import("Integer.zig");
pub const Global = @import("global.zig").Global; pub const Global = @import("global.zig").Global;

View File

@@ -30,7 +30,7 @@ const CustomElementDefinition = @import("CustomElementDefinition.zig");
const CustomElementRegistry = @This(); const CustomElementRegistry = @This();
_definitions: std.StringHashMapUnmanaged(*CustomElementDefinition) = .{}, _definitions: std.StringHashMapUnmanaged(*CustomElementDefinition) = .{},
_when_defined: std.StringHashMapUnmanaged(js.PersistentPromiseResolver) = .{}, _when_defined: std.StringHashMapUnmanaged(js.PromiseResolver) = .{},
const DefineOptions = struct { const DefineOptions = struct {
extends: ?[]const u8 = null, extends: ?[]const u8 = null,
@@ -131,7 +131,7 @@ pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page)
errdefer _ = self._when_defined.remove(name); errdefer _ = self._when_defined.remove(name);
const owned_name = try page.dupeString(name); const owned_name = try page.dupeString(name);
const resolver = try page.js.createPromiseResolver(.page); const resolver = try page.js.createPromiseResolver().persist();
gop.key_ptr.* = owned_name; gop.key_ptr.* = owned_name;
gop.value_ptr.* = resolver; gop.value_ptr.* = resolver;

View File

@@ -46,7 +46,7 @@ pub fn getPending(_: *const Animation) bool {
pub fn getFinished(self: *Animation, page: *Page) !js.Promise { pub fn getFinished(self: *Animation, page: *Page) !js.Promise {
if (self._finished_resolver == null) { if (self._finished_resolver == null) {
const resolver = page.js.createPromiseResolver(.none); const resolver = try page.js.createPromiseResolver().persist();
resolver.resolve("Animation.getFinished", self); resolver.resolve("Animation.getFinished", self);
self._finished_resolver = resolver; self._finished_resolver = resolver;
} }
@@ -56,7 +56,7 @@ pub fn getFinished(self: *Animation, page: *Page) !js.Promise {
pub fn getReady(self: *Animation, page: *Page) !js.Promise { pub fn getReady(self: *Animation, page: *Page) !js.Promise {
// never resolved, because we're always "finished" // never resolved, because we're always "finished"
if (self._ready_resolver == null) { if (self._ready_resolver == null) {
const resolver = page.js.createPromiseResolver(.none); const resolver = try page.js.createPromiseResolver().persist();
self._ready_resolver = resolver; self._ready_resolver = resolver;
} }
return self._ready_resolver.?.promise(); return self._ready_resolver.?.promise();

View File

@@ -265,8 +265,8 @@ pub fn navigateInner(
// //
// These will only settle on same-origin navigation (mostly intended for SPAs). // These will only settle on same-origin navigation (mostly intended for SPAs).
// It is fine (and expected) for these to not settle on cross-origin requests :) // It is fine (and expected) for these to not settle on cross-origin requests :)
const committed = try page.js.createPromiseResolver(.page); const committed = try page.js.createPromiseResolver().persist();
const finished = try page.js.createPromiseResolver(.page); const finished = try page.js.createPromiseResolver().persist();
const new_url = try URL.resolve(arena, page.url, url, .{}); const new_url = try URL.resolve(arena, page.url, url, .{});
const is_same_document = URL.eqlDocument(new_url, page.url); const is_same_document = URL.eqlDocument(new_url, page.url);

View File

@@ -36,7 +36,7 @@ _page: *Page,
_url: []const u8, _url: []const u8,
_buf: std.ArrayList(u8), _buf: std.ArrayList(u8),
_response: *Response, _response: *Response,
_resolver: js.PersistentPromiseResolver, _resolver: js.PromiseResolver,
pub const Input = Request.Input; pub const Input = Request.Input;
pub const InitOpts = Request.InitOpts; pub const InitOpts = Request.InitOpts;
@@ -49,7 +49,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
._page = page, ._page = page,
._buf = .empty, ._buf = .empty,
._url = try page.arena.dupe(u8, request._url), ._url = try page.arena.dupe(u8, request._url),
._resolver = try page.js.createPromiseResolver(.page), ._resolver = try page.js.createPromiseResolver().persist(),
._response = try Response.init(null, .{ .status = 0 }, page), ._response = try Response.init(null, .{ .status = 0 }, page),
}; };

View File

@@ -182,7 +182,7 @@ pub fn cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !js.Promi
var c = &self._cancel.?; var c = &self._cancel.?;
if (c.resolver == null) { if (c.resolver == null) {
c.resolver = try page.js.createPromiseResolver(.self); c.resolver = try page.js.createPromiseResolver().persist();
} }
// Execute the cancel callback if provided // Execute the cancel callback if provided
@@ -213,7 +213,7 @@ pub fn cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !js.Promi
const Cancel = struct { const Cancel = struct {
callback: ?js.Function = null, callback: ?js.Function = null,
reason: ?[]const u8 = null, reason: ?[]const u8 = null,
resolver: ?js.PersistentPromiseResolver = null, resolver: ?js.PromiseResolver = null,
}; };
pub const JsApi = struct { pub const JsApi = struct {

View File

@@ -42,7 +42,7 @@ _page: *Page,
_stream: *ReadableStream, _stream: *ReadableStream,
_arena: std.mem.Allocator, _arena: std.mem.Allocator,
_queue: std.ArrayList(Chunk), _queue: std.ArrayList(Chunk),
_pending_reads: std.ArrayList(js.PersistentPromiseResolver), _pending_reads: std.ArrayList(js.PromiseResolver),
_high_water_mark: u32, _high_water_mark: u32,
pub fn init(stream: *ReadableStream, high_water_mark: u32, page: *Page) !*ReadableStreamDefaultController { pub fn init(stream: *ReadableStream, high_water_mark: u32, page: *Page) !*ReadableStreamDefaultController {
@@ -57,7 +57,7 @@ pub fn init(stream: *ReadableStream, high_water_mark: u32, page: *Page) !*Readab
} }
pub fn addPendingRead(self: *ReadableStreamDefaultController, page: *Page) !js.Promise { pub fn addPendingRead(self: *ReadableStreamDefaultController, page: *Page) !js.Promise {
const resolver = try page.js.createPromiseResolver(.page); const resolver = try page.js.createPromiseResolver().persist();
try self._pending_reads.append(self._arena, resolver); try self._pending_reads.append(self._arena, resolver);
return resolver.promise(); return resolver.promise();
} }