webapi, idb: Add IDBCursor and IDBCursorWithValue

This commit is contained in:
Karl Seguin
2026-06-30 16:47:30 +08:00
parent 6b7d0c6391
commit edbd24bbac
9 changed files with 556 additions and 14 deletions

View File

@@ -329,6 +329,7 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
// but this can never be valid.
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
},
Undefined => return isolate.initUndefined(),
inline String, BigInt, Integer, Number, Value, Object => return value.handle,
else => {},
}

View File

@@ -632,6 +632,147 @@
}
</script>
<!-- A forward cursor walks every record in key order; key/value/primaryKey are
exposed and continue() advances it. -->
<script id="cursor_forward" type=module>
{
const state = await testing.async();
const open = indexedDB.open("cursor-db", 1);
open.onupgradeneeded = (e) => {
const s = e.target.result.createObjectStore("s");
s.add("a", 1);
s.add("b", 2);
s.add("c", 3);
};
open.onsuccess = (e) => {
const store = e.target.result.transaction("s", "readonly").objectStore("s");
const seen = [];
const req = store.openCursor();
req.onsuccess = () => {
const cursor = req.result;
if (cursor) {
seen.push([cursor.key, cursor.primaryKey, cursor.value, cursor.direction]);
cursor.continue();
} else {
state.resolve(seen);
}
};
};
await state.done((seen) => {
testing.expectEqual(3, seen.length);
testing.expectEqual(1, seen[0][0]); // key
testing.expectEqual(1, seen[0][1]); // primaryKey == key for object store
testing.expectEqual("a", seen[0][2]); // value
testing.expectEqual("next", seen[0][3]); // direction
testing.expectEqual(3, seen[2][0]);
testing.expectEqual("c", seen[2][2]);
});
}
</script>
<!-- A "prev" cursor walks in reverse; a range clamps it; continue(key) jumps. -->
<script id="cursor_prev_and_range" type=module>
{
const state = await testing.async();
const open = indexedDB.open("cursor2-db", 1);
open.onupgradeneeded = (e) => {
const s = e.target.result.createObjectStore("s");
for (let i = 1; i <= 5; i++) s.add("v" + i, i);
};
open.onsuccess = (e) => {
// Both cursors share one transaction (a new transaction can't begin
// while this one is still draining — single active txn per connection).
const store = e.target.result.transaction("s").objectStore("s");
const reverse = [];
const r1 = store.openCursor(null, "prev");
r1.onsuccess = () => {
const c = r1.result;
if (c) { reverse.push(c.key); c.continue(); return; }
// Range [2,4], then continue(4) to jump from 2 straight to 4.
const jumped = [];
const r2 = store.openCursor(IDBKeyRange.bound(2, 4));
r2.onsuccess = () => {
const c2 = r2.result;
if (!c2) { state.resolve({ reverse, jumped }); return; }
jumped.push(c2.key);
if (c2.key === 2) c2.continue(4); else c2.continue();
};
};
};
await state.done((got) => {
testing.expectEqual([5, 4, 3, 2, 1].join(), got.reverse.join());
testing.expectEqual([2, 4].join(), got.jumped.join()); // skipped 3 via continue(4)
});
}
</script>
<!-- advance(n) skips records; update()/delete() mutate at the cursor. -->
<script id="cursor_advance_update_delete" type=module>
{
const state = await testing.async();
const open = indexedDB.open("cursor3-db", 1);
open.onupgradeneeded = (e) => {
const s = e.target.result.createObjectStore("s");
for (let i = 1; i <= 4; i++) s.add("v" + i, i);
};
open.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction("s", "readwrite");
const store = tx.objectStore("s");
// advance(2) from the start lands on key 3; update it, then delete key 4.
const req = store.openCursor();
let advanced = false;
req.onsuccess = () => {
const c = req.result;
if (!c) return;
if (!advanced) { advanced = true; c.advance(2); return; }
if (c.key === 3) { c.update("V3"); c.continue(); return; }
if (c.key === 4) { c.delete(); c.continue(); return; }
};
tx.oncomplete = () => {
const store2 = db.transaction("s", "readonly").objectStore("s");
const all = store2.getAll();
const keys = store2.getAllKeys();
keys.onsuccess = () => state.resolve({ all: all.result, keys: keys.result });
};
};
await state.done((got) => {
testing.expectEqual([1, 2, 3].join(), got.keys.join()); // key 4 deleted
testing.expectEqual("V3", got.all[2]); // key 3 updated
});
}
</script>
<!-- openKeyCursor yields keys without values. -->
<script id="key_cursor" type=module>
{
const state = await testing.async();
const open = indexedDB.open("keycursor-db", 1);
open.onupgradeneeded = (e) => {
const s = e.target.result.createObjectStore("s");
s.add("a", 1);
s.add("b", 2);
};
open.onsuccess = (e) => {
const store = e.target.result.transaction("s").objectStore("s");
const req = store.openKeyCursor();
const seen = [];
req.onsuccess = () => {
const c = req.result;
if (c) { seen.push([c.key, c.value]); c.continue(); }
else state.resolve(seen);
};
};
await state.done((seen) => {
testing.expectEqual(2, seen.length);
testing.expectEqual(1, seen[0][0]);
testing.expectEqual(undefined, seen[0][1]); // key cursor has no value
});
}
</script>
<!-- clear() empties the store; commit() settles the transaction explicitly. -->
<script id="clear_and_commit" type=module>
{

View File

@@ -307,6 +307,49 @@ pub fn getAllRange(self: *const Engine, arena: Allocator, object_store_id: i64,
return list.items;
}
pub const CursorRecord = struct {
key: []u8,
// null for a key-only cursor (the value column isn't selected or duped).
value: ?[]u8,
};
// Seek a single record for a cursor: within `b`, in `reverse` order or not,
// positioned by `from_op ?4` (e.g. "> " last_key) and skipping `offset` matches
// (for advance). `from_key` of the min/max sentinel makes the position clause a
// no-op for the first step. `with_value` is false for openKeyCursor, which skips
// the (potentially large) value column entirely.
pub fn cursorSeek(
self: *const Engine,
arena: Allocator,
object_store_id: i64,
b: Bounds,
reverse: bool,
from_op: []const u8,
from_key: []const u8,
offset: u32,
with_value: bool,
) !?CursorRecord {
// A point range stores its operators empty; a cursor still wants a closed
// range over the single key.
const lower_op = if (b.is_point) ">= " else b.lower_op;
const upper_op = if (b.is_point) "<= " else b.upper_op;
const order = if (reverse) "desc" else "asc";
var buf: [320]u8 = undefined;
const sql = try std.fmt.bufPrint(
&buf,
"{s} from idb_records where object_store_id = ?1 and key {s} ?2 and key {s} ?3 and key {s} ?4 order by key {s} limit 1 offset ?5",
.{ if (with_value) "select key, value" else "select key", lower_op, upper_op, from_op, order },
);
var row = (try self.conn.row(sql, .{ object_store_id, b.lower, b.upper, from_key, @as(i64, offset) })) orelse return null;
defer row.deinit();
return .{
.key = try arena.dupe(u8, row.get([]const u8, 0)),
.value = if (with_value) try arena.dupe(u8, row.get([]const u8, 1)) else null,
};
}
const testing = @import("../../../../testing.zig");
test "IDB - Engine: ranged getAll/count honour open and closed bounds" {
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);

View File

@@ -0,0 +1,268 @@
// 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 std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../../js/js.zig");
const Key = @import("Key.zig");
const Engine = @import("Engine.zig");
const IDBRequest = @import("IDBRequest.zig");
const IDBObjectStore = @import("IDBObjectStore.zig");
const IDBTransaction = @import("IDBTransaction.zig");
const IDBCursorWithValue = @import("IDBCursorWithValue.zig");
const log = lp.log;
const Execution = js.Execution;
const IDBCursor = @This();
_engine: *Engine,
_store: *IDBObjectStore,
_txn: *IDBTransaction,
_request: *IDBRequest,
_bounds: Engine.Bounds,
_direction: Direction,
_key_only: bool,
// the JS value of this Cursor, pre-converted and cached as an optimization
// since this cursor will be the request value on every iteration.
_js: js.Value.Global,
// Encoded current key; null before iteration and at the end
_key: ?[]const u8 = null,
// Current record's serialized value bytes (null when key-only or exhausted).
_value: ?[]const u8 = null,
// The spec's "got value flag": gated true only while a success handler can read
// the cursor, set just before each delivery and cleared by continue/advance.
_got_value: bool = false,
pub const Direction = enum {
next,
nextunique,
prev,
prevunique,
pub const js_enum_from_string = true;
pub fn toString(self: Direction) []const u8 {
return @tagName(self);
}
fn reverse(self: Direction) bool {
return self == .prev or self == .prevunique;
}
};
// Create a cursor over `store` and run the first seek. Returns the request that
// delivers the cursor (or null) via repeated `success` events.
pub fn open(store: *IDBObjectStore, bounds: Engine.Bounds, direction: Direction, key_only: bool, exec: *Execution) !*IDBRequest {
const txn = store._txn orelse return error.TransactionInactiveError;
try txn.ensureBegun();
const request = try txn.newRequest();
const self = try exec._factory.create(IDBCursor{
._engine = store._engine,
._store = store,
._txn = txn,
._request = request,
._bounds = bounds,
._direction = direction,
._key_only = key_only,
._js = undefined,
});
request._cursor = self;
const local = exec.js.local.?;
const public: js.Value = if (key_only)
try local.zigValueToJs(self, .{})
else
try local.zigValueToJs(try IDBCursorWithValue.init(self, exec), .{});
// An optimization. Potentially looked up _a lot_, so calculating upfront
// storing it, and setting it in the IDBRequest (which is already js.Value.Global
// aware), avoids the lookup we'd normally have to do in the bridge.
self._js = try public.persist();
const reverse = direction.reverse();
try self.iterate(if (reverse) "<= " else ">= ", if (reverse) Engine.Bounds.max_sentinel else Engine.Bounds.min_sentinel, 0, exec);
return request;
}
// Called by IDBRequest.deliver right before a cursor's success handler runs.
pub fn beforeDeliver(self: *IDBCursor) void {
self._got_value = self._key != null;
}
pub fn @"continue"(self: *IDBCursor, key_arg: ?js.Value, exec: *Execution) !void {
try self.prepareIterate();
const reverse = self._direction.reverse();
if (key_arg) |k| {
const encoded = try Key.encodeValue(exec.arena, k);
// The target must move past the current key in the iteration direction.
const order = std.mem.order(u8, encoded, self._key.?);
if (if (reverse) order != .lt else order != .gt) {
return error.DataError;
}
try self.iterate(if (reverse) "<= " else ">= ", encoded, 0, exec);
} else {
try self.iterate(if (reverse) "< " else "> ", self._key.?, 0, exec);
}
try self._txn.enqueue(self._request);
}
pub fn advance(self: *IDBCursor, count: u32, exec: *Execution) !void {
if (count == 0) {
return error.TypeError;
}
try self.prepareIterate();
const reverse = self._direction.reverse();
// `count` records forward = skip count-1 past the immediate next.
try self.iterate(if (reverse) "< " else "> ", self._key.?, count - 1, exec);
try self._txn.enqueue(self._request);
}
pub fn update(self: *IDBCursor, value: js.Value, exec: *Execution) !*IDBRequest {
if (self._txn._mode == .readonly) {
return error.ReadOnlyError;
}
if (self._txn._settled == true) {
return error.TransactionInactiveError;
}
if (self._got_value == false) {
return error.InvalidStateError;
}
const key = self._key orelse return error.InvalidStateError;
// For an in-line store, the value's own key must match the cursor's key.
if (self._store._key_path) |kp| {
const extracted = Key.evaluatePath(value, kp) orelse return error.DataError;
const encoded = try Key.encodeValue(exec.call_arena, extracted);
if (!std.mem.eql(u8, encoded, key)) {
return error.DataError;
}
}
const serialized = try value.serialize();
defer serialized.deinit();
const request = try self._txn.newRequest();
self._engine.put(self._store._store_id, key, serialized.bytes()) catch |err| {
log.warn(.storage, "idb cursor update", .{ .err = err });
request.setError(err);
return request;
};
try request.setValue(try Key.decodeToJs(exec.call_arena, exec.js.local.?, key));
return request;
}
pub fn delete(self: *IDBCursor) !*IDBRequest {
if (self._txn._mode == .readonly) {
return error.ReadOnlyError;
}
if (self._txn._settled == true) {
return error.TransactionInactiveError;
}
if (self._got_value == false) {
return error.InvalidStateError;
}
const key = self._key orelse return error.InvalidStateError;
const request = try self._txn.newRequest();
self._engine.deleteRange(self._store._store_id, Engine.Bounds.point(key)) catch |err| {
log.warn(.storage, "idb cursor delete", .{ .err = err });
request.setError(err);
};
return request;
}
pub fn getKey(self: *const IDBCursor, exec: *Execution) !?js.Value {
const encoded = self._key orelse return null;
return try Key.decodeToJs(exec.call_arena, exec.js.local.?, encoded);
}
// For an object store the primary key is the key.
pub fn getPrimaryKey(self: *const IDBCursor, exec: *Execution) !?js.Value {
return self.getKey(exec);
}
pub fn getDirection(self: *const IDBCursor) Direction {
return self._direction;
}
pub fn getSource(self: *const IDBCursor) *IDBObjectStore {
return self._store;
}
// Seek the next record and stage it as the request result. The key/value live on
// the page arena (they must survive across event-loop turns within the txn).
fn iterate(self: *IDBCursor, from_op: []const u8, from_key: []const u8, offset: u32, exec: *Execution) !void {
const reverse = self._direction.reverse();
const rec = try self._engine.cursorSeek(exec.arena, self._store._store_id, self._bounds, reverse, from_op, from_key, offset, !self._key_only);
if (rec) |r| {
self._key = r.key;
self._value = r.value; // already null for a key-only cursor
self._request.setValueGlobal(self._js);
} else {
self._key = null;
self._value = null;
self._request.setNull();
}
}
// validate the state before we can advance/continue
fn prepareIterate(self: *IDBCursor) !void {
if (self._txn._settled == true) {
return error.TransactionInactiveError;
}
if (self._got_value == false) {
return error.InvalidStateError;
}
self._got_value = false;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(IDBCursor);
pub const Meta = struct {
pub const name = "IDBCursor";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const key = bridge.accessor(IDBCursor.getKey, null, .{ .null_as_undefined = true });
pub const primaryKey = bridge.accessor(IDBCursor.getPrimaryKey, null, .{ .null_as_undefined = true });
pub const direction = bridge.accessor(IDBCursor.getDirection, null, .{});
pub const source = bridge.accessor(IDBCursor.getSource, null, .{});
pub const @"continue" = bridge.function(IDBCursor.@"continue", .{ });
pub const advance = bridge.function(IDBCursor.advance, .{ });
pub const update = bridge.function(IDBCursor.update, .{ });
pub const delete = bridge.function(IDBCursor.delete, .{ });
};

View File

@@ -0,0 +1,48 @@
// 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 IDBCursor = @import("IDBCursor.zig");
const Execution = js.Execution;
const IDBCursorWithValue = @This();
_proto: *IDBCursor,
pub fn init(cursor: *IDBCursor, exec: *Execution) !*IDBCursorWithValue {
return exec._factory.create(IDBCursorWithValue{ ._proto = cursor });
}
pub fn getValue(self: *const IDBCursorWithValue, exec: *Execution) !?js.Value {
const bytes = self._proto._value orelse return null;
return try js.Value.deserialize(exec.js.local.?, bytes);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(IDBCursorWithValue);
pub const Meta = struct {
pub const name = "IDBCursorWithValue";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const value = bridge.accessor(IDBCursorWithValue.getValue, null, .{ .null_as_undefined = true });
};

View File

@@ -22,6 +22,7 @@ const js = @import("../../../js/js.zig");
const Key = @import("Key.zig");
const Engine = @import("Engine.zig");
const IDBCursor = @import("IDBCursor.zig");
const IDBRequest = @import("IDBRequest.zig");
const IDBKeyRange = @import("IDBKeyRange.zig");
const IDBTransaction = @import("IDBTransaction.zig");
@@ -85,7 +86,7 @@ pub fn get(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBReques
const b = bytes orelse return request;
const value = try js.Value.deserialize(exec.js.local.?, b);
try request.setValueResult(value);
try request.setValue(value);
return request;
}
@@ -132,7 +133,7 @@ pub fn count(self: *IDBObjectStore, query: ?js.Value, exec: *Execution) !*IDBReq
request.setError(err);
return request;
};
try request.setValueResult(try exec.js.local.?.zigValueToJs(n, .{}));
try request.setValue(try exec.js.local.?.zigValueToJs(n, .{}));
return request;
}
@@ -156,7 +157,7 @@ pub fn getAll(self: *IDBObjectStore, query: ?js.Value, count_: ?u32, exec: *Exec
const value = try js.Value.deserialize(local, bytes);
_ = try arr.set(@intCast(i), value, .{});
}
try request.setValueResult(arr.toValue());
try request.setValue(arr.toValue());
return request;
}
@@ -175,7 +176,7 @@ pub fn getKey(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBReq
};
const bytes = found orelse return request; // no record -> undefined
try request.setValueResult(try Key.decodeToJs(arena, exec.js.local.?, bytes));
try request.setValue(try Key.decodeToJs(arena, exec.js.local.?, bytes));
return request;
}
@@ -198,10 +199,20 @@ pub fn getAllKeys(self: *IDBObjectStore, query: ?js.Value, count_: ?u32, exec: *
for (keys, 0..) |bytes, i| {
_ = try arr.set(@intCast(i), try Key.decodeToJs(arena, local, bytes), .{});
}
try request.setValueResult(arr.toValue());
try request.setValue(arr.toValue());
return request;
}
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);
}
pub fn openKeyCursor(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, true, exec);
}
pub fn getName(self: *const IDBObjectStore) []const u8 {
return self._name;
}
@@ -295,7 +306,7 @@ fn write(self: *IDBObjectStore, value: js.Value, key_arg: ?js.Value, kind: Write
return request;
};
try request.setValueResult(key_value);
try request.setValue(key_value);
return request;
}
@@ -321,4 +332,6 @@ pub const JsApi = struct {
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, .{ });
};

