mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-06 03:35:46 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d25a00c1d | ||
|
|
277337af40 | ||
|
|
da3e2c983f | ||
|
|
185ec2c392 | ||
|
|
b1d7f3b622 |
+13
-1
@@ -528,7 +528,19 @@ Sync automatically after merging files
|
||||
Setting key: keepReplicationActiveInBackground
|
||||
Desktop only; uses more battery and network. This setting applies to continuous and periodic replication.
|
||||
|
||||
Finite remote operations, including one-shot replication, P2P peer discovery and selection, rebuilds, fetches, and remote chunk fetching, request best-effort screen-awake protection automatically and do not require this setting. That protection does not guarantee execution while Obsidian is hidden or while the operating system suspends the device.
|
||||
#### Allow sleep during synchronisation
|
||||
|
||||
Setting key: allowSleepDuringSynchronisation
|
||||
|
||||
Allow the operating system to sleep while finite synchronisation operations are in progress. This option applies on every platform and is disabled by default. When it is disabled, finite operations request best-effort screen-awake protection.
|
||||
|
||||
#### Allow sleep during synchronisation on the desktop
|
||||
|
||||
Setting key: allowSleepDuringSynchronisationOnDesktop
|
||||
|
||||
Desktop only. Allow the operating system to sleep during finite synchronisation operations even when the general option is disabled. This option is enabled by default, so periodic or event-driven synchronisation does not repeatedly prevent automatic desktop sleep. Disable it to retain best-effort screen-awake protection on desktop.
|
||||
|
||||
Setup URIs preserve both sleep preferences. Older Setup URIs which do not contain them use the defaults described above. The preferences cover finite remote operations, including one-shot replication, P2P peer discovery and selection, rebuilds, fetches, remote chunk fetching, and applying downloaded documents to the Vault. They do not control whether continuous replication remains active while Obsidian is hidden, and allowing sleep does not force the operating system to suspend the device.
|
||||
|
||||
### 3. Update thinning
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"minAppVersion": "1.7.2",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
Generated
+9
-9
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"src/apps/cli",
|
||||
@@ -23,7 +23,7 @@
|
||||
"@smithy/types": "^4.14.3",
|
||||
"@smithy/util-retry": "^4.4.5",
|
||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.3",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.4",
|
||||
"@vrtmrz/obsidian-plugin-kit": "0.1.3",
|
||||
"@vrtmrz/ui-interactions": "0.1.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
@@ -4775,9 +4775,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vrtmrz/livesync-commonlib": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.3.tgz",
|
||||
"integrity": "sha512-M6+mOlf4R60pf6mMuQdIlOoeIjOlDQn/hIRjELCkAwAcJXo5V/l8LfBCujeuvVk9FOzO3S0Q4WET9VaDHDxosw==",
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.4.tgz",
|
||||
"integrity": "sha512-qQst1QCZEgfxwpdjnjFmXE3JNy4ATKhN8ihJSUUho4mdWC1O4zyRRhmQjg0GHu5hQPZokufr2mrP4Hur8niDNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
@@ -15924,7 +15924,7 @@
|
||||
},
|
||||
"src/apps/cli": {
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"version": "1.0.3-cli",
|
||||
"version": "1.0.4-cli",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"minimatch": "^10.2.5",
|
||||
@@ -15949,7 +15949,7 @@
|
||||
},
|
||||
"src/apps/webapp": {
|
||||
"name": "livesync-webapp",
|
||||
"version": "1.0.3-webapp",
|
||||
"version": "1.0.4-webapp",
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.52"
|
||||
},
|
||||
@@ -15961,7 +15961,7 @@
|
||||
}
|
||||
},
|
||||
"src/apps/webpeer": {
|
||||
"version": "1.0.3-webpeer",
|
||||
"version": "1.0.4-webpeer",
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.52"
|
||||
},
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
@@ -177,7 +177,7 @@
|
||||
"@smithy/types": "^4.14.3",
|
||||
"@smithy/util-retry": "^4.4.5",
|
||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.3",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.4",
|
||||
"@vrtmrz/obsidian-plugin-kit": "0.1.3",
|
||||
"@vrtmrz/ui-interactions": "0.1.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"private": true,
|
||||
"version": "1.0.3-cli",
|
||||
"version": "1.0.4-cli",
|
||||
"main": "dist/index.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "livesync-webapp",
|
||||
"private": true,
|
||||
"version": "1.0.3-webapp",
|
||||
"version": "1.0.4-webapp",
|
||||
"type": "module",
|
||||
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "webpeer",
|
||||
"private": true,
|
||||
"version": "1.0.3-webpeer",
|
||||
"version": "1.0.4-webpeer",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -194,6 +194,10 @@ export function paneSyncSettings(
|
||||
),
|
||||
});
|
||||
}
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("allowSleepDuringSynchronisation");
|
||||
if (!this.services.API.isMobile()) {
|
||||
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("allowSleepDuringSynchronisationOnDesktop");
|
||||
}
|
||||
});
|
||||
|
||||
void addPanel(
|
||||
|
||||
@@ -91,6 +91,7 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
|
||||
appLifecycleService: appLifecycle,
|
||||
databaseEventService: databaseEvents,
|
||||
activityRunner: screenWakeLock,
|
||||
isMobile: () => API.isMobile(),
|
||||
});
|
||||
const replication = new ObsidianReplicationService(context, {
|
||||
APIService: API,
|
||||
|
||||
@@ -11,17 +11,53 @@ import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceC
|
||||
import { KeyValueDBService } from "@vrtmrz/livesync-commonlib/compat/services/base/KeyValueDBService";
|
||||
import { ControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/ControlService";
|
||||
import { reactiveSource } from "octagonal-wheels/dataobject/reactive";
|
||||
import type { ReplicatorServiceDependencies } from "@vrtmrz/livesync-commonlib/compat/services/base/ReplicatorService";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
type ActivityOptions = {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
type ObsidianReplicatorServiceDependencies = ReplicatorServiceDependencies & {
|
||||
isMobile: () => boolean;
|
||||
};
|
||||
|
||||
type SleepPreferenceSettings = Pick<
|
||||
ObsidianLiveSyncSettings,
|
||||
"allowSleepDuringSynchronisation" | "allowSleepDuringSynchronisationOnDesktop"
|
||||
>;
|
||||
|
||||
export function shouldAllowSleepDuringSynchronisation(settings: SleepPreferenceSettings, isMobile: boolean): boolean {
|
||||
return settings.allowSleepDuringSynchronisation || (!isMobile && settings.allowSleepDuringSynchronisationOnDesktop);
|
||||
}
|
||||
|
||||
function withSleepPreference(dependencies: ObsidianReplicatorServiceDependencies): ReplicatorServiceDependencies {
|
||||
const activityRunner = dependencies.activityRunner;
|
||||
if (!activityRunner) return dependencies;
|
||||
return {
|
||||
...dependencies,
|
||||
activityRunner: {
|
||||
async run<T>(task: () => T | PromiseLike<T>, options?: ActivityOptions): Promise<T> {
|
||||
const allowSleep = shouldAllowSleepDuringSynchronisation(
|
||||
dependencies.settingService.currentSettings(),
|
||||
dependencies.isMobile()
|
||||
);
|
||||
return allowSleep ? await task() : await activityRunner.run(task, options);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export class ObsidianDatabaseEventService extends InjectableDatabaseEventService<ObsidianServiceContext> {}
|
||||
|
||||
// InjectableReplicatorService
|
||||
export class ObsidianReplicatorService extends InjectableReplicatorService<ObsidianServiceContext> {
|
||||
readonly boundedLocalApplicationActivityCount = reactiveSource(0);
|
||||
|
||||
constructor(context: ObsidianServiceContext, dependencies: ObsidianReplicatorServiceDependencies) {
|
||||
super(context, withSleepPreference(dependencies));
|
||||
}
|
||||
|
||||
async runBoundedLocalApplicationActivity<T>(
|
||||
task: () => T | PromiseLike<T>,
|
||||
options?: ActivityOptions
|
||||
|
||||
@@ -1,25 +1,53 @@
|
||||
import { promiseWithResolvers } from "octagonal-wheels/promises";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { ObsidianReplicatorService } from "./ObsidianServices";
|
||||
import { ObsidianReplicatorService, shouldAllowSleepDuringSynchronisation } from "./ObsidianServices";
|
||||
|
||||
function handler() {
|
||||
return { addHandler: vi.fn() };
|
||||
}
|
||||
|
||||
describe("ObsidianReplicatorService", () => {
|
||||
it.each([
|
||||
{ general: false, desktop: false, mobile: false, expected: false },
|
||||
{ general: false, desktop: true, mobile: false, expected: true },
|
||||
{ general: false, desktop: true, mobile: true, expected: false },
|
||||
{ general: true, desktop: false, mobile: false, expected: true },
|
||||
{ general: true, desktop: false, mobile: true, expected: true },
|
||||
])("applies the sleep preference policy: $general/$desktop/$mobile", ({ general, desktop, mobile, expected }) => {
|
||||
expect(
|
||||
shouldAllowSleepDuringSynchronisation(
|
||||
{
|
||||
allowSleepDuringSynchronisation: general,
|
||||
allowSleepDuringSynchronisationOnDesktop: desktop,
|
||||
},
|
||||
mobile
|
||||
)
|
||||
).toBe(expected);
|
||||
});
|
||||
|
||||
it("tracks local application activity without extending remote activity", async () => {
|
||||
const activity = promiseWithResolvers<void>();
|
||||
const service = new ObsidianReplicatorService({ events: {}, translate: String } as never, {
|
||||
settingService: { onRealiseSetting: handler() },
|
||||
appLifecycleService: { onSuspending: handler(), getUnresolvedMessages: handler() },
|
||||
databaseEventService: {
|
||||
onResetDatabase: handler(),
|
||||
onDatabaseInitialisation: handler(),
|
||||
onDatabaseInitialised: handler(),
|
||||
onDatabaseHasReady: handler(),
|
||||
},
|
||||
activityRunner: { run: vi.fn(async (task: () => Promise<void>) => await task()) },
|
||||
} as never);
|
||||
const service = new ObsidianReplicatorService(
|
||||
{ events: {}, translate: String } as never,
|
||||
{
|
||||
settingService: {
|
||||
onRealiseSetting: handler(),
|
||||
currentSettings: () => ({
|
||||
allowSleepDuringSynchronisation: false,
|
||||
allowSleepDuringSynchronisationOnDesktop: false,
|
||||
}),
|
||||
},
|
||||
appLifecycleService: { onSuspending: handler(), getUnresolvedMessages: handler() },
|
||||
databaseEventService: {
|
||||
onResetDatabase: handler(),
|
||||
onDatabaseInitialisation: handler(),
|
||||
onDatabaseInitialised: handler(),
|
||||
onDatabaseHasReady: handler(),
|
||||
},
|
||||
activityRunner: { run: vi.fn(async (task: () => Promise<void>) => await task()) },
|
||||
isMobile: () => false,
|
||||
} as never
|
||||
);
|
||||
|
||||
const running = service.runBoundedLocalApplicationActivity(() => activity.promise);
|
||||
|
||||
@@ -32,4 +60,34 @@ describe("ObsidianReplicatorService", () => {
|
||||
expect(service.boundedLocalApplicationActivityCount.value).toBe(0);
|
||||
expect(service.boundedRemoteActivityCount.value).toBe(0);
|
||||
});
|
||||
|
||||
it("allows desktop sleep throughout bounded synchronisation activity when configured", async () => {
|
||||
const runWithWakeLock = vi.fn(async (task: () => Promise<void>) => await task());
|
||||
const service = new ObsidianReplicatorService(
|
||||
{ events: {}, translate: String } as never,
|
||||
{
|
||||
settingService: {
|
||||
onRealiseSetting: handler(),
|
||||
currentSettings: () => ({
|
||||
allowSleepDuringSynchronisation: false,
|
||||
allowSleepDuringSynchronisationOnDesktop: true,
|
||||
}),
|
||||
},
|
||||
appLifecycleService: { onSuspending: handler(), getUnresolvedMessages: handler() },
|
||||
databaseEventService: {
|
||||
onResetDatabase: handler(),
|
||||
onDatabaseInitialisation: handler(),
|
||||
onDatabaseInitialised: handler(),
|
||||
onDatabaseHasReady: handler(),
|
||||
},
|
||||
activityRunner: { run: runWithWakeLock },
|
||||
isMobile: () => false,
|
||||
} as never
|
||||
);
|
||||
|
||||
await service.runBoundedRemoteActivity(async () => undefined);
|
||||
await service.runBoundedLocalApplicationActivity(async () => undefined);
|
||||
|
||||
expect(runWithWakeLock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,14 @@ type LiveSyncTestPlugin = {
|
||||
core: {
|
||||
services: {
|
||||
setting: {
|
||||
currentSettings(): { versionUpFlash: string };
|
||||
currentSettings(): {
|
||||
versionUpFlash: string;
|
||||
allowSleepDuringSynchronisation: boolean;
|
||||
allowSleepDuringSynchronisationOnDesktop: boolean;
|
||||
useAdvancedMode: boolean;
|
||||
usePowerUserMode: boolean;
|
||||
useEdgeCaseMode: boolean;
|
||||
};
|
||||
getSmallConfig(key: string): string | null;
|
||||
};
|
||||
};
|
||||
@@ -196,6 +203,30 @@ async function verifyConfigDoctorFollowsCompatibilityReview(): Promise<void> {
|
||||
|
||||
async function verifyEffectiveSettings(): Promise<void> {
|
||||
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||
const sleepPreferences = await page.evaluate(() => {
|
||||
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
||||
if (plugin === undefined) throw new Error("Self-hosted LiveSync is unavailable");
|
||||
const settings = plugin.core.services.setting.currentSettings();
|
||||
return {
|
||||
general: settings.allowSleepDuringSynchronisation,
|
||||
desktop: settings.allowSleepDuringSynchronisationOnDesktop,
|
||||
advancedMode: settings.useAdvancedMode,
|
||||
powerUserMode: settings.usePowerUserMode,
|
||||
edgeCaseMode: settings.useEdgeCaseMode,
|
||||
};
|
||||
});
|
||||
if (
|
||||
sleepPreferences.general !== false ||
|
||||
sleepPreferences.desktop !== true ||
|
||||
sleepPreferences.advancedMode !== false ||
|
||||
sleepPreferences.powerUserMode !== false ||
|
||||
sleepPreferences.edgeCaseMode !== false
|
||||
) {
|
||||
throw new Error(
|
||||
`Unexpected effective sleep preferences: ${JSON.stringify(sleepPreferences)}`
|
||||
);
|
||||
}
|
||||
|
||||
await page.evaluate(() => {
|
||||
const setting = (globalThis as ObsidianTestGlobal).app?.setting;
|
||||
if (setting === undefined) throw new Error("Obsidian settings are unavailable");
|
||||
@@ -205,6 +236,16 @@ async function verifyEffectiveSettings(): Promise<void> {
|
||||
|
||||
const liveSyncSettings = page.locator(".sls-setting");
|
||||
await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
const settingsClass = await liveSyncSettings.getAttribute("class");
|
||||
for (const modeClass of [
|
||||
"menu-setting-advanced-disabled",
|
||||
"menu-setting-poweruser-disabled",
|
||||
"menu-setting-edgecase-disabled",
|
||||
]) {
|
||||
if (!settingsClass?.split(/\s+/u).includes(modeClass)) {
|
||||
throw new Error(`The settings UI did not disable ${modeClass}.`);
|
||||
}
|
||||
}
|
||||
|
||||
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Change Log"]').click();
|
||||
const removedAcknowledgements = liveSyncSettings.getByRole("button", {
|
||||
@@ -226,6 +267,35 @@ async function verifyEffectiveSettings(): Promise<void> {
|
||||
});
|
||||
|
||||
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Sync Settings"]').click();
|
||||
const generalSleepSetting = liveSyncSettings.locator(".setting-item").filter({
|
||||
has: page.getByText("Allow sleep during synchronisation", { exact: true }),
|
||||
});
|
||||
await generalSleepSetting.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
if ((await generalSleepSetting.locator(".checkbox-container.is-enabled").count()) !== 0) {
|
||||
throw new Error("The general sleep preference must be disabled by default.");
|
||||
}
|
||||
|
||||
const desktopSleepSetting = liveSyncSettings.locator(".setting-item").filter({
|
||||
has: page.getByText("Allow sleep during synchronisation on the desktop", { exact: true }),
|
||||
});
|
||||
await desktopSleepSetting.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
if ((await desktopSleepSetting.locator(".checkbox-container.is-enabled").count()) !== 1) {
|
||||
throw new Error("The desktop sleep preference must be enabled by default.");
|
||||
}
|
||||
|
||||
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Setup"]').click();
|
||||
const advancedModeSetting = liveSyncSettings.locator(".setting-item").filter({
|
||||
has: page.getByText("Enable advanced features", { exact: true }),
|
||||
});
|
||||
await advancedModeSetting.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await advancedModeSetting.locator(".checkbox-container").click();
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector(".sls-setting")?.classList.contains("menu-setting-advanced-enabled") === true,
|
||||
undefined,
|
||||
{ timeout: uiTimeoutMs }
|
||||
);
|
||||
await liveSyncSettings.locator('.sls-setting-menu-btn[title="Sync Settings"]').click();
|
||||
|
||||
const deletionPanel = liveSyncSettings
|
||||
.locator("h4.sls-setting-panel-title")
|
||||
.filter({ hasText: "Deletion Propagation" })
|
||||
@@ -271,8 +341,9 @@ async function main(): Promise<void> {
|
||||
syncAfterMerge: false,
|
||||
periodicReplication: false,
|
||||
handleFilenameCaseSensitive: false,
|
||||
useAdvancedMode: true,
|
||||
useEdgeCaseMode: true,
|
||||
useAdvancedMode: false,
|
||||
usePowerUserMode: false,
|
||||
useEdgeCaseMode: false,
|
||||
},
|
||||
});
|
||||
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
||||
|
||||
+16
@@ -12,6 +12,22 @@ Earlier releases remain available in the 0.25 release history and the legacy rel
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Synchronisation and storage
|
||||
|
||||
#### Improved
|
||||
|
||||
- Added settings to control whether finite synchronisation operations keep the screen awake. Desktop devices now allow automatic sleep by default, while mobile devices retain screen-awake protection unless the general option is enabled (#1073).
|
||||
|
||||
## 1.0.4
|
||||
|
||||
5th August, 2026
|
||||
|
||||
### Synchronisation and storage
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Testing or saving a fresh remote configuration no longer tries to access the local database while constructing a replicator, avoiding 'Local database is not ready yet' failures before local database initialisation (#1064).
|
||||
|
||||
### Command-line tool
|
||||
|
||||
#### Fixed
|
||||
|
||||
+2
-1
@@ -15,5 +15,6 @@
|
||||
"1.0.0": "1.7.2",
|
||||
"1.0.1": "1.7.2",
|
||||
"1.0.2": "1.7.2",
|
||||
"1.0.3": "1.7.2"
|
||||
"1.0.3": "1.7.2",
|
||||
"1.0.4": "1.7.2"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user