Refactor plugin setup and avoid dangling resources

This commit is contained in:
Andras Schmelczer 2025-11-22 12:38:34 +00:00
parent a1a4610109
commit fbf03c41e0
3 changed files with 139 additions and 113 deletions

View file

@ -5,7 +5,7 @@ import type {
TAbstractFile, TAbstractFile,
WorkspaceLeaf WorkspaceLeaf
} from "obsidian"; } from "obsidian";
import { Platform, Plugin, TFile } from "obsidian"; import { Notice, Platform, Plugin, TFile } from "obsidian";
import "../manifest.json"; import "../manifest.json";
import { HistoryView } from "./views/history/history-view"; import { HistoryView } from "./views/history/history-view";
import { StatusBar } from "./views/status-bar/status-bar"; import { StatusBar } from "./views/status-bar/status-bar";
@ -30,124 +30,46 @@ import { LocalCursorUpdateListener } from "./views/cursors/local-cursor-update-l
import { renderCursorsInFileExplorer } from "./views/cursors/file-explorer"; import { renderCursorsInFileExplorer } from "./views/cursors/file-explorer";
const MIN_WAIT_BETWEEN_UPDATES_IN_MS = 250; const MIN_WAIT_BETWEEN_UPDATES_IN_MS = 250;
const IS_DEBUG_BUILD = process.env.NODE_ENV === "development";
export default class VaultLinkPlugin extends Plugin { export default class VaultLinkPlugin extends Plugin {
private readonly disposables: (() => unknown)[] = [];
private settingsTab: SyncSettingsTab | undefined;
private client!: SyncClient;
private readonly rateLimitedUpdatesPerFile = new Map< private readonly rateLimitedUpdatesPerFile = new Map<
string, string,
() => Promise<unknown> () => Promise<unknown>
>(); >();
private syncClient: SyncClient | undefined;
private settingsTab: SyncSettingsTab | undefined;
public async onload(): Promise<void> { public async onload(): Promise<void> {
DEFAULT_SETTINGS.ignorePatterns.push(
".obsidian/**",
".git/**",
".trash/**"
);
const isDebugBuild = process.env.NODE_ENV === "development";
const debugOptions = isDebugBuild
? {
fetch: debugging.slowFetchFactory(1),
webSocket: debugging.slowWebSocketFactory(1, new Logger())
}
: {};
this.client = await SyncClient.create({
fs: new ObsidianFileSystemOperations(
this.app.vault,
this.app.workspace
),
persistence: {
load: this.loadData.bind(this),
save: this.saveData.bind(this)
},
nativeLineEndings: Platform.isWin ? "\r\n" : "\n",
...debugOptions
});
if (isDebugBuild) {
debugging.logToConsole(this.client);
}
const statusDescription = new StatusDescription(this.client);
this.settingsTab = new SyncSettingsTab({
app: this.app,
plugin: this,
syncClient: this.client,
statusDescription
});
this.addSettingTab(this.settingsTab);
new StatusBar(this, this.client);
this.registerView(
HistoryView.TYPE,
(leaf) => new HistoryView(this.client, leaf)
);
this.registerView(
LogsView.TYPE,
(leaf) => new LogsView(this.client, leaf)
);
this.registerEditorExtension([remoteCursorsTheme, remoteCursorsPlugin]);
this.client.addRemoteCursorsUpdateListener((cursors) => {
RemoteCursorsPluginValue.setCursors(cursors, this.app);
renderCursorsInFileExplorer(cursors, this.app);
});
const cursorListener = new LocalCursorUpdateListener(
this.client,
this.app.workspace
);
this.disposables.push(() => {
cursorListener.dispose();
});
this.app.workspace.updateOptions();
this.addRibbonIcon(
HistoryView.ICON,
"Open VaultLink events",
async (_: MouseEvent) => this.activateView(HistoryView.TYPE)
);
this.addRibbonIcon(
LogsView.ICON,
"Open VaultLink logs",
async (_: MouseEvent) => this.activateView(LogsView.TYPE)
);
this.app.workspace.onLayoutReady(async () => { this.app.workspace.onLayoutReady(async () => {
this.registerEditorEvents(); const client = await this.createSyncClient();
await this.client.start();
const editorStatusDisplayManager = new EditorStatusDisplayManager( this.registerObsidianExtensions(client);
this,
this.app.workspace, this.registerEditorEvents(client);
this.client
); this.register(() => client.destroy());
this.disposables.push(() => { await client.start();
editorStatusDisplayManager.stop();
});
}); });
} }
public onunload(): void { public onUserEnable(): void {
this.client.waitAndStop().catch((err: unknown) => { new Notice(
this.client.logger.error( "VaultLink has been enabled, check out the docs for tips on getting started!"
`Error while stopping the sync client: ${err}` );
this.activateView(LogsView.TYPE);
this.activateView(HistoryView.TYPE);
this.openSettings();
}
public onExternalSettingsChange(): void {
new Notice("VaultLink settings have changed externally, applying...");
this.syncClient?.reloadSettings().catch((err: unknown) => {
throw new Error(
`Error while reloading settings after external change: ${err}`
); );
}); });
this.disposables.forEach((disposable) => {
disposable();
});
} }
public openSettings(): void { public openSettings(): void {
@ -180,7 +102,102 @@ export default class VaultLinkPlugin extends Plugin {
} }
} }
private registerEditorEvents(): void { private async createSyncClient(): Promise<SyncClient> {
DEFAULT_SETTINGS.ignorePatterns.push(
".obsidian/**",
".git/**",
".trash/**"
);
const client = await SyncClient.create({
fs: new ObsidianFileSystemOperations(
this.app.vault,
this.app.workspace
),
persistence: {
load: this.loadData.bind(this),
save: this.saveData.bind(this)
},
nativeLineEndings: Platform.isWin ? "\r\n" : "\n",
...(IS_DEBUG_BUILD
? {
fetch: debugging.slowFetchFactory(1),
webSocket: debugging.slowWebSocketFactory(
1,
new Logger()
)
}
: {})
});
if (IS_DEBUG_BUILD) {
debugging.logToConsole(client);
}
return client;
}
private registerObsidianExtensions(client: SyncClient): void {
const statusDescription = new StatusDescription(client);
this.settingsTab = new SyncSettingsTab({
app: this.app,
plugin: this,
syncClient: client,
statusDescription
});
this.addSettingTab(this.settingsTab);
new StatusBar(this, client);
this.registerView(HistoryView.TYPE, (leaf) => {
const view = new HistoryView(client, leaf);
this.register(() => view.onClose());
return view;
});
this.registerView(LogsView.TYPE, (leaf) => new LogsView(client, leaf));
this.registerEditorExtension([remoteCursorsTheme, remoteCursorsPlugin]);
client.addRemoteCursorsUpdateListener((cursors) => {
RemoteCursorsPluginValue.setCursors(cursors, this.app);
renderCursorsInFileExplorer(cursors, this.app);
});
const cursorListener = new LocalCursorUpdateListener(
client,
this.app.workspace
);
this.register(() => cursorListener.dispose);
this.app.workspace.updateOptions();
this.addRibbonIcons();
const editorStatusDisplayManager = new EditorStatusDisplayManager(
this,
this.app.workspace,
client
);
this.register(() => editorStatusDisplayManager.dispose());
}
private addRibbonIcons(): void {
this.addRibbonIcon(
HistoryView.ICON,
"Open VaultLink events",
async (_: MouseEvent) => this.activateView(HistoryView.TYPE)
);
this.addRibbonIcon(
LogsView.ICON,
"Open VaultLink logs",
async (_: MouseEvent) => this.activateView(LogsView.TYPE)
);
}
private registerEditorEvents(client: SyncClient): void {
[ [
this.app.workspace.on( this.app.workspace.on(
"editor-change", "editor-change",
@ -190,28 +207,28 @@ export default class VaultLinkPlugin extends Plugin {
) => { ) => {
const { file } = info; const { file } = info;
if (file) { if (file) {
await this.rateLimitedUpdate(file.path); await this.rateLimitedUpdate(file.path, client);
} }
} }
), ),
this.app.vault.on("create", async (file: TAbstractFile) => { this.app.vault.on("create", async (file: TAbstractFile) => {
if (file instanceof TFile) { if (file instanceof TFile) {
await this.client.syncLocallyCreatedFile(file.path); await client.syncLocallyCreatedFile(file.path);
} }
}), }),
this.app.vault.on("modify", async (file: TAbstractFile) => { this.app.vault.on("modify", async (file: TAbstractFile) => {
if (file instanceof TFile) { if (file instanceof TFile) {
await this.rateLimitedUpdate(file.path); await this.rateLimitedUpdate(file.path, client);
} }
}), }),
this.app.vault.on("delete", async (file: TAbstractFile) => { this.app.vault.on("delete", async (file: TAbstractFile) => {
await this.client.syncLocallyDeletedFile(file.path); await client.syncLocallyDeletedFile(file.path);
}), }),
this.app.vault.on( this.app.vault.on(
"rename", "rename",
async (file: TAbstractFile, oldPath: string) => { async (file: TAbstractFile, oldPath: string) => {
if (file instanceof TFile) { if (file instanceof TFile) {
await this.client.syncLocallyUpdatedFile({ await client.syncLocallyUpdatedFile({
oldPath, oldPath,
relativePath: file.path relativePath: file.path
}); });
@ -223,13 +240,16 @@ export default class VaultLinkPlugin extends Plugin {
}); });
} }
private async rateLimitedUpdate(path: string): Promise<void> { private async rateLimitedUpdate(
path: string,
client: SyncClient
): Promise<void> {
if (!this.rateLimitedUpdatesPerFile.has(path)) { if (!this.rateLimitedUpdatesPerFile.has(path)) {
this.rateLimitedUpdatesPerFile.set( this.rateLimitedUpdatesPerFile.set(
path, path,
rateLimit( rateLimit(
async () => async () =>
this.client.syncLocallyUpdatedFile({ client.syncLocallyUpdatedFile({
relativePath: path relativePath: path
}), }),
MIN_WAIT_BETWEEN_UPDATES_IN_MS MIN_WAIT_BETWEEN_UPDATES_IN_MS

View file

@ -22,7 +22,7 @@ export class EditorStatusDisplayManager {
}, EditorStatusDisplayManager.UPDATE_INTERVAL_IN_MS); }, EditorStatusDisplayManager.UPDATE_INTERVAL_IN_MS);
} }
public stop(): void { public dispose(): void {
clearInterval(this.intervalId); clearInterval(this.intervalId);
} }

View file

@ -108,6 +108,7 @@ export class HistoryView extends ItemView {
this.historyContainer = container.createDiv({ cls: "logs-container" }); this.historyContainer = container.createDiv({ cls: "logs-container" });
await this.updateView(); await this.updateView();
this.clearTimer();
this.timer = setInterval( this.timer = setInterval(
() => () =>
void this.updateView().catch((error: unknown) => { void this.updateView().catch((error: unknown) => {
@ -120,8 +121,13 @@ export class HistoryView extends ItemView {
} }
public async onClose(): Promise<void> { public async onClose(): Promise<void> {
this.clearTimer();
}
private clearTimer(): void {
if (this.timer) { if (this.timer) {
clearInterval(this.timer); clearInterval(this.timer);
this.timer = null;
} }
} }