commit 084eebe909d5aad613d5f31f729f0d2910f3f326
parent fa0b2336d29ef7419158b647b3f304aaf5bc87f2
Author: Florian Dold <dold@taler.net>
Date: Sat, 18 Jul 2026 21:22:46 +0200
db: add transaction listing queries
Diffstat:
3 files changed, 148 insertions(+), 47 deletions(-)
diff --git a/packages/taler-wallet-core/src/dbtx-indexeddb.ts b/packages/taler-wallet-core/src/dbtx-indexeddb.ts
@@ -46,6 +46,7 @@ import {
WalletToken,
WalletDenomination,
WalletTransactionMeta,
+ DbPreciseTimestamp,
WalletOperationRetry,
WalletContractTerms,
DenominationVerificationStatus,
@@ -240,6 +241,76 @@ export class IdbWalletTransaction implements WalletDbTransaction {
await tx.transactionsMeta.delete(transactionId);
}
+ async getTransactionMeta(
+ transactionId: string,
+ ): Promise<WalletTransactionMeta | undefined> {
+ const tx = this.tx;
+ return await tx.transactionsMeta.get(transactionId);
+ }
+
+ async getTransactionMetaAtTimestamp(
+ timestamp: DbPreciseTimestamp,
+ ): Promise<WalletTransactionMeta | undefined> {
+ const tx = this.tx;
+ return await tx.transactionsMeta.indexes.byTimestamp.get(timestamp);
+ }
+
+ async getTransactionMetaBefore(
+ timestamp: DbPreciseTimestamp,
+ ): Promise<WalletTransactionMeta | undefined> {
+ const tx = this.tx;
+ // Slow, but only reached when a pagination offset transaction was deleted.
+ const recs = await tx.transactionsMeta.indexes.byTimestamp.getAll(
+ GlobalIDB.KeyRange.upperBound(timestamp, false),
+ );
+ if (recs.length > 0) {
+ return recs[recs.length - 1];
+ }
+ return undefined;
+ }
+
+ async getTransactionMetaAfter(
+ timestamp: DbPreciseTimestamp,
+ ): Promise<WalletTransactionMeta | undefined> {
+ const tx = this.tx;
+ const recs = await tx.transactionsMeta.indexes.byTimestamp.getAll(
+ GlobalIDB.KeyRange.lowerBound(timestamp, false),
+ 1,
+ );
+ return recs[0];
+ }
+
+ async listTransactionMetaByTimestamp(req: {
+ afterTimestamp?: DbPreciseTimestamp;
+ limit?: number;
+ }): Promise<WalletTransactionMeta[]> {
+ const tx = this.tx;
+ const range =
+ req.afterTimestamp != null
+ ? GlobalIDB.KeyRange.lowerBound(req.afterTimestamp, true)
+ : undefined;
+ return await tx.transactionsMeta.indexes.byTimestamp.getAll(
+ range,
+ req.limit,
+ );
+ }
+
+ async listTransactionMetaByStatus(req: {
+ onlyActive: boolean;
+ }): Promise<WalletTransactionMeta[]> {
+ const tx = this.tx;
+ const range = req.onlyActive ? getActiveKeyRange() : undefined;
+ return await tx.transactionsMeta.indexes.byStatus.getAll(range);
+ }
+
+ async deleteAllTransactionMeta(): Promise<void> {
+ const tx = this.tx;
+ const all = await tx.transactionsMeta.getAll();
+ for (const rec of all) {
+ await tx.transactionsMeta.delete(rec.transactionId);
+ }
+ }
+
async getOperationRetry(
taskId: string,
): Promise<WalletOperationRetry | undefined> {
diff --git a/packages/taler-wallet-core/src/dbtx.ts b/packages/taler-wallet-core/src/dbtx.ts
@@ -52,6 +52,7 @@ import {
WalletToken,
WalletDenomination,
WalletTransactionMeta,
+ DbPreciseTimestamp,
WalletOperationRetry,
WalletContractTerms,
DenominationVerificationStatus,
@@ -158,6 +159,63 @@ export interface WalletDbTransaction {
deleteTransactionMeta(transactionId: string): Promise<void>;
/**
+ * Get the transaction metadata for a transaction.
+ */
+ getTransactionMeta(
+ transactionId: string,
+ ): Promise<WalletTransactionMeta | undefined>;
+
+ /**
+ * Get the transaction metadata at exactly the given timestamp, if any.
+ */
+ getTransactionMetaAtTimestamp(
+ timestamp: DbPreciseTimestamp,
+ ): Promise<WalletTransactionMeta | undefined>;
+
+ /**
+ * Get the latest transaction metadata at or before the given timestamp.
+ *
+ * Used to resolve a pagination offset whose transaction has been deleted.
+ */
+ getTransactionMetaBefore(
+ timestamp: DbPreciseTimestamp,
+ ): Promise<WalletTransactionMeta | undefined>;
+
+ /**
+ * Get the earliest transaction metadata at or after the given timestamp.
+ */
+ getTransactionMetaAfter(
+ timestamp: DbPreciseTimestamp,
+ ): Promise<WalletTransactionMeta | undefined>;
+
+ /**
+ * List transaction metadata ordered by timestamp ascending.
+ *
+ * If afterTimestamp is given, only records strictly after it are returned.
+ * If limit is given, at most that many records are returned.
+ */
+ listTransactionMetaByTimestamp(req: {
+ afterTimestamp?: DbPreciseTimestamp;
+ limit?: number;
+ }): Promise<WalletTransactionMeta[]>;
+
+ /**
+ * List all transaction metadata, optionally restricted to transactions in a
+ * non-final ("active") state.
+ */
+ listTransactionMetaByStatus(req: {
+ onlyActive: boolean;
+ }): Promise<WalletTransactionMeta[]>;
+
+ /**
+ * Delete all transaction metadata.
+ *
+ * Used when re-materializing the transactionsMeta view from the underlying
+ * transaction stores.
+ */
+ deleteAllTransactionMeta(): Promise<void>;
+
+ /**
* Get the retry state of a task, if the task has been retried before.
*/
getOperationRetry(taskId: string): Promise<WalletOperationRetry | undefined>;
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
@@ -18,11 +18,6 @@
* Imports.
*/
import {
- BridgeIDBKeyRange,
- GlobalIDB,
- IDBKeyRange,
-} from "@gnu-taler/idb-bridge";
-import {
AbsoluteTime,
Amounts,
assertUnreachable,
@@ -314,7 +309,7 @@ async function findOffsetTransaction(
let forwards = req?.limit == null || req.limit >= 0;
let closestTimestamp: DbPreciseTimestamp | undefined = undefined;
if (req?.offsetTransactionId) {
- const res = await tx.transactionsMeta.get(req.offsetTransactionId);
+ const res = await tx.wtx.getTransactionMeta(req.offsetTransactionId);
if (res) {
return res;
}
@@ -327,7 +322,7 @@ async function findOffsetTransaction(
}
} else if (req?.offsetTimestamp) {
const dbStamp = timestampPreciseToDb(req.offsetTimestamp);
- const res = await tx.transactionsMeta.indexes.byTimestamp.get(dbStamp);
+ const res = await tx.wtx.getTransactionMetaAtTimestamp(dbStamp);
if (res) {
return res;
}
@@ -345,21 +340,10 @@ async function findOffsetTransaction(
// We don't want to skip transactions in pagination,
// so get the transaction before the timestamp
- // Slow query, but should not happen often!
- const recs = await tx.transactionsMeta.indexes.byTimestamp.getAll(
- BridgeIDBKeyRange.upperBound(closestTimestamp, false),
- );
- if (recs.length > 0) {
- return recs[recs.length - 1];
- }
- return undefined;
+ return await tx.wtx.getTransactionMetaBefore(closestTimestamp);
} else {
// Likewise, get the transaction after the timestamp
- const recs = await tx.transactionsMeta.indexes.byTimestamp.getAll(
- BridgeIDBKeyRange.lowerBound(closestTimestamp, false),
- 1,
- );
- return recs[0];
+ return await tx.wtx.getTransactionMetaAfter(closestTimestamp);
}
}
@@ -387,13 +371,15 @@ export async function getTransactionsV2(
if (limit == null && offsetMtx == null) {
// Fast path for returning *everything* that matches the filter.
// FIXME: We could use the DB for filtering here
- const res = await tx.transactionsMeta.indexes.byStatus.getAll();
+ const res = await tx.wtx.listTransactionMetaByStatus({
+ onlyActive: false,
+ });
await addFiltered(wex, tx, transactionsRequest, resultTransactions, res);
} else if (!forwards) {
// Descending, backwards request.
// Slow implementation. Doing it properly would require using cursors,
// which are also slow in IndexedDB.
- const res = await tx.transactionsMeta.indexes.byTimestamp.getAll();
+ const res = await tx.wtx.listTransactionMetaByTimestamp({});
res.reverse();
let start: number;
if (offsetMtx != null) {
@@ -415,15 +401,13 @@ export async function getTransactionsV2(
}
} else {
// Forward request
- let query: BridgeIDBKeyRange | undefined = undefined;
- if (offsetMtx != null) {
- query = GlobalIDB.KeyRange.lowerBound(offsetMtx.timestamp, true);
- }
+ let afterTimestamp: DbPreciseTimestamp | undefined =
+ offsetMtx != null ? offsetMtx.timestamp : undefined;
while (true) {
- const res = await tx.transactionsMeta.indexes.byTimestamp.getAll(
- query,
+ const res = await tx.wtx.listTransactionMetaByTimestamp({
+ afterTimestamp,
limit,
- );
+ });
if (res.length === 0) {
break;
}
@@ -438,10 +422,7 @@ export async function getTransactionsV2(
break;
}
// Continue after last result
- query = BridgeIDBKeyRange.lowerBound(
- res[res.length - 1].timestamp,
- true,
- );
+ afterTimestamp = res[res.length - 1].timestamp;
}
}
});
@@ -462,18 +443,12 @@ export async function getTransactions(
): Promise<TransactionsResponse> {
const transactions: Transaction[] = [];
- let keyRange: IDBKeyRange | undefined = undefined;
-
- if (transactionsRequest?.filterByState === "nonfinal") {
- keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_NONFINAL_FIRST,
- OPERATION_STATUS_NONFINAL_LAST,
- );
- }
+ const onlyActive = transactionsRequest?.filterByState === "nonfinal";
await wex.runLegacyWalletDbTx(async (tx) => {
- const allMetaTransactions =
- await tx.transactionsMeta.indexes.byStatus.getAll(keyRange);
+ const allMetaTransactions = await tx.wtx.listTransactionMetaByStatus({
+ onlyActive,
+ });
for (const metaTx of allMetaTransactions) {
if (
shouldSkipCurrency(
@@ -567,10 +542,7 @@ export async function rematerializeTransactions(
): Promise<void> {
logger.trace("re-materializing transactions");
- const allTxMeta = await tx.transactionsMeta.getAll();
- for (const txMeta of allTxMeta) {
- await tx.transactionsMeta.delete(txMeta.transactionId);
- }
+ await tx.wtx.deleteAllTransactionMeta();
await tx.peerPushDebit.iter().forEachAsync(async (x) => {
const ctx = new PeerPushDebitTransactionContext(wex, x.pursePub);