Route application diagnostics through owned log paths

This commit is contained in:
vorotamoroz
2026-07-18 16:53:20 +00:00
parent 3e22c2eda8
commit 81d27a932f
22 changed files with 154 additions and 105 deletions
+4
View File
@@ -0,0 +1,4 @@
import type { LOG_LEVEL } from "@vrtmrz/livesync-commonlib/compat/common/types";
/** Diagnostic output supplied by the Webapp host. */
export type WebAppLog = (message: unknown, level: LOG_LEVEL, key?: string) => void;
@@ -1,4 +1,4 @@
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { LOG_LEVEL_NOTICE, type FilePath, type UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IFileSystemAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import { FSAPIPathAdapter } from "./FSAPIPathAdapter";
import { FSAPITypeGuardAdapter } from "./FSAPITypeGuardAdapter";
@@ -7,6 +7,7 @@ import { FileSystemAccessStorageAdapter } from "@vrtmrz/livesync-commonlib/brows
import { FSAPIVaultAdapter } from "./FSAPIVaultAdapter";
import type { FSAPIFile, FSAPIFolder, FSAPIStat } from "./FSAPITypes";
import { shareRunningResult } from "octagonal-wheels/concurrency/lock_v2";
import type { WebAppLog } from "../WebAppLog";
/**
* Complete file system adapter implementation for FileSystem API
@@ -21,7 +22,10 @@ export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSA
private fileCache = new Map<string, FSAPIFile>();
private handleCache = new Map<string, FileSystemFileHandle>();
constructor(private rootHandle: FileSystemDirectoryHandle) {
constructor(
private rootHandle: FileSystemDirectoryHandle,
private readonly addLog: WebAppLog
) {
this.path = new FSAPIPathAdapter();
this.typeGuard = new FSAPITypeGuardAdapter();
this.conversion = new FSAPIConversionAdapter();
@@ -30,14 +34,14 @@ export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSA
}
private normalisePath(path: FilePath | string): string {
return this.path.normalisePath(path as string);
return this.path.normalisePath(path);
}
/**
* Get file handle for a given path
*/
private async getFileHandleByPath(p: FilePath | string): Promise<FileSystemFileHandle | null> {
const pathStr = p as string;
const pathStr = p;
// Check cache first
const cached = this.handleCache.get(pathStr);
@@ -213,7 +217,11 @@ export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSA
}
}
} catch (error) {
console.error(`Error scanning directory ${relativePath}:`, error);
this.addLog(
`Error scanning directory '${relativePath || "."}': ${String(error)}`,
LOG_LEVEL_NOTICE,
"fsapi-scan"
);
}
});
}
@@ -1,8 +1,9 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { storageAdapterContractCases } from "@/apps/_test/storageAdapterContract";
import { FSAPIFileSystemAdapter } from "./FSAPIFileSystemAdapter";
import { FileSystemAccessStorageAdapter } from "@vrtmrz/livesync-commonlib/browser";
import { FSAPIVaultAdapter } from "./FSAPIVaultAdapter";
import { LOG_LEVEL_NOTICE } from "@vrtmrz/livesync-commonlib/compat/common/types";
class MemoryFileHandle {
readonly kind = "file";
@@ -146,11 +147,30 @@ describe("FSAPIFileSystemAdapter path case", () => {
const root = memoryRoot as unknown as FileSystemDirectoryHandle;
const vault = new FSAPIVaultAdapter(root);
await vault.create("Calculus.md", "content");
const adapter = new FSAPIFileSystemAdapter(root);
const adapter = new FSAPIFileSystemAdapter(root, vi.fn());
await expect(adapter.getAbstractFileByPath("calculus.md")).resolves.toBeNull();
await expect(adapter.getAbstractFileByPathInsensitive("calculus.md")).resolves.toEqual(
expect.objectContaining({ path: "Calculus.md" })
);
});
it("reports scan failures through the injected Webapp log", async () => {
const root = {
name: "root",
async *entries(): AsyncIterableIterator<[string, FileSystemHandle]> {
throw new Error("scan failed");
},
} as unknown as FileSystemDirectoryHandle;
const addLog = vi.fn();
const adapter = new FSAPIFileSystemAdapter(root, addLog);
await adapter.scanDirectory();
expect(addLog).toHaveBeenCalledWith(
"Error scanning directory '.': Error: scan failed",
LOG_LEVEL_NOTICE,
"fsapi-scan"
);
});
});
-3
View File
@@ -101,7 +101,6 @@ async function startWithHistory(item: VaultHistoryItem): Promise<void> {
const handle = await historyStore.activateHistoryItem(item);
await startWithHandle(handle);
} catch (error) {
console.error("[Directory] Failed to open history vault:", error);
setStatus("error", `Failed to open saved vault: ${String(error)}`);
setBusyState(false);
}
@@ -113,7 +112,6 @@ async function startWithNewPicker(): Promise<void> {
const handle = await historyStore.pickNewVault();
await startWithHandle(handle);
} catch (error) {
console.error("[Directory] Failed to pick vault:", error);
setStatus("warning", `Vault selection was cancelled or failed: ${String(error)}`);
setBusyState(false);
}
@@ -132,7 +130,6 @@ async function initializeVaultSelector(): Promise<void> {
compatGlobal.addEventListener("load", () => {
initializeVaultSelector().catch((error) => {
console.error("Failed to initialize vault selector:", error);
setStatus("error", `Initialization failed: ${String(error)}`);
});
});
+37 -34
View File
@@ -7,7 +7,13 @@ import type { BrowserServiceHub } from "@vrtmrz/livesync-commonlib/compat/servic
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { initialiseServiceModulesFSAPI, type FSAPIServiceModules } from "./serviceModules/FSAPIServiceModules";
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import {
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
type LOG_LEVEL,
type ObsidianLiveSyncSettings,
} from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { BrowserAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/BrowserAPIService";
import type { InjectableSettingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableSettingService";
import { useOfflineScanner } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner";
@@ -59,14 +65,16 @@ class LiveSyncWebApp {
this.rootHandle = rootHandle;
}
private addLog(message: unknown, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string): void {
this.serviceHub?.API.addLog(message, level, key);
}
async initialize() {
console.log("Self-hosted LiveSync WebApp");
console.log("Initializing...");
console.log(`Vault directory: ${this.rootHandle.name}`);
// Create service context and hub
this.serviceHub = createLiveSyncBrowserServiceHub<ServiceContext>();
this.addLog("Self-hosted LiveSync WebApp", LOG_LEVEL_INFO, "initialise");
this.addLog("Initialising...", LOG_LEVEL_VERBOSE, "initialise");
this.addLog(`Vault directory: ${this.rootHandle.name}`, LOG_LEVEL_VERBOSE, "initialise");
// Setup API service
(this.serviceHub.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
@@ -79,9 +87,9 @@ class LiveSyncWebApp {
settingService.saveData.setHandler(async (data: ObsidianLiveSyncSettings) => {
try {
await this.saveSettingsToFile(data);
console.log("[Settings] Saved to .livesync/settings.json");
this.addLog("Saved to .livesync/settings.json", LOG_LEVEL_VERBOSE, "settings");
} catch (error) {
console.error("[Settings] Failed to save:", error);
this.addLog(`Failed to save settings: ${String(error)}`, LOG_LEVEL_NOTICE, "settings");
}
});
@@ -89,11 +97,11 @@ class LiveSyncWebApp {
try {
const data = await this.loadSettingsFromFile();
if (data) {
console.log("[Settings] Loaded from .livesync/settings.json");
this.addLog("Loaded from .livesync/settings.json", LOG_LEVEL_VERBOSE, "settings");
return { ...DEFAULT_SETTINGS, ...data } as ObsidianLiveSyncSettings;
}
} catch {
console.log("[Settings] Failed to load, using defaults");
this.addLog("Failed to load settings; using defaults", LOG_LEVEL_NOTICE, "settings");
}
return DEFAULT_SETTINGS as ObsidianLiveSyncSettings;
});
@@ -101,7 +109,7 @@ class LiveSyncWebApp {
// App lifecycle handlers
this.serviceHub.appLifecycle.scheduleRestart.setHandler(() => {
void (async () => {
console.log("[AppLifecycle] Restart requested");
this.addLog("Restart requested", LOG_LEVEL_INFO, "app-lifecycle");
await this.shutdown();
await this.initialize();
compatGlobal.setTimeout(() => {
@@ -152,19 +160,14 @@ class LiveSyncWebApp {
}
private async saveSettingsToFile(data: ObsidianLiveSyncSettings): Promise<void> {
try {
// Create .livesync directory if it doesn't exist
const livesyncDir = await this.rootHandle.getDirectoryHandle(SETTINGS_DIR, { create: true });
// Create .livesync directory if it does not exist
const livesyncDir = await this.rootHandle.getDirectoryHandle(SETTINGS_DIR, { create: true });
// Create/overwrite settings.json
const fileHandle = await livesyncDir.getFileHandle(SETTINGS_FILE, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(data, null, 2));
await writable.close();
} catch (error) {
console.error("[Settings] Error saving to file:", error);
throw error;
}
// Create/overwrite settings.json
const fileHandle = await livesyncDir.getFileHandle(SETTINGS_FILE, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(data, null, 2));
await writable.close();
}
private async loadSettingsFromFile(): Promise<Partial<ObsidianLiveSyncSettings> | null> {
@@ -186,47 +189,47 @@ class LiveSyncWebApp {
}
try {
console.log("[Starting] Initializing LiveSync...");
this.addLog("Initialising LiveSync...", LOG_LEVEL_INFO, "start");
const loadResult = await this.core.services.control.onLoad();
if (!loadResult) {
console.error("[Error] Failed to initialize LiveSync");
this.addLog("Failed to initialise LiveSync", LOG_LEVEL_NOTICE, "start");
this.showError("Failed to initialize LiveSync");
return;
}
await this.core.services.control.onReady();
console.log("[Ready] LiveSync is running");
this.addLog("LiveSync is running", LOG_LEVEL_INFO, "ready");
// Check if configured
const settings = this.core.services.setting.currentSettings();
if (!settings.isConfigured) {
console.warn("[Warning] LiveSync is not configured yet");
this.addLog("LiveSync is not configured yet", LOG_LEVEL_NOTICE, "configuration");
this.showWarning("Please configure CouchDB connection in settings");
} else {
console.log("[Info] LiveSync is configured and ready");
console.log(`[Info] Database: ${settings.couchDB_URI}/${settings.couchDB_DBNAME}`);
this.addLog("LiveSync is configured and ready", LOG_LEVEL_INFO, "configuration");
this.addLog(`Database: ${settings.couchDB_DBNAME}`, LOG_LEVEL_VERBOSE, "configuration");
this.showSuccess("LiveSync is ready!");
}
// Scan the directory to populate file cache
const fileAccess = this.platformServiceModules?.vaultAccess;
if (fileAccess) {
console.log("[Scanning] Scanning vault directory...");
this.addLog("Scanning vault directory...", LOG_LEVEL_VERBOSE, "scan");
await fileAccess.fsapiAdapter.scanDirectory();
const files = await fileAccess.fsapiAdapter.getFiles();
console.log(`[Scanning] Found ${files.length} files`);
this.addLog(`Found ${files.length} files`, LOG_LEVEL_VERBOSE, "scan");
}
} catch (error) {
console.error("[Error] Failed to start:", error);
this.addLog(`Failed to start: ${String(error)}`, LOG_LEVEL_NOTICE, "start");
this.showError(`Failed to start: ${error}`);
}
}
async shutdown() {
if (this.core) {
console.log("[Shutdown] Shutting down...");
this.addLog("Shutting down...", LOG_LEVEL_INFO, "shutdown");
// Stop file watching
const storageEventManager = this.platformServiceModules?.storageEventManager;
@@ -236,7 +239,7 @@ class LiveSyncWebApp {
await this.core.services.control.onUnload();
this.platformServiceModules = null;
console.log("[Shutdown] Complete");
this.addLog("Shutdown complete", LOG_LEVEL_INFO, "shutdown");
}
}
@@ -12,6 +12,8 @@ import type {
import type { FileEventItemSentinel } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager";
import type { FSAPIFile, FSAPIFolder } from "@/apps/webapp/adapters/FSAPITypes";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { WebAppLog } from "../WebAppLog";
type FileSystemObserverRecord = {
changedHandle?: FileSystemFileHandle | FileSystemDirectoryHandle;
@@ -66,6 +68,8 @@ class FSAPIPersistenceAdapter implements IStorageEventPersistenceAdapter {
private storeName = "snapshots";
private snapshotKey = "file-events";
constructor(private readonly addLog: WebAppLog) {}
private async openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
@@ -96,7 +100,7 @@ class FSAPIPersistenceAdapter implements IStorageEventPersistenceAdapter {
db.close();
} catch (error) {
console.error("Failed to save snapshot:", error);
this.addLog(`Failed to save snapshot: ${String(error)}`, LOG_LEVEL_NOTICE, "fsapi-snapshot");
}
}
@@ -128,12 +132,16 @@ class FSAPIStatusAdapter implements IStorageEventStatusAdapter {
private lastUpdate = 0;
private updateInterval = 5000; // Update every 5 seconds
constructor(private readonly addLog: WebAppLog) {}
updateStatus(status: { batched: number; processing: number; totalQueued: number }): void {
const now = Date.now();
if (now - this.lastUpdate > this.updateInterval) {
if (status.totalQueued > 0 || status.processing > 0) {
console.log(
`[StorageEventManager] Batched: ${status.batched}, Processing: ${status.processing}, Total Queued: ${status.totalQueued}`
this.addLog(
`Batched: ${status.batched}, Processing: ${status.processing}, Total queued: ${status.totalQueued}`,
LOG_LEVEL_VERBOSE,
"storage-events"
);
}
this.lastUpdate = now;
@@ -177,14 +185,17 @@ class FSAPIConverterAdapter implements IStorageEventConverterAdapter<FSAPIFile>
class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
private observer: FileSystemObserverInstance | null = null;
constructor(private rootHandle: FileSystemDirectoryHandle) {}
constructor(
private rootHandle: FileSystemDirectoryHandle,
private readonly addLog: WebAppLog
) {}
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
// Use FileSystemObserver if available (Chrome 124+)
const FileSystemObserver = (compatGlobal as GlobalWithFileSystemObserver).FileSystemObserver;
if (!FileSystemObserver) {
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
this.addLog("FileSystemObserver is not available; file watching is disabled", LOG_LEVEL_INFO, "fsapi-watch");
this.addLog("Chrome 124 or later supports real-time file watching", LOG_LEVEL_INFO, "fsapi-watch");
return Promise.resolve();
}
@@ -203,7 +214,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
continue;
}
console.log(`[FileSystemObserver] ${type}: ${relativePath}`);
this.addLog(`${type}: ${relativePath}`, LOG_LEVEL_VERBOSE, "filesystem-observer");
// Convert to our event handlers
try {
@@ -259,9 +270,10 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
}
}
} catch (error) {
console.error(
`[FileSystemObserver] Error processing ${type} event for ${relativePath}:`,
error
this.addLog(
`Error processing ${type} event for ${relativePath}: ${String(error)}`,
LOG_LEVEL_NOTICE,
"filesystem-observer"
);
}
}
@@ -269,10 +281,10 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
// Start observing
await this.observer.observe(this.rootHandle, { recursive: true });
console.log("[FSAPIWatchAdapter] FileSystemObserver started successfully");
this.addLog("FileSystemObserver started successfully", LOG_LEVEL_INFO, "fsapi-watch");
} catch (error) {
console.error("[FSAPIWatchAdapter] Failed to start FileSystemObserver:", error);
console.log("[FSAPIWatchAdapter] Falling back to manual sync mode");
this.addLog(`Failed to start FileSystemObserver: ${String(error)}`, LOG_LEVEL_NOTICE, "fsapi-watch");
this.addLog("Falling back to manual sync mode", LOG_LEVEL_INFO, "fsapi-watch");
}
return Promise.resolve();
@@ -283,9 +295,9 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
try {
this.observer.disconnect();
this.observer = null;
console.log("[FSAPIWatchAdapter] FileSystemObserver stopped");
this.addLog("FileSystemObserver stopped", LOG_LEVEL_INFO, "fsapi-watch");
} catch (error) {
console.error("[FSAPIWatchAdapter] Error stopping observer:", error);
this.addLog(`Error stopping observer: ${String(error)}`, LOG_LEVEL_NOTICE, "fsapi-watch");
}
}
}
@@ -301,11 +313,11 @@ export class FSAPIStorageEventManagerAdapter implements IStorageEventManagerAdap
readonly status: FSAPIStatusAdapter;
readonly converter: FSAPIConverterAdapter;
constructor(rootHandle: FileSystemDirectoryHandle) {
constructor(rootHandle: FileSystemDirectoryHandle, addLog: WebAppLog) {
this.typeGuard = new FSAPITypeGuardAdapter();
this.persistence = new FSAPIPersistenceAdapter();
this.watch = new FSAPIWatchAdapter(rootHandle);
this.status = new FSAPIStatusAdapter();
this.persistence = new FSAPIPersistenceAdapter(addLog);
this.watch = new FSAPIWatchAdapter(rootHandle, addLog);
this.status = new FSAPIStatusAdapter(addLog);
this.converter = new FSAPIConverterAdapter();
}
}
@@ -12,7 +12,9 @@ export class StorageEventManagerFSAPI extends StorageEventManagerBase<FSAPIStora
core: LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands>,
dependencies: StorageEventManagerBaseDependencies
) {
const adapter = new FSAPIStorageEventManagerAdapter(rootHandle);
const adapter = new FSAPIStorageEventManagerAdapter(rootHandle, (message, level, key) =>
dependencies.APIService.addLog(message, level, key)
);
super(adapter, dependencies);
this.fsapiAdapter = adapter;
this.core = core;
@@ -7,7 +7,9 @@ import { FSAPIFileSystemAdapter } from "@/apps/webapp/adapters/FSAPIFileSystemAd
*/
export class FileAccessFSAPI extends FileAccessBase<FSAPIFileSystemAdapter> {
constructor(rootHandle: FileSystemDirectoryHandle, dependencies: FileAccessBaseDependencies) {
const adapter = new FSAPIFileSystemAdapter(rootHandle);
const adapter = new FSAPIFileSystemAdapter(rootHandle, (message, level, key) =>
dependencies.APIService.addLog(message, level, key)
);
super(adapter, dependencies);
}