mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-17 17:22:43 -04:00
indexeddb: WPT conformance
Some low-hanging fruit from the IndexedDB tests. Added IDBDatabase.close() and IDBFactory.databases(). Map count to an f64 which can handle NaNs and such. Plus some reordering so validation happens in the correct order.
This commit is contained in:
12 files changed
+301
-45
No files matched your search
@@ -33,6 +33,7 @@ const Document = @import("webapi/Document.zig");
|
||||
const EventTarget = @import("webapi/EventTarget.zig");
|
||||
const AbortSignal = @import("webapi/AbortSignal.zig");
|
||||
const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig");
|
||||
const IDBRequest = @import("webapi/storage/idb/IDBRequest.zig");
|
||||
const Blob = @import("webapi/Blob.zig");
|
||||
const AbstractRange = @import("webapi/AbstractRange.zig");
|
||||
const DOMRect = @import("webapi/DOMRect.zig");
|
||||
@@ -505,6 +506,12 @@ pub fn xhrEventTarget(_: *const Factory, allocator: Allocator, child: anytype) !
|
||||
).create(allocator, child);
|
||||
}
|
||||
|
||||
pub fn idbOpenRequest(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
return try AutoPrototypeChain(
|
||||
&.{ EventTarget, IDBRequest, @TypeOf(child) },
|
||||
).create(self._slab.allocator(), child);
|
||||
}
|
||||
|
||||
pub fn taskSignal(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
return try AutoPrototypeChain(
|
||||
&.{ EventTarget, AbortSignal, @TypeOf(child) },
|
||||
|
||||
@@ -329,9 +329,9 @@
|
||||
db.deleteObjectStore("temp");
|
||||
};
|
||||
open.onsuccess = (e) => {
|
||||
const tx = e.target.result.transaction("temp", "readonly");
|
||||
// The deleted store is not in the database: transaction() itself throws.
|
||||
let threw = null;
|
||||
try { tx.objectStore("temp"); } catch (err) { threw = err; }
|
||||
try { e.target.result.transaction("temp", "readonly"); } catch (err) { threw = err; }
|
||||
state.resolve(threw && threw.name);
|
||||
};
|
||||
await state.done((name) => {
|
||||
@@ -1189,6 +1189,7 @@
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<<<<<<< HEAD
|
||||
|
||||
<!-- put()/add() structured-clone the value at call time: the record is the
|
||||
clone as of the call (later mutation of the argument is not stored), a
|
||||
@@ -1228,6 +1229,42 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Open requests are IDBOpenDBRequests; result/error throw while pending and
|
||||
error is null (not undefined) after success; databases() lists the
|
||||
origin's databases. -->
|
||||
<script id="request_conformance" type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
const open = indexedDB.open("conformance-db", 2);
|
||||
testing.expectEqual("IDBOpenDBRequest", Object.getPrototypeOf(open).constructor.name);
|
||||
testing.expectEqual(true, open instanceof IDBRequest);
|
||||
testing.expectEqual(true, "onblocked" in open);
|
||||
testing.expectError("InvalidStateError", () => open.result);
|
||||
testing.expectError("InvalidStateError", () => open.error);
|
||||
open.onupgradeneeded = (e) => e.target.result.createObjectStore("s");
|
||||
open.onsuccess = async () => {
|
||||
const db = open.result;
|
||||
const req = db.transaction("s").objectStore("s").get(1);
|
||||
testing.expectError("InvalidStateError", () => req.result);
|
||||
req.onsuccess = async () => {
|
||||
const all = await indexedDB.databases();
|
||||
state.resolve({
|
||||
openError: open.error,
|
||||
openTransaction: open.transaction,
|
||||
reqError: req.error,
|
||||
listed: all.filter((d) => d.name === "conformance-db"),
|
||||
});
|
||||
};
|
||||
};
|
||||
await state.done((got) => {
|
||||
testing.expectEqual(null, got.openError);
|
||||
testing.expectEqual(null, got.openTransaction);
|
||||
testing.expectEqual(null, got.reqError);
|
||||
testing.expectEqual([{ name: "conformance-db", version: 2 }], got.listed);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- A version above 2^32 (Date.now() is a common choice) survives the round
|
||||
trip as a Number; out-of-range versions are a TypeError. -->
|
||||
<script id="open_version_64bit" type=module>
|
||||
@@ -1367,3 +1404,39 @@
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
<!-- transaction() validates up front: closed connection, empty scope, unknown
|
||||
store; a DOMStringList scope (db.objectStoreNames) is accepted; getAll's
|
||||
count is [EnforceRange]; objectStore()/index() throw once the
|
||||
transaction finished. -->
|
||||
<script id="transaction_argument_checks" type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
const open = indexedDB.open("txn-args-db", 1);
|
||||
open.onupgradeneeded = (e) => {
|
||||
const db = e.target.result;
|
||||
db.createObjectStore("a").createIndex("by_x", "x");
|
||||
db.createObjectStore("b");
|
||||
};
|
||||
open.onsuccess = () => {
|
||||
const db = open.result;
|
||||
testing.expectError("InvalidAccessError", () => db.transaction([]));
|
||||
testing.expectError("NotFoundError", () => db.transaction("nope"));
|
||||
testing.expectError("NotFoundError", () => db.transaction(["a", "nope"]));
|
||||
const tx = db.transaction(db.objectStoreNames, "readonly");
|
||||
testing.expectEqual(["a", "b"], [...tx.objectStoreNames]);
|
||||
const store = tx.objectStore("a");
|
||||
testing.expectError("TypeError", () => store.getAll(null, -1));
|
||||
testing.expectError("TypeError", () => store.getAll(null, NaN));
|
||||
testing.expectError("TypeError", () => store.index("by_x").getAllKeys({ count: Infinity }));
|
||||
tx.abort();
|
||||
testing.expectError("InvalidStateError", () => tx.objectStore("a"));
|
||||
testing.expectError("InvalidStateError", () => store.index("by_x"));
|
||||
db.close();
|
||||
testing.expectError("InvalidStateError", () => db.transaction("a"));
|
||||
state.resolve(true);
|
||||
};
|
||||
await state.done((got) => testing.expectEqual(true, got));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
@@ -200,6 +200,31 @@ pub fn databaseVersion(self: *const Engine, name: []const u8) !?i64 {
|
||||
return self.conn.scalar(i64, "select version from idb_databases where name = ?1", .{name});
|
||||
}
|
||||
|
||||
pub const DatabaseInfo = struct {
|
||||
name: []const u8,
|
||||
version: i64,
|
||||
};
|
||||
|
||||
// Every database of the origin, by name (IDBFactory.databases).
|
||||
pub fn databases(self: *const Engine, arena: Allocator) ![]const DatabaseInfo {
|
||||
var rows = try self.conn.rows("select name, version from idb_databases order by name", .{});
|
||||
defer rows.deinit();
|
||||
|
||||
var list: std.ArrayList(DatabaseInfo) = .empty;
|
||||
while (try rows.next()) |row| {
|
||||
try list.append(arena, .{
|
||||
.name = try arena.dupe(u8, row.get([]const u8, 0)),
|
||||
.version = row.get(i64, 1),
|
||||
});
|
||||
}
|
||||
return list.items;
|
||||
}
|
||||
|
||||
pub fn indexExists(self: *const Engine, object_store_id: i64, name: []const u8) !bool {
|
||||
const id = try self.conn.scalar(i64, "select id from idb_indexes where object_store_id = ?1 and name = ?2", .{ object_store_id, name });
|
||||
return id != null;
|
||||
}
|
||||
|
||||
pub fn upsertDatabase(self: *Engine, name: []const u8, version: i64) !i64 {
|
||||
try self.conn.exec(
|
||||
\\ insert into idb_databases (name, version) values (?1, ?2)
|
||||
|
||||
@@ -47,6 +47,7 @@ _version: i64,
|
||||
_txn: ?*IDBTransaction = null, // only set during upgradeneeded
|
||||
_on_error: ?js.Function.Global = null,
|
||||
_on_abort: ?js.Function.Global = null,
|
||||
_closed: bool = false,
|
||||
|
||||
pub fn init(exec: *Execution, engine: *Engine, database_id: i64, name: []const u8, version: i64) !*IDBDatabase {
|
||||
return exec._factory.eventTarget(IDBDatabase{
|
||||
@@ -82,12 +83,19 @@ pub fn createObjectStore(
|
||||
|
||||
const opts = options orelse CreateObjectStoreOptions{};
|
||||
|
||||
// Validate + copy the key path onto the transaction arena so it outlives the
|
||||
// call. autoIncrement is incompatible with an empty or compound key path.
|
||||
const key_path: ?Key.KeyPath = if (opts.keyPath) |kp| blk: {
|
||||
// Spec order: key path syntax, then the name, then autoIncrement vs. key path.
|
||||
if (opts.keyPath) |kp| {
|
||||
if (Key.isValidKeyPathSpec(kp) == false) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
}
|
||||
if ((try self._engine.objectStoreId(self._database_id, name)) != null) {
|
||||
return error.ConstraintError;
|
||||
}
|
||||
|
||||
// Copy the key path onto the transaction arena so it outlives the call.
|
||||
// autoIncrement is incompatible with an empty or compound key path.
|
||||
const key_path: ?Key.KeyPath = if (opts.keyPath) |kp| blk: {
|
||||
if (opts.autoIncrement and keyPathBlocksAutoIncrement(kp)) {
|
||||
return error.InvalidAccessError;
|
||||
}
|
||||
@@ -139,8 +147,17 @@ const TransactionMode = enum {
|
||||
};
|
||||
|
||||
const StoreNames = union(enum) {
|
||||
list: *DOMStringList,
|
||||
name: []const u8,
|
||||
names: []const []const u8,
|
||||
|
||||
fn slice(self: *const StoreNames) []const []const u8 {
|
||||
return switch (self.*) {
|
||||
.names => |names| names,
|
||||
.list => |l| l._items,
|
||||
.name => |*n| (n)[0..1],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const TransactionOptions = struct {
|
||||
@@ -154,6 +171,25 @@ pub fn transaction(
|
||||
options: ?TransactionOptions,
|
||||
exec: *Execution,
|
||||
) !*IDBTransaction {
|
||||
if (self._closed) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
if (self._txn) |upgrade| {
|
||||
// An upgrade still running owns the connection; once it settled, new
|
||||
// transactions may start even before the open request's success fires.
|
||||
if (!upgrade._settled) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
}
|
||||
|
||||
const names = store_names.slice();
|
||||
if (names.len == 0) {
|
||||
return error.InvalidAccessError;
|
||||
}
|
||||
for (names) |name| {
|
||||
try self.assertStoreExists(name);
|
||||
}
|
||||
|
||||
const opts = options orelse TransactionOptions{};
|
||||
const txn = try IDBTransaction.init(self, switch (mode orelse .readonly) {
|
||||
.readonly => .readonly,
|
||||
@@ -163,18 +199,19 @@ pub fn transaction(
|
||||
return txn;
|
||||
}
|
||||
|
||||
fn assertStoreExists(self: *const IDBDatabase, name: []const u8) !void {
|
||||
if ((try self._engine.objectStoreId(self._database_id, name)) == null) {
|
||||
return error.NotFound;
|
||||
}
|
||||
}
|
||||
|
||||
// The transaction's scope: the requested store names, sorted with duplicates
|
||||
// removed (per the IndexedDB spec's "transaction scope" steps).
|
||||
fn normalizeStoreNames(arena: Allocator, store_names: StoreNames) ![]const []const u8 {
|
||||
var list: std.ArrayList([]const u8) = .empty;
|
||||
switch (store_names) {
|
||||
.name => |name| try list.append(arena, try arena.dupe(u8, name)),
|
||||
.names => |names| {
|
||||
try list.ensureUnusedCapacity(arena, names.len);
|
||||
for (names) |name| {
|
||||
list.appendAssumeCapacity(try arena.dupe(u8, name));
|
||||
}
|
||||
},
|
||||
const names = store_names.slice();
|
||||
var list: std.ArrayList([]const u8) = try .initCapacity(arena, names.len);
|
||||
for (names) |name| {
|
||||
list.appendAssumeCapacity(try arena.dupe(u8, name));
|
||||
}
|
||||
|
||||
std.mem.sort([]const u8, list.items, {}, struct {
|
||||
@@ -199,9 +236,8 @@ fn normalizeStoreNames(arena: Allocator, store_names: StoreNames) ![]const []con
|
||||
return list.items[0..write];
|
||||
}
|
||||
|
||||
pub fn close(_: *IDBDatabase) void {
|
||||
// Connections are pooled on the Manager and shared across handles, so a
|
||||
// single handle's close() is a no-op for the bare slice.
|
||||
pub fn close(self: *IDBDatabase) void {
|
||||
self._closed = true;
|
||||
}
|
||||
|
||||
pub fn getName(self: *const IDBDatabase) []const u8 {
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn open(_: *IDBFactory, name: []const u8, version: ?u64, exec: *Execution) !
|
||||
if (v == 0) return error.TypeError;
|
||||
}
|
||||
|
||||
const request = try IDBRequest.init(exec);
|
||||
const request = try IDBRequest.initOpen(exec);
|
||||
|
||||
const ctx = try exec._factory.create(OpenContext{
|
||||
.request = request,
|
||||
@@ -177,7 +177,7 @@ const OpenContext = struct {
|
||||
txn._db._txn = null;
|
||||
txn.releaseRef(exec.page);
|
||||
|
||||
if (aborted) {
|
||||
if (aborted or txn._db._closed) {
|
||||
self.request._result = .{ .none = js.Undefined{} };
|
||||
self.request.setError(error.AbortError);
|
||||
return self.request.deliver(exec);
|
||||
@@ -295,7 +295,7 @@ pub fn deleteDatabase(_: *IDBFactory, name: []const u8, exec: *Execution) !*IDBR
|
||||
return error.SecurityError;
|
||||
}
|
||||
|
||||
const request = try IDBRequest.init(exec);
|
||||
const request = try IDBRequest.initOpen(exec);
|
||||
|
||||
const ctx = try exec._factory.create(DeleteContext{
|
||||
.request = request,
|
||||
@@ -386,6 +386,14 @@ const DeleteContext = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub fn databases(_: *IDBFactory, exec: *Execution) !js.Promise {
|
||||
const local = exec.js.local.?;
|
||||
// unavailable for opaque origins, e.g. about:blank
|
||||
const origin = exec.origin() orelse return local.rejectPromise(.{ .dom_exception = .{ .err = error.SecurityError } });
|
||||
const engine = try exec.session.idb.engineForOrigin(origin);
|
||||
return local.resolvePromise(try engine.databases(exec.call_arena));
|
||||
}
|
||||
|
||||
pub fn cmp(_: *IDBFactory, first: js.Value, second: js.Value, exec: *Execution) !i32 {
|
||||
const a = try Key.encodeValue(exec.call_arena, first);
|
||||
const b = try Key.encodeValue(exec.call_arena, second);
|
||||
@@ -408,5 +416,6 @@ pub const JsApi = struct {
|
||||
|
||||
pub const open = bridge.function(IDBFactory.open, .{});
|
||||
pub const deleteDatabase = bridge.function(IDBFactory.deleteDatabase, .{});
|
||||
pub const databases = bridge.function(IDBFactory.databases, .{});
|
||||
pub const cmp = bridge.function(IDBFactory.cmp, .{});
|
||||
};
|
||||
@@ -123,11 +123,11 @@ 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_or_options: ?js.Value, count_: ?u32, exec: *Execution) !*IDBRequest {
|
||||
pub fn getAll(self: *IDBIndex, query_or_options: ?js.Value, count_: ?f64, exec: *Execution) !*IDBRequest {
|
||||
return self._getAll(query_or_options, count_, .value, exec);
|
||||
}
|
||||
|
||||
pub fn getAllKeys(self: *IDBIndex, query_or_options: ?js.Value, count_: ?u32, exec: *Execution) !*IDBRequest {
|
||||
pub fn getAllKeys(self: *IDBIndex, query_or_options: ?js.Value, count_: ?f64, exec: *Execution) !*IDBRequest {
|
||||
return self._getAll(query_or_options, count_, .key, exec);
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ pub fn getAllRecords(self: *IDBIndex, options: ?js.Value, exec: *Execution) !*ID
|
||||
return request.submit(.{ .index_get_all = .{ .index = self, .args = args, .mode = .record } }, exec);
|
||||
}
|
||||
|
||||
fn _getAll(self: *IDBIndex, query_or_options: ?js.Value, count_: ?u32, mode: IDBObjectStore.GetAllMode, exec: *Execution) !*IDBRequest {
|
||||
fn _getAll(self: *IDBIndex, query_or_options: ?js.Value, count_: ?f64, mode: IDBObjectStore.GetAllMode, exec: *Execution) !*IDBRequest {
|
||||
const t = try self.txn();
|
||||
const args = try IDBKeyRange.resolveGetAll(t._arena.allocator(), query_or_options, count_, exec);
|
||||
const request = try t.newRequest();
|
||||
@@ -199,14 +199,14 @@ pub fn runCount(self: *IDBIndex, request: *IDBRequest, bounds: Engine.Bounds, ex
|
||||
}
|
||||
|
||||
pub fn openCursor(self: *IDBIndex, query: ?js.Value, direction: ?IDBCursor.Direction, exec: *Execution) !*IDBRequest {
|
||||
try self.assertLive();
|
||||
const bounds = try IDBKeyRange.resolveQuery(self._store._txn._arena.allocator(), query, exec);
|
||||
const t = try self.txn();
|
||||
const bounds = try IDBKeyRange.resolveQuery(t._arena.allocator(), query, exec);
|
||||
return IDBCursor.initIndex(self, bounds, direction orelse .next, false, exec);
|
||||
}
|
||||
|
||||
pub fn openKeyCursor(self: *IDBIndex, query: ?js.Value, direction: ?IDBCursor.Direction, exec: *Execution) !*IDBRequest {
|
||||
try self.assertLive();
|
||||
const bounds = try IDBKeyRange.resolveQuery(self._store._txn._arena.allocator(), query, exec);
|
||||
const t = try self.txn();
|
||||
const bounds = try IDBKeyRange.resolveQuery(t._arena.allocator(), query, exec);
|
||||
return IDBCursor.initIndex(self, bounds, direction orelse .next, true, exec);
|
||||
}
|
||||
|
||||
|
||||
@@ -161,13 +161,13 @@ pub const GetAllArgs = struct {
|
||||
// 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 {
|
||||
pub fn resolveGetAll(arena: Allocator, query_or_options: ?js.Value, count: ?f64, 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) };
|
||||
return .{ .bounds = try resolveQuery(arena, query_or_options, exec), .count = try normalizeCount(count) };
|
||||
}
|
||||
|
||||
// getAllRecords always takes an IDBGetAllOptions dictionary (or nothing).
|
||||
@@ -200,7 +200,7 @@ fn resolveOptions(arena: Allocator, v: js.Value, exec: *Execution) !GetAllArgs {
|
||||
|
||||
const count = try obj.get("count");
|
||||
if (!count.isNullOrUndefined()) {
|
||||
args.count = normalizeCount(try count.toU32());
|
||||
args.count = try normalizeCount(try count.toF64());
|
||||
}
|
||||
|
||||
const direction = try obj.get("direction");
|
||||
@@ -211,9 +211,19 @@ fn resolveOptions(arena: Allocator, v: js.Value, exec: *Execution) !GetAllArgs {
|
||||
return args;
|
||||
}
|
||||
|
||||
fn normalizeCount(count: ?u32) ?u32 {
|
||||
// count is [EnforceRange] unsigned long: NaN, the infinities and anything
|
||||
// outside [0, 2^32) are a TypeError (fractions truncate). 0 means "no limit".
|
||||
fn normalizeCount(count: ?f64) !?u32 {
|
||||
const c = count orelse return null;
|
||||
return if (c == 0) null else c;
|
||||
if (!std.math.isFinite(c)) {
|
||||
return error.TypeError;
|
||||
}
|
||||
const truncated = @trunc(c);
|
||||
if (truncated < 0 or truncated > std.math.maxInt(u32)) {
|
||||
return error.TypeError;
|
||||
}
|
||||
const n: u32 = @intFromFloat(truncated);
|
||||
return if (n == 0) null else n;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -112,10 +112,10 @@ pub fn runGet(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Bounds
|
||||
pub fn delete(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBRequest {
|
||||
try self.assertLive();
|
||||
const txn = self._txn;
|
||||
try txn.assertActive();
|
||||
if (txn._mode == .readonly) {
|
||||
return error.ReadOnlyError;
|
||||
}
|
||||
try txn.assertActive();
|
||||
const bounds = try IDBKeyRange.resolveKey(txn._arena.allocator(), query, exec);
|
||||
const request = try txn.newRequest();
|
||||
return request.submit(.{ .store_delete = .{ .store = self, .bounds = bounds } }, exec);
|
||||
@@ -177,11 +177,11 @@ pub fn runCount(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Boun
|
||||
// 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 {
|
||||
pub fn getAll(self: *IDBObjectStore, query_or_options: ?js.Value, count_: ?f64, exec: *Execution) !*IDBRequest {
|
||||
return self._getAll(query_or_options, count_, .value, exec);
|
||||
}
|
||||
|
||||
pub fn getAllKeys(self: *IDBObjectStore, query_or_options: ?js.Value, count_: ?u32, exec: *Execution) !*IDBRequest {
|
||||
pub fn getAllKeys(self: *IDBObjectStore, query_or_options: ?js.Value, count_: ?f64, exec: *Execution) !*IDBRequest {
|
||||
return self._getAll(query_or_options, count_, .key, exec);
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ pub fn getAllRecords(self: *IDBObjectStore, options: ?js.Value, exec: *Execution
|
||||
return request.submit(.{ .store_get_all = .{ .store = self, .args = args, .mode = .record } }, exec);
|
||||
}
|
||||
|
||||
fn _getAll(self: *IDBObjectStore, query_or_options: ?js.Value, count_: ?u32, mode: GetAllMode, exec: *Execution) !*IDBRequest {
|
||||
fn _getAll(self: *IDBObjectStore, query_or_options: ?js.Value, count_: ?f64, mode: GetAllMode, exec: *Execution) !*IDBRequest {
|
||||
try self.assertLive();
|
||||
const txn = self._txn;
|
||||
try txn.assertActive();
|
||||
@@ -259,12 +259,14 @@ pub fn runGetKey(self: *IDBObjectStore, request: *IDBRequest, bounds: Engine.Bou
|
||||
|
||||
pub fn openCursor(self: *IDBObjectStore, query: ?js.Value, direction: ?IDBCursor.Direction, exec: *Execution) !*IDBRequest {
|
||||
try self.assertLive();
|
||||
try self._txn.assertActive();
|
||||
const bounds = try IDBKeyRange.resolveQuery(self._txn._arena.allocator(), query, exec);
|
||||
return IDBCursor.init(self, bounds, direction orelse .next, false, exec);
|
||||
}
|
||||
|
||||
pub fn openKeyCursor(self: *IDBObjectStore, query: ?js.Value, direction: ?IDBCursor.Direction, exec: *Execution) !*IDBRequest {
|
||||
try self.assertLive();
|
||||
try self._txn.assertActive();
|
||||
const bounds = try IDBKeyRange.resolveQuery(self._txn._arena.allocator(), query, exec);
|
||||
return IDBCursor.init(self, bounds, direction orelse .next, true, exec);
|
||||
}
|
||||
@@ -509,8 +511,10 @@ pub fn createIndex(self: *IDBObjectStore, name: []const u8, key_path: Key.KeyPat
|
||||
if (txn._mode != .versionchange) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
// Spec order: the transaction-state check precedes the index-name check.
|
||||
try txn.assertActive();
|
||||
if (try self._engine.indexExists(self._store_id, name)) {
|
||||
return error.ConstraintError;
|
||||
}
|
||||
if (Key.isValidKeyPathSpec(key_path) == false) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
@@ -580,7 +584,6 @@ pub fn deleteIndex(self: *IDBObjectStore, name: []const u8, _: *Execution) !void
|
||||
if (txn._mode != .versionchange) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
// Spec order: the transaction-state check precedes the index-name check.
|
||||
try txn.assertActive();
|
||||
self._engine.deleteIndexRow(self._store_id, name) catch |err| switch (err) {
|
||||
error.NotFound => return error.NotFoundError,
|
||||
@@ -598,6 +601,9 @@ pub fn deleteIndex(self: *IDBObjectStore, name: []const u8, _: *Execution) !void
|
||||
|
||||
pub fn index(self: *IDBObjectStore, name: []const u8, _: *Execution) !*IDBIndex {
|
||||
try self.assertLive();
|
||||
if (self._txn._settled) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
for (self._indexes.items) |idx| {
|
||||
if (std.mem.eql(u8, idx._name, name)) {
|
||||
return idx;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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 idb = @import("idb.zig");
|
||||
const IDBRequest = @import("IDBRequest.zig");
|
||||
|
||||
const FunctionSetter = idb.FunctionSetter;
|
||||
|
||||
// The request returned by IDBFactory.open / deleteDatabase.
|
||||
const IDBOpenDBRequest = @This();
|
||||
|
||||
pub const Proto = IDBRequest;
|
||||
|
||||
_proto: *IDBRequest,
|
||||
// Stored, never fired: an open that upgrades does not wait on other
|
||||
// connections (see IDBFactory.OpenContext), so nothing ever blocks.
|
||||
_on_blocked: ?js.Function.Global = null,
|
||||
|
||||
pub fn getOnBlocked(self: *const IDBOpenDBRequest) ?js.Function.Global {
|
||||
return self._on_blocked;
|
||||
}
|
||||
|
||||
pub fn setOnBlocked(self: *IDBOpenDBRequest, setter: ?FunctionSetter) void {
|
||||
self._on_blocked = idb.functionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnUpgradeNeeded(self: *const IDBOpenDBRequest) ?js.Function.Global {
|
||||
return self._proto.getOnUpgradeNeeded();
|
||||
}
|
||||
|
||||
pub fn setOnUpgradeNeeded(self: *IDBOpenDBRequest, setter: ?FunctionSetter) void {
|
||||
self._proto.setOnUpgradeNeeded(setter);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(IDBOpenDBRequest);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "IDBOpenDBRequest";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const onblocked = bridge.accessor(IDBOpenDBRequest.getOnBlocked, IDBOpenDBRequest.setOnBlocked, .{});
|
||||
pub const onupgradeneeded = bridge.accessor(IDBOpenDBRequest.getOnUpgradeNeeded, IDBOpenDBRequest.setOnUpgradeNeeded, .{});
|
||||
};
|
||||
@@ -29,6 +29,7 @@ const idb = @import("idb.zig");
|
||||
const Engine = @import("Engine.zig");
|
||||
const IDBIndex = @import("IDBIndex.zig");
|
||||
const IDBCursor = @import("IDBCursor.zig");
|
||||
const IDBOpenDBRequest = @import("IDBOpenDBRequest.zig");
|
||||
const IDBDatabase = @import("IDBDatabase.zig");
|
||||
const IDBKeyRange = @import("IDBKeyRange.zig");
|
||||
const IDBObjectStore = @import("IDBObjectStore.zig");
|
||||
@@ -44,6 +45,7 @@ const IDBRequest = @This();
|
||||
pub const Proto = EventTarget;
|
||||
|
||||
_proto: *EventTarget,
|
||||
_type: Type = .generic,
|
||||
_op: Operation = .none,
|
||||
_error: ?anyerror = null,
|
||||
_txn: Txn = .none,
|
||||
@@ -101,8 +103,15 @@ const Txn = union(enum) {
|
||||
borrowed: *IDBTransaction,
|
||||
};
|
||||
|
||||
pub fn init(exec: *Execution) !*IDBRequest {
|
||||
return exec._factory.eventTarget(IDBRequest{ ._proto = undefined });
|
||||
pub const Type = union(enum) {
|
||||
generic,
|
||||
open: *IDBOpenDBRequest,
|
||||
};
|
||||
|
||||
// An open/deleteDatabase request: page-scoped, exposed as IDBOpenDBRequest.
|
||||
pub fn initOpen(exec: *Execution) !*IDBRequest {
|
||||
const open = try exec._factory.idbOpenRequest(IDBOpenDBRequest{ ._proto = undefined });
|
||||
return open._proto;
|
||||
}
|
||||
|
||||
pub fn asEventTarget(self: *IDBRequest) *EventTarget {
|
||||
@@ -321,7 +330,10 @@ const JsResult = union(enum) {
|
||||
database: *IDBDatabase,
|
||||
};
|
||||
|
||||
pub fn getResult(self: *const IDBRequest, exec: *Execution) JsResult {
|
||||
pub fn getResult(self: *const IDBRequest, exec: *Execution) !JsResult {
|
||||
if (self._ready_state == .pending) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
return switch (self._result) {
|
||||
.none => |n| .{ .none = n },
|
||||
.value => |global| .{ .value = global.local(exec.js.local.?) },
|
||||
@@ -344,7 +356,10 @@ pub fn getTransaction(self: *const IDBRequest) ?*IDBTransaction {
|
||||
|
||||
// Return this as a DOMException directly. If we return an error, the bridge
|
||||
// *will* convert it to a DOMException, but it'll throw it, not return it.
|
||||
pub fn getError(self: *const IDBRequest) ?DOMException {
|
||||
pub fn getError(self: *const IDBRequest) !?DOMException {
|
||||
if (self._ready_state == .pending) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
const err = self._error orelse return null;
|
||||
const mapped: anyerror = switch (err) {
|
||||
// sqlite's generic constraint failure is IDB's ConstraintError.
|
||||
@@ -493,9 +508,8 @@ pub const JsApi = struct {
|
||||
pub const readyState = bridge.accessor(IDBRequest.getReadyState, null, .{});
|
||||
pub const result = bridge.accessor(IDBRequest.getResult, null, .{});
|
||||
pub const source = bridge.accessor(IDBRequest.getSource, null, .{});
|
||||
pub const transaction = bridge.accessor(IDBRequest.getTransaction, null, .{ .null_as_undefined = true });
|
||||
pub const @"error" = bridge.accessor(IDBRequest.getError, null, .{ .null_as_undefined = true });
|
||||
pub const transaction = bridge.accessor(IDBRequest.getTransaction, null, .{});
|
||||
pub const @"error" = bridge.accessor(IDBRequest.getError, null, .{});
|
||||
pub const onsuccess = bridge.accessor(IDBRequest.getOnSuccess, IDBRequest.setOnSuccess, .{});
|
||||
pub const onerror = bridge.accessor(IDBRequest.getOnError, IDBRequest.setOnError, .{});
|
||||
pub const onupgradeneeded = bridge.accessor(IDBRequest.getOnUpgradeNeeded, IDBRequest.setOnUpgradeNeeded, .{});
|
||||
};
|
||||
@@ -507,6 +507,9 @@ pub fn enqueue(self: *IDBTransaction, request: *IDBRequest) !void {
|
||||
}
|
||||
|
||||
pub fn objectStore(self: *IDBTransaction, name: []const u8) !*IDBObjectStore {
|
||||
if (self._settled) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
for (self._stores.items) |store| {
|
||||
if (std.mem.eql(u8, store._name, name)) {
|
||||
return store;
|
||||
|
||||
@@ -26,6 +26,7 @@ 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 IDBOpenDBRequest = @import("IDBOpenDBRequest.zig");
|
||||
pub const IDBCursor = @import("IDBCursor.zig");
|
||||
pub const IDBIndex = @import("IDBIndex.zig");
|
||||
pub const IDBDatabase = @import("IDBDatabase.zig");
|
||||
@@ -40,6 +41,7 @@ pub fn registerTypes() []const type {
|
||||
IDBFactory,
|
||||
IDBRecord,
|
||||
IDBRequest,
|
||||
IDBOpenDBRequest,
|
||||
IDBCursor,
|
||||
IDBIndex,
|
||||
IDBDatabase,
|
||||
@@ -76,6 +78,14 @@ pub const FunctionSetter = union(enum) {
|
||||
anything: js.Value,
|
||||
};
|
||||
|
||||
pub fn functionFromSetter(setter: ?FunctionSetter) ?js.Function.Global {
|
||||
const s = setter orelse return null;
|
||||
return switch (s) {
|
||||
.func => |f| f,
|
||||
.anything => null,
|
||||
};
|
||||
}
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
test "WebApi: IndexedDB" {
|
||||
try testing.htmlRunner("indexeddb.html", .{});
|
||||
|
||||
Reference in new issue
Block a user