View File

@@ -26,6 +26,7 @@ const EventTarget = @import("../../EventTarget.zig");
const DOMException = @import("../../DOMException.zig");
const idb = @import("idb.zig");
const IDBCursor = @import("IDBCursor.zig");
const IDBDatabase = @import("IDBDatabase.zig");
const IDBTransaction = @import("IDBTransaction.zig");
const IDBVersionChangeEvent = @import("IDBVersionChangeEvent.zig");
@@ -36,9 +37,10 @@ const FunctionSetter = idb.FunctionSetter;
const IDBRequest = @This();
_proto: *EventTarget,
_result: Result = .none,
_result: Result = .{.none = js.Undefined{}},
_error: ?anyerror = null,
_txn: ?*IDBTransaction = null,
_cursor: ?*IDBCursor = null,
_ready_state: ReadyState = .pending,
_on_success: ?js.Function.Global = null,
@@ -55,8 +57,8 @@ const ReadyState = enum {
};
const Result = union(enum) {
none,
value: js.Value.Global, // the result of a get/add/put
none: ?js.Undefined, // null or undefined (different APIs return different values)
value: js.Value.Global, // the result of a get/add/put, or a positioned cursor
database: *IDBDatabase, // the result of an open
};
@@ -68,14 +70,27 @@ pub fn asEventTarget(self: *IDBRequest) *EventTarget {
return self._proto;
}
pub fn setValueResult(self: *IDBRequest, value: js.Value) !void {
self._result = .{ .value = try value.persist() };
// Not exposed to JS, called internally
pub fn setValue(self: *IDBRequest, value: js.Value) !void {
return self.setValueGlobal(try value.persist());
}
// Not exposed to JS, called internally
pub fn setValueGlobal(self: *IDBRequest, global: js.Value.Global) void {
self._result = .{ .value = global };
}
// Not exposed to JS, called internally. Result becomes JS `null` (not undefined).
pub fn setNull(self: *IDBRequest) void {
self._result = .{.none = null};
}
// Not exposed to JS, called internally
pub fn setDatabaseResult(self: *IDBRequest, database: *IDBDatabase) void {
self._result = .{ .database = database };
}
// Not exposed to JS, called internally
pub fn setError(self: *IDBRequest, err: anyerror) void {
self._error = err;
}
@@ -89,6 +104,11 @@ pub fn deliver(self: *IDBRequest, exec: *Execution) !void {
if (self._error != null) {
return self.fire(exec, comptime .wrap("error"), self._on_error);
}
if (self._cursor) |cursor| {
// A cursor request re-fires on every iteration; let the cursor mark itself
// readable (got value) right before the success handler runs.
cursor.beforeDeliver();
}
return self.fire(exec, comptime .wrap("success"), self._on_success);
}
@@ -112,7 +132,7 @@ pub fn getReadyState(self: *const IDBRequest) ReadyState {
return self._ready_state;
}
pub fn getResult(self: *const IDBRequest) !Result {
pub fn getResult(self: *const IDBRequest) Result {
return self._result;
}
@@ -174,7 +194,7 @@ pub const JsApi = struct {
};
pub const readyState = bridge.accessor(IDBRequest.getReadyState, null, .{});
pub const result = bridge.accessor(IDBRequest.getResult, null, .{ .null_as_undefined = true });
pub const result = bridge.accessor(IDBRequest.getResult, 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 onsuccess = bridge.accessor(IDBRequest.getOnSuccess, IDBRequest.setOnSuccess, .{});

View File

@@ -171,10 +171,14 @@ pub fn ensureBegun(self: *IDBTransaction) !void {
pub fn newRequest(self: *IDBTransaction) !*IDBRequest {
const request = try IDBRequest.init(self._exec);
request._txn = self;
try self._requests.append(self._exec.arena, request);
try self.enqueue(request);
return request;
}
pub fn enqueue(self: *IDBTransaction, request: *IDBRequest) !void {
try self._requests.append(self._exec.arena, request);
}
pub fn objectStore(self: *IDBTransaction, name: []const u8, exec: *Execution) !*IDBObjectStore {
const database_id = self._db._database_id;
const info = (try self._engine.objectStoreInfo(exec.arena, database_id, name)) orelse {

View File

@@ -24,20 +24,24 @@ pub const Manager = @import("Manager.zig");
pub const IDBFactory = @import("IDBFactory.zig");
pub const IDBRequest = @import("IDBRequest.zig");
pub const IDBCursor = @import("IDBCursor.zig");
pub const IDBDatabase = @import("IDBDatabase.zig");
pub const IDBKeyRange = @import("IDBKeyRange.zig");
pub const IDBTransaction = @import("IDBTransaction.zig");
pub const IDBObjectStore = @import("IDBObjectStore.zig");
pub const IDBCursorWithValue = @import("IDBCursorWithValue.zig");
pub const IDBVersionChangeEvent = @import("IDBVersionChangeEvent.zig");
pub fn registerTypes() []const type {
return &.{
IDBFactory,
IDBRequest,
IDBCursor,
IDBDatabase,
IDBKeyRange,
IDBTransaction,
IDBObjectStore,
IDBCursorWithValue,
IDBVersionChangeEvent,
};
}