fix(ics): restore workspace event listeners for ICS updates

- Add ICS-specific event types to Obsidian workspace interface
- Restore ics-config-changed and ics-cache-updated event listeners
- Maintain backward compatibility with existing ICS update mechanism
- Fix formatting and indentation in IcsSource

This ensures ICS events are properly synchronized when configuration
or cache updates occur, maintaining real-time calendar integration.
This commit is contained in:
Quorafind 2025-08-21 21:22:10 +08:00
parent d3a850b029
commit 316518dfc4
2 changed files with 162 additions and 151 deletions

View file

@ -11,168 +11,167 @@ 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;
private eventRefs: EventRef[] = [];
private isInitialized = false;
private lastIcsUpdateSeq = 0;
constructor(
private app: App,
private getIcsManager: () => IcsManager | undefined
) {}
constructor(
private app: App,
private getIcsManager: () => IcsManager | undefined,
) {}
/**
* Initialize the ICS source and start listening for calendar updates
*/
initialize(): void {
if (this.isInitialized) return;
/**
* Initialize the ICS source and start listening for calendar updates
*/
initialize(): void {
if (this.isInitialized) return;
console.log("[IcsSource] Initializing ICS event source...");
console.log("[IcsSource] Initializing ICS event source...");
// Subscribe to ICS manager updates first so we don't miss early signals
this.subscribeToIcsUpdates();
// Subscribe to ICS manager updates first so we don't miss early signals
this.subscribeToIcsUpdates();
// Initial load of ICS events (may be no-op if manager not ready yet)
this.loadAndEmitIcsEvents();
// Initial load of ICS events (may be no-op if manager not ready yet)
this.loadAndEmitIcsEvents();
// Fallback: retry until ICS manager becomes available (up to ~30s)
this.ensureManagerAndLoad(0);
// Fallback: retry until ICS manager becomes available (up to ~30s)
this.ensureManagerAndLoad(0);
this.isInitialized = true;
}
/**
* Ensure ICS manager becomes available shortly after startup and then load
*/
private ensureManagerAndLoad(attempt: number): void {
const maxAttempts = 30; // ~30s with 1s interval
if (this.getIcsManager()) {
this.loadAndEmitIcsEvents();
return;
}
if (attempt >= maxAttempts) {
console.warn("[IcsSource] ICS manager not available after retries");
return;
}
setTimeout(() => this.ensureManagerAndLoad(attempt + 1), 1000);
}
this.isInitialized = true;
}
/**
* Ensure ICS manager becomes available shortly after startup and then load
*/
private ensureManagerAndLoad(attempt: number): void {
const maxAttempts = 30; // ~30s with 1s interval
if (this.getIcsManager()) {
this.loadAndEmitIcsEvents();
return;
}
if (attempt >= maxAttempts) {
console.warn("[IcsSource] ICS manager not available after retries");
return;
}
setTimeout(() => this.ensureManagerAndLoad(attempt + 1), 1000);
}
/**
* 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();
});
/**
* 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();
});
}
// Listen for ICS configuration changes
// this.app.workspace.on("task-genius:ics-config-changed", () => {
// 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;
}
/**
* 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();
try {
// Get all ICS events with sync
const icsEvents = await icsManager.getAllEventsWithSync();
// Convert ICS events to IcsTask format via manager to ensure proper shape
const icsTasks: Task[] = icsManager.convertEventsToTasks(icsEvents);
// Convert ICS events to IcsTask format via manager to ensure proper shape
const icsTasks: Task[] = icsManager.convertEventsToTasks(icsEvents);
console.log(`[IcsSource] Loaded ${icsTasks.length} ICS events`);
console.log(`[IcsSource] Loaded ${icsTasks.length} ICS events`);
// Generate sequence for this update
this.lastIcsUpdateSeq = Seq.next();
// 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)
}
});
// 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);
} 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,
});
}
}
// 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> = {};
/**
* 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;
}
for (const event of events) {
const sourceId = event.metadata?.source?.id || 'unknown';
stats[sourceId] = (stats[sourceId] || 0) + 1;
}
return stats;
}
return stats;
}
/**
* Refresh ICS events manually
*/
async refresh(): Promise<void> {
console.log("[IcsSource] Manual refresh triggered");
await this.loadAndEmitIcsEvents();
}
/**
* 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,
};
}
/**
* 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...");
/**
* 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 = [];
// 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,
});
// Emit clear event
emit(this.app, Events.ICS_EVENTS_UPDATED, {
events: [],
timestamp: Date.now(),
seq: Seq.next(),
destroyed: true
});
this.isInitialized = false;
}
}
this.isInitialized = false;
}
}

View file

@ -368,39 +368,49 @@ declare module "obsidian" {
*/
registerBasesView(
viewId: string,
config: BasesViewRegistration
config: BasesViewRegistration,
): boolean;
}
interface Workspace {
on(
event: "task-genius:task-added",
callback: (task: Task) => void
callback: (task: Task) => void,
): EventRef;
on(
event: "task-genius:task-updated",
callback: (task: Task) => void
callback: (task: Task) => void,
): EventRef;
on(
event: "task-genius:task-deleted",
callback: (taskId: string) => void
callback: (taskId: string) => void,
): EventRef;
on(
event: "task-genius:task-cache-updated",
callback: (cache: TaskCache) => void
callback: (cache: TaskCache) => void,
): EventRef;
on(
event: "task-genius:ics-config-changed",
callback: () => void,
): EventRef;
on(
event: "task-genius:ics-cache-updated",
callback: () => void,
): EventRef;
on(
event: "task-genius:task-completed",
callback: (task: Task) => void
callback: (task: Task) => void,
): EventRef;
on(
event: "task-genius:habit-index-updated",
callback: (habits: HabitProps[]) => void
callback: (habits: HabitProps[]) => void,
): EventRef;
on(
event: "task-genius:filter-changed",
callback: (filterState: RootFilterState, leafId?: string) => void
callback: (filterState: RootFilterState, leafId?: string) => void,
): EventRef;
trigger(event: "task-genius:task-completed", task: Task): void;
@ -409,16 +419,18 @@ declare module "obsidian" {
trigger(event: "task-genius:task-deleted", taskId: string): void;
trigger(
event: "task-genius:task-cache-updated",
cache: TaskCache
cache: TaskCache,
): void;
trigger(event: "task-genius:ics-config-changed"): void;
trigger(event: "task-genius:ics-cache-updated"): void;
trigger(
event: "task-genius:habit-index-updated",
habits: HabitProps[]
habits: HabitProps[],
): void;
trigger(
event: "task-genius:filter-changed",
filterState: RootFilterState,
leafId?: string
leafId?: string,
): void;
}