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

@ -2,7 +2,7 @@
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { readFile, unlink, writeFile } from "node:fs/promises";
import { readFileSync, writeFileSync, unlinkSync } from "node:fs";
import type { ApiAuthenticationProvider, ApiEvent, ApiSchedule, ApiSubscription, ApiUserType } from "~/shared/types/api";
import type { Id } from "~/shared/types/common";
@ -53,9 +53,9 @@ const nextAuthenticationMethodIdPath = "data/auth-method-id.json"
const nextEventIdPath = "data/next-event-id.json";
const eventsPath = "data/events.json";
async function remove(path: string) {
function remove(path: string) {
try {
await unlink(path);
unlinkSync(path);
} catch (err: any) {
if (err.code !== "ENOENT") {
throw err;
@ -63,17 +63,17 @@ async function remove(path: string) {
}
}
export async function deleteDatabase() {
await remove(schedulePath);
await remove(subscriptionsPath);
await remove(usersPath);
await remove(sessionsPath);
export function deleteDatabase() {
remove(schedulePath);
remove(subscriptionsPath);
remove(usersPath);
remove(sessionsPath);
}
async function readJson<T>(filePath: string, fallback: T) {
function readJson<T>(filePath: string, fallback: T) {
let data: T extends () => infer R ? R : T;
try {
data = JSON.parse(await readFile(filePath, "utf-8"));
data = JSON.parse(readFileSync(filePath, "utf-8"));
} catch (err: any) {
if (err.code !== "ENOENT")
throw err;
@ -82,19 +82,19 @@ async function readJson<T>(filePath: string, fallback: T) {
return data;
}
export async function readSchedule() {
export function readSchedule() {
return readJson(schedulePath, (): ApiSchedule => ({
id: 111,
updatedAt: new Date().toISOString(),
}));
}
export async function writeSchedule(schedule: ApiSchedule) {
await writeFile(schedulePath, JSON.stringify(schedule, undefined, "\t") + "\n", "utf-8");
export function writeSchedule(schedule: ApiSchedule) {
writeFileSync(schedulePath, JSON.stringify(schedule, undefined, "\t") + "\n", "utf-8");
}
export async function readSubscriptions() {
let subscriptions = await readJson<ApiSubscription[]>(subscriptionsPath, []);
export function readSubscriptions() {
let subscriptions = readJson<ApiSubscription[]>(subscriptionsPath, []);
if (subscriptions.length && "keys" in subscriptions[0]) {
// Discard old format
subscriptions = [];
@ -102,89 +102,89 @@ export async function readSubscriptions() {
return subscriptions;
}
export async function writeSubscriptions(subscriptions: ApiSubscription[]) {
await writeFile(subscriptionsPath, JSON.stringify(subscriptions, undefined, "\t") + "\n", "utf-8");
export function writeSubscriptions(subscriptions: ApiSubscription[]) {
writeFileSync(subscriptionsPath, JSON.stringify(subscriptions, undefined, "\t") + "\n", "utf-8");
}
export async function readNextUserId() {
return await readJson(nextUserIdPath, 0);
export function readNextUserId() {
return readJson(nextUserIdPath, 0);
}
export async function writeNextUserId(nextId: number) {
await writeFile(nextUserIdPath, String(nextId), "utf-8");
export function writeNextUserId(nextId: number) {
writeFileSync(nextUserIdPath, String(nextId), "utf-8");
}
export async function nextUserId() {
let nextId = await readJson(nextUserIdPath, 0);
export function nextUserId() {
let nextId = readJson(nextUserIdPath, 0);
if (nextId === 0) {
nextId = Math.max(...(await readUsers()).map(user => user.id), -1) + 1;
nextId = Math.max(...(readUsers()).map(user => user.id), -1) + 1;
}
await writeFile(nextUserIdPath, String(nextId + 1), "utf-8");
writeFileSync(nextUserIdPath, String(nextId + 1), "utf-8");
return nextId;
}
export async function readUsers() {
return await readJson(usersPath, (): ServerUser[] => []);
export function readUsers() {
return readJson(usersPath, (): ServerUser[] => []);
}
export async function writeUsers(users: ServerUser[]) {
await writeFile(usersPath, JSON.stringify(users, undefined, "\t") + "\n", "utf-8");
export function writeUsers(users: ServerUser[]) {
writeFileSync(usersPath, JSON.stringify(users, undefined, "\t") + "\n", "utf-8");
}
export async function readNextSessionId() {
return await readJson(nextSessionIdPath, 0);
export function readNextSessionId() {
return readJson(nextSessionIdPath, 0);
}
export async function writeNextSessionId(nextId: number) {
await writeFile(nextSessionIdPath, String(nextId), "utf-8");
export function writeNextSessionId(nextId: number) {
writeFileSync(nextSessionIdPath, String(nextId), "utf-8");
}
export async function nextSessionId() {
const nextId = await readJson(nextSessionIdPath, 0);
await writeFile(nextSessionIdPath, String(nextId + 1), "utf-8");
export function nextSessionId() {
const nextId = readJson(nextSessionIdPath, 0);
writeFileSync(nextSessionIdPath, String(nextId + 1), "utf-8");
return nextId;
}
export async function readSessions() {
export function readSessions() {
return readJson<ServerSession[]>(sessionsPath, [])
}
export async function writeSessions(sessions: ServerSession[]) {
await writeFile(sessionsPath, JSON.stringify(sessions, undefined, "\t") + "\n", "utf-8");
export function writeSessions(sessions: ServerSession[]) {
writeFileSync(sessionsPath, JSON.stringify(sessions, undefined, "\t") + "\n", "utf-8");
}
export async function nextAuthenticationMethodId() {
const nextId = await readJson(nextAuthenticationMethodIdPath, 0);
await writeFile(nextAuthenticationMethodIdPath, String(nextId + 1), "utf-8");
export function nextAuthenticationMethodId() {
const nextId = readJson(nextAuthenticationMethodIdPath, 0);
writeFileSync(nextAuthenticationMethodIdPath, String(nextId + 1), "utf-8");
return nextId;
}
export async function writeNextAuthenticationMethodId(nextId: number) {
await writeFile(nextAuthenticationMethodIdPath, String(nextId), "utf-8");
export function writeNextAuthenticationMethodId(nextId: number) {
writeFileSync(nextAuthenticationMethodIdPath, String(nextId), "utf-8");
}
export async function readAuthenticationMethods() {
export function readAuthenticationMethods() {
return readJson<ServerAuthenticationMethod[]>(authMethodPath, [])
}
export async function writeAuthenticationMethods(authMethods: ServerAuthenticationMethod[]) {
await writeFile(authMethodPath, JSON.stringify(authMethods, undefined, "\t") + "\n", "utf-8");
export function writeAuthenticationMethods(authMethods: ServerAuthenticationMethod[]) {
writeFileSync(authMethodPath, JSON.stringify(authMethods, undefined, "\t") + "\n", "utf-8");
}
export async function nextEventId() {
const nextId = await readJson(nextEventIdPath, 0);
await writeFile(nextEventIdPath, String(nextId + 1), "utf-8");
export function nextEventId() {
const nextId = readJson(nextEventIdPath, 0);
writeFileSync(nextEventIdPath, String(nextId + 1), "utf-8");
return nextId;
}
export async function writeNextEventId(nextId: number) {
await writeFile(nextEventIdPath, String(nextId), "utf-8");
export function writeNextEventId(nextId: number) {
writeFileSync(nextEventIdPath, String(nextId), "utf-8");
}
export async function readEvents() {
export function readEvents() {
return readJson<ApiEvent[]>(eventsPath, [])
}
export async function writeEvents(events: ApiEvent[]) {
await writeFile(eventsPath, JSON.stringify(events, undefined, "\t") + "\n", "utf-8");
export function writeEvents(events: ApiEvent[]) {
writeFileSync(eventsPath, JSON.stringify(events, undefined, "\t") + "\n", "utf-8");
}