mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-23 22:53:56 +00:00
+20
-19
@@ -1,22 +1,22 @@
|
||||
import { LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
|
||||
import type PouchDB from "pouchdb-core";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "./lib/src/common/types";
|
||||
import { __$checkInstanceBinding } from "./lib/src/dev/checks";
|
||||
import type { Confirm } from "./lib/src/interfaces/Confirm";
|
||||
import type { DatabaseFileAccess } from "./lib/src/interfaces/DatabaseFileAccess";
|
||||
import type { Rebuilder } from "./lib/src/interfaces/DatabaseRebuilder";
|
||||
import type { IFileHandler } from "./lib/src/interfaces/FileHandler";
|
||||
import type { StorageAccess } from "./lib/src/interfaces/StorageAccess";
|
||||
import type { LiveSyncLocalDBEnv } from "./lib/src/pouchdb/LiveSyncLocalDB";
|
||||
import type { LiveSyncCouchDBReplicatorEnv } from "./lib/src/replication/couchdb/LiveSyncReplicator";
|
||||
import type { CheckPointInfo } from "./lib/src/replication/journal/JournalSyncTypes";
|
||||
import type { LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicatorEnv";
|
||||
import type { LiveSyncReplicatorEnv } from "./lib/src/replication/LiveSyncAbstractReplicator";
|
||||
import { useTargetFilters } from "./lib/src/serviceFeatures/targetFilter";
|
||||
import { useRemoteConfigurationMigration } from "./lib/src/serviceFeatures/remoteConfig";
|
||||
import type { ServiceContext } from "./lib/src/services/base/ServiceBase";
|
||||
import type { InjectableServiceHub } from "./lib/src/services/InjectableServices";
|
||||
import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "@lib/common/types";
|
||||
import { __$checkInstanceBinding } from "@lib/dev/checks";
|
||||
import type { Confirm } from "@lib/interfaces/Confirm";
|
||||
import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess";
|
||||
import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder";
|
||||
import type { IFileHandler } from "@lib/interfaces/FileHandler";
|
||||
import type { StorageAccess } from "@lib/interfaces/StorageAccess";
|
||||
import type { LiveSyncLocalDBEnv } from "@lib/pouchdb/LiveSyncLocalDB";
|
||||
import type { LiveSyncCouchDBReplicatorEnv } from "@lib/replication/couchdb/LiveSyncReplicator";
|
||||
import type { CheckPointInfo } from "@lib/replication/journal/JournalSyncTypes";
|
||||
import type { LiveSyncJournalReplicatorEnv } from "@lib/replication/journal/LiveSyncJournalReplicatorEnv";
|
||||
import type { LiveSyncReplicatorEnv } from "@lib/replication/LiveSyncAbstractReplicator";
|
||||
import { useTargetFilters } from "@lib/serviceFeatures/targetFilter";
|
||||
import { useRemoteConfigurationMigration } from "@lib/serviceFeatures/remoteConfig";
|
||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
|
||||
import { AbstractModule } from "./modules/AbstractModule";
|
||||
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess";
|
||||
import { ModuleReplicator } from "./modules/core/ModuleReplicator";
|
||||
@@ -26,9 +26,10 @@ import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChec
|
||||
import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver";
|
||||
import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks";
|
||||
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain";
|
||||
import type { ServiceModules } from "./lib/src/interfaces/ServiceModule";
|
||||
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
|
||||
import { ModuleBasicMenu } from "./modules/essential/ModuleBasicMenu";
|
||||
import { usePrepareDatabaseForUse } from "./lib/src/serviceFeatures/prepareDatabaseForUse";
|
||||
import { usePrepareDatabaseForUse } from "@lib/serviceFeatures/prepareDatabaseForUse";
|
||||
import type { Constructor } from "@lib/common/utils.type";
|
||||
|
||||
export class LiveSyncBaseCore<
|
||||
T extends ServiceContext = ServiceContext,
|
||||
@@ -120,7 +121,7 @@ export class LiveSyncBaseCore<
|
||||
* @param constructor
|
||||
* @returns
|
||||
*/
|
||||
getModule<T extends AbstractModule>(constructor: new (...args: any[]) => T): T {
|
||||
getModule<T extends AbstractModule>(constructor: Constructor<T>): T {
|
||||
for (const module of this.modules) {
|
||||
if (module.constructor === constructor) return module as T;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# Self-hosted LiveSync CLI
|
||||
Command-line version of Self-hosted LiveSync plugin for syncing vaults without Obsidian.
|
||||
Command-line version of Self-hosted LiveSync plug-in for syncing vaults without Obsidian.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Sync Obsidian vaults using CouchDB without running Obsidian
|
||||
- ✅ Compatible with Self-hosted LiveSync plugin settings
|
||||
- ✅ Compatible with Self-hosted LiveSync plug-in settings
|
||||
- ✅ Supports all core sync features (encryption, conflict resolution, etc.)
|
||||
- ✅ Lightweight and headless operation
|
||||
- ✅ Cross-platform (Windows, macOS, Linux)
|
||||
|
||||
## Architecture
|
||||
|
||||
This CLI version is built using the same core as the Obsidian plugin:
|
||||
This CLI version is built using the same core as the Obsidian plug-in:
|
||||
|
||||
```
|
||||
CLI Main
|
||||
@@ -290,7 +290,7 @@ livesync-cli /path/to/your-local-database --settings /path/to/settings.json unlo
|
||||
|
||||
### Configuration
|
||||
|
||||
The CLI uses the same settings format as the Obsidian plugin. Create a `.livesync/settings.json` file in your vault directory:
|
||||
The CLI uses the same settings format as the Obsidian plug-in. Create a `.livesync/settings.json` file in your vault directory:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||
import { path } from "../node-compat";
|
||||
import { path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Conversion adapter implementation for Node.js
|
||||
@@ -22,7 +22,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 FilePath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NodeConversionAdapter } from "./NodeConversionAdapter";
|
||||
import { NodeStorageAdapter } from "./NodeStorageAdapter";
|
||||
import { NodeVaultAdapter } from "./NodeVaultAdapter";
|
||||
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Complete file system adapter implementation for Node.js
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FilePath } from "@lib/common/types";
|
||||
import type { IPathAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile } from "./NodeTypes";
|
||||
import { path } from "../node-compat";
|
||||
import { path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Path adapter implementation for Node.js
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UXDataWriteOptions } from "@lib/common/types";
|
||||
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeStat } from "./NodeTypes";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Storage adapter implementation for Node.js
|
||||
@@ -60,6 +60,7 @@ export class NodeStorageAdapter implements IStorageAdapter<NodeStat> {
|
||||
|
||||
async readBinary(p: string): Promise<ArrayBuffer> {
|
||||
const buffer = await fs.readFile(this.resolvePath(p));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required in environments where Buffer.buffer is ArrayBufferLike
|
||||
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,22 @@ 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 &&
|
||||
!(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 type { UXDataWriteOptions } from "@lib/common/types";
|
||||
import type { FilePath, UXDataWriteOptions } from "@lib/common/types";
|
||||
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Vault adapter implementation for Node.js
|
||||
@@ -31,6 +31,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
||||
const buffer = await fs.readFile(this.resolvePath(file.path));
|
||||
// Same correction as read() — ensure stat.size matches actual byte length.
|
||||
file.stat.size = buffer.length;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required in environments where Buffer.buffer is ArrayBufferLike
|
||||
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
|
||||
}
|
||||
|
||||
@@ -69,7 +70,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),
|
||||
@@ -92,7 +93,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),
|
||||
@@ -117,7 +118,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
||||
await this.delete(file, force);
|
||||
}
|
||||
|
||||
trigger(name: string, ...data: any[]): any {
|
||||
trigger(name: string, ...data: unknown[]): void {
|
||||
// No-op in CLI version (no event system)
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { P2P_DEFAULT_SETTINGS } from "@lib/common/types";
|
||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
import { LiveSyncError } from "@lib/common/LSError";
|
||||
|
||||
type CLIP2PPeer = {
|
||||
peerId: string;
|
||||
@@ -21,7 +22,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)");
|
||||
@@ -33,7 +34,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) {
|
||||
@@ -52,7 +53,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);
|
||||
@@ -81,7 +82,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> {
|
||||
@@ -107,11 +108,14 @@ 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 instanceof Error ? pullResult.error : LiveSyncError.fromError(pullResult.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");
|
||||
const err = pushResult?.error;
|
||||
throw err instanceof Error
|
||||
? err
|
||||
: LiveSyncError.fromError(err ?? "P2P sync failed while requesting remote sync");
|
||||
}
|
||||
|
||||
return targetPeer;
|
||||
@@ -120,7 +124,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;
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
type ObsidianLiveSyncSettings,
|
||||
REMOTE_COUCHDB,
|
||||
REMOTE_MINIO,
|
||||
type EntryMilestoneInfo,
|
||||
type EntryDoc,
|
||||
} from "@lib/common/types";
|
||||
import { ConnectionStringParser } from "@lib/common/ConnectionString";
|
||||
import { activateRemoteConfiguration, createRemoteConfigurationId } from "@lib/serviceFeatures/remoteConfig";
|
||||
@@ -17,7 +19,9 @@ import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./
|
||||
import { performFullScan } from "@lib/serviceFeatures/offlineScanner";
|
||||
import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
|
||||
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, "//***@");
|
||||
@@ -38,16 +42,20 @@ async function verifyRemoteState(
|
||||
}
|
||||
|
||||
try {
|
||||
let milestone: any;
|
||||
let milestone: EntryMilestoneInfo | false | undefined = undefined;
|
||||
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) {
|
||||
@@ -62,8 +70,9 @@ 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) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
process.stderr.write(`[Verification] Failed to fetch milestone document: ${message}\n`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,7 +102,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;
|
||||
@@ -150,9 +159,13 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
);
|
||||
}
|
||||
}
|
||||
pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
|
||||
pollTimer = compatGlobal.setTimeout(() => {
|
||||
void poll();
|
||||
}, currentIntervalMs);
|
||||
};
|
||||
let pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
|
||||
let pollTimer = compatGlobal.setTimeout(() => {
|
||||
void poll();
|
||||
}, currentIntervalMs);
|
||||
core.services.appLifecycle.onUnload.addHandler(async () => {
|
||||
compatGlobal.clearTimeout(pollTimer);
|
||||
return true;
|
||||
@@ -201,7 +214,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");
|
||||
}
|
||||
@@ -218,14 +231,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;
|
||||
@@ -438,9 +451,9 @@ 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 conflictsText = (doc._conflicts?.length ?? 0) > 0 ? doc._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<EntryDoc>(doc._id, {
|
||||
revs_info: true,
|
||||
});
|
||||
const pastRevisions = (rawDoc._revs_info ?? [])
|
||||
@@ -512,7 +525,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
if (revision === revisionToKeep) {
|
||||
continue;
|
||||
}
|
||||
const resolved = await core.services.conflict.resolveByDeletingRevision(targetPath, revision, "CLI");
|
||||
const resolved = await core.services.conflict.resolveByDeletingRevision(targetPath, revision ?? "", "CLI");
|
||||
if (!resolved) {
|
||||
process.stderr.write(`[Info] Failed to delete revision ${revision} for ${targetPath}\n`);
|
||||
return false;
|
||||
@@ -525,7 +538,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
console.error("[Command] mirror");
|
||||
const log = (msg: unknown) => console.error(`[Mirror] ${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") {
|
||||
|
||||
@@ -47,7 +47,7 @@ export interface CLIOptions {
|
||||
export interface CLICommandContext {
|
||||
databasePath: string;
|
||||
vaultPath: string;
|
||||
core: LiveSyncBaseCore<ServiceContext, any>;
|
||||
core: LiveSyncBaseCore<ServiceContext, never>;
|
||||
settingsPath: string;
|
||||
originalSyncSettings: Pick<
|
||||
ObsidianLiveSyncSettings,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { path, readline } from "../node-compat";
|
||||
import { path, readline } from "@/apps/cli/node-compat";
|
||||
|
||||
export function toArrayBuffer(data: Buffer): ArrayBuffer {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required in environments where Buffer.buffer is ArrayBufferLike
|
||||
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
// eslint-disable -- This is the entry point for the CLI application.
|
||||
import * as polyfill from "werift";
|
||||
import { main } from "./main";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill
|
||||
const rtcPolyfillCtor = (polyfill as any).RTCPeerConnection;
|
||||
if (typeof (globalThis as any).RTCPeerConnection === "undefined" && typeof rtcPolyfillCtor === "function") {
|
||||
if (
|
||||
typeof (compatGlobal as unknown as Record<string, unknown>).RTCPeerConnection === "undefined" &&
|
||||
typeof rtcPolyfillCtor === "function"
|
||||
) {
|
||||
// Fill only the standard WebRTC global in Node CLI runtime.
|
||||
(globalThis as any).RTCPeerConnection = rtcPolyfillCtor;
|
||||
(compatGlobal as unknown as Record<string, unknown>).RTCPeerConnection = rtcPolyfillCtor;
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -25,42 +25,45 @@ type PurgeMultiResult = {
|
||||
};
|
||||
type PurgeMultiParam = [docId: string, rev$$1: string];
|
||||
function appendPurgeSeqs(db: PouchDB.Database, docs: PurgeMultiParam[]) {
|
||||
return db
|
||||
.get("_local/purges")
|
||||
.then(function (doc: any) {
|
||||
for (const [docId, rev$$1] of docs) {
|
||||
const purgeSeq = doc.purgeSeq + 1;
|
||||
doc.purges.push({
|
||||
docId,
|
||||
rev: rev$$1,
|
||||
purgeSeq,
|
||||
});
|
||||
//@ts-ignore : missing type def
|
||||
if (doc.purges.length > db.purged_infos_limit) {
|
||||
return (
|
||||
db
|
||||
.get("_local/purges")
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Internal method patching.
|
||||
.then(function (doc: any) {
|
||||
for (const [docId, rev$$1] of docs) {
|
||||
const purgeSeq = doc.purgeSeq + 1;
|
||||
doc.purges.push({
|
||||
docId,
|
||||
rev: rev$$1,
|
||||
purgeSeq,
|
||||
});
|
||||
//@ts-ignore : missing type def
|
||||
doc.purges.splice(0, doc.purges.length - db.purged_infos_limit);
|
||||
if (doc.purges.length > db.purged_infos_limit) {
|
||||
//@ts-ignore : missing type def
|
||||
doc.purges.splice(0, doc.purges.length - db.purged_infos_limit);
|
||||
}
|
||||
doc.purgeSeq = purgeSeq;
|
||||
}
|
||||
doc.purgeSeq = purgeSeq;
|
||||
}
|
||||
return doc;
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err.status !== 404) {
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
_id: "_local/purges",
|
||||
purges: docs.map(([docId, rev$$1], idx) => ({
|
||||
docId,
|
||||
rev: rev$$1,
|
||||
purgeSeq: idx,
|
||||
})),
|
||||
purgeSeq: docs.length,
|
||||
};
|
||||
})
|
||||
.then(function (doc) {
|
||||
return db.put(doc);
|
||||
});
|
||||
return doc;
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err.status !== 404) {
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
_id: "_local/purges",
|
||||
purges: docs.map(([docId, rev$$1], idx) => ({
|
||||
docId,
|
||||
rev: rev$$1,
|
||||
purgeSeq: idx,
|
||||
})),
|
||||
purgeSeq: docs.length,
|
||||
};
|
||||
})
|
||||
.then(function (doc) {
|
||||
return db.put(doc);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+16
-14
@@ -184,6 +184,7 @@ export function parseArgs(): CLIOptions {
|
||||
break;
|
||||
default: {
|
||||
if (!databasePath) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Set checking
|
||||
if (command === "daemon" && VALID_COMMANDS.has(token as any)) {
|
||||
command = token as CLICommand;
|
||||
break;
|
||||
@@ -195,6 +196,7 @@ export function parseArgs(): CLIOptions {
|
||||
databasePath = token;
|
||||
break;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Set checking
|
||||
if (command === "daemon" && VALID_COMMANDS.has(token as any)) {
|
||||
command = token as CLICommand;
|
||||
break;
|
||||
@@ -232,15 +234,15 @@ async function createDefaultSettingsFile(options: CLIOptions) {
|
||||
const targetPath = options.settingsPath
|
||||
? path.resolve(options.settingsPath)
|
||||
: options.commandArgs[0]
|
||||
? path.resolve(options.commandArgs[0])
|
||||
: path.resolve(process.cwd(), "data.json");
|
||||
? path.resolve(options.commandArgs[0])
|
||||
: path.resolve(process.cwd(), "data.json");
|
||||
|
||||
if (!options.force) {
|
||||
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) {
|
||||
if (!(ex && (ex as { code?: string })?.code === "ENOENT")) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
@@ -303,7 +305,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);
|
||||
}
|
||||
@@ -323,8 +325,8 @@ export async function main() {
|
||||
options.command === "mirror" && options.commandArgs[0]
|
||||
? path.resolve(options.commandArgs[0])
|
||||
: options.vaultPath
|
||||
? path.resolve(options.vaultPath)
|
||||
: databasePath!;
|
||||
? path.resolve(options.vaultPath)
|
||||
: databasePath;
|
||||
|
||||
// Check if vault directory exists
|
||||
try {
|
||||
@@ -333,7 +335,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);
|
||||
}
|
||||
@@ -415,7 +417,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`);
|
||||
}
|
||||
@@ -427,14 +429,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
|
||||
(core) => {
|
||||
// Register P2P replicator feature.
|
||||
const _replicator = useP2PReplicatorFeature(core);
|
||||
useP2PReplicatorFeature(core);
|
||||
// Add target filter to prevent internal files are handled
|
||||
core.services.vault.isTargetFile.addHandler(async (target) => {
|
||||
const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target));
|
||||
@@ -458,8 +460,8 @@ export async function main() {
|
||||
if (rules.shouldIgnore(targetPath)) {
|
||||
return false;
|
||||
}
|
||||
// undefined = pass through to next handler in chain
|
||||
return undefined;
|
||||
// At least this handler think it is a target file, but other handlers may still veto it.
|
||||
return true;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -557,7 +559,7 @@ export async function main() {
|
||||
|
||||
if (options.command === "daemon" && result) {
|
||||
// Keep the process running
|
||||
await new Promise(() => { });
|
||||
await new Promise(() => {});
|
||||
} else {
|
||||
await core.services.control.onUnload();
|
||||
}
|
||||
|
||||
@@ -13,18 +13,29 @@ import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
|
||||
import type { NodeFile, NodeFolder } from "@/apps/cli/adapters/NodeTypes";
|
||||
import { watch as chokidarWatch, type FSWatcher } from "chokidar";
|
||||
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
|
||||
import { fsPromises as fs, path, type Stats } from "../node-compat";
|
||||
import { fsPromises as fs, path, type Stats } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +110,7 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
private basePath: string,
|
||||
private ignoreRules?: IgnoreRules,
|
||||
private watchEnabled: boolean = false
|
||||
) { }
|
||||
) {}
|
||||
|
||||
private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile {
|
||||
return {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
/* eslint-disable obsidianmd/no-nodejs-builtins */
|
||||
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
|
||||
import * as nodeFs from "node:fs";
|
||||
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
|
||||
import * as nodeFsPromises from "node:fs/promises";
|
||||
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
|
||||
import * as nodePath from "node:path";
|
||||
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
|
||||
import * as nodeReadlinePromises from "node:readline/promises";
|
||||
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
|
||||
import type { Stats } from "node:fs";
|
||||
export {
|
||||
nodeFs as fs,
|
||||
nodeFsPromises as fsPromises,
|
||||
nodePath as path,
|
||||
nodeReadlinePromises as readline,
|
||||
type Stats,
|
||||
};
|
||||
export { nodeFs as fs, nodeFsPromises as fsPromises, nodePath as path, nodeReadlinePromises as readline, type Stats };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"private": true,
|
||||
"version": "0.25.76-cli",
|
||||
"version": "0.25.77-cli",
|
||||
"main": "dist/index.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { IgnoreRules } from "./IgnoreRules";
|
||||
*/
|
||||
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,5 +1,5 @@
|
||||
import { minimatch } from "minimatch";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
|
||||
|
||||
/**
|
||||
* Loads and evaluates ignore rules from `.livesync/ignore` inside the vault.
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { InjectableDatabaseEventService } from "@lib/services/implements/in
|
||||
import type { IVaultService } from "@lib/services/base/IService";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
|
||||
import { fs as nodeFs, path as nodePath } from "../node-compat";
|
||||
import { fs as nodeFs, path as nodePath } from "@/apps/cli/node-compat";
|
||||
|
||||
const NODE_KV_TYPED_KEY = "__nodeKvType";
|
||||
const NODE_KV_VALUES_KEY = "values";
|
||||
@@ -178,7 +178,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");
|
||||
|
||||
@@ -248,7 +248,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,7 +264,7 @@ 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: unknown): Promise<void> => {
|
||||
await getDB().set(`${prefix}${key}`, value);
|
||||
},
|
||||
delete: async (key: string): Promise<void> => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fs as nodeFs, path as nodePath } from "../node-compat";
|
||||
import { fs as nodeFs, path as nodePath } from "@/apps/cli/node-compat";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions";
|
||||
|
||||
type LocalStorageShape = {
|
||||
getItem(key: string): string | null;
|
||||
@@ -83,8 +84,12 @@ function createNodeLocalStorageShim(): LocalStorageShape {
|
||||
}
|
||||
|
||||
export function ensureGlobalNodeLocalStorage() {
|
||||
if (!("localStorage" in globalThis) || typeof (globalThis as any).localStorage?.getItem !== "function") {
|
||||
(globalThis as any).localStorage = createNodeLocalStorageShim();
|
||||
const g = compatGlobal as unknown as Record<string, unknown>;
|
||||
if (
|
||||
!("localStorage" in g) ||
|
||||
typeof (g.localStorage as Record<string, unknown> | undefined)?.getItem !== "function"
|
||||
) {
|
||||
g.localStorage = createNodeLocalStorageShim();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ import { NodeKeyValueDBService } from "./NodeKeyValueDBService";
|
||||
import { NodeSettingService } from "./NodeSettingService";
|
||||
import { DatabaseService } from "@lib/services/base/DatabaseService";
|
||||
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||
import { path as nodePath } from "../node-compat";
|
||||
import { path as nodePath } from "@/apps/cli/node-compat";
|
||||
import type { KeyValueDBService } from "@lib/services/base/KeyValueDBService";
|
||||
|
||||
export class NodeServiceContext extends ServiceContext {
|
||||
databasePath: string;
|
||||
@@ -197,10 +198,10 @@ export class NodeServiceHub<T extends NodeServiceContext> extends InjectableServ
|
||||
path,
|
||||
API,
|
||||
config,
|
||||
keyValueDB: keyValueDB as any,
|
||||
keyValueDB: keyValueDB as unknown as KeyValueDBService<T>,
|
||||
control,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- (Forcibly )
|
||||
super(context, serviceInstancesToInit as any);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +77,7 @@ export class BackgroundCliProcess {
|
||||
if (this.combined.includes(needle)) return;
|
||||
const status = await Promise.race([
|
||||
this.child.status.then((s) => ({ type: "status" as const, status: s })),
|
||||
new Promise<{ type: "tick" }>((resolve) =>
|
||||
setTimeout(() => resolve({ type: "tick" }), 100)
|
||||
),
|
||||
new Promise<{ type: "tick" }>((resolve) => setTimeout(() => resolve({ type: "tick" }), 100)),
|
||||
]);
|
||||
if (status.type === "status") {
|
||||
throw new Error(
|
||||
|
||||
@@ -132,7 +132,7 @@ Deno.test("CLI file operations: push / cat / ls / info / rm / resolve / cat-rev
|
||||
assertEquals(data.path, REMOTE_PATH, "info .path mismatch");
|
||||
assertEquals(data.filename, REMOTE_PATH.split("/").at(-1), "info .filename mismatch");
|
||||
assert(typeof data.size === "number" && data.size >= 0, `info .size invalid: ${data.size}`);
|
||||
assert(typeof data.chunks === "number" && (data.chunks) >= 1, `info .chunks invalid: ${data.chunks}`);
|
||||
assert(typeof data.chunks === "number" && data.chunks >= 1, `info .chunks invalid: ${data.chunks}`);
|
||||
assertEquals(data.conflicts, "N/A", "info .conflicts should be N/A");
|
||||
console.log("[PASS] info output format matched");
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
/* Path mapping */
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
"@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
|
||||
|
||||
@@ -2,9 +2,12 @@ import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import path from "node:path";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/");
|
||||
const packageJson = JSON.parse(readFileSync("../../../package.json", "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync("../../../manifest.json", "utf-8"));
|
||||
const repoRoot = path.resolve(__dirname, "../../..");
|
||||
const packageJson = JSON.parse(readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
|
||||
// https://vite.dev/config/
|
||||
const defaultExternal = [
|
||||
"obsidian",
|
||||
|
||||
@@ -6,7 +6,7 @@ Note: (I vrtmrz have not tested this so much yet).
|
||||
|
||||
- 🌐 Runs entirely in the browser
|
||||
- 📁 Uses FileSystem API to access your local vault
|
||||
- 🔄 Syncs with CouchDB, Object Storage server (compatible with Self-hosted LiveSync plugin)
|
||||
- 🔄 Syncs with CouchDB, Object Storage server (compatible with Self-hosted LiveSync plug-in)
|
||||
- 🚫 No server-side code required!!
|
||||
- 💾 Settings stored in `.livesync/settings.json` within your vault
|
||||
- 👁️ Real-time file watching (Chrome 124+ with FileSystemObserver)
|
||||
@@ -127,7 +127,7 @@ webapp/
|
||||
1. **Adapters**: Implement `IFileSystemAdapter` interface using FileSystem API
|
||||
2. **Managers**: Handle storage events and file watching
|
||||
3. **Service Modules**: Integrate with LiveSyncBaseCore
|
||||
4. **Main**: Application initialization and lifecycle management
|
||||
4. **Main**: Application initialisation and lifecycle management
|
||||
|
||||
### Service Hub
|
||||
|
||||
@@ -154,11 +154,11 @@ Uses `BrowserServiceHub` which provides:
|
||||
- Settings stored in `.livesync/settings.json` in vault
|
||||
- Real-time file watching only with FileSystemObserver (Chrome 124+)
|
||||
|
||||
## Differences from Obsidian Plugin
|
||||
## Differences from Obsidian Plug-in
|
||||
|
||||
- No Obsidian-specific modules (UI, settings dialog, etc.)
|
||||
- No Obsidian-specific modules (UI, settings dialogue, etc.)
|
||||
- Simplified configuration
|
||||
- No plugin/theme sync features
|
||||
- No plug-in/theme sync features
|
||||
- No internal file handling (`.obsidian` folder)
|
||||
|
||||
## Development Notes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
||||
|
||||
@@ -28,7 +28,7 @@ export class FSAPIConversionAdapter implements IConversionAdapter<FSAPIFile, FSA
|
||||
path: folder.path,
|
||||
isFolder: true,
|
||||
children: [],
|
||||
parent: parentPath as any,
|
||||
parent: parentPath as FilePath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,11 @@ 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,11 @@ 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,24 @@ 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 &&
|
||||
!(file as { isFolder?: boolean }).isFolder
|
||||
);
|
||||
}
|
||||
|
||||
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 &&
|
||||
(item as { isFolder?: boolean }).isFolder === true &&
|
||||
"handle" in item
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,8 +116,7 @@ export class FSAPIVaultAdapter implements IVaultAdapter<FSAPIFile> {
|
||||
await this.delete(file, force);
|
||||
}
|
||||
|
||||
trigger(name: string, ...data: any[]): any {
|
||||
trigger(name: string, ...data: unknown[]): void {
|
||||
// No-op in webapp version (no event system yet)
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,19 +121,17 @@ async function initializeVaultSelector(): Promise<void> {
|
||||
await renderHistoryList();
|
||||
}
|
||||
|
||||
compatGlobal.addEventListener("load", async () => {
|
||||
try {
|
||||
await initializeVaultSelector();
|
||||
} catch (error) {
|
||||
compatGlobal.addEventListener("load", () => {
|
||||
initializeVaultSelector().catch((error) => {
|
||||
console.error("Failed to initialize vault selector:", error);
|
||||
setStatus("error", `Initialization failed: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
compatGlobal.addEventListener("beforeunload", () => {
|
||||
void app?.shutdown();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- patching
|
||||
(compatGlobal as any).livesyncApp = {
|
||||
getApp: () => app,
|
||||
historyStore,
|
||||
|
||||
+14
-12
@@ -23,7 +23,6 @@ import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
const SETTINGS_DIR = ".livesync";
|
||||
const SETTINGS_FILE = "settings.json";
|
||||
const DB_NAME = "livesync-webapp";
|
||||
|
||||
/**
|
||||
* Default settings for the webapp
|
||||
@@ -51,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) {
|
||||
@@ -65,7 +64,6 @@ class LiveSyncWebApp {
|
||||
console.log(`Vault directory: ${this.rootHandle.name}`);
|
||||
|
||||
// Create service context and hub
|
||||
const context = new ServiceContext();
|
||||
this.serviceHub = new BrowserServiceHub<ServiceContext>();
|
||||
|
||||
// Setup API service
|
||||
@@ -99,17 +97,19 @@ class LiveSyncWebApp {
|
||||
});
|
||||
|
||||
// App lifecycle handlers
|
||||
this.serviceHub.appLifecycle.scheduleRestart.setHandler(async () => {
|
||||
console.log("[AppLifecycle] Restart requested");
|
||||
await this.shutdown();
|
||||
await this.initialize();
|
||||
compatGlobal.setTimeout(() => {
|
||||
compatGlobal.location.reload();
|
||||
}, 1000);
|
||||
this.serviceHub.appLifecycle.scheduleRestart.setHandler(() => {
|
||||
void (async () => {
|
||||
console.log("[AppLifecycle] Restart requested");
|
||||
await this.shutdown();
|
||||
await this.initialize();
|
||||
compatGlobal.setTimeout(() => {
|
||||
compatGlobal.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);
|
||||
@@ -129,7 +129,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);
|
||||
@@ -207,6 +207,7 @@ class LiveSyncWebApp {
|
||||
}
|
||||
|
||||
// Scan the directory to populate file cache
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
|
||||
const fileAccess = (this.core as any)._serviceModules?.storageAccess?.vaultAccess;
|
||||
if (fileAccess?.fsapiAdapter) {
|
||||
console.log("[Scanning] Scanning vault directory...");
|
||||
@@ -225,6 +226,7 @@ class LiveSyncWebApp {
|
||||
console.log("[Shutdown] Shutting down...");
|
||||
|
||||
// Stop file watching
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
|
||||
const storageEventManager = (this.core as any)._serviceModules?.storageAccess?.storageEventManager;
|
||||
if (storageEventManager?.cleanup) {
|
||||
await storageEventManager.cleanup();
|
||||
|
||||
@@ -17,14 +17,25 @@ import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
* 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 &&
|
||||
!(file as { isFolder?: boolean }).isFolder
|
||||
);
|
||||
}
|
||||
|
||||
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 &&
|
||||
(item as { isFolder?: boolean }).isFolder === true &&
|
||||
"handle" in item
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,12 +155,14 @@ class FSAPIConverterAdapter implements IStorageEventConverterAdapter<FSAPIFile>
|
||||
* FileSystem API-specific watch adapter using FileSystemObserver (Chrome only)
|
||||
*/
|
||||
class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
|
||||
private observer: any = null; // FileSystemObserver type
|
||||
|
||||
constructor(private rootHandle: FileSystemDirectoryHandle) {}
|
||||
|
||||
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
|
||||
// Use FileSystemObserver if available (Chrome 124+)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing global FileSystemObserver
|
||||
if (typeof (compatGlobal as any).FileSystemObserver === "undefined") {
|
||||
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
|
||||
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
|
||||
@@ -157,11 +170,12 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
|
||||
const FileSystemObserver = (compatGlobal as any).FileSystemObserver;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
|
||||
this.observer = new FileSystemObserver(async (records: any[]) => {
|
||||
for (const record of records) {
|
||||
const handle = record.root;
|
||||
const changedHandle = record.changedHandle;
|
||||
const relativePathComponents = record.relativePathComponents;
|
||||
const type = record.type; // "appeared", "disappeared", "modified", "moved", "unknown", "errored"
|
||||
@@ -193,9 +207,9 @@ 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") {
|
||||
@@ -207,9 +221,9 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
ctime: Date.now(),
|
||||
type: "file" as const,
|
||||
},
|
||||
handle: null as any,
|
||||
handle: null as unknown as FileSystemFileHandle, // No handle available for disappeared files
|
||||
};
|
||||
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
|
||||
@@ -226,7 +240,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.stopWatch?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "livesync-webapp",
|
||||
"private": true,
|
||||
"version": "0.25.76-webapp",
|
||||
"version": "0.25.77-webapp",
|
||||
"type": "module",
|
||||
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
||||
"scripts": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -28,11 +28,38 @@ function stripPrefix(raw: string): string {
|
||||
return raw.replace(/^[^:]+:/, "");
|
||||
}
|
||||
|
||||
interface TestCore {
|
||||
services?: {
|
||||
replication?: {
|
||||
databaseQueueCount?: { value: number };
|
||||
storageApplyingCount?: { value: number };
|
||||
};
|
||||
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;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
@@ -47,8 +74,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 | null })?.core;
|
||||
if (!core) throw new Error("Vault not initialised – call livesyncTest.init() first");
|
||||
return core;
|
||||
}
|
||||
@@ -178,10 +205,10 @@ const livesyncTest: LiveSyncTestAPI = {
|
||||
if (docPath !== vaultPath) continue;
|
||||
return {
|
||||
path: docPath,
|
||||
revision: (doc._rev as string) ?? "",
|
||||
conflicts: (doc._conflicts as string[]) ?? [],
|
||||
size: (doc.size as number) ?? 0,
|
||||
mtime: (doc.mtime as number) ?? 0,
|
||||
revision: doc._rev ?? "",
|
||||
conflicts: doc._conflicts ?? [],
|
||||
size: doc.size ?? 0,
|
||||
mtime: doc.mtime ?? 0,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -201,4 +228,4 @@ const livesyncTest: LiveSyncTestAPI = {
|
||||
};
|
||||
|
||||
// Expose on window for Playwright page.evaluate() calls.
|
||||
(compatGlobal as any).livesyncTest = livesyncTest;
|
||||
(compatGlobal as unknown as Record<string, unknown>).livesyncTest = livesyncTest;
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
// "baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
"@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["*.ts", "**/*.ts", "**/*.tsx", "**/*.svelte"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -32,7 +32,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: "readwrite" }) => Promise<PermissionState>;
|
||||
requestPermission?: (options: { mode: "readwrite" }) => Promise<PermissionState>;
|
||||
};
|
||||
if (typeof h.queryPermission === "function") {
|
||||
const queried = await h.queryPermission({ mode: "readwrite" });
|
||||
if (queried === "granted") {
|
||||
@@ -91,7 +94,7 @@ export class VaultHistoryStore {
|
||||
|
||||
async getVaultHistory(): Promise<VaultHistoryItem[]> {
|
||||
return this.withStore("readonly", async (store) => {
|
||||
const keys = (await this.requestAsPromise(store.getAllKeys()));
|
||||
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++) {
|
||||
@@ -172,15 +175,17 @@ export class VaultHistoryStore {
|
||||
}
|
||||
|
||||
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
|
||||
const picker = (compatGlobal as any).showDirectoryPicker;
|
||||
const picker = (compatGlobal as unknown as Record<string, unknown>).showDirectoryPicker as
|
||||
| ((options?: { mode?: "readwrite" | "read"; startIn?: string }) => Promise<FileSystemDirectoryHandle>)
|
||||
| undefined;
|
||||
if (typeof picker !== "function") {
|
||||
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
|
||||
}
|
||||
|
||||
const handle = (await picker({
|
||||
const handle = await picker({
|
||||
mode: "readwrite",
|
||||
startIn: "documents",
|
||||
})) as FileSystemDirectoryHandle;
|
||||
});
|
||||
|
||||
const granted = await hasReadWritePermission(handle, true);
|
||||
if (!granted) {
|
||||
|
||||
@@ -3,10 +3,12 @@ import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import istanbul from "vite-plugin-istanbul";
|
||||
import path from "node:path";
|
||||
import { readFileSync } from "node:fs";
|
||||
const packageJson = JSON.parse(readFileSync("../../../package.json", "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync("../../../manifest.json", "utf-8"));
|
||||
const enableCoverage = process.env.PW_COVERAGE === "1";
|
||||
import { fileURLToPath } from "node:url";
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, "../../..");
|
||||
const packageJson = JSON.parse(readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
|
||||
const enableCoverage = process.env.PW_COVERAGE === "1";
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
font-family: system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
@@ -254,7 +254,7 @@ body.livesync-log-visible {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 8px 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-family: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #e2e8f0;
|
||||
@@ -397,6 +397,4 @@ popup {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(15px);
|
||||
border-radius: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "webpeer",
|
||||
"private": true,
|
||||
"version": "0.25.76-webpeer",
|
||||
"version": "0.25.77-webpeer",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
import { eventHub } from "@lib/hub/hub";
|
||||
|
||||
import type { Confirm } from "@lib/interfaces/Confirm";
|
||||
import { LOG_LEVEL_NOTICE, Logger } from "@lib/common/logger";
|
||||
import { storeP2PStatusLine } from "./CommandsShim";
|
||||
import { LOG_LEVEL_NOTICE, Logger, type LOG_LEVEL } from "@lib/common/logger";
|
||||
import {
|
||||
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
|
||||
type PeerStatus,
|
||||
@@ -66,7 +65,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
||||
}
|
||||
return this.db;
|
||||
}
|
||||
_simpleStore!: SimpleStore<any>;
|
||||
_simpleStore!: SimpleStore<unknown>;
|
||||
|
||||
async closeDB() {
|
||||
if (this.db) {
|
||||
@@ -83,11 +82,11 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
||||
replicator,
|
||||
p2pLogCollector,
|
||||
storeP2PStatusLine: p2pStatusLine,
|
||||
} = useP2PReplicator({ services: this.services } as any);
|
||||
} = useP2PReplicator({ services: this.services } as unknown as Parameters<typeof useP2PReplicator>[0]);
|
||||
this._liveSyncReplicator = replicator;
|
||||
this.p2pLogCollector = p2pLogCollector;
|
||||
p2pLogCollector.p2pReplicationLine.onChanged((line) => {
|
||||
storeP2PStatusLine.set(line.value);
|
||||
p2pStatusLine.value = line.value;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,15 +97,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;
|
||||
});
|
||||
@@ -148,7 +147,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 {
|
||||
@@ -157,7 +156,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> {
|
||||
@@ -281,7 +280,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);
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"moduleDetection": "force",
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
"@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
|
||||
@@ -2,7 +2,7 @@ import { deleteDB, type IDBPDatabase, openDB } from "idb";
|
||||
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
import { Logger } from "octagonal-wheels/common/logger";
|
||||
const databaseCache: { [key: string]: IDBPDatabase<any> } = {};
|
||||
const databaseCache: { [key: string]: IDBPDatabase<unknown> } = {};
|
||||
export { OpenKeyValueDatabase } from "./KeyValueDBv2.ts";
|
||||
|
||||
export const _OpenKeyValueDatabase = async (dbKey: string): Promise<KeyValueDatabase> => {
|
||||
@@ -11,7 +11,7 @@ export const _OpenKeyValueDatabase = async (dbKey: string): Promise<KeyValueData
|
||||
delete databaseCache[dbKey];
|
||||
}
|
||||
const storeKey = dbKey;
|
||||
let db: IDBPDatabase<any> | null = null;
|
||||
let db: IDBPDatabase<unknown> | null = null;
|
||||
const _openDB = () => {
|
||||
return serialized("keyvaluedb-" + dbKey, async () => {
|
||||
const dbInstance = await openDB(dbKey, 1, {
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function OpenKeyValueDatabase(dbKey: string): Promise<KeyValueDatab
|
||||
}
|
||||
|
||||
export class IDBKeyValueDatabase implements KeyValueDatabase {
|
||||
protected _dbPromise: Promise<IDBPDatabase<any>> | null = null;
|
||||
protected _dbPromise: Promise<IDBPDatabase<unknown>> | null = null;
|
||||
protected dbKey: string;
|
||||
protected storeKey: string;
|
||||
protected _isDestroyed: boolean = false;
|
||||
@@ -104,7 +104,7 @@ export class IDBKeyValueDatabase implements KeyValueDatabase {
|
||||
this.destroyedPromise = Promise.resolve();
|
||||
}
|
||||
}
|
||||
get DB(): Promise<IDBPDatabase<any>> {
|
||||
get DB(): Promise<IDBPDatabase<unknown>> {
|
||||
if (this._isDestroyed) {
|
||||
return Promise.reject(new Error("Database is destroyed"));
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export class IDBKeyValueDatabase implements KeyValueDatabase {
|
||||
}
|
||||
async get<U>(key: IDBValidKey): Promise<U> {
|
||||
const db = await this.DB;
|
||||
return await db.get(this.storeKey, key);
|
||||
return (await db.get(this.storeKey, key)) as U;
|
||||
}
|
||||
async set<U>(key: IDBValidKey, value: U): Promise<IDBValidKey> {
|
||||
const db = await this.DB;
|
||||
|
||||
@@ -4,10 +4,10 @@ import { eventHub, EVENT_PLUGIN_UNLOADED } from "./events";
|
||||
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
|
||||
type PeriodicProcessorHost = NecessaryServices<"API" | "control", never>;
|
||||
export class PeriodicProcessor {
|
||||
_process: () => Promise<any>;
|
||||
_process: () => Promise<unknown>;
|
||||
_timer?: number = undefined;
|
||||
_core: PeriodicProcessorHost;
|
||||
constructor(core: PeriodicProcessorHost, process: () => Promise<any>) {
|
||||
constructor(core: PeriodicProcessorHost, process: () => Promise<unknown>) {
|
||||
// this._plugin = plugin;
|
||||
this._core = core;
|
||||
this._process = process;
|
||||
|
||||
@@ -9,16 +9,16 @@ import { isCloudantURI } from "@lib/pouchdb/utils_couchdb";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions";
|
||||
import { manifestVersion, packageVersion } from "@lib/common/coreEnvVars";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
function redactObject(obj: Record<string, any>, dotted: string, redactedValue = "REDACTED") {
|
||||
function redactObject(obj: Record<string, unknown>, dotted: string, redactedValue = "REDACTED") {
|
||||
const keys = dotted.split(".");
|
||||
let current = obj;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current)) {
|
||||
current[key] = {} as Record<string, any>;
|
||||
current[key] = {};
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
current = current[key];
|
||||
current = current[key] as Record<string, unknown>;
|
||||
}
|
||||
const lastKey = keys[keys.length - 1];
|
||||
if (lastKey in current) {
|
||||
@@ -27,7 +27,7 @@ function redactObject(obj: Record<string, any>, dotted: string, redactedValue =
|
||||
return obj;
|
||||
}
|
||||
export async function generateReport(settings: ObsidianLiveSyncSettings, core: LiveSyncBaseCore) {
|
||||
let responseConfig: Record<string, any> = {};
|
||||
let responseConfig: Record<string, unknown> = {};
|
||||
const REDACTED = "𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷";
|
||||
if (settings.remoteType == REMOTE_COUCHDB) {
|
||||
try {
|
||||
@@ -42,7 +42,7 @@ export async function generateReport(settings: ObsidianLiveSyncSettings, core: L
|
||||
undefined,
|
||||
customHeaders
|
||||
);
|
||||
responseConfig = r.json as Record<string, any>;
|
||||
responseConfig = r.json as Record<string, unknown>;
|
||||
redactObject(responseConfig, "couch_httpd_auth.secret");
|
||||
redactObject(responseConfig, "couch_httpd_auth.authentication_db");
|
||||
redactObject(responseConfig, "couch_httpd_auth.authentication_redirect");
|
||||
|
||||
+13
-14
@@ -72,7 +72,7 @@ import {
|
||||
} from "@lib/common/typeUtils.ts";
|
||||
export { isInternalFile, getPathFromUXFileInfo, getStoragePathFromUXFileInfo, getDatabasePathFromUXFileInfo };
|
||||
|
||||
const memos: { [key: string]: any } = {};
|
||||
const memos: { [key: string]: unknown } = {};
|
||||
export function memoObject<T>(key: string, obj: T): T {
|
||||
memos[key] = obj;
|
||||
return memos[key] as T;
|
||||
@@ -87,7 +87,7 @@ export async function memoIfNotExist<T>(key: string, func: () => T | Promise<T>)
|
||||
}
|
||||
export function retrieveMemoObject<T>(key: string): T | false {
|
||||
if (key in memos) {
|
||||
return memos[key];
|
||||
return memos[key] as T;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export const _requestToCouchDBFetch = async (
|
||||
username: string,
|
||||
password: string,
|
||||
path?: string,
|
||||
body?: any,
|
||||
body?: unknown,
|
||||
method?: string
|
||||
) => {
|
||||
const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
|
||||
@@ -154,7 +154,7 @@ export const _requestToCouchDB = async (
|
||||
credentials: CouchDBCredentials,
|
||||
origin: string,
|
||||
path?: string,
|
||||
body?: any,
|
||||
body?: unknown,
|
||||
method?: string,
|
||||
customHeaders?: Record<string, string>
|
||||
) => {
|
||||
@@ -263,38 +263,37 @@ export function compareFileFreshness(
|
||||
const _cached = new Map<
|
||||
string,
|
||||
{
|
||||
value: any;
|
||||
context: Map<string, any>;
|
||||
value: unknown;
|
||||
context: Map<string, unknown>;
|
||||
}
|
||||
>();
|
||||
|
||||
export type MemoOption = {
|
||||
key: string;
|
||||
forceUpdate?: boolean;
|
||||
validator?: (context: Map<string, any>) => boolean;
|
||||
validator?: (context: Map<string, unknown>) => boolean;
|
||||
};
|
||||
|
||||
export function useMemo<T>(
|
||||
{ key, forceUpdate, validator }: MemoOption,
|
||||
updateFunc: (context: Map<string, any>, prev: T) => T
|
||||
updateFunc: (context: Map<string, unknown>, prev: T) => T
|
||||
): T {
|
||||
const cached = _cached.get(key);
|
||||
const context = cached?.context || new Map<string, any>();
|
||||
const context = cached?.context || new Map<string, unknown>();
|
||||
if (cached && !forceUpdate && (!validator || (validator && !validator(context)))) {
|
||||
return cached.value;
|
||||
return cached.value as T;
|
||||
}
|
||||
const value = updateFunc(context, cached?.value);
|
||||
const value = updateFunc(context, cached?.value as T);
|
||||
if (value !== cached?.value) {
|
||||
_cached.set(key, { value, context });
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// const _static = new Map<string, any>();
|
||||
const _staticObj = new Map<
|
||||
string,
|
||||
{
|
||||
value: any;
|
||||
value: unknown;
|
||||
}
|
||||
>();
|
||||
|
||||
@@ -390,7 +389,7 @@ export async function autosaveCache<K, V>(db: KeyValueDatabase, mapKey: string):
|
||||
};
|
||||
}
|
||||
|
||||
export function onlyInNTimes(n: number, proc: (progress: number) => any) {
|
||||
export function onlyInNTimes(n: number, proc: (progress: number) => unknown) {
|
||||
let counter = 0;
|
||||
return function () {
|
||||
if (counter++ % n == 0) {
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
import { type FilePath } from "./lib/src/common/types.ts";
|
||||
import { type FilePath } from "@lib/common/types.ts";
|
||||
|
||||
export {
|
||||
addIcon,
|
||||
@@ -31,6 +31,7 @@ export {
|
||||
TextComponent,
|
||||
ToggleComponent,
|
||||
DropdownComponent,
|
||||
Component,
|
||||
} from "obsidian";
|
||||
export type {
|
||||
DataWriteOptions,
|
||||
@@ -41,6 +42,8 @@ export type {
|
||||
ListedFiles,
|
||||
ValueComponent,
|
||||
Stat,
|
||||
Command,
|
||||
ViewCreator,
|
||||
} from "obsidian";
|
||||
import { normalizePath as normalizePath_ } from "obsidian";
|
||||
const normalizePath = normalizePath_ as <T extends string | FilePath>(from: T) => T;
|
||||
|
||||
@@ -1101,7 +1101,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
this._log(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL_NOTICE);
|
||||
if (data.category == "PLUGIN_DATA" || data.category == "PLUGIN_MAIN") {
|
||||
//@ts-ignore
|
||||
const manifests = Object.values(this.app.plugins.manifests) as any as PluginManifest[];
|
||||
const manifests = Object.values(this.app.plugins.manifests) as unknown as PluginManifest[];
|
||||
//@ts-ignore
|
||||
const enabledPlugins = this.app.plugins.enabledPlugins as Set<string>;
|
||||
const pluginManifest = manifests.find(
|
||||
|
||||
@@ -1225,7 +1225,7 @@ Offline Changed files: ${files.length}`;
|
||||
this.queuedNotificationFiles.clear();
|
||||
try {
|
||||
//@ts-ignore
|
||||
const manifests = Object.values(this.app.plugins.manifests) as any as PluginManifest[];
|
||||
const manifests = Object.values(this.app.plugins.manifests) as unknown as PluginManifest[];
|
||||
//@ts-ignore
|
||||
const enabledPlugins = this.app.plugins.enabledPlugins as Set<string>;
|
||||
const enabledPluginManifests = manifests.filter((e) => enabledPlugins.has(e.id));
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
import { MARK_DONE } from "@/modules/features/ModuleLog.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { __$checkInstanceBinding } from "@lib/dev/checks.ts";
|
||||
// import { __$checkInstanceBinding } from "@lib/dev/checks.ts";
|
||||
import { createInstanceLogFunction } from "@lib/services/lib/logUtils.ts";
|
||||
|
||||
let noticeIndex = 0;
|
||||
@@ -50,7 +50,7 @@ export abstract class LiveSyncCommands {
|
||||
this.core = core;
|
||||
this.onBindFunction(this.core, this.core.services);
|
||||
this._log = createInstanceLogFunction(this.constructor.name, this.services.API);
|
||||
__$checkInstanceBinding(this);
|
||||
// __$checkInstanceBinding(this);
|
||||
}
|
||||
abstract onunload(): void;
|
||||
abstract onload(): void | Promise<void>;
|
||||
@@ -67,24 +67,24 @@ export abstract class LiveSyncCommands {
|
||||
|
||||
_log: ReturnType<typeof createInstanceLogFunction>;
|
||||
|
||||
_verbose = (msg: any, key?: string) => {
|
||||
_verbose = (msg: unknown, key?: string) => {
|
||||
this._log(msg, LOG_LEVEL_VERBOSE, key);
|
||||
};
|
||||
|
||||
_info = (msg: any, key?: string) => {
|
||||
_info = (msg: unknown, key?: string) => {
|
||||
this._log(msg, LOG_LEVEL_INFO, key);
|
||||
};
|
||||
|
||||
_notice = (msg: any, key?: string) => {
|
||||
_notice = (msg: unknown, key?: string) => {
|
||||
this._log(msg, LOG_LEVEL_NOTICE, key);
|
||||
};
|
||||
_progress = (prefix: string = "", level: LOG_LEVEL = LOG_LEVEL_NOTICE) => {
|
||||
const key = `keepalive-progress-${noticeIndex++}`;
|
||||
return {
|
||||
log: (msg: any) => {
|
||||
log: (msg: string) => {
|
||||
this._log(prefix + msg, level, key);
|
||||
},
|
||||
once: (msg: any) => {
|
||||
once: (msg: string) => {
|
||||
this._log(prefix + msg, level);
|
||||
},
|
||||
done: (msg: string = "Done") => {
|
||||
@@ -93,7 +93,7 @@ export abstract class LiveSyncCommands {
|
||||
};
|
||||
};
|
||||
|
||||
_debug = (msg: any, key?: string) => {
|
||||
_debug = (msg: unknown, key?: string) => {
|
||||
this._log(msg, LOG_LEVEL_VERBOSE, key);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { arrayToChunkedArray } from "octagonal-wheels/collection";
|
||||
import { EVENT_ANALYSE_DB_USAGE, EVENT_REQUEST_PERFORM_GC_V3, eventHub } from "@/common/events";
|
||||
import type { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
|
||||
import { delay } from "@lib/common/utils";
|
||||
import { isNotFoundError } from "@lib/common/utils.doc";
|
||||
// import { _requestToCouchDB } from "@/common/utils";
|
||||
const DB_KEY_SEQ = "gc-seq";
|
||||
const DB_KEY_CHUNK_SET = "chunk-set";
|
||||
@@ -394,7 +395,7 @@ Note: **Make sure to synchronise all devices before deletion.**
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
if ((ex as any)?.status == 404) {
|
||||
if (isNotFoundError(ex)) {
|
||||
this._log(`No revisions found for ${doc._id}`, LOG_LEVEL_VERBOSE);
|
||||
} else {
|
||||
this._log(`Error finding revisions for ${doc._id}`);
|
||||
@@ -474,14 +475,14 @@ Are you ready to delete unused chunks?`;
|
||||
include_docs: true,
|
||||
});
|
||||
for (const chunk of deleteChunks.rows) {
|
||||
if ((chunk as any)?.value?.deleted) {
|
||||
if ((chunk as { value?: { deleted?: boolean } })?.value?.deleted) {
|
||||
chunkSet.delete(chunk.key as DocumentID);
|
||||
}
|
||||
}
|
||||
const deleteDocs = deleteChunks.rows
|
||||
.filter((e) => "doc" in e)
|
||||
.map((e) => ({
|
||||
...(e as any).doc!,
|
||||
...(e as { doc?: EntryLeaf }).doc!,
|
||||
_deleted: true,
|
||||
}));
|
||||
|
||||
@@ -490,7 +491,7 @@ Are you ready to delete unused chunks?`;
|
||||
let successCount = 0;
|
||||
let errored = 0;
|
||||
for (const batch of deleteChunkBatch) {
|
||||
const results = await this.database.bulkDocs(batch as EntryLeaf[]);
|
||||
const results = await this.database.bulkDocs(batch);
|
||||
for (const result of results) {
|
||||
if ("ok" in result) {
|
||||
chunkSet.delete(result.id as DocumentID);
|
||||
@@ -698,7 +699,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
sharedChunkCount: 0,
|
||||
uniqueChunkSize: orphanChunkSize,
|
||||
sharedChunkSize: 0,
|
||||
} as any);
|
||||
} as const);
|
||||
|
||||
const csvSrc = result.map((e) => {
|
||||
return [
|
||||
@@ -737,7 +738,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
});
|
||||
// Probably no need to wait, but just in case.
|
||||
let timeout = 2 * 60 * 1000; // 2 minutes
|
||||
do {
|
||||
for (;;) {
|
||||
const status = await remote.db.info();
|
||||
if ("compact_running" in status && status?.compact_running) {
|
||||
this._notice("Compaction in progress on remote database...", "gc-compact");
|
||||
@@ -750,7 +751,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
if (compactResult && "ok" in compactResult) {
|
||||
this._notice("Compaction on remote database completed successfully.", "gc-compact");
|
||||
} else {
|
||||
|
||||
+1
-1
Submodule src/lib updated: c926417f82...90de158461
+3
-3
@@ -1,10 +1,10 @@
|
||||
import { getLanguage, Notice, Plugin, type App, type PluginManifest } from "./deps";
|
||||
import { setGetLanguage } from "./lib/src/common/coreEnvFunctions.ts";
|
||||
import { setGetLanguage } from "@lib/common/coreEnvFunctions.ts";
|
||||
setGetLanguage(getLanguage);
|
||||
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
|
||||
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
|
||||
import { ModuleDev } from "./modules/extras/ModuleDev.ts";
|
||||
// import { ModuleDev } from "./modules/extras/ModuleDev.ts";
|
||||
|
||||
import { ModuleInteractiveConflictResolver } from "./modules/features/ModuleInteractiveConflictResolver.ts";
|
||||
import { ModuleLog } from "./modules/features/ModuleLog.ts";
|
||||
@@ -153,7 +153,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
new ModuleObsidianDocumentHistory(this, core),
|
||||
new ModuleInteractiveConflictResolver(this, core),
|
||||
new ModuleObsidianGlobalHistory(this, core),
|
||||
new ModuleDev(this, core),
|
||||
// new ModuleDev(this, core),
|
||||
new SetupManager(core), // this should be moved to core?
|
||||
new ModuleMigration(core),
|
||||
];
|
||||
|
||||
@@ -20,7 +20,7 @@ import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "@/modules/c
|
||||
* Obsidian-specific type guard adapter
|
||||
*/
|
||||
class ObsidianTypeGuardAdapter implements IStorageEventTypeGuardAdapter<TFile, TFolder> {
|
||||
isFile(file: any): file is TFile {
|
||||
isFile(file: unknown): file is TFile {
|
||||
if (file instanceof TFile) {
|
||||
return true;
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class ObsidianTypeGuardAdapter implements IStorageEventTypeGuardAdapter<TFile, T
|
||||
return false;
|
||||
}
|
||||
|
||||
isFolder(item: any): item is TFolder {
|
||||
isFolder(item: unknown): item is TFolder {
|
||||
if (item instanceof TFolder) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -61,21 +61,21 @@ export abstract class AbstractModule<
|
||||
return this.testDone(false);
|
||||
}
|
||||
|
||||
async _test(key: string, process: () => Promise<any>) {
|
||||
this._log(`Testing ${key}`, LOG_LEVEL_VERBOSE);
|
||||
try {
|
||||
const ret = await process();
|
||||
if (ret !== true) {
|
||||
this.addTestResult(key, false, ret.toString());
|
||||
return this.testFail(`${key} failed: ${ret}`);
|
||||
}
|
||||
this.addTestResult(key, true, "");
|
||||
} catch (ex: any) {
|
||||
this.addTestResult(key, false, "Failed by Exception", ex.toString());
|
||||
return this.testFail(`${key} failed: ${ex}`);
|
||||
}
|
||||
return this.testDone();
|
||||
}
|
||||
// async _test(key: string, process: () => Promise<any>) {
|
||||
// this._log(`Testing ${key}`, LOG_LEVEL_VERBOSE);
|
||||
// try {
|
||||
// const ret = await process();
|
||||
// if (ret !== true) {
|
||||
// this.addTestResult(key, false, ret.toString());
|
||||
// return this.testFail(`${key} failed: ${ret}`);
|
||||
// }
|
||||
// this.addTestResult(key, true, "");
|
||||
// } catch (ex: any) {
|
||||
// this.addTestResult(key, false, "Failed by Exception", ex.toString());
|
||||
// return this.testFail(`${key} failed: ${ex}`);
|
||||
// }
|
||||
// return this.testDone();
|
||||
// }
|
||||
|
||||
isMainReady() {
|
||||
return this.services.appLifecycle.isReady();
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { type Prettify } from "@lib/common/types";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import type ObsidianLiveSyncPlugin from "@/main";
|
||||
import { AbstractModule } from "./AbstractModule.ts";
|
||||
import type { ChainableExecuteFunction, OverridableFunctionsKeys } from "./ModuleTypes";
|
||||
|
||||
export type IObsidianModuleBase = OverridableFunctionsKeys<ObsidianLiveSyncPlugin>;
|
||||
export type IObsidianModule = Prettify<Partial<IObsidianModuleBase>>;
|
||||
export type ModuleKeys = keyof IObsidianModule;
|
||||
export type ChainableModuleProps = ChainableExecuteFunction<ObsidianLiveSyncPlugin>;
|
||||
|
||||
export abstract class AbstractObsidianModule extends AbstractModule {
|
||||
get app() {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { Prettify } from "@lib/common/types";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
|
||||
export type OverridableFunctionsKeys<T> = {
|
||||
[K in keyof T as K extends `$${string}` ? K : never]: T[K];
|
||||
};
|
||||
|
||||
export type ChainableExecuteFunction<T> = {
|
||||
[K in keyof T as K extends `$${string}`
|
||||
? T[K] extends (...args: any) => ChainableFunctionResult
|
||||
? K
|
||||
: never
|
||||
: never]: T[K];
|
||||
};
|
||||
|
||||
export type ICoreModuleBase = OverridableFunctionsKeys<LiveSyncCore>;
|
||||
export type ICoreModule = Prettify<Partial<ICoreModuleBase>>;
|
||||
export type CoreModuleKeys = keyof ICoreModule;
|
||||
|
||||
export type ChainableFunctionResult =
|
||||
| Promise<boolean | undefined | string>
|
||||
| Promise<boolean | undefined>
|
||||
| Promise<boolean>
|
||||
| Promise<void>;
|
||||
export type ChainableFunctionResultOrAll = Promise<boolean | undefined | string | void>;
|
||||
|
||||
type AllExecuteFunction<T> = {
|
||||
[K in keyof T as K extends `$all${string}`
|
||||
? T[K] extends (...args: any[]) => ChainableFunctionResultOrAll
|
||||
? K
|
||||
: never
|
||||
: never]: T[K];
|
||||
};
|
||||
type EveryExecuteFunction<T> = {
|
||||
[K in keyof T as K extends `$every${string}`
|
||||
? T[K] extends (...args: any[]) => ChainableFunctionResult
|
||||
? K
|
||||
: never
|
||||
: never]: T[K];
|
||||
};
|
||||
type AnyExecuteFunction<T> = {
|
||||
[K in keyof T as K extends `$any${string}`
|
||||
? T[K] extends (...args: any[]) => ChainableFunctionResult
|
||||
? K
|
||||
: never
|
||||
: never]: T[K];
|
||||
};
|
||||
type InjectableFunction<T> = {
|
||||
[K in keyof T as K extends `$$${string}` ? (T[K] extends (...args: any[]) => any ? K : never) : never]: T[K];
|
||||
};
|
||||
export type AllExecuteProps = AllExecuteFunction<LiveSyncCore>;
|
||||
export type EveryExecuteProps = EveryExecuteFunction<LiveSyncCore>;
|
||||
export type AnyExecuteProps = AnyExecuteFunction<LiveSyncCore>;
|
||||
|
||||
export type AllInjectableProps = InjectableFunction<LiveSyncCore>;
|
||||
@@ -21,7 +21,7 @@ import { MARK_LOG_NETWORK_ERROR } from "@lib/services/lib/logUtils";
|
||||
|
||||
function isOnlineAndCanReplicate(
|
||||
errorManager: UnresolvedErrorManager,
|
||||
host: NecessaryServices<"API", any>,
|
||||
host: NecessaryServices<"API", never>,
|
||||
showMessage: boolean
|
||||
): Promise<boolean> {
|
||||
const errorMessage = "Network is offline";
|
||||
@@ -34,7 +34,7 @@ function isOnlineAndCanReplicate(
|
||||
}
|
||||
async function canReplicateWithPBKDF2(
|
||||
errorManager: UnresolvedErrorManager,
|
||||
host: NecessaryServices<"replicator" | "setting", any>,
|
||||
host: NecessaryServices<"replicator" | "setting", never>,
|
||||
showMessage: boolean
|
||||
): Promise<boolean> {
|
||||
const currentSettings = host.services.setting.currentSettings();
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Semaphore } from "octagonal-wheels/concurrency/semaphore_v2";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { isNotFoundError } from "@lib/common/utils.doc";
|
||||
|
||||
const KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT = "replicationResultProcessorSnapshot";
|
||||
type ReplicateResultProcessorState = {
|
||||
@@ -39,7 +40,7 @@ export class ReplicateResultProcessor {
|
||||
private log(message: string, level: LOG_LEVEL = LOG_LEVEL_INFO) {
|
||||
Logger(`[ReplicateResultProcessor] ${message}`, level);
|
||||
}
|
||||
private logError(e: any) {
|
||||
private logError(e: unknown) {
|
||||
Logger(e, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
private replicator: ModuleReplicator;
|
||||
@@ -466,8 +467,8 @@ export class ReplicateResultProcessor {
|
||||
return false; // This means that the document already processed (While no conflict existed).
|
||||
}
|
||||
return true; // This mostly should not happen, but we have to process it just in case.
|
||||
} catch (e: any) {
|
||||
if ("status" in e && e.status == 404) {
|
||||
} catch (e) {
|
||||
if (isNotFoundError(e)) {
|
||||
// getRaw failed due to not existing, it may not be happened normally especially on replication.
|
||||
// If the process caused by some other reason, we **probably** have to process it.
|
||||
// Note that this is not a common case.
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { REMOTE_P2P } from "@lib/common/models/setting.const.ts";
|
||||
|
||||
function valueToString(value: any) {
|
||||
function valueToString(value: string | number | boolean | object | undefined): string {
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ButtonComponent } from "@/deps.ts";
|
||||
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "@/deps.ts";
|
||||
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting, Component } from "@/deps.ts";
|
||||
import { EVENT_PLUGIN_UNLOADED, eventHub } from "@/common/events.ts";
|
||||
import { compatGlobal, type CompatIntervalHandle } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
@@ -148,6 +148,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
wideButton: boolean;
|
||||
|
||||
onSubmit: (result: string | false) => void;
|
||||
component: Component = new Component();
|
||||
|
||||
constructor(
|
||||
plugin: Plugin,
|
||||
@@ -189,6 +190,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
}
|
||||
|
||||
override onOpen() {
|
||||
this.component.load();
|
||||
const { contentEl } = this;
|
||||
this.titleEl.setText(this.title);
|
||||
const div = contentEl.createDiv();
|
||||
@@ -196,7 +198,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
userSelect: "text",
|
||||
webkitUserSelect: "text",
|
||||
});
|
||||
void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.plugin);
|
||||
void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.component);
|
||||
const buttonSetting = new Setting(contentEl);
|
||||
const labelWrapper = contentEl.createDiv();
|
||||
labelWrapper.addClass("sls-dialogue-note-wrapper");
|
||||
@@ -254,6 +256,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
|
||||
override onClose() {
|
||||
super.onClose();
|
||||
this.component.unload();
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
if (this.timer) {
|
||||
|
||||
@@ -80,7 +80,7 @@ export class ObsHttpHandler extends FetchHttpHandler {
|
||||
contentType = transformedHeaders["content-type"];
|
||||
}
|
||||
|
||||
let transformedBody: any = body;
|
||||
let transformedBody = body;
|
||||
if (ArrayBuffer.isView(body)) {
|
||||
transformedBody = new Uint8Array(body.buffer).buffer;
|
||||
}
|
||||
|
||||
@@ -36,10 +36,11 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
|
||||
this.services.appLifecycle.performRestart();
|
||||
}
|
||||
|
||||
initialCallback: any;
|
||||
initialCallback: (() => void) | undefined = undefined;
|
||||
|
||||
swapSaveCommand() {
|
||||
this._log("Modifying callback of the save command", LOG_LEVEL_VERBOSE);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Editor Tweaking
|
||||
const saveCommandDefinition = (this.app as any).commands?.commands?.["editor:save-file"];
|
||||
const save = saveCommandDefinition?.callback;
|
||||
if (typeof save === "function") {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
||||
import { delay } from "octagonal-wheels/promises";
|
||||
import { __onMissingTranslation } from "@lib/common/i18n";
|
||||
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
|
||||
import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||
import { eventHub } from "@/common/events";
|
||||
import { enableTestFunction } from "./devUtil/testUtils.ts";
|
||||
// import { enableTestFunction } from "./devUtil/testUtils.ts";
|
||||
import { TestPaneView, VIEW_TYPE_TEST } from "./devUtil/TestPaneView.ts";
|
||||
import { writable } from "svelte/store";
|
||||
import type { FilePathWithPrefix } from "@lib/common/types.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import type { WorkspaceLeaf } from "@/deps.ts";
|
||||
export class ModuleDev extends AbstractObsidianModule {
|
||||
_everyOnloadStart(): Promise<boolean> {
|
||||
__onMissingTranslation(() => {});
|
||||
@@ -37,57 +37,7 @@ export class ModuleDev extends AbstractObsidianModule {
|
||||
|
||||
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||
this.onMissingTranslation = this.onMissingTranslation.bind(this);
|
||||
__onMissingTranslation((key) => {
|
||||
void this.onMissingTranslation(key);
|
||||
});
|
||||
type STUB = {
|
||||
toc: Set<string>;
|
||||
stub: { [key: string]: { [key: string]: Map<string, Record<string, string>> } };
|
||||
};
|
||||
eventHub.onEvent("document-stub-created", (detail: STUB) => {
|
||||
fireAndForget(async () => {
|
||||
const stub = detail.stub;
|
||||
const toc = detail.toc;
|
||||
|
||||
const stubDocX = Object.entries(stub)
|
||||
.map(([key, value]) => {
|
||||
return [
|
||||
`## ${key}`,
|
||||
Object.entries(value)
|
||||
.map(([key2, value2]) => {
|
||||
return [
|
||||
`### ${key2}`,
|
||||
[...value2.entries()].map(([key3, value3]) => {
|
||||
// return `#### ${key3}` + "\n" + JSON.stringify(value3);
|
||||
const isObsolete = value3["is_obsolete"] ? " (obsolete)" : "";
|
||||
const desc = value3["desc"] ?? "";
|
||||
const key = value3["key"] ? "Setting key: " + value3["key"] + "\n" : "";
|
||||
return `#### ${key3}${isObsolete}\n${key}${desc}\n`;
|
||||
}),
|
||||
].flat();
|
||||
})
|
||||
.flat(),
|
||||
].flat();
|
||||
})
|
||||
.flat();
|
||||
const stubDocMD =
|
||||
`
|
||||
| Icon | Description |
|
||||
| :---: | ----------------------------------------------------------------- |
|
||||
` +
|
||||
[...toc.values()].map((e) => `${e}`).join("\n") +
|
||||
"\n\n" +
|
||||
stubDocX.join("\n");
|
||||
await this.core.storageAccess.writeHiddenFileAuto(
|
||||
this.app.vault.configDir + "/ls-debug/stub-doc.md",
|
||||
stubDocMD
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
enableTestFunction(this.plugin);
|
||||
this.registerView(VIEW_TYPE_TEST, (leaf) => new TestPaneView(leaf, this.plugin, this));
|
||||
this.registerView(VIEW_TYPE_TEST, (leaf: WorkspaceLeaf) => new TestPaneView(leaf, this.plugin, this));
|
||||
this.addCommand({
|
||||
id: "view-test",
|
||||
name: "Open Test dialogue",
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { fireAndForget } from "@lib/common/utils.ts";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
|
||||
let plugin: ObsidianLiveSyncPlugin;
|
||||
export function enableTestFunction(plugin_: ObsidianLiveSyncPlugin) {
|
||||
plugin = plugin_;
|
||||
}
|
||||
export function addDebugFileLog(message: any, stackLog = false) {
|
||||
fireAndForget(
|
||||
serialized("debug-log", async () => {
|
||||
const now = new Date();
|
||||
const filename = `debug-log`;
|
||||
const time = now.toISOString().split("T")[0];
|
||||
const outFile = `${filename}${time}.jsonl`;
|
||||
// const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
|
||||
const timestamp = now.toLocaleString();
|
||||
const timestampEpoch = now;
|
||||
let out = { timestamp: timestamp, epoch: timestampEpoch } as Record<string, any>;
|
||||
if (message instanceof Error) {
|
||||
// debugger;
|
||||
// console.dir(message.stack);
|
||||
out = { ...out, message };
|
||||
} else if (stackLog) {
|
||||
if (stackLog) {
|
||||
const stackE = new Error();
|
||||
const stack = stackE.stack;
|
||||
out = { ...out, stack };
|
||||
}
|
||||
}
|
||||
if (typeof message == "object") {
|
||||
out = { ...out, ...message };
|
||||
} else {
|
||||
out = {
|
||||
result: message,
|
||||
};
|
||||
}
|
||||
// const out = "--" + timestamp + "--\n" + messageContent + " " + (stack || "");
|
||||
// const out
|
||||
try {
|
||||
await plugin.core.storageAccess.appendHiddenFile(
|
||||
plugin.core.services.API.getSystemConfigDir() + "/ls-debug/" + outFile,
|
||||
JSON.stringify(out) + "\n"
|
||||
);
|
||||
} catch {
|
||||
//NO OP
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -99,9 +99,11 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (!file && id) {
|
||||
this.file = this.services.path.id2path(id);
|
||||
}
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api -- loadLocalStorage is supported in Obsidian 1.7.2+
|
||||
if (this.app.loadLocalStorage("ols-history-highlightdiff") == "1") {
|
||||
this.showDiff = true;
|
||||
}
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api -- loadLocalStorage is supported in Obsidian 1.7.2+
|
||||
if (this.app.loadLocalStorage("ols-history-diffonly") == "1") {
|
||||
this.diffOnly = true;
|
||||
}
|
||||
@@ -139,7 +141,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.range.value = `${this.revs_info.length - 1 - rIndex}`;
|
||||
}
|
||||
}
|
||||
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
|
||||
const index = this.revs_info.length - 1 - (Number(this.range.value) || 0);
|
||||
const rev = this.revs_info[index];
|
||||
await this.showExactRev(rev.rev);
|
||||
}
|
||||
@@ -251,7 +253,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
}
|
||||
let rendered = false;
|
||||
if (this.showDiff) {
|
||||
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
||||
const prevRevIdx = this.revs_info.length - 1 - ((Number(this.range.value) || 0) - 1);
|
||||
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
||||
const oldRev = this.revs_info[prevRevIdx].rev;
|
||||
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
|
||||
@@ -550,8 +552,9 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (this.showDiff) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
checkbox.addEventListener("input", (evt: any) => {
|
||||
checkbox.addEventListener("input", (evt: Event) => {
|
||||
this.showDiff = checkbox.checked;
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api -- saveLocalStorage is supported in Obsidian 1.7.2+
|
||||
this.app.saveLocalStorage("ols-history-highlightdiff", this.showDiff == true ? "1" : null);
|
||||
this.updateDiffNavVisibility();
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
@@ -565,8 +568,9 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (this.diffOnly) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
checkbox.addEventListener("input", (evt: any) => {
|
||||
checkbox.addEventListener("input", (evt: Event) => {
|
||||
this.diffOnly = checkbox.checked;
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api -- saveLocalStorage is supported in Obsidian 1.7.2+
|
||||
this.app.saveLocalStorage("ols-history-diffonly", this.diffOnly == true ? "1" : null);
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
|
||||
import { VIEW_TYPE_GLOBAL_HISTORY, GlobalHistoryView } from "./GlobalHistory/GlobalHistoryView.ts";
|
||||
import type { WorkspaceLeaf } from "@/deps.ts";
|
||||
|
||||
export class ModuleObsidianGlobalHistory extends AbstractObsidianModule {
|
||||
_everyOnloadStart(): Promise<boolean> {
|
||||
@@ -11,7 +12,7 @@ export class ModuleObsidianGlobalHistory extends AbstractObsidianModule {
|
||||
},
|
||||
});
|
||||
|
||||
this.registerView(VIEW_TYPE_GLOBAL_HISTORY, (leaf) => new GlobalHistoryView(leaf, this.plugin));
|
||||
this.registerView(VIEW_TYPE_GLOBAL_HISTORY, (leaf: WorkspaceLeaf) => new GlobalHistoryView(leaf, this.plugin));
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ import { generateReport } from "@/common/reportTool.ts";
|
||||
|
||||
// DI the log again.
|
||||
const recentLogEntries = reactiveSource<LogEntry[]>([]);
|
||||
const globalLogFunction = (message: any, level?: number, key?: string) => {
|
||||
const globalLogFunction = (message: unknown, level?: number, key?: string) => {
|
||||
const messageX =
|
||||
message instanceof Error
|
||||
? new LiveSyncError("[Error Logged]: " + message.message, { cause: message })
|
||||
@@ -501,7 +501,7 @@ ${stringifyYaml(info)}
|
||||
})
|
||||
);
|
||||
}
|
||||
__addLog(message: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void {
|
||||
__addLog(message: unknown, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void {
|
||||
if (level == LOG_LEVEL_DEBUG && !showDebugLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@/deps.ts";
|
||||
import { unique } from "octagonal-wheels/collection";
|
||||
import { LEVEL_ADVANCED, LEVEL_POWER_USER, statusDisplay, type ConfigurationItem } from "@lib/common/types.ts";
|
||||
import { createStub, type ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import { type ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import {
|
||||
type AllSettingItemKey,
|
||||
getConfig,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
type AllBooleanItemKey,
|
||||
} from "./settingConstants.ts";
|
||||
import { $msg } from "@lib/common/i18n.ts";
|
||||
import { findAttrFromParent, wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts";
|
||||
import { wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts";
|
||||
|
||||
export class LiveSyncSetting extends Setting {
|
||||
autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent;
|
||||
@@ -35,37 +35,19 @@ export class LiveSyncSetting extends Setting {
|
||||
hasPassword: boolean = false;
|
||||
|
||||
invalidateValue?: () => void;
|
||||
setValue?: (value: any) => void;
|
||||
setValue?: (value: unknown) => void;
|
||||
constructor(containerEl: HTMLElement) {
|
||||
super(containerEl);
|
||||
LiveSyncSetting.env.settingComponents.push(this);
|
||||
}
|
||||
|
||||
_createDocStub(key: string, value: string | DocumentFragment) {
|
||||
DEV: {
|
||||
const paneName = findAttrFromParent(this.settingEl, "data-pane");
|
||||
const panelName = findAttrFromParent(this.settingEl, "data-panel");
|
||||
const itemName =
|
||||
typeof this.nameBuf == "string" ? this.nameBuf : (this.nameBuf.textContent?.toString() ?? "");
|
||||
const strValue = typeof value == "string" ? value : (value.textContent?.toString() ?? "");
|
||||
|
||||
createStub(itemName, key, strValue, panelName, paneName);
|
||||
}
|
||||
}
|
||||
|
||||
override setDesc(desc: string | DocumentFragment): this {
|
||||
this.descBuf = desc;
|
||||
DEV: {
|
||||
this._createDocStub("desc", desc);
|
||||
}
|
||||
super.setDesc(desc);
|
||||
return this;
|
||||
}
|
||||
override setName(name: string | DocumentFragment): this {
|
||||
this.nameBuf = name;
|
||||
DEV: {
|
||||
this._createDocStub("name", name);
|
||||
}
|
||||
super.setName(name);
|
||||
return this;
|
||||
}
|
||||
@@ -84,11 +66,6 @@ export class LiveSyncSetting extends Setting {
|
||||
if (conf.desc) {
|
||||
this.setDesc(conf.desc);
|
||||
}
|
||||
DEV: {
|
||||
this._createDocStub("key", key);
|
||||
if (conf.obsolete) this._createDocStub("is_obsolete", "true");
|
||||
if (conf.level) this._createDocStub("level", conf.level);
|
||||
}
|
||||
|
||||
this.holdValue = opt?.holdValue || this.holdValue;
|
||||
this.selfKey = key;
|
||||
@@ -102,7 +79,7 @@ export class LiveSyncSetting extends Setting {
|
||||
}
|
||||
return conf;
|
||||
}
|
||||
autoWireComponent(component: ValueComponent<any>, conf?: ConfigurationItem, opt?: AutoWireOption) {
|
||||
autoWireComponent<T>(component: ValueComponent<T>, conf?: ConfigurationItem, opt?: AutoWireOption) {
|
||||
this.placeHolderBuf = conf?.placeHolder || opt?.placeHolder || "";
|
||||
if (conf?.level == LEVEL_ADVANCED) {
|
||||
this.settingEl.toggleClass("sls-setting-advanced", true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, PluginSettingTab } from "@/deps.ts";
|
||||
import { App, Component, PluginSettingTab } from "@/deps.ts";
|
||||
import {
|
||||
type ObsidianLiveSyncSettings,
|
||||
type RemoteDBSettings,
|
||||
@@ -40,12 +40,13 @@ import { JournalSyncMinio } from "@lib/replication/journal/objectstore/JournalSy
|
||||
import { paneChangeLog } from "./PaneChangeLog.ts";
|
||||
import {
|
||||
enableOnly,
|
||||
findAttrFromParent,
|
||||
getLevelStr,
|
||||
// findAttrFromParent,
|
||||
// getLevelStr,
|
||||
setLevelClass,
|
||||
setStyle,
|
||||
visibleOnly,
|
||||
type OnSavedHandler,
|
||||
type OnSavedHandlerFunc,
|
||||
type OnUpdateFunc,
|
||||
type OnUpdateResult,
|
||||
type PageFunctions,
|
||||
@@ -65,28 +66,14 @@ import { paneMaintenance } from "./PaneMaintenance.ts";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
// For creating a document
|
||||
const toc = new Set<string>();
|
||||
const stubs = {} as {
|
||||
[key: string]: { [key: string]: Map<string, Record<string, string>> };
|
||||
};
|
||||
export function createStub(name: string, key: string, value: string, panel: string, pane: string) {
|
||||
DEV: {
|
||||
if (!(pane in stubs)) {
|
||||
stubs[pane] = {};
|
||||
}
|
||||
if (!(panel in stubs[pane])) {
|
||||
stubs[pane][panel] = new Map<string, Record<string, string>>();
|
||||
}
|
||||
const old = stubs[pane][panel].get(name) ?? {};
|
||||
stubs[pane][panel].set(name, { ...old, [key]: value });
|
||||
scheduleTask("update-stub", 100, () => {
|
||||
eventHub.emitEvent("document-stub-created", { toc: toc, stub: stubs });
|
||||
});
|
||||
}
|
||||
}
|
||||
// const toc = new Set<string>();
|
||||
|
||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
private _lifetimeComponent: Component = new Component();
|
||||
get lifetimeComponent(): Component {
|
||||
return this._lifetimeComponent;
|
||||
}
|
||||
get core() {
|
||||
return this.plugin.core;
|
||||
}
|
||||
@@ -181,7 +168,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
// if (runOnSaved) {
|
||||
const handlers = this.onSavedHandlers
|
||||
.filter((e) => appliedKeys.indexOf(e.key) !== -1)
|
||||
.map((e) => Promise.resolve(e.handler(this.editingSettings[e.key as AllSettingItemKey])));
|
||||
.map((e) => Promise.resolve(e.handler(this.editingSettings[e.key])));
|
||||
await Promise.all(handlers);
|
||||
// }
|
||||
keys.forEach((e) => this.refreshSetting(e));
|
||||
@@ -287,7 +274,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
// UI Element Wrapper -->
|
||||
settingComponents = [] as Setting[];
|
||||
controlledElementFunc = [] as UpdateFunction[];
|
||||
onSavedHandlers = [] as OnSavedHandler<any>[];
|
||||
onSavedHandlers = [] as OnSavedHandler<AllSettingItemKey>[];
|
||||
|
||||
inWizard: boolean = false;
|
||||
|
||||
@@ -370,8 +357,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
return Promise.resolve(elm);
|
||||
}
|
||||
|
||||
addOnSaved<T extends AllSettingItemKey>(key: T, func: (value: AllSettings[T]) => Promise<void> | void) {
|
||||
this.onSavedHandlers.push({ key, handler: func });
|
||||
addOnSaved<T extends AllSettingItemKey>(key: T, func: OnSavedHandlerFunc<T>) {
|
||||
const newHandler = { key, handler: func } as OnSavedHandler<AllSettingItemKey>;
|
||||
this.onSavedHandlers.push(newHandler);
|
||||
}
|
||||
resetEditingSettings() {
|
||||
this._editingSettings = undefined;
|
||||
@@ -379,6 +367,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
|
||||
override hide() {
|
||||
super.hide();
|
||||
this._lifetimeComponent.unload();
|
||||
this.isShown = false;
|
||||
}
|
||||
isShown: boolean = false;
|
||||
@@ -663,8 +653,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
}
|
||||
|
||||
display(): void {
|
||||
override display(): void {
|
||||
const changeDisplay = this.changeDisplay.bind(this);
|
||||
// Make sure lifetime component is loaded for markdown rendering in panes.
|
||||
this._lifetimeComponent.load();
|
||||
const { containerEl } = this;
|
||||
this.settingComponents.length = 0;
|
||||
this.controlledElementFunc.length = 0;
|
||||
@@ -718,7 +710,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
visibleOnly(() => this.isNeedRebuildLocal() || this.isNeedRebuildRemote())
|
||||
);
|
||||
|
||||
let paneNo = 0;
|
||||
// let paneNo = 0;
|
||||
const addPane = (
|
||||
parentEl: HTMLElement,
|
||||
title: string,
|
||||
@@ -728,20 +720,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
level?: ConfigLevel
|
||||
) => {
|
||||
const el = this.createEl(parentEl, "div", { text: "" });
|
||||
DEV: {
|
||||
const mdTitle = `${paneNo++}. ${title}${getLevelStr(level ?? "")}`;
|
||||
el.setAttribute("data-pane", mdTitle);
|
||||
toc.add(
|
||||
`| ${icon} | [${mdTitle}](#${mdTitle
|
||||
.toLowerCase()
|
||||
.replace(/ /g, "-")
|
||||
.replace(/[^\w\s-]/g, "")}) | `
|
||||
);
|
||||
}
|
||||
|
||||
setLevelClass(el, level);
|
||||
// TODO: Refactor to use Obsidian's recommended way to create heading.
|
||||
// eslint-disable-next-line obsidianmd/settings-tab/no-manual-html-headings
|
||||
el.createEl("h3", { text: title, cls: "sls-setting-pane-title" });
|
||||
new Setting(el).setName(title).setHeading().setClass("sls-setting-pane-title");
|
||||
if (this.menuEl) {
|
||||
this.menuEl.createEl(
|
||||
"label",
|
||||
@@ -772,7 +753,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
// });
|
||||
return p;
|
||||
};
|
||||
const panelNoMap = {} as { [key: string]: number };
|
||||
// const panelNoMap = {} as { [key: string]: number };
|
||||
const addPanel = (
|
||||
parentEl: HTMLElement,
|
||||
title: string,
|
||||
@@ -781,15 +762,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
level?: ConfigLevel
|
||||
) => {
|
||||
const el = this.createEl(parentEl, "div", { text: "" }, callback, func);
|
||||
DEV: {
|
||||
const paneNo = findAttrFromParent(parentEl, "data-pane");
|
||||
if (!(paneNo in panelNoMap)) {
|
||||
panelNoMap[paneNo] = 0;
|
||||
}
|
||||
panelNoMap[paneNo] += 1;
|
||||
const panelNo = panelNoMap[paneNo];
|
||||
el.setAttribute("data-panel", `${panelNo}. ${title}${getLevelStr(level ?? "")}`);
|
||||
}
|
||||
setLevelClass(el, level);
|
||||
this.createEl(el, "h4", { text: title, cls: "sls-setting-panel-title" });
|
||||
const p = Promise.resolve(el);
|
||||
@@ -823,6 +795,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
return callback;
|
||||
};
|
||||
|
||||
// Add panes
|
||||
|
||||
// TODO: Refactor to new API style.
|
||||
void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelChangeLog"), "💬", 100, false).then(
|
||||
bindPane(paneChangeLog)
|
||||
);
|
||||
|
||||
@@ -20,7 +20,6 @@ export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElem
|
||||
undefined,
|
||||
visibleOnly(() => !this.isConfiguredAs("versionUpFlash", ""))
|
||||
);
|
||||
|
||||
this.createEl(
|
||||
cx,
|
||||
"div",
|
||||
@@ -58,6 +57,6 @@ export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElem
|
||||
});
|
||||
}
|
||||
fireAndForget(() =>
|
||||
MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", this.plugin)
|
||||
MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", this.lifetimeComponent)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type LoadedEntry,
|
||||
type MetaEntry,
|
||||
type FilePath,
|
||||
type EntryDoc,
|
||||
} from "@lib/common/types.ts";
|
||||
import { createBlob, getFileRegExp, isDocContentSame, readAsBlob } from "@lib/common/utils.ts";
|
||||
import { Logger } from "@lib/common/logger.ts";
|
||||
@@ -441,8 +442,8 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
||||
const newData = entriesToDelete.rows.map((e) => ({
|
||||
...e.doc,
|
||||
_deleted: true,
|
||||
}));
|
||||
const r = await this.core.localDatabase.bulkDocsRaw(newData as any[]);
|
||||
})) as EntryDoc[];
|
||||
const r = await this.core.localDatabase.bulkDocsRaw(newData);
|
||||
// Do not care about the result.
|
||||
Logger(
|
||||
`${r.length} items have been removed, to confirm how many items are left, please perform it again.`,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type ObsidianLiveSyncSettings,
|
||||
LOG_LEVEL_VERBOSE,
|
||||
} from "@lib/common/types.ts";
|
||||
import { Menu } from "@/deps.ts";
|
||||
import { Menu, type ButtonComponent } from "@/deps.ts";
|
||||
import { $msg } from "@lib/common/i18n.ts";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
@@ -37,9 +37,9 @@ import { syncActivatedRemoteSettings } from "./remoteConfigBuffer.ts";
|
||||
|
||||
function getSettingsFromEditingSettings(editingSettings: AllSettings): ObsidianLiveSyncSettings {
|
||||
const workObj = { ...editingSettings } as ObsidianLiveSyncSettings;
|
||||
const keys = Object.keys(OnDialogSettingsDefault);
|
||||
const keys = Object.keys(OnDialogSettingsDefault) as (keyof ObsidianLiveSyncSettings)[];
|
||||
for (const k of keys) {
|
||||
delete (workObj as any)[k];
|
||||
delete workObj[k];
|
||||
}
|
||||
return workObj;
|
||||
}
|
||||
@@ -72,7 +72,7 @@ function serializeRemoteConfiguration(settings: ObsidianLiveSyncSettings): strin
|
||||
return ConnectionStringParser.serialize({ type: "couchdb", settings });
|
||||
}
|
||||
|
||||
function setEmojiButton(button: any, emoji: string, tooltip: string) {
|
||||
function setEmojiButton(button: ButtonComponent, emoji: string, tooltip: string) {
|
||||
button.setButtonText(emoji);
|
||||
button.setTooltip(tooltip, { delay: 10, placement: "top" });
|
||||
// button.buttonEl.addClass("clickable-icon");
|
||||
|
||||
@@ -14,6 +14,7 @@ import { visibleOnly } from "./SettingPane.ts";
|
||||
import { DEFAULT_SETTINGS } from "@lib/common/types.ts";
|
||||
import { request } from "@/deps.ts";
|
||||
import { SetupManager, UserMode } from "@/modules/features/SetupManager.ts";
|
||||
import { LiveSyncError } from "@lib/common/LSError.ts";
|
||||
export function paneSetup(
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
@@ -145,8 +146,9 @@ export function paneSetup(
|
||||
let remoteTroubleShootMDSrc = "";
|
||||
try {
|
||||
remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`);
|
||||
} catch (ex: any) {
|
||||
remoteTroubleShootMDSrc = `${$msg("obsidianLiveSyncSettingTab.logErrorOccurred")}\n${ex.toString()}`;
|
||||
} catch (ex) {
|
||||
const err = LiveSyncError.fromError(ex);
|
||||
remoteTroubleShootMDSrc = `${$msg("obsidianLiveSyncSettingTab.logErrorOccurred")}\n${err.toString()}`;
|
||||
}
|
||||
const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace(
|
||||
/\((.*?(.png)|(.jpg))\)/g,
|
||||
@@ -158,7 +160,7 @@ export function paneSetup(
|
||||
`<a class='sls-troubleshoot-anchor'></a> [${$msg("obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting")}](${topPath}) [${$msg("obsidianLiveSyncSettingTab.linkPageTop")}](${filename})\n\n${remoteTroubleShootMD}`,
|
||||
troubleShootEl,
|
||||
`${rawRepoURI}`,
|
||||
this.plugin
|
||||
this.lifetimeComponent
|
||||
);
|
||||
// Menu
|
||||
troubleShootEl.querySelector<HTMLAnchorElement>(".sls-troubleshoot-anchor")?.parentElement?.setCssStyles({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { type Writable, writable, get } from "svelte/store";
|
||||
* Props passed to Svelte panels, containing a writable port
|
||||
* to communicate with the panel
|
||||
*/
|
||||
export type SveltePanelProps<T = any> = {
|
||||
export type SveltePanelProps<T = unknown> = {
|
||||
port: Writable<T | undefined>;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export type SveltePanelProps<T = any> = {
|
||||
* A class to manage a Svelte panel within Obsidian
|
||||
* Especially useful for settings panels
|
||||
*/
|
||||
export class SveltePanel<T = any> {
|
||||
export class SveltePanel<T = unknown> {
|
||||
private _mountedComponent: ReturnType<typeof mount>;
|
||||
private _componentValue = writable<T | undefined>(undefined);
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@lib/common/utils";
|
||||
import { getConfig, type AllSettingItemKey } from "./settingConstants";
|
||||
import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
|
||||
import { isNotFoundError } from "@lib/common/utils.doc";
|
||||
|
||||
/**
|
||||
* Generates a summary of P2P configuration settings
|
||||
@@ -90,10 +91,10 @@ export function getSummaryFromPartialSettings(setting: Partial<ObsidianLiveSyncS
|
||||
export async function copyMigrationDocs(docName: string, dbFrom: PouchDB.Database, dbTo: PouchDB.Database) {
|
||||
try {
|
||||
const doc = await dbFrom.get(docName);
|
||||
delete (doc as any)._rev;
|
||||
delete (doc as { _rev?: string })._rev;
|
||||
await dbTo.put(doc);
|
||||
} catch (e) {
|
||||
if ((e as any).status === 404) {
|
||||
if (isNotFoundError(e)) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fireAndForget, parseHeaderValues } from "@lib/common/utils";
|
||||
import { isCloudantURI } from "@lib/pouchdb/utils_couchdb";
|
||||
import { generateCredentialObject } from "@lib/replication/httplib";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
import { isUnauthorizedError } from "@lib/common/utils.doc";
|
||||
|
||||
export const checkConfig = async (
|
||||
checkResultDiv: HTMLDivElement | undefined,
|
||||
@@ -260,8 +261,8 @@ export const checkConfig = async (
|
||||
addResult($msg("obsidianLiveSyncSettingTab.msgDone"), ["ob-btn-config-head"]);
|
||||
addResult($msg("obsidianLiveSyncSettingTab.msgConnectionProxyNote"), ["ob-btn-config-info"]);
|
||||
Logger($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"), LOG_LEVEL_INFO);
|
||||
} catch (ex: any) {
|
||||
if (ex?.status == 401) {
|
||||
} catch (ex) {
|
||||
if (isUnauthorizedError(ex)) {
|
||||
isSuccessful = false;
|
||||
addResult($msg("obsidianLiveSyncSettingTab.errAccessForbidden"));
|
||||
addResult($msg("obsidianLiveSyncSettingTab.errCannotContinueTest"));
|
||||
|
||||
@@ -22,17 +22,17 @@
|
||||
detectedIssues.push({ message: `Error during testAndFixSettings: ${e}`, result: "error", classes: [] });
|
||||
}
|
||||
}
|
||||
function isErrorResult(result: ConfigCheckResult): result is ResultError | ResultErrorMessage {
|
||||
function isErrorResult(result: ConfigCheckResult): result is ResultError<unknown> | ResultErrorMessage {
|
||||
return "result" in result && result.result === "error";
|
||||
}
|
||||
function isFixableError(result: ConfigCheckResult): result is ResultError {
|
||||
function isFixableError(result: ConfigCheckResult): result is ResultError<unknown> {
|
||||
return isErrorResult(result) && "fix" in result && typeof result.fix === "function";
|
||||
}
|
||||
function isSuccessResult(result: ConfigCheckResult): result is { message: string; result: "ok"; value?: any } {
|
||||
return "result" in result && result.result === "ok";
|
||||
}
|
||||
let processing = $state(false);
|
||||
async function fixIssue(issue: ResultError) {
|
||||
async function fixIssue(issue: ResultError<unknown>) {
|
||||
try {
|
||||
processing = true;
|
||||
await issue.fix();
|
||||
|
||||
@@ -6,12 +6,17 @@ import { parseHeaderValues } from "@lib/common/utils";
|
||||
import { isCloudantURI } from "@lib/pouchdb/utils_couchdb";
|
||||
import { generateCredentialObject } from "@lib/replication/httplib";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
import { isUnauthorizedError } from "@lib/common/utils.doc";
|
||||
|
||||
export type ResultMessage = { message: string; classes: string[] };
|
||||
export type ResultErrorMessage = { message: string; result: "error"; classes: string[] };
|
||||
export type ResultOk = { message: string; result: "ok"; value?: any };
|
||||
export type ResultError = { message: string; result: "error"; value: any; fixMessage: string; fix(): Promise<void> };
|
||||
export type ConfigCheckResult = ResultOk | ResultError | ResultMessage | ResultErrorMessage;
|
||||
export type ResultOk<T> = { message: string; result: "ok"; value?: T };
|
||||
export type ResultError<T> = { message: string; result: "error"; value: T; fixMessage: string; fix(): Promise<void> };
|
||||
export type ConfigCheckResult<T = unknown, U = unknown> =
|
||||
| ResultOk<T>
|
||||
| ResultError<U>
|
||||
| ResultMessage
|
||||
| ResultErrorMessage;
|
||||
/**
|
||||
* Compares two version strings to determine if the baseVersion is greater than or equal to the version.
|
||||
* @param baseVersion a.b.c format
|
||||
@@ -37,7 +42,11 @@ function isGreaterThanOrEqual(baseVersion: string, version: string) {
|
||||
* @param value setting value to update
|
||||
* @returns true if the update was successful, false otherwise
|
||||
*/
|
||||
async function updateRemoteSetting(setting: ObsidianLiveSyncSettings, key: string, value: any) {
|
||||
async function updateRemoteSetting(
|
||||
setting: ObsidianLiveSyncSettings,
|
||||
key: string,
|
||||
value: string
|
||||
): Promise<true | string> {
|
||||
const customHeaders = parseHeaderValues(setting.couchDB_CustomHeaders);
|
||||
const credential = generateCredentialObject(setting);
|
||||
const res = await requestToCouchDBWithCredentials(
|
||||
@@ -62,24 +71,29 @@ async function updateRemoteSetting(setting: ObsidianLiveSyncSettings, key: strin
|
||||
* @returns Array of ConfigCheckResult
|
||||
*/
|
||||
export const checkConfig = async (editingSettings: ObsidianLiveSyncSettings) => {
|
||||
const result = [] as ConfigCheckResult[];
|
||||
const result = [] as ConfigCheckResult<unknown, unknown>[];
|
||||
const addMessage = (msg: string, classes: string[] = []) => {
|
||||
result.push({ message: msg, classes });
|
||||
};
|
||||
const addSuccess = (msg: string, value?: any) => {
|
||||
const addSuccess = <T>(msg: string, value?: T) => {
|
||||
result.push({ message: msg, result: "ok", value });
|
||||
};
|
||||
const _addError = (message: string, fixMessage: string, fix: () => Promise<void>, value?: any) => {
|
||||
const _addError = <T>(message: string, fixMessage: string, fix: () => Promise<void>, value?: T) => {
|
||||
result.push({ message, result: "error", fixMessage, fix, value });
|
||||
};
|
||||
const addErrorMessage = (msg: string, classes: string[] = []) => {
|
||||
result.push({ message: msg, result: "error", classes });
|
||||
};
|
||||
|
||||
const addError = (message: string, fixMessage: string, key: string, expected: any) => {
|
||||
_addError(message, fixMessage, async () => {
|
||||
await updateRemoteSetting(editingSettings, key, expected);
|
||||
});
|
||||
const addError = (message: string, fixMessage: string, key: string, expected: string) => {
|
||||
_addError(
|
||||
message,
|
||||
fixMessage,
|
||||
async () => {
|
||||
await updateRemoteSetting(editingSettings, key, expected);
|
||||
},
|
||||
expected
|
||||
);
|
||||
};
|
||||
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.logCheckingDbConfig"));
|
||||
@@ -281,8 +295,8 @@ export const checkConfig = async (editingSettings: ObsidianLiveSyncSettings) =>
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgDone"), ["ob-btn-config-head"]);
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgConnectionProxyNote"), ["ob-btn-config-info"]);
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"));
|
||||
} catch (ex: any) {
|
||||
if (ex?.status == 401) {
|
||||
} catch (ex) {
|
||||
if (isUnauthorizedError(ex)) {
|
||||
addErrorMessage($msg("obsidianLiveSyncSettingTab.errAccessForbidden"));
|
||||
addErrorMessage($msg("obsidianLiveSyncSettingTab.errCannotContinueTest"));
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService";
|
||||
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext";
|
||||
import { Platform, type Command, type ViewCreator } from "obsidian";
|
||||
import { Platform, type Command, type ViewCreator } from "@/deps.ts";
|
||||
import { ObsHttpHandler } from "@/modules/essentialObsidian/APILib/ObsHttpHandler";
|
||||
import { ObsidianConfirm } from "./ObsidianConfirm";
|
||||
import type { Confirm } from "@lib/interfaces/Confirm";
|
||||
@@ -122,14 +122,15 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
|
||||
return this.context.plugin.addCommand(command) as TCommand;
|
||||
}
|
||||
|
||||
registerWindow(type: string, factory: ViewCreator): void {
|
||||
return this.context.plugin.registerView(type, factory);
|
||||
registerWindow<T>(type: string, factory: (leaf: T) => unknown): void {
|
||||
return this.context.plugin.registerView(type, factory as ViewCreator);
|
||||
}
|
||||
addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => any): HTMLElement {
|
||||
|
||||
addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => unknown): HTMLElement {
|
||||
return this.context.plugin.addRibbonIcon(icon, title, callback);
|
||||
}
|
||||
|
||||
registerProtocolHandler(action: string, handler: (params: Record<string, string>) => any): void {
|
||||
registerProtocolHandler(action: string, handler: (params: Record<string, string>) => unknown): void {
|
||||
return this.context.plugin.registerObsidianProtocolHandler(action, handler);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getLanguage } from "@/deps";
|
||||
import { createServiceFeature } from "@lib/interfaces/ServiceModule";
|
||||
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@lib/common/rosetta";
|
||||
import { $msg, setLang } from "@lib/common/i18n";
|
||||
import { $msg, __onMissingTranslation, setLang } from "@lib/common/i18n";
|
||||
|
||||
function tryGetLanguage() {
|
||||
try {
|
||||
@@ -15,6 +15,8 @@ function tryGetLanguage() {
|
||||
}
|
||||
|
||||
export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API } }) => {
|
||||
// Clear missing translation handler to avoid unnecessary warnings.
|
||||
__onMissingTranslation(() => {});
|
||||
let isChanged = false;
|
||||
const settings = setting.currentSettings();
|
||||
if (settings.displayLanguage == "") {
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function deleteFlagFile(host: NecessaryServices<never, "storageAcce
|
||||
}
|
||||
const REMOTE_KEEP_CURRENT = "Use active remote";
|
||||
const REMOTE_CANCEL = "Cancel";
|
||||
async function askAndActivateRemoteDatabase(host: NecessaryServices<"UI" | "setting", any>, log: LogFunction) {
|
||||
async function askAndActivateRemoteDatabase(host: NecessaryServices<"UI" | "setting", never>, log: LogFunction) {
|
||||
const settings = host.services.setting.currentSettings();
|
||||
if (settings.remoteConfigurations && Object.keys(settings.remoteConfigurations).length > 1) {
|
||||
const message =
|
||||
@@ -216,7 +216,7 @@ export function createFetchAllFlagHandler(
|
||||
* @returns updated configuration if applied, otherwise null.
|
||||
*/
|
||||
export async function adjustSettingToRemote(
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", any>,
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", never>,
|
||||
log: LogFunction,
|
||||
config: ObsidianLiveSyncSettings
|
||||
) {
|
||||
@@ -243,7 +243,7 @@ export async function adjustSettingToRemote(
|
||||
const necessary = extractObject(TweakValuesShouldMatchedTemplate, remoteTweaks);
|
||||
// Check if any necessary tweak value is different from current config.
|
||||
const differentItems = Object.entries(necessary).filter(([key, value]) => {
|
||||
return (config as any)[key] !== value;
|
||||
return config[key as keyof ObsidianLiveSyncSettings] !== value;
|
||||
});
|
||||
if (differentItems.length === 0) {
|
||||
log("Remote configuration matches local configuration. No changes applied.", LOG_LEVEL_NOTICE);
|
||||
@@ -260,7 +260,7 @@ export async function adjustSettingToRemote(
|
||||
|
||||
config = {
|
||||
...config,
|
||||
...Object.fromEntries(differentItems),
|
||||
...(Object.fromEntries(differentItems) as Partial<ObsidianLiveSyncSettings>),
|
||||
} satisfies ObsidianLiveSyncSettings;
|
||||
await host.services.setting.applyExternalSettings(config, true);
|
||||
log("Remote configuration applied.", LOG_LEVEL_NOTICE);
|
||||
@@ -277,7 +277,7 @@ export async function adjustSettingToRemote(
|
||||
* @param config current configuration to retrieve remote preferred config
|
||||
*/
|
||||
export async function adjustSettingToRemoteIfNeeded(
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", any>,
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", never>,
|
||||
log: LogFunction,
|
||||
extra: { preventFetchingConfig: boolean },
|
||||
config: ObsidianLiveSyncSettings
|
||||
@@ -309,7 +309,7 @@ export async function adjustSettingToRemoteIfNeeded(
|
||||
* @returns result of the process, or false if error occurs.
|
||||
*/
|
||||
export async function processVaultInitialisation(
|
||||
host: NecessaryServices<"setting", any>,
|
||||
host: NecessaryServices<"setting", never>,
|
||||
log: LogFunction,
|
||||
proc: () => Promise<boolean>,
|
||||
keepSuspending = false
|
||||
@@ -341,7 +341,7 @@ export async function processVaultInitialisation(
|
||||
}
|
||||
|
||||
export async function verifyAndUnlockSuspension(
|
||||
host: NecessaryServices<"setting" | "appLifecycle" | "UI", any>,
|
||||
host: NecessaryServices<"setting" | "appLifecycle" | "UI", never>,
|
||||
log: LogFunction
|
||||
) {
|
||||
if (!host.services.setting.currentSettings().suspendFileWatching) {
|
||||
|
||||
@@ -6,11 +6,11 @@ import { TFile, TFolder } from "obsidian";
|
||||
*/
|
||||
|
||||
export class ObsidianTypeGuardAdapter implements ITypeGuardAdapter<TFile, TFolder> {
|
||||
isFile(file: any): file is TFile {
|
||||
isFile(file: unknown): file is TFile {
|
||||
return file instanceof TFile;
|
||||
}
|
||||
|
||||
isFolder(item: any): item is TFolder {
|
||||
isFolder(item: unknown): item is TFolder {
|
||||
return item instanceof TFolder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { TFile, App, TFolder } from "obsidian";
|
||||
/**
|
||||
* Vault adapter implementation for Obsidian
|
||||
*/
|
||||
export class ObsidianVaultAdapter implements IVaultAdapter<TFile> {
|
||||
export class ObsidianVaultAdapter implements IVaultAdapter<TFile, TFolder> {
|
||||
constructor(private app: App) {}
|
||||
|
||||
async read(file: TFile): Promise<string> {
|
||||
@@ -38,24 +38,24 @@ export class ObsidianVaultAdapter implements IVaultAdapter<TFile> {
|
||||
}
|
||||
|
||||
async delete(file: TFile | TFolder, force = false): Promise<void> {
|
||||
// if ("trashFile" in this.app.fileManager) {
|
||||
// // eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||
// return await this.app.fileManager.trashFile(file);
|
||||
// }
|
||||
//TODO: need fix
|
||||
if ("trashFile" in this.app.fileManager) {
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||
return await this.app.fileManager.trashFile(file);
|
||||
}
|
||||
// eslint-disable-next-line obsidianmd/prefer-file-manager-trash-file -- Fallback for older versions of Obsidian without trashFile support
|
||||
return await this.app.vault.delete(file, force);
|
||||
}
|
||||
|
||||
async trash(file: TFile | TFolder, force = false): Promise<void> {
|
||||
// if ("trashFile" in this.app.fileManager) {
|
||||
// // eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||
// return await this.app.fileManager.trashFile(file);
|
||||
// }
|
||||
//TODO: need fix
|
||||
if ("trashFile" in this.app.fileManager) {
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||
return await this.app.fileManager.trashFile(file);
|
||||
}
|
||||
// eslint-disable-next-line obsidianmd/prefer-file-manager-trash-file -- Fallback for older versions of Obsidian without trashFile support
|
||||
return await this.app.vault.trash(file, force);
|
||||
}
|
||||
|
||||
trigger(name: string, ...data: any[]): any {
|
||||
trigger(name: string, ...data: unknown[]): void {
|
||||
return this.app.vault.trigger(name, ...data);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess";
|
||||
import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder";
|
||||
import type { IFileHandler } from "@lib/interfaces/FileHandler";
|
||||
import type { StorageAccess } from "@lib/interfaces/StorageAccess";
|
||||
import type { IServiceHub } from "./lib/src/services/base/IService";
|
||||
import type { IServiceHub } from "@lib/services/base/IService";
|
||||
|
||||
export interface ServiceModules {
|
||||
storageAccess: StorageAccess;
|
||||
|
||||
Reference in New Issue
Block a user