perf: limit recurring task projection + O(1) calendar event dedup
- tasks/route.ts: add per-unit instance cap for virtual task projection (daily: 60, weekly: 26, monthly: 12, yearly: 3) to prevent unbounded generation from high-frequency recurring tasks - WeeklyView: extract calendarEditabilityMap as a separate useMemo keyed on connections; replace O(n²) nested find() loop with O(1) Map.get() lookup in the calendarEvents deduplication useMemo v1.79.0
This commit is contained in:
parent
6e8ed34578
commit
6fed38381c
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.78.0",
|
||||
"version": "1.79.0",
|
||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -11,6 +11,15 @@ const generateVirtualId = (originalId: string, dateStr: string) => {
|
||||
return `virtual-${originalId}-${dateStr}`;
|
||||
};
|
||||
|
||||
// Max virtual instances generated per recurring series, keyed by recurrence unit.
|
||||
// Caps pathological cases (e.g. a daily task with 90-day horizon = 90 instances).
|
||||
const MAX_INSTANCES_PER_SERIES: Record<string, number> = {
|
||||
days: 60,
|
||||
weeks: 26,
|
||||
months: 12,
|
||||
years: 3,
|
||||
};
|
||||
|
||||
// Helper to project future tasks
|
||||
const projectFutureTasks = (tasks: Task[], horizonDays = 90) => {
|
||||
const projectedTasks: any[] = [];
|
||||
@ -53,6 +62,7 @@ const projectFutureTasks = (tasks: Task[], horizonDays = 90) => {
|
||||
const currentDate = new Date(baseDate);
|
||||
const interval = latestTask.recurrenceInterval || 1;
|
||||
const unit = latestTask.recurrenceUnit || 'weeks'; // 'days', 'weeks', 'months', 'years'
|
||||
const maxInstances = MAX_INSTANCES_PER_SERIES[unit] ?? 26;
|
||||
|
||||
// Parse recurrenceDays for weekly multi-day recurrence
|
||||
let recDays: number[] | null = null;
|
||||
@ -60,9 +70,10 @@ const projectFutureTasks = (tasks: Task[], horizonDays = 90) => {
|
||||
try { recDays = JSON.parse(latestTask.recurrenceDays); } catch { recDays = null; }
|
||||
}
|
||||
|
||||
// Safety break
|
||||
let instanceCount = 0;
|
||||
// Safety break: stop at horizon OR per-unit instance cap, whichever comes first
|
||||
let iterations = 0;
|
||||
while (currentDate < horizonDate && iterations < 500) {
|
||||
while (currentDate < horizonDate && iterations < 500 && instanceCount < maxInstances) {
|
||||
iterations++;
|
||||
|
||||
// Advance date
|
||||
@ -123,6 +134,7 @@ const projectFutureTasks = (tasks: Task[], horizonDays = 90) => {
|
||||
isVirtual: true, // Flag for frontend if needed (not in Prisma type, but JS object accepts it)
|
||||
originalTaskId: latestTask.id // Reference
|
||||
});
|
||||
instanceCount++;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -1674,6 +1674,22 @@ export default function WeeklyView() {
|
||||
// When a recurring event is created, the master is cached. Then the sync
|
||||
// returns expanded instances with different IDs but the same recurringEventId.
|
||||
// We keep instances and discard masters that overlap with them.
|
||||
|
||||
// Build calendarId → editable map once per connections change (O(connections × calendars))
|
||||
const calendarEditabilityMap = useMemo(() => {
|
||||
const map = new Map<string, boolean>();
|
||||
for (const conn of connections) {
|
||||
if (conn.calendars && Array.isArray(conn.calendars)) {
|
||||
for (const cal of conn.calendars as any[]) {
|
||||
if (cal.id && !map.has(cal.id)) {
|
||||
map.set(cal.id, !!cal.editable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [connections]);
|
||||
|
||||
const calendarEvents = useMemo(() => {
|
||||
const seen = new Set<string>();
|
||||
const seenSlot = new Set<string>();
|
||||
@ -1694,24 +1710,11 @@ export default function WeeklyView() {
|
||||
if (seenSlot.has(slotKey)) return false;
|
||||
seenSlot.add(slotKey);
|
||||
return true;
|
||||
}).map((event) => {
|
||||
let isEditable = false;
|
||||
if (event.calendarId) {
|
||||
for (const conn of connections) {
|
||||
if (conn.calendars && Array.isArray(conn.calendars)) {
|
||||
const cal = conn.calendars.find(
|
||||
(c: any) => c.id === event.calendarId,
|
||||
);
|
||||
if (cal && cal.editable) {
|
||||
isEditable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ...event, editable: isEditable };
|
||||
});
|
||||
}, [rawCalendarEvents, connections]);
|
||||
}).map((event) => ({
|
||||
...event,
|
||||
editable: event.calendarId ? (calendarEditabilityMap.get(event.calendarId) ?? false) : false,
|
||||
}));
|
||||
}, [rawCalendarEvents, calendarEditabilityMap]);
|
||||
const [currentWeekStart, setCurrentWeekStart] = useState(() => {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user