Skip to content

Commit 7e5801c

Browse files
authored
Merge pull request #653 from callumalpass/refactor/advanced-calendar-performance-improvements
2 parents 1338968 + 93eef4a commit 7e5801c

14 files changed

+1671
-383
lines changed

src/bases/base-view-factory.ts

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export interface ViewConfig {
2626
export function buildTasknotesBaseViewFactory(plugin: TaskNotesPlugin, config: ViewConfig) {
2727
return function tasknotesBaseViewFactory(basesContainer: BasesContainerLike) {
2828
let currentRoot: HTMLElement | null = null;
29+
let eventListener: any = null;
30+
let updateDebounceTimer: number | null = null;
31+
let currentTaskElements = new Map<string, HTMLElement>();
2932

3033
const viewContainerEl = (basesContainer as any)?.viewContainerEl as HTMLElement | undefined;
3134
if (!viewContainerEl) {
@@ -144,7 +147,9 @@ export function buildTasknotesBaseViewFactory(plugin: TaskNotesPlugin, config: V
144147
}
145148

146149
// Render tasks using existing helper
147-
await renderTaskNotesInBasesView(itemsContainer, taskNotes, plugin, basesContainer);
150+
// Clear existing task elements tracking before re-render
151+
currentTaskElements.clear();
152+
await renderTaskNotesInBasesView(itemsContainer, taskNotes, plugin, basesContainer, currentTaskElements);
148153
}
149154
} catch (error: any) {
150155
console.error(`[TaskNotes][BasesPOC] Error rendering Bases ${config.errorPrefix}:`, error);
@@ -156,9 +161,88 @@ export function buildTasknotesBaseViewFactory(plugin: TaskNotesPlugin, config: V
156161
}
157162
};
158163

164+
// Setup selective update handling for real-time task changes
165+
const setupTaskUpdateListener = () => {
166+
if (!eventListener) {
167+
const { EVENT_TASK_UPDATED } = require('../types');
168+
eventListener = plugin.emitter.on(EVENT_TASK_UPDATED, async (eventData: any) => {
169+
try {
170+
const updatedTask = eventData?.task || eventData?.taskInfo;
171+
if (!updatedTask || !updatedTask.path) return;
172+
173+
// Check if this task affects our current Bases view
174+
const currentTasks = extractDataItems();
175+
const relevantTask = currentTasks.find(item => item.path === updatedTask.path);
176+
177+
if (relevantTask) {
178+
// Task is visible in this Bases view - perform selective update
179+
await selectiveUpdateTaskInBasesView(updatedTask);
180+
}
181+
} catch (error) {
182+
console.error('[TaskNotes][Bases] Error in selective task update:', error);
183+
// Fallback to full refresh
184+
debouncedFullRefresh();
185+
}
186+
});
187+
}
188+
};
189+
190+
// Selective update for a single task within Bases view
191+
const selectiveUpdateTaskInBasesView = async (updatedTask: any) => {
192+
if (!currentRoot) return;
193+
194+
try {
195+
const taskElement = currentTaskElements.get(updatedTask.path);
196+
if (taskElement) {
197+
// Update existing task element
198+
const { updateTaskCard } = await import('../ui/TaskCard');
199+
const basesProperties = (basesContainer as any)?.ctx?.formulas ?
200+
Object.keys((basesContainer as any).ctx.formulas) : [];
201+
202+
updateTaskCard(taskElement, updatedTask, plugin, basesProperties, {
203+
showDueDate: true,
204+
showCheckbox: false,
205+
showArchiveButton: false,
206+
showTimeTracking: false,
207+
showRecurringControls: true,
208+
});
209+
210+
// Add update animation
211+
taskElement.classList.add('task-card--updated');
212+
window.setTimeout(() => {
213+
taskElement.classList.remove('task-card--updated');
214+
}, 1000);
215+
216+
console.log(`[TaskNotes][Bases] Selectively updated task: ${updatedTask.path}`);
217+
} else {
218+
// Task not currently visible, might need to be added - refresh to be safe
219+
debouncedFullRefresh();
220+
}
221+
} catch (error) {
222+
console.error('[TaskNotes][Bases] Error in selective task update:', error);
223+
debouncedFullRefresh();
224+
}
225+
};
226+
227+
// Debounced refresh to prevent multiple rapid refreshes
228+
const debouncedFullRefresh = () => {
229+
if (updateDebounceTimer) {
230+
clearTimeout(updateDebounceTimer);
231+
}
232+
233+
updateDebounceTimer = window.setTimeout(async () => {
234+
console.log('[TaskNotes][Bases] Performing debounced full refresh');
235+
await render();
236+
updateDebounceTimer = null;
237+
}, 150);
238+
};
239+
159240
// Kick off initial async render
160241
void render();
161242

243+
// Setup real-time updates
244+
setupTaskUpdateListener();
245+
162246
// Create view object with proper listener management
163247
let queryListener: (() => void) | null = null;
164248

@@ -179,18 +263,36 @@ export function buildTasknotesBaseViewFactory(plugin: TaskNotesPlugin, config: V
179263
}
180264
},
181265
destroy: () => {
266+
// Clean up task update listener
267+
if (eventListener) {
268+
plugin.emitter.offref(eventListener);
269+
eventListener = null;
270+
}
271+
272+
// Clean up debounce timer
273+
if (updateDebounceTimer) {
274+
clearTimeout(updateDebounceTimer);
275+
updateDebounceTimer = null;
276+
}
277+
278+
// Clean up query listener
182279
if (queryListener && (basesContainer as any)?.query?.off) {
183280
try {
184281
(basesContainer as any).query.off('change', queryListener);
185282
} catch (e) {
186283
// Query listener removal may fail if already disposed
187284
}
188285
}
286+
287+
// Clean up DOM and state
189288
if (currentRoot) {
190289
currentRoot.remove();
191290
currentRoot = null;
192291
}
292+
currentTaskElements.clear();
193293
queryListener = null;
294+
295+
console.log('[TaskNotes][Bases] Cleaned up view with real-time updates');
194296
},
195297
load: () => {
196298
if ((basesContainer as any)?.query?.on && !queryListener) {

src/bases/helpers.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ export async function renderTaskNotesInBasesView(
161161
container: HTMLElement,
162162
taskNotes: TaskInfo[],
163163
plugin: TaskNotesPlugin,
164-
basesContainer?: any
164+
basesContainer?: any,
165+
taskElementsMap?: Map<string, HTMLElement>
165166
): Promise<void> {
166167
const { createTaskCard } = await import('../ui/TaskCard');
167168

@@ -244,6 +245,11 @@ export async function renderTaskNotesInBasesView(
244245
try {
245246
const taskCard = createTaskCard(taskInfo, plugin, visibleProperties, cardOptions);
246247
taskListEl.appendChild(taskCard);
248+
249+
// Track task elements for selective updates
250+
if (taskElementsMap && taskInfo.path) {
251+
taskElementsMap.set(taskInfo.path, taskCard);
252+
}
247253
} catch (error) {
248254
console.warn('[TaskNotes][BasesPOC] Error creating task card:', error);
249255
}

src/editor/ProjectNoteDecorations.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -640,12 +640,12 @@ class ProjectNoteDecorationsPlugin implements PluginValue {
640640

641641
constructor(view: EditorView, private plugin: TaskNotesPlugin) {
642642
this.view = view;
643-
this.projectService = new ProjectSubtasksService(plugin);
643+
this.projectService = plugin.projectSubtasksService;
644644
this.decorations = this.buildDecorations(view);
645-
645+
646646
// Set up event listeners for data changes
647647
this.setupEventListeners();
648-
648+
649649
// Load tasks for current file asynchronously
650650
this.loadTasksForCurrentFile(view);
651651
}
@@ -755,23 +755,24 @@ class ProjectNoteDecorationsPlugin implements PluginValue {
755755

756756
private async loadTasksForCurrentFile(view: EditorView) {
757757
const file = this.getFileFromView(view);
758-
758+
759+
759760
if (file instanceof TFile) {
760761
try {
761762
const newTasks = await this.projectService.getTasksLinkedToProject(file);
762-
763+
763764
// Check if tasks actually changed
764765
const tasksChanged = newTasks.length !== this.cachedTasks.length ||
765766
newTasks.some((newTask, index) => {
766767
const oldTask = this.cachedTasks[index];
767-
return !oldTask ||
768+
return !oldTask ||
768769
newTask.title !== oldTask.title ||
769770
newTask.status !== oldTask.status ||
770771
newTask.priority !== oldTask.priority ||
771772
newTask.due !== oldTask.due ||
772773
newTask.path !== oldTask.path;
773774
});
774-
775+
775776
if (tasksChanged) {
776777
this.cachedTasks = newTasks;
777778
this.dispatchUpdate();
@@ -834,23 +835,23 @@ class ProjectNoteDecorationsPlugin implements PluginValue {
834835

835836
private buildDecorations(view: EditorView): DecorationSet {
836837
const builder = new RangeSetBuilder<Decoration>();
837-
838+
838839
try {
839840
// Don't show widget in table cell editors
840841
if (this.isTableCellEditor(view)) {
841842
return builder.finish();
842843
}
843-
844+
844845
// Check if project subtasks widget is enabled
845846
if (!this.plugin.settings.showProjectSubtasks) {
846847
return builder.finish();
847848
}
848-
849+
849850
// Only show in live preview mode, not source mode
850851
if (!view.state.field(editorLivePreviewField)) {
851852
return builder.finish();
852853
}
853-
854+
854855
// Only build decorations if we have cached tasks
855856
if (this.cachedTasks.length === 0) {
856857
return builder.finish();

src/main.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { StatusManager } from './services/StatusManager';
5252
import { PriorityManager } from './services/PriorityManager';
5353
import { TaskService } from './services/TaskService';
5454
import { FilterService } from './services/FilterService';
55+
import { ViewPerformanceService } from './services/ViewPerformanceService';
5556
import { AutoArchiveService } from './services/AutoArchiveService';
5657
import { ViewStateManager } from './services/ViewStateManager';
5758
import { createTaskLinkOverlay, dispatchTaskUpdate } from './editor/TaskLinkOverlay';
@@ -133,6 +134,7 @@ export default class TaskNotesPlugin extends Plugin {
133134
projectSubtasksService: ProjectSubtasksService;
134135
expandedProjectsService: ExpandedProjectsService;
135136
autoArchiveService: AutoArchiveService;
137+
viewPerformanceService: ViewPerformanceService;
136138

137139
// Editor services
138140
taskLinkDetectionService?: import('./services/TaskLinkDetectionService').TaskLinkDetectionService;
@@ -229,6 +231,7 @@ export default class TaskNotesPlugin extends Plugin {
229231
this.migrationService = new MigrationService(this.app);
230232
this.statusBarService = new StatusBarService(this);
231233
this.notificationService = new NotificationService(this);
234+
this.viewPerformanceService = new ViewPerformanceService(this);
232235

233236
// Connect AutoArchiveService to TaskService for status-based auto-archiving
234237
this.taskService.setAutoArchiveService(this.autoArchiveService);
@@ -410,6 +413,12 @@ export default class TaskNotesPlugin extends Plugin {
410413
// Initialize notification service
411414
await this.notificationService.initialize();
412415

416+
// Build project status cache for better TaskCard performance
417+
await this.projectSubtasksService.buildProjectStatusCache();
418+
419+
// Ensure MinimalNativeCache project indexes are warmed up
420+
await this.warmupProjectIndexes();
421+
413422
// Initialize and start auto-archive service
414423
await this.autoArchiveService.start();
415424

@@ -541,6 +550,29 @@ export default class TaskNotesPlugin extends Plugin {
541550
}, 10); // Small delay to ensure startup completes first
542551
}
543552

553+
/**
554+
* Warmup project indexes in MinimalNativeCache for better performance
555+
*/
556+
private async warmupProjectIndexes(): Promise<void> {
557+
try {
558+
// Simple approach: just trigger the lazy index building once
559+
// This is much more efficient than processing individual files
560+
const warmupStartTime = Date.now();
561+
562+
// Trigger index building with a single call - this will process all files internally
563+
this.cacheManager.getTasksForDate(new Date().toISOString().split('T')[0]);
564+
565+
const duration = Date.now() - warmupStartTime;
566+
// Only log slow warmup for debugging large vaults
567+
if (duration > 2000) {
568+
console.log(`[TaskNotes] Project indexes warmed up in ${duration}ms`);
569+
}
570+
571+
} catch (error) {
572+
console.error('[TaskNotes] Error during project index warmup:', error);
573+
}
574+
}
575+
544576
/**
545577
* Public method for views to wait for readiness
546578
*/
@@ -880,6 +912,11 @@ export default class TaskNotesPlugin extends Plugin {
880912
this.filterService.cleanup();
881913
}
882914

915+
// Clean up ViewPerformanceService
916+
if (this.viewPerformanceService) {
917+
this.viewPerformanceService.destroy();
918+
}
919+
883920
// Clean up AutoArchiveService
884921
if (this.autoArchiveService) {
885922
this.autoArchiveService.stop();

0 commit comments

Comments
 (0)