refactor: consume Commonlib as a package

This commit is contained in:
vorotamoroz
2026-07-17 11:13:16 +00:00
parent 6475d32769
commit a721d3b602
665 changed files with 3438 additions and 31592 deletions
+19 -19
View File
@@ -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/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 type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { __$checkInstanceBinding } from "@vrtmrz/livesync-commonlib/compat/dev/checks";
import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm";
import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess";
import type { Rebuilder } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseRebuilder";
import type { IFileHandler } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileHandler";
import type { StorageAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/StorageAccess";
import type { LiveSyncLocalDBEnv } from "@vrtmrz/livesync-commonlib/compat/pouchdb/LiveSyncLocalDB";
import type { LiveSyncCouchDBReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
import type { CheckPointInfo } from "@vrtmrz/livesync-commonlib/compat/replication/journal/JournalSyncTypes";
import type { LiveSyncJournalReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicatorEnv";
import type { LiveSyncReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/LiveSyncAbstractReplicator";
import { useTargetFilters } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/targetFilter";
import { useRemoteConfigurationMigration } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
import { AbstractModule } from "./modules/AbstractModule";
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess";
import { ModuleReplicator } from "./modules/core/ModuleReplicator";
@@ -26,10 +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/interfaces/ServiceModule";
import type { ServiceModules } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import { ModuleBasicMenu } from "./modules/essential/ModuleBasicMenu";
import { usePrepareDatabaseForUse } from "@lib/serviceFeatures/prepareDatabaseForUse";
import type { Constructor } from "@lib/common/utils.type";
import { usePrepareDatabaseForUse } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/prepareDatabaseForUse";
import type { Constructor } from "@vrtmrz/livesync-commonlib/compat/common/utils.type";
export class LiveSyncBaseCore<
T extends ServiceContext = ServiceContext,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
import type { IStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
/** One platform-neutral storage adapter contract case. */
export interface StorageAdapterContractCase {
+107
View File
@@ -0,0 +1,107 @@
import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm";
import MessageBox from "./ui/MessageBox.svelte";
import TextInputBox from "./ui/TextInputBox.svelte";
import { mount } from "svelte";
import { promiseWithResolvers } from "octagonal-wheels/promises";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
function displayMessageBox<T, U extends string[]>(
message: string,
buttons: U,
title: string,
commit: (ret: U[number]) => T
): Promise<T> {
const el = _activeDocument.createElement("div");
const p = promiseWithResolvers<T>();
mount(MessageBox, {
target: el,
props: {
message,
buttons: buttons as string[],
title: title,
commit: (action: U[number]) => {
const ret = commit(action);
p.resolve(ret);
},
},
});
_activeDocument.body.appendChild(el);
void p.promise.finally(() => {
el.remove();
});
return p.promise;
}
function promptForInput(
title: string,
key: string,
placeholder: string,
isPassword?: boolean
): Promise<string | false> {
const el = _activeDocument.createElement("div");
const p = promiseWithResolvers<string | false>();
mount(TextInputBox, {
target: el,
props: {
title,
message: key,
placeholder,
isPassword,
commit: (text: string | false) => {
p.resolve(text);
},
},
});
_activeDocument.body.appendChild(el);
void p.promise.finally(() => {
el.remove();
});
return p.promise;
}
export class BrowserConfirm<T extends ServiceContext> implements Confirm {
_context: T;
constructor(context: T) {
this._context = context;
}
askYesNo(message: string): Promise<"yes" | "no"> {
return displayMessageBox(message, ["Yes", "No"] as const, "Confirm", (action) =>
action == "Yes" ? "yes" : "no"
);
}
askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise<string | false> {
return promptForInput(title, key, placeholder, isPassword);
}
askYesNoDialog(
message: string,
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number }
): Promise<"yes" | "no"> {
return displayMessageBox(message, ["Yes", "No"] as const, opt.title ?? "Confirm", (action) =>
action == "Yes" ? "yes" : "no"
);
}
askSelectString(message: string, items: string[]): Promise<string> {
return displayMessageBox(message, [...items] as const, "Confirm", (action) => action);
}
askSelectStringDialogue<T extends readonly string[]>(
message: string,
buttons: T,
opt: { title?: string; defaultAction: T[number]; timeout?: number }
): Promise<T[number] | false> {
return displayMessageBox(message, [...buttons] as const, opt.title ?? "Confirm", (action) => action);
}
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void {
throw new Error("Method not implemented.");
}
confirmWithMessage(
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
timeout?: number
): Promise<(typeof buttons)[number] | false> {
return displayMessageBox(contentMd, [...buttons] as const, title ?? "Confirm", (action) => action);
}
}
+71
View File
@@ -0,0 +1,71 @@
import { promiseWithResolver, type PromiseWithResolvers } from "octagonal-wheels/promises";
import { mount } from "svelte";
import MenuView from "./ui/MenuView.svelte";
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
export class MenuItem {
type = "item";
title = "";
handler?: () => void | Promise<void>;
icon: string = "";
setTitle(title: string) {
this.title = title;
return this;
}
onClick(callback: () => void | Promise<void>) {
this.handler = callback;
return this;
}
setIcon(icon: string | null) {
this.icon = icon || "";
return this;
}
}
export class MenuSeparator {
type = "separator";
}
export class Menu {
type = "menu";
items: (MenuItem | MenuSeparator)[] = [];
constructor() {}
addItem(callback: (item: MenuItem) => void) {
const item = new MenuItem();
callback(item);
this.items.push(item);
return this;
}
addSeparator() {
this.items.push(new MenuSeparator());
return this;
}
waitingForClose?: PromiseWithResolvers<void>;
showAtPosition(pos: { x: number; y: number }) {
const el = _activeDocument.createElement("div");
if (this.waitingForClose) {
this.waitingForClose.resolve();
}
this.waitingForClose = promiseWithResolver<void>();
mount(MenuView, {
target: el,
props: {
items: this.items,
closeMenu: () => {
this.waitingForClose?.resolve();
this.waitingForClose = undefined;
},
x: pos.x,
y: pos.y,
},
});
_activeDocument.body.appendChild(el);
void this.waitingForClose.promise.finally(() => {
el.remove();
});
return this.waitingForClose.promise;
}
hide() {
this.waitingForClose?.resolve();
this.waitingForClose = undefined;
}
}
@@ -0,0 +1,79 @@
import {
type ComponentHasResult,
SvelteDialogManagerBase,
SvelteDialogMixIn,
} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import type { SvelteDialogManagerDependencies } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog";
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import DialogHost from "@/modules/services/LiveSyncUI/DialogHost.svelte";
export class ShimModal {
contentEl: HTMLElement;
titleEl: HTMLElement;
modalEl: HTMLElement;
isOpen: boolean = false;
baseEl: HTMLElement;
constructor() {
const baseEl = _activeDocument.createElement("popup");
this.baseEl = baseEl;
this.contentEl = _activeDocument.createElement("div");
this.contentEl.className = "modal-content";
this.titleEl = _activeDocument.createElement("div");
this.titleEl.className = "modal-title";
this.modalEl = _activeDocument.createElement("div");
this.modalEl.className = "modal";
this.modalEl.style.display = "none";
this.modalEl.appendChild(this.titleEl);
this.modalEl.appendChild(this.contentEl);
this.baseEl.appendChild(this.modalEl);
}
open() {
this.isOpen = true;
this.modalEl.style.display = "block";
if (!this.baseEl.parentElement) {
_activeDocument.body.appendChild(this.baseEl);
}
this.onOpen();
}
close() {
this.isOpen = false;
this.baseEl.style.display = "none";
this.baseEl.remove();
this.onClose();
}
onOpen() {}
onClose() {}
setPlaceholder(p: string) {}
setTitle(t: string) {
this.titleEl.textContent = t;
}
}
const BrowserSvelteDialogBase = SvelteDialogMixIn(ShimModal, DialogHost);
export class LiveSyncBrowserDialog<T, U, C extends ServiceContext = ServiceContext> extends BrowserSvelteDialogBase<
T,
U,
C
> {
constructor(
context: C,
dependents: SvelteDialogManagerDependencies<C>,
component: ComponentHasResult<T, U>,
initialData?: U
) {
super();
this.initDialog(context, dependents, component, initialData);
}
}
export class BrowserSvelteDialogManager<T extends ServiceContext> extends SvelteDialogManagerBase<T> {
override async openSvelteDialog<TT, TU>(
component: ComponentHasResult<TT, TU>,
initialData?: TU
): Promise<TT | undefined> {
const dialog = new LiveSyncBrowserDialog<TT, TU, T>(this.context, this.dependents, component, initialData);
dialog.open();
return await dialog.waitForClose();
}
}
@@ -0,0 +1,25 @@
import type { BrowserServiceHostDependencies } from "@vrtmrz/livesync-commonlib/compat/services/BrowserServices";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { UIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/UIService";
import DialogToCopy from "@/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte";
import { BrowserSvelteDialogManager } from "./BrowserSvelteDialogManager";
export class LiveSyncBrowserUIService<T extends ServiceContext> extends UIService<T> {
override get dialogToCopy() {
return DialogToCopy;
}
constructor(context: T, dependents: BrowserServiceHostDependencies<T>) {
const browserConfirm = dependents.API.confirm;
const obsidianSvelteDialogManager = new BrowserSvelteDialogManager<T>(context, {
appLifecycle: dependents.appLifecycle,
config: dependents.config,
replicator: dependents.replicator,
confirm: browserConfirm,
control: dependents.control,
});
super(context, {
dialogManager: obsidianSvelteDialogManager,
APIService: dependents.API,
});
}
}
@@ -0,0 +1,33 @@
import { BrowserServiceHub, type BrowserServiceHost } from "@vrtmrz/livesync-commonlib/compat/services/BrowserServices";
import type { KeyValueDatabaseFactory } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { BrowserAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/BrowserAPIService";
import { BrowserConfirm } from "./BrowserConfirm";
import { LiveSyncBrowserUIService } from "./LiveSyncBrowserUIService";
export type LiveSyncBrowserServiceHubOptions<T extends ServiceContext> = {
context?: T;
openKeyValueDatabase?: KeyValueDatabaseFactory;
};
function createLiveSyncBrowserHost<T extends ServiceContext>(): BrowserServiceHost<T> {
return {
createAPI(context) {
return new BrowserAPIService(context, {
confirm: new BrowserConfirm(context),
});
},
createUI(context, dependencies) {
return new LiveSyncBrowserUIService(context, dependencies);
},
};
}
export function createLiveSyncBrowserServiceHub<T extends ServiceContext>(
options: LiveSyncBrowserServiceHubOptions<T> = {}
): BrowserServiceHub<T> {
return new BrowserServiceHub<T>({
...options,
host: createLiveSyncBrowserHost<T>(),
});
}
@@ -0,0 +1,28 @@
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
import { describe, expect, it } from "vitest";
import {
observeServiceComposition,
observeServiceContext,
SERVICE_CONTEXT_MEMBERS,
} from "../../../test/contracts/serviceContext";
import { createLiveSyncBrowserServiceHub } from "./createLiveSyncBrowserServiceHub";
describe("LiveSync browser service context contract", () => {
it("preserves one injected context and its API results throughout the Webapp composition", () => {
const context = createServiceContext({
translate: (key) => `webapp:${key}`,
});
const hub = createLiveSyncBrowserServiceHub({ context });
expect(observeServiceContext(context, "message")).toEqual({
translation: "webapp:message",
receivedEvents: ["context-contract-event"],
});
const composition = observeServiceComposition(hub, context);
expect(composition.hubUsesExpectedContext).toBe(true);
expect(SERVICE_CONTEXT_MEMBERS.filter((member) => !composition.servicesUsingExpectedContext[member])).toEqual(
[]
);
});
});
+51
View File
@@ -0,0 +1,51 @@
<script lang="ts">
import type { MenuItem } from "../BrowserMenu";
type Props = {
item: MenuItem;
closeMenu: () => void;
};
const { item = $bindable(), closeMenu }: Props = $props();
function handleCommit(event: MouseEvent | KeyboardEvent) {
event.preventDefault();
try {
if (item.handler) {
item.handler?.();
}
} catch (ex) {
console.error(ex);
}
closeMenu();
}
const icons = {
checkmark: "✓",
} as { [key: string]: string };
function renderIcon(item: MenuItem) {
if (item.icon && item.icon in icons) {
return icons[item.icon] ?? item.icon;
} else if (item.icon !== undefined) {
return "";
}
return "";
}
</script>
<li>
<span class="icon">{renderIcon(item)}</span>
<label for=""
><!-- svelte-ignore a11y_invalid_attribute -->
<a onclick={handleCommit} onkeydown={handleCommit} role="button" tabindex="0" href="#">{item.title}</a></label
>
</li>
<style>
span.icon {
display: inline-block;
min-width: 1.5em;
text-align: center;
}
li {
list-style: none;
padding: 0.5em 1em;
}
</style>
@@ -0,0 +1,10 @@
<script lang="ts">
import type { MenuSeparator } from "../BrowserMenu";
type Props = {
item: MenuSeparator;
};
const { item = $bindable() }: Props = $props();
</script>
<hr />
+89
View File
@@ -0,0 +1,89 @@
<script lang="ts">
import type { Menu, MenuItem, MenuSeparator } from "../BrowserMenu";
import MenuItemView from "./MenuItemView.svelte";
import MenuSeparatorView from "./MenuSeparatorView.svelte";
type Props = {
items: (MenuItem | MenuSeparator)[];
closeMenu: () => void;
};
const { items = $bindable(), closeMenu }: Props = $props();
function handleKey(event: KeyboardEvent) {
if (event.key === "Escape") {
event.preventDefault();
closeMenu();
}
}
</script>
<popup>
<nav>
{#each items as item}
{#if item.type === "separator"}
<MenuSeparatorView {item} />
{:else if item.type === "item"}
<MenuItemView item={item as MenuItem} {closeMenu} />
{/if}
{/each}
</nav>
</popup>
<div class="background" onclick={() => closeMenu()} onkeydown={handleKey} role="none"></div>
<style>
popup {
z-index: 1000;
position: fixed;
background: rgba(255, 255, 255, 0.8);
max-width: 70vw;
max-height: 80vh;
margin: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border: 1px solid var(--background-primary-alt);
justify-content: space-between;
width: fit-content;
height: fit-content;
}
/* popup header {
background: var(--background-primary);
color: var(--text-normal);
padding: 1em;
border-bottom: 1px solid var(--background-primary-alt);
font: size 1.4em;
}
popup article {
padding: 1em;
display: flex;
justify-content: center;
overflow-y: auto;
}
popup article .msg {
white-space: pre-wrap;
}
popup .buttons {
border-top: 1px solid var(--background-primary-alt);
display: flex;
justify-content: center;
align-items: center;
padding: 1em;
}
popup .buttons button {
margin: 0 0.5em;
background-color: var(--background-primary-alt);
} */
popup ~ .background {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.125);
}
</style>
+127
View File
@@ -0,0 +1,127 @@
<script lang="ts">
import { renderMessageMarkdown } from "./renderMessageMarkdown";
type Props = {
title: string;
message: string;
buttons: string[];
commit: (button: string) => void;
};
let { title, message, buttons, commit }: Props = $props();
const renderedMessage = $derived(renderMessageMarkdown(message));
function handleEsc(event: KeyboardEvent) {
if (event.key === "Escape") {
commit("");
}
}
</script>
<popup>
<header>{title}</header>
<article><div class="msg">{@html renderedMessage}</div></article>
<div class="buttons">
{#each buttons as button}
<button onclick={() => commit(button)}>{button}</button>
{/each}
</div>
</popup>
<div class="background" onclick={() => commit("")} onkeydown={handleEsc} role="none"></div>
<style>
popup {
z-index: 1000;
position: fixed;
background: rgba(255, 255, 255, 0.8);
max-width: 70vw;
max-height: 80vh;
margin: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
min-width: 50vw;
min-height: 50vh;
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border: 1px solid var(--background-primary-alt);
justify-content: space-between;
}
popup header {
background: var(--background-primary);
color: var(--text-normal);
padding: 1em;
border-bottom: 1px solid var(--background-primary-alt);
font: size 1.4em;
}
popup article {
padding: 1em;
display: flex;
justify-content: center;
overflow-y: auto;
}
popup article .msg {
width: 100%;
line-height: 1.5;
overflow-wrap: anywhere;
}
popup article .msg :global(:first-child) {
margin-top: 0;
}
popup article .msg :global(:last-child) {
margin-bottom: 0;
}
popup article .msg :global(pre) {
overflow-x: auto;
padding: 0.75em;
border-radius: 4px;
background: var(--background-secondary);
}
popup article .msg :global(code) {
font-family: var(--font-monospace);
}
popup article .msg :global(blockquote) {
margin: 0;
padding-left: 1em;
border-left: 3px solid var(--background-modifier-border);
color: var(--text-muted);
}
popup article .msg :global(ul),
popup article .msg :global(ol) {
padding-left: 1.5em;
}
popup article .msg :global(table) {
width: 100%;
border-collapse: collapse;
}
popup article .msg :global(th),
popup article .msg :global(td) {
padding: 0.4em 0.6em;
border: 1px solid var(--background-modifier-border);
}
popup article .msg :global(a) {
color: var(--text-accent);
}
popup .buttons {
border-top: 1px solid var(--background-primary-alt);
display: flex;
justify-content: center;
align-items: center;
padding: 1em;
}
popup .buttons button {
margin: 0 0.5em;
background-color: var(--background-primary-alt);
}
popup ~ .background {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.125);
}
</style>
+122
View File
@@ -0,0 +1,122 @@
<script lang="ts">
type Props = {
title: string;
message: string;
initialText?: string;
placeholder?: string;
isPassword?: boolean;
commit: (text: string | false) => void;
};
const { title, message, commit, initialText, placeholder, isPassword }: Props = $props();
let text = $state(initialText || "");
let type = $state(isPassword ? "password" : "text");
function cancel() {
commit(false);
}
function handleKey(event: KeyboardEvent) {
if (event.key === "Escape") {
handleCancel(event);
} else if (event.key === "Enter") {
handleCommit(event);
}
}
function handleCancel(event: KeyboardEvent | MouseEvent) {
cancel();
event.preventDefault();
}
function handleCommit(event: KeyboardEvent | MouseEvent) {
commit(text);
event.preventDefault();
}
let textEl: HTMLInputElement;
$effect(() => {
textEl.focus();
});
</script>
<popup>
<header>{title}</header>
<article>
<div class="msg">{message}</div>
<div class="input">
<input
bind:this={textEl}
{type}
bind:value={text}
{placeholder}
onkeydown={handleKey}
onkeyup={handleKey}
/>
</div>
</article>
<div class="buttons">
<button onclick={handleCommit}>OK</button>
<button onclick={handleCancel}>Cancel</button>
</div>
</popup>
<div class="background" onclick={handleCancel} onkeydown={handleKey} role="none"></div>
<style>
popup {
z-index: 1000;
position: fixed;
background: rgba(255, 255, 255, 0.8);
max-width: 70vw;
max-height: 80vh;
margin: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
min-width: 50vw;
min-height: 50vh;
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border: 1px solid var(--background-primary-alt);
justify-content: space-between;
}
popup header {
background: var(--background-primary);
color: var(--text-normal);
padding: 1em;
border-bottom: 1px solid var(--background-primary-alt);
font: size 1.4em;
}
popup article {
align-items: center;
padding: 1em;
display: flex;
justify-content: center;
flex-direction: column;
}
popup article .msg {
overflow-y: auto;
white-space: pre-wrap;
}
popup .buttons {
border-top: 1px solid var(--background-primary-alt);
display: flex;
justify-content: center;
align-items: center;
padding: 1em;
}
popup .buttons button {
margin: 0 0.5em;
background-color: var(--background-primary-alt);
}
popup ~ .background {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.125);
}
</style>
@@ -0,0 +1,21 @@
import MarkdownIt from "markdown-it";
const markdownRenderer = new MarkdownIt({
html: false,
breaks: true,
linkify: true,
});
const defaultLinkOpenRenderer =
markdownRenderer.renderer.rules.link_open ??
((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options));
markdownRenderer.renderer.rules.link_open = (tokens, idx, options, env, self) => {
tokens[idx].attrSet("target", "_blank");
tokens[idx].attrSet("rel", "noopener noreferrer");
return defaultLinkOpenRenderer(tokens, idx, options, env, self);
};
export function renderMessageMarkdown(message: string): string {
return markdownRenderer.render(message);
}
@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { renderMessageMarkdown } from "./renderMessageMarkdown";
describe("renderMessageMarkdown", () => {
it("renders basic markdown features used by browser dialogues", () => {
const html = renderMessageMarkdown("# Title\n\n| left | right |\n| --- | --- |\n| a | b |\n");
expect(html).toContain("<h1>Title</h1>");
expect(html).toContain("<table>");
expect(html).toContain("<td>a</td>");
});
it("escapes inline HTML instead of rendering it", () => {
const html = renderMessageMarkdown("Before<script>alert('xss')</script>After");
expect(html).not.toContain("<script>");
expect(html).toContain("&lt;script&gt;alert('xss')&lt;/script&gt;");
});
it("opens Markdown links safely in a new tab", () => {
const html = renderMessageMarkdown("[docs](https://example.com)");
expect(html).toContain('target="_blank"');
expect(html).toContain('rel="noopener noreferrer"');
});
});
+3 -6
View File
@@ -108,13 +108,10 @@ livesync-cli ./my-db pull folder/note.md ./note.md
### Build from source
```bash
# Clone with submodules, because the shared core lives in src/lib
git clone --recurse-submodules <repository-url>
# Clone the repository
git clone <repository-url>
cd obsidian-livesync
# If you already cloned without submodules, run this once instead
git submodule update --init --recursive
# Install dependencies from the repository root
npm install
@@ -126,7 +123,7 @@ cd src/apps/cli
npm run build
```
If `src/lib` is missing, the build process stops early with a targeted message instead of a low-level Vite `ENOENT` error.
The shared core is installed as the exact `@vrtmrz/livesync-commonlib` package artefact recorded in the root lockfile.
Run the CLI:
@@ -1,7 +1,7 @@
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IConversionAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { NodeFile, NodeFolder } from "./NodeTypes";
import { path } from "@/apps/cli/node-compat";
import { path } from "@vrtmrz/livesync-commonlib/node";
/**
* Conversion adapter implementation for Node.js
@@ -1,12 +1,12 @@
import type { FilePath, UXStat } from "@lib/common/types";
import type { IFileSystemAdapter } from "@lib/serviceModules/adapters";
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IFileSystemAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import { NodePathAdapter } from "./NodePathAdapter";
import { NodeTypeGuardAdapter } from "./NodeTypeGuardAdapter";
import { NodeConversionAdapter } from "./NodeConversionAdapter";
import { NodeStorageAdapter } from "./NodeStorageAdapter";
import { NodeStorageAdapter } from "@vrtmrz/livesync-commonlib/node";
import { NodeVaultAdapter } from "./NodeVaultAdapter";
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
import { fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node";
/**
* Complete file system adapter implementation for Node.js
+3 -3
View File
@@ -1,7 +1,7 @@
import type { FilePath } from "@lib/common/types";
import type { IPathAdapter } from "@lib/serviceModules/adapters";
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IPathAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { NodeFile } from "./NodeTypes";
import { path } from "@/apps/cli/node-compat";
import { path } from "@vrtmrz/livesync-commonlib/node";
/**
* Path adapter implementation for Node.js
-127
View File
@@ -1,127 +0,0 @@
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 "@/apps/cli/node-compat";
import { validateStoragePath } from "@/apps/storagePath";
/**
* Storage adapter implementation for Node.js
*/
export class NodeStorageAdapter implements IStorageAdapter<NodeStat> {
constructor(private readonly basePath: string) {}
private resolvePath(p: string, allowRoot: boolean = true): string {
return path.join(this.basePath, validateStoragePath(p, allowRoot));
}
async exists(p: string): Promise<boolean> {
const fullPath = this.resolvePath(p);
try {
await fs.access(fullPath);
return true;
} catch {
return false;
}
}
async trystat(p: string): Promise<NodeStat | null> {
const fullPath = this.resolvePath(p);
try {
const stat = await fs.stat(fullPath);
return {
size: stat.size,
mtime: Math.floor(stat.mtimeMs),
ctime: Math.floor(stat.ctimeMs),
type: stat.isDirectory() ? "folder" : "file",
};
} catch {
return null;
}
}
async stat(p: string): Promise<NodeStat | null> {
return await this.trystat(p);
}
async mkdir(p: string): Promise<void> {
await fs.mkdir(this.resolvePath(p), { recursive: true });
}
async remove(p: string): Promise<void> {
const fullPath = this.resolvePath(p, false);
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
await fs.rm(fullPath, { recursive: true, force: true });
} else {
await fs.unlink(fullPath);
}
}
async read(p: string): Promise<string> {
return await fs.readFile(this.resolvePath(p, false), "utf-8");
}
async readBinary(p: string): Promise<ArrayBuffer> {
const buffer = await fs.readFile(this.resolvePath(p, false));
// 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;
}
async write(p: string, data: string, options?: UXDataWriteOptions): Promise<void> {
const fullPath = this.resolvePath(p, false);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, data, "utf-8");
if (options?.mtime || options?.ctime) {
const atime = options.mtime ? new Date(options.mtime) : new Date();
const mtime = options.mtime ? new Date(options.mtime) : new Date();
await fs.utimes(fullPath, atime, mtime);
}
}
async writeBinary(p: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
const fullPath = this.resolvePath(p, false);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, new Uint8Array(data));
if (options?.mtime || options?.ctime) {
const atime = options.mtime ? new Date(options.mtime) : new Date();
const mtime = options.mtime ? new Date(options.mtime) : new Date();
await fs.utimes(fullPath, atime, mtime);
}
}
async append(p: string, data: string, options?: UXDataWriteOptions): Promise<void> {
const fullPath = this.resolvePath(p, false);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.appendFile(fullPath, data, "utf-8");
if (options?.mtime || options?.ctime) {
const atime = options.mtime ? new Date(options.mtime) : new Date();
const mtime = options.mtime ? new Date(options.mtime) : new Date();
await fs.utimes(fullPath, atime, mtime);
}
}
async list(basePath: string): Promise<{ files: string[]; folders: string[] }> {
const fullPath = this.resolvePath(basePath);
try {
const entries = await fs.readdir(fullPath, { withFileTypes: true });
const files: string[] = [];
const folders: string[] = [];
for (const entry of entries) {
const entryPath = path.join(basePath, entry.name).replace(/\\/g, "/");
if (entry.isDirectory()) {
folders.push(entryPath);
} else if (entry.isFile()) {
files.push(entryPath);
}
}
return { files, folders };
} catch {
return { files: [], folders: [] };
}
}
}
@@ -1,9 +1,6 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { storageAdapterContractCases } from "@/apps/_test/storageAdapterContract";
import { NodeStorageAdapter } from "./NodeStorageAdapter";
import { fsPromises as fs, os, path, NodeStorageAdapter } from "@vrtmrz/livesync-commonlib/node";
describe("NodeStorageAdapter", () => {
const tempDirs: string[] = [];
@@ -1,4 +1,4 @@
import type { ITypeGuardAdapter } from "@lib/serviceModules/adapters";
import type { ITypeGuardAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { NodeFile, NodeFolder } from "./NodeTypes";
/**
+1 -1
View File
@@ -1,4 +1,4 @@
import type { FilePath, UXStat } from "@lib/common/types";
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
/**
* Node.js file representation
+3 -3
View File
@@ -1,7 +1,7 @@
import type { FilePath, UXDataWriteOptions } from "@lib/common/types";
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
import type { FilePath, UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IVaultAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { NodeFile, NodeFolder } from "./NodeTypes";
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
import { fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node";
/**
* Vault adapter implementation for Node.js
@@ -1,16 +1,14 @@
import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fsPromises, os, path } from "@vrtmrz/livesync-commonlib/node";
import { describe, expect, it } from "vitest";
import type { FilePath } from "@lib/common/types";
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { NodeFileSystemAdapter } from "./NodeFileSystemAdapter";
import { NodeVaultAdapter } from "./NodeVaultAdapter";
describe("NodeVaultAdapter.rename", () => {
it("changes the directory entry case without changing the content", async () => {
const directory = await mkdtemp(join(tmpdir(), "livesync-case-rename-"));
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-case-rename-"));
try {
await writeFile(join(directory, "Calculus.md"), "content", "utf8");
await fsPromises.writeFile(path.join(directory, "Calculus.md"), "content", "utf8");
const adapter = new NodeVaultAdapter(directory);
const file = {
path: "Calculus.md" as FilePath,
@@ -19,20 +17,20 @@ describe("NodeVaultAdapter.rename", () => {
await adapter.rename(file, "calculus.md");
expect(await readdir(directory)).toEqual(["calculus.md"]);
expect(await readFile(join(directory, "calculus.md"), "utf8")).toBe("content");
expect(await fsPromises.readdir(directory)).toEqual(["calculus.md"]);
expect(await fsPromises.readFile(path.join(directory, "calculus.md"), "utf8")).toBe("content");
expect(file.path).toBe("calculus.md");
} finally {
await rm(directory, { recursive: true, force: true });
await fsPromises.rm(directory, { recursive: true, force: true });
}
});
});
describe("NodeFileSystemAdapter path case", () => {
it("finds the stored case and refreshes the cache after a case-only rename", async () => {
const directory = await mkdtemp(join(tmpdir(), "livesync-case-cache-"));
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-case-cache-"));
try {
await writeFile(join(directory, "Calculus.md"), "content", "utf8");
await fsPromises.writeFile(path.join(directory, "Calculus.md"), "content", "utf8");
const adapter = new NodeFileSystemAdapter(directory);
await expect(adapter.getAbstractFileByPath("calculus.md")).resolves.toBeNull();
@@ -43,9 +41,9 @@ describe("NodeFileSystemAdapter path case", () => {
expect(renamedFile.path).toBe("calculus.md");
expect((await adapter.getFiles()).map((file) => file.path)).toEqual(["calculus.md"]);
expect(await readdir(directory)).toEqual(["calculus.md"]);
expect(await fsPromises.readdir(directory)).toEqual(["calculus.md"]);
} finally {
await rm(directory, { recursive: true, force: true });
await fsPromises.rm(directory, { recursive: true, force: true });
}
});
});
@@ -1,14 +1,15 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
import { runCommand } from "./runCommand";
import type { CLIOptions } from "./types";
// Mock performFullScan so daemon tests don't require a real CouchDB connection.
vi.mock("@lib/serviceFeatures/offlineScanner", () => ({
vi.mock("@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner", () => ({
performFullScan: vi.fn(async () => true),
}));
// Mock UnresolvedErrorManager to avoid event-hub side effects.
vi.mock("@lib/services/base/UnresolvedErrorManager", () => ({
vi.mock("@vrtmrz/livesync-commonlib/compat/services/base/UnresolvedErrorManager", () => ({
UnresolvedErrorManager: class UnresolvedErrorManager {
showError() {}
clearError() {}
@@ -16,11 +17,12 @@ vi.mock("@lib/services/base/UnresolvedErrorManager", () => ({
},
}));
import * as offlineScanner from "@lib/serviceFeatures/offlineScanner";
import * as offlineScanner from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner";
function createCoreMock() {
return {
services: {
context: createServiceContext(),
control: {
activated: Promise.resolve(),
applySettings: vi.fn(async () => {}),
+8 -8
View File
@@ -1,11 +1,11 @@
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
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";
import { getPeerConnectionStats } from "@lib/rpc/transports/DiagRTCPeerConnections.utils";
import { appendFile } from "node:fs/promises";
import { P2P_DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError";
import { getPeerConnectionStats } from "@vrtmrz/livesync-commonlib/compat/rpc/transports/DiagRTCPeerConnections.utils";
import { fsPromises } from "@vrtmrz/livesync-commonlib/node";
type CLIP2PPeer = {
peerId: string;
@@ -155,7 +155,7 @@ async function writePeerConnectionStatsIfRequested(
localCandidate,
remoteCandidate,
};
await appendFile(outputPath, `${JSON.stringify(payload)}\n`, "utf8");
await fsPromises.appendFile(outputPath, `${JSON.stringify(payload)}\n`, "utf8");
}
export async function syncWithPeer(
+14 -14
View File
@@ -1,5 +1,5 @@
import { decodeSettingsFromSetupURI } from "@lib/API/processSetting";
import { configURIBase } from "@lib/common/models/shared.const";
import { decodeSettingsFromSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
import { configURIBase } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const";
import {
DEFAULT_SETTINGS,
MILESTONE_DOCID,
@@ -9,19 +9,19 @@ import {
REMOTE_MINIO,
type EntryMilestoneInfo,
type EntryDoc,
} from "@lib/common/types";
import { ConnectionStringParser } from "@lib/common/ConnectionString";
import { activateRemoteConfiguration, createRemoteConfigurationId } from "@lib/serviceFeatures/remoteConfig";
import { stripAllPrefixes } from "@lib/string_and_binary/path";
} from "@vrtmrz/livesync-commonlib/compat/common/types";
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
import { activateRemoteConfiguration, createRemoteConfigurationId } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig";
import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
import type { CLICommandContext, CLIOptions } from "./types";
import { promptForPassphrase, readStdinAsUtf8, toArrayBuffer, toDatabaseRelativePath } from "./utils";
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
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 "@/apps/cli/node-compat";
import type { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
import type { LiveSyncJournalReplicator } from "@lib/replication/journal/LiveSyncJournalReplicator";
import { performFullScan } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner";
import { UnresolvedErrorManager } from "@vrtmrz/livesync-commonlib/compat/services/base/UnresolvedErrorManager";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import { fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node";
import type { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
import type { LiveSyncJournalReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator";
function redactConnectionString(uri: string): string {
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
@@ -100,7 +100,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
log("CouchDB replication complete");
// 2. Mirror scan to reconcile PouchDB ↔ local filesystem.
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle);
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle, core.services.context.events);
log("Running mirror scan...");
const scanOk = await performFullScan(core, log, errorManager, false, true);
if (!scanOk) {
@@ -537,7 +537,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
if (options.command === "mirror") {
console.error("[Command] mirror");
const log = (msg: unknown) => console.error(`[Mirror] ${msg}`);
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle);
const errorManager = new UnresolvedErrorManager(core.services.appLifecycle, core.services.context.events);
return await performFullScan(core, log, errorManager, false, true);
}
@@ -1,10 +1,8 @@
import * as path from "path";
import * as fs from "fs/promises";
import * as os from "os";
import * as processSetting from "@lib/API/processSetting";
import { ConnectionStringParser } from "@lib/common/ConnectionString";
import { configURIBase } from "@lib/common/models/shared.const";
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P } from "@lib/common/types";
import { fsPromises as fs, os, path } from "@vrtmrz/livesync-commonlib/node";
import * as processSetting from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
import { configURIBase } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const";
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { runCommand } from "./runCommand";
import type { CLIOptions } from "./types";
+2 -2
View File
@@ -1,6 +1,6 @@
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { ServiceContext } from "@lib/services/base/ServiceBase";
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
import { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
export type CLICommand =
| "daemon"
+1 -1
View File
@@ -1,4 +1,4 @@
import { path, readline } from "@/apps/cli/node-compat";
import { path, readline } from "@vrtmrz/livesync-commonlib/node";
export function toArrayBuffer(data: Buffer): ArrayBuffer {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required in environments where Buffer.buffer is ArrayBufferLike
+1 -1
View File
@@ -1,4 +1,4 @@
import * as path from "path";
import { path } from "@vrtmrz/livesync-commonlib/node";
import { describe, expect, it } from "vitest";
import { toDatabaseRelativePath } from "./utils";
+1 -1
View File
@@ -2,7 +2,7 @@
// 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";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill
const rtcPolyfillCtor = (polyfill as any).RTCPeerConnection;
+10 -9
View File
@@ -2,9 +2,9 @@ import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub";
import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage";
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules";
import { DEFAULT_SETTINGS, LOG_LEVEL_VERBOSE, type LOG_LEVEL, type ObsidianLiveSyncSettings } from "@lib/common/types";
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
import type { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
import { DEFAULT_SETTINGS, LOG_LEVEL_VERBOSE, type LOG_LEVEL, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
import type { InjectableSettingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableSettingService";
import {
LOG_LEVEL_DEBUG,
setGlobalLogFunction,
@@ -16,11 +16,11 @@ import {
import { runCommand } from "./commands/runCommand";
import { VALID_COMMANDS } from "./commands/types";
import type { CLICommand, CLIOptions } from "./commands/types";
import { getPathFromUXFileInfo } from "@lib/common/typeUtils";
import { stripAllPrefixes } from "@lib/string_and_binary/path";
import { getPathFromUXFileInfo } from "@vrtmrz/livesync-commonlib/compat/common/typeUtils";
import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
import { IgnoreRules } from "./serviceModules/IgnoreRules";
import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature";
import { fsPromises as fs, path, fs as fsSync } from "./node-compat";
import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature";
import { fsPromises as fs, path, fs as fsSync } from "@vrtmrz/livesync-commonlib/node";
const SETTINGS_FILE = ".livesync/settings.json";
ensureGlobalNodeLocalStorage();
@@ -173,7 +173,8 @@ export function parseArgs(): CLIOptions {
case "-d":
// debugging automatically enables verbose logging, as it is intended for debugging issues.
debug = true;
// falls through
verbose = true;
break;
case "--verbose":
case "-v":
verbose = true;
@@ -354,7 +355,7 @@ export async function main() {
// Create service context and hub
const context = new NodeServiceContext(databasePath);
const serviceHubInstance = new NodeServiceHub<NodeServiceContext>(databasePath, context);
serviceHubInstance.API.addLog.setHandler((message: string, level: LOG_LEVEL) => {
serviceHubInstance.API.addLog.setHandler((message: unknown, level: LOG_LEVEL) => {
let levelStr = "";
switch (level) {
case LOG_LEVEL_DEBUG:
@@ -1,6 +1,6 @@
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types";
import type { FileEventItem } from "@lib/common/types";
import type { IStorageEventManagerAdapter } from "@lib/managers/adapters";
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { FileEventItem } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IStorageEventManagerAdapter } from "@vrtmrz/livesync-commonlib/compat/managers/adapters";
import type {
IStorageEventTypeGuardAdapter,
IStorageEventPersistenceAdapter,
@@ -8,12 +8,12 @@ import type {
IStorageEventStatusAdapter,
IStorageEventConverterAdapter,
IStorageEventWatchHandlers,
} from "@lib/managers/adapters";
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
} from "@vrtmrz/livesync-commonlib/compat/managers/adapters";
import type { FileEventItemSentinel } from "@vrtmrz/livesync-commonlib/compat/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 "@/apps/cli/node-compat";
import { fsPromises as fs, path, type Stats } from "@vrtmrz/livesync-commonlib/node";
/**
* CLI-specific type guard adapter
@@ -1,5 +1,5 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import type { IStorageEventWatchHandlers } from "@lib/managers/adapters";
import type { IStorageEventWatchHandlers } from "@vrtmrz/livesync-commonlib/compat/managers/adapters";
import type { NodeFile } from "@/apps/cli/adapters/NodeTypes";
// ── chokidar mock ──────────────────────────────────────────────────────────────
@@ -1,9 +1,9 @@
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager";
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager";
import { CLIStorageEventManagerAdapter } from "./CLIStorageEventManagerAdapter";
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import type { ServiceContext } from "@lib/services/base/ServiceBase";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
// import type { IMinimumLiveSyncCommands } from "@lib/services/base/IService";
// import type { IMinimumLiveSyncCommands } from "@vrtmrz/livesync-commonlib/compat/services/base/IService";
export class StorageEventManagerCLI extends StorageEventManagerBase<CLIStorageEventManagerAdapter> {
core: LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands>;
-11
View File
@@ -1,11 +0,0 @@
// 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 };
+1 -3
View File
@@ -6,13 +6,12 @@
"type": "module",
"scripts": {
"dev": "vite",
"prebuild": "node scripts/check-submodule.mjs",
"build": "vite build",
"preview": "vite preview",
"cli": "node dist/index.cjs",
"buildRun": "npm run build && npm run cli --",
"build:docker": "docker build -f Dockerfile -t livesync-cli ../../..",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
"check": "tsc -p tsconfig.json",
"test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts src/apps/cli/commands/p2p.unit.spec.ts",
"test:e2e:two-vaults": "bash test/test-e2e-two-vaults-with-docker-linux.sh",
"test:e2e:two-vaults:common": "bash test/test-e2e-two-vaults-common.sh",
@@ -55,7 +54,6 @@
"werift": "^0.23.0"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"typescript": "5.9.3",
"vite": "^8.0.16",
"vitest": "^4.1.8"
-36
View File
@@ -1,36 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
const cliDir = process.cwd();
const repoRoot = path.resolve(cliDir, "../../..");
const requiredFiles = [
path.join(repoRoot, "src/lib/src/common/types.ts"),
];
const missingFiles = requiredFiles.filter((filePath) => !fs.existsSync(filePath));
if (missingFiles.length === 0) {
process.exit(0);
}
console.error("[CLI Build Error] Required shared sources were not found.");
console.error("This repository uses Git submodules, and the CLI depends on src/lib.");
console.error("");
console.error("Missing file(s):");
for (const filePath of missingFiles) {
console.error(` - ${path.relative(repoRoot, filePath)}`);
}
console.error("");
console.error("Initialize submodules, then retry the CLI build:");
console.error(" git submodule update --init --recursive");
console.error("");
console.error("For a fresh clone, prefer:");
console.error(" git clone --recurse-submodules <repository-url>");
console.error("");
console.error("Then run:");
console.error(" npm install");
console.error(" cd src/apps/cli");
console.error(" npm run build");
process.exit(1);
@@ -1,14 +1,14 @@
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder";
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
import { ServiceRebuilder } from "@vrtmrz/livesync-commonlib/compat/serviceModules/Rebuilder";
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
import { StorageAccessManager } from "@lib/managers/StorageProcessingManager";
import { StorageAccessManager } from "@vrtmrz/livesync-commonlib/compat/managers/StorageProcessingManager";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import type { ServiceContext } from "@lib/services/base/ServiceBase";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { FileAccessCLI } from "./FileAccessCLI";
import { ServiceFileAccessCLI } from "./ServiceFileAccessImpl";
import { ServiceDatabaseFileAccessCLI } from "./DatabaseFileAccess";
import { StorageEventManagerCLI } from "@/apps/cli/managers/StorageEventManagerCLI";
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
import type { ServiceModules } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import type { IgnoreRules } from "./IgnoreRules";
/**
@@ -73,6 +73,7 @@ export function initialiseServiceModulesCLI(
// Database file access (platform-independent)
const databaseFileAccess = new ServiceDatabaseFileAccessCLI({
events: services.context.events,
API: services.API,
database: services.database,
path: services.path,
@@ -82,6 +83,7 @@ export function initialiseServiceModulesCLI(
// File handler (platform-independent)
const fileHandler = new ServiceFileHandler({
events: services.context.events,
API: services.API,
databaseFileAccess: databaseFileAccess,
conflict: services.conflict,
@@ -95,6 +97,7 @@ export function initialiseServiceModulesCLI(
// Rebuilder (platform-independent)
const rebuilder = new ServiceRebuilder({
events: services.context.events,
API: services.API,
database: services.database,
appLifecycle: services.appLifecycle,
@@ -1,8 +1,8 @@
import {
ServiceDatabaseFileAccessBase,
type ServiceDatabaseFileAccessDependencies,
} from "@lib/serviceModules/ServiceDatabaseFileAccessBase";
import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess";
} from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceDatabaseFileAccessBase";
import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess";
/**
* CLI-specific implementation of ServiceDatabaseFileAccess
+1 -1
View File
@@ -1,4 +1,4 @@
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
import { FileAccessBase, type FileAccessBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase";
import { NodeFileSystemAdapter } from "@/apps/cli/adapters/NodeFileSystemAdapter";
/**
+1 -1
View File
@@ -1,5 +1,5 @@
import { Minimatch } from "minimatch";
import { fsPromises as fs, path } from "@/apps/cli/node-compat";
import { fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node";
/**
* Loads and evaluates ignore rules from `.livesync/ignore` inside the vault.
@@ -1,6 +1,4 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { fsPromises as fs, os, path } from "@vrtmrz/livesync-commonlib/node";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const minimatchStats = vi.hoisted(() => ({ constructions: 0 }));
@@ -1,4 +1,4 @@
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileAccessBase";
import { NodeFileSystemAdapter } from "@/apps/cli/adapters/NodeFileSystemAdapter";
/**
@@ -1,13 +1,13 @@
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@lib/common/logger";
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase";
import type { IKeyValueDBService } from "@lib/services/base/IService";
import { ServiceBase, type ServiceContext } from "@lib/services/base/ServiceBase";
import type { InjectableAppLifecycleService } from "@lib/services/implements/injectable/InjectableAppLifecycleService";
import type { InjectableDatabaseEventService } from "@lib/services/implements/injectable/InjectableDatabaseEventService";
import type { IVaultService } from "@lib/services/base/IService";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/compat/common/logger";
import type { KeyValueDatabase } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase";
import type { IKeyValueDBService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService";
import { ServiceBase, type ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import type { InjectableAppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAppLifecycleService";
import type { InjectableDatabaseEventService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableDatabaseEventService";
import type { IVaultService } from "@vrtmrz/livesync-commonlib/compat/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 "@/apps/cli/node-compat";
import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
import { fs as nodeFs, path as nodePath } from "@vrtmrz/livesync-commonlib/node";
const NODE_KV_TYPED_KEY = "__nodeKvType";
const NODE_KV_VALUES_KEY = "values";
+2 -2
View File
@@ -1,5 +1,5 @@
import { fs as nodeFs, path as nodePath } from "@/apps/cli/node-compat";
import { compatGlobal } from "@lib/common/coreEnvFunctions";
import { fs as nodeFs, path as nodePath } from "@vrtmrz/livesync-commonlib/node";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
type LocalStorageShape = {
getItem(key: string): string | null;
@@ -1,6 +1,4 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { fs, os, path } from "@vrtmrz/livesync-commonlib/node";
import { afterEach, describe, expect, it } from "vitest";
import {
clearNodeLocalStorage,
@@ -0,0 +1,10 @@
import { eventHub } from "@/common/events";
import { translateLiveSyncMessage } from "@/common/translation";
import { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
/** Host capabilities owned by one Self-hosted LiveSync CLI composition. */
export class NodeServiceContext extends ServiceContext {
constructor(readonly databasePath: string) {
super({ events: eventHub, translate: translateLiveSyncMessage });
}
}
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { eventHub } from "@/common/events";
import { translateLiveSyncMessage } from "@/common/translation";
import {
observeServiceComposition,
observeServiceContext,
SERVICE_CONTEXT_MEMBERS,
} from "../../../../test/contracts/serviceContext";
import { NodeServiceContext } from "./NodeServiceContext";
import { NodeServiceHub } from "./NodeServiceHub";
const TRANSLATION_KEY = "Replicator.Message.InitialiseFatalError";
describe("NodeServiceContext contract", () => {
it("preserves the CLI capabilities and host-neutral API results", () => {
const context = new NodeServiceContext("/tmp/livesync-context-contract");
expect(observeServiceContext(context, TRANSLATION_KEY)).toEqual({
translation: translateLiveSyncMessage(TRANSLATION_KEY),
receivedEvents: ["context-contract-event"],
});
expect(context.events).toBe(eventHub);
expect(context.databasePath).toBe("/tmp/livesync-context-contract");
const hub = new NodeServiceHub(context.databasePath, context);
const composition = observeServiceComposition(hub, context);
expect(composition.hubUsesExpectedContext).toBe(true);
expect(SERVICE_CONTEXT_MEMBERS.filter((member) => !composition.servicesUsingExpectedContext[member])).toEqual(
[]
);
});
});
+45 -55
View File
@@ -1,39 +1,36 @@
import type { AppLifecycleService, AppLifecycleServiceDependencies } from "@lib/services/base/AppLifecycleService";
import { ServiceContext } from "@lib/services/base/ServiceBase";
import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat";
import { SvelteDialogManagerBase, type ComponentHasResult } from "@lib/services/implements/base/SvelteDialog";
import { UIService } from "@lib/services/implements/base/UIService";
import { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
import { InjectableAppLifecycleService } from "@lib/services/implements/injectable/InjectableAppLifecycleService";
import { InjectableConflictService } from "@lib/services/implements/injectable/InjectableConflictService";
import { InjectableDatabaseEventService } from "@lib/services/implements/injectable/InjectableDatabaseEventService";
import { InjectableFileProcessingService } from "@lib/services/implements/injectable/InjectableFileProcessingService";
import { PathServiceCompat } from "@lib/services/implements/injectable/InjectablePathService";
import { InjectableRemoteService } from "@lib/services/implements/injectable/InjectableRemoteService";
import { InjectableReplicationService } from "@lib/services/implements/injectable/InjectableReplicationService";
import { InjectableReplicatorService } from "@lib/services/implements/injectable/InjectableReplicatorService";
import { InjectableTestService } from "@lib/services/implements/injectable/InjectableTestService";
import { InjectableTweakValueService } from "@lib/services/implements/injectable/InjectableTweakValueService";
import { InjectableVaultServiceCompat } from "@lib/services/implements/injectable/InjectableVaultService";
import { ControlService } from "@lib/services/base/ControlService";
import type { IControlService } from "@lib/services/base/IService";
import { HeadlessAPIService } from "@lib/services/implements/headless/HeadlessAPIService";
// import { HeadlessDatabaseService } from "@lib/services/implements/headless/HeadlessDatabaseService";
import type { ServiceInstances } from "@lib/services/ServiceHub";
import type { AppLifecycleServiceDependencies } from "@vrtmrz/livesync-commonlib/compat/services/base/AppLifecycleService";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { ConfigServiceBrowserCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/ConfigServiceBrowserCompat";
import type {
ComponentHasResult,
SvelteDialogManager,
} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog";
import { UIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/UIService";
import { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
import { InjectableAppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAppLifecycleService";
import { InjectableConflictService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableConflictService";
import { InjectableDatabaseEventService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableDatabaseEventService";
import { InjectableFileProcessingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableFileProcessingService";
import { PathServiceCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectablePathService";
import { InjectableRemoteService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableRemoteService";
import { InjectableReplicationService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableReplicationService";
import { InjectableReplicatorService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableReplicatorService";
import { InjectableTestService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableTestService";
import { InjectableTweakValueService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableTweakValueService";
import { InjectableVaultServiceCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableVaultService";
import { ControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/ControlService";
import { HeadlessAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/headless/HeadlessAPIService";
import type { ServiceInstances } from "@vrtmrz/livesync-commonlib/compat/services/ServiceHub";
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 "@/apps/cli/node-compat";
import type { KeyValueDBService } from "@lib/services/base/KeyValueDBService";
import { DatabaseService } from "@vrtmrz/livesync-commonlib/compat/services/base/DatabaseService";
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { path as nodePath } from "@vrtmrz/livesync-commonlib/node";
import type { KeyValueDBService } from "@vrtmrz/livesync-commonlib/compat/services/base/KeyValueDBService";
import { PouchDB } from "@/apps/cli/lib/pouchdb-node";
import { NodeServiceContext } from "./NodeServiceContext";
export class NodeServiceContext extends ServiceContext {
databasePath: string;
constructor(databasePath: string) {
super();
this.databasePath = databasePath;
}
}
export { NodeServiceContext } from "./NodeServiceContext";
class NodeAppLifecycleService<T extends ServiceContext> extends InjectableAppLifecycleService<T> {
constructor(context: T, dependencies: AppLifecycleServiceDependencies) {
@@ -41,21 +38,24 @@ class NodeAppLifecycleService<T extends ServiceContext> extends InjectableAppLif
}
}
class NodeSvelteDialogManager<T extends ServiceContext> extends SvelteDialogManagerBase<T> {
openSvelteDialog<TValue, UInitial>(
component: ComponentHasResult<TValue, UInitial>,
initialData?: UInitial
class NodeDialogManager<T extends ServiceContext> implements SvelteDialogManager<T> {
open<TValue, UInitial>(
_component: ComponentHasResult<TValue, UInitial>,
_initialData?: UInitial
): Promise<TValue | undefined> {
throw new Error("Method not implemented.");
return Promise.reject(new Error("Interactive dialogues are not available in the CLI."));
}
openWithExplicitCancel<TValue, UInitial>(
_component: ComponentHasResult<TValue, UInitial>,
_initialData?: UInitial
): Promise<TValue> {
return Promise.reject(new Error("Interactive dialogues are not available in the CLI."));
}
}
type NodeUIServiceDependencies<T extends ServiceContext = ServiceContext> = {
appLifecycle: AppLifecycleService<T>;
config: ConfigServiceBrowserCompat<T>;
replicator: InjectableReplicatorService<T>;
APIService: HeadlessAPIService<T>;
control: IControlService;
};
class NodeDatabaseService<T extends NodeServiceContext> extends DatabaseService<T> {
protected override modifyDatabaseOptions(
@@ -77,17 +77,9 @@ class NodeUIService<T extends ServiceContext> extends UIService<T> {
}
constructor(context: T, dependencies: NodeUIServiceDependencies<T>) {
const headlessConfirm = dependencies.APIService.confirm;
const dialogManager = new NodeSvelteDialogManager<T>(context, {
confirm: headlessConfirm,
appLifecycle: dependencies.appLifecycle,
config: dependencies.config,
replicator: dependencies.replicator,
control: dependencies.control,
});
const dialogManager = new NodeDialogManager<T>();
super(context, {
appLifecycle: dependencies.appLifecycle,
dialogManager,
APIService: dependencies.APIService,
});
@@ -111,6 +103,7 @@ export class NodeServiceHub<T extends NodeServiceContext> extends InjectableServ
});
const remote = new InjectableRemoteService(context, {
pouchDB: PouchDB,
APIService: API,
appLifecycle,
setting,
@@ -128,6 +121,7 @@ export class NodeServiceHub<T extends NodeServiceContext> extends InjectableServ
});
const database = new NodeDatabaseService<T>(context, {
pouchDB: PouchDB,
API: API,
path,
vault,
@@ -174,11 +168,7 @@ export class NodeServiceHub<T extends NodeServiceContext> extends InjectableServ
});
const ui = new NodeUIService<T>(context, {
appLifecycle,
config,
replicator,
APIService: API,
control,
});
const serviceInstancesToInit: Required<ServiceInstances<T>> = {
+7 -8
View File
@@ -1,10 +1,9 @@
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
import { EVENT_SETTING_SAVED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { EVENT_REQUEST_RELOAD_SETTING_TAB } from "@/common/events";
import { eventHub } from "@lib/hub/hub";
import { handlers } from "@lib/services/lib/HandlerUtils";
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
import type { ServiceContext } from "@lib/services/base/ServiceBase";
import { SettingService, type SettingServiceDependencies } from "@lib/services/base/SettingService";
import { handlers } from "@vrtmrz/livesync-commonlib/compat/services/lib/HandlerUtils";
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { SettingService, type SettingServiceDependencies } from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService";
import {
configureNodeLocalStorage,
deleteNodeLocalStorageItem,
@@ -17,11 +16,11 @@ export class NodeSettingService<T extends ServiceContext> extends SettingService
super(context, dependencies);
configureNodeLocalStorage(storagePath);
this.onSettingSaved.addHandler((settings) => {
eventHub.emitEvent(EVENT_SETTING_SAVED, settings);
this.context.events.emitEvent(EVENT_SETTING_SAVED, settings);
return Promise.resolve(true);
});
this.onSettingLoaded.addHandler((settings) => {
eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB);
this.context.events.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB);
return Promise.resolve(true);
});
}
@@ -3,7 +3,6 @@ set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
CLI_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd -- "$CLI_DIR/../../.." && pwd)"
cd "$CLI_DIR"
source "$SCRIPT_DIR/test-helpers.sh"
display_test_info
@@ -28,10 +27,10 @@ cli_test_init_settings_file "$SETTINGS_FILE"
echo "[INFO] creating setup URI from settings"
SETUP_URI="$(
REPO_ROOT="$REPO_ROOT" SETTINGS_FILE="$SETTINGS_FILE" SETUP_PASSPHRASE="$SETUP_PASSPHRASE" npx tsx -e '
import fs from "node:fs";
SETTINGS_FILE="$SETTINGS_FILE" SETUP_PASSPHRASE="$SETUP_PASSPHRASE" npx tsx -e '
import { fs } from "@vrtmrz/livesync-commonlib/node";
import { encodeSettingsToSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
(async () => {
const { encodeSettingsToSetupURI } = await import(process.env.REPO_ROOT + "/src/lib/src/API/processSetting.ts");
const settingsPath = process.env.SETTINGS_FILE;
const setupPassphrase = process.env.SETUP_PASSPHRASE;
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
+4 -9
View File
@@ -1,4 +1,3 @@
import { join } from "@std/path";
import { CLI_DIR, runCliOrFail } from "./cli.ts";
// ---------------------------------------------------------------------------
@@ -11,18 +10,14 @@ export async function initSettingsFile(settingsFile: string): Promise<void> {
}
/**
* Generate a full setup URI from a settings file via src/lib API.
* Generate a full setup URI from a settings file via the Commonlib package API.
* Mirrors the bash flow in test-setup-put-cat-linux.sh.
*/
export async function generateSetupUriFromSettings(settingsFile: string, setupPassphrase: string): Promise<string> {
const repoRoot = join(CLI_DIR, "..", "..", "..");
const script = [
"import fs from 'node:fs';",
"import { pathToFileURL } from 'node:url';",
"import { fs } from '@vrtmrz/livesync-commonlib/node';",
"import { encodeSettingsToSetupURI } from '@vrtmrz/livesync-commonlib/compat/API/processSetting';",
"(async () => {",
" const modulePath = process.env.REPO_ROOT + '/src/lib/src/API/processSetting.ts';",
" const moduleUrl = pathToFileURL(modulePath).href;",
" const { encodeSettingsToSetupURI } = await import(moduleUrl);",
" const settingsPath = process.env.SETTINGS_FILE;",
" const passphrase = process.env.SETUP_PASSPHRASE;",
" const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));",
@@ -39,6 +34,7 @@ export async function generateSetupUriFromSettings(settingsFile: string, setupPa
].join("\n");
const scriptPath = await Deno.makeTempFile({
dir: CLI_DIR,
prefix: "livesync-setup-uri-",
suffix: ".mts",
});
@@ -49,7 +45,6 @@ export async function generateSetupUriFromSettings(settingsFile: string, setupPa
args: ["tsx", scriptPath],
cwd: CLI_DIR,
env: {
REPO_ROOT: repoRoot,
SETTINGS_FILE: settingsFile,
SETUP_PASSPHRASE: setupPassphrase,
},
+1 -2
View File
@@ -23,8 +23,7 @@
// "rootDir": "../../../",
/* Path mapping */
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"]
"@/*": ["../../*"]
}
},
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
+10 -14
View File
@@ -1,13 +1,11 @@
import { defineConfig } from "vite";
import { defaultServerConditions, defaultServerMainFields, 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";
import { fileURLToPath, fs, isBuiltin, path } from "@vrtmrz/livesync-commonlib/node";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/");
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 packageJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
const manifestJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
// https://vite.dev/config/
const defaultExternal = [
"obsidian",
@@ -65,17 +63,17 @@ function injectBanner(): import("vite").Plugin {
export default defineConfig({
plugins: [svelte(), injectBanner()],
resolve: {
// This bundle runs in Node. Vite's client defaults include the `browser`
// export condition, which would select Commonlib's inline Web Worker.
conditions: [...defaultServerConditions],
mainFields: [...defaultServerMainFields],
alias: {
"@lib/worker/bgWorker.ts": "../../lib/src/worker/bgWorker.mock.ts",
"@lib/pouchdb/pouchdb-browser.ts": resolve(__dirname, "lib/pouchdb-node.ts"),
// The CLI runs on Node.js; force AWS XML builder to its CJS Node entry
// so Vite does not resolve the browser DOMParser-based XML parser.
"@aws-sdk/xml-builder": resolve(__dirname, "../../../node_modules/@aws-sdk/xml-builder/dist-cjs/index.js"),
// Force fflate to the Node CJS entry; browser entry expects Web Worker globals.
fflate: resolve(__dirname, "../../../node_modules/fflate/lib/node.cjs"),
"@": resolve(__dirname, "../../"),
"@lib": resolve(__dirname, "../../lib/src"),
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
},
},
@@ -89,15 +87,13 @@ export default defineConfig({
index: resolve(__dirname, "entrypoint.ts"),
},
external: (id) => {
if (isBuiltin(id)) return true;
if (defaultExternal.includes(id)) return true;
if (id.startsWith(".") || id.startsWith("/")) return false;
if (id.startsWith("@/") || id.startsWith("@lib/")) return false;
if (id.startsWith("@/")) return false;
if (id.endsWith(".ts") || id.endsWith(".js")) return false;
if (id === "fs" || id === "fs/promises" || id === "path" || id === "crypto" || id === "worker_threads")
return true;
if (id.startsWith("pouchdb-")) return true;
if (id.startsWith("werift")) return true;
if (id.startsWith("node:")) return true;
return false;
},
},
-28
View File
@@ -1,28 +0,0 @@
/**
* Validate the platform-neutral path vocabulary used by rooted storage adapters.
*
* Paths are slash-separated and relative to the root bound to the adapter. Root
* selection and authorisation happen before the adapter is constructed.
*/
export function validateStoragePath(storagePath: string, allowRoot: boolean = true): string {
if (storagePath === "") {
if (allowRoot) return storagePath;
throw new Error("The storage root is not a valid entry path");
}
// LiveSync normally rejects ':' in portable filenames before paths reach storage. A leading letter and colon
// therefore represents drive-qualified input or a leaked non-storage namespace, not a supported physical path.
if (storagePath.startsWith("/") || storagePath.startsWith("\\") || /^[A-Za-z]:/.test(storagePath)) {
throw new Error(`Storage paths must be relative to the configured root: ${storagePath}`);
}
if (storagePath.includes("\\")) {
throw new Error(`Storage paths must use forward slashes: ${storagePath}`);
}
const segments = storagePath.split("/");
if (segments.some((segment) => segment === "." || segment === "..")) {
throw new Error(`Storage paths must not contain traversal segments: ${storagePath}`);
}
return storagePath;
}
@@ -1,5 +1,5 @@
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IConversionAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
/**
@@ -1,9 +1,9 @@
import type { FilePath, UXStat } from "@lib/common/types";
import type { IFileSystemAdapter } from "@lib/serviceModules/adapters";
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IFileSystemAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import { FSAPIPathAdapter } from "./FSAPIPathAdapter";
import { FSAPITypeGuardAdapter } from "./FSAPITypeGuardAdapter";
import { FSAPIConversionAdapter } from "./FSAPIConversionAdapter";
import { FSAPIStorageAdapter } from "./FSAPIStorageAdapter";
import { FileSystemAccessStorageAdapter } from "@vrtmrz/livesync-commonlib/browser";
import { FSAPIVaultAdapter } from "./FSAPIVaultAdapter";
import type { FSAPIFile, FSAPIFolder, FSAPIStat } from "./FSAPITypes";
import { shareRunningResult } from "octagonal-wheels/concurrency/lock_v2";
@@ -15,7 +15,7 @@ export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSA
readonly path: FSAPIPathAdapter;
readonly typeGuard: FSAPITypeGuardAdapter;
readonly conversion: FSAPIConversionAdapter;
readonly storage: FSAPIStorageAdapter;
readonly storage: FileSystemAccessStorageAdapter;
readonly vault: FSAPIVaultAdapter;
private fileCache = new Map<string, FSAPIFile>();
@@ -25,7 +25,7 @@ export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSA
this.path = new FSAPIPathAdapter();
this.typeGuard = new FSAPITypeGuardAdapter();
this.conversion = new FSAPIConversionAdapter();
this.storage = new FSAPIStorageAdapter(rootHandle);
this.storage = new FileSystemAccessStorageAdapter(rootHandle);
this.vault = new FSAPIVaultAdapter(rootHandle);
}
+2 -2
View File
@@ -1,5 +1,5 @@
import type { FilePath } from "@lib/common/types";
import type { IPathAdapter } from "@lib/serviceModules/adapters";
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IPathAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { FSAPIFile } from "./FSAPITypes";
/**
@@ -1,225 +0,0 @@
import type { UXDataWriteOptions } from "@lib/common/types";
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
import type { FSAPIStat } from "./FSAPITypes";
import { validateStoragePath } from "@/apps/storagePath";
/**
* Storage adapter implementation for FileSystem API
*/
export class FSAPIStorageAdapter implements IStorageAdapter<FSAPIStat> {
constructor(private readonly rootHandle: FileSystemDirectoryHandle) {}
/**
* Resolve a path to directory and file handles
*/
private async resolvePath(p: string): Promise<{
dirHandle: FileSystemDirectoryHandle;
fileName: string;
} | null> {
validateStoragePath(p, false);
try {
const parts = p.split("/").filter((part) => part !== "");
let currentHandle = this.rootHandle;
const fileName = parts[parts.length - 1];
// Navigate to the parent directory
for (let i = 0; i < parts.length - 1; i++) {
currentHandle = await currentHandle.getDirectoryHandle(parts[i]);
}
return { dirHandle: currentHandle, fileName };
} catch {
return null;
}
}
/**
* Get file handle for a given path
*/
private async getFileHandle(p: string): Promise<FileSystemFileHandle | null> {
validateStoragePath(p);
if (p === "") return null;
const resolved = await this.resolvePath(p);
if (!resolved) return null;
try {
return await resolved.dirHandle.getFileHandle(resolved.fileName);
} catch {
return null;
}
}
/**
* Get directory handle for a given path
*/
private async getDirectoryHandle(p: string): Promise<FileSystemDirectoryHandle | null> {
validateStoragePath(p);
try {
const parts = p.split("/").filter((part) => part !== "");
if (parts.length === 0) {
return this.rootHandle;
}
let currentHandle = this.rootHandle;
for (const part of parts) {
currentHandle = await currentHandle.getDirectoryHandle(part);
}
return currentHandle;
} catch {
return null;
}
}
/** Resolve a writable file path after creating its parent directories. */
private async resolveWritablePath(p: string): Promise<{
dirHandle: FileSystemDirectoryHandle;
fileName: string;
} | null> {
validateStoragePath(p, false);
const parts = p.split("/").filter((part) => part !== "");
const fileName = parts.pop()!;
const parentPath = parts.join("/");
await this.mkdir(parentPath);
const dirHandle = await this.getDirectoryHandle(parentPath);
return dirHandle ? { dirHandle, fileName } : null;
}
async exists(p: string): Promise<boolean> {
const fileHandle = await this.getFileHandle(p);
if (fileHandle) return true;
const dirHandle = await this.getDirectoryHandle(p);
return dirHandle !== null;
}
async trystat(p: string): Promise<FSAPIStat | null> {
// Try as file first
const fileHandle = await this.getFileHandle(p);
if (fileHandle) {
const file = await fileHandle.getFile();
return {
size: file.size,
mtime: file.lastModified,
ctime: file.lastModified,
type: "file",
};
}
// Try as directory
const dirHandle = await this.getDirectoryHandle(p);
if (dirHandle) {
return {
size: 0,
mtime: Date.now(),
ctime: Date.now(),
type: "folder",
};
}
return null;
}
async stat(p: string): Promise<FSAPIStat | null> {
return await this.trystat(p);
}
async mkdir(p: string): Promise<void> {
validateStoragePath(p);
const parts = p.split("/").filter((part) => part !== "");
let currentHandle = this.rootHandle;
for (const part of parts) {
currentHandle = await currentHandle.getDirectoryHandle(part, { create: true });
}
}
async remove(p: string): Promise<void> {
const resolved = await this.resolvePath(p);
if (!resolved) return;
await resolved.dirHandle.removeEntry(resolved.fileName, { recursive: true });
}
async read(p: string): Promise<string> {
const fileHandle = await this.getFileHandle(p);
if (!fileHandle) {
throw new Error(`File not found: ${p}`);
}
const file = await fileHandle.getFile();
return await file.text();
}
async readBinary(p: string): Promise<ArrayBuffer> {
const fileHandle = await this.getFileHandle(p);
if (!fileHandle) {
throw new Error(`File not found: ${p}`);
}
const file = await fileHandle.getFile();
return await file.arrayBuffer();
}
async write(p: string, data: string, options?: UXDataWriteOptions): Promise<void> {
const resolved = await this.resolveWritablePath(p);
if (!resolved) {
throw new Error(`Invalid path: ${p}`);
}
const fileHandle = await resolved.dirHandle.getFileHandle(resolved.fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(data);
await writable.close();
}
async writeBinary(p: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
const resolved = await this.resolveWritablePath(p);
if (!resolved) {
throw new Error(`Invalid path: ${p}`);
}
const fileHandle = await resolved.dirHandle.getFileHandle(resolved.fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(data);
await writable.close();
}
async append(p: string, data: string, options?: UXDataWriteOptions): Promise<void> {
const existing = await this.exists(p);
if (existing) {
const currentContent = await this.read(p);
await this.write(p, currentContent + data, options);
} else {
await this.write(p, data, options);
}
}
async list(basePath: string): Promise<{ files: string[]; folders: string[] }> {
const dirHandle = await this.getDirectoryHandle(basePath);
if (!dirHandle) {
return { files: [], folders: [] };
}
const files: string[] = [];
const folders: string[] = [];
// Use AsyncIterator instead of .values() for better compatibility
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") {
folders.push(entryPath);
} else if (entry.kind === "file") {
files.push(entryPath);
}
}
return { files, folders };
}
}
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { storageAdapterContractCases } from "@/apps/_test/storageAdapterContract";
import { FSAPIFileSystemAdapter } from "./FSAPIFileSystemAdapter";
import { FSAPIStorageAdapter } from "./FSAPIStorageAdapter";
import { FileSystemAccessStorageAdapter } from "@vrtmrz/livesync-commonlib/browser";
import { FSAPIVaultAdapter } from "./FSAPIVaultAdapter";
class MemoryFileHandle {
@@ -100,11 +100,11 @@ class MemoryDirectoryHandle {
}
}
describe("FSAPIStorageAdapter", () => {
describe("FileSystemAccessStorageAdapter", () => {
for (const contractCase of storageAdapterContractCases) {
it(contractCase.name, async () => {
const root = new MemoryDirectoryHandle("root") as unknown as FileSystemDirectoryHandle;
await contractCase.run(new FSAPIStorageAdapter(root));
await contractCase.run(new FileSystemAccessStorageAdapter(root));
});
}
});
@@ -1,4 +1,4 @@
import type { ITypeGuardAdapter } from "@lib/serviceModules/adapters";
import type { ITypeGuardAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
/**
+1 -1
View File
@@ -1,4 +1,4 @@
import type { FilePath, UXStat } from "@lib/common/types";
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
/**
* FileSystem API file representation
@@ -1,5 +1,5 @@
import type { FilePath, UXDataWriteOptions } from "@lib/common/types";
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
import type { FilePath, UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IVaultAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
/**
+1 -1
View File
@@ -1,6 +1,6 @@
import { LiveSyncWebApp } from "./main";
import { VaultHistoryStore, type VaultHistoryItem } from "./vaultSelector";
import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
import { compatGlobal, _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
const historyStore = new VaultHistoryStore();
let app: LiveSyncWebApp | null = null;
+14 -13
View File
@@ -3,23 +3,24 @@
* Browser-based version of Self-hosted LiveSync plugin using FileSystem API
*/
import { BrowserServiceHub } from "@lib/services/BrowserServices";
import type { BrowserServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/BrowserServices";
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { ServiceContext } from "@lib/services/base/ServiceBase";
import { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { initialiseServiceModulesFSAPI } from "./serviceModules/FSAPIServiceModules";
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
import type { BrowserAPIService } from "@lib/services/implements/browser/BrowserAPIService";
import type { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
import { useOfflineScanner } from "@lib/serviceFeatures/offlineScanner";
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { BrowserAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/BrowserAPIService";
import type { InjectableSettingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableSettingService";
import { useOfflineScanner } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner";
import { useRedFlagFeatures } from "@/serviceFeatures/redFlag";
import { useCheckRemoteSize } from "@lib/serviceFeatures/checkRemoteSize";
import { useSetupURIFeature } from "@lib/serviceFeatures/setupObsidian/setupUri";
import { useRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig";
import { useCheckRemoteSize } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/checkRemoteSize";
import { useSetupURIFeature } from "@/serviceFeatures/setupObsidian/setupUri";
import { useRemoteConfiguration } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig";
import { SetupManager } from "@/modules/features/SetupManager";
import { useSetupManagerHandlersFeature } from "@/serviceFeatures/setupObsidian/setupManagerHandlers";
import { useP2PReplicatorCommands } from "@lib/replication/trystero/useP2PReplicatorCommands";
import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature";
import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
import { useP2PReplicatorCommands } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorCommands";
import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature";
import { compatGlobal, _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import { createLiveSyncBrowserServiceHub } from "@/apps/browser/createLiveSyncBrowserServiceHub";
const SETTINGS_DIR = ".livesync";
const SETTINGS_FILE = "settings.json";
@@ -64,7 +65,7 @@ class LiveSyncWebApp {
console.log(`Vault directory: ${this.rootHandle.name}`);
// Create service context and hub
this.serviceHub = new BrowserServiceHub<ServiceContext>();
this.serviceHub = createLiveSyncBrowserServiceHub<ServiceContext>();
// Setup API service
(this.serviceHub.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
@@ -1,6 +1,6 @@
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types";
import type { FileEventItem } from "@lib/common/types";
import type { IStorageEventManagerAdapter } from "@lib/managers/adapters";
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { FileEventItem } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IStorageEventManagerAdapter } from "@vrtmrz/livesync-commonlib/compat/managers/adapters";
import type {
IStorageEventTypeGuardAdapter,
IStorageEventPersistenceAdapter,
@@ -8,10 +8,10 @@ import type {
IStorageEventStatusAdapter,
IStorageEventConverterAdapter,
IStorageEventWatchHandlers,
} from "@lib/managers/adapters";
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
} from "@vrtmrz/livesync-commonlib/compat/managers/adapters";
import type { FileEventItemSentinel } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager";
import type { FSAPIFile, FSAPIFolder } from "@/apps/webapp/adapters/FSAPITypes";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
/**
* FileSystem API-specific type guard adapter
@@ -1,7 +1,7 @@
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager";
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager";
import { FSAPIStorageEventManagerAdapter } from "./FSAPIStorageEventManagerAdapter";
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import type { ServiceContext } from "@lib/services/base/ServiceBase";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
export class StorageEventManagerFSAPI extends StorageEventManagerBase<FSAPIStorageEventManagerAdapter> {
core: LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands>;
+1 -3
View File
@@ -1,7 +1,5 @@
import { defineConfig, devices } from "@playwright/test";
import * as path from "path";
import * as fs from "fs";
import { fileURLToPath } from "url";
import { fileURLToPath, fs, path } from "@vrtmrz/livesync-commonlib/node";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -1,8 +1,8 @@
import {
ServiceDatabaseFileAccessBase,
type ServiceDatabaseFileAccessDependencies,
} from "@lib/serviceModules/ServiceDatabaseFileAccessBase";
import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess";
} from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceDatabaseFileAccessBase";
import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess";
/**
* FileSystem API-specific implementation of ServiceDatabaseFileAccess
@@ -1,14 +1,14 @@
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder";
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
import { ServiceRebuilder } from "@vrtmrz/livesync-commonlib/compat/serviceModules/Rebuilder";
import { StorageAccessManager } from "@lib/managers/StorageProcessingManager";
import { StorageAccessManager } from "@vrtmrz/livesync-commonlib/compat/managers/StorageProcessingManager";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import type { ServiceContext } from "@lib/services/base/ServiceBase";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { FileAccessFSAPI } from "./FileAccessFSAPI";
import { ServiceFileAccessFSAPI } from "./ServiceFileAccessImpl";
import { ServiceDatabaseFileAccessFSAPI } from "./DatabaseFileAccess";
import { StorageEventManagerFSAPI } from "@/apps/webapp/managers/StorageEventManagerFSAPI";
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
import type { ServiceModules } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
/**
@@ -59,6 +59,7 @@ export function initialiseServiceModulesFSAPI(
// Database file access (platform-independent)
const databaseFileAccess = new ServiceDatabaseFileAccessFSAPI({
events: services.context.events,
API: services.API,
database: services.database,
path: services.path,
@@ -68,6 +69,7 @@ export function initialiseServiceModulesFSAPI(
// File handler (platform-independent)
const fileHandler = new ServiceFileHandler({
events: services.context.events,
API: services.API,
databaseFileAccess: databaseFileAccess,
conflict: services.conflict,
@@ -81,6 +83,7 @@ export function initialiseServiceModulesFSAPI(
// Rebuilder (platform-independent)
const rebuilder = new ServiceRebuilder({
events: services.context.events,
API: services.API,
database: services.database,
appLifecycle: services.appLifecycle,
@@ -1,4 +1,4 @@
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
import { FileAccessBase, type FileAccessBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase";
import { FSAPIFileSystemAdapter } from "@/apps/webapp/adapters/FSAPIFileSystemAdapter";
/**
@@ -1,4 +1,4 @@
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileAccessBase";
import { FSAPIFileSystemAdapter } from "@/apps/webapp/adapters/FSAPIFileSystemAdapter";
/**
+10 -3
View File
@@ -10,9 +10,9 @@
*/
import { LiveSyncWebApp } from "./main";
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
import type { FilePathWithPrefix } from "@lib/common/types";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
// --------------------------------------------------------------------------
// Internal state one app instance per page / browser context
@@ -29,10 +29,17 @@ function stripPrefix(raw: string): string {
}
interface TestCore {
serviceModules: {
databaseFileAccess: {
storeContent(path: FilePathWithPrefix, content: string): Promise<unknown>;
delete(path: FilePathWithPrefix): Promise<unknown>;
};
};
services?: {
replication?: {
databaseQueueCount?: { value: number };
storageApplyingCount?: { value: number };
replicate(showMessage: boolean): Promise<unknown>;
};
fileProcessing?: {
totalQueued?: { value: number };
+4 -6
View File
@@ -18,9 +18,7 @@
import { test, expect, type BrowserContext, type Page, type TestInfo } from "@playwright/test";
import type { LiveSyncTestAPI } from "@/apps/webapp/test-entry";
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, fs, path } from "@vrtmrz/livesync-commonlib/node";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -138,16 +136,16 @@ async function dumpCoverage(page: Page | undefined, label: string, testInfo: Tes
(window as any).__coverage__ = {};
return data;
})
.catch(() => null!);
.catch((): null => null);
if (!cov) return;
if (typeof cov === "object" && Object.keys(cov as Record<string, unknown>).length === 0) {
return;
}
const outDir = path.resolve(__dirname, "../.nyc_output");
mkdirSync(outDir, { recursive: true });
fs.mkdirSync(outDir, { recursive: true });
const name = `${testInfo.testId.replace(/[^a-zA-Z0-9_-]/g, "_")}-${label}.json`;
writeFileSync(path.join(outDir, name), JSON.stringify(cov), "utf-8");
fs.writeFileSync(path.join(outDir, name), JSON.stringify(cov), "utf-8");
}
// ---------------------------------------------------------------------------
+1 -2
View File
@@ -23,8 +23,7 @@
/* Path mapping */
// "baseUrl": ".",
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"]
"@/*": ["../../*"]
}
},
"include": ["*.ts", "**/*.ts", "**/*.tsx", "**/*.svelte"],
+1 -1
View File
@@ -1,4 +1,4 @@
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
const HANDLE_DB_NAME = "livesync-webapp-handles";
const HANDLE_STORE_NAME = "handles";
+3 -6
View File
@@ -1,13 +1,11 @@
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import istanbul from "vite-plugin-istanbul";
import path from "node:path";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { fileURLToPath, fs, path } from "@vrtmrz/livesync-commonlib/node";
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 packageJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
const manifestJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
const enableCoverage = process.env.PW_COVERAGE === "1";
// https://vite.dev/config/
export default defineConfig({
@@ -40,7 +38,6 @@ export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "../../"),
"@lib": path.resolve(__dirname, "../../lib/src"),
obsidian: path.resolve(__dirname, "../../../test/harness/obsidian-mock.ts"),
},
},
-4
View File
@@ -22,9 +22,5 @@
"svelte-check": "^4.6.0",
"typescript": "5.9.3",
"vite": "^8.0.16"
},
"imports": {
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
"@lib/worker/bgWorker.ts": "@lib/worker/bgWorker.mock.ts"
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { LOG_LEVEL_VERBOSE } from "@lib/common/types";
import { LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { defaultLoggerEnv, setGlobalLogFunction } from "@lib/common/logger";
import { defaultLoggerEnv, setGlobalLogFunction } from "@vrtmrz/livesync-commonlib/compat/common/logger";
import { writable } from "svelte/store";
export const logs = writable([] as string[]);
+49 -22
View File
@@ -1,4 +1,4 @@
import { PouchDB } from "@lib/pouchdb/pouchdb-browser";
import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser";
import {
type EntryDoc,
type ObsidianLiveSyncSettings,
@@ -6,33 +6,32 @@ import {
LOG_LEVEL_VERBOSE,
P2P_DEFAULT_SETTINGS,
REMOTE_P2P,
} from "@lib/common/types";
import { eventHub } from "@lib/hub/hub";
} from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { Confirm } from "@lib/interfaces/Confirm";
import { LOG_LEVEL_NOTICE, Logger, type LOG_LEVEL } from "@lib/common/logger";
import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm";
import { LOG_LEVEL_NOTICE, Logger, type LOG_LEVEL } from "@vrtmrz/livesync-commonlib/compat/common/logger";
import {
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
type PeerStatus,
type PluginShim,
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
import { useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore";
import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector";
import type { P2PReplicatorBase } from "@lib/replication/trystero/P2PReplicatorBase.ts";
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
import { useP2PReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorCore";
import { P2PLogCollector } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PLogCollector";
import type { P2PReplicatorBase } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorBase";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
import { EVENT_SETTING_SAVED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { unique } from "octagonal-wheels/collection";
import { BrowserServiceHub } from "@lib/services/BrowserServices";
import { SETTING_KEY_P2P_DEVICE_NAME } from "@lib/common/types";
import { ServiceContext } from "@lib/services/base/ServiceBase";
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
import { Menu } from "@lib/services/implements/browser/Menu";
import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
import { Menu } from "@/apps/browser/BrowserMenu";
import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
import type { BrowserAPIService } from "@lib/services/implements/browser/BrowserAPIService";
import type { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
import type { BrowserAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/BrowserAPIService";
import type { InjectableSettingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableSettingService";
import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import { createLiveSyncBrowserServiceHub } from "@/apps/browser/createLiveSyncBrowserServiceHub";
function addToList(item: string, list: string) {
return unique(
@@ -91,7 +90,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
}
constructor() {
const browserServiceHub = new BrowserServiceHub<ServiceContext>();
const browserServiceHub = createLiveSyncBrowserServiceHub<ServiceContext>();
this.services = browserServiceHub;
(this.services.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
@@ -103,7 +102,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
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.context.events.emitEvent(EVENT_SETTING_SAVED, data);
});
(this.services.setting as InjectableSettingService<ServiceContext>).loadData.setHandler(async () => {
const settings = { ..._settings, ...((await repStore.get("settings")) as ObsidianLiveSyncSettings) };
@@ -180,7 +179,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
m?: Menu;
afterConstructor(): void {
eventHub.onEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, ({ peer, event }) => {
this.services.context.events.onEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, ({ peer, event }) => {
if (this.m) {
this.m.hide();
}
@@ -231,6 +230,34 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
return this._liveSyncReplicator?.disableBroadcastChanges();
}
enableBroadcastChanges() {
return this._liveSyncReplicator?.enableBroadcastChanges();
}
disableBroadcastChanges() {
return this._liveSyncReplicator?.disableBroadcastChanges();
}
async makeDecision(decision: Parameters<LiveSyncTrysteroReplicator["makeDecision"]>[0]): Promise<void> {
await this._liveSyncReplicator?.makeDecision(decision);
}
async revokeDecision(decision: Parameters<LiveSyncTrysteroReplicator["revokeDecision"]>[0]): Promise<void> {
await this._liveSyncReplicator?.revokeDecision(decision);
}
watchPeer(peerId: string): void {
this._liveSyncReplicator?.watchPeer(peerId);
}
unwatchPeer(peerId: string): void {
this._liveSyncReplicator?.unwatchPeer(peerId);
}
async sync(peerId: string, showNotice?: boolean): Promise<unknown> {
return await this._liveSyncReplicator?.sync(peerId, showNotice);
}
get replicator() {
return this._liveSyncReplicator;
}
+3 -4
View File
@@ -3,13 +3,12 @@
import P2PReplicatorPane from "@/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte";
import { onMount, tick } from "svelte";
import { cmdSyncShim } from "./P2PReplicatorShim";
import { eventHub } from "@lib/hub/hub";
import { EVENT_LAYOUT_READY } from "@lib/events/coreEvents";
import { EVENT_LAYOUT_READY } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
let synchronised = $state(cmdSyncShim.init());
onMount(() => {
eventHub.emitEvent(EVENT_LAYOUT_READY);
void synchronised.then((shim) => shim.services.context.events.emitEvent(EVENT_LAYOUT_READY));
return () => {
synchronised.then((e) => e.close());
};
@@ -27,7 +26,7 @@
<main>
<div class="control">
{#await synchronised then cmdSync}
<P2PReplicatorPane plugin={cmdSync.plugin} {cmdSync} core={cmdSync.plugin.core}></P2PReplicatorPane>
<P2PReplicatorPane {cmdSync} core={cmdSync.plugin.core}></P2PReplicatorPane>
{:catch error}
<p>{error.message}</p>
{/await}
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { Menu } from "@lib/services/implements/browser/Menu";
import { getDialogContext } from "@lib/services/implements/base/SvelteDialog";
import { Menu } from "@/apps/browser/BrowserMenu";
import { getDialogContext } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog";
let result = $state<string | boolean>("");
const context = getDialogContext();
+1 -1
View File
@@ -1,7 +1,7 @@
import { mount } from "svelte";
import "./app.css";
import App from "./App.svelte";
import { _activeDocument } from "@lib/common/coreEnvFunctions.ts";
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
const app = mount(App, {
target: _activeDocument.getElementById("app")!,
+1 -1
View File
@@ -1,7 +1,7 @@
import { mount } from "svelte";
import "./app.css";
import App from "./UITest.svelte";
import { _activeDocument } from "@lib/common/coreEnvFunctions.ts";
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
const app = mount(App, {
target: _activeDocument.getElementById("app")!,
+1 -2
View File
@@ -18,8 +18,7 @@
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"]
"@/*": ["../../*"]
}
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
+1 -2
View File
@@ -20,8 +20,7 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]
"@/*": ["../../*"]
}
},
"include": ["vite.config.ts"]
+1 -2
View File
@@ -1,13 +1,12 @@
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import path from "node:path";
import { path } from "@vrtmrz/livesync-commonlib/node";
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte()],
resolve: {
alias: {
"@": path.resolve(__dirname, "../../"),
"@lib": path.resolve(__dirname, "../../lib/src"),
},
},
base: "./",
+1 -1
View File
@@ -1,5 +1,5 @@
import { deleteDB, type IDBPDatabase, openDB } from "idb";
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts";
import type { KeyValueDatabase } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase";
import { serialized } from "octagonal-wheels/concurrency/lock";
import { Logger } from "octagonal-wheels/common/logger";
const databaseCache: { [key: string]: IDBPDatabase<unknown> } = {};
+2 -2
View File
@@ -1,5 +1,5 @@
import { LOG_LEVEL_VERBOSE, Logger } from "@lib/common/logger";
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase";
import { LOG_LEVEL_VERBOSE, Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
import type { KeyValueDatabase } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase";
import { deleteDB, openDB, type IDBPDatabase } from "idb";
import { serialized } from "octagonal-wheels/concurrency/lock";
+1 -1
View File
@@ -1,7 +1,7 @@
import { Logger } from "octagonal-wheels/common/logger";
import { fireAndForget } from "octagonal-wheels/promises";
import { eventHub, EVENT_PLUGIN_UNLOADED } from "./events";
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
type PeriodicProcessorHost = NecessaryServices<"API" | "control", never>;
export class PeriodicProcessor {
_process: () => Promise<unknown>;
+10 -3
View File
@@ -1,4 +1,4 @@
import { eventHub } from "@lib/hub/hub";
import { createLiveSyncEventHub } from "@vrtmrz/livesync-commonlib/compat/hub/hub";
// import type ObsidianLiveSyncPlugin from "../main";
export const EVENT_PLUGIN_LOADED = "plugin-loaded";
@@ -43,5 +43,12 @@ declare global {
}
}
export * from "@lib/events/coreEvents.ts";
export { eventHub };
export * from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
/**
* Self-hosted LiveSync's host event channel.
*
* Commonlib does not own a process-global channel. Each LiveSync host context is
* composed with this channel while legacy host UI modules migrate to context.events.
*/
export const eventHub = createLiveSyncEventHub();
+1 -1
View File
@@ -1,5 +1,5 @@
import type { TFile } from "@/deps";
import type { FilePathWithPrefix, LoadedEntry } from "@lib/common/types";
import type { FilePathWithPrefix, LoadedEntry } from "@vrtmrz/livesync-commonlib/compat/common/types";
export const EVENT_REQUEST_SHOW_HISTORY = "show-history";
+9 -9
View File
@@ -1,13 +1,13 @@
import { REMOTE_COUCHDB, REMOTE_MINIO } from "@lib/common/models/setting.const";
import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type";
import { generateCredentialObject } from "@lib/replication/httplib";
import { parseHeaderValues } from "@lib/common/utils";
import { REMOTE_COUCHDB, REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type";
import { generateCredentialObject } from "@vrtmrz/livesync-commonlib/compat/replication/httplib";
import { parseHeaderValues } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import { requestToCouchDBWithCredentials } from "./utils";
import { LOG_LEVEL_VERBOSE, Logger } from "@lib/common/logger";
import { DEFAULT_SETTINGS } from "@lib/common/models/setting.const.defaults";
import { isCloudantURI } from "@lib/pouchdb/utils_couchdb";
import { compatGlobal } from "@lib/common/coreEnvFunctions";
import { manifestVersion, packageVersion } from "@lib/common/coreEnvVars";
import { LOG_LEVEL_VERBOSE, Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const.defaults";
import { isCloudantURI } from "@vrtmrz/livesync-commonlib/compat/pouchdb/utils_couchdb";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import { manifestVersion, packageVersion } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvVars";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
function redactObject(obj: Record<string, unknown>, dotted: string, redactedValue = "REDACTED") {
const keys = dotted.split(".");
+6
View File
@@ -0,0 +1,6 @@
import { $msg } from "@vrtmrz/livesync-commonlib/compat/common/i18n";
import type { MessageTranslator } from "@vrtmrz/livesync-commonlib/context";
/** Supplies the LiveSync language catalogue through the Commonlib host boundary. */
export const translateLiveSyncMessage: MessageTranslator = (key, params) =>
$msg(key as Parameters<typeof $msg>[0], params === undefined ? undefined : { ...params });
+4 -4
View File
@@ -1,6 +1,6 @@
import { type PluginManifest, TFile } from "@/deps.ts";
import { type DatabaseEntry, type EntryBody, type FilePath } from "@lib/common/types.ts";
export type { CacheData, FileEventItem } from "@lib/common/types.ts";
import { type DatabaseEntry, type EntryBody, type FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
export type { CacheData, FileEventItem } from "@vrtmrz/livesync-commonlib/compat/common/types";
export interface PluginDataEntry extends DatabaseEntry {
deviceVaultName: string;
@@ -51,7 +51,7 @@ export type queueItem = {
export const FileWatchEventQueueMax = 10;
export { configURIBase, configURIBaseQR } from "@lib/common/types.ts";
export { configURIBase, configURIBaseQR } from "@vrtmrz/livesync-commonlib/compat/common/types";
export {
CHeader,
@@ -61,4 +61,4 @@ export {
ICHeaderEnd,
ICHeaderLength,
ICXHeader,
} from "@lib/common/models/fileaccess.const.ts";
} from "@vrtmrz/livesync-commonlib/compat/common/models/fileaccess.const";
+16 -13
View File
@@ -7,9 +7,9 @@ import {
isValidFilenameInWidows,
isValidFilenameInAndroid,
stripAllPrefixes,
} from "@lib/string_and_binary/path.ts";
} from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
import { Logger } from "@lib/common/logger.ts";
import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
import {
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
@@ -22,14 +22,14 @@ import {
type FilePathWithPrefix,
type UXFileInfo,
type UXFileInfoStub,
} from "@lib/common/types.ts";
} from "@vrtmrz/livesync-commonlib/compat/common/types";
export { ICHeader, ICXHeader } from "./types.ts";
import { writeString } from "@lib/string_and_binary/convert.ts";
import { writeString } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/convert";
import { sameChangePairs } from "./stores.ts";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { AuthorizationHeaderGenerator } from "@lib/replication/httplib.ts";
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts";
import { AuthorizationHeaderGenerator } from "@vrtmrz/livesync-commonlib/compat/replication/httplib";
import type { KeyValueDatabase } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase";
export { scheduleTask, cancelTask, cancelAllTasks } from "octagonal-wheels/concurrency/task";
@@ -69,7 +69,7 @@ import {
getPathFromUXFileInfo,
getStoragePathFromUXFileInfo,
getDatabasePathFromUXFileInfo,
} from "@lib/common/typeUtils.ts";
} from "@vrtmrz/livesync-commonlib/compat/common/typeUtils";
export { isInternalFile, getPathFromUXFileInfo, getStoragePathFromUXFileInfo, getDatabasePathFromUXFileInfo };
const memos: { [key: string]: unknown } = {};
@@ -121,7 +121,7 @@ export {
isCustomisationSyncMetadata,
isPluginMetadata,
stripInternalMetadataPrefix,
} from "@lib/common/typeUtils.ts";
} from "@vrtmrz/livesync-commonlib/compat/common/typeUtils";
export const _requestToCouchDBFetch = async (
baseUri: string,
@@ -210,11 +210,11 @@ export function requestToCouchDBWithCredentials(
return _requestToCouchDB(baseUri, credentials, origin, uri, body, method, customHeaders);
}
import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols.ts";
import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const.symbols";
export { BASE_IS_NEW, EVEN, TARGET_IS_NEW };
// Why 2000? : ZIP FILE Does not have enough resolution.
import { compareMTime } from "@lib/common/utils.ts";
import { _fetch, compatGlobal } from "@lib/common/coreEnvFunctions.ts";
import { compareMTime } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import { _fetch, compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
export { compareMTime };
function getKey(file: AnyEntry | string | UXFileInfoStub) {
const key = typeof file == "string" ? file : stripAllPrefixes(file.path);
@@ -236,7 +236,10 @@ export function unmarkChanges(file: AnyEntry | string | UXFileInfoStub) {
const key = getKey(file);
sameChangePairs.delete(key);
}
export function isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | string, mtimes: number[]) {
export function isMarkedAsSameChanges(
file: UXFileInfoStub | AnyEntry | string,
mtimes: number[]
): undefined | typeof EVEN {
const key = getKey(file);
const pairs = sameChangePairs.get(key, []) || [];
if (mtimes.every((e) => pairs.indexOf(e) !== -1)) {
@@ -398,4 +401,4 @@ export function onlyInNTimes(n: number, proc: (progress: number) => unknown) {
};
}
export { displayRev } from "@lib/common/utils.ts";
export { displayRev } from "@vrtmrz/livesync-commonlib/compat/common/utils";
+1 -1
View File
@@ -1,4 +1,4 @@
import { type FilePath } from "@lib/common/types.ts";
import { type FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
export {
addIcon,

Some files were not shown because too many files have changed in this diff Show More