refactor: remove sync interval preference

This commit is contained in:
tianma-if committed 2026-08-29 20:00:41 +08:00
1 parent 3a49b16d9c
commit d4821eb486
12 files changed
+16 -196

No files matched your search

-6
View File
@@ -45,8 +45,6 @@ interface SettingsPaneProps {
onOpenAiPrompts: () => void;
imageCompressionEnabled: boolean;
onImageCompressionChange: (enabled: boolean) => void;
syncIntervalMs: number | null;
onSyncIntervalChange: (intervalMs: number | null) => void;
shortcutSettings: ShortcutSettings;
onShortcutSettingsChange: (settings: ShortcutSettings) => void;
editorContentAlignment: EditorContentAlignment;
@@ -86,8 +84,6 @@ export const SettingsPane = ({
onOpenAiPrompts,
imageCompressionEnabled,
onImageCompressionChange,
syncIntervalMs,
onSyncIntervalChange,
shortcutSettings,
onShortcutSettingsChange,
editorContentAlignment,
@@ -208,8 +204,6 @@ export const SettingsPane = ({
<PreferenceCard
imageCompressionEnabled={imageCompressionEnabled}
onImageCompressionChange={onImageCompressionChange}
syncIntervalMs={syncIntervalMs}
onSyncIntervalChange={onSyncIntervalChange}
shortcutSettings={shortcutSettings}
onShortcutSettingsChange={onShortcutSettingsChange}
editorContentAlignment={editorContentAlignment}
+1 -6
View File
@@ -795,9 +795,7 @@ export const WorkspaceApp = ({
setImageCompressionEnabled,
setMemoListWidth,
setShortcutSettings,
setSyncIntervalMs,
shortcutSettings,
syncIntervalMs,
} = useWorkspacePreferences();
const [rightView, setRightView] = useState<"editor" | "settings" | "plugins" | "assets" | "tags" | "templates" | "ai-prompts" | "evernote-migration">(() =>
isInitialSettingsRoute
@@ -1311,12 +1309,11 @@ export const WorkspaceApp = ({
}, [mobilePullToRefreshActive, refreshLatestMemos]);
useWorkspaceSyncLifecycle({
pendingSyncCount: syncSummary.total,
failedSyncCount: syncSummary.error,
backgroundRefreshKey: localDataScope,
refreshWorkspace: refreshWorkspaceFromServer,
runQueuedSync,
setOnline: setIsOnline,
syncIntervalMs,
});
const selectedNotebookDescendantIds = useMemo(
@@ -3100,8 +3097,6 @@ export const WorkspaceApp = ({
onOpenAiPrompts={handleOpenAiPrompts}
imageCompressionEnabled={imageCompressionEnabled}
onImageCompressionChange={setImageCompressionEnabled}
syncIntervalMs={syncIntervalMs}
onSyncIntervalChange={setSyncIntervalMs}
shortcutSettings={shortcutSettings}
onShortcutSettingsChange={setShortcutSettings}
editorContentAlignment={editorContentAlignment}
@@ -1,7 +1,7 @@
import { AlignHorizontalJustifyCenter, ChartNoAxesCombined, FileCode2, Image, Keyboard, Languages, MousePointerClick, Palette, RefreshCw, Sparkles } from "lucide-react";
import { AlignHorizontalJustifyCenter, ChartNoAxesCombined, FileCode2, Image, Keyboard, Languages, MousePointerClick, Palette, Sparkles } from "lucide-react";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { writeSyncIntervalPreference, type EditorContentAlignment, type ShortcutSettings, type SyncIntervalPreference } from "@/lib/app-helpers";
import type { EditorContentAlignment, ShortcutSettings } from "@/lib/app-helpers";
import {
EDITOR_LINK_OPEN_MODE_CHANGED_EVENT,
getStoredEditorLinkOpenMode,
@@ -45,8 +45,6 @@ import {
interface PreferenceCardProps {
imageCompressionEnabled: boolean;
onImageCompressionChange: (enabled: boolean) => void;
syncIntervalMs: number | null;
onSyncIntervalChange: (intervalMs: number | null) => void;
shortcutSettings: ShortcutSettings;
onShortcutSettingsChange: (settings: ShortcutSettings) => void;
editorContentAlignment: EditorContentAlignment;
@@ -56,8 +54,6 @@ interface PreferenceCardProps {
export const PreferenceCard = ({
imageCompressionEnabled,
onImageCompressionChange,
syncIntervalMs,
onSyncIntervalChange,
shortcutSettings,
onShortcutSettingsChange,
editorContentAlignment,
@@ -185,9 +181,6 @@ export const PreferenceCard = ({
void changeAppLocalePreference(preference);
};
const syncIntervalPreference: SyncIntervalPreference =
syncIntervalMs === 300_000 ? "5m" : syncIntervalMs === 900_000 ? "15m" : syncIntervalMs === 1_800_000 ? "30m" : syncIntervalMs === 3_600_000 ? "1h" : syncIntervalMs === 7_200_000 ? "2h" : "30s";
return (
<Card className="w-full min-w-0 overflow-hidden shadow-none">
<CardHeader className="p-4">
@@ -357,40 +350,6 @@ export const PreferenceCard = ({
</div>
</div>
<div className="flex min-h-16 flex-col items-start gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div className="flex min-w-0 items-start gap-3">
<RefreshCw className="mt-0.5 h-4 w-4 shrink-0 text-emerald-700" />
<div className="min-w-0">
<div className="text-sm font-semibold text-slate-900">{t("settings.syncIntervalTitle")}</div>
<div className="mt-0.5 text-xs leading-4 text-slate-500">{t("settings.syncIntervalDescription")}</div>
</div>
</div>
<div className="w-full shrink-0 sm:w-44">
<Select
value={syncIntervalPreference}
onValueChange={(value) => {
const preference = value as SyncIntervalPreference;
writeSyncIntervalPreference(preference);
onSyncIntervalChange(
preference === "5m" ? 300_000 : preference === "15m" ? 900_000 : preference === "30m" ? 1_800_000 : preference === "1h" ? 3_600_000 : preference === "2h" ? 7_200_000 : 30_000
);
}}
>
<SelectTrigger aria-label={t("settings.syncIntervalTitle")} className="h-9 bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="30s">{t("settings.syncIntervals.30s")}</SelectItem>
<SelectItem value="5m">{t("settings.syncIntervals.5m")}</SelectItem>
<SelectItem value="15m">{t("settings.syncIntervals.15m")}</SelectItem>
<SelectItem value="30m">{t("settings.syncIntervals.30m")}</SelectItem>
<SelectItem value="1h">{t("settings.syncIntervals.1h")}</SelectItem>
<SelectItem value="2h">{t("settings.syncIntervals.2h")}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex min-h-16 flex-col items-start gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div className="flex min-w-0 items-start gap-3">
<Image className="mt-0.5 h-4 w-4 shrink-0 text-emerald-700" />
@@ -1,13 +0,0 @@
import { describe, expect, test } from "bun:test";
import { syncIntervalMsToPreference } from "./useWorkspacePreferences.ts";
describe("workspace sync interval preference", () => {
test("serializes every supported interval", () => {
expect([null, 30_000, 300_000, 900_000, 1_800_000, 3_600_000, 7_200_000].map(syncIntervalMsToPreference))
.toEqual(["off", "30s", "5m", "15m", "30m", "1h", "2h"]);
});
test("falls back to the default interval for an unknown value", () => {
expect(syncIntervalMsToPreference(1234)).toBe("30s");
});
});
@@ -7,41 +7,23 @@ import {
readImageCompressionPreference,
readMemoListWidthPreference,
readShortcutSettingsPreference,
readSyncIntervalPreference,
writeDesktopFocusModePreference,
writeEditorContentAlignmentPreference,
writeImageCompressionPreference,
writeMemoListWidthPreference,
writeShortcutSettingsPreference,
writeSyncIntervalPreference,
type ShortcutSettings,
type EditorContentAlignment,
} from "@/lib/app-helpers";
export type SyncIntervalPreference = "off" | "30s" | "5m" | "15m" | "30m" | "1h" | "2h";
export const syncIntervalMsToPreference = (intervalMs: number | null): SyncIntervalPreference => {
switch (intervalMs) {
case null: return "off";
case 300_000: return "5m";
case 900_000: return "15m";
case 1_800_000: return "30m";
case 3_600_000: return "1h";
case 7_200_000: return "2h";
default: return "30s";
}
};
export const useWorkspacePreferences = () => {
const [imageCompressionEnabled, setImageCompressionEnabled] = useState(readImageCompressionPreference);
const [syncIntervalMs, setSyncIntervalMs] = useState(readSyncIntervalPreference);
const [desktopFocusMode, setDesktopFocusModeState] = useState(readDesktopFocusModePreference);
const [editorContentAlignment, setEditorContentAlignmentState] = useState(readEditorContentAlignmentPreference);
const [shortcutSettings, setShortcutSettings] = useState<ShortcutSettings>(readShortcutSettingsPreference);
const [memoListWidth, setMemoListWidthState] = useState(readMemoListWidthPreference);
useEffect(() => writeImageCompressionPreference(imageCompressionEnabled), [imageCompressionEnabled]);
useEffect(() => writeSyncIntervalPreference(syncIntervalMsToPreference(syncIntervalMs)), [syncIntervalMs]);
useEffect(() => writeShortcutSettingsPreference(shortcutSettings), [shortcutSettings]);
const setDesktopFocusMode = useCallback((enabled: boolean) => {
@@ -75,8 +57,6 @@ export const useWorkspacePreferences = () => {
setImageCompressionEnabled,
setMemoListWidth,
setShortcutSettings,
setSyncIntervalMs,
shortcutSettings,
syncIntervalMs,
};
};
@@ -6,6 +6,7 @@ import {
BACKGROUND_WORKSPACE_REFRESH_INTERVAL_MS,
claimBackgroundRefreshLease,
createRefreshSingleFlight,
DEFERRED_MEMO_SYNC_DELAY_MS,
releaseBackgroundRefreshLease,
type WorkspaceRefreshMode,
} from "@/lib/workspace-refresh";
@@ -13,22 +14,19 @@ import {
type RefreshWorkspace = (mode: WorkspaceRefreshMode) => Promise<unknown>;
export const useWorkspaceSyncLifecycle = ({
pendingSyncCount,
failedSyncCount,
backgroundRefreshKey,
refreshWorkspace,
runQueuedSync,
setOnline,
syncIntervalMs,
}: {
pendingSyncCount: number;
failedSyncCount: number;
backgroundRefreshKey: string;
refreshWorkspace: RefreshWorkspace;
runQueuedSync: () => Promise<void>;
setOnline: (online: boolean) => void;
syncIntervalMs: number | null;
}) => {
const deferredSyncTimerRef = useRef<number | null>(null);
const deferredSyncPendingRef = useRef(false);
const runQueuedSyncRef = useRef(runQueuedSync);
const refreshWorkspaceRef = useRef(refreshWorkspace);
const backgroundRefreshOwnerRef = useRef(createClientUuid());
@@ -108,7 +106,6 @@ export const useWorkspaceSyncLifecycle = ({
useEffect(() => {
const handleQueueChanged = () => {
deferredSyncPendingRef.current = false;
if (deferredSyncTimerRef.current !== null) {
window.clearTimeout(deferredSyncTimerRef.current);
deferredSyncTimerRef.current = null;
@@ -121,21 +118,17 @@ export const useWorkspaceSyncLifecycle = ({
useEffect(() => {
const scheduleDeferredSync = () => {
deferredSyncPendingRef.current = true;
if (deferredSyncTimerRef.current !== null) {
window.clearTimeout(deferredSyncTimerRef.current);
deferredSyncTimerRef.current = null;
}
if (syncIntervalMs === null) return;
deferredSyncTimerRef.current = window.setTimeout(() => {
deferredSyncTimerRef.current = null;
deferredSyncPendingRef.current = false;
void runQueuedSyncRef.current();
}, syncIntervalMs);
}, DEFERRED_MEMO_SYNC_DELAY_MS);
};
window.addEventListener(SYNC_QUEUE_DEFERRED_EVENT, scheduleDeferredSync);
if (deferredSyncPendingRef.current) scheduleDeferredSync();
return () => {
window.removeEventListener(SYNC_QUEUE_DEFERRED_EVENT, scheduleDeferredSync);
if (deferredSyncTimerRef.current !== null) {
@@ -143,7 +136,7 @@ export const useWorkspaceSyncLifecycle = ({
deferredSyncTimerRef.current = null;
}
};
}, [syncIntervalMs]);
}, []);
useEffect(() => {
const leaseKey = `edgeever.background-refresh:${backgroundRefreshKey}`;
@@ -201,8 +194,8 @@ export const useWorkspaceSyncLifecycle = ({
}, [backgroundRefreshKey]);
useEffect(() => {
if (pendingSyncCount === 0) return;
if (failedSyncCount === 0) return;
const timer = window.setInterval(() => void runQueuedSync(), 15_000);
return () => window.clearInterval(timer);
}, [pendingSyncCount, runQueuedSync]);
}, [failedSyncCount, runQueuedSync]);
};
-51
View File
@@ -1,25 +1,21 @@
import { afterEach, describe, expect, test } from "bun:test";
import {
DEFAULT_SHORTCUT_SETTINGS,
DEFAULT_SYNC_INTERVAL_MS,
DESKTOP_FOCUS_MODE_STORAGE_KEY,
DESKTOP_READING_PROTECTION_STORAGE_KEY,
EDITOR_CONTENT_ALIGNMENT_STORAGE_KEY,
NOTEBOOK_SORT_STORAGE_KEY,
SHORTCUT_SETTINGS_STORAGE_KEY,
SYNC_INTERVAL_STORAGE_KEY,
getSearchShortcutScope,
getShortcutActionForEvent,
getNotebookSortComparator,
readEditorContentAlignmentPreference,
readNotebookSortPreference,
readSyncIntervalPreference,
readDesktopFocusModePreference,
readDesktopReadingProtectionPreference,
readShortcutSettingsPreference,
writeEditorContentAlignmentPreference,
writeNotebookSortPreference,
writeSyncIntervalPreference,
writeDesktopFocusModePreference,
writeDesktopReadingProtectionPreference,
} from "./app-helpers.ts";
@@ -194,53 +190,6 @@ describe("custom notebook sorting", () => {
});
});
describe("automatic sync interval preference", () => {
test("defaults to 30 seconds", () => {
installLocalStorage();
expect(readSyncIntervalPreference()).toBe(DEFAULT_SYNC_INTERVAL_MS);
expect(DEFAULT_SYNC_INTERVAL_MS).toBe(30_000);
});
test("reads and writes sync intervals", () => {
const values = installLocalStorage();
writeSyncIntervalPreference("30s");
expect(values.get(SYNC_INTERVAL_STORAGE_KEY)).toBe("30s");
expect(readSyncIntervalPreference()).toBe(30_000);
writeSyncIntervalPreference("5m");
expect(values.get(SYNC_INTERVAL_STORAGE_KEY)).toBe("5m");
expect(readSyncIntervalPreference()).toBe(300_000);
});
test("preserves the legacy preference stored under the old key", () => {
const values = installLocalStorage();
values.set("edgeever.autoSaveInterval", "15m");
expect(readSyncIntervalPreference()).toBe(900_000);
});
test("migrates the former one-minute default to 30 seconds", () => {
const values = installLocalStorage();
values.set(SYNC_INTERVAL_STORAGE_KEY, "1m");
expect(readSyncIntervalPreference()).toBe(30_000);
});
test("falls back to the default for unknown or unavailable storage", () => {
const values = installLocalStorage();
values.set(SYNC_INTERVAL_STORAGE_KEY, "unexpected");
expect(readSyncIntervalPreference()).toBe(30_000);
globalThis.window = {
localStorage: {
getItem: () => {
throw new Error("blocked");
},
},
};
expect(readSyncIntervalPreference()).toBe(30_000);
});
});
describe("workspace shortcut preferences", () => {
test("provides navigation, AI, save, reading protection, and editor mode defaults", () => {
expect(DEFAULT_SHORTCUT_SETTINGS.focusGlobalSearch).toEqual({
-37
View File
@@ -129,8 +129,6 @@ export { buildNotebookTree, type NotebookNode };
export { DEFAULT_MEMO_TITLE };
export const IMAGE_COMPRESSION_STORAGE_KEY = "edgeever.imageCompressionEnabled";
export const SYNC_INTERVAL_STORAGE_KEY = "edgeever.syncInterval";
const LEGACY_AUTO_SAVE_INTERVAL_STORAGE_KEY = "edgeever.autoSaveInterval";
export const DESKTOP_FOCUS_MODE_STORAGE_KEY = "edgeever.desktopFocusMode";
export const DESKTOP_READING_PROTECTION_STORAGE_KEY = "edgeever.desktopReadingProtection";
export const EDITOR_CONTENT_ALIGNMENT_STORAGE_KEY = "edgeever.editorContentAlignment";
@@ -143,18 +141,6 @@ export const MIN_MEMO_LIST_WIDTH_PX = 300;
export const MAX_MEMO_LIST_WIDTH_PX = 540;
export const EDITOR_LOCAL_SAVE_DELAY_MS = 1_200;
export type SyncIntervalPreference = "30s" | "5m" | "15m" | "30m" | "1h" | "2h" | "off";
export const DEFAULT_SYNC_INTERVAL_MS = 30_000;
const SYNC_INTERVAL_VALUES: Record<SyncIntervalPreference, number | null> = {
"30s": 30_000,
"5m": 300_000,
"15m": 900_000,
"30m": 1_800_000,
"1h": 3_600_000,
"2h": 7_200_000,
off: null,
};
export const MEMO_DRAG_MIME = "application/x-edgeever-memos";
export const NOTEBOOK_DRAG_MIME = "application/x-edgeever-notebook";
@@ -337,29 +323,6 @@ export const writeImageCompressionPreference = (enabled: boolean) => {
}
};
export const readSyncIntervalPreference = (): number | null => {
try {
const stored = (
window.localStorage.getItem(SYNC_INTERVAL_STORAGE_KEY)
?? window.localStorage.getItem(LEGACY_AUTO_SAVE_INTERVAL_STORAGE_KEY)
);
const preference = stored === "1m" ? "30s" : stored;
return preference && preference in SYNC_INTERVAL_VALUES
? SYNC_INTERVAL_VALUES[preference as SyncIntervalPreference]
: DEFAULT_SYNC_INTERVAL_MS;
} catch {
return DEFAULT_SYNC_INTERVAL_MS;
}
};
export const writeSyncIntervalPreference = (preference: SyncIntervalPreference) => {
try {
window.localStorage.setItem(SYNC_INTERVAL_STORAGE_KEY, preference);
} catch {
// Local storage can be unavailable in private or restricted browser contexts.
}
};
export const readDesktopFocusModePreference = () => {
try {
return window.localStorage.getItem(DESKTOP_FOCUS_MODE_STORAGE_KEY) === "true";
@@ -4,6 +4,7 @@ import {
BACKGROUND_WORKSPACE_REFRESH_INTERVAL_MS,
claimBackgroundRefreshLease,
createRefreshSingleFlight,
DEFERRED_MEMO_SYNC_DELAY_MS,
releaseBackgroundRefreshLease,
refreshWorkspaceData,
resolveCreatedMemoSelection,
@@ -12,6 +13,10 @@ import {
} from "./workspace-refresh.ts";
describe("refreshWorkspaceData", () => {
it("uses a fixed 30-second delay for uploading memo edits", () => {
assert.equal(DEFERRED_MEMO_SYNC_DELAY_MS, 30_000);
});
it("uses a shared five-minute background refresh interval", () => {
assert.equal(BACKGROUND_WORKSPACE_REFRESH_INTERVAL_MS, 5 * 60_000);
});
+1
View File
@@ -1,6 +1,7 @@
export type WorkspaceRefreshMode = "background" | "manual";
export type WorkspaceMemoView = "notebook" | "trash";
export const DEFERRED_MEMO_SYNC_DELAY_MS = 30_000;
export const BACKGROUND_WORKSPACE_REFRESH_INTERVAL_MS = 5 * 60_000;
export const BACKGROUND_WORKSPACE_REFRESH_LEASE_MS = BACKGROUND_WORKSPACE_REFRESH_INTERVAL_MS * 2;
export const BACKGROUND_WORKSPACE_REFRESH_COALESCE_MS = 1_000;
-3
View File
@@ -244,9 +244,6 @@ export const enUS = {
linkOpenModifierTitle: "Open web links with Ctrl/⌘ + click while editing",
linkOpenModifierDescription: "On by default: hold Ctrl/⌘ and click to open web links, preventing accidental opens while editing. Note references always open with one click. Desktop only.",
linkOpenModifierAria: "Require Ctrl or ⌘ click to open web links while editing",
syncIntervalTitle: "Automatic sync interval",
syncIntervalDescription: "Sync edits to the server automatically while keeping local drafts available immediately.",
syncIntervals: { "30s": "Every 30 seconds", "5m": "Every 5 minutes", "15m": "Every 15 minutes", "30m": "Every 30 minutes", "1h": "Every hour", "2h": "Every 2 hours" },
mermaidThemeTitle: "Diagram theme",
mermaidThemeDescription: "Follow the app appearance or choose a fixed visual theme for diagrams.",
markdownThemeTitle: "Markdown source theme",
-3
View File
@@ -244,9 +244,6 @@ export const zhCN = {
linkOpenModifierTitle: "编辑时用 Ctrl/⌘ + 点击打开网页链接",
linkOpenModifierDescription: "默认开启:网页链接需按住 Ctrl/⌘ 再点击,避免编辑时误触;笔记引用始终单击打开(仅桌面)。",
linkOpenModifierAria: "编辑时是否需要 Ctrl 或 ⌘ 再点击才能打开网页链接",
syncIntervalTitle: "自动同步间隔",
syncIntervalDescription: "定期将编辑内容同步到服务器,本地草稿仍会即时保留。",
syncIntervals: { "30s": "每 30 秒", "5m": "每 5 分钟", "15m": "每 15 分钟", "30m": "每 30 分钟", "1h": "每 1 小时", "2h": "每 2 小时" },
mermaidThemeTitle: "图表主题",
mermaidThemeDescription: "跟随应用外观,或为笔记图表选择固定主题。",
markdownThemeTitle: "Markdown 源码主题",