Merge shared runtime changes onto current main

This commit is contained in:
vorotamoroz committed 2026-11-01 05:14:55 +00:00
commit 6211597d2d
128 files changed
+9204 -1285

No files matched your search

+7 -21
View File
@@ -8,6 +8,7 @@ import { NodeVaultAdapter } from "./NodeVaultAdapter";
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
import { path } from "@vrtmrz/livesync-commonlib/node";
import type { CliDiagnosticReporter } from "@/apps/cli/cliOutput";
import { findCaseInsensitiveFile } from "@/apps/caseInsensitiveFileLookup";
/**
* Complete file system adapter implementation for Node.js
@@ -87,27 +88,12 @@ export class NodeFileSystemAdapter implements IFileSystemAdapter<NodeFile, NodeF
return await this.refreshResolvedFile(resolvedPath);
}
const exact = await this.getAbstractFileByPath(pathStr);
if (exact) {
return exact;
}
const lowerPath = pathStr.toLowerCase();
for (const [cachedPath, cachedFile] of this.fileCache.entries()) {
if (cachedPath.toLowerCase() === lowerPath) {
return cachedFile;
}
}
await this.scanDirectory();
for (const [cachedPath, cachedFile] of this.fileCache.entries()) {
if (cachedPath.toLowerCase() === lowerPath) {
return cachedFile;
}
}
return null;
return await findCaseInsensitiveFile(
pathStr,
() => this.getAbstractFileByPath(pathStr),
this.fileCache,
() => this.scanDirectory()
);
}
async getFiles(): Promise<NodeFile[]> {
@@ -124,6 +124,83 @@ describe("NodeFileSystemAdapter file enumeration", () => {
});
});
describe("NodeFileSystemAdapter case-insensitive path lookup", () => {
const tempDirs: string[] = [];
async function createVault(files: string[] = []) {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "livesync-cli-insensitive-"));
tempDirs.push(directory);
for (const file of files) {
await fs.mkdir(path.dirname(path.join(directory, file)), { recursive: true });
await fs.writeFile(path.join(directory, file), `content of ${file}`);
}
return { directory, adapter: new NodeFileSystemAdapter(directory) };
}
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((directory) => fs.rm(directory, { recursive: true, force: true })));
});
it("returns an exact path without scanning the directory", async () => {
const { adapter } = await createVault(["Calculus.md"]);
const scanDirectory = vi.spyOn(adapter, "scanDirectory");
await expect(adapter.getAbstractFileByPathInsensitive("Calculus.md")).resolves.toMatchObject({
path: "Calculus.md",
});
expect(scanDirectory).not.toHaveBeenCalled();
});
it("returns a cached case-insensitive match without rescanning", async () => {
const { adapter } = await createVault(["Calculus.md"]);
await adapter.scanDirectory();
const scanDirectory = vi.spyOn(adapter, "scanDirectory");
await expect(adapter.getAbstractFileByPathInsensitive("calculus.md")).resolves.toMatchObject({
path: "Calculus.md",
});
expect(scanDirectory).not.toHaveBeenCalled();
});
it("rescans once and returns a newly cached case-insensitive match", async () => {
const { adapter } = await createVault(["Calculus.md"]);
const scanDirectory = vi.spyOn(adapter, "scanDirectory");
await expect(adapter.getAbstractFileByPathInsensitive("calculus.md")).resolves.toMatchObject({
path: "Calculus.md",
});
expect(scanDirectory).toHaveBeenCalledOnce();
await expect(adapter.getAbstractFileByPath("Calculus.md")).resolves.toMatchObject({ path: "Calculus.md" });
});
it("returns null after one scan when no cached path matches", async () => {
const { adapter } = await createVault(["other.md"]);
const scanDirectory = vi.spyOn(adapter, "scanDirectory");
await expect(adapter.getAbstractFileByPathInsensitive("missing.md")).resolves.toBeNull();
expect(scanDirectory).toHaveBeenCalledOnce();
});
it("returns null and reports a directory scan failure", async () => {
const missingDirectory = path.join(os.tmpdir(), `livesync-cli-insensitive-missing-${Date.now()}`);
const reportDiagnostic = vi.fn();
const adapter = new NodeFileSystemAdapter(missingDirectory, reportDiagnostic);
const scanDirectory = vi.spyOn(adapter, "scanDirectory");
await expect(adapter.getAbstractFileByPathInsensitive("missing.md")).resolves.toBeNull();
expect(scanDirectory).toHaveBeenCalledOnce();
expect(reportDiagnostic).toHaveBeenCalledWith(
expect.stringContaining("Error scanning directory"),
expect.any(Error)
);
});
});
describe("NodeFileSystemAdapter Unicode path resolution", () => {
const tempDirs: string[] = [];
@@ -2,7 +2,6 @@ import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@vrtmrz/l
import type { FileEventItem } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IStorageEventManagerAdapter } from "@vrtmrz/livesync-commonlib/compat/managers/adapters";
import type {
IStorageEventTypeGuardAdapter,
IStorageEventPersistenceAdapter,
IStorageEventWatchAdapter,
IStorageEventStatusAdapter,
@@ -15,30 +14,7 @@ import { watch as chokidarWatch, type FSWatcher } from "chokidar";
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
import { fsPromises as fs, getNodeFileCreationTime, path, type Stats } from "@vrtmrz/livesync-commonlib/node";
import type { CliDiagnosticReporter } from "@/apps/cli/cliOutput";
/**
* CLI-specific type guard adapter
*/
class CLITypeGuardAdapter implements IStorageEventTypeGuardAdapter<NodeFile, NodeFolder> {
isFile(file: unknown): file is NodeFile {
return !!(
file &&
typeof file === "object" &&
"path" in file &&
"stat" in file &&
!(file as { isFolder?: boolean }).isFolder
);
}
isFolder(item: unknown): item is NodeFolder {
return !!(
item &&
typeof item === "object" &&
"path" in item &&
(item as { isFolder?: boolean }).isFolder === true
);
}
}
import { NodeTypeGuardAdapter } from "@/apps/cli/adapters/NodeTypeGuardAdapter";
/**
* CLI-specific persistence adapter (file-based snapshot)
@@ -211,7 +187,7 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter {
* Composite adapter for CLI StorageEventManager
*/
export class CLIStorageEventManagerAdapter implements IStorageEventManagerAdapter<NodeFile, NodeFolder> {
readonly typeGuard: CLITypeGuardAdapter;
readonly typeGuard: NodeTypeGuardAdapter;
readonly persistence: CLIPersistenceAdapter;
readonly watch: CLIWatchAdapter;
readonly status: CLIStatusAdapter;
@@ -223,7 +199,7 @@ export class CLIStorageEventManagerAdapter implements IStorageEventManagerAdapte
watchEnabled: boolean = false,
reportDiagnostic: CliDiagnosticReporter = () => undefined
) {
this.typeGuard = new CLITypeGuardAdapter();
this.typeGuard = new NodeTypeGuardAdapter();
this.persistence = new CLIPersistenceAdapter(basePath, reportDiagnostic);
this.watch = new CLIWatchAdapter(basePath, ignoreRules, watchEnabled, reportDiagnostic);
this.status = new CLIStatusAdapter();
@@ -201,3 +201,27 @@ describe("CLIStorageEventManagerAdapter", () => {
processExitSpy.mockRestore();
});
});
describe("CLIStorageEventManagerAdapter type guard", () => {
const typeGuard = new CLIStorageEventManagerAdapter("/base").typeGuard;
it.each([
{ label: "null", value: null, expected: false },
{ label: "an object without a path", value: { stat: {} }, expected: false },
{ label: "an object without a stat", value: { path: "note.md" }, expected: false },
{ label: "a file-shaped object", value: { path: "note.md", stat: {} }, expected: true },
{ label: "a folder-shaped object", value: { path: "folder", isFolder: true }, expected: false },
])("classifies $label as a file", ({ value, expected }) => {
expect(typeGuard.isFile(value)).toBe(expected);
});
it.each([
{ label: "null", value: null, expected: false },
{ label: "an object without a path", value: { isFolder: true }, expected: false },
{ label: "an object without the folder marker", value: { path: "folder" }, expected: false },
{ label: "a folder-shaped object", value: { path: "folder", isFolder: true }, expected: true },
{ label: "a file-shaped object", value: { path: "note.md", stat: {} }, expected: false },
])("classifies $label as a folder", ({ value, expected }) => {
expect(typeGuard.isFolder(value)).toBe(expected);
});
});
@@ -1,6 +1,4 @@
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
import { ServiceRebuilder } from "@vrtmrz/livesync-commonlib/compat/serviceModules/Rebuilder";
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
import { StorageAccessManager } from "@vrtmrz/livesync-commonlib/compat/managers/StorageProcessingManager";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
@@ -10,7 +8,7 @@ import { ServiceDatabaseFileAccessCLI } from "./DatabaseFileAccess";
import { StorageEventManagerCLI } from "@/apps/cli/managers/StorageEventManagerCLI";
import type { ServiceModules } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import type { IgnoreRules } from "./IgnoreRules";
import { createFileReflectionProvenance } from "@/serviceModules/FileReflectionProvenance";
import { composeSharedServiceModules } from "@/apps/serviceModules/composeSharedServiceModules";
/**
* Initialize service modules for CLI version
@@ -82,40 +80,7 @@ export function initialiseServiceModulesCLI(
vault: services.vault,
});
// File handler (platform-independent)
const fileHandler = new ServiceFileHandler({
events: services.context.events,
API: services.API,
databaseFileAccess: databaseFileAccess,
conflict: services.conflict,
setting: services.setting,
fileProcessing: services.fileProcessing,
vault: services.vault,
path: services.path,
replication: services.replication,
storageAccess: storageAccess,
fileReflectionProvenance: createFileReflectionProvenance(services.keyValueDB),
});
// Rebuilder (platform-independent)
const rebuilder = new ServiceRebuilder({
path: services.path,
events: services.context.events,
API: services.API,
database: services.database,
appLifecycle: services.appLifecycle,
setting: services.setting,
remote: services.remote,
databaseEvents: services.databaseEvents,
replication: services.replication,
replicator: services.replicator,
UI: services.UI,
vault: services.vault,
fileHandler: fileHandler,
fileProcessing: services.fileProcessing,
storageAccess: storageAccess,
control: services.control,
});
const { fileHandler, rebuilder } = composeSharedServiceModules(services, databaseFileAccess, storageAccess);
return {
rebuilder,
+2 -9
View File
@@ -1,18 +1,11 @@
import { defaultServerConditions, defaultServerMainFields, defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { fileURLToPath, fs, isBuiltin, path } from "@vrtmrz/livesync-commonlib/node";
import { fileURLToPath, isBuiltin, path } from "@vrtmrz/livesync-commonlib/node";
import { readVersion } from "../readVersion";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/");
const repoRoot = path.resolve(__dirname, "../../..");
function readVersion(filePath: string): string | undefined {
const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8"));
if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) {
return undefined;
}
return typeof parsed.version === "string" ? parsed.version : undefined;
}
const packageVersion = readVersion(path.resolve(repoRoot, "package.json"));
const manifestVersion = readVersion(path.resolve(repoRoot, "manifest.json"));
// https://vite.dev/config/