taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit be2c4a1d5926fa9220ff9272cba8b637e4bed8b1
parent 796bf3f95aebf24d5a88d59c0df21d494c5d864d
Author: Florian Dold <dold@taler.net>
Date:   Sun, 19 Jul 2026 23:51:45 +0200

db: run the conformance suite against WalletDbHandle

DbTxRunner was a parallel abstraction over the same thing.  It is now an
alias for WalletDbHandle, and the runners construct the production
handles.

Diffstat:
Mpackages/taler-wallet-core/src/dbtx-bench.ts | 30+++++++++++++++---------------
Mpackages/taler-wallet-core/src/dbtx-cache-invalidation.test.ts | 6+++---
Mpackages/taler-wallet-core/src/dbtx-conformance-cases.ts | 736+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mpackages/taler-wallet-core/src/dbtx-conformance.ts | 30++++++------------------------
Mpackages/taler-wallet-core/src/dbtx-handle-impl.ts | 28+++++++++++++++++++++++++---
Mpackages/taler-wallet-core/src/dbtx-runners.ts | 74+++++++++++++-------------------------------------------------------------
Mpackages/taler-wallet-core/src/dbtx-sqlite.test.ts | 2+-
Mpackages/taler-wallet-core/src/wallet.ts | 12+-----------
8 files changed, 487 insertions(+), 431 deletions(-)

