mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-06 02:30:47 -08:00
fix: prevent echo by exclude title
This commit is contained in:
parent
1c88aca19b
commit
86aa838d24
3 changed files with 93 additions and 1 deletions
|
|
@ -27,6 +27,8 @@ export class InverseFilesIndex {
|
||||||
private mainExcludedFiles: Set<string> = new Set();
|
private mainExcludedFiles: Set<string> = new Set();
|
||||||
/** Temporarily excluded files for each sub-wiki watcher (by absolute path) */
|
/** Temporarily excluded files for each sub-wiki watcher (by absolute path) */
|
||||||
private subWikiExcludedFiles: Map<string, Set<string>> = new Map();
|
private subWikiExcludedFiles: Map<string, Set<string>> = new Map();
|
||||||
|
/** Temporarily excluded tiddler titles during save/delete operations */
|
||||||
|
private excludedTiddlerTitles: Set<string> = new Set();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the main wiki path
|
* Set the main wiki path
|
||||||
|
|
@ -227,4 +229,48 @@ export class InverseFilesIndex {
|
||||||
const excluded = this.subWikiExcludedFiles.get(subWikiId);
|
const excluded = this.subWikiExcludedFiles.get(subWikiId);
|
||||||
return excluded ? Array.from(excluded) : [];
|
return excluded ? Array.from(excluded) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a tiddler title to the exclusion list
|
||||||
|
* @param title Tiddler title to exclude
|
||||||
|
*/
|
||||||
|
excludeTiddlerTitle(title: string): void {
|
||||||
|
this.excludedTiddlerTitles.add(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a tiddler title from the exclusion list
|
||||||
|
* @param title Tiddler title to include
|
||||||
|
*/
|
||||||
|
includeTiddlerTitle(title: string): void {
|
||||||
|
this.excludedTiddlerTitles.delete(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a tiddler title is currently excluded
|
||||||
|
* @param title Tiddler title
|
||||||
|
* @returns True if title is excluded
|
||||||
|
*/
|
||||||
|
isTiddlerTitleExcluded(title: string): boolean;
|
||||||
|
/**
|
||||||
|
* Filter out excluded tiddlers from a changes object
|
||||||
|
* @param changes Changed tiddlers object
|
||||||
|
* @returns Filtered changes with excluded tiddlers removed
|
||||||
|
*/
|
||||||
|
isTiddlerTitleExcluded(changes: Record<string, unknown>): Record<string, unknown>;
|
||||||
|
isTiddlerTitleExcluded(input: string | Record<string, unknown>): boolean | Record<string, unknown> {
|
||||||
|
// Single title check
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
return this.excludedTiddlerTitles.has(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter changes object
|
||||||
|
const filteredChanges: Record<string, unknown> = {};
|
||||||
|
for (const title in input) {
|
||||||
|
if (input[title] && !this.excludedTiddlerTitles.has(title)) {
|
||||||
|
filteredChanges[title] = input[title];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredChanges;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
* Can be used with callback (legacy) or as async/await
|
* Can be used with callback (legacy) or as async/await
|
||||||
*/
|
*/
|
||||||
override async saveTiddler(tiddler: Tiddler, callback?: IFileSystemAdaptorCallback, options?: { tiddlerInfo?: Record<string, unknown> }): Promise<void> {
|
override async saveTiddler(tiddler: Tiddler, callback?: IFileSystemAdaptorCallback, options?: { tiddlerInfo?: Record<string, unknown> }): Promise<void> {
|
||||||
|
const title = tiddler.fields.title;
|
||||||
|
// Exclude title to prevent Wiki change events from being sent to frontend
|
||||||
|
this.inverseFilesIndex.excludeTiddlerTitle(title);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get file info to calculate path for watching
|
// Get file info to calculate path for watching
|
||||||
const fileInfo = await this.getTiddlerFileInfo(tiddler);
|
const fileInfo = await this.getTiddlerFileInfo(tiddler);
|
||||||
|
|
@ -104,7 +108,14 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
|
|
||||||
// Schedule file re-inclusion after save completes
|
// Schedule file re-inclusion after save completes
|
||||||
this.scheduleFileInclusion(fileInfo.filepath);
|
this.scheduleFileInclusion(fileInfo.filepath);
|
||||||
|
|
||||||
|
// Remove title from exclusion after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
this.inverseFilesIndex.includeTiddlerTitle(title);
|
||||||
|
}, FILE_EXCLUSION_CLEANUP_DELAY_MS);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Clean up title exclusion on error
|
||||||
|
this.inverseFilesIndex.includeTiddlerTitle(title);
|
||||||
const errorObject = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
|
const errorObject = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
|
||||||
callback?.(errorObject);
|
callback?.(errorObject);
|
||||||
throw errorObject;
|
throw errorObject;
|
||||||
|
|
@ -116,9 +127,13 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
* Can be used with callback (legacy) or as async/await
|
* Can be used with callback (legacy) or as async/await
|
||||||
*/
|
*/
|
||||||
override async deleteTiddler(title: string, callback?: IFileSystemAdaptorCallback, _options?: unknown): Promise<void> {
|
override async deleteTiddler(title: string, callback?: IFileSystemAdaptorCallback, _options?: unknown): Promise<void> {
|
||||||
|
// Exclude title to prevent Wiki change events from being sent to frontend
|
||||||
|
this.inverseFilesIndex.excludeTiddlerTitle(title);
|
||||||
|
|
||||||
const fileInfo = this.boot.files[title];
|
const fileInfo = this.boot.files[title];
|
||||||
|
|
||||||
if (!fileInfo) {
|
if (!fileInfo) {
|
||||||
|
this.inverseFilesIndex.includeTiddlerTitle(title);
|
||||||
callback?.(null, null);
|
callback?.(null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +156,14 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
|
|
||||||
// Schedule file re-inclusion after deletion completes
|
// Schedule file re-inclusion after deletion completes
|
||||||
this.scheduleFileInclusion(fileRelativePath);
|
this.scheduleFileInclusion(fileRelativePath);
|
||||||
|
|
||||||
|
// Remove title from exclusion after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
this.inverseFilesIndex.includeTiddlerTitle(title);
|
||||||
|
}, FILE_EXCLUSION_CLEANUP_DELAY_MS);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Clean up title exclusion on error
|
||||||
|
this.inverseFilesIndex.includeTiddlerTitle(title);
|
||||||
// Schedule file re-inclusion on error to clean up exclusion list
|
// Schedule file re-inclusion on error to clean up exclusion list
|
||||||
this.scheduleFileInclusion(fileRelativePath);
|
this.scheduleFileInclusion(fileRelativePath);
|
||||||
const errorObject = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
|
const errorObject = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,31 @@ export class IpcServerRoutes {
|
||||||
observer.error(new Error(`this.wikiInstance is undefined, maybe something went wrong between waitForIpcServerRoutesAvailable and return new Observable.`));
|
observer.error(new Error(`this.wikiInstance is undefined, maybe something went wrong between waitForIpcServerRoutesAvailable and return new Observable.`));
|
||||||
}
|
}
|
||||||
this.wikiInstance.wiki.addEventListener('change', (changes) => {
|
this.wikiInstance.wiki.addEventListener('change', (changes) => {
|
||||||
observer.next(changes);
|
// Filter out changes for tiddlers that are currently being saved/deleted
|
||||||
|
// This prevents echo: backend saves file → change event → frontend syncs → loads old file
|
||||||
|
const syncAdaptor = this.wikiInstance.syncadaptor as {
|
||||||
|
inverseFilesIndex?: {
|
||||||
|
isTiddlerTitleExcluded: ((title: string) => boolean) & ((changes: IChangedTiddlers) => IChangedTiddlers);
|
||||||
|
};
|
||||||
|
} | undefined;
|
||||||
|
|
||||||
|
let filteredChanges: IChangedTiddlers = changes;
|
||||||
|
|
||||||
|
// Try to filter out excluded tiddlers if the method exists
|
||||||
|
if (syncAdaptor?.inverseFilesIndex?.isTiddlerTitleExcluded) {
|
||||||
|
try {
|
||||||
|
filteredChanges = syncAdaptor.inverseFilesIndex.isTiddlerTitleExcluded(changes);
|
||||||
|
} catch (error) {
|
||||||
|
// If filtering fails, send all changes
|
||||||
|
console.error('Failed to filter excluded tiddlers:', error);
|
||||||
|
filteredChanges = changes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send changes if there are any
|
||||||
|
if (filteredChanges && Object.keys(filteredChanges).length > 0) {
|
||||||
|
observer.next(filteredChanges);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// Log SSE ready every time a new observer subscribes (including after worker restart)
|
// Log SSE ready every time a new observer subscribes (including after worker restart)
|
||||||
// Include timestamp to make each log entry unique for test detection
|
// Include timestamp to make each log entry unique for test detection
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue