feat(options): add nicer sync timeout selector (closes #5513)

This commit is contained in:
Elian Doran
2026-04-10 23:12:07 +03:00
parent d3c596aaa0
commit 97256ba291
6 changed files with 96 additions and 27 deletions

View File

@@ -1,16 +1,18 @@
import { SyncTestResponse } from "@triliumnext/commons";
import { useRef } from "preact/hooks";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import toast from "../../../services/toast";
import { openInAppHelpFromUrl } from "../../../services/utils";
import Button from "../../react/Button";
import FormGroup from "../../react/FormGroup";
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
import FormText from "../../react/FormText";
import FormTextBox from "../../react/FormTextBox";
import { useTriliumOptions } from "../../react/hooks";
import RawHtml from "../../react/RawHtml";
import OptionsSection from "./components/OptionsSection";
import { useTriliumOptions } from "../../react/hooks";
import FormText from "../../react/FormText";
import server from "../../../services/server";
import toast from "../../../services/toast";
import { SyncTestResponse } from "@triliumnext/commons";
import TimeSelector from "./components/TimeSelector";
export default function SyncOptions() {
return (
@@ -18,13 +20,12 @@ export default function SyncOptions() {
<SyncConfiguration />
<SyncTest />
</>
)
);
}
export function SyncConfiguration() {
const [ options, setOptions ] = useTriliumOptions("syncServerHost", "syncServerTimeout", "syncProxy");
const [ options, setOptions ] = useTriliumOptions("syncServerHost", "syncProxy");
const syncServerHost = useRef(options.syncServerHost);
const syncServerTimeout = useRef(options.syncServerTimeout);
const syncProxy = useRef(options.syncProxy);
return (
@@ -32,13 +33,12 @@ export function SyncConfiguration() {
<form onSubmit={(e) => {
setOptions({
syncServerHost: syncServerHost.current,
syncServerTimeout: syncServerTimeout.current,
syncProxy: syncProxy.current
});
e.preventDefault();
}}>
<FormGroup name="sync-server-host" label={t("sync_2.server_address")}>
<FormTextBox
<FormTextBox
placeholder="https://<host>:<port>"
currentValue={syncServerHost.current} onChange={(newValue) => syncServerHost.current = newValue}
/>
@@ -50,27 +50,28 @@ export function SyncConfiguration() {
<RawHtml html={t("sync_2.special_value_description")} />
</>}
>
<FormTextBox
<FormTextBox
placeholder="https://<host>:<port>"
currentValue={syncProxy.current} onChange={(newValue) => syncProxy.current = newValue}
/>
</FormGroup>
<FormGroup name="sync-server-timeout" label={t("sync_2.timeout")}>
<FormTextBoxWithUnit
min={1} max={10000000} type="number"
unit={t("sync_2.timeout_unit")}
currentValue={syncServerTimeout.current} onChange={(newValue) => syncServerTimeout.current = newValue}
/>
</FormGroup>
<div style={{ display: "flex", justifyContent: "spaceBetween"}}>
<Button text={t("sync_2.save")} kind="primary" />
<Button text={t("sync_2.help")} onClick={() => openInAppHelpFromUrl("cbkrhQjrkKrh")} />
</div>
</form>
<FormGroup name="sync-server-timeout" label={t("sync_2.timeout")}>
<TimeSelector
name="sync-server-timeout"
optionValueId="syncServerTimeout"
optionTimeScaleId="syncServerTimeoutTimeScale"
minimumSeconds={1}
/>
</FormGroup>
</OptionsSection>
)
);
}
export function SyncTest() {
@@ -90,5 +91,5 @@ export function SyncTest() {
}}
/>
</OptionsSection>
)
}
);
}

View File

