mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-01 10:16:25 -04:00
idb: abort transaction on callback exception
This commit is contained in:
@@ -288,7 +288,11 @@ pub fn dispatchDirect(
|
||||
if (getFunction(handler, &ls.local)) |func| {
|
||||
event._current_target = target;
|
||||
_ = func.callWithThis(void, target, .{event}) catch |err| {
|
||||
log.warn(.event, opts.context, .{ .err = err });
|
||||
if (err == error.JsException) {
|
||||
event._listeners_did_throw = true;
|
||||
} else {
|
||||
log.warn(.event, opts.context, .{ .err = err });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -432,24 +436,45 @@ pub const Listener = struct {
|
||||
) error{OutOfMemory}!void {
|
||||
switch (self.function) {
|
||||
.value => |value| local.toLocal(value).callWithThis(void, event._current_target.?, .{event}) catch |err| {
|
||||
log.warn(.event, context, .{ .err = err });
|
||||
if (err == error.JsException) {
|
||||
event._listeners_did_throw = true;
|
||||
} else {
|
||||
log.warn(.event, context, .{ .err = err });
|
||||
}
|
||||
},
|
||||
.string => |string| {
|
||||
const str = try arena.dupeZ(u8, string.str());
|
||||
local.eval(str, null) catch |err| {
|
||||
log.warn(.event, context, .{ .err = err });
|
||||
if (err == error.JsException) {
|
||||
event._listeners_did_throw = true;
|
||||
} else {
|
||||
log.warn(.event, context, .{ .err = err });
|
||||
}
|
||||
};
|
||||
},
|
||||
.object => |obj_global| {
|
||||
const obj = local.toLocal(obj_global);
|
||||
const handle_event = obj.getFunction("handleEvent") catch |err| blk: {
|
||||
log.warn(.event, context, .{ .err = err });
|
||||
// Getting "handleEvent" threw (e.g. a throwing getter).
|
||||
if (err == error.JsException) {
|
||||
event._listeners_did_throw = true;
|
||||
} else {
|
||||
log.warn(.event, context, .{ .err = err });
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
if (handle_event) |handleEvent| {
|
||||
handleEvent.callWithThis(void, obj, .{event}) catch |err| {
|
||||
log.warn(.event, context, .{ .err = err });
|
||||
if (err == error.JsException) {
|
||||
event._listeners_did_throw = true;
|
||||
} else {
|
||||
log.warn(.event, context, .{ .err = err });
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// The listener was an object without the handleEvent function
|
||||
// This is throwing a TypeError
|
||||
event._listeners_did_throw = true;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ _time_stamp: u64,
|
||||
_needs_retargeting: bool = false,
|
||||
_is_trusted: bool = false,
|
||||
_in_passive_listener: bool = false,
|
||||
_listeners_did_throw: bool = false, // IndexedDB needs to abort on callback throw
|
||||
|
||||
// There's a period of time between creating an event and handing it off to v8
|
||||
// where things can fail. If it does fail, we need to deinit the event. The timing
|
||||
|
||||
@@ -106,7 +106,6 @@ pub const JsApi = struct {
|
||||
pub const name = "DOMStringList";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const length = bridge.accessor(DOMStringList.length, null, .{});
|
||||
|
||||
@@ -23,11 +23,14 @@ const js = @import("../../../js/js.zig");
|
||||
|
||||
const EventTarget = @import("../../EventTarget.zig");
|
||||
|
||||
const idb = @import("idb.zig");
|
||||
const Engine = @import("Engine.zig");
|
||||
const IDBTransaction = @import("IDBTransaction.zig");
|
||||
const IDBObjectStore = @import("IDBObjectStore.zig");
|
||||
const DOMStringList = @import("../../collections.zig").DOMStringList;
|
||||
|
||||
const FunctionSetter = idb.FunctionSetter;
|
||||
|
||||
const Execution = js.Execution;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -40,6 +43,7 @@ _database_id: i64,
|
||||
_name: []const u8,
|
||||
_version: i64,
|
||||
_txn: ?*IDBTransaction = null, // only set during upgradeneeded
|
||||
_on_error: ?js.Function.Global = null,
|
||||
|
||||
pub fn init(exec: *Execution, engine: *Engine, database_id: i64, name: []const u8, version: i64) !*IDBDatabase {
|
||||
return exec._factory.eventTarget(IDBDatabase{
|
||||
@@ -192,6 +196,17 @@ pub fn getObjectStoreNames(self: *IDBDatabase, exec: *Execution) !*DOMStringList
|
||||
return list;
|
||||
}
|
||||
|
||||
pub fn getOnError(self: *const IDBDatabase) ?js.Function.Global {
|
||||
return self._on_error;
|
||||
}
|
||||
|
||||
pub fn setOnError(self: *IDBDatabase, setter: ?FunctionSetter) void {
|
||||
self._on_error = if (setter) |s| switch (s) {
|
||||
.func => |f| f,
|
||||
.anything => null,
|
||||
} else null;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(IDBDatabase);
|
||||
|
||||
@@ -208,4 +223,5 @@ pub const JsApi = struct {
|
||||
pub const deleteObjectStore = bridge.function(IDBDatabase.deleteObjectStore, .{});
|
||||
pub const transaction = bridge.function(IDBDatabase.transaction, .{});
|
||||
pub const close = bridge.function(IDBDatabase.close, .{});
|
||||
pub const onerror = bridge.accessor(IDBDatabase.getOnError, IDBDatabase.setOnError, .{});
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ const IDBObjectStore = @import("IDBObjectStore.zig");
|
||||
const IDBTransaction = @import("IDBTransaction.zig");
|
||||
const IDBVersionChangeEvent = @import("IDBVersionChangeEvent.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const Execution = js.Execution;
|
||||
const FunctionSetter = idb.FunctionSetter;
|
||||
|
||||
@@ -178,14 +179,14 @@ pub fn failed(self: *const IDBRequest) bool {
|
||||
pub fn deliver(self: *IDBRequest, exec: *Execution) !void {
|
||||
self._ready_state = .done;
|
||||
if (self._error != null) {
|
||||
return self.fire(exec, comptime .wrap("error"), self._on_error);
|
||||
return self.fireError(exec);
|
||||
}
|
||||
if (self._cursor) |cursor| {
|
||||
// A cursor request re-fires on every iteration; let the cursor mark itself
|
||||
// readable (got value) right before the success handler runs.
|
||||
cursor.beforeDeliver();
|
||||
}
|
||||
return self.fire(exec, comptime .wrap("success"), self._on_success);
|
||||
return self.fireSuccess(exec);
|
||||
}
|
||||
|
||||
pub fn fireUpgradeNeeded(self: *IDBRequest, exec: *Execution, old_version: u64, new_version: u64) !void {
|
||||
@@ -196,12 +197,73 @@ pub fn fireUpgradeNeeded(self: *IDBRequest, exec: *Execution, old_version: u64,
|
||||
|
||||
pub fn fireSuccess(self: *IDBRequest, exec: *Execution) !void {
|
||||
self._ready_state = .done;
|
||||
return self.fire(exec, comptime .wrap("success"), self._on_success);
|
||||
|
||||
const event = try Event.initTrusted(comptime .wrap("success"), null, exec.page);
|
||||
event.acquireRef();
|
||||
defer _ = event.releaseRef(exec.page);
|
||||
|
||||
try exec.dispatch(self.asEventTarget(), event, self._on_success, .{ .context = "IDBRequest.success" });
|
||||
|
||||
if (event._listeners_did_throw) blk: {
|
||||
// if the event threw, we must abort
|
||||
const txn = switch (self._txn) {
|
||||
.owned => |t| t,
|
||||
.none, .borrowed => break :blk,
|
||||
};
|
||||
|
||||
if (!txn._settled and !txn._committing) {
|
||||
txn.abortWith(exec, error.AbortError) catch |err| {
|
||||
log.warn(.storage, "idb success-event abort", .{ .err = err });
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fire(self: *IDBRequest, exec: *Execution, typ: lp.String, handler: ?js.Function.Global) !void {
|
||||
const event = try Event.initTrusted(typ, null, exec.page);
|
||||
try exec.dispatch(self.asEventTarget(), event, handler, .{ .context = "IDBRequest" });
|
||||
fn fireError(self: *IDBRequest, exec: *Execution) !void {
|
||||
// Requests created inside a transaction own an abortable transaction; open/
|
||||
// delete requests (and the borrowed upgrade-transaction view) do not.
|
||||
const txn: ?*IDBTransaction = switch (self._txn) {
|
||||
.owned => |t| t,
|
||||
.none, .borrowed => null,
|
||||
};
|
||||
|
||||
const event = try Event.initTrusted(comptime .wrap("error"), .{ .bubbles = true, .cancelable = true }, exec.page);
|
||||
event.acquireRef();
|
||||
defer _ = event.releaseRef(exec.page);
|
||||
|
||||
const et = self.asEventTarget();
|
||||
event._target = et;
|
||||
event._dispatch_target = et;
|
||||
|
||||
try exec.dispatch(et, event, self._on_error, .{ .context = "IDBRequest.error", .inject_target = false });
|
||||
if (txn) |tx| {
|
||||
if (!event._stop_propagation) {
|
||||
try exec.dispatch(tx.asEventTarget(), event, tx._on_error, .{ .context = "IDBTransaction.error", .inject_target = false });
|
||||
}
|
||||
if (!event._stop_propagation) {
|
||||
const db = tx._db;
|
||||
try exec.dispatch(db.asEventTarget(), event, db._on_error, .{ .context = "IDBDatabase.error", .inject_target = false });
|
||||
}
|
||||
|
||||
// Don't re-abort a transaction that's already finishing — the AbortError
|
||||
// events an abort itself delivers come back through here.
|
||||
if (!tx._settled and !tx._committing) {
|
||||
const reason: ?anyerror = if (event._listeners_did_throw)
|
||||
error.AbortError
|
||||
else if (!event._prevent_default)
|
||||
self._error
|
||||
else
|
||||
null;
|
||||
if (reason != null) {
|
||||
// catch (rather than propagate): the abort's own event dispatch
|
||||
// can re-enter here, so keeping the error set out of deliver's
|
||||
// recursion is both simpler and avoids an unresolvable inferred set.
|
||||
tx.abortWith(exec, reason) catch |err| {
|
||||
log.warn(.storage, "idb error-event abort", .{ .err = err });
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getReadyState(self: *const IDBRequest) ReadyState {
|
||||
|
||||
@@ -24,6 +24,7 @@ const js = @import("../../../js/js.zig");
|
||||
const Page = @import("../../../Page.zig");
|
||||
const Event = @import("../../Event.zig");
|
||||
const EventTarget = @import("../../EventTarget.zig");
|
||||
const DOMException = @import("../../DOMException.zig");
|
||||
|
||||
const idb = @import("idb.zig");
|
||||
const Engine = @import("Engine.zig");
|
||||
@@ -80,6 +81,7 @@ _begun: bool = false,
|
||||
_settled: bool = false,
|
||||
_aborted: bool = false,
|
||||
_committing: bool = false,
|
||||
_error: ?anyerror = null,
|
||||
_gate_waiter: Engine.GateWaiter,
|
||||
// A transaction is only active for one execution of a Scheduler's task. We
|
||||
// capture the scheduler's generation here and reject any request made in a
|
||||
@@ -230,13 +232,20 @@ pub fn commit(self: *IDBTransaction, exec: *Execution) !void {
|
||||
}
|
||||
}
|
||||
|
||||
// JS-facing, no reason
|
||||
pub fn abort(self: *IDBTransaction, exec: *Execution) !void {
|
||||
return self.abortWith(exec, null);
|
||||
}
|
||||
|
||||
// Internal, optional reason
|
||||
pub fn abortWith(self: *IDBTransaction, exec: *Execution, reason: ?anyerror) error{InvalidStateError}!void {
|
||||
if (self._settled or self._committing) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
|
||||
self._aborted = true;
|
||||
self._settled = true;
|
||||
self._error = reason;
|
||||
|
||||
if (self._begun) {
|
||||
self._engine.rollback();
|
||||
@@ -417,6 +426,15 @@ pub fn getObjectStoreNames(self: *IDBTransaction, exec: *Execution) !*DOMStringL
|
||||
return list;
|
||||
}
|
||||
|
||||
pub fn getError(self: *const IDBTransaction) ?DOMException {
|
||||
const err = self._error orelse return null;
|
||||
const mapped: anyerror = switch (err) {
|
||||
error.Constraint => error.ConstraintError,
|
||||
else => err,
|
||||
};
|
||||
return DOMException.fromError(mapped) orelse DOMException.init(null, "UnknownError");
|
||||
}
|
||||
|
||||
pub fn getOnComplete(self: *const IDBTransaction) ?js.Function.Global {
|
||||
return self._on_complete;
|
||||
}
|
||||
@@ -629,6 +647,7 @@ pub const JsApi = struct {
|
||||
pub const durability = bridge.accessor(IDBTransaction.getDurability, null, .{});
|
||||
pub const db = bridge.accessor(IDBTransaction.getDb, null, .{});
|
||||
pub const objectStoreNames = bridge.accessor(IDBTransaction.getObjectStoreNames, null, .{});
|
||||
pub const @"error" = bridge.accessor(IDBTransaction.getError, null, .{ .null_as_undefined = true });
|
||||
pub const objectStore = bridge.function(IDBTransaction.objectStore, .{});
|
||||
pub const abort = bridge.function(IDBTransaction.abort, .{});
|
||||
pub const commit = bridge.function(IDBTransaction.commit, .{});
|
||||
|
||||
Reference in New Issue
Block a user