3.2 KiB
3.2 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Use RunInTransaction with the Scoped txApp, Never the Outer App | CRITICAL | Mixing scoped and outer app inside a transaction silently deadlocks or writes outside the tx | transactions, extending, deadlock, runInTransaction, atomicity |
Use RunInTransaction with the Scoped txApp, Never the Outer App
app.RunInTransaction (Go) and $app.runInTransaction (JS) wrap a block of work in a SQLite write transaction. The callback receives a transaction-scoped app instance (txApp / txApp). Every database call inside the block must go through that scoped instance - reusing the outer app / $app bypasses the transaction (silent partial writes) or deadlocks (SQLite allows only one writer).
Incorrect (outer app used inside the tx block):
// ❌ Uses the outer app for the second Save - deadlocks on the writer lock
err := app.RunInTransaction(func(txApp core.App) error {
user := core.NewRecord(usersCol)
user.Set("email", "a@b.co")
if err := txApp.Save(user); err != nil {
return err
}
audit := core.NewRecord(auditCol)
audit.Set("user", user.Id)
return app.Save(audit) // ❌ NOT txApp - blocks forever
})
Correct (always txApp inside the block, return errors to roll back):
err := app.RunInTransaction(func(txApp core.App) error {
user := core.NewRecord(usersCol)
user.Set("email", "a@b.co")
if err := txApp.Save(user); err != nil {
return err // rollback
}
audit := core.NewRecord(auditCol)
audit.Set("user", user.Id)
if err := txApp.Save(audit); err != nil {
return err // rollback
}
return nil // commit
})
if err != nil {
return err
}
// JSVM - the callback receives the transactional app
$app.runInTransaction((txApp) => {
const user = new Record(txApp.findCollectionByNameOrId("users"));
user.set("email", "a@b.co");
txApp.save(user);
const audit = new Record(txApp.findCollectionByNameOrId("audit"));
audit.set("user", user.id);
txApp.save(audit);
// throw anywhere in this block to roll back the whole tx
});
Rules of the transaction:
- Use only
txApp/ the callback's scoped app inside the block. Capturing the outerappdefeats the purpose and can deadlock. - Inside event hooks,
e.Appis already the transactional app when the hook fires inside a tx - prefer it over a captured parent-scopeappfor the same reason. - Return an error (Go) or
throw(JS) to roll back. A successful return commits. - SQLite serializes writers - keep transactions short. Do not make HTTP calls, send emails, or wait on external systems inside the block.
- Do not start a transaction inside another transaction on the same app - nested
RunInTransactionontxAppis supported and reuses the existing transaction, but nested calls on the outerappwill deadlock. - Hooks (
OnRecordAfterCreateSuccess, etc.) fired from aSaveinside a tx run inside that tx. Anything they do throughe.Appparticipates in the rollback; anything they do through a captured outerappdoes not.
Reference: Go database · JS database