more static typing for transactions (fixes #6653)

This commit is contained in:
Florian Dold 2020-11-26 22:14:46 +01:00
parent 2b19594e7a
commit 4e481a51c6
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
3 changed files with 164 additions and 95 deletions

View File

@ -285,7 +285,7 @@ async function processTipImpl(
); );
if (!isValid) { if (!isValid) {
await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => { await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => {
const tipRecord = await tx.get(Stores.tips, walletTipId); const tipRecord = await tx.get(Stores.tips, walletTipId);
if (!tipRecord) { if (!tipRecord) {
return; return;

View File

@ -785,7 +785,7 @@ export interface CoinRecord {
/** /**
* Blinding key used when withdrawing the coin. * Blinding key used when withdrawing the coin.
* Potentionally sed again during payback. * Potentionally used again during payback.
*/ */
blindingKey: string; blindingKey: string;
@ -1531,135 +1531,160 @@ export enum ImportPayloadType {
CoreSchema = "core-schema", CoreSchema = "core-schema",
} }
class ExchangesStore extends Store<ExchangeRecord> { class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
constructor() { constructor() {
super("exchanges", { keyPath: "baseUrl" }); super("exchanges", { keyPath: "baseUrl" });
} }
} }
class CoinsStore extends Store<CoinRecord> { class CoinsStore extends Store<"coins", CoinRecord> {
constructor() { constructor() {
super("coins", { keyPath: "coinPub" }); super("coins", { keyPath: "coinPub" });
} }
exchangeBaseUrlIndex = new Index<string, CoinRecord>( exchangeBaseUrlIndex = new Index<
this, "coins",
"exchangeBaseUrl", "exchangeBaseUrl",
"exchangeBaseUrl", string,
); CoinRecord
denomPubHashIndex = new Index<string, CoinRecord>( >(this, "exchangeBaseUrl", "exchangeBaseUrl");
this,
denomPubHashIndex = new Index<
"coins",
"denomPubHashIndex", "denomPubHashIndex",
"denomPubHash", string,
); CoinRecord
>(this, "denomPubHashIndex", "denomPubHash");
} }
class ProposalsStore extends Store<ProposalRecord> { class ProposalsStore extends Store<"proposals", ProposalRecord> {
constructor() { constructor() {
super("proposals", { keyPath: "proposalId" }); super("proposals", { keyPath: "proposalId" });
} }
urlAndOrderIdIndex = new Index<string, ProposalRecord>(this, "urlIndex", [ urlAndOrderIdIndex = new Index<
"merchantBaseUrl", "proposals",
"orderId", "urlIndex",
]); string,
ProposalRecord
>(this, "urlIndex", ["merchantBaseUrl", "orderId"]);
} }
class PurchasesStore extends Store<PurchaseRecord> { class PurchasesStore extends Store<"purchases", PurchaseRecord> {
constructor() { constructor() {
super("purchases", { keyPath: "proposalId" }); super("purchases", { keyPath: "proposalId" });
} }
fulfillmentUrlIndex = new Index<string, PurchaseRecord>( fulfillmentUrlIndex = new Index<
this, "purchases",
"fulfillmentUrlIndex", "fulfillmentUrlIndex",
"contractData.fulfillmentUrl", string,
PurchaseRecord
>(this, "fulfillmentUrlIndex", "contractData.fulfillmentUrl");
orderIdIndex = new Index<"purchases", "orderIdIndex", string, PurchaseRecord>(
this,
"orderIdIndex",
["contractData.merchantBaseUrl", "contractData.orderId"],
); );
orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", [
"contractData.merchantBaseUrl",
"contractData.orderId",
]);
} }
class DenominationsStore extends Store<DenominationRecord> { class DenominationsStore extends Store<"denominations", DenominationRecord> {
constructor() { constructor() {
// cast needed because of bug in type annotations // cast needed because of bug in type annotations
super("denominations", { super("denominations", {
keyPath: (["exchangeBaseUrl", "denomPubHash"] as any) as IDBKeyPath, keyPath: (["exchangeBaseUrl", "denomPubHash"] as any) as IDBKeyPath,
}); });
} }
exchangeBaseUrlIndex = new Index<string, DenominationRecord>( exchangeBaseUrlIndex = new Index<
this, "denominations",
"exchangeBaseUrlIndex", "exchangeBaseUrlIndex",
"exchangeBaseUrl", string,
); DenominationRecord
>(this, "exchangeBaseUrlIndex", "exchangeBaseUrl");
} }
class CurrenciesStore extends Store<CurrencyRecord> { class CurrenciesStore extends Store<"currencies", CurrencyRecord> {
constructor() { constructor() {
super("currencies", { keyPath: "name" }); super("currencies", { keyPath: "name" });
} }
} }
class ConfigStore extends Store<ConfigRecord> { class ConfigStore extends Store<"config", ConfigRecord> {
constructor() { constructor() {
super("config", { keyPath: "key" }); super("config", { keyPath: "key" });
} }
} }
class ReservesStore extends Store<ReserveRecord> { class ReservesStore extends Store<"reserves", ReserveRecord> {
constructor() { constructor() {
super("reserves", { keyPath: "reservePub" }); super("reserves", { keyPath: "reservePub" });
} }
} }
class ReserveHistoryStore extends Store<ReserveHistoryRecord> { class ReserveHistoryStore extends Store<
"reserveHistory",
ReserveHistoryRecord
> {
constructor() { constructor() {
super("reserveHistory", { keyPath: "reservePub" }); super("reserveHistory", { keyPath: "reservePub" });
} }
} }
class TipsStore extends Store<TipRecord> { class TipsStore extends Store<"tips", TipRecord> {
constructor() { constructor() {
super("tips", { keyPath: "walletTipId" }); super("tips", { keyPath: "walletTipId" });
} }
// Added in version 2 // Added in version 2
byMerchantTipIdAndBaseUrl = new Index<[string, string], TipRecord>( byMerchantTipIdAndBaseUrl = new Index<
"tips",
"tipsByMerchantTipIdAndOriginIndex",
[string, string],
TipRecord
>(
this, this,
"tipsByMerchantTipIdAndOriginIndex", "tipsByMerchantTipIdAndOriginIndex",
["merchantTipId", "merchantBaseUrl"], ["merchantTipId", "merchantBaseUrl"],
{ {
versionAdded: 2, versionAdded: 2,
} },
); );
} }
class WithdrawalGroupsStore extends Store<WithdrawalGroupRecord> { class WithdrawalGroupsStore extends Store<
"withdrawals",
WithdrawalGroupRecord
> {
constructor() { constructor() {
super("withdrawals", { keyPath: "withdrawalGroupId" }); super("withdrawals", { keyPath: "withdrawalGroupId" });
} }
} }
class PlanchetsStore extends Store<PlanchetRecord> { class PlanchetsStore extends Store<"planchets", PlanchetRecord> {
constructor() { constructor() {
super("planchets", { keyPath: "coinPub" }); super("planchets", { keyPath: "coinPub" });
} }
byGroupAndIndex = new Index<string, PlanchetRecord>( byGroupAndIndex = new Index<
this, "planchets",
"withdrawalGroupAndCoinIdxIndex", "withdrawalGroupAndCoinIdxIndex",
["withdrawalGroupId", "coinIdx"], string,
); PlanchetRecord
byGroup = new Index<string, PlanchetRecord>( >(this, "withdrawalGroupAndCoinIdxIndex", ["withdrawalGroupId", "coinIdx"]);
this, byGroup = new Index<
"planchets",
"withdrawalGroupIndex", "withdrawalGroupIndex",
"withdrawalGroupId", string,
); PlanchetRecord
>(this, "withdrawalGroupIndex", "withdrawalGroupId");
} }
/** /**
* This store is effectively a materialized index for * This store is effectively a materialized index for
* reserve records that are for a bank-integrated withdrawal. * reserve records that are for a bank-integrated withdrawal.
*/ */
class BankWithdrawUrisStore extends Store<BankWithdrawUriRecord> { class BankWithdrawUrisStore extends Store<
"bankWithdrawUris",
BankWithdrawUriRecord
> {
constructor() { constructor() {
super("bankWithdrawUris", { keyPath: "talerWithdrawUri" }); super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
} }
@ -1675,10 +1700,13 @@ export const Stores = {
denominations: new DenominationsStore(), denominations: new DenominationsStore(),
exchanges: new ExchangesStore(), exchanges: new ExchangesStore(),
proposals: new ProposalsStore(), proposals: new ProposalsStore(),
refreshGroups: new Store<RefreshGroupRecord>("refreshGroups", { refreshGroups: new Store<"refreshGroups", RefreshGroupRecord>(
keyPath: "refreshGroupId", "refreshGroups",
}), {
recoupGroups: new Store<RecoupGroupRecord>("recoupGroups", { keyPath: "refreshGroupId",
},
),
recoupGroups: new Store<"recoupGroups", RecoupGroupRecord>("recoupGroups", {
keyPath: "recoupGroupId", keyPath: "recoupGroupId",
}), }),
reserves: new ReservesStore(), reserves: new ReservesStore(),

View File

@ -59,11 +59,8 @@ export interface StoreParams<T> {
/** /**
* Definition of an object store. * Definition of an object store.
*/ */
export class Store<T> { export class Store<N extends string, T> {
constructor( constructor(public name: N, public storeParams?: StoreParams<T>) {}
public name: string,
public storeParams?: StoreParams<T>,
) {}
} }
/** /**
@ -273,26 +270,48 @@ class ResultStream<T> {
} }
} }
export class TransactionHandle { type StrKey<T> = string & keyof T;
type StoreName<S> = S extends Store<infer N, any> ? N : never;
type StoreContent<S> = S extends Store<any, infer R> ? R : never;
type IndexRecord<Ind> = Ind extends Index<any, any, any, infer R> ? R : never;
export class TransactionHandle<StoreTypes extends Store<string, {}>> {
constructor(private tx: IDBTransaction) {} constructor(private tx: IDBTransaction) {}
put<T>(store: Store<T>, value: T, key?: any): Promise<any> { put<S extends StoreTypes>(
store: S,
value: StoreContent<S>,
key?: any,
): Promise<any> {
const req = this.tx.objectStore(store.name).put(value, key); const req = this.tx.objectStore(store.name).put(value, key);
return requestToPromise(req); return requestToPromise(req);
} }
add<T>(store: Store<T>, value: T, key?: any): Promise<any> { add<S extends StoreTypes>(
store: S,
value: StoreContent<S>,
key?: any,
): Promise<any> {
const req = this.tx.objectStore(store.name).add(value, key); const req = this.tx.objectStore(store.name).add(value, key);
return requestToPromise(req); return requestToPromise(req);
} }
get<T>(store: Store<T>, key: any): Promise<T | undefined> { get<S extends StoreTypes>(
store: S,
key: any,
): Promise<StoreContent<S> | undefined> {
const req = this.tx.objectStore(store.name).get(key); const req = this.tx.objectStore(store.name).get(key);
return requestToPromise(req); return requestToPromise(req);
} }
getIndexed<S extends IDBValidKey, T>( getIndexed<
index: Index<S, T>, StoreName extends StrKey<StoreTypes>,
IndexName extends string,
S extends IDBValidKey,
T
>(
index: Index<StoreName, IndexName, S, T>,
key: any, key: any,
): Promise<T | undefined> { ): Promise<T | undefined> {
const req = this.tx const req = this.tx
@ -302,15 +321,20 @@ export class TransactionHandle {
return requestToPromise(req); return requestToPromise(req);
} }
iter<T>(store: Store<T>, key?: any): ResultStream<T> { iter<N extends StrKey<StoreTypes>, T extends StoreTypes[N]>(
store: Store<N, T>,
key?: any,
): ResultStream<T> {
const req = this.tx.objectStore(store.name).openCursor(key); const req = this.tx.objectStore(store.name).openCursor(key);
return new ResultStream<T>(req); return new ResultStream<T>(req);
} }
iterIndexed<S extends IDBValidKey, T>( iterIndexed<
index: Index<S, T>, StoreName extends StrKey<StoreTypes>,
key?: any, IndexName extends string,
): ResultStream<T> { S extends IDBValidKey,
T
>(index: Index<StoreName, IndexName, S, T>, key?: any): ResultStream<T> {
const req = this.tx const req = this.tx
.objectStore(index.storeName) .objectStore(index.storeName)
.index(index.indexName) .index(index.indexName)
@ -318,13 +342,16 @@ export class TransactionHandle {
return new ResultStream<T>(req); return new ResultStream<T>(req);
} }
delete<T>(store: Store<T>, key: any): Promise<void> { delete<N extends StrKey<StoreTypes>, T extends StoreTypes[N]>(
store: Store<N, T>,
key: any,
): Promise<void> {
const req = this.tx.objectStore(store.name).delete(key); const req = this.tx.objectStore(store.name).delete(key);
return requestToPromise(req); return requestToPromise(req);
} }
mutate<T>( mutate<N extends StrKey<StoreTypes>, T extends StoreTypes[N]>(
store: Store<T>, store: Store<N, T>,
key: any, key: any,
f: (x: T) => T | undefined, f: (x: T) => T | undefined,
): Promise<void> { ): Promise<void> {
@ -333,10 +360,10 @@ export class TransactionHandle {
} }
} }
function runWithTransaction<T>( function runWithTransaction<T, StoreTypes extends Store<string, {}>>(
db: IDBDatabase, db: IDBDatabase,
stores: Store<any>[], stores: StoreTypes[],
f: (t: TransactionHandle) => Promise<T>, f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
mode: "readonly" | "readwrite", mode: "readonly" | "readwrite",
): Promise<T> { ): Promise<T> {
const stack = Error("Failed transaction was started here."); const stack = Error("Failed transaction was started here.");
@ -397,7 +424,12 @@ function runWithTransaction<T>(
/** /**
* Definition of an index. * Definition of an index.
*/ */
export class Index<S extends IDBValidKey, T> { export class Index<
StoreName extends string,
IndexName extends string,
S extends IDBValidKey,
T
> {
/** /**
* Name of the store that this index is associated with. * Name of the store that this index is associated with.
*/ */
@ -409,8 +441,8 @@ export class Index<S extends IDBValidKey, T> {
options: IndexOptions; options: IndexOptions;
constructor( constructor(
s: Store<T>, s: Store<StoreName, T>,
public indexName: string, public indexName: IndexName,
public keyPath: string | string[], public keyPath: string | string[],
options?: IndexOptions, options?: IndexOptions,
) { ) {
@ -539,7 +571,10 @@ export class Database {
}); });
} }
async get<T>(store: Store<T>, key: any): Promise<T | undefined> { async get<N extends string, T>(
store: Store<N, T>,
key: any,
): Promise<T | undefined> {
const tx = this.db.transaction([store.name], "readonly"); const tx = this.db.transaction([store.name], "readonly");
const req = tx.objectStore(store.name).get(key); const req = tx.objectStore(store.name).get(key);
const v = await requestToPromise(req); const v = await requestToPromise(req);
@ -547,10 +582,12 @@ export class Database {
return v; return v;
} }
async getIndexed<S extends IDBValidKey, T>( async getIndexed<Ind extends Index<string, string, any, any>>(
index: Index<S, T>, index: Ind extends Index<infer IndN, infer StN, any, infer R>
? Index<IndN, StN, any, R>
: never,
key: any, key: any,
): Promise<T | undefined> { ): Promise<IndexRecord<Ind> | undefined> {
const tx = this.db.transaction([index.storeName], "readonly"); const tx = this.db.transaction([index.storeName], "readonly");
const req = tx.objectStore(index.storeName).index(index.indexName).get(key); const req = tx.objectStore(index.storeName).index(index.indexName).get(key);
const v = await requestToPromise(req); const v = await requestToPromise(req);
@ -558,7 +595,11 @@ export class Database {
return v; return v;
} }
async put<T>(store: Store<T>, value: T, key?: any): Promise<any> { async put<St extends Store<string, any>>(
store: St extends Store<infer N, infer R> ? Store<N, R> : never,
value: St extends Store<any, infer R> ? R : never,
key?: any,
): Promise<any> {
const tx = this.db.transaction([store.name], "readwrite"); const tx = this.db.transaction([store.name], "readwrite");
const req = tx.objectStore(store.name).put(value, key); const req = tx.objectStore(store.name).put(value, key);
const v = await requestToPromise(req); const v = await requestToPromise(req);
@ -566,8 +607,8 @@ export class Database {
return v; return v;
} }
async mutate<T>( async mutate<N extends string, T>(
store: Store<T>, store: Store<N, T>,
key: any, key: any,
f: (x: T) => T | undefined, f: (x: T) => T | undefined,
): Promise<void> { ): Promise<void> {
@ -577,14 +618,14 @@ export class Database {
await transactionToPromise(tx); await transactionToPromise(tx);
} }
iter<T>(store: Store<T>): ResultStream<T> { iter<N extends string, T>(store: Store<N, T>): ResultStream<T> {
const tx = this.db.transaction([store.name], "readonly"); const tx = this.db.transaction([store.name], "readonly");
const req = tx.objectStore(store.name).openCursor(); const req = tx.objectStore(store.name).openCursor();
return new ResultStream<T>(req); return new ResultStream<T>(req);
} }
iterIndex<S extends IDBValidKey, T>( iterIndex<N extends string, I extends string, S extends IDBValidKey, T>(
index: Index<S, T>, index: Index<N, I, S, T>,
query?: any, query?: any,
): ResultStream<T> { ): ResultStream<T> {
const tx = this.db.transaction([index.storeName], "readonly"); const tx = this.db.transaction([index.storeName], "readonly");
@ -595,17 +636,17 @@ export class Database {
return new ResultStream<T>(req); return new ResultStream<T>(req);
} }
async runWithReadTransaction<T>( async runWithReadTransaction<T, StoreTypes extends Store<string, any>>(
stores: Store<any>[], stores: StoreTypes[],
f: (t: TransactionHandle) => Promise<T>, f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
): Promise<T> { ): Promise<T> {
return runWithTransaction<T>(this.db, stores, f, "readonly"); return runWithTransaction<T, StoreTypes>(this.db, stores, f, "readonly");
} }
async runWithWriteTransaction<T>( async runWithWriteTransaction<T, StoreTypes extends Store<string, any>>(
stores: Store<any>[], stores: StoreTypes[],
f: (t: TransactionHandle) => Promise<T>, f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
): Promise<T> { ): Promise<T> {
return runWithTransaction<T>(this.db, stores, f, "readwrite"); return runWithTransaction<T, StoreTypes>(this.db, stores, f, "readwrite");
} }
} }