refactor (okay, i know that this cannot be merged ; too large diff. I will pick some cheery).

This commit is contained in:
vorotamoroz
2026-06-12 09:14:10 +01:00
parent b62a565c8a
commit f91b38d4ab
52 changed files with 975 additions and 318 deletions
+42
View File
@@ -0,0 +1,42 @@
// Copy package.json dependencies and devDependencies from the repo root to the target sub-apps package.json, and set their versions to match the repo root version with a suffix.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = __dirname;
const repoPackageJsonPath = path.join(repoRoot, "package.json");
const repoPackageJson = JSON.parse(fs.readFileSync(repoPackageJsonPath, "utf-8"));
const devDependenciesToCopy = repoPackageJson.devDependencies || {};
const dependenciesToCopy = repoPackageJson.dependencies || {};
const TARGET_APPS = ["cli", "webapp", "webpeer"];
for (const app of TARGET_APPS) {
const appDir = path.join(repoRoot, "src", "apps", app);
const packageJsonPath = path.join(appDir, "package.json");
if (!fs.existsSync(packageJsonPath)) {
continue;
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
packageJson.dependencies = {
...packageJson.dependencies,
...dependenciesToCopy,
};
packageJson.devDependencies = {
...packageJson.devDependencies,
...devDependenciesToCopy,
};
packageJson.version = `${repoPackageJson.version}-${app}`;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4), "utf-8");
console.log(`Applied package.json dependencies and version from repo root to ${app} package.json`);
}
console.log("\nApplied dependencies and version to all target applications.");
console.log(
"Please do not forget to pick dependencies that are actually needed in the target package.json, and remove the ones that are not needed."
);
+111 -35
View File
@@ -1,23 +1,74 @@
import tsParser from "@typescript-eslint/parser";
import tsPlugin from "@typescript-eslint/eslint-plugin";
import svelteParser from "svelte-eslint-parser";
const restrictedGlobalsOptions = [
{
name: "app",
message: "Avoid using the global app object. Instead use the reference provided by your plugin instance.",
},
"warn",
{
name: "fetch",
message: "Use the built-in `requestUrl` function instead of `fetch` for network requests in Obsidian.",
},
{
name: "localStorage",
message:
"Prefer `App#saveLocalStorage` / `App#loadLocalStorage` functions to write / read localStorage data that's unique to a vault.",
},
];
const restrictedImportsOptions = [
{
name: "axios",
message: "Use the built-in `requestUrl` function instead of `axios`.",
},
{
name: "superagent",
message: "Use the built-in `requestUrl` function instead of `superagent`.",
},
{
name: "got",
message: "Use the built-in `requestUrl` function instead of `got`.",
},
{
name: "ofetch",
message: "Use the built-in `requestUrl` function instead of `ofetch`.",
},
{
name: "ky",
message: "Use the built-in `requestUrl` function instead of `ky`.",
},
{
name: "node-fetch",
message: "Use the built-in `requestUrl` function instead of `node-fetch`.",
},
{
name: "moment",
message: "The 'moment' package is bundled with Obsidian. Please import it from 'obsidian' instead.",
},
];
const warnWhileDev = "off";
export const tsBaseRules = {
/**
* @type {import("eslint").Linter.RulesRecord}
*/
export const baseRules = {
// -- Base rules (turned off in favour of TS specific versions or explicitly disabled).
"no-unused-vars": "off",
"no-unused-labels": "off",
"no-prototype-builtins": "off",
"require-await": "off",
// -- TypeScript specific rules
// -- TypeScript specific rules (Gradual adoption of stricter rules, currently set to 'warn' for a while).
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-redundant-type-constituents": "warn",
// -- TypeScript specific rules
// @typescript-eslint/no-unsafe-* rules and @typescript-eslint/no-explicit-any:
// This project contains a lot of library-sh code where the use of `any` is often necessary and justified.
// Rules is now set to 'off' for a while.
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
// -- Reasonable rules.
"@typescript-eslint/no-deprecated": warnWhileDev,
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
"@typescript-eslint/ban-ts-comment": "off",
@@ -30,38 +81,63 @@ export const tsBaseRules = {
// -- General rules
"no-async-promise-executor": warnWhileDev,
"no-constant-condition": ["error", { checkLoops: false }],
// -- Disabled rules
// no-undef: This option breaks the global declarations for the library files and is not worth the effort to fix at this time.
"no-undef": "off",
};
export const tsBaseConfig = {
files: ["**/*.ts"],
plugins: {
"@typescript-eslint": tsPlugin,
},
languageOptions: {
parser: tsParser,
parserOptions: {
project: "./tsconfig.json",
rootDir: "./",
},
},
rules: tsBaseRules,
};
/**
* @type {import("eslint").Linter.RulesRecord}
*/
export const obsidianRules = {
// -- Obsidian rules
// obsidianmd/no-unsupported-api: usually this project checks for API support at runtime, so this rule is not critical but can be helpful to catch potential issues.
"obsidianmd/no-unsupported-api": warnWhileDev,
export const svelteBaseConfig = {
files: ["**/*.svelte"],
plugins: {
"@typescript-eslint": tsPlugin,
},
languageOptions: {
parser: svelteParser,
parserOptions: {
parser: tsParser,
extraFileExtensions: [".svelte"],
rootDir: "./",
},
},
rules: {
"no-unused-vars": "off",
},
// -- Plugin specific overrides
"obsidianmd/rule-custom-message": "off",
"obsidianmd/ui/sentence-case": "off",
"obsidianmd/no-plugin-as-component": "off",
// -- Temporary overrides for migration
"obsidianmd/no-static-styles-assignment": "off",
};
/**
* @type {(base:string) => import("eslint").Linter.RulesRecord}
*/
export const ImportAliasRules = (base) => ({
"@dword-design/import-alias/prefer-alias": [
"error",
{
aliasForSubpaths: true,
alias: {
"@": `${base}/src`,
"@lib": `${base}/src/lib/src`,
},
},
],
});
/**
* @type {import("eslint").Linter.RulesRecord}
*/
export const CommunityReviewRecommendedRules = {
"no-unused-vars": "off",
"no-prototype-bultins": "off",
"no-self-compare": "warn",
"no-eval": "error",
"no-implied-eval": "error",
"prefer-const": "off",
"no-implicit-globals": "error",
"no-console": "off", // overridden by obsidianmd/rule-custom-message
"no-restricted-globals": ["error", ...restrictedGlobalsOptions],
"no-restricted-imports": ["error", ...restrictedImportsOptions],
"no-alert": "error",
"no-undef": "error",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-deprecated": "error",
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-explicit-any": ["error", { fixToUnknown: true }],
// "import/no-nodejs-modules": "off",
// "import/no-extraneous-dependencies": "error",
};
+27 -41
View File
@@ -5,7 +5,7 @@ import { defineConfig, globalIgnores } from "eslint/config";
import * as sveltePlugin from "eslint-plugin-svelte";
import svelteParser from "svelte-eslint-parser";
import importAlias from "@dword-design/eslint-plugin-import-alias";
import { tsBaseConfig, svelteBaseConfig } from "./eslint.config.common.mjs";
import { baseRules, ImportAliasRules, obsidianRules } from "./eslint.config.common.mjs";
const warnWhileDev = "off"; // Change to "warn" to enable warnings for rules that are currently disabled.
export default defineConfig([
globalIgnores([
@@ -58,59 +58,45 @@ export default defineConfig([
...obsidianmd.configs.recommended,
importAlias.configs.recommended,
{
...tsBaseConfig,
files: ["**/*.ts"],
// ignores:["src/lib/**/*.ts"], // Exclude library files from root linting (they have different environments and rules).
languageOptions: {
...tsBaseConfig.languageOptions,
globals: { ...globals.browser, PouchDB: "readonly" },
parser: tsParser,
parserOptions: {
project: "./tsconfig.json",
rootDir: "./",
},
},
plugins: {
...tsBaseConfig.plugins,
linterOptions: {
reportUnusedDisableDirectives: false,
},
rules: {
...tsBaseConfig.rules,
// -- Obsidian rules
"obsidianmd/no-unsupported-api": warnWhileDev,
"obsidianmd/rule-custom-message": "off",
"obsidianmd/ui/sentence-case": "off",
"obsidianmd/no-plugin-as-component": "off",
"obsidianmd/no-static-styles-assignment": "off",
...baseRules,
...obsidianRules,
// -- Project specific rules
"@dword-design/import-alias/prefer-alias": [
"error",
{
aliasForSubpaths: true,
alias: {
"@": "./src",
"@lib": "./src/lib/src",
},
},
],
...ImportAliasRules("."),
},
},
{
...svelteBaseConfig,
files: ["**/*.svelte"],
languageOptions: {
...svelteBaseConfig.languageOptions,
globals: { ...globals.browser, PouchDB: "readonly" },
},
plugins: {
...svelteBaseConfig.plugins,
parser: svelteParser,
parserOptions: {
parser: tsParser,
extraFileExtensions: [".svelte"],
project: "./tsconfig.json",
rootDir: "./",
},
},
rules: {
...svelteBaseConfig.rules,
"obsidianmd/no-plugin-as-component": "off",
"obsidianmd/ui/sentence-case": "off",
"@dword-design/import-alias/prefer-alias": [
"error",
{
aliasForSubpaths: true,
alias: {
"@": "./src",
"@lib": "./src/lib/src",
},
},
],
// no-unused-vars:
// Svelte template's declarations have a lot of false positives and the rule is not worth the effort to fix at this time.
// it may improve in the future with some options as like ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],]
"no-unused-vars": "off",
...obsidianRules,
...ImportAliasRules("."),
},
},
]);
+163 -4
View File
@@ -16624,22 +16624,181 @@
},
"src/apps/webapp": {
"name": "livesync-webapp",
"version": "0.0.1",
"version": "0.25.74-webapp",
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
"@smithy/fetch-http-handler": "^5.3.10",
"@smithy/md5-js": "^4.2.9",
"@smithy/middleware-apply-body-checksum": "^4.3.9",
"@smithy/protocol-http": "^5.3.9",
"@smithy/querystring-builder": "^4.2.9",
"@smithy/types": "^4.14.3",
"@smithy/util-retry": "^4.4.5",
"@trystero-p2p/nostr": "^0.24.0",
"chokidar": "^4.0.0",
"diff-match-patch": "^1.0.5",
"fflate": "^0.8.2",
"idb": "^8.0.3",
"markdown-it": "^14.1.1",
"minimatch": "^10.2.2",
"obsidian": "^1.12.3",
"octagonal-wheels": "^0.1.46",
"pouchdb-adapter-leveldb": "^9.0.0",
"qrcode-generator": "^1.4.4",
"werift": "^0.23.0",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
},
"devDependencies": {
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
"@eslint/js": "^9.39.3",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tsconfig/svelte": "^5.0.8",
"@types/deno": "^2.5.0",
"@types/diff-match-patch": "^1.0.36",
"@types/markdown-it": "^14.1.2",
"@types/micromatch": "^4.0.10",
"@types/node": "^24.10.13",
"@types/pouchdb": "^6.4.2",
"@types/pouchdb-adapter-http": "^6.1.6",
"@types/pouchdb-adapter-idb": "^6.1.7",
"@types/pouchdb-browser": "^6.1.5",
"@types/pouchdb-core": "^7.0.15",
"@types/pouchdb-mapreduce": "^6.1.10",
"@types/pouchdb-replication": "^6.4.7",
"@types/transform-pouch": "^1.0.6",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@vitest/browser": "^4.1.8",
"@vitest/browser-playwright": "^4.1.8",
"@vitest/coverage-v8": "^4.1.8",
"dotenv-cli": "^11.0.0",
"esbuild": "0.25.0",
"esbuild-plugin-inline-worker": "^0.1.1",
"esbuild-svelte": "^0.9.4",
"eslint": "^9.39.3",
"eslint-plugin-obsidianmd": "^0.3.0",
"eslint-plugin-svelte": "^3.15.0",
"events": "^3.3.0",
"globals": "^14.0.0",
"playwright": "^1.58.2",
"postcss": "^8.5.6",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-adapter-idb": "^9.0.0",
"pouchdb-adapter-indexeddb": "^9.0.0",
"pouchdb-adapter-memory": "^9.0.0",
"pouchdb-core": "^9.0.0",
"pouchdb-errors": "^9.0.0",
"pouchdb-find": "^9.0.0",
"pouchdb-mapreduce": "^9.0.0",
"pouchdb-merge": "^9.0.0",
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"prettier": "3.8.1",
"rollup-plugin-copy": "^3.5.0",
"svelte": "5.41.1",
"svelte-check": "^4.4.3",
"svelte-eslint-parser": "^1.8.0",
"svelte-preprocess": "^6.0.3",
"terser": "^5.39.0",
"tinyglobby": "^0.2.15",
"transform-pouch": "^2.0.0",
"tsx": "^4.21.0",
"typescript": "5.9.3",
"vite": "^7.3.1"
"typescript-eslint": "^8.61.0",
"vite": "^7.3.1",
"vite-plugin-istanbul": "^8.0.0",
"vitest": "^4.1.8",
"webdriverio": "^9.27.0",
"yaml": "^2.8.2"
}
},
"src/apps/webpeer": {
"version": "0.0.0",
"version": "0.25.74-webpeer",
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
"@smithy/fetch-http-handler": "^5.3.10",
"@smithy/md5-js": "^4.2.9",
"@smithy/middleware-apply-body-checksum": "^4.3.9",
"@smithy/protocol-http": "^5.3.9",
"@smithy/querystring-builder": "^4.2.9",
"@smithy/types": "^4.14.3",
"@smithy/util-retry": "^4.4.5",
"@trystero-p2p/nostr": "^0.24.0",
"chokidar": "^4.0.0",
"diff-match-patch": "^1.0.5",
"fflate": "^0.8.2",
"idb": "^8.0.3",
"markdown-it": "^14.1.1",
"minimatch": "^10.2.2",
"obsidian": "^1.12.3",
"octagonal-wheels": "^0.1.46",
"pouchdb-adapter-leveldb": "^9.0.0",
"qrcode-generator": "^1.4.4",
"werift": "^0.23.0",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
},
"devDependencies": {
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
"@eslint/js": "^9.39.3",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tsconfig/svelte": "^5.0.8",
"@types/deno": "^2.5.0",
"@types/diff-match-patch": "^1.0.36",
"@types/markdown-it": "^14.1.2",
"@types/micromatch": "^4.0.10",
"@types/node": "^24.10.13",
"@types/pouchdb": "^6.4.2",
"@types/pouchdb-adapter-http": "^6.1.6",
"@types/pouchdb-adapter-idb": "^6.1.7",
"@types/pouchdb-browser": "^6.1.5",
"@types/pouchdb-core": "^7.0.15",
"@types/pouchdb-mapreduce": "^6.1.10",
"@types/pouchdb-replication": "^6.4.7",
"@types/transform-pouch": "^1.0.6",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@vitest/browser": "^4.1.8",
"@vitest/browser-playwright": "^4.1.8",
"@vitest/coverage-v8": "^4.1.8",
"dotenv-cli": "^11.0.0",
"esbuild": "0.25.0",
"esbuild-plugin-inline-worker": "^0.1.1",
"esbuild-svelte": "^0.9.4",
"eslint": "^9.39.3",
"eslint-plugin-obsidianmd": "^0.3.0",
"eslint-plugin-svelte": "^3.15.0",
"events": "^3.3.0",
"globals": "^14.0.0",
"playwright": "^1.58.2",
"postcss": "^8.5.6",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-adapter-idb": "^9.0.0",
"pouchdb-adapter-indexeddb": "^9.0.0",
"pouchdb-adapter-memory": "^9.0.0",
"pouchdb-core": "^9.0.0",
"pouchdb-errors": "^9.0.0",
"pouchdb-find": "^9.0.0",
"pouchdb-mapreduce": "^9.0.0",
"pouchdb-merge": "^9.0.0",
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"prettier": "3.8.1",
"rollup-plugin-copy": "^3.5.0",
"svelte": "5.41.1",
"svelte-check": "^4.4.3",
"svelte-eslint-parser": "^1.8.0",
"svelte-preprocess": "^6.0.3",
"terser": "^5.39.0",
"tinyglobby": "^0.2.15",
"transform-pouch": "^2.0.0",
"tsx": "^4.21.0",
"typescript": "5.9.3",
"vite": "^7.3.1"
"typescript-eslint": "^8.61.0",
"vite": "^7.3.1",
"vite-plugin-istanbul": "^8.0.0",
"vitest": "^4.1.8",
"webdriverio": "^9.27.0",
"yaml": "^2.8.2"
}
}
}
+2 -1
View File
@@ -115,7 +115,8 @@ cd obsidian-livesync
# If you already cloned without submodules, run this once instead
git submodule update --init --recursive
# Install dependencies from the repository root
# Install dependencies (now the CLI is a on the same monorepo, shared dependencies will be hoisted to the root node_modules)
cd src/apps/cli
npm install
# Build the CLI from its package directory
@@ -2,6 +2,7 @@ import * as path from "path";
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/models/fileaccess.type";
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
import type { NodeFile, NodeFolder } from "./NodeTypes";
import type { FilePathWithPrefix } from "@lib/common/models/db.type";
/**
* Conversion adapter implementation for Node.js
@@ -22,7 +23,7 @@ export class NodeConversionAdapter implements IConversionAdapter<NodeFile, NodeF
path: folder.path,
isFolder: true,
children: [],
parent: path.dirname(folder.path) as any,
parent: path.dirname(folder.path) as FilePathWithPrefix,
};
}
}
@@ -35,7 +35,7 @@ export class NodeFileSystemAdapter implements IFileSystemAdapter<NodeFile, NodeF
}
private normalisePath(p: FilePath | string): string {
return this.path.normalisePath(p as string);
return this.path.normalisePath(p);
}
async getAbstractFileByPath(p: FilePath | string): Promise<NodeFile | null> {
@@ -77,7 +77,7 @@ export class NodeFileSystemAdapter implements IFileSystemAdapter<NodeFile, NodeF
}
async statFromNative(file: NodeFile): Promise<UXStat> {
return file.stat;
return await Promise.resolve(file.stat);
}
async reconcileInternalFile(p: string): Promise<void> {
+10 -4
View File
@@ -5,11 +5,17 @@ import type { NodeFile, NodeFolder } from "./NodeTypes";
* Type guard adapter implementation for Node.js
*/
export class NodeTypeGuardAdapter implements ITypeGuardAdapter<NodeFile, NodeFolder> {
isFile(file: any): file is NodeFile {
return file && typeof file === "object" && "path" in file && "stat" in file && !file.isFolder;
isFile(file: unknown): file is NodeFile {
return !!(
file &&
typeof file === "object" &&
"path" in file &&
"stat" in file &&
!("isFolder" in file && file.isFolder === true)
);
}
isFolder(item: any): item is NodeFolder {
return item && typeof item === "object" && "path" in item && item.isFolder === true;
isFolder(item: unknown): item is NodeFolder {
return !!(item && typeof item === "object" && "path" in item && "isFolder" in item && item.isFolder === true);
}
}
+5 -4
View File
@@ -2,7 +2,8 @@ import * as fs from "fs/promises";
import * as path from "path";
import type { UXDataWriteOptions } from "@lib/common/models/fileaccess.type";
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
import type { NodeFile, NodeFolder } from "./NodeTypes";
import type { FilePath } from "@lib/common/types";
/**
* Vault adapter implementation for Node.js
@@ -70,7 +71,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
const stat = await fs.stat(fullPath);
return {
path: p as any,
path: p as FilePath,
stat: {
size: stat.size,
mtime: Math.floor(stat.mtimeMs),
@@ -93,7 +94,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
const stat = await fs.stat(fullPath);
return {
path: p as any,
path: p as FilePath,
stat: {
size: stat.size,
mtime: Math.floor(stat.mtimeMs),
@@ -118,7 +119,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
await this.delete(file, force);
}
trigger(name: string, ...data: any[]): any {
trigger(name: string, ...data: unknown[]): unknown {
// No-op in CLI version (no event system)
return undefined;
}
+9 -9
View File
@@ -1,7 +1,7 @@
import { P2P_DEFAULT_SETTINGS } from "@lib/common/models/setting.const.defaults";
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
import type { ServiceContext } from "@lib/services/base/ServiceBase";
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
type CLIP2PPeer = {
peerId: string;
name: string;
@@ -19,7 +19,7 @@ export function parseTimeoutSeconds(value: string, commandName: string): number
return timeoutSec;
}
function validateP2PSettings(core: LiveSyncBaseCore<ServiceContext, any>) {
function validateP2PSettings(core: LiveSyncBaseCore<ServiceContext, never>) {
const settings = core.services.setting.currentSettings();
if (!settings.P2P_Enabled) {
throw new Error("P2P is disabled in settings (P2P_Enabled=false)");
@@ -31,7 +31,7 @@ function validateP2PSettings(core: LiveSyncBaseCore<ServiceContext, any>) {
settings.P2P_IsHeadless = true;
}
async function createReplicator(core: LiveSyncBaseCore<ServiceContext, any>): Promise<LiveSyncTrysteroReplicator> {
async function createReplicator(core: LiveSyncBaseCore<ServiceContext, never>): Promise<LiveSyncTrysteroReplicator> {
validateP2PSettings(core);
const replicator = await core.services.replicator.getNewReplicator();
if (!replicator) {
@@ -50,7 +50,7 @@ function getSortedPeers(replicator: LiveSyncTrysteroReplicator): CLIP2PPeer[] {
}
export async function collectPeers(
core: LiveSyncBaseCore<ServiceContext, any>,
core: LiveSyncBaseCore<ServiceContext, never>,
timeoutSec: number
): Promise<CLIP2PPeer[]> {
const replicator = await createReplicator(core);
@@ -79,7 +79,7 @@ function resolvePeer(peers: CLIP2PPeer[], peerToken: string): CLIP2PPeer | undef
}
export async function syncWithPeer(
core: LiveSyncBaseCore<ServiceContext, any>,
core: LiveSyncBaseCore<ServiceContext, never>,
peerToken: string,
timeoutSec: number
): Promise<CLIP2PPeer> {
@@ -105,11 +105,11 @@ export async function syncWithPeer(
const pullResult = await replicator.replicateFrom(targetPeer.peerId, false);
if (pullResult && "error" in pullResult && pullResult.error) {
throw pullResult.error;
throw pullResult.error as Error;
}
const pushResult = (await replicator.requestSynchroniseToPeer(targetPeer.peerId)) as any;
const pushResult = await replicator.requestSynchroniseToPeer(targetPeer.peerId);
if (!pushResult || pushResult.ok !== true) {
throw pushResult?.error ?? new Error("P2P sync failed while requesting remote sync");
throw (pushResult?.error as Error) ?? new Error("P2P sync failed while requesting remote sync");
}
return targetPeer;
@@ -118,7 +118,7 @@ export async function syncWithPeer(
}
}
export async function openP2PHost(core: LiveSyncBaseCore<ServiceContext, any>): Promise<LiveSyncTrysteroReplicator> {
export async function openP2PHost(core: LiveSyncBaseCore<ServiceContext, never>): Promise<LiveSyncTrysteroReplicator> {
const replicator = await createReplicator(core);
await replicator.open();
return replicator;
+31 -17
View File
@@ -15,6 +15,9 @@ import * as path from "path";
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
import type { CLICommandContext, CLIOptions } from "./types";
import { promptForPassphrase, readStdinAsUtf8, toArrayBuffer, toDatabaseRelativePath } from "./utils";
import type { EntryMilestoneInfo } from "@lib/common/models/db.definition";
import type { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
import type { LiveSyncJournalReplicator } from "@lib/replication/journal/LiveSyncJournalReplicator";
function redactConnectionString(uri: string): string {
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
@@ -35,16 +38,20 @@ async function verifyRemoteState(
}
try {
let milestone: any;
let milestone: EntryMilestoneInfo | null | false = null;
if (settings.remoteType === REMOTE_COUCHDB) {
const dbRet = await (replicator as any).connectRemoteCouchDBWithSetting(settings, false, true);
const dbRet = await (replicator as LiveSyncCouchDBReplicator).connectRemoteCouchDBWithSetting(
settings,
false,
true
);
if (typeof dbRet === "string") {
process.stderr.write(`[Verification] Failed to connect to remote CouchDB: ${dbRet}\n`);
return false;
}
milestone = await dbRet.db.get(MILESTONE_DOCID);
} else if (settings.remoteType === REMOTE_MINIO) {
milestone = await (replicator as any).client.downloadJson("_00000000-milestone.json");
milestone = await (replicator as LiveSyncJournalReplicator).client.downloadJson("_00000000-milestone.json");
}
if (milestone) {
@@ -59,8 +66,10 @@ async function verifyRemoteState(
process.stderr.write("[Verification] Milestone document not found on remote.\n");
return false;
}
} catch (e: any) {
process.stderr.write(`[Verification] Failed to fetch milestone document: ${e?.message || e}\n`);
} catch (e: unknown) {
process.stderr.write(
`[Verification] Failed to fetch milestone document: ${e instanceof Error ? e.message : String(e)}\n`
);
return false;
}
}
@@ -71,7 +80,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
await core.services.control.activated;
if (options.command === "daemon") {
const log = (msg: unknown) => console.error(`[Daemon] ${msg}`);
const log = (msg: unknown) => console.error(`[Daemon] ${msg instanceof Error ? msg.message : String(msg)}`);
// Skip the config mismatch dialog — the daemon cannot resolve it interactively
// and the default "Dismiss" action would block replication. The daemon should
@@ -90,7 +99,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
// 2. Mirror scan to reconcile PouchDB ↔ local filesystem.
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle);
log("Running mirror scan...");
const scanOk = await performFullScan(core as any, log, errorManager, false, true);
const scanOk = await performFullScan(core, log, errorManager, false, true);
if (!scanOk) {
console.error("[Daemon] Mirror scan failed, cannot continue");
return false;
@@ -147,12 +156,12 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
);
}
}
pollTimer = setTimeout(poll, currentIntervalMs);
pollTimer = setTimeout(poll as unknown as () => void, currentIntervalMs);
};
let pollTimer: ReturnType<typeof setTimeout> = setTimeout(poll, currentIntervalMs);
let pollTimer: ReturnType<typeof setTimeout> = setTimeout(poll as unknown as () => void, currentIntervalMs);
core.services.appLifecycle.onUnload.addHandler(async () => {
clearTimeout(pollTimer);
return true;
return await Promise.resolve(true);
});
} else {
log("LiveSync mode: restoring sync settings and starting _changes feed");
@@ -198,7 +207,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
}
const timeoutSec = parseTimeoutSeconds(options.commandArgs[0], "p2p-peers");
console.error(`[Command] p2p-peers timeout=${timeoutSec}s`);
const peers = await collectPeers(core as any, timeoutSec);
const peers = await collectPeers(core, timeoutSec);
if (peers.length > 0) {
process.stdout.write(peers.map((peer) => `[peer]\t${peer.peerId}\t${peer.name}`).join("\n") + "\n");
}
@@ -215,14 +224,14 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
}
const timeoutSec = parseTimeoutSeconds(options.commandArgs[1], "p2p-sync");
console.error(`[Command] p2p-sync peer=${peerToken} timeout=${timeoutSec}s`);
const peer = await syncWithPeer(core as any, peerToken, timeoutSec);
const peer = await syncWithPeer(core, peerToken, timeoutSec);
console.error(`[Done] P2P sync completed with ${peer.name} (${peer.peerId})`);
return true;
}
if (options.command === "p2p-host") {
console.error("[Command] p2p-host");
await openP2PHost(core as any);
await openP2PHost(core);
console.error("[Ready] P2P host is running. Press Ctrl+C to stop.");
await new Promise(() => {});
return true;
@@ -435,9 +444,10 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
if (docPath !== targetPath) continue;
const filename = path.basename(docPath);
const conflictsText = (doc._conflicts?.length ?? 0) > 0 ? doc._conflicts.join("\n ") : "N/A";
const conflicts = doc._conflicts ?? [];
const conflictsText = conflicts.length > 0 ? conflicts.join("\n ") : "N/A";
const children = "children" in doc ? doc.children : [];
const rawDoc = await core.services.database.localDatabase.getRaw<any>(doc._id, {
const rawDoc = await core.services.database.localDatabase.getRaw(doc._id, {
revs_info: true,
});
const pastRevisions = (rawDoc._revs_info ?? [])
@@ -491,6 +501,10 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
process.stderr.write(`[Info] File not found: ${targetPath}\n`);
return false;
}
if (!currentMeta._rev) {
process.stderr.write(`[Info] Current revision not found for ${targetPath}\n`);
return false;
}
const conflicts = await core.serviceModules.databaseFileAccess.getConflictedRevs(targetPath);
const candidateRevisions = [currentMeta._rev, ...conflicts];
@@ -520,9 +534,9 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
if (options.command === "mirror") {
console.error("[Command] mirror");
const log = (msg: unknown) => console.error(`[Mirror] ${msg}`);
const log = (msg: unknown) => console.error(`[Mirror] ${msg instanceof Error ? msg.message : String(msg)}`);
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle);
return await performFullScan(core as any, log, errorManager, false, true);
return await performFullScan(core, log, errorManager, false, true);
}
if (options.command === "remote-add") {
+2 -2
View File
@@ -1,6 +1,6 @@
import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type";
import { ServiceContext } from "@lib/services/base/ServiceBase";
import { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
export type CLICommand =
| "daemon"
| "sync"
@@ -46,7 +46,7 @@ export interface CLIOptions {
export interface CLICommandContext {
databasePath: string;
vaultPath: string;
core: LiveSyncBaseCore<ServiceContext, any>;
core: LiveSyncBaseCore<ServiceContext, never>;
settingsPath: string;
originalSyncSettings: Pick<
ObsidianLiveSyncSettings,
+2
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env node
// Polyfill WebRTC in Node.js environment for CLI app.
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as polyfill from "werift";
import { main } from "./main";
+38 -8
View File
@@ -1,19 +1,49 @@
import { tsBaseConfig } from "../../../eslint.config.common.mjs";
import globals from "globals";
import importAlias from "@dword-design/eslint-plugin-import-alias";
import tsParser from "@typescript-eslint/parser";
// import obsidianmd from "eslint-plugin-obsidianmd";
// const obsidianRules = obsidianmd.configs.recommended.find((config) => config.rules)?.rules || {};
// console.dir(obsidianRules);
import { defineConfig, globalIgnores } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";
import { baseRules, CommunityReviewRecommendedRules, ImportAliasRules } from "../../../eslint.config.common.mjs";
export default defineConfig([
globalIgnores([
"dist",
"node_modules",
"test",
"testdeno"
"testdeno",
"**/*.test.ts",
"**/*.spec.ts",
"**/test.ts",
"**/tests.ts",
"**/*.js",
"**/*.mjs",
]),
...tseslint.configs.recommendedTypeChecked,
importAlias.configs.recommended,
{
...tsBaseConfig,
files: ["**/*.ts"],
// ignores:["src/lib/**/*.ts"], // Exclude library files from root linting (they have different environments and rules).
languageOptions: {
...tsBaseConfig.languageOptions,
globals: { ...globals.node },
globals: { ...globals.node, PouchDB: "readonly" },
parser: tsParser,
parserOptions: {
project: "./tsconfig.json",
rootDir: "../../../",
},
},
}
linterOptions: {
reportUnusedDisableDirectives: false,
},
rules: {
...baseRules,
// ...obsidianRules,
// -- Project specific rules
...ImportAliasRules("../../../"),
// cli specific rules
"@typescript-eslint/no-this-alias": "off", // This rule is often inconvenient in CLI code where `this` is commonly used in various contexts, including callbacks and class methods.
...CommunityReviewRecommendedRules,
},
},
]);
+1 -1
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import PouchDB from "pouchdb-core";
import HttpPouch from "pouchdb-adapter-http";
@@ -85,7 +86,6 @@ PouchDB.prototype.purgeMulti = adapterFun(
);
}
//@ts-ignore
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const tasks = docs.map(
(param) => () =>
+25 -19
View File
@@ -4,10 +4,11 @@
*/
import * as fs from "fs/promises";
import * as fsSync from "fs";
import * as path from "path";
import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub";
import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage";
import { LiveSyncBaseCore } from "../../LiveSyncBaseCore";
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules";
import { DEFAULT_SETTINGS } from "@lib/common/models/setting.const.defaults";
import type { LOG_LEVEL } from "@lib/common/logger";
@@ -178,11 +179,13 @@ export function parseArgs(): CLIOptions {
interval = n;
break;
}
// fallthrough
case "--debug":
// @ts-ignore - fallthrough intended for debug implying verbose
case "-d":
// debugging automatically enables verbose logging, as it is intended for debugging issues.
debug = true;
// falls through
// fallthrough
case "--verbose":
case "-v":
verbose = true;
@@ -193,6 +196,7 @@ export function parseArgs(): CLIOptions {
break;
default: {
if (!databasePath) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (command === "daemon" && VALID_COMMANDS.has(token as any)) {
command = token as CLICommand;
break;
@@ -204,6 +208,7 @@ export function parseArgs(): CLIOptions {
databasePath = token;
break;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (command === "daemon" && VALID_COMMANDS.has(token as any)) {
command = token as CLICommand;
break;
@@ -248,8 +253,8 @@ async function createDefaultSettingsFile(options: CLIOptions) {
try {
await fs.stat(targetPath);
throw new Error(`Settings file already exists: ${targetPath} (use --force to overwrite)`);
} catch (ex: any) {
if (!(ex && ex?.code === "ENOENT")) {
} catch (ex: unknown) {
if (!(ex && (ex as { code?: string }).code === "ENOENT")) {
throw ex;
}
}
@@ -304,7 +309,7 @@ export async function main() {
}
// Resolve database path
const databasePath = path.resolve(options.databasePath!);
const databasePath = path.resolve(options.databasePath || "");
// Check if database directory exists
try {
const stat = await fs.stat(databasePath);
@@ -312,7 +317,7 @@ export async function main() {
console.error(`Error: ${databasePath} is not a directory`);
process.exit(1);
}
} catch (error) {
} catch {
console.error(`Error: Database directory ${databasePath} does not exist`);
process.exit(1);
}
@@ -333,7 +338,7 @@ export async function main() {
? path.resolve(options.commandArgs[0])
: options.vaultPath
? path.resolve(options.vaultPath)
: databasePath!;
: databasePath;
// Check if vault directory exists
try {
@@ -342,7 +347,7 @@ export async function main() {
console.error(`Error: Vault path ${vaultPath} is not a directory`);
process.exit(1);
}
} catch (error) {
} catch {
console.error(`Error: Vault directory ${vaultPath} does not exist`);
process.exit(1);
}
@@ -380,7 +385,7 @@ export async function main() {
levelStr = "Urgent";
break;
default:
levelStr = `${level}`;
levelStr = `${level as unknown as string}`;
}
const prefix = `(${levelStr})`;
if (level <= LOG_LEVEL_INFO) {
@@ -424,7 +429,7 @@ export async function main() {
// Force disable IndexedDB adapter in CLI environment
data.useIndexedDBAdapter = false;
return data;
} catch (error) {
} catch {
if (options.verbose) {
console.error(`[Settings] File not found, using defaults`);
}
@@ -436,13 +441,14 @@ export async function main() {
// Create LiveSync core
const core = new LiveSyncBaseCore(
serviceHubInstance,
(core: LiveSyncBaseCore<NodeServiceContext, any>, serviceHub: InjectableServiceHub<NodeServiceContext>) => {
(core: LiveSyncBaseCore<NodeServiceContext, never>, serviceHub: InjectableServiceHub<NodeServiceContext>) => {
return initialiseServiceModulesCLI(vaultPath, core, serviceHub, ignoreRules, watchEnabled);
},
(core) => [],
() => [], // No add-ons
() => [] as never[], // No add-ons
(core) => {
// Register P2P replicator feature.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _replicator = useP2PReplicatorFeature(core);
// Add target filter to prevent internal files are handled
core.services.vault.isTargetFile.addHandler(async (target) => {
@@ -463,12 +469,12 @@ export async function main() {
if (ignoreRules) {
const rules = ignoreRules;
core.services.vault.isTargetFile.addHandler(async (target) => {
await Promise.resolve();
const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target));
if (rules.shouldIgnore(targetPath)) {
return false;
}
// undefined = pass through to next handler in chain
return undefined;
return true;
}, 0);
}
}
@@ -488,13 +494,13 @@ export async function main() {
}
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));
process.on("SIGTERM", () => void shutdown("SIGTERM"));
// Save the settings file before any lifecycle events can mutate and persist them.
// suspendAllSync and other lifecycle hooks clobber sync settings in memory, and
// various code paths persist the clobbered state to disk. We restore on shutdown.
const settingsBackup = await fs.readFile(settingsPath, "utf-8").catch(() => null!);
const settingsBackup = await fs.readFile(settingsPath, "utf-8").catch(() => null);
// Restore settings file on any exit to undo lifecycle mutations.
// Write to a temp path first so a crash mid-write doesn't leave a truncated file.
@@ -502,8 +508,8 @@ export async function main() {
if (settingsBackup) {
const tmpPath = settingsPath + ".tmp";
try {
require("fs").writeFileSync(tmpPath, settingsBackup, "utf-8");
require("fs").renameSync(tmpPath, settingsPath);
fsSync.writeFileSync(tmpPath, settingsBackup, "utf-8");
fsSync.renameSync(tmpPath, settingsPath);
} catch (err) {
console.error("[Settings] Failed to restore settings on exit:", err);
}
@@ -14,19 +14,30 @@ import { watch as chokidarWatch, type FSWatcher } from "chokidar";
import type { Stats } from "fs";
import * as fs from "fs/promises";
import * as path from "path";
import type { NodeFile, NodeFolder } from "../adapters/NodeTypes";
import type { IgnoreRules } from "../serviceModules/IgnoreRules";
import type { NodeFile, NodeFolder } from "@cli/adapters/NodeTypes";
import type { IgnoreRules } from "@cli/serviceModules/IgnoreRules";
/**
* CLI-specific type guard adapter
*/
class CLITypeGuardAdapter implements IStorageEventTypeGuardAdapter<NodeFile, NodeFolder> {
isFile(file: any): file is NodeFile {
return file && typeof file === "object" && "path" in file && "stat" in file && !file.isFolder;
isFile(file: unknown): file is NodeFile {
return !!(
file &&
typeof file === "object" &&
"path" in file &&
"stat" in file &&
!(file as { isFolder?: boolean }).isFolder
);
}
isFolder(item: any): item is NodeFolder {
return item && typeof item === "object" && "path" in item && item.isFolder === true;
isFolder(item: unknown): item is NodeFolder {
return !!(
item &&
typeof item === "object" &&
"path" in item &&
(item as { isFolder?: boolean }).isFolder === true
);
}
}
@@ -1,7 +1,7 @@
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager";
import type { ServiceContext } from "@lib/services/base/ServiceBase";
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
import type { IgnoreRules } from "../serviceModules/IgnoreRules";
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import type { IgnoreRules } from "@cli/serviceModules/IgnoreRules";
import { CLIStorageEventManagerAdapter } from "./CLIStorageEventManagerAdapter";
// import type { IMinimumLiveSyncCommands } from "@lib/services/base/IService";
+1 -1
View File
@@ -6,7 +6,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"package:apply-repo": "node scripts/apply-package.mjs",
"package:apply-repo": "node ../../../apply-package.mjs",
"prebuild": "node scripts/check-submodule.mjs",
"build": "vite build",
"preview": "vite preview",
-28
View File
@@ -1,28 +0,0 @@
// Copy package.json dependencies and devDependencies from the repo root to the cli package.json, and set the version to match the repo root version with a -cli suffix.
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
const cliDir = process.cwd();
const repoRoot = path.resolve(cliDir, "../../..");
const repoPackageJsonPath = path.join(repoRoot, "package.json");
const repoPackageJson = JSON.parse(fs.readFileSync(repoPackageJsonPath, "utf-8"));
const devDependenciesToCopy = repoPackageJson.devDependencies || {};
const dependenciesToCopy = repoPackageJson.dependencies || {};
const packageJsonPath = path.join(cliDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
packageJson.dependencies = {
...packageJson.dependencies,
...dependenciesToCopy,
};
packageJson.devDependencies = {
...packageJson.devDependencies,
...devDependenciesToCopy,
};
packageJson.version = `${repoPackageJson.version}-cli`;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4), "utf-8");
console.log("Applied package.json dependencies and version from repo root to cli package.json");
console.log(
"Please do not forget to pick dependencies that are actually needed in the cli package.json, and remove the ones that are not needed."
);
@@ -3,9 +3,9 @@ import { StorageAccessManager } from "@lib/managers/StorageProcessingManager";
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder";
import type { ServiceContext } from "@lib/services/base/ServiceBase";
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
import { ServiceFileHandler } from "../../../serviceModules/FileHandler";
import { StorageEventManagerCLI } from "../managers/StorageEventManagerCLI";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
import { StorageEventManagerCLI } from "@cli/managers/StorageEventManagerCLI";
import { ServiceDatabaseFileAccessCLI } from "./DatabaseFileAccess";
import { FileAccessCLI } from "./FileAccessCLI";
import type { IgnoreRules } from "./IgnoreRules";
@@ -22,7 +22,7 @@ import { ServiceFileAccessCLI } from "./ServiceFileAccessImpl";
*/
export function initialiseServiceModulesCLI(
basePath: string,
core: LiveSyncBaseCore<ServiceContext, any>,
core: LiveSyncBaseCore<ServiceContext, never>,
services: InjectableServiceHub<ServiceContext>,
ignoreRules?: IgnoreRules,
watchEnabled: boolean = false
@@ -81,7 +81,7 @@ export function initialiseServiceModulesCLI(
});
// File handler (platform-independent)
const fileHandler = new (ServiceFileHandler as any)({
const fileHandler = new ServiceFileHandler({
API: services.API,
databaseFileAccess: databaseFileAccess,
conflict: services.conflict,
+1 -1
View File
@@ -1,5 +1,5 @@
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
import { NodeFileSystemAdapter } from "../adapters/NodeFileSystemAdapter";
import { NodeFileSystemAdapter } from "@cli/adapters/NodeFileSystemAdapter";
/**
* CLI-specific implementation of FileAccessBase
@@ -1,5 +1,5 @@
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
import { NodeFileSystemAdapter } from "../adapters/NodeFileSystemAdapter";
import { NodeFileSystemAdapter } from "@cli/adapters/NodeFileSystemAdapter";
/**
* CLI-specific implementation of ServiceFileAccess
+16 -8
View File
@@ -115,23 +115,25 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
}
async get<T>(key: IDBValidKey): Promise<T> {
return this.data.get(this.asKeyString(key)) as T;
return await Promise.resolve(this.data.get(this.asKeyString(key)) as T);
}
async set<T>(key: IDBValidKey, value: T): Promise<IDBValidKey> {
this.data.set(this.asKeyString(key), value);
this.flush();
return key;
return await Promise.resolve(key);
}
async del(key: IDBValidKey): Promise<void> {
this.data.delete(this.asKeyString(key));
this.flush();
await Promise.resolve();
}
async clear(): Promise<void> {
this.data.clear();
this.flush();
await Promise.resolve();
}
private isIDBKeyRangeLike(value: unknown): value is { lower?: IDBValidKey; upper?: IDBValidKey } {
@@ -143,10 +145,13 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
let filtered = allKeys;
if (typeof query !== "undefined") {
if (this.isIDBKeyRangeLike(query)) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const lower = query.lower?.toString() ?? "";
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const upper = query.upper?.toString() ?? "\uffff";
filtered = filtered.filter((key) => key >= lower && key <= upper);
} else {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const exact = query.toString();
filtered = filtered.filter((key) => key === exact);
}
@@ -154,16 +159,18 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
if (typeof count === "number") {
filtered = filtered.slice(0, count);
}
return filtered;
return await Promise.resolve(filtered);
}
async close(): Promise<void> {
this.flush();
await Promise.resolve();
}
async destroy(): Promise<void> {
this.data.clear();
nodeFs.rmSync(this.filePath, { force: true });
await Promise.resolve();
}
}
@@ -178,7 +185,7 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
implements IKeyValueDBService
{
private _kvDB: KeyValueDatabase | undefined;
private _simpleStore: SimpleStore<any> | undefined;
private _simpleStore: SimpleStore<unknown> | undefined;
private filePath: string;
private _log = createInstanceLogFunction("NodeKeyValueDBService");
@@ -210,11 +217,11 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
private async openKeyValueDB(): Promise<boolean> {
try {
this._kvDB = new NodeFileKeyValueDatabase(this.filePath);
return true;
return await Promise.resolve(true);
} catch (ex) {
this._log("Failed to open Node key-value database", LOG_LEVEL_NOTICE);
this._log(ex, LOG_LEVEL_VERBOSE);
return false;
return await Promise.resolve(false);
}
}
@@ -248,7 +255,7 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
if (!(await this.openKeyValueDB())) {
return false;
}
this._simpleStore = this.openSimpleStore<any>("os");
this._simpleStore = this.openSimpleStore<unknown>("os");
return true;
}
@@ -264,13 +271,14 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
get: async (key: string): Promise<T> => {
return await getDB().get(`${prefix}${key}`);
},
set: async (key: string, value: any): Promise<void> => {
set: async (key: string, value: T): Promise<void> => {
await getDB().set(`${prefix}${key}`, value);
},
delete: async (key: string): Promise<void> => {
await getDB().del(`${prefix}${key}`);
},
keys: async (from: string | undefined, to: string | undefined, count?: number): Promise<string[]> => {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const allKeys = (await getDB().keys(undefined, count)).map((e) => e.toString());
const lower = `${prefix}${from ?? ""}`;
const upper = `${prefix}${to ?? "\uffff"}`;
@@ -84,7 +84,9 @@ function createNodeLocalStorageShim(): LocalStorageShape {
}
export function ensureGlobalNodeLocalStorage() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!("localStorage" in globalThis) || typeof (globalThis as any).localStorage?.getItem !== "function") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).localStorage = createNodeLocalStorageShim();
}
}
+2
View File
@@ -197,10 +197,12 @@ export class NodeServiceHub<T extends NodeServiceContext> extends InjectableServ
path,
API,
config,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
keyValueDB: keyValueDB as any,
control,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
super(context, serviceInstancesToInit as any);
}
}
+9 -8
View File
@@ -15,17 +15,18 @@
"noEmit": true,
/* Linting */
"strict": false,
"strict": true,
// "noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
// /* Path mapping */
// "paths": {
// "@/*": ["../../*"],
// "@lib/*": ["../../lib/src/*"]
// }
"noFallthroughCasesInSwitch": true,
// "rootDir": "../../../",
/* Path mapping */
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"],
"@cli/*": ["./*"]
}
},
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist", "test", "testdeno"]
+1
View File
@@ -72,6 +72,7 @@ export default defineConfig({
fflate: resolve(__dirname, "../../../node_modules/fflate/lib/node.cjs"),
"@": resolve(__dirname, "../../"),
"@lib": resolve(__dirname, "../../lib/src"),
"@cli": resolve(__dirname, "../../apps/cli"),
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
},
},
@@ -1,6 +1,7 @@
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
import type { FilePathWithPrefix } from "@lib/common/models/db.type";
/**
* Conversion adapter implementation for FileSystem API
@@ -28,7 +29,7 @@ export class FSAPIConversionAdapter implements IConversionAdapter<FSAPIFile, FSA
path: folder.path,
isFolder: true,
children: [],
parent: parentPath as any,
parent: parentPath as FilePathWithPrefix,
};
}
}
@@ -30,14 +30,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);
@@ -173,7 +173,9 @@ export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSA
}
// Use AsyncIterator instead of .values() for better compatibility
for await (const [name, entry] of (currentHandle as any).entries()) {
for await (const [name, entry] of (
currentHandle as unknown as { entries: () => AsyncIterable<[string, FileSystemHandle]> }
).entries()) {
const entryPath = relativePath ? `${relativePath}/${name}` : name;
if (entry.kind === "directory") {
@@ -195,7 +195,9 @@ export class FSAPIStorageAdapter implements IStorageAdapter<FSAPIStat> {
const folders: string[] = [];
// Use AsyncIterator instead of .values() for better compatibility
for await (const [name, entry] of (dirHandle as any).entries()) {
for await (const [name, entry] of (
dirHandle as unknown as { entries: () => AsyncIterable<[string, FileSystemHandle]> }
).entries()) {
const entryPath = basePath ? `${basePath}/${name}` : name;
if (entry.kind === "directory") {
@@ -5,13 +5,25 @@ import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
* Type guard adapter implementation for FileSystem API
*/
export class FSAPITypeGuardAdapter implements ITypeGuardAdapter<FSAPIFile, FSAPIFolder> {
isFile(file: any): file is FSAPIFile {
return (
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
isFile(file: unknown): file is FSAPIFile {
return !!(
file &&
typeof file === "object" &&
"path" in file &&
"stat" in file &&
"handle" in file &&
!("isFolder" in file && file.isFolder === true)
);
}
isFolder(item: any): item is FSAPIFolder {
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
isFolder(item: unknown): item is FSAPIFolder {
return !!(
item &&
typeof item === "object" &&
"path" in item &&
"isFolder" in item &&
item.isFolder === true &&
"handle" in item
);
}
}
@@ -116,7 +116,7 @@ export class FSAPIVaultAdapter implements IVaultAdapter<FSAPIFile> {
await this.delete(file, force);
}
trigger(name: string, ...data: any[]): any {
trigger(name: string, ...data: unknown[]): unknown {
// No-op in webapp version (no event system yet)
return undefined;
}
+4 -6
View File
@@ -120,20 +120,18 @@ async function initializeVaultSelector(): Promise<void> {
await renderHistoryList();
}
window.addEventListener("load", async () => {
try {
await initializeVaultSelector();
} catch (error) {
window.addEventListener("load", () => {
initializeVaultSelector().catch((error) => {
console.error("Failed to initialize vault selector:", error);
setStatus("error", `Initialization failed: ${String(error)}`);
}
});
});
window.addEventListener("beforeunload", () => {
void app?.shutdown();
});
(window as any).livesyncApp = {
(window as unknown as { livesyncApp: unknown }).livesyncApp = {
getApp: () => app,
historyStore,
};
+32 -7
View File
@@ -1,18 +1,43 @@
import { tsBaseConfig } from "../../../eslint.config.common.mjs";
import globals from "globals";
import importAlias from "@dword-design/eslint-plugin-import-alias";
import tsParser from "@typescript-eslint/parser";
import { defineConfig, globalIgnores } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";
import { baseRules, CommunityReviewRecommendedRules, ImportAliasRules } from "../../../eslint.config.common.mjs";
export default defineConfig([
globalIgnores([
"dist",
"node_modules",
"test"
"test",
"**/*.test.ts",
"**/*.spec.ts",
"**/test.ts",
"**/tests.ts",
"**/*.js",
"**/*.mjs",
"vite.config.ts",
"playwright.config.ts",
]),
...tseslint.configs.recommendedTypeChecked,
importAlias.configs.recommended,
{
...tsBaseConfig,
files: ["**/*.ts"],
languageOptions: {
...tsBaseConfig.languageOptions,
globals: { ...globals.browser },
globals: { ...globals.browser, PouchDB: "readonly" },
parser: tsParser,
parserOptions: {
project: "./tsconfig.json",
rootDir: "../../../",
},
},
}
linterOptions: {
reportUnusedDisableDirectives: false,
},
rules: {
...baseRules,
...ImportAliasRules("../../../"),
...CommunityReviewRecommendedRules,
},
},
]);
+16 -12
View File
@@ -50,7 +50,7 @@ const DEFAULT_SETTINGS: Partial<ObsidianLiveSyncSettings> = {
class LiveSyncWebApp {
private rootHandle: FileSystemDirectoryHandle;
private core: LiveSyncBaseCore<ServiceContext, any> | null = null;
private core: LiveSyncBaseCore<ServiceContext, never> | null = null;
private serviceHub: BrowserServiceHub<ServiceContext> | null = null;
constructor(rootHandle: FileSystemDirectoryHandle) {
@@ -98,17 +98,19 @@ class LiveSyncWebApp {
});
// App lifecycle handlers
this.serviceHub.appLifecycle.scheduleRestart.setHandler(async () => {
console.log("[AppLifecycle] Restart requested");
await this.shutdown();
await this.initialize();
setTimeout(() => {
window.location.reload();
}, 1000);
this.serviceHub.appLifecycle.scheduleRestart.setHandler(() => {
void (async () => {
console.log("[AppLifecycle] Restart requested");
await this.shutdown();
await this.initialize();
setTimeout(() => {
window.location.reload();
}, 1000);
})();
});
// Create LiveSync core
this.core = new LiveSyncBaseCore(
this.core = new LiveSyncBaseCore<ServiceContext, never>(
this.serviceHub,
(core, serviceHub) => {
return initialiseServiceModulesFSAPI(this.rootHandle, core, serviceHub);
@@ -128,7 +130,7 @@ class LiveSyncWebApp {
// new ModuleReplicatorP2P(core), // Register P2P replicator for CLI (useP2PReplicator is not used here)
new SetupManager(core),
],
() => [], // No add-ons
() => [] as never[], // No add-ons
(core) => {
useOfflineScanner(core);
useRedFlagFeatures(core);
@@ -206,7 +208,8 @@ class LiveSyncWebApp {
}
// Scan the directory to populate file cache
const fileAccess = (this.core as any)._serviceModules?.storageAccess?.vaultAccess;
const fileAccess = (this.core as unknown as { _serviceModules?: any })._serviceModules?.storageAccess
?.vaultAccess;
if (fileAccess?.fsapiAdapter) {
console.log("[Scanning] Scanning vault directory...");
await fileAccess.fsapiAdapter.scanDirectory();
@@ -224,7 +227,8 @@ class LiveSyncWebApp {
console.log("[Shutdown] Shutting down...");
// Stop file watching
const storageEventManager = (this.core as any)._serviceModules?.storageAccess?.storageEventManager;
const storageEventManager = (this.core as unknown as { _serviceModules?: any })._serviceModules
?.storageAccess?.storageEventManager;
if (storageEventManager?.cleanup) {
await storageEventManager.cleanup();
}
@@ -10,20 +10,32 @@ import type {
IStorageEventWatchHandlers,
} from "@lib/managers/adapters";
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
import type { FSAPIFile, FSAPIFolder } from "../adapters/FSAPITypes";
import type { FSAPIFile, FSAPIFolder } from "@/apps/webapp/adapters/FSAPITypes";
/**
* FileSystem API-specific type guard adapter
*/
class FSAPITypeGuardAdapter implements IStorageEventTypeGuardAdapter<FSAPIFile, FSAPIFolder> {
isFile(file: any): file is FSAPIFile {
return (
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
isFile(file: unknown): file is FSAPIFile {
return !!(
file &&
typeof file === "object" &&
"path" in file &&
"stat" in file &&
"handle" in file &&
!("isFolder" in file && file.isFolder === true)
);
}
isFolder(item: any): item is FSAPIFolder {
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
isFolder(item: unknown): item is FSAPIFolder {
return !!(
item &&
typeof item === "object" &&
"path" in item &&
"isFolder" in item &&
item.isFolder === true &&
"handle" in item
);
}
}
@@ -143,22 +155,40 @@ class FSAPIConverterAdapter implements IStorageEventConverterAdapter<FSAPIFile>
* FileSystem API-specific watch adapter using FileSystemObserver (Chrome only)
*/
class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
private observer: any = null; // FileSystemObserver type
private observer: {
observe: (handle: FileSystemDirectoryHandle, options: { recursive: boolean }) => Promise<void>;
disconnect: () => void;
} | null = null;
constructor(private rootHandle: FileSystemDirectoryHandle) {}
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
// Use FileSystemObserver if available (Chrome 124+)
if (typeof (window as any).FileSystemObserver === "undefined") {
const win = window as unknown as {
FileSystemObserver?: new (
callback: (
records: {
root: FileSystemDirectoryHandle;
changedHandle: FileSystemFileHandle;
relativePathComponents: string[];
type: string;
}[]
) => Promise<void>
) => {
observe: (handle: FileSystemDirectoryHandle, options: { recursive: boolean }) => Promise<void>;
disconnect: () => void;
};
};
if (typeof win.FileSystemObserver === "undefined") {
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
return Promise.resolve();
}
try {
const FileSystemObserver = (window as any).FileSystemObserver;
const FileSystemObserver = win.FileSystemObserver;
this.observer = new FileSystemObserver(async (records: any[]) => {
this.observer = new FileSystemObserver(async (records) => {
for (const record of records) {
const handle = record.root;
const changedHandle = record.changedHandle;
@@ -181,7 +211,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
if (changedHandle && changedHandle.kind === "file") {
const file = await changedHandle.getFile();
const fileInfo = {
path: relativePath as any,
path: relativePath,
stat: {
size: file.size,
mtime: file.lastModified,
@@ -192,23 +222,23 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
};
if (type === "appeared") {
await handlers.onCreate(fileInfo, undefined);
handlers.onCreate(fileInfo, undefined);
} else {
await handlers.onChange(fileInfo, undefined);
handlers.onChange(fileInfo, undefined);
}
}
} else if (type === "disappeared") {
const fileInfo = {
path: relativePath as any,
path: relativePath,
stat: {
size: 0,
mtime: Date.now(),
ctime: Date.now(),
type: "file" as const,
},
handle: null as any,
handle: null as unknown,
};
await handlers.onDelete(fileInfo, undefined);
handlers.onDelete(fileInfo, undefined);
} else if (type === "moved") {
// Handle as delete + create
// Note: FileSystemObserver provides both old and new paths in some cases
@@ -216,7 +246,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
if (changedHandle && changedHandle.kind === "file") {
const file = await changedHandle.getFile();
const fileInfo = {
path: relativePath as any,
path: relativePath,
stat: {
size: file.size,
mtime: file.lastModified,
@@ -225,7 +255,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
},
handle: changedHandle,
};
await handlers.onChange(fileInfo, undefined);
handlers.onChange(fileInfo, undefined);
}
}
} catch (error) {
@@ -30,7 +30,7 @@ export class StorageEventManagerFSAPI extends StorageEventManagerBase<FSAPIStora
async cleanup() {
// Stop file watching
if (this.fsapiAdapter?.watch) {
await (this.fsapiAdapter.watch as any).stopWatch?.();
await (this.fsapiAdapter.watch as unknown as { stopWatch?: () => Promise<void> }).stopWatch?.();
}
}
}
+86 -4
View File
@@ -1,7 +1,7 @@
{
"name": "livesync-webapp",
"private": true,
"version": "0.0.1",
"version": "0.25.74-webapp",
"type": "module",
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
"scripts": {
@@ -11,11 +11,93 @@
"run:docker": "docker run -p 8002:80 livesync-webapp",
"preview": "vite preview",
"check": "tsc --noEmit",
"lint": "eslint --cache ."
"lint": "eslint --cache .",
"package:apply-repo": "node ../../../apply-package.mjs"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
"@smithy/fetch-http-handler": "^5.3.10",
"@smithy/md5-js": "^4.2.9",
"@smithy/middleware-apply-body-checksum": "^4.3.9",
"@smithy/protocol-http": "^5.3.9",
"@smithy/querystring-builder": "^4.2.9",
"@smithy/types": "^4.14.3",
"@smithy/util-retry": "^4.4.5",
"@trystero-p2p/nostr": "^0.24.0",
"chokidar": "^4.0.0",
"diff-match-patch": "^1.0.5",
"fflate": "^0.8.2",
"idb": "^8.0.3",
"markdown-it": "^14.1.1",
"minimatch": "^10.2.2",
"obsidian": "^1.12.3",
"octagonal-wheels": "^0.1.46",
"pouchdb-adapter-leveldb": "^9.0.0",
"qrcode-generator": "^1.4.4",
"werift": "^0.23.0",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
},
"dependencies": {},
"devDependencies": {
"typescript": "5.9.3",
"vite": "^7.3.1"
"vite": "^7.3.1",
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
"@eslint/js": "^9.39.3",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tsconfig/svelte": "^5.0.8",
"@types/deno": "^2.5.0",
"@types/diff-match-patch": "^1.0.36",
"@types/markdown-it": "^14.1.2",
"@types/micromatch": "^4.0.10",
"@types/node": "^24.10.13",
"@types/pouchdb": "^6.4.2",
"@types/pouchdb-adapter-http": "^6.1.6",
"@types/pouchdb-adapter-idb": "^6.1.7",
"@types/pouchdb-browser": "^6.1.5",
"@types/pouchdb-core": "^7.0.15",
"@types/pouchdb-mapreduce": "^6.1.10",
"@types/pouchdb-replication": "^6.4.7",
"@types/transform-pouch": "^1.0.6",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@vitest/browser": "^4.1.8",
"@vitest/browser-playwright": "^4.1.8",
"@vitest/coverage-v8": "^4.1.8",
"dotenv-cli": "^11.0.0",
"esbuild": "0.25.0",
"esbuild-plugin-inline-worker": "^0.1.1",
"esbuild-svelte": "^0.9.4",
"eslint": "^9.39.3",
"eslint-plugin-obsidianmd": "^0.3.0",
"eslint-plugin-svelte": "^3.15.0",
"events": "^3.3.0",
"globals": "^14.0.0",
"playwright": "^1.58.2",
"postcss": "^8.5.6",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-adapter-idb": "^9.0.0",
"pouchdb-adapter-indexeddb": "^9.0.0",
"pouchdb-adapter-memory": "^9.0.0",
"pouchdb-core": "^9.0.0",
"pouchdb-errors": "^9.0.0",
"pouchdb-find": "^9.0.0",
"pouchdb-mapreduce": "^9.0.0",
"pouchdb-merge": "^9.0.0",
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"prettier": "3.8.1",
"rollup-plugin-copy": "^3.5.0",
"svelte": "5.41.1",
"svelte-check": "^4.4.3",
"svelte-eslint-parser": "^1.8.0",
"svelte-preprocess": "^6.0.3",
"terser": "^5.39.0",
"tinyglobby": "^0.2.15",
"transform-pouch": "^2.0.0",
"tsx": "^4.21.0",
"typescript-eslint": "^8.61.0",
"vite-plugin-istanbul": "^8.0.0",
"vitest": "^4.1.8",
"webdriverio": "^9.27.0",
"yaml": "^2.8.2"
}
}
@@ -7,7 +7,7 @@ import type { ServiceContext } from "@lib/services/base/ServiceBase";
import { FileAccessFSAPI } from "./FileAccessFSAPI";
import { ServiceFileAccessFSAPI } from "./ServiceFileAccessImpl";
import { ServiceDatabaseFileAccessFSAPI } from "./DatabaseFileAccess";
import { StorageEventManagerFSAPI } from "../managers/StorageEventManagerFSAPI";
import { StorageEventManagerFSAPI } from "@/apps/webapp/managers/StorageEventManagerFSAPI";
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
@@ -22,7 +22,7 @@ import { ServiceFileHandler } from "@/serviceModules/FileHandler";
*/
export function initialiseServiceModulesFSAPI(
rootHandle: FileSystemDirectoryHandle,
core: LiveSyncBaseCore<ServiceContext, any>,
core: LiveSyncBaseCore<ServiceContext, never>,
services: InjectableServiceHub<ServiceContext>
): ServiceModules {
const storageAccessManager = new StorageAccessManager();
@@ -67,7 +67,7 @@ export function initialiseServiceModulesFSAPI(
});
// File handler (platform-independent)
const fileHandler = new (ServiceFileHandler as any)({
const fileHandler = new ServiceFileHandler({
API: services.API,
databaseFileAccess: databaseFileAccess,
conflict: services.conflict,
@@ -1,5 +1,5 @@
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
import { FSAPIFileSystemAdapter } from "@/apps/webapp/adapters/FSAPIFileSystemAdapter";
/**
* FileSystem API-specific implementation of FileAccessBase
@@ -1,5 +1,5 @@
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
import { FSAPIFileSystemAdapter } from "@/apps/webapp/adapters/FSAPIFileSystemAdapter";
/**
* FileSystem API-specific implementation of ServiceFileAccess
+40 -9
View File
@@ -27,11 +27,45 @@ function stripPrefix(raw: string): string {
return raw.replace(/^[^:]+:/, "");
}
interface TestCore {
services: {
replication: {
databaseQueueCount?: { value: number };
storageApplyingCount?: { value: number };
replicate: (force: boolean) => Promise<boolean>;
};
fileProcessing: {
totalQueued?: { value: number };
batched?: { value: number };
processing?: { value: number };
};
database: {
localDatabase: {
findAllNormalDocs: (options: { conflicts: boolean }) => AsyncIterable<{
_deleted?: boolean;
deleted?: boolean;
path?: string;
_rev?: string;
_conflicts?: string[];
size?: number;
mtime?: number;
}>;
};
};
};
serviceModules: {
databaseFileAccess: {
storeContent: (path: string, content: string) => Promise<boolean>;
delete: (path: string) => Promise<boolean>;
};
};
}
/**
* Poll every 300 ms until all known processing queues are drained, or until
* the timeout elapses. Mirrors `waitForIdle` in the existing vitest harness.
*/
async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
async function waitForIdle(core: TestCore, timeoutMs = 60_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const q =
@@ -46,8 +80,8 @@ async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
throw new Error(`waitForIdle timed out after ${timeoutMs} ms`);
}
function getCore(): any {
const core = (app as any)?.core;
function getCore(): TestCore {
const core = (app as unknown as { core: TestCore })?.core;
if (!core) throw new Error("Vault not initialised call livesyncTest.init() first");
return core;
}
@@ -140,17 +174,14 @@ const livesyncTest: LiveSyncTestAPI = {
async putFile(vaultPath: string, content: string): Promise<boolean> {
const core = getCore();
const result = await core.serviceModules.databaseFileAccess.storeContent(
vaultPath as FilePathWithPrefix,
content
);
const result = await core.serviceModules.databaseFileAccess.storeContent(vaultPath, content);
await waitForIdle(core);
return result !== false;
},
async deleteFile(vaultPath: string): Promise<boolean> {
const core = getCore();
const result = await core.serviceModules.databaseFileAccess.delete(vaultPath as FilePathWithPrefix);
const result = await core.serviceModules.databaseFileAccess.delete(vaultPath);
await waitForIdle(core);
return result !== false;
},
@@ -200,4 +231,4 @@ const livesyncTest: LiveSyncTestAPI = {
};
// Expose on window for Playwright page.evaluate() calls.
(window as any).livesyncTest = livesyncTest;
(window as unknown as { livesyncTest: unknown }).livesyncTest = livesyncTest;
+18 -4
View File
@@ -30,7 +30,10 @@ function randomId(): string {
}
async function hasReadWritePermission(handle: FileSystemDirectoryHandle, requestIfNeeded: boolean): Promise<boolean> {
const h = handle as any;
const h = handle as unknown as {
queryPermission?: (options: { mode: "read" | "readwrite" }) => Promise<PermissionState>;
requestPermission?: (options: { mode: "read" | "readwrite" }) => Promise<PermissionState>;
};
if (typeof h.queryPermission === "function") {
const queried = await h.queryPermission({ mode: "readwrite" });
if (queried === "granted") {
@@ -89,11 +92,15 @@ export class VaultHistoryStore {
async getVaultHistory(): Promise<VaultHistoryItem[]> {
return this.withStore("readonly", async (store) => {
const keys = (await this.requestAsPromise(store.getAllKeys())) as IDBValidKey[];
const keys = await this.requestAsPromise(store.getAllKeys());
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
const items: VaultHistoryItem[] = [];
for (let i = 0; i < keys.length; i++) {
const key = String(keys[i]);
const keyVal = keys[i];
if (typeof keyVal !== "string" && typeof keyVal !== "number") {
continue;
}
const key = String(keyVal);
const id = parseVaultId(key);
const value = values[i] as Partial<VaultHistoryValue> | undefined;
if (!id || !value || !value.handle || !value.name) {
@@ -170,7 +177,14 @@ export class VaultHistoryStore {
}
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
const picker = (window as any).showDirectoryPicker;
const picker = (
window as unknown as {
showDirectoryPicker?: (options?: {
mode?: "read" | "readwrite";
startIn?: "documents" | "desktop" | "downloads" | "music" | "pictures" | "videos";
}) => Promise<FileSystemDirectoryHandle>;
}
).showDirectoryPicker;
if (typeof picker !== "function") {
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
}
+42 -9
View File
@@ -1,28 +1,61 @@
import { tsBaseConfig, svelteBaseConfig } from "../../../eslint.config.common.mjs";
import globals from "globals";
import importAlias from "@dword-design/eslint-plugin-import-alias";
import tsParser from "@typescript-eslint/parser";
import { defineConfig, globalIgnores } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";
import * as sveltePlugin from "eslint-plugin-svelte";
import svelteParser from "svelte-eslint-parser";
import { baseRules, CommunityReviewRecommendedRules, ImportAliasRules } from "../../../eslint.config.common.mjs";
export default defineConfig([
globalIgnores([
"dist",
"node_modules"
"node_modules",
"vite.config.ts",
"svelte.config.js",
"**/*.js",
"**/*.mjs",
]),
...tseslint.configs.recommendedTypeChecked,
...sveltePlugin.configs["flat/base"],
importAlias.configs.recommended,
{
...tsBaseConfig,
files: ["src/**/*.ts"],
languageOptions: {
...tsBaseConfig.languageOptions,
globals: { ...globals.browser },
globals: { ...globals.browser, PouchDB: "readonly" },
parser: tsParser,
parserOptions: {
project: "./tsconfig.app.json",
rootDir: "../../../",
},
},
linterOptions: {
reportUnusedDisableDirectives: false,
},
rules: {
...baseRules,
...ImportAliasRules("../../../"),
...CommunityReviewRecommendedRules,
"no-restricted-globals": "off",
},
},
{
...svelteBaseConfig,
files: ["src/**/*.svelte"],
languageOptions: {
...svelteBaseConfig.languageOptions,
globals: { ...globals.browser },
globals: { ...globals.browser, PouchDB: "readonly" },
parser: svelteParser,
parserOptions: {
parser: tsParser,
extraFileExtensions: [".svelte"],
project: "./tsconfig.app.json",
rootDir: "../../../",
},
},
rules: {
"no-unused-vars": "off",
...ImportAliasRules("../../../"),
...CommunityReviewRecommendedRules,
"no-restricted-globals": "off",
},
},
]);
+81 -4
View File
@@ -1,7 +1,7 @@
{
"name": "webpeer",
"private": true,
"version": "0.0.0",
"version": "0.25.74-webpeer",
"type": "module",
"scripts": {
"dev": "vite",
@@ -10,9 +10,32 @@
"run:docker": "docker run -p 8001:80 livesync-webpeer",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
"lint": "eslint --cache src"
"lint": "eslint --cache src",
"package:apply-repo": "node ../../../apply-package.mjs"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
"@smithy/fetch-http-handler": "^5.3.10",
"@smithy/md5-js": "^4.2.9",
"@smithy/middleware-apply-body-checksum": "^4.3.9",
"@smithy/protocol-http": "^5.3.9",
"@smithy/querystring-builder": "^4.2.9",
"@smithy/types": "^4.14.3",
"@smithy/util-retry": "^4.4.5",
"@trystero-p2p/nostr": "^0.24.0",
"chokidar": "^4.0.0",
"diff-match-patch": "^1.0.5",
"fflate": "^0.8.2",
"idb": "^8.0.3",
"markdown-it": "^14.1.1",
"minimatch": "^10.2.2",
"obsidian": "^1.12.3",
"octagonal-wheels": "^0.1.46",
"pouchdb-adapter-leveldb": "^9.0.0",
"qrcode-generator": "^1.4.4",
"werift": "^0.23.0",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
},
"dependencies": {},
"devDependencies": {
"eslint-plugin-svelte": "^3.15.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
@@ -20,7 +43,61 @@
"svelte": "5.41.1",
"svelte-check": "^4.4.3",
"typescript": "5.9.3",
"vite": "^7.3.1"
"vite": "^7.3.1",
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
"@eslint/js": "^9.39.3",
"@types/deno": "^2.5.0",
"@types/diff-match-patch": "^1.0.36",
"@types/markdown-it": "^14.1.2",
"@types/micromatch": "^4.0.10",
"@types/node": "^24.10.13",
"@types/pouchdb": "^6.4.2",
"@types/pouchdb-adapter-http": "^6.1.6",
"@types/pouchdb-adapter-idb": "^6.1.7",
"@types/pouchdb-browser": "^6.1.5",
"@types/pouchdb-core": "^7.0.15",
"@types/pouchdb-mapreduce": "^6.1.10",
"@types/pouchdb-replication": "^6.4.7",
"@types/transform-pouch": "^1.0.6",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@vitest/browser": "^4.1.8",
"@vitest/browser-playwright": "^4.1.8",
"@vitest/coverage-v8": "^4.1.8",
"dotenv-cli": "^11.0.0",
"esbuild": "0.25.0",
"esbuild-plugin-inline-worker": "^0.1.1",
"esbuild-svelte": "^0.9.4",
"eslint": "^9.39.3",
"eslint-plugin-obsidianmd": "^0.3.0",
"events": "^3.3.0",
"globals": "^14.0.0",
"playwright": "^1.58.2",
"postcss": "^8.5.6",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-adapter-idb": "^9.0.0",
"pouchdb-adapter-indexeddb": "^9.0.0",
"pouchdb-adapter-memory": "^9.0.0",
"pouchdb-core": "^9.0.0",
"pouchdb-errors": "^9.0.0",
"pouchdb-find": "^9.0.0",
"pouchdb-mapreduce": "^9.0.0",
"pouchdb-merge": "^9.0.0",
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"prettier": "3.8.1",
"rollup-plugin-copy": "^3.5.0",
"svelte-eslint-parser": "^1.8.0",
"svelte-preprocess": "^6.0.3",
"terser": "^5.39.0",
"tinyglobby": "^0.2.15",
"transform-pouch": "^2.0.0",
"tsx": "^4.21.0",
"typescript-eslint": "^8.61.0",
"vite-plugin-istanbul": "^8.0.0",
"vitest": "^4.1.8",
"webdriverio": "^9.27.0",
"yaml": "^2.8.2"
},
"imports": {
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
+27 -11
View File
@@ -10,14 +10,16 @@ import {
import { eventHub } from "@lib/hub/hub";
import type { Confirm } from "@lib/interfaces/Confirm";
import { LOG_LEVEL_NOTICE, Logger } from "@lib/common/logger";
import { LOG_LEVEL_NOTICE, Logger, type LOG_LEVEL } from "@lib/common/logger";
import { storeP2PStatusLine } from "./CommandsShim";
import {
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
type PeerStatus,
type PluginShim,
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
import { P2PLogCollector, type P2PReplicatorBase, useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore";
import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector";
import type { P2PReplicatorBase } from "@lib/replication/trystero/P2PReplicatorBase";
import { useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
@@ -26,6 +28,7 @@ import { BrowserServiceHub } from "@lib/services/BrowserServices";
import { SETTING_KEY_P2P_DEVICE_NAME } from "@lib/common/types";
import { ServiceContext } from "@lib/services/base/ServiceBase";
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
import { Menu } from "@lib/services/implements/browser/Menu";
import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
import type { BrowserAPIService } from "@lib/services/implements/browser/BrowserAPIService";
@@ -63,7 +66,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
}
return this.db;
}
_simpleStore!: SimpleStore<any>;
_simpleStore!: SimpleStore<unknown>;
async closeDB() {
if (this.db) {
@@ -80,7 +83,20 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
replicator,
p2pLogCollector,
storeP2PStatusLine: p2pStatusLine,
} = useP2PReplicator({ services: this.services } as any);
} = useP2PReplicator({ services: this.services } as unknown as NecessaryServices<
| "API"
| "appLifecycle"
| "setting"
| "vault"
| "database"
| "databaseEvents"
| "keyValueDB"
| "replication"
| "config"
| "UI"
| "replicator",
never
>);
this._liveSyncReplicator = replicator;
this.p2pLogCollector = p2pLogCollector;
p2pLogCollector.p2pReplicationLine.onChanged((line) => {
@@ -95,15 +111,15 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
(this.services.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
() => "p2p-livesync-web-peer"
);
const repStore = SimpleStoreIDBv2.open<any>("p2p-livesync-web-peer");
const repStore = SimpleStoreIDBv2.open<unknown>("p2p-livesync-web-peer");
this._simpleStore = repStore;
let _settings = { ...P2P_DEFAULT_SETTINGS, additionalSuffixOfDatabaseName: "" } as ObsidianLiveSyncSettings;
this.services.setting.settings = _settings as any;
(this.services.setting as InjectableSettingService<any>).saveData.setHandler(async (data) => {
this.services.setting.settings = _settings;
(this.services.setting as InjectableSettingService<ServiceContext>).saveData.setHandler(async (data) => {
await repStore.set("settings", data);
eventHub.emitEvent(EVENT_SETTING_SAVED, data);
});
(this.services.setting as InjectableSettingService<any>).loadData.setHandler(async () => {
(this.services.setting as InjectableSettingService<ServiceContext>).loadData.setHandler(async () => {
const settings = { ..._settings, ...((await repStore.get("settings")) as ObsidianLiveSyncSettings) };
return settings;
});
@@ -145,7 +161,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
return this;
}
_log(msg: any, level?: any): void {
_log(msg: unknown, level?: LOG_LEVEL): void {
Logger(msg, level);
}
_notice(msg: string, key?: string): void {
@@ -154,7 +170,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
getSettings(): P2PSyncSetting {
return this.settings;
}
simpleStore(): SimpleStore<any> {
simpleStore(): SimpleStore<unknown> {
return this._simpleStore;
}
handleReplicatedDocuments(_docs: EntryDoc[]): Promise<boolean> {
@@ -278,7 +294,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
}
await this.services.setting.applyExternalSettings(remoteConfig, true);
if (yn !== DROP) {
await this.plugin.core.services.appLifecycle.scheduleRestart();
this.plugin.core.services.appLifecycle.scheduleRestart();
}
} else {
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
+8 -3
View File
@@ -5,18 +5,20 @@
import { cmdSyncShim } from "./P2PReplicatorShim";
import { eventHub } from "@lib/hub/hub";
import { EVENT_LAYOUT_READY } from "@lib/events/coreEvents";
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
let synchronised = $state(cmdSyncShim.init());
onMount(() => {
eventHub.emitEvent(EVENT_LAYOUT_READY);
return () => {
synchronised.then((e) => e.close());
void synchronised.then((e) => e.close());
};
});
let elP: HTMLDivElement;
logs.subscribe((log) => {
tick().then(() => elP?.scrollTo({ top: elP.scrollHeight }));
void tick().then(() => elP?.scrollTo({ top: elP.scrollHeight }));
});
let statusLine = $state("");
storeP2PStatusLine.subscribe((status) => {
@@ -27,7 +29,10 @@
<main>
<div class="control">
{#await synchronised then cmdSync}
<P2PReplicatorPane plugin={cmdSync.plugin} {cmdSync} core={cmdSync.plugin.core}></P2PReplicatorPane>
<P2PReplicatorPane
cmdSync={cmdSync as unknown as LiveSyncTrysteroReplicator}
core={cmdSync.plugin.core as unknown as LiveSyncBaseCore}
></P2PReplicatorPane>
{:catch error}
<p>{error.message}</p>
{/await}
+3 -3
View File
@@ -6,13 +6,13 @@
const context = getDialogContext();
async function testUI() {
const confirm = await context.services.confirm;
const confirm = context.services.confirm;
const ret = await confirm.askString("Your name", "What is your name?", "John Doe", false);
result = ret;
}
let resultPassword = $state<string | boolean>("");
async function testPassword() {
const confirm = await context.services.confirm;
const confirm = context.services.confirm;
const ret = await confirm.askString("passphrase", "?", "anythingonlyyouknow", true);
resultPassword = ret;
}
@@ -52,7 +52,7 @@
.onClick(async () => {})
.setIcon(mark);
});
m.showAtPosition({ x: event.x, y: event.y });
void m.showAtPosition({ x: event.x, y: event.y });
}
</script>
+4 -2
View File
@@ -1,5 +1,5 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"extends": "../../../tsconfig.json",
"compilerOptions": {
"sourceRoot": "../",
"target": "ESNext",
@@ -15,11 +15,13 @@
"allowJs": true,
"checkJs": true,
"isolatedModules": true,
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"exclude": ["node_modules", "dist"]
}
+2 -1
View File
@@ -20,7 +20,8 @@
"strictFunctionTypes": true,
"paths": {
"@/*": ["./src/*"],
"@lib/*": ["./src/lib/src/*", "./_types/src/lib/src/*"]
"@lib/*": ["./src/lib/src/*", "./_types/src/lib/src/*"],
"@cli/*": ["./src/apps/cli/*"]
}
},
"include": ["**/*.ts", "test/**/*.test.ts", "**/*.unit.spec.ts", "**/*.svelte"],
+1
View File
@@ -91,6 +91,7 @@ export default defineConfig({
alias: {
"@": path.resolve(__dirname, "./src"),
"@lib": path.resolve(__dirname, "./src/lib/src"),
"@cli": path.resolve(__dirname, "./src/apps/cli"),
src: path.resolve(__dirname, "./src"),
},
},