Support automatically creating recurring events
Also implement locks for tasks that can have unintended side-effects if ran simultaneously. Other minor UI tweaks.
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { RRuleSet } from 'rrule-rust';
|
||||
|
||||
import Database, { sql } from '/lib/interfaces/database.ts';
|
||||
import { Calendar, CalendarEvent } from '/lib/types.ts';
|
||||
import Locker from '/lib/interfaces/locker.ts';
|
||||
import { Calendar, CalendarEvent, CalendarEventReminder } from '/lib/types.ts';
|
||||
import { getRandomItem } from '/lib/utils/misc.ts';
|
||||
import { CALENDAR_COLOR_OPTIONS } from '/lib/utils/calendar.ts';
|
||||
import { CALENDAR_COLOR_OPTIONS, getVCalendarDate } from '/lib/utils/calendar.ts';
|
||||
import { getUserById } from './user.ts';
|
||||
|
||||
const db = new Database();
|
||||
@@ -33,8 +36,129 @@ export async function getCalendarEvents(
|
||||
|
||||
return calendarEvents;
|
||||
} else {
|
||||
// Fetch initial recurring events and calculate any necessary to create/show for the date range, if it's not in the past
|
||||
if (dateRange.end >= new Date()) {
|
||||
const lock = new Locker(`events-${userId}`);
|
||||
|
||||
await lock.acquire();
|
||||
|
||||
const initialRecurringCalendarEvents = await db.query<CalendarEvent>(
|
||||
sql`SELECT * FROM "bewcloud_calendar_events"
|
||||
WHERE "user_id" = $1
|
||||
AND "calendar_id" = ANY($2)
|
||||
AND "start_date" <= $3
|
||||
AND ("extra" ->> 'is_recurring')::boolean IS TRUE
|
||||
AND ("extra" ->> 'recurring_id')::uuid = "id"
|
||||
ORDER BY "start_date" ASC`,
|
||||
[
|
||||
userId,
|
||||
calendarIds,
|
||||
dateRange.end,
|
||||
],
|
||||
);
|
||||
|
||||
// For each initial recurring event, check instance dates, check if those exist in calendarEvents. If not, create them.
|
||||
for (const initialRecurringCalendarEvent of initialRecurringCalendarEvents) {
|
||||
try {
|
||||
const oneMonthAgo = new Date(new Date().setUTCMonth(new Date().getUTCMonth() - 1));
|
||||
|
||||
let recurringInstanceStartDate = initialRecurringCalendarEvent.start_date;
|
||||
let lastSequence = initialRecurringCalendarEvent.extra.recurring_sequence!;
|
||||
|
||||
if (recurringInstanceStartDate <= oneMonthAgo) {
|
||||
// Fetch the latest recurring sample, so we don't have to calculate as many recurring dates, but still preserve the original date's properties for generating the recurring instances
|
||||
const latestRecurringInstance = (await db.query<CalendarEvent>(
|
||||
sql`SELECT * FROM "bewcloud_calendar_events"
|
||||
WHERE "user_id" = $1
|
||||
AND "calendar_id" = ANY($2)
|
||||
AND "start_date" <= $3
|
||||
AND ("extra" ->> 'is_recurring')::boolean IS TRUE
|
||||
AND ("extra" ->> 'recurring_id')::uuid = $4
|
||||
ORDER BY ("extra" ->> 'recurring_sequence')::number DESC
|
||||
LIMIT 1`,
|
||||
[
|
||||
userId,
|
||||
calendarIds,
|
||||
dateRange.end,
|
||||
initialRecurringCalendarEvent.extra.recurring_id!,
|
||||
],
|
||||
))[0];
|
||||
|
||||
if (latestRecurringInstance) {
|
||||
recurringInstanceStartDate = latestRecurringInstance.start_date;
|
||||
lastSequence = latestRecurringInstance.extra.recurring_sequence!;
|
||||
}
|
||||
}
|
||||
|
||||
const rRuleSet = RRuleSet.parse(
|
||||
`DTSTART:${
|
||||
getVCalendarDate(recurringInstanceStartDate)
|
||||
}\n${initialRecurringCalendarEvent.extra.recurring_rrule}`,
|
||||
);
|
||||
|
||||
const maxRecurringDatesToGenerate = 30;
|
||||
|
||||
const timestamps = rRuleSet.all(maxRecurringDatesToGenerate);
|
||||
|
||||
const validDates = timestamps.map((timestamp) => new Date(timestamp)).filter((date) => date <= dateRange.end);
|
||||
|
||||
// For each date, check if an instance already exists. If not, create it and add it.
|
||||
for (const instanceDate of validDates) {
|
||||
instanceDate.setHours(recurringInstanceStartDate.getHours()); // NOTE: Something is making the hour shift when it shouldn't
|
||||
|
||||
const matchingRecurringInstance = (await db.query<CalendarEvent>(
|
||||
sql`SELECT * FROM "bewcloud_calendar_events"
|
||||
WHERE "user_id" = $1
|
||||
AND "calendar_id" = ANY($2)
|
||||
AND "start_date" = $3
|
||||
AND ("extra" ->> 'is_recurring')::boolean IS TRUE
|
||||
AND ("extra" ->> 'recurring_id')::uuid = $4
|
||||
ORDER BY "start_date" ASC
|
||||
LIMIT 1`,
|
||||
[
|
||||
userId,
|
||||
calendarIds,
|
||||
instanceDate,
|
||||
initialRecurringCalendarEvent.extra.recurring_id!,
|
||||
],
|
||||
))[0];
|
||||
|
||||
if (!matchingRecurringInstance) {
|
||||
const oneHourLater = new Date(new Date(instanceDate).setHours(instanceDate.getHours() + 1));
|
||||
const newCalendarEvent = await createCalendarEvent(
|
||||
userId,
|
||||
initialRecurringCalendarEvent.calendar_id,
|
||||
initialRecurringCalendarEvent.title,
|
||||
instanceDate,
|
||||
oneHourLater,
|
||||
initialRecurringCalendarEvent.is_all_day,
|
||||
);
|
||||
|
||||
newCalendarEvent.extra = { ...newCalendarEvent.extra, ...initialRecurringCalendarEvent.extra };
|
||||
|
||||
newCalendarEvent.extra.recurring_sequence = ++lastSequence;
|
||||
|
||||
await updateCalendarEvent(newCalendarEvent);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error generating recurring instances: ${error}`);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
lock.release();
|
||||
}
|
||||
|
||||
const calendarEvents = await db.query<CalendarEvent>(
|
||||
sql`SELECT * FROM "bewcloud_calendar_events" WHERE "user_id" = $1 AND "calendar_id" = ANY($2) AND (("start_date" >= $3 OR "end_date" <= $4) OR ("start_date" < $3 AND "end_date" > $4)) ORDER BY "start_date" ASC`,
|
||||
sql`SELECT * FROM "bewcloud_calendar_events"
|
||||
WHERE "user_id" = $1
|
||||
AND "calendar_id" = ANY($2)
|
||||
AND (
|
||||
("start_date" >= $3 OR "end_date" <= $4)
|
||||
OR ("start_date" < $3 AND "end_date" > $4)
|
||||
)
|
||||
ORDER BY "start_date" ASC`,
|
||||
[
|
||||
userId,
|
||||
calendarIds,
|
||||
@@ -43,8 +167,6 @@ export async function getCalendarEvents(
|
||||
],
|
||||
);
|
||||
|
||||
// TODO: Fetch initial recurring events and calculate any necessary to create/show for the date range
|
||||
|
||||
return calendarEvents;
|
||||
}
|
||||
}
|
||||
@@ -183,9 +305,18 @@ export async function createCalendarEvent(
|
||||
throw new Error('Calendar not found');
|
||||
}
|
||||
|
||||
const oneHourEarlier = new Date(new Date(startDate).setHours(new Date(startDate).getHours() - 1));
|
||||
const sameDayAtNine = new Date(new Date(startDate).setHours(9));
|
||||
|
||||
const newReminder: CalendarEventReminder = {
|
||||
start_date: isAllDay ? sameDayAtNine.toISOString() : oneHourEarlier.toISOString(),
|
||||
type: 'display',
|
||||
};
|
||||
|
||||
const extra: CalendarEvent['extra'] = {
|
||||
organizer_email: user.email,
|
||||
transparency: 'default',
|
||||
reminders: [newReminder],
|
||||
};
|
||||
|
||||
const revision = crypto.randomUUID();
|
||||
@@ -240,6 +371,30 @@ export async function updateCalendarEvent(calendarEvent: CalendarEvent, oldCalen
|
||||
|
||||
const oldCalendar = oldCalendarId ? await getCalendar(oldCalendarId, user.id) : null;
|
||||
|
||||
const oldCalendarEvent = await getCalendarEvent(calendarEvent.id, user.id);
|
||||
|
||||
if (oldCalendarEvent.start_date !== calendarEvent.start_date) {
|
||||
const oneHourEarlier = new Date(
|
||||
new Date(calendarEvent.start_date).setHours(new Date(calendarEvent.start_date).getHours() - 1),
|
||||
);
|
||||
const sameDayAtNine = new Date(new Date(calendarEvent.start_date).setHours(9));
|
||||
|
||||
const newReminder: CalendarEventReminder = {
|
||||
start_date: calendarEvent.is_all_day ? sameDayAtNine.toISOString() : oneHourEarlier.toISOString(),
|
||||
type: 'display',
|
||||
};
|
||||
|
||||
if (!Array.isArray(calendarEvent.extra.reminders)) {
|
||||
calendarEvent.extra.reminders = [newReminder];
|
||||
} else {
|
||||
if (calendarEvent.extra.reminders.length === 0) {
|
||||
calendarEvent.extra.reminders.push(newReminder);
|
||||
} else {
|
||||
calendarEvent.extra.reminders[0] = { ...calendarEvent.extra.reminders[0], start_date: newReminder.start_date };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db.query(
|
||||
sql`UPDATE "bewcloud_calendar_events" SET
|
||||
"revision" = $3,
|
||||
|
||||
Reference in New Issue
Block a user