idb: reject operations on deleted datastore

This commit is contained in:
Karl Seguin committed 2026-07-03 12:47:41 +08:00
1 parent 5993e9889a
commit 55894afd65
11 files changed
+120 -59

No files matched your search

+3
View File
@@ -1000,6 +1000,7 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/XPathResult.zig"),
@import("../webapi/XPathExpression.zig"),
@import("../webapi/XPathEvaluator.zig"),
@import("../webapi/collections/DOMStringList.zig"),
});
// APIs available on Worker context globals (constructors like URL, Headers, etc.)
@@ -1061,6 +1062,8 @@ pub const WorkerJsApis = flattenTypes(&.{
@import("../webapi/storage/idb/idb.zig"),
@import("../webapi/event/CookieChangeEvent.zig"),
@import("../webapi/BroadcastChannel.zig"),
@import("../webapi/BroadcastChannel.zig"),
@import("../webapi/collections/DOMStringList.zig"),
});
// Master list of ALL JS APIs across all contexts.
-4
View File
@@ -43,9 +43,5 @@ pub fn registerTypes() []const type {
DOMTokenList.KeyIterator,
DOMTokenList.ValueIterator,
DOMTokenList.EntryIterator,
DOMStringList,
DOMStringList.KeyIterator,
DOMStringList.ValueIterator,
DOMStringList.EntryIterator,
};
}
@@ -21,7 +21,6 @@ const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const GenericIterator = @import("iterator.zig").Entry;
pub const KeyIterator = GenericIterator(Iterator, "0");
@@ -31,6 +30,17 @@ pub const EntryIterator = GenericIterator(Iterator, null);
const Execution = js.Execution;
const Allocator = std.mem.Allocator;
// not registered in collections.zig, because this is one of the rare
// collections that's also available in Worker
pub fn registerTypes() []const type {
return &.{
DOMStringList,
DOMStringList.KeyIterator,
DOMStringList.ValueIterator,
DOMStringList.EntryIterator,
};
}
pub const DOMStringList = @This();
_rc: lp.RC(u8) = .{},
@@ -172,6 +172,10 @@ pub fn close(self: *Engine) void {
self.conn.close();
}
pub fn lastError(self: *const Engine) [:0]const u8 {
return self.conn.lastError();
}
pub fn begin(self: *Engine) !void {
return self.conn.exec("begin immediate", .{});
}
+61 -37
View File
@@ -60,6 +60,9 @@ _key: ?[]const u8 = null,
_primary_key: ?[]const u8 = null,
// Current record's serialized value bytes (null when key-only or exhausted).
_value: ?[]const u8 = null,
// The deserialized JS value, cached so repeated `.value` reads return the same
// object (and observe mutations to it). Reset whenever the cursor repositions.
_value_js: ?*js.Value.BareGlobal = null,
// Backing storage for _key/_primary_key/_value, reused across positions so a
// long scan holds one record's worth of memory, not the whole traversal.
@@ -160,7 +163,7 @@ pub fn releaseRef(self: *IDBCursor, page: *Page) void {
// Run the deferred seek, staging the positioned cursor (or null) on the request.
pub fn runIterate(self: *IDBCursor, seek: Seek, offset: u32, exec: *Execution) !void {
self.iterate(seek, offset, exec) catch |err| {
log.warn(.storage, "idb cursor iterate", .{ .err = err });
log.warn(.storage, "idb cursor iterate", .{ .err = err, .sqlite = self._engine.lastError() });
self._request.setError(err);
};
}
@@ -222,21 +225,17 @@ pub fn advance(self: *IDBCursor, count: u32, exec: *Execution) !void {
}
pub fn update(self: *IDBCursor, value: js.Value, exec: *Execution) !*IDBRequest {
if (self._txn._mode == .readonly) {
return error.ReadOnlyError;
}
if (self._txn._settled == true) {
return error.TransactionInactiveError;
}
if (self._got_value == false) {
return error.InvalidStateError;
}
try self.assertCanUpdate();
// The record sits at the primary (store) key, even for an index cursor.
const current_key = self._primary_key orelse return error.InvalidStateError;
// Structured-clone the value now, synchronously: an unserializable value must
// throw DataCloneError from update() itself, not fail later in the drain. The
// stored clone also decouples the record from any later mutation of the arg.
const serialized = value.serialize() catch return error.TryCatchRethrow;
defer serialized.deinit();
// For an in-line store, the value's own key must match the record's key.
if (self._store._key_path) |kp| {
const extracted = Key.evaluatePath(value, kp) orelse return error.DataError;
@@ -246,28 +245,24 @@ pub fn update(self: *IDBCursor, value: js.Value, exec: *Execution) !*IDBRequest
}
}
// Snapshot the record key: the write runs in the drain, by which point a
// `continue` could have moved the cursor's live position (and reused its
// key buffer).
// Snapshot the record key and the serialized clone onto the transaction arena:
// the write runs in the drain, by which point a `continue` could have moved
// the cursor's live position (and reused its key buffer).
const key = try self._txn._arena.dupe(u8, current_key);
const bytes = try self._txn._arena.dupe(u8, serialized.bytes());
const request = try self._txn.newRequest();
const value_global = try self._txn.persist(value);
return request.submit(.{ .cursor_update = .{ .cursor = self, .key = key, .value = value_global } }, exec);
return request.submit(.{ .cursor_update = .{ .cursor = self, .key = key, .value = bytes } }, exec);
}
pub fn runUpdate(self: *IDBCursor, request: *IDBRequest, key: []const u8, value_global: *js.Value.BareGlobal, exec: *Execution) !void {
defer value_global.deinit();
pub fn runUpdate(self: *IDBCursor, request: *IDBRequest, key: []const u8, bytes: []const u8, exec: *Execution) !void {
const local = exec.js.local.?;
const value = value_global.local(local);
const serialized = value.serialize() catch |err| {
const value = js.Value.deserialize(local, bytes) catch |err| {
request.setError(err);
return;
};
defer serialized.deinit();
self._store.writeAt(key, value, serialized.bytes(), exec) catch |err| {
log.warn(.storage, "idb cursor update", .{ .err = err });
self._store.writeAt(key, value, bytes, exec) catch |err| {
log.warn(.storage, "idb cursor update", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
return;
};
@@ -275,17 +270,7 @@ pub fn runUpdate(self: *IDBCursor, request: *IDBRequest, key: []const u8, value_
}
pub fn delete(self: *IDBCursor, exec: *Execution) !*IDBRequest {
if (self._txn._mode == .readonly) {
return error.ReadOnlyError;
}
if (self._txn._settled == true) {
return error.TransactionInactiveError;
}
if (self._got_value == false) {
return error.InvalidStateError;
}
try self.assertCanUpdate();
// Snapshot the record key (see update): the delete runs later, in the drain.
const current_key = self._primary_key orelse return error.InvalidStateError;
@@ -296,7 +281,7 @@ pub fn delete(self: *IDBCursor, exec: *Execution) !*IDBRequest {
pub fn runDelete(self: *IDBCursor, request: *IDBRequest, key: []const u8) !void {
self._store.deleteAt(key) catch |err| {
log.warn(.storage, "idb cursor delete", .{ .err = err });
log.warn(.storage, "idb cursor delete", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
};
}
@@ -359,6 +344,7 @@ fn reiterate(self: *IDBCursor, seek: Seek, offset: u32, exec: *Execution) !void
fn position(self: *IDBCursor, key: []const u8, primary_key: []const u8, value: ?[]const u8) !void {
const arena = self._txn._arena;
self.invalidateValue();
self._key_buf.clearRetainingCapacity();
try self._key_buf.appendSlice(arena, key);
@@ -379,12 +365,36 @@ fn position(self: *IDBCursor, key: []const u8, primary_key: []const u8, value: ?
}
fn exhaust(self: *IDBCursor) void {
self.invalidateValue();
self._key = null;
self._primary_key = null;
self._value = null;
self._request.setNull();
}
// Drop any cached `.value` object; the next read re-deserializes at the new
// position. The persisted slot's handle is reset here so it doesn't pin the old
// value until transaction teardown.
fn invalidateValue(self: *IDBCursor) void {
if (self._value_js) |slot| {
slot.deinit();
self._value_js = null;
}
}
// The deserialized current value, created on first read and cached so repeated
// `.value` accesses return the same JS object (see IDBCursorWithValue.getValue).
pub fn getValueJs(self: *IDBCursor, exec: *Execution) !?js.Value {
const bytes = self._value orelse return null;
const local = exec.js.local.?;
if (self._value_js) |slot| {
return slot.local(local);
}
const value = try js.Value.deserialize(local, bytes);
self._value_js = try self._txn.persist(value);
return value;
}
// validate the state before we can advance/continue
fn prepareIterate(self: *IDBCursor) !void {
if (self._txn._settled == true) {
@@ -398,6 +408,20 @@ fn prepareIterate(self: *IDBCursor) !void {
self._got_value = false;
}
fn assertCanUpdate(self: *IDBCursor) !void {
try self._txn.assertActive();
if (self._txn._mode == .readonly) {
return error.ReadOnlyError;
}
if (self._store._deleted) {
return error.InvalidStateError;
}
if (self._got_value == false) {
return error.InvalidStateError;
}
}
pub const JsApi = struct {
pub const bridge = js.Bridge(IDBCursor);
@@ -33,8 +33,7 @@ pub fn init(cursor: *IDBCursor) !*IDBCursorWithValue {
}
pub fn getValue(self: *const IDBCursorWithValue, exec: *Execution) !?js.Value {
const bytes = self._proto._value orelse return null;
return try js.Value.deserialize(exec.js.local.?, bytes);
return self._proto.getValueJs(exec);
}
pub const JsApi = struct {
@@ -131,7 +131,7 @@ const OpenContext = struct {
}
const upgrading = self.runOpen(engine) catch |err| blk: {
log.warn(.storage, "idb open", .{ .err = err, .name = self.name });
log.warn(.storage, "idb open", .{ .err = err, .name = self.name, .sqlite = engine.lastError() });
self.request.setError(err);
self.request.deliver(self.exec) catch {};
break :blk false;
@@ -350,7 +350,7 @@ const DeleteContext = struct {
defer _ = engine.releaseGate(&self._gate_waiter);
self.runDelete(engine) catch |err| {
log.warn(.storage, "idb deleteDatabase", .{ .err = err, .name = self.name });
log.warn(.storage, "idb deleteDatabase", .{ .err = err, .name = self.name, .sqlite = engine.lastError() });
self.request.setError(err);
self.request.deliver(self.exec) catch {};
};
+4 -4
View File
@@ -82,7 +82,7 @@ pub fn get(self: *IDBIndex, query: js.Value, exec: *Execution) !*IDBRequest {
pub fn runGet(self: *IDBIndex, request: *IDBRequest, bounds: Engine.Bounds, exec: *Execution) !void {
const arena = exec.call_arena;
const bytes = self._engine.indexGetRange(arena, self._store._store_id, self._index_id, bounds) catch |err| {
log.warn(.storage, "idb index get", .{ .err = err });
log.warn(.storage, "idb index get", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
return;
};
@@ -100,7 +100,7 @@ pub fn getKey(self: *IDBIndex, query: js.Value, exec: *Execution) !*IDBRequest {
pub fn runGetKey(self: *IDBIndex, request: *IDBRequest, bounds: Engine.Bounds, exec: *Execution) !void {
const arena = exec.call_arena;
const bytes = self._engine.indexGetKeyRange(arena, self._index_id, bounds) catch |err| {
log.warn(.storage, "idb index getKey", .{ .err = err });
log.warn(.storage, "idb index getKey", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
return;
};
@@ -132,7 +132,7 @@ fn _getAll(self: *IDBIndex, query_or_options: ?js.Value, count_: ?u32, mode: IDB
pub fn runGetAll(self: *IDBIndex, request: *IDBRequest, args: IDBKeyRange.GetAllArgs, mode: IDBObjectStore.GetAllMode, exec: *Execution) !void {
const arr = self.collectAll(args, mode, exec) catch |err| {
log.warn(.storage, "idb index getAll", .{ .err = err });
log.warn(.storage, "idb index getAll", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
return;
};
@@ -176,7 +176,7 @@ pub fn count(self: *IDBIndex, query: ?js.Value, exec: *Execution) !*IDBRequest {
pub fn runCount(self: *IDBIndex, request: *IDBRequest, bounds: Engine.Bounds, exec: *Execution) !void {
const n = self._engine.indexCountRange(self._index_id, bounds) catch |err| {
log.warn(.storage, "idb index count", .{ .err = err });
log.warn(.storage, "idb index count", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
return;
};
@@ -44,6 +44,7 @@ _name: []const u8,
_key_path: ?[]const u8,
_auto_increment: bool,
_txn: *IDBTransaction,
_deleted: bool = false,
// identity map, store.indexes('a') === store.index('a')
_indexes: std.ArrayList(*IDBIndex) = .empty,
@@ -83,6 +84,7 @@ pub fn put(self: *IDBObjectStore, value: js.Value, key: ?js.Value, exec: *Execut
}
pub fn get(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBRequest {
try self.assertLive();
const txn = self._txn;
try txn.assertActive();
const bounds = try IDBKeyRange.resolveQuery(txn._arena, query, exec);
@@ -93,7 +95,7 @@ pub fn get(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBReques
pub fn runGet(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Bounds, exec: *Execution) !void {
const arena = exec.call_arena;
const bytes = self._engine.getRange(arena, self._store_id, bounds) catch |err| {
log.warn(.storage, "idb get", .{ .err = err });
log.warn(.storage, "idb get", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
return;
};
@@ -102,6 +104,7 @@ pub fn runGet(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Bounds
}
pub fn delete(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBRequest {
try self.assertLive();
const txn = self._txn;
if (txn._mode == .readonly) {
return error.ReadOnlyError;
@@ -114,7 +117,7 @@ pub fn delete(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBReq
pub fn runDelete(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Bounds, _: *Execution) !void {
self.deleteBounds(bounds) catch |err| {
log.warn(.storage, "idb delete", .{ .err = err });
log.warn(.storage, "idb delete", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
};
}
@@ -125,6 +128,7 @@ fn deleteBounds(self: *IDBObjectStore, bounds: Engine.Bounds) !void {
}
pub fn clear(self: *IDBObjectStore, exec: *Execution) !*IDBRequest {
try self.assertLive();
const txn = self._txn;
if (txn._mode == .readonly) {
return error.ReadOnlyError;
@@ -136,7 +140,7 @@ pub fn clear(self: *IDBObjectStore, exec: *Execution) !*IDBRequest {
pub fn runClear(self: *IDBObjectStore, request: *IDBRequest, _: *Execution) !void {
self.clearAll() catch |err| {
log.warn(.storage, "idb clear", .{ .err = err });
log.warn(.storage, "idb clear", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
};
}
@@ -147,6 +151,7 @@ fn clearAll(self: *IDBObjectStore) !void {
}
pub fn count(self: *IDBObjectStore, query: ?js.Value, exec: *Execution) !*IDBRequest {
try self.assertLive();
const txn = self._txn;
try txn.assertActive();
const bounds = try IDBKeyRange.resolveQuery(txn._arena, query, exec);
@@ -156,7 +161,7 @@ pub fn count(self: *IDBObjectStore, query: ?js.Value, exec: *Execution) !*IDBReq
pub fn runCount(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Bounds, exec: *Execution) !void {
const n = self._engine.countRange(self._store_id, bounds) catch |err| {
log.warn(.storage, "idb count", .{ .err = err });
log.warn(.storage, "idb count", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
return;
};
@@ -175,6 +180,7 @@ pub fn getAllKeys(self: *IDBObjectStore, query_or_options: ?js.Value, count_: ?u
}
pub fn getAllRecords(self: *IDBObjectStore, options: ?js.Value, exec: *Execution) !*IDBRequest {
try self.assertLive();
const txn = self._txn;
try txn.assertActive();
const args = try IDBKeyRange.resolveGetAllOptions(txn._arena, options, exec);
@@ -183,6 +189,7 @@ pub fn getAllRecords(self: *IDBObjectStore, options: ?js.Value, exec: *Execution
}
fn _getAll(self: *IDBObjectStore, query_or_options: ?js.Value, count_: ?u32, mode: GetAllMode, exec: *Execution) !*IDBRequest {
try self.assertLive();
const txn = self._txn;
try txn.assertActive();
const args = try IDBKeyRange.resolveGetAll(txn._arena, query_or_options, count_, exec);
@@ -192,7 +199,7 @@ fn _getAll(self: *IDBObjectStore, query_or_options: ?js.Value, count_: ?u32, mod
pub fn runGetAll(self: *IDBObjectStore, request: *IDBRequest, args: IDBKeyRange.GetAllArgs, mode: GetAllMode, exec: *Execution) !void {
const arr = self.collectAll(exec, args, mode) catch |err| {
log.warn(.storage, "idb getAll", .{ .err = err });
log.warn(.storage, "idb getAll", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
return;
};
@@ -225,6 +232,7 @@ fn collectAll(self: *IDBObjectStore, exec: *Execution, args: IDBKeyRange.GetAllA
}
pub fn getKey(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBRequest {
try self.assertLive();
const txn = self._txn;
try txn.assertActive();
const bounds = try IDBKeyRange.resolveQuery(txn._arena, query, exec);
@@ -235,7 +243,7 @@ pub fn getKey(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBReq
pub fn runGetKey(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Bounds, exec: *Execution) !void {
const arena = exec.call_arena;
const found = self._engine.getKeyRange(arena, self._store_id, bounds) catch |err| {
log.warn(.storage, "idb getKey", .{ .err = err });
log.warn(.storage, "idb getKey", .{ .err = err, .sqlite = self._engine.lastError() });
request.setError(err);
return;
};
@@ -244,11 +252,13 @@ pub fn runGetKey(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Bou
}
pub fn openCursor(self: *IDBObjectStore, query: ?js.Value, direction: ?IDBCursor.Direction, exec: *Execution) !*IDBRequest {
try self.assertLive();
const bounds = try IDBKeyRange.resolveQuery(self._txn._arena, query, exec);
return IDBCursor.init(self, bounds, direction orelse .next, false, exec);
}
pub fn openKeyCursor(self: *IDBObjectStore, query: ?js.Value, direction: ?IDBCursor.Direction, exec: *Execution) !*IDBRequest {
try self.assertLive();
const bounds = try IDBKeyRange.resolveQuery(self._txn._arena, query, exec);
return IDBCursor.init(self, bounds, direction orelse .next, true, exec);
}
@@ -285,6 +295,7 @@ pub const PreparedKey = union(enum) {
};
fn write(self: *IDBObjectStore, value: js.Value, key_arg: ?js.Value, kind: WriteKind, exec: *Execution) !*IDBRequest {
try self.assertLive();
const txn = self._txn;
if (txn._mode == .readonly) {
return error.ReadOnlyError;
@@ -345,7 +356,7 @@ pub fn runWrite(self: *IDBObjectStore, request: *IDBRequest, kind: WriteKind, va
defer value_global.deinit();
self.writeInner(request, kind, value_global, prepared, exec) catch |err| {
if (err != error.Constraint) {
log.warn(.storage, "idb write", .{ .err = err, .kind = kind });
log.warn(.storage, "idb write", .{ .err = err, .kind = kind, .sqlite = self._engine.lastError() });
}
request.setError(err);
};
@@ -463,10 +474,13 @@ const CreateIndexOptions = struct {
// Only callable during an upgrade (versionchange transaction).
pub fn createIndex(self: *IDBObjectStore, name: []const u8, key_path: []const u8, options: ?CreateIndexOptions, exec: *Execution) !*IDBIndex {
try self.assertLive();
const txn = self._txn;
if (txn._mode != .versionchange) {
return error.InvalidStateError;
}
// Spec order: the transaction-state check precedes the index-name check.
try txn.assertActive();
const opts = options orelse CreateIndexOptions{};
try self._engine.savepoint();
@@ -508,10 +522,13 @@ pub fn createIndex(self: *IDBObjectStore, name: []const u8, key_path: []const u8
// Only callable during an upgrade (versionchange transaction).
pub fn deleteIndex(self: *IDBObjectStore, name: []const u8, _: *Execution) !void {
try self.assertLive();
const txn = self._txn;
if (txn._mode != .versionchange) {
return error.InvalidStateError;
}
// Spec order: the transaction-state check precedes the index-name check.
try txn.assertActive();
self._engine.deleteIndexRow(self._store_id, name) catch |err| switch (err) {
error.NotFound => return error.NotFoundError,
else => return err,
@@ -525,6 +542,7 @@ pub fn deleteIndex(self: *IDBObjectStore, name: []const u8, _: *Execution) !void
}
pub fn index(self: *IDBObjectStore, name: []const u8, _: *Execution) !*IDBIndex {
try self.assertLive();
for (self._indexes.items) |idx| {
if (std.mem.eql(u8, idx._name, name)) {
return idx;
@@ -549,6 +567,12 @@ pub fn getIndexNames(self: *IDBObjectStore, exec: *Execution) !*DOMStringList {
return list;
}
fn assertLive(self: *const IDBObjectStore) !void {
if (self._deleted) {
return error.InvalidStateError;
}
}
pub const JsApi = struct {
pub const bridge = js.Bridge(IDBObjectStore);
@@ -361,7 +361,7 @@ pub const Operation = union(enum) {
const IndexQuery = struct { index: *IDBIndex, bounds: Engine.Bounds };
const IndexGetAll = struct { index: *IDBIndex, args: IDBKeyRange.GetAllArgs, mode: IDBObjectStore.GetAllMode };
const CursorIterate = struct { cursor: *IDBCursor, seek: IDBCursor.Seek, offset: u32 };
const CursorUpdate = struct { cursor: *IDBCursor, key: []const u8, value: *js.Value.BareGlobal };
const CursorUpdate = struct { cursor: *IDBCursor, key: []const u8, value: []const u8 };
const CursorDelete = struct { cursor: *IDBCursor, key: []const u8 };
};
@@ -307,7 +307,7 @@ pub fn settleStep(self: *IDBTransaction, exec: *Execution) bool {
fn commitAndComplete(self: *IDBTransaction, exec: *Execution) void {
if (self._begun) {
self._engine.commit() catch |err| {
log.warn(.storage, "idb commit", .{ .err = err, .sqlite = self._engine.conn.lastError() });
log.warn(.storage, "idb commit", .{ .err = err, .sqlite = self._engine.lastError() });
self._engine.rollback();
self._begun = false;
_ = self._engine.releaseGate(&self._gate_waiter);
@@ -385,6 +385,7 @@ pub fn cacheStore(self: *IDBTransaction, store: *IDBObjectStore) !void {
pub fn uncacheStore(self: *IDBTransaction, name: []const u8) void {
for (self._stores.items, 0..) |store, i| {
if (std.mem.eql(u8, store._name, name)) {
store._deleted = true;
_ = self._stores.swapRemove(i);
return;
}