mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 17:55:59 -04:00
idb: add IDBRecord
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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, .{ });
|
||||
|
||||
79
src/browser/webapi/storage/idb/IDBRecord.zig
Normal file
79
src/browser/webapi/storage/idb/IDBRecord.zig
Normal 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, .{});
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user