diff --git a/src/browser/tests/indexeddb.html b/src/browser/tests/indexeddb.html index 7b4a33c66..8fb02c003 100644 --- a/src/browser/tests/indexeddb.html +++ b/src/browser/tests/indexeddb.html @@ -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)); } + + + + + + diff --git a/src/browser/webapi/storage/idb/Engine.zig b/src/browser/webapi/storage/idb/Engine.zig index 8bd7060e8..6638ca5ca 100644 --- a/src/browser/webapi/storage/idb/Engine.zig +++ b/src/browser/webapi/storage/idb/Engine.zig @@ -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(); diff --git a/src/browser/webapi/storage/idb/IDBIndex.zig b/src/browser/webapi/storage/idb/IDBIndex.zig index b9023393f..5fb92b9d0 100644 --- a/src/browser/webapi/storage/idb/IDBIndex.zig +++ b/src/browser/webapi/storage/idb/IDBIndex.zig @@ -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 }); diff --git a/src/browser/webapi/storage/idb/IDBKeyRange.zig b/src/browser/webapi/storage/idb/IDBKeyRange.zig index 25a0d3256..0107d2693 100644 --- a/src/browser/webapi/storage/idb/IDBKeyRange.zig +++ b/src/browser/webapi/storage/idb/IDBKeyRange.zig @@ -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); diff --git a/src/browser/webapi/storage/idb/IDBObjectStore.zig b/src/browser/webapi/storage/idb/IDBObjectStore.zig index 58d688d15..25f29fa69 100644 --- a/src/browser/webapi/storage/idb/IDBObjectStore.zig +++ b/src/browser/webapi/storage/idb/IDBObjectStore.zig @@ -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, .{ }); diff --git a/src/browser/webapi/storage/idb/IDBRecord.zig b/src/browser/webapi/storage/idb/IDBRecord.zig new file mode 100644 index 000000000..35a1e04dc --- /dev/null +++ b/src/browser/webapi/storage/idb/IDBRecord.zig @@ -0,0 +1,79 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +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, .{}); +}; diff --git a/src/browser/webapi/storage/idb/IDBRequest.zig b/src/browser/webapi/storage/idb/IDBRequest.zig index b7e1b9267..1b90fe035 100644 --- a/src/browser/webapi/storage/idb/IDBRequest.zig +++ b/src/browser/webapi/storage/idb/IDBRequest.zig @@ -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), diff --git a/src/browser/webapi/storage/idb/IDBTransaction.zig b/src/browser/webapi/storage/idb/IDBTransaction.zig index de743c03c..64727b21f 100644 --- a/src/browser/webapi/storage/idb/IDBTransaction.zig +++ b/src/browser/webapi/storage/idb/IDBTransaction.zig @@ -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; } } diff --git a/src/browser/webapi/storage/idb/idb.zig b/src/browser/webapi/storage/idb/idb.zig index 2df0695c7..65071aa6d 100644 --- a/src/browser/webapi/storage/idb/idb.zig +++ b/src/browser/webapi/storage/idb/idb.zig @@ -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,