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", .{});