diff --git a/package-lock.json b/package-lock.json index 874dbfa7..e0517b7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", "@vrtmrz/livesync-commonlib": "0.1.19", - "@vrtmrz/obsidian-plugin-kit": "0.1.3", + "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", @@ -4834,9 +4834,9 @@ } }, "node_modules/@vrtmrz/obsidian-plugin-kit": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-plugin-kit/-/obsidian-plugin-kit-0.1.3.tgz", - "integrity": "sha512-6fsKdhFZtBv6FXlZHtSmpqwROohFzDmres6q08nr2xYGVeh2ooBGU3zJS94WN/tOjDT+wa/Vr3yE42wmI0pIZA==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-plugin-kit/-/obsidian-plugin-kit-0.1.4.tgz", + "integrity": "sha512-MxZgd7UOr8DXk0e+JsAXJmYj/8bfIoEliI4ruvp1Cu4U+mmVI/p63nZ5we3dDW+EaCgRAgF+ztuVDkcMUk1DwA==", "license": "MIT", "dependencies": { "@vrtmrz/ui-interactions": "0.1.2" diff --git a/package.json b/package.json index 7c1ee602..6ce3a4c0 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,7 @@ "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", "@vrtmrz/livesync-commonlib": "0.1.19", - "@vrtmrz/obsidian-plugin-kit": "0.1.3", + "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", diff --git a/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts b/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts index e09efe85..0237360c 100644 --- a/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts +++ b/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts @@ -4,7 +4,6 @@ vi.mock("@/deps.ts", () => ({ addIcon: vi.fn(), diff_match_patch: class DiffMatchPatch {}, normalizePath: vi.fn((path: string) => path), - Notice: class Notice {}, parseYaml: vi.fn(), Platform: {}, })); @@ -30,13 +29,10 @@ vi.mock("@/common/types.ts", () => ({ PERIODIC_PLUGIN_SWEEP: 60, })); vi.mock("@/common/utils.ts", () => ({ + cancelTask: vi.fn(), EVEN: Symbol("even"), - disposeMemoObject: vi.fn(), isCustomisationSyncMetadata: vi.fn(), isPluginMetadata: vi.fn(), - memoIfNotExist: vi.fn(), - memoObject: vi.fn(), - retrieveMemoObject: vi.fn(), scheduleTask: vi.fn(), })); vi.mock("@/common/PeriodicProcessor.ts", () => ({ @@ -55,6 +51,7 @@ vi.mock("@/common/obsidianCommunityPlugins.ts", () => ({ getObsidianCommunityPluginManager: vi.fn(), })); +import { cancelTask } from "@/common/utils.ts"; import { ConfigSync } from "./CmdConfigSync"; describe("ConfigSync commands", () => { @@ -93,4 +90,24 @@ describe("ConfigSync commands", () => { expect(command?.checkCallback?.(false)).toBe(true); expect(showPluginSyncModal).toHaveBeenCalledOnce(); }); + + it("cancels the pending configuration Notice before releasing its owned UI", () => { + const notices = { hide: vi.fn() }; + const periodicPluginSweepProcessor = { disable: vi.fn() }; + const configSync = Object.create(ConfigSync.prototype) as ConfigSync; + Object.assign(configSync, { + core: { + services: { + context: { notices }, + }, + }, + periodicPluginSweepProcessor, + }); + + configSync.onunload(); + + expect(cancelTask).toHaveBeenCalledWith("config-sync:updated-configuration"); + expect(notices.hide).toHaveBeenCalledWith("config-sync:updated-configuration"); + expect(periodicPluginSweepProcessor.disable).toHaveBeenCalledOnce(); + }); }); diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index d88e8bd8..4201385f 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -1,7 +1,6 @@ import { writable } from "svelte/store"; import type PouchDB from "pouchdb-core"; import { - Notice, type PluginManifest, parseYaml, normalizePath, @@ -53,21 +52,11 @@ import { import { serialized, shareRunningResult } from "octagonal-wheels/concurrency/lock"; import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts"; import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; -import { - EVEN, - disposeMemoObject, - isCustomisationSyncMetadata, - isPluginMetadata, - memoIfNotExist, - memoObject, - retrieveMemoObject, - scheduleTask, -} from "@/common/utils.ts"; +import { cancelTask, EVEN, isCustomisationSyncMetadata, isPluginMetadata, scheduleTask } from "@/common/utils.ts"; import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts"; import { JsonResolveModal } from "@/features/HiddenFileCommon/JsonResolveModal.ts"; import { QueueProcessor } from "octagonal-wheels/concurrency/processor"; import { pluginScanningCount } from "@vrtmrz/livesync-commonlib/compat/mock_and_interop/stores"; -import type ObsidianLiveSyncPlugin from "@/main.ts"; import { base64ToArrayBuffer, base64ToString } from "octagonal-wheels/binary/base64"; import { ConflictResolveModal } from "@/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; @@ -82,6 +71,7 @@ import { getObsidianCommunityPluginManager } from "@/common/obsidianCommunityPlu const d = "\u200b"; const d2 = "\n"; +const UPDATED_CONFIGURATION_NOTICE_KEY = "config-sync:updated-configuration"; function serialize(data: PluginDataEx): string { // For higher performance, create custom plug-in data strings. @@ -393,8 +383,8 @@ export type PluginDataEx = { }; export class ConfigSync extends LiveSyncCommands { - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) { - super(plugin, core); + constructor(core: LiveSyncCore) { + super(core); pluginScanningCount.onChanged((e) => { const total = e.value; pluginIsEnumerating.set(total != 0); @@ -428,7 +418,7 @@ export class ConfigSync extends LiveSyncCommands { if (this.pluginDialog) { this.pluginDialog.open(); } else { - this.pluginDialog = new PluginDialogModal(this.app, this.plugin); + this.pluginDialog = new PluginDialogModal(this.app, this.services.context.liveSyncPlugin); this.pluginDialog.open(); } } @@ -440,8 +430,10 @@ export class ConfigSync extends LiveSyncCommands { } } onunload() { + cancelTask(UPDATED_CONFIGURATION_NOTICE_KEY); this.hidePluginSyncModal(); this.periodicPluginSweepProcessor?.disable(); + this.services.context.notices.hide(UPDATED_CONFIGURATION_NOTICE_KEY); } addRibbonIcon = this.services.API.addRibbonIcon.bind(this.services.API); onload() { @@ -1196,22 +1188,9 @@ export class ConfigSync extends LiveSyncCommands { }); }); - const updatedPluginKey = "popupUpdated-plugins"; - scheduleTask(updatedPluginKey, 1000, async () => { - const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0)); - //@ts-ignore -- retained for compatibility with Obsidian versions before Notice.messageEl. - const isShown = popup?.noticeEl?.isShown(); - if (!isShown) { - memoObject(updatedPluginKey, new Notice(fragment, 0)); - } - scheduleTask(updatedPluginKey + "-close", 20000, () => { - const popup = retrieveMemoObject(updatedPluginKey); - if (!popup) return; - //@ts-ignore -- retained for compatibility with Obsidian versions before Notice.messageEl. - if (popup?.noticeEl?.isShown()) { - popup.hide(); - } - disposeMemoObject(updatedPluginKey); + scheduleTask(UPDATED_CONFIGURATION_NOTICE_KEY, 1000, () => { + this.services.context.notices.show(UPDATED_CONFIGURATION_NOTICE_KEY, fragment, { + durationMs: 20_000, }); }); } @@ -1716,8 +1695,6 @@ export class ConfigSync extends LiveSyncCommands { } async configureHiddenFileSync(mode: OptionalSyncFeatureMode) { if (mode == "DISABLE") { - // this.plugin.settings.usePluginSync = false; - // await this.plugin.saveSettings(); await this.core.services.setting.applyPartial( { usePluginSync: false, @@ -1758,9 +1735,6 @@ export class ConfigSync extends LiveSyncCommands { } this.services.setting.setDeviceAndVaultName(name); } - // this.core.settings.usePluginSync = true; - // this.core.settings.useAdvancedMode = true; - // await this.core.saveSettings(); await this.core.services.setting.applyPartial( { usePluginSync: true, diff --git a/src/features/LiveSyncCommands.ts b/src/features/LiveSyncCommands.ts index ed7a13f6..3f1ec783 100644 --- a/src/features/LiveSyncCommands.ts +++ b/src/features/LiveSyncCommands.ts @@ -8,7 +8,6 @@ import { type FilePathWithPrefix, type LOG_LEVEL, } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import type ObsidianLiveSyncPlugin from "@/main.ts"; import { MARK_DONE } from "@/modules/features/ModuleLog.ts"; import type { LiveSyncCore } from "@/main.ts"; // import { __$checkInstanceBinding } from "@vrtmrz/livesync-commonlib/compat/dev/checks"; @@ -16,13 +15,9 @@ import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/ser let noticeIndex = 0; export abstract class LiveSyncCommands { - /** - * @deprecated This class is deprecated. Please use core - */ - plugin: ObsidianLiveSyncPlugin; core: LiveSyncCore; get app() { - return this.plugin.app; + return this.services.context.app; } get settings() { return this.core.settings; @@ -34,9 +29,6 @@ export abstract class LiveSyncCommands { return this.core.services; } - // id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix { - // return this.plugin.$$id2path(id, entry, stripPrefix); - // } async path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise { return await this.services.path.path2id(filename, prefix); } @@ -45,8 +37,7 @@ export abstract class LiveSyncCommands { return this.services.path.getPath(entry); } - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) { - this.plugin = plugin; + constructor(core: LiveSyncCore) { this.core = core; this.onBindFunction(this.core, this.core.services); this._log = createInstanceLogFunction(this.constructor.name, this.services.API); diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts index 932436c2..75b00dcd 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts @@ -35,7 +35,7 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands { } onload(): void | Promise { // NO OP. - this.plugin.addCommand({ + this.services.API.addCommand({ id: "analyse-database", name: "Analyse Database Usage (advanced)", icon: "database-search", @@ -47,7 +47,7 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands { return true; }, }); - this.plugin.addCommand({ + this.services.API.addCommand({ id: "gc-v3", name: "Garbage Collection V3 (advanced, beta)", icon: "trash-2", diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts index 6e64bb02..0d337484 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.unit.spec.ts @@ -18,10 +18,13 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/common/utils", async (importOriginal) }); vi.mock("@/features/LiveSyncCommands", () => ({ LiveSyncCommands: class LiveSyncCommands { - core!: { settings: unknown }; + core!: { settings: unknown; services: unknown }; get settings() { return this.core.settings; } + get services() { + return this.core.services; + } }, })); vi.mock("@/common/events", () => ({ @@ -76,11 +79,13 @@ describe("LocalDatabaseMaintenance prerequisites", () => { }; const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance; Object.assign(maintenance, { - plugin: { - addCommand: vi.fn((command) => commands.push(command)), - }, core: { settings, + services: { + API: { + addCommand: vi.fn((command) => commands.push(command)), + }, + }, }, _isDatabaseReady: vi.fn(() => true), }); diff --git a/src/main.ts b/src/main.ts index 8f5f2d6c..cb68bd79 100644 --- a/src/main.ts +++ b/src/main.ts @@ -170,11 +170,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin { return extraModules; }, (core) => { - const addOns = [ - new ConfigSync(this, core), - new HiddenFileSync(this, core), - new LocalDatabaseMaintenance(this, core), - ]; + const addOns = [new ConfigSync(core), new HiddenFileSync(core), new LocalDatabaseMaintenance(core)]; return addOns; }, (core) => { diff --git a/src/modules/AbstractObsidianModule.ts b/src/modules/AbstractObsidianModule.ts index 0dcfb13f..3de7a5fc 100644 --- a/src/modules/AbstractObsidianModule.ts +++ b/src/modules/AbstractObsidianModule.ts @@ -2,7 +2,11 @@ import type { LiveSyncCore } from "@/main"; import type ObsidianLiveSyncPlugin from "@/main"; import { AbstractModule } from "./AbstractModule.ts"; -export abstract class AbstractObsidianModule extends AbstractModule { +export abstract class AbstractObsidianModule extends AbstractModule { + override get services() { + return this.core.services; + } + get app() { return this.plugin.app; } diff --git a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts index 4f911e7b..bc7082ea 100644 --- a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts +++ b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts @@ -4,7 +4,8 @@ // Original Implementation is here: https://github.com/remotely-save/remotely-save/blob/28b99557a864ef59c19d2ad96101196e401718f0/src/remoteForS3.ts import { FetchHttpHandler, type FetchHttpHandlerOptions } from "@smithy/fetch-http-handler"; -import { HttpRequest, HttpResponse, type HttpHandlerOptions } from "@smithy/protocol-http"; +import { HttpRequest, HttpResponse } from "@smithy/protocol-http"; +import type { HttpHandlerOptions } from "@smithy/types"; import { buildQueryString } from "@smithy/querystring-builder"; import { requestUrl, type RequestUrlParam } from "@/deps.ts"; import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; diff --git a/src/modules/features/ModuleLog.ts b/src/modules/features/ModuleLog.ts index b44334ba..485014b1 100644 --- a/src/modules/features/ModuleLog.ts +++ b/src/modules/features/ModuleLog.ts @@ -7,7 +7,7 @@ import { type DatabaseConnectingStatus, type LOG_LEVEL, } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { cancelTask, scheduleTask } from "octagonal-wheels/concurrency/task"; +import { scheduleTask } from "octagonal-wheels/concurrency/task"; import { fireAndForget, isDirty, throttle } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { collectingChunks, @@ -119,7 +119,7 @@ export class ModuleLog extends AbstractObsidianModule { statusBarLabels!: ReactiveValue<{ message: string; status: string }>; statusLog = reactiveSource(""); activeFileStatus = reactiveSource(""); - notifies: { [key: string]: { notice: Notice; count: number } } = {}; + notifies: { [key: string]: { count: number } } = {}; p2pLogCollector = new P2PLogCollector(this.services.context.events); observeForLogs() { @@ -407,6 +407,10 @@ export class ModuleLog extends AbstractObsidianModule { } private _allStartOnUnload(): Promise { + for (const key of Object.keys(this.notifies)) { + this.services.context.notices.hide(`log:${key}`); + } + this.notifies = {}; if (this.statusDiv) { this.statusDiv.remove(); } @@ -559,35 +563,26 @@ ${stringifyYaml(info)} if (level >= LOG_LEVEL_NOTICE) { if (!key) key = messageContent; if (key in this.notifies) { - // @ts-ignore - const isShown = this.notifies[key].notice.noticeEl?.isShown(); - if (!isShown) { - this.notifies[key].notice = new Notice(messageContent, 0); - } - cancelTask(`notify-${key}`); if (key == messageContent) { this.notifies[key].count++; - this.notifies[key].notice.setMessage(`(${this.notifies[key].count}):${messageContent}`); - } else { - this.notifies[key].notice.setMessage(`${messageContent}`); } } else { - const notify = new Notice(messageContent, 0); this.notifies[key] = { count: 0, - notice: notify, }; } const timeout = 5000; - if (!key.startsWith("keepalive-") || messageContent.indexOf(MARK_DONE) !== -1) { + const shouldExpire = !key.startsWith("keepalive-") || messageContent.indexOf(MARK_DONE) !== -1; + const noticeMessage = + key == messageContent && this.notifies[key].count > 0 + ? `(${this.notifies[key].count}):${messageContent}` + : messageContent; + this.services.context.notices.show(`log:${key}`, noticeMessage, { + durationMs: shouldExpire ? timeout : false, + }); + if (shouldExpire) { scheduleTask(`notify-${key}`, timeout, () => { - const notify = this.notifies[key].notice; delete this.notifies[key]; - try { - notify.hide(); - } catch { - // NO OP - } }); } } diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index 86ec7a6e..d25d9e8d 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -3,8 +3,7 @@ import { type ObsidianLiveSyncSettings, type RemoteDBSettings, LOG_LEVEL_NOTICE, - FLAGMD_REDFLAG2_HR, - FLAGMD_REDFLAG3_HR, + FlagFilesHumanReadable, REMOTE_COUCHDB, REMOTE_MINIO, type ConfigLevel, @@ -874,12 +873,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { await this.saveAllDirtySettings(); await Promise.resolve(this.applyAllSettings()); if (result == OPTION_FETCH) { - await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, ""); + await this.core.storageAccess.writeFileAuto(FlagFilesHumanReadable.FETCH_ALL, ""); this.services.appLifecycle.scheduleRestart(); this.closeSetting(); // await rebuildDB("localOnly"); } else if (result == OPTION_REBUILD_BOTH) { - await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, ""); + await this.core.storageAccess.writeFileAuto(FlagFilesHumanReadable.REBUILD_ALL, ""); this.services.appLifecycle.scheduleRestart(); this.closeSetting(); } else if (result == OPTION_ONLY_SETTING) { diff --git a/src/modules/features/SettingDialogue/PaneMaintenance.ts b/src/modules/features/SettingDialogue/PaneMaintenance.ts index 828cd286..36d9ccaa 100644 --- a/src/modules/features/SettingDialogue/PaneMaintenance.ts +++ b/src/modules/features/SettingDialogue/PaneMaintenance.ts @@ -1,6 +1,6 @@ import { EVENT_REQUEST_PERFORM_GC_V3, eventHub } from "@/common/events.ts"; import { LOG_LEVEL_NOTICE, Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger"; -import { FlagFilesHumanReadable, FLAGMD_REDFLAG } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { FlagFilesHumanReadable, FlagFilesOriginal } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; @@ -90,7 +90,7 @@ export function paneMaintenance( .setButtonText("Flag and restart") .setDisabled(false) .onClick(async () => { - await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG, ""); + await this.core.storageAccess.writeFileAuto(FlagFilesOriginal.SUSPEND_ALL, ""); this.services.appLifecycle.performRestart(); }) ); diff --git a/src/modules/services/ObsidianConfirm.ts b/src/modules/services/ObsidianConfirm.ts index f9197e02..a2e3c493 100644 --- a/src/modules/services/ObsidianConfirm.ts +++ b/src/modules/services/ObsidianConfirm.ts @@ -1,5 +1,5 @@ -import { type App, type Plugin, Notice } from "@/deps"; -import { scheduleTask, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject } from "@/common/utils"; +import { type App, type Plugin } from "@/deps"; +import { scheduleTask } from "@/common/utils"; import { EVENT_PLUGIN_UNLOADED } from "@/common/events"; import { $msg } from "@/common/translation"; import type { Confirm, ConfirmActionLayout } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm"; @@ -180,30 +180,20 @@ export class ObsidianConfirm { + scheduleTask(popupKey, 1000, () => { if (this.dialogueController.signal.aborted) { this.popupKeys.delete(popupKey); return; } - 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", durationMs, () => this.closePopup(popupKey)); + this._context.notices.show(popupKey, fragment, { durationMs }); + scheduleTask(`${popupKey}-forget`, durationMs, () => { + this.popupKeys.delete(popupKey); + }); }); } private closePopup(popupKey: string) { - const popup = retrieveMemoObject(popupKey); - if (!popup) { - this.popupKeys.delete(popupKey); - return; - } - if (popup.noticeEl?.isShown()) { - popup.hide(); - } - disposeMemoObject(popupKey); + this._context.notices.hide(popupKey); this.popupKeys.delete(popupKey); } diff --git a/src/modules/services/ObsidianConfirm.unit.spec.ts b/src/modules/services/ObsidianConfirm.unit.spec.ts index afb7e3a0..18fe29b0 100644 --- a/src/modules/services/ObsidianConfirm.unit.spec.ts +++ b/src/modules/services/ObsidianConfirm.unit.spec.ts @@ -32,7 +32,6 @@ vi.mock("@/deps", () => ({ })); import { EVENT_PLUGIN_UNLOADED } from "@/common/events"; -import { memoObject, retrieveMemoObject } from "@/common/utils"; import { createLiveSyncEventHub } from "@vrtmrz/livesync-commonlib/context"; import { ObsidianConfirm } from "./ObsidianConfirm"; import type { ObsidianServiceContext } from "./ObsidianServiceContext"; @@ -41,8 +40,12 @@ function createConfirm() { const app = { id: "app" }; const plugin = { app }; const events = createLiveSyncEventHub(); - const context = { app, plugin, events } as unknown as ObsidianServiceContext; - return { confirm: new ObsidianConfirm(context), events, app, plugin }; + const notices = { + show: vi.fn(), + hide: vi.fn(), + }; + const context = { app, plugin, events, notices } as unknown as ObsidianServiceContext; + return { confirm: new ObsidianConfirm(context), events, app, plugin, notices }; } beforeEach(() => { @@ -259,19 +262,46 @@ describe("ObsidianConfirm Fancy Kit adapter", () => { expect(observedSignal?.aborted).toBe(true); }); - it("closes an active Notice when the plug-in unload event is emitted", () => { - const { confirm, events } = createConfirm(); + it("routes popup display and expiry through the context-owned keyed Notice manager", async () => { + vi.useFakeTimers(); const popupKey = "popup-remote-size-exceeded"; - const popup = { - hide: vi.fn(), - noticeEl: { isShown: vi.fn(() => true) }, + const fragment = {} as DocumentFragment; + const anchor = { addEventListener: vi.fn() } as unknown as HTMLAnchorElement; + const span = { + appendText: vi.fn(), + appendChild: vi.fn(), + createEl: vi.fn((_tag, _options, callback: (element: HTMLAnchorElement) => void) => { + callback(anchor); + return anchor; + }), }; - memoObject(popupKey, popup); + vi.stubGlobal("createFragment", (callback: (document: unknown) => void) => { + callback({ + createSpan: (_options: unknown, build: (element: typeof span) => void) => build(span), + }); + return fragment; + }); + const { confirm, notices } = createConfirm(); + + try { + confirm.askInPopup("remote-size-exceeded", "Review {HERE} details", vi.fn(), 20_000); + await vi.advanceTimersByTimeAsync(1_000); + + expect(notices.show).toHaveBeenCalledWith(popupKey, fragment, { durationMs: 20_000 }); + } finally { + await vi.runAllTimersAsync(); + vi.useRealTimers(); + vi.unstubAllGlobals(); + } + }); + + it("closes an owned keyed Notice when the plug-in unload event is emitted", () => { + const { confirm, events, notices } = createConfirm(); + const popupKey = "popup-remote-size-exceeded"; (confirm as unknown as { popupKeys: Set }).popupKeys.add(popupKey); events.emitEvent(EVENT_PLUGIN_UNLOADED); - expect(popup.hide).toHaveBeenCalledOnce(); - expect(retrieveMemoObject(popupKey)).toBe(false); + expect(notices.hide).toHaveBeenCalledWith(popupKey); }); }); diff --git a/src/modules/services/ObsidianServiceContext.ts b/src/modules/services/ObsidianServiceContext.ts index 694b336e..f31fbea7 100644 --- a/src/modules/services/ObsidianServiceContext.ts +++ b/src/modules/services/ObsidianServiceContext.ts @@ -4,19 +4,28 @@ import { ServiceContext } from "@vrtmrz/livesync-commonlib/context"; import { eventHub } from "@/common/events"; import { translateLiveSyncMessage } from "@/common/translation"; import type { ObsidianNoticeGroups } from "./ObsidianNoticeGroups"; +import type { KeyedNoticeManager } from "@vrtmrz/obsidian-plugin-kit/notice"; /** Host capabilities owned by one Self-hosted LiveSync plug-in instance. */ export class ObsidianServiceContext extends ServiceContext { app: App; plugin: Plugin; liveSyncPlugin: ObsidianLiveSyncPlugin; + readonly notices: KeyedNoticeManager; readonly noticeGroups: ObsidianNoticeGroups; - constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin, noticeGroups: ObsidianNoticeGroups) { + constructor( + app: App, + plugin: Plugin, + liveSyncPlugin: ObsidianLiveSyncPlugin, + notices: KeyedNoticeManager, + noticeGroups: ObsidianNoticeGroups + ) { super({ events: eventHub, translate: translateLiveSyncMessage }); this.app = app; this.plugin = plugin; this.liveSyncPlugin = liveSyncPlugin; + this.notices = notices; this.noticeGroups = noticeGroups; } } diff --git a/src/modules/services/ObsidianServiceContext.unit.spec.ts b/src/modules/services/ObsidianServiceContext.unit.spec.ts index d95f9306..32d74a57 100644 --- a/src/modules/services/ObsidianServiceContext.unit.spec.ts +++ b/src/modules/services/ObsidianServiceContext.unit.spec.ts @@ -13,8 +13,9 @@ describe("ObsidianServiceContext contract", () => { const app = {} as Parameters[0]; const plugin = {} as Parameters[1]; const liveSyncPlugin = {} as Parameters[2]; - const noticeGroups = {} as Parameters[3]; - const context = new ObsidianServiceContext(app, plugin, liveSyncPlugin, noticeGroups); + const notices = {} as Parameters[3]; + const noticeGroups = {} as Parameters[4]; + const context = new ObsidianServiceContext(app, plugin, liveSyncPlugin, notices, noticeGroups); expect(observeServiceContext(context, TRANSLATION_KEY)).toEqual({ translation: translateLiveSyncMessage(TRANSLATION_KEY), @@ -24,6 +25,7 @@ describe("ObsidianServiceContext contract", () => { expect(context.app).toBe(app); expect(context.plugin).toBe(plugin); expect(context.liveSyncPlugin).toBe(liveSyncPlugin); + expect(context.notices).toBe(notices); expect(context.noticeGroups).toBe(noticeGroups); }); }); diff --git a/src/modules/services/ObsidianServiceHub.ts b/src/modules/services/ObsidianServiceHub.ts index a5d25801..3613ea27 100644 --- a/src/modules/services/ObsidianServiceHub.ts +++ b/src/modules/services/ObsidianServiceHub.ts @@ -26,14 +26,16 @@ import { createScreenWakeLockManager } from "octagonal-wheels/browser/wakeLock"; import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser"; import { OpenKeyValueDatabase } from "@/common/KeyValueDB"; import { ObsidianNoticeGroupManager } from "./ObsidianNoticeGroups"; +import { KeyedNoticeManager } from "@vrtmrz/obsidian-plugin-kit/notice"; import { setLang } from "@/common/translation"; // InjectableServiceHub export class ObsidianServiceHub extends InjectableServiceHub { constructor(plugin: ObsidianLiveSyncPlugin) { + const notices = new KeyedNoticeManager(); const noticeGroups = new ObsidianNoticeGroupManager(); - const context = new ObsidianServiceContext(plugin.app, plugin, plugin, noticeGroups); + const context = new ObsidianServiceContext(plugin.app, plugin, plugin, notices, noticeGroups); const API = new ObsidianAPIService(context); const conflict = new ObsidianConflictService(context); @@ -66,6 +68,7 @@ export class ObsidianServiceHub extends InjectableServiceHub { await screenWakeLock.dispose(); + notices.dispose(); noticeGroups.dispose(); return true; }); diff --git a/src/serviceFeatures/fileDatabaseInfo.ts b/src/serviceFeatures/fileDatabaseInfo.ts index 40fc01e3..300ab4e3 100644 --- a/src/serviceFeatures/fileDatabaseInfo.ts +++ b/src/serviceFeatures/fileDatabaseInfo.ts @@ -1,4 +1,5 @@ import { $msg } from "@/common/translation"; +import type PouchDB from "pouchdb-core"; import type { FilePath, FilePathWithPrefix,