From eb2eceb3d64ad0782efbed863929c81a96956e51 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 3 Sep 2026 08:48:03 +0800 Subject: [PATCH] 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. --- src/browser/Factory.zig | 7 ++ src/browser/tests/indexeddb.html | 77 ++++++++++++++++++- src/browser/webapi/storage/idb/Engine.zig | 25 ++++++ .../webapi/storage/idb/IDBDatabase.zig | 66 ++++++++++++---- src/browser/webapi/storage/idb/IDBFactory.zig | 15 +++- src/browser/webapi/storage/idb/IDBIndex.zig | 14 ++-- .../webapi/storage/idb/IDBKeyRange.zig | 20 +++-- .../webapi/storage/idb/IDBObjectStore.zig | 18 +++-- .../webapi/storage/idb/IDBOpenDBRequest.zig | 63 +++++++++++++++ src/browser/webapi/storage/idb/IDBRequest.zig | 28 +++++-- .../webapi/storage/idb/IDBTransaction.zig | 3 + src/browser/webapi/storage/idb/idb.zig | 10 +++ 12 files changed, 301 insertions(+), 45 deletions(-) create mode 100644 src/browser/webapi/storage/idb/IDBOpenDBRequest.zig diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index a1a7ed600..424ae7ee3 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -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) }, diff --git a/src/browser/tests/indexeddb.html b/src/browser/tests/indexeddb.html index fd0d0637a..4e0e657fa 100644 --- a/src/browser/tests/indexeddb.html +++ b/src/browser/tests/indexeddb.html @@ -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 @@ }); } +<<<<<<< HEAD + + + + + + diff --git a/src/browser/webapi/storage/idb/Engine.zig b/src/browser/webapi/storage/idb/Engine.zig index d43b93c82..7f51e5bce 100644 --- a/src/browser/webapi/storage/idb/Engine.zig +++ b/src/browser/webapi/storage/idb/Engine.zig @@ -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) diff --git a/src/browser/webapi/storage/idb/IDBDatabase.zig b/src/browser/webapi/storage/idb/IDBDatabase.zig index 7eef142ec..0b3dde2e2 100644 --- a/src/browser/webapi/storage/idb/IDBDatabase.zig +++ b/src/browser/webapi/storage/idb/IDBDatabase.zig @@ -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 { diff --git a/src/browser/webapi/storage/idb/IDBFactory.zig b/src/browser/webapi/storage/idb/IDBFactory.zig index 878921c0f..b520acd53 100644 --- a/src/browser/webapi/storage/idb/IDBFactory.zig +++ b/src/browser/webapi/storage/idb/IDBFactory.zig @@ -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, .{}); }; diff --git a/src/browser/webapi/storage/idb/IDBIndex.zig b/src/browser/webapi/storage/idb/IDBIndex.zig index 5c55f5a97..ab2002264 100644 --- a/src/browser/webapi/storage/idb/IDBIndex.zig +++ b/src/browser/webapi/storage/idb/IDBIndex.zig @@ -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); } diff --git a/src/browser/webapi/storage/idb/IDBKeyRange.zig b/src/browser/webapi/storage/idb/IDBKeyRange.zig index 5e9c90927..7833cb122 100644 --- a/src/browser/webapi/storage/idb/IDBKeyRange.zig +++ b/src/browser/webapi/storage/idb/IDBKeyRange.zig @@ -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 { diff --git a/src/browser/webapi/storage/idb/IDBObjectStore.zig b/src/browser/webapi/storage/idb/IDBObjectStore.zig index 418743386..620a2f29c 100644 --- a/src/browser/webapi/storage/idb/IDBObjectStore.zig +++ b/src/browser/webapi/storage/idb/IDBObjectStore.zig @@ -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; diff --git a/src/browser/webapi/storage/idb/IDBOpenDBRequest.zig b/src/browser/webapi/storage/idb/IDBOpenDBRequest.zig new file mode 100644 index 000000000..29dadef12 --- /dev/null +++ b/src/browser/webapi/storage/idb/IDBOpenDBRequest.zig @@ -0,0 +1,63 @@ +// 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 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, .{}); +}; diff --git a/src/browser/webapi/storage/idb/IDBRequest.zig b/src/browser/webapi/storage/idb/IDBRequest.zig index 591d0e600..a1758563f 100644 --- a/src/browser/webapi/storage/idb/IDBRequest.zig +++ b/src/browser/webapi/storage/idb/IDBRequest.zig @@ -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, .{}); }; diff --git a/src/browser/webapi/storage/idb/IDBTransaction.zig b/src/browser/webapi/storage/idb/IDBTransaction.zig index 7ab2f6e3b..e742f1f2d 100644 --- a/src/browser/webapi/storage/idb/IDBTransaction.zig +++ b/src/browser/webapi/storage/idb/IDBTransaction.zig @@ -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; diff --git a/src/browser/webapi/storage/idb/idb.zig b/src/browser/webapi/storage/idb/idb.zig index 33d7da71a..2bd2b3cd0 100644 --- a/src/browser/webapi/storage/idb/idb.zig +++ b/src/browser/webapi/storage/idb/idb.zig @@ -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", .{});