Skip to content

Commit f7d3ae9

Browse files
fix: Sync the KV session when necessary
1 parent 8e77067 commit f7d3ae9

File tree

1 file changed

+40
-7
lines changed

1 file changed

+40
-7
lines changed

src/utils/credits.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import "server-only";
22
import { eq, sql, desc, and, lt, isNull, gt, or, asc } from "drizzle-orm";
33
import { getDB } from "@/db";
44
import { userTable, creditTransactionTable, CREDIT_TRANSACTION_TYPE, purchasedItemsTable } from "@/db/schema";
5-
import { updateAllSessionsOfUser, KVSession } from "./kv-session";
5+
import { updateAllSessionsOfUser, updateKVSession, KVSession } from "./kv-session";
66
import { CREDIT_PACKAGES, FREE_MONTHLY_CREDITS, DISABLE_CREDIT_BILLING_SYSTEM } from "@/constants";
77

88
export type CreditPackage = typeof CREDIT_PACKAGES[number];
@@ -18,8 +18,22 @@ function shouldRefreshCredits(session: KVSession, currentTime: Date): boolean {
1818
}
1919

2020
// Calculate the date exactly one month after the last refresh
21-
const oneMonthAfterLastRefresh = new Date(session.user.lastCreditRefreshAt);
22-
oneMonthAfterLastRefresh.setMonth(oneMonthAfterLastRefresh.getMonth() + 1);
21+
// Using a more reliable approach to avoid edge cases with setMonth()
22+
const lastRefresh = new Date(session.user.lastCreditRefreshAt);
23+
const year = lastRefresh.getFullYear();
24+
const month = lastRefresh.getMonth();
25+
const day = lastRefresh.getDate();
26+
27+
// Calculate one month later, handling edge cases (e.g., Jan 31 + 1 month = Feb 28/29)
28+
let oneMonthAfterLastRefresh = new Date(year, month + 1, day);
29+
30+
// If the day changed (e.g., Jan 31 -> Mar 3 instead of Feb 28), use last day of target month
31+
if (oneMonthAfterLastRefresh.getDate() !== day) {
32+
oneMonthAfterLastRefresh = new Date(year, month + 2, 0); // Last day of target month
33+
}
34+
35+
// Preserve the original time of day
36+
oneMonthAfterLastRefresh.setHours(lastRefresh.getHours(), lastRefresh.getMinutes(), lastRefresh.getSeconds(), lastRefresh.getMilliseconds());
2337

2438
// Only refresh if we've passed the one month mark
2539
return currentTime >= oneMonthAfterLastRefresh;
@@ -149,14 +163,33 @@ export async function addFreeMonthlyCreditsIfNeeded(session: KVSession): Promise
149163
},
150164
});
151165

166+
// Convert DB date string to Date object to ensure consistent type handling
167+
const dbLastRefreshAt = user?.lastCreditRefreshAt
168+
? new Date(user.lastCreditRefreshAt)
169+
: null;
170+
152171
// This should prevent race conditions between multiple sessions
153-
if (!shouldRefreshCredits({ ...session, user: { ...session.user, lastCreditRefreshAt: user?.lastCreditRefreshAt ?? null } }, currentTime)) {
172+
if (!shouldRefreshCredits({ ...session, user: { ...session.user, lastCreditRefreshAt: dbLastRefreshAt } }, currentTime)) {
173+
// KV session is out of sync with DB - update it to prevent this check on next request
174+
await updateKVSession(session.id, session.userId, new Date(session.expiresAt));
154175
return user?.currentCredits ?? 0;
155176
}
156177

157-
// Calculate one month ago from current time (using calendar month logic)
158-
const oneMonthAgo = new Date(currentTime);
159-
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
178+
// Calculate one month ago from current time
179+
// Using a more reliable approach to avoid edge cases with setMonth()
180+
const year = currentTime.getFullYear();
181+
const month = currentTime.getMonth();
182+
const day = currentTime.getDate();
183+
184+
let oneMonthAgo = new Date(year, month - 1, day);
185+
186+
// Handle edge cases (e.g., Mar 31 - 1 month should be Feb 28/29, not Mar 3)
187+
if (oneMonthAgo.getDate() !== day) {
188+
oneMonthAgo = new Date(year, month, 0); // Last day of previous month
189+
}
190+
191+
// Preserve the original time of day
192+
oneMonthAgo.setHours(currentTime.getHours(), currentTime.getMinutes(), currentTime.getSeconds(), currentTime.getMilliseconds());
160193

161194
// Update last refresh date FIRST to act as a distributed lock
162195
// This prevents race conditions where multiple requests try to add credits simultaneously

0 commit comments

Comments
 (0)