mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
basic filewatcher using websocket
This commit is contained in:
31
src/shims/fs/echo-guard.js
Normal file
31
src/shims/fs/echo-guard.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// Shared echo suppression for file watcher.
|
||||
// fs operations mark paths as "locally modified" so the watcher client
|
||||
// can skip events that originated from this client.
|
||||
|
||||
const ECHO_SUPPRESS_MS = 1500;
|
||||
const recentOps = new Map(); // normalized path -> timestamp
|
||||
|
||||
function normalize(p) {
|
||||
return (p || "")
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/^\/+/, "")
|
||||
.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function markLocalOp(path) {
|
||||
recentOps.set(normalize(path), Date.now());
|
||||
}
|
||||
|
||||
export function isRecentLocalOp(path) {
|
||||
const norm = normalize(path);
|
||||
const ts = recentOps.get(norm);
|
||||
|
||||
if (!ts) return false;
|
||||
|
||||
if (Date.now() - ts < ECHO_SUPPRESS_MS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
recentOps.delete(norm);
|
||||
return false;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { transport } from "./transport.js";
|
||||
import { createFsPromises } from "./promises.js";
|
||||
import { createFsSync } from "./sync.js";
|
||||
import { createFsWatch } from "./watch.js";
|
||||
import { createWatcherClient } from "./watcher-client.js";
|
||||
import { constants } from "./constants.js";
|
||||
|
||||
const metadataCache = new MetadataCache();
|
||||
@@ -12,6 +13,7 @@ const contentCache = new ContentCache();
|
||||
const fsPromises = createFsPromises(metadataCache, contentCache, transport);
|
||||
const fsSync = createFsSync(metadataCache, contentCache, transport);
|
||||
const fsWatch = createFsWatch(transport);
|
||||
const watcherClient = createWatcherClient(metadataCache, contentCache, fsWatch);
|
||||
|
||||
export const fsShim = {
|
||||
promises: fsPromises,
|
||||
@@ -29,6 +31,7 @@ export const fsShim = {
|
||||
|
||||
_metadataCache: metadataCache,
|
||||
_contentCache: contentCache,
|
||||
_watcherClient: watcherClient,
|
||||
|
||||
async _init(basePath) {
|
||||
const tree = await transport.fetchTree(basePath);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { markLocalOp } from "./echo-guard.js";
|
||||
|
||||
export function createFsPromises(metadataCache, contentCache, transport) {
|
||||
return {
|
||||
async stat(path) {
|
||||
@@ -85,6 +87,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||
encoding = encoding?.encoding;
|
||||
}
|
||||
|
||||
markLocalOp(path);
|
||||
contentCache.set(path, data);
|
||||
|
||||
const size =
|
||||
@@ -110,6 +113,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||
},
|
||||
|
||||
async appendFile(path, data, encoding) {
|
||||
markLocalOp(path);
|
||||
contentCache.invalidate(path);
|
||||
|
||||
await transport.appendFile(path, data);
|
||||
@@ -119,6 +123,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||
},
|
||||
|
||||
async unlink(path) {
|
||||
markLocalOp(path);
|
||||
contentCache.delete(path);
|
||||
metadataCache.delete(path);
|
||||
|
||||
@@ -126,6 +131,8 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||
},
|
||||
|
||||
async rename(oldPath, newPath) {
|
||||
markLocalOp(oldPath);
|
||||
markLocalOp(newPath);
|
||||
const content = contentCache.get(oldPath);
|
||||
|
||||
if (content !== null) {
|
||||
@@ -142,12 +149,14 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||
const recursive =
|
||||
typeof options === "object" ? !!options.recursive : !!options;
|
||||
|
||||
markLocalOp(path);
|
||||
metadataCache.set(path, { type: "directory" });
|
||||
|
||||
await transport.mkdir(path, recursive);
|
||||
},
|
||||
|
||||
async rmdir(path) {
|
||||
markLocalOp(path);
|
||||
metadataCache.delete(path);
|
||||
await transport.rmdir(path);
|
||||
},
|
||||
@@ -156,6 +165,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||
const recursive =
|
||||
typeof options === "object" ? !!options.recursive : false;
|
||||
|
||||
markLocalOp(path);
|
||||
metadataCache.delete(path);
|
||||
contentCache.delete(path);
|
||||
|
||||
@@ -163,6 +173,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||
},
|
||||
|
||||
async copyFile(src, dest) {
|
||||
markLocalOp(dest);
|
||||
await transport.copyFile(src, dest);
|
||||
|
||||
const meta = await transport.stat(dest);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { markLocalOp } from "./echo-guard.js";
|
||||
|
||||
export function createFsSync(metadataCache, contentCache, transport) {
|
||||
return {
|
||||
existsSync(path) {
|
||||
@@ -64,6 +66,7 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||
encoding = encoding?.encoding;
|
||||
}
|
||||
|
||||
markLocalOp(path);
|
||||
contentCache.set(path, data);
|
||||
|
||||
const size =
|
||||
@@ -87,6 +90,7 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||
},
|
||||
|
||||
unlinkSync(path) {
|
||||
markLocalOp(path);
|
||||
contentCache.delete(path);
|
||||
metadataCache.delete(path);
|
||||
|
||||
|
||||
141
src/shims/fs/watcher-client.js
Normal file
141
src/shims/fs/watcher-client.js
Normal file
@@ -0,0 +1,141 @@
|
||||
// Client-side WebSocket file watcher.
|
||||
// Connects to the server's /ws endpoint, receives file change events,
|
||||
// updates the metadata/content caches, and dispatches to fs.watch listeners
|
||||
// so Obsidian's vault picks them up automatically.
|
||||
|
||||
import { isRecentLocalOp } from "./echo-guard.js";
|
||||
|
||||
const RECONNECT_DELAY = 2000;
|
||||
|
||||
export function createWatcherClient(metadataCache, contentCache, fsWatch) {
|
||||
let ws = null;
|
||||
let vaultId = null;
|
||||
let reconnectTimer = null;
|
||||
|
||||
function connect(vault) {
|
||||
vaultId = vault;
|
||||
|
||||
if (!vaultId) {
|
||||
console.warn("[watcher] No vault ID, skipping WebSocket connection");
|
||||
return;
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const url = `${protocol}//${window.location.host}/ws?vault=${encodeURIComponent(vaultId)}`;
|
||||
|
||||
try {
|
||||
ws = new WebSocket(url);
|
||||
} catch (e) {
|
||||
console.error("[watcher] Failed to create WebSocket:", e);
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("[watcher] Connected to file watcher");
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
handleEvent(msg);
|
||||
} catch (e) {
|
||||
console.error("[watcher] Failed to parse message:", e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("[watcher] Disconnected");
|
||||
ws = null;
|
||||
scheduleReconnect();
|
||||
};
|
||||
|
||||
ws.onerror = (e) => {
|
||||
console.error("[watcher] WebSocket error:", e);
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (reconnectTimer) return;
|
||||
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
|
||||
if (vaultId) {
|
||||
console.log("[watcher] Reconnecting...");
|
||||
connect(vaultId);
|
||||
}
|
||||
}, RECONNECT_DELAY);
|
||||
}
|
||||
|
||||
function handleEvent(msg) {
|
||||
const { type, path, stat } = msg;
|
||||
|
||||
if (!type || !path) return;
|
||||
|
||||
// Suppress echo from our own operations
|
||||
if (isRecentLocalOp(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "created":
|
||||
if (stat) {
|
||||
metadataCache.set(path, {
|
||||
type: "file",
|
||||
size: stat.size,
|
||||
mtime: stat.mtime,
|
||||
ctime: stat.ctime,
|
||||
});
|
||||
}
|
||||
contentCache.invalidate(path);
|
||||
fsWatch._dispatch("created", path);
|
||||
break;
|
||||
|
||||
case "folder-created":
|
||||
metadataCache.set(path, { type: "directory" });
|
||||
fsWatch._dispatch("folder-created", path);
|
||||
break;
|
||||
|
||||
case "modified":
|
||||
if (stat) {
|
||||
metadataCache.set(path, {
|
||||
type: "file",
|
||||
size: stat.size,
|
||||
mtime: stat.mtime,
|
||||
ctime: stat.ctime,
|
||||
});
|
||||
}
|
||||
contentCache.invalidate(path);
|
||||
fsWatch._dispatch("modified", path);
|
||||
break;
|
||||
|
||||
case "deleted":
|
||||
metadataCache.delete(path);
|
||||
contentCache.invalidate(path);
|
||||
fsWatch._dispatch("deleted", path);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("[watcher] Unknown event type:", type);
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (ws) {
|
||||
ws.onclose = null; // prevent reconnect
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
}
|
||||
@@ -259,4 +259,9 @@ window.__currentVaultId =
|
||||
|
||||
installRequestUrlShim();
|
||||
|
||||
// Connect file watcher WebSocket after everything is initialized
|
||||
if (window.__currentVaultId) {
|
||||
fsShim._watcherClient.connect(window.__currentVaultId);
|
||||
}
|
||||
|
||||
console.log("[ignis] Shim loader initialized");
|
||||
|
||||
Reference in New Issue
Block a user