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:
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;