@@ -32,6 +32,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
"codeNoteTheme",
"syncServerHost",
"syncServerTimeout",
"syncServerTimeoutTimeScale",
"syncProxy",
"hoistedNoteId",
"mainFontSize",

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { migrateSyncTimeoutFromMilliseconds } from "./options_init.js";
describe("migrateSyncTimeoutFromMilliseconds", () => {
it("returns null when no migration is needed", () => {
// Values < 1000 are already in seconds/minutes format
expect(migrateSyncTimeoutFromMilliseconds(120)).toBeNull();
expect(migrateSyncTimeoutFromMilliseconds(500)).toBeNull();
expect(migrateSyncTimeoutFromMilliseconds(999)).toBeNull();
expect(migrateSyncTimeoutFromMilliseconds(NaN)).toBeNull();
});
it("migrates to minutes when divisible by 60", () => {
expect(migrateSyncTimeoutFromMilliseconds(60000)).toEqual({ value: 1, scale: 60 }); // 1 minute
expect(migrateSyncTimeoutFromMilliseconds(120000)).toEqual({ value: 2, scale: 60 }); // 2 minutes
expect(migrateSyncTimeoutFromMilliseconds(300000)).toEqual({ value: 5, scale: 60 }); // 5 minutes
expect(migrateSyncTimeoutFromMilliseconds(3600000)).toEqual({ value: 60, scale: 60 }); // 60 minutes
});
it("migrates to seconds when not divisible by 60", () => {
expect(migrateSyncTimeoutFromMilliseconds(1000)).toEqual({ value: 1, scale: 1 }); // 1 second
expect(migrateSyncTimeoutFromMilliseconds(45000)).toEqual({ value: 45, scale: 1 }); // 45 seconds
expect(migrateSyncTimeoutFromMilliseconds(90000)).toEqual({ value: 90, scale: 1 }); // 90 seconds
expect(migrateSyncTimeoutFromMilliseconds(150000)).toEqual({ value: 150, scale: 1 }); // 150 seconds
});
it("rounds milliseconds to nearest second", () => {
expect(migrateSyncTimeoutFromMilliseconds(120500)).toEqual({ value: 121, scale: 1 });
});
});

View File

@@ -66,7 +66,7 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts =
optionService.createOption("textNoteEditorType", "ckeditor-classic", true);
optionService.createOption("syncServerHost", opts.syncServerHost || "", false);
optionService.createOption("syncServerTimeout", "120000", false);
optionService.createOption("syncServerTimeout", "2", false); // 2 minutes (with default scale of 60)
optionService.createOption("syncProxy", opts.syncProxy || "", false);
}
@@ -74,6 +74,7 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts =
* Contains all the default options that must be initialized on new and existing databases (at startup). The value can also be determined based on other options, provided they have already been initialized.
*/
const defaultOptions: DefaultOption[] = [
{ name: "syncServerTimeoutTimeScale", value: "60", isSynced: false }, // default to Minutes
{ name: "revisionSnapshotTimeInterval", value: "600", isSynced: true },
{ name: "revisionSnapshotTimeIntervalTimeScale", value: "60", isSynced: true }, // default to Minutes
{ name: "revisionSnapshotNumberLimit", value: "-1", isSynced: true },
@@ -255,6 +256,36 @@ function initStartupOptions() {
])
);
}
// Migrate syncServerTimeout from milliseconds to seconds/minutes (for existing installations)
const syncTimeout = parseInt(optionsMap.syncServerTimeout, 10);
const migrated = migrateSyncTimeoutFromMilliseconds(syncTimeout);
if (migrated) {
optionService.setOption("syncServerTimeout", String(migrated.value));
optionService.setOption("syncServerTimeoutTimeScale", String(migrated.scale));
const unit = migrated.scale === 60 ? "minutes" : "seconds";
log.info(`Migrated syncServerTimeout from ${syncTimeout}ms to ${migrated.value} ${unit}`);
}
}
/**
* Migrates a sync timeout value from milliseconds to a value/scale pair.
* Values >= 1000 are assumed to be in milliseconds (since 1000+ seconds = 16+ minutes is unlikely).
*
* @returns The migrated value and scale, or null if no migration is needed.
*/
export function migrateSyncTimeoutFromMilliseconds(milliseconds: number): { value: number; scale: number } | null {
if (isNaN(milliseconds) || milliseconds < 1000) {
return null;
}
const seconds = Math.round(milliseconds / 1000);
// If divisible by 60, store as minutes; otherwise store as seconds
if (seconds >= 60 && seconds % 60 === 0) {
return { value: seconds / 60, scale: 60 };
}
return { value: seconds, scale: 1 };
}
function getKeyboardDefaultOptions() {

View File

@@ -1,7 +1,7 @@
"use strict";
import optionService from "./options.js";
import config from "./config.js";
import optionService from "./options.js";
import { normalizeUrl } from "./utils.js";
/*
@@ -29,6 +29,11 @@ export default {
// and we need to override it with config from config.ini
return !!syncServerHost && syncServerHost !== "disabled";
},
getSyncTimeout: () => parseInt(get("syncServerTimeout")) || 120000,
// Value is stored with a time scale, convert to milliseconds for use
getSyncTimeout: () => {
const value = parseInt(get("syncServerTimeout"), 10) || 2;
const scale = parseInt(optionService.getOption("syncServerTimeoutTimeScale"), 10) || 60;
return value * scale * 1000;
},
getSyncProxy: () => get("syncProxy")
};

View File

@@ -23,6 +23,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
theme: string;
syncServerHost: string;
syncServerTimeout: string;
syncServerTimeoutTimeScale: number;
syncProxy: string;
mainFontFamily: FontFamily;
treeFontFamily: FontFamily;