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.
101 lines
2.7 KiB
TypeScript
101 lines
2.7 KiB
TypeScript
/*
|
|
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
import { nextEventId, readSchedule, type ServerUser, writeSchedule } from '~/server/database';
|
|
import { broadcastEvent } from '~/server/streams';
|
|
import type { ApiSchedule, ApiTombstone } from '~/shared/types/api';
|
|
|
|
export async function updateScheduleInterestedCounts(users: ServerUser[]) {
|
|
const eventCounts = new Map<number, number>();
|
|
const eventSlotCounts = new Map<number, number>();
|
|
for (const user of users) {
|
|
if (user.deleted)
|
|
continue;
|
|
if (user.interestedEventIds)
|
|
for (const id of user.interestedEventIds)
|
|
eventCounts.set(id, (eventCounts.get(id) ?? 0) + 1);
|
|
if (user.interestedEventSlotIds)
|
|
for (const id of user.interestedEventSlotIds)
|
|
eventSlotCounts.set(id, (eventSlotCounts.get(id) ?? 0) + 1);
|
|
}
|
|
|
|
const schedule = readSchedule();
|
|
if (schedule.deleted) {
|
|
throw new Error("Deleted schedule not implemented");
|
|
}
|
|
const update: ApiSchedule = {
|
|
id: schedule.id,
|
|
updatedAt: new Date().toISOString(),
|
|
events: [],
|
|
};
|
|
const updatedFrom = schedule.updatedAt;
|
|
for (const event of schedule.events ?? []) {
|
|
let modified = false;
|
|
if (event.deleted)
|
|
continue;
|
|
|
|
let count = eventCounts.get(event.id);
|
|
if (count !== event.interested) {
|
|
event.interested = eventCounts.get(event.id);
|
|
modified = true;
|
|
}
|
|
for (const slot of event.slots) {
|
|
let slotCount = eventSlotCounts.get(slot.id);
|
|
if (slotCount !== slot.interested) {
|
|
slot.interested = slotCount;
|
|
modified = true;
|
|
}
|
|
}
|
|
if (modified) {
|
|
event.updatedAt = update.updatedAt;
|
|
update.events!.push(event);
|
|
}
|
|
}
|
|
if (!update.events!.length) {
|
|
return; // No changes
|
|
}
|
|
schedule.updatedAt = updatedFrom;
|
|
await writeSchedule(schedule);
|
|
await broadcastEvent({
|
|
id: nextEventId(),
|
|
type: "schedule-update",
|
|
updatedFrom,
|
|
data: update,
|
|
});
|
|
}
|
|
|
|
export function canSeeAnonymous(userType: string | undefined) {
|
|
return userType === "admin";
|
|
}
|
|
|
|
export function canSeeCrew(userType: string | undefined) {
|
|
return userType === "crew" || userType === "admin";
|
|
}
|
|
|
|
/** Filters out crew visible only parts of schedule */
|
|
export function filterSchedule(schedule: ApiSchedule | ApiTombstone): ApiSchedule | ApiTombstone {
|
|
if (schedule.deleted) {
|
|
return schedule;
|
|
}
|
|
return {
|
|
id: schedule.id,
|
|
updatedAt: schedule.updatedAt,
|
|
locations: schedule.locations,
|
|
events: (schedule.events ?? [])
|
|
.map(event => (
|
|
event.deleted
|
|
? event
|
|
: event.crew
|
|
// Pretend crew events are deleted.
|
|
? { id: event.id, deleted: true, updatedAt: event.updatedAt }
|
|
: {
|
|
...event,
|
|
slots: event.slots.map(slot => ({
|
|
...slot,
|
|
assigned: undefined,
|
|
}
|
|
)),
|
|
})),
|
|
}
|
|
}
|