TidGi-Desktop/src/services/wiki/plugin/watchFileSystemAdaptor/WatchFileSystemAdaptor.ts
lin onetwo fe66b9ecb9 Improve logging and cleanup in file system watcher and git ops
Added detailed logging to WatchFileSystemAdaptor and FileSystemWatcher for better traceability during initialization and test stabilization. Introduced a constant for the temporary git index prefix in gitOperations. Removed the unused comparison.ts utility for tiddler comparison. Enhanced comments and logging for AI commit message generation context.
2026-01-05 19:06:50 +08:00

209 lines
7.4 KiB
TypeScript

import { workspace } from '@services/wiki/wikiWorker/services';
import { isWikiWorkspace, IWikiWorkspace } from '@services/workspaces/interface';
import type { IFileInfo, Syncer, Tiddler, Wiki } from 'tiddlywiki';
import { FileSystemAdaptor } from './FileSystemAdaptor';
import { FileSystemWatcher, type IUpdatedTiddlers } from './FileSystemWatcher';
/**
* Enhanced filesystem adaptor that extends FileSystemAdaptor with file watching capabilities.
*
* Architecture (after refactoring):
* - FileSystemWatcher: Monitors file system changes, collects updates to updatedTiddlers list
* - WatchFileSystemAdaptor: Coordinates between watcher and syncer, implements syncadaptor interface
*
* Key design decisions:
* 1. File changes are collected by FileSystemWatcher and processed by syncer
* 2. syncer calls getUpdatedTiddlers() to get the list of changes
* 3. syncer calls loadTiddler() to load each modified tiddler
* 4. This eliminates direct wiki.addTiddler() calls, preventing echo loops
*
* Echo prevention:
* - When we save/delete, we temporarily exclude the file from watching
* - This prevents our own operations from being detected as external changes
* - After operation completes, we re-include the file (with delay to handle nsfw debounce)
*/
export class WatchFileSystemAdaptor extends FileSystemAdaptor {
name = 'watch-filesystem';
supportsLazyLoading = true;
private watcher: FileSystemWatcher | undefined;
private workspace: IWikiWorkspace | undefined;
constructor(options: { boot?: typeof $tw.boot; wiki: Wiki }) {
super(options);
this.logger = new $tw.utils.Logger('watch-filesystem', { colour: 'purple' });
// Initialize asynchronously
void this.initializeAsync();
}
private async initializeAsync(): Promise<void> {
this.logger.log('WatchFileSystemAdaptor initializeAsync starting');
try {
const workspaceId = this.workspaceID;
this.logger.log(`WatchFileSystemAdaptor loading workspace config for ${workspaceId}`);
if (workspaceId) {
const loadedWorkspaceData = await workspace.get(workspaceId);
if (!loadedWorkspaceData || typeof loadedWorkspaceData !== 'object' || !isWikiWorkspace(loadedWorkspaceData)) {
throw new Error('Invalid workspace data');
}
this.workspace = loadedWorkspaceData;
this.logger.log(`WatchFileSystemAdaptor workspace config loaded, enableFileSystemWatch=${this.workspace.enableFileSystemWatch}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.log(`Failed to load workspace data: ${errorMessage}`);
}
this.logger.log('WatchFileSystemAdaptor creating FileSystemWatcher');
// Create and initialize the file watcher
this.watcher = new FileSystemWatcher({
wiki: this.wiki,
boot: this.boot,
logger: this.logger,
workspaceID: this.workspaceID,
workspaceConfig: this.workspace,
});
this.logger.log('WatchFileSystemAdaptor calling watcher.initialize()');
await this.watcher.initialize();
this.logger.log('WatchFileSystemAdaptor initialization complete');
}
/**
* Get updated tiddlers from the file watcher.
* Called by syncer's SyncFromServerTask.
*/
getUpdatedTiddlers(syncer: Syncer, callback: (error: Error | null, updates: IUpdatedTiddlers) => void): void {
if (!this.watcher) {
callback(null, { modifications: [], deletions: [] });
return;
}
this.watcher.getUpdatedTiddlers(syncer, callback);
}
/**
* Load a tiddler from the file system.
* Called by syncer's LoadTiddlerTask for lazy loading.
*/
override loadTiddler(
title: string,
callback: (error: Error | null | string, tiddlerFields?: Record<string, unknown> | null) => void,
): void {
if (!this.watcher) {
callback(null, null);
return;
}
this.watcher.loadTiddler(title, callback);
}
/**
* Save a tiddler to the filesystem (with file watching support)
*/
override async saveTiddler(
tiddler: Tiddler,
callback?: (error: Error | null | string, adaptorInfo?: IFileInfo | null, revision?: string) => void,
options?: { tiddlerInfo?: Record<string, unknown> },
): Promise<void> {
try {
const oldFileInfo = this.boot.files[tiddler.fields.title];
// Pre-calculate file path for new tiddlers and exclude it
let excludedNewFilePath: string | undefined;
if (!oldFileInfo) {
try {
const newFileInfo = this.getTiddlerFileInfo(tiddler);
if (newFileInfo?.filepath) {
this.watcher?.excludeFile(newFileInfo.filepath);
this.watcher?.excludeFile(`${newFileInfo.filepath}.meta`);
excludedNewFilePath = newFileInfo.filepath;
}
} catch (error) {
this.logger.alert(`WatchFileSystemAdaptor Failed to pre-calculate file path for new tiddler: ${tiddler.fields.title}`, error);
}
}
// Exclude old file path before save
if (oldFileInfo) {
this.watcher?.excludeFile(oldFileInfo.filepath);
this.watcher?.excludeFile(`${oldFileInfo.filepath}.meta`);
}
// Call parent's saveTiddler
await super.saveTiddler(tiddler, undefined, options);
// Update inverse index after successful save
const finalFileInfo = this.boot.files[tiddler.fields.title];
if (finalFileInfo && this.watcher) {
this.watcher.updateIndexAfterSave(tiddler.fields.title, finalFileInfo);
}
callback?.(null, finalFileInfo);
// Schedule re-inclusion after delay
if (finalFileInfo) {
this.watcher?.scheduleFileInclusion(finalFileInfo.filepath);
this.watcher?.scheduleFileInclusion(`${finalFileInfo.filepath}.meta`);
}
// Re-include wrongly pre-excluded path
if (excludedNewFilePath && excludedNewFilePath !== finalFileInfo?.filepath) {
this.watcher?.scheduleFileInclusion(excludedNewFilePath);
this.watcher?.scheduleFileInclusion(`${excludedNewFilePath}.meta`);
}
} catch (error) {
const errorObject = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
callback?.(errorObject);
throw errorObject;
}
}
/**
* Delete a tiddler from the filesystem (with file watching support)
*/
override async deleteTiddler(
title: string,
callback?: (error: Error | null | string, adaptorInfo?: IFileInfo | null) => void,
_options?: unknown,
): Promise<void> {
const fileInfo = this.boot.files[title];
if (!fileInfo) {
callback?.(null, null);
return;
}
try {
// Exclude file before deletion
this.watcher?.excludeFile(fileInfo.filepath);
// Call parent's deleteTiddler
await super.deleteTiddler(title, undefined, _options);
// Update inverse index
if (this.watcher) {
this.watcher.removeFromIndex(fileInfo.filepath);
}
callback?.(null, null);
// Schedule re-inclusion
this.watcher?.scheduleFileInclusion(fileInfo.filepath);
} catch (error) {
this.watcher?.scheduleFileInclusion(fileInfo.filepath);
const errorObject = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
callback?.(errorObject);
throw errorObject;
}
}
/**
* Cleanup resources when shutting down
*/
async cleanup(): Promise<void> {
if (this.watcher) {
await this.watcher.cleanup();
this.watcher = undefined;
}
}
}