using the helper:

using the helper:
export async function withSQLiteTransaction<T>(
db: any,
logic: (db: any) => Promise<{ rollback?: boolean; reason?: string; result?: T }>
): Promise<{ success: boolean; result?: T; error?: any; reason?: string }> {
try {
await db.prepare?.("BEGIN").run?.() ?? db.exec("BEGIN");

const { rollback, result, reason } = await logic(db);

if (rollback) {
await db.prepare?.("ROLLBACK").run?.() ?? db.exec("ROLLBACK");
return { success: false, reason };
}

await db.prepare?.("COMMIT").run?.() ?? db.exec("COMMIT");
return { success: true, result };
} catch (error) {
await db.prepare?.("ROLLBACK").run?.() ?? db.exec("ROLLBACK");
return { success: false, error };
}
}
export async function withSQLiteTransaction<T>(
db: any,
logic: (db: any) => Promise<{ rollback?: boolean; reason?: string; result?: T }>
): Promise<{ success: boolean; result?: T; error?: any; reason?: string }> {
try {
await db.prepare?.("BEGIN").run?.() ?? db.exec("BEGIN");

const { rollback, result, reason } = await logic(db);

if (rollback) {
await db.prepare?.("ROLLBACK").run?.() ?? db.exec("ROLLBACK");
return { success: false, reason };
}

await db.prepare?.("COMMIT").run?.() ?? db.exec("COMMIT");
return { success: true, result };
} catch (error) {
await db.prepare?.("ROLLBACK").run?.() ?? db.exec("ROLLBACK");
return { success: false, error };
}
}
34 Replies
João Castro
João Castro2mo ago
You don't have fully tracked errors with this. You just know that some error may happen
Marak
MarakOP2mo ago
You return the reason where you had your failure
João Castro
João Castro2mo ago
The approach that leverages the type system the most and allows for things like exhaustive matching against literals is to have tracking of each type of error that might happen
Marak
MarakOP2mo ago
Let's see if someone else has had to deal with this. I'm not using TypeScript and generally rely on throw when problems happen.
João Castro
João Castro2mo ago
This just says that its some error. Forgot to handle a specific one in a specific way? The type system will think its all fine. While if you had tracking of each one, you would be forced to handle it There are many other benefits
Marak
MarakOP2mo ago
You could wrap each call in the try catch and explicitly create a unique reason that gets returned Instead of throwing, you return a json object with reason field
João Castro
João Castro2mo ago
Yeah but then I lose inference And I need to type it by hand in a separate place, every time
Marak
MarakOP2mo ago
Yes, if you want that level of control. Let's see if one of the TypeScript developers here knows better.
João Castro
João Castro2mo ago
Again, the point is not having to go through all of this It could just be a function call that does the rollback
Marak
MarakOP2mo ago
transactionSync() isn't an option? if so, why not? I think unless you are calling fetch() or using a dependency that enforces async operations, you can probably use transactionSync() If it's all SQLite, you may be good
João Castro
João Castro2mo ago
@Burrito better to move here
Burrito
Burrito2mo ago
Sure. Which part are you having trouble with?
João Castro
João Castro2mo ago
I think there is a way to do it, but I'm not sure if it will end up having some limitations, specially with RPC
Burrito
Burrito2mo ago
I've given you a way. By having your code use error as value paradigm, and then do your own throw at one single point. You get fully type safe error.
João Castro
João Castro2mo ago
Oh yeah The thing is, I have to pass the error value as the Error message
Burrito
Burrito2mo ago
No you don't.
João Castro
João Castro2mo ago
So that its available at runtime and i can check against it
Burrito
Burrito2mo ago
The error doesn't even have to be an Error. Okay just to get us on the same page, do you understand what the error as value paradigm is?
João Castro
João Castro2mo ago
... "An exception thrown by an RPC method implementation will propagate to the caller. If it is one of the standard JavaScript Error types, the message and prototype's name will be retained, though the stack trace is not."
Burrito
Burrito2mo ago
Yes but that's not the point, throwing is just a mechanism to let transactionSync rollback, you won't be using the thrown error as the mechanism to pass the type safe error back.
João Castro
João Castro2mo ago
Yeah
Burrito
Burrito2mo ago
Because as you already know, the catch (error) has no type information, that's not what we will be using to pass the error back.
João Castro
João Castro2mo ago
Yup, I was thinking about some other case But you are right I can do it with a wrapper Still would be nice to not need it though
Burrito
Burrito2mo ago
It's very simple, you first decide on how you do want to encode your success and error in the error as value paradigm.
João Castro
João Castro2mo ago
Yeah, its pretty straightforward
Burrito
Burrito2mo ago
For the sake of the example, I'm going to encode success as undefined, and encode error as anything that is not undefined.
João Castro
João Castro2mo ago
I already have some sample code working with it here hahaha But its nice to leave it in chat
Burrito
Burrito2mo ago
Then you can trivially write a type safe error transaction like:
function safeTransactionSync<E>(closure: () => E | undefined) {
let error: E | undefined
try {
ctx.storage.transactionSync(() => {
error = closure()
if (error !== undefined) throw new Error('Roll back')
})
} catch {
return error
}
}
function safeTransactionSync<E>(closure: () => E | undefined) {
let error: E | undefined
try {
ctx.storage.transactionSync(() => {
error = closure()
if (error !== undefined) throw new Error('Roll back')
})
} catch {
return error
}
}
That's it, now instead of calling ctx.storage.transactionSync(...) you just call safeTransactionSync. And all errors in the callback of safeTransactionSync will be type safely propagated back as the return value. If you have a different encoding of success and error, you just need to change the generic types and the line of error !== undefined.
João Castro
João Castro2mo ago
Yeah that is definitely a nice solution for it Again, would be nice to just have a function to rollback, specially as some people will end up not going through this just to get better types
Burrito
Burrito2mo ago
I would agree, ideally the transactionSync callback would provide a rollback: () => never as the first argument so you can call that, and () => never will preserve TS CFA. But that's only a viable solution if you assume people write throwless code in callback. Otherwise people will effectively have to wrap try { ... } catch { rollback() } in every single call. Especially because storage.sql.exec and friends can already throw. So in practice, the current API design of throwing to rollback is the only practical one.
João Castro
João Castro2mo ago
Not really. I can treat the other errors as unknown_error by default when I do the catch that I will have to do anyway. And I would have all my errors tracked the same way Yes I would need to catch, but in both scenarios I need it And throw could still rollback the same way it does now
Burrito
Burrito2mo ago
My point is that if you do an API design of:
storage.transactionSync(rollback => {
// ...
})
storage.transactionSync(rollback => {
// ...
})
Then for 99% of the people writing code, they have to write:
storage.transactionSync(rollback => {
try {
// ...
} catch {
rollback()
}
})
storage.transactionSync(rollback => {
try {
// ...
} catch {
rollback()
}
})
It's just unnecessary boilerplate, and on top of that if you did forget to call rollback, that transaction is doing nothing.
João Castro
João Castro2mo ago
Why? Unhandled errors can have the same behavior as it has today.
Burrito
Burrito2mo ago
Oh that's what you mean, yeah sure I can agree with that. But you still have to decide how rollback behaves especially in regards to control flow. Should rollback throw or not? If rollback throws, that means you still have to try the entire transactionSync call anyways. If rollback doesn't throw, then you are forcing people to write rollback(); return; to stop rest of the code from executing, and that's also error prone boilerplate.

Did you find this page helpful?