I have below function from a 3rd party package which I can't modify
async function runTransaction(callback) {
const client = await createClient();
try {
await client.query("BEGIN");
await callback(client);
} finally {
await client.query("COMMIT");
}
}
Normally I have to create an async function and pass it to the runTransaction
async function update(client) {
await client.query("UPDATE ...")
}
await runTransaction(update);
I wonder if there is a way to use runTransaction
in an async generator pattern. Here is my sketch. The end result I want is BEGIN
then UPDATE
then COMMIT
async *clientGenerator() {
await runTransaction(async client => {
yield client
});
// this will not work as yield can't be used in a callback
}
async function main() {
const dbClientGenerator = await clientGenerator();
const dbClientHolder = await dbClientGenerator.next();
await dbClientHolder.value.query("UPDATE ...");
}
I also tried below, but the callback gets called at the same time as await client.query("COMMIT");
in runTransaction
.
async *clientGenerator() {
yield new Promise(resolve => {
runTransaction(async client => {
resolve(sql);
})
})
}
Any idea how to achieve this?
You could write1
async function* transaction() {
const client = await createClient();
try {
await client.query("BEGIN");
yield client;
} finally {
await client.query("COMMIT");
}
}
and use it as
for await (const client of transaction()) {
await client.query("UPDATE ...");
}
but this is no better (and much more confusing) than the current
await runTransaction(async client => {
await client.query("UPDATE ...");
});
Really there is nothing wrong with this. It is known as the promise disposer pattern and guarantees proper cleanup of resources after you're done. Your main
function that manually advances the async iterator is a prime example of what's wrong with this approach: it fails to do error handling and would never commit the transaction. The promise disposer pattern prevents such abuse.
The pattern may get superseded by disposables from the explicit resource management proposal which will allow writing
{
await using client = transaction();
await client.query("UPDATE ...");
}
1: or even wrap the runTransaction
you're given into an equivalent of this, instead of rewriting it, but this is complicated and I wanted to make the point clear.
If you absolutely have to do this, here's how:
async function* transaction() {
// © Bergi, https://stackoverflow.com/a/78043294/1048572, CC BY-SA 4.0
let final, result, resume;
const client = await new Promise((resolve, reject) => {
final = runTransaction(client => {
resolve(client);
return new Promise(resolve => {
resume = resolve;
});
})
final.catch(reject);
})
try {
result = yield client;
} catch(err) {
result = Promise.reject(err);
} finally {
resume(result);
return final;
}
}
This handles all kinds of possible failure cases in runTransaction
and in the callback iteration, but relies on runTransaction
calling the callback exactly once (unless there's an error in createClient
).