commit ce49869a0bfe7e13b92b40caeba550cade97fa44
parent d768391c048a6581ad719a21d83c1c544a4540d3
Author: Florian Dold <dold@taler.net>
Date: Sun, 19 Jul 2026 23:20:03 +0200
wallet: invalidate caches in the DAL instead of via DB triggers
Both backends drop the exchange, denomination and refresh-cost caches
after a mutating transaction. The native backend had no invalidation.
The IndexedDB trigger fired on any store access, dropping caches on a
plain read.
Diffstat:
8 files changed, 335 insertions(+), 105 deletions(-)
diff --git a/packages/taler-wallet-core/src/db-indexeddb.ts b/packages/taler-wallet-core/src/db-indexeddb.ts
@@ -2017,7 +2017,6 @@ export async function openStoredBackupsDatabase(
const handle = new DbAccessImpl(
backupsDbHandle,
StoredBackupStores,
- {},
CancellationToken.CONTINUE,
);
return handle;
@@ -2045,7 +2044,6 @@ export async function openTalerDatabase(
const metaDb = new DbAccessImpl(
metaDbHandle,
walletMetadataStore,
- {},
CancellationToken.CONTINUE,
);
let currentMainVersion: string | undefined;
diff --git a/packages/taler-wallet-core/src/dbtx-cache-invalidation.test.ts b/packages/taler-wallet-core/src/dbtx-cache-invalidation.test.ts
@@ -0,0 +1,232 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * The wallet caches exchange entries, denomination info and refresh costs in
+ * memory. Those caches are dropped after any transaction that changed a
+ * record they are derived from, which the DAL detects by watching for calls
+ * to the methods named in CACHE_INVALIDATING_METHODS.
+ *
+ * That list is maintained by hand, and a stale entry fails silently: the
+ * wallet keeps serving a cached denomination that no longer exists in the
+ * database, with no error anywhere. This test derives the list that *should*
+ * be there from the IndexedDB implementation and compares.
+ */
+
+import assert from "node:assert";
+import { existsSync, readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { test } from "node:test";
+
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
+
+import { runnerFactories } from "./dbtx-runners.js";
+import {
+ CACHE_INVALIDATING_METHODS,
+ watchForCacheInvalidation,
+} from "./dbtx-shared.js";
+
+/**
+ * The IndexedDB object stores the three caches are derived from, and the
+ * sqlite tables holding the same entities. A write to any of these can
+ * invalidate a cached value; a write to anything else cannot.
+ */
+const CACHE_BACKING_STORES = [
+ "exchanges",
+ "exchangeDetails",
+ "denominations",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+];
+const CACHE_BACKING_TABLES = [
+ "exchanges",
+ "exchange_details",
+ "denominations",
+ "global_currency_auditors",
+ "global_currency_exchanges",
+];
+
+/**
+ * Locate a file under src/, from wherever this test happens to be running.
+ * It normally runs from the compiled lib/, so "next to me" is not the answer.
+ */
+function readSourceFile(name: string): string {
+ let dir = dirname(fileURLToPath(import.meta.url));
+ for (let i = 0; i < 5; i++) {
+ const candidate = join(dir, "src", name);
+ if (existsSync(candidate)) {
+ return readFileSync(candidate, "utf-8");
+ }
+ dir = dirname(dir);
+ }
+ throw Error(`could not locate src/${name}`);
+}
+
+/**
+ * Split a transaction implementation into its methods, keyed by name.
+ *
+ * Whole methods rather than single lines, because an SQL statement is often
+ * built across several concatenated string literals, with the table name on a
+ * different line from the verb.
+ */
+function methodBodies(src: string): Map<string, string> {
+ // Class members sit at exactly two spaces of indentation.
+ const methodRe = /^ {2}(?:async )?([A-Za-z_$][\w$]*)\s*[(<]/;
+ const bodies = new Map<string, string>();
+ let current: string | undefined;
+ let buf: string[] = [];
+ const flush = () => {
+ if (current !== undefined) {
+ bodies.set(current, (bodies.get(current) ?? "") + buf.join("\n"));
+ }
+ };
+ for (const line of src.split("\n")) {
+ const m = methodRe.exec(line);
+ if (m) {
+ flush();
+ current = m[1];
+ buf = [];
+ }
+ buf.push(line);
+ }
+ flush();
+ return bodies;
+}
+
+/**
+ * Find the methods of one implementation that write a cache-backing entity.
+ */
+function findMutators(src: string, writeRe: RegExp): Set<string> {
+ const found = new Set<string>();
+ for (const [name, body] of methodBodies(src)) {
+ // Collapse the string concatenation SQL is assembled from, so a statement
+ // split across lines reads as one.
+ const flat = body.replace(/"\s*\+\s*"/g, "").replace(/\s+/g, " ");
+ if (writeRe.test(flat)) {
+ found.add(name);
+ }
+ }
+ return found;
+}
+
+test("every mutator of a cache-backing store invalidates the caches", () => {
+ const stores = CACHE_BACKING_STORES.join("|");
+ const tables = CACHE_BACKING_TABLES.join("|");
+
+ const impls: Array<{ file: string; writeRe: RegExp }> = [
+ {
+ // Writes look like `tx.denominations.put(rec)`, or `this.tx.exchanges
+ // .delete(baseUrl)` where the store handle was taken off `this`.
+ file: "dbtx-indexeddb.ts",
+ writeRe: new RegExp(
+ `\\b(?:this\\.)?tx\\.(?:${stores})\\.(?:put|add|delete|clear)\\(`,
+ ),
+ },
+ {
+ // Both implementations are scanned, not just IndexedDB: a method that
+ // writes one of these tables only on the sqlite side would otherwise
+ // never be noticed, and would silently stop invalidating caches on the
+ // backend it applies to.
+ file: "dbtx-sqlite.ts",
+ writeRe: new RegExp(
+ `(?:INSERT(?:\\s+OR\\s+\\w+)?\\s+INTO|DELETE\\s+FROM|UPDATE)\\s+"?(?:${tables})"?\\b`,
+ ),
+ },
+ ];
+
+ const mutators = new Set<string>();
+ for (const impl of impls) {
+ const found = findMutators(readSourceFile(impl.file), impl.writeRe);
+ // A scan that finds nothing would make this test vacuously pass, which is
+ // exactly the failure mode a source-scanning test has to rule out.
+ assert.ok(
+ found.size >= 8,
+ `only found ${found.size} cache-backing mutators in ${impl.file}` +
+ ` — the scan is probably broken, not the code`,
+ );
+ for (const name of found) {
+ mutators.add(name);
+ }
+ }
+
+ assert.deepStrictEqual(
+ [...mutators].sort(),
+ [...CACHE_INVALIDATING_METHODS].sort(),
+ "methods that write a cache-backing entity must be listed in" +
+ " CACHE_INVALIDATING_METHODS (left: found in the implementations," +
+ " right: declared in dbtx-shared.ts)",
+ );
+});
+
+/**
+ * The static test above only checks that the list is complete. This one
+ * checks that the wrapper acting on it actually works, against both real
+ * transaction implementations rather than a stub — the two differ in how
+ * their methods are defined (prototype methods vs. own properties), which is
+ * exactly what a Proxy `get` trap is sensitive to.
+ */
+for (const makeRunner of runnerFactories) {
+ test(`cache invalidation fires on writes only`, async (t) => {
+ const runner = await makeRunner();
+ await t.test(runner.name, async () => {
+ // A read must not invalidate. The old store-based trigger got this
+ // wrong: it fired whenever a readwrite transaction so much as *looked*
+ // at the denominations store, dropping every cache on a pure read.
+ const readFlag = { dirty: false };
+ await runner.runTx(async (tx) => {
+ await watchForCacheInvalidation(
+ tx,
+ readFlag,
+ ).listGlobalCurrencyExchanges();
+ });
+ assert.strictEqual(
+ readFlag.dirty,
+ false,
+ "a read-only transaction must not invalidate the caches",
+ );
+
+ // A write to a cache-backing store must.
+ const writeFlag = { dirty: false };
+ await runner.runTx(async (tx) => {
+ await watchForCacheInvalidation(
+ tx,
+ writeFlag,
+ ).addGlobalCurrencyExchange({
+ currency: "TESTKUDOS",
+ exchangeBaseUrl: "https://exchange.test/",
+ exchangeMasterPub: encodeCrock(stringToBytes("master-pub-0000")),
+ });
+ });
+ assert.strictEqual(
+ writeFlag.dirty,
+ true,
+ "a transaction that wrote a cache-backing store must invalidate",
+ );
+
+ // The wrapper must not otherwise change behaviour: results still come
+ // back, and methods still see the right `this`.
+ const rows = await runner.runTx(async (tx) => {
+ return await watchForCacheInvalidation(tx, {
+ dirty: false,
+ }).listGlobalCurrencyExchanges();
+ });
+ assert.strictEqual(rows.length, 1);
+ assert.strictEqual(rows[0].currency, "TESTKUDOS");
+ });
+ await runner.close();
+ });
+}
diff --git a/packages/taler-wallet-core/src/dbtx-runners.ts b/packages/taler-wallet-core/src/dbtx-runners.ts
@@ -58,7 +58,6 @@ export async function makeIdbRunner(
const db = new DbAccessImpl(
dbHandle,
WalletIndexedDbStoresV1,
- {},
CancellationToken.CONTINUE,
);
return {
diff --git a/packages/taler-wallet-core/src/dbtx-shared.ts b/packages/taler-wallet-core/src/dbtx-shared.ts
@@ -103,3 +103,58 @@ export async function getExchangeScopeInfoGeneric(
url: det.exchangeBaseUrl,
};
}
+
+/**
+ * DAL methods after which the wallet's in-memory caches are stale.
+ *
+ * The caches hold exchange summaries, denomination info and refresh costs,
+ * all derived from these entities. Listing the methods here rather than
+ * having each backend decide keeps the two implementations from drifting: a
+ * cache that is dropped on one backend and not the other is a bug that only
+ * shows up as stale data much later.
+ *
+ * Mutations only. Reading a denomination cannot invalidate anything derived
+ * from denominations.
+ */
+export const CACHE_INVALIDATING_METHODS: ReadonlySet<string> = new Set([
+ "upsertExchange",
+ "deleteExchange",
+ "upsertExchangeDetails",
+ "deleteExchangeDetails",
+ "upsertDenomination",
+ "deleteDenomination",
+ "addGlobalCurrencyExchange",
+ "deleteGlobalCurrencyExchange",
+ "addGlobalCurrencyAuditor",
+ "deleteGlobalCurrencyAuditor",
+]);
+
+/**
+ * Wrap a transaction so that calls to cache-invalidating methods are noticed.
+ *
+ * `flag.dirty` is set as a side effect; the caller drops the caches after the
+ * transaction commits, never before, so a rolled-back transaction does not
+ * invalidate anything.
+ */
+export function watchForCacheInvalidation<T extends WalletDbTransaction>(
+ tx: T,
+ flag: { dirty: boolean },
+): T {
+ return new Proxy(tx, {
+ get(target, prop, receiver) {
+ const value = Reflect.get(target, prop, receiver);
+ if (typeof prop === "string" && CACHE_INVALIDATING_METHODS.has(prop)) {
+ if (typeof value === "function") {
+ return (...args: unknown[]) => {
+ flag.dirty = true;
+ return value.apply(target, args);
+ };
+ }
+ }
+ if (typeof value === "function") {
+ return value.bind(target);
+ }
+ return value;
+ },
+ });
+}
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
@@ -1891,8 +1891,6 @@ export async function updateExchangeFromUrlHandler(
return TaskRunResult.progress();
}
- wex.ws.clearAllCaches();
-
const oldExchangeState = getExchangeState(r);
const existingDetails = await getExchangeRecordsInternal(tx, r.baseUrl);
let detailsPointerChanged = false;
diff --git a/packages/taler-wallet-core/src/query.ts b/packages/taler-wallet-core/src/query.ts
@@ -737,13 +737,11 @@ function makeTxClientContext(
indexes[indexAlias] = {
get(key) {
internalContext.throwIfInactive();
- internalContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).index(indexName).get(key);
return requestToPromise(req, internalContext);
},
iter(query) {
internalContext.throwIfInactive();
- internalContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
@@ -752,7 +750,6 @@ function makeTxClientContext(
},
getAll(query, count) {
internalContext.throwIfInactive();
- internalContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
@@ -761,7 +758,6 @@ function makeTxClientContext(
},
getAllKeys(query, count) {
internalContext.throwIfInactive();
- internalContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
@@ -770,7 +766,6 @@ function makeTxClientContext(
},
count(query) {
internalContext.throwIfInactive();
- internalContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).index(indexName).count(query);
return requestToPromise(req, internalContext);
},
@@ -780,19 +775,16 @@ function makeTxClientContext(
indexes,
get(key) {
internalContext.throwIfInactive();
- internalContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).get(key);
return requestToPromise(req, internalContext);
},
getAll(query, count) {
internalContext.throwIfInactive();
- internalContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).getAll(query, count);
return requestToPromise(req, internalContext);
},
iter(query) {
internalContext.throwIfInactive();
- internalContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).openCursor(query);
return new ResultStream<any>(req);
},
@@ -801,8 +793,6 @@ function makeTxClientContext(
if (!internalContext.allowWrite) {
throw Error("attempting write in a read-only transaction");
}
- internalContext.storesAccessed.add(storeName);
- internalContext.storesModified.add(storeName);
const req = tx.objectStore(storeName).add(r, k);
const key = await requestToPromise(req, internalContext);
return {
@@ -814,8 +804,6 @@ function makeTxClientContext(
if (!internalContext.allowWrite) {
throw Error("attempting write in a read-only transaction");
}
- internalContext.storesAccessed.add(storeName);
- internalContext.storesModified.add(storeName);
const req = tx.objectStore(storeName).put(r, k);
const key = await requestToPromise(req, internalContext);
return {
@@ -827,14 +815,11 @@ function makeTxClientContext(
if (!internalContext.allowWrite) {
throw Error("attempting write in a read-only transaction");
}
- internalContext.storesAccessed.add(storeName);
- internalContext.storesModified.add(storeName);
const req = tx.objectStore(storeName).delete(k);
return requestToPromise(req, internalContext);
},
count(query) {
internalContext.throwIfInactive();
- internalContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).count(query);
return requestToPromise(req, internalContext);
},
@@ -873,46 +858,22 @@ export interface DbAccess<Stores extends StoreMap> {
): Promise<T>;
}
-export interface AfterCommitInfo {
- mode: IDBTransactionMode;
- scope: Set<string>;
- accessedStores: Set<string>;
- modifiedStores: Set<string>;
-}
-
-export interface TriggerSpec {
- /**
- * Trigger run after every successful commit, run outside of the transaction.
- */
- afterCommit?: (info: AfterCommitInfo) => void;
-
- // onRead(store, value)
- // initState<State> () => State
- // beforeCommit<State>? (tx: Transaction, s: State | undefined) => Promise<void>;
-}
-
/**
* Additional state we store for every IndexedDB transaction opened
* via the query helper.
*/
class InternalTransactionContext {
isAborted = false;
- storesScope: Set<string>;
- storesAccessed: Set<string> = new Set();
- storesModified: Set<string> = new Set();
allowWrite: boolean;
abortExn: TransactionAbortedError | undefined;
notifications: WalletNotification[] = [];
afterCommitHandlers: (() => void)[] = [];
constructor(
- private readonly triggerSpec: TriggerSpec,
private readonly mode: IDBTransactionMode,
- readonly scope: string[],
public readonly cancellationToken: CancellationToken,
public readonly applyNotifications?: (notifs: WalletNotification[]) => void,
) {
- this.storesScope = new Set(scope);
this.allowWrite = mode === "readwrite" || mode === "versionchange";
}
@@ -925,14 +886,6 @@ class InternalTransactionContext {
}
handleAfterCommit() {
- if (this.triggerSpec.afterCommit) {
- this.triggerSpec.afterCommit({
- mode: this.mode,
- accessedStores: this.storesAccessed,
- modifiedStores: this.storesModified,
- scope: this.storesScope,
- });
- }
for (const f of this.afterCommitHandlers) {
f();
}
@@ -955,7 +908,6 @@ export class DbAccessImpl<Stores extends StoreMap> implements DbAccess<Stores> {
constructor(
private db: IDBDatabase,
private stores: Stores,
- private triggers: TriggerSpec = {},
private cancellationToken: CancellationToken,
private applyNotifications?: (notifs: WalletNotification[]) => void,
) {}
@@ -982,10 +934,8 @@ export class DbAccessImpl<Stores extends StoreMap> implements DbAccess<Stores> {
accessibleStores[swi.storeName] = swi;
}
const mode = "readwrite";
- const triggerContext = new InternalTransactionContext(
- this.triggers,
+ const internalContext = new InternalTransactionContext(
mode,
- strStoreNames,
this.cancellationToken,
this.applyNotifications,
);
@@ -993,8 +943,8 @@ export class DbAccessImpl<Stores extends StoreMap> implements DbAccess<Stores> {
const writeContext = makeTxClientContext(
tx,
accessibleStores,
- triggerContext,
+ internalContext,
);
- return await runTx(tx, writeContext, txf, triggerContext);
+ return await runTx(tx, writeContext, txf, internalContext);
}
}
diff --git a/packages/taler-wallet-core/src/requests.ts b/packages/taler-wallet-core/src/requests.ts
@@ -690,6 +690,10 @@ async function recoverStoredBackup(
await rematerializeTransactions(wex, tx);
});
}
+
+ // Both branches replaced the database wholesale, bypassing the DAL that
+ // normally notices such changes, so the caches have to be dropped by hand.
+ wex.ws.clearAllCaches();
logger.info(`import done`);
}
@@ -1658,6 +1662,12 @@ async function handleImportDb(
await wex.runWalletDbTx(async (tx) => {
await rematerializeTransactions(wex, tx);
});
+
+ // The import replaced the database underneath the DAL, writing through the
+ // raw IndexedDB handle, so none of the automatic invalidation saw it. Every
+ // cached exchange, denomination and refresh cost now describes records that
+ // are gone.
+ wex.ws.clearAllCaches();
return {};
}
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -75,15 +75,12 @@ import {
import { ConfigRecordKey, WalletDenomination } from "./db-common.js";
import {
WalletIndexedDbStoresV1,
- WalletIndexedDbTransaction,
applyFixups,
openTalerDatabase,
} from "./db-indexeddb.js";
import { WalletDbTransaction } from "./dbtx.js";
+import { watchForCacheInvalidation } from "./dbtx-shared.js";
import {
- clearNativeSqliteWalletDb,
- exportNativeSqliteDb,
- importNativeSqliteDb,
NativeSqliteWalletDb,
runNativeSqliteWalletTx,
} from "./dbtx-sqlite.js";
@@ -102,13 +99,7 @@ import {
observeTalerCrypto,
} from "./observable-wrappers.js";
import { ProgressContext } from "./progress.js";
-import {
- AfterCommitInfo,
- DbAccess,
- DbAccessImpl,
- TransactionAbortedError,
- TriggerSpec,
-} from "./query.js";
+import { DbAccess, DbAccessImpl, TransactionAbortedError } from "./query.js";
import { TaskScheduler, TaskSchedulerImpl } from "./shepherd.js";
import { rematerializeTransactions } from "./transactions.js";
import {
@@ -395,6 +386,11 @@ async function runWalletDbTx<T>(
wex: WalletExecutionContext,
f: (tx: WalletDbTransaction) => Promise<T>,
): Promise<T> {
+ // Set by the wrapper below when the transaction calls a method that makes
+ // the wallet's caches stale. Acted on only after a successful commit: a
+ // transaction that rolls back changed nothing, and an attempt that is about
+ // to be retried resets the flag so a discarded write cannot carry over.
+ const dirty = { dirty: false };
const ndb = wex.ws.nativeSqliteDb;
if (ndb) {
// Native backend: a real sqlite transaction, no IndexedDB emulation.
@@ -407,6 +403,7 @@ async function runWalletDbTx<T>(
// not touch, so without this the native backend would be invisible to
// the observability API.
return await handleTxRetries(wex, async () => {
+ dirty.dirty = false;
const location = getCallerInfo();
wex.oc.observe({
type: ObservabilityEventType.DbQueryStart,
@@ -418,7 +415,7 @@ async function runWalletDbTx<T>(
const ret = await runNativeSqliteWalletTx(
ndb,
(notif) => wex.ws.notify(notif),
- async (tx) => await f(tx),
+ async (tx) => await f(watchForCacheInvalidation(tx, dirty)),
);
wex.oc.observe({
type: ObservabilityEventType.DbQueryFinishSuccess,
@@ -426,6 +423,9 @@ async function runWalletDbTx<T>(
location,
durationMs: performanceDelta(start, performanceNow()),
});
+ if (dirty.dirty) {
+ wex.ws.clearAllCaches();
+ }
return ret;
} catch (e) {
wex.oc.observe({
@@ -440,10 +440,15 @@ async function runWalletDbTx<T>(
});
}
return await handleTxRetries(wex, async () => {
- return await wex.db.runAllStoresReadWriteTx({}, async (mytx) => {
+ dirty.dirty = false;
+ const ret = await wex.db.runAllStoresReadWriteTx({}, async (mytx) => {
const tx = new IdbWalletTransaction(mytx);
- return await f(tx);
+ return await f(watchForCacheInvalidation(tx, dirty));
});
+ if (dirty.dirty) {
+ wex.ws.clearAllCaches();
+ }
+ return ret;
});
}
@@ -668,34 +673,6 @@ export class Wallet {
}
/**
- * Implementation of triggers for the wallet DB.
- */
-class WalletDbTriggerSpec implements TriggerSpec {
- constructor(public ws: InternalWalletState) {}
-
- afterCommit(info: AfterCommitInfo): void {
- if (info.mode !== "readwrite") {
- return;
- }
- logger.trace(
- `in after commit callback for readwrite, modified ${j2s([
- ...info.modifiedStores,
- ])}`,
- );
- const modified = info.accessedStores;
- if (
- modified.has(WalletIndexedDbStoresV1.exchanges.storeName) ||
- modified.has(WalletIndexedDbStoresV1.exchangeDetails.storeName) ||
- modified.has(WalletIndexedDbStoresV1.denominations.storeName) ||
- modified.has(WalletIndexedDbStoresV1.globalCurrencyAuditors.storeName) ||
- modified.has(WalletIndexedDbStoresV1.globalCurrencyExchanges.storeName)
- ) {
- this.ws.clearAllCaches();
- }
- }
-}
-
-/**
* Internal state of the wallet.
*
* This ties together all the operation implementations.
@@ -796,23 +773,35 @@ export class InternalWalletState {
async runStandaloneWalletDbTx<T>(
f: (tx: WalletDbTransaction) => Promise<T>,
): Promise<T> {
+ const dirty = { dirty: false };
const ndb = this.nativeSqliteDb;
if (ndb) {
- return await runNativeSqliteWalletTx(
+ const ret = await runNativeSqliteWalletTx(
ndb,
(notif) => this.notify(notif),
- async (tx) => await f(tx),
+ async (tx) => await f(watchForCacheInvalidation(tx, dirty)),
);
+ if (dirty.dirty) {
+ this.clearAllCaches();
+ }
+ return ret;
}
if (!this._dbAccessHandle) {
this._dbAccessHandle = this.createDbAccessHandle(
CancellationToken.CONTINUE,
);
}
- return await this._dbAccessHandle.runAllStoresReadWriteTx(
+ const ret = await this._dbAccessHandle.runAllStoresReadWriteTx(
{},
- async (mytx) => await f(new IdbWalletTransaction(mytx)),
+ async (mytx) =>
+ await f(
+ watchForCacheInvalidation(new IdbWalletTransaction(mytx), dirty),
+ ),
);
+ if (dirty.dirty) {
+ this.clearAllCaches();
+ }
+ return ret;
}
/**
@@ -878,7 +867,6 @@ export class InternalWalletState {
return new DbAccessImpl(
this._indexedDbHandle,
WalletIndexedDbStoresV1,
- new WalletDbTriggerSpec(this),
cancellationToken,
(notifs: WalletNotification[]): void => {
for (const notif of notifs) {