This is a async function that does a sqlite transaction:
async transaction<V>(done: (conn: this) => V): Promise<V> {
this.depth += 1;
await this.execute(`SAVEPOINT tt_${this.depth}`);
try {
return await done(this);
} catch (err) {
await this.execute(`ROLLBACK TO tt_${this.depth}`);
throw err;
} finally {
await this.execute(`RELEASE tt_${this.depth}`);
this.depth -= 1;
}
}
It can be nested, meaning that inside done I can call transaction() .
But calls on the same level cannot be made in parallel, for eg.
promise.all([
db.transaction(...),
db.transaction(...),
]);
because it messes up the save/release in sqlite apparently.
example above is just for simplicity, in real world parallel calls happen when 2 or more requests reach the server at same time, and all reqs use the same db instance
any way to detect inside the function if there is another call of the function running on same level at same time ?
In case anyone is interested in the solution, I managed to do it with a "wrapper" class.
class DbInstance{
private depth = 0;
....
async transaction<V>(done: (conn: this) => V): Promise<V> {
this.depth += 1;
await this.execute(`SAVEPOINT tt_${this.depth}`);
try {
return await done(this);
} catch (err) {
await this.execute(`ROLLBACK TO tt_${this.depth}`);
throw err;
} finally {
await this.execute(`RELEASE tt_${this.depth}`);
this.depth -= 1;
}
}
}
class Db{
private dbInstance: DbInstance;
private activeTrans?: Promise<any>;
constructor(){
this.dbInstance = new DbInstance();
}
transaction<T>(done: (conn: DbInstance) => Promise<T>): Promise<T> {
if(this.activeTrans){
await this.activeTrans;
return this.transaction(done);
}
this.activeTrans = this.dbInstance.transaction(done);
try{
return await this.activeTrans;
}catch(err){
throw err;
}finally{
this.activeTrans = undefined;
}
}
}
export const db = new Db();
db is exposed everywhere, but dbInstance is only exposed in the transaction callback, so you can have nested transactions using dbInstance.
downside is that both classes have to implement same methods...
You can track concurrent transaction() calls using a Map<number, number>, where the key is the depth and the value is the active count. Increment on start, check for existing transactions, and decrement on completion.
class Database {
private depth = 0;
private activeTransactions = new Map<number, Set<Promise<void>>>();
async transaction<V>(done: (conn: this) => Promise<V>): Promise<V> {
const depth = ++this.depth;
const activeAtDepth = this.activeTransactions.get(depth) ?? new Set();
if (activeAtDepth.size > 0) {
throw new Error(`Concurrent transactions at depth ${depth} are not allowed.`);
}
let transactionPromise!: Promise<V>;
transactionPromise = (async () => {
try {
await this.execute(`SAVEPOINT tt_${depth}`);
return await done(this);
} catch (err) {
await this.execute(`ROLLBACK TO tt_${depth}`);
throw err;
} finally {
await this.execute(`RELEASE tt_${depth}`);
activeAtDepth.delete(transactionPromise);
if (!activeAtDepth.size) this.activeTransactions.delete(depth);
this.depth--;
}
})();
activeAtDepth.add(transactionPromise);
this.activeTransactions.set(depth, activeAtDepth);
return transactionPromise;
}
async execute(query: string) {
console.log("Executing:", query);
// ...
}
}
// test code
const db = new Database();
async function test() {
await db.transaction(async (conn) => {
console.log("Transaction 1 started");
// Allowed: Nested transaction (same instance)
await conn.transaction(async () => {
console.log("Nested transaction");
});
console.log("Transaction 1 finished");
});
// Not Allowed: Parallel transactions at the same level
await Promise.all([
db.transaction(async () => console.log("Transaction A")),
db.transaction(async () => console.log("Transaction B")),
]);
}
test().catch(console.error);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With