Coalesce auto sync notifications

This commit is contained in:
shi.changliang 2026-06-05 16:35:33 +08:00
parent 5823d5c4cc
commit 3f4ddcbf80
2 changed files with 112 additions and 26 deletions

View file

@ -81,6 +81,22 @@ describe("auto sync scheduling", () => {
expect(plugin.syncNow).toHaveBeenCalledTimes(1);
});
test("keeps file-event suppression active briefly after sync finishes", async () => {
const plugin = createPlugin();
plugin.startFileEventSuppression();
expect(plugin.suppressFileSyncEvents).toBe(true);
plugin.finishFileEventSuppression();
expect(plugin.suppressFileSyncEvents).toBe(true);
await vi.advanceTimersByTimeAsync(999);
expect(plugin.suppressFileSyncEvents).toBe(true);
await vi.advanceTimersByTimeAsync(1);
expect(plugin.suppressFileSyncEvents).toBe(false);
});
test("runs one follow-up sync when triggered during an active sync", async () => {
const plugin = createPlugin();
let finishSync: (() => void) | undefined;
@ -236,6 +252,9 @@ type TestPlugin = {
saveSettings: ReturnType<typeof vi.fn<() => Promise<void>>>;
queueAutoSync: (delayMs?: number) => void;
runAutoSync: () => Promise<void>;
startFileEventSuppression: () => void;
finishFileEventSuppression: () => void;
suppressFileSyncEvents: boolean;
syncNow: ReturnType<typeof vi.fn<() => Promise<void>>>;
};

View file

@ -56,12 +56,19 @@ const DEFAULT_SETTINGS: ObsyncSettings = {
};
const AUTO_SYNC_DEBOUNCE_MS = 2_000;
const FILE_EVENT_SUPPRESSION_MS = 1_000;
type SyncTrigger = "manual" | "auto";
export default class ObsyncPlugin extends Plugin {
settings: ObsyncSettings = DEFAULT_SETTINGS;
private autoSyncTimer: number | null = null;
private autoSyncRunning = false;
private autoSyncQueued = false;
private syncRunning = false;
private suppressFileSyncEvents = false;
private suppressFileSyncTimer: number | null = null;
private activeNotice: Notice | null = null;
private statusBarItem: HTMLElement | null = null;
async onload(): Promise<void> {
@ -74,12 +81,12 @@ export default class ObsyncPlugin extends Plugin {
id: "manual-sync",
name: "立即同步",
callback: async () => {
await this.syncNow();
await this.syncNow("manual");
},
});
const syncRibbonIcon = this.addRibbonIcon("refresh-cw", "Obsync 立即同步", async () => {
await this.syncNow();
await this.syncNow("manual");
});
syncRibbonIcon.classList.add("obsync-ribbon-sync");
this.app.workspace.onLayoutReady(() => {
@ -107,27 +114,51 @@ export default class ObsyncPlugin extends Plugin {
this.registerAutoSyncTriggers();
}
async syncNow(): Promise<void> {
await this.runConfiguredOperation("同步中...", "Obsync 同步失败", async () => {
const store = this.createObjectStore();
const result = await syncOnce({
vault: new ObsidianVaultIO(this.app),
store,
state: this.settings.syncState,
deviceName: this.settings.deviceName || this.settings.deviceId,
deviceId: this.settings.deviceId,
now: () => Date.now(),
});
async syncNow(trigger: SyncTrigger = "manual"): Promise<void> {
if (this.syncRunning) {
this.autoSyncQueued = true;
await this.saveSettings();
if (result.locked) {
new Notice("另一台设备正在同步,本次已跳过。");
return;
if (trigger === "manual") {
this.showNotice("Obsync 正在同步,稍后会再同步一次。");
}
new Notice(`Obsync 同步完成:上传 ${result.uploaded},下载 ${result.downloaded},冲突 ${result.conflicts}`);
});
return;
}
this.syncRunning = true;
this.startFileEventSuppression();
try {
await this.runConfiguredOperation("同步中...", "Obsync 同步失败", async () => {
const store = this.createObjectStore();
const result = await syncOnce({
vault: new ObsidianVaultIO(this.app),
store,
state: this.settings.syncState,
deviceName: this.settings.deviceName || this.settings.deviceId,
deviceId: this.settings.deviceId,
now: () => Date.now(),
});
await this.saveSettings();
if (result.locked) {
if (trigger === "manual") {
this.showNotice("另一台设备正在同步,本次已跳过。");
}
return;
}
if (trigger === "manual") {
this.showNotice(`Obsync 同步完成:上传 ${result.uploaded},下载 ${result.downloaded},冲突 ${result.conflicts}`);
}
}, {
notifyMissingConfig: trigger === "manual",
});
} finally {
this.syncRunning = false;
this.finishFileEventSuppression();
}
}
private moveRibbonIconToBottom(iconEl: HTMLElement): void {
@ -152,6 +183,10 @@ export default class ObsyncPlugin extends Plugin {
const queueFileSync = (file: TAbstractFile) => {
if (file instanceof TFile) {
if (this.suppressFileSyncEvents) {
return;
}
this.queueAutoSync();
}
};
@ -190,7 +225,7 @@ export default class ObsyncPlugin extends Plugin {
this.autoSyncRunning = true;
try {
await this.syncNow();
await this.syncNow("auto");
} finally {
this.autoSyncRunning = false;
@ -210,19 +245,26 @@ export default class ObsyncPlugin extends Plugin {
const result = await releaseDeletedContent({ store: this.createObjectStore(), now: () => Date.now() });
if (result.locked) {
new Notice("另一台设备正在同步,本次释放空间已跳过。");
this.showNotice("另一台设备正在同步,本次释放空间已跳过。");
return;
}
this.pruneLocalDeletedState();
await this.saveSettings();
new Notice(`Obsync 已释放:清理删除记录 ${result.deletedTombstones},删除 Blob ${result.deletedBlobs}`);
this.showNotice(`Obsync 已释放:清理删除记录 ${result.deletedTombstones},删除 Blob ${result.deletedBlobs}`);
});
}
private async runConfiguredOperation(progressText: string, failurePrefix: string, operation: () => Promise<void>): Promise<void> {
private async runConfiguredOperation(
progressText: string,
failurePrefix: string,
operation: () => Promise<void>,
options: { notifyMissingConfig?: boolean } = {},
): Promise<void> {
if (!this.hasConnectionSettings()) {
new Notice("请先填写端点、Bucket、Access Key ID 和 Secret Access Key。");
if (options.notifyMissingConfig ?? true) {
this.showNotice("请先填写端点、Bucket、Access Key ID 和 Secret Access Key。");
}
return;
}
@ -232,12 +274,37 @@ export default class ObsyncPlugin extends Plugin {
await operation();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
new Notice(`${failurePrefix}${message}`);
this.showNotice(`${failurePrefix}${message}`);
} finally {
this.clearOperationStatus();
}
}
private startFileEventSuppression(): void {
if (this.suppressFileSyncTimer !== null) {
window.clearTimeout(this.suppressFileSyncTimer);
this.suppressFileSyncTimer = null;
}
this.suppressFileSyncEvents = true;
}
private finishFileEventSuppression(): void {
if (this.suppressFileSyncTimer !== null) {
window.clearTimeout(this.suppressFileSyncTimer);
}
this.suppressFileSyncTimer = window.setTimeout(() => {
this.suppressFileSyncTimer = null;
this.suppressFileSyncEvents = false;
}, FILE_EVENT_SUPPRESSION_MS);
}
private showNotice(message: string): void {
this.activeNotice?.hide();
this.activeNotice = new Notice(message);
}
private setOperationStatus(text: string): void {
this.statusBarItem?.classList.remove("is-hidden");
this.statusBarItem?.setText(text);