mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-23 13:02:58 +00:00
Reserve setup initialisation before enabling settings
This commit is contained in:
@@ -29,6 +29,10 @@ Internal database or settings compatibility reviews use a separate safety dialog
|
|||||||
|
|
||||||
This pane is used for setting up Self-hosted LiveSync. There are several options to set up Self-hosted LiveSync.
|
This pane is used for setting up Self-hosted LiveSync. There are several options to set up Self-hosted LiveSync.
|
||||||
|
|
||||||
|
An unconfigured installation does not open the onboarding dialogue automatically or scan the Vault into the local database. A long-lived Notice offers the onboarding action, and **Open onboarding wizard** remains available from the command palette after that Notice closes.
|
||||||
|
|
||||||
|
Choose the new-device path when this device owns the files which should initialise synchronisation. Choose the existing-device path when it should receive an established remote state. The wizard reserves Rebuild or Fetch respectively before enabling the settings and requesting a restart, so the selected initialisation runs before the ordinary start-up scan.
|
||||||
|
|
||||||
### 1. Quick Setup
|
### 1. Quick Setup
|
||||||
|
|
||||||
Most preferred method to setup Self-hosted LiveSync. You can setup Self-hosted LiveSync with a few clicks.
|
Most preferred method to setup Self-hosted LiveSync. You can setup Self-hosted LiveSync with a few clicks.
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ import type {
|
|||||||
SetupRemoteResultType,
|
SetupRemoteResultType,
|
||||||
UseSetupURIResultType,
|
UseSetupURIResultType,
|
||||||
} from "./SetupWizard/dialogs/setupDialogTypes.ts";
|
} from "./SetupWizard/dialogs/setupDialogTypes.ts";
|
||||||
|
import {
|
||||||
|
applySettingsAndFetchOnActivation,
|
||||||
|
applySettingsWithScheduledInitialisation,
|
||||||
|
} from "@/serviceFeatures/setupObsidian/setupActivationLifecycle.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User modes for onboarding and setup
|
* User modes for onboarding and setup
|
||||||
@@ -341,9 +345,9 @@ export class SetupManager extends AbstractModule {
|
|||||||
// console.dir(patch);
|
// console.dir(patch);
|
||||||
if (!activate) {
|
if (!activate) {
|
||||||
extra();
|
extra();
|
||||||
await this.applySetting(newConf, UserMode.ExistingUser);
|
const applied = await this.applySettingAndScheduleFetchOnActivation(newConf, UserMode.ExistingUser);
|
||||||
this._log("Setting Applied", LOG_LEVEL_NOTICE);
|
if (applied) this._log("Setting Applied", LOG_LEVEL_NOTICE);
|
||||||
return true;
|
return applied;
|
||||||
}
|
}
|
||||||
// Check virtual changes
|
// Check virtual changes
|
||||||
const original = { ...this.settings, P2P_DevicePeerName: "" } as ObsidianLiveSyncSettings;
|
const original = { ...this.settings, P2P_DevicePeerName: "" } as ObsidianLiveSyncSettings;
|
||||||
@@ -351,9 +355,9 @@ export class SetupManager extends AbstractModule {
|
|||||||
const isOnlyVirtualChange = isObjectDifferent(original, modified, true) === false;
|
const isOnlyVirtualChange = isObjectDifferent(original, modified, true) === false;
|
||||||
if (isOnlyVirtualChange) {
|
if (isOnlyVirtualChange) {
|
||||||
extra();
|
extra();
|
||||||
await this.applySetting(newConf, UserMode.ExistingUser);
|
const applied = await this.applySettingAndScheduleFetchOnActivation(newConf, UserMode.ExistingUser);
|
||||||
this._log("Settings from wizard applied.", LOG_LEVEL_NOTICE);
|
if (applied) this._log("Settings from wizard applied.", LOG_LEVEL_NOTICE);
|
||||||
return true;
|
return applied;
|
||||||
} else {
|
} else {
|
||||||
const userModeResult =
|
const userModeResult =
|
||||||
await this.dialogManager.openWithExplicitCancel<OutroAskUserModeResultType>(OutroAskUserMode);
|
await this.dialogManager.openWithExplicitCancel<OutroAskUserModeResultType>(OutroAskUserMode);
|
||||||
@@ -363,9 +367,12 @@ export class SetupManager extends AbstractModule {
|
|||||||
userMode = UserMode.ExistingUser;
|
userMode = UserMode.ExistingUser;
|
||||||
} else if (userModeResult === "compatible-existing-user") {
|
} else if (userModeResult === "compatible-existing-user") {
|
||||||
extra();
|
extra();
|
||||||
await this.applySetting(newConf, UserMode.ExistingUser);
|
const applied = await this.applySettingAndScheduleFetchOnActivation(
|
||||||
this._log("Settings from wizard applied.", LOG_LEVEL_NOTICE);
|
newConf,
|
||||||
return true;
|
UserMode.ExistingUser
|
||||||
|
);
|
||||||
|
if (applied) this._log("Settings from wizard applied.", LOG_LEVEL_NOTICE);
|
||||||
|
return applied;
|
||||||
} else if (userModeResult === "cancelled") {
|
} else if (userModeResult === "cancelled") {
|
||||||
this._log("User cancelled applying settings from wizard.", LOG_LEVEL_NOTICE);
|
this._log("User cancelled applying settings from wizard.", LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
@@ -382,13 +389,17 @@ export class SetupManager extends AbstractModule {
|
|||||||
}
|
}
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
extra();
|
extra();
|
||||||
await this.applySetting(newConf, userMode);
|
|
||||||
if (userMode === UserMode.NewUser) {
|
if (userMode === UserMode.NewUser) {
|
||||||
// For new users, schedule a rebuild everything.
|
// Reserve Rebuild before enabling the imported settings, so
|
||||||
await this.core.rebuilder.scheduleRebuild();
|
// the current runtime cannot begin ordinary processing first.
|
||||||
|
await applySettingsWithScheduledInitialisation(this.core.rebuilder, "rebuild", async () => {
|
||||||
|
await this.applySetting(newConf, userMode);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// For existing users, schedule a fetch.
|
// Existing data must be fetched before the ordinary startup scan.
|
||||||
await this.core.rebuilder.scheduleFetch();
|
await applySettingsWithScheduledInitialisation(this.core.rebuilder, "fetch", async () => {
|
||||||
|
await this.applySetting(newConf, userMode);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Settings applied, but may require rebuild to take effect.
|
// Settings applied, but may require rebuild to take effect.
|
||||||
@@ -430,4 +441,19 @@ export class SetupManager extends AbstractModule {
|
|||||||
await this.services.setting.applyExternalSettings(newConf, true);
|
await this.services.setting.applyExternalSettings(newConf, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async applySettingAndScheduleFetchOnActivation(
|
||||||
|
newConf: ObsidianLiveSyncSettings,
|
||||||
|
userMode: UserMode
|
||||||
|
): Promise<boolean> {
|
||||||
|
const wasConfigured = this.settings.isConfigured;
|
||||||
|
return await applySettingsAndFetchOnActivation(
|
||||||
|
this.core.rebuilder,
|
||||||
|
wasConfigured,
|
||||||
|
newConf.isConfigured,
|
||||||
|
async () => {
|
||||||
|
await this.applySetting(newConf, userMode);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,8 +98,14 @@ function createSetupManager() {
|
|||||||
const core: any = {
|
const core: any = {
|
||||||
_services: services,
|
_services: services,
|
||||||
rebuilder: {
|
rebuilder: {
|
||||||
scheduleRebuild: vi.fn(() => Promise.resolve()),
|
scheduleRebuild: vi.fn(async (prepareBeforeRestart?: () => Promise<void>) => {
|
||||||
scheduleFetch: vi.fn(() => Promise.resolve()),
|
await prepareBeforeRestart?.();
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
scheduleFetch: vi.fn(async (prepareBeforeRestart?: () => Promise<void>) => {
|
||||||
|
await prepareBeforeRestart?.();
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Object.defineProperty(core, "services", {
|
Object.defineProperty(core, "services", {
|
||||||
@@ -169,4 +175,69 @@ describe("SetupManager", () => {
|
|||||||
);
|
);
|
||||||
expect(setting.currentSettings().activeConfigurationId).toBe("legacy-couchdb");
|
expect(setting.currentSettings().activeConfigurationId).toBe("legacy-couchdb");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reserves Rebuild before saving a new-user configuration", async () => {
|
||||||
|
const { manager, setting, dialogManager, core } = createSetupManager();
|
||||||
|
setting.settings = { ...setting.currentSettings(), isConfigured: false };
|
||||||
|
const applyExternalSettings = vi.spyOn(setting, "applyExternalSettings");
|
||||||
|
dialogManager.openWithExplicitCancel.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
await manager.onConfirmApplySettingsFromWizard(
|
||||||
|
{ ...createLegacyRemoteSetting(), isConfigured: true },
|
||||||
|
UserMode.NewUser
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(core.rebuilder.scheduleRebuild).toHaveBeenCalledWith(expect.any(Function));
|
||||||
|
expect(core.rebuilder.scheduleRebuild.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
applyExternalSettings.mock.invocationCallOrder[0]
|
||||||
|
);
|
||||||
|
expect(setting.currentSettings().isConfigured).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reserves Fetch when compatible imported settings activate an unconfigured device", async () => {
|
||||||
|
const { manager, setting, dialogManager, core } = createSetupManager();
|
||||||
|
setting.settings = { ...setting.currentSettings(), isConfigured: false };
|
||||||
|
const applyExternalSettings = vi.spyOn(setting, "applyExternalSettings");
|
||||||
|
dialogManager.openWithExplicitCancel
|
||||||
|
.mockResolvedValueOnce({ ...createLegacyRemoteSetting(), isConfigured: true })
|
||||||
|
.mockResolvedValueOnce("compatible-existing-user");
|
||||||
|
|
||||||
|
await manager.onUseSetupURI(UserMode.Unknown, "mock-config://settings");
|
||||||
|
|
||||||
|
expect(core.rebuilder.scheduleFetch).toHaveBeenCalledWith(expect.any(Function));
|
||||||
|
expect(core.rebuilder.scheduleFetch.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
applyExternalSettings.mock.invocationCallOrder[0]
|
||||||
|
);
|
||||||
|
expect(setting.currentSettings().isConfigured).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies compatible settings to an already configured device without scheduling Fetch", async () => {
|
||||||
|
const { manager, setting, dialogManager, core } = createSetupManager();
|
||||||
|
setting.settings = { ...setting.currentSettings(), isConfigured: true };
|
||||||
|
dialogManager.openWithExplicitCancel
|
||||||
|
.mockResolvedValueOnce({ ...createLegacyRemoteSetting(), isConfigured: true })
|
||||||
|
.mockResolvedValueOnce("compatible-existing-user");
|
||||||
|
|
||||||
|
await manager.onUseSetupURI(UserMode.Unknown, "mock-config://settings");
|
||||||
|
|
||||||
|
expect(core.rebuilder.scheduleFetch).not.toHaveBeenCalled();
|
||||||
|
expect(setting.currentSettings().isConfigured).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not enable imported settings when the initialisation flag cannot be reserved", async () => {
|
||||||
|
const { manager, setting, dialogManager, core } = createSetupManager();
|
||||||
|
setting.settings = { ...setting.currentSettings(), isConfigured: false };
|
||||||
|
const applyExternalSettings = vi.spyOn(setting, "applyExternalSettings");
|
||||||
|
core.rebuilder.scheduleRebuild.mockResolvedValueOnce(false);
|
||||||
|
dialogManager.openWithExplicitCancel.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
await manager.onConfirmApplySettingsFromWizard(
|
||||||
|
{ ...createLegacyRemoteSetting(), isConfigured: true },
|
||||||
|
UserMode.NewUser
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(core.rebuilder.scheduleRebuild).toHaveBeenCalledWith(expect.any(Function));
|
||||||
|
expect(applyExternalSettings).not.toHaveBeenCalled();
|
||||||
|
expect(setting.currentSettings().isConfigured).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export type SetupInitialisationMode = "fetch" | "rebuild";
|
||||||
|
|
||||||
|
export interface SetupInitialisationScheduler {
|
||||||
|
scheduleFetch(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
|
||||||
|
scheduleRebuild(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reserves the next-start initialisation operation before enabling settings.
|
||||||
|
* The scheduler owns suspension, rollback, and restart ordering.
|
||||||
|
*/
|
||||||
|
export function applySettingsWithScheduledInitialisation(
|
||||||
|
scheduler: SetupInitialisationScheduler,
|
||||||
|
mode: SetupInitialisationMode,
|
||||||
|
applySettings: () => Promise<void>
|
||||||
|
): Promise<boolean> {
|
||||||
|
return mode === "fetch" ? scheduler.scheduleFetch(applySettings) : scheduler.scheduleRebuild(applySettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses Fetch only for the transition from an unconfigured device to an
|
||||||
|
* explicitly configured existing device. Ordinary edits apply immediately.
|
||||||
|
*/
|
||||||
|
export async function applySettingsAndFetchOnActivation(
|
||||||
|
scheduler: SetupInitialisationScheduler,
|
||||||
|
wasConfigured: boolean | undefined,
|
||||||
|
willBeConfigured: boolean | undefined,
|
||||||
|
applySettings: () => Promise<void>
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!wasConfigured && willBeConfigured) {
|
||||||
|
return await applySettingsWithScheduledInitialisation(scheduler, "fetch", applySettings);
|
||||||
|
}
|
||||||
|
await applySettings();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
applySettingsAndFetchOnActivation,
|
||||||
|
applySettingsWithScheduledInitialisation,
|
||||||
|
type SetupInitialisationScheduler,
|
||||||
|
} from "./setupActivationLifecycle";
|
||||||
|
|
||||||
|
function createScheduler() {
|
||||||
|
const events: string[] = [];
|
||||||
|
const scheduler: SetupInitialisationScheduler = {
|
||||||
|
scheduleFetch: vi.fn(async (prepare) => {
|
||||||
|
events.push("fetch-reserved");
|
||||||
|
await prepare?.();
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
scheduleRebuild: vi.fn(async (prepare) => {
|
||||||
|
events.push("rebuild-reserved");
|
||||||
|
await prepare?.();
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const applySettings = vi.fn(async () => {
|
||||||
|
events.push("settings-applied");
|
||||||
|
});
|
||||||
|
return { applySettings, events, scheduler };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("setup activation lifecycle", () => {
|
||||||
|
it.each([
|
||||||
|
["fetch", "fetch-reserved"],
|
||||||
|
["rebuild", "rebuild-reserved"],
|
||||||
|
] as const)("reserves %s before applying settings", async (mode, reservedEvent) => {
|
||||||
|
const { applySettings, events, scheduler } = createScheduler();
|
||||||
|
|
||||||
|
await expect(applySettingsWithScheduledInitialisation(scheduler, mode, applySettings)).resolves.toBe(true);
|
||||||
|
|
||||||
|
expect(events).toEqual([reservedEvent, "settings-applied"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reserves Fetch when existing settings activate an unconfigured device", async () => {
|
||||||
|
const { applySettings, events, scheduler } = createScheduler();
|
||||||
|
|
||||||
|
await expect(applySettingsAndFetchOnActivation(scheduler, false, true, applySettings)).resolves.toBe(true);
|
||||||
|
|
||||||
|
expect(events).toEqual(["fetch-reserved", "settings-applied"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies an ordinary configured-device edit without scheduling initialisation", async () => {
|
||||||
|
const { applySettings, events, scheduler } = createScheduler();
|
||||||
|
|
||||||
|
await expect(applySettingsAndFetchOnActivation(scheduler, true, true, applySettings)).resolves.toBe(true);
|
||||||
|
|
||||||
|
expect(events).toEqual(["settings-applied"]);
|
||||||
|
expect(scheduler.scheduleFetch).not.toHaveBeenCalled();
|
||||||
|
expect(scheduler.scheduleRebuild).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not apply settings when the scheduler cannot reserve its flag", async () => {
|
||||||
|
const { applySettings, scheduler } = createScheduler();
|
||||||
|
vi.mocked(scheduler.scheduleFetch).mockResolvedValueOnce(false);
|
||||||
|
|
||||||
|
await expect(applySettingsWithScheduledInitialisation(scheduler, "fetch", applySettings)).resolves.toBe(false);
|
||||||
|
|
||||||
|
expect(applySettings).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,6 +12,10 @@ Earlier releases remain available in the [0.25 release history](docs/releases/0.
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Wizard-driven new-device and existing-device setup now reserves Rebuild or Fetch before enabling imported settings, preventing ordinary start-up work from running ahead of the selected initialisation.
|
||||||
|
|
||||||
## 1.0.0-rc.0
|
## 1.0.0-rc.0
|
||||||
|
|
||||||
19th July, 2026
|
19th July, 2026
|
||||||
|
|||||||
Reference in New Issue
Block a user