diff --git a/packages/taler-wallet-core/src/dbtx-bench.ts b/packages/taler-wallet-core/src/dbtx-bench.ts @@ -138,7 +138,7 @@ async function populate( ); // Denominations and their availability rows. - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { for (let d = 0; d < opts.numDenominations; d++) { const ex = exchangeUrl(d % opts.numExchanges); const dph = hash(`denom-${d}`); @@ -186,7 +186,7 @@ async function populate( // Coins, in batches so a single transaction does not grow unbounded. const batch = 2000; for (let start = 0; start < opts.numCoins; start += batch) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { for (let i = start; i < Math.min(start + batch, opts.numCoins); i++) { const d = i % opts.numDenominations; const coin: WalletCoin = { @@ -252,7 +252,7 @@ async function measure( // A point lookup on the primary key, the single most common operation. const someCoin = key(`coin-${Math.floor(opts.numCoins / 2)}`); await time("getCoin (point lookup)", async () => - runner.runTx(async (tx) => ((await tx.getCoin(someCoin)) ? 1 : 0)), + runner.runReadWriteTx(async (tx) => ((await tx.getCoin(someCoin)) ? 1 : 0)), ); // Batch lookup: this is the shape refresh uses, and was an N+1 until @@ -262,28 +262,28 @@ async function measure( pubs.push(key(`coin-${i}`)); } await time("getCoinsByPubs (200)", async () => - runner.runTx(async (tx) => (await tx.getCoinsByPubs(pubs)).length), + runner.runReadWriteTx(async (tx) => (await tx.getCoinsByPubs(pubs)).length), ); await time("getCoinsByExchange", async () => - runner.runTx( + runner.runReadWriteTx( async (tx) => (await tx.getCoinsByExchange(exchangeUrl(0))).length, ), ); await time("countCoinsByExchange", async () => - runner.runTx(async (tx) => tx.countCoinsByExchange(exchangeUrl(0))), + runner.runReadWriteTx(async (tx) => tx.countCoinsByExchange(exchangeUrl(0))), ); await time("getCoinsByDenomPubHash", async () => - runner.runTx( + runner.runReadWriteTx( async (tx) => (await tx.getCoinsByDenomPubHash(hash("denom-0"))).length, ), ); // Indexed multi-column lookup with a limit -- coin selection's hot path. await time("getFreshCoinsByDenomAndAge (limit 10)", async () => - runner.runTx( + runner.runReadWriteTx( async (tx) => ( await tx.getFreshCoinsByDenomAndAge( @@ -297,7 +297,7 @@ async function measure( ); await time("getCoinAvailabilityByExchangeAndAgeRange", async () => - runner.runTx( + runner.runReadWriteTx( async (tx) => ( await tx.getCoinAvailabilityByExchangeAndAgeRange( @@ -312,7 +312,7 @@ async function measure( // The early-terminating keyset scan. Deliberately matches nothing until // late, so a backend that materialises the whole family shows up here. await time("findDenominationByFamilyFromExpiry", async () => - runner.runTx(async (tx) => { + runner.runReadWriteTx(async (tx) => { const found = await tx.findDenominationByFamilyFromExpiry( 1, 0 as DbProtocolTimestamp, @@ -323,7 +323,7 @@ async function measure( ); await time("getDenominationsByExchange", async () => - runner.runTx( + runner.runReadWriteTx( async (tx) => (await tx.getDenominationsByExchange(exchangeUrl(0))).length, ), @@ -331,16 +331,16 @@ async function measure( // Full scans: the wallet does these on balance computation and purge. await time("listAllCoins (full scan)", async () => - runner.runTx(async (tx) => (await tx.listAllCoins()).length), + runner.runReadWriteTx(async (tx) => (await tx.listAllCoins()).length), ); await time("getCoinAvailabilities (full scan)", async () => - runner.runTx(async (tx) => (await tx.getCoinAvailabilities()).length), + runner.runReadWriteTx(async (tx) => (await tx.getCoinAvailabilities()).length), ); // A write-heavy transaction, to keep an eye on commit cost. await time("upsertCoin x100 (one tx)", async () => - runner.runTx(async (tx) => { + runner.runReadWriteTx(async (tx) => { for (let i = 0; i < 100; i++) { const coin = await tx.getCoin(key(`coin-${i}`)); if (coin) { @@ -375,7 +375,7 @@ export async function benchmarkOneBackend( // Verify the database really holds what was asked for. A benchmark on a // database that silently failed to populate reports beautiful numbers for // queries that match nothing, which is worse than no benchmark at all. - const actualCoins = await runner.runTx( + const actualCoins = await runner.runReadWriteTx( async (tx) => (await tx.listAllCoins()).length, ); if (actualCoins !== opts.numCoins) { diff --git a/packages/taler-wallet-core/src/dbtx-cache-invalidation.test.ts b/packages/taler-wallet-core/src/dbtx-cache-invalidation.test.ts @@ -187,7 +187,7 @@ for (const makeRunner of runnerFactories) { // 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 runner.runReadWriteTx(async (tx) => { await watchForCacheInvalidation( tx, readFlag, @@ -201,7 +201,7 @@ for (const makeRunner of runnerFactories) { // A write to a cache-backing store must. const writeFlag = { dirty: false }; - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await watchForCacheInvalidation( tx, writeFlag, @@ -219,7 +219,7 @@ for (const makeRunner of runnerFactories) { // 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) => { + const rows = await runner.runReadWriteTx(async (tx) => { return await watchForCacheInvalidation(tx, { dirty: false, }).listGlobalCurrencyExchanges(); diff --git a/packages/taler-wallet-core/src/dbtx-conformance-cases.ts b/packages/taler-wallet-core/src/dbtx-conformance-cases.ts @@ -682,13 +682,13 @@ export const conformanceCases: ConformanceCase[] = [ { name: "config: upsert then get round trips", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertConfig({ key: ConfigRecordKey.MaterializedTransactionsVersion, value: 7, }); }); - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getConfig(ConfigRecordKey.MaterializedTransactionsVersion), ); t.ok(got, "config record should exist"); @@ -699,7 +699,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "get on a missing key returns undefined, not a throw", async run(t, runner) { - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getCoin(ck("no-such-coin-pub")), ); t.equal(got, undefined); @@ -714,10 +714,12 @@ export const conformanceCases: ConformanceCase[] = [ unicode: "ünïcödé", num: 1.5, }; - await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertContractTerms({ h: "hash-1", contractTermsRaw: raw }), ); - const got = await runner.runTx((tx) => tx.getContractTerms("hash-1")); + const got = await runner.runReadWriteTx((tx) => + tx.getContractTerms("hash-1"), + ); t.deepEqual(got?.contractTermsRaw, raw); }, }, @@ -727,11 +729,11 @@ export const conformanceCases: ConformanceCase[] = [ { name: "denomination: compound primary key (exchange, denomPubHash)", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertDenomination(makeDenomination("https://e1/", "dph-a")); await tx.upsertDenomination(makeDenomination("https://e2/", "dph-a")); }); - const [d1, d2] = await runner.runTx(async (tx) => [ + const [d1, d2] = await runner.runReadWriteTx(async (tx) => [ await tx.getDenomination("https://e1/", ckh("dph-a")), await tx.getDenomination("https://e2/", ckh("dph-a")), ]); @@ -749,7 +751,7 @@ export const conformanceCases: ConformanceCase[] = [ // nothing, getRefundItemsByGroup silently returned [], and refunds ended // in state "done" instead of "failed". It passed tsc and every unit test. async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { // Items are written before their group here, deliberately: that is // the order pay-merchant.ts uses, and a backend must tolerate it // within a transaction. @@ -759,15 +761,15 @@ export const conformanceCases: ConformanceCase[] = [ await tx.upsertRefundGroup(makeRefundGroup("grp-1")); await tx.upsertRefundGroup(makeRefundGroup("grp-2")); }); - const items = await runner.runTx((tx) => + const items = await runner.runReadWriteTx((tx) => tx.getRefundItemsByGroup("grp-1"), ); t.equal(items.length, 2, "must find both items of the group"); - const other = await runner.runTx((tx) => + const other = await runner.runReadWriteTx((tx) => tx.getRefundItemsByGroup("grp-2"), ); t.equal(other.length, 1); - const none = await runner.runTx((tx) => + const none = await runner.runReadWriteTx((tx) => tx.getRefundItemsByGroup("grp-missing"), ); t.equal(none.length, 0); @@ -777,16 +779,16 @@ export const conformanceCases: ConformanceCase[] = [ { name: "refund item lookup by (coinPub, rtxid)", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertRefundItem(makeRefundItem("grp-3", "coin-9", 42)); await tx.upsertRefundGroup(makeRefundGroup("grp-3")); }); - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getRefundItemByCoinAndRtxid(ck("coin-9"), 42), ); t.ok(got, "compound index lookup must find the item"); t.equal(got?.refundGroupId, "grp-3"); - const miss = await runner.runTx((tx) => + const miss = await runner.runReadWriteTx((tx) => tx.getRefundItemByCoinAndRtxid(ck("coin-9"), 43), ); t.equal(miss, undefined, "wrong rtxid must not match"); @@ -802,13 +804,13 @@ export const conformanceCases: ConformanceCase[] = [ // as a foreign key. A backend returning 0 or undefined would corrupt the // exchange entry silently. async run(t, runner) { - const id1 = await runner.runTx((tx) => + const id1 = await runner.runReadWriteTx((tx) => tx.upsertReserve({ reservePub: ck("rp-1"), reservePriv: ck("rv-1"), } as any), ); - const id2 = await runner.runTx((tx) => + const id2 = await runner.runReadWriteTx((tx) => tx.upsertReserve({ reservePub: ck("rp-2"), reservePriv: ck("rv-2"), @@ -817,7 +819,7 @@ export const conformanceCases: ConformanceCase[] = [ t.equal(typeof id1, "number"); t.equal(typeof id2, "number"); t.ok(id1 !== id2, "generated ids must be distinct"); - const back = await runner.runTx((tx) => tx.getReserve(id1)); + const back = await runner.runReadWriteTx((tx) => tx.getReserve(id1)); t.equal( back?.reservePub, ck("rp-1"), @@ -831,7 +833,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "findDenominationByFamilyFromExpiry returns the first match", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { // Same family, ascending expiry. The first two are not offered, so // the predicate must skip them. await tx.upsertDenomination( @@ -855,7 +857,7 @@ export const conformanceCases: ConformanceCase[] = [ }), ); }); - const found = await runner.runTx((tx) => + const found = await runner.runReadWriteTx((tx) => tx.findDenominationByFamilyFromExpiry(77, ts(0), (d) => d.isOffered), ); t.equal( @@ -873,7 +875,7 @@ export const conformanceCases: ConformanceCase[] = [ // denominations, so that turned a one-record read into a full range scan. // The predicate is the only place we can observe how far the scan ran. async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { for (let i = 0; i < 20; i++) { await tx.upsertDenomination( makeDenomination("https://e/", `s-${i}`, { @@ -884,14 +886,14 @@ export const conformanceCases: ConformanceCase[] = [ } }); let examined = 0; - const before = runner.recordsRead?.(); - const found = await runner.runTx((tx) => + const before = runner.getAccessStats()?.recordsRead; + const found = await runner.runReadWriteTx((tx) => tx.findDenominationByFamilyFromExpiry(88, ts(0), () => { examined++; return true; }), ); - const after = runner.recordsRead?.(); + const after = runner.getAccessStats()?.recordsRead; t.ok(found, "should find a denomination"); t.equal(examined, 1, "the predicate must be consulted once"); if (before !== undefined && after !== undefined) { @@ -909,7 +911,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "findDenominationByFamilyFromExpiry respects the family boundary", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertDenomination( makeDenomination("https://e/", "f1-a", { familySerial: 101, @@ -924,7 +926,7 @@ export const conformanceCases: ConformanceCase[] = [ }), ); }); - const found = await runner.runTx((tx) => + const found = await runner.runReadWriteTx((tx) => tx.findDenominationByFamilyFromExpiry(101, ts(0), (d) => d.isOffered), ); t.equal( @@ -938,7 +940,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "findDenominationByFamilyFromExpiry honours the expiry lower bound", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertDenomination( makeDenomination("https://e/", "old", { familySerial: 111, @@ -952,7 +954,7 @@ export const conformanceCases: ConformanceCase[] = [ }), ); }); - const found = await runner.runTx((tx) => + const found = await runner.runReadWriteTx((tx) => tx.findDenominationByFamilyFromExpiry(111, ts(10000), () => true), ); t.equal( @@ -969,7 +971,7 @@ export const conformanceCases: ConformanceCase[] = [ name: "a throwing transaction rolls back its writes", async run(t, runner) { try { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertContractTerms({ h: "rollback-hash", contractTermsRaw: { x: 1 }, @@ -980,7 +982,7 @@ export const conformanceCases: ConformanceCase[] = [ } catch (e) { // expected } - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getContractTerms("rollback-hash"), ); t.equal(got, undefined, "writes before the throw must not be visible"); @@ -990,7 +992,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "writes are visible within the same transaction", async run(t, runner) { - const got = await runner.runTx(async (tx) => { + const got = await runner.runReadWriteTx(async (tx) => { await tx.upsertContractTerms({ h: "same-tx", contractTermsRaw: { y: 2 }, @@ -1005,7 +1007,7 @@ export const conformanceCases: ConformanceCase[] = [ name: "scheduleOnCommit runs after the transaction, not during it", async run(t, runner) { const order: string[] = []; - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { tx.scheduleOnCommit(() => order.push("after-commit")); order.push("in-tx"); }); @@ -1018,7 +1020,7 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { let ran = false; try { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { tx.scheduleOnCommit(() => { ran = true; }); @@ -1037,7 +1039,7 @@ export const conformanceCases: ConformanceCase[] = [ // pass it as a bare function (applyNotifyTransition(tx.notify, ...)), which // is well-typed but lost `this` and broke every transaction that used it. async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { const notify = tx.notify; notify({ type: "balance-change" } as any); }); @@ -1050,7 +1052,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "deleting a missing row is a no-op, not an error", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.deletePurchase("no-such-proposal"); await tx.deleteDenomination("https://nope/", ckh("no-such-hash")); await tx.deleteRefundGroup("no-such-group"); @@ -1062,12 +1064,12 @@ export const conformanceCases: ConformanceCase[] = [ { name: "getRecordCounts reflects what was written", async run(t, runner) { - const before = await runner.runTx((tx) => tx.getRecordCounts()); - await runner.runTx(async (tx) => { + const before = await runner.runReadWriteTx((tx) => tx.getRecordCounts()); + await runner.runReadWriteTx(async (tx) => { await tx.upsertDenomination(makeDenomination("https://cnt/", "c-1")); await tx.upsertDenomination(makeDenomination("https://cnt/", "c-2")); }); - const after = await runner.runTx((tx) => tx.getRecordCounts()); + const after = await runner.runReadWriteTx((tx) => tx.getRecordCounts()); t.equal( after.denominations - before.denominations, 2, @@ -1079,7 +1081,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "upsert overwrites an existing row rather than duplicating it", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertDenomination( makeDenomination("https://e-up/", "dup", { stampExpireWithdraw: 111, @@ -1091,7 +1093,7 @@ export const conformanceCases: ConformanceCase[] = [ }), ); }); - const all = await runner.runTx((tx) => + const all = await runner.runReadWriteTx((tx) => tx.getDenominationsByExchange("https://e-up/"), ); t.equal(all.length, 1, "the second upsert must replace, not append"); @@ -1103,13 +1105,13 @@ export const conformanceCases: ConformanceCase[] = [ { name: "reserve: upsert returns a generated row id, retrievable by pub", async run(t, runner) { - const [id1, id2] = await runner.runTx(async (tx) => [ + const [id1, id2] = await runner.runReadWriteTx(async (tx) => [ await tx.upsertReserve(makeReserve("rpub-1")), await tx.upsertReserve(makeReserve("rpub-2")), ]); t.ok(typeof id1 === "number", "upsertReserve must return the row id"); t.ok(id1 !== id2, "row ids must be distinct"); - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getReserveByReservePub(ck("rpub-2")), ); t.equal(got?.reservePub, ck("rpub-2")); @@ -1122,10 +1124,10 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { // Callers persist this id on the exchange entry, so a reused id would // silently repoint an exchange at a different reserve. - const first = await runner.runTx((tx) => + const first = await runner.runReadWriteTx((tx) => tx.upsertReserve(makeReserve("rpub-r1")), ); - const second = await runner.runTx((tx) => + const second = await runner.runReadWriteTx((tx) => tx.upsertReserve(makeReserve("rpub-r2")), ); t.ok(second > first, "ids must be strictly increasing"); @@ -1143,12 +1145,16 @@ export const conformanceCases: ConformanceCase[] = [ retryCounter: 3, }, }; - await runner.runTx((tx) => tx.upsertOperationRetry(rec)); - const got = await runner.runTx((tx) => tx.getOperationRetry("task-1")); + await runner.runReadWriteTx((tx) => tx.upsertOperationRetry(rec)); + const got = await runner.runReadWriteTx((tx) => + tx.getOperationRetry("task-1"), + ); t.equal(got?.id, "task-1"); t.equal(got?.retryInfo.retryCounter, 3); - await runner.runTx((tx) => tx.deleteOperationRetry("task-1")); - const gone = await runner.runTx((tx) => tx.getOperationRetry("task-1")); + await runner.runReadWriteTx((tx) => tx.deleteOperationRetry("task-1")); + const gone = await runner.runReadWriteTx((tx) => + tx.getOperationRetry("task-1"), + ); t.equal(gone, undefined, "delete must remove the record"); }, }, @@ -1156,7 +1162,9 @@ export const conformanceCases: ConformanceCase[] = [ { name: "operation retry: deleting a missing task is not an error", async run(t, runner) { - await runner.runTx((tx) => tx.deleteOperationRetry("never-existed")); + await runner.runReadWriteTx((tx) => + tx.deleteOperationRetry("never-existed"), + ); t.ok(true, "delete of an absent key must be a no-op"); }, }, @@ -1169,7 +1177,7 @@ export const conformanceCases: ConformanceCase[] = [ hint: "unexpected", detail: { nested: [1, null, "x"] }, }; - await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertOperationRetry({ id: "task-err", lastError, @@ -1180,7 +1188,9 @@ export const conformanceCases: ConformanceCase[] = [ }, }), ); - const got = await runner.runTx((tx) => tx.getOperationRetry("task-err")); + const got = await runner.runReadWriteTx((tx) => + tx.getOperationRetry("task-err"), + ); t.deepEqual(got?.lastError, lastError); }, }, @@ -1191,7 +1201,7 @@ export const conformanceCases: ConformanceCase[] = [ // The DAL exposes no way to read tombstones back, so this can only // assert that the second write does not raise (a primary-key conflict // would). See the note in the sqlite schema. - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertTombstone({ id: "tmb:dup" }); await tx.upsertTombstone({ id: "tmb:dup" }); }); @@ -1202,14 +1212,16 @@ export const conformanceCases: ConformanceCase[] = [ { name: "refund group: get by id and list by proposal", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertRefundGroup(makeRefundGroup("rg-a", "prop-x")); await tx.upsertRefundGroup(makeRefundGroup("rg-b", "prop-x")); await tx.upsertRefundGroup(makeRefundGroup("rg-c", "prop-y")); }); - const one = await runner.runTx((tx) => tx.getRefundGroup("rg-b")); + const one = await runner.runReadWriteTx((tx) => + tx.getRefundGroup("rg-b"), + ); t.equal(one?.refundGroupId, "rg-b"); - const byProp = await runner.runTx((tx) => + const byProp = await runner.runReadWriteTx((tx) => tx.getRefundGroupsByProposal("prop-x"), ); t.equal(byProp.length, 2, "must return only the groups of that proposal"); @@ -1220,7 +1232,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "refund group: unknown proposal yields an empty list, not undefined", async run(t, runner) { - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getRefundGroupsByProposal("no-such-proposal"), ); t.deepEqual(got, [], "list queries must return [] when nothing matches"); @@ -1230,19 +1242,19 @@ export const conformanceCases: ConformanceCase[] = [ { name: "refund item: delete removes only the targeted item", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertRefundGroup(makeRefundGroup("rg-del")); await tx.upsertRefundItem(makeRefundItem("rg-del", "coin-d1", 101)); await tx.upsertRefundItem(makeRefundItem("rg-del", "coin-d2", 102)); }); - const items = await runner.runTx((tx) => + const items = await runner.runReadWriteTx((tx) => tx.getRefundItemsByGroup("rg-del"), ); t.equal(items.length, 2); const victim = items.find((i) => i.coinPub === ck("coin-d1")); t.ok(victim?.id !== undefined, "stored items must carry their row id"); - await runner.runTx((tx) => tx.deleteRefundItem(victim!.id!)); - const left = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.deleteRefundItem(victim!.id!)); + const left = await runner.runReadWriteTx((tx) => tx.getRefundItemsByGroup("rg-del"), ); t.equal(left.length, 1); @@ -1253,7 +1265,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "denomination: list by verification status", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { const a = makeDenomination("https://e1/", "vs-a"); a.verificationStatus = DenominationVerificationStatus.Unverified; const b = makeDenomination("https://e1/", "vs-b"); @@ -1261,7 +1273,7 @@ export const conformanceCases: ConformanceCase[] = [ await tx.upsertDenomination(a); await tx.upsertDenomination(b); }); - const unverified = await runner.runTx((tx) => + const unverified = await runner.runReadWriteTx((tx) => tx.getDenominationsByVerificationStatus( DenominationVerificationStatus.Unverified, ), @@ -1276,8 +1288,8 @@ export const conformanceCases: ConformanceCase[] = [ name: "coin: round trips with age commitment proof absent", async run(t, runner) { const coin = makeCoin("cp-1"); - await runner.runTx((tx) => tx.upsertCoin(coin)); - const got = await runner.runTx((tx) => tx.getCoin(ck("cp-1"))); + await runner.runReadWriteTx((tx) => tx.upsertCoin(coin)); + const got = await runner.runReadWriteTx((tx) => tx.getCoin(ck("cp-1"))); t.ok(got, "coin should exist"); t.deepEqual(got, coin, "every field must survive the round trip"); t.ok( @@ -1297,8 +1309,8 @@ export const conformanceCases: ConformanceCase[] = [ coinIndex: 3, reservePub: ck("rp-1"), }; - await runner.runTx((tx) => tx.upsertCoin(coin)); - const got = await runner.runTx((tx) => tx.getCoin(ck("cp-src"))); + await runner.runReadWriteTx((tx) => tx.upsertCoin(coin)); + const got = await runner.runReadWriteTx((tx) => tx.getCoin(ck("cp-src"))); t.deepEqual(got?.coinSource, coin.coinSource); t.deepEqual(got?.denomSig, coin.denomSig); }, @@ -1309,8 +1321,10 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { const coin = makeCoin("cp-status"); coin.status = CoinStatus.Dormant; - await runner.runTx((tx) => tx.upsertCoin(coin)); - const got = await runner.runTx((tx) => tx.getCoin(ck("cp-status"))); + await runner.runReadWriteTx((tx) => tx.upsertCoin(coin)); + const got = await runner.runReadWriteTx((tx) => + tx.getCoin(ck("cp-status")), + ); t.equal(got?.status, CoinStatus.Dormant); t.equal(typeof got?.status, "string", "CoinStatus must not be coerced"); }, @@ -1320,11 +1334,11 @@ export const conformanceCases: ConformanceCase[] = [ name: "coin: upsert overwrites rather than duplicating", async run(t, runner) { const coin = makeCoin("cp-up"); - await runner.runTx((tx) => tx.upsertCoin(coin)); + await runner.runReadWriteTx((tx) => tx.upsertCoin(coin)); coin.status = CoinStatus.Dormant; coin.visible = 1; - await runner.runTx((tx) => tx.upsertCoin(coin)); - const all = await runner.runTx((tx) => tx.listAllCoins()); + await runner.runReadWriteTx((tx) => tx.upsertCoin(coin)); + const all = await runner.runReadWriteTx((tx) => tx.listAllCoins()); const mine = all.filter((c) => c.coinPub === ck("cp-up")); t.equal(mine.length, 1, "a second upsert must replace, not append"); t.equal(mine[0].status, CoinStatus.Dormant); @@ -1335,7 +1349,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "coin: queries by exchange, denom and source transaction", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { const a = makeCoin("cq-1"); a.exchangeBaseUrl = "https://ex-a/"; a.denomPubHash = ckh("dq-1"); @@ -1351,19 +1365,19 @@ export const conformanceCases: ConformanceCase[] = [ await tx.upsertCoin(b); await tx.upsertCoin(c); }); - const byEx = await runner.runTx((tx) => + const byEx = await runner.runReadWriteTx((tx) => tx.getCoinsByExchange("https://ex-a/"), ); t.equal(byEx.length, 2); - const count = await runner.runTx((tx) => + const count = await runner.runReadWriteTx((tx) => tx.countCoinsByExchange("https://ex-a/"), ); t.equal(count, 2, "count must agree with the list"); - const byDenom = await runner.runTx((tx) => + const byDenom = await runner.runReadWriteTx((tx) => tx.getCoinsByDenomPubHash(ckh("dq-1")), ); t.equal(byDenom.length, 2, "denom hash spans exchanges"); - const bySrc = await runner.runTx((tx) => + const bySrc = await runner.runReadWriteTx((tx) => tx.getCoinsBySourceTransaction("txn-1"), ); t.equal(bySrc.length, 1); @@ -1374,11 +1388,11 @@ export const conformanceCases: ConformanceCase[] = [ { name: "coin: getCoinsByPubs skips missing pubs and keeps argument order", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertCoin(makeCoin("cb-1")); await tx.upsertCoin(makeCoin("cb-2")); }); - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getCoinsByPubs([ck("cb-2"), ck("cb-missing"), ck("cb-1")]), ); t.deepEqual( @@ -1392,7 +1406,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "coin: fresh-coin lookup filters on status and respects the limit", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { for (let i = 0; i < 4; i++) { const c = makeCoin(`cf-${i}`); c.exchangeBaseUrl = "https://ex-f/"; @@ -1402,11 +1416,11 @@ export const conformanceCases: ConformanceCase[] = [ await tx.upsertCoin(c); } }); - const all = await runner.runTx((tx) => + const all = await runner.runReadWriteTx((tx) => tx.getFreshCoinsByDenomAndAge("https://ex-f/", ckh("df-1"), 21, 100), ); t.equal(all.length, 3, "the dormant coin must be excluded"); - const limited = await runner.runTx((tx) => + const limited = await runner.runReadWriteTx((tx) => tx.getFreshCoinsByDenomAndAge("https://ex-f/", ckh("df-1"), 21, 2), ); t.equal(limited.length, 2, "the limit must be applied"); @@ -1416,20 +1430,25 @@ export const conformanceCases: ConformanceCase[] = [ { name: "coin: delete removes the coin and its history independently", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertCoin(makeCoin("cd-1")); await tx.upsertCoinHistory({ coinPub: ck("cd-1"), history: [{ type: "withdraw", transactionId: txnId("txn:cd-1") }], }); }); - await runner.runTx((tx) => tx.deleteCoin(ck("cd-1"))); - t.equal(await runner.runTx((tx) => tx.getCoin(ck("cd-1"))), undefined); - const hist = await runner.runTx((tx) => tx.getCoinHistory(ck("cd-1"))); + await runner.runReadWriteTx((tx) => tx.deleteCoin(ck("cd-1"))); + t.equal( + await runner.runReadWriteTx((tx) => tx.getCoin(ck("cd-1"))), + undefined, + ); + const hist = await runner.runReadWriteTx((tx) => + tx.getCoinHistory(ck("cd-1")), + ); t.ok(hist, "deleteCoin must not delete the history record"); - await runner.runTx((tx) => tx.deleteCoinHistory(ck("cd-1"))); + await runner.runReadWriteTx((tx) => tx.deleteCoinHistory(ck("cd-1"))); t.equal( - await runner.runTx((tx) => tx.getCoinHistory(ck("cd-1"))), + await runner.runReadWriteTx((tx) => tx.getCoinHistory(ck("cd-1"))), undefined, ); }, @@ -1451,10 +1470,12 @@ export const conformanceCases: ConformanceCase[] = [ amount: amt("TESTKUDOS:0.25"), }, ]; - await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertCoinHistory({ coinPub: ck("ch-1"), history }), ); - const got = await runner.runTx((tx) => tx.getCoinHistory(ck("ch-1"))); + const got = await runner.runReadWriteTx((tx) => + tx.getCoinHistory(ck("ch-1")), + ); t.deepEqual(got?.history, history); }, }, @@ -1464,14 +1485,14 @@ export const conformanceCases: ConformanceCase[] = [ { name: "coin availability: compound primary key of (exchange, denom, age)", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertCoinAvailability(makeAvail("https://ea/", "da", 0)); await tx.upsertCoinAvailability(makeAvail("https://ea/", "da", 21)); }); - const a = await runner.runTx((tx) => + const a = await runner.runReadWriteTx((tx) => tx.getCoinAvailability("https://ea/", ckh("da"), 0), ); - const b = await runner.runTx((tx) => + const b = await runner.runReadWriteTx((tx) => tx.getCoinAvailability("https://ea/", ckh("da"), 21), ); t.ok(a && b, "differing maxAge must be distinct rows"); @@ -1484,18 +1505,18 @@ export const conformanceCases: ConformanceCase[] = [ name: "coin availability: upsert updates counts in place", async run(t, runner) { const rec = makeAvail("https://eu/", "du", 0); - await runner.runTx((tx) => tx.upsertCoinAvailability(rec)); + await runner.runReadWriteTx((tx) => tx.upsertCoinAvailability(rec)); rec.freshCoinCount = 9; rec.visibleCoinCount = 4; rec.pendingRefreshOutputCount = 2; - await runner.runTx((tx) => tx.upsertCoinAvailability(rec)); - const got = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertCoinAvailability(rec)); + const got = await runner.runReadWriteTx((tx) => tx.getCoinAvailability("https://eu/", ckh("du"), 0), ); t.equal(got?.freshCoinCount, 9); t.equal(got?.visibleCoinCount, 4); t.equal(got?.pendingRefreshOutputCount, 2); - const byEx = await runner.runTx((tx) => + const byEx = await runner.runReadWriteTx((tx) => tx.getCoinAvailabilityByExchange("https://eu/"), ); t.equal(byEx.length, 1, "the upsert must not have appended a row"); @@ -1512,7 +1533,7 @@ export const conformanceCases: ConformanceCase[] = [ // a row with a higher max_age and zero fresh coins is still inside it. // A naive `max_age BETWEEN ? AND ? AND fresh_coin_count >= 1` would // drop that row. - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { const atLowerNoFresh = makeAvail("https://er/", "d-lo", 0); atLowerNoFresh.freshCoinCount = 0; const atLowerFresh = makeAvail("https://er/", "d-lf", 0); @@ -1523,7 +1544,7 @@ export const conformanceCases: ConformanceCase[] = [ await tx.upsertCoinAvailability(atLowerFresh); await tx.upsertCoinAvailability(aboveNoFresh); }); - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getCoinAvailabilityByExchangeAndAgeRange("https://er/", 0, 21), ); const hashes = got.map((a) => a.denomPubHash).sort(); @@ -1538,14 +1559,14 @@ export const conformanceCases: ConformanceCase[] = [ { name: "coin availability: delete targets one (exchange, denom, age)", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertCoinAvailability(makeAvail("https://ed/", "dd", 0)); await tx.upsertCoinAvailability(makeAvail("https://ed/", "dd", 21)); }); - await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.deleteCoinAvailability("https://ed/", ckh("dd"), 0), ); - const left = await runner.runTx((tx) => + const left = await runner.runReadWriteTx((tx) => tx.getCoinAvailabilityByExchange("https://ed/"), ); t.equal(left.length, 1); @@ -1563,8 +1584,10 @@ export const conformanceCases: ConformanceCase[] = [ currency: "TESTKUDOS", updateClock: tsPrecise(4242), }; - await runner.runTx((tx) => tx.upsertExchange(ex)); - const got = await runner.runTx((tx) => tx.getExchange("https://ex-rt/")); + await runner.runReadWriteTx((tx) => tx.upsertExchange(ex)); + const got = await runner.runReadWriteTx((tx) => + tx.getExchange("https://ex-rt/"), + ); t.deepEqual(got, ex, "every field must survive the round trip"); }, }, @@ -1574,8 +1597,10 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { const ex = makeExchange("https://ex-np/"); ex.detailsPointer = undefined; - await runner.runTx((tx) => tx.upsertExchange(ex)); - const got = await runner.runTx((tx) => tx.getExchange("https://ex-np/")); + await runner.runReadWriteTx((tx) => tx.upsertExchange(ex)); + const got = await runner.runReadWriteTx((tx) => + tx.getExchange("https://ex-np/"), + ); t.equal(got?.detailsPointer, undefined); t.ok( got !== undefined && "detailsPointer" in got, @@ -1591,12 +1616,16 @@ export const conformanceCases: ConformanceCase[] = [ const explicitlyFalse = makeExchange("https://ex-b2/"); explicitlyFalse.noFees = false; explicitlyFalse.peerPaymentsDisabled = false; - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertExchange(unset); await tx.upsertExchange(explicitlyFalse); }); - const a = await runner.runTx((tx) => tx.getExchange("https://ex-b1/")); - const b = await runner.runTx((tx) => tx.getExchange("https://ex-b2/")); + const a = await runner.runReadWriteTx((tx) => + tx.getExchange("https://ex-b1/"), + ); + const b = await runner.runReadWriteTx((tx) => + tx.getExchange("https://ex-b2/"), + ); t.equal(a?.noFees, undefined, "an unset flag must not become false"); t.equal(b?.noFees, false, "an explicit false must not become undefined"); t.equal(b?.peerPaymentsDisabled, false); @@ -1606,20 +1635,20 @@ export const conformanceCases: ConformanceCase[] = [ { name: "exchange: list and delete", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertExchange(makeExchange("https://ex-l1/")); await tx.upsertExchange(makeExchange("https://ex-l2/")); }); - const all = await runner.runTx((tx) => tx.getExchanges()); + const all = await runner.runReadWriteTx((tx) => tx.getExchanges()); const mine = all.filter((e) => e.baseUrl.startsWith("https://ex-l")); t.equal(mine.length, 2); - await runner.runTx((tx) => tx.deleteExchange("https://ex-l1/")); + await runner.runReadWriteTx((tx) => tx.deleteExchange("https://ex-l1/")); t.equal( - await runner.runTx((tx) => tx.getExchange("https://ex-l1/")), + await runner.runReadWriteTx((tx) => tx.getExchange("https://ex-l1/")), undefined, ); t.ok( - await runner.runTx((tx) => tx.getExchange("https://ex-l2/")), + await runner.runReadWriteTx((tx) => tx.getExchange("https://ex-l2/")), "deleting one exchange must not affect the other", ); }, @@ -1631,9 +1660,11 @@ export const conformanceCases: ConformanceCase[] = [ name: "exchange details: upsert returns a row id and round trips", async run(t, runner) { const det = makeExchangeDetails("https://ed-1/", "mpk-a"); - const rowId = await runner.runTx((tx) => tx.upsertExchangeDetails(det)); + const rowId = await runner.runReadWriteTx((tx) => + tx.upsertExchangeDetails(det), + ); t.ok(typeof rowId === "number", "must return the generated row id"); - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getExchangeDetailsByPointer( "https://ed-1/", "TESTKUDOS", @@ -1651,12 +1682,16 @@ export const conformanceCases: ConformanceCase[] = [ name: "exchange details: a second upsert with the row id updates in place", async run(t, runner) { const det = makeExchangeDetails("https://ed-u/", "mpk-u"); - const rowId = await runner.runTx((tx) => tx.upsertExchangeDetails(det)); + const rowId = await runner.runReadWriteTx((tx) => + tx.upsertExchangeDetails(det), + ); det.rowId = rowId; det.currency = "EUR"; - const again = await runner.runTx((tx) => tx.upsertExchangeDetails(det)); + const again = await runner.runReadWriteTx((tx) => + tx.upsertExchangeDetails(det), + ); t.equal(again, rowId, "the row id must be stable across updates"); - const list = await runner.runTx((tx) => + const list = await runner.runReadWriteTx((tx) => tx.listExchangeDetailsByBaseUrl("https://ed-u/"), ); t.equal(list.length, 1, "the update must not have inserted a row"); @@ -1667,7 +1702,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "exchange details: resolved through the exchange's pointer", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { const det = makeExchangeDetails("https://ep/", "mpk-p"); await tx.upsertExchangeDetails(det); const ex = makeExchange("https://ep/"); @@ -1678,7 +1713,7 @@ export const conformanceCases: ConformanceCase[] = [ }; await tx.upsertExchange(ex); }); - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getExchangeDetails("https://ep/"), ); t.equal(got?.masterPublicKey, ck("mpk-p")); @@ -1688,7 +1723,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "exchange details: no pointer means no details, not a throw", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertExchangeDetails( makeExchangeDetails("https://enp/", "mpk-n"), ); @@ -1696,7 +1731,7 @@ export const conformanceCases: ConformanceCase[] = [ ex.detailsPointer = undefined; await tx.upsertExchange(ex); }); - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getExchangeDetails("https://enp/"), ); t.equal(got, undefined, "details must not be found without a pointer"); @@ -1706,7 +1741,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "exchange details: unknown exchange yields undefined", async run(t, runner) { - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getExchangeDetails("https://never-added/"), ); t.equal(got, undefined); @@ -1718,14 +1753,14 @@ export const conformanceCases: ConformanceCase[] = [ { name: "exchange sign keys: keyed by (details row id, signkey pub)", async run(t, runner) { - const rowId = await runner.runTx((tx) => + const rowId = await runner.runReadWriteTx((tx) => tx.upsertExchangeDetails(makeExchangeDetails("https://esk/", "mpk-s")), ); - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertExchangeSignKey(makeSignKey(rowId, "sk-1")); await tx.upsertExchangeSignKey(makeSignKey(rowId, "sk-2")); }); - const keys = await runner.runTx((tx) => + const keys = await runner.runReadWriteTx((tx) => tx.getExchangeSignKeysByDetailsRowId(rowId), ); t.equal(keys.length, 2); @@ -1733,8 +1768,10 @@ export const conformanceCases: ConformanceCase[] = [ keys.map((k) => k.signkeyPub).sort(), [ck("sk-1"), ck("sk-2")].sort(), ); - await runner.runTx((tx) => tx.deleteExchangeSignKey(rowId, ck("sk-1"))); - const left = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => + tx.deleteExchangeSignKey(rowId, ck("sk-1")), + ); + const left = await runner.runReadWriteTx((tx) => tx.getExchangeSignKeysByDetailsRowId(rowId), ); t.equal(left.length, 1); @@ -1745,14 +1782,14 @@ export const conformanceCases: ConformanceCase[] = [ { name: "exchange sign keys: re-upserting the same pub updates it", async run(t, runner) { - const rowId = await runner.runTx((tx) => + const rowId = await runner.runReadWriteTx((tx) => tx.upsertExchangeDetails(makeExchangeDetails("https://esk2/", "mpk-t")), ); const key = makeSignKey(rowId, "sk-dup"); - await runner.runTx((tx) => tx.upsertExchangeSignKey(key)); + await runner.runReadWriteTx((tx) => tx.upsertExchangeSignKey(key)); key.masterSig = ckh("sig-updated"); - await runner.runTx((tx) => tx.upsertExchangeSignKey(key)); - const keys = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertExchangeSignKey(key)); + const keys = await runner.runReadWriteTx((tx) => tx.getExchangeSignKeysByDetailsRowId(rowId), ); t.equal(keys.length, 1, "must replace, not append"); @@ -1766,11 +1803,11 @@ export const conformanceCases: ConformanceCase[] = [ name: "denomination family: looked up by the full seven-part params", async run(t, runner) { const params = makeFamilyParams("https://df/", "TESTKUDOS:1"); - const serial = await runner.runTx((tx) => + const serial = await runner.runReadWriteTx((tx) => tx.upsertDenominationFamily({ familyParams: params }), ); t.ok(typeof serial === "number", "must return the generated serial"); - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getDenominationFamilyByParams(params), ); t.equal(got?.denominationFamilySerial, serial); @@ -1784,11 +1821,11 @@ export const conformanceCases: ConformanceCase[] = [ // Five of the seven components are amounts, so a transposition between // two of them would otherwise silently resolve to the wrong family. const base = makeFamilyParams("https://df2/", "TESTKUDOS:1"); - await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertDenominationFamily({ familyParams: base }), ); const differing = { ...base, feeRefund: amt("TESTKUDOS:0.99") }; - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getDenominationFamilyByParams(differing), ); t.equal(got, undefined, "a different fee must not match"); @@ -1801,7 +1838,7 @@ export const conformanceCases: ConformanceCase[] = [ const params = makeFamilyParams("https://df3/", "TESTKUDOS:1"); params.feeDeposit = amt("TESTKUDOS:0.01"); params.feeRefresh = amt("TESTKUDOS:0.02"); - await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertDenominationFamily({ familyParams: params }), ); const swapped = { @@ -1809,7 +1846,7 @@ export const conformanceCases: ConformanceCase[] = [ feeDeposit: params.feeRefresh, feeRefresh: params.feeDeposit, }; - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getDenominationFamilyByParams(swapped), ); t.equal(got, undefined, "component order must be significant"); @@ -1819,7 +1856,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "denomination family: list by exchange and delete by serial", async run(t, runner) { - const [s1] = await runner.runTx(async (tx) => [ + const [s1] = await runner.runReadWriteTx(async (tx) => [ await tx.upsertDenominationFamily({ familyParams: makeFamilyParams("https://df4/", "TESTKUDOS:1"), }), @@ -1827,12 +1864,12 @@ export const conformanceCases: ConformanceCase[] = [ familyParams: makeFamilyParams("https://df4/", "TESTKUDOS:2"), }), ]); - const fams = await runner.runTx((tx) => + const fams = await runner.runReadWriteTx((tx) => tx.getDenominationFamiliesByExchange("https://df4/"), ); t.equal(fams.length, 2); - await runner.runTx((tx) => tx.deleteDenominationFamily(s1)); - const left = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.deleteDenominationFamily(s1)); + const left = await runner.runReadWriteTx((tx) => tx.getDenominationFamiliesByExchange("https://df4/"), ); t.equal(left.length, 1); @@ -1844,23 +1881,23 @@ export const conformanceCases: ConformanceCase[] = [ { name: "exchange base URL fixup: round trips and updates", async run(t, runner) { - await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertExchangeBaseUrlFixup({ exchangeBaseUrl: "https://old/", replacement: "https://new/", }), ); - let got = await runner.runTx((tx) => + let got = await runner.runReadWriteTx((tx) => tx.getExchangeBaseUrlFixup("https://old/"), ); t.equal(got?.replacement, "https://new/"); - await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertExchangeBaseUrlFixup({ exchangeBaseUrl: "https://old/", replacement: "https://newer/", }), ); - got = await runner.runTx((tx) => + got = await runner.runReadWriteTx((tx) => tx.getExchangeBaseUrlFixup("https://old/"), ); t.equal(got?.replacement, "https://newer/"); @@ -1876,8 +1913,8 @@ export const conformanceCases: ConformanceCase[] = [ timestamp: tsPrecise(9000), reason: ExchangeMigrationReason.MismatchedBaseUrl, }; - await runner.runTx((tx) => tx.upsertExchangeMigrationLog(rec)); - const got = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertExchangeMigrationLog(rec)); + const got = await runner.runReadWriteTx((tx) => tx.getExchangeMigrationLog("https://a/", "https://b/"), ); t.deepEqual(withoutUndefined(got), withoutUndefined(rec)); @@ -1886,7 +1923,7 @@ export const conformanceCases: ConformanceCase[] = [ "string", "ExchangeMigrationReason is a string enum and must not be coerced", ); - const other = await runner.runTx((tx) => + const other = await runner.runReadWriteTx((tx) => tx.getExchangeMigrationLog("https://b/", "https://a/"), ); t.equal(other, undefined, "the pair is ordered"); @@ -1896,12 +1933,12 @@ export const conformanceCases: ConformanceCase[] = [ { name: "denominations: listed by exchange", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertDenomination(makeDenomination("https://dl1/", "d-1")); await tx.upsertDenomination(makeDenomination("https://dl1/", "d-2")); await tx.upsertDenomination(makeDenomination("https://dl2/", "d-3")); }); - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getDenominationsByExchange("https://dl1/"), ); t.equal(got.length, 2); @@ -1928,8 +1965,10 @@ export const conformanceCases: ConformanceCase[] = [ }, exchangeCreditAccounts: [], }; - await runner.runTx((tx) => tx.upsertWithdrawalGroup(wg)); - const got = await runner.runTx((tx) => tx.getWithdrawalGroup("wg-bi")); + await runner.runReadWriteTx((tx) => tx.upsertWithdrawalGroup(wg)); + const got = await runner.runReadWriteTx((tx) => + tx.getWithdrawalGroup("wg-bi"), + ); t.deepEqual(got?.wgInfo, wg.wgInfo, "the whole variant must survive"); }, }, @@ -1947,27 +1986,29 @@ export const conformanceCases: ConformanceCase[] = [ withdrawalType: WithdrawalRecordType.BankIntegrated, bankInfo: makeBankInfo("taler://withdraw/example/first"), }; - await runner.runTx((tx) => tx.upsertWithdrawalGroup(wg)); + await runner.runReadWriteTx((tx) => tx.upsertWithdrawalGroup(wg)); wg.wgInfo = { withdrawalType: WithdrawalRecordType.BankIntegrated, bankInfo: makeBankInfo("taler://withdraw/example/second"), }; - await runner.runTx((tx) => tx.upsertWithdrawalGroup(wg)); + await runner.runReadWriteTx((tx) => tx.upsertWithdrawalGroup(wg)); - const byOld = await runner.runTx((tx) => + const byOld = await runner.runReadWriteTx((tx) => tx.getWithdrawalGroupByTalerWithdrawUri( "taler://withdraw/example/first", ), ); t.equal(byOld, undefined, "the old URI must no longer resolve"); - const byNew = await runner.runTx((tx) => + const byNew = await runner.runReadWriteTx((tx) => tx.getWithdrawalGroupByTalerWithdrawUri( "taler://withdraw/example/second", ), ); t.equal(byNew?.withdrawalGroupId, "wg-uri"); - const rec = await runner.runTx((tx) => tx.getWithdrawalGroup("wg-uri")); + const rec = await runner.runReadWriteTx((tx) => + tx.getWithdrawalGroup("wg-uri"), + ); t.equal(rec?.wgInfo.withdrawalType, WithdrawalRecordType.BankIntegrated); if (rec?.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated) { t.equal( @@ -1997,8 +2038,8 @@ export const conformanceCases: ConformanceCase[] = [ for (let i = 0; i < variants.length; i++) { const wg = makeWithdrawalGroup(`wg-var-${i}`); wg.wgInfo = variants[i]; - await runner.runTx((tx) => tx.upsertWithdrawalGroup(wg)); - const got = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertWithdrawalGroup(wg)); + const got = await runner.runReadWriteTx((tx) => tx.getWithdrawalGroup(`wg-var-${i}`), ); t.deepEqual( @@ -2020,17 +2061,17 @@ export const conformanceCases: ConformanceCase[] = [ withdrawalType: WithdrawalRecordType.BankIntegrated, bankInfo: makeBankInfo("taler://withdraw/example/switch"), }; - await runner.runTx((tx) => tx.upsertWithdrawalGroup(wg)); + await runner.runReadWriteTx((tx) => tx.upsertWithdrawalGroup(wg)); wg.wgInfo = { withdrawalType: WithdrawalRecordType.BankManual }; - await runner.runTx((tx) => tx.upsertWithdrawalGroup(wg)); + await runner.runReadWriteTx((tx) => tx.upsertWithdrawalGroup(wg)); - const got = await runner.runTx((tx) => + const got = await runner.runReadWriteTx((tx) => tx.getWithdrawalGroup("wg-switch"), ); t.deepEqual(got?.wgInfo, { withdrawalType: WithdrawalRecordType.BankManual, }); - const stale = await runner.runTx((tx) => + const stale = await runner.runReadWriteTx((tx) => tx.getWithdrawalGroupByTalerWithdrawUri( "taler://withdraw/example/switch", ), @@ -2042,7 +2083,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "withdrawal group: active groups are the non-final ones", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { const pending = makeWithdrawalGroup("wg-act"); pending.status = WithdrawalGroupStatus.PendingRegisteringBank; const done = makeWithdrawalGroup("wg-done"); @@ -2050,7 +2091,9 @@ export const conformanceCases: ConformanceCase[] = [ await tx.upsertWithdrawalGroup(pending); await tx.upsertWithdrawalGroup(done); }); - const active = await runner.runTx((tx) => tx.getActiveWithdrawalGroups()); + const active = await runner.runReadWriteTx((tx) => + tx.getActiveWithdrawalGroups(), + ); const ids = active.map((w) => w.withdrawalGroupId); t.ok(ids.includes("wg-act"), "a pending group must be active"); t.ok(!ids.includes("wg-done"), "a finished group must not be active"); @@ -2060,7 +2103,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "withdrawal group: query and count by exchange agree", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { for (let i = 0; i < 3; i++) { const wg = makeWithdrawalGroup(`wg-ex-${i}`); wg.exchangeBaseUrl = "https://wex/"; @@ -2070,11 +2113,11 @@ export const conformanceCases: ConformanceCase[] = [ other.exchangeBaseUrl = "https://wother/"; await tx.upsertWithdrawalGroup(other); }); - const list = await runner.runTx((tx) => + const list = await runner.runReadWriteTx((tx) => tx.getWithdrawalGroupsByExchange("https://wex/"), ); t.equal(list.length, 3); - const count = await runner.runTx((tx) => + const count = await runner.runReadWriteTx((tx) => tx.countWithdrawalGroupsByExchange("https://wex/"), ); t.equal(count, 3, "count must agree with the list, not be capped at 1"); @@ -2084,12 +2127,12 @@ export const conformanceCases: ConformanceCase[] = [ { name: "withdrawal group: delete", async run(t, runner) { - await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertWithdrawalGroup(makeWithdrawalGroup("wg-del")), ); - await runner.runTx((tx) => tx.deleteWithdrawalGroup("wg-del")); + await runner.runReadWriteTx((tx) => tx.deleteWithdrawalGroup("wg-del")); t.equal( - await runner.runTx((tx) => tx.getWithdrawalGroup("wg-del")), + await runner.runReadWriteTx((tx) => tx.getWithdrawalGroup("wg-del")), undefined, ); }, @@ -2100,15 +2143,17 @@ export const conformanceCases: ConformanceCase[] = [ { name: "planchet: round trips and is addressable by group and index", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertPlanchet(makePlanchet("pl-1", "wg-p", 0)); await tx.upsertPlanchet(makePlanchet("pl-2", "wg-p", 1)); }); - const byIdx = await runner.runTx((tx) => + const byIdx = await runner.runReadWriteTx((tx) => tx.getPlanchetByGroupAndIndex("wg-p", 1), ); t.equal(byIdx?.coinPub, ck("pl-2")); - const direct = await runner.runTx((tx) => tx.getPlanchet(ck("pl-1"))); + const direct = await runner.runReadWriteTx((tx) => + tx.getPlanchet(ck("pl-1")), + ); t.deepEqual(direct, makePlanchet("pl-1", "wg-p", 0)); }, }, @@ -2118,8 +2163,10 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { const pl = makePlanchet("pl-err", "wg-e", 0); pl.lastError = undefined; - await runner.runTx((tx) => tx.upsertPlanchet(pl)); - const got = await runner.runTx((tx) => tx.getPlanchet(ck("pl-err"))); + await runner.runReadWriteTx((tx) => tx.upsertPlanchet(pl)); + const got = await runner.runReadWriteTx((tx) => + tx.getPlanchet(ck("pl-err")), + ); t.ok( got !== undefined && "lastError" in got, "lastError is declared as a required key", @@ -2131,28 +2178,32 @@ export const conformanceCases: ConformanceCase[] = [ { name: "planchet: list, count and bulk delete by group", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { for (let i = 0; i < 4; i++) { await tx.upsertPlanchet(makePlanchet(`pl-g${i}`, "wg-bulk", i)); } await tx.upsertPlanchet(makePlanchet("pl-keep", "wg-keep", 0)); }); t.equal( - (await runner.runTx((tx) => tx.getPlanchetsByGroup("wg-bulk"))).length, + (await runner.runReadWriteTx((tx) => tx.getPlanchetsByGroup("wg-bulk"))) + .length, 4, ); t.equal( - await runner.runTx((tx) => tx.countPlanchetsByGroup("wg-bulk")), + await runner.runReadWriteTx((tx) => + tx.countPlanchetsByGroup("wg-bulk"), + ), 4, "count must agree with the list", ); - await runner.runTx((tx) => tx.deletePlanchetsByGroup("wg-bulk")); + await runner.runReadWriteTx((tx) => tx.deletePlanchetsByGroup("wg-bulk")); t.equal( - (await runner.runTx((tx) => tx.getPlanchetsByGroup("wg-bulk"))).length, + (await runner.runReadWriteTx((tx) => tx.getPlanchetsByGroup("wg-bulk"))) + .length, 0, ); t.ok( - await runner.runTx((tx) => tx.getPlanchet(ck("pl-keep"))), + await runner.runReadWriteTx((tx) => tx.getPlanchet(ck("pl-keep"))), "another group must be untouched", ); }, @@ -2169,14 +2220,18 @@ export const conformanceCases: ConformanceCase[] = [ currency: "TESTKUDOS", exchanges: ["https://e1/", "https://e2/"], }; - await runner.runTx((tx) => tx.upsertTransactionMeta(rec)); - let got = await runner.runTx((tx) => tx.getTransactionMeta("txn:meta:1")); + await runner.runReadWriteTx((tx) => tx.upsertTransactionMeta(rec)); + let got = await runner.runReadWriteTx((tx) => + tx.getTransactionMeta("txn:meta:1"), + ); t.deepEqual(withoutUndefined(got), withoutUndefined(rec)); rec.status = WithdrawalGroupStatus.Done; - await runner.runTx((tx) => tx.upsertTransactionMeta(rec)); - got = await runner.runTx((tx) => tx.getTransactionMeta("txn:meta:1")); + await runner.runReadWriteTx((tx) => tx.upsertTransactionMeta(rec)); + got = await runner.runReadWriteTx((tx) => + tx.getTransactionMeta("txn:meta:1"), + ); t.equal(got?.status, WithdrawalGroupStatus.Done); - const all = await runner.runTx((tx) => + const all = await runner.runReadWriteTx((tx) => tx.listTransactionMetaByTimestamp({}), ); t.equal( @@ -2194,7 +2249,7 @@ export const conformanceCases: ConformanceCase[] = [ // and lowerBound(ts, false) are inclusive, while the pagination cursor // uses lowerBound(ts, true) -- exclusive. Getting one of them wrong // either skips a transaction or loops on it forever. - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { for (const [id, at] of [ ["txn:p:10", 10], ["txn:p:20", 20], @@ -2209,19 +2264,19 @@ export const conformanceCases: ConformanceCase[] = [ }); } }); - const before = await runner.runTx((tx) => + const before = await runner.runReadWriteTx((tx) => tx.getTransactionMetaBefore(tsPrecise(20)), ); t.equal(before?.transactionId, "txn:p:20", "before is inclusive"); - const after = await runner.runTx((tx) => + const after = await runner.runReadWriteTx((tx) => tx.getTransactionMetaAfter(tsPrecise(20)), ); t.equal(after?.transactionId, "txn:p:20", "after is inclusive"); - const at = await runner.runTx((tx) => + const at = await runner.runReadWriteTx((tx) => tx.getTransactionMetaAtTimestamp(tsPrecise(30)), ); t.equal(at?.transactionId, "txn:p:30"); - const page = await runner.runTx((tx) => + const page = await runner.runReadWriteTx((tx) => tx.listTransactionMetaByTimestamp({ afterTimestamp: tsPrecise(20) }), ); t.deepEqual( @@ -2235,7 +2290,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "transaction meta: ordered by timestamp, and limited", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { for (const [id, at] of [ ["txn:o:c", 300], ["txn:o:a", 100], @@ -2250,7 +2305,7 @@ export const conformanceCases: ConformanceCase[] = [ }); } }); - const all = await runner.runTx((tx) => + const all = await runner.runReadWriteTx((tx) => tx.listTransactionMetaByTimestamp({}), ); const mine = all.filter((m) => m.transactionId.startsWith("txn:o:")); @@ -2259,7 +2314,7 @@ export const conformanceCases: ConformanceCase[] = [ ["txn:o:a", "txn:o:b", "txn:o:c"], "must be ordered by timestamp ascending, not insertion order", ); - const limited = await runner.runTx((tx) => + const limited = await runner.runReadWriteTx((tx) => tx.listTransactionMetaByTimestamp({ limit: 1 }), ); t.equal(limited.length, 1); @@ -2269,7 +2324,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "transaction meta: onlyActive selects the non-final range", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertTransactionMeta({ transactionId: "txn:act:1", timestamp: tsPrecise(1), @@ -2285,13 +2340,13 @@ export const conformanceCases: ConformanceCase[] = [ exchanges: [], }); }); - const active = await runner.runTx((tx) => + const active = await runner.runReadWriteTx((tx) => tx.listTransactionMetaByStatus({ onlyActive: true }), ); const ids = active.map((m) => m.transactionId); t.ok(ids.includes("txn:act:1")); t.ok(!ids.includes("txn:act:2"), "a final state must not be active"); - const all = await runner.runTx((tx) => + const all = await runner.runReadWriteTx((tx) => tx.listTransactionMetaByStatus({ onlyActive: false }), ); t.ok(all.length >= active.length); @@ -2301,7 +2356,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "transaction meta: delete one and delete all", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertTransactionMeta({ transactionId: "txn:d:1", timestamp: tsPrecise(1), @@ -2317,14 +2372,16 @@ export const conformanceCases: ConformanceCase[] = [ exchanges: [], }); }); - await runner.runTx((tx) => tx.deleteTransactionMeta("txn:d:1")); + await runner.runReadWriteTx((tx) => tx.deleteTransactionMeta("txn:d:1")); t.equal( - await runner.runTx((tx) => tx.getTransactionMeta("txn:d:1")), + await runner.runReadWriteTx((tx) => tx.getTransactionMeta("txn:d:1")), undefined, ); - t.ok(await runner.runTx((tx) => tx.getTransactionMeta("txn:d:2"))); - await runner.runTx((tx) => tx.deleteAllTransactionMeta()); - const left = await runner.runTx((tx) => + t.ok( + await runner.runReadWriteTx((tx) => tx.getTransactionMeta("txn:d:2")), + ); + await runner.runReadWriteTx((tx) => tx.deleteAllTransactionMeta()); + const left = await runner.runReadWriteTx((tx) => tx.listTransactionMetaByTimestamp({}), ); t.equal(left.length, 0, "deleteAll must clear the whole view"); @@ -2338,8 +2395,10 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { const p = makePurchase("prop-rt"); p.exchanges = ["https://ex-a/", "https://ex-b/"]; - await runner.runTx((tx) => tx.upsertPurchase(p)); - const got = await runner.runTx((tx) => tx.getPurchase("prop-rt")); + await runner.runReadWriteTx((tx) => tx.upsertPurchase(p)); + const got = await runner.runReadWriteTx((tx) => + tx.getPurchase("prop-rt"), + ); t.deepEqual(got, p, "every field must survive, exchange order included"); }, }, @@ -2352,19 +2411,22 @@ export const conformanceCases: ConformanceCase[] = [ // remove rows, not leave a stale one that byExchange still matches. const p = makePurchase("prop-ex"); p.exchanges = ["https://keep/", "https://drop/"]; - await runner.runTx((tx) => tx.upsertPurchase(p)); + await runner.runReadWriteTx((tx) => tx.upsertPurchase(p)); t.equal( - (await runner.runTx((tx) => tx.getPurchasesByExchange("https://drop/"))) - .length, + ( + await runner.runReadWriteTx((tx) => + tx.getPurchasesByExchange("https://drop/"), + ) + ).length, 1, ); p.exchanges = ["https://keep/"]; - await runner.runTx((tx) => tx.upsertPurchase(p)); - const stillDropped = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertPurchase(p)); + const stillDropped = await runner.runReadWriteTx((tx) => tx.getPurchasesByExchange("https://drop/"), ); t.equal(stillDropped.length, 0, "the removed exchange must not match"); - const kept = await runner.runTx((tx) => + const kept = await runner.runReadWriteTx((tx) => tx.getPurchasesByExchange("https://keep/"), ); t.equal(kept.length, 1); @@ -2377,14 +2439,14 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { const p = makePurchase("prop-ff"); p.download = makeDownloadInfo("https://shop/fulfil/first"); - await runner.runTx((tx) => tx.upsertPurchase(p)); + await runner.runReadWriteTx((tx) => tx.upsertPurchase(p)); p.download = makeDownloadInfo("https://shop/fulfil/second"); - await runner.runTx((tx) => tx.upsertPurchase(p)); - const byOld = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertPurchase(p)); + const byOld = await runner.runReadWriteTx((tx) => tx.getPurchasesByFulfillmentUrl("https://shop/fulfil/first"), ); t.equal(byOld.length, 0, "the old URL must no longer resolve"); - const byNew = await runner.runTx((tx) => + const byNew = await runner.runReadWriteTx((tx) => tx.getPurchasesByFulfillmentUrl("https://shop/fulfil/second"), ); t.equal(byNew.length, 1); @@ -2402,8 +2464,10 @@ export const conformanceCases: ConformanceCase[] = [ const p = makePurchase("prop-nf"); const dl = makeDownloadInfo(undefined); p.download = dl; - await runner.runTx((tx) => tx.upsertPurchase(p)); - const got = await runner.runTx((tx) => tx.getPurchase("prop-nf")); + await runner.runReadWriteTx((tx) => tx.upsertPurchase(p)); + const got = await runner.runReadWriteTx((tx) => + tx.getPurchase("prop-nf"), + ); t.deepEqual(got?.download, dl); t.equal(got?.download?.fulfillmentUrl, undefined); }, @@ -2412,7 +2476,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "purchase: lookup by merchant URL and order id", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { const a = makePurchase("prop-o1"); a.merchantBaseUrl = "https://m1/"; a.orderId = "order-1"; @@ -2422,15 +2486,15 @@ export const conformanceCases: ConformanceCase[] = [ await tx.upsertPurchase(a); await tx.upsertPurchase(b); }); - const one = await runner.runTx((tx) => + const one = await runner.runReadWriteTx((tx) => tx.getPurchaseByUrlAndOrderId("https://m1/", "order-2"), ); t.equal(one?.proposalId, "prop-o2"); - const many = await runner.runTx((tx) => + const many = await runner.runReadWriteTx((tx) => tx.getPurchasesByUrlAndOrderId("https://m1/", "order-1"), ); t.equal(many.length, 1); - const none = await runner.runTx((tx) => + const none = await runner.runReadWriteTx((tx) => tx.getPurchaseByUrlAndOrderId("https://m1/", "no-such-order"), ); t.equal(none, undefined); @@ -2442,13 +2506,13 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { const p = makePurchase("prop-del"); p.exchanges = ["https://gone/"]; - await runner.runTx((tx) => tx.upsertPurchase(p)); - await runner.runTx((tx) => tx.deletePurchase("prop-del")); + await runner.runReadWriteTx((tx) => tx.upsertPurchase(p)); + await runner.runReadWriteTx((tx) => tx.deletePurchase("prop-del")); t.equal( - await runner.runTx((tx) => tx.getPurchase("prop-del")), + await runner.runReadWriteTx((tx) => tx.getPurchase("prop-del")), undefined, ); - const orphaned = await runner.runTx((tx) => + const orphaned = await runner.runReadWriteTx((tx) => tx.getPurchasesByExchange("https://gone/"), ); t.equal(orphaned.length, 0, "no orphaned exchange rows may remain"); @@ -2458,7 +2522,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "purchase: status filters and the active range", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { const pending = makePurchase("prop-s1"); pending.purchaseStatus = PurchaseStatus.PendingDownloadingProposal; const failed = makePurchase("prop-s2"); @@ -2466,16 +2530,18 @@ export const conformanceCases: ConformanceCase[] = [ await tx.upsertPurchase(pending); await tx.upsertPurchase(failed); }); - const byStatus = await runner.runTx((tx) => + const byStatus = await runner.runReadWriteTx((tx) => tx.getPurchasesByStatus(PurchaseStatus.Failed), ); t.equal(byStatus.length, 1); t.equal(byStatus[0].proposalId, "prop-s2"); - const active = await runner.runTx((tx) => tx.getActivePurchases()); + const active = await runner.runReadWriteTx((tx) => + tx.getActivePurchases(), + ); const ids = active.map((p) => p.proposalId); t.ok(ids.includes("prop-s1")); t.ok(!ids.includes("prop-s2"), "a failed purchase is not active"); - const all = await runner.runTx((tx) => tx.listAllPurchases()); + const all = await runner.runReadWriteTx((tx) => tx.listAllPurchases()); t.equal(all.length, 2); }, }, @@ -2492,23 +2558,27 @@ export const conformanceCases: ConformanceCase[] = [ name: "deposit group: round trip and active range", async run(t, runner) { const dg = makeDepositGroup("dg-1"); - await runner.runTx((tx) => tx.upsertDepositGroup(dg)); - const got = await runner.runTx((tx) => tx.getDepositGroup("dg-1")); + await runner.runReadWriteTx((tx) => tx.upsertDepositGroup(dg)); + const got = await runner.runReadWriteTx((tx) => + tx.getDepositGroup("dg-1"), + ); t.deepEqual(withoutUndefined(got), withoutUndefined(dg)); const done = makeDepositGroup("dg-2"); done.operationStatus = DepositOperationStatus.Finished; - await runner.runTx((tx) => tx.upsertDepositGroup(done)); - const active = await runner.runTx((tx) => tx.getActiveDepositGroups()); + await runner.runReadWriteTx((tx) => tx.upsertDepositGroup(done)); + const active = await runner.runReadWriteTx((tx) => + tx.getActiveDepositGroups(), + ); const ids = active.map((d) => d.depositGroupId); t.ok(ids.includes("dg-1")); t.ok(!ids.includes("dg-2"), "a finished group is not active"); t.equal( - (await runner.runTx((tx) => tx.listAllDepositGroups())).length, + (await runner.runReadWriteTx((tx) => tx.listAllDepositGroups())).length, 2, ); - await runner.runTx((tx) => tx.deleteDepositGroup("dg-1")); + await runner.runReadWriteTx((tx) => tx.deleteDepositGroup("dg-1")); t.equal( - await runner.runTx((tx) => tx.getDepositGroup("dg-1")), + await runner.runReadWriteTx((tx) => tx.getDepositGroup("dg-1")), undefined, ); }, @@ -2519,8 +2589,10 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { const rg = makeRefreshGroup("rg-1"); rg.originatingTransactionId = "txn:orig:1"; - await runner.runTx((tx) => tx.upsertRefreshGroup(rg)); - const got = await runner.runTx((tx) => tx.getRefreshGroup("rg-1")); + await runner.runReadWriteTx((tx) => tx.upsertRefreshGroup(rg)); + const got = await runner.runReadWriteTx((tx) => + tx.getRefreshGroup("rg-1"), + ); t.deepEqual( withoutUndefined(got), withoutUndefined(rg), @@ -2528,21 +2600,23 @@ export const conformanceCases: ConformanceCase[] = [ ); const done = makeRefreshGroup("rg-2"); done.operationStatus = RefreshOperationStatus.Finished; - await runner.runTx((tx) => tx.upsertRefreshGroup(done)); - const active = await runner.runTx((tx) => tx.getActiveRefreshGroups()); + await runner.runReadWriteTx((tx) => tx.upsertRefreshGroup(done)); + const active = await runner.runReadWriteTx((tx) => + tx.getActiveRefreshGroups(), + ); t.ok(active.map((r) => r.refreshGroupId).includes("rg-1")); t.ok(!active.map((r) => r.refreshGroupId).includes("rg-2")); - const byOrig = await runner.runTx((tx) => + const byOrig = await runner.runReadWriteTx((tx) => tx.getRefreshGroupsByOriginatingTransaction("txn:orig:1"), ); t.equal(byOrig.length, 1); t.equal( - (await runner.runTx((tx) => tx.listAllRefreshGroups())).length, + (await runner.runReadWriteTx((tx) => tx.listAllRefreshGroups())).length, 2, ); - await runner.runTx((tx) => tx.deleteRefreshGroup("rg-1")); + await runner.runReadWriteTx((tx) => tx.deleteRefreshGroup("rg-1")); t.equal( - await runner.runTx((tx) => tx.getRefreshGroup("rg-1")), + await runner.runReadWriteTx((tx) => tx.getRefreshGroup("rg-1")), undefined, ); }, @@ -2551,14 +2625,16 @@ export const conformanceCases: ConformanceCase[] = [ { name: "refresh session: keyed by (group, coin index)", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertRefreshSession(makeRefreshSession("rs-g", 0)); await tx.upsertRefreshSession(makeRefreshSession("rs-g", 1)); await tx.upsertRefreshSession(makeRefreshSession("rs-other", 0)); }); - const one = await runner.runTx((tx) => tx.getRefreshSession("rs-g", 1)); + const one = await runner.runReadWriteTx((tx) => + tx.getRefreshSession("rs-g", 1), + ); t.equal(one?.coinIndex, 1); - const byGroup = await runner.runTx((tx) => + const byGroup = await runner.runReadWriteTx((tx) => tx.getRefreshSessionsByGroup("rs-g"), ); t.equal(byGroup.length, 2); @@ -2567,10 +2643,13 @@ export const conformanceCases: ConformanceCase[] = [ [0, 1], "sessions must come back in coin-index order", ); - await runner.runTx((tx) => tx.deleteRefreshSession("rs-g", 0)); + await runner.runReadWriteTx((tx) => tx.deleteRefreshSession("rs-g", 0)); t.equal( - (await runner.runTx((tx) => tx.getRefreshSessionsByGroup("rs-g"))) - .length, + ( + await runner.runReadWriteTx((tx) => + tx.getRefreshSessionsByGroup("rs-g"), + ) + ).length, 1, ); }, @@ -2580,21 +2659,28 @@ export const conformanceCases: ConformanceCase[] = [ name: "recoup group: round trip, by exchange and active range", async run(t, runner) { const rc = makeRecoupGroup("rc-1", "https://rex/"); - await runner.runTx((tx) => tx.upsertRecoupGroup(rc)); - const got = await runner.runTx((tx) => tx.getRecoupGroup("rc-1")); + await runner.runReadWriteTx((tx) => tx.upsertRecoupGroup(rc)); + const got = await runner.runReadWriteTx((tx) => + tx.getRecoupGroup("rc-1"), + ); t.deepEqual(withoutUndefined(got), withoutUndefined(rc)); const done = makeRecoupGroup("rc-2", "https://rex/"); done.operationStatus = RecoupOperationStatus.Finished; - await runner.runTx((tx) => tx.upsertRecoupGroup(done)); - const byEx = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertRecoupGroup(done)); + const byEx = await runner.runReadWriteTx((tx) => tx.getRecoupGroupsByExchange("https://rex/"), ); t.equal(byEx.length, 2); - const active = await runner.runTx((tx) => tx.getActiveRecoupGroups()); + const active = await runner.runReadWriteTx((tx) => + tx.getActiveRecoupGroups(), + ); t.ok(active.map((r) => r.recoupGroupId).includes("rc-1")); t.ok(!active.map((r) => r.recoupGroupId).includes("rc-2")); - await runner.runTx((tx) => tx.deleteRecoupGroup("rc-1")); - t.equal(await runner.runTx((tx) => tx.getRecoupGroup("rc-1")), undefined); + await runner.runReadWriteTx((tx) => tx.deleteRecoupGroup("rc-1")); + t.equal( + await runner.runReadWriteTx((tx) => tx.getRecoupGroup("rc-1")), + undefined, + ); }, }, @@ -2602,22 +2688,27 @@ export const conformanceCases: ConformanceCase[] = [ name: "peer push debit: round trip and active range", async run(t, runner) { const rec = makePeerPushDebit("ppd-1"); - await runner.runTx((tx) => tx.upsertPeerPushDebit(rec)); - const got = await runner.runTx((tx) => tx.getPeerPushDebit(ck("ppd-1"))); + await runner.runReadWriteTx((tx) => tx.upsertPeerPushDebit(rec)); + const got = await runner.runReadWriteTx((tx) => + tx.getPeerPushDebit(ck("ppd-1")), + ); t.deepEqual(withoutUndefined(got), withoutUndefined(rec)); const done = makePeerPushDebit("ppd-2"); done.status = PeerPushDebitStatus.Done; - await runner.runTx((tx) => tx.upsertPeerPushDebit(done)); - const active = await runner.runTx((tx) => tx.getActivePeerPushDebits()); + await runner.runReadWriteTx((tx) => tx.upsertPeerPushDebit(done)); + const active = await runner.runReadWriteTx((tx) => + tx.getActivePeerPushDebits(), + ); t.ok(active.map((r) => r.pursePub).includes(ck("ppd-1"))); t.ok(!active.map((r) => r.pursePub).includes(ck("ppd-2"))); t.equal( - (await runner.runTx((tx) => tx.listAllPeerPushDebits())).length, + (await runner.runReadWriteTx((tx) => tx.listAllPeerPushDebits())) + .length, 2, ); - await runner.runTx((tx) => tx.deletePeerPushDebit(ck("ppd-1"))); + await runner.runReadWriteTx((tx) => tx.deletePeerPushDebit(ck("ppd-1"))); t.equal( - await runner.runTx((tx) => tx.getPeerPushDebit(ck("ppd-1"))), + await runner.runReadWriteTx((tx) => tx.getPeerPushDebit(ck("ppd-1"))), undefined, ); }, @@ -2628,17 +2719,19 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { const rec = makePeerPushCredit("ppc-1"); rec.contractPriv = ck("cpriv-find-me"); - await runner.runTx((tx) => tx.upsertPeerPushCredit(rec)); - const got = await runner.runTx((tx) => tx.getPeerPushCredit("ppc-1")); + await runner.runReadWriteTx((tx) => tx.upsertPeerPushCredit(rec)); + const got = await runner.runReadWriteTx((tx) => + tx.getPeerPushCredit("ppc-1"), + ); t.deepEqual(withoutUndefined(got), withoutUndefined(rec)); - const byPriv = await runner.runTx((tx) => + const byPriv = await runner.runReadWriteTx((tx) => tx.getPeerPushCreditByExchangeAndContractPriv( rec.exchangeBaseUrl, ck("cpriv-find-me"), ), ); t.equal(byPriv?.peerPushCreditId, "ppc-1"); - const wrongExchange = await runner.runTx((tx) => + const wrongExchange = await runner.runReadWriteTx((tx) => tx.getPeerPushCreditByExchangeAndContractPriv( "https://other/", ck("cpriv-find-me"), @@ -2647,13 +2740,15 @@ export const conformanceCases: ConformanceCase[] = [ t.equal(wrongExchange, undefined, "both components must match"); const done = makePeerPushCredit("ppc-2"); done.status = PeerPushCreditStatus.Done; - await runner.runTx((tx) => tx.upsertPeerPushCredit(done)); - const active = await runner.runTx((tx) => tx.getActivePeerPushCredits()); + await runner.runReadWriteTx((tx) => tx.upsertPeerPushCredit(done)); + const active = await runner.runReadWriteTx((tx) => + tx.getActivePeerPushCredits(), + ); t.ok(active.map((r) => r.peerPushCreditId).includes("ppc-1")); t.ok(!active.map((r) => r.peerPushCreditId).includes("ppc-2")); - await runner.runTx((tx) => tx.deletePeerPushCredit("ppc-1")); + await runner.runReadWriteTx((tx) => tx.deletePeerPushCredit("ppc-1")); t.equal( - await runner.runTx((tx) => tx.getPeerPushCredit("ppc-1")), + await runner.runReadWriteTx((tx) => tx.getPeerPushCredit("ppc-1")), undefined, ); }, @@ -2664,10 +2759,12 @@ export const conformanceCases: ConformanceCase[] = [ async run(t, runner) { const rec = makePeerPullDebit("ppld-1"); rec.contractPriv = ck("cpriv-pull"); - await runner.runTx((tx) => tx.upsertPeerPullDebit(rec)); - const got = await runner.runTx((tx) => tx.getPeerPullDebit("ppld-1")); + await runner.runReadWriteTx((tx) => tx.upsertPeerPullDebit(rec)); + const got = await runner.runReadWriteTx((tx) => + tx.getPeerPullDebit("ppld-1"), + ); t.deepEqual(withoutUndefined(got), withoutUndefined(rec)); - const byPriv = await runner.runTx((tx) => + const byPriv = await runner.runReadWriteTx((tx) => tx.getPeerPullDebitByExchangeAndContractPriv( rec.exchangeBaseUrl, ck("cpriv-pull"), @@ -2676,17 +2773,20 @@ export const conformanceCases: ConformanceCase[] = [ t.equal(byPriv?.peerPullDebitId, "ppld-1"); const done = makePeerPullDebit("ppld-2"); done.status = PeerPullDebitRecordStatus.Done; - await runner.runTx((tx) => tx.upsertPeerPullDebit(done)); - const active = await runner.runTx((tx) => tx.getActivePeerPullDebits()); + await runner.runReadWriteTx((tx) => tx.upsertPeerPullDebit(done)); + const active = await runner.runReadWriteTx((tx) => + tx.getActivePeerPullDebits(), + ); t.ok(active.map((r) => r.peerPullDebitId).includes("ppld-1")); t.ok(!active.map((r) => r.peerPullDebitId).includes("ppld-2")); t.equal( - (await runner.runTx((tx) => tx.listAllPeerPullDebits())).length, + (await runner.runReadWriteTx((tx) => tx.listAllPeerPullDebits())) + .length, 2, ); - await runner.runTx((tx) => tx.deletePeerPullDebit("ppld-1")); + await runner.runReadWriteTx((tx) => tx.deletePeerPullDebit("ppld-1")); t.equal( - await runner.runTx((tx) => tx.getPeerPullDebit("ppld-1")), + await runner.runReadWriteTx((tx) => tx.getPeerPullDebit("ppld-1")), undefined, ); }, @@ -2696,24 +2796,29 @@ export const conformanceCases: ConformanceCase[] = [ name: "peer pull credit: round trip and active range", async run(t, runner) { const rec = makePeerPullCredit("pplc-1"); - await runner.runTx((tx) => tx.upsertPeerPullCredit(rec)); - const got = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertPeerPullCredit(rec)); + const got = await runner.runReadWriteTx((tx) => tx.getPeerPullCredit(ck("pplc-1")), ); t.deepEqual(withoutUndefined(got), withoutUndefined(rec)); const done = makePeerPullCredit("pplc-2"); done.status = PeerPullPaymentCreditStatus.Done; - await runner.runTx((tx) => tx.upsertPeerPullCredit(done)); - const active = await runner.runTx((tx) => tx.getActivePeerPullCredits()); + await runner.runReadWriteTx((tx) => tx.upsertPeerPullCredit(done)); + const active = await runner.runReadWriteTx((tx) => + tx.getActivePeerPullCredits(), + ); t.ok(active.map((r) => r.pursePub).includes(ck("pplc-1"))); t.ok(!active.map((r) => r.pursePub).includes(ck("pplc-2"))); t.equal( - (await runner.runTx((tx) => tx.listAllPeerPullCredits())).length, + (await runner.runReadWriteTx((tx) => tx.listAllPeerPullCredits())) + .length, 2, ); - await runner.runTx((tx) => tx.deletePeerPullCredit(ck("pplc-1"))); + await runner.runReadWriteTx((tx) => + tx.deletePeerPullCredit(ck("pplc-1")), + ); t.equal( - await runner.runTx((tx) => tx.getPeerPullCredit(ck("pplc-1"))), + await runner.runReadWriteTx((tx) => tx.getPeerPullCredit(ck("pplc-1"))), undefined, ); }, @@ -2728,8 +2833,8 @@ export const conformanceCases: ConformanceCase[] = [ // declared in a different interface. Reading the type through only // its own body once cost five silently dropped columns here. const tok = makeToken("tk-1"); - await runner.runTx((tx) => tx.upsertToken(tok)); - const got = await runner.runTx((tx) => tx.getToken(ck("tk-1"))); + await runner.runReadWriteTx((tx) => tx.upsertToken(tok)); + const got = await runner.runReadWriteTx((tx) => tx.getToken(ck("tk-1"))); t.deepEqual(withoutUndefined(got), withoutUndefined(tok)); t.equal(got?.slug, tok.slug, "inherited fields must persist"); t.deepEqual(got?.tokenIssuePub, tok.tokenIssuePub); @@ -2742,7 +2847,7 @@ export const conformanceCases: ConformanceCase[] = [ { name: "token: lookup by issue pub hash, list and delete", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { const a = makeToken("tk-a"); a.tokenIssuePubHash = ckh("tiph-shared"); const b = makeToken("tk-b"); @@ -2753,16 +2858,19 @@ export const conformanceCases: ConformanceCase[] = [ await tx.upsertToken(b); await tx.upsertToken(c); }); - const byHash = await runner.runTx((tx) => + const byHash = await runner.runReadWriteTx((tx) => tx.getTokensByIssuePubHash(ckh("tiph-shared")), ); t.equal(byHash.length, 2); - t.equal((await runner.runTx((tx) => tx.listTokens())).length, 3); - await runner.runTx((tx) => tx.deleteToken(ck("tk-a"))); - t.equal(await runner.runTx((tx) => tx.getToken(ck("tk-a"))), undefined); + t.equal((await runner.runReadWriteTx((tx) => tx.listTokens())).length, 3); + await runner.runReadWriteTx((tx) => tx.deleteToken(ck("tk-a"))); + t.equal( + await runner.runReadWriteTx((tx) => tx.getToken(ck("tk-a"))), + undefined, + ); t.equal( ( - await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.getTokensByIssuePubHash(ckh("tiph-shared")), ) ).length, @@ -2774,25 +2882,27 @@ export const conformanceCases: ConformanceCase[] = [ { name: "slate: addressed by the full (purchase, choice, output, repeat)", async run(t, runner) { - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { await tx.upsertSlate(makeSlate("sl-1", "pur-1", 0, 0, 0)); await tx.upsertSlate(makeSlate("sl-2", "pur-1", 0, 0, 1)); await tx.upsertSlate(makeSlate("sl-3", "pur-1", 0, 1, 0)); await tx.upsertSlate(makeSlate("sl-4", "pur-1", 1, 0, 0)); }); - const one = await runner.runTx((tx) => tx.getSlate("pur-1", 0, 0, 1)); + const one = await runner.runReadWriteTx((tx) => + tx.getSlate("pur-1", 0, 0, 1), + ); t.equal( one?.tokenUsePub, ck("sl-2"), "all four components must select the slate", ); - const byChoice = await runner.runTx((tx) => + const byChoice = await runner.runReadWriteTx((tx) => tx.getSlatesByPurchaseAndChoice("pur-1", 0), ); t.equal(byChoice.length, 3, "choice 1 must not be included"); - await runner.runTx((tx) => tx.deleteSlate(ck("sl-2"))); + await runner.runReadWriteTx((tx) => tx.deleteSlate(ck("sl-2"))); t.equal( - await runner.runTx((tx) => tx.getSlate("pur-1", 0, 0, 1)), + await runner.runReadWriteTx((tx) => tx.getSlate("pur-1", 0, 0, 1)), undefined, ); }, @@ -2802,8 +2912,8 @@ export const conformanceCases: ConformanceCase[] = [ name: "slate: round trip with the use signature set and unset", async run(t, runner) { const unsigned = makeSlate("sl-u", "pur-2", 0, 0, 0); - await runner.runTx((tx) => tx.upsertSlate(unsigned)); - const gotUnsigned = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertSlate(unsigned)); + const gotUnsigned = await runner.runReadWriteTx((tx) => tx.getSlate("pur-2", 0, 0, 0), ); t.deepEqual(withoutUndefined(gotUnsigned), withoutUndefined(unsigned)); @@ -2815,8 +2925,8 @@ export const conformanceCases: ConformanceCase[] = [ ub_sig: { cipher: DenomKeyType.Rsa, rsa_signature: "usig" }, h_issue: "hissue", }; - await runner.runTx((tx) => tx.upsertSlate(signed)); - const gotSigned = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertSlate(signed)); + const gotSigned = await runner.runReadWriteTx((tx) => tx.getSlate("pur-3", 0, 0, 0), ); t.deepEqual(gotSigned?.tokenUseSig, signed.tokenUseSig); @@ -2829,14 +2939,14 @@ export const conformanceCases: ConformanceCase[] = [ // storage-class test can only verify columns that have rows. const rec = makeAvail("https://emp/", "d-emp", 0); rec.exchangeMasterPub = ck("master-emp"); - await runner.runTx((tx) => tx.upsertCoinAvailability(rec)); - const got = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertCoinAvailability(rec)); + const got = await runner.runReadWriteTx((tx) => tx.getCoinAvailability("https://emp/", ckh("d-emp"), 0), ); t.equal(got?.exchangeMasterPub, ck("master-emp")); const unset = makeAvail("https://emp2/", "d-emp2", 0); - await runner.runTx((tx) => tx.upsertCoinAvailability(unset)); - const got2 = await runner.runTx((tx) => + await runner.runReadWriteTx((tx) => tx.upsertCoinAvailability(unset)); + const got2 = await runner.runReadWriteTx((tx) => tx.getCoinAvailability("https://emp2/", ckh("d-emp2"), 0), ); t.equal( @@ -2854,7 +2964,7 @@ export const conformanceCases: ConformanceCase[] = [ // answer either way. async run(t, runner) { const N = 40; - await runner.runTx(async (tx) => { + await runner.runReadWriteTx(async (tx) => { for (let i = 0; i < N; i++) { const c = makeCoin(`scan-${i}`); c.exchangeBaseUrl = "https://scan/"; @@ -2877,9 +2987,9 @@ export const conformanceCases: ConformanceCase[] = [ limit: number, f: (tx: WalletDbTransaction) => Promise<unknown>, ): Promise<void> => { - const before = runner.recordsRead?.(); - await runner.runTx(f); - const after = runner.recordsRead?.(); + const before = runner.getAccessStats()?.recordsRead; + await runner.runReadWriteTx(f); + const after = runner.getAccessStats()?.recordsRead; if (before === undefined || after === undefined) { return; } diff --git a/packages/taler-wallet-core/src/dbtx-conformance.ts b/packages/taler-wallet-core/src/dbtx-conformance.ts @@ -27,33 +27,15 @@ */ import { WalletDbTransaction } from "./dbtx.js"; +import { WalletDbHandle } from "./dbtx-handle.js"; /** - * Runs a callback inside one database transaction. - * - * An implementation under test supplies this; the suite never opens a - * database itself. + * The suite runs against a WalletDbHandle -- the same abstraction the wallet + * itself holds, not a parallel one built for tests. A backend that passes the + * suite has therefore been exercised through the interface production code + * uses, including transaction serialisation and post-commit notification. */ -export interface DbTxRunner { - /** Human-readable name of the implementation, used in test names. */ - name: string; - - /** Run f in a read-write transaction and return its result. */ - runTx<T>(f: (tx: WalletDbTransaction) => Promise<T>): Promise<T>; - - /** - * Total number of records the backend has read so far, if it can report it. - * - * Cases use this to assert that a query is bounded rather than scanning: - * counting callbacks is not enough, since a backend can materialise a whole - * range and still invoke a predicate only once. A runner that cannot report - * this returns undefined and the relevant cases skip the assertion. - */ - recordsRead?(): number | undefined; - - /** Release any resources. */ - close(): Promise<void>; -} +export type DbTxRunner = WalletDbHandle; /** * A single conformance check. diff --git a/packages/taler-wallet-core/src/dbtx-handle-impl.ts b/packages/taler-wallet-core/src/dbtx-handle-impl.ts @@ -22,7 +22,11 @@ */ import { CancellationToken, WalletNotification } from "@gnu-taler/taler-util"; -import { BridgeIDBFactory, IDBDatabase } from "@gnu-taler/idb-bridge"; +import { + AccessStats, + BridgeIDBFactory, + IDBDatabase, +} from "@gnu-taler/idb-bridge"; import { applyFixups, @@ -60,7 +64,11 @@ export class IdbWalletDbHandle implements WalletDbHandle { constructor( private idbFactory: BridgeIDBFactory, private notify: (n: WalletNotification) => void, - private getStats: () => WalletDbAccessStats | undefined, + /** + * Raw backend counters, when the backend was asked to track them. + * Summed into a single figure by getAccessStats. + */ + private rawStats?: () => AccessStats | undefined, ) {} /** @@ -163,7 +171,21 @@ export class IdbWalletDbHandle implements WalletDbHandle { } getAccessStats(): WalletDbAccessStats | undefined { - return this.getStats(); + const st = this.rawStats?.(); + if (!st) { + return undefined; + } + // Index reads and store reads both count: a query served entirely from an + // index still read those records, and leaving them out would make a + // full index scan look bounded. + let recordsRead = 0; + for (const k of Object.keys(st.readItemsPerIndex)) { + recordsRead += st.readItemsPerIndex[k]; + } + for (const k of Object.keys(st.readItemsPerStore)) { + recordsRead += st.readItemsPerStore[k]; + } + return { recordsRead }; } async close(): Promise<void> { diff --git a/packages/taler-wallet-core/src/dbtx-runners.ts b/packages/taler-wallet-core/src/dbtx-runners.ts @@ -24,18 +24,10 @@ import { BridgeIDBFactory, createSqliteBackend } from "@gnu-taler/idb-bridge"; import { createNodeHelperSqlite3Impl } from "@gnu-taler/idb-bridge/node-helper-sqlite3-impl"; -import { CancellationToken, Logger } from "@gnu-taler/taler-util"; -import { openTalerDatabase, WalletIndexedDbStoresV1 } from "./db-indexeddb.js"; -import { DbAccessImpl } from "./query.js"; import { DbTxRunner } from "./dbtx-conformance.js"; -import { IdbWalletTransaction } from "./dbtx-indexeddb.js"; -import { - openNativeSqliteWalletDb, - runNativeSqliteWalletTx, -} from "./dbtx-sqlite.js"; - -const logger = new Logger("dbtx-runners.ts"); +import { IdbWalletDbHandle, SqliteWalletDbHandle } from "./dbtx-handle-impl.js"; +import { openNativeSqliteWalletDb } from "./dbtx-sqlite.js"; /** * Runner for the IndexedDB implementation, on an in-memory sqlite-backed @@ -54,43 +46,22 @@ export async function makeIdbRunner( backend.trackStats = true; BridgeIDBFactory.enableTracing = false; const idbFactory = new BridgeIDBFactory(backend); - const dbHandle = await openTalerDatabase(idbFactory as any, () => {}); - const db = new DbAccessImpl( - dbHandle, - WalletIndexedDbStoresV1, - CancellationToken.CONTINUE, + const handle = new IdbWalletDbHandle( + idbFactory as any, + () => {}, + () => backend.accessStats, ); - return { - name: "IdbWalletTransaction", - async runTx<T>(f: (tx: any) => Promise<T>): Promise<T> { - return await db.runAllStoresReadWriteTx({}, async (mytx) => { - return await f(new IdbWalletTransaction(mytx)); - }); - }, - recordsRead() { - const st = backend.accessStats; - let n = 0; - for (const k of Object.keys(st.readItemsPerIndex)) { - n += st.readItemsPerIndex[k]; - } - for (const k of Object.keys(st.readItemsPerStore)) { - n += st.readItemsPerStore[k]; - } - return n; - }, - async close() { - dbHandle.close(); - }, - }; + await handle.ensureOpen(); + return handle; } /** * Runner for the native sqlite3 implementation. * - * Only a subset of WalletDbTransaction exists so far, so the suite runs the - * cases that the implemented methods cover and reports the rest as skipped - * rather than failed. That keeps the suite honest about progress without - * turning the checklist red. + * Both runners return the handle the wallet itself uses, so the suite + * exercises transaction serialisation, the shared statement cache, + * checkpoint-on-idle and post-commit notification delivery rather than a + * reimplementation of them. */ export async function makeSqliteRunner( filename = ":memory:", @@ -99,26 +70,7 @@ export async function makeSqliteRunner( enableTracing: false, }); const ndb = await openNativeSqliteWalletDb(await sqlite3Impl.open(filename)); - return { - name: "SqliteWalletTransaction", - async runTx<T>(f: (tx: any) => Promise<T>): Promise<T> { - // Goes through the same helper the wallet uses, rather than a copy of - // it: otherwise the suite would not exercise transaction - // serialisation, the shared statement cache, checkpoint-on-idle or - // post-commit notification delivery. - return await runNativeSqliteWalletTx( - ndb, - () => {}, - async (tx) => f(tx), - ); - }, - recordsRead() { - return ndb.stats.rowsRead; - }, - async close() { - await ndb.db.close(); - }, - }; + return new SqliteWalletDbHandle(ndb, () => {}); } export const runnerFactories: Array< diff --git a/packages/taler-wallet-core/src/dbtx-sqlite.test.ts b/packages/taler-wallet-core/src/dbtx-sqlite.test.ts @@ -70,7 +70,7 @@ test("sqlite: BLOB columns really hold blobs", async (t) => { for (const [table, columns] of Object.entries(BLOB_COLUMNS)) { for (const column of columns) { - const rows = await runner.runTx(async (tx: any) => { + const rows = await runner.runReadWriteTx(async (tx: any) => { // Reach past the DAL deliberately: the point is to see the storage // class, which the DAL exists to hide. return await (tx as any).all( diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -710,17 +710,7 @@ export class InternalWalletState { : new IdbWalletDbHandle( this.dbImplementation.idbFactory, (n) => this.notify(n), - () => { - // readItemsPerStore counts records handed back, which is what - // the sqlite backend's rowsRead counts; the per-store split is - // not comparable across backends, so it is summed away here. - const st = this.dbImplementation.getStats(); - let recordsRead = 0; - for (const k of Object.keys(st.readItemsPerStore)) { - recordsRead += st.readItemsPerStore[k]; - } - return { recordsRead }; - }, + () => this.dbImplementation.getStats(), ); } return this._dbHandle;