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",
|
"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",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -11,6 +11,15 @@ const generateVirtualId = (originalId: string, dateStr: string) => {
|
|||||||
return `virtual-${originalId}-${dateStr}`;
|
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
|
// Helper to project future tasks
|
||||||
const projectFutureTasks = (tasks: Task[], horizonDays = 90) => {
|
const projectFutureTasks = (tasks: Task[], horizonDays = 90) => {
|
||||||
const projectedTasks: any[] = [];
|
const projectedTasks: any[] = [];
|
||||||
@ -53,6 +62,7 @@ const projectFutureTasks = (tasks: Task[], horizonDays = 90) => {
|
|||||||
const currentDate = new Date(baseDate);
|
const currentDate = new Date(baseDate);
|
||||||
const interval = latestTask.recurrenceInterval || 1;
|
const interval = latestTask.recurrenceInterval || 1;
|
||||||
const unit = latestTask.recurrenceUnit || 'weeks'; // 'days', 'weeks', 'months', 'years'
|
const unit = latestTask.recurrenceUnit || 'weeks'; // 'days', 'weeks', 'months', 'years'
|
||||||
|
const maxInstances = MAX_INSTANCES_PER_SERIES[unit] ?? 26;
|
||||||
|
|
||||||
// Parse recurrenceDays for weekly multi-day recurrence
|
// Parse recurrenceDays for weekly multi-day recurrence
|
||||||
let recDays: number[] | null = null;
|
let recDays: number[] | null = null;
|
||||||
@ -60,9 +70,10 @@ const projectFutureTasks = (tasks: Task[], horizonDays = 90) => {
|
|||||||
try { recDays = JSON.parse(latestTask.recurrenceDays); } catch { recDays = null; }
|
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;
|
let iterations = 0;
|
||||||
while (currentDate < horizonDate && iterations < 500) {
|
while (currentDate < horizonDate && iterations < 500 && instanceCount < maxInstances) {
|
||||||
iterations++;
|
iterations++;
|
||||||
|
|
||||||
// Advance date
|
// 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)
|
isVirtual: true, // Flag for frontend if needed (not in Prisma type, but JS object accepts it)
|
||||||
originalTaskId: latestTask.id // Reference
|
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
|
// When a recurring event is created, the master is cached. Then the sync
|
||||||
// returns expanded instances with different IDs but the same recurringEventId.
|
// returns expanded instances with different IDs but the same recurringEventId.
|
||||||
// We keep instances and discard masters that overlap with them.
|
// 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 calendarEvents = useMemo(() => {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const seenSlot = new Set<string>();
|
const seenSlot = new Set<string>();
|
||||||
@ -1694,24 +1710,11 @@ export default function WeeklyView() {
|
|||||||
if (seenSlot.has(slotKey)) return false;
|
if (seenSlot.has(slotKey)) return false;
|
||||||
seenSlot.add(slotKey);
|
seenSlot.add(slotKey);
|
||||||
return true;
|
return true;
|
||||||
}).map((event) => {
|
}).map((event) => ({
|
||||||
let isEditable = false;
|
...event,
|
||||||
if (event.calendarId) {
|
editable: event.calendarId ? (calendarEditabilityMap.get(event.calendarId) ?? false) : false,
|
||||||
for (const conn of connections) {
|
}));
|
||||||
if (conn.calendars && Array.isArray(conn.calendars)) {
|
}, [rawCalendarEvents, calendarEditabilityMap]);
|
||||||
const cal = conn.calendars.find(
|
|
||||||
(c: any) => c.id === event.calendarId,
|
|
||||||
);
|
|
||||||
if (cal && cal.editable) {
|
|
||||||
isEditable = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { ...event, editable: isEditable };
|
|
||||||
});
|
|
||||||
}, [rawCalendarEvents, connections]);
|
|
||||||
const [currentWeekStart, setCurrentWeekStart] = useState(() => {
|
const [currentWeekStart, setCurrentWeekStart] = useState(() => {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
d.setHours(0, 0, 0, 0);
|
d.setHours(0, 0, 0, 0);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user