Narrow service and command composition

This commit is contained in:
vorotamoroz committed 2026-10-29 15:00:19 +00:00
1 parent 273079e827
commit 4027417bf5
22 files changed
+928 -202

No files matched your search

+13 -16
View File
@@ -1,3 +1,4 @@
import { createCommandCheck } from "@vrtmrz/obsidian-plugin-kit/command";
import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
import { fireAndForget } from "octagonal-wheels/promises";
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
@@ -69,14 +70,12 @@ export function useBasicCommandsFeature(host: BasicCommandsHost): void {
services.API.addCommand({
id: "livesync-dump",
name: $msg("Copy database information for the active file"),
checkCallback: (checking) => {
const file = services.vault.getActiveFilePath();
if (!file) return false;
if (!checking) {
checkCallback: createCommandCheck(
() => services.vault.getActiveFilePath(),
(file) => {
fireAndForget(() => copyFileDatabaseInfo(createFileDatabaseInfoCore(host), file));
}
return true;
},
),
});
services.API.addCommand({
@@ -115,13 +114,12 @@ export function useBasicCommandsFeature(host: BasicCommandsHost): void {
services.API.addCommand({
id: "livesync-scan-files",
name: "Scan storage and database again",
checkCallback: (checking) => {
if (!services.setting.currentSettings().useAdvancedMode) return false;
if (!checking) {
checkCallback: createCommandCheck(
() => services.setting.currentSettings().useAdvancedMode,
() => {
fireAndForget(() => services.vault.scanVault(true));
}
return true;
},
),
});
services.API.addCommand({
@@ -135,13 +133,12 @@ export function useBasicCommandsFeature(host: BasicCommandsHost): void {
services.API.addCommand({
id: "livesync-abortsync",
name: "Abort synchronization immediately",
checkCallback: (checking) => {
if (!services.setting.currentSettings().useAdvancedMode) return false;
if (!checking) {
checkCallback: createCommandCheck(
() => services.setting.currentSettings().useAdvancedMode,
() => {
fireAndForget(() => services.replication.stopActiveTransfer());
}
return true;
},
),
});
return Promise.resolve(true);
@@ -228,6 +228,49 @@ describe("useBasicCommandsFeature", () => {
expect(adapter).not.toBe(fixture.host);
});
it("rechecks availability on execution and captures the current file only once", async () => {
const fixture = createFixture();
await initialise(fixture);
vi.mocked(copyFileDatabaseInfo).mockClear();
const dump = fixture.getCommand("livesync-dump");
expect(dump.checkCallback?.(true)).toBe(true);
fixture.services.vault.getActiveFilePath.mockReturnValue(undefined);
expect(dump.checkCallback?.(false)).toBe(false);
expect(copyFileDatabaseInfo).not.toHaveBeenCalled();
fixture.services.vault.getActiveFilePath
.mockReset()
.mockReturnValueOnce("selected.md")
.mockReturnValue("later.md");
expect(dump.checkCallback?.(false)).toBe(true);
await vi.waitFor(() => expect(copyFileDatabaseInfo).toHaveBeenCalledOnce());
expect(fixture.services.vault.getActiveFilePath).toHaveBeenCalledOnce();
expect(vi.mocked(copyFileDatabaseInfo).mock.calls[0][1]).toBe("selected.md");
fixture.settings.useAdvancedMode = true;
for (const id of ["livesync-scan-files", "livesync-abortsync"]) {
expect(fixture.getCommand(id).checkCallback?.(true)).toBe(true);
}
fixture.settings.useAdvancedMode = false;
for (const id of ["livesync-scan-files", "livesync-abortsync"]) {
expect(fixture.getCommand(id).checkCallback?.(false)).toBe(false);
}
expect(fixture.services.vault.scanVault).not.toHaveBeenCalled();
expect(fixture.services.replication.stopActiveTransfer).not.toHaveBeenCalled();
});
it("propagates an availability failure without running the command", async () => {
const fixture = createFixture();
await initialise(fixture);
vi.mocked(copyFileDatabaseInfo).mockClear();
const failure = new Error("Vault unavailable");
fixture.services.vault.getActiveFilePath.mockImplementation(() => {
throw failure;
});
expect(() => fixture.getCommand("livesync-dump").checkCallback?.(false)).toThrow(failure);
expect(copyFileDatabaseInfo).not.toHaveBeenCalled();
});
it("commits pending file events from the batch command", async () => {
const fixture = createFixture();
await initialise(fixture);
+19 -3
View File
@@ -1,6 +1,9 @@
import { fireAndForget } from "octagonal-wheels/promises";
import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { LiveSyncCore } from "@/main.ts";
import type {
NecessaryServices,
NecessaryServicesInterfaces,
} from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import {
COMPATIBILITY_PAUSE_SETTING_MESSAGE,
DATABASE_COMPATIBILITY_VERSION_KEY,
@@ -24,6 +27,16 @@ export interface CompatibilityReviewUi {
clearReminder(): void;
}
export type CompatibilityReviewDependencies = NecessaryServicesInterfaces<
"setting" | "config" | "vault" | "control",
never
>;
export type CompatibilityReviewHost = NecessaryServices<
"setting" | "config" | "vault" | "control" | "appLifecycle" | "API",
never
>;
export class CompatibilityReviewController {
private pause: CompatibilityPause | undefined;
private activeReview: Promise<void> | undefined;
@@ -31,7 +44,7 @@ export class CompatibilityReviewController {
private disposed = false;
constructor(
private readonly core: LiveSyncCore,
private readonly core: CompatibilityReviewDependencies,
private readonly ui: CompatibilityReviewUi,
private readonly currentVersion: number = VER
) {}
@@ -161,7 +174,10 @@ export class CompatibilityReviewController {
}
}
export function useCompatibilityReview(core: LiveSyncCore, ui: CompatibilityReviewUi): CompatibilityReviewController {
export function useCompatibilityReview(
core: CompatibilityReviewHost,
ui: CompatibilityReviewUi
): CompatibilityReviewController {
const controller = new CompatibilityReviewController(core, ui);
core.services.appLifecycle.onSettingLoaded.addHandler(() => controller.initialise());
core.services.appLifecycle.onLayoutReady.addHandler(() => {
@@ -1,4 +1,8 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest";
import type {
NecessaryServices,
NecessaryServicesInterfaces,
} from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import {
COMPATIBILITY_PAUSE_SETTING_MESSAGE,
DATABASE_COMPATIBILITY_VERSION_KEY,
@@ -101,6 +105,32 @@ function createFixture(
}
describe("compatibility review controller", () => {
it("accepts operation services without retaining an application-core contract", () => {
type Dependencies = NecessaryServicesInterfaces<"setting" | "config" | "vault" | "control", never>;
type ControllerHost = ConstructorParameters<typeof CompatibilityReviewController>[0];
expectTypeOf<Dependencies>().toExtend<ControllerHost>();
expectTypeOf<keyof ControllerHost>().toEqualTypeOf<"services" | "serviceModules">();
expectTypeOf<keyof ControllerHost["services"]>().toEqualTypeOf<"setting" | "config" | "vault" | "control">();
expectTypeOf<keyof ControllerHost["serviceModules"]>().toBeNever();
});
it("composes the review from its operation services and lifecycle and command ports", () => {
type Dependencies = NecessaryServices<
"setting" | "config" | "vault" | "control" | "appLifecycle" | "API",
never
>;
type FeatureHost = Parameters<typeof useCompatibilityReview>[0];
expectTypeOf<Dependencies>().toExtend<FeatureHost>();
expectTypeOf<keyof FeatureHost>().toEqualTypeOf<"services" | "serviceModules">();
expectTypeOf<keyof FeatureHost["services"]>().toEqualTypeOf<
"context" | "setting" | "config" | "vault" | "control" | "appLifecycle" | "API"
>();
expectTypeOf<keyof FeatureHost["serviceModules"]>().toBeNever();
expectTypeOf<ReturnType<typeof useCompatibilityReview>>().toEqualTypeOf<CompatibilityReviewController>();
});
beforeEach(() => {
vi.restoreAllMocks();
});
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, expectTypeOf, it, vi } from "vitest";
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
import { InjectableConflictService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableConflictService";
import {
@@ -15,6 +15,12 @@ import {
} from "@vrtmrz/livesync-commonlib/compat/common/types";
import { EVENT_CONFLICT_CANCELLED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { NO_INTERACTION } from "@vrtmrz/livesync-commonlib/replication";
import type { IAPIService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService";
import {
createInstanceLogFunction,
MARK_LOG_SEPARATOR,
type LogFunction,
} from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
import type { ConflictResolutionHost } from "./index";
import { createConflictResolutionOperations, useConflictResolutionFeature } from "./index";
import type { ConflictResolutionOperationsDependencies } from "./operations";
@@ -64,13 +70,15 @@ function createHarness(options: HarnessOptions = {}) {
vault: { getActiveFilePath: activeFile },
};
const serviceModules = { databaseFileAccess, fileHandler, storageAccess };
const log = createInstanceLogFunction("SF:ConflictResolution", services.API as unknown as IAPIService);
if (options.compose !== false) {
useConflictResolutionFeature({ services, serviceModules } as unknown as ConflictResolutionHost);
useConflictResolutionFeature({ services, serviceModules, log } as unknown as ConflictResolutionHost);
}
return {
addLog,
log,
conflict,
context,
databaseFileAccess,
@@ -151,6 +159,87 @@ function createOperationsHarness() {
}
describe("conflict resolution serviceFeature", () => {
it("declares a borrowed logger and only the services used by conflict resolution", () => {
expectTypeOf<keyof ConflictResolutionHost["services"]>().toEqualTypeOf<
"context" | "appLifecycle" | "conflict" | "database" | "replication" | "setting" | "vault"
>();
expectTypeOf<ConflictResolutionHost["log"]>().toEqualTypeOf<LogFunction>();
});
it("uses the injected logger for the checker and resolver without an API service", async () => {
const harness = createHarness({
compose: false,
settings: { checkConflictOnlyOnOpen: true },
activeFile: "active.md" as FilePathWithPrefix,
});
const { API, ...services } = harness.services;
const log = vi.fn<LogFunction>();
useConflictResolutionFeature({
services,
serviceModules: harness.serviceModules,
log,
} as unknown as ConflictResolutionHost);
harness.databaseFileAccess.fetchEntryMeta.mockResolvedValue(false);
await harness.conflict.queueCheckForIfOpen("other.md" as FilePathWithPrefix);
await harness.conflict.resolveByNewest("missing.md" as FilePathWithPrefix);
expect(log).toHaveBeenNthCalledWith(
1,
"other.md is conflicted, merging process has been postponed.",
LOG_LEVEL_NOTICE
);
expect(log).toHaveBeenNthCalledWith(2, "Could not get current revision of missing.md");
expect(API.addLog).not.toHaveBeenCalled();
});
it("keeps separately selected loggers isolated when legacy API services are shared", async () => {
const first = createHarness({ compose: false });
const second = createHarness({ compose: false });
const firstLog = vi.fn<LogFunction>();
const secondLog = vi.fn<LogFunction>();
const sharedAPI = first.services.API;
useConflictResolutionFeature({
services: first.services,
serviceModules: first.serviceModules,
log: firstLog,
} as unknown as ConflictResolutionHost);
useConflictResolutionFeature({
services: { ...second.services, API: sharedAPI },
serviceModules: second.serviceModules,
log: secondLog,
} as unknown as ConflictResolutionHost);
first.databaseFileAccess.fetchEntryMeta.mockResolvedValue(false);
second.databaseFileAccess.fetchEntryMeta.mockResolvedValue(false);
await first.conflict.resolveByNewest("first.md" as FilePathWithPrefix);
await second.conflict.resolveByNewest("second.md" as FilePathWithPrefix);
expect(firstLog).toHaveBeenCalledExactlyOnceWith("Could not get current revision of first.md");
expect(secondLog).toHaveBeenCalledExactlyOnceWith("Could not get current revision of second.md");
expect(sharedAPI.addLog).not.toHaveBeenCalled();
});
it("retains the prefix, level, and key selected by the host logger", async () => {
const harness = createHarness({ compose: false });
const { API, ...services } = harness.services;
useConflictResolutionFeature({
services,
serviceModules: harness.serviceModules,
log: harness.log,
} as unknown as ConflictResolutionHost);
await harness.conflict.resolveByDeletingRevision("same.md" as FilePathWithPrefix, "1-old", "same");
await harness.conflict.resolveAllConflictedFilesByNewerOnes();
expect(API.addLog).toHaveBeenCalledWith(
`[SF:ConflictResolution]${MARK_LOG_SEPARATOR} same.md has been merged automatically`,
LOG_LEVEL_INFO,
""
);
expect(API.addLog).toHaveBeenLastCalledWith("Done!", LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes");
});
it("keeps resolver operations on narrow collaborators and extension seams", async () => {
const harness = createOperationsHarness();
const path = "same.md" as FilePathWithPrefix;
@@ -557,6 +646,7 @@ describe("conflict resolution serviceFeature", () => {
useConflictResolutionFeature({
services: harness.services,
serviceModules: harness.serviceModules,
log: harness.log,
} as unknown as ConflictResolutionHost);
for (const registration of registrations) {
+16 -18
View File
@@ -1,25 +1,23 @@
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
import type { InjectableConflictService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableConflictService";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
import {
createServiceFeature,
type NecessaryServices,
} from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
import { createConflictCheckingHandlers } from "./checker";
import { createConflictResolutionOperations } from "./operations";
type ConflictResolutionServices = NecessaryServices<
"API" | "appLifecycle" | "conflict" | "database" | "replication" | "setting" | "vault",
"databaseFileAccess" | "fileHandler" | "storageAccess"
>;
export type ConflictResolutionHost = ConflictResolutionServices & {
readonly services: ConflictResolutionServices["services"] & {
readonly conflict: InjectableConflictService<ServiceContext>;
};
export type ConflictResolutionHost = NecessaryServices<
"appLifecycle" | "conflict" | "database" | "replication" | "setting" | "vault",
"databaseFileAccess" | "fileHandler" | "storageAccess",
InjectableServiceHub
> & {
readonly log: LogFunction;
};
/** Compose the host-neutral conflict checker and resolver handlers. */
export function useConflictResolutionFeature(host: ConflictResolutionHost): void {
const { services, serviceModules } = host;
const log = createInstanceLogFunction("SF:ConflictResolution", services.API);
/** Compose the host-neutral conflict handlers with the host's borrowed logger. */
export const useConflictResolutionFeature = createServiceFeature((host: ConflictResolutionHost): void => {
const { services, serviceModules, log } = host;
const operations = createConflictResolutionOperations({
events: services.context.events,
databaseFileAccess: serviceModules.databaseFileAccess,
@@ -48,7 +46,7 @@ export function useConflictResolutionFeature(host: ConflictResolutionHost): void
services.conflict.resolve.setHandler(operations.resolve);
services.conflict.resolveByNewest.setHandler(operations.resolveByNewest);
services.conflict.resolveAllConflictedFilesByNewerOnes.setHandler(operations.resolveAllConflictedFilesByNewerOnes);
}
});
export { createConflictCheckingHandlers } from "./checker";
export type { ConflictCheckingDependencies, ConflictCheckingHandlers } from "./checker";
@@ -1,9 +1,12 @@
import { getLanguage, Notice, requireApiVersion } from "@/deps";
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import { createServiceFeature } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@/common/rosetta";
import { $msg, __onMissingTranslation, setLang } from "@/common/translation";
import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
type EnableI18nHost = NecessaryServices<"setting" | "API" | "appLifecycle", never>;
function tryGetLanguage(onError: (error: unknown) => void) {
if (requireApiVersion("1.8.7")) {
try {
@@ -48,7 +51,8 @@ class ObsidianLanguageAppliedNotice {
}
}
export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API, appLifecycle } }) => {
export const enableI18nFeature = createServiceFeature(async (host: EnableI18nHost) => {
const { setting, API, appLifecycle } = host.services;
// Clear missing translation handler to avoid unnecessary warnings.
__onMissingTranslation(() => {});
let isChanged = false;
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest";
const noticeState = vi.hoisted(() => ({
instances: [] as Array<{ hide: ReturnType<typeof vi.fn>; duration: number }>,
@@ -63,6 +63,14 @@ describe("automatic display language", () => {
vi.restoreAllMocks();
});
it("declares only the services it needs and returns a boolean promise", () => {
type FeatureHost = Parameters<typeof enableI18nFeature>[0];
expectTypeOf<keyof FeatureHost["services"]>().toEqualTypeOf<"context" | "setting" | "API" | "appLifecycle">();
expectTypeOf<keyof FeatureHost["serviceModules"]>().toBeNever();
expectTypeOf<ReturnType<typeof enableI18nFeature>>().toEqualTypeOf<Promise<boolean>>();
});
it("lets start-up continue and opens translation details only from a persistent Notice", async () => {
const settings = { displayLanguage: "" };
const applyPartial = vi.fn(async (partial: Partial<typeof settings>) => Object.assign(settings, partial));
+25 -17
View File
@@ -1,3 +1,4 @@
import { createCommandCheck } from "@vrtmrz/obsidian-plugin-kit/command";
import { LOG_LEVEL_NOTICE, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
@@ -112,33 +113,40 @@ export function useSetupURIFeature(host: NecessaryServices<"API" | "UI" | "setti
host.services.API.addCommand({
id: "livesync-copysetupuri",
name: "Copy settings as a new setup URI",
checkCallback: (checking) => {
if (!host.services.setting.currentSettings().isConfigured) return false;
if (!checking) fireAndForget(copySetupURI(host, log));
return true;
},
checkCallback: createCommandCheck(
() => host.services.setting.currentSettings().isConfigured,
() => {
fireAndForget(copySetupURI(host, log));
}
),
});
host.services.API.addCommand({
id: "livesync-copysetupuri-short",
name: "Copy settings as a new setup URI (With customization sync)",
checkCallback: (checking) => {
const settings = host.services.setting.currentSettings();
if (!settings.isConfigured || !settings.usePluginSync) return false;
if (!checking) fireAndForget(copySetupURI(host, log, false));
return true;
},
checkCallback: createCommandCheck(
() => {
const settings = host.services.setting.currentSettings();
return settings.isConfigured && settings.usePluginSync;
},
() => {
fireAndForget(copySetupURI(host, log, false));
}
),
});
host.services.API.addCommand({
id: "livesync-copysetupurifull",
name: "Copy settings as a new setup URI (Full)",
checkCallback: (checking) => {
const settings = host.services.setting.currentSettings();
if (!settings.isConfigured || !settings.useAdvancedMode) return false;
if (!checking) fireAndForget(copySetupURIFull(host, log));
return true;
},
checkCallback: createCommandCheck(
() => {
const settings = host.services.setting.currentSettings();
return settings.isConfigured && settings.useAdvancedMode;
},
() => {
fireAndForget(copySetupURIFull(host, log));
}
),
});
host.services.context.events.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () =>
@@ -221,7 +221,7 @@ describe("setupObsidian/setupUri", () => {
},
UI: {
confirm: {
askString: vi.fn(() => "pass"),
askString: vi.fn(async () => false),
},
promptCopyToClipboard: vi.fn(() => true),
},
@@ -269,7 +269,7 @@ describe("setupObsidian/setupUri", () => {
},
UI: {
confirm: {
askString: vi.fn(() => "pass"),
askString: vi.fn(async () => false),
},
promptCopyToClipboard: vi.fn(() => true),
},
@@ -292,5 +292,12 @@ describe("setupObsidian/setupUri", () => {
settings.useAdvancedMode = true;
expect(command("livesync-copysetupuri-short").checkCallback?.(true)).toBe(true);
expect(command("livesync-copysetupurifull").checkCallback?.(true)).toBe(true);
expect(host.services.UI.confirm.askString).not.toHaveBeenCalled();
settings.isConfigured = false;
for (const registered of commands) expect(registered.checkCallback?.(false)).toBe(false);
expect(host.services.UI.confirm.askString).not.toHaveBeenCalled();
settings.isConfigured = true;
for (const registered of commands) expect(registered.checkCallback?.(false)).toBe(true);
expect(host.services.UI.confirm.askString).toHaveBeenCalledTimes(3);
});
});
+13 -11
View File
@@ -1,3 +1,4 @@
import { createCommandCheck } from "@vrtmrz/obsidian-plugin-kit/command";
import {
createServiceFeature,
type NecessaryServices,
@@ -46,22 +47,23 @@ export const useDatabaseDiagnosticsFeature = createServiceFeature((host: Databas
id: "analyse-database",
name: "Analyse Database Usage (advanced)",
icon: "database-search",
checkCallback: (checking) => {
if (!services.setting.currentSettings().useAdvancedMode || !services.database.isDatabaseReady())
return false;
if (!checking) void open("usage");
return true;
},
checkCallback: createCommandCheck(
() => services.setting.currentSettings().useAdvancedMode && services.database.isDatabaseReady(),
() => {
void open("usage");
}
),
});
services.API.addCommand({
id: "inspect-database-integrity",
name: $msg("Inspect database integrity"),
icon: "database-search",
checkCallback: (checking) => {
if (!services.database.isDatabaseReady()) return false;
if (!checking) void open("integrity");
return true;
},
checkCallback: createCommandCheck(
() => services.database.isDatabaseReady(),
() => {
void open("integrity");
}
),
});
return Promise.resolve(true);
});
@@ -82,6 +82,26 @@ describe("database diagnostics serviceFeature", () => {
expect(data.openDialog).not.toHaveBeenCalled();
});
it.each(["analyse-database", "inspect-database-integrity"])(
"rechecks database availability before executing %s",
async (id) => {
const data = fixture();
data.settings.useAdvancedMode = true;
for (const initialise of data.initialise) await initialise();
const command = data.commands.find((candidate) => candidate.id === id)!;
expect(command.checkCallback(true)).toBe(true);
expect(data.openDialog).not.toHaveBeenCalled();
data.setReady(false);
expect(command.checkCallback(false)).toBe(false);
expect(data.openDialog).not.toHaveBeenCalled();
data.setReady(true);
expect(command.checkCallback(false)).toBe(true);
expect(data.openDialog).toHaveBeenCalledOnce();
expect(data.openDialog.mock.calls[0][1].kind).toBe(id === "analyse-database" ? "usage" : "integrity");
data.close();
}
);
it("uses the existing dialogue manager and performs no analysis until the run choice is submitted", async () => {
vi.mocked(runDatabaseDiagnostic).mockClear();
const data = fixture();
+3 -16
View File
@@ -1,6 +1,6 @@
import { eventHub, EVENT_REQUEST_OPEN_P2P } from "@/common/events";
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import { type UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";
import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import {
P2PServerStatusPaneView,
VIEW_TYPE_P2P_SERVER_STATUS,
@@ -47,22 +47,9 @@ export function hasP2PConfiguration(settings: Partial<ObsidianLiveSyncSettings>)
*/
export function useP2PReplicatorUI(
host: NecessaryServices<
| "API"
| "appLifecycle"
| "setting"
| "vault"
| "database"
| "databaseEvents"
| "keyValueDB"
| "replication"
| "config"
| "UI"
| "replicator",
never
>,
host: NecessaryServices<"API" | "appLifecycle" | "setting" | "replicator", never>,
core: LiveSyncCore,
replicator: UseP2PReplicatorResult,
replicator: P2PServiceViews,
openInteractiveReplication: OpenInteractiveP2PReplication
) {
const api = host.services.API as {
@@ -1,4 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, expectTypeOf, it, vi } from "vitest";
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
import { eventHub, EVENT_REQUEST_OPEN_P2P } from "@/common/events";
@@ -16,6 +18,19 @@ import { useP2PReplicatorUI } from "./useP2PReplicatorUI";
const noopOpenInteractiveReplication = () => Promise.resolve(false);
describe("useP2PReplicatorUI commands", () => {
it("accepts only the UI services and focused P2P views", () => {
type Dependencies = NecessaryServices<"API" | "appLifecycle" | "setting" | "replicator", never>;
type Arguments = Parameters<typeof useP2PReplicatorUI>;
expectTypeOf<Dependencies>().toExtend<Arguments[0]>();
expectTypeOf<keyof Arguments[0]["services"]>().toEqualTypeOf<
"context" | "API" | "appLifecycle" | "setting" | "replicator"
>();
expectTypeOf<keyof Arguments[0]["serviceModules"]>().toBeNever();
expectTypeOf<P2PServiceViews>().toExtend<Arguments[2]>();
expectTypeOf<Arguments[2]>().toEqualTypeOf<P2PServiceViews>();
});
it("waits for settings to load before deciding whether to show the P2P ribbon", async () => {
let initialise: (() => Promise<unknown>) | undefined;
let settingLoaded: (() => Promise<unknown>) | undefined;