From 28146eec2cd49cb8b40ea1f32936aa0448c73abf Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 26 Jan 2026 09:13:40 +0000 Subject: [PATCH 1/8] Refactor: Migrate the outdated, unstable platform abstraction layer to Services --- package-lock.json | 14 +- package.json | 2 +- src/features/P2PSync/CmdP2PReplicator.ts | 7 +- src/lib | 2 +- src/main.ts | 7 +- src/modules/coreFeatures/ModuleRedFlag.ts | 8 +- src/modules/essential/ModuleKeyValueDB.ts | 7 +- src/modules/features/SetupManager.ts | 12 +- .../SetupWizard/ObsidianSvelteDialog.ts | 141 -------------- .../dialogs/SetupRemoteBucket.svelte | 5 +- .../dialogs/SetupRemoteCouchDB.svelte | 5 +- .../SetupWizard/dialogs/SetupRemoteP2P.svelte | 7 +- src/modules/main/ModuleLiveSyncMain.ts | 4 +- src/modules/services/ObsidianConfirm.ts | 111 +++++++++++ src/modules/services/ObsidianServiceHub.ts | 73 ++++++++ src/modules/services/ObsidianServices.ts | 126 +++++-------- src/modules/services/ObsidianUIService.ts | 174 +++--------------- src/modules/services/SvelteDialogObsidian.ts | 37 ++++ test/harness/harness.ts | 19 +- 19 files changed, 342 insertions(+), 419 deletions(-) delete mode 100644 src/modules/features/SetupWizard/ObsidianSvelteDialog.ts create mode 100644 src/modules/services/ObsidianConfirm.ts create mode 100644 src/modules/services/ObsidianServiceHub.ts create mode 100644 src/modules/services/SvelteDialogObsidian.ts diff --git a/package-lock.json b/package-lock.json index 0cfdf5d..800945c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "fflate": "^0.8.2", "idb": "^8.0.3", "minimatch": "^10.0.2", - "octagonal-wheels": "^0.1.44", + "octagonal-wheels": "^0.1.45", "qrcode-generator": "^1.4.4", "trystero": "^0.22.0", "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" @@ -11151,9 +11151,9 @@ "license": "MIT" }, "node_modules/octagonal-wheels": { - "version": "0.1.44", - "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.44.tgz", - "integrity": "sha512-sUn/dkYQ2AbMB0R8CubVd75BjkcsteW9B14ArO99F6wM5JRwOo/yPIBBoxCUFE7JjBFOfuWG21C9E3NTga6XrA==", + "version": "0.1.45", + "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.45.tgz", + "integrity": "sha512-gXoCrwoUIXhmu57YN4BxAtBe+JaYNJNaXaZuVjqjopwYKpH5p2mn1om6KjA22rgGPiIJFXkse2U28FFXoT3/0Q==", "license": "MIT", "dependencies": { "idb": "^8.0.3" @@ -23036,9 +23036,9 @@ "dev": true }, "octagonal-wheels": { - "version": "0.1.44", - "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.44.tgz", - "integrity": "sha512-sUn/dkYQ2AbMB0R8CubVd75BjkcsteW9B14ArO99F6wM5JRwOo/yPIBBoxCUFE7JjBFOfuWG21C9E3NTga6XrA==", + "version": "0.1.45", + "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.45.tgz", + "integrity": "sha512-gXoCrwoUIXhmu57YN4BxAtBe+JaYNJNaXaZuVjqjopwYKpH5p2mn1om6KjA22rgGPiIJFXkse2U28FFXoT3/0Q==", "requires": { "idb": "^8.0.3" } diff --git a/package.json b/package.json index 6914468..ac4c18a 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "fflate": "^0.8.2", "idb": "^8.0.3", "minimatch": "^10.0.2", - "octagonal-wheels": "^0.1.44", + "octagonal-wheels": "^0.1.45", "qrcode-generator": "^1.4.4", "trystero": "^0.22.0", "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" diff --git a/src/features/P2PSync/CmdP2PReplicator.ts b/src/features/P2PSync/CmdP2PReplicator.ts index 7b85b33..211fd3a 100644 --- a/src/features/P2PSync/CmdP2PReplicator.ts +++ b/src/features/P2PSync/CmdP2PReplicator.ts @@ -29,7 +29,7 @@ import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; import type { Confirm } from "../../lib/src/interfaces/Confirm.ts"; import type ObsidianLiveSyncPlugin from "../../main.ts"; import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -import { getPlatformName } from "../../lib/src/PlatformAPIs/obsidian/Environment.ts"; +// import { getPlatformName } from "../../lib/src/PlatformAPIs/obsidian/Environment.ts"; import type { LiveSyncCore } from "../../main.ts"; import { TrysteroReplicator } from "../../lib/src/replication/trystero/TrysteroReplicator.ts"; import { SETTING_KEY_P2P_DEVICE_NAME } from "../../lib/src/common/types.ts"; @@ -130,7 +130,7 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase const getDB = () => this.getDB(); const getConfirm = () => this.confirm; - const getPlatform = () => this.getPlatform(); + const getPlatform = () => this.services.API.getPlatform(); const env = { get db() { return getDB(); @@ -166,9 +166,6 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase throw e; } } - getPlatform(): string { - return getPlatformName(); - } onunload(): void { removeP2PReplicatorInstance(); diff --git a/src/lib b/src/lib index cd32d3d..7c275d5 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit cd32d3d32635a536266efbf7f974b47f70b878c5 +Subproject commit 7c275d50ae3988903f6d518603450d24e979c1ff diff --git a/src/main.ts b/src/main.ts index 7be8578..fc65dc1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -67,9 +67,10 @@ import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleE import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts"; import { P2PReplicator } from "./features/P2PSync/CmdP2PReplicator.ts"; import type { LiveSyncManagers } from "./lib/src/managers/LiveSyncManagers.ts"; -import { ObsidianServiceHub } from "./modules/services/ObsidianServices.ts"; -import type { InjectableServiceHub } from "./lib/src/services/InjectableServices.ts"; -import type { ServiceContext } from "./lib/src/services/ServiceHub.ts"; +import type { InjectableServiceHub } from "./lib/src/services/implements/injectable/InjectableServiceHub.ts"; +import { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts"; +import type { ServiceContext } from "./lib/src/services/base/ServiceBase.ts"; +// import type { InjectableServiceHub } from "./lib/src/services/InjectableServices.ts"; export default class ObsidianLiveSyncPlugin extends Plugin diff --git a/src/modules/coreFeatures/ModuleRedFlag.ts b/src/modules/coreFeatures/ModuleRedFlag.ts index 931e39f..5ed6e1e 100644 --- a/src/modules/coreFeatures/ModuleRedFlag.ts +++ b/src/modules/coreFeatures/ModuleRedFlag.ts @@ -9,10 +9,11 @@ import { } from "../../lib/src/common/types.ts"; import { AbstractModule } from "../AbstractModule.ts"; import type { LiveSyncCore } from "../../main.ts"; -import { SvelteDialogManager } from "../features/SetupWizard/ObsidianSvelteDialog.ts"; import FetchEverything from "../features/SetupWizard/dialogs/FetchEverything.svelte"; import RebuildEverything from "../features/SetupWizard/dialogs/RebuildEverything.svelte"; import { extractObject } from "octagonal-wheels/object"; +import { SvelteDialogManagerBase } from "@/lib/src/UI/svelteDialog.ts"; +import type { ServiceContext } from "@/lib/src/services/base/ServiceBase.ts"; export class ModuleRedFlag extends AbstractModule { async isFlagFileExist(path: string) { @@ -52,7 +53,10 @@ export class ModuleRedFlag extends AbstractModule { await this.deleteFlagFile(FlagFilesOriginal.FETCH_ALL); await this.deleteFlagFile(FlagFilesHumanReadable.FETCH_ALL); } - dialogManager = new SvelteDialogManager(this.core); + // dialogManager = new SvelteDialogManagerBase(this.core); + get dialogManager(): SvelteDialogManagerBase { + return this.core.services.UI.dialogManager; + } /** * Adjust setting to remote if needed. diff --git a/src/modules/essential/ModuleKeyValueDB.ts b/src/modules/essential/ModuleKeyValueDB.ts index 3c58edf..6d657e8 100644 --- a/src/modules/essential/ModuleKeyValueDB.ts +++ b/src/modules/essential/ModuleKeyValueDB.ts @@ -5,6 +5,8 @@ import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/log import { AbstractModule } from "../AbstractModule.ts"; import type { LiveSyncCore } from "../../main.ts"; import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; +import type { InjectableServiceHub } from "@/lib/src/services/InjectableServices.ts"; +import type { ObsidianDatabaseService } from "../services/ObsidianServices.ts"; export class ModuleKeyValueDB extends AbstractModule { async tryCloseKvDB() { @@ -77,6 +79,7 @@ export class ModuleKeyValueDB extends AbstractModule { .filter((e) => e.startsWith(prefix)) .map((e) => e.substring(prefix.length)); }, + db: Promise.resolve(getDB()), } satisfies SimpleStore; } _everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise { @@ -100,12 +103,12 @@ export class ModuleKeyValueDB extends AbstractModule { } return true; } - onBindFunction(core: LiveSyncCore, services: typeof core.services): void { + onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void { services.databaseEvents.onUnloadDatabase.addHandler(this._onDBUnload.bind(this)); services.databaseEvents.onCloseDatabase.addHandler(this._onDBClose.bind(this)); services.databaseEvents.onDatabaseInitialisation.addHandler(this._everyOnInitializeDatabase.bind(this)); services.databaseEvents.onResetDatabase.addHandler(this._everyOnResetDatabase.bind(this)); - services.database.openSimpleStore.setHandler(this._getSimpleStore.bind(this)); + (services.database as ObsidianDatabaseService).openSimpleStore.setHandler(this._getSimpleStore.bind(this)); services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this)); } } diff --git a/src/modules/features/SetupManager.ts b/src/modules/features/SetupManager.ts index 83ebbe1..6691338 100644 --- a/src/modules/features/SetupManager.ts +++ b/src/modules/features/SetupManager.ts @@ -9,7 +9,6 @@ import { } from "../../lib/src/common/types.ts"; import { generatePatchObj, isObjectDifferent } from "../../lib/src/common/utils.ts"; import { AbstractObsidianModule } from "../AbstractObsidianModule.ts"; -import { SvelteDialogManager } from "./SetupWizard/ObsidianSvelteDialog.ts"; import Intro from "./SetupWizard/dialogs/Intro.svelte"; import SelectMethodNewUser from "./SetupWizard/dialogs/SelectMethodNewUser.svelte"; import SelectMethodExisting from "./SetupWizard/dialogs/SelectMethodExisting.svelte"; @@ -52,10 +51,13 @@ export const enum UserMode { * Setup Manager to handle onboarding and configuration setup */ export class SetupManager extends AbstractObsidianModule { - /** - * Dialog manager for handling Svelte dialogs - */ - private dialogManager: SvelteDialogManager = new SvelteDialogManager(this.plugin); + // /** + // * Dialog manager for handling Svelte dialogs + // */ + // private dialogManager: SvelteDialogManager = new SvelteDialogManager(this.plugin); + get dialogManager() { + return this.services.UI.dialogManager; + } /** * Starts the onboarding process diff --git a/src/modules/features/SetupWizard/ObsidianSvelteDialog.ts b/src/modules/features/SetupWizard/ObsidianSvelteDialog.ts deleted file mode 100644 index 51e57ad..0000000 --- a/src/modules/features/SetupWizard/ObsidianSvelteDialog.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { eventHub, EVENT_PLUGIN_UNLOADED } from "@/common/events"; -import { Modal } from "@/deps"; -import type ObsidianLiveSyncPlugin from "@/main"; -import { mount, unmount } from "svelte"; -import DialogHost from "@lib/UI/DialogHost.svelte"; -import { fireAndForget, promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises"; -import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger"; -import { - type DialogControlBase, - type DialogSvelteComponentBaseProps, - type ComponentHasResult, - setupDialogContext, - getDialogContext, - type SvelteDialogManagerBase, -} from "@/lib/src/UI/svelteDialog.ts"; - -export type DialogSvelteComponentProps = DialogSvelteComponentBaseProps & { - plugin: ObsidianLiveSyncPlugin; - services: ObsidianLiveSyncPlugin["services"]; -}; - -export type DialogControls = DialogControlBase & { - plugin: ObsidianLiveSyncPlugin; - services: ObsidianLiveSyncPlugin["services"]; -}; - -export type DialogMessageProps = Record; -// type DialogSvelteComponent = Component,any>; - -export class SvelteDialog extends Modal { - plugin: ObsidianLiveSyncPlugin; - mountedComponent?: ReturnType; - component: ComponentHasResult; - result?: T; - initialData?: U; - title: string = "Obsidian LiveSync - Setup Wizard"; - constructor(plugin: ObsidianLiveSyncPlugin, component: ComponentHasResult, initialData?: U) { - super(plugin.app); - this.plugin = plugin; - this.component = component; - this.initialData = initialData; - } - resolveResult() { - this.resultPromiseWithResolvers?.resolve(this.result); - this.resultPromiseWithResolvers = undefined; - } - resultPromiseWithResolvers?: PromiseWithResolvers; - onOpen() { - const { contentEl } = this; - contentEl.empty(); - // eslint-disable-next-line @typescript-eslint/no-this-alias - const dialog = this; - - if (this.resultPromiseWithResolvers) { - this.resultPromiseWithResolvers.reject("Dialog opened again"); - } - const pr = promiseWithResolvers(); - eventHub.once(EVENT_PLUGIN_UNLOADED, () => { - if (this.resultPromiseWithResolvers === pr) { - pr.reject("Plugin unloaded"); - this.close(); - } - }); - this.resultPromiseWithResolvers = pr; - this.mountedComponent = mount(DialogHost, { - target: contentEl, - props: { - onSetupContext: (props: DialogSvelteComponentBaseProps) => { - setupDialogContext({ - ...props, - plugin: this.plugin, - services: this.plugin.services, - }); - }, - setTitle: (title: string) => { - dialog.setTitle(title); - }, - closeDialog: () => { - dialog.close(); - }, - setResult: (result: T) => { - this.result = result; - }, - getInitialData: () => this.initialData, - mountComponent: this.component, - }, - }); - } - waitForClose(): Promise { - if (!this.resultPromiseWithResolvers) { - throw new Error("Dialog not opened yet"); - } - return this.resultPromiseWithResolvers.promise; - } - onClose() { - this.resolveResult(); - fireAndForget(async () => { - if (this.mountedComponent) { - await unmount(this.mountedComponent); - } - }); - } -} - -export async function openSvelteDialog( - plugin: ObsidianLiveSyncPlugin, - component: ComponentHasResult, - initialData?: U -): Promise { - const dialog = new SvelteDialog(plugin, component, initialData); - dialog.open(); - - return await dialog.waitForClose(); -} - -export class SvelteDialogManager implements SvelteDialogManagerBase { - plugin: ObsidianLiveSyncPlugin; - constructor(plugin: ObsidianLiveSyncPlugin) { - this.plugin = plugin; - } - async open(component: ComponentHasResult, initialData?: U): Promise { - return await openSvelteDialog(this.plugin, component, initialData); - } - async openWithExplicitCancel(component: ComponentHasResult, initialData?: U): Promise { - for (let i = 0; i < 10; i++) { - const ret = await openSvelteDialog(this.plugin, component, initialData); - if (ret !== undefined) { - return ret; - } - if (this.plugin.services.appLifecycle.hasUnloaded()) { - throw new Error("Operation cancelled due to app shutdown."); - } - Logger("Please select 'Cancel' explicitly to cancel this operation.", LOG_LEVEL_NOTICE); - } - throw new Error("Operation Forcibly cancelled by user."); - } -} - -export function getObsidianDialogContext(): DialogControls { - return getDialogContext() as DialogControls; -} diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte index d4f905b..f2270dc 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte @@ -16,8 +16,7 @@ } from "../../../../lib/src/common/types"; import { onMount } from "svelte"; - import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; - import { getObsidianDialogContext } from "../ObsidianSvelteDialog"; + import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; import { copyTo, pickBucketSyncSettings } from "../../../../lib/src/common/utils"; const default_setting = pickBucketSyncSettings(DEFAULT_SETTINGS); @@ -39,7 +38,7 @@ } }); let error = $state(""); - const context = getObsidianDialogContext(); + const context = getDialogContext(); const isEndpointSecure = $derived.by(() => { return syncSetting.endpoint.trim().toLowerCase().startsWith("https://"); }); diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte index 2d7134b..671af71 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte @@ -17,9 +17,8 @@ } from "../../../../lib/src/common/types"; import { isCloudantURI } from "../../../../lib/src/pouchdb/utils_couchdb"; - import { getObsidianDialogContext } from "../ObsidianSvelteDialog"; import { onMount } from "svelte"; - import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; + import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; import { copyTo, pickCouchDBSyncSettings } from "../../../../lib/src/common/utils"; import PanelCouchDBCheck from "./PanelCouchDBCheck.svelte"; @@ -40,7 +39,7 @@ }); let error = $state(""); - const context = getObsidianDialogContext(); + const context = getDialogContext(); function generateSetting() { const connSetting: CouchDBConnection = { diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte index 3ab053c..b07c157 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte @@ -22,16 +22,15 @@ import { TrysteroReplicator } from "../../../../lib/src/replication/trystero/TrysteroReplicator"; import type { ReplicatorHostEnv } from "../../../../lib/src/replication/trystero/types"; import { copyTo, pickP2PSyncSettings, type SimpleStore } from "../../../../lib/src/common/utils"; - import { getObsidianDialogContext } from "../ObsidianSvelteDialog"; import { onMount } from "svelte"; - import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; + import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; import { SETTING_KEY_P2P_DEVICE_NAME } from "../../../../lib/src/common/types"; import ExtraItems from "../../../../lib/src/UI/components/ExtraItems.svelte"; const default_setting = pickP2PSyncSettings(DEFAULT_SETTINGS); let syncSetting = $state({ ...default_setting }); - const context = getObsidianDialogContext(); + const context = getDialogContext(); let error = $state(""); const TYPE_CANCELLED = "cancelled"; type SettingInfo = P2PConnectionInfo; @@ -104,7 +103,7 @@ processReplicatedDocs: async (docs: any[]) => { return; }, - confirm: context.plugin.confirm, + confirm: context.services.confirm, db: dummyPouch, simpleStore: store, deviceName: syncSetting.P2P_DevicePeerName || "unnamed-device", diff --git a/src/modules/main/ModuleLiveSyncMain.ts b/src/modules/main/ModuleLiveSyncMain.ts index 5fa27b3..bc7f95a 100644 --- a/src/modules/main/ModuleLiveSyncMain.ts +++ b/src/modules/main/ModuleLiveSyncMain.ts @@ -13,8 +13,8 @@ import { versionNumberString2Number } from "../../lib/src/string_and_binary/conv import { cancelAllPeriodicTask, cancelAllTasks } from "octagonal-wheels/concurrency/task"; import { stopAllRunningProcessors } from "octagonal-wheels/concurrency/processor"; import { AbstractModule } from "../AbstractModule.ts"; -import { EVENT_PLATFORM_UNLOADED } from "../../lib/src/PlatformAPIs/base/APIBase.ts"; -import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts"; +import { EVENT_PLATFORM_UNLOADED } from "@lib/events/coreEvents"; +import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts"; import type { LiveSyncCore } from "../../main.ts"; import { initialiseWorkerModule } from "@/lib/src/worker/bgWorker.ts"; diff --git a/src/modules/services/ObsidianConfirm.ts b/src/modules/services/ObsidianConfirm.ts new file mode 100644 index 0000000..eb4e3b3 --- /dev/null +++ b/src/modules/services/ObsidianConfirm.ts @@ -0,0 +1,111 @@ +import { type App, type Plugin, Notice } from "@/deps"; +import { scheduleTask, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject } from "@/common/utils"; +import { $msg } from "@/lib/src/common/i18n"; +import type { Confirm } from "@/lib/src/interfaces/Confirm"; +import type { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext"; +import { + askYesNo, + askString, + confirmWithMessageWithWideButton, + askSelectString, + confirmWithMessage, +} from "../coreObsidian/UILib/dialogs"; + +export class ObsidianConfirm implements Confirm { + private _context: T; + get _app(): App { + return this._context.app; + } + get _plugin(): Plugin { + return this._context.plugin; + } + constructor(context: T) { + this._context = context; + } + askYesNo(message: string): Promise<"yes" | "no"> { + return askYesNo(this._app, message); + } + askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise { + return askString(this._app, title, key, placeholder, isPassword); + } + + async askYesNoDialog( + message: string, + opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" } + ): Promise<"yes" | "no"> { + const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleConfirmation"); + const yesLabel = $msg("moduleInputUIObsidian.optionYes"); + const noLabel = $msg("moduleInputUIObsidian.optionNo"); + const defaultOption = opt.defaultOption === "Yes" ? yesLabel : noLabel; + const ret = await confirmWithMessageWithWideButton( + this._plugin, + opt.title || defaultTitle, + message, + [yesLabel, noLabel], + defaultOption, + opt.timeout + ); + return ret === yesLabel ? "yes" : "no"; + } + + askSelectString(message: string, items: string[]): Promise { + return askSelectString(this._app, message, items); + } + + askSelectStringDialogue( + message: string, + buttons: T, + opt: { title?: string; defaultAction: T[number]; timeout?: number } + ): Promise { + const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect"); + return confirmWithMessageWithWideButton( + this._plugin, + opt.title || defaultTitle, + message, + buttons, + opt.defaultAction, + opt.timeout + ); + } + + askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) { + const fragment = createFragment((doc) => { + const [beforeText, afterText] = dialogText.split("{HERE}", 2); + doc.createEl("span", undefined, (a) => { + a.appendText(beforeText); + a.appendChild( + a.createEl("a", undefined, (anchor) => { + anchorCallback(anchor); + }) + ); + a.appendText(afterText); + }); + }); + const popupKey = "popup-" + key; + scheduleTask(popupKey, 1000, async () => { + const popup = await memoIfNotExist(popupKey, () => new Notice(fragment, 0)); + const isShown = popup?.noticeEl?.isShown(); + if (!isShown) { + memoObject(popupKey, new Notice(fragment, 0)); + } + scheduleTask(popupKey + "-close", 20000, () => { + const popup = retrieveMemoObject(popupKey); + if (!popup) return; + if (popup?.noticeEl?.isShown()) { + popup.hide(); + } + disposeMemoObject(popupKey); + }); + }); + } + + confirmWithMessage( + title: string, + contentMd: string, + buttons: string[], + defaultAction: (typeof buttons)[number], + timeout?: number + ): Promise<(typeof buttons)[number] | false> { + return confirmWithMessage(this._plugin, title, contentMd, buttons, defaultAction, timeout); + } +} diff --git a/src/modules/services/ObsidianServiceHub.ts b/src/modules/services/ObsidianServiceHub.ts new file mode 100644 index 0000000..c01d747 --- /dev/null +++ b/src/modules/services/ObsidianServiceHub.ts @@ -0,0 +1,73 @@ +import { InjectableServiceHub } from "@/lib/src/services/implements/injectable/InjectableServiceHub"; +import { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext"; +import type { ServiceInstances } from "@/lib/src/services/ServiceHub"; +import type ObsidianLiveSyncPlugin from "@/main"; +import { + ObsidianAPIService, + ObsidianAppLifecycleService, + ObsidianConflictService, + ObsidianDatabaseService, + ObsidianFileProcessingService, + ObsidianReplicationService, + ObsidianReplicatorService, + ObsidianRemoteService, + ObsidianSettingService, + ObsidianTweakValueService, + ObsidianVaultService, + ObsidianTestService, + ObsidianDatabaseEventService, + ObsidianPathService, + ObsidianConfigService, +} from "./ObsidianServices"; +import { ObsidianUIService } from "./ObsidianUIService"; + +// InjectableServiceHub + +export class ObsidianServiceHub extends InjectableServiceHub { + constructor(plugin: ObsidianLiveSyncPlugin) { + const context = new ObsidianServiceContext(plugin.app, plugin, plugin); + + const API = new ObsidianAPIService(context); + const appLifecycle = new ObsidianAppLifecycleService(context); + const conflict = new ObsidianConflictService(context); + const database = new ObsidianDatabaseService(context); + const fileProcessing = new ObsidianFileProcessingService(context); + const replication = new ObsidianReplicationService(context); + const replicator = new ObsidianReplicatorService(context); + const remote = new ObsidianRemoteService(context); + const setting = new ObsidianSettingService(context); + const tweakValue = new ObsidianTweakValueService(context); + const vault = new ObsidianVaultService(context); + const test = new ObsidianTestService(context); + const databaseEvents = new ObsidianDatabaseEventService(context); + const path = new ObsidianPathService(context); + const config = new ObsidianConfigService(context, vault); + const ui = new ObsidianUIService(context, { + appLifecycle, + config, + replicator, + }); + + // Using 'satisfies' to ensure all services are provided + const serviceInstancesToInit = { + appLifecycle: appLifecycle, + conflict: conflict, + database: database, + databaseEvents: databaseEvents, + fileProcessing: fileProcessing, + replication: replication, + replicator: replicator, + remote: remote, + setting: setting, + tweakValue: tweakValue, + vault: vault, + test: test, + ui: ui, + path: path, + API: API, + config: config, + } satisfies Required>; + + super(context, serviceInstancesToInit); + } +} diff --git a/src/modules/services/ObsidianServices.ts b/src/modules/services/ObsidianServices.ts index 7acd888..2f79c67 100644 --- a/src/modules/services/ObsidianServices.ts +++ b/src/modules/services/ObsidianServices.ts @@ -1,48 +1,56 @@ -import { ServiceContext, type ServiceInstances } from "@/lib/src/services/ServiceHub.ts"; -import { - InjectableAPIService, - InjectableAppLifecycleService, - InjectableConflictService, - InjectableDatabaseEventService, - InjectableDatabaseService, - InjectableFileProcessingService, - InjectablePathService, - InjectableRemoteService, - InjectableReplicationService, - InjectableReplicatorService, - InjectableSettingService, - InjectableTestService, - InjectableTweakValueService, - InjectableVaultService, -} from "../../lib/src/services/InjectableServices.ts"; -import { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts"; -import { ConfigServiceBrowserCompat } from "../../lib/src/services/Services.ts"; -import type ObsidianLiveSyncPlugin from "../../main.ts"; -import { ObsidianUIService } from "./ObsidianUIService.ts"; -import type { App, Plugin } from "@/deps"; - -export class ObsidianServiceContext extends ServiceContext { - app: App; - plugin: Plugin; - liveSyncPlugin: ObsidianLiveSyncPlugin; - constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin) { - super(); - this.app = app; - this.plugin = plugin; - this.liveSyncPlugin = liveSyncPlugin; - } -} +import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService"; +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 { InjectableDatabaseService } from "@lib/services/implements/injectable/InjectableDatabaseService"; +import { InjectableFileProcessingService } from "@lib/services/implements/injectable/InjectableFileProcessingService"; +import { InjectablePathService } 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 { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService"; +import { InjectableTestService } from "@lib/services/implements/injectable/InjectableTestService"; +import { InjectableTweakValueService } from "@lib/services/implements/injectable/InjectableTweakValueService"; +import { InjectableVaultService } from "@lib/services/implements/injectable/InjectableVaultService"; +import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat"; +import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts"; +import { Platform } from "@/deps"; +import type { SimpleStore } from "@/lib/src/common/utils"; +import type { IDatabaseService } from "@/lib/src/services/base/IService"; +import { handlers } from "@/lib/src/services/lib/HandlerUtils"; // All Services will be migrated to be based on Plain Services, not Injectable Services. // This is a migration step. export class ObsidianAPIService extends InjectableAPIService { getPlatform(): string { - return "obsidian"; + if (Platform.isAndroidApp) { + return "android-app"; + } else if (Platform.isIosApp) { + return "ios"; + } else if (Platform.isMacOS) { + return "macos"; + } else if (Platform.isMobileApp) { + return "mobile-app"; + } else if (Platform.isMobile) { + return "mobile"; + } else if (Platform.isSafari) { + return "safari"; + } else if (Platform.isDesktop) { + return "desktop"; + } else if (Platform.isDesktopApp) { + return "desktop-app"; + } else { + return "unknown-obsidian"; + } } } export class ObsidianPathService extends InjectablePathService {} -export class ObsidianDatabaseService extends InjectableDatabaseService {} +export class ObsidianDatabaseService extends InjectableDatabaseService { + openSimpleStore = handlers().binder("openSimpleStore") as (( + kind: string + ) => SimpleStore) & { setHandler: (handler: IDatabaseService["openSimpleStore"], override?: boolean) => void }; +} export class ObsidianDatabaseEventService extends InjectableDatabaseEventService {} // InjectableReplicatorService @@ -66,49 +74,3 @@ export class ObsidianVaultService extends InjectableVaultService {} export class ObsidianConfigService extends ConfigServiceBrowserCompat {} - -// InjectableServiceHub - -export class ObsidianServiceHub extends InjectableServiceHub { - constructor(plugin: ObsidianLiveSyncPlugin) { - const context = new ObsidianServiceContext(plugin.app, plugin, plugin); - - const API = new ObsidianAPIService(context); - const appLifecycle = new ObsidianAppLifecycleService(context); - const conflict = new ObsidianConflictService(context); - const database = new ObsidianDatabaseService(context); - const fileProcessing = new ObsidianFileProcessingService(context); - const replication = new ObsidianReplicationService(context); - const replicator = new ObsidianReplicatorService(context); - const remote = new ObsidianRemoteService(context); - const setting = new ObsidianSettingService(context); - const tweakValue = new ObsidianTweakValueService(context); - const vault = new ObsidianVaultService(context); - const test = new ObsidianTestService(context); - const databaseEvents = new ObsidianDatabaseEventService(context); - const path = new ObsidianPathService(context); - const ui = new ObsidianUIService(context); - const config = new ObsidianConfigService(context, vault); - // Using 'satisfies' to ensure all services are provided - const serviceInstancesToInit = { - appLifecycle: appLifecycle, - conflict: conflict, - database: database, - databaseEvents: databaseEvents, - fileProcessing: fileProcessing, - replication: replication, - replicator: replicator, - remote: remote, - setting: setting, - tweakValue: tweakValue, - vault: vault, - test: test, - ui: ui, - path: path, - API: API, - config: config, - } satisfies Required>; - - super(context, serviceInstancesToInit); - } -} diff --git a/src/modules/services/ObsidianUIService.ts b/src/modules/services/ObsidianUIService.ts index e4df15e..7c0bd43 100644 --- a/src/modules/services/ObsidianUIService.ts +++ b/src/modules/services/ObsidianUIService.ts @@ -1,156 +1,30 @@ -import { UIService } from "../../lib/src/services/Services"; -import { Notice, type App, type Plugin } from "@/deps"; -import { SvelteDialogManager } from "../features/SetupWizard/ObsidianSvelteDialog"; -import DialogueToCopy from "../../lib/src/UI/dialogues/DialogueToCopy.svelte"; -import type { ObsidianServiceContext } from "./ObsidianServices"; -import type ObsidianLiveSyncPlugin from "@/main"; -import type { Confirm } from "@/lib/src/interfaces/Confirm"; -import { - askSelectString, - askString, - askYesNo, - confirmWithMessage, - confirmWithMessageWithWideButton, -} from "../coreObsidian/UILib/dialogs"; -import { $msg } from "@/lib/src/common/i18n"; -import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "@/common/utils"; -export class ObsidianConfirm implements Confirm { - private _app: App; - private _plugin: Plugin; - constructor(app: App, plugin: Plugin) { - this._app = app; - this._plugin = plugin; - } - askYesNo(message: string): Promise<"yes" | "no"> { - return askYesNo(this._app, message); - } - askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise { - return askString(this._app, title, key, placeholder, isPassword); - } +import type { ConfigService } from "@lib/services/base/ConfigService"; +import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; +import type { ReplicatorService } from "@lib/services/base/ReplicatorService"; +import { UIService } from "@lib/services//implements/base/UIService"; +import { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext"; +import { ObsidianSvelteDialogManager } from "./SvelteDialogObsidian"; +import { ObsidianConfirm } from "./ObsidianConfirm"; - async askYesNoDialog( - message: string, - opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" } - ): Promise<"yes" | "no"> { - const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleConfirmation"); - const yesLabel = $msg("moduleInputUIObsidian.optionYes"); - const noLabel = $msg("moduleInputUIObsidian.optionNo"); - const defaultOption = opt.defaultOption === "Yes" ? yesLabel : noLabel; - const ret = await confirmWithMessageWithWideButton( - this._plugin, - opt.title || defaultTitle, - message, - [yesLabel, noLabel], - defaultOption, - opt.timeout - ); - return ret === yesLabel ? "yes" : "no"; - } +export type ObsidianUIServiceDependencies = { + appLifecycle: AppLifecycleService; + config: ConfigService; + replicator: ReplicatorService; +}; - askSelectString(message: string, items: string[]): Promise { - return askSelectString(this._app, message, items); - } - - askSelectStringDialogue( - message: string, - buttons: T, - opt: { title?: string; defaultAction: T[number]; timeout?: number } - ): Promise { - const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect"); - return confirmWithMessageWithWideButton( - this._plugin, - opt.title || defaultTitle, - message, - buttons, - opt.defaultAction, - opt.timeout - ); - } - - askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) { - const fragment = createFragment((doc) => { - const [beforeText, afterText] = dialogText.split("{HERE}", 2); - doc.createEl("span", undefined, (a) => { - a.appendText(beforeText); - a.appendChild( - a.createEl("a", undefined, (anchor) => { - anchorCallback(anchor); - }) - ); - a.appendText(afterText); - }); - }); - const popupKey = "popup-" + key; - scheduleTask(popupKey, 1000, async () => { - const popup = await memoIfNotExist(popupKey, () => new Notice(fragment, 0)); - const isShown = popup?.noticeEl?.isShown(); - if (!isShown) { - memoObject(popupKey, new Notice(fragment, 0)); - } - scheduleTask(popupKey + "-close", 20000, () => { - const popup = retrieveMemoObject(popupKey); - if (!popup) return; - if (popup?.noticeEl?.isShown()) { - popup.hide(); - } - disposeMemoObject(popupKey); - }); - }); - } - - confirmWithMessage( - title: string, - contentMd: string, - buttons: string[], - defaultAction: (typeof buttons)[number], - timeout?: number - ): Promise<(typeof buttons)[number] | false> { - return confirmWithMessage(this._plugin, title, contentMd, buttons, defaultAction, timeout); - } -} export class ObsidianUIService extends UIService { - private _dialogManager: SvelteDialogManager; - private _plugin: Plugin; - private _liveSyncPlugin: ObsidianLiveSyncPlugin; - private _confirmInstance: ObsidianConfirm; - get dialogManager() { - return this._dialogManager; - } - constructor(context: ObsidianServiceContext) { - super(context); - this._liveSyncPlugin = context.liveSyncPlugin; - this._dialogManager = new SvelteDialogManager(this._liveSyncPlugin); - this._plugin = context.plugin; - this._confirmInstance = new ObsidianConfirm(this._plugin.app, this._plugin); - } - - async promptCopyToClipboard(title: string, value: string): Promise { - const param = { - title: title, - dataToCopy: value, - }; - const result = await this._dialogManager.open(DialogueToCopy, param); - if (result !== "ok") { - return false; - } - return true; - } - - showMarkdownDialog( - title: string, - contentMD: string, - buttons: T, - defaultAction?: (typeof buttons)[number] - ): Promise<(typeof buttons)[number] | false> { - // TODO: implement `confirm` to this service - return this._liveSyncPlugin.confirm.askSelectStringDialogue(contentMD, buttons, { - title, - defaultAction: defaultAction ?? buttons[0], - timeout: 0, + constructor(context: ObsidianServiceContext, dependents: ObsidianUIServiceDependencies) { + const obsidianConfirm = new ObsidianConfirm(context); + const obsidianSvelteDialogManager = new ObsidianSvelteDialogManager(context, { + appLifecycle: dependents.appLifecycle, + config: dependents.config, + replicator: dependents.replicator, + confirm: obsidianConfirm, + }); + super(context, { + appLifecycle: dependents.appLifecycle, + dialogManager: obsidianSvelteDialogManager, + confirm: obsidianConfirm, }); } - - get confirm(): Confirm { - return this._confirmInstance; - } } diff --git a/src/modules/services/SvelteDialogObsidian.ts b/src/modules/services/SvelteDialogObsidian.ts new file mode 100644 index 0000000..c57d8f1 --- /dev/null +++ b/src/modules/services/SvelteDialogObsidian.ts @@ -0,0 +1,37 @@ +import { Modal } from "@/deps"; + +import { + SvelteDialogManagerBase, + SvelteDialogMixIn, + type ComponentHasResult, + type SvelteDialogManagerDependencies, +} from "@lib/services/implements/base/SvelteDialog"; +import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; + +export const SvelteDialogBase = SvelteDialogMixIn(Modal); +export class SvelteDialogObsidian< + T, + U, + C extends ObsidianServiceContext = ObsidianServiceContext, +> extends SvelteDialogBase { + constructor( + context: C, + dependents: SvelteDialogManagerDependencies, + component: ComponentHasResult, + initialData?: U + ) { + super(context.app); + this.initDialog(context, dependents, component, initialData); + } +} + +export class ObsidianSvelteDialogManager extends SvelteDialogManagerBase { + override async openSvelteDialog( + component: ComponentHasResult, + initialData?: TU + ): Promise { + const dialog = new SvelteDialogObsidian(this.context, this.dependents, component, initialData); + dialog.open(); + return await dialog.waitForClose(); + } +} diff --git a/test/harness/harness.ts b/test/harness/harness.ts index 329e532..390934f 100644 --- a/test/harness/harness.ts +++ b/test/harness/harness.ts @@ -3,9 +3,10 @@ import ObsidianLiveSyncPlugin from "@/main"; import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; import { LOG_LEVEL_VERBOSE, setGlobalLogFunction } from "@lib/common/logger"; import { SettingCache } from "./obsidian-mock"; -import { delay, promiseWithResolvers } from "octagonal-wheels/promises"; +import { delay, fireAndForget, promiseWithResolvers } from "octagonal-wheels/promises"; +import { EVENT_PLATFORM_UNLOADED } from "@lib/events/coreEvents"; import { EVENT_LAYOUT_READY, eventHub } from "@/common/events"; -import { EVENT_PLATFORM_UNLOADED } from "@/lib/src/PlatformAPIs/base/APIBase"; + import { env } from "../suite/variables"; export type LiveSyncHarness = { @@ -79,12 +80,14 @@ export async function generateHarness( await plugin.onload(); let isDisposed = false; const waitPromise = promiseWithResolvers(); - eventHub.once(EVENT_PLATFORM_UNLOADED, async () => { - console.log(`Harness for vault '${vaultName}' disposed.`); - await delay(100); - eventHub.offAll(); - isDisposed = true; - waitPromise.resolve(); + eventHub.once(EVENT_PLATFORM_UNLOADED, () => { + fireAndForget(async () => { + console.log(`Harness for vault '${vaultName}' disposed.`); + await delay(100); + eventHub.offAll(); + isDisposed = true; + waitPromise.resolve(); + }); }); eventHub.once(EVENT_LAYOUT_READY, () => { plugin.app.vault.trigger("layout-ready"); From 46546e121f8374a146238eb2da2d349630723842 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 26 Jan 2026 09:17:01 +0000 Subject: [PATCH 2/8] Refactor: Move webpeer from lib to main repository. --- .eslintrc | 1 + eslint.config.mjs | 2 +- src/apps/webpeer/.gitignore | 24 + src/apps/webpeer/README.md | 30 + src/apps/webpeer/deno.lock | 1101 +++++++++++++++++++++ src/apps/webpeer/index.html | 17 + src/apps/webpeer/package.json | 25 + src/apps/webpeer/public/icon.svg | 52 + src/apps/webpeer/public/manifest.json | 26 + src/apps/webpeer/src/App.svelte | 5 + src/apps/webpeer/src/CommandsShim.ts | 23 + src/apps/webpeer/src/P2PReplicatorShim.ts | 364 +++++++ src/apps/webpeer/src/SyncMain.svelte | 112 +++ src/apps/webpeer/src/UITest.svelte | 74 ++ src/apps/webpeer/src/app.css | 112 +++ src/apps/webpeer/src/assets/svelte.svg | 1 + src/apps/webpeer/src/main.ts | 9 + src/apps/webpeer/src/uitest.ts | 9 + src/apps/webpeer/src/vite-env.d.ts | 2 + src/apps/webpeer/svelte.config.js | 7 + src/apps/webpeer/tsconfig.app.json | 25 + src/apps/webpeer/tsconfig.json | 4 + src/apps/webpeer/tsconfig.node.json | 28 + src/apps/webpeer/ui.html | 16 + src/apps/webpeer/vite.config.ts | 24 + test/shell/p2p-start.sh | 2 +- tsconfig.json | 9 +- vite.config.ts | 5 +- 28 files changed, 2097 insertions(+), 12 deletions(-) create mode 100644 src/apps/webpeer/.gitignore create mode 100644 src/apps/webpeer/README.md create mode 100644 src/apps/webpeer/deno.lock create mode 100644 src/apps/webpeer/index.html create mode 100644 src/apps/webpeer/package.json create mode 100644 src/apps/webpeer/public/icon.svg create mode 100644 src/apps/webpeer/public/manifest.json create mode 100644 src/apps/webpeer/src/App.svelte create mode 100644 src/apps/webpeer/src/CommandsShim.ts create mode 100644 src/apps/webpeer/src/P2PReplicatorShim.ts create mode 100644 src/apps/webpeer/src/SyncMain.svelte create mode 100644 src/apps/webpeer/src/UITest.svelte create mode 100644 src/apps/webpeer/src/app.css create mode 100644 src/apps/webpeer/src/assets/svelte.svg create mode 100644 src/apps/webpeer/src/main.ts create mode 100644 src/apps/webpeer/src/uitest.ts create mode 100644 src/apps/webpeer/src/vite-env.d.ts create mode 100644 src/apps/webpeer/svelte.config.js create mode 100644 src/apps/webpeer/tsconfig.app.json create mode 100644 src/apps/webpeer/tsconfig.json create mode 100644 src/apps/webpeer/tsconfig.node.json create mode 100644 src/apps/webpeer/ui.html create mode 100644 src/apps/webpeer/vite.config.ts diff --git a/.eslintrc b/.eslintrc index 0f74a9b..f55fe98 100644 --- a/.eslintrc +++ b/.eslintrc @@ -26,6 +26,7 @@ "**/tests.ts", "**/**test.ts", "**/**.test.ts", + "src/apps/**", "esbuild.*.mjs", "terser.*.mjs" ], diff --git a/eslint.config.mjs b/eslint.config.mjs index 7dd29b2..b2fbf76 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -40,7 +40,7 @@ export default [ "src/lib/test", "src/lib/src/cli", "**/main.js", - "src/lib/apps/webpeer/*", + "src/apps/**/*", ".prettierrc.*.mjs", ".prettierrc.mjs", "*.config.mjs" diff --git a/src/apps/webpeer/.gitignore b/src/apps/webpeer/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/src/apps/webpeer/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/apps/webpeer/README.md b/src/apps/webpeer/README.md new file mode 100644 index 0000000..81a2af5 --- /dev/null +++ b/src/apps/webpeer/README.md @@ -0,0 +1,30 @@ +# A pseudo client for Self-hosted LiveSync Peer-to-Peer Sync mode + +## What is it for? + +This is a pseudo client for the Self-hosted LiveSync Peer-to-Peer Sync mode. It is a simple pure-client-side web-application that can be connected to the Self-hosted LiveSync in peer-to-peer. + +As long as you have a browser, it starts up, so if you leave it opened some device, it can replace your existing remote servers such as CouchDB. + +> [!IMPORTANT] +> Of course, it has not been fully tested. Rather, it was created to be tested. + +This pseudo client actually receives the data from other devices, and sends if some device requests it. However, it does not store **files** in the local storage. If you want to purge the data, please purge the browser's cache and indexedDB, local storage, etc. + +## How to use it? + +We can build the application by running the following command: + +```bash +$ deno task build +``` + +Then, open the `dist/index.html` in the browser. It can be configured as the same as the Self-hosted LiveSync (Same components are used[^1]). + +## Some notes + +I will launch this application in the github pages later, so will be able to use it without building it. However, that shares the origin. Hence, the application that your have built and deployed would be more secure. + + +[^1]: Congrats! I made it modular. Finally... + diff --git a/src/apps/webpeer/deno.lock b/src/apps/webpeer/deno.lock new file mode 100644 index 0000000..e908f88 --- /dev/null +++ b/src/apps/webpeer/deno.lock @@ -0,0 +1,1101 @@ +{ + "version": "5", + "specifiers": { + "npm:@sveltejs/vite-plugin-svelte@^6.2.1": "6.2.4_svelte@5.41.1__acorn@8.15.0_vite@7.3.1__picomatch@4.0.3", + "npm:@tsconfig/svelte@^5.0.5": "5.0.6", + "npm:eslint-plugin-svelte@^3.12.4": "3.14.0_eslint@9.39.2_svelte@5.41.1__acorn@8.15.0_postcss@8.5.6", + "npm:svelte-check@^4.3.3": "4.3.5_svelte@5.41.1__acorn@8.15.0_typescript@5.9.3", + "npm:svelte@5.41.1": "5.41.1_acorn@8.15.0", + "npm:typescript@5.9.3": "5.9.3", + "npm:vite@^7.3.0": "7.3.1_picomatch@4.0.3" + }, + "npm": { + "@esbuild/aix-ppc64@0.27.2": { + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@esbuild/android-arm64@0.27.2": { + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@esbuild/android-arm@0.27.2": { + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "os": ["android"], + "cpu": ["arm"] + }, + "@esbuild/android-x64@0.27.2": { + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "os": ["android"], + "cpu": ["x64"] + }, + "@esbuild/darwin-arm64@0.27.2": { + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@esbuild/darwin-x64@0.27.2": { + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@esbuild/freebsd-arm64@0.27.2": { + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@esbuild/freebsd-x64@0.27.2": { + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@esbuild/linux-arm64@0.27.2": { + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@esbuild/linux-arm@0.27.2": { + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@esbuild/linux-ia32@0.27.2": { + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "os": ["linux"], + "cpu": ["ia32"] + }, + "@esbuild/linux-loong64@0.27.2": { + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@esbuild/linux-mips64el@0.27.2": { + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@esbuild/linux-ppc64@0.27.2": { + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@esbuild/linux-riscv64@0.27.2": { + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@esbuild/linux-s390x@0.27.2": { + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@esbuild/linux-x64@0.27.2": { + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@esbuild/netbsd-arm64@0.27.2": { + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@esbuild/netbsd-x64@0.27.2": { + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@esbuild/openbsd-arm64@0.27.2": { + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@esbuild/openbsd-x64@0.27.2": { + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@esbuild/openharmony-arm64@0.27.2": { + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@esbuild/sunos-x64@0.27.2": { + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@esbuild/win32-arm64@0.27.2": { + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@esbuild/win32-ia32@0.27.2": { + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@esbuild/win32-x64@0.27.2": { + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@eslint-community/eslint-utils@4.9.1_eslint@9.39.2": { + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dependencies": [ + "eslint", + "eslint-visitor-keys@3.4.3" + ] + }, + "@eslint-community/regexpp@4.12.2": { + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==" + }, + "@eslint/config-array@0.21.1": { + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dependencies": [ + "@eslint/object-schema", + "debug", + "minimatch" + ] + }, + "@eslint/config-helpers@0.4.2": { + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dependencies": [ + "@eslint/core" + ] + }, + "@eslint/core@0.17.0": { + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dependencies": [ + "@types/json-schema" + ] + }, + "@eslint/eslintrc@3.3.3": { + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dependencies": [ + "ajv", + "debug", + "espree", + "globals@14.0.0", + "ignore", + "import-fresh", + "js-yaml", + "minimatch", + "strip-json-comments" + ] + }, + "@eslint/js@9.39.2": { + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==" + }, + "@eslint/object-schema@2.1.7": { + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==" + }, + "@eslint/plugin-kit@0.4.1": { + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dependencies": [ + "@eslint/core", + "levn" + ] + }, + "@humanfs/core@0.19.1": { + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==" + }, + "@humanfs/node@0.16.7": { + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dependencies": [ + "@humanfs/core", + "@humanwhocodes/retry" + ] + }, + "@humanwhocodes/module-importer@1.0.1": { + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + }, + "@humanwhocodes/retry@0.4.3": { + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" + }, + "@jridgewell/gen-mapping@0.3.13": { + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": [ + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/remapping@2.3.5": { + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dependencies": [ + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/resolve-uri@3.1.2": { + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/sourcemap-codec@1.5.5": { + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "@jridgewell/trace-mapping@0.3.31": { + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": [ + "@jridgewell/resolve-uri", + "@jridgewell/sourcemap-codec" + ] + }, + "@rollup/rollup-android-arm-eabi@4.56.0": { + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "os": ["android"], + "cpu": ["arm"] + }, + "@rollup/rollup-android-arm64@4.56.0": { + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-arm64@4.56.0": { + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-x64@4.56.0": { + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@rollup/rollup-freebsd-arm64@4.56.0": { + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@rollup/rollup-freebsd-x64@4.56.0": { + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-arm-gnueabihf@4.56.0": { + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm-musleabihf@4.56.0": { + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm64-gnu@4.56.0": { + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-arm64-musl@4.56.0": { + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-loong64-gnu@4.56.0": { + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@rollup/rollup-linux-loong64-musl@4.56.0": { + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@rollup/rollup-linux-ppc64-gnu@4.56.0": { + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@rollup/rollup-linux-ppc64-musl@4.56.0": { + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@rollup/rollup-linux-riscv64-gnu@4.56.0": { + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-riscv64-musl@4.56.0": { + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-s390x-gnu@4.56.0": { + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@rollup/rollup-linux-x64-gnu@4.56.0": { + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-x64-musl@4.56.0": { + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-openbsd-x64@4.56.0": { + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@rollup/rollup-openharmony-arm64@4.56.0": { + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@rollup/rollup-win32-arm64-msvc@4.56.0": { + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@rollup/rollup-win32-ia32-msvc@4.56.0": { + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@rollup/rollup-win32-x64-gnu@4.56.0": { + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@rollup/rollup-win32-x64-msvc@4.56.0": { + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@sveltejs/acorn-typescript@1.0.8_acorn@8.15.0": { + "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", + "dependencies": [ + "acorn" + ] + }, + "@sveltejs/vite-plugin-svelte-inspector@5.0.2_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.41.1___acorn@8.15.0__vite@7.3.1___picomatch@4.0.3_svelte@5.41.1__acorn@8.15.0_vite@7.3.1__picomatch@4.0.3": { + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte", + "obug", + "svelte", + "vite" + ] + }, + "@sveltejs/vite-plugin-svelte@6.2.4_svelte@5.41.1__acorn@8.15.0_vite@7.3.1__picomatch@4.0.3": { + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte-inspector", + "deepmerge", + "magic-string", + "obug", + "svelte", + "vite", + "vitefu" + ] + }, + "@tsconfig/svelte@5.0.6": { + "integrity": "sha512-yGxYL0I9eETH1/DR9qVJey4DAsCdeau4a9wYPKuXfEhm8lFO8wg+LLYJjIpAm6Fw7HSlhepPhYPDop75485yWQ==" + }, + "@types/estree@1.0.8": { + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, + "@types/json-schema@7.0.15": { + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "acorn-jsx@5.3.2_acorn@8.15.0": { + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dependencies": [ + "acorn" + ] + }, + "acorn@8.15.0": { + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "bin": true + }, + "ajv@6.12.6": { + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": [ + "fast-deep-equal", + "fast-json-stable-stringify", + "json-schema-traverse", + "uri-js" + ] + }, + "ansi-styles@4.3.0": { + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": [ + "color-convert" + ] + }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "aria-query@5.3.2": { + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" + }, + "axobject-query@4.1.0": { + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" + }, + "balanced-match@1.0.2": { + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion@1.1.12": { + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dependencies": [ + "balanced-match", + "concat-map" + ] + }, + "callsites@3.1.0": { + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "chalk@4.1.2": { + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": [ + "ansi-styles", + "supports-color" + ] + }, + "chokidar@4.0.3": { + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dependencies": [ + "readdirp" + ] + }, + "clsx@2.1.1": { + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "color-convert@2.0.1": { + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": [ + "color-name" + ] + }, + "color-name@1.1.4": { + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "concat-map@0.0.1": { + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "cross-spawn@7.0.6": { + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": [ + "path-key", + "shebang-command", + "which" + ] + }, + "cssesc@3.0.0": { + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": true + }, + "debug@4.4.3": { + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": [ + "ms" + ] + }, + "deep-is@0.1.4": { + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "deepmerge@4.3.1": { + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + }, + "esbuild@0.27.2": { + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "optionalDependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/openharmony-arm64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ], + "scripts": true, + "bin": true + }, + "escape-string-regexp@4.0.0": { + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "eslint-plugin-svelte@3.14.0_eslint@9.39.2_svelte@5.41.1__acorn@8.15.0_postcss@8.5.6": { + "integrity": "sha512-Isw0GvaMm0yHxAj71edAdGFh28ufYs+6rk2KlbbZphnqZAzrH3Se3t12IFh2H9+1F/jlDhBBL4oiOJmLqmYX0g==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@jridgewell/sourcemap-codec", + "eslint", + "esutils", + "globals@16.5.0", + "known-css-properties", + "postcss", + "postcss-load-config", + "postcss-safe-parser", + "semver", + "svelte", + "svelte-eslint-parser" + ], + "optionalPeers": [ + "svelte" + ] + }, + "eslint-scope@8.4.0": { + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dependencies": [ + "esrecurse", + "estraverse" + ] + }, + "eslint-visitor-keys@3.4.3": { + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + }, + "eslint-visitor-keys@4.2.1": { + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==" + }, + "eslint@9.39.2": { + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@eslint-community/regexpp", + "@eslint/config-array", + "@eslint/config-helpers", + "@eslint/core", + "@eslint/eslintrc", + "@eslint/js", + "@eslint/plugin-kit", + "@humanfs/node", + "@humanwhocodes/module-importer", + "@humanwhocodes/retry", + "@types/estree", + "ajv", + "chalk", + "cross-spawn", + "debug", + "escape-string-regexp", + "eslint-scope", + "eslint-visitor-keys@4.2.1", + "espree", + "esquery", + "esutils", + "fast-deep-equal", + "file-entry-cache", + "find-up", + "glob-parent", + "ignore", + "imurmurhash", + "is-glob", + "json-stable-stringify-without-jsonify", + "lodash.merge", + "minimatch", + "natural-compare", + "optionator" + ], + "bin": true + }, + "esm-env@1.2.2": { + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" + }, + "espree@10.4.0_acorn@8.15.0": { + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dependencies": [ + "acorn", + "acorn-jsx", + "eslint-visitor-keys@4.2.1" + ] + }, + "esquery@1.7.0": { + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dependencies": [ + "estraverse" + ] + }, + "esrap@2.2.2": { + "integrity": "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==", + "dependencies": [ + "@jridgewell/sourcemap-codec" + ] + }, + "esrecurse@4.3.0": { + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": [ + "estraverse" + ] + }, + "estraverse@5.3.0": { + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "esutils@2.0.3": { + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "fast-deep-equal@3.1.3": { + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify@2.1.0": { + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein@2.0.6": { + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "fdir@6.5.0_picomatch@4.0.3": { + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dependencies": [ + "picomatch" + ], + "optionalPeers": [ + "picomatch" + ] + }, + "file-entry-cache@8.0.0": { + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dependencies": [ + "flat-cache" + ] + }, + "find-up@5.0.0": { + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": [ + "locate-path", + "path-exists" + ] + }, + "flat-cache@4.0.1": { + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dependencies": [ + "flatted", + "keyv" + ] + }, + "flatted@3.3.3": { + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" + }, + "fsevents@2.3.3": { + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "os": ["darwin"], + "scripts": true + }, + "glob-parent@6.0.2": { + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": [ + "is-glob" + ] + }, + "globals@14.0.0": { + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" + }, + "globals@16.5.0": { + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==" + }, + "has-flag@4.0.0": { + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "ignore@5.3.2": { + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" + }, + "import-fresh@3.3.1": { + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": [ + "parent-module", + "resolve-from" + ] + }, + "imurmurhash@0.1.4": { + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": [ + "is-extglob" + ] + }, + "is-reference@3.0.3": { + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dependencies": [ + "@types/estree" + ] + }, + "isexe@2.0.0": { + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "js-yaml@4.1.1": { + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dependencies": [ + "argparse" + ], + "bin": true + }, + "json-buffer@3.0.1": { + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "json-schema-traverse@0.4.1": { + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify@1.0.1": { + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "keyv@4.5.4": { + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": [ + "json-buffer" + ] + }, + "known-css-properties@0.37.0": { + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==" + }, + "levn@0.4.1": { + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": [ + "prelude-ls", + "type-check" + ] + }, + "lilconfig@2.1.0": { + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==" + }, + "locate-character@3.0.0": { + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" + }, + "locate-path@6.0.0": { + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": [ + "p-locate" + ] + }, + "lodash.merge@4.6.2": { + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "magic-string@0.30.21": { + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": [ + "@jridgewell/sourcemap-codec" + ] + }, + "minimatch@3.1.2": { + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": [ + "brace-expansion" + ] + }, + "mri@1.2.0": { + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid@3.3.11": { + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "bin": true + }, + "natural-compare@1.4.0": { + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "obug@2.1.1": { + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==" + }, + "optionator@0.9.4": { + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dependencies": [ + "deep-is", + "fast-levenshtein", + "levn", + "prelude-ls", + "type-check", + "word-wrap" + ] + }, + "p-limit@3.1.0": { + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": [ + "yocto-queue" + ] + }, + "p-locate@5.0.0": { + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": [ + "p-limit" + ] + }, + "parent-module@1.0.1": { + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": [ + "callsites" + ] + }, + "path-exists@4.0.0": { + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-key@3.1.1": { + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch@4.0.3": { + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" + }, + "postcss-load-config@3.1.4_postcss@8.5.6": { + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dependencies": [ + "lilconfig", + "postcss", + "yaml" + ], + "optionalPeers": [ + "postcss" + ] + }, + "postcss-safe-parser@7.0.1_postcss@8.5.6": { + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dependencies": [ + "postcss" + ] + }, + "postcss-scss@4.0.9_postcss@8.5.6": { + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dependencies": [ + "postcss" + ] + }, + "postcss-selector-parser@7.1.1": { + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dependencies": [ + "cssesc", + "util-deprecate" + ] + }, + "postcss@8.5.6": { + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + "prelude-ls@1.2.1": { + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + }, + "punycode@2.3.1": { + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "readdirp@4.1.2": { + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" + }, + "resolve-from@4.0.0": { + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "rollup@4.56.0": { + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "dependencies": [ + "@types/estree" + ], + "optionalDependencies": [ + "@rollup/rollup-android-arm-eabi", + "@rollup/rollup-android-arm64", + "@rollup/rollup-darwin-arm64", + "@rollup/rollup-darwin-x64", + "@rollup/rollup-freebsd-arm64", + "@rollup/rollup-freebsd-x64", + "@rollup/rollup-linux-arm-gnueabihf", + "@rollup/rollup-linux-arm-musleabihf", + "@rollup/rollup-linux-arm64-gnu", + "@rollup/rollup-linux-arm64-musl", + "@rollup/rollup-linux-loong64-gnu", + "@rollup/rollup-linux-loong64-musl", + "@rollup/rollup-linux-ppc64-gnu", + "@rollup/rollup-linux-ppc64-musl", + "@rollup/rollup-linux-riscv64-gnu", + "@rollup/rollup-linux-riscv64-musl", + "@rollup/rollup-linux-s390x-gnu", + "@rollup/rollup-linux-x64-gnu", + "@rollup/rollup-linux-x64-musl", + "@rollup/rollup-openbsd-x64", + "@rollup/rollup-openharmony-arm64", + "@rollup/rollup-win32-arm64-msvc", + "@rollup/rollup-win32-ia32-msvc", + "@rollup/rollup-win32-x64-gnu", + "@rollup/rollup-win32-x64-msvc", + "fsevents" + ], + "bin": true + }, + "sade@1.8.1": { + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": [ + "mri" + ] + }, + "semver@7.7.3": { + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "bin": true + }, + "shebang-command@2.0.0": { + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": [ + "shebang-regex" + ] + }, + "shebang-regex@3.0.0": { + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "strip-json-comments@3.1.1": { + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "supports-color@7.2.0": { + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": [ + "has-flag" + ] + }, + "svelte-check@4.3.5_svelte@5.41.1__acorn@8.15.0_typescript@5.9.3": { + "integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==", + "dependencies": [ + "@jridgewell/trace-mapping", + "chokidar", + "fdir", + "picocolors", + "sade", + "svelte", + "typescript" + ], + "bin": true + }, + "svelte-eslint-parser@1.4.1_svelte@5.41.1__acorn@8.15.0_postcss@8.5.6": { + "integrity": "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA==", + "dependencies": [ + "eslint-scope", + "eslint-visitor-keys@4.2.1", + "espree", + "postcss", + "postcss-scss", + "postcss-selector-parser", + "svelte" + ], + "optionalPeers": [ + "svelte" + ] + }, + "svelte@5.41.1_acorn@8.15.0": { + "integrity": "sha512-0a/huwc8e2es+7KFi70esqsReRfRbrT8h1cJSY/+z1lF0yKM6TT+//HYu28Yxstr50H7ifaqZRDGd0KuKDxP7w==", + "dependencies": [ + "@jridgewell/remapping", + "@jridgewell/sourcemap-codec", + "@sveltejs/acorn-typescript", + "@types/estree", + "acorn", + "aria-query", + "axobject-query", + "clsx", + "esm-env", + "esrap", + "is-reference", + "locate-character", + "magic-string", + "zimmerframe" + ] + }, + "tinyglobby@0.2.15_picomatch@4.0.3": { + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dependencies": [ + "fdir", + "picomatch" + ] + }, + "type-check@0.4.0": { + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": [ + "prelude-ls" + ] + }, + "typescript@5.9.3": { + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "bin": true + }, + "uri-js@4.4.1": { + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": [ + "punycode" + ] + }, + "util-deprecate@1.0.2": { + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "vite@7.3.1_picomatch@4.0.3": { + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dependencies": [ + "esbuild", + "fdir", + "picomatch", + "postcss", + "rollup", + "tinyglobby" + ], + "optionalDependencies": [ + "fsevents" + ], + "bin": true + }, + "vitefu@1.1.1_vite@7.3.1__picomatch@4.0.3": { + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dependencies": [ + "vite" + ], + "optionalPeers": [ + "vite" + ] + }, + "which@2.0.2": { + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": [ + "isexe" + ], + "bin": true + }, + "word-wrap@1.2.5": { + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + }, + "yaml@1.10.2": { + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "yocto-queue@0.1.0": { + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zimmerframe@1.1.4": { + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==" + } + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@sveltejs/vite-plugin-svelte@^6.2.1", + "npm:@tsconfig/svelte@^5.0.5", + "npm:eslint-plugin-svelte@^3.12.4", + "npm:svelte-check@^4.3.3", + "npm:svelte@5.41.1", + "npm:typescript@5.9.3", + "npm:vite@^7.3.0" + ] + } + } +} diff --git a/src/apps/webpeer/index.html b/src/apps/webpeer/index.html new file mode 100644 index 0000000..1753155 --- /dev/null +++ b/src/apps/webpeer/index.html @@ -0,0 +1,17 @@ + + + + + + + + + Peer-to-Peer Daemon on Browser + + + +
+ + + + \ No newline at end of file diff --git a/src/apps/webpeer/package.json b/src/apps/webpeer/package.json new file mode 100644 index 0000000..dd12898 --- /dev/null +++ b/src/apps/webpeer/package.json @@ -0,0 +1,25 @@ +{ + "name": "webpeer", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json" + }, + "dependencies": {}, + "devDependencies": { + "eslint-plugin-svelte": "^3.12.4", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tsconfig/svelte": "^5.0.5", + "svelte": "5.41.1", + "svelte-check": "^4.3.3", + "typescript": "5.9.3", + "vite": "^7.3.0" + }, + "imports": { + "../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts" + } +} diff --git a/src/apps/webpeer/public/icon.svg b/src/apps/webpeer/public/icon.svg new file mode 100644 index 0000000..a7fd775 --- /dev/null +++ b/src/apps/webpeer/public/icon.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + diff --git a/src/apps/webpeer/public/manifest.json b/src/apps/webpeer/public/manifest.json new file mode 100644 index 0000000..39cb501 --- /dev/null +++ b/src/apps/webpeer/public/manifest.json @@ -0,0 +1,26 @@ +{ + "name": "WebPeer - Pseudo client for Self-hosted LiveSync Peer-to-Peer Replication", + "short_name": "WepPeer", + "description": "A web-based pseudo peer-to-peer replication client using as like server for background sync.", + "start_url": "./", + "display": "standalone", + "background_color": "#fff", + "theme_color": "#fff", + "orientation": "any", + "icons": [ + { + "src": "./icon.svg", + "sizes": "512x512", + "type": "image/svg+xml", + "maskable": true + } + ], + "additional_icons": [ + { + "src": "./icon.svg", + "sizes": "512x512", + "type": "image/svg+xml", + "maskable": true + } + ] +} diff --git a/src/apps/webpeer/src/App.svelte b/src/apps/webpeer/src/App.svelte new file mode 100644 index 0000000..bf32f50 --- /dev/null +++ b/src/apps/webpeer/src/App.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/apps/webpeer/src/CommandsShim.ts b/src/apps/webpeer/src/CommandsShim.ts new file mode 100644 index 0000000..0f53cd1 --- /dev/null +++ b/src/apps/webpeer/src/CommandsShim.ts @@ -0,0 +1,23 @@ +import { LOG_LEVEL_VERBOSE } from "@lib/common/types"; + +import { defaultLoggerEnv, setGlobalLogFunction } from "@lib/common/logger"; +import { writable } from "svelte/store"; + +export const logs = writable([] as string[]); + +let _logs = [] as string[]; + +const maxLines = 10000; +setGlobalLogFunction((msg, level) => { + console.log(msg); + const msgstr = typeof msg === "string" ? msg : JSON.stringify(msg); + const strLog = `${new Date().toISOString()}\u2001${msgstr}`; + _logs.push(strLog); + if (_logs.length > maxLines) { + _logs = _logs.slice(_logs.length - maxLines); + } + logs.set(_logs); +}); +defaultLoggerEnv.minLogLevel = LOG_LEVEL_VERBOSE; + +export const storeP2PStatusLine = writable(""); diff --git a/src/apps/webpeer/src/P2PReplicatorShim.ts b/src/apps/webpeer/src/P2PReplicatorShim.ts new file mode 100644 index 0000000..c548fd1 --- /dev/null +++ b/src/apps/webpeer/src/P2PReplicatorShim.ts @@ -0,0 +1,364 @@ +import { PouchDB } from "@lib/pouchdb/pouchdb-browser"; +import { + type EntryDoc, + type LOG_LEVEL, + type P2PSyncSetting, + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, + P2P_DEFAULT_SETTINGS, + REMOTE_P2P, +} from "@lib/common/types"; +import { eventHub } from "@lib/hub/hub"; + +import type { Confirm } from "@lib/interfaces/Confirm"; +import { LOG_LEVEL_INFO, Logger } from "@lib/common/logger"; +import { storeP2PStatusLine } from "./CommandsShim"; +import { + EVENT_P2P_PEER_SHOW_EXTRA_MENU, + type CommandShim, + type PeerStatus, + type PluginShim, +} from "@lib/replication/trystero/P2PReplicatorPaneCommon"; +import { + closeP2PReplicator, + openP2PReplicator, + P2PLogCollector, + type P2PReplicatorBase, +} from "@lib/replication/trystero/P2PReplicatorCore"; +import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; +import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; +import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents"; +import { unique } from "octagonal-wheels/collection"; +import { BrowserServiceHub } from "@lib/services/BrowserServices"; +import { TrysteroReplicator } from "@lib/replication/trystero/TrysteroReplicator"; +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/src/services/implements/browser/Menu"; + +function addToList(item: string, list: string) { + return unique( + list + .split(",") + .map((e) => e.trim()) + .concat(item) + .filter((p) => p) + ).join(","); +} +function removeFromList(item: string, list: string) { + return list + .split(",") + .map((e) => e.trim()) + .filter((p) => p !== item) + .filter((p) => p) + .join(","); +} + +export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim { + storeP2PStatusLine = reactiveSource(""); + plugin!: PluginShim; + // environment!: IEnvironment; + confirm!: Confirm; + // simpleStoreAPI!: ISimpleStoreAPI; + db?: PouchDB.Database; + services: InjectableServiceHub; + + getDB() { + if (!this.db) { + throw new Error("DB not initialized"); + } + return this.db; + } + _simpleStore!: SimpleStore; + async closeDB() { + if (this.db) { + await this.db.close(); + this.db = undefined; + } + } + constructor() { + const browserServiceHub = new BrowserServiceHub(); + this.services = browserServiceHub; + this.services.vault.getVaultName.setHandler(() => "p2p-livesync-web-peer"); + } + async init() { + // const { simpleStoreAPI } = await getWrappedSynchromesh(); + // this.confirm = confirm; + this.confirm = this.services.UI.confirm; + // this.environment = environment; + + if (this.db) { + try { + await this.closeDB(); + } catch (ex) { + Logger("Error closing db", LOG_LEVEL_VERBOSE); + Logger(ex, LOG_LEVEL_VERBOSE); + } + } + + const repStore = this.services.database.openSimpleStore("p2p-livesync-web-peer"); + this._simpleStore = repStore; + let _settings = (await repStore.get("settings")) || ({ ...P2P_DEFAULT_SETTINGS } as P2PSyncSetting); + + this.plugin = { + saveSettings: async () => { + await repStore.set("settings", _settings); + eventHub.emitEvent(EVENT_SETTING_SAVED, _settings); + }, + get settings() { + return _settings; + }, + set settings(newSettings: P2PSyncSetting) { + _settings = { ..._settings, ...newSettings }; + }, + rebuilder: null, + services: this.services, + // $$scheduleAppReload: () => {}, + // $$getVaultName: () => "p2p-livesync-web-peer", + }; + // const deviceName = this.getDeviceName(); + const database_name = this.settings.P2P_AppID + "-" + this.settings.P2P_roomID + "p2p-livesync-web-peer"; + this.db = new PouchDB(database_name); + setTimeout(() => { + if (this.settings.P2P_AutoStart && this.settings.P2P_Enabled) { + void this.open(); + } + }, 1000); + return this; + } + get settings() { + return this.plugin.settings; + } + _log(msg: any, level?: LOG_LEVEL): void { + Logger(msg, level); + } + _notice(msg: string, key?: string): void { + Logger(msg, LOG_LEVEL_NOTICE, key); + } + getSettings(): P2PSyncSetting { + return this.settings; + } + simpleStore(): SimpleStore { + return this._simpleStore; + } + handleReplicatedDocuments(docs: EntryDoc[]): Promise { + // No op. This is a client and does not need to process the docs + return Promise.resolve(); + } + + getPluginShim() { + return {}; + } + getConfig(key: string) { + const vaultName = this.services.vault.getVaultName(); + const dbKey = `${vaultName}-${key}`; + return localStorage.getItem(dbKey); + } + setConfig(key: string, value: string) { + const vaultName = this.services.vault.getVaultName(); + const dbKey = `${vaultName}-${key}`; + localStorage.setItem(dbKey, value); + } + + getDeviceName(): string { + return this.getConfig(SETTING_KEY_P2P_DEVICE_NAME) ?? this.plugin.services.vault.getVaultName(); + } + getPlatform(): string { + return "pseudo-replicator"; + } + m?: Menu; + afterConstructor(): void { + eventHub.onEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, ({ peer, event }) => { + if (this.m) { + this.m.hide(); + } + this.m = new Menu() + .addItem((item) => item.setTitle("📥 Only Fetch").onClick(() => this.replicateFrom(peer))) + .addItem((item) => item.setTitle("📤 Only Send").onClick(() => this.replicateTo(peer))) + .addSeparator() + // .addItem((item) => { + // item.setTitle("🔧 Get Configuration").onClick(async () => { + // await this.getRemoteConfig(peer); + // }); + // }) + // .addSeparator() + .addItem((item) => { + const mark = peer.syncOnConnect ? "checkmark" : null; + item.setTitle("Toggle Sync on connect") + .onClick(async () => { + await this.toggleProp(peer, "syncOnConnect"); + }) + .setIcon(mark); + }) + .addItem((item) => { + const mark = peer.watchOnConnect ? "checkmark" : null; + item.setTitle("Toggle Watch on connect") + .onClick(async () => { + await this.toggleProp(peer, "watchOnConnect"); + }) + .setIcon(mark); + }) + .addItem((item) => { + const mark = peer.syncOnReplicationCommand ? "checkmark" : null; + item.setTitle("Toggle Sync on `Replicate now` command") + .onClick(async () => { + await this.toggleProp(peer, "syncOnReplicationCommand"); + }) + .setIcon(mark); + }); + void this.m.showAtPosition({ x: event.x, y: event.y }); + }); + this.p2pLogCollector.p2pReplicationLine.onChanged((line) => { + storeP2PStatusLine.set(line.value); + }); + } + + _replicatorInstance?: TrysteroReplicator; + p2pLogCollector = new P2PLogCollector(); + async open() { + await openP2PReplicator(this); + } + async close() { + await closeP2PReplicator(this); + } + enableBroadcastCastings() { + return this?._replicatorInstance?.enableBroadcastChanges(); + } + disableBroadcastCastings() { + return this?._replicatorInstance?.disableBroadcastChanges(); + } + + async initialiseP2PReplicator(): Promise { + await this.init(); + try { + if (this._replicatorInstance) { + await this._replicatorInstance.close(); + this._replicatorInstance = undefined; + } + + if (!this.settings.P2P_AppID) { + this.settings.P2P_AppID = P2P_DEFAULT_SETTINGS.P2P_AppID; + } + const getInitialDeviceName = () => + this.getConfig(SETTING_KEY_P2P_DEVICE_NAME) || this.services.vault.getVaultName(); + + const getSettings = () => this.settings; + const store = () => this.simpleStore(); + const getDB = () => this.getDB(); + + const getConfirm = () => this.confirm; + const getPlatform = () => this.getPlatform(); + const env = { + get db() { + return getDB(); + }, + get confirm() { + return getConfirm(); + }, + get deviceName() { + return getInitialDeviceName(); + }, + get platform() { + return getPlatform(); + }, + get settings() { + return getSettings(); + }, + processReplicatedDocs: async (docs: EntryDoc[]): Promise => { + await this.handleReplicatedDocuments(docs); + // No op. This is a client and does not need to process the docs + }, + get simpleStore() { + return store(); + }, + }; + this._replicatorInstance = new TrysteroReplicator(env); + return this._replicatorInstance; + } catch (e) { + this._log( + e instanceof Error ? e.message : "Something occurred on Initialising P2P Replicator", + LOG_LEVEL_INFO + ); + this._log(e, LOG_LEVEL_VERBOSE); + throw e; + } + } + + get replicator() { + return this._replicatorInstance!; + } + async replicateFrom(peer: PeerStatus) { + await this.replicator.replicateFrom(peer.peerId); + } + async replicateTo(peer: PeerStatus) { + await this.replicator.requestSynchroniseToPeer(peer.peerId); + } + async getRemoteConfig(peer: PeerStatus) { + Logger( + `Requesting remote config for ${peer.name}. Please input the passphrase on the remote device`, + LOG_LEVEL_NOTICE + ); + const remoteConfig = await this.replicator.getRemoteConfig(peer.peerId); + if (remoteConfig) { + Logger(`Remote config for ${peer.name} is retrieved successfully`); + const DROP = "Yes, and drop local database"; + const KEEP = "Yes, but keep local database"; + const CANCEL = "No, cancel"; + const yn = await this.confirm.askSelectStringDialogue( + `Do you really want to apply the remote config? This will overwrite your current config immediately and restart. + And you can also drop the local database to rebuild from the remote device.`, + [DROP, KEEP, CANCEL] as const, + { + defaultAction: CANCEL, + title: "Apply Remote Config ", + } + ); + if (yn === DROP || yn === KEEP) { + if (yn === DROP) { + if (remoteConfig.remoteType !== REMOTE_P2P) { + const yn2 = await this.confirm.askYesNoDialog( + `Do you want to set the remote type to "P2P Sync" to rebuild by "P2P replication"?`, + { + title: "Rebuild from remote device", + } + ); + if (yn2 === "yes") { + remoteConfig.remoteType = REMOTE_P2P; + remoteConfig.P2P_RebuildFrom = peer.name; + } + } + } + this.plugin.settings = remoteConfig; + await this.plugin.saveSettings(); + if (yn === DROP) { + await this.plugin.rebuilder.scheduleFetch(); + } else { + await this.plugin.services.appLifecycle.scheduleRestart(); + } + } else { + Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE); + } + } else { + Logger(`Cannot retrieve remote config for ${peer.peerId}`); + } + } + + async toggleProp(peer: PeerStatus, prop: "syncOnConnect" | "watchOnConnect" | "syncOnReplicationCommand") { + const settingMap = { + syncOnConnect: "P2P_AutoSyncPeers", + watchOnConnect: "P2P_AutoWatchPeers", + syncOnReplicationCommand: "P2P_SyncOnReplication", + } as const; + + const targetSetting = settingMap[prop]; + if (peer[prop]) { + this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]); + await this.plugin.saveSettings(); + } else { + this.plugin.settings[targetSetting] = addToList(peer.name, this.plugin.settings[targetSetting]); + await this.plugin.saveSettings(); + } + } +} + +export const cmdSyncShim = new P2PReplicatorShim(); diff --git a/src/apps/webpeer/src/SyncMain.svelte b/src/apps/webpeer/src/SyncMain.svelte new file mode 100644 index 0000000..853ce86 --- /dev/null +++ b/src/apps/webpeer/src/SyncMain.svelte @@ -0,0 +1,112 @@ + + +
+
+ {#await synchronised then cmdSync} + + {:catch error} +

{error.message}

+ {/await} +
+
+
+ {statusLine} +
+
+ {#each $logs as log} +

{log}

+ {/each} +
+
+
+ + diff --git a/src/apps/webpeer/src/UITest.svelte b/src/apps/webpeer/src/UITest.svelte new file mode 100644 index 0000000..8cd83fc --- /dev/null +++ b/src/apps/webpeer/src/UITest.svelte @@ -0,0 +1,74 @@ + + +
+

UI Test

+
+
+ + → {result} +
+
+ + → {resultPassword} +
+
+ +
+
+
diff --git a/src/apps/webpeer/src/app.css b/src/apps/webpeer/src/app.css new file mode 100644 index 0000000..85b8da6 --- /dev/null +++ b/src/apps/webpeer/src/app.css @@ -0,0 +1,112 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + --background-primary: #ffffff; + --background-primary-alt: #e9e9e9; + --size-4-1: 0.25em; + --tag-background: #f0f0f0; + --tag-border-width: 1px; + --tag-border-color: #cfffdd; + --background-modifier-success: #d4f3e9; + --background-secondary: #f0f0f0; + --background-modifier-error: #f8d7da; + --background-modifier-error-hover: #f5c6cb; + --interactive-accent: #007bff; + --interactive-accent-hover: #0056b3; + --text-normal: #333; + --text-warning: #f0ad4e; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +body { + margin: 0; + padding: 0; + display: flex; + min-width: 320px; + min-height: 100vh; + box-sizing: border-box; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +.card { + padding: 2em; +} + +#app { + margin: 0; + padding: 0; + text-align: center; + display: flex; + flex-grow: 1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +input, +select { + border-radius: 8px; + border: 1px solid #1a1a1a; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + transition: border-color 0.25s; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} \ No newline at end of file diff --git a/src/apps/webpeer/src/assets/svelte.svg b/src/apps/webpeer/src/assets/svelte.svg new file mode 100644 index 0000000..c5e0848 --- /dev/null +++ b/src/apps/webpeer/src/assets/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/apps/webpeer/src/main.ts b/src/apps/webpeer/src/main.ts new file mode 100644 index 0000000..befae65 --- /dev/null +++ b/src/apps/webpeer/src/main.ts @@ -0,0 +1,9 @@ +import { mount } from "svelte"; +import "./app.css"; +import App from "./App.svelte"; + +const app = mount(App, { + target: document.getElementById("app")!, +}); + +export default app; diff --git a/src/apps/webpeer/src/uitest.ts b/src/apps/webpeer/src/uitest.ts new file mode 100644 index 0000000..04132b0 --- /dev/null +++ b/src/apps/webpeer/src/uitest.ts @@ -0,0 +1,9 @@ +import { mount } from "svelte"; +import "./app.css"; +import App from "./UITest.svelte"; + +const app = mount(App, { + target: document.getElementById("app")!, +}); + +export default app; diff --git a/src/apps/webpeer/src/vite-env.d.ts b/src/apps/webpeer/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/src/apps/webpeer/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/src/apps/webpeer/svelte.config.js b/src/apps/webpeer/svelte.config.js new file mode 100644 index 0000000..9a3adfb --- /dev/null +++ b/src/apps/webpeer/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +}; diff --git a/src/apps/webpeer/tsconfig.app.json b/src/apps/webpeer/tsconfig.app.json new file mode 100644 index 0000000..5bd9975 --- /dev/null +++ b/src/apps/webpeer/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "sourceRoot": "../", + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "isolatedModules": true, + "moduleDetection": "force", + "paths": { + "@/*": ["../../*"], + "@lib/*": ["../../lib/src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/src/apps/webpeer/tsconfig.json b/src/apps/webpeer/tsconfig.json new file mode 100644 index 0000000..eb69b0d --- /dev/null +++ b/src/apps/webpeer/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] +} diff --git a/src/apps/webpeer/tsconfig.node.json b/src/apps/webpeer/tsconfig.node.json new file mode 100644 index 0000000..c5f047b --- /dev/null +++ b/src/apps/webpeer/tsconfig.node.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": ["../../*"], + "@lib/*": ["../../lib/src/*"] + } + }, + "include": ["vite.config.ts"] +} diff --git a/src/apps/webpeer/ui.html b/src/apps/webpeer/ui.html new file mode 100644 index 0000000..631df86 --- /dev/null +++ b/src/apps/webpeer/ui.html @@ -0,0 +1,16 @@ + + + + + + + + Peer-to-Peer Daemon on Browser + + + +
+ + + + \ No newline at end of file diff --git a/src/apps/webpeer/vite.config.ts b/src/apps/webpeer/vite.config.ts new file mode 100644 index 0000000..e7ded0b --- /dev/null +++ b/src/apps/webpeer/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import path from "node:path"; +// https://vite.dev/config/ +export default defineConfig({ + plugins: [svelte()], + resolve: { + alias: { + "@": path.resolve(__dirname, "../../"), + "@lib": path.resolve(__dirname, "../../lib/src"), + }, + }, + base: "./", + build: { + outDir: "dist", + emptyOutDir: true, + rollupOptions: { + input: { + index: "index.html", + // uitest: "uitest.html", + }, + }, + }, +}); diff --git a/test/shell/p2p-start.sh b/test/shell/p2p-start.sh index f875a61..b4218e0 100755 --- a/test/shell/p2p-start.sh +++ b/test/shell/p2p-start.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e script_dir=$(dirname "$0") -webpeer_dir=$script_dir/../../src/lib/apps/webpeer +webpeer_dir=$script_dir/../../src/apps/webpeer docker run -d --name relay-test -p 4000:8080 scsibug/nostr-rs-relay:latest npm run --prefix $webpeer_dir build diff --git a/tsconfig.json b/tsconfig.json index 15968e6..1ee9b7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,12 +24,5 @@ } }, "include": ["**/*.ts", "test/**/*.test.ts"], - "exclude": [ - "pouchdb-browser-webpack", - "utils", - "src/lib/apps", - "src/**/*.test.ts", - "**/_test/**", - "src/**/*.test.ts" - ] + "exclude": ["pouchdb-browser-webpack", "utils", "src/apps", "src/**/*.test.ts", "**/_test/**", "src/**/*.test.ts"] } diff --git a/vite.config.ts b/vite.config.ts index 6150f9a..85ea529 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ -import { defineConfig } from "vite"; +import { defineConfig, mergeConfig } from "vitest/config"; +import viteConfig from "./vitest.config.common"; -export default defineConfig({}); +export default mergeConfig(viteConfig, defineConfig({})); From cf1954b10e759a3f2a6a487b08b494dfa8256f61 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Thu, 29 Jan 2026 07:15:22 +0000 Subject: [PATCH 3/8] Bump for beta --- manifest-beta.json | 10 ---------- manifest.json | 2 +- updates.md | 39 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 13 deletions(-) delete mode 100644 manifest-beta.json diff --git a/manifest-beta.json b/manifest-beta.json deleted file mode 100644 index 26b0d7f..0000000 --- a/manifest-beta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "obsidian-livesync", - "name": "Self-hosted LiveSync", - "version": "0.25.24.beta3", - "minAppVersion": "0.9.12", - "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", - "author": "vorotamoroz", - "authorUrl": "https://github.com/vrtmrz", - "isDesktopOnly": false -} diff --git a/manifest.json b/manifest.json index 3ab7955..f8e1aec 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.25.41", + "version": "0.25.41-patched-1", "minAppVersion": "0.9.12", "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "author": "vorotamoroz", diff --git a/updates.md b/updates.md index bf640d9..7330484 100644 --- a/updates.md +++ b/updates.md @@ -3,6 +3,39 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. +## 0.25.41-patched-1 + +- 29th January, 2026 + +Yes, I have changed my mind. Let's release the beta version... + +We have set up the CI environment too. You know, naturally, the current CI is still in the foundational stage with mocked `harness` and checks quite the basic features; merely exists, for now, so I am eagerly awaiting your contributions. + +Schemes for beta releases: + +- Beta versions are denoted by appending `-patched-N` to the base version number. + - `The base version` mostly corresponds to the stable release version. + - e.g., v0.25.41-patched-1 is equivalent to v0.25.42-beta1. + - This notation is due to SemVer incompatibility of Obsidian's plugin system. + - Hence, this release is `0.25.41-patched-1`. +- Each beta version may include larger changes, but bug fixes will often not be included. + - I think that in most cases, bug fixes will cause the stable releases. + - They will not be released per branch or backported; they will simply be released. + - Bug fixes for previous versions will be applied to the latest beta version. + This means, if xx.yy.02-patched-1 exists and there is a defect in xx.yy.01, a fix is applied to xx.yy.02-patched-1 and yields xx.yy.02-patched-2. + If the fix is required immediately, it is released as xx.yy.02 (with xx.yy.01-patched-1). + - This procedure remains unchanged from the current one. +- At the very least, I am using the latest beta. + - However, I will not be using a beta continuously for a week after it has been released. It is probably closer to an RC in nature. + +In short, the situation remains unchanged for me, but it means you all become a little safer. Thank you for your understanding! + +### Refactored + +- `WebPeer` has been moved to the main repository from the sub repository `livesync-commonlib` for correct dependency management. +- Migrated from the outdated, unstable platform abstraction layer to services. + - A bit more services will be added in the future for better maintainability. + ## 0.25.41 24th January, 2026 @@ -18,16 +51,18 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid ### New feature - We can now set the maximum modified time for reflect events in the settings. (for #754) - - This setting can be configured from `Patches` -> `Remediation` in the settings dialogue. + - This setting can be configured from `Patches` -> `Remediation` in the settings dialogue. - Enabling this setting will restrict the propagation from the database to storage to only those changes made before the specified date and time. - This feature is primarily intended for recovery purposes. After placing `redflag.md` in an empty vault and importing the Self-hosted LiveSync configuration, please perform this configuration, and then fetch the local database from the remote. - This feature is useful when we want to prevent recent unwanted changes from being reflected in the local storage. ### Refactored + - Module to service refactoring has been started for better maintainability: - - UI module has been moved to UI service. + - UI module has been moved to UI service. ### Behaviour change + - Default chunk splitter version has been changed to `Rabin-Karp` for new installations. ## 0.25.40 From edcdfd97c41bca542a67e54fbfa003fbbd54f212 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 30 Jan 2026 09:23:54 +0000 Subject: [PATCH 4/8] Reduce dynamic function binding from APIService from --- src/lib | 2 +- .../essentialObsidian/ModuleObsidianAPI.ts | 36 +------------- .../essentialObsidian/ModuleObsidianMenu.ts | 18 ------- src/modules/services/ObsidianServices.ts | 48 +++++++++++++++++++ 4 files changed, 50 insertions(+), 54 deletions(-) diff --git a/src/lib b/src/lib index 7c275d5..a02102b 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 7c275d50ae3988903f6d518603450d24e979c1ff +Subproject commit a02102b13197826f1591448a6bf5ecdee8673d00 diff --git a/src/modules/essentialObsidian/ModuleObsidianAPI.ts b/src/modules/essentialObsidian/ModuleObsidianAPI.ts index 1ddc4fd..cdd085b 100644 --- a/src/modules/essentialObsidian/ModuleObsidianAPI.ts +++ b/src/modules/essentialObsidian/ModuleObsidianAPI.ts @@ -15,7 +15,6 @@ import { replicationFilter } from "@/lib/src/pouchdb/compress.ts"; import { disableEncryption } from "@/lib/src/pouchdb/encryption.ts"; import { enableEncryption } from "@/lib/src/pouchdb/encryption.ts"; import { setNoticeClass } from "../../lib/src/mock_and_interop/wrapper.ts"; -import { ObsHttpHandler } from "./APILib/ObsHttpHandler.ts"; import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.ts"; import { AuthorizationHeaderGenerator } from "../../lib/src/replication/httplib.ts"; import type { LiveSyncCore } from "../../main.ts"; @@ -27,10 +26,7 @@ async function fetchByAPI(request: RequestUrlParam, errorAsResult = false): Prom const ret = await requestUrl({ ...request, throw: !errorAsResult }); return ret; } - export class ModuleObsidianAPI extends AbstractObsidianModule { - _customHandler!: ObsHttpHandler; - _authHeader = new AuthorizationHeaderGenerator(); _previousErrors = new Set(); @@ -47,10 +43,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule { eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR); } last_successful_post = false; - _customFetchHandler(): ObsHttpHandler { - if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined); - return this._customHandler; - } + _getLastPostFailedBySize(): boolean { return !this.last_successful_post; } @@ -286,11 +279,6 @@ export class ModuleObsidianAPI extends AbstractObsidianModule { } } - _isMobile(): boolean { - //@ts-ignore : internal API - return this.app.isMobile; - } - _vaultName(): string { return this.app.vault.getName(); } @@ -308,38 +296,16 @@ export class ModuleObsidianAPI extends AbstractObsidianModule { return undefined; } - _anyGetAppId(): string { - return `${"appId" in this.app ? this.app.appId : ""}`; - } - private _reportUnresolvedMessages(): Promise<(string | Error)[]> { return Promise.resolve([...this._previousErrors]); } - private _getAppVersion(): string { - const navigatorString = globalThis.navigator?.userAgent ?? ""; - const match = navigatorString.match(/obsidian\/([0-9]+\.[0-9]+\.[0-9]+)/); - if (match && match.length >= 2) { - return match[1]; - } - return "0.0.0"; - } - - private _getPluginVersion(): string { - return this.plugin.manifest.version; - } - onBindFunction(core: LiveSyncCore, services: typeof core.services) { - services.API.getCustomFetchHandler.setHandler(this._customFetchHandler.bind(this)); services.API.isLastPostFailedDueToPayloadSize.setHandler(this._getLastPostFailedBySize.bind(this)); services.remote.connect.setHandler(this._connectRemoteCouchDB.bind(this)); - services.API.isMobile.setHandler(this._isMobile.bind(this)); services.vault.getVaultName.setHandler(this._getVaultName.bind(this)); services.vault.vaultName.setHandler(this._vaultName.bind(this)); services.vault.getActiveFilePath.setHandler(this._getActiveFilePath.bind(this)); - services.API.getAppID.setHandler(this._anyGetAppId.bind(this)); - services.API.getAppVersion.setHandler(this._getAppVersion.bind(this)); - services.API.getPluginVersion.setHandler(this._getPluginVersion.bind(this)); services.appLifecycle.getUnresolvedMessages.addHandler(this._reportUnresolvedMessages.bind(this)); } } diff --git a/src/modules/essentialObsidian/ModuleObsidianMenu.ts b/src/modules/essentialObsidian/ModuleObsidianMenu.ts index 0186504..736ae10 100644 --- a/src/modules/essentialObsidian/ModuleObsidianMenu.ts +++ b/src/modules/essentialObsidian/ModuleObsidianMenu.ts @@ -113,26 +113,8 @@ export class ModuleObsidianMenu extends AbstractObsidianModule { return Promise.resolve(true); } - private async _showView(viewType: string) { - const leaves = this.app.workspace.getLeavesOfType(viewType); - if (leaves.length == 0) { - await this.app.workspace.getLeaf(true).setViewState({ - type: viewType, - active: true, - }); - } else { - await leaves[0].setViewState({ - type: viewType, - active: true, - }); - } - if (leaves.length > 0) { - await this.app.workspace.revealLeaf(leaves[0]); - } - } onBindFunction(core: LiveSyncCore, services: typeof core.services): void { services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this)); services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this)); - services.API.showWindow.setHandler(this._showView.bind(this)); } } diff --git a/src/modules/services/ObsidianServices.ts b/src/modules/services/ObsidianServices.ts index 2f79c67..e4b05b7 100644 --- a/src/modules/services/ObsidianServices.ts +++ b/src/modules/services/ObsidianServices.ts @@ -18,11 +18,40 @@ import { Platform } from "@/deps"; import type { SimpleStore } from "@/lib/src/common/utils"; import type { IDatabaseService } from "@/lib/src/services/base/IService"; import { handlers } from "@/lib/src/services/lib/HandlerUtils"; +import { ObsHttpHandler } from "../essentialObsidian/APILib/ObsHttpHandler"; // All Services will be migrated to be based on Plain Services, not Injectable Services. // This is a migration step. export class ObsidianAPIService extends InjectableAPIService { + _customHandler: ObsHttpHandler | undefined; + getCustomFetchHandler(): ObsHttpHandler { + if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined); + return this._customHandler; + } + + async showWindow(viewType: string): Promise { + const leaves = this.app.workspace.getLeavesOfType(viewType); + if (leaves.length == 0) { + await this.app.workspace.getLeaf(true).setViewState({ + type: viewType, + active: true, + }); + } else { + await leaves[0].setViewState({ + type: viewType, + active: true, + }); + } + if (leaves.length > 0) { + await this.app.workspace.revealLeaf(leaves[0]); + } + } + + private get app() { + return this.context.app; + } + getPlatform(): string { if (Platform.isAndroidApp) { return "android-app"; @@ -44,6 +73,25 @@ export class ObsidianAPIService extends InjectableAPIService= 2) { + return match[1]; + } + return "0.0.0"; + } + + override getPluginVersion(): string { + return this.context.plugin.manifest.version; + } } export class ObsidianPathService extends InjectablePathService {} export class ObsidianDatabaseService extends InjectableDatabaseService { From 0e903c3520cdf20c0e87bfe09d1a6385adffc507 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 30 Jan 2026 09:25:12 +0000 Subject: [PATCH 5/8] Change: enabling PATH_TEST_INSTALL in production build --- esbuild.config.mjs | 10 +++------- package.json | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 18de237..fe62f52 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -21,14 +21,10 @@ const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + ""); const PATHS_TEST_INSTALL = process.env?.PATHS_TEST_INSTALL || ""; const PATH_TEST_INSTALL = PATHS_TEST_INSTALL.split(path.delimiter).map(p => p.trim()).filter(p => p.length); -if (!prod) { - if (PATH_TEST_INSTALL) { - console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`); - } else { - console.log("Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows)."); - } +if (PATH_TEST_INSTALL) { + console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`); } else { - console.log("Production build"); + console.log("Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows)."); } const moduleAliasPlugin = { diff --git a/package.json b/package.json index ac4c18a..027a2b0 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "predev": "npm run bakei18n", "dev": "node --env-file=.env esbuild.config.mjs", "prebuild": "npm run bakei18n", - "build": "node esbuild.config.mjs production", + "build": "node --env-file=.env esbuild.config.mjs production", "buildDev": "node esbuild.config.mjs dev", "lint": "eslint src", "svelte-check": "svelte-check --tsconfig ./tsconfig.json", From b1f518071cee0ca03f48d0bbe34ba91efdb9ac2b Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 30 Jan 2026 09:27:31 +0000 Subject: [PATCH 6/8] add note --- updates.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/updates.md b/updates.md index 7330484..5d5cf6c 100644 --- a/updates.md +++ b/updates.md @@ -3,9 +3,19 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. +## 0.25.41-patched-2 + +30th January, 2026 + +### Refactored + +- Now the service context is `protected` instead of `private` in `ServiceBase`. + - This change allows derived classes to access the context directly. +- Some dynamically bound services have been moved to services for better dependency management. + ## 0.25.41-patched-1 -- 29th January, 2026 +29th January, 2026 Yes, I have changed my mind. Let's release the beta version... From f464623bf6bc4e4234a974eb756e8a2e51e20600 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 30 Jan 2026 09:36:39 +0000 Subject: [PATCH 7/8] revert for release script --- package.json | 2 +- updates.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 027a2b0..ac4c18a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "predev": "npm run bakei18n", "dev": "node --env-file=.env esbuild.config.mjs", "prebuild": "npm run bakei18n", - "build": "node --env-file=.env esbuild.config.mjs production", + "build": "node esbuild.config.mjs production", "buildDev": "node esbuild.config.mjs dev", "lint": "eslint src", "svelte-check": "svelte-check --tsconfig ./tsconfig.json", diff --git a/updates.md b/updates.md index 5d5cf6c..4ff9d44 100644 --- a/updates.md +++ b/updates.md @@ -3,7 +3,9 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. -## 0.25.41-patched-2 +## 0.25.41-patched-3 + +(0.25.41-patched-2 was skipped) 30th January, 2026 From 6915b160a2cb6affe0c4011d9b90fb6c9f9086c2 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 2 Feb 2026 10:57:15 +0900 Subject: [PATCH 8/8] Bump to stable --- manifest.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- updates.md | 36 +++--------------------------------- 4 files changed, 7 insertions(+), 37 deletions(-) diff --git a/manifest.json b/manifest.json index f8e1aec..7f0726f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.25.41-patched-1", + "version": "0.25.42", "minAppVersion": "0.9.12", "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "author": "vorotamoroz", diff --git a/package-lock.json b/package-lock.json index 800945c..5f463eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.25.41", + "version": "0.25.42", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.25.41", + "version": "0.25.42", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", diff --git a/package.json b/package.json index ac4c18a..c31423b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.25.41", + "version": "0.25.42", "description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "main": "main.js", "type": "module", diff --git a/updates.md b/updates.md index 4ff9d44..d1024ba 100644 --- a/updates.md +++ b/updates.md @@ -3,47 +3,17 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. -## 0.25.41-patched-3 +## 0.25.42 -(0.25.41-patched-2 was skipped) +2nd, February, 2026 -30th January, 2026 +This release is identical to 0.25.41-patched-3, except for the version number. ### Refactored - Now the service context is `protected` instead of `private` in `ServiceBase`. - This change allows derived classes to access the context directly. - Some dynamically bound services have been moved to services for better dependency management. - -## 0.25.41-patched-1 - -29th January, 2026 - -Yes, I have changed my mind. Let's release the beta version... - -We have set up the CI environment too. You know, naturally, the current CI is still in the foundational stage with mocked `harness` and checks quite the basic features; merely exists, for now, so I am eagerly awaiting your contributions. - -Schemes for beta releases: - -- Beta versions are denoted by appending `-patched-N` to the base version number. - - `The base version` mostly corresponds to the stable release version. - - e.g., v0.25.41-patched-1 is equivalent to v0.25.42-beta1. - - This notation is due to SemVer incompatibility of Obsidian's plugin system. - - Hence, this release is `0.25.41-patched-1`. -- Each beta version may include larger changes, but bug fixes will often not be included. - - I think that in most cases, bug fixes will cause the stable releases. - - They will not be released per branch or backported; they will simply be released. - - Bug fixes for previous versions will be applied to the latest beta version. - This means, if xx.yy.02-patched-1 exists and there is a defect in xx.yy.01, a fix is applied to xx.yy.02-patched-1 and yields xx.yy.02-patched-2. - If the fix is required immediately, it is released as xx.yy.02 (with xx.yy.01-patched-1). - - This procedure remains unchanged from the current one. -- At the very least, I am using the latest beta. - - However, I will not be using a beta continuously for a week after it has been released. It is probably closer to an RC in nature. - -In short, the situation remains unchanged for me, but it means you all become a little safer. Thank you for your understanding! - -### Refactored - - `WebPeer` has been moved to the main repository from the sub repository `livesync-commonlib` for correct dependency management. - Migrated from the outdated, unstable platform abstraction layer to services. - A bit more services will be added in the future for better maintainability.