mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-22 02:27:07 +00:00
Align host integrations with current API recommendations
This commit is contained in:
@@ -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<LiveSyncCore> {
|
||||
override get services() {
|
||||
return this.core.services;
|
||||
}
|
||||
|
||||
get app() {
|
||||
return this.plugin.app;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<boolean> {
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user