idb: add IDBRecord

This commit is contained in:
Karl Seguin
2026-07-01 13:53:55 +08:00
parent 1dcd0dc6fb
commit fb39792f2d
9 changed files with 364 additions and 84 deletions

View File

@@ -925,7 +925,6 @@
const db = e.target.result;
const tx = db.transaction("s", "readwrite");
tx.objectStore("s").clear();
// oncomplete must be set before commit(): commit settles synchronously.
tx.oncomplete = () => {
const c = db.transaction("s", "readonly").objectStore("s").count();
c.onsuccess = () => state.resolve(c.result);
@@ -997,4 +996,80 @@
await state.done((count) => testing.expectEqual(2, count));
}
</script>
<!-- After commit(), the transaction is "committing": abort() must throw
InvalidStateError (commit wins), and the writes still land. -->
<script id="commit_then_abort" type=module>
{
const state = await testing.async();
const open = indexedDB.open("commit-abort-db", 1);
open.onupgradeneeded = (e) => e.target.result.createObjectStore("s");
open.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction("s", "readwrite");
tx.objectStore("s").put("v", "k");
tx.commit();
let threw = null;
try { tx.abort(); } catch (err) { threw = err.name; }
tx.oncomplete = () => {
const g = db.transaction("s", "readonly").objectStore("s").get("k");
g.onsuccess = () => state.resolve({ threw, value: g.result });
};
};
await state.done((got) => {
testing.expectEqual("InvalidStateError", got.threw);
testing.expectEqual("v", got.value); // commit won, the write persisted
});
}
</script>
<!-- getAllRecords + the IDBGetAllOptions dictionary (query/count/direction),
returning IDBRecord {key, primaryKey, value}. Also checks the dictionary
overload of getAll/getAllKeys, and that commit() delivers asynchronously (the
request handler is attached *after* commit()). -->
<script id="get_all_records" type=module>
{
const state = await testing.async();
const open = indexedDB.open("records-db", 1);
open.onupgradeneeded = (e) => {
const store = e.target.result.createObjectStore("s");
for (const k of ["a", "b", "c", "d"]) store.add("v" + k, k);
};
open.onsuccess = (e) => {
const db = e.target.result;
const out = {};
// getAllRecords with a bound range, reversed.
const tx = db.transaction("s", "readonly");
const r1 = tx.objectStore("s").getAllRecords(
{ query: IDBKeyRange.bound("b", "d"), direction: "prev", count: 2 });
r1.onsuccess = () => { out.records = r1.result.map((rec) => [rec.key, rec.primaryKey, rec.value]); };
// getAll via the options-dictionary overload.
const r2 = tx.objectStore("s").getAll({ count: 2 });
r2.onsuccess = () => { out.values = r2.result; };
// commit() is async: this success handler is attached after commit().
const r3 = tx.objectStore("s").getAllKeys();
tx.commit();
r3.onsuccess = () => { out.keys = r3.result; state.resolve(out); };
};
await state.done((got) => {
// prev over [b,d] => d,c,b; count 2 => d,c. record = [key, primaryKey, value].
testing.expectEqual(2, got.records.length);
testing.expectEqual("d", got.records[0][0]);
testing.expectEqual("d", got.records[0][1]);
testing.expectEqual("vd", got.records[0][2]);
testing.expectEqual("c", got.records[1][0]);
// getAll({count:2}) => first two values.
testing.expectEqual(2, got.values.length);
testing.expectEqual("va", got.values[0]);
// getAllKeys still delivered after the post-commit() handler was attached.
testing.expectEqual(4, got.keys.length);
testing.expectEqual("a", got.keys[0]);
});
}
</script>
</body>

View File

