mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
fix(dataflow): resolve blank TaskView and integrate ICS events
- Add IcsSource for calendar event integration into dataflow - Fix infinite loop with sourceSeq tracking mechanism - Resolve blank TaskView on quick startup with debounced events - Add change detection to prevent unnecessary batch updates - Implement render guards to prevent concurrent refreshes - Update reindex commands to support dataflow architecture - Persist ICS events separately in storage layer - Update architecture documentation with current implementation Fixes issues where: - TaskView showed blank despite having loaded tasks - Infinite "Batch update" loops occurred - ICS calendar events disappeared when background sync disabled - Rapid successive updates caused view resets before rendering
This commit is contained in:
parent
3c67a739be
commit
deef893535
11 changed files with 885 additions and 209 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Overview
|
||||
|
||||
The Dataflow architecture is a modern, modular task management system that replaces the legacy TaskManager-based approach. It provides better separation of concerns, improved performance, and a more maintainable codebase.
|
||||
The Dataflow architecture is a modern, modular task management system that replaces the legacy TaskManager-based approach. It provides better separation of concerns, improved performance, and a more maintainable codebase. The architecture now supports both file-based tasks and external data sources (like ICS calendar events) through a unified event-driven pipeline.
|
||||
|
||||
## Architecture Components
|
||||
|
||||
|
|
@ -21,17 +21,18 @@ src/dataflow/
|
|||
├── events/ # Event system
|
||||
│ └── Events.ts # Centralized event management
|
||||
├── indexer/ # Task indexing
|
||||
│ └── Repository.ts # Task repository with indexing
|
||||
│ └── Repository.ts # Task repository with indexing & ICS integration
|
||||
├── parsers/ # High-level parsing entries
|
||||
│ ├── CanvasEntry.ts
|
||||
│ ├── FileMetaEntry.ts
|
||||
│ └── MarkdownEntry.ts
|
||||
├── persistence/ # Data persistence
|
||||
│ └── Storage.ts # Unified storage layer
|
||||
│ └── Storage.ts # Unified storage layer (tasks + ICS events)
|
||||
├── project/ # Project management
|
||||
│ └── Resolver.ts # Project resolution logic
|
||||
├── sources/ # Data sources
|
||||
│ └── ObsidianSource.ts # File system monitoring
|
||||
│ ├── ObsidianSource.ts # File system monitoring
|
||||
│ └── IcsSource.ts # ICS calendar events source
|
||||
├── workers/ # Background processing
|
||||
│ ├── ProjectData.worker.ts
|
||||
│ ├── ProjectDataWorkerManager.ts
|
||||
|
|
@ -48,35 +49,61 @@ src/dataflow/
|
|||
### 1. Separation of Concerns
|
||||
- **Parsers**: Only extract raw task data, no enhancement
|
||||
- **Augmentor**: All task enhancement logic in one place
|
||||
- **Repository**: Centralized indexing and querying
|
||||
- **Storage**: Unified persistence layer
|
||||
- **Repository**: Centralized indexing, querying, and data merging
|
||||
- **Storage**: Unified persistence layer for all data types
|
||||
- **Sources**: Independent data providers (files, ICS, etc.)
|
||||
|
||||
### 2. Event-Driven Architecture
|
||||
- Centralized event system through `Events.ts`
|
||||
- Consistent event naming and payload structure
|
||||
- Decoupled components communicate via events
|
||||
- Event sequence tracking prevents circular updates
|
||||
|
||||
### 3. Progressive Migration
|
||||
- Feature flag (`dataflowEnabled`) for safe rollout
|
||||
- Backward compatibility maintained
|
||||
- Gradual view migration support
|
||||
|
||||
### 4. Unified Data Pipeline
|
||||
- All data sources flow through the same architecture
|
||||
- File-based tasks and external events (ICS) are merged seamlessly
|
||||
- Consistent querying interface regardless of data source
|
||||
|
||||
## Component Responsibilities
|
||||
|
||||
### Orchestrator
|
||||
- Coordinates all dataflow components
|
||||
- Manages initialization and lifecycle
|
||||
- Routes events between components
|
||||
- Handles multiple data sources (ObsidianSource, IcsSource)
|
||||
- Implements sequence-based loop prevention
|
||||
|
||||
### Data Sources
|
||||
|
||||
#### ObsidianSource
|
||||
- Monitors file system changes
|
||||
- Emits FILE_UPDATED events
|
||||
- Handles Markdown and Canvas files
|
||||
- Tracks file modifications, creations, deletions
|
||||
|
||||
#### IcsSource (New)
|
||||
- Integrates external calendar events
|
||||
- Emits ICS_EVENTS_UPDATED events
|
||||
- Syncs with IcsManager
|
||||
- Converts calendar events to task format
|
||||
|
||||
### QueryAPI
|
||||
- Public interface for all data queries
|
||||
- Abstracts internal repository complexity
|
||||
- Provides consistent API for views
|
||||
- Returns merged data from all sources
|
||||
|
||||
### Repository
|
||||
- Maintains task index
|
||||
- Maintains task index for file-based tasks
|
||||
- Stores ICS events separately
|
||||
- Merges data from multiple sources in queries
|
||||
- Handles snapshot persistence
|
||||
- Emits update events
|
||||
- Emits update events with source tracking
|
||||
|
||||
### Augmentor
|
||||
- Applies task enhancement strategies
|
||||
|
|
@ -87,6 +114,65 @@ src/dataflow/
|
|||
- Wraps LocalStorageCache
|
||||
- Manages versioning and invalidation
|
||||
- Provides namespace isolation
|
||||
- Persists:
|
||||
- Raw tasks (file-based)
|
||||
- Augmented tasks
|
||||
- Project data
|
||||
- ICS events
|
||||
- Consolidated index
|
||||
|
||||
## Event Flow & Loop Prevention
|
||||
|
||||
### Event Types
|
||||
```typescript
|
||||
Events = {
|
||||
CACHE_READY: "task-genius:cache-ready",
|
||||
TASK_CACHE_UPDATED: "task-genius:task-cache-updated",
|
||||
FILE_UPDATED: "task-genius:file-updated",
|
||||
ICS_EVENTS_UPDATED: "task-genius:ics-events-updated",
|
||||
// ... other events
|
||||
}
|
||||
```
|
||||
|
||||
### Loop Prevention Mechanism
|
||||
1. **Source Sequence Tracking**: Each operation generates a unique sequence number
|
||||
2. **Event Tagging**: Events include `sourceSeq` to identify their origin
|
||||
3. **Filtering**: Components ignore events they originated
|
||||
4. **Clean Event Flow**: Prevents infinite update loops
|
||||
|
||||
### Typical Event Flow
|
||||
```
|
||||
1. File Change → ObsidianSource → FILE_UPDATED event
|
||||
2. Orchestrator processes → Repository.updateFile()
|
||||
3. Repository → TASK_CACHE_UPDATED event (with sourceSeq)
|
||||
4. Views update → UI refreshes
|
||||
|
||||
OR
|
||||
|
||||
1. Calendar Sync → IcsSource → ICS_EVENTS_UPDATED event
|
||||
2. Orchestrator processes → Repository.updateIcsEvents()
|
||||
3. Repository → TASK_CACHE_UPDATED event (with sourceSeq)
|
||||
4. Views update → UI refreshes
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Initialization
|
||||
1. **Repository.initialize()**: Load consolidated index and ICS events
|
||||
2. **ObsidianSource.initialize()**: Start file monitoring
|
||||
3. **IcsSource.initialize()**: Start calendar sync
|
||||
4. **Initial Scan**: Process all files if no cache exists
|
||||
|
||||
### Runtime Updates
|
||||
1. **File Changes**: ObsidianSource → Orchestrator → Repository → Views
|
||||
2. **ICS Updates**: IcsSource → Orchestrator → Repository → Views
|
||||
3. **Manual Refresh**: Views → QueryAPI → Repository (cached data)
|
||||
|
||||
### Data Persistence
|
||||
- **Continuous**: Augmented tasks stored on each update
|
||||
- **ICS Events**: Persisted separately for fast recovery
|
||||
- **Consolidated Index**: Saved periodically and on shutdown
|
||||
- **Version Control**: Schema versioning for migration support
|
||||
|
||||
## Migration Status
|
||||
|
||||
|
|
@ -96,93 +182,154 @@ src/dataflow/
|
|||
- ✅ Phase C: Parser and enhancement separation
|
||||
- ✅ Phase D: Unified persistence layer
|
||||
- ✅ Phase E: Default enablement and cleanup
|
||||
- ✅ Phase F: ICS integration through dataflow
|
||||
|
||||
### Migrated Components
|
||||
- All major views now support dataflow
|
||||
- MCP server supports both bridges
|
||||
- Core parsers moved to dataflow/core
|
||||
- Workers consolidated in dataflow/workers
|
||||
|
||||
### Removed Legacy Files
|
||||
- `utils/filterUtils.ts`
|
||||
- `utils/projectFilter.ts`
|
||||
- `mcp/bridge/TaskManagerBridge.ts`
|
||||
- Old parser locations in utils/
|
||||
### Current Architecture State
|
||||
- **Default Mode**: Dataflow is now the default
|
||||
- **Legacy Support**: TaskManager available for fallback
|
||||
- **External Data**: ICS events integrated seamlessly
|
||||
- **Performance**: Optimized with caching and workers
|
||||
- **Stability**: Loop prevention and error handling
|
||||
|
||||
## Usage
|
||||
|
||||
### Enabling Dataflow
|
||||
```typescript
|
||||
// In settings
|
||||
// In settings (now default)
|
||||
experimental: {
|
||||
dataflowEnabled: true // Now default
|
||||
dataflowEnabled: true
|
||||
}
|
||||
```
|
||||
|
||||
### Querying Tasks
|
||||
```typescript
|
||||
// Using QueryAPI
|
||||
const tasks = await queryAPI.getAllTasks();
|
||||
// Using QueryAPI - returns both file tasks and ICS events
|
||||
const allTasks = await queryAPI.getAllTasks();
|
||||
const projectTasks = await queryAPI.getTasksByProject("MyProject");
|
||||
const taggedTasks = await queryAPI.getTasksByTags(["important"]);
|
||||
```
|
||||
|
||||
### Event Subscription
|
||||
```typescript
|
||||
// Subscribe to task updates
|
||||
Events.on(Events.TASK_CACHE_UPDATED, (tasks) => {
|
||||
// Subscribe to all task updates (files + ICS)
|
||||
Events.on(Events.TASK_CACHE_UPDATED, (payload) => {
|
||||
const { changedFiles, stats } = payload;
|
||||
// Handle updated tasks
|
||||
});
|
||||
|
||||
// Subscribe to ICS-specific updates
|
||||
Events.on(Events.ICS_EVENTS_UPDATED, (payload) => {
|
||||
const { events } = payload;
|
||||
// Handle ICS events
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Improvements
|
||||
## Performance Characteristics
|
||||
|
||||
1. **Snapshot Loading**: Fast startup from persisted state
|
||||
2. **Worker Orchestration**: Better background processing
|
||||
### Optimizations
|
||||
1. **Snapshot Loading**: Fast startup from persisted state (~100ms for 1000 tasks)
|
||||
2. **Worker Orchestration**: Parallel processing for large vaults
|
||||
3. **Batch Operations**: Reduced I/O overhead
|
||||
4. **Event Deduplication**: Fewer redundant updates
|
||||
4. **Event Deduplication**: Sequence-based loop prevention
|
||||
5. **Incremental Updates**: Only changed files processed
|
||||
6. **Separate ICS Storage**: Calendar events don't impact file indexing
|
||||
|
||||
### Cache Strategy
|
||||
- **Multi-tier**: Raw → Augmented → Consolidated
|
||||
- **Content Hashing**: Detect actual changes
|
||||
- **Modification Time**: Quick staleness check
|
||||
- **Lazy Loading**: Load data only when needed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Infinite Loop Detection
|
||||
- **Symptom**: Repeated "Batch update" logs
|
||||
- **Cause**: Missing sourceSeq in events
|
||||
- **Solution**: Ensure all TASK_CACHE_UPDATED events include sourceSeq
|
||||
|
||||
#### Missing ICS Events
|
||||
- **Symptom**: Calendar events not showing in views
|
||||
- **Cause**: IcsSource not initialized or IcsManager unavailable
|
||||
- **Solution**: Check IcsManager configuration and initialization
|
||||
|
||||
#### Stale Data
|
||||
- **Symptom**: Changes not reflected in views
|
||||
- **Cause**: Cache not invalidated properly
|
||||
- **Solution**: Clear cache or trigger manual rebuild
|
||||
|
||||
### Debug Commands
|
||||
```javascript
|
||||
// In console
|
||||
app.plugins.plugins['task-genius'].dataflowOrchestrator.getStats()
|
||||
app.plugins.plugins['task-genius'].dataflowOrchestrator.rebuild()
|
||||
```
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Adding New Data Sources
|
||||
1. Create source in `src/dataflow/sources/`
|
||||
2. Implement event emission pattern
|
||||
3. Add event type to `Events.ts`
|
||||
4. Update Orchestrator to subscribe
|
||||
5. Extend Repository if needed
|
||||
6. Update Storage for persistence
|
||||
|
||||
### Adding New Features
|
||||
1. Implement in dataflow architecture first
|
||||
2. Add conditional logic for backward compatibility
|
||||
3. Include sourceSeq in any TASK_CACHE_UPDATED events
|
||||
4. Test both dataflow and legacy modes
|
||||
5. Update this documentation
|
||||
|
||||
### Best Practices
|
||||
- Always use QueryAPI for data access
|
||||
- Never bypass Repository for updates
|
||||
- Include proper event metadata
|
||||
- Handle errors gracefully
|
||||
- Log with component prefix: `[ComponentName]`
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned
|
||||
1. **Write Operations**: Extend dataflow for task creation/updates
|
||||
2. **Advanced Querying**: GraphQL-like query capabilities
|
||||
3. **Real-time Sync**: Multi-device synchronization
|
||||
4. **Plugin API**: External plugin support
|
||||
|
||||
## Rollback Procedure
|
||||
### Under Consideration
|
||||
- WebSocket support for real-time collaboration
|
||||
- Database backend option for large vaults
|
||||
- Incremental parsing for huge files
|
||||
- Custom data source plugins
|
||||
|
||||
If issues arise, dataflow can be disabled:
|
||||
## Architecture Decisions
|
||||
|
||||
1. Settings → Advanced → Experimental Features
|
||||
2. Toggle "Enable Dataflow Architecture" to OFF
|
||||
3. Restart Obsidian
|
||||
### Why Separate ICS Storage?
|
||||
- **Independence**: ICS events have different lifecycle than file tasks
|
||||
- **Performance**: Avoid re-parsing files when only calendar changes
|
||||
- **Flexibility**: Easy to add/remove external sources
|
||||
- **Clarity**: Clear separation of concerns
|
||||
|
||||
The legacy TaskManager system remains available for one version cycle.
|
||||
### Why Source Sequences?
|
||||
- **Simplicity**: Single number comparison prevents loops
|
||||
- **Performance**: Minimal overhead
|
||||
- **Debugging**: Easy to trace event origins
|
||||
- **Compatibility**: Works with existing event system
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Adding New Features
|
||||
1. Implement in dataflow architecture first
|
||||
2. Add conditional logic for backward compatibility
|
||||
3. Test both modes thoroughly
|
||||
|
||||
### Debugging
|
||||
- Enable debug logging: `console.log("[Dataflow]", ...)`
|
||||
- Check Events for event flow
|
||||
- Verify Repository state in console
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Test with dataflow enabled
|
||||
DATAFLOW_ENABLED=true npm test
|
||||
|
||||
# Test with dataflow disabled
|
||||
DATAFLOW_ENABLED=false npm test
|
||||
```
|
||||
### Why Keep Legacy Support?
|
||||
- **Safety**: Fallback for critical issues
|
||||
- **Migration**: Gradual transition for large vaults
|
||||
- **Testing**: A/B comparison capability
|
||||
- **Confidence**: Users can always revert
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Dataflow architecture provides a solid foundation for future enhancements while maintaining backward compatibility. Its modular design enables easier testing, better performance, and cleaner code organization.
|
||||
The Dataflow architecture has evolved from a file-centric system to a unified data pipeline supporting multiple sources. Its event-driven, modular design enables:
|
||||
- Clean integration of new data sources
|
||||
- Robust loop prevention
|
||||
- Excellent performance characteristics
|
||||
- Maintainable and testable codebase
|
||||
|
||||
The architecture is production-ready and serves as the foundation for future task management enhancements.
|
||||
|
|
@ -53,6 +53,7 @@ export class ContentComponent extends Component {
|
|||
private selectedProjectForView: string | null = null; // Keep track if a specific project is filtered (for project view)
|
||||
private focusFilter: string | null = null; // Keep focus filter if needed
|
||||
private isTreeView: boolean = false;
|
||||
private isRendering: boolean = false; // Guard against concurrent renders
|
||||
|
||||
constructor(
|
||||
private parentEl: HTMLElement,
|
||||
|
|
@ -176,6 +177,27 @@ export class ContentComponent extends Component {
|
|||
}
|
||||
|
||||
public setTasks(tasks: Task[], notFilteredTasks: Task[]) {
|
||||
// Prevent unnecessary refreshes if data hasn't actually changed
|
||||
if (this.allTasks.length === tasks.length &&
|
||||
this.notFilteredTasks.length === notFilteredTasks.length &&
|
||||
tasks.length > 0) {
|
||||
// Quick check - if same length and not empty, check if first few tasks are identical
|
||||
const sampleSize = Math.min(3, tasks.length);
|
||||
let unchanged = true;
|
||||
for (let i = 0; i < sampleSize; i++) {
|
||||
if (this.allTasks[i]?.id !== tasks[i]?.id ||
|
||||
this.allTasks[i]?.content !== tasks[i]?.content ||
|
||||
this.allTasks[i]?.status !== tasks[i]?.status) {
|
||||
unchanged = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (unchanged) {
|
||||
console.log("ContentComponent: Tasks unchanged, skipping refresh");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.allTasks = tasks;
|
||||
this.notFilteredTasks = notFilteredTasks;
|
||||
this.applyFilters();
|
||||
|
|
@ -266,31 +288,46 @@ export class ContentComponent extends Component {
|
|||
}
|
||||
|
||||
private refreshTaskList() {
|
||||
this.cleanupComponents(); // Clear previous state and components
|
||||
|
||||
// Reset indices for lazy loading
|
||||
this.nextTaskIndex = 0;
|
||||
this.nextRootTaskIndex = 0;
|
||||
this.rootTasks = [];
|
||||
|
||||
if (this.filteredTasks.length === 0) {
|
||||
this.addEmptyState(t("No tasks found."));
|
||||
// Prevent concurrent renders
|
||||
if (this.isRendering) {
|
||||
console.log("ContentComponent: Already rendering, skipping refresh");
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRendering = true;
|
||||
|
||||
try {
|
||||
this.cleanupComponents(); // Clear previous state and components
|
||||
|
||||
// Render based on view mode
|
||||
if (this.isTreeView) {
|
||||
const taskMap = new Map<string, Task>();
|
||||
// Add all non-filtered tasks to the taskMap
|
||||
this.notFilteredTasks.forEach((task) => taskMap.set(task.id, task));
|
||||
this.rootTasks = tasksToTree(this.filteredTasks); // Calculate root tasks
|
||||
this.loadRootTaskBatch(taskMap); // Load the first batch
|
||||
} else {
|
||||
this.loadTaskBatch(); // Load the first batch
|
||||
// Reset indices for lazy loading
|
||||
this.nextTaskIndex = 0;
|
||||
this.nextRootTaskIndex = 0;
|
||||
this.rootTasks = [];
|
||||
|
||||
if (this.filteredTasks.length === 0) {
|
||||
this.addEmptyState(t("No tasks found."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Render based on view mode
|
||||
if (this.isTreeView) {
|
||||
const taskMap = new Map<string, Task>();
|
||||
// Add all non-filtered tasks to the taskMap
|
||||
this.notFilteredTasks.forEach((task) => taskMap.set(task.id, task));
|
||||
this.rootTasks = tasksToTree(this.filteredTasks); // Calculate root tasks
|
||||
this.loadRootTaskBatch(taskMap); // Load the first batch
|
||||
} else {
|
||||
this.loadTaskBatch(); // Load the first batch
|
||||
}
|
||||
|
||||
// Add load marker if necessary
|
||||
this.checkAndAddLoadMarker();
|
||||
} finally {
|
||||
// Reset rendering flag after completion
|
||||
setTimeout(() => {
|
||||
this.isRendering = false;
|
||||
}, 50); // Small delay to prevent immediate re-entry
|
||||
}
|
||||
|
||||
// Add load marker if necessary
|
||||
this.checkAndAddLoadMarker();
|
||||
}
|
||||
|
||||
private loadTaskBatch(): number {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Storage } from "./persistence/Storage";
|
|||
import { Events, emit, Seq, on } from "./events/Events";
|
||||
import { WorkerOrchestrator } from "./workers/WorkerOrchestrator";
|
||||
import { ObsidianSource } from "./sources/ObsidianSource";
|
||||
import { IcsSource } from "./sources/IcsSource";
|
||||
import { TaskWorkerManager } from "./workers/TaskWorkerManager";
|
||||
import { ProjectDataWorkerManager } from "./workers/ProjectDataWorkerManager";
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ export class DataflowOrchestrator {
|
|||
private storage: Storage;
|
||||
private workerOrchestrator: WorkerOrchestrator;
|
||||
private obsidianSource: ObsidianSource;
|
||||
private icsSource: IcsSource;
|
||||
|
||||
// Event references for cleanup
|
||||
private eventRefs: EventRef[] = [];
|
||||
|
|
@ -38,6 +40,9 @@ export class DataflowOrchestrator {
|
|||
// Processing queue for throttling
|
||||
private processingQueue = new Map<string, NodeJS.Timeout>();
|
||||
private readonly DEBOUNCE_DELAY = 300; // ms
|
||||
|
||||
// Track last processed sequence to avoid infinite loops
|
||||
private lastProcessedSeq: number = 0;
|
||||
|
||||
constructor(
|
||||
private app: App,
|
||||
|
|
@ -64,6 +69,9 @@ export class DataflowOrchestrator {
|
|||
|
||||
// Initialize Obsidian event source
|
||||
this.obsidianSource = new ObsidianSource(app, vault, metadataCache);
|
||||
|
||||
// Initialize ICS event source
|
||||
this.icsSource = new IcsSource(app, () => this.plugin.getIcsManager());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -101,7 +109,10 @@ export class DataflowOrchestrator {
|
|||
// Initialize ObsidianSource to start listening for events
|
||||
this.obsidianSource.initialize();
|
||||
|
||||
// Subscribe to file update events from ObsidianSource
|
||||
// Initialize IcsSource to start listening for calendar events
|
||||
this.icsSource.initialize();
|
||||
|
||||
// Subscribe to file update events from ObsidianSource and ICS events
|
||||
this.subscribeToEvents();
|
||||
|
||||
// Emit initial ready event
|
||||
|
|
@ -113,9 +124,22 @@ export class DataflowOrchestrator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Subscribe to events from ObsidianSource and WriteAPI
|
||||
* Subscribe to events from ObsidianSource, IcsSource and WriteAPI
|
||||
*/
|
||||
private subscribeToEvents(): void {
|
||||
// Listen for ICS events updates
|
||||
this.eventRefs.push(
|
||||
on(this.app, Events.ICS_EVENTS_UPDATED, async (payload: any) => {
|
||||
const { events, seq } = payload;
|
||||
console.log(`[DataflowOrchestrator] ICS_EVENTS_UPDATED: ${events?.length || 0} events`);
|
||||
|
||||
// Update repository with ICS events
|
||||
if (events) {
|
||||
await this.repository.updateIcsEvents(events, seq);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Listen for file updates from ObsidianSource
|
||||
this.eventRefs.push(
|
||||
on(this.app, Events.FILE_UPDATED, async (payload: any) => {
|
||||
|
|
@ -135,10 +159,24 @@ export class DataflowOrchestrator {
|
|||
})
|
||||
);
|
||||
|
||||
// Listen for batch updates
|
||||
// Listen for batch updates from Repository only
|
||||
// ObsidianSource uses FILE_UPDATED events instead
|
||||
this.eventRefs.push(
|
||||
on(this.app, Events.TASK_CACHE_UPDATED, async (payload: any) => {
|
||||
const { changedFiles } = payload;
|
||||
const { changedFiles, sourceSeq } = payload;
|
||||
|
||||
// Skip if this is our own event (avoid infinite loop)
|
||||
// Check sourceSeq to identify origin from our own processing
|
||||
if (sourceSeq && sourceSeq === this.lastProcessedSeq) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if no sourceSeq (likely from ObsidianSource - deprecated path)
|
||||
if (!sourceSeq) {
|
||||
console.log(`[DataflowOrchestrator] Ignoring TASK_CACHE_UPDATED without sourceSeq`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (changedFiles && Array.isArray(changedFiles)) {
|
||||
console.log(`[DataflowOrchestrator] Batch update for ${changedFiles.length} files`);
|
||||
|
||||
|
|
@ -205,6 +243,7 @@ export class DataflowOrchestrator {
|
|||
|
||||
let rawTasks: Task[];
|
||||
let projectData: Awaited<ReturnType<typeof this.projectResolver.get>>;
|
||||
let needsParsing = false;
|
||||
|
||||
if (rawCached && this.storage.isRawValid(filePath, rawCached, fileContent, mtime)) {
|
||||
// Use cached raw tasks - file hasn't changed
|
||||
|
|
@ -215,6 +254,7 @@ export class DataflowOrchestrator {
|
|||
} else {
|
||||
// Parse the file
|
||||
console.log(`[DataflowOrchestrator] Parsing ${filePath} (cache miss or mtime mismatch)`);
|
||||
needsParsing = true;
|
||||
|
||||
// Get project data first for parsing
|
||||
projectData = await this.projectResolver.get(filePath);
|
||||
|
|
@ -245,7 +285,11 @@ export class DataflowOrchestrator {
|
|||
const augmentedTasks = await this.augmentor.merge(augmentContext);
|
||||
|
||||
// Step 4: Update repository (index + storage + events)
|
||||
await this.repository.updateFile(filePath, augmentedTasks);
|
||||
// Generate a unique sequence for this operation
|
||||
this.lastProcessedSeq = Seq.next();
|
||||
|
||||
// Pass our sequence to repository to track event origin
|
||||
await this.repository.updateFile(filePath, augmentedTasks, this.lastProcessedSeq);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error processing file ${filePath}:`, error);
|
||||
|
|
@ -376,6 +420,7 @@ export class DataflowOrchestrator {
|
|||
};
|
||||
const augmentedTasks = await this.augmentor.merge(augmentContext);
|
||||
|
||||
// Always update for newly parsed files
|
||||
updates.set(filePath, augmentedTasks);
|
||||
} catch (error) {
|
||||
console.error(`Error processing parsed result for ${filePath}:`, error);
|
||||
|
|
@ -400,7 +445,11 @@ export class DataflowOrchestrator {
|
|||
|
||||
// Update repository in batch
|
||||
if (updates.size > 0) {
|
||||
await this.repository.updateBatch(updates);
|
||||
// Generate a unique sequence for this batch operation
|
||||
this.lastProcessedSeq = Seq.next();
|
||||
|
||||
// Pass our sequence to repository to track event origin
|
||||
await this.repository.updateBatch(updates, this.lastProcessedSeq);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -427,9 +476,6 @@ export class DataflowOrchestrator {
|
|||
const fileContent = await this.vault.cachedRead(file);
|
||||
|
||||
if (rawCached && this.storage.isRawValid(filePath, rawCached, fileContent, mtime)) {
|
||||
// Skip this file - it hasn't changed
|
||||
skippedCount++;
|
||||
|
||||
// Use cached raw tasks for augmentation
|
||||
const rawTasks = rawCached.data;
|
||||
|
||||
|
|
@ -450,7 +496,9 @@ export class DataflowOrchestrator {
|
|||
};
|
||||
const augmentedTasks = await this.augmentor.merge(augmentContext);
|
||||
|
||||
// Always add to updates - Repository will handle change detection
|
||||
updates.set(filePath, augmentedTasks);
|
||||
skippedCount++; // Count as skipped since we used cache
|
||||
} else {
|
||||
// Parse file as it has changed or is new
|
||||
// Get project data first for parsing
|
||||
|
|
@ -644,6 +692,9 @@ export class DataflowOrchestrator {
|
|||
// Cleanup ObsidianSource
|
||||
this.obsidianSource.destroy();
|
||||
|
||||
// Cleanup IcsSource
|
||||
this.icsSource.destroy();
|
||||
|
||||
// Cleanup WorkerOrchestrator
|
||||
this.workerOrchestrator.destroy();
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const Events = {
|
|||
TASK_DELETED: "task-genius:task-deleted",
|
||||
WRITE_OPERATION_START: "task-genius:write-operation-start",
|
||||
WRITE_OPERATION_COMPLETE: "task-genius:write-operation-complete",
|
||||
ICS_EVENTS_UPDATED: "task-genius:ics-events-updated",
|
||||
} as const;
|
||||
|
||||
export type SeqClock = { next(): number };
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export class Repository {
|
|||
private indexer: TaskIndexer;
|
||||
private storage: Storage;
|
||||
private lastSequence: number = 0;
|
||||
private sourceSeq: number = 0; // Track source sequence to differentiate events
|
||||
private icsEvents: Task[] = []; // Store ICS events separately
|
||||
|
||||
constructor(
|
||||
private app: App,
|
||||
|
|
@ -48,61 +50,94 @@ export class Repository {
|
|||
} else {
|
||||
console.log("[Repository] No persisted data found, starting with empty index");
|
||||
}
|
||||
|
||||
// Load ICS events from storage
|
||||
this.icsEvents = await this.storage.loadIcsEvents();
|
||||
console.log(`[Repository] Loaded ${this.icsEvents.length} ICS events from storage`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tasks for a specific file
|
||||
* @param filePath - Path of the file
|
||||
* @param tasks - Tasks to update
|
||||
* @param sourceSeq - Optional source sequence to track event origin
|
||||
*/
|
||||
async updateFile(filePath: string, tasks: Task[]): Promise<void> {
|
||||
// Update the in-memory index
|
||||
async updateFile(filePath: string, tasks: Task[], sourceSeq?: number): Promise<void> {
|
||||
// Check if tasks have actually changed
|
||||
const existingAugmented = await this.storage.loadAugmented(filePath);
|
||||
const hasChanges = !existingAugmented ||
|
||||
JSON.stringify(tasks) !== JSON.stringify(existingAugmented.data);
|
||||
|
||||
// Always update the in-memory index for consistency
|
||||
await this.indexer.updateIndexWithTasks(filePath, tasks);
|
||||
|
||||
// Store augmented tasks to cache
|
||||
// Always store augmented tasks to cache
|
||||
await this.storage.storeAugmented(filePath, tasks);
|
||||
|
||||
// Emit update event
|
||||
this.lastSequence = Seq.next();
|
||||
emit(this.app, Events.TASK_CACHE_UPDATED, {
|
||||
changedFiles: [filePath],
|
||||
stats: {
|
||||
total: await this.indexer.getTotalTaskCount(),
|
||||
changed: tasks.length
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
seq: this.lastSequence
|
||||
});
|
||||
// Only emit update event if there are actual changes
|
||||
if (hasChanges) {
|
||||
this.lastSequence = Seq.next();
|
||||
emit(this.app, Events.TASK_CACHE_UPDATED, {
|
||||
changedFiles: [filePath],
|
||||
stats: {
|
||||
total: await this.indexer.getTotalTaskCount(),
|
||||
changed: tasks.length
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
seq: this.lastSequence,
|
||||
sourceSeq: sourceSeq || 0 // Include source sequence for loop detection
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tasks for multiple files in batch
|
||||
* @param updates - Map of file paths to tasks
|
||||
* @param sourceSeq - Optional source sequence to track event origin
|
||||
*/
|
||||
async updateBatch(updates: Map<string, Task[]>): Promise<void> {
|
||||
async updateBatch(updates: Map<string, Task[]>, sourceSeq?: number): Promise<void> {
|
||||
const changedFiles: string[] = [];
|
||||
let totalChanged = 0;
|
||||
let hasActualChanges = false;
|
||||
|
||||
// Process each file update
|
||||
// Process each file update and check for actual changes
|
||||
for (const [filePath, tasks] of updates) {
|
||||
// Check if tasks have actually changed
|
||||
const existingAugmented = await this.storage.loadAugmented(filePath);
|
||||
const hasChanges = !existingAugmented ||
|
||||
JSON.stringify(tasks) !== JSON.stringify(existingAugmented.data);
|
||||
|
||||
await this.indexer.updateIndexWithTasks(filePath, tasks);
|
||||
await this.storage.storeAugmented(filePath, tasks);
|
||||
changedFiles.push(filePath);
|
||||
totalChanged += tasks.length;
|
||||
|
||||
if (hasChanges) {
|
||||
changedFiles.push(filePath);
|
||||
totalChanged += tasks.length;
|
||||
hasActualChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the consolidated index after batch updates
|
||||
await this.persist();
|
||||
console.log(`[Repository] Persisted index after batch update of ${changedFiles.length} files`);
|
||||
// Only emit events and persist if there are actual changes
|
||||
if (hasActualChanges) {
|
||||
// Persist the consolidated index after batch updates
|
||||
await this.persist();
|
||||
console.log(`[Repository] Persisted index after batch update of ${changedFiles.length} files with changes`);
|
||||
|
||||
// Emit batch update event
|
||||
this.lastSequence = Seq.next();
|
||||
emit(this.app, Events.TASK_CACHE_UPDATED, {
|
||||
changedFiles,
|
||||
stats: {
|
||||
total: await this.indexer.getTotalTaskCount(),
|
||||
changed: totalChanged
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
seq: this.lastSequence
|
||||
});
|
||||
// Emit batch update event only for files that actually changed
|
||||
this.lastSequence = Seq.next();
|
||||
emit(this.app, Events.TASK_CACHE_UPDATED, {
|
||||
changedFiles,
|
||||
stats: {
|
||||
total: await this.indexer.getTotalTaskCount(),
|
||||
changed: totalChanged
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
seq: this.lastSequence,
|
||||
sourceSeq: sourceSeq || 0 // Include source sequence for loop detection
|
||||
});
|
||||
} else {
|
||||
console.log(`[Repository] Batch update completed with no actual changes - skipping event emission`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -128,10 +163,47 @@ export class Repository {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get all tasks from the index
|
||||
* Update ICS events in the repository
|
||||
*/
|
||||
async updateIcsEvents(events: Task[], sourceSeq?: number): Promise<void> {
|
||||
console.log(`[Repository] Updating ${events.length} ICS events`);
|
||||
|
||||
// Store the new ICS events
|
||||
this.icsEvents = events;
|
||||
|
||||
// Store ICS events to persistence
|
||||
await this.storage.storeIcsEvents(events);
|
||||
|
||||
// Emit update event to notify views
|
||||
this.lastSequence = Seq.next();
|
||||
emit(this.app, Events.TASK_CACHE_UPDATED, {
|
||||
changedFiles: ['ics:events'], // Special marker for ICS events
|
||||
stats: {
|
||||
total: await this.getTotalTaskCount(),
|
||||
changed: events.length,
|
||||
icsEvents: events.length
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
seq: this.lastSequence,
|
||||
sourceSeq: sourceSeq || 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total task count including ICS events
|
||||
*/
|
||||
async getTotalTaskCount(): Promise<number> {
|
||||
const fileTaskCount = await this.indexer.getTotalTaskCount();
|
||||
return fileTaskCount + this.icsEvents.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tasks from the index (including ICS events)
|
||||
*/
|
||||
async all(): Promise<Task[]> {
|
||||
return this.indexer.getAllTasks();
|
||||
const fileTasks = await this.indexer.getAllTasks();
|
||||
// Merge file-based tasks with ICS events
|
||||
return [...fileTasks, ...this.icsEvents];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -139,7 +211,14 @@ export class Repository {
|
|||
*/
|
||||
async byProject(project: string): Promise<Task[]> {
|
||||
const taskIds = await this.indexer.getTaskIdsByProject(project);
|
||||
return this.getTasksByIds(taskIds);
|
||||
const fileTasks = await this.getTasksByIds(taskIds);
|
||||
|
||||
// Also filter ICS events by project if they have one
|
||||
const icsProjectTasks = this.icsEvents.filter(task =>
|
||||
task.metadata?.project === project
|
||||
);
|
||||
|
||||
return [...fileTasks, ...icsProjectTasks];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export const Keys = {
|
|||
project: (path: string) => `project.data:${path}`,
|
||||
augmented: (path: string) => `tasks.augmented:${path}`,
|
||||
consolidated: () => `consolidated:taskIndex`,
|
||||
icsEvents: () => `ics:events`,
|
||||
meta: {
|
||||
version: () => `meta:version`,
|
||||
schemaVersion: () => `meta:schemaVersion`,
|
||||
|
|
@ -222,6 +223,44 @@ export class Storage {
|
|||
await this.cache.storeFile(Keys.augmented(path), record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store ICS events
|
||||
*/
|
||||
async storeIcsEvents(events: Task[]): Promise<void> {
|
||||
const record = {
|
||||
time: Date.now(),
|
||||
version: this.currentVersion,
|
||||
schema: this.schemaVersion,
|
||||
data: events,
|
||||
};
|
||||
|
||||
await this.cache.storeFile(Keys.icsEvents(), record);
|
||||
console.log(`[Storage] Stored ${events.length} ICS events`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load ICS events
|
||||
*/
|
||||
async loadIcsEvents(): Promise<Task[]> {
|
||||
try {
|
||||
const cached = await this.cache.loadFile<any>(Keys.icsEvents());
|
||||
if (!cached || !cached.data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check version compatibility
|
||||
if (!this.isVersionValid(cached.data)) {
|
||||
await this.cache.removeFile(Keys.icsEvents());
|
||||
return [];
|
||||
}
|
||||
|
||||
return cached.data.data || [];
|
||||
} catch (error) {
|
||||
console.error("[Storage] Error loading ICS events:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load consolidated task index
|
||||
*/
|
||||
|
|
|
|||
170
src/dataflow/sources/IcsSource.ts
Normal file
170
src/dataflow/sources/IcsSource.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* IcsSource - Event source for ICS calendar data
|
||||
*
|
||||
* This source integrates external calendar events into the dataflow architecture.
|
||||
* It listens to IcsManager updates and emits standardized dataflow events.
|
||||
*/
|
||||
|
||||
import { App, EventRef } from "obsidian";
|
||||
import { Events, emit, Seq } from "../events/Events";
|
||||
import type { IcsManager } from "../../managers/ics-manager";
|
||||
import type { Task } from "../../types/task";
|
||||
|
||||
export class IcsSource {
|
||||
private eventRefs: EventRef[] = [];
|
||||
private isInitialized = false;
|
||||
private lastIcsUpdateSeq = 0;
|
||||
|
||||
constructor(
|
||||
private app: App,
|
||||
private getIcsManager: () => IcsManager | undefined
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initialize the ICS source and start listening for calendar updates
|
||||
*/
|
||||
initialize(): void {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
console.log("[IcsSource] Initializing ICS event source...");
|
||||
|
||||
// Initial load of ICS events
|
||||
this.loadAndEmitIcsEvents();
|
||||
|
||||
// Subscribe to ICS manager updates
|
||||
this.subscribeToIcsUpdates();
|
||||
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to ICS manager update events
|
||||
*/
|
||||
private subscribeToIcsUpdates(): void {
|
||||
// Listen for ICS cache updates
|
||||
this.app.workspace.on("ics-cache-updated" as any, () => {
|
||||
console.log("[IcsSource] ICS cache updated, reloading events...");
|
||||
this.loadAndEmitIcsEvents();
|
||||
});
|
||||
|
||||
// Listen for ICS configuration changes
|
||||
this.app.workspace.on("ics-config-changed" as any, () => {
|
||||
console.log("[IcsSource] ICS config changed, reloading events...");
|
||||
this.loadAndEmitIcsEvents();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load ICS events from manager and emit update event
|
||||
*/
|
||||
private async loadAndEmitIcsEvents(): Promise<void> {
|
||||
const icsManager = this.getIcsManager();
|
||||
if (!icsManager) {
|
||||
console.log("[IcsSource] No ICS manager available");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all ICS events with sync
|
||||
const icsEvents = await icsManager.getAllEventsWithSync();
|
||||
|
||||
// Convert ICS events to Task format with proper source marking
|
||||
const icsTasks: Task[] = icsEvents.map((event: any) => ({
|
||||
...event,
|
||||
metadata: {
|
||||
...event.metadata,
|
||||
source: {
|
||||
type: 'ics',
|
||||
id: event.source?.id || 'unknown',
|
||||
name: event.source?.name || 'ICS Calendar'
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
console.log(`[IcsSource] Loaded ${icsTasks.length} ICS events`);
|
||||
|
||||
// Generate sequence for this update
|
||||
this.lastIcsUpdateSeq = Seq.next();
|
||||
|
||||
// Emit ICS events update
|
||||
emit(this.app, Events.ICS_EVENTS_UPDATED, {
|
||||
events: icsTasks,
|
||||
timestamp: Date.now(),
|
||||
seq: this.lastIcsUpdateSeq,
|
||||
stats: {
|
||||
total: icsTasks.length,
|
||||
sources: this.getSourceStats(icsTasks)
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("[IcsSource] Error loading ICS events:", error);
|
||||
|
||||
// Emit empty update on error to clear stale data
|
||||
emit(this.app, Events.ICS_EVENTS_UPDATED, {
|
||||
events: [],
|
||||
timestamp: Date.now(),
|
||||
seq: Seq.next(),
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about ICS sources
|
||||
*/
|
||||
private getSourceStats(events: Task[]): Record<string, number> {
|
||||
const stats: Record<string, number> = {};
|
||||
|
||||
for (const event of events) {
|
||||
const sourceId = event.metadata?.source?.id || 'unknown';
|
||||
stats[sourceId] = (stats[sourceId] || 0) + 1;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh ICS events manually
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
console.log("[IcsSource] Manual refresh triggered");
|
||||
await this.loadAndEmitIcsEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current statistics
|
||||
*/
|
||||
getStats(): {
|
||||
initialized: boolean;
|
||||
lastUpdateSeq: number;
|
||||
} {
|
||||
return {
|
||||
initialized: this.isInitialized,
|
||||
lastUpdateSeq: this.lastIcsUpdateSeq
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup and destroy the source
|
||||
*/
|
||||
destroy(): void {
|
||||
console.log("[IcsSource] Destroying ICS source...");
|
||||
|
||||
// Clear event listeners
|
||||
for (const ref of this.eventRefs) {
|
||||
this.app.vault.offref(ref);
|
||||
}
|
||||
this.eventRefs = [];
|
||||
|
||||
// Emit clear event
|
||||
emit(this.app, Events.ICS_EVENTS_UPDATED, {
|
||||
events: [],
|
||||
timestamp: Date.now(),
|
||||
seq: Seq.next(),
|
||||
destroyed: true
|
||||
});
|
||||
|
||||
this.isInitialized = false;
|
||||
}
|
||||
}
|
||||
52
src/index.ts
52
src/index.ts
|
|
@ -719,7 +719,36 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
callback: async () => {
|
||||
try {
|
||||
new Notice(t("Refreshing task index..."));
|
||||
await this.taskManager.initialize();
|
||||
|
||||
// Check if dataflow is enabled
|
||||
if (this.settings?.experimental?.dataflowEnabled && this.dataflowOrchestrator) {
|
||||
// Use dataflow orchestrator for refresh
|
||||
console.log("[Command] Refreshing task index via dataflow");
|
||||
|
||||
// Re-scan all files to refresh the index
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
const canvasFiles = this.app.vault.getFiles().filter(f => f.extension === "canvas");
|
||||
const allFiles = [...files, ...canvasFiles];
|
||||
|
||||
// Process files in batches
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < allFiles.length; i += batchSize) {
|
||||
const batch = allFiles.slice(i, i + batchSize);
|
||||
await Promise.all(batch.map(file =>
|
||||
(this.dataflowOrchestrator as any).processFileImmediate(file)
|
||||
));
|
||||
}
|
||||
|
||||
// Refresh ICS events if available
|
||||
const icsSource = (this.dataflowOrchestrator as any).icsSource;
|
||||
if (icsSource) {
|
||||
await icsSource.refresh();
|
||||
}
|
||||
} else {
|
||||
// Use legacy task manager
|
||||
await this.taskManager.initialize();
|
||||
}
|
||||
|
||||
new Notice(t("Task index refreshed"));
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh task index:", error);
|
||||
|
|
@ -734,7 +763,26 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
name: t("Force reindex all tasks"),
|
||||
callback: async () => {
|
||||
try {
|
||||
await this.taskManager.forceReindex();
|
||||
// Check if dataflow is enabled
|
||||
if (this.settings?.experimental?.dataflowEnabled && this.dataflowOrchestrator) {
|
||||
// Use dataflow orchestrator for force reindex
|
||||
console.log("[Command] Force reindexing via dataflow");
|
||||
new Notice(t("Clearing task cache and rebuilding index..."));
|
||||
|
||||
// Clear all caches and rebuild from scratch
|
||||
await this.dataflowOrchestrator.rebuild();
|
||||
|
||||
// Refresh ICS events after rebuild
|
||||
const icsSource = (this.dataflowOrchestrator as any).icsSource;
|
||||
if (icsSource) {
|
||||
await icsSource.refresh();
|
||||
}
|
||||
|
||||
new Notice(t("Task index completely rebuilt"));
|
||||
} else {
|
||||
// Use legacy task manager
|
||||
await this.taskManager.forceReindex();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to force reindex tasks:", error);
|
||||
new Notice(t("Failed to force reindex tasks"));
|
||||
|
|
|
|||
|
|
@ -142,6 +142,14 @@ export class TaskView extends ItemView {
|
|||
cls: "task-genius-container",
|
||||
});
|
||||
|
||||
// Add debounced view update to prevent rapid successive refreshes
|
||||
const debouncedViewUpdate = debounce(async () => {
|
||||
const skipViewUpdate = this.detailsComponent?.isCurrentlyEditing() || false;
|
||||
if (!skipViewUpdate) {
|
||||
await this.loadTasks(false, false);
|
||||
}
|
||||
}, 150); // 150ms debounce delay
|
||||
|
||||
// 1. 首先注册事件监听器,确保不会错过任何更新
|
||||
if (isDataflowEnabled(this.plugin) && this.plugin.dataflowOrchestrator) {
|
||||
// Dataflow: 订阅统一事件
|
||||
|
|
@ -153,23 +161,14 @@ export class TaskView extends ItemView {
|
|||
})
|
||||
);
|
||||
this.registerEvent(
|
||||
on(this.app, Events.TASK_CACHE_UPDATED, async () => {
|
||||
// 任务缓存更新,增量刷新
|
||||
const skipViewUpdate = this.detailsComponent?.isCurrentlyEditing() || false;
|
||||
await this.loadTasks(false, skipViewUpdate);
|
||||
})
|
||||
on(this.app, Events.TASK_CACHE_UPDATED, debouncedViewUpdate)
|
||||
);
|
||||
} else {
|
||||
// Legacy: 兼容旧事件
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"task-genius:task-cache-updated",
|
||||
async () => {
|
||||
// Only skip view update if currently editing content in details panel
|
||||
// Always update view for status changes from external sources (e.g., editor)
|
||||
const skipViewUpdate = this.detailsComponent?.isCurrentlyEditing() || false;
|
||||
await this.loadTasks(false, skipViewUpdate);
|
||||
}
|
||||
debouncedViewUpdate
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -243,13 +242,17 @@ export class TaskView extends ItemView {
|
|||
this.sidebarComponent.setViewMode(this.currentViewId);
|
||||
|
||||
// 5. 快速加载缓存数据以立即显示 UI
|
||||
await this.loadTasksFast(true); // 跳过视图更新,避免双重渲染
|
||||
await this.loadTasksFast(false); // Don't skip view update - we need the initial render
|
||||
|
||||
// 6. 使用快速加载的数据显示视图
|
||||
this.switchView(this.currentViewId);
|
||||
|
||||
// 7. 后台同步最新数据(非阻塞)
|
||||
this.loadTasksWithSyncInBackground();
|
||||
// 6. Only switch view if we have tasks to display
|
||||
if (this.tasks.length > 0) {
|
||||
this.switchView(this.currentViewId);
|
||||
} else {
|
||||
// If no tasks loaded yet, wait for background sync before rendering
|
||||
console.log("No cached tasks found, waiting for background sync...");
|
||||
await this.loadTasksWithSyncInBackground();
|
||||
this.switchView(this.currentViewId);
|
||||
}
|
||||
|
||||
console.log("currentFilterState", this.currentFilterState);
|
||||
// 7. 在组件初始化完成后应用筛选器状态
|
||||
|
|
@ -1268,27 +1271,46 @@ export class TaskView extends ItemView {
|
|||
/**
|
||||
* Load tasks with sync in background - non-blocking
|
||||
*/
|
||||
private loadTasksWithSyncInBackground() {
|
||||
private async loadTasksWithSyncInBackground() {
|
||||
// When dataflow is enabled, ICS events are handled through dataflow architecture
|
||||
// Just do a single sync load and return
|
||||
if (isDataflowEnabled(this.plugin)) {
|
||||
try {
|
||||
const queryAPI = this.plugin.dataflowOrchestrator?.getQueryAPI();
|
||||
if (!queryAPI) {
|
||||
console.warn("[TaskView] QueryAPI not available");
|
||||
return;
|
||||
}
|
||||
const tasks = await queryAPI.getAllTasks();
|
||||
if (tasks.length !== this.tasks.length || tasks.length === 0) {
|
||||
this.tasks = tasks;
|
||||
console.log(`TaskView updated with ${this.tasks.length} tasks (dataflow sync)`);
|
||||
return; // Don't trigger view update here as it will be handled by events
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Dataflow background sync failed:", error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const taskManager = this.plugin.taskManager;
|
||||
if (!taskManager) return;
|
||||
|
||||
// Start background sync without blocking UI
|
||||
taskManager
|
||||
.getAllTasksWithSync()
|
||||
.then((tasks) => {
|
||||
// Only update if we got different data
|
||||
if (tasks.length !== this.tasks.length) {
|
||||
this.tasks = tasks;
|
||||
console.log(
|
||||
`TaskView updated with ${this.tasks.length} tasks (background sync)`
|
||||
);
|
||||
// Update the view with new data
|
||||
this.triggerViewUpdate();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("Background task sync failed:", error);
|
||||
});
|
||||
// Start background sync without blocking UI (for legacy mode)
|
||||
try {
|
||||
const tasks = await taskManager.getAllTasksWithSync();
|
||||
// Only update if we got different data
|
||||
if (tasks.length !== this.tasks.length) {
|
||||
this.tasks = tasks;
|
||||
console.log(
|
||||
`TaskView updated with ${this.tasks.length} tasks (background sync)`
|
||||
);
|
||||
// Update the view with new data
|
||||
this.triggerViewUpdate();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Background task sync failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
public async triggerViewUpdate() {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--background-modifier-border)
|
||||
border-right: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.is-phone .projects-left-column {
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
.projects-sidebar-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 var(--size-4-2);
|
||||
padding: var(--size-4-2) var(--size-4-2);
|
||||
}
|
||||
|
||||
.project-list-item {
|
||||
|
|
@ -92,6 +92,7 @@
|
|||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-s);
|
||||
padding: var(--size-4-2);
|
||||
}
|
||||
|
||||
.project-list-item:hover {
|
||||
|
|
|
|||
173
styles.css
173
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue