mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
expose Ignis API, implement shared ws client
This commit is contained in:
@@ -50,14 +50,9 @@ COPY apps/ignis-server/scripts/ ./apps/ignis-server/scripts/
|
||||
COPY images/ ./images/
|
||||
COPY packages/server-core/src/ ./packages/server-core/src/
|
||||
|
||||
# Bridge plugin: manifest + styles for auto-install into vaults; built main.js comes from the build stage.
|
||||
COPY packages/bridge/manifest.json ./packages/bridge/
|
||||
COPY packages/bridge/styles.css ./packages/bridge/
|
||||
|
||||
# Built artifacts from the build stage.
|
||||
COPY --from=build /app/packages/shim/dist/shim-loader.js ./packages/shim/dist/shim-loader.js
|
||||
COPY --from=build /app/packages/ui/dist/ignis-ui.js ./packages/ui/dist/ignis-ui.js
|
||||
COPY --from=build /app/packages/bridge/main.js ./packages/bridge/main.js
|
||||
COPY --from=build /app/apps/ignis-server/server/plugins/headless-sync/obsidian/dist/ ./apps/ignis-server/server/plugins/headless-sync/obsidian/dist/
|
||||
|
||||
RUN chmod +x /app/apps/ignis-server/scripts/entrypoint.sh
|
||||
|
||||
@@ -5,8 +5,7 @@ const REPO_ROOT = path.join(__dirname, "..", "..", "..");
|
||||
|
||||
// VAULT_ROOT: a directory that contains vault folders.
|
||||
// Each subdirectory is a vault. New vaults are created as new subdirs.
|
||||
const vaultRoot =
|
||||
process.env.VAULT_ROOT || path.join(REPO_ROOT, "vaults");
|
||||
const vaultRoot = process.env.VAULT_ROOT || path.join(REPO_ROOT, "vaults");
|
||||
|
||||
const dataRoot = process.env.DATA_ROOT || path.join(REPO_ROOT, "data");
|
||||
|
||||
@@ -81,6 +80,12 @@ module.exports = {
|
||||
? parseInt(process.env.WRITE_COALESCE_MS)
|
||||
: 5000,
|
||||
|
||||
wsOrigins: process.env.WS_ORIGINS
|
||||
? process.env.WS_ORIGINS.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: null,
|
||||
|
||||
demoMode: process.env.DEMO_MODE === "true",
|
||||
demoMaxSessions: parseInt(process.env.DEMO_MAX_SESSIONS) || 20,
|
||||
demoVaultsPerSession: parseInt(process.env.DEMO_VAULTS_PER_SESSION) || 3,
|
||||
@@ -88,8 +93,7 @@ module.exports = {
|
||||
parseInt(process.env.DEMO_SESSION_QUOTA_BYTES) || 700 * 1024,
|
||||
demoTimeoutMs: parseInt(process.env.DEMO_TIMEOUT_MS) || 30 * 60 * 1000,
|
||||
demoTemplateDir:
|
||||
process.env.DEMO_TEMPLATE_DIR ||
|
||||
path.join(__dirname, "demo-template"),
|
||||
process.env.DEMO_TEMPLATE_DIR || path.join(__dirname, "demo-template"),
|
||||
|
||||
obsidianAssetsPath:
|
||||
process.env.OBSIDIAN_ASSETS_PATH ||
|
||||
@@ -99,6 +103,7 @@ module.exports = {
|
||||
const assetsPath =
|
||||
process.env.OBSIDIAN_ASSETS_PATH ||
|
||||
path.join(__dirname, "..", "investigation", "obsidian_1.12.7_unpacked");
|
||||
q;
|
||||
try {
|
||||
const pkg = JSON.parse(
|
||||
fs.readFileSync(path.join(assetsPath, "package.json"), "utf-8"),
|
||||
|
||||
@@ -195,7 +195,10 @@ const server = app.listen(config.port, async () => {
|
||||
.catch((e) => console.warn("[bootstrap] warm-up error:", e.message));
|
||||
});
|
||||
|
||||
const wss = setupWebSocket(server, { getVaultPath: config.getVaultPath });
|
||||
const wss = setupWebSocket(server, {
|
||||
getVaultPath: config.getVaultPath,
|
||||
originAllowlist: config.wsOrigins,
|
||||
});
|
||||
wireDemoWebSocket(server);
|
||||
|
||||
async function gracefulShutdown(signal) {
|
||||
|
||||
@@ -3,6 +3,7 @@ const path = require("path");
|
||||
const express = require("express");
|
||||
const { discoverPlugins } = require("./discovery");
|
||||
const configStore = require("./config-store");
|
||||
const { getVersion } = require("../version");
|
||||
|
||||
let discoveredPlugins = new Map();
|
||||
const loadedPlugins = new Map();
|
||||
@@ -171,6 +172,23 @@ async function enablePluginForVault(pluginId, vaultId) {
|
||||
if (loaded?.module?.onVaultEnabled) {
|
||||
await loaded.module.onVaultEnabled(vaultId, vaultPath);
|
||||
}
|
||||
|
||||
// Broadcast to any open tabs on this vault so they load the plugin properly.
|
||||
if (discovered.obsidianPlugin && discovered.bundledPluginId) {
|
||||
const v = `?v=${getVersion()}`;
|
||||
const entry = {
|
||||
id: discovered.bundledPluginId,
|
||||
scriptUrl: `/${discovered.bundledPluginId}.js${v}`,
|
||||
cssUrl: `/${discovered.bundledPluginId}.css${v}`,
|
||||
manifest: discovered.bundledManifest,
|
||||
};
|
||||
|
||||
serverCtx.wss?.broadcastToVault?.(vaultId, {
|
||||
type: "virtual-plugin-enable",
|
||||
vault: vaultId,
|
||||
entry,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function disablePluginForVault(pluginId, vaultId) {
|
||||
@@ -200,6 +218,14 @@ async function disablePluginForVault(pluginId, vaultId) {
|
||||
if (updated.length === 0) {
|
||||
await unloadPlugin(pluginId);
|
||||
}
|
||||
|
||||
if (discovered.bundledPluginId) {
|
||||
serverCtx.wss?.broadcastToVault?.(vaultId, {
|
||||
type: "virtual-plugin-disable",
|
||||
vault: vaultId,
|
||||
id: discovered.bundledPluginId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getBundledPluginDirs() {
|
||||
|
||||
@@ -2,57 +2,26 @@ 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 });
|
||||
this._channel = wss.channel(CHANNEL);
|
||||
}
|
||||
|
||||
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,
|
||||
this._channel.broadcastToVault(vaultId, {
|
||||
type: "sync-log",
|
||||
payload: { vaultId, line },
|
||||
});
|
||||
}
|
||||
|
||||
broadcastStatus(state) {
|
||||
if (!state) {
|
||||
if (!state || !state.vaultId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._send({
|
||||
channel: CHANNEL,
|
||||
this._channel.broadcastToVault(state.vaultId, {
|
||||
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 };
|
||||
|
||||
@@ -63,22 +63,9 @@ module.exports = {
|
||||
|
||||
const { mountRoutes } = require("./routes");
|
||||
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() {
|
||||
if (this._ctx?.wss?.messageHandlers) {
|
||||
this._ctx.wss.messageHandlers.delete("subscribe-logs");
|
||||
}
|
||||
|
||||
if (this._syncManager) {
|
||||
await this._syncManager.shutdown();
|
||||
this._syncManager = null;
|
||||
|
||||
@@ -32,13 +32,12 @@ function showConflictWarning(title, message) {
|
||||
});
|
||||
}
|
||||
|
||||
function startCoreSyncGuard(plugin, api, wsListener) {
|
||||
function startCoreSyncGuard(plugin, api) {
|
||||
const app = plugin.app;
|
||||
const vaultId = app.vault.getName();
|
||||
|
||||
// Monkey-patch syncPlugin.enable() to clear the shim flag before
|
||||
// Obsidian writes core-plugins.json. This ensures the read transform
|
||||
// doesn't block a user-initiated core sync enable.
|
||||
// Monkey-patch syncPlugin.enable() to clear the shim flag before Obsidian writes core-plugins.json.
|
||||
// This ensures the read transform doesn't block a user-initiated core sync enable.
|
||||
const syncPlugin = app.internalPlugins.getPluginById("sync");
|
||||
let origEnable = null;
|
||||
|
||||
@@ -52,16 +51,13 @@ function startCoreSyncGuard(plugin, api, wsListener) {
|
||||
};
|
||||
}
|
||||
|
||||
// Watch for core-plugins.json changes via WebSocket.
|
||||
let wasEnabled = isCoreSyncEnabled();
|
||||
|
||||
const rawHandler = (msg) => {
|
||||
if (msg.type === "modified" && msg.path === CORE_PLUGINS_PATH) {
|
||||
const unsubModified = window.__ignis.ws.subscribe("modified", (msg) => {
|
||||
if (msg.path === CORE_PLUGINS_PATH) {
|
||||
handleCoreSyncChange();
|
||||
}
|
||||
};
|
||||
|
||||
wsListener.onRaw(rawHandler);
|
||||
});
|
||||
|
||||
function handleCoreSyncChange() {
|
||||
const enabled = isCoreSyncEnabled();
|
||||
@@ -80,7 +76,7 @@ function startCoreSyncGuard(plugin, api, wsListener) {
|
||||
|
||||
return {
|
||||
cleanup() {
|
||||
wsListener.offRaw();
|
||||
unsubModified();
|
||||
|
||||
if (syncPlugin && origEnable) {
|
||||
syncPlugin.enable = origEnable;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const api = require("./api");
|
||||
|
||||
async function renderLogViewer(containerEl, vaultId, wsListener) {
|
||||
const CHANNEL = "plugin:headless-sync";
|
||||
|
||||
async function renderLogViewer(containerEl, vaultId) {
|
||||
const details = containerEl.createEl("details", {
|
||||
cls: "ignis-log-details",
|
||||
});
|
||||
@@ -32,19 +34,12 @@ async function renderLogViewer(containerEl, vaultId, wsListener) {
|
||||
|
||||
logBox.scrollTop = logBox.scrollHeight;
|
||||
|
||||
if (!wsListener) {
|
||||
return () => {};
|
||||
}
|
||||
const channel = window.__ignis.ws.channel(CHANNEL);
|
||||
let unsubLog = null;
|
||||
|
||||
details.addEventListener("toggle", () => {
|
||||
if (details.open) {
|
||||
wsListener.subscribeLogs(vaultId);
|
||||
} else {
|
||||
wsListener.unsubscribeLogs();
|
||||
}
|
||||
});
|
||||
const onLog = (msg) => {
|
||||
const payload = msg.payload || {};
|
||||
|
||||
const onLog = (payload) => {
|
||||
if (payload.vaultId !== vaultId) {
|
||||
return;
|
||||
}
|
||||
@@ -66,11 +61,22 @@ async function renderLogViewer(containerEl, vaultId, wsListener) {
|
||||
}
|
||||
};
|
||||
|
||||
wsListener.on("sync-log", onLog);
|
||||
details.addEventListener("toggle", () => {
|
||||
if (details.open) {
|
||||
if (!unsubLog) {
|
||||
unsubLog = channel.subscribe("sync-log", onLog);
|
||||
}
|
||||
} else if (unsubLog) {
|
||||
unsubLog();
|
||||
unsubLog = null;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
wsListener.off("sync-log", onLog);
|
||||
wsListener.unsubscribeLogs();
|
||||
if (unsubLog) {
|
||||
unsubLog();
|
||||
unsubLog = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const { Plugin } = require("obsidian");
|
||||
const { HeadlessSyncSettingTab } = require("./settings-tab");
|
||||
const { WsListener } = require("./ws-listener");
|
||||
const { initSyncStatusBar } = require("./sync-status-bar");
|
||||
const { startCoreSyncGuard } = require("./core-sync-guard");
|
||||
const api = require("./api");
|
||||
@@ -14,14 +13,11 @@ class IgnisHeadlessSyncPlugin extends Plugin {
|
||||
return;
|
||||
}
|
||||
|
||||
this.wsListener = new WsListener();
|
||||
this.wsListener.start();
|
||||
|
||||
this._syncStatusBarCleanup = initSyncStatusBar(this, this.wsListener);
|
||||
this._syncStatusBarCleanup = initSyncStatusBar(this);
|
||||
|
||||
this.addSettingTab(new HeadlessSyncSettingTab(this.app, this));
|
||||
|
||||
this._coreSyncGuard = startCoreSyncGuard(this, api, this.wsListener);
|
||||
this._coreSyncGuard = startCoreSyncGuard(this, api);
|
||||
|
||||
this.addCommand({
|
||||
id: "start-sync",
|
||||
@@ -75,11 +71,6 @@ class IgnisHeadlessSyncPlugin extends Plugin {
|
||||
this._syncStatusBarCleanup();
|
||||
this._syncStatusBarCleanup = null;
|
||||
}
|
||||
|
||||
if (this.wsListener) {
|
||||
this.wsListener.stop();
|
||||
this.wsListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -316,11 +316,7 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
|
||||
async renderLogs(containerEl, vaultId) {
|
||||
this._logCleanup = await renderLogViewer(
|
||||
containerEl,
|
||||
vaultId,
|
||||
this.plugin.wsListener,
|
||||
);
|
||||
this._logCleanup = await renderLogViewer(containerEl, vaultId);
|
||||
}
|
||||
|
||||
hide() {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const { setIcon } = require("obsidian");
|
||||
const api = require("./api");
|
||||
|
||||
const CHANNEL = "plugin:headless-sync";
|
||||
|
||||
const TOOLTIP_MAP = {
|
||||
running: "Syncing...",
|
||||
synced: "Synced",
|
||||
@@ -8,8 +10,11 @@ const TOOLTIP_MAP = {
|
||||
error: "Sync error",
|
||||
};
|
||||
|
||||
function initSyncStatusBar(plugin, wsListener) {
|
||||
function initSyncStatusBar(plugin) {
|
||||
const vaultId = plugin.app.vault.getName();
|
||||
const ws = window.__ignis.ws;
|
||||
const channel = ws.channel(CHANNEL);
|
||||
|
||||
const item = plugin.addStatusBarItem();
|
||||
item.addClass("ignis-sync-statusbar");
|
||||
item.style.display = "none";
|
||||
@@ -21,6 +26,7 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
let popoverOpen = false;
|
||||
let currentStatus = "stopped";
|
||||
let outsideClickHandler = null;
|
||||
let unsubLog = null;
|
||||
|
||||
function updateState(status, error) {
|
||||
currentStatus = status;
|
||||
@@ -62,7 +68,7 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
|
||||
popoverOpen = true;
|
||||
|
||||
wsListener.subscribeLogs(vaultId);
|
||||
unsubLog = channel.subscribe("sync-log", onLog);
|
||||
|
||||
outsideClickHandler = (e) => {
|
||||
if (!item.contains(e.target)) {
|
||||
@@ -86,7 +92,11 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
outsideClickHandler = null;
|
||||
}
|
||||
|
||||
wsListener.unsubscribeLogs();
|
||||
if (unsubLog) {
|
||||
unsubLog();
|
||||
unsubLog = null;
|
||||
}
|
||||
|
||||
popoverOpen = false;
|
||||
}
|
||||
|
||||
@@ -95,7 +105,7 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return "\u2026" + path.slice(-(maxLen - 1));
|
||||
return "…" + path.slice(-(maxLen - 1));
|
||||
}
|
||||
|
||||
function formatPopoverText(prefix, path) {
|
||||
@@ -115,35 +125,30 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
}
|
||||
|
||||
function extractFileActivity(line) {
|
||||
// Downloading/Downloaded path
|
||||
let match = line.match(/^(?:Downloading|Downloaded)\s+(.+)$/);
|
||||
|
||||
if (match) {
|
||||
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
|
||||
match = line.match(/^Deleting\s+(.+)$/);
|
||||
|
||||
if (match) {
|
||||
return { prefix: "Deleting", path: match[1].trim() };
|
||||
}
|
||||
|
||||
// Push: path (updated)
|
||||
match = line.match(/^Push:\s+(.+?)\s+\(updated\)$/);
|
||||
|
||||
if (match) {
|
||||
return { prefix: "Syncing", path: match[1].trim() };
|
||||
}
|
||||
|
||||
// Push: path (deleted)
|
||||
match = line.match(/^Push:\s+(.+?)\s+\(deleted\)$/);
|
||||
|
||||
if (match) {
|
||||
@@ -157,7 +162,6 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
return /Fully synced/i.test(line);
|
||||
}
|
||||
|
||||
// Click toggles popover
|
||||
item.addEventListener("click", () => {
|
||||
if (popoverOpen) {
|
||||
hidePopover();
|
||||
@@ -166,16 +170,15 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for status updates
|
||||
const onStatus = (payload) => {
|
||||
const onStatus = (msg) => {
|
||||
const payload = msg.payload || {};
|
||||
|
||||
if (payload.vaultId !== vaultId) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.style.display = "";
|
||||
|
||||
// "running" from server means the process is alive, but we refine
|
||||
// the visual state based on log activity.
|
||||
if (payload.status === "running") {
|
||||
updateState("synced");
|
||||
} else {
|
||||
@@ -183,10 +186,8 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
}
|
||||
};
|
||||
|
||||
wsListener.on("sync-status", onStatus);
|
||||
const unsubStatus = channel.subscribe("sync-status", onStatus);
|
||||
|
||||
// Debounce the transition to "synced" state to avoid flickering
|
||||
// during rapid delete cycles (Fully synced -> Deleting -> Fully synced).
|
||||
let syncedTimer = null;
|
||||
|
||||
function deferSynced() {
|
||||
@@ -208,8 +209,9 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for log lines
|
||||
const onLog = (payload) => {
|
||||
function onLog(msg) {
|
||||
const payload = msg.payload || {};
|
||||
|
||||
if (payload.vaultId !== vaultId) {
|
||||
return;
|
||||
}
|
||||
@@ -226,11 +228,8 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
updateState("running");
|
||||
updatePopoverText(formatPopoverText(activity.prefix, activity.path));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
wsListener.on("sync-log", onLog);
|
||||
|
||||
// Fetch initial state
|
||||
api
|
||||
.getVaults()
|
||||
.then((data) => {
|
||||
@@ -244,16 +243,16 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Poll WebSocket state to detect server disconnect/reconnect
|
||||
// Reflect WebSocket disconnect/reconnect in the indicator.
|
||||
let wasDisconnected = false;
|
||||
|
||||
const wsCheckInterval = setInterval(() => {
|
||||
const disconnected = !wsListener.isConnected();
|
||||
const unsubState = ws.onStateChange((state) => {
|
||||
const open = state === "open";
|
||||
|
||||
if (disconnected && currentStatus === "running") {
|
||||
if (!open && currentStatus === "running") {
|
||||
updateState("error", "Server connection lost");
|
||||
wasDisconnected = true;
|
||||
} else if (!disconnected && wasDisconnected) {
|
||||
} else if (open && wasDisconnected) {
|
||||
wasDisconnected = false;
|
||||
|
||||
api
|
||||
@@ -268,14 +267,12 @@ function initSyncStatusBar(plugin, wsListener) {
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
clearInterval(wsCheckInterval);
|
||||
cancelDeferredSynced();
|
||||
wsListener.off("sync-status", onStatus);
|
||||
wsListener.off("sync-log", onLog);
|
||||
unsubStatus();
|
||||
unsubState();
|
||||
hidePopover();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
const CHANNEL = "plugin:headless-sync";
|
||||
const POLL_INTERVAL = 3000;
|
||||
const LOG_KEEPALIVE_INTERVAL = 7000;
|
||||
|
||||
class WsListener {
|
||||
constructor() {
|
||||
this._callbacks = new Map();
|
||||
this._handler = null;
|
||||
this._rawHandler = null;
|
||||
this._pollTimer = null;
|
||||
this._currentWs = null;
|
||||
this._logSubInterval = null;
|
||||
this._logSubVaultId = null;
|
||||
}
|
||||
|
||||
start() {
|
||||
this._attachToWs();
|
||||
|
||||
this._pollTimer = setInterval(() => {
|
||||
this._attachToWs();
|
||||
}, POLL_INTERVAL);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this._pollTimer) {
|
||||
clearInterval(this._pollTimer);
|
||||
this._pollTimer = null;
|
||||
}
|
||||
|
||||
this.unsubscribeLogs();
|
||||
this._detachFromWs();
|
||||
}
|
||||
|
||||
isConnected() {
|
||||
const ws = window.__ignisWs;
|
||||
return ws && ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
on(type, callback) {
|
||||
if (!this._callbacks.has(type)) {
|
||||
this._callbacks.set(type, []);
|
||||
}
|
||||
|
||||
this._callbacks.get(type).push(callback);
|
||||
}
|
||||
|
||||
off(type, callback) {
|
||||
const list = this._callbacks.get(type);
|
||||
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = list.indexOf(callback);
|
||||
|
||||
if (idx !== -1) {
|
||||
list.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
const ws = window.__ignisWs;
|
||||
|
||||
if (!ws || ws === this._currentWs) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._detachFromWs();
|
||||
this._currentWs = ws;
|
||||
|
||||
this._handler = (event) => {
|
||||
try {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listeners = this._callbacks.get(msg.type);
|
||||
|
||||
if (listeners) {
|
||||
for (const cb of listeners) {
|
||||
cb(msg.payload);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
ws.addEventListener("message", this._handler);
|
||||
}
|
||||
|
||||
_detachFromWs() {
|
||||
if (this._currentWs && this._handler) {
|
||||
this._currentWs.removeEventListener("message", this._handler);
|
||||
}
|
||||
|
||||
this._currentWs = null;
|
||||
this._handler = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { WsListener };
|
||||
Reference in New Issue
Block a user