Use sync access for the temp JSON file database
All checks were successful
/ build (push) Successful in 1m37s
/ deploy (push) Has been skipped

Replace all async reads and writes to the JSON database with the sync
reads and writes to prevent a data corruption race condition where two
requests are processed at the same time and write to the same file, or
one reads while the other writes causing read of partially written data.
This commit is contained in:
Hornwitser 2025-09-20 23:04:16 +02:00
parent f9d188b2ba
commit 80cec71308
23 changed files with 138 additions and 138 deletions

View file

@ -9,11 +9,11 @@ export default defineEventHandler(async (event) => {
setHeader(event, "Content-Disposition", 'attachment; filename="database-dump.json"');
setHeader(event, "Content-Type", "application/json; charset=utf-8");
return {
nextUserId: await readNextUserId(),
users: await readUsers(),
nextSessionId: await readNextSessionId(),
sessions: await readSessions(),
subscriptions: await readSubscriptions(),
schedule: await readSchedule(),
nextUserId: readNextUserId(),
users: readUsers(),
nextSessionId: readNextSessionId(),
sessions: readSessions(),
subscriptions: readSubscriptions(),
schedule: readSchedule(),
};
})

View file

@ -7,14 +7,14 @@ import { generateDemoSchedule, generateDemoAccounts } from "~/server/generate-de
export default defineEventHandler(async (event) => {
await requireServerSessionWithAdmin(event);
const accounts = generateDemoAccounts();
await writeUsers(accounts);
await writeSchedule(generateDemoSchedule());
await writeAuthenticationMethods(accounts.map((user, index) => ({
writeUsers(accounts);
writeSchedule(generateDemoSchedule());
writeAuthenticationMethods(accounts.map((user, index) => ({
id: index,
userId: user.id,
provider: "demo",
slug: user.name!,
name: user.name!,
})));
await writeNextAuthenticationMethodId(Math.max(await nextAuthenticationMethodId(), accounts.length));
writeNextAuthenticationMethodId(Math.max(nextAuthenticationMethodId(), accounts.length));
})

View file

@ -29,20 +29,20 @@ export default defineEventHandler(async (event) => {
});
}
const currentNextUserId = await readNextUserId();
await writeNextUserId(Math.max(currentNextUserId, snapshot.nextUserId));
await writeUsers(snapshot.users);
const currentNextSessionId = await readNextSessionId();
await writeNextSessionId(Math.max(currentNextSessionId, snapshot.nextSessionId));
const currentSessions = new Map((await readSessions()).map(session => [session.id, session]));
await writeSessions(snapshot.sessions.filter(session => {
const currentNextUserId = readNextUserId();
writeNextUserId(Math.max(currentNextUserId, snapshot.nextUserId));
writeUsers(snapshot.users);
const currentNextSessionId = readNextSessionId();
writeNextSessionId(Math.max(currentNextSessionId, snapshot.nextSessionId));
const currentSessions = new Map((readSessions()).map(session => [session.id, session]));
writeSessions(snapshot.sessions.filter(session => {
const current = currentSessions.get(session.id);
// Only keep sessions that match the account id in both sets to avoid
// resurrecting deleted sessions. This will still cause session cross
// pollution if a snapshot from another instance is loaded here.
return current?.accountId !== undefined && current.accountId === session.accountId;
}));
await writeSubscriptions(snapshot.subscriptions);
await writeSchedule(snapshot.schedule);
writeSubscriptions(snapshot.subscriptions);
writeSchedule(snapshot.schedule);
await sendRedirect(event, "/");
})

View file

@ -6,5 +6,5 @@ import { deleteDatabase } from "~/server/database";
export default defineEventHandler(async (event) => {
await requireServerSessionWithAdmin(event);
await deleteDatabase();
deleteDatabase();
})

View file

@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
});
}
const users = await readUsers();
const users = readUsers();
const user = users.find(user => user.id === patch.id);
if (!user || user.deleted) {
throw createError({
@ -52,28 +52,28 @@ export default defineEventHandler(async (event) => {
user.name = patch.name;
}
user.updatedAt = new Date().toISOString();
await writeUsers(users);
writeUsers(users);
broadcastEvent({
id: await nextEventId(),
id: nextEventId(),
type: "user-update",
data: serverUserToApi(user),
});
// Rotate sessions with the user in it if the access changed
if (accessChanged) {
const sessions = await readSessions();
const sessions = readSessions();
const nowMs = Date.now();
for (const session of sessions) {
if (session.accountId === user.id) {
session.rotatesAtMs = nowMs;
broadcastEvent({
id: await nextEventId(),
id: nextEventId(),
type: "session-expired",
sessionId: session.id,
});
}
}
await writeSessions(sessions);
writeSessions(sessions);
}
// Update Schedule counts.