mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-19 15:52:31 +00:00
Complete WebApp P2P local preparation
This commit is contained in:
@@ -146,6 +146,13 @@ export class WebAppRuntime {
|
|||||||
return this.paneHost;
|
return this.paneHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import local files and complete the readiness boundary needed by optional P2P.
|
||||||
|
*
|
||||||
|
* An unconfigured central remote cannot use the normal offline-scan path, so
|
||||||
|
* the explicit WebApp scan completes the same post-scan finalisation without
|
||||||
|
* treating the central remote as configured.
|
||||||
|
*/
|
||||||
async scanLocalFiles(): Promise<boolean> {
|
async scanLocalFiles(): Promise<boolean> {
|
||||||
const core = this.core;
|
const core = this.core;
|
||||||
const fileAccess = this.platformServiceModules?.vaultAccess;
|
const fileAccess = this.platformServiceModules?.vaultAccess;
|
||||||
@@ -171,7 +178,17 @@ export class WebAppRuntime {
|
|||||||
this.addLog(`Failed to import ${path}: ${String(error)}`, LOG_LEVEL_NOTICE, "scan");
|
this.addLog(`Failed to import ${path}: ${String(error)}`, LOG_LEVEL_NOTICE, "scan");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return succeeded;
|
if (!succeeded || core.services.appLifecycle.isReady()) {
|
||||||
|
return succeeded;
|
||||||
|
}
|
||||||
|
if (!(await core.services.databaseEvents.onDatabaseInitialised(false))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!(await core.services.fileProcessing.commitPendingFileEvents())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
core.services.appLifecycle.markIsReady();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
|
|||||||
@@ -15,13 +15,23 @@ const runtimeMocks = vi.hoisted(() => {
|
|||||||
const clearCache = vi.fn();
|
const clearCache = vi.fn();
|
||||||
const collectFilesOnStorage = vi.fn();
|
const collectFilesOnStorage = vi.fn();
|
||||||
const updateToDatabase = vi.fn();
|
const updateToDatabase = vi.fn();
|
||||||
|
const applicationReady = { value: false };
|
||||||
|
const isReady = vi.fn(() => applicationReady.value);
|
||||||
|
const markIsReady = vi.fn(() => {
|
||||||
|
applicationReady.value = true;
|
||||||
|
});
|
||||||
|
const onDatabaseInitialised = vi.fn();
|
||||||
|
const commitPendingFileEvents = vi.fn();
|
||||||
const p2p = {
|
const p2p = {
|
||||||
replicator: {},
|
replicator: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const serviceHub = {
|
const serviceHub = {
|
||||||
API: { addLog },
|
API: { addLog },
|
||||||
|
appLifecycle: { isReady, markIsReady },
|
||||||
control: { onLoad, onReady, onUnload },
|
control: { onLoad, onReady, onUnload },
|
||||||
|
databaseEvents: { onDatabaseInitialised },
|
||||||
|
fileProcessing: { commitPendingFileEvents },
|
||||||
setting: { currentSettings },
|
setting: { currentSettings },
|
||||||
vault: { scanVault },
|
vault: { scanVault },
|
||||||
};
|
};
|
||||||
@@ -40,12 +50,17 @@ const runtimeMocks = vi.hoisted(() => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
addLog,
|
addLog,
|
||||||
|
applicationReady,
|
||||||
options: undefined as LiveSyncBrowserServiceHubOptions<never> | undefined,
|
options: undefined as LiveSyncBrowserServiceHubOptions<never> | undefined,
|
||||||
clearCache,
|
clearCache,
|
||||||
cleanup,
|
cleanup,
|
||||||
collectFilesOnStorage,
|
collectFilesOnStorage,
|
||||||
|
commitPendingFileEvents,
|
||||||
currentSettings,
|
currentSettings,
|
||||||
getFiles,
|
getFiles,
|
||||||
|
isReady,
|
||||||
|
markIsReady,
|
||||||
|
onDatabaseInitialised,
|
||||||
onLoad,
|
onLoad,
|
||||||
onReady,
|
onReady,
|
||||||
onUnload,
|
onUnload,
|
||||||
@@ -125,9 +140,12 @@ async function waitForMicrotasks(): Promise<void> {
|
|||||||
describe("WebAppRuntime lifecycle", () => {
|
describe("WebAppRuntime lifecycle", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
runtimeMocks.applicationReady.value = false;
|
||||||
runtimeMocks.options = undefined;
|
runtimeMocks.options = undefined;
|
||||||
|
runtimeMocks.commitPendingFileEvents.mockResolvedValue(true);
|
||||||
runtimeMocks.currentSettings.mockReturnValue(unconfiguredSettings);
|
runtimeMocks.currentSettings.mockReturnValue(unconfiguredSettings);
|
||||||
runtimeMocks.getFiles.mockResolvedValue([{ path: "one.md" }]);
|
runtimeMocks.getFiles.mockResolvedValue([{ path: "one.md" }]);
|
||||||
|
runtimeMocks.onDatabaseInitialised.mockResolvedValue(true);
|
||||||
runtimeMocks.onLoad.mockResolvedValue(true);
|
runtimeMocks.onLoad.mockResolvedValue(true);
|
||||||
runtimeMocks.onReady.mockResolvedValue(undefined);
|
runtimeMocks.onReady.mockResolvedValue(undefined);
|
||||||
runtimeMocks.onUnload.mockResolvedValue(undefined);
|
runtimeMocks.onUnload.mockResolvedValue(undefined);
|
||||||
@@ -174,7 +192,7 @@ describe("WebAppRuntime lifecycle", () => {
|
|||||||
expect(runtimeMocks.onUnload).toHaveBeenCalledOnce();
|
expect(runtimeMocks.onUnload).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("imports local files for optional P2P while the main remote remains unconfigured", async () => {
|
it("finalises application readiness after importing local files for optional P2P", async () => {
|
||||||
const runtime = new WebAppRuntime(createRootHandle());
|
const runtime = new WebAppRuntime(createRootHandle());
|
||||||
await runtime.start();
|
await runtime.start();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -203,11 +221,93 @@ describe("WebAppRuntime lifecycle", () => {
|
|||||||
expect.any(Function)
|
expect.any(Function)
|
||||||
);
|
);
|
||||||
expect(runtimeMocks.updateToDatabase).toHaveBeenCalledOnce();
|
expect(runtimeMocks.updateToDatabase).toHaveBeenCalledOnce();
|
||||||
|
expect(runtimeMocks.onDatabaseInitialised).toHaveBeenCalledOnce();
|
||||||
|
expect(runtimeMocks.commitPendingFileEvents).toHaveBeenCalledOnce();
|
||||||
|
expect(runtimeMocks.markIsReady).toHaveBeenCalledOnce();
|
||||||
|
expect(runtimeMocks.updateToDatabase.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
runtimeMocks.onDatabaseInitialised.mock.invocationCallOrder[0]
|
||||||
|
);
|
||||||
|
expect(runtimeMocks.onDatabaseInitialised.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
runtimeMocks.commitPendingFileEvents.mock.invocationCallOrder[0]
|
||||||
|
);
|
||||||
|
expect(runtimeMocks.commitPendingFileEvents.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
runtimeMocks.markIsReady.mock.invocationCallOrder[0]
|
||||||
|
);
|
||||||
expect(runtimeMocks.scanVault).not.toHaveBeenCalled();
|
expect(runtimeMocks.scanVault).not.toHaveBeenCalled();
|
||||||
expect(runtimeMocks.currentSettings()).toBe(unconfiguredSettings);
|
expect(runtimeMocks.currentSettings()).toBe(unconfiguredSettings);
|
||||||
expect(unconfiguredSettings.isConfigured).toBe(false);
|
expect(unconfiguredSettings.isConfigured).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not publish readiness after a partial local-file import", async () => {
|
||||||
|
const runtime = new WebAppRuntime(createRootHandle());
|
||||||
|
await runtime.start();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
runtimeMocks.collectFilesOnStorage.mockResolvedValue({
|
||||||
|
storageFileNameMap: {
|
||||||
|
"one.md": {
|
||||||
|
path: "one.md",
|
||||||
|
stat: { ctime: 1, mtime: 1, size: 3, type: "file" },
|
||||||
|
},
|
||||||
|
"two.md": {
|
||||||
|
path: "two.md",
|
||||||
|
stat: { ctime: 2, mtime: 2, size: 3, type: "file" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
storageFileNames: ["one.md", "two.md"],
|
||||||
|
storageFileNameCI2CS: { "one.md": "one.md", "two.md": "two.md" },
|
||||||
|
});
|
||||||
|
runtimeMocks.updateToDatabase
|
||||||
|
.mockRejectedValueOnce(new Error("one.md could not be imported"))
|
||||||
|
.mockResolvedValueOnce(undefined);
|
||||||
|
|
||||||
|
await expect(runtime.scanLocalFiles()).resolves.toBe(false);
|
||||||
|
|
||||||
|
expect(runtimeMocks.updateToDatabase).toHaveBeenCalledTimes(2);
|
||||||
|
expect(runtimeMocks.onDatabaseInitialised).not.toHaveBeenCalled();
|
||||||
|
expect(runtimeMocks.commitPendingFileEvents).not.toHaveBeenCalled();
|
||||||
|
expect(runtimeMocks.markIsReady).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not publish readiness when database initialisation rejects manual preparation", async () => {
|
||||||
|
const runtime = new WebAppRuntime(createRootHandle());
|
||||||
|
await runtime.start();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
runtimeMocks.onDatabaseInitialised.mockResolvedValue(false);
|
||||||
|
|
||||||
|
await expect(runtime.scanLocalFiles()).resolves.toBe(false);
|
||||||
|
|
||||||
|
expect(runtimeMocks.onDatabaseInitialised).toHaveBeenCalledOnce();
|
||||||
|
expect(runtimeMocks.commitPendingFileEvents).not.toHaveBeenCalled();
|
||||||
|
expect(runtimeMocks.markIsReady).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not publish readiness when pending file events cannot be committed", async () => {
|
||||||
|
const runtime = new WebAppRuntime(createRootHandle());
|
||||||
|
await runtime.start();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
runtimeMocks.commitPendingFileEvents.mockResolvedValue(false);
|
||||||
|
|
||||||
|
await expect(runtime.scanLocalFiles()).resolves.toBe(false);
|
||||||
|
|
||||||
|
expect(runtimeMocks.onDatabaseInitialised).toHaveBeenCalledOnce();
|
||||||
|
expect(runtimeMocks.commitPendingFileEvents).toHaveBeenCalledOnce();
|
||||||
|
expect(runtimeMocks.markIsReady).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not repeat readiness finalisation after a successful manual scan", async () => {
|
||||||
|
const runtime = new WebAppRuntime(createRootHandle());
|
||||||
|
await runtime.start();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
await expect(runtime.scanLocalFiles()).resolves.toBe(true);
|
||||||
|
await expect(runtime.scanLocalFiles()).resolves.toBe(true);
|
||||||
|
|
||||||
|
expect(runtimeMocks.updateToDatabase).toHaveBeenCalledTimes(2);
|
||||||
|
expect(runtimeMocks.onDatabaseInitialised).toHaveBeenCalledOnce();
|
||||||
|
expect(runtimeMocks.commitPendingFileEvents).toHaveBeenCalledOnce();
|
||||||
|
expect(runtimeMocks.markIsReady).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects a failed core start after cleaning up the partial runtime", async () => {
|
it("rejects a failed core start after cleaning up the partial runtime", async () => {
|
||||||
runtimeMocks.onLoad.mockResolvedValue(false);
|
runtimeMocks.onLoad.mockResolvedValue(false);
|
||||||
const reportStatus = vi.fn();
|
const reportStatus = vi.fn();
|
||||||
|
|||||||
Reference in New Issue
Block a user