@@ -422,33 +422,38 @@ pub fn indexCountRange(self: *const Engine, index_id: i64, b: Bounds) !i64 {
return (try self.conn.scalar(i64, sql, .{ index_id, b.lower, b.upper })) orelse 0;
}
// Open an index getAll/getAllKeys cursor (.value joins the store, .key is the
// primary key). The JS layer streams rows straight into a JS array.
pub fn indexGetAllRangeRows(self: *const Engine, object_store_id: i64, index_id: i64, b: Bounds, column: Column, limit_: ?u32) !Sqlite.Rows {
// Full (index key, primary key, value) rows over an index range, ordered by index
// key (then primary key), ascending or — for a reverse direction — descending, and
// limited to `limit_` rows (null = no limit).
//
// When unique is true, we use sqlite3 bare aggregates to resolve the uniqueness
// Note that min(ir.primary_key) is used regardless of `reverse` because
// uniqueness is always based on ascending order.
pub fn indexGetAllRows(self: *const Engine, object_store_id: i64, index_id: i64, b: Bounds, reverse: bool, unique: bool, limit_: ?u32) !Sqlite.Rows {
const ops = rangeOps(b);
const limit: i64 = if (limit_) |c| @intCast(c) else -1;
var buf: [512]u8 = undefined;
if (column == .value) {
const sql = try std.fmt.bufPrint(&buf,
\\ select r.value
\\ from idb_index_records ir
\\ join idb_records r on r.object_store_id = ?1 and r.key = ir.primary_key
\\ where ir.index_id = ?2 and ir.key {s} ?3 and ir.key {s} ?4
\\ order by ir.key, ir.primary_key
\\ limit ?5
, .{ ops.lo, ops.hi });
return self.conn.rows(sql, .{ object_store_id, index_id, b.lower, b.upper, limit });
}
const order = if (reverse) "desc" else "asc";
var buf: [640]u8 = undefined;
const sql = try std.fmt.bufPrint(&buf,
\\ select primary_key
\\ from idb_index_records
\\ where index_id = ?1 and key {s} ?2 and key {s} ?3
\\ order by key, primary_key
\\ limit ?4
, .{ ops.lo, ops.hi });
return self.conn.rows(sql, .{ index_id, b.lower, b.upper, limit });
\\ select ir.key, {s}, r.value
\\ from idb_index_records ir
\\ join idb_records r on r.object_store_id = ?1 and r.key = ir.primary_key
\\ where ir.index_id = ?2 and ir.key {s} ?3 and ir.key {s} ?4
\\ {s}
\\ order by ir.key {s}{s}
\\ limit ?5
, .{
if (unique) "min(ir.primary_key)" else "ir.primary_key",
ops.lo,
ops.hi,
if (unique) "group by ir.key" else "",
order,
if (unique) "" else if (reverse) ", ir.primary_key desc" else ", ir.primary_key asc",
});
// SQLite treats a negative LIMIT as "no limit".
const limit: i64 = if (limit_) |c| @intCast(c) else -1;
return self.conn.rows(sql, .{ object_store_id, index_id, b.lower, b.upper, limit });
}
pub const IndexCursorRecord = struct {
@@ -603,6 +608,15 @@ pub fn getAllRangeRows(self: *const Engine, object_store_id: i64, b: Bounds, col
return self.conn.rows(sql, .{ object_store_id, b.lower, b.upper, limit });
}
// Full (key, value) rows over a store range, ordered by key
pub fn getAllRows(self: *const Engine, object_store_id: i64, b: Bounds, reverse: bool, limit_: ?u32) !Sqlite.Rows {
var buf: [256]u8 = undefined;
const sql = try rangeSql(&buf, "select key, value", b, if (reverse) " order by key desc limit ?4" else " order by key limit ?4");
// SQLite treats a negative LIMIT as "no limit".
const limit: i64 = if (limit_) |c| @intCast(c) else -1;
return self.conn.rows(sql, .{ object_store_id, b.lower, b.upper, limit });
}
pub fn getAllRange(self: *const Engine, arena: Allocator, object_store_id: i64, b: Bounds, column: Column, limit_: ?u32) ![]const []u8 {
var rows = try self.getAllRangeRows(object_store_id, b, column, limit_);
defer rows.deinit();

View File

@@ -24,6 +24,7 @@ const js = @import("../../../js/js.zig");
const Key = @import("Key.zig");
const Engine = @import("Engine.zig");
const IDBCursor = @import("IDBCursor.zig");
const IDBRecord = @import("IDBRecord.zig");
const IDBRequest = @import("IDBRequest.zig");
const IDBKeyRange = @import("IDBKeyRange.zig");
const IDBTransaction = @import("IDBTransaction.zig");
@@ -96,23 +97,30 @@ pub fn runGetKey(self: *IDBIndex, request: *IDBRequest, bounds: Engine.Bounds, e
try request.setValue(try Key.decodeToJs(arena, exec.js.local.?, b));
}
pub fn getAll(self: *IDBIndex, query: ?js.Value, count_: ?u32, exec: *Execution) !*IDBRequest {
return self._getAll(query, count_, .value, exec);
pub fn getAll(self: *IDBIndex, query_or_options: ?js.Value, count_: ?u32, exec: *Execution) !*IDBRequest {
return self._getAll(query_or_options, count_, .value, exec);
}
pub fn getAllKeys(self: *IDBIndex, query: ?js.Value, count_: ?u32, exec: *Execution) !*IDBRequest {
return self._getAll(query, count_, .key, exec);
pub fn getAllKeys(self: *IDBIndex, query_or_options: ?js.Value, count_: ?u32, exec: *Execution) !*IDBRequest {
return self._getAll(query_or_options, count_, .key, exec);
}
fn _getAll(self: *IDBIndex, query: ?js.Value, count_: ?u32, column: Engine.Column, exec: *Execution) !*IDBRequest {
pub fn getAllRecords(self: *IDBIndex, options: ?js.Value, exec: *Execution) !*IDBRequest {
const t = try self.txn();
const bounds = try IDBKeyRange.resolveQuery(exec.arena, query, exec);
const args = try IDBKeyRange.resolveGetAllOptions(exec.arena, options, exec);
const request = try t.newRequest();
return request.submit(.{ .index_get_all = .{ .index = self, .bounds = bounds, .column = column, .count = count_ } }, exec);
return request.submit(.{ .index_get_all = .{ .index = self, .args = args, .mode = .record } }, exec);
}
pub fn runGetAll(self: *IDBIndex, request: *IDBRequest, bounds: Engine.Bounds, column: Engine.Column, count_: ?u32, exec: *Execution) !void {
const arr = self.collectAll(bounds, count_, column, exec) catch |err| {
fn _getAll(self: *IDBIndex, query_or_options: ?js.Value, count_: ?u32, mode: IDBObjectStore.GetAllMode, exec: *Execution) !*IDBRequest {
const t = try self.txn();
const args = try IDBKeyRange.resolveGetAll(exec.arena, query_or_options, count_, exec);
const request = try t.newRequest();
return request.submit(.{ .index_get_all = .{ .index = self, .args = args, .mode = mode } }, exec);
}
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 });
request.setError(err);
return;
@@ -120,26 +128,34 @@ pub fn runGetAll(self: *IDBIndex, request: *IDBRequest, bounds: Engine.Bounds, c
try request.setValue(arr);
}
// Stream an index getAll/getAllKeys straight into a JS array: .value rows are
// the joined records (deserialized), .key rows are primary keys (decoded).
fn collectAll(self: *IDBIndex, bounds: Engine.Bounds, count_: ?u32, column: Engine.Column, exec: *Execution) !js.Value {
// Build a JS array of an index result: for .value the joined record value; for .key
// the primary key; for .record the (index key, primary key, value) triple.
fn collectAll(self: *IDBIndex, args: IDBKeyRange.GetAllArgs, mode: IDBObjectStore.GetAllMode, exec: *Execution) !js.Value {
const local = exec.js.local.?;
const arena = exec.call_arena;
const reverse = args.direction == .prev or args.direction == .prevunique;
const unique = args.direction == .nextunique or args.direction == .prevunique;
var rows = try self._engine.indexGetAllRangeRows(self._store._store_id, self._index_id, bounds, column, count_);
var rows = try self._engine.indexGetAllRows(self._store._store_id, self._index_id, args.bounds, reverse, unique, args.count);
defer rows.deinit();
const arr = local.newArray(0);
var i: u32 = 0;
while (try rows.next()) |row| {
const bytes = row.get([]const u8, 0);
const value = if (column == .value) try js.Value.deserialize(local, bytes) else try Key.decodeToJs(arena, local, bytes);
_ = try arr.set(i, value, .{});
_ = try arr.set(i, try rowToValue(exec, mode, row.get([]const u8, 0), row.get([]const u8, 1), row.get([]const u8, 2)), .{});
i += 1;
}
return arr.toValue();
}
fn rowToValue(exec: *Execution, mode: IDBObjectStore.GetAllMode, key: []const u8, primary_key: []const u8, value: []const u8) !js.Value {
const local = exec.js.local.?;
return switch (mode) {
.value => js.Value.deserialize(local, value),
.key => Key.decodeToJs(exec.call_arena, local, primary_key),
.record => IDBRecord.initValue(exec, local, key, primary_key, value),
};
}
pub fn count(self: *IDBIndex, query: ?js.Value, exec: *Execution) !*IDBRequest {
const t = try self.txn();
const bounds = try IDBKeyRange.resolveQuery(exec.arena, query, exec);
@@ -204,6 +220,7 @@ pub const JsApi = struct {
pub const getKey = bridge.function(IDBIndex.getKey, .{ .dom_exception = true });
pub const getAll = bridge.function(IDBIndex.getAll, .{ .dom_exception = true });
pub const getAllKeys = bridge.function(IDBIndex.getAllKeys, .{ .dom_exception = true });
pub const getAllRecords = bridge.function(IDBIndex.getAllRecords, .{ .dom_exception = true });
pub const count = bridge.function(IDBIndex.count, .{ .dom_exception = true });
pub const openCursor = bridge.function(IDBIndex.openCursor, .{ .dom_exception = true });
pub const openKeyCursor = bridge.function(IDBIndex.openKeyCursor, .{ .dom_exception = true });

View File

@@ -22,9 +22,11 @@ const js = @import("../../../js/js.zig");
const Key = @import("Key.zig");
const Engine = @import("Engine.zig");
const IDBCursor = @import("IDBCursor.zig");
const Allocator = std.mem.Allocator;
const Execution = js.Execution;
const Direction = IDBCursor.Direction;
const IDBKeyRange = @This();
@@ -121,20 +123,84 @@ pub fn toBounds(self: *const IDBKeyRange) Engine.Bounds {
};
}
// Resolve a get/getAll/count/delete query argument — a key, an IDBKeyRange, or
// nothing (null/undefined) — into engine bounds. A bare key becomes a point
// range; the encoded key is allocated in `arena`.
pub fn resolveQuery(arena: Allocator, query: ?js.Value, exec: *Execution) !Engine.Bounds {
const q = query orelse return Engine.Bounds.unbounded();
if (q.isNullOrUndefined()) {
return Engine.Bounds.unbounded();
}
if (exec.js.local.?.jsValueToZig(*IDBKeyRange, q)) |range| {
return range.toBounds();
} else |_| {}
return Engine.Bounds.point(try Key.encodeValue(arena, q));
}
pub const GetAllArgs = struct {
bounds: Engine.Bounds,
direction: Direction = .next,
count: ?u32 = null,
};
// getAll/getAllKeys take either the legacy (query, count) pair or a single
// IDBGetAllOptions dictionary as the first argument. Per Web IDL, the first
// argument is the options dictionary when it's an object that is not itself a key
// or an IDBKeyRange; otherwise it's the query and `count` is the count.
pub fn resolveGetAll(arena: Allocator, query_or_options: ?js.Value, count: ?u32, exec: *Execution) !GetAllArgs {
if (query_or_options) |v| {
if (isOptionsDictionary(v, exec)) {
return resolveOptions(arena, v, exec);
}
}
return .{ .bounds = try resolveQuery(arena, query_or_options, exec), .count = normalizeCount(count) };
}
// getAllRecords always takes an IDBGetAllOptions dictionary (or nothing).
pub fn resolveGetAllOptions(arena: Allocator, options: ?js.Value, exec: *Execution) !GetAllArgs {
const v = options orelse return .{ .bounds = Engine.Bounds.unbounded() };
if (v.isNullOrUndefined()) {
return .{ .bounds = Engine.Bounds.unbounded() };
}
return resolveOptions(arena, v, exec);
}
fn isOptionsDictionary(v: js.Value, exec: *Execution) bool {
if (!v.isObject()) {
return false;
}
// Objects that are themselves valid keys (array / binary / date) or a key
// range are queries, not the options dictionary.
if (v.isArray() or v.isArrayBuffer() or v.isTypedArray() or v.isDate()) {
return false;
}
if (exec.js.local.?.jsValueToZig(*IDBKeyRange, v)) |_| return false else |_| {}
return true;
}
fn resolveOptions(arena: Allocator, v: js.Value, exec: *Execution) !GetAllArgs {
const obj = v.toObject();
var args: GetAllArgs = .{ .bounds = try resolveQuery(arena, try obj.get("query"), exec) };
const count = try obj.get("count");
if (!count.isNullOrUndefined()) {
args.count = normalizeCount(try count.toU32());
}
const direction = try obj.get("direction");
if (!direction.isNullOrUndefined()) {
args.direction = try direction.toZig(Direction);
}
return args;
}
fn normalizeCount(count: ?u32) ?u32 {
const c = count orelse return null;
return if (c == 0) null else c;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(IDBKeyRange);

View File

@@ -25,6 +25,7 @@ const Key = @import("Key.zig");
const Engine = @import("Engine.zig");
const IDBIndex = @import("IDBIndex.zig");
const IDBCursor = @import("IDBCursor.zig");
const IDBRecord = @import("IDBRecord.zig");
const IDBRequest = @import("IDBRequest.zig");
const IDBKeyRange = @import("IDBKeyRange.zig");
const IDBTransaction = @import("IDBTransaction.zig");
@@ -156,20 +157,35 @@ pub fn runCount(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Boun
try request.setValue(try exec.js.local.?.zigValueToJs(n, .{}));
}
pub fn getAll(self: *IDBObjectStore, query: ?js.Value, count_: ?u32, exec: *Execution) !*IDBRequest {
return self._getAll(query, count_, .value, exec);
// What a getAll/getAllKeys/getAllRecords produces
pub const GetAllMode = enum { value, key, record };
pub fn getAll(self: *IDBObjectStore, query_or_options: ?js.Value, count_: ?u32, exec: *Execution) !*IDBRequest {
return self._getAll(query_or_options, count_, .value, exec);
}
fn _getAll(self: *IDBObjectStore, query: ?js.Value, count_: ?u32, column: Engine.Column, exec: *Execution) !*IDBRequest {
pub fn getAllKeys(self: *IDBObjectStore, query_or_options: ?js.Value, count_: ?u32, exec: *Execution) !*IDBRequest {
return self._getAll(query_or_options, count_, .key, exec);
}
pub fn getAllRecords(self: *IDBObjectStore, options: ?js.Value, exec: *Execution) !*IDBRequest {
const txn = self._txn orelse return error.TransactionInactiveError;
try txn.assertActive();
const bounds = try IDBKeyRange.resolveQuery(exec.arena, query, exec);
const args = try IDBKeyRange.resolveGetAllOptions(exec.arena, options, exec);
const request = try txn.newRequest();
return request.submit(.{ .store_get_all = .{ .store = self, .bounds = bounds, .column = column, .count = count_ } }, exec);
return request.submit(.{ .store_get_all = .{ .store = self, .args = args, .mode = .record } }, exec);
}
pub fn runGetAll(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Bounds, column: Engine.Column, count_: ?u32, exec: *Execution) !void {
const arr = self.collectAll(exec, bounds, column, count_) catch |err| {
fn _getAll(self: *IDBObjectStore, query_or_options: ?js.Value, count_: ?u32, mode: GetAllMode, exec: *Execution) !*IDBRequest {
const txn = self._txn orelse return error.TransactionInactiveError;
try txn.assertActive();
const args = try IDBKeyRange.resolveGetAll(exec.arena, query_or_options, count_, exec);
const request = try txn.newRequest();
return request.submit(.{ .store_get_all = .{ .store = self, .args = args, .mode = mode } }, exec);
}
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 });
request.setError(err);
return;
@@ -177,21 +193,26 @@ pub fn runGetAll(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Bou
try request.setValue(arr);
}
// Stream a getAll/getAllKeys result straight into a JS array: .value rows
// deserialize, .key rows decode — nothing is copied out of sqlite first.
fn collectAll(self: *IDBObjectStore, exec: *Execution, bounds: Engine.Bounds, column: Engine.Column, count_: ?u32) !js.Value {
fn collectAll(self: *IDBObjectStore, exec: *Execution, args: IDBKeyRange.GetAllArgs, mode: GetAllMode) !js.Value {
const local = exec.js.local.?;
const arena = exec.call_arena;
// A store's primary keys are unique, so nextunique/prevunique are just
// next/prev here — no de-duplication needed. For an object store the key is the
// primary key.
const reverse = args.direction == .prev or args.direction == .prevunique;
var rows = try self._engine.getAllRangeRows(self._store_id, bounds, column, count_);
var rows = try self._engine.getAllRows(self._store_id, args.bounds, reverse, args.count);
defer rows.deinit();
const arr = local.newArray(0);
var i: u32 = 0;
while (try rows.next()) |row| {
const bytes = row.get([]const u8, 0);
const value = if (column == .value) try js.Value.deserialize(local, bytes) else try Key.decodeToJs(arena, local, bytes);
_ = try arr.set(i, value, .{});
const key = row.get([]const u8, 0);
const out: js.Value = switch (mode) {
.value => try js.Value.deserialize(local, row.get([]const u8, 1)),
.key => try Key.decodeToJs(exec.call_arena, local, key),
.record => try IDBRecord.initValue(exec, local, key, key, row.get([]const u8, 1)),
};
_ = try arr.set(i, out, .{});
i += 1;
}
return arr.toValue();
@@ -216,10 +237,6 @@ pub fn runGetKey(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Bou
try request.setValue(try Key.decodeToJs(arena, exec.js.local.?, bytes));
}
pub fn getAllKeys(self: *IDBObjectStore, query: ?js.Value, count_: ?u32, exec: *Execution) !*IDBRequest {
return self._getAll(query, count_, .key, exec);
}
pub fn openCursor(self: *IDBObjectStore, query: ?js.Value, direction: ?IDBCursor.Direction, exec: *Execution) !*IDBRequest {
const bounds = try IDBKeyRange.resolveQuery(exec.arena, query, exec);
return IDBCursor.open(self, bounds, direction orelse .next, false, exec);
@@ -521,17 +538,18 @@ pub const JsApi = struct {
pub const keyPath = bridge.accessor(IDBObjectStore.getKeyPath, null, .{});
pub const autoIncrement = bridge.accessor(IDBObjectStore.getAutoIncrement, null, .{});
pub const transaction = bridge.accessor(IDBObjectStore.getTransaction, null, .{ .null_as_undefined = true });
pub const add = bridge.function(IDBObjectStore.add, .{ });
pub const put = bridge.function(IDBObjectStore.put, .{ });
pub const get = bridge.function(IDBObjectStore.get, .{ });
pub const getKey = bridge.function(IDBObjectStore.getKey, .{ });
pub const delete = bridge.function(IDBObjectStore.delete, .{ });
pub const clear = bridge.function(IDBObjectStore.clear, .{ });
pub const count = bridge.function(IDBObjectStore.count, .{ });
pub const getAll = bridge.function(IDBObjectStore.getAll, .{ });
pub const getAllKeys = bridge.function(IDBObjectStore.getAllKeys, .{ });
pub const openCursor = bridge.function(IDBObjectStore.openCursor, .{ });
pub const openKeyCursor = bridge.function(IDBObjectStore.openKeyCursor, .{ });
pub const add = bridge.function(IDBObjectStore.add, .{});
pub const put = bridge.function(IDBObjectStore.put, .{});
pub const get = bridge.function(IDBObjectStore.get, .{});
pub const getKey = bridge.function(IDBObjectStore.getKey, .{});
pub const delete = bridge.function(IDBObjectStore.delete, .{});
pub const clear = bridge.function(IDBObjectStore.clear, .{});
pub const count = bridge.function(IDBObjectStore.count, .{});
pub const getAll = bridge.function(IDBObjectStore.getAll, .{});
pub const getAllKeys = bridge.function(IDBObjectStore.getAllKeys, .{});
pub const getAllRecords = bridge.function(IDBObjectStore.getAllRecords, .{});
pub const openCursor = bridge.function(IDBObjectStore.openCursor, .{});
pub const openKeyCursor = bridge.function(IDBObjectStore.openKeyCursor, .{});
pub const indexNames = bridge.accessor(IDBObjectStore.getIndexNames, null, .{});
pub const createIndex = bridge.function(IDBObjectStore.createIndex, .{ });
pub const deleteIndex = bridge.function(IDBObjectStore.deleteIndex, .{ });

View File

@@ -0,0 +1,79 @@
// Copyright (C) 2023-2026 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/js.zig");
const Key = @import("Key.zig");
const Execution = js.Execution;
const IDBRecord = @This();
// A single result of getAllRecords: the (index or store) key, the primary/store
// key, and the value. Stored encoded/serialized and decoded lazily on access
// (mirroring IDBKeyRange). The bytes are page-arena lived — the record is handed
// back inside a JS array the page may retain past the request.
_key: []const u8,
_primary_key: []const u8,
_value: []const u8,
pub fn init(exec: *Execution, key: []const u8, primary_key: []const u8, value: []const u8) !*IDBRecord {
return exec._factory.create(IDBRecord{
._key = key,
._primary_key = primary_key,
._value = value,
});
}
// Build a record from borrowed bytes — duped onto the page arena so the record
// outlives this call — and return its JS value.
pub fn initValue(exec: *Execution, local: *const js.Local, key: []const u8, primary_key: []const u8, value: []const u8) !js.Value {
const record = try init(
exec,
try exec.arena.dupe(u8, key),
try exec.arena.dupe(u8, primary_key),
try exec.arena.dupe(u8, value),
);
return local.zigValueToJs(record, .{});
}
pub fn getKey(self: *const IDBRecord, exec: *Execution) !js.Value {
return Key.decodeToJs(exec.call_arena, exec.js.local.?, self._key);
}
pub fn getPrimaryKey(self: *const IDBRecord, exec: *Execution) !js.Value {
return Key.decodeToJs(exec.call_arena, exec.js.local.?, self._primary_key);
}
pub fn getValue(self: *const IDBRecord, exec: *Execution) !js.Value {
return js.Value.deserialize(exec.js.local.?, self._value);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(IDBRecord);
pub const Meta = struct {
pub const name = "IDBRecord";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const key = bridge.accessor(IDBRecord.getKey, null, .{});
pub const primaryKey = bridge.accessor(IDBRecord.getPrimaryKey, null, .{});
pub const value = bridge.accessor(IDBRecord.getValue, null, .{});
};

View File

@@ -30,6 +30,7 @@ const Engine = @import("Engine.zig");
const IDBIndex = @import("IDBIndex.zig");
const IDBCursor = @import("IDBCursor.zig");
const IDBDatabase = @import("IDBDatabase.zig");
const IDBKeyRange = @import("IDBKeyRange.zig");
const IDBObjectStore = @import("IDBObjectStore.zig");
const IDBTransaction = @import("IDBTransaction.zig");
const IDBVersionChangeEvent = @import("IDBVersionChangeEvent.zig");
@@ -207,10 +208,10 @@ pub const Operation = union(enum) {
cursor_delete: CursorDelete,
const StoreQuery = struct { store: *IDBObjectStore, bounds: Engine.Bounds };
const StoreGetAll = struct { store: *IDBObjectStore, bounds: Engine.Bounds, column: Engine.Column, count: ?u32 };
const StoreGetAll = struct { store: *IDBObjectStore, args: IDBKeyRange.GetAllArgs, mode: IDBObjectStore.GetAllMode };
const StoreWrite = struct { store: *IDBObjectStore, kind: IDBObjectStore.WriteKind, value: js.Value.Global, key: IDBObjectStore.PreparedKey };
const IndexQuery = struct { index: *IDBIndex, bounds: Engine.Bounds };
const IndexGetAll = struct { index: *IDBIndex, bounds: Engine.Bounds, column: Engine.Column, count: ?u32 };
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.Global };
const CursorDelete = struct { cursor: *IDBCursor, key: []const u8 };
@@ -234,7 +235,6 @@ pub fn submit(self: *IDBRequest, op: Operation, exec: *Execution) !*IDBRequest {
return self;
}
pub fn execute(self: *IDBRequest, exec: *Execution) !void {
const op = self._op;
if (op == .none) {
@@ -249,14 +249,14 @@ pub fn execute(self: *IDBRequest, exec: *Execution) !void {
.none => unreachable,
.store_get => |o| try o.store.runGet(self, o.bounds, exec),
.store_get_key => |o| try o.store.runGetKey(self, o.bounds, exec),
.store_get_all => |o| try o.store.runGetAll(self, o.bounds, o.column, o.count, exec),
.store_get_all => |o| try o.store.runGetAll(self, o.args, o.mode, exec),
.store_count => |o| try o.store.runCount(self, o.bounds, exec),
.store_delete => |o| try o.store.runDelete(self, o.bounds, exec),
.store_clear => |store| try store.runClear(self, exec),
.store_write => |o| try o.store.runWrite(self, o.kind, o.value, o.key, exec),
.index_get => |o| try o.index.runGet(self, o.bounds, exec),
.index_get_key => |o| try o.index.runGetKey(self, o.bounds, exec),
.index_get_all => |o| try o.index.runGetAll(self, o.bounds, o.column, o.count, exec),
.index_get_all => |o| try o.index.runGetAll(self, o.args, o.mode, exec),
.index_count => |o| try o.index.runCount(self, o.bounds, exec),
.cursor_iterate => |o| try o.cursor.runIterate(o.seek, o.offset, exec),
.cursor_update => |o| try o.cursor.runUpdate(self, o.key, o.value, exec),

View File

@@ -52,6 +52,7 @@ _requests: std.ArrayList(*IDBRequest) = .empty,
_begun: bool = false,
_settled: bool = false,
_aborted: bool = false,
_committing: bool = false,
_on_complete: ?js.Function.Global = null,
_on_error: ?js.Function.Global = null,
@@ -122,11 +123,18 @@ pub fn commit(self: *IDBTransaction, exec: *Execution) !void {
if (self._settled) {
return error.InvalidStateError;
}
self.settle(exec);
if (self._mode == .versionchange) {
self.settle(exec);
} else {
// The drain is already scheduled and will settle us; just enter the
// "committing" state. It isn't _settled yet, so we can't use that flag
self._committing = true;
}
}
pub fn abort(self: *IDBTransaction, exec: *Execution) !void {
if (self._settled) {
if (self._settled or self._committing) {
return error.InvalidStateError;
}
@@ -166,9 +174,10 @@ pub fn settle(self: *IDBTransaction, exec: *Execution) void {
self.fire(exec, comptime .wrap("complete"), self._on_complete);
}
// "is this transaction still usable". Once settled, it cannot be used.
// "is this transaction still usable". Once settled or explicitly committing, it no
// longer accepts new requests.
pub fn assertActive(self: *const IDBTransaction) !void {
if (self._settled) {
if (self._settled or self._committing) {
return error.TransactionInactiveError;
}
}

View File

@@ -23,6 +23,7 @@ pub const Engine = @import("Engine.zig");
pub const Manager = @import("Manager.zig");
pub const IDBFactory = @import("IDBFactory.zig");
pub const IDBRecord = @import("IDBRecord.zig");
pub const IDBRequest = @import("IDBRequest.zig");
pub const IDBCursor = @import("IDBCursor.zig");
pub const IDBIndex = @import("IDBIndex.zig");
@@ -36,6 +37,7 @@ pub const IDBVersionChangeEvent = @import("IDBVersionChangeEvent.zig");
pub fn registerTypes() []const type {
return &.{
IDBFactory,
IDBRecord,
IDBRequest,
IDBCursor,
IDBIndex,