refactor headless sync
This commit is contained in:
parent
ecad257587
commit
300e251734
14 changed files with 619 additions and 223 deletions
2
build.js
2
build.js
|
|
@ -69,7 +69,7 @@ Promise.all([
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
target: ["chrome90"],
|
target: ["chrome90"],
|
||||||
external: ["obsidian"],
|
external: ["obsidian", "fs"], //using fs shim
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
}),
|
}),
|
||||||
]).catch(() => process.exit(1));
|
]).catch(() => process.exit(1));
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const {
|
||||||
reconcilePluginTabs,
|
reconcilePluginTabs,
|
||||||
hideIgnisFromCommunityPlugins,
|
hideIgnisFromCommunityPlugins,
|
||||||
restoreCommunityPlugins,
|
restoreCommunityPlugins,
|
||||||
clearOwnedNavItems,
|
clearOwnedPluginIds,
|
||||||
} = require("./plugin-tabs");
|
} = require("./plugin-tabs");
|
||||||
|
|
||||||
function removeExistingIgnisGroups(tabHeadersEl) {
|
function removeExistingIgnisGroups(tabHeadersEl) {
|
||||||
|
|
@ -24,9 +24,42 @@ function removeExistingIgnisGroups(tabHeadersEl) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All ignis-managed nav elements (both Ignis group and Ignis Core Plugins group).
|
||||||
|
// Collected here so the openTab patch can manage is-active across all of them.
|
||||||
|
const allIgnisNavEls = new Map(); // tab id -> nav element
|
||||||
|
|
||||||
|
function patchOpenTab(setting) {
|
||||||
|
if (setting._ignisOpenTabPatched) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const original = setting.openTab.bind(setting);
|
||||||
|
|
||||||
|
setting.openTab = function (tab) {
|
||||||
|
// Clear is-active from all ignis nav items.
|
||||||
|
for (const [, el] of allIgnisNavEls) {
|
||||||
|
el.removeClass("is-active");
|
||||||
|
}
|
||||||
|
|
||||||
|
original(tab);
|
||||||
|
|
||||||
|
// If the opened tab is one of ours, highlight it.
|
||||||
|
const navEl = allIgnisNavEls.get(tab.id);
|
||||||
|
|
||||||
|
if (navEl) {
|
||||||
|
navEl.addClass("is-active");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setting._ignisOpenTabPatched = true;
|
||||||
|
}
|
||||||
|
|
||||||
function injectIgnisSettings(setting, app) {
|
function injectIgnisSettings(setting, app) {
|
||||||
removeExistingIgnisGroups(setting.tabHeadersEl);
|
removeExistingIgnisGroups(setting.tabHeadersEl);
|
||||||
clearOwnedNavItems();
|
clearOwnedPluginIds();
|
||||||
|
allIgnisNavEls.clear();
|
||||||
|
|
||||||
|
patchOpenTab(setting);
|
||||||
|
|
||||||
const ignis = createGroup("Ignis");
|
const ignis = createGroup("Ignis");
|
||||||
|
|
||||||
|
|
@ -44,6 +77,7 @@ function injectIgnisSettings(setting, app) {
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
tab.navEl = createNavEl(tab, setting);
|
tab.navEl = createNavEl(tab, setting);
|
||||||
ignis.items.appendChild(tab.navEl);
|
ignis.items.appendChild(tab.navEl);
|
||||||
|
allIgnisNavEls.set(tab.id, tab.navEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
setting.tabHeadersEl.appendChild(ignis.group);
|
setting.tabHeadersEl.appendChild(ignis.group);
|
||||||
|
|
@ -52,7 +86,7 @@ function injectIgnisSettings(setting, app) {
|
||||||
setting.tabHeadersEl.appendChild(corePlugins.group);
|
setting.tabHeadersEl.appendChild(corePlugins.group);
|
||||||
|
|
||||||
hideIgnisFromCommunityPlugins(setting);
|
hideIgnisFromCommunityPlugins(setting);
|
||||||
setupPluginTabs(setting, corePlugins.items);
|
setupPluginTabs(setting, corePlugins.items, allIgnisNavEls);
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchSettingsModal(plugin) {
|
function patchSettingsModal(plugin) {
|
||||||
|
|
@ -71,10 +105,13 @@ function unpatchSettingsModal(plugin) {
|
||||||
plugin.app.setting.onOpen = plugin._originalOnOpen;
|
plugin.app.setting.onOpen = plugin._originalOnOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete plugin.app.setting._ignisOpenTabPatched;
|
||||||
|
|
||||||
restoreCommunityPlugins(plugin.app.setting);
|
restoreCommunityPlugins(plugin.app.setting);
|
||||||
clearOwnedNavItems();
|
clearOwnedPluginIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.__ignisReconcilePluginTabs = reconcilePluginTabs;
|
window.__ignisReconcilePluginTabs = (setting) =>
|
||||||
|
reconcilePluginTabs(setting, allIgnisNavEls);
|
||||||
|
|
||||||
module.exports = { patchSettingsModal, unpatchSettingsModal, reconcilePluginTabs };
|
module.exports = { patchSettingsModal, unpatchSettingsModal, reconcilePluginTabs };
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,20 @@ const { setIcon } = require("obsidian");
|
||||||
const { findGroupByTitle } = require("./settings-ui");
|
const { findGroupByTitle } = require("./settings-ui");
|
||||||
const { isIgnisPlugin } = require("../plugin-registry");
|
const { isIgnisPlugin } = require("../plugin-registry");
|
||||||
|
|
||||||
// Tracks our own nav items in the "Ignis Core Plugins" group, keyed by plugin ID.
|
// Tracks which plugin IDs have nav items we created.
|
||||||
const ownedNavItems = new Map();
|
const ownedPluginIds = new Set();
|
||||||
|
|
||||||
function addPluginNavItem(pluginId, setting, corePluginsItems) {
|
function addPluginNavItem(pluginId, setting, corePluginsItems, ignisNavEls) {
|
||||||
// Find the tab object Obsidian created for this plugin.
|
|
||||||
const tab = setting.pluginTabs.find((t) => t.id === pluginId);
|
const tab = setting.pluginTabs.find((t) => t.id === pluginId);
|
||||||
|
|
||||||
if (!tab) {
|
if (!tab) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't add if we already have one for this ID.
|
if (ownedPluginIds.has(pluginId)) {
|
||||||
if (ownedNavItems.has(pluginId)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create our own nav item that delegates to Obsidian's tab.
|
|
||||||
const nav = document.createElement("div");
|
const nav = document.createElement("div");
|
||||||
nav.className = "vertical-tab-nav-item tappable";
|
nav.className = "vertical-tab-nav-item tappable";
|
||||||
|
|
||||||
|
|
@ -43,15 +40,17 @@ function addPluginNavItem(pluginId, setting, corePluginsItems) {
|
||||||
});
|
});
|
||||||
|
|
||||||
corePluginsItems.appendChild(nav);
|
corePluginsItems.appendChild(nav);
|
||||||
ownedNavItems.set(pluginId, nav);
|
ownedPluginIds.add(pluginId);
|
||||||
|
ignisNavEls.set(pluginId, nav);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePluginNavItem(pluginId) {
|
function removePluginNavItem(pluginId, ignisNavEls) {
|
||||||
const nav = ownedNavItems.get(pluginId);
|
const nav = ignisNavEls.get(pluginId);
|
||||||
|
|
||||||
if (nav) {
|
if (nav && ownedPluginIds.has(pluginId)) {
|
||||||
nav.remove();
|
nav.remove();
|
||||||
ownedNavItems.delete(pluginId);
|
ownedPluginIds.delete(pluginId);
|
||||||
|
ignisNavEls.delete(pluginId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,14 +103,12 @@ function hideIgnisNavFromCommunityGroup(setting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide any ignis plugin nav items that Obsidian placed here.
|
|
||||||
for (const tab of setting.pluginTabs) {
|
for (const tab of setting.pluginTabs) {
|
||||||
if (isIgnisPlugin(tab.id) && tab.navEl?.parentElement === items) {
|
if (isIgnisPlugin(tab.id) && tab.navEl?.parentElement === items) {
|
||||||
tab.navEl.style.display = "none";
|
tab.navEl.style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the entire group if no visible items remain.
|
|
||||||
const hasVisible = Array.from(items.children).some(
|
const hasVisible = Array.from(items.children).some(
|
||||||
(el) => el.style.display !== "none",
|
(el) => el.style.display !== "none",
|
||||||
);
|
);
|
||||||
|
|
@ -119,45 +116,40 @@ function hideIgnisNavFromCommunityGroup(setting) {
|
||||||
communityGroup.style.display = hasVisible ? "" : "none";
|
communityGroup.style.display = hasVisible ? "" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideCorePluginsGroupIfEmpty() {
|
function hideCorePluginsGroupIfEmpty(ignisNavEls) {
|
||||||
for (const [, nav] of ownedNavItems) {
|
let hasConnected = false;
|
||||||
if (nav.isConnected) {
|
|
||||||
const group = nav.closest(".vertical-tab-header-group");
|
|
||||||
|
|
||||||
if (group) {
|
for (const id of ownedPluginIds) {
|
||||||
group.style.display = "";
|
const nav = ignisNavEls.get(id);
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
if (nav?.isConnected) {
|
||||||
|
hasConnected = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No connected items -- find and hide the group.
|
|
||||||
const groups = document.querySelectorAll(".vertical-tab-header-group");
|
const groups = document.querySelectorAll(".vertical-tab-header-group");
|
||||||
|
|
||||||
for (const g of groups) {
|
for (const g of groups) {
|
||||||
const title = g.querySelector(".vertical-tab-header-group-title");
|
const title = g.querySelector(".vertical-tab-header-group-title");
|
||||||
|
|
||||||
if (title?.textContent === "Ignis Core Plugins") {
|
if (title?.textContent === "Ignis Core Plugins") {
|
||||||
g.style.display = ownedNavItems.size > 0 ? "" : "none";
|
g.style.display = hasConnected ? "" : "none";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupPluginTabs(setting, corePluginsItems) {
|
function setupPluginTabs(setting, corePluginsItems, ignisNavEls) {
|
||||||
// Create our own nav items for ignis plugin tabs.
|
|
||||||
for (const tab of setting.pluginTabs) {
|
for (const tab of setting.pluginTabs) {
|
||||||
if (isIgnisPlugin(tab.id) && tab.id !== "ignis-bridge") {
|
if (isIgnisPlugin(tab.id) && tab.id !== "ignis-bridge") {
|
||||||
addPluginNavItem(tab.id, setting, corePluginsItems);
|
addPluginNavItem(tab.id, setting, corePluginsItems, ignisNavEls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideIgnisNavFromCommunityGroup(setting);
|
hideIgnisNavFromCommunityGroup(setting);
|
||||||
hideCorePluginsGroupIfEmpty();
|
hideCorePluginsGroupIfEmpty(ignisNavEls);
|
||||||
|
|
||||||
// Watch the community group for changes. When Obsidian adds new ignis
|
|
||||||
// plugin nav items (async after enable), hide them and create our own.
|
|
||||||
const communityGroup = findGroupByTitle(
|
const communityGroup = findGroupByTitle(
|
||||||
setting.tabHeadersEl,
|
setting.tabHeadersEl,
|
||||||
"Community plugins",
|
"Community plugins",
|
||||||
|
|
@ -167,17 +159,19 @@ function setupPluginTabs(setting, corePluginsItems) {
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
for (const tab of setting.pluginTabs) {
|
for (const tab of setting.pluginTabs) {
|
||||||
if (isIgnisPlugin(tab.id) && tab.id !== "ignis-bridge") {
|
if (isIgnisPlugin(tab.id) && tab.id !== "ignis-bridge") {
|
||||||
addPluginNavItem(tab.id, setting, corePluginsItems);
|
addPluginNavItem(tab.id, setting, corePluginsItems, ignisNavEls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-evaluate visibility since non-ignis items may have appeared.
|
|
||||||
hideIgnisNavFromCommunityGroup(setting);
|
hideIgnisNavFromCommunityGroup(setting);
|
||||||
hideCorePluginsGroupIfEmpty();
|
hideCorePluginsGroupIfEmpty(ignisNavEls);
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(communityGroup, { childList: true, subtree: true });
|
observer.observe(communityGroup, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
const modalEl = setting.tabHeadersEl.closest(".modal");
|
||||||
|
|
||||||
|
if (modalEl && modalEl.parentElement) {
|
||||||
const cleanupObserver = new MutationObserver(() => {
|
const cleanupObserver = new MutationObserver(() => {
|
||||||
if (!setting.tabHeadersEl.isConnected) {
|
if (!setting.tabHeadersEl.isConnected) {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
|
|
@ -185,14 +179,14 @@ function setupPluginTabs(setting, corePluginsItems) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cleanupObserver.observe(document.body, {
|
cleanupObserver.observe(modalEl.parentElement, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function reconcilePluginTabs(setting) {
|
function reconcilePluginTabs(setting, ignisNavEls) {
|
||||||
const corePluginsGroup = findGroupByTitle(
|
const corePluginsGroup = findGroupByTitle(
|
||||||
setting.tabHeadersEl,
|
setting.tabHeadersEl,
|
||||||
"Ignis Core Plugins",
|
"Ignis Core Plugins",
|
||||||
|
|
@ -210,31 +204,28 @@ function reconcilePluginTabs(setting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current set of ignis plugin IDs from pluginTabs.
|
|
||||||
const activeIds = new Set(
|
const activeIds = new Set(
|
||||||
setting.pluginTabs
|
setting.pluginTabs
|
||||||
.filter((t) => isIgnisPlugin(t.id) && t.id !== "ignis-bridge")
|
.filter((t) => isIgnisPlugin(t.id) && t.id !== "ignis-bridge")
|
||||||
.map((t) => t.id),
|
.map((t) => t.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove nav items for plugins that are no longer active.
|
for (const id of ownedPluginIds) {
|
||||||
for (const [id] of ownedNavItems) {
|
|
||||||
if (!activeIds.has(id)) {
|
if (!activeIds.has(id)) {
|
||||||
removePluginNavItem(id);
|
removePluginNavItem(id, ignisNavEls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add nav items for newly active plugins.
|
|
||||||
for (const id of activeIds) {
|
for (const id of activeIds) {
|
||||||
addPluginNavItem(id, setting, corePluginsItems);
|
addPluginNavItem(id, setting, corePluginsItems, ignisNavEls);
|
||||||
}
|
}
|
||||||
|
|
||||||
hideIgnisNavFromCommunityGroup(setting);
|
hideIgnisNavFromCommunityGroup(setting);
|
||||||
hideCorePluginsGroupIfEmpty();
|
hideCorePluginsGroupIfEmpty(ignisNavEls);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearOwnedNavItems() {
|
function clearOwnedPluginIds() {
|
||||||
ownedNavItems.clear();
|
ownedPluginIds.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
@ -242,5 +233,5 @@ module.exports = {
|
||||||
reconcilePluginTabs,
|
reconcilePluginTabs,
|
||||||
hideIgnisFromCommunityPlugins,
|
hideIgnisFromCommunityPlugins,
|
||||||
restoreCommunityPlugins,
|
restoreCommunityPlugins,
|
||||||
clearOwnedNavItems,
|
clearOwnedPluginIds,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
58
server/plugins/headless-sync/broadcaster.js
Normal file
58
server/plugins/headless-sync/broadcaster.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
const CHANNEL = "plugin:headless-sync";
|
||||||
|
|
||||||
|
class SyncBroadcaster {
|
||||||
|
constructor(wss) {
|
||||||
|
this._wss = wss;
|
||||||
|
this._logSubscriptions = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeToLogs(vaultId) {
|
||||||
|
this._logSubscriptions.set(vaultId, { expires: Date.now() + 10000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastLog(vaultId, line) {
|
||||||
|
if (!this._wss?.clients) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = this._logSubscriptions.get(vaultId);
|
||||||
|
|
||||||
|
if (!sub || Date.now() > sub.expires) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._send({
|
||||||
|
channel: CHANNEL,
|
||||||
|
type: "sync-log",
|
||||||
|
payload: { vaultId, line },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastStatus(state) {
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._send({
|
||||||
|
channel: CHANNEL,
|
||||||
|
type: "sync-status",
|
||||||
|
payload: state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_send(msg) {
|
||||||
|
if (!this._wss?.clients) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.stringify(msg);
|
||||||
|
|
||||||
|
for (const client of this._wss.clients) {
|
||||||
|
if (client.readyState === 1) {
|
||||||
|
client.send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { SyncBroadcaster };
|
||||||
|
|
@ -2,6 +2,7 @@ const path = require("path");
|
||||||
const obCli = require("./ob-cli");
|
const obCli = require("./ob-cli");
|
||||||
const auth = require("./auth");
|
const auth = require("./auth");
|
||||||
const { SyncManager } = require("./sync-manager");
|
const { SyncManager } = require("./sync-manager");
|
||||||
|
const { SyncBroadcaster } = require("./broadcaster");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
id: "headless-sync",
|
id: "headless-sync",
|
||||||
|
|
@ -15,6 +16,7 @@ module.exports = {
|
||||||
_ctx: null,
|
_ctx: null,
|
||||||
_obStatus: null,
|
_obStatus: null,
|
||||||
_syncManager: null,
|
_syncManager: null,
|
||||||
|
_broadcaster: null,
|
||||||
|
|
||||||
async register(ctx) {
|
async register(ctx) {
|
||||||
this._ctx = ctx;
|
this._ctx = ctx;
|
||||||
|
|
@ -33,7 +35,8 @@ module.exports = {
|
||||||
ctx.log("Auth token loaded");
|
ctx.log("Auth token loaded");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._syncManager = new SyncManager(ctx);
|
this._broadcaster = new SyncBroadcaster(ctx.wss);
|
||||||
|
this._syncManager = new SyncManager(ctx, this._broadcaster);
|
||||||
|
|
||||||
// Load saved sync states for enabled vaults
|
// Load saved sync states for enabled vaults
|
||||||
const enabledVaults = ctx.getEnabledVaults();
|
const enabledVaults = ctx.getEnabledVaults();
|
||||||
|
|
@ -56,9 +59,22 @@ module.exports = {
|
||||||
|
|
||||||
const { mountRoutes } = require("./routes");
|
const { mountRoutes } = require("./routes");
|
||||||
mountRoutes(ctx.router, this);
|
mountRoutes(ctx.router, this);
|
||||||
|
|
||||||
|
// Register WebSocket message handler for log subscriptions
|
||||||
|
if (ctx.wss && ctx.wss.messageHandlers) {
|
||||||
|
ctx.wss.messageHandlers.set("subscribe-logs", (msg) => {
|
||||||
|
if (msg.vaultId && this._broadcaster) {
|
||||||
|
this._broadcaster.subscribeToLogs(msg.vaultId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async shutdown() {
|
async shutdown() {
|
||||||
|
if (this._ctx?.wss?.messageHandlers) {
|
||||||
|
this._ctx.wss.messageHandlers.delete("subscribe-logs");
|
||||||
|
}
|
||||||
|
|
||||||
if (this._syncManager) {
|
if (this._syncManager) {
|
||||||
await this._syncManager.shutdown();
|
await this._syncManager.shutdown();
|
||||||
this._syncManager = null;
|
this._syncManager = null;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "ignis-headless-sync",
|
"id": "ignis-headless-sync",
|
||||||
"name": "Ignis Headless Sync",
|
"name": "Ignis Headless Sync",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"minAppVersion": "1.12.4",
|
"minAppVersion": "1.12.4",
|
||||||
"description": "Client-side companion for server-side Obsidian Sync",
|
"description": "Client-side companion for server-side Obsidian Sync",
|
||||||
"author": "Ignis",
|
"author": "Ignis",
|
||||||
|
|
|
||||||
69
server/plugins/headless-sync/plugin/src/core-sync-guard.js
Normal file
69
server/plugins/headless-sync/plugin/src/core-sync-guard.js
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
const { Notice } = require("obsidian");
|
||||||
|
const fs = require("fs"); // Using fs shim
|
||||||
|
|
||||||
|
function isCoreSyncEnabled() {
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(".obsidian/core-plugins.json", "utf-8");
|
||||||
|
const config = JSON.parse(data);
|
||||||
|
return config.sync === true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showConflictWarning(title, message) {
|
||||||
|
if (!window.IgnisUI?.MessageDialog) {
|
||||||
|
new Notice(`${title}: ${message}`, 10000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialog = new window.IgnisUI.MessageDialog({
|
||||||
|
target: document.body,
|
||||||
|
props: { title, message },
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.$on("confirm", () => {
|
||||||
|
dialog.$destroy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCoreSyncWatcher(plugin, api, wsListener) {
|
||||||
|
let wasEnabled = isCoreSyncEnabled();
|
||||||
|
|
||||||
|
const rawHandler = (msg) => {
|
||||||
|
if (msg.type === "modified" && msg.path === ".obsidian/core-plugins.json") {
|
||||||
|
handleCoreSyncChange();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
wsListener.onRaw(rawHandler);
|
||||||
|
|
||||||
|
function handleCoreSyncChange() {
|
||||||
|
const enabled = isCoreSyncEnabled();
|
||||||
|
|
||||||
|
if (enabled && !wasEnabled) {
|
||||||
|
const vaultId = plugin.app.vault.getName();
|
||||||
|
|
||||||
|
api.stopSync(vaultId).catch(() => {});
|
||||||
|
showConflictWarning(
|
||||||
|
"Headless Sync Stopped",
|
||||||
|
"Obsidian Sync has been enabled. Headless Sync has been automatically " +
|
||||||
|
"stopped to avoid conflicts between the two sync methods.\n\n" +
|
||||||
|
"To use Headless Sync again, disable Obsidian Sync in Core Plugins.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wasEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cleanup() {
|
||||||
|
wsListener.offRaw();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isCoreSyncEnabled,
|
||||||
|
startCoreSyncWatcher,
|
||||||
|
};
|
||||||
77
server/plugins/headless-sync/plugin/src/log-viewer.js
Normal file
77
server/plugins/headless-sync/plugin/src/log-viewer.js
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
const api = require("./api");
|
||||||
|
|
||||||
|
async function renderLogViewer(containerEl, vaultId, wsListener) {
|
||||||
|
const details = containerEl.createEl("details", {
|
||||||
|
cls: "ignis-log-details",
|
||||||
|
});
|
||||||
|
|
||||||
|
details.createEl("summary", { text: "Sync logs" });
|
||||||
|
|
||||||
|
const logBox = details.createEl("pre", { cls: "ignis-log-terminal" });
|
||||||
|
const codeEl = logBox.createEl("code");
|
||||||
|
|
||||||
|
let logsData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logsData = await api.getLogs(vaultId, 50);
|
||||||
|
} catch (e) {
|
||||||
|
codeEl.textContent = `Failed to load logs: ${e.message}`;
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logsData.logs.length === 0) {
|
||||||
|
codeEl.textContent = "No log entries yet.";
|
||||||
|
} else {
|
||||||
|
const lines = logsData.logs.map((entry) => {
|
||||||
|
const time = new Date(entry.timestamp).toLocaleTimeString();
|
||||||
|
return `[${time}] ${entry.line}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
codeEl.textContent = lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
logBox.scrollTop = logBox.scrollHeight;
|
||||||
|
|
||||||
|
if (!wsListener) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
details.addEventListener("toggle", () => {
|
||||||
|
if (details.open) {
|
||||||
|
wsListener.subscribeLogs(vaultId);
|
||||||
|
} else {
|
||||||
|
wsListener.unsubscribeLogs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onLog = (payload) => {
|
||||||
|
if (payload.vaultId !== vaultId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
const line = `[${time}] ${payload.line}`;
|
||||||
|
|
||||||
|
if (codeEl.textContent === "No log entries yet.") {
|
||||||
|
codeEl.textContent = line;
|
||||||
|
} else {
|
||||||
|
codeEl.textContent += "\n" + line;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNearBottom =
|
||||||
|
logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 50;
|
||||||
|
|
||||||
|
if (isNearBottom) {
|
||||||
|
logBox.scrollTop = logBox.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
wsListener.on("sync-log", onLog);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
wsListener.off("sync-log", onLog);
|
||||||
|
wsListener.unsubscribeLogs();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { renderLogViewer };
|
||||||
|
|
@ -2,6 +2,7 @@ const { Plugin } = require("obsidian");
|
||||||
const { HeadlessSyncSettingTab } = require("./settings-tab");
|
const { HeadlessSyncSettingTab } = require("./settings-tab");
|
||||||
const { WsListener } = require("./ws-listener");
|
const { WsListener } = require("./ws-listener");
|
||||||
const { initSyncStatusBar } = require("./sync-status-bar");
|
const { initSyncStatusBar } = require("./sync-status-bar");
|
||||||
|
const { startCoreSyncWatcher } = require("./core-sync-guard");
|
||||||
const api = require("./api");
|
const api = require("./api");
|
||||||
|
|
||||||
class IgnisHeadlessSyncPlugin extends Plugin {
|
class IgnisHeadlessSyncPlugin extends Plugin {
|
||||||
|
|
@ -13,6 +14,8 @@ class IgnisHeadlessSyncPlugin extends Plugin {
|
||||||
|
|
||||||
this.addSettingTab(new HeadlessSyncSettingTab(this.app, this));
|
this.addSettingTab(new HeadlessSyncSettingTab(this.app, this));
|
||||||
|
|
||||||
|
this._coreSyncWatcher = startCoreSyncWatcher(this, api, this.wsListener);
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "start-sync",
|
id: "start-sync",
|
||||||
name: "Start server-side sync",
|
name: "Start server-side sync",
|
||||||
|
|
@ -50,6 +53,11 @@ class IgnisHeadlessSyncPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
|
if (this._coreSyncWatcher) {
|
||||||
|
this._coreSyncWatcher.cleanup();
|
||||||
|
this._coreSyncWatcher = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this._syncStatusBarCleanup) {
|
if (this._syncStatusBarCleanup) {
|
||||||
this._syncStatusBarCleanup();
|
this._syncStatusBarCleanup();
|
||||||
this._syncStatusBarCleanup = null;
|
this._syncStatusBarCleanup = null;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,53 @@
|
||||||
const { PluginSettingTab, Setting, Notice } = require("obsidian");
|
const { PluginSettingTab, Setting, Notice } = require("obsidian");
|
||||||
const api = require("./api");
|
const api = require("./api");
|
||||||
const auth = require("./auth");
|
const auth = require("./auth");
|
||||||
|
const { isCoreSyncEnabled } = require("./core-sync-guard");
|
||||||
|
const { renderLogViewer } = require("./log-viewer");
|
||||||
|
|
||||||
class HeadlessSyncSettingTab extends PluginSettingTab {
|
class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
constructor(app, plugin) {
|
constructor(app, plugin) {
|
||||||
super(app, plugin);
|
super(app, plugin);
|
||||||
this._cancelWait = null;
|
this._cancelWait = null;
|
||||||
|
this._logCleanup = null;
|
||||||
|
|
||||||
|
// Persistent container refs
|
||||||
|
this._authEl = null;
|
||||||
|
this._syncEl = null;
|
||||||
|
this._logsEl = null;
|
||||||
|
this._logsRendered = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async display() {
|
async display() {
|
||||||
|
// Clean up previous log listener before rebuilding
|
||||||
|
if (this._logCleanup) {
|
||||||
|
this._logCleanup();
|
||||||
|
this._logCleanup = null;
|
||||||
|
}
|
||||||
|
|
||||||
const { containerEl } = this;
|
const { containerEl } = this;
|
||||||
containerEl.empty();
|
containerEl.empty();
|
||||||
|
|
||||||
|
this._logsRendered = false;
|
||||||
|
|
||||||
|
if (isCoreSyncEnabled()) {
|
||||||
|
const syncWarningSetting = new Setting(containerEl)
|
||||||
|
.setName("Obsidian Sync is active");
|
||||||
|
|
||||||
|
syncWarningSetting.descEl.createEl("span", {
|
||||||
|
text: "Headless Sync cannot run alongside Obsidian's built-in sync to avoid conflicts. Disable Obsidian Sync in Core Plugins to use Headless Sync instead.",
|
||||||
|
cls: "mod-warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
syncWarningSetting
|
||||||
|
.addButton((btn) => {
|
||||||
|
btn.setButtonText("Open Core Plugins").onClick(() => {
|
||||||
|
this.app.setting.openTabById("plugins");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let serverStatus;
|
let serverStatus;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -32,16 +68,21 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderAuthSection(containerEl, serverStatus);
|
this._authEl = containerEl.createDiv();
|
||||||
await this.renderSyncSection(containerEl, serverStatus.authenticated);
|
this._syncEl = containerEl.createDiv();
|
||||||
|
this._logsEl = containerEl.createDiv();
|
||||||
|
|
||||||
|
this.renderAuthSection(serverStatus);
|
||||||
|
await this.renderSyncSection(serverStatus.authenticated);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAuthSection(containerEl, serverStatus) {
|
renderAuthSection(serverStatus) {
|
||||||
|
this._authEl.empty();
|
||||||
|
|
||||||
const localToken = auth.getObsidianSyncToken();
|
const localToken = auth.getObsidianSyncToken();
|
||||||
|
|
||||||
if (serverStatus.authenticated) {
|
if (serverStatus.authenticated) {
|
||||||
// State C: connected to server
|
new Setting(this._authEl)
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("Obsidian Sync account")
|
.setName("Obsidian Sync account")
|
||||||
.setDesc(
|
.setDesc(
|
||||||
`Signed in as ${serverStatus.name || "unknown"} (${serverStatus.email || "unknown"})`,
|
`Signed in as ${serverStatus.name || "unknown"} (${serverStatus.email || "unknown"})`,
|
||||||
|
|
@ -53,15 +94,16 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
try {
|
try {
|
||||||
await api.logout();
|
await api.logout();
|
||||||
new Notice("Disconnected from Headless Sync");
|
new Notice("Disconnected from Headless Sync");
|
||||||
this.display();
|
const status = await api.getStatus();
|
||||||
|
this.renderAuthSection(status);
|
||||||
|
await this.renderSyncSection(status.authenticated);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Notice(`Failed to disconnect: ${e.message}`);
|
new Notice(`Failed to disconnect: ${e.message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (localToken) {
|
} else if (localToken) {
|
||||||
// State B: signed into Obsidian, not connected to server
|
new Setting(this._authEl)
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("Obsidian Sync account detected")
|
.setName("Obsidian Sync account detected")
|
||||||
.setDesc(`${localToken.name} (${localToken.email})`)
|
.setDesc(`${localToken.name} (${localToken.email})`)
|
||||||
.addButton((btn) => {
|
.addButton((btn) => {
|
||||||
|
|
@ -72,15 +114,16 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
try {
|
try {
|
||||||
await auth.sendTokenToServer(localToken);
|
await auth.sendTokenToServer(localToken);
|
||||||
new Notice("Connected to Headless Sync");
|
new Notice("Connected to Headless Sync");
|
||||||
this.display();
|
const status = await api.getStatus();
|
||||||
|
this.renderAuthSection(status);
|
||||||
|
await this.renderSyncSection(status.authenticated);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Notice(`Failed to connect: ${e.message}`);
|
new Notice(`Failed to connect: ${e.message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// State A: not signed into Obsidian
|
new Setting(this._authEl)
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("Obsidian Sync account")
|
.setName("Obsidian Sync account")
|
||||||
.setDesc("Sign in to your Obsidian account to enable sync.")
|
.setDesc("Sign in to your Obsidian account to enable sync.")
|
||||||
.addButton((btn) => {
|
.addButton((btn) => {
|
||||||
|
|
@ -88,16 +131,20 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
const triggered = auth.triggerLogin(this.app);
|
const triggered = auth.triggerLogin(this.app);
|
||||||
|
|
||||||
if (!triggered) {
|
if (!triggered) {
|
||||||
new Notice("Could not open login dialog. Try logging in from Settings > General.");
|
new Notice(
|
||||||
|
"Could not open login dialog. Try logging in from Settings > General.",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._cancelWait = auth.waitForLogin((token) => {
|
this._cancelWait = auth.waitForLogin(async (token) => {
|
||||||
this._cancelWait = null;
|
this._cancelWait = null;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
new Notice(`Detected login: ${token.name}`);
|
new Notice(`Detected login: ${token.name}`);
|
||||||
this.display();
|
const status = await api.getStatus();
|
||||||
|
this.renderAuthSection(status);
|
||||||
|
await this.renderSyncSection(status.authenticated);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -105,11 +152,13 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderSyncSection(containerEl, authenticated) {
|
async renderSyncSection(authenticated) {
|
||||||
containerEl.createEl("h3", { text: "Vault sync" });
|
this._syncEl.empty();
|
||||||
|
|
||||||
|
this._syncEl.createEl("h3", { text: "Vault sync" });
|
||||||
|
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
new Setting(containerEl)
|
new Setting(this._syncEl)
|
||||||
.setName("Sync not configured")
|
.setName("Sync not configured")
|
||||||
.setDesc("Sign in to your Obsidian Sync account to set up sync.")
|
.setDesc("Sign in to your Obsidian Sync account to set up sync.")
|
||||||
.addButton((btn) => {
|
.addButton((btn) => {
|
||||||
|
|
@ -127,7 +176,7 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
try {
|
try {
|
||||||
vaultsData = await api.getVaults();
|
vaultsData = await api.getVaults();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
containerEl.createEl("p", {
|
this._syncEl.createEl("p", {
|
||||||
text: `Failed to load sync state: ${e.message}`,
|
text: `Failed to load sync state: ${e.message}`,
|
||||||
cls: "mod-warning",
|
cls: "mod-warning",
|
||||||
});
|
});
|
||||||
|
|
@ -137,7 +186,7 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
const vaultState = vaultsData.vaults.find((v) => v.vaultId === vaultId);
|
const vaultState = vaultsData.vaults.find((v) => v.vaultId === vaultId);
|
||||||
|
|
||||||
if (!vaultState) {
|
if (!vaultState) {
|
||||||
new Setting(containerEl)
|
new Setting(this._syncEl)
|
||||||
.setName("Sync not configured")
|
.setName("Sync not configured")
|
||||||
.setDesc("This vault has not been linked to a remote vault yet.")
|
.setDesc("This vault has not been linked to a remote vault yet.")
|
||||||
.addButton((btn) => {
|
.addButton((btn) => {
|
||||||
|
|
@ -157,10 +206,10 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: {
|
props: {
|
||||||
vaultId,
|
vaultId,
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
cleanup();
|
cleanup();
|
||||||
modal.$destroy();
|
modal.$destroy();
|
||||||
this.display();
|
await this.renderSyncSection(true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -176,9 +225,11 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show current sync config
|
// Show current sync config
|
||||||
new Setting(containerEl)
|
new Setting(this._syncEl)
|
||||||
.setName("Remote vault")
|
.setName("Remote vault")
|
||||||
.setDesc(vaultState.remoteVaultName || vaultState.remoteVault || "unknown")
|
.setDesc(
|
||||||
|
vaultState.remoteVaultName || vaultState.remoteVault || "unknown",
|
||||||
|
)
|
||||||
.addButton((btn) => {
|
.addButton((btn) => {
|
||||||
btn.setButtonText("Unlink");
|
btn.setButtonText("Unlink");
|
||||||
btn.buttonEl.addClass("mod-destructive");
|
btn.buttonEl.addClass("mod-destructive");
|
||||||
|
|
@ -186,18 +237,44 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
try {
|
try {
|
||||||
await api.unlinkVault(vaultId);
|
await api.unlinkVault(vaultId);
|
||||||
new Notice("Vault unlinked");
|
new Notice("Vault unlinked");
|
||||||
this.display();
|
await this.renderSyncSection(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Notice(`Failed to unlink: ${e.message}`);
|
new Notice(`Failed to unlink: ${e.message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(this._syncEl)
|
||||||
.setName("Sync mode")
|
.setName("Sync mode")
|
||||||
.setDesc(vaultState.config?.mode || "bidirectional");
|
.setDesc(vaultState.config?.mode || "bidirectional");
|
||||||
|
|
||||||
// Sync controls
|
// Sync controls
|
||||||
|
const controlsEl = this._syncEl.createDiv();
|
||||||
|
this.renderSyncControls(controlsEl, vaultId, vaultState);
|
||||||
|
|
||||||
|
// Log viewer - only render once, persists across sync section rebuilds
|
||||||
|
if (!this._logsRendered) {
|
||||||
|
await this.renderLogs(this._logsEl, vaultId);
|
||||||
|
this._logsRendered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderSyncControls(containerEl, vaultId, vaultState) {
|
||||||
|
containerEl.empty();
|
||||||
|
|
||||||
|
if (!vaultState) {
|
||||||
|
try {
|
||||||
|
const data = await api.getVaults();
|
||||||
|
vaultState = (data.vaults || []).find((v) => v.vaultId === vaultId);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vaultState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const statusText =
|
const statusText =
|
||||||
vaultState.status === "running"
|
vaultState.status === "running"
|
||||||
? "Sync is running"
|
? "Sync is running"
|
||||||
|
|
@ -216,91 +293,34 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||||
try {
|
try {
|
||||||
await api.stopSync(vaultId);
|
await api.stopSync(vaultId);
|
||||||
new Notice("Sync stopped");
|
new Notice("Sync stopped");
|
||||||
this.display();
|
this.renderSyncControls(containerEl, vaultId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Notice(`Failed to stop: ${e.message}`);
|
new Notice(`Failed to stop: ${e.message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
btn.setButtonText("Start sync").setCta().onClick(async () => {
|
btn
|
||||||
|
.setButtonText("Start sync")
|
||||||
|
.setCta()
|
||||||
|
.onClick(async () => {
|
||||||
try {
|
try {
|
||||||
await api.startSync(vaultId);
|
await api.startSync(vaultId);
|
||||||
new Notice("Sync started");
|
new Notice("Sync started");
|
||||||
this.display();
|
this.renderSyncControls(containerEl, vaultId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Notice(`Failed to start: ${e.message}`);
|
new Notice(`Failed to start: ${e.message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log viewer (collapsible)
|
|
||||||
await this.renderLogs(containerEl, vaultId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderLogs(containerEl, vaultId) {
|
async renderLogs(containerEl, vaultId) {
|
||||||
const details = containerEl.createEl("details", {
|
this._logCleanup = await renderLogViewer(
|
||||||
cls: "ignis-log-details",
|
containerEl,
|
||||||
});
|
vaultId,
|
||||||
|
this.plugin.wsListener,
|
||||||
details.createEl("summary", { text: "Sync logs" });
|
);
|
||||||
|
|
||||||
const logBox = details.createEl("pre", { cls: "ignis-log-terminal" });
|
|
||||||
const codeEl = logBox.createEl("code");
|
|
||||||
|
|
||||||
let logsData;
|
|
||||||
|
|
||||||
try {
|
|
||||||
logsData = await api.getLogs(vaultId, 50);
|
|
||||||
} catch (e) {
|
|
||||||
codeEl.textContent = `Failed to load logs: ${e.message}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logsData.logs.length === 0) {
|
|
||||||
codeEl.textContent = "No log entries yet.";
|
|
||||||
} else {
|
|
||||||
const lines = logsData.logs.map((entry) => {
|
|
||||||
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
||||||
return `[${time}] ${entry.line}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
codeEl.textContent = lines.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
logBox.scrollTop = logBox.scrollHeight;
|
|
||||||
|
|
||||||
// Live updates via WebSocket
|
|
||||||
const wsListener = this.plugin.wsListener;
|
|
||||||
|
|
||||||
if (!wsListener) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onLog = (payload) => {
|
|
||||||
if (payload.vaultId !== vaultId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const time = new Date().toLocaleTimeString();
|
|
||||||
const line = `[${time}] ${payload.line}`;
|
|
||||||
|
|
||||||
if (codeEl.textContent === "No log entries yet.") {
|
|
||||||
codeEl.textContent = line;
|
|
||||||
} else {
|
|
||||||
codeEl.textContent += "\n" + line;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNearBottom =
|
|
||||||
logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 50;
|
|
||||||
|
|
||||||
if (isNearBottom) {
|
|
||||||
logBox.scrollTop = logBox.scrollHeight;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
wsListener.on("sync-log", onLog);
|
|
||||||
this._logCleanup = () => wsListener.off("sync-log", onLog);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||||
|
|
||||||
popoverOpen = true;
|
popoverOpen = true;
|
||||||
|
|
||||||
|
wsListener.subscribeLogs(vaultId);
|
||||||
|
|
||||||
outsideClickHandler = (e) => {
|
outsideClickHandler = (e) => {
|
||||||
if (!item.contains(e.target)) {
|
if (!item.contains(e.target)) {
|
||||||
hidePopover();
|
hidePopover();
|
||||||
|
|
@ -84,6 +86,7 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||||
outsideClickHandler = null;
|
outsideClickHandler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wsListener.unsubscribeLogs();
|
||||||
popoverOpen = false;
|
popoverOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,6 +122,13 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||||
return { prefix: "Syncing", path: match[1].trim() };
|
return { prefix: "Syncing", path: match[1].trim() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uploading file / Upload complete path
|
||||||
|
match = line.match(/^(?:Uploading file|Upload complete|New file)\s+(.+)$/);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return { prefix: "Syncing", path: match[1].trim() };
|
||||||
|
}
|
||||||
|
|
||||||
// Deleting path
|
// Deleting path
|
||||||
match = line.match(/^Deleting\s+(.+)$/);
|
match = line.match(/^Deleting\s+(.+)$/);
|
||||||
|
|
||||||
|
|
@ -238,8 +248,7 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||||
let wasDisconnected = false;
|
let wasDisconnected = false;
|
||||||
|
|
||||||
const wsCheckInterval = setInterval(() => {
|
const wsCheckInterval = setInterval(() => {
|
||||||
const ws = window.__ignisWs;
|
const disconnected = !wsListener.isConnected();
|
||||||
const disconnected = !ws || ws.readyState !== WebSocket.OPEN;
|
|
||||||
|
|
||||||
if (disconnected && currentStatus === "running") {
|
if (disconnected && currentStatus === "running") {
|
||||||
updateState("error", "Server connection lost");
|
updateState("error", "Server connection lost");
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
const CHANNEL = "plugin:headless-sync";
|
const CHANNEL = "plugin:headless-sync";
|
||||||
const POLL_INTERVAL = 3000;
|
const POLL_INTERVAL = 3000;
|
||||||
|
const LOG_KEEPALIVE_INTERVAL = 7000;
|
||||||
|
|
||||||
class WsListener {
|
class WsListener {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._callbacks = new Map();
|
this._callbacks = new Map();
|
||||||
this._handler = null;
|
this._handler = null;
|
||||||
|
this._rawHandler = null;
|
||||||
this._pollTimer = null;
|
this._pollTimer = null;
|
||||||
this._currentWs = null;
|
this._currentWs = null;
|
||||||
|
this._logSubInterval = null;
|
||||||
|
this._logSubVaultId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
|
@ -23,9 +27,15 @@ class WsListener {
|
||||||
this._pollTimer = null;
|
this._pollTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.unsubscribeLogs();
|
||||||
this._detachFromWs();
|
this._detachFromWs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isConnected() {
|
||||||
|
const ws = window.__ignisWs;
|
||||||
|
return ws && ws.readyState === WebSocket.OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
on(type, callback) {
|
on(type, callback) {
|
||||||
if (!this._callbacks.has(type)) {
|
if (!this._callbacks.has(type)) {
|
||||||
this._callbacks.set(type, []);
|
this._callbacks.set(type, []);
|
||||||
|
|
@ -48,6 +58,52 @@ class WsListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for raw WebSocket messages (not channel-filtered).
|
||||||
|
// Used by core-sync-guard to watch for file changes.
|
||||||
|
onRaw(callback) {
|
||||||
|
this._rawHandler = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
offRaw() {
|
||||||
|
this._rawHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
send(type, payload) {
|
||||||
|
const ws = window.__ignisWs;
|
||||||
|
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type, ...payload }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to server log broadcasts for a vault.
|
||||||
|
// Sends the initial subscribe message and keeps the subscription alive.
|
||||||
|
subscribeLogs(vaultId) {
|
||||||
|
// If already subscribed to this vault, no-op.
|
||||||
|
if (this._logSubVaultId === vaultId && this._logSubInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unsubscribeLogs();
|
||||||
|
this._logSubVaultId = vaultId;
|
||||||
|
|
||||||
|
this.send("subscribe-logs", { vaultId });
|
||||||
|
|
||||||
|
this._logSubInterval = setInterval(() => {
|
||||||
|
this.send("subscribe-logs", { vaultId });
|
||||||
|
}, LOG_KEEPALIVE_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the log subscription keepalive.
|
||||||
|
unsubscribeLogs() {
|
||||||
|
if (this._logSubInterval) {
|
||||||
|
clearInterval(this._logSubInterval);
|
||||||
|
this._logSubInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._logSubVaultId = null;
|
||||||
|
}
|
||||||
|
|
||||||
_attachToWs() {
|
_attachToWs() {
|
||||||
const ws = window.__ignisWs;
|
const ws = window.__ignisWs;
|
||||||
|
|
||||||
|
|
@ -62,6 +118,11 @@ class WsListener {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// Dispatch raw messages (for non-channel listeners like file watchers)
|
||||||
|
if (this._rawHandler) {
|
||||||
|
this._rawHandler(msg);
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.channel !== CHANNEL) {
|
if (msg.channel !== CHANNEL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,26 @@
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const { spawn } = require("child_process");
|
||||||
const { spawnOb, runCommand } = require("./ob-cli");
|
const { spawnOb, runCommand } = require("./ob-cli");
|
||||||
|
|
||||||
const MAX_LOG_ENTRIES = 200;
|
const MAX_LOG_ENTRIES = 200;
|
||||||
|
|
||||||
|
function killProcess(proc) {
|
||||||
|
if (!proc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
spawn("taskkill", ["/pid", String(proc.pid), "/t", "/f"]);
|
||||||
|
} else {
|
||||||
|
proc.kill("SIGTERM");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SyncManager {
|
class SyncManager {
|
||||||
constructor(ctx) {
|
constructor(ctx, broadcaster) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
|
this.broadcaster = broadcaster;
|
||||||
this.states = new Map();
|
this.states = new Map();
|
||||||
this.stateFile = path.join(ctx.dataDir, "sync-states.json");
|
this.stateFile = path.join(ctx.dataDir, "sync-states.json");
|
||||||
}
|
}
|
||||||
|
|
@ -66,8 +80,6 @@ class SyncManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupSync(vaultId, vaultPath, remoteVault, options = {}) {
|
async setupSync(vaultId, vaultPath, remoteVault, options = {}) {
|
||||||
const obCli = require("./ob-cli");
|
|
||||||
|
|
||||||
const args = ["sync-setup", "--vault", remoteVault, "--path", "."];
|
const args = ["sync-setup", "--vault", remoteVault, "--path", "."];
|
||||||
|
|
||||||
if (options.vaultPassword) {
|
if (options.vaultPassword) {
|
||||||
|
|
@ -78,7 +90,7 @@ class SyncManager {
|
||||||
args.push("--device-name", options.deviceName);
|
args.push("--device-name", options.deviceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
await obCli.runCommand(args, { cwd: vaultPath });
|
await runCommand(args, { cwd: vaultPath });
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
vaultId,
|
vaultId,
|
||||||
|
|
@ -112,6 +124,12 @@ class SyncManager {
|
||||||
throw new Error(`No sync configuration for vault: ${vaultId}`);
|
throw new Error(`No sync configuration for vault: ${vaultId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isCoreSyncEnabled(state.vaultPath)) {
|
||||||
|
const msg = `Cannot start sync for ${vaultId}: Obsidian Sync core plugin is enabled`;
|
||||||
|
this.ctx.log(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.status === "running") {
|
if (state.status === "running") {
|
||||||
this.ctx.log(`Sync already running for ${vaultId}`);
|
this.ctx.log(`Sync already running for ${vaultId}`);
|
||||||
return this.getState(vaultId);
|
return this.getState(vaultId);
|
||||||
|
|
@ -142,7 +160,7 @@ class SyncManager {
|
||||||
if (line.trim()) {
|
if (line.trim()) {
|
||||||
this.addLog(state, line.trim());
|
this.addLog(state, line.trim());
|
||||||
state.lastActivity = new Date().toISOString();
|
state.lastActivity = new Date().toISOString();
|
||||||
this.broadcastLog(vaultId, line.trim());
|
this.broadcaster.broadcastLog(vaultId, line.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -158,6 +176,13 @@ class SyncManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.on("close", (code) => {
|
proc.on("close", (code) => {
|
||||||
|
// If the user explicitly stopped sync, don't overwrite the clean
|
||||||
|
// "stopped" state with an error from the non-zero exit code.
|
||||||
|
if (state._userStopped) {
|
||||||
|
state._userStopped = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
state.status = code === 0 ? "stopped" : "error";
|
state.status = code === 0 ? "stopped" : "error";
|
||||||
state.pid = null;
|
state.pid = null;
|
||||||
state._process = null;
|
state._process = null;
|
||||||
|
|
@ -170,7 +195,7 @@ class SyncManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ctx.log(`Sync stopped for ${vaultId} (code: ${code})`);
|
this.ctx.log(`Sync stopped for ${vaultId} (code: ${code})`);
|
||||||
this.broadcastStatus(vaultId);
|
this.broadcaster.broadcastStatus(this.getState(vaultId));
|
||||||
this.saveStates();
|
this.saveStates();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -182,11 +207,11 @@ class SyncManager {
|
||||||
|
|
||||||
this.addLog(state, `Error: ${err.message}`);
|
this.addLog(state, `Error: ${err.message}`);
|
||||||
this.ctx.log(`Sync error for ${vaultId}: ${err.message}`);
|
this.ctx.log(`Sync error for ${vaultId}: ${err.message}`);
|
||||||
this.broadcastStatus(vaultId);
|
this.broadcaster.broadcastStatus(this.getState(vaultId));
|
||||||
this.saveStates();
|
this.saveStates();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.broadcastStatus(vaultId);
|
this.broadcaster.broadcastStatus(this.getState(vaultId));
|
||||||
this.ctx.log(`Started sync for ${vaultId} (pid: ${proc.pid})`);
|
this.ctx.log(`Started sync for ${vaultId} (pid: ${proc.pid})`);
|
||||||
this.saveStates();
|
this.saveStates();
|
||||||
|
|
||||||
|
|
@ -200,7 +225,8 @@ class SyncManager {
|
||||||
throw new Error(`No active sync for vault: ${vaultId}`);
|
throw new Error(`No active sync for vault: ${vaultId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
state._process.kill("SIGTERM");
|
state._userStopped = true;
|
||||||
|
killProcess(state._process);
|
||||||
state.status = "stopped";
|
state.status = "stopped";
|
||||||
state.pid = null;
|
state.pid = null;
|
||||||
state.autoStart = false;
|
state.autoStart = false;
|
||||||
|
|
@ -208,7 +234,7 @@ class SyncManager {
|
||||||
|
|
||||||
this.addLog(state, "Sync stopped by user");
|
this.addLog(state, "Sync stopped by user");
|
||||||
this.ctx.log(`Stopped sync for ${vaultId}`);
|
this.ctx.log(`Stopped sync for ${vaultId}`);
|
||||||
this.broadcastStatus(vaultId);
|
this.broadcaster.broadcastStatus(this.getState(vaultId));
|
||||||
this.saveStates();
|
this.saveStates();
|
||||||
|
|
||||||
return this.getState(vaultId);
|
return this.getState(vaultId);
|
||||||
|
|
@ -222,7 +248,8 @@ class SyncManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state._process) {
|
if (state._process) {
|
||||||
state._process.kill("SIGTERM");
|
state._userStopped = true;
|
||||||
|
killProcess(state._process);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell ob to disconnect from the remote vault and clear its stored config
|
// Tell ob to disconnect from the remote vault and clear its stored config
|
||||||
|
|
@ -289,51 +316,31 @@ class SyncManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcastLog(vaultId, line) {
|
isCoreSyncEnabled(vaultPath) {
|
||||||
if (!this.ctx.wss || !this.ctx.wss.clients) {
|
try {
|
||||||
return;
|
const configPath = path.join(vaultPath, ".obsidian", "core-plugins.json");
|
||||||
}
|
const data = fs.readFileSync(configPath, "utf-8");
|
||||||
|
const config = JSON.parse(data);
|
||||||
const message = JSON.stringify({
|
return config.sync === true;
|
||||||
channel: "plugin:headless-sync",
|
} catch {
|
||||||
type: "sync-log",
|
return false;
|
||||||
payload: { vaultId, line },
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const client of this.ctx.wss.clients) {
|
|
||||||
if (client.readyState === 1) {
|
|
||||||
client.send(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastStatus(vaultId) {
|
|
||||||
const state = this.getState(vaultId);
|
|
||||||
|
|
||||||
if (!state) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = JSON.stringify({
|
|
||||||
channel: "plugin:headless-sync",
|
|
||||||
type: "sync-status",
|
|
||||||
payload: state,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.ctx.wss && this.ctx.wss.clients) {
|
|
||||||
for (const client of this.ctx.wss.clients) {
|
|
||||||
if (client.readyState === 1) {
|
|
||||||
client.send(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
autoStartAll() {
|
autoStartAll() {
|
||||||
let started = 0;
|
let started = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
for (const [vaultId, state] of this.states) {
|
for (const [vaultId, state] of this.states) {
|
||||||
if (state.autoStart && state.status === "stopped") {
|
if (state.autoStart && state.status === "stopped") {
|
||||||
|
if (this.isCoreSyncEnabled(state.vaultPath)) {
|
||||||
|
this.ctx.log(
|
||||||
|
`Skipping auto-start for ${vaultId}: Obsidian Sync core plugin is enabled`,
|
||||||
|
);
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.startSync(vaultId);
|
this.startSync(vaultId);
|
||||||
started++;
|
started++;
|
||||||
|
|
@ -346,22 +353,50 @@ class SyncManager {
|
||||||
if (started > 0) {
|
if (started > 0) {
|
||||||
this.ctx.log(`Auto-started sync for ${started} vault(s)`);
|
this.ctx.log(`Auto-started sync for ${started} vault(s)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (skipped > 0) {
|
||||||
|
this.ctx.log(
|
||||||
|
`Skipped ${skipped} vault(s) due to Obsidian Sync being enabled`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async shutdown() {
|
async shutdown() {
|
||||||
this.ctx.log("Shutting down sync manager...");
|
this.ctx.log("Shutting down sync manager...");
|
||||||
|
|
||||||
|
const waitPromises = [];
|
||||||
|
|
||||||
for (const [vaultId, state] of this.states) {
|
for (const [vaultId, state] of this.states) {
|
||||||
if (state._process) {
|
if (state._process) {
|
||||||
this.ctx.log(`Stopping sync for ${vaultId}...`);
|
this.ctx.log(`Stopping sync for ${vaultId}...`);
|
||||||
|
state._userStopped = true;
|
||||||
|
|
||||||
|
const proc = state._process;
|
||||||
|
|
||||||
|
waitPromises.push(
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const timeout = setTimeout(resolve, 5000);
|
||||||
|
|
||||||
|
proc.on("close", () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
state._process.kill("SIGTERM");
|
killProcess(proc);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.ctx.log(`Error stopping sync for ${vaultId}: ${e.message}`);
|
this.ctx.log(`Error stopping sync for ${vaultId}: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (waitPromises.length > 0) {
|
||||||
|
await Promise.all(waitPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveStates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
15
server/ws.js
15
server/ws.js
|
|
@ -6,6 +6,9 @@ const watcher = require("./watcher");
|
||||||
function setupWebSocket(server) {
|
function setupWebSocket(server) {
|
||||||
const wss = new WebSocketServer({ server, path: "/ws" });
|
const wss = new WebSocketServer({ server, path: "/ws" });
|
||||||
|
|
||||||
|
// Plugin-registered message handlers: type -> handler(msg, ws)
|
||||||
|
wss.messageHandlers = new Map();
|
||||||
|
|
||||||
wss.on("connection", (ws, req) => {
|
wss.on("connection", (ws, req) => {
|
||||||
const params = new url.URL(req.url, "http://localhost").searchParams;
|
const params = new url.URL(req.url, "http://localhost").searchParams;
|
||||||
const vaultId = params.get("vault");
|
const vaultId = params.get("vault");
|
||||||
|
|
@ -30,6 +33,18 @@ function setupWebSocket(server) {
|
||||||
|
|
||||||
watcher.addListener(vaultId, listener);
|
watcher.addListener(vaultId, listener);
|
||||||
|
|
||||||
|
// Dispatch incoming messages to registered handlers
|
||||||
|
ws.on("message", (data) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(data);
|
||||||
|
const handler = wss.messageHandlers.get(msg.type);
|
||||||
|
|
||||||
|
if (handler) {
|
||||||
|
handler(msg, ws);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
console.log(`[ws] Client disconnected from vault: ${vaultId}`);
|
console.log(`[ws] Client disconnected from vault: ${vaultId}`);
|
||||||
watcher.removeListener(vaultId, listener);
|
watcher.removeListener(vaultId, listener);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue