mirror of
https://github.com/ichaly/cohere.git
synced 2026-07-22 07:47:20 +00:00
Enable automatic sync triggers
This commit is contained in:
parent
4408b1f981
commit
2ec89b9bd0
2 changed files with 243 additions and 1 deletions
166
src/main.test.ts
Normal file
166
src/main.test.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import ObsyncPlugin from "./main";
|
||||
|
||||
vi.mock("obsidian", () => ({
|
||||
App: class {},
|
||||
Notice: class {
|
||||
hide(): void {}
|
||||
},
|
||||
Platform: {
|
||||
isAndroidApp: false,
|
||||
isIosApp: false,
|
||||
isTablet: false,
|
||||
},
|
||||
Plugin: class {},
|
||||
PluginSettingTab: class {},
|
||||
requestUrl: vi.fn(),
|
||||
TFile: class {},
|
||||
TFolder: class {},
|
||||
}));
|
||||
|
||||
describe("auto sync scheduling", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.stubGlobal("window", {
|
||||
clearTimeout: globalThis.clearTimeout,
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test("does not schedule when auto sync is disabled", async () => {
|
||||
const plugin = createPlugin({ autoSync: false });
|
||||
|
||||
plugin.queueAutoSync(0);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(plugin.syncNow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("skips scheduled sync when auto sync is disabled before the timer fires", async () => {
|
||||
const plugin = createPlugin();
|
||||
|
||||
plugin.queueAutoSync();
|
||||
plugin.settings.autoSync = false;
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
|
||||
expect(plugin.syncNow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("skips when connection settings are incomplete", async () => {
|
||||
const plugin = createPlugin({ secretAccessKey: "" });
|
||||
|
||||
plugin.queueAutoSync(0);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(plugin.syncNow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("debounces repeated file-change triggers into one sync", async () => {
|
||||
const plugin = createPlugin();
|
||||
|
||||
plugin.queueAutoSync();
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
plugin.queueAutoSync();
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
plugin.queueAutoSync();
|
||||
await vi.advanceTimersByTimeAsync(1_999);
|
||||
|
||||
expect(plugin.syncNow).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
expect(plugin.syncNow).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("runs one follow-up sync when triggered during an active sync", async () => {
|
||||
const plugin = createPlugin();
|
||||
let finishSync: (() => void) | undefined;
|
||||
plugin.syncNow.mockImplementationOnce(() => new Promise<void>((resolve) => {
|
||||
finishSync = resolve;
|
||||
}));
|
||||
|
||||
const firstRun = plugin.runAutoSync();
|
||||
await Promise.resolve();
|
||||
|
||||
void plugin.runAutoSync();
|
||||
expect(plugin.syncNow).toHaveBeenCalledTimes(1);
|
||||
|
||||
finishSync?.();
|
||||
await firstRun;
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
|
||||
expect(plugin.syncNow).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("does not run queued follow-up if auto sync is disabled while syncing", async () => {
|
||||
const plugin = createPlugin();
|
||||
let finishSync: (() => void) | undefined;
|
||||
plugin.syncNow.mockImplementationOnce(() => new Promise<void>((resolve) => {
|
||||
finishSync = resolve;
|
||||
}));
|
||||
|
||||
const firstRun = plugin.runAutoSync();
|
||||
await Promise.resolve();
|
||||
|
||||
void plugin.runAutoSync();
|
||||
plugin.settings.autoSync = false;
|
||||
finishSync?.();
|
||||
await firstRun;
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(plugin.syncNow).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
function createPlugin(settings: Partial<TestSettings> = {}): TestPlugin {
|
||||
const plugin = Object.create(ObsyncPlugin.prototype) as TestPlugin;
|
||||
plugin.settings = {
|
||||
endpoint: "https://example.com",
|
||||
bucket: "vault",
|
||||
region: "auto",
|
||||
accessKeyId: "key",
|
||||
secretAccessKey: "secret",
|
||||
rootPrefix: "obsync/v1",
|
||||
accountKey: "default",
|
||||
vaultKey: "vault",
|
||||
vaultId: "vlt_test",
|
||||
deviceId: "dev_test",
|
||||
deviceName: "Mac",
|
||||
syncIntervalMinutes: 5,
|
||||
autoSync: true,
|
||||
syncState: { files: {} },
|
||||
...settings,
|
||||
};
|
||||
plugin.syncNow = vi.fn(async () => {});
|
||||
return plugin;
|
||||
}
|
||||
|
||||
type TestPlugin = {
|
||||
settings: TestSettings;
|
||||
queueAutoSync: (delayMs?: number) => void;
|
||||
runAutoSync: () => Promise<void>;
|
||||
syncNow: ReturnType<typeof vi.fn<() => Promise<void>>>;
|
||||
};
|
||||
|
||||
interface TestSettings {
|
||||
endpoint: string;
|
||||
bucket: string;
|
||||
region: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
rootPrefix: string;
|
||||
accountKey: string;
|
||||
vaultKey: string;
|
||||
vaultId: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
syncIntervalMinutes: number;
|
||||
autoSync: boolean;
|
||||
syncState: { files: Record<string, unknown> };
|
||||
}
|
||||
78
src/main.ts
78
src/main.ts
|
|
@ -1,4 +1,4 @@
|
|||
import { App, Notice, Platform, Plugin, PluginSettingTab, requestUrl, TFile, TFolder } from "obsidian";
|
||||
import { App, Notice, Platform, Plugin, PluginSettingTab, requestUrl, type TAbstractFile, TFile, TFolder } from "obsidian";
|
||||
import { createApp, reactive, type App as VueApp } from "vue";
|
||||
import { createRandomId, createVaultId, normalizeKey } from "./core/ids";
|
||||
import SettingsApp from "./settings/SettingsApp.vue";
|
||||
|
|
@ -55,8 +55,13 @@ const DEFAULT_SETTINGS: ObsyncSettings = {
|
|||
},
|
||||
};
|
||||
|
||||
const AUTO_SYNC_DEBOUNCE_MS = 2_000;
|
||||
|
||||
export default class ObsyncPlugin extends Plugin {
|
||||
settings: ObsyncSettings = DEFAULT_SETTINGS;
|
||||
private autoSyncTimer: number | null = null;
|
||||
private autoSyncRunning = false;
|
||||
private autoSyncQueued = false;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
|
|
@ -87,6 +92,7 @@ export default class ObsyncPlugin extends Plugin {
|
|||
});
|
||||
|
||||
this.addSettingTab(new ObsyncSettingTab(this.app, this));
|
||||
this.registerAutoSyncTriggers();
|
||||
}
|
||||
|
||||
async syncNow(): Promise<void> {
|
||||
|
|
@ -154,6 +160,76 @@ export default class ObsyncPlugin extends Plugin {
|
|||
(leftRibbonActions ?? iconEl.parentElement)?.append(iconEl);
|
||||
}
|
||||
|
||||
private registerAutoSyncTriggers(): void {
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
this.queueAutoSync(0);
|
||||
});
|
||||
|
||||
this.registerDomEvent(window, "focus", () => {
|
||||
this.queueAutoSync();
|
||||
});
|
||||
|
||||
this.registerDomEvent(document, "visibilitychange", () => {
|
||||
if (!document.hidden) {
|
||||
this.queueAutoSync();
|
||||
}
|
||||
});
|
||||
|
||||
const queueFileSync = (file: TAbstractFile) => {
|
||||
if (file instanceof TFile) {
|
||||
this.queueAutoSync();
|
||||
}
|
||||
};
|
||||
|
||||
this.registerEvent(this.app.vault.on("create", queueFileSync));
|
||||
this.registerEvent(this.app.vault.on("modify", queueFileSync));
|
||||
this.registerEvent(this.app.vault.on("delete", queueFileSync));
|
||||
this.registerEvent(this.app.vault.on("rename", queueFileSync));
|
||||
}
|
||||
|
||||
private queueAutoSync(delayMs = AUTO_SYNC_DEBOUNCE_MS): void {
|
||||
if (!this.settings.autoSync) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoSyncTimer !== null) {
|
||||
window.clearTimeout(this.autoSyncTimer);
|
||||
}
|
||||
|
||||
this.autoSyncTimer = window.setTimeout(() => {
|
||||
this.autoSyncTimer = null;
|
||||
void this.runAutoSync();
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
private async runAutoSync(): Promise<void> {
|
||||
if (!this.settings.autoSync || !this.hasConnectionSettings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoSyncRunning) {
|
||||
this.autoSyncQueued = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.autoSyncRunning = true;
|
||||
|
||||
try {
|
||||
await this.syncNow();
|
||||
} finally {
|
||||
this.autoSyncRunning = false;
|
||||
|
||||
if (this.autoSyncQueued) {
|
||||
this.autoSyncQueued = false;
|
||||
this.queueAutoSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private hasConnectionSettings(): boolean {
|
||||
return Boolean(this.settings.endpoint && this.settings.bucket && this.settings.accessKeyId && this.settings.secretAccessKey);
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
const loaded = await this.loadData();
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded);
|
||||
|
|
|
|||
Loading…
Reference in a new issue