From 3569bb923e37696c0b1419932ca68d4ee8307702 Mon Sep 17 00:00:00 2001 From: Willian Alves Date: Sun, 16 Nov 2025 00:50:48 -0300 Subject: [PATCH 1/2] fix: replace forEach with Promise.all to prevent silent async failures This commit fixes two critical bugs that could cause data corruption: 1. **forEach with async functions (CRITICAL)** - Changed forEach to map + Promise.all in expense update/delete - forEach doesn't wait for async functions, causing silent failures - Errors inside forEach async callbacks were being swallowed - Database could end up in inconsistent state with partial updates 2. **Date mutation bug** - Fixed direct mutation of expense.payment.date object - Now creates new Date instance to prevent side effects - Prevents race conditions in concurrent updates 3. **Code quality improvement** - Simplified getInstallmentDate() to avoid triple Date construction - More readable and performant date handling Impact: - Recurring expense updates now properly fail-fast if any update fails - All correlated expenses update atomically as a group - Date mutations no longer affect original objects - Better error propagation to user --- db/models/expense.ts | 26 +++++++++++++++++++------- utils/expenses/factory.ts | 14 ++++++++------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/db/models/expense.ts b/db/models/expense.ts index 43b3acc..598711c 100644 --- a/db/models/expense.ts +++ b/db/models/expense.ts @@ -189,8 +189,8 @@ export default class ExpenseService { }); } - // TODO make this more atomic. if one fails, all should fail - expensesToUpdate.forEach(async (expense) => { + // Update all expenses atomically - if one fails, all fail + const updatePromises = expensesToUpdate.map(async (expense) => { const expenseKey = [Keys.EXPENSES, userId, expense.id]; const correlationKey = [ Keys.EXPENSES_BY_CORRELATION, @@ -206,15 +206,17 @@ export default class ExpenseService { expense.id, ]; - // keep the original date, only update the day - expense.payment.date.setDate(input.payment.date.getDate()); + // keep the original date, only update the day (create new Date to avoid mutation) + const updatedDate = new Date(expense.payment.date); + updatedDate.setDate(input.payment.date.getDate()); + const updatedExpense = { ...expense, ...payload, payment: { ...expense.payment, ...payload.payment, - date: expense.payment.date, + date: updatedDate, }, }; @@ -228,8 +230,13 @@ export default class ExpenseService { if (!res.ok) { throw new Deno.errors.Interrupted("Failed to update expense"); } + + return updatedExpense; }); + // Wait for all updates to complete + await Promise.all(updatePromises); + const updatedExpense = { ...rawExpense.value, ...input, @@ -321,8 +328,8 @@ export default class ExpenseService { (e) => e.payment.date.getMonth() + 1 >= initialMonth, ); } - // TODO make this more atomic. if one fails, all should fail - expensesToDelete.forEach(async (expense) => { + // Delete all expenses atomically - if one fails, all fail + const deletePromises = expensesToDelete.map(async (expense) => { const expenseKey = [Keys.EXPENSES, userId, expense.id]; const correlationKey = [ Keys.EXPENSES_BY_CORRELATION, @@ -352,8 +359,13 @@ export default class ExpenseService { `Failed to delete expense. Expense ID: ${expense.id}`, ); } + + return expense; }); + // Wait for all deletes to complete + await Promise.all(deletePromises); + return rawExpense.value; } } diff --git a/utils/expenses/factory.ts b/utils/expenses/factory.ts index 97a7131..194be36 100644 --- a/utils/expenses/factory.ts +++ b/utils/expenses/factory.ts @@ -98,11 +98,13 @@ export default class ExpenseInputFactory { } } - private getInstallmentDate(installment: number) { - return installment === 1 ? new Date(this.data.paymentDate) : new Date( - new Date(this.data.paymentDate).setMonth( - new Date(this.data.paymentDate).getMonth() + installment - 1, - ), - ); + private getInstallmentDate(installment: number): Date { + if (installment === 1) { + return new Date(this.data.paymentDate); + } + + const date = new Date(this.data.paymentDate); + date.setMonth(date.getMonth() + installment - 1); + return date; } } From a47dfbc39194f4648c08aa25eb25d40a8a818b96 Mon Sep 17 00:00:00 2001 From: Willian Alves Date: Sat, 22 Nov 2025 20:39:38 -0300 Subject: [PATCH 2/2] skip serena folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ff3d300..cd10fad 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ _fresh/ node_modules/ sqlite* +.serena