Align host integrations with current API recommendations

This commit is contained in:
vorotamoroz
2026-08-24 16:39:32 +00:00
parent 47759f6205
commit dd11a753d5
19 changed files with 146 additions and 129 deletions
+8 -18
View File
@@ -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<T extends ObsidianServiceContext = ObsidianServiceC
a.appendText(afterText);
});
});
scheduleTask(popupKey, 1000, async () => {
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<Notice>(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);
}
@@ -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<string> }).popupKeys.add(popupKey);
events.emitEvent(EVENT_PLUGIN_UNLOADED);
expect(popup.hide).toHaveBeenCalledOnce();
expect(retrieveMemoObject(popupKey)).toBe(false);
expect(notices.hide).toHaveBeenCalledWith(popupKey);
});
});
+10 -1
View File
@@ -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;
}
}
@@ -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);
});
});
+4 -1
View File
@@ -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<ObsidianServiceContext> {
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<ObsidianServiceCont
const screenWakeLock = createScreenWakeLockManager();
appLifecycle.onUnload.addHandler(async () => {
await screenWakeLock.dispose();
notices.dispose();
noticeGroups.dispose();
return